Skip to content

Commit ed5c6f8

Browse files
committed
More convenience updates in kernprof
CHANGELOG.rst Edited entry kernprof.py __doc__ Updated main() - Updated function used to find name of the Python executable to be more lenient in abbreviating the name, requiring file identity (via `os.path.samefile()`) instead of string-path equality - Added new option `-c`, which causes the positional argument to be interpreted as an inline script (as with `python -c`) instead of the path to a script file - Added special case for `script = -` to read the script to profile from stdin
1 parent 3537a50 commit ed5c6f8

File tree

2 files changed

+87
-23
lines changed

2 files changed

+87
-23
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Changes
1010
* ENH: Added CLI argument ``-m`` to ``kernprof`` for running a library module as a script; also made it possible for profiling targets to be supplied across multiple ``-p`` flags
1111
* 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
1212
* FIX: Fixed namespace bug when running ``kernprof -m`` on certain modules (e.g. ``calendar`` on Python 3.12+).
13+
* 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``
14+
1315

1416
4.2.0
1517
~~~~~

kernprof.py

Lines changed: 85 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -51,29 +51,31 @@ def main():
5151
5252
.. code::
5353
54-
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} ...
54+
usage: kernprof [-h] [-V] [-l] [-b] [-o OUTFILE] [-s SETUP] [-v] [-r] [-u UNIT] [-z] [-i [OUTPUT_INTERVAL]] [-p PROF_MOD] [--prof-imports]
55+
{path/to/script | -m path.to.module | -c "literal code"} ...
5556
5657
Run and profile a python script.
5758
5859
positional arguments:
59-
{script | -m module} The python script file or module to run
60+
{path/to/script | -m path.to.module | -c "literal code"}
61+
The python script file, module, or literal code to run
6062
args Optional script arguments
6163
6264
options:
6365
-h, --help show this help message and exit
6466
-V, --version show program's version number and exit
6567
-l, --line-by-line Use the line-by-line profiler instead of cProfile. Implies --builtin.
6668
-b, --builtin Put 'profile' in the builtins. Use 'profile.enable()'/'.disable()', '@profile' to decorate functions, or 'with profile:' to profile a section of code.
67-
-o OUTFILE, --outfile OUTFILE
69+
-o, --outfile OUTFILE
6870
Save stats to <outfile> (default: 'scriptname.lprof' with --line-by-line, 'scriptname.prof' without)
69-
-s SETUP, --setup SETUP
70-
Code to execute before the code to profile
71+
-s, --setup SETUP Code to execute before the code to profile
7172
-v, --view View the results of the profile in addition to saving it
7273
-r, --rich Use rich formatting if viewing output
73-
-u UNIT, --unit UNIT Output unit (in seconds) in which the timing info is displayed (default: 1e-6)
74+
-u, --unit UNIT Output unit (in seconds) in which the timing info is displayed (default: 1e-6)
7475
-z, --skip-zero Hide functions which have not been called
75-
-i [OUTPUT_INTERVAL], --output-interval [OUTPUT_INTERVAL]
76-
Enables outputting of cumulative profiling results to file every n seconds. Uses the threading module. Minimum value is 1 (second). Defaults to disabled.
76+
-i, --output-interval [OUTPUT_INTERVAL]
77+
Enables outputting of cumulative profiling results to file every n seconds. Uses the threading module. Minimum value is 1 (second). Defaults to
78+
disabled.
7779
-p, --prof-mod PROF_MOD
7880
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
7981
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
@@ -216,12 +218,10 @@ def _python_command():
216218
Return a command that corresponds to :py:obj:`sys.executable`.
217219
"""
218220
import shutil
219-
if shutil.which('python') == sys.executable:
220-
return 'python'
221-
elif shutil.which('python3') == sys.executable:
222-
return 'python3'
223-
else:
224-
return sys.executable
221+
for abbr in 'python', 'python3':
222+
if os.path.samefile(shutil.which(abbr), sys.executable):
223+
return abbr
224+
return sys.executable
225225

226226

227227
@contextlib.contextmanager
@@ -332,11 +332,22 @@ def positive_float(value):
332332
if args is None:
333333
args = sys.argv[1:]
334334

335-
# Special case: `kernprof [...] -m <module>` should terminate the
336-
# parsing of all subsequent options
337-
args, module, post_args = pre_parse_single_arg_directive(args, '-m')
335+
# Special cases: `kernprof [...] -m <module>` or
336+
# `kernprof [...] -c <script>` should terminate the parsing of all
337+
# subsequent options
338+
if '-m' in args and '-c' in args:
339+
special_mode = min(['-c', '-m'], key=args.index)
340+
elif '-m' in args:
341+
special_mode = '-m'
342+
else:
343+
special_mode = '-c'
344+
args, thing, post_args = pre_parse_single_arg_directive(args, special_mode)
345+
if special_mode == '-m':
346+
module, literal_code = thing, None
347+
else:
348+
module, literal_code = None, thing
338349

339-
if module is None: # Normal execution
350+
if module is literal_code is None: # Normal execution
340351
real_parser, = parsers = [create_parser()]
341352
help_parser = None
342353
else:
@@ -383,27 +394,78 @@ def positive_float(value):
383394
help="If specified, modules specified to `--prof-mod` will also autoprofile modules that they import. "
384395
"Only works with line_profiler -l, --line-by-line")
385396

386-
if parser is help_parser or module is None:
397+
if parser is help_parser or module is literal_code is None:
387398
parser.add_argument('script',
388-
metavar='{script | -m module}',
389-
help='The python script file or module to run')
399+
metavar='{path/to/script'
400+
' | -m path.to.module | -c "literal code"}',
401+
help='The python script file, module, or '
402+
'literal code to run')
390403
parser.add_argument('args', nargs='...', help='Optional script arguments')
391404

392405
# Hand off to the dummy parser if necessary to generate the help
393406
# text
394407
options = real_parser.parse_args(args)
395408
if help_parser and getattr(options, 'help', False):
396409
# This should raise a `SystemExit`
397-
help_parser.parse_args([*args, '-m', module])
410+
help_parser.parse_args([*args, '-m', 'dummy'])
398411
try:
399412
del options.help
400413
except AttributeError:
401414
pass
402-
# Add in the pre-partitioned arguments cut off by `-m <module>`
415+
# Add in the pre-partitioned arguments cut off by `-m <module>` or
416+
# `-c <script>`
403417
options.args += post_args
404418
if module is not None:
405419
options.script = module
406420

421+
tempfile_source_and_content = None
422+
if literal_code is not None:
423+
tempfile_source_and_content = 'command', literal_code
424+
elif options.script == '-' and not module:
425+
tempfile_source_and_content = 'stdin', sys.stdin.read()
426+
427+
if not tempfile_source_and_content:
428+
return _main(options, module)
429+
430+
# Importing `ast` is IIRC somewhat expensive, so don't do that if we
431+
# don't have to
432+
import ast
433+
import tempfile
434+
435+
source, content = tempfile_source_and_content
436+
file_prefix = f'kernprof-{source}-'
437+
with tempfile.NamedTemporaryFile(mode='w',
438+
prefix=file_prefix,
439+
suffix='.py') as fobj:
440+
# Set up the script to be run
441+
try:
442+
content = ast.unparse(ast.parse(content))
443+
except (
444+
# `ast.unparse()` unavailable in Python < 3.9
445+
AttributeError,
446+
# Big module - shouldn't happen since the script should
447+
# just be one inline thing (except when reading from
448+
# stdin), which can't be all that complicated
449+
RecursionError):
450+
pass
451+
print(content, file=fobj, flush=True)
452+
options.script = fobj.name
453+
# Add the tempfile to `--prof-mod`
454+
if options.prof_mod:
455+
options.prof_mod += ',' + fobj.name
456+
else:
457+
options.prof_mod = fobj.name
458+
# Set the output file to somewhere nicer (also take care of
459+
# possible filename clash)
460+
if not options.outfile:
461+
extension = 'lprof' if options.line_by_line else 'prof'
462+
_, options.outfile = tempfile.mkstemp(dir=os.curdir,
463+
prefix=file_prefix,
464+
suffix='.' + extension)
465+
return _main(options, module)
466+
467+
468+
def _main(options, module=False):
407469
if not options.outfile:
408470
extension = 'lprof' if options.line_by_line else 'prof'
409471
options.outfile = '%s.%s' % (os.path.basename(options.script), extension)

0 commit comments

Comments
 (0)