diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 457ae496..8c16894a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,7 +11,7 @@ Changes * FIX: Fixed explicit profiling of class methods; added handling for profiling static, bound, and partial methods, ``functools.partial`` objects, (cached) properties, and async generator functions * FIX: Fixed namespace bug when running ``kernprof -m`` on certain modules (e.g. ``calendar`` on Python 3.12+). * FIX: Fixed ``@contextlib.contextmanager`` bug where the cleanup code (e.g. restoration of ``sys`` attributes) is not run if exceptions occurred inside the context -* ENH: Added CLI arguments ``-c`` to ``kernprof`` for (auto-)profiling inline-script execution instead of that of script files; passing ``'-'`` as the script-file name now also reads from and profiles ``stdin`` +* ENH: Added CLI arguments ``-c`` to ``kernprof`` for (auto-)profiling module/package/inline-script execution instead of that of script files; passing ``'-'`` as the script-file name now also reads from and profiles ``stdin`` * ENH: In Python >=3.11, profiled objects are reported using their qualified name. * ENH: Highlight final summary using rich if enabled * ENH: Made it possible to use multiple profiler instances simultaneously @@ -43,6 +43,11 @@ Changes callbacks during profiling * Now allowing switching back to the "legacy" trace system on Python 3.12+, controlled by an environment variable +* ENH: Added capability to parse TOML config files for defaults (#335): + + * ``kernprof`` and ``python -m line_profiler`` CLI options + * ``GlobalProfiler`` configurations, and + * profiler output (e.g. ``LineProfiler.print_stats()``) formatting 4.2.0 ~~~~~ diff --git a/MANIFEST.in b/MANIFEST.in index c9d9793e..93aba83b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ +graft line_profiler/rc include *.md include *.rst include *.py diff --git a/docs/source/auto/line_profiler.cli_utils.rst b/docs/source/auto/line_profiler.cli_utils.rst new file mode 100644 index 00000000..a5a1eeeb --- /dev/null +++ b/docs/source/auto/line_profiler.cli_utils.rst @@ -0,0 +1,7 @@ +line\_profiler.cli\_utils module +================================ + +.. automodule:: line_profiler.cli_utils + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/auto/line_profiler.rst b/docs/source/auto/line_profiler.rst index 28a17f71..38985a4c 100644 --- a/docs/source/auto/line_profiler.rst +++ b/docs/source/auto/line_profiler.rst @@ -17,11 +17,13 @@ Submodules line_profiler.__main__ line_profiler._line_profiler + line_profiler.cli_utils line_profiler.explicit_profiler line_profiler.ipython_extension line_profiler.line_profiler line_profiler.profiler_mixin line_profiler.scoping_policy + line_profiler.toml_config Module contents --------------- diff --git a/docs/source/auto/line_profiler.toml_config.rst b/docs/source/auto/line_profiler.toml_config.rst new file mode 100644 index 00000000..54a81532 --- /dev/null +++ b/docs/source/auto/line_profiler.toml_config.rst @@ -0,0 +1,7 @@ +line\_profiler.toml\_config module +================================ + +.. automodule:: line_profiler.toml_config + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/manual/examples/example_toml_config.rst b/docs/source/manual/examples/example_toml_config.rst new file mode 100644 index 00000000..a2ccd0e6 --- /dev/null +++ b/docs/source/manual/examples/example_toml_config.rst @@ -0,0 +1,221 @@ +Using the line-profiler TOML configuration +------------------------------------------ + +This tutorial walks the user through setting up a toy Python project and then +interacting with it via the new line-profiler TOML configuration. + +First, we need to setup a small project, for which we will use ``uv``. We will +also use the ``tomlkit`` package to edit the config file programatically. If +you don't have these installed, first run: + +.. code:: bash + + pip install uv tomlkit + + +Next, we are going to setup a small package for this demonstration. + +.. code:: bash + + TEMP_DIR=$(mktemp -d --suffix=demo_pkg) + mkdir -p $TEMP_DIR + cd $TEMP_DIR + + uv init --lib --name demo_pkg + + # helper to prevent indentation errors + codeblock(){ + echo "$1" | python -c "import sys; from textwrap import dedent; print(dedent(sys.stdin.read()).strip('\n'))" + } + + codeblock " + import time + from demo_pkg.utils import leq + from demo_pkg import utils + + def fib(n): + if leq(n, 1): + return n + part1 = fib(n - 1) + part2 = fib(n - 2) + result = utils.add(part1, part2) + return result + + def sleep_loop(n): + for _ in range(n): + time.sleep(0.01) + " > src/demo_pkg/core.py + + codeblock " + def leq(a, b): + return a <= b + + def add(a, b): + return a + b + " > src/demo_pkg/utils.py + + codeblock " + from demo_pkg import core + import uuid + + def main(): + run_uuid = uuid.uuid4() + print('The UUID of this run is', run_uuid) + print('compute fib 10') + result = core.fib(10) + print('result', result) + print('sleeping 5') + core.sleep_loop(5) + print('done') + + if __name__ == '__main__': + main() + " > src/demo_pkg/__main__.py + + # Run `uv pip install -e .` to install the project locally: + uv pip install -e . + + +Test that the main entrypoint works. + +.. code:: bash + + python -m demo_pkg + + +Running kernprof with a main script that uses your package behaves as in 4.x in that no defaults are modified. + +.. code:: bash + + kernprof -m demo_pkg + + +However, you can modify pyproject.toml to specify new defaults. After doing +this, running kernprof will use defaults specified in your pyproject.toml (You +may also pass ``--config`` to tell kernprof to use a different file to load the +default config). + +.. code:: bash + + # Edit the `pyproject.toml` file to modify default behavior + update_pyproject_toml(){ + python -c "if 1: + import pathlib + import tomllib + import tomlkit + import sys + config_path = pathlib.Path('pyproject.toml') + config = tomllib.loads(config_path.read_text()) + + # Add in new values + from textwrap import dedent + new_text = dedent(sys.argv[1]) + + new_parts = tomllib.loads(new_text) + config.update(new_parts) + + new_text = tomlkit.dumps(config) + config_path.write_text(new_text) + " "$1" + } + + update_pyproject_toml " + # New Config + [tool.line_profiler.kernprof] + line-by-line = true + rich = true + verbose = true + skip-zero = true + prof-mod = ['demo_pkg'] + " + + # Now, running kernprof uses the new defaults + kernprof -m demo_pkg + + +You will now see how long each function took, and what the line-by line breakdown is + +.. code:: + + # line-by-line breakdown omitted here + + 0.05 seconds - /tmp/tmp.vKpODQr6wndemo_pkg/src/demo_pkg/__main__.py:4 - main + 0.00 seconds - /tmp/tmp.vKpODQr6wndemo_pkg/src/demo_pkg/core.py:5 - fib + 0.05 seconds - /tmp/tmp.vKpODQr6wndemo_pkg/src/demo_pkg/core.py:13 - sleep_loop + 0.00 seconds - /tmp/tmp.vKpODQr6wndemo_pkg/src/demo_pkg/utils.py:1 - leq + 0.00 seconds - /tmp/tmp.vKpODQr6wndemo_pkg/src/demo_pkg/utils.py:4 - add + + +Note that by specifying ``prof-mod``, every function within the package is +automatically profiled without any need for the ``@profile`` decorator. + +It is worth noting, there is no requirement that the module you are profiling +is part of your package. You can specify any module name as part of +``prof-mod``. For example, lets profile the stdlib uuid module. + + +.. code:: bash + + update_pyproject_toml " + # New Config + [tool.line_profiler.kernprof] + line-by-line = true + rich = true + verbose = 0 + skip-zero = true + prof-mod = ['uuid'] + " + + # Now, running kernprof uses the new defaults + kernprof -m demo_pkg + python -m line_profiler -rmtz demo_pkg.lprof + + +This results in only showing calls in the uuid package: + +.. code:: + + # line-by-line breakdown omitted here + + 0.00 seconds - .pyenv/versions/3.13.2/lib/python3.13/uuid.py:142 - UUID.__init__ + 0.00 seconds - .pyenv/versions/3.13.2/lib/python3.13/uuid.py:283 - UUID.__str__ + 0.00 seconds - .pyenv/versions/3.13.2/lib/python3.13/uuid.py:277 - UUID.__repr__ + 0.00 seconds - .pyenv/versions/3.13.2/lib/python3.13/uuid.py:710 - uuid4 + + +You can list exact functions to profile as long as they are addressable by +dotted names. The above only profiles the ``fib`` function in our package: + +.. code:: bash + + update_pyproject_toml " + # New Config + [tool.line_profiler.kernprof] + line-by-line = true + rich = true + verbose = 0 + skip-zero = true + prof-mod = ['demo_pkg.core.fib'] + " + + # Now, running kernprof uses the new defaults + kernprof -m demo_pkg + python -m line_profiler -rmtz demo_pkg.lprof + + +The output is: + +.. code:: + + Line # Hits Time Per Hit % Time Line Contents + ============================================================== + 5 def fib(n): + 6 177 145.1 0.8 42.5 if leq(n, 1): + 7 89 29.7 0.3 8.7 return n + 8 88 29.1 0.3 8.5 part1 = fib(n - 1) + 9 88 27.7 0.3 8.1 part2 = fib(n - 2) + 10 88 78.0 0.9 22.8 result = utils.add(part1, part2) + 11 88 32.2 0.4 9.4 return result + + + 0.00 seconds - /tmp/tmp.vKpODQr6wndemo_pkg/src/demo_pkg/core.py:5 - fib diff --git a/docs/source/manual/examples/index.rst b/docs/source/manual/examples/index.rst index 18589aa6..b0aa80c7 100644 --- a/docs/source/manual/examples/index.rst +++ b/docs/source/manual/examples/index.rst @@ -5,10 +5,12 @@ Examples of line profiler usage: + `Basic Usage <../../index.html#line-profiler-basic-usage>`_ -+ `kernprof invocations `_ ++ `Kernprof Usage `_ + `Auto Profiling <../../auto/line_profiler.autoprofile.html#auto-profiling>`_ + `Explicit Profiler <../../auto/line_profiler.explicit_profiler.html#module-line_profiler.explicit_profiler>`_ + `Timing Units `_ + ++ `TOML Config Usage `_ diff --git a/kernprof.py b/kernprof.py index f37edbd4..ce91c9b1 100755 --- a/kernprof.py +++ b/kernprof.py @@ -73,11 +73,21 @@ def main(): .. code:: - usage: kernprof [-h] [-V] [-l] [-b] [-o OUTFILE] [-s SETUP] [-v] [-q] [-r] [-u UNIT] [-z] [-i [OUTPUT_INTERVAL]] [-p {path/to/script | object.dotted.path}[,...]] - [--no-preimports] [--prof-imports] + usage: kernprof [-h] [-V] [--config CONFIG] [--no-config] + [--line-by-line [Y[es] | N[o] | T[rue] | F[alse] | on | off | 1 | 0]] + [--builtin [Y[es] | N[o] | T[rue] | F[alse] | on | off | 1 | 0]] + [-s SETUP] [-p {path/to/script | object.dotted.path}[,...]] + [--preimports [Y[es] | N[o] | T[rue] | F[alse] | on | off | 1 | 0]] + [--prof-imports [Y[es] | N[o] | T[rue] | F[alse] | on | off | 1 | 0]] + [-o OUTFILE] [-v] [-q] + [--rich [Y[es] | N[o] | T[rue] | F[alse] | on | off | 1 | 0]] + [-u UNIT] + [--skip-zero [Y[es] | N[o] | T[rue] | F[alse] | on | off | 1 | 0]] + [--summarize [Y[es] | N[o] | T[rue] | F[alse] | on | off | 1 | 0]] + [-i [OUTPUT_INTERVAL]] {path/to/script | -m path.to.module | -c "literal code"} ... - Run and profile a python script. + Run and profile a python script or module. positional arguments: {path/to/script | -m path.to.module | -c "literal code"} @@ -87,28 +97,75 @@ def main(): options: -h, --help show this help message and exit -V, --version show program's version number and exit - -l, --line-by-line Use the line-by-line profiler instead of cProfile. Implies --builtin. - -b, --builtin Put 'profile' in the builtins. Use 'profile.enable()'/'.disable()', '@profile' to decorate functions, or 'with profile:' to profile a section of code. + --config CONFIG Path to the TOML file, from the `tool.line_profiler.kernprof` + table of which to load defaults for the options. (Default: + 'pyproject.toml') + --no-config Disable the loading of configuration files other than the + default one + + profiling options: + --line-by-line [Y[es] | N[o] | T[rue] | F[alse] | on | off | 1 | 0] + Use the line-by-line profiler instead of cProfile. Implies + `--builtin`. (Default: False; short form: -l) + --builtin [Y[es] | N[o] | T[rue] | F[alse] | on | off | 1 | 0] + Put `profile` in the builtins. Use + `profile.enable()`/`.disable()` to toggle profiling, + `@profile` to decorate functions, or `with profile:` to + profile a section of code. (Default: False; short form: -b) + -s, --setup SETUP Path to the Python source file containing setup code to + execute before the code to profile. (Default: N/A) + -p, --prof-mod PROF_MOD + List of modules, functions and/or classes to profile specified + by their name or path. These profiling targets can be supplied + both as comma-separated items, or separately with multiple + copies of this flag. Packages are automatically recursed into + unless they are specified with `.__init__`. Adding the + current script/module profiles the entirety of it. Only works + with line profiling (`-l`/`--line-by-line`). (Default: N/A; + pass an empty string to clear the defaults (or any `-p` target + specified earlier) + ---preimports [Y[es] | N[o] | T[rue] | F[alse] | on | off | 1 | 0] + Instead of eagerly importing all profiling targets specified + via `-p` and profiling them, only profile those that are + directly imported in the profiled code. Only works with + line profiling (`-l`/`--line-by-line`). (Default: False) + Eagerly import all profiling targets specified via `-p` and + profile them, instead of only profiling those that are + directly imported in the profiled code. Only works with line + profiling (`-l`/`--line-by-line`). (Default: True) + --prof-imports [Y[es] | N[o] | T[rue] | F[alse] | on | off | 1 | 0] + If the script/module profiled is in `--prof-mod`, autoprofile + all its imports. Only works with line profiling (`-l`/`--line- + by-line`). (Default: False) + + output options: -o, --outfile OUTFILE - Save stats to (default: 'scriptname.lprof' with --line-by-line, 'scriptname.prof' without) - -s, --setup SETUP Code to execute before the code to profile + Save stats to OUTFILE. (Default: + '.lprof' in line-profiling mode + (`-l`/`--line-by-line`); '.prof' + otherwise) -v, --verbose, --view - Increase verbosity level. At level 1, view the profiling results in addition to saving them; at level 2, show other diagnostic info. - -q, --quiet Decrease verbosity level. At level -1, disable helpful messages (e.g. "Wrote profile results to <...>"); at level -2, silence the stdout; at level -3, - silence the stderr. - -r, --rich Use rich formatting if viewing output - -u, --unit UNIT Output unit (in seconds) in which the timing info is displayed (default: 1e-6) - -z, --skip-zero Hide functions which have not been called + Increase verbosity level (default: 0). At level 1, view the + profiling results in addition to saving them; at level 2, + show other diagnostic info. + -q, --quiet Decrease verbosity level (default: 0). At level -1, disable + helpful messages (e.g. "Wrote profile results to <...>"); at + level -2, silence the stdout; at level -3, silence the stderr. + --rich [Y[es] | N[o] | T[rue] | F[alse] | on | off | 1 | 0] + Use rich formatting if viewing output. (Default: False; short + form: -r) + -u, --unit UNIT Output unit (in seconds) in which the timing info is + displayed. (Default: 1e-06 s) + --skip-zero [Y[es] | N[o] | T[rue] | F[alse] | on | off | 1 | 0] + Hide functions which have not been called. (Default: False; + short form: -z) + --summarize [Y[es] | N[o] | T[rue] | F[alse] | on | off | 1 | 0] + Print a summary of total function time. (Default: False) -i, --output-interval [OUTPUT_INTERVAL] - Enables outputting of cumulative profiling results to file every n seconds. Uses the threading module. Minimum value is 1 (second). Defaults to - disabled. - -p, --prof-mod {path/to/script | object.dotted.path}[,...] - List of modules, functions and/or classes to profile specified by their name or path. These profiling targets can be supplied both as comma-separated - items, or separately with multiple copies of this flag. Packages are automatically recursed into unless they are specified with `.__init__`. Adding - the current script/module profiles the entirety of it. Only works with line_profiler -l, --line-by-line. - --no-preimports Instead of eagerly importing all profiling targets specified via -p and profiling them, only profile those that are directly imported in the profiled - code. Only works with line_profiler -l, --line-by-line. - --prof-imports If specified, modules specified to `--prof-mod` will also autoprofile modules that they import. Only works with line_profiler -l, --line-by-line + Enables outputting of cumulative profiling results to OUTFILE + every OUTPUT_INTERVAL seconds. Uses the threading module. + Minimum value (and the value implied if the bare option is + given) is 1 s. (Default: 0 s (disabled)) NOTE: @@ -127,7 +184,7 @@ def main(): To restore the old behavior, pass the :option:`!--no-preimports` flag. -""" +""" # noqa: E501 import atexit import builtins import functools @@ -141,7 +198,7 @@ def main(): import tempfile import time import warnings -from argparse import ArgumentError, ArgumentParser +from argparse import ArgumentParser from io import StringIO from operator import methodcaller from runpy import run_module @@ -162,6 +219,11 @@ def main(): except ImportError: from profile import Profile # type: ignore[assignment,no-redef] +import line_profiler +from line_profiler.cli_utils import ( + add_argument, get_cli_config, + get_python_executable as _python_command, # Compatibility + positive_float, short_string_path) from line_profiler.profiler_mixin import ByCountProfilerMixin from line_profiler._logger import Logger from line_profiler import _diagnostics as diagnostics @@ -217,7 +279,7 @@ class RepeatedTimer: References: .. [SO474528] https://stackoverflow.com/questions/474528/execute-function-every-x-seconds/40965385#40965385 - """ + """ # noqa: E501 def __init__(self, interval, dump_func, outfile): self._timer = None self.interval = interval @@ -235,7 +297,8 @@ def _run(self): def start(self): if not self.is_running: self.next_call += self.interval - self._timer = threading.Timer(self.next_call - time.time(), self._run) + self._timer = threading.Timer(self.next_call - time.time(), + self._run) self._timer.start() self.is_running = True @@ -244,7 +307,7 @@ def stop(self): self.is_running = False -def find_module_script(module_name): +def find_module_script(module_name, *, exit_on_error=True): """Find the path to the executable script for a module or package.""" from line_profiler.autoprofile.util_static import modname_to_modpath @@ -253,11 +316,15 @@ def find_module_script(module_name): if fname: return fname - sys.stderr.write('Could not find module %s\n' % module_name) - raise SystemExit(1) + msg = f'Could not find module `{module_name}`' + if exit_on_error: + print(msg, file=sys.stderr) + raise SystemExit(1) + else: + raise ModuleNotFoundError(msg) -def find_script(script_name, exit_on_error=True): +def find_script(script_name, *, exit_on_error=True): """ Find the script. If the input is not a file, then :envvar:`PATH` will be searched. @@ -280,16 +347,6 @@ def find_script(script_name, exit_on_error=True): raise FileNotFoundError(msg) -def _python_command(): - """ - Return a command that corresponds to :py:data:`sys.executable`. - """ - for abbr in 'python', 'python3': - if os.path.samefile(shutil.which(abbr), sys.executable): - return abbr - return sys.executable - - def _normalize_profiling_targets(targets): """ Normalize the parsed :option:`!--prof-mod` by: @@ -298,6 +355,8 @@ def _normalize_profiling_targets(targets): subsequently to absolute paths. * Splitting non-file paths at commas into (presumably) file paths and/or dotted paths. + * Allowing paths specified earlier to be invalidated by an empty + string. * Removing duplicates. """ def find(path): @@ -309,6 +368,9 @@ def find(path): results = {} for chunk in targets: + if not chunk: + results.clear() + continue filename = find(chunk) if filename is not None: results.setdefault(filename) @@ -490,80 +552,123 @@ def pre_parse_single_arg_directive(args, flag, sep='--'): return args, thing, post_args -def positive_float(value): - val = float(value) - if val <= 0: - raise ArgumentError - return val - - def no_op(*_, **__) -> None: pass def _add_core_parser_arguments(parser): """ - Add the core kernprof args to a ArgumentParser + Add the core kernprof args to a + :py:class:`~argparse.ArgumentParser`. """ - parser.add_argument('-V', '--version', action='version', version=__version__) - parser.add_argument('-l', '--line-by-line', action='store_true', - help='Use the line-by-line profiler instead of cProfile. Implies --builtin.') - parser.add_argument('-b', '--builtin', action='store_true', - help="Put 'profile' in the builtins. Use 'profile.enable()'/'.disable()', " - "'@profile' to decorate functions, or 'with profile:' to profile a " - 'section of code.') - parser.add_argument('-o', '--outfile', - help="Save stats to (default: 'scriptname.lprof' with " - "--line-by-line, 'scriptname.prof' without)") - parser.add_argument('-s', '--setup', - help='Code to execute before the code to profile') - parser.add_argument('-v', '--verbose', '--view', - action='count', default=0, - help='Increase verbosity level. ' - 'At level 1, view the profiling results ' - 'in addition to saving them; ' - 'at level 2, show other diagnostic info.') - parser.add_argument('-q', '--quiet', - action='count', default=0, - help='Decrease verbosity level. ' - 'At level -1, disable helpful messages ' - '(e.g. "Wrote profile results to <...>"); ' - 'at level -2, silence the stdout; ' - 'at level -3, silence the stderr.') - parser.add_argument('-r', '--rich', action='store_true', - help='Use rich formatting if viewing output') - parser.add_argument('-u', '--unit', default='1e-6', type=positive_float, - help='Output unit (in seconds) in which the timing info is ' - 'displayed (default: 1e-6)') - parser.add_argument('-z', '--skip-zero', action='store_true', - help="Hide functions which have not been called") - parser.add_argument('-i', '--output-interval', type=int, default=0, const=0, nargs='?', - help="Enables outputting of cumulative profiling results to file every n seconds. Uses the threading module. " - "Minimum value is 1 (second). Defaults to disabled.") - parser.add_argument('-p', '--prof-mod', - action='append', - metavar=("{path/to/script | object.dotted.path}" - "[,...]"), - help="List of modules, functions and/or classes " - "to profile specified by their name or path. " - "These profiling targets can be supplied both as " - "comma-separated items, or separately with " - "multiple copies of this flag. " - "Packages are automatically recursed into unless " - "they are specified with `.__init__`. " - "Adding the current script/module profiles the " - "entirety of it. " - "Only works with line_profiler -l, --line-by-line.") - parser.add_argument('--no-preimports', - action='store_true', - help="Instead of eagerly importing all profiling " - "targets specified via -p and profiling them, " - "only profile those that are directly imported in " - "the profiled code. " - "Only works with line_profiler -l, --line-by-line.") - parser.add_argument('--prof-imports', action='store_true', - help="If specified, modules specified to `--prof-mod` will also autoprofile modules that they import. " - "Only works with line_profiler -l, --line-by-line") + default = get_cli_config('kernprof') + add_argument(parser, '-V', '--version', + action='version', version=__version__) + add_argument(parser, '--config', + help='Path to the TOML file, from the ' + '`tool.line_profiler.kernprof` table of which to load ' + 'defaults for the options. ' + f'(Default: {short_string_path(default.path)!r})') + add_argument(parser, '--no-config', + action='store_const', dest='config', const=False, + help='Disable the loading of configuration files other ' + 'than the default one') + prof_opts = parser.add_argument_group('profiling options') + add_argument(prof_opts, '-l', '--line-by-line', action='store_true', + help='Use the line-by-line profiler instead of cProfile. ' + 'Implies `--builtin`. ' + f'(Default: {default.conf_dict["line_by_line"]})') + add_argument(prof_opts, '-b', '--builtin', action='store_true', + help="Put `profile` in the builtins. " + "Use `profile.enable()`/`.disable()` to " + "toggle profiling, " + "`@profile` to decorate functions, " + "or `with profile:` to profile a section of code. " + f"(Default: {default.conf_dict['builtin']})") + if default.conf_dict['setup']: + def_setupfile = repr(default.conf_dict['setup']) + else: + def_setupfile = 'N/A' + add_argument(prof_opts, '-s', '--setup', + help='Path to the Python source file containing setup ' + 'code to execute before the code to profile. ' + f'(Default: {def_setupfile})') + if default.conf_dict['prof_mod']: + def_prof_mod = repr(default.conf_dict['prof_mod']) + else: + def_prof_mod = 'N/A' + add_argument(prof_opts, '-p', '--prof-mod', action='append', + help="List of modules, functions and/or classes to profile " + "specified by their name or path. These profiling targets " + "can be supplied both as comma-separated items, or " + "separately with multiple copies of this flag. Packages " + "are automatically recursed into unless they are specified " + "with `.__init__`. Adding the current script/module " + "profiles the entirety of it. Only works with line " + "profiling (`-l`/`--line-by-line`). " + f"(Default: {def_prof_mod}; " + "pass an empty string to clear the defaults (or any `-p` " + "target specified earlier))") + add_argument(prof_opts, '--preimports', action='store_true', + help="Eagerly import all profiling targets specified via " + "`-p` and profile them, instead of only profiling those " + "that are directly imported in the profiled code. " + "Only works with line profiling (`-l`/`--line-by-line`). " + f"(Default: {default.conf_dict['preimports']})") + add_argument(prof_opts, '--prof-imports', action='store_true', + help="If the script/module profiled is in `--prof-mod`, " + "autoprofile all its imports. " + "Only works with line profiling (`-l`/`--line-by-line`). " + f"(Default: {default.conf_dict['prof_imports']})") + out_opts = parser.add_argument_group('output options') + if default.conf_dict['outfile']: + def_outfile = repr(default.conf_dict['outfile']) + else: + def_outfile = ( + "'.lprof' in line-profiling mode " + "(`-l`/`--line-by-line`); " + "'.prof' otherwise") + add_argument(out_opts, '-o', '--outfile', + help=f'Save stats to OUTFILE. (Default: {def_outfile})') + add_argument(out_opts, '-v', '--verbose', '--view', + action='count', default=default.conf_dict['verbose'], + help="Increase verbosity level " + f"(default: {default.conf_dict['verbose']}). " + "At level 1, view the profiling results in addition to " + "saving them; " + "at level 2, show other diagnostic info.") + add_argument(out_opts, '-q', '--quiet', + action='count', default=0, + help='Decrease verbosity level ' + f"(default: {default.conf_dict['verbose']}). " + 'At level -1, disable ' + 'helpful messages (e.g. "Wrote profile results to <...>"); ' + 'at level -2, silence the stdout; ' + 'at level -3, silence the stderr.') + add_argument(out_opts, '-r', '--rich', action='store_true', + help='Use rich formatting if viewing output. ' + f'(Default: {default.conf_dict["rich"]})') + add_argument(out_opts, '-u', '--unit', type=positive_float, + help='Output unit (in seconds) in which ' + 'the timing info is displayed. ' + f'(Default: {default.conf_dict["unit"]} s)') + add_argument(out_opts, '-z', '--skip-zero', action='store_true', + help="Hide functions which have not been called. " + f"(Default: {default.conf_dict['skip_zero']})") + add_argument(out_opts, '--summarize', action='store_true', + help='Print a summary of total function time. ' + f'(Default: {default.conf_dict["summarize"]})') + if default.conf_dict['output_interval']: + def_out_int = f'{default.conf_dict["output_interval"]} s' + else: + def_out_int = '0 s (disabled)' + add_argument(out_opts, '-i', '--output-interval', + type=int, const=1, nargs='?', + help="Enables outputting of cumulative profiling results " + "to OUTFILE every OUTPUT_INTERVAL seconds. " + "Uses the threading module. " + "Minimum value (and the value implied if the bare option " + f"is given) is 1 s. (Default: {def_out_int})") def _build_parsers(args=None): @@ -606,12 +711,13 @@ def _build_parsers(args=None): _add_core_parser_arguments(parser) if parser is help_parser or module is literal_code is None: - parser.add_argument('script', - metavar='{path/to/script' - ' | -m path.to.module | -c "literal code"}', - help='The python script file, module, or ' - 'literal code to run') - parser.add_argument('args', nargs='...', help='Optional script arguments') + add_argument(parser, 'script', + metavar='{path/to/script' + ' | -m path.to.module | -c "literal code"}', + help='The python script file, module, or ' + 'literal code to run') + add_argument(parser, 'args', + nargs='...', help='Optional script arguments') special_info = { 'module': module, 'literal_code': literal_code, @@ -621,7 +727,8 @@ def _build_parsers(args=None): return real_parser, help_parser, special_info -def _parse_arguments(real_parser, help_parser, special_info, args): +def _parse_arguments( + real_parser, help_parser, special_info, args, exit_on_error): module = special_info['module'] literal_code = special_info['literal_code'] @@ -629,17 +736,42 @@ def _parse_arguments(real_parser, help_parser, special_info, args): # Hand off to the dummy parser if necessary to generate the help # text - options = SimpleNamespace(**vars(real_parser.parse_args(args))) + try: + options = SimpleNamespace(**vars(real_parser.parse_args(args))) + except SystemExit as e: + # If `exit_on_error` is true, let `SystemExit` bubble up and + # kill the interpretor; + # else, catch and handle it more gracefully + # (Note: can't use `ArgumentParser(exit_on_error=False)` in + # Python 3.8) + if exit_on_error: + raise + elif e.code: + raise RuntimeError from None + else: + return # TODO: make flags later where appropriate options.dryrun = diagnostics.NO_EXEC options.static = diagnostics.STATIC_ANALYSIS if help_parser and getattr(options, 'help', False): help_parser.print_help() - exit() + if exit_on_error: + raise SystemExit(0) + else: + return + + # Parse the provided config file (if any), and resolve the values + # of the un-specified options try: del options.help except AttributeError: pass + default = get_cli_config('kernprof', options.config) + options.config = default.path + for key, default in default.conf_dict.items(): + if getattr(options, key, None) is None: + setattr(options, key, default) + # Add in the pre-partitioned arguments cut off by `-m ` or # `-c