]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-138122: Implement PEP 799 (#138142)
authorPablo Galindo Salgado <Pablogsal@gmail.com>
Wed, 27 Aug 2025 16:52:50 +0000 (17:52 +0100)
committerGitHub <noreply@github.com>
Wed, 27 Aug 2025 16:52:50 +0000 (17:52 +0100)
23 files changed:
Lib/cProfile.py
Lib/profile.py [moved from Lib/profile/profile.py with 89% similarity]
Lib/profile/__init__.py [deleted file]
Lib/profile/__main__.py [deleted file]
Lib/profiling/__init__.py [new file with mode: 0644]
Lib/profiling/sampling/__init__.py [new file with mode: 0644]
Lib/profiling/sampling/__main__.py [new file with mode: 0644]
Lib/profiling/sampling/_sync_coordinator.py [moved from Lib/profile/_sync_coordinator.py with 100% similarity]
Lib/profiling/sampling/collector.py [moved from Lib/profile/collector.py with 100% similarity]
Lib/profiling/sampling/pstats_collector.py [moved from Lib/profile/pstats_collector.py with 100% similarity]
Lib/profiling/sampling/sample.py [moved from Lib/profile/sample.py with 97% similarity]
Lib/profiling/sampling/stack_collector.py [moved from Lib/profile/stack_collector.py with 100% similarity]
Lib/profiling/tracing/__init__.py [new file with mode: 0644]
Lib/profiling/tracing/__main__.py [new file with mode: 0644]
Lib/profiling/tracing/_utils.py [new file with mode: 0644]
Lib/test/test_profile.py
Lib/test/test_profiling/__init__.py [new file with mode: 0644]
Lib/test/test_profiling/__main__.py [new file with mode: 0644]
Lib/test/test_profiling/test_sampling_profiler.py [moved from Lib/test/test_sample_profiler.py with 92% similarity]
Lib/test/test_profiling/test_tracing_profiler.py [moved from Lib/test/test_cprofile.py with 99% similarity]
Makefile.pre.in
Misc/NEWS.d/next/Library/2025-08-25-16-22-32.gh-issue-138122.eMNDZ1.rst [new file with mode: 0644]
Python/stdlib_module_names.h

index 770d26f79628fe14aa735cfe0258a84f912d5bea..4af82f2cb8c8485cbaf778b11e0d7faa65606434 100644 (file)
-"""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()
similarity index 89%
rename from Lib/profile/profile.py
rename to Lib/profile.py
index 1d9e2fd41f85b45daa9fa51705d0b9864884db12..20c500d28bc5b9c11ed2c8449f4ebd19cdcbbbd8 100644 (file)
@@ -28,9 +28,18 @@ import io
 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 @@ class Profile:
         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 (file)
index 21c8864..0000000
+++ /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 (file)
index d900e54..0000000
+++ /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 (file)
index 0000000..c94e1e1
--- /dev/null
@@ -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 (file)
index 0000000..1745067
--- /dev/null
@@ -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 (file)
index 0000000..3f04ba8
--- /dev/null
@@ -0,0 +1,6 @@
+"""Run the sampling profiler from the command line."""
+
+from .sample import main
+
+if __name__ == '__main__':
+    main()
similarity index 97%
rename from Lib/profile/sample.py
rename to Lib/profiling/sampling/sample.py
index 5f3c86d4839a6631159151dcb690df2496ff385f..929a92e1dda3b2ddf0a7a8c9e5335a23a4a04dab 100644 (file)
@@ -25,40 +25,40 @@ Supports the following target modes:
 
 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/profiling/tracing/__init__.py b/Lib/profiling/tracing/__init__.py
new file mode 100644 (file)
index 0000000..2dc7ea9
--- /dev/null
@@ -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 (file)
index 0000000..95041d1
--- /dev/null
@@ -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 (file)
index 0000000..8c4b658
--- /dev/null
@@ -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)
index 0f16b92334999ce8467a0902cdbbfbc8013f3e96..c4b2d7ca05c0a6c1be07a39c56a92b9d49f21059 100644 (file)
@@ -4,12 +4,16 @@ import sys
 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 (file)
index 0000000..4b16ecc
--- /dev/null
@@ -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 (file)
index 0000000..40a23a2
--- /dev/null
@@ -0,0 +1,4 @@
+from . import load_tests
+import unittest
+
+unittest.main()
similarity index 92%
rename from Lib/test/test_sample_profiler.py
rename to Lib/test/test_profiling/test_sampling_profiler.py
index 6446247cb5cea1d1f8c9a192a580e6238eb6b35a..02ed2abc2b912d15f193c398024734674de85f2c 100644 (file)
@@ -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 tempfile
 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 @@ except ImportError:
         "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 @@ class TestSampleProfiler(unittest.TestCase):
 
     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 @@ class TestSampleProfiler(unittest.TestCase):
 
     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 @@ class TestSampleProfiler(unittest.TestCase):
 
     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 @@ class TestPrintSampledStats(unittest.TestCase):
 
     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 @@ class TestPrintSampledStats(unittest.TestCase):
 
     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 @@ class TestPrintSampledStats(unittest.TestCase):
 
     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 @@ class TestPrintSampledStats(unittest.TestCase):
 
     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 @@ class TestPrintSampledStats(unittest.TestCase):
 
     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 @@ class TestPrintSampledStats(unittest.TestCase):
 
     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 @@ class TestPrintSampledStats(unittest.TestCase):
 
     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 @@ class TestPrintSampledStats(unittest.TestCase):
 
     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 @@ class TestPrintSampledStats(unittest.TestCase):
 
     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 @@ class TestPrintSampledStats(unittest.TestCase):
 
     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 @@ class TestPrintSampledStats(unittest.TestCase):
 
     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 @@ class TestPrintSampledStats(unittest.TestCase):
 
     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 @@ class TestPrintSampledStats(unittest.TestCase):
 
     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 @@ if __name__ == "__main__":
             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 @@ if __name__ == "__main__":
                 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 @@ if __name__ == "__main__":
                 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 @@ if __name__ == "__main__":
             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 @@ if __name__ == "__main__":
         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 @@ if __name__ == "__main__":
             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 @@ if __name__ == "__main__":
         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 @@ if __name__ == "__main__":
             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 @@ if __name__ == "__main__":
 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 @@ class TestSampleProfilerErrorHandling(unittest.TestCase):
                 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 @@ class TestSampleProfilerErrorHandling(unittest.TestCase):
 
     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 @@ class TestSampleProfilerErrorHandling(unittest.TestCase):
     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 @@ class TestSampleProfilerCLI(unittest.TestCase):
         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 @@ class TestSampleProfilerCLI(unittest.TestCase):
 
     @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 @@ class TestSampleProfilerCLI(unittest.TestCase):
 
     @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 @@ class TestSampleProfilerCLI(unittest.TestCase):
 
     @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 @@ class TestSampleProfilerCLI(unittest.TestCase):
             # 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 @@ class TestSampleProfilerCLI(unittest.TestCase):
     @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 @@ class TestSampleProfilerCLI(unittest.TestCase):
     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 @@ class TestSampleProfilerCLI(unittest.TestCase):
             )
 
     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 @@ class TestSampleProfilerCLI(unittest.TestCase):
 
     @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 @@ class TestSampleProfilerCLI(unittest.TestCase):
             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 @@ class TestSampleProfilerCLI(unittest.TestCase):
         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 @@ class TestSampleProfilerCLI(unittest.TestCase):
             ),
             (
                 [
-                    "profile.sample",
+                    "profiling.sampling.sample",
                     "--collapsed",
                     "--sort-sample-pct",
                     "-p",
@@ -2092,7 +2092,7 @@ class TestSampleProfilerCLI(unittest.TestCase):
             ),
             (
                 [
-                    "profile.sample",
+                    "profiling.sampling.sample",
                     "--collapsed",
                     "--sort-cumul-pct",
                     "-p",
@@ -2101,18 +2101,18 @@ class TestSampleProfilerCLI(unittest.TestCase):
                 "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 @@ class TestSampleProfilerCLI(unittest.TestCase):
                 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 @@ class TestSampleProfilerCLI(unittest.TestCase):
 
     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 @@ class TestSampleProfilerCLI(unittest.TestCase):
         """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 @@ class TestSampleProfilerCLI(unittest.TestCase):
         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 @@ class TestSampleProfilerCLI(unittest.TestCase):
     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 @@ class TestSampleProfilerCLI(unittest.TestCase):
         ]
 
         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]
similarity index 99%
rename from Lib/test/test_cprofile.py
rename to Lib/test/test_profiling/test_tracing_profiler.py
index 57e818b1c68b3881a9535ca1a19effbd2a0d8d6b..d09ca441d4ae46e87c8dc9d7f749e2a9e81a7d36 100644 (file)
@@ -4,7 +4,7 @@ import sys
 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
index bcf19654adfb3552b0f5696cc82b0828d09fc742..1328716da53de7aa5482b3ec5d4a4ab378bcf5ee 100644 (file)
@@ -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 (file)
index 0000000..41ca22c
--- /dev/null
@@ -0,0 +1,2 @@
+Implement :pep:`799` -- A dedicated profiling package for organizing Python
+profiling tools. Patch by Pablo Galindo.
index 63e4599c31efc3781905dd04835d0b93a1c2f464..47c88839bc87b2ff8b00208dcd3705067a6f8067 100644 (file)
@@ -215,6 +215,7 @@ static const char* _Py_stdlib_module_names[] = {
 "posixpath",
 "pprint",
 "profile",
+"profiling",
 "pstats",
 "pty",
 "pwd",