Skip to content

Commit 3537a50

Browse files
authored
Merge pull request #339 from TTsangSC/module-fix
FIX: `kernprof -m`: presence of the executed module as `sys.modules['__main__']`
2 parents 155c4c2 + 53a3846 commit 3537a50

4 files changed

Lines changed: 117 additions & 27 deletions

File tree

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Changes
99
* FIX: Fixed auto-profiling of async function definitions #330
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
12+
* FIX: Fixed namespace bug when running ``kernprof -m`` on certain modules (e.g. ``calendar`` on Python 3.12+).
1213

1314
4.2.0
1415
~~~~~

kernprof.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,8 @@ def positive_float(value):
470470
try:
471471
try:
472472
execfile_ = execfile
473-
rmod_ = run_module
473+
rmod_ = functools.partial(run_module,
474+
run_name='__main__', alter_sys=True)
474475
ns = locals()
475476
if options.prof_mod and options.line_by_line:
476477
from line_profiler.autoprofile import autoprofile
@@ -487,13 +488,11 @@ def positive_float(value):
487488
profile_imports=options.prof_imports,
488489
as_module=module is not None)
489490
elif module and options.builtin:
490-
run_module(options.script, ns, '__main__')
491+
rmod_(options.script, ns)
491492
elif options.builtin:
492493
execfile(script_file, ns, ns)
493494
elif module:
494-
prof.runctx(f'rmod_({options.script!r}, globals(), "__main__")',
495-
ns,
496-
ns)
495+
prof.runctx(f'rmod_({options.script!r}, globals())', ns, ns)
497496
else:
498497
prof.runctx('execfile_(%r, globals())' % (script_file,), ns, ns)
499498
except (KeyboardInterrupt, SystemExit):

line_profiler/autoprofile/autoprofile.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,16 @@ def main():
4444
python -m kernprof -p demo.py -l demo.py
4545
python -m line_profiler -rmt demo.py.lprof
4646
"""
47-
47+
import contextlib
48+
import functools
49+
import importlib.util
50+
import operator
51+
import sys
4852
import types
4953
from .ast_tree_profiler import AstTreeProfiler
5054
from .run_module import AstTreeModuleProfiler
5155
from .line_profiler_utils import add_imported_function_or_module
56+
from .util_static import modpath_to_modname
5257

5358
PROFILER_LOCALS_NAME = 'prof'
5459

@@ -92,10 +97,42 @@ def run(script_file, ns, prof_mod, profile_imports=False, as_module=False):
9297
as_module (bool):
9398
Whether we're running script_file as a module
9499
"""
95-
Profiler = AstTreeModuleProfiler if as_module else AstTreeProfiler
100+
@contextlib.contextmanager
101+
def restore_dict(d, target=None):
102+
copy = d.copy()
103+
yield target
104+
d.clear()
105+
d.update(copy)
106+
107+
if as_module:
108+
Profiler = AstTreeModuleProfiler
109+
module_name = modpath_to_modname(script_file)
110+
if not module_name:
111+
raise ModuleNotFoundError(f'script_file = {script_file!r}: '
112+
'cannot find corresponding module')
113+
114+
module_obj = types.ModuleType(module_name)
115+
namespace = vars(module_obj)
116+
namespace.update(ns)
117+
118+
# Set the `__spec__` correctly
119+
module_obj.__spec__ = importlib.util.find_spec(module_name)
120+
121+
# Set the module object to `sys.modules` via a callback, and
122+
# then restore it via the context manager
123+
callback = functools.partial(
124+
operator.setitem, sys.modules, '__main__', module_obj)
125+
ctx = restore_dict(sys.modules, callback)
126+
else:
127+
Profiler = AstTreeProfiler
128+
namespace = ns
129+
ctx = contextlib.nullcontext(lambda: None)
130+
96131
profiler = Profiler(script_file, prof_mod, profile_imports)
97132
tree_profiled = profiler.profile()
98133

99134
_extend_line_profiler_for_profiling_imports(ns[PROFILER_LOCALS_NAME])
100135
code_obj = compile(tree_profiled, script_file, 'exec')
101-
exec(code_obj, ns, ns)
136+
with ctx as callback:
137+
callback()
138+
exec(code_obj, namespace, namespace)

tests/test_kernprof.py

