https://github.com/python/cpython/commit/56eb6b64a0323e3ad391324a69bdbec6448729b1
commit: 56eb6b64a0323e3ad391324a69bdbec6448729b1
branch: main
author: Pablo Galindo Salgado <[email protected]>
committer: pablogsal <[email protected]>
date: 2025-08-27T17:52:50+01:00
summary:
gh-138122: Implement PEP 799 (#138142)
files:
A Lib/profile.py
A Lib/profiling/__init__.py
A Lib/profiling/sampling/__init__.py
A Lib/profiling/sampling/__main__.py
A Lib/profiling/sampling/_sync_coordinator.py
A Lib/profiling/sampling/collector.py
A Lib/profiling/sampling/pstats_collector.py
A Lib/profiling/sampling/sample.py
A Lib/profiling/sampling/stack_collector.py
A Lib/profiling/tracing/__init__.py
A Lib/profiling/tracing/__main__.py
A Lib/profiling/tracing/_utils.py
A Lib/test/test_profiling/__init__.py
A Lib/test/test_profiling/__main__.py
A Lib/test/test_profiling/test_sampling_profiler.py
A Lib/test/test_profiling/test_tracing_profiler.py
A Misc/NEWS.d/next/Library/2025-08-25-16-22-32.gh-issue-138122.eMNDZ1.rst
D Lib/profile/__init__.py
D Lib/profile/__main__.py
D Lib/profile/_sync_coordinator.py
D Lib/profile/collector.py
D Lib/profile/profile.py
D Lib/profile/pstats_collector.py
D Lib/profile/sample.py
D Lib/profile/stack_collector.py
D Lib/test/test_cprofile.py
D Lib/test/test_sample_profiler.py
M Lib/cProfile.py
M Lib/test/test_profile.py
M Makefile.pre.in
M Python/stdlib_module_names.h
diff --git a/Lib/cProfile.py b/Lib/cProfile.py
index 770d26f79628fe..4af82f2cb8c848 100644
--- a/Lib/cProfile.py
+++ b/Lib/cProfile.py
@@ -1,205 +1,14 @@
-"""Python interface for the 'lsprof' profiler.
- Compatible with the 'profile' module.
-"""
-
-__all__ = ["run", "runctx", "Profile"]
-
-import _lsprof
-import importlib.machinery
-import importlib.util
-import io
-import profile as _pyprofile
-
-# ____________________________________________________________
-# Simple interface
-
-def run(statement, filename=None, sort=-1):
- return _pyprofile._Utils(Profile).run(statement, filename, sort)
-
-def runctx(statement, globals, locals, filename=None, sort=-1):
- return _pyprofile._Utils(Profile).runctx(statement, globals, locals,
- filename, sort)
-
-run.__doc__ = _pyprofile.run.__doc__
-runctx.__doc__ = _pyprofile.runctx.__doc__
-
-# ____________________________________________________________
-
-class Profile(_lsprof.Profiler):
- """Profile(timer=None, timeunit=None, subcalls=True, builtins=True)
-
- Builds a profiler object using the specified timer function.
- The default timer is a fast built-in one based on real time.
- For custom timer functions returning integers, timeunit can
- be a float specifying a scale (i.e. how long each integer unit
- is, in seconds).
- """
-
- # Most of the functionality is in the base class.
- # This subclass only adds convenient and backward-compatible methods.
-
- def print_stats(self, sort=-1):
- import pstats
- if not isinstance(sort, tuple):
- sort = (sort,)
- pstats.Stats(self).strip_dirs().sort_stats(*sort).print_stats()
-
- def dump_stats(self, file):
- import marshal
- with open(file, 'wb') as f:
- self.create_stats()
- marshal.dump(self.stats, f)
-
- def create_stats(self):
- self.disable()
- self.snapshot_stats()
+"""Compatibility wrapper for cProfile module.
- def snapshot_stats(self):
- entries = self.getstats()
- self.stats = {}
- callersdicts = {}
- # call information
- for entry in entries:
- func = label(entry.code)
- nc = entry.callcount # ncalls column of pstats (before '/')
- cc = nc - entry.reccallcount # ncalls column of pstats (after '/')
- tt = entry.inlinetime # tottime column of pstats
- ct = entry.totaltime # cumtime column of pstats
- callers = {}
- callersdicts[id(entry.code)] = callers
- self.stats[func] = cc, nc, tt, ct, callers
- # subcall information
- for entry in entries:
- if entry.calls:
- func = label(entry.code)
- for subentry in entry.calls:
- try:
- callers = callersdicts[id(subentry.code)]
- except KeyError:
- continue
- nc = subentry.callcount
- cc = nc - subentry.reccallcount
- tt = subentry.inlinetime
- ct = subentry.totaltime
- if func in callers:
- prev = callers[func]
- nc += prev[0]
- cc += prev[1]
- tt += prev[2]
- ct += prev[3]
- callers[func] = nc, cc, tt, ct
-
- # The following two methods can be called by clients to use
- # a profiler to profile a statement, given as a string.
-
- def run(self, cmd):
- import __main__
- dict = __main__.__dict__
- return self.runctx(cmd, dict, dict)
-
- def runctx(self, cmd, globals, locals):
- self.enable()
- try:
- exec(cmd, globals, locals)
- finally:
- self.disable()
- return self
-
- # This method is more useful to profile a single function call.
- def runcall(self, func, /, *args, **kw):
- self.enable()
- try:
- return func(*args, **kw)
- finally:
- self.disable()
-
- def __enter__(self):
- self.enable()
- return self
-
- def __exit__(self, *exc_info):
- self.disable()
-
-# ____________________________________________________________
+This module maintains backward compatibility by importing from the new
+profiling.tracing module.
+"""
-def label(code):
- if isinstance(code, str):
- return ('~', 0, code) # built-in functions ('~' sorts at the end)
- else:
- return (code.co_filename, code.co_firstlineno, code.co_name)
+from profiling.tracing import run, runctx, Profile
-# ____________________________________________________________
+__all__ = ["run", "runctx", "Profile"]
-def main():
- import os
+if __name__ == "__main__":
import sys
- import runpy
- import pstats
- from optparse import OptionParser
- usage = "cProfile.py [-o output_file_path] [-s sort] [-m module |
scriptfile] [arg] ..."
- parser = OptionParser(usage=usage)
- parser.allow_interspersed_args = False
- parser.add_option('-o', '--outfile', dest="outfile",
- help="Save stats to <outfile>", default=None)
- parser.add_option('-s', '--sort', dest="sort",
- help="Sort order when printing to stdout, based on pstats.Stats class",
- default=2,
- choices=sorted(pstats.Stats.sort_arg_dict_default))
- parser.add_option('-m', dest="module", action="store_true",
- help="Profile a library module", default=False)
-
- if not sys.argv[1:]:
- parser.print_usage()
- sys.exit(2)
-
- (options, args) = parser.parse_args()
- sys.argv[:] = args
-
- # The script that we're profiling may chdir, so capture the absolute path
- # to the output file at startup.
- if options.outfile is not None:
- options.outfile = os.path.abspath(options.outfile)
-
- if len(args) > 0:
- if options.module:
- code = "run_module(modname, run_name='__main__')"
- globs = {
- 'run_module': runpy.run_module,
- 'modname': args[0]
- }
- else:
- progname = args[0]
- sys.path.insert(0, os.path.dirname(progname))
- with io.open_code(progname) as fp:
- code = compile(fp.read(), progname, 'exec')
- spec = importlib.machinery.ModuleSpec(name='__main__', loader=None,
- origin=progname)
- module = importlib.util.module_from_spec(spec)
- # Set __main__ so that importing __main__ in the profiled code will
- # return the same namespace that the code is executing under.
- sys.modules['__main__'] = module
- # Ensure that we're using the same __dict__ instance as the module
- # for the global variables so that updates to globals are reflected
- # in the module's namespace.
- globs = module.__dict__
- globs.update({
- '__spec__': spec,
- '__file__': spec.origin,
- '__name__': spec.name,
- '__package__': None,
- '__cached__': None,
- })
-
- try:
- runctx(code, globs, None, options.outfile, options.sort)
- except BrokenPipeError as exc:
- # Prevent "Exception ignored" during interpreter shutdown.
- sys.stdout = None
- sys.exit(exc.errno)
- else:
- parser.print_usage()
- return parser
-
-# When invoked as main program, invoke the profiler on a script
-if __name__ == '__main__':
+ from profiling.tracing.__main__ import main
main()
diff --git a/Lib/profile/profile.py b/Lib/profile.py
similarity index 89%
rename from Lib/profile/profile.py
rename to Lib/profile.py
index 1d9e2fd41f85b4..20c500d28bc5b9 100644
--- a/Lib/profile/profile.py
+++ b/Lib/profile.py
@@ -28,9 +28,18 @@
import sys
import time
import marshal
+import warnings
__all__ = ["run", "runctx", "Profile"]
+# Emit deprecation warning as per PEP 799
+warnings.warn(
+ "The profile module is deprecated and will be removed in Python 3.17. "
+ "Use profiling.tracing (or cProfile) for tracing profilers instead.",
+ DeprecationWarning,
+ stacklevel=2
+)
+
# Sample timer for use with
#i_count = 0
#def integer_timer():
@@ -550,3 +559,66 @@ def f(m, f1=f1):
return mean
#****************************************************************************
+
+def main():
+ import os
+ from optparse import OptionParser
+
+ usage = "profile.py [-o output_file_path] [-s sort] [-m module |
scriptfile] [arg] ..."
+ parser = OptionParser(usage=usage)
+ parser.allow_interspersed_args = False
+ parser.add_option('-o', '--outfile', dest="outfile",
+ help="Save stats to <outfile>", default=None)
+ parser.add_option('-m', dest="module", action="store_true",
+ help="Profile a library module.", default=False)
+ parser.add_option('-s', '--sort', dest="sort",
+ help="Sort order when printing to stdout, based on pstats.Stats class",
+ default=-1)
+
+ if not sys.argv[1:]:
+ parser.print_usage()
+ sys.exit(2)
+
+ (options, args) = parser.parse_args()
+ sys.argv[:] = args
+
+ # The script that we're profiling may chdir, so capture the absolute path
+ # to the output file at startup.
+ if options.outfile is not None:
+ options.outfile = os.path.abspath(options.outfile)
+
+ if len(args) > 0:
+ if options.module:
+ import runpy
+ code = "run_module(modname, run_name='__main__')"
+ globs = {
+ 'run_module': runpy.run_module,
+ 'modname': args[0]
+ }
+ else:
+ progname = args[0]
+ sys.path.insert(0, os.path.dirname(progname))
+ with io.open_code(progname) as fp:
+ code = compile(fp.read(), progname, 'exec')
+ spec = importlib.machinery.ModuleSpec(name='__main__', loader=None,
+ origin=progname)
+ globs = {
+ '__spec__': spec,
+ '__file__': spec.origin,
+ '__name__': spec.name,
+ '__package__': None,
+ '__cached__': None,
+ }
+ try:
+ runctx(code, globs, None, options.outfile, options.sort)
+ except BrokenPipeError as exc:
+ # Prevent "Exception ignored" during interpreter shutdown.
+ sys.stdout = None
+ sys.exit(exc.errno)
+ else:
+ parser.print_usage()
+ return parser
+
+# When invoked as main program, invoke the profiler on a script
+if __name__ == '__main__':
+ main()
diff --git a/Lib/profile/__init__.py b/Lib/profile/__init__.py
deleted file mode 100644
index 21c886448ac2af..00000000000000
--- a/Lib/profile/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from .profile import run
-from .profile import runctx
-from .profile import Profile
-from .profile import _Utils
-
-__all__ = ['run', 'runctx', 'Profile']
diff --git a/Lib/profile/__main__.py b/Lib/profile/__main__.py
deleted file mode 100644
index d900e547330e76..00000000000000
--- a/Lib/profile/__main__.py
+++ /dev/null
@@ -1,69 +0,0 @@
-import io
-import importlib.machinery
-import os
-import sys
-from optparse import OptionParser
-
-from .profile import runctx
-
-
-def main():
-
- usage = "profile.py [-o output_file_path] [-s sort] [-m module |
scriptfile] [arg] ..."
- parser = OptionParser(usage=usage)
- parser.allow_interspersed_args = False
- parser.add_option('-o', '--outfile', dest="outfile",
- help="Save stats to <outfile>", default=None)
- parser.add_option('-m', dest="module", action="store_true",
- help="Profile a library module.", default=False)
- parser.add_option('-s', '--sort', dest="sort",
- help="Sort order when printing to stdout, based on pstats.Stats class",
- default=-1)
-
- if not sys.argv[1:]:
- parser.print_usage()
- sys.exit(2)
-
- (options, args) = parser.parse_args()
- sys.argv[:] = args
-
- # The script that we're profiling may chdir, so capture the absolute path
- # to the output file at startup.
- if options.outfile is not None:
- options.outfile = os.path.abspath(options.outfile)
-
- if len(args) > 0:
- if options.module:
- import runpy
- code = "run_module(modname, run_name='__main__')"
- globs = {
- 'run_module': runpy.run_module,
- 'modname': args[0]
- }
- else:
- progname = args[0]
- sys.path.insert(0, os.path.dirname(progname))
- with io.open_code(progname) as fp:
- code = compile(fp.read(), progname, 'exec')
- spec = importlib.machinery.ModuleSpec(name='__main__', loader=None,
- origin=progname)
- globs = {
- '__spec__': spec,
- '__file__': spec.origin,
- '__name__': spec.name,
- '__package__': None,
- '__cached__': None,
- }
- try:
- runctx(code, globs, None, options.outfile, options.sort)
- except BrokenPipeError as exc:
- # Prevent "Exception ignored" during interpreter shutdown.
- sys.stdout = None
- sys.exit(exc.errno)
- else:
- parser.print_usage()
- return parser
-
-# When invoked as main program, invoke the profiler on a script
-if __name__ == '__main__':
- main()
diff --git a/Lib/profiling/__init__.py b/Lib/profiling/__init__.py
new file mode 100644
index 00000000000000..c94e1e16e35ba2
--- /dev/null
+++ b/Lib/profiling/__init__.py
@@ -0,0 +1,13 @@
+"""Python profiling tools.
+
+This package provides two types of profilers:
+
+- profiling.tracing: Deterministic tracing profiler that instruments every
+ function call and return. Higher overhead but provides exact call counts
+ and timing.
+
+- profiling.sampling: Statistical sampling profiler that periodically samples
+ the call stack. Low overhead and suitable for production use.
+"""
+
+__all__ = ("tracing", "sampling")
diff --git a/Lib/profiling/sampling/__init__.py
b/Lib/profiling/sampling/__init__.py
new file mode 100644
index 00000000000000..1745067bbb7003
--- /dev/null
+++ b/Lib/profiling/sampling/__init__.py
@@ -0,0 +1,11 @@
+"""Statistical sampling profiler for Python.
+
+This module provides low-overhead profiling by periodically sampling the
+call stack rather than tracing every function call.
+"""
+
+from .collector import Collector
+from .pstats_collector import PstatsCollector
+from .stack_collector import CollapsedStackCollector
+
+__all__ = ("Collector", "PstatsCollector", "CollapsedStackCollector")
diff --git a/Lib/profiling/sampling/__main__.py
b/Lib/profiling/sampling/__main__.py
new file mode 100644
index 00000000000000..3f04ba850425a6
--- /dev/null
+++ b/Lib/profiling/sampling/__main__.py
@@ -0,0 +1,6 @@
+"""Run the sampling profiler from the command line."""
+
+from .sample import main
+
+if __name__ == '__main__':
+ main()
diff --git a/Lib/profile/_sync_coordinator.py
b/Lib/profiling/sampling/_sync_coordinator.py
similarity index 100%
rename from Lib/profile/_sync_coordinator.py
rename to Lib/profiling/sampling/_sync_coordinator.py
diff --git a/Lib/profile/collector.py b/Lib/profiling/sampling/collector.py
similarity index 100%
rename from Lib/profile/collector.py
rename to Lib/profiling/sampling/collector.py
diff --git a/Lib/profile/pstats_collector.py
b/Lib/profiling/sampling/pstats_collector.py
similarity index 100%
rename from Lib/profile/pstats_collector.py
rename to Lib/profiling/sampling/pstats_collector.py
diff --git a/Lib/profile/sample.py b/Lib/profiling/sampling/sample.py
similarity index 97%
rename from Lib/profile/sample.py
rename to Lib/profiling/sampling/sample.py
index 5f3c86d4839a66..929a92e1dda3b2 100644
--- a/Lib/profile/sample.py
+++ b/Lib/profiling/sampling/sample.py
@@ -25,40 +25,40 @@
Examples:
# Profile process 1234 for 10 seconds with default settings
- python -m profile.sample -p 1234
+ python -m profiling.sampling -p 1234
# Profile a script by running it in a subprocess
- python -m profile.sample myscript.py arg1 arg2
+ python -m profiling.sampling myscript.py arg1 arg2
# Profile a module by running it as python -m module in a subprocess
- python -m profile.sample -m mymodule arg1 arg2
+ python -m profiling.sampling -m mymodule arg1 arg2
# Profile with custom interval and duration, save to file
- python -m profile.sample -i 50 -d 30 -o profile.stats -p 1234
+ python -m profiling.sampling -i 50 -d 30 -o profile.stats -p 1234
# Generate collapsed stacks for flamegraph
- python -m profile.sample --collapsed -p 1234
+ python -m profiling.sampling --collapsed -p 1234
# Profile all threads, sort by total time
- python -m profile.sample -a --sort-tottime -p 1234
+ python -m profiling.sampling -a --sort-tottime -p 1234
# Profile for 1 minute with 1ms sampling interval
- python -m profile.sample -i 1000 -d 60 -p 1234
+ python -m profiling.sampling -i 1000 -d 60 -p 1234
# Show only top 20 functions sorted by direct samples
- python -m profile.sample --sort-nsamples -l 20 -p 1234
+ python -m profiling.sampling --sort-nsamples -l 20 -p 1234
# Profile all threads and save collapsed stacks
- python -m profile.sample -a --collapsed -o stacks.txt -p 1234
+ python -m profiling.sampling -a --collapsed -o stacks.txt -p 1234
# Profile with real-time sampling statistics
- python -m profile.sample --realtime-stats -p 1234
+ python -m profiling.sampling --realtime-stats -p 1234
# Sort by sample percentage to find most sampled functions
- python -m profile.sample --sort-sample-pct -p 1234
+ python -m profiling.sampling --sort-sample-pct -p 1234
# Sort by cumulative samples to find functions most on call stack
- python -m profile.sample --sort-nsamples-cumul -p 1234"""
+ python -m profiling.sampling --sort-nsamples-cumul -p 1234"""
# Constants for socket synchronization
@@ -84,7 +84,7 @@ def _run_with_sync(original_cmd):
# Build command using the sync coordinator
target_args = original_cmd[1:] # Remove python executable
- cmd = (sys.executable, "-m", "profile._sync_coordinator",
str(sync_port), cwd) + tuple(target_args)
+ cmd = (sys.executable, "-m", "profiling.sampling._sync_coordinator",
str(sync_port), cwd) + tuple(target_args)
# Start the process with coordinator
process = subprocess.Popen(cmd)
diff --git a/Lib/profile/stack_collector.py
b/Lib/profiling/sampling/stack_collector.py
similarity index 100%
rename from Lib/profile/stack_collector.py
rename to Lib/profiling/sampling/stack_collector.py
diff --git a/Lib/profiling/tracing/__init__.py
b/Lib/profiling/tracing/__init__.py
new file mode 100644
index 00000000000000..2dc7ea92c8ca4d
--- /dev/null
+++ b/Lib/profiling/tracing/__init__.py
@@ -0,0 +1,219 @@
+"""Tracing profiler for Python.
+
+This module provides deterministic profiling of Python programs by tracing
+every function call and return.
+"""
+
+__all__ = ("run", "runctx", "Profile")
+
+import _lsprof
+import importlib.machinery
+import importlib.util
+import io
+from profiling.tracing._utils import _Utils
+
+# ____________________________________________________________
+# Simple interface
+
+def run(statement, filename=None, sort=-1):
+ """Run statement under profiler optionally saving results in filename
+
+ This function takes a single argument that can be passed to the
+ "exec" statement, and an optional file name. In all cases this
+ routine attempts to "exec" its first argument and gather profiling
+ statistics from the execution. If no file name is present, then this
+ function automatically prints a simple profiling report, sorted by the
+ standard name string (file/line/function-name) that is presented in
+ each line.
+ """
+ return _Utils(Profile).run(statement, filename, sort)
+
+def runctx(statement, globals, locals, filename=None, sort=-1):
+ """Run statement under profiler, supplying your own globals and locals,
+ optionally saving results in filename.
+
+ statement and filename have the same semantics as profile.run
+ """
+ return _Utils(Profile).runctx(statement, globals, locals,
+ filename, sort)
+
+# ____________________________________________________________
+
+class Profile(_lsprof.Profiler):
+ """Profile(timer=None, timeunit=None, subcalls=True, builtins=True)
+
+ Builds a profiler object using the specified timer function.
+ The default timer is a fast built-in one based on real time.
+ For custom timer functions returning integers, timeunit can
+ be a float specifying a scale (i.e. how long each integer unit
+ is, in seconds).
+ """
+
+ # Most of the functionality is in the base class.
+ # This subclass only adds convenient and backward-compatible methods.
+
+ def print_stats(self, sort=-1):
+ import pstats
+ if not isinstance(sort, tuple):
+ sort = (sort,)
+ pstats.Stats(self).strip_dirs().sort_stats(*sort).print_stats()
+
+ def dump_stats(self, file):
+ import marshal
+ with open(file, 'wb') as f:
+ self.create_stats()
+ marshal.dump(self.stats, f)
+
+ def create_stats(self):
+ self.disable()
+ self.snapshot_stats()
+
+ def snapshot_stats(self):
+ entries = self.getstats()
+ self.stats = {}
+ callersdicts = {}
+ # call information
+ for entry in entries:
+ func = label(entry.code)
+ nc = entry.callcount # ncalls column of pstats (before '/')
+ cc = nc - entry.reccallcount # ncalls column of pstats (after '/')
+ tt = entry.inlinetime # tottime column of pstats
+ ct = entry.totaltime # cumtime column of pstats
+ callers = {}
+ callersdicts[id(entry.code)] = callers
+ self.stats[func] = cc, nc, tt, ct, callers
+ # subcall information
+ for entry in entries:
+ if entry.calls:
+ func = label(entry.code)
+ for subentry in entry.calls:
+ try:
+ callers = callersdicts[id(subentry.code)]
+ except KeyError:
+ continue
+ nc = subentry.callcount
+ cc = nc - subentry.reccallcount
+ tt = subentry.inlinetime
+ ct = subentry.totaltime
+ if func in callers:
+ prev = callers[func]
+ nc += prev[0]
+ cc += prev[1]
+ tt += prev[2]
+ ct += prev[3]
+ callers[func] = nc, cc, tt, ct
+
+ # The following two methods can be called by clients to use
+ # a profiler to profile a statement, given as a string.
+
+ def run(self, cmd):
+ import __main__
+ dict = __main__.__dict__
+ return self.runctx(cmd, dict, dict)
+
+ def runctx(self, cmd, globals, locals):
+ self.enable()
+ try:
+ exec(cmd, globals, locals)
+ finally:
+ self.disable()
+ return self
+
+ # This method is more useful to profile a single function call.
+ def runcall(self, func, /, *args, **kw):
+ self.enable()
+ try:
+ return func(*args, **kw)
+ finally:
+ self.disable()
+
+ def __enter__(self):
+ self.enable()
+ return self
+
+ def __exit__(self, *exc_info):
+ self.disable()
+
+# ____________________________________________________________
+
+def label(code):
+ if isinstance(code, str):
+ return ('~', 0, code) # built-in functions ('~' sorts at the end)
+ else:
+ return (code.co_filename, code.co_firstlineno, code.co_name)
+
+# ____________________________________________________________
+
+def main():
+ import os
+ import sys
+ import runpy
+ import pstats
+ from optparse import OptionParser
+ usage = "cProfile.py [-o output_file_path] [-s sort] [-m module |
scriptfile] [arg] ..."
+ parser = OptionParser(usage=usage)
+ parser.allow_interspersed_args = False
+ parser.add_option('-o', '--outfile', dest="outfile",
+ help="Save stats to <outfile>", default=None)
+ parser.add_option('-s', '--sort', dest="sort",
+ help="Sort order when printing to stdout, based on pstats.Stats class",
+ default=2,
+ choices=sorted(pstats.Stats.sort_arg_dict_default))
+ parser.add_option('-m', dest="module", action="store_true",
+ help="Profile a library module", default=False)
+
+ if not sys.argv[1:]:
+ parser.print_usage()
+ sys.exit(2)
+
+ (options, args) = parser.parse_args()
+ sys.argv[:] = args
+
+ # The script that we're profiling may chdir, so capture the absolute path
+ # to the output file at startup.
+ if options.outfile is not None:
+ options.outfile = os.path.abspath(options.outfile)
+
+ if len(args) > 0:
+ if options.module:
+ code = "run_module(modname, run_name='__main__')"
+ globs = {
+ 'run_module': runpy.run_module,
+ 'modname': args[0]
+ }
+ else:
+ progname = args[0]
+ sys.path.insert(0, os.path.dirname(progname))
+ with io.open_code(progname) as fp:
+ code = compile(fp.read(), progname, 'exec')
+ spec = importlib.machinery.ModuleSpec(name='__main__', loader=None,
+ origin=progname)
+ module = importlib.util.module_from_spec(spec)
+ # Set __main__ so that importing __main__ in the profiled code will
+ # return the same namespace that the code is executing under.
+ sys.modules['__main__'] = module
+ # Ensure that we're using the same __dict__ instance as the module
+ # for the global variables so that updates to globals are reflected
+ # in the module's namespace.
+ globs = module.__dict__
+ globs.update({
+ '__spec__': spec,
+ '__file__': spec.origin,
+ '__name__': spec.name,
+ '__package__': None,
+ '__cached__': None,
+ })
+
+ try:
+ runctx(code, globs, None, options.outfile, options.sort)
+ except BrokenPipeError as exc:
+ # Prevent "Exception ignored" during interpreter shutdown.
+ sys.stdout = None
+ sys.exit(exc.errno)
+ else:
+ parser.print_usage()
+ return parser
+
+# When invoked as main program, invoke the profiler on a script
+if __name__ == '__main__':
+ main()
diff --git a/Lib/profiling/tracing/__main__.py
b/Lib/profiling/tracing/__main__.py
new file mode 100644
index 00000000000000..95041d1368b165
--- /dev/null
+++ b/Lib/profiling/tracing/__main__.py
@@ -0,0 +1,6 @@
+"""Run the tracing profiler from the command line."""
+
+from profiling.tracing import main
+
+if __name__ == '__main__':
+ main()
diff --git a/Lib/profiling/tracing/_utils.py b/Lib/profiling/tracing/_utils.py
new file mode 100644
index 00000000000000..8c4b65889c707f
--- /dev/null
+++ b/Lib/profiling/tracing/_utils.py
@@ -0,0 +1,32 @@
+class _Utils:
+ """Support class for utility functions which are shared by
+ profile.py and cProfile.py modules.
+ Not supposed to be used directly.
+ """
+
+ def __init__(self, profiler):
+ self.profiler = profiler
+
+ def run(self, statement, filename, sort):
+ prof = self.profiler()
+ try:
+ prof.run(statement)
+ except SystemExit:
+ pass
+ finally:
+ self._show(prof, filename, sort)
+
+ def runctx(self, statement, globals, locals, filename, sort):
+ prof = self.profiler()
+ try:
+ prof.runctx(statement, globals, locals)
+ except SystemExit:
+ pass
+ finally:
+ self._show(prof, filename, sort)
+
+ def _show(self, prof, filename, sort):
+ if filename is not None:
+ prof.dump_stats(filename)
+ else:
+ prof.print_stats(sort)
diff --git a/Lib/test/test_profile.py b/Lib/test/test_profile.py
index 0f16b92334999c..c4b2d7ca05c0a6 100644
--- a/Lib/test/test_profile.py
+++ b/Lib/test/test_profile.py
@@ -4,12 +4,16 @@
import pstats
import unittest
import os
+import warnings
from difflib import unified_diff
from io import StringIO
from test.support.os_helper import TESTFN, unlink, temp_dir, change_cwd
from contextlib import contextmanager, redirect_stdout
-import profile
+# Suppress deprecation warning for profile module (PEP 799)
+with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ import profile
from test.profilee import testfunc, timer
from test.support.script_helper import assert_python_failure, assert_python_ok
diff --git a/Lib/test/test_profiling/__init__.py
b/Lib/test/test_profiling/__init__.py
new file mode 100644
index 00000000000000..4b16ecc31156a5
--- /dev/null
+++ b/Lib/test/test_profiling/__init__.py
@@ -0,0 +1,5 @@
+import os
+from test.support import load_package_tests
+
+def load_tests(*args):
+ return load_package_tests(os.path.dirname(__file__), *args)
diff --git a/Lib/test/test_profiling/__main__.py
b/Lib/test/test_profiling/__main__.py
new file mode 100644
index 00000000000000..40a23a297ec2b4
--- /dev/null
+++ b/Lib/test/test_profiling/__main__.py
@@ -0,0 +1,4 @@
+from . import load_tests
+import unittest
+
+unittest.main()
diff --git a/Lib/test/test_sample_profiler.py
b/Lib/test/test_profiling/test_sampling_profiler.py
similarity index 92%
rename from Lib/test/test_sample_profiler.py
rename to Lib/test/test_profiling/test_sampling_profiler.py
index 6446247cb5cea1..02ed2abc2b912d 100644
--- a/Lib/test/test_sample_profiler.py
+++ b/Lib/test/test_profiling/test_sampling_profiler.py
@@ -1,4 +1,4 @@
-"""Tests for the sampling profiler (profile.sample)."""
+"""Tests for the sampling profiler (profiling.sampling)."""
import contextlib
import io
@@ -12,8 +12,8 @@
import unittest
from unittest import mock
-from profile.pstats_collector import PstatsCollector
-from profile.stack_collector import (
+from profiling.sampling.pstats_collector import PstatsCollector
+from profiling.sampling.stack_collector import (
CollapsedStackCollector,
)
@@ -32,8 +32,8 @@
"Test only runs when _remote_debugging is available"
)
else:
- import profile.sample
- from profile.sample import SampleProfiler
+ import profiling.sampling
+ from profiling.sampling.sample import SampleProfiler
@@ -472,7 +472,7 @@ class TestSampleProfiler(unittest.TestCase):
def test_sample_profiler_initialization(self):
"""Test SampleProfiler initialization with various parameters."""
- from profile.sample import SampleProfiler
+ from profiling.sampling.sample import SampleProfiler
# Mock RemoteUnwinder to avoid permission issues
with mock.patch(
@@ -498,7 +498,7 @@ def test_sample_profiler_initialization(self):
def test_sample_profiler_sample_method_timing(self):
"""Test that the sample method respects duration and handles timing
correctly."""
- from profile.sample import SampleProfiler
+ from profiling.sampling.sample import SampleProfiler
# Mock the unwinder to avoid needing a real process
mock_unwinder = mock.MagicMock()
@@ -548,7 +548,7 @@ def test_sample_profiler_sample_method_timing(self):
def test_sample_profiler_error_handling(self):
"""Test that the sample method handles errors gracefully."""
- from profile.sample import SampleProfiler
+ from profiling.sampling.sample import SampleProfiler
# Mock unwinder that raises errors
mock_unwinder = mock.MagicMock()
@@ -612,7 +612,7 @@ def test_sample_profiler_error_handling(self):
def test_sample_profiler_missed_samples_warning(self):
"""Test that the profiler warns about missed samples when sampling is
too slow."""
- from profile.sample import SampleProfiler
+ from profiling.sampling.sample import SampleProfiler
mock_unwinder = mock.MagicMock()
mock_unwinder.get_stack_trace.return_value = [
@@ -698,7 +698,7 @@ def setUp(self):
def test_print_sampled_stats_basic(self):
"""Test basic print_sampled_stats functionality."""
- from profile.sample import print_sampled_stats
+ from profiling.sampling.sample import print_sampled_stats
# Capture output
with io.StringIO() as output:
@@ -720,7 +720,7 @@ def test_print_sampled_stats_basic(self):
def test_print_sampled_stats_sorting(self):
"""Test different sorting options."""
- from profile.sample import print_sampled_stats
+ from profiling.sampling.sample import print_sampled_stats
# Test sort by calls
with io.StringIO() as output:
@@ -753,7 +753,7 @@ def test_print_sampled_stats_sorting(self):
def test_print_sampled_stats_limit(self):
"""Test limiting output rows."""
- from profile.sample import print_sampled_stats
+ from profiling.sampling.sample import print_sampled_stats
with io.StringIO() as output:
with mock.patch("sys.stdout", output):
@@ -782,7 +782,7 @@ def test_print_sampled_stats_limit(self):
def test_print_sampled_stats_time_units(self):
"""Test proper time unit selection."""
- from profile.sample import print_sampled_stats
+ from profiling.sampling.sample import print_sampled_stats
with io.StringIO() as output:
with mock.patch("sys.stdout", output):
@@ -812,7 +812,7 @@ def test_print_sampled_stats_time_units(self):
def test_print_sampled_stats_summary(self):
"""Test summary section generation."""
- from profile.sample import print_sampled_stats
+ from profiling.sampling.sample import print_sampled_stats
with io.StringIO() as output:
with mock.patch("sys.stdout", output):
@@ -840,7 +840,7 @@ def test_print_sampled_stats_summary(self):
def test_print_sampled_stats_no_summary(self):
"""Test disabling summary output."""
- from profile.sample import print_sampled_stats
+ from profiling.sampling.sample import print_sampled_stats
with io.StringIO() as output:
with mock.patch("sys.stdout", output):
@@ -857,7 +857,7 @@ def test_print_sampled_stats_no_summary(self):
def test_print_sampled_stats_empty_stats(self):
"""Test with empty stats."""
- from profile.sample import print_sampled_stats
+ from profiling.sampling.sample import print_sampled_stats
empty_stats = mock.MagicMock()
empty_stats.stats = {}
@@ -873,7 +873,7 @@ def test_print_sampled_stats_empty_stats(self):
def test_print_sampled_stats_sample_percentage_sorting(self):
"""Test sample percentage sorting options."""
- from profile.sample import print_sampled_stats
+ from profiling.sampling.sample import print_sampled_stats
# Add a function with high sample percentage (more direct calls than
func3's 200)
self.mock_stats.stats[("expensive.py", 60, "expensive_func")] = (
@@ -900,7 +900,7 @@ def
test_print_sampled_stats_sample_percentage_sorting(self):
def test_print_sampled_stats_with_recursive_calls(self):
"""Test print_sampled_stats with recursive calls where nc != cc."""
- from profile.sample import print_sampled_stats
+ from profiling.sampling.sample import print_sampled_stats
# Create stats with recursive calls (nc != cc)
recursive_stats = mock.MagicMock()
@@ -936,7 +936,7 @@ def test_print_sampled_stats_with_recursive_calls(self):
def test_print_sampled_stats_with_zero_call_counts(self):
"""Test print_sampled_stats with zero call counts to trigger division
protection."""
- from profile.sample import print_sampled_stats
+ from profiling.sampling.sample import print_sampled_stats
# Create stats with zero call counts
zero_stats = mock.MagicMock()
@@ -964,7 +964,7 @@ def test_print_sampled_stats_with_zero_call_counts(self):
def test_print_sampled_stats_sort_by_name(self):
"""Test sort by function name option."""
- from profile.sample import print_sampled_stats
+ from profiling.sampling.sample import print_sampled_stats
with io.StringIO() as output:
with mock.patch("sys.stdout", output):
@@ -1022,7 +1022,7 @@ def test_print_sampled_stats_sort_by_name(self):
def test_print_sampled_stats_with_zero_time_functions(self):
"""Test summary sections with functions that have zero time."""
- from profile.sample import print_sampled_stats
+ from profiling.sampling.sample import print_sampled_stats
# Create stats with zero-time functions
zero_time_stats = mock.MagicMock()
@@ -1060,7 +1060,7 @@ def
test_print_sampled_stats_with_zero_time_functions(self):
def test_print_sampled_stats_with_malformed_qualified_names(self):
"""Test summary generation with function names that don't contain
colons."""
- from profile.sample import print_sampled_stats
+ from profiling.sampling.sample import print_sampled_stats
# Create stats with function names that would create malformed
qualified names
malformed_stats = mock.MagicMock()
@@ -1451,7 +1451,7 @@ def test_sampling_basic_functionality(self):
mock.patch("sys.stdout", captured_output),
):
try:
- profile.sample.sample(
+ profiling.sampling.sample.sample(
proc.pid,
duration_sec=2,
sample_interval_usec=1000, # 1ms
@@ -1483,7 +1483,7 @@ def test_sampling_with_pstats_export(self):
mock.patch("sys.stdout", captured_output),
):
try:
- profile.sample.sample(
+ profiling.sampling.sample.sample(
proc.pid,
duration_sec=1,
filename=pstats_out.name,
@@ -1528,7 +1528,7 @@ def test_sampling_with_collapsed_export(self):
mock.patch("sys.stdout", captured_output),
):
try:
- profile.sample.sample(
+ profiling.sampling.sample.sample(
proc.pid,
duration_sec=1,
filename=collapsed_file.name,
@@ -1576,7 +1576,7 @@ def test_sampling_all_threads(self):
mock.patch("sys.stdout", captured_output),
):
try:
- profile.sample.sample(
+ profiling.sampling.sample.sample(
proc.pid,
duration_sec=1,
all_threads=True,
@@ -1595,7 +1595,7 @@ def test_sample_target_script(self):
script_file.flush()
self.addCleanup(close_and_unlink, script_file)
- test_args = ["profile.sample", "-d", "1", script_file.name]
+ test_args = ["profiling.sampling.sample", "-d", "1", script_file.name]
with (
mock.patch("sys.argv", test_args),
@@ -1603,7 +1603,7 @@ def test_sample_target_script(self):
mock.patch("sys.stdout", captured_output),
):
try:
- profile.sample.main()
+ profiling.sampling.sample.main()
except PermissionError:
self.skipTest("Insufficient permissions for remote profiling")
@@ -1627,7 +1627,7 @@ def test_sample_target_module(self):
with open(module_path, "w") as f:
f.write(self.test_script)
- test_args = ["profile.sample", "-d", "1", "-m", "test_module"]
+ test_args = ["profiling.sampling.sample", "-d", "1", "-m",
"test_module"]
with (
mock.patch("sys.argv", test_args),
@@ -1637,7 +1637,7 @@ def test_sample_target_module(self):
contextlib.chdir(tempdir.name),
):
try:
- profile.sample.main()
+ profiling.sampling.sample.main()
except PermissionError:
self.skipTest("Insufficient permissions for remote profiling")
@@ -1660,7 +1660,7 @@ def test_sample_target_module(self):
class TestSampleProfilerErrorHandling(unittest.TestCase):
def test_invalid_pid(self):
with self.assertRaises((OSError, RuntimeError)):
- profile.sample.sample(-1, duration_sec=1)
+ profiling.sampling.sample.sample(-1, duration_sec=1)
def test_process_dies_during_sampling(self):
with test_subprocess("import time; time.sleep(0.5); exit()") as proc:
@@ -1669,7 +1669,7 @@ def test_process_dies_during_sampling(self):
mock.patch("sys.stdout", captured_output),
):
try:
- profile.sample.sample(
+ profiling.sampling.sample.sample(
proc.pid,
duration_sec=2, # Longer than process lifetime
sample_interval_usec=50000,
@@ -1685,7 +1685,7 @@ def test_process_dies_during_sampling(self):
def test_invalid_output_format(self):
with self.assertRaises(ValueError):
- profile.sample.sample(
+ profiling.sampling.sample.sample(
os.getpid(),
duration_sec=1,
output_format="invalid_format",
@@ -1694,13 +1694,13 @@ def test_invalid_output_format(self):
def test_invalid_output_format_with_mocked_profiler(self):
"""Test invalid output format with proper mocking to avoid permission
issues."""
with mock.patch(
- "profile.sample.SampleProfiler"
+ "profiling.sampling.sample.SampleProfiler"
) as mock_profiler_class:
mock_profiler = mock.MagicMock()
mock_profiler_class.return_value = mock_profiler
with self.assertRaises(ValueError) as cm:
- profile.sample.sample(
+ profiling.sampling.sample.sample(
12345,
duration_sec=1,
output_format="unknown_format",
@@ -1787,23 +1787,23 @@ def _verify_coordinator_command(self, mock_popen,
expected_target_args):
coordinator_cmd = args[0]
self.assertEqual(coordinator_cmd[0], sys.executable)
self.assertEqual(coordinator_cmd[1], "-m")
- self.assertEqual(coordinator_cmd[2], "profile._sync_coordinator")
+ self.assertEqual(coordinator_cmd[2],
"profiling.sampling._sync_coordinator")
self.assertEqual(coordinator_cmd[3], "12345") # port
# cwd is coordinator_cmd[4]
self.assertEqual(coordinator_cmd[5:], expected_target_args)
@unittest.skipIf(is_emscripten, "socket.SO_REUSEADDR does not exist")
def test_cli_module_argument_parsing(self):
- test_args = ["profile.sample", "-m", "mymodule"]
+ test_args = ["profiling.sampling.sample", "-m", "mymodule"]
with (
mock.patch("sys.argv", test_args),
- mock.patch("profile.sample.sample") as mock_sample,
+ mock.patch("profiling.sampling.sample.sample") as mock_sample,
mock.patch("subprocess.Popen") as mock_popen,
mock.patch("socket.socket") as mock_socket,
):
self._setup_sync_mocks(mock_socket, mock_popen)
- profile.sample.main()
+ profiling.sampling.sample.main()
self._verify_coordinator_command(mock_popen, ("-m", "mymodule"))
mock_sample.assert_called_once_with(
@@ -1821,16 +1821,16 @@ def test_cli_module_argument_parsing(self):
@unittest.skipIf(is_emscripten, "socket.SO_REUSEADDR does not exist")
def test_cli_module_with_arguments(self):
- test_args = ["profile.sample", "-m", "mymodule", "arg1", "arg2",
"--flag"]
+ test_args = ["profiling.sampling.sample", "-m", "mymodule", "arg1",
"arg2", "--flag"]
with (
mock.patch("sys.argv", test_args),
- mock.patch("profile.sample.sample") as mock_sample,
+ mock.patch("profiling.sampling.sample.sample") as mock_sample,
mock.patch("subprocess.Popen") as mock_popen,
mock.patch("socket.socket") as mock_socket,
):
self._setup_sync_mocks(mock_socket, mock_popen)
- profile.sample.main()
+ profiling.sampling.sample.main()
self._verify_coordinator_command(mock_popen, ("-m", "mymodule",
"arg1", "arg2", "--flag"))
mock_sample.assert_called_once_with(
@@ -1848,16 +1848,16 @@ def test_cli_module_with_arguments(self):
@unittest.skipIf(is_emscripten, "socket.SO_REUSEADDR does not exist")
def test_cli_script_argument_parsing(self):
- test_args = ["profile.sample", "myscript.py"]
+ test_args = ["profiling.sampling.sample", "myscript.py"]
with (
mock.patch("sys.argv", test_args),
- mock.patch("profile.sample.sample") as mock_sample,
+ mock.patch("profiling.sampling.sample.sample") as mock_sample,
mock.patch("subprocess.Popen") as mock_popen,
mock.patch("socket.socket") as mock_socket,
):
self._setup_sync_mocks(mock_socket, mock_popen)
- profile.sample.main()
+ profiling.sampling.sample.main()
self._verify_coordinator_command(mock_popen, ("myscript.py",))
mock_sample.assert_called_once_with(
@@ -1875,11 +1875,11 @@ def test_cli_script_argument_parsing(self):
@unittest.skipIf(is_emscripten, "socket.SO_REUSEADDR does not exist")
def test_cli_script_with_arguments(self):
- test_args = ["profile.sample", "myscript.py", "arg1", "arg2", "--flag"]
+ test_args = ["profiling.sampling.sample", "myscript.py", "arg1",
"arg2", "--flag"]
with (
mock.patch("sys.argv", test_args),
- mock.patch("profile.sample.sample") as mock_sample,
+ mock.patch("profiling.sampling.sample.sample") as mock_sample,
mock.patch("subprocess.Popen") as mock_popen,
mock.patch("socket.socket") as mock_socket,
):
@@ -1888,55 +1888,55 @@ def test_cli_script_with_arguments(self):
# Override specific behavior for this test
mock_process.wait.side_effect =
[subprocess.TimeoutExpired(test_args, 0.1), None]
- profile.sample.main()
+ profiling.sampling.sample.main()
# Verify the coordinator command was called
args, kwargs = mock_popen.call_args
coordinator_cmd = args[0]
self.assertEqual(coordinator_cmd[0], sys.executable)
self.assertEqual(coordinator_cmd[1], "-m")
- self.assertEqual(coordinator_cmd[2], "profile._sync_coordinator")
+ self.assertEqual(coordinator_cmd[2],
"profiling.sampling._sync_coordinator")
self.assertEqual(coordinator_cmd[3], "12345") # port
# cwd is coordinator_cmd[4]
self.assertEqual(coordinator_cmd[5:], ("myscript.py", "arg1",
"arg2", "--flag"))
def test_cli_mutually_exclusive_pid_module(self):
- test_args = ["profile.sample", "-p", "12345", "-m", "mymodule"]
+ test_args = ["profiling.sampling.sample", "-p", "12345", "-m",
"mymodule"]
with (
mock.patch("sys.argv", test_args),
mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
self.assertRaises(SystemExit) as cm,
):
- profile.sample.main()
+ profiling.sampling.sample.main()
self.assertEqual(cm.exception.code, 2) # argparse error
error_msg = mock_stderr.getvalue()
self.assertIn("not allowed with argument", error_msg)
def test_cli_mutually_exclusive_pid_script(self):
- test_args = ["profile.sample", "-p", "12345", "myscript.py"]
+ test_args = ["profiling.sampling.sample", "-p", "12345", "myscript.py"]
with (
mock.patch("sys.argv", test_args),
mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
self.assertRaises(SystemExit) as cm,
):
- profile.sample.main()
+ profiling.sampling.sample.main()
self.assertEqual(cm.exception.code, 2) # argparse error
error_msg = mock_stderr.getvalue()
self.assertIn("only one target type can be specified", error_msg)
def test_cli_no_target_specified(self):
- test_args = ["profile.sample", "-d", "5"]
+ test_args = ["profiling.sampling.sample", "-d", "5"]
with (
mock.patch("sys.argv", test_args),
mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
self.assertRaises(SystemExit) as cm,
):
- profile.sample.main()
+ profiling.sampling.sample.main()
self.assertEqual(cm.exception.code, 2) # argparse error
error_msg = mock_stderr.getvalue()
@@ -1945,18 +1945,18 @@ def test_cli_no_target_specified(self):
@unittest.skipIf(is_emscripten, "socket.SO_REUSEADDR does not exist")
def test_cli_module_with_profiler_options(self):
test_args = [
- "profile.sample", "-i", "1000", "-d", "30", "-a",
+ "profiling.sampling.sample", "-i", "1000", "-d", "30", "-a",
"--sort-tottime", "-l", "20", "-m", "mymodule",
]
with (
mock.patch("sys.argv", test_args),
- mock.patch("profile.sample.sample") as mock_sample,
+ mock.patch("profiling.sampling.sample.sample") as mock_sample,
mock.patch("subprocess.Popen") as mock_popen,
mock.patch("socket.socket") as mock_socket,
):
self._setup_sync_mocks(mock_socket, mock_popen)
- profile.sample.main()
+ profiling.sampling.sample.main()
self._verify_coordinator_command(mock_popen, ("-m", "mymodule"))
mock_sample.assert_called_once_with(
@@ -1976,19 +1976,19 @@ def test_cli_module_with_profiler_options(self):
def test_cli_script_with_profiler_options(self):
"""Test script with various profiler options."""
test_args = [
- "profile.sample", "-i", "2000", "-d", "60",
+ "profiling.sampling.sample", "-i", "2000", "-d", "60",
"--collapsed", "-o", "output.txt",
"myscript.py", "scriptarg",
]
with (
mock.patch("sys.argv", test_args),
- mock.patch("profile.sample.sample") as mock_sample,
+ mock.patch("profiling.sampling.sample.sample") as mock_sample,
mock.patch("subprocess.Popen") as mock_popen,
mock.patch("socket.socket") as mock_socket,
):
self._setup_sync_mocks(mock_socket, mock_popen)
- profile.sample.main()
+ profiling.sampling.sample.main()
self._verify_coordinator_command(mock_popen, ("myscript.py",
"scriptarg"))
# Verify profiler options were passed correctly
@@ -2006,14 +2006,14 @@ def test_cli_script_with_profiler_options(self):
)
def test_cli_empty_module_name(self):
- test_args = ["profile.sample", "-m"]
+ test_args = ["profiling.sampling.sample", "-m"]
with (
mock.patch("sys.argv", test_args),
mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
self.assertRaises(SystemExit) as cm,
):
- profile.sample.main()
+ profiling.sampling.sample.main()
self.assertEqual(cm.exception.code, 2) # argparse error
error_msg = mock_stderr.getvalue()
@@ -2021,29 +2021,29 @@ def test_cli_empty_module_name(self):
@unittest.skipIf(is_emscripten, "socket.SO_REUSEADDR does not exist")
def test_cli_long_module_option(self):
- test_args = ["profile.sample", "--module", "mymodule", "arg1"]
+ test_args = ["profiling.sampling.sample", "--module", "mymodule",
"arg1"]
with (
mock.patch("sys.argv", test_args),
- mock.patch("profile.sample.sample") as mock_sample,
+ mock.patch("profiling.sampling.sample.sample") as mock_sample,
mock.patch("subprocess.Popen") as mock_popen,
mock.patch("socket.socket") as mock_socket,
):
self._setup_sync_mocks(mock_socket, mock_popen)
- profile.sample.main()
+ profiling.sampling.sample.main()
self._verify_coordinator_command(mock_popen, ("-m", "mymodule",
"arg1"))
def test_cli_complex_script_arguments(self):
test_args = [
- "profile.sample", "script.py",
+ "profiling.sampling.sample", "script.py",
"--input", "file.txt", "-v", "--output=/tmp/out", "positional"
]
with (
mock.patch("sys.argv", test_args),
- mock.patch("profile.sample.sample") as mock_sample,
- mock.patch("profile.sample._run_with_sync") as mock_run_with_sync,
+ mock.patch("profiling.sampling.sample.sample") as mock_sample,
+ mock.patch("profiling.sampling.sample._run_with_sync") as
mock_run_with_sync,
):
mock_process = mock.MagicMock()
mock_process.pid = 12345
@@ -2051,7 +2051,7 @@ def test_cli_complex_script_arguments(self):
mock_process.poll.return_value = None
mock_run_with_sync.return_value = mock_process
- profile.sample.main()
+ profiling.sampling.sample.main()
mock_run_with_sync.assert_called_once_with((
sys.executable, "script.py",
@@ -2063,16 +2063,16 @@ def test_cli_collapsed_format_validation(self):
test_cases = [
# Test sort options are invalid with collapsed
(
- ["profile.sample", "--collapsed", "--sort-nsamples", "-p",
"12345"],
+ ["profiling.sampling.sample", "--collapsed",
"--sort-nsamples", "-p", "12345"],
"sort",
),
(
- ["profile.sample", "--collapsed", "--sort-tottime", "-p",
"12345"],
+ ["profiling.sampling.sample", "--collapsed", "--sort-tottime",
"-p", "12345"],
"sort",
),
(
[
- "profile.sample",
+ "profiling.sampling.sample",
"--collapsed",
"--sort-cumtime",
"-p",
@@ -2082,7 +2082,7 @@ def test_cli_collapsed_format_validation(self):
),
(
[
- "profile.sample",
+ "profiling.sampling.sample",
"--collapsed",
"--sort-sample-pct",
"-p",
@@ -2092,7 +2092,7 @@ def test_cli_collapsed_format_validation(self):
),
(
[
- "profile.sample",
+ "profiling.sampling.sample",
"--collapsed",
"--sort-cumul-pct",
"-p",
@@ -2101,18 +2101,18 @@ def test_cli_collapsed_format_validation(self):
"sort",
),
(
- ["profile.sample", "--collapsed", "--sort-name", "-p",
"12345"],
+ ["profiling.sampling.sample", "--collapsed", "--sort-name",
"-p", "12345"],
"sort",
),
# Test limit option is invalid with collapsed
- (["profile.sample", "--collapsed", "-l", "20", "-p", "12345"],
"limit"),
+ (["profiling.sampling.sample", "--collapsed", "-l", "20", "-p",
"12345"], "limit"),
(
- ["profile.sample", "--collapsed", "--limit", "20", "-p",
"12345"],
+ ["profiling.sampling.sample", "--collapsed", "--limit", "20",
"-p", "12345"],
"limit",
),
# Test no-summary option is invalid with collapsed
(
- ["profile.sample", "--collapsed", "--no-summary", "-p",
"12345"],
+ ["profiling.sampling.sample", "--collapsed", "--no-summary",
"-p", "12345"],
"summary",
),
]
@@ -2123,7 +2123,7 @@ def test_cli_collapsed_format_validation(self):
mock.patch("sys.stderr", io.StringIO()) as mock_stderr,
self.assertRaises(SystemExit) as cm,
):
- profile.sample.main()
+ profiling.sampling.sample.main()
self.assertEqual(cm.exception.code, 2) # argparse error code
error_msg = mock_stderr.getvalue()
@@ -2132,13 +2132,13 @@ def test_cli_collapsed_format_validation(self):
def test_cli_default_collapsed_filename(self):
"""Test that collapsed format gets a default filename when not
specified."""
- test_args = ["profile.sample", "--collapsed", "-p", "12345"]
+ test_args = ["profiling.sampling.sample", "--collapsed", "-p", "12345"]
with (
mock.patch("sys.argv", test_args),
- mock.patch("profile.sample.sample") as mock_sample,
+ mock.patch("profiling.sampling.sample.sample") as mock_sample,
):
- profile.sample.main()
+ profiling.sampling.sample.main()
# Check that filename was set to default collapsed format
mock_sample.assert_called_once()
@@ -2150,12 +2150,12 @@ def test_cli_custom_output_filenames(self):
"""Test custom output filenames for both formats."""
test_cases = [
(
- ["profile.sample", "--pstats", "-o", "custom.pstats", "-p",
"12345"],
+ ["profiling.sampling.sample", "--pstats", "-o",
"custom.pstats", "-p", "12345"],
"custom.pstats",
"pstats",
),
(
- ["profile.sample", "--collapsed", "-o", "custom.txt", "-p",
"12345"],
+ ["profiling.sampling.sample", "--collapsed", "-o",
"custom.txt", "-p", "12345"],
"custom.txt",
"collapsed",
),
@@ -2164,9 +2164,9 @@ def test_cli_custom_output_filenames(self):
for test_args, expected_filename, expected_format in test_cases:
with (
mock.patch("sys.argv", test_args),
- mock.patch("profile.sample.sample") as mock_sample,
+ mock.patch("profiling.sampling.sample.sample") as mock_sample,
):
- profile.sample.main()
+ profiling.sampling.sample.main()
mock_sample.assert_called_once()
call_args = mock_sample.call_args[1]
@@ -2176,32 +2176,32 @@ def test_cli_custom_output_filenames(self):
def test_cli_missing_required_arguments(self):
"""Test that CLI requires PID argument."""
with (
- mock.patch("sys.argv", ["profile.sample"]),
+ mock.patch("sys.argv", ["profiling.sampling.sample"]),
mock.patch("sys.stderr", io.StringIO()),
):
with self.assertRaises(SystemExit):
- profile.sample.main()
+ profiling.sampling.sample.main()
def test_cli_mutually_exclusive_format_options(self):
"""Test that pstats and collapsed options are mutually exclusive."""
with (
mock.patch(
"sys.argv",
- ["profile.sample", "--pstats", "--collapsed", "-p", "12345"],
+ ["profiling.sampling.sample", "--pstats", "--collapsed", "-p",
"12345"],
),
mock.patch("sys.stderr", io.StringIO()),
):
with self.assertRaises(SystemExit):
- profile.sample.main()
+ profiling.sampling.sample.main()
def test_argument_parsing_basic(self):
- test_args = ["profile.sample", "-p", "12345"]
+ test_args = ["profiling.sampling.sample", "-p", "12345"]
with (
mock.patch("sys.argv", test_args),
- mock.patch("profile.sample.sample") as mock_sample,
+ mock.patch("profiling.sampling.sample.sample") as mock_sample,
):
- profile.sample.main()
+ profiling.sampling.sample.main()
mock_sample.assert_called_once_with(
12345,
@@ -2227,13 +2227,13 @@ def test_sort_options(self):
]
for option, expected_sort_value in sort_options:
- test_args = ["profile.sample", option, "-p", "12345"]
+ test_args = ["profiling.sampling.sample", option, "-p", "12345"]
with (
mock.patch("sys.argv", test_args),
- mock.patch("profile.sample.sample") as mock_sample,
+ mock.patch("profiling.sampling.sample.sample") as mock_sample,
):
- profile.sample.main()
+ profiling.sampling.sample.main()
mock_sample.assert_called_once()
call_args = mock_sample.call_args[1]
diff --git a/Lib/test/test_cprofile.py
b/Lib/test/test_profiling/test_tracing_profiler.py
similarity index 99%
rename from Lib/test/test_cprofile.py
rename to Lib/test/test_profiling/test_tracing_profiler.py
index 57e818b1c68b38..d09ca441d4ae46 100644
--- a/Lib/test/test_cprofile.py
+++ b/Lib/test/test_profiling/test_tracing_profiler.py
@@ -4,7 +4,7 @@
import unittest
# rip off all interesting stuff from test_profile
-import cProfile
+import profiling.tracing as cProfile
import tempfile
import textwrap
from test.test_profile import ProfileTest, regenerate_expected_output
diff --git a/Makefile.pre.in b/Makefile.pre.in
index bcf19654adfb35..1328716da53de7 100644
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -2565,6 +2565,7 @@ LIBSUBDIRS= asyncio \
multiprocessing multiprocessing/dummy \
pathlib \
profile \
+ profiling profiling/sampling profiling/tracing \
pydoc_data \
re \
site-packages \
@@ -2677,6 +2678,7 @@ TESTSUBDIRS= idlelib/idle_test \
test/test_pathlib \
test/test_pathlib/support \
test/test_peg_generator \
+ test/test_profiling \
test/test_pydoc \
test/test_pyrepl \
test/test_string \
diff --git
a/Misc/NEWS.d/next/Library/2025-08-25-16-22-32.gh-issue-138122.eMNDZ1.rst
b/Misc/NEWS.d/next/Library/2025-08-25-16-22-32.gh-issue-138122.eMNDZ1.rst
new file mode 100644
index 00000000000000..41ca22c78287cd
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-08-25-16-22-32.gh-issue-138122.eMNDZ1.rst
@@ -0,0 +1,2 @@
+Implement :pep:`799` -- A dedicated profiling package for organizing Python
+profiling tools. Patch by Pablo Galindo.
diff --git a/Python/stdlib_module_names.h b/Python/stdlib_module_names.h
index 63e4599c31efc3..47c88839bc87b2 100644
--- a/Python/stdlib_module_names.h
+++ b/Python/stdlib_module_names.h
@@ -215,6 +215,7 @@ static const char* _Py_stdlib_module_names[] = {
"posixpath",
"pprint",
"profile",
+"profiling",
"pstats",
"pty",
"pwd",
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]