https://github.com/python/cpython/commit/70e67f579e20036d82c59cd604b5dc73dd3c6a8a
commit: 70e67f579e20036d82c59cd604b5dc73dd3c6a8a
branch: main
author: Petr Viktorin <[email protected]>
committer: encukou <[email protected]>
date: 2026-01-23T17:51:29+01:00
summary:
gh-141376: smelly.py: Print only smelly symbols, or all of them with --verbose
(GH-141394)
Instead of long and uninteresting output for all checked libraries, only print
found issues by default.
Add a new -v/--verbose option to list all symbols (useful for checking that the
script finds the symbols).
files:
M Tools/build/smelly.py
diff --git a/Tools/build/smelly.py b/Tools/build/smelly.py
index 424fa6ad4a1371..7197d70bc8bd0c 100755
--- a/Tools/build/smelly.py
+++ b/Tools/build/smelly.py
@@ -1,7 +1,14 @@
#!/usr/bin/env python
-# Script checking that all symbols exported by libpython start with Py or _Py
+"""Check exported symbols
-import os.path
+Check that all symbols exported by CPython (libpython, stdlib extension
+modules, and similar) start with Py or _Py, or are covered by an exception.
+"""
+
+import argparse
+import dataclasses
+import functools
+import pathlib
import subprocess
import sys
import sysconfig
@@ -23,150 +30,167 @@
IGNORED_EXTENSION = "_ctypes_test"
-def is_local_symbol_type(symtype):
- # Ignore local symbols.
-
- # If lowercase, the symbol is usually local; if uppercase, the symbol
- # is global (external). There are however a few lowercase symbols that
- # are shown for special global symbols ("u", "v" and "w").
- if symtype.islower() and symtype not in "uvw":
[email protected]
+class Library:
+ path: pathlib.Path
+ is_dynamic: bool
+
+ @functools.cached_property
+ def is_ignored(self):
+ name_without_extemnsions = self.path.name.partition('.')[0]
+ return name_without_extemnsions == IGNORED_EXTENSION
+
+
[email protected]
+class Symbol:
+ name: str
+ type: str
+ library: str
+
+ def __str__(self):
+ return f"{self.name!r} (type {self.type}) from {self.library.path}"
+
+ @functools.cached_property
+ def is_local(self):
+ # If lowercase, the symbol is usually local; if uppercase, the symbol
+ # is global (external). There are however a few lowercase symbols that
+ # are shown for special global symbols ("u", "v" and "w").
+ if self.type.islower() and self.type not in "uvw":
+ return True
+
+ return False
+
+ @functools.cached_property
+ def is_smelly(self):
+ if self.is_local:
+ return False
+ if self.name.startswith(ALLOWED_PREFIXES):
+ return False
+ if self.name in EXCEPTIONS:
+ return False
+ if not self.library.is_dynamic and self.name.startswith(
+ ALLOWED_STATIC_PREFIXES):
+ return False
+ if self.library.is_ignored:
+ return False
return True
- return False
+ @functools.cached_property
+ def _sort_key(self):
+ return self.name, self.library.path
+ def __lt__(self, other_symbol):
+ return self._sort_key < other_symbol._sort_key
-def get_exported_symbols(library, dynamic=False):
- print(f"Check that {library} only exports symbols starting with Py or _Py")
+def get_exported_symbols(library):
+ # Only look at dynamic symbols
args = ['nm', '--no-sort']
- if dynamic:
+ if library.is_dynamic:
args.append('--dynamic')
- args.append(library)
- print(f"+ {' '.join(args)}")
+ args.append(library.path)
proc = subprocess.run(args, stdout=subprocess.PIPE, encoding='utf-8')
if proc.returncode:
+ print("+", args)
sys.stdout.write(proc.stdout)
sys.exit(proc.returncode)
stdout = proc.stdout.rstrip()
if not stdout:
raise Exception("command output is empty")
- return stdout
-
-
-def get_smelly_symbols(stdout, dynamic=False):
- smelly_symbols = []
- python_symbols = []
- local_symbols = []
+ symbols = []
for line in stdout.splitlines():
- # Split line '0000000000001b80 D PyTextIOWrapper_Type'
if not line:
continue
+ # Split lines like '0000000000001b80 D PyTextIOWrapper_Type'
parts = line.split(maxsplit=2)
+ # Ignore lines like ' U PyDict_SetItemString'
+ # and headers like 'pystrtod.o:'
if len(parts) < 3:
continue
- symtype = parts[1].strip()
- symbol = parts[-1]
- result = f'{symbol} (type: {symtype})'
-
- if (symbol.startswith(ALLOWED_PREFIXES) or
- symbol in EXCEPTIONS or
- (not dynamic and symbol.startswith(ALLOWED_STATIC_PREFIXES))):
- python_symbols.append(result)
- continue
-
- if is_local_symbol_type(symtype):
- local_symbols.append(result)
- else:
- smelly_symbols.append(result)
-
- if local_symbols:
- print(f"Ignore {len(local_symbols)} local symbols")
- return smelly_symbols, python_symbols
-
-
-def check_library(library, dynamic=False):
- nm_output = get_exported_symbols(library, dynamic)
- smelly_symbols, python_symbols = get_smelly_symbols(nm_output, dynamic)
+ symbol = Symbol(name=parts[-1], type=parts[1], library=library)
+ if not symbol.is_local:
+ symbols.append(symbol)
- if not smelly_symbols:
- print(f"OK: no smelly symbol found ({len(python_symbols)} Python
symbols)")
- return 0
+ return symbols
- print()
- smelly_symbols.sort()
- for symbol in smelly_symbols:
- print(f"Smelly symbol: {symbol}")
-
- print()
- print(f"ERROR: Found {len(smelly_symbols)} smelly symbols!")
- return len(smelly_symbols)
-
-def check_extensions():
- print(__file__)
+def get_extension_libraries():
# This assumes pybuilddir.txt is in same directory as pyconfig.h.
# In the case of out-of-tree builds, we can't assume pybuilddir.txt is
# in the source folder.
- config_dir = os.path.dirname(sysconfig.get_config_h_filename())
- filename = os.path.join(config_dir, "pybuilddir.txt")
+ config_dir = pathlib.Path(sysconfig.get_config_h_filename()).parent
try:
- with open(filename, encoding="utf-8") as fp:
- pybuilddir = fp.readline()
- except FileNotFoundError:
- print(f"Cannot check extensions because {filename} does not exist")
- return True
-
- print(f"Check extension modules from {pybuilddir} directory")
- builddir = os.path.join(config_dir, pybuilddir)
- nsymbol = 0
- for name in os.listdir(builddir):
- if not name.endswith(".so"):
- continue
- if IGNORED_EXTENSION in name:
- print()
- print(f"Ignore extension: {name}")
+ config_dir = config_dir.relative_to(pathlib.Path.cwd(), walk_up=True)
+ except ValueError:
+ pass
+ filename = config_dir / "pybuilddir.txt"
+ pybuilddir = filename.read_text().strip()
+
+ builddir = config_dir / pybuilddir
+ result = []
+ for path in sorted(builddir.glob('**/*.so')):
+ if path.stem == IGNORED_EXTENSION:
continue
+ result.append(Library(path, is_dynamic=True))
- print()
- filename = os.path.join(builddir, name)
- nsymbol += check_library(filename, dynamic=True)
-
- return nsymbol
+ return result
def main():
- nsymbol = 0
+ parser = argparse.ArgumentParser(
+ description=__doc__.split('\n', 1)[-1])
+ parser.add_argument('-v', '--verbose', action='store_true',
+ help='be verbose (currently: print out all symbols)')
+ args = parser.parse_args()
+
+ libraries = []
# static library
- LIBRARY = sysconfig.get_config_var('LIBRARY')
- if not LIBRARY:
- raise Exception("failed to get LIBRARY variable from sysconfig")
- if os.path.exists(LIBRARY):
- nsymbol += check_library(LIBRARY)
+ try:
+ LIBRARY = pathlib.Path(sysconfig.get_config_var('LIBRARY'))
+ except TypeError as exc:
+ raise Exception("failed to get LIBRARY sysconfig variable") from exc
+ LIBRARY = pathlib.Path(LIBRARY)
+ if LIBRARY.exists():
+ libraries.append(Library(LIBRARY, is_dynamic=False))
# dynamic library
- LDLIBRARY = sysconfig.get_config_var('LDLIBRARY')
- if not LDLIBRARY:
- raise Exception("failed to get LDLIBRARY variable from sysconfig")
+ try:
+ LDLIBRARY = pathlib.Path(sysconfig.get_config_var('LDLIBRARY'))
+ except TypeError as exc:
+ raise Exception("failed to get LDLIBRARY sysconfig variable") from exc
if LDLIBRARY != LIBRARY:
- print()
- nsymbol += check_library(LDLIBRARY, dynamic=True)
+ libraries.append(Library(LDLIBRARY, is_dynamic=True))
# Check extension modules like _ssl.cpython-310d-x86_64-linux-gnu.so
- nsymbol += check_extensions()
+ libraries.extend(get_extension_libraries())
- if nsymbol:
- print()
- print(f"ERROR: Found {nsymbol} smelly symbols in total!")
- sys.exit(1)
+ smelly_symbols = []
+ for library in libraries:
+ symbols = get_exported_symbols(library)
+ if args.verbose:
+ print(f"{library.path}: {len(symbols)} symbol(s) found")
+ for symbol in sorted(symbols):
+ if args.verbose:
+ print(" -", symbol.name)
+ if symbol.is_smelly:
+ smelly_symbols.append(symbol)
print()
- print(f"OK: all exported symbols of all libraries "
- f"are prefixed with {' or '.join(map(repr, ALLOWED_PREFIXES))}")
+
+ if smelly_symbols:
+ print(f"Found {len(smelly_symbols)} smelly symbols in total!")
+ for symbol in sorted(smelly_symbols):
+ print(f" - {symbol.name} from {symbol.library.path}")
+ sys.exit(1)
+
+ print(f"OK: all exported symbols of all libraries",
+ f"are prefixed with {' or '.join(map(repr, ALLOWED_PREFIXES))}",
+ f"or are covered by exceptions")
if __name__ == "__main__":
_______________________________________________
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]