diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b9d96b1c..7d2f1809 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +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 module/package/inline-script execution instead of that of script files; passing ``'-'`` as the script-file name now also reads from and profiles ``stdin`` 4.2.0 ~~~~~ diff --git a/docs/source/manual/examples/example_kernprof.rst b/docs/source/manual/examples/example_kernprof.rst new file mode 100644 index 00000000..97c157ca --- /dev/null +++ b/docs/source/manual/examples/example_kernprof.rst @@ -0,0 +1,383 @@ +``kernprof`` invocations +======================== + +The module (and installed script) :py:mod:`kernprof` can be used to run +and profile Python code in various forms. + +For the following, we assume that we have: + +* the below file ``fib.py`` in the current directory, and +* :py:mod:`line_profiler` and :py:mod:`kernprof` installed. + +.. code:: python + + import functools + import sys + from argparse import ArgumentParser + from typing import Callable, Optional, Sequence + + + @functools.lru_cache() + def fib(n: int) -> int: + return _run_fib(fib, n) + + + def fib_no_cache(n: int) -> int: + return _run_fib(fib_no_cache, n) + + + def _run_fib(fib: Callable[[int], int], n: int) -> int: + if n < 0: + raise ValueError(f'{n = !r}: expected non-negative integer') + if n < 2: + return 1 + prev_prev = fib(n - 2) + prev = fib(n - 1) + return prev_prev + prev + + + def main(args: Optional[Sequence[str]] = None) -> None: + parser = ArgumentParser() + parser.add_argument('n', nargs='+', type=int) + parser.add_argument('--verbose', action='store_true') + parser.add_argument('--no-cache', action='store_true') + arguments = parser.parse_args(args) + + pattern = 'fib({!r}) = {!r}' if arguments.verbose else '{1!r}' + func = fib_no_cache if arguments.no_cache else fib + + for n in arguments.n: + result = func(n) + print(pattern.format(n, result)) + + + if __name__ == '__main__': + main() + + +Script execution +---------------- + +In the most basic form, one passes the path to the executed script and +its arguments to ``kernprof``: + +.. code:: bash + + kernprof --prof-mod fib.py --line-by-line --view \ + fib.py --verbose 10 20 30 + +.. raw:: html + +
+ Output (click to expand) + +.. code:: + + fib(10) = 89 + fib(20) = 10946 + fib(30) = 1346269 + Wrote profile results to fib.py.lprof + Timer unit: 1e-06 s + + Total time: 5.6e-05 s + File: fib.py + Function: fib at line 7 + + Line # Hits Time Per Hit % Time Line Contents + ============================================================== + 7 @functools.lru_cache() + 8 def fib(n: int) -> int: + 9 31 56.0 1.8 100.0 return _run_fib(fib, n) + + Total time: 0 s + File: fib.py + Function: fib_no_cache at line 12 + + Line # Hits Time Per Hit % Time Line Contents + ============================================================== + 12 def fib_no_cache(n: int) -> int: + 13 return _run_fib(fib_no_cache, n) + + Total time: 3.8e-05 s + File: fib.py + Function: _run_fib at line 16 + + Line # Hits Time Per Hit % Time Line Contents + ============================================================== + 16 def _run_fib(fib: Callable[[int], int], n: int) -> int: + 17 31 3.0 0.1 7.9 if n < 0: + 18 raise ValueError(f'{n = !r}: expected non-negative integer') + 19 31 2.0 0.1 5.3 if n < 2: + 20 2 0.0 0.0 0.0 return 1 + 21 29 18.0 0.6 47.4 prev_prev = fib(n - 2) + 22 29 12.0 0.4 31.6 prev = fib(n - 1) + 23 29 3.0 0.1 7.9 return prev_prev + prev + + Total time: 0.000486 s + File: fib.py + Function: main at line 26 + + Line # Hits Time Per Hit % Time Line Contents + ============================================================== + 26 def main(args: Optional[Sequence[str]] = None) -> None: + 27 1 184.0 184.0 37.9 parser = ArgumentParser() + 28 1 17.0 17.0 3.5 parser.add_argument('n', nargs='+', type=int) + 29 1 16.0 16.0 3.3 parser.add_argument('--verbose', action='store_true') + 30 1 14.0 14.0 2.9 parser.add_argument('--no-cache', action='store_true') + 31 1 144.0 144.0 29.6 arguments = parser.parse_args(args) + 32 + 33 1 0.0 0.0 0.0 pattern = 'fib({!r}) = {!r}' if arguments.verbose else '{1!r}' + 34 1 0.0 0.0 0.0 func = fib_no_cache if arguments.no_cache else fib + 35 + 36 4 0.0 0.0 0.0 for n in arguments.n: + 37 3 91.0 30.3 18.7 result = func(n) + 38 3 20.0 6.7 4.1 print(pattern.format(n, result)) + +.. raw:: html + +
+

+ + +.. _kernprof-script-note: +.. note:: + + Instead of passing the ``--view`` flag to ``kernprof`` to view the + profiling results immediately, sometimes it can be more convenient to + just generate the profiling results and view them later by running + the :py:mod:`line_profiler` module (``python -m line_profiler``). + + +Module execution +---------------- + +It is also possible to use ``kernprof -m`` to run installed modules and +packages: + +.. code:: bash + + PYTHONPATH="${PYTHONPATH}:${PWD}" \ + kernprof --prof-mod fib --line-by-line --view -m \ + fib --verbose 10 20 30 + +.. raw:: html + +

+ Output (click to expand) + +.. code:: + + fib(10) = 89 + fib(20) = 10946 + fib(30) = 1346269 + Wrote profile results to fib.lprof + ... + +.. raw:: html + +
+

+ +.. _kernprof-m-note: +.. note:: + + As with ``python -m``, the ``-m`` option terminates further parsing + of arguments by ``kernprof`` and passes them all to the argument + thereafter (the run module). + If there isn't one, an error is raised: + + .. code:: bash + + kernprof -m + + .. raw:: html + +

+ Output (click to expand) + + .. code:: pycon + + Traceback (most recent call last): + ... + ValueError: argument expected for the -m option + + .. raw:: html + +
+ + +Literal-code execution +---------------------- + +Like how ``kernprof -m`` parallels ``python -m``, ``kernprof -c`` can be +used to run and profile literal snippets supplied on the command line +like ``python -c``: + +.. code:: bash + + PYTHONPATH="${PYTHONPATH}:${PWD}" \ + kernprof --prof-mod fib._run_fib --line-by-line --view -c " + import sys + from fib import _run_fib, fib_no_cache as fib + for n in sys.argv[1:]: + print(f'fib({n})', '=', fib(int(n))) + " 10 20 + +.. raw:: html + +
+ Output (click to expand) + +.. code:: + + fib(10) = 89 + fib(20) = 10946 + Wrote profile results to <...>/kernprof-command-imuhz89_.lprof + Timer unit: 1e-06 s + + Total time: 0.007666 s + File: <...>/fib.py + Function: _run_fib at line 16 + + Line # Hits Time Per Hit % Time Line Contents + ============================================================== + 16 def _run_fib(fib: Callable[[int], int], n: int) -> int: + 17 22068 1656.0 0.1 20.6 if n < 0: + 18 raise ValueError(f'{n = !r}: expected non-negative integer') + 19 22068 1663.0 0.1 20.7 if n < 2: + 20 11035 814.0 0.1 10.1 return 1 + 21 11033 1668.0 0.2 20.7 prev_prev = fib(n - 2) + 22 11033 1477.0 0.1 18.4 prev = fib(n - 1) + 23 11033 770.0 0.1 9.6 return prev_prev + prev + +.. raw:: html + +
+

+ +.. note:: + + * As with ``python -c``, the ``-c`` option terminates further + parsing of arguments by ``kernprof`` and passes them all to the + argument thereafter (the executed code). + If there isn't one, an error is raised as + :ref:`above ` with ``kernprof -m``. + * .. _kernprof-c-note: + Since the temporary file containing the executed code will not + exist beyond the ``kernprof`` process, profiling results + pertaining to targets (function definitions) local to said code + :ref:`will not be accessible later ` by + ``python -m line_profiler`` and has to be ``--view``-ed + immediately: + + .. code:: bash + + PYTHONPATH="${PYTHONPATH}:${PWD}" \ + kernprof --line-by-line --view -c " + from fib import fib + + def my_func(n=50): + result = fib(n) + print(n, '->', result) + + my_func()" + + .. raw:: html + +

+ Output (click to expand) + + .. code:: + + 50 -> 20365011074 + Wrote profile results to <...>/kernprof-command-ni6nis6t.lprof + Timer unit: 1e-06 s + + Total time: 3.8e-05 s + File: <...>/kernprof-command.py + Function: my_func at line 3 + + Line # Hits Time Per Hit % Time Line Contents + ============================================================== + 3 def my_func(n=50): + 4 1 26.0 26.0 68.4 result = fib(n) + 5 1 12.0 12.0 31.6 print(n, '->', result) + + .. raw:: html + +
+

+ + .. code:: bash + + python -m line_profiler kernprof-command-ni6nis6t.lprof + + .. raw:: html + +

+ Output (click to expand) + + .. code:: + + Timer unit: 1e-06 s + + Total time: 3.6e-05 s + + Could not find file <...>/kernprof-command.py + Are you sure you are running this program from the same directory + that you ran the profiler from? + Continuing without the function's contents. + + Line # Hits Time Per Hit % Time Line Contents + ============================================================== + 3 + 4 1 26.0 26.0 72.2 + 5 1 10.0 10.0 27.8 + + .. raw:: html + +
+ + +Executing code read from ``stdin`` +---------------------------------- + +It is also possible to read, run, and profile code from ``stdin``, by +passing ``-`` to ``kernprof`` in place of a filename: + +.. code:: bash + + { + # This example doesn't make much sense on its own, but just + # imagine if this is a command generating code dynamically + echo 'import sys' + echo 'from fib import _run_fib, fib_no_cache as fib' + echo 'for n in sys.argv[1:]:' + echo ' print(f"fib({n})", "=", fib(int(n)))' + } | PYTHONPATH="${PYTHONPATH}:${PWD}" \ + kernprof --prof-mod fib._run_fib --line-by-line --view - 10 20 + +.. raw:: html + +
+ Output (click to expand) + +.. code:: + + fib(10) = 89 + fib(20) = 10946 + Wrote profile results to <...>/kernprof-stdin-kntk2lo1.lprof + ... + +.. raw:: html + +
+

+ +.. note:: + + Since the temporary file containing the executed code will not exist + beyond the ``kernprof`` process, profiling results pertaining to + targets (function definitions) local to said code will not be + accessible later and has to be ``--view``-ed immediately + (see :ref:`above note ` on ``kernprof -c``). diff --git a/docs/source/manual/examples/index.rst b/docs/source/manual/examples/index.rst index 03acadc7..18589aa6 100644 --- a/docs/source/manual/examples/index.rst +++ b/docs/source/manual/examples/index.rst @@ -5,6 +5,8 @@ Examples of line profiler usage: + `Basic Usage <../../index.html#line-profiler-basic-usage>`_ ++ `kernprof invocations `_ + + `Auto Profiling <../../auto/line_profiler.autoprofile.html#auto-profiling>`_ + `Explicit Profiler <../../auto/line_profiler.explicit_profiler.html#module-line_profiler.explicit_profiler>`_ diff --git a/kernprof.py b/kernprof.py index 52867331..39bbfde5 100755 --- a/kernprof.py +++ b/kernprof.py @@ -39,6 +39,18 @@ def main(): kernprof -lb script_to_profile.py +NOTE: + + New in 4.3.0: more code execution options are added: + + * ``kernprof -m some.module `` parallels + ``python -m`` and runs the provided module as ``__main__``. + * ``kernprof -c "some code" `` parallels + ``python -c`` and executes the provided literal code. + * ``kernprof - `` parallels ``python -`` and + executes literal code passed via the ``stdin``. + + See also :doc:`kernprof invocations `. For more details and options, refer to the CLI help. To view kernprof help run: @@ -51,12 +63,14 @@ def main(): .. code:: - usage: kernprof [-h] [-V] [-l] [-b] [-o OUTFILE] [-s SETUP] [-v] [-r] [-u UNIT] [-z] [-i [OUTPUT_INTERVAL]] [-p PROF_MOD] [-m] [--prof-imports] {script | -m module} ... + usage: kernprof [-h] [-V] [-l] [-b] [-o OUTFILE] [-s SETUP] [-v] [-r] [-u UNIT] [-z] [-i [OUTPUT_INTERVAL]] [-p PROF_MOD] [--prof-imports] + {path/to/script | -m path.to.module | -c "literal code"} ... Run and profile a python script. positional arguments: - {script | -m module} The python script file or module to run + {path/to/script | -m path.to.module | -c "literal code"} + The python script file, module, or literal code to run args Optional script arguments options: @@ -64,16 +78,16 @@ def main(): -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. - -o OUTFILE, --outfile OUTFILE + -o, --outfile OUTFILE Save stats to (default: 'scriptname.lprof' with --line-by-line, 'scriptname.prof' without) - -s SETUP, --setup SETUP - Code to execute before the code to profile + -s, --setup SETUP Code to execute before the code to profile -v, --view View the results of the profile in addition to saving it -r, --rich Use rich formatting if viewing output - -u UNIT, --unit UNIT Output unit (in seconds) in which the timing info is displayed (default: 1e-6) + -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 - -i [OUTPUT_INTERVAL], --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. + -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 PROF_MOD List of modules, functions and/or classes to profile specified by their name or path. List is comma separated, adding the current script path profiles the full script. Multiple copies of this flag can be supplied and the.list is extended. Only works with line_profiler -l, --line-by-line @@ -86,6 +100,7 @@ def main(): import threading import asyncio # NOQA import concurrent.futures # NOQA +import tempfile import time from argparse import ArgumentError, ArgumentParser from runpy import run_module @@ -215,12 +230,10 @@ def _python_command(): Return a command that corresponds to :py:obj:`sys.executable`. """ import shutil - if shutil.which('python') == sys.executable: - return 'python' - elif shutil.which('python3') == sys.executable: - return 'python3' - else: - return sys.executable + for abbr in 'python', 'python3': + if os.path.samefile(shutil.which(abbr), sys.executable): + return abbr + return sys.executable class _restore_list: @@ -345,11 +358,22 @@ def positive_float(value): if args is None: args = sys.argv[1:] - # Special case: `kernprof [...] -m ` should terminate the - # parsing of all subsequent options - args, module, post_args = pre_parse_single_arg_directive(args, '-m') + # Special cases: `kernprof [...] -m ` or + # `kernprof [...] -c