Skip to content

Commit e19f7b0

Browse files
committed
kernprof.py doc updates and refactoring
kernprof.py __doc__ - Added `sphinx` roles to generic inlined code (e.g. ':command:', ':option:') - Rephrased documentation for the `-l` flag execfile.__doc__ RepeatedTimer.__doc__ find_script.__doc__ _python_command.__doc__ _normalize_profiling_targets.__doc__ _restore_list.__doc__ pre_parse_single_arg_directive.__doc__ _write_tempfile.__doc__ _main.__doc__ Added `sphinx` roles to generic inlined code (e.g. ':command:', ':option:') _write_preimports() Refactored from big chunk of code in `_main()` handling the pre-imports
1 parent 2d7d1c5 commit e19f7b0

File tree

1 file changed

+120
-99
lines changed

1 file changed

+120
-99
lines changed

kernprof.py

Lines changed: 120 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
#!/usr/bin/env python
22
"""
3-
Script to conveniently run profilers on code in a variety of circumstances.
3+
Script to conveniently run profilers on code in a variety of
4+
circumstances.
45
5-
To profile a script, decorate the functions of interest with ``@profile``
6+
To profile a script, decorate the functions of interest with
7+
:py:deco:`profile`:
68
79
.. code:: bash
810
@@ -15,25 +17,27 @@ def main():
1517
1618
NOTE:
1719
18-
New in 4.1.0: Instead of relying on injecting ``profile`` into the builtins
19-
you can now ``import line_profiler`` and use ``line_profiler.profile`` to
20-
decorate your functions. This allows the script to remain functional even
21-
if it is not actively profiled. See :py:mod:`line_profiler` for details.
20+
New in 4.1.0: Instead of relying on injecting :py:deco:`profile`
21+
into the builtins you can now ``import line_profiler`` and use
22+
:py:deco:`line_profiler.profile` to decorate your functions. This
23+
allows the script to remain functional even if it is not actively
24+
profiled. See :py:mod:`line_profiler` for details.
2225
2326
24-
Then run the script using kernprof:
27+
Then run the script using :command:`kernprof`:
2528
2629
.. code:: bash
2730
2831
kernprof -b script_to_profile.py
2932
30-
By default this runs with the default :py:mod:`cProfile` profiler and does not
31-
require compiled modules. Instructions to view the results will be given in the
32-
output. Alternatively, adding ``-v`` to the command line will write results to
33-
stdout.
33+
By default this runs with the default :py:mod:`cProfile` profiler and
34+
does not require compiled modules. Instructions to view the results will
35+
be given in the output. Alternatively, adding :option:`-v` to the
36+
command line will write results to stdout.
3437
35-
To enable line-by-line profiling, then :py:mod:`line_profiler` must be
36-
available and compiled. Add the ``-l`` argument to the kernprof invocation.
38+
To enable line-by-line profiling, :py:mod:`line_profiler` must be
39+
available and compiled, and the :option:`-l` argument should be added to
40+
the :command:`kernprof` invocation:
3741
3842
.. code:: bash
3943
@@ -43,17 +47,21 @@ def main():
4347
4448
New in 4.3.0: More code execution options are added:
4549
46-
* ``kernprof <options> -m some.module <args to module>`` parallels
47-
``python -m`` and runs the provided module as ``__main__``.
48-
* ``kernprof <options> -c "some code" <args to code>`` parallels
49-
``python -c`` and executes the provided literal code.
50-
* ``kernprof <options> - <args to code>`` parallels ``python -`` and
51-
executes literal code passed via the ``stdin``.
50+
* :command:`kernprof <options> -m some.module <args to module>`
51+
parallels :command:`python -m` and runs the provided module as
52+
``__main__``.
53+
* :command:`kernprof <options> -c "some code" <args to code>`
54+
parallels :command:`python -c` and executes the provided literal
55+
code.
56+
* :command:`kernprof <options> - <args to code>` parallels
57+
:command:`python -` and executes literal code passed via the
58+
``stdin``.
5259
53-
See also :doc:`kernprof invocations </manual/examples/example_kernprof>`.
60+
See also
61+
:doc:`kernprof invocations </manual/examples/example_kernprof>`.
5462
5563
For more details and options, refer to the CLI help.
56-
To view kernprof help run:
64+
To view :command:`kernprof` help run:
5765
5866
.. code:: bash
5967
@@ -98,12 +106,13 @@ def main():
98106
NOTE:
99107
100108
New in 4.3.0: For more intuitive profiling behavior, profiling
101-
targets in ``--prof-mod`` (except the profiled script/code) are now
102-
eagerly pre-imported to be profiled
109+
targets in :option:`--prof-mod` (except the profiled script/code)
110+
are now eagerly pre-imported to be profiled
103111
(see :py:mod:`line_profiler.autoprofile.eager_preimports`),
104112
regardless of whether those imports directly occur in the profiled
105113
script/module/code.
106-
To restore the old behavior, pass the ``--no-preimports`` flag.
114+
To restore the old behavior, pass the :option:`--no-preimports`
115+
flag.
107116
"""
108117
import builtins
109118
import functools
@@ -134,15 +143,15 @@ def main():
134143