Lines changed: 72 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import os
2+
import re
3+
import shlex
14
import sys
25
import tempfile
36
import unittest
@@ -24,14 +27,14 @@ def g(x):
2427
'use_kernprof_exec, args, expected_output, expect_error',
2528
[([False, ['-m'], '', True]),
2629
# `python -m kernprof`
27-
(False, ['-m', 'mymod'], "['mymod']", False),
30+
(False, ['-m', 'mymod'], "[__MYMOD__]", False),
2831
# `kernprof`
29-
(True, ['-m', 'mymod'], "['mymod']", False),
30-
(False, ['-m', 'mymod', '-p', 'bar'], "['mymod', '-p', 'bar']", False),
32+
(True, ['-m', 'mymod'], "[__MYMOD__]", False),
33+
(False, ['-m', 'mymod', '-p', 'bar'], "[__MYMOD__, '-p', 'bar']", False),
3134
# `-p bar` consumed by `kernprof`, `-p baz` are not
3235
(False,
3336
['-p', 'bar', '-m', 'mymod', '-p', 'baz'],
34-
"['mymod', '-p', 'baz']",
37+
"[__MYMOD__, '-p', 'baz']",
3538
False),
3639
# Separator `--` broke off the remainder, so the requisite arg for
3740
# `-m` is not found and we error out
@@ -49,26 +52,75 @@ def test_kernprof_m_parsing(
4952
an argument and cuts off everything after it, passing that along
5053
to the module to be executed.
5154
"""
52-
temp_dpath = ub.Path(tempfile.mkdtemp())
53-
(temp_dpath / 'mymod.py').write_text(ub.codeblock(
54-
'''
55-
import sys
56-
57-
58-
if __name__ == '__main__':
59-
print(sys.argv)
60-
'''))
61-
if use_kernprof_exec:
62-
cmd = ['kernprof']
63-
else:
64-
cmd = [sys.executable, '-m', 'kernprof']
65-
proc = ub.cmd(cmd + args, cwd=temp_dpath, verbose=2)
55+
with tempfile.TemporaryDirectory() as tmpdir:
56+
temp_dpath = ub.Path(tmpdir)
57+
mod = (temp_dpath / 'mymod.py').resolve()
58+
mod.write_text(ub.codeblock(
59+
'''
60+
import sys
61+
62+
63+
if __name__ == '__main__':
64+
print(sys.argv)
65+
'''))
66+
if use_kernprof_exec:
67+
cmd = ['kernprof']
68+
else:
69+
cmd = [sys.executable, '-m', 'kernprof']
70+
proc = ub.cmd(cmd + args, cwd=temp_dpath, verbose=2)
6671
if expect_error:
6772
assert proc.returncode
6873
return
6974
else:
7075
proc.check_returncode()
71-
assert proc.stdout.startswith(expected_output)
76+
expected_output = re.escape(expected_output).replace(
77+
'__MYMOD__', "'.*{}'".format(re.escape(os.path.sep + mod.name)))
78+
assert re.match('^' + expected_output, proc.stdout)
79+
80+
81+
@pytest.mark.skipif(sys.version_info[:2] < (3, 11),
82+
reason='no `@enum.global_enum` in Python '
83+
f'{".".join(str(v) for v in sys.version_info[:3])}')
84+
@pytest.mark.parametrize(('flags', 'profiled_main'),
85+
[('-lv -p mymod', True), # w/autoprofile
86+
('-lv', True), # w/o autoprofile
87+
('-b', False)]) # w/o line profiling
88+
def test_kernprof_m_sys_modules(flags, profiled_main):
89+
"""
90+
Test that `kernprof -m` is amenable to modules relying on the global
91+
`sys` state (e.g. those using `@enum.global_enum`).
92+
"""
93+
with tempfile.TemporaryDirectory() as tmpdir:
94+
temp_dpath = ub.Path(tmpdir)
95+
(temp_dpath / 'mymod.py').write_text(ub.codeblock(
96+
'''
97+
import enum
98+
import os
99+
import sys
100+
101+
102+
@enum.global_enum
103+
class MyEnum(enum.Enum):
104+
FOO = 1
105+
BAR = 2
106+
107+
108+
@profile
109+
def main():
110+
x = FOO.value + BAR.value
111+
print(x)
112+
assert x == 3
113+
114+
115+
if __name__ == '__main__':
116+
main()
117+
'''))
118+
cmd = [sys.executable, '-m', 'kernprof',
119+
*shlex.split(flags), '-m', 'mymod']
120+
proc = ub.cmd(cmd, cwd=temp_dpath, verbose=2)
121+
proc.check_returncode()
122+
assert proc.stdout.startswith('3\n')
123+
assert ('Function: main' in proc.stdout) == profiled_main
72124

73125

74126
class TestKernprof(unittest.TestCase):
@@ -130,6 +182,7 @@ def test_gen_decorator(self):
130182
next(i)
131183
self.assertEqual(profile.enable_count, 0)
132184

185+
133186
if __name__ == '__main__':
134187
"""
135188
CommandLine:

0 commit comments

Comments
 (0)