diff --git a/Lib/profiling/sampling/module_utils.py b/Lib/profiling/sampling/module_utils.py index dfde2b28ab29a41..f6a859110da49b5 100644 --- a/Lib/profiling/sampling/module_utils.py +++ b/Lib/profiling/sampling/module_utils.py @@ -65,14 +65,17 @@ def extract_module_name(filename, path_info): except (ValueError, OSError): return (str(filename), 'other') - # Check if it's in stdlib - if path_info['stdlib'] and file_path.is_relative_to(path_info['stdlib']): - return (_path_to_module(file_path.relative_to(path_info['stdlib'])), 'stdlib') - - # Check site-packages - for site_pkg in path_info['site_packages']: - if file_path.is_relative_to(site_pkg): - return (_path_to_module(file_path.relative_to(site_pkg)), 'site-packages') + # Check stdlib and site-packages, preferring the most specific directory + # because site-packages is usually located inside the stdlib directory + bases = [(path_info['stdlib'], 'stdlib')] + bases.extend((site_pkg, 'site-packages') + for site_pkg in path_info['site_packages']) + matches = [(base, module_type) for base, module_type in bases + if base and file_path.is_relative_to(base)] + if matches: + # max() returns the first of equal candidates, so stdlib wins ties + base, module_type = max(matches, key=lambda m: len(m[0].parts)) + return (_path_to_module(file_path.relative_to(base)), module_type) # Check other sys.path entries (project files) if not str(file_path).startswith(('<', '[')): # Skip special files diff --git a/Lib/test/test_profiling/test_heatmap.py b/Lib/test/test_profiling/test_heatmap.py index ee27fdd3fa3053c..e2ab3fdd8434253 100644 --- a/Lib/test/test_profiling/test_heatmap.py +++ b/Lib/test/test_profiling/test_heatmap.py @@ -86,6 +86,25 @@ def test_extract_module_name_with_stdlib_file(self): self.assertEqual(module_type, 'stdlib') self.assertIn('os', module_name) + def test_extract_module_name_with_site_packages_inside_stdlib(self): + """Test extract_module_name when site-packages is inside stdlib.""" + stdlib = Path('/usr/lib/python3.15') + site_packages = stdlib / 'site-packages' + # On Windows, site.getsitepackages() also includes sys.prefix + path_info = { + 'stdlib': stdlib, + 'site_packages': [Path('/usr'), site_packages], + 'sys_path': [], + } + + site_packages_file = str(site_packages / 'requests' / 'api.py') + self.assertEqual(extract_module_name(site_packages_file, path_info), + ('requests.api', 'site-packages')) + + stdlib_file = str(stdlib / 'json' / 'decoder.py') + self.assertEqual(extract_module_name(stdlib_file, path_info), + ('json.decoder', 'stdlib')) + def test_extract_module_name_with_project_file(self): """Test extract_module_name with a project file.""" path_info = get_python_path_info() diff --git a/Misc/NEWS.d/next/Library/2026-10-08-01-33-46.gh-issue-158986.iXoUCo.rst b/Misc/NEWS.d/next/Library/2026-10-08-01-33-46.gh-issue-158986.iXoUCo.rst new file mode 100644 index 000000000000000..f234a601a97103d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-08-01-33-46.gh-issue-158986.iXoUCo.rst @@ -0,0 +1,4 @@ +Fix :mod:`profiling.sampling` reporting modules in a ``site-packages`` +directory located inside the standard library directory as standard library +modules. The heatmap now lists them under "Site Packages" and flamegraph +labels no longer start with ``site-packages.``.