135144

136145
def execfile(filename, globals=None, locals=None):
137-
""" Python 3.x doesn't have 'execfile' builtin """
146+
""" Python 3.x doesn't have :py:func:`execfile` builtin """
138147
with open(filename, 'rb') as f:
139148
exec(compile(f.read(), filename, 'exec'), globals, locals)
140149
# =====================================
141150

142151

143152
class ContextualProfile(ByCountProfilerMixin, Profile):
144-
""" A subclass of Profile that adds a context manager for Python
145-
2.5 with: statements and a decorator.
153+
""" A subclass of :py:class:`Profile` that adds a context manager
154+
for Python 2.5 with: statements and a decorator.
146155
"""
147156
def __init__(self, *args, **kwds):
148157
super(ByCountProfilerMixin, self).__init__(*args, **kwds)
@@ -174,7 +183,7 @@ def disable_by_count(self):
174183

175184
class RepeatedTimer:
176185
"""
177-
Background timer for outputting file every n seconds.
186+
Background timer for outputting file every ``n`` seconds.
178187
179188
Adapted from [SO474528]_.
180189
@@ -223,7 +232,7 @@ def find_module_script(module_name):
223232
def find_script(script_name, exit_on_error=True):
224233
""" Find the script.
225234
226-
If the input is not a file, then $PATH will be searched.
235+
If the input is not a file, then :envvar:`PATH` will be searched.
227236
"""
228237
if os.path.isfile(script_name):
229238
return script_name
@@ -245,7 +254,7 @@ def find_script(script_name, exit_on_error=True):
245254

246255
def _python_command():
247256
"""
248-
Return a command that corresponds to :py:obj:`sys.executable`.
257+
Return a command that corresponds to :py:data:`sys.executable`.
249258
"""
250259
import shutil
251260
for abbr in 'python', 'python3':
@@ -256,7 +265,7 @@ def _python_command():
256265

257266
def _normalize_profiling_targets(targets):
258267
"""
259-
Normalize the parsed ``--prof-mod`` by:
268+
Normalize the parsed :option:`--prof-mod` by:
260269
261270
* Normalizing file paths with :py:func:`find_script()`, and
262271
subsequently to absolute paths.
@@ -285,7 +294,7 @@ def find(path):
285294

286295
class _restore_list:
287296
"""
288-
Restore a list like ``sys.path`` after running code which
297+
Restore a list like :py:data:`sys.path` after running code which
289298
potentially modifies it.
290299
291300
Example
@@ -329,7 +338,8 @@ def wrapper(*args, **kwargs):
329338
def pre_parse_single_arg_directive(args, flag, sep='--'):
330339
"""
331340
Pre-parse high-priority single-argument directives like
332-
``-m module`` to emulate the behavior of ``python [...]``.
341+
:option:`-m module` to emulate the behavior of
342+
:command:`python [...]`.
333343
334344
Examples
335345
--------
@@ -521,7 +531,8 @@ def positive_float(value):
521531

522532
def _write_tempfile(source, content, options, tmpdir):
523533
"""
524-
Called by ``main()`` to handle ``kernprof -c`` and ``kernprof -``;
534+
Called by :py:func:`main()` to handle :command:`kernprof -c` and
535+
:command:`kernprof -`;
525536
not to be invoked on its own.
526537
"""
527538
import textwrap
@@ -548,9 +559,81 @@ def _write_tempfile(source, content, options, tmpdir):
548559
suffix='.' + extension)
549560

550561

562+
def _write_preimports(prof, prof_mod, exclude):
563+
"""
564+
Called by :py:func:`main()` to handle eager pre-imports;
565+
not to be invoked on its own.
566+
"""
567+
from line_profiler.autoprofile.eager_preimports import (
568+
is_dotted_path, propose_names, write_eager_import_module)
569+
from line_profiler.autoprofile.util_static import modpath_to_modname
570+
from line_profiler.autoprofile.autoprofile import (
571+
_extend_line_profiler_for_profiling_imports as upgrade_profiler)
572+
573+
filtered_targets = []
574+
invalid_targets = []
575+
for target in prof_mod:
576+
if is_dotted_path(target):
577+
filtered_targets.append(target)
578+
continue
579+
# Paths already normalized by `_normalize_profiling_targets()`
580+
if not os.path.exists(target):
581+
invalid_targets.append(target)
582+
continue
583+
if any(os.path.samefile(target, excluded) for excluded in exclude):
584+
# Ignore the script to be run in eager importing
585+
# (`line_profiler.autoprofile.autoprofile.run()` will handle
586+
# it)
587+
continue
588+
modname = modpath_to_modname(target)
589+
if modname is None: # Not import-able
590+
invalid_targets.append(target)
591+
continue
592+
filtered_targets.append(modname)
593+
if invalid_targets:
594+
invalid_targets = sorted(set(invalid_targets))
595+
msg = ('{} profile-on-import target{} cannot be converted to '
596+
'dotted-path form: {!r}'
597+
.format(len(invalid_targets),
598+
'' if len(invalid_targets) == 1 else 's',
599+
invalid_targets))
600+
warnings.warn(msg)
601+
if not filtered_targets:
602+
return
603+
# - We could've done everything in-memory with `io.StringIO` and
604+
# `exec()`, but that results in indecipherable tracebacks should
605+
# anything goes wrong;
606+
# so we write to a tempfile and `execfile()` it
607+
# - While this works theoretically for preserving traceback, the
608+
# catch is that the tempfile will already have been deleted by the
609+
# time the traceback is formatted;
610+
# so we have to format the traceback and manually print the
611+
# context before re-raising the error
612+
upgrade_profiler(prof)
613+
temp_mod_name = next(
614+
name for name in propose_names(['_kernprof_eager_preimports'])
615+
if name not in sys.modules)
616+
with tempfile.TemporaryDirectory() as tmpdir:
617+
temp_mod_path = os.path.join(tmpdir, temp_mod_name + '.py')
618+
with open(temp_mod_path, mode='w') as fobj:
619+
write_eager_import_module(filtered_targets, stream=fobj)
620+
ns = {} # Use a fresh namespace
621+
try:
622+
execfile(temp_mod_path, ns, ns)
623+
except Exception as e:
624+
tb_lines = traceback.format_tb(e.__traceback__)
625+
i_last_temp_frame = max(
626+
i for i, line in enumerate(tb_lines)
627+
if temp_mod_path in line)
628+
print('\nContext:', ''.join(tb_lines[i_last_temp_frame:]),
629+
end='', sep='\n', file=sys.stderr)
630+
raise
631+
632+
551633
def _main(options, module=False):
552634
"""
553-
Called by ``main()`` for the actual execution and profiling of code;
635+
Called by :py:func:`main()` for the actual execution and profiling
636+
of code;
554637
not to be invoked on its own.
555638
"""
556639
if not options.outfile:
@@ -626,70 +709,8 @@ def _main(options, module=False):
626709
# contains the script file to be run. E.g. the script may not
627710
# even have a `if __name__ == '__main__': ...` guard. So don't
628711
# eager-import it.
629-
from line_profiler.autoprofile.eager_preimports import (
630-
is_dotted_path, propose_names, write_eager_import_module)
631-
from line_profiler.autoprofile.util_static import modpath_to_modname
632-
from line_profiler.autoprofile.autoprofile import (
633-
_extend_line_profiler_for_profiling_imports as upgrade_profiler)
634-
635-
filtered_targets = []
636-
invalid_targets = []
637-
for target in options.prof_mod:
638-
if is_dotted_path(target):
639-
filtered_targets.append(target)
640-
continue
641-
# Filenames are already normalized in
642-
# `_normalize_profiling_targets()`
643-
if not os.path.exists(target):
644-
invalid_targets.append(target)
645-
continue
646-
if not module and os.path.samefile(target, script_file):
647-
# Ignore the script to be run in eager importing
648-
# (`line_profiler.autoprofile.autoprofile.run()` will
649-
# handle it)
650-
continue
651-
modname = modpath_to_modname(target)
652-
if modname is None: # Not import-able
653-
invalid_targets.append(target)
654-
continue
655-
filtered_targets.append(modname)
656-
if invalid_targets:
657-
invalid_targets = sorted(set(invalid_targets))
658-
msg = ('{} profile-on-import target{} cannot be converted to '
659-
'dotted-path form: {!r}'
660-
.format(len(invalid_targets),
661-
'' if len(invalid_targets) == 1 else 's',
662-
invalid_targets))
663-
warnings.warn(msg)
664-
if filtered_targets:
665-
# - We could've done everything in-memory with `io.StringIO`
666-
# and `exec()`, but that results in indecipherable
667-
# tracebacks should anything goes wrong;
668-
# so we write to a tempfile and `execfile()` it
669-
# - While this works theoretically for preserving traceback,
670-
# the catch is that the tempfile will already have been
671-
# deleted by the time the traceback is formatted;
672-
# so we have to format the traceback and manually print
673-
# the context before re-raising the error
674-
upgrade_profiler(prof)
675-
temp_mod_name = next(
676-
name for name in propose_names(['_kernprof_eager_preimports'])
677-
if name not in sys.modules)
678-
with tempfile.TemporaryDirectory() as tmpdir:
679-
temp_mod_path = os.path.join(tmpdir, temp_mod_name + '.py')
680-
with open(temp_mod_path, mode='w') as fobj:
681-
write_eager_import_module(filtered_targets, stream=fobj)
682-
ns = {} # Use a fresh namespace
683-
try:
684-
execfile(temp_mod_path, ns, ns)
685-
except Exception as e:
686-
tb_lines = traceback.format_tb(e.__traceback__)
687-
i_last_temp_frame = max(
688-
i for i, line in enumerate(tb_lines)
689-
if temp_mod_path in line)
690-
print('\nContext:', ''.join(tb_lines[i_last_temp_frame:]),
691-
end='', sep='\n', file=sys.stderr)
692-
raise
712+
exclude = set() if module else {script_file}
713+
_write_preimports(prof, options.prof_mod, exclude)
693714

694715
if options.output_interval:
695716
rt = RepeatedTimer(max(options.output_interval, 1), prof.dump_stats, options.outfile)

0 commit comments

Comments
 (0)