diff options
author | Petr Viktorin <encukou@gmail.com> | 2022-04-29 14:18:08 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-04-29 14:18:08 (GMT) |
commit | 83bce8ef14ee5546cb3d60f398f0cbb04bd3d9df (patch) | |
tree | af3c0eaba50e7a3bb15824ee7b1d08178e186247 /Tools/scripts | |
parent | 89c6b2b8f615a1c1827a92c4582c213b1a5027fb (diff) | |
download | cpython-83bce8ef14ee5546cb3d60f398f0cbb04bd3d9df.zip cpython-83bce8ef14ee5546cb3d60f398f0cbb04bd3d9df.tar.gz cpython-83bce8ef14ee5546cb3d60f398f0cbb04bd3d9df.tar.bz2 |
gh-91324: Convert the stable ABI manifest to TOML (GH-92026)
Diffstat (limited to 'Tools/scripts')
-rwxr-xr-x | Tools/scripts/stable_abi.py | 249 |
1 files changed, 123 insertions, 126 deletions
diff --git a/Tools/scripts/stable_abi.py b/Tools/scripts/stable_abi.py index 5407524..f5a9f8d 100755 --- a/Tools/scripts/stable_abi.py +++ b/Tools/scripts/stable_abi.py @@ -14,8 +14,10 @@ import subprocess import sysconfig import argparse import textwrap +import tomllib import difflib import shutil +import pprint import sys import os import os.path @@ -46,17 +48,15 @@ MACOS = (sys.platform == "darwin") UNIXY = MACOS or (sys.platform == "linux") # XXX should this be "not Windows"? -# The stable ABI manifest (Misc/stable_abi.txt) exists only to fill the +# The stable ABI manifest (Misc/stable_abi.toml) exists only to fill the # following dataclasses. # Feel free to change its syntax (and the `parse_manifest` function) # to better serve that purpose (while keeping it human-readable). -@dataclasses.dataclass class Manifest: """Collection of `ABIItem`s forming the stable ABI/limited API.""" - - kind = 'manifest' - contents: dict = dataclasses.field(default_factory=dict) + def __init__(self): + self.contents = dict() def add(self, item): if item.name in self.contents: @@ -65,14 +65,6 @@ class Manifest: raise ValueError(f'duplicate ABI item {item.name}') self.contents[item.name] = item - @property - def feature_defines(self): - """Return all feature defines which affect what's available - - These are e.g. HAVE_FORK and MS_WINDOWS. - """ - return set(item.ifdef for item in self.contents.values()) - {None} - def select(self, kinds, *, include_abi_only=True, ifdef=None): """Yield selected items of the manifest @@ -81,7 +73,7 @@ class Manifest: stable ABI. If False, include only items from the limited API (i.e. items people should use today) - ifdef: set of feature defines (e.g. {'HAVE_FORK', 'MS_WINDOWS'}). + ifdef: set of feature macros (e.g. {'HAVE_FORK', 'MS_WINDOWS'}). If None (default), items are not filtered by this. (This is different from the empty set, which filters out all such conditional items.) @@ -99,109 +91,74 @@ class Manifest: def dump(self): """Yield lines to recreate the manifest file (sans comments/newlines)""" - # Recursive in preparation for struct member & function argument nodes for item in self.contents.values(): - yield from item.dump(indent=0) - + fields = dataclasses.fields(item) + yield f"[{item.kind}.{item.name}]" + for field in fields: + if field.name in {'name', 'value', 'kind'}: + continue + value = getattr(item, field.name) + if value == field.default: + pass + elif value is True: + yield f" {field.name} = true" + elif value: + yield f" {field.name} = {value!r}" + + +itemclasses = {} +def itemclass(kind): + """Register the decorated class in `itemclasses`""" + def decorator(cls): + itemclasses[kind] = cls + return cls + return decorator + +@itemclass('function') +@itemclass('macro') +@itemclass('data') +@itemclass('const') +@itemclass('typedef') @dataclasses.dataclass class ABIItem: """Information on one item (function, macro, struct, etc.)""" - kind: str name: str + kind: str added: str = None - contents: list = dataclasses.field(default_factory=list) abi_only: bool = False ifdef: str = None - struct_abi_kind: str = None - members: list = None - doc: str = None + +@itemclass('feature_macro') +@dataclasses.dataclass(kw_only=True) +class FeatureMacro(ABIItem): + name: str + doc: str windows: bool = False + abi_only: bool = True - KINDS = frozenset({ - 'struct', 'function', 'macro', 'data', 'const', 'typedef', 'ifdef', - }) +@itemclass('struct') +@dataclasses.dataclass(kw_only=True) +class Struct(ABIItem): + struct_abi_kind: str + members: list = None - def dump(self, indent=0): - yield f"{' ' * indent}{self.kind} {self.name}" - if self.added: - yield f"{' ' * (indent+1)}added {self.added}" - if self.ifdef: - yield f"{' ' * (indent+1)}ifdef {self.ifdef}" - if self.abi_only: - yield f"{' ' * (indent+1)}abi_only" def parse_manifest(file): """Parse the given file (iterable of lines) to a Manifest""" - LINE_RE = re.compile('(?P<indent>[ ]*)(?P<kind>[^ ]+)[ ]*(?P<content>.*)') manifest = Manifest() - # parents of currently processed line, each with its indentation level - levels = [(manifest, -1)] + data = tomllib.load(file) - def raise_error(msg): - raise SyntaxError(f'line {lineno}: {msg}') - - for lineno, line in enumerate(file, start=1): - line, sep, comment = line.partition('#') - line = line.rstrip() - if not line: - continue - match = LINE_RE.fullmatch(line) - if not match: - raise_error(f'invalid syntax: {line}') - level = len(match['indent']) - kind = match['kind'] - content = match['content'] - while level <= levels[-1][1]: - levels.pop() - parent = levels[-1][0] - entry = None - if parent.kind == 'manifest': - if kind not in kind in ABIItem.KINDS: - raise_error(f'{kind} cannot go in {parent.kind}') - entry = ABIItem(kind, content) - parent.add(entry) - elif kind in {'added', 'ifdef'}: - if parent.kind not in ABIItem.KINDS: - raise_error(f'{kind} cannot go in {parent.kind}') - setattr(parent, kind, content) - elif kind in {'abi_only'}: - if parent.kind not in {'function', 'data'}: - raise_error(f'{kind} cannot go in {parent.kind}') - parent.abi_only = True - elif kind in {'members', 'full-abi', 'opaque'}: - if parent.kind not in {'struct'}: - raise_error(f'{kind} cannot go in {parent.kind}') - if prev := getattr(parent, 'struct_abi_kind', None): - raise_error( - f'{parent.name} already has {prev}, cannot add {kind}') - parent.struct_abi_kind = kind - if kind == 'members': - parent.members = content.split() - elif kind in {'doc'}: - if parent.kind not in {'ifdef'}: - raise_error(f'{kind} cannot go in {parent.kind}') - parent.doc = content - elif kind in {'windows'}: - if parent.kind not in {'ifdef'}: - raise_error(f'{kind} cannot go in {parent.kind}') - if not content: - parent.windows = True - elif content == 'maybe': - parent.windows = content - else: - raise_error(f'Unexpected: {content}') - else: - raise_error(f"unknown kind {kind!r}") - # When adding more, update the comment in stable_abi.txt. - levels.append((entry, level)) - - ifdef_names = {i.name for i in manifest.select({'ifdef'})} - for item in manifest.contents.values(): - if item.ifdef and item.ifdef not in ifdef_names: - raise ValueError(f'{item.name} uses undeclared ifdef {item.ifdef}') + for kind, itemclass in itemclasses.items(): + for name, item_data in data[kind].items(): + try: + item = itemclass(name=name, kind=kind, **item_data) + manifest.add(item) + except BaseException as exc: + exc.add_note(f'in {kind} {name}') + raise return manifest @@ -246,12 +203,14 @@ def gen_python3dll(manifest, args, outfile): def sort_key(item): return item.name.lower() - windows_ifdefs = { - item.name for item in manifest.select({'ifdef'}) if item.windows + windows_feature_macros = { + item.name for item in manifest.select({'feature_macro'}) if item.windows } for item in sorted( manifest.select( - {'function'}, include_abi_only=True, ifdef=windows_ifdefs), + {'function'}, + include_abi_only=True, + ifdef=windows_feature_macros), key=sort_key): write(f'EXPORT_FUNC({item.name})') @@ -259,7 +218,9 @@ def gen_python3dll(manifest, args, outfile): for item in sorted( manifest.select( - {'data'}, include_abi_only=True, ifdef=windows_ifdefs), + {'data'}, + include_abi_only=True, + ifdef=windows_feature_macros), key=sort_key): write(f'EXPORT_DATA({item.name})') @@ -285,17 +246,20 @@ def gen_doc_annotations(manifest, args, outfile): ifdef_note = manifest.contents[item.ifdef].doc else: ifdef_note = None - writer.writerow({ + row = { 'role': REST_ROLES[item.kind], 'name': item.name, 'added': item.added, - 'ifdef_note': ifdef_note, - 'struct_abi_kind': item.struct_abi_kind}) - for member_name in item.members or (): - writer.writerow({ - 'role': 'member', - 'name': f'{item.name}.{member_name}', - 'added': item.added}) + 'ifdef_note': ifdef_note} + rows = [row] + if item.kind == 'struct': + row['struct_abi_kind'] = item.struct_abi_kind + for member_name in item.members or (): + rows.append({ + 'role': 'member', + 'name': f'{item.name}.{member_name}', + 'added': item.added}) + writer.writerows(rows) @generator("ctypes_test", 'Lib/test/test_stable_abi_ctypes.py') def gen_ctypes_test(manifest, args, outfile): @@ -323,7 +287,8 @@ def gen_ctypes_test(manifest, args, outfile): ctypes_test.pythonapi[symbol_name] def test_feature_macros(self): - self.assertEqual(set(get_feature_macros()), EXPECTED_IFDEFS) + self.assertEqual( + set(get_feature_macros()), EXPECTED_FEATURE_MACROS) # The feature macros for Windows are used in creating the DLL # definition, so they must be known on all platforms. @@ -331,7 +296,7 @@ def gen_ctypes_test(manifest, args, outfile): # the reality. @unittest.skipIf(sys.platform != "win32", "Windows specific test") def test_windows_feature_macros(self): - for name, value in WINDOWS_IFDEFS.items(): + for name, value in WINDOWS_FEATURE_MACROS.items(): if value != 'maybe': with self.subTest(name): self.assertEqual(feature_macros[name], value) @@ -342,7 +307,7 @@ def gen_ctypes_test(manifest, args, outfile): {'function', 'data'}, include_abi_only=True, ) - ifdef_items = {} + optional_items = {} for item in items: if item.name in ( # Some symbols aren't exported on all platforms. @@ -351,23 +316,23 @@ def gen_ctypes_test(manifest, args, outfile): ): continue if item.ifdef: - ifdef_items.setdefault(item.ifdef, []).append(item.name) + optional_items.setdefault(item.ifdef, []).append(item.name) else: write(f' "{item.name}",') write(")") - for ifdef, names in ifdef_items.items(): + for ifdef, names in optional_items.items(): write(f"if feature_macros[{ifdef!r}]:") write(f" SYMBOL_NAMES += (") for name in names: write(f" {name!r},") write(" )") write("") - write(f"EXPECTED_IFDEFS = set({sorted(ifdef_items)})") + feature_macros = list(manifest.select({'feature_macro'})) + feature_names = sorted(m.name for m in feature_macros) + write(f"EXPECTED_FEATURE_MACROS = set({pprint.pformat(feature_names)})") - windows_ifdef_values = { - name: manifest.contents[name].windows for name in ifdef_items - } - write(f"WINDOWS_IFDEFS = {windows_ifdef_values}") + windows_feature_macros = {m.name: m.windows for m in feature_macros} + write(f"WINDOWS_FEATURE_MACROS = {pprint.pformat(windows_feature_macros)}") @generator("testcapi_feature_macros", 'Modules/_testcapi_feature_macros.inc') @@ -378,7 +343,7 @@ def gen_testcapi_feature_macros(manifest, args, outfile): write() write('// Add an entry in dict `result` for each Stable ABI feature macro.') write() - for macro in manifest.select({'ifdef'}): + for macro in manifest.select({'feature_macro'}): name = macro.name write(f'#ifdef {name}') write(f' res = PyDict_SetItemString(result, "{name}", Py_True);') @@ -425,7 +390,8 @@ def do_unixy_check(manifest, args): # Get all macros first: we'll need feature macros like HAVE_FORK and # MS_WINDOWS for everything else present_macros = gcc_get_limited_api_macros(['Include/Python.h']) - feature_defines = manifest.feature_defines & present_macros + feature_macros = set(m.name for m in manifest.select({'feature_macro'})) + feature_macros &= present_macros # Check that we have all needed macros expected_macros = set( @@ -438,7 +404,7 @@ def do_unixy_check(manifest, args): + 'with Py_LIMITED_API:') expected_symbols = set(item.name for item in manifest.select( - {'function', 'data'}, include_abi_only=True, ifdef=feature_defines, + {'function', 'data'}, include_abi_only=True, ifdef=feature_macros, )) # Check the static library (*.a) @@ -458,7 +424,7 @@ def do_unixy_check(manifest, args): # Check definitions in the header files expected_defs = set(item.name for item in manifest.select( - {'function', 'data'}, include_abi_only=False, ifdef=feature_defines, + {'function', 'data'}, include_abi_only=False, ifdef=feature_macros, )) found_defs = gcc_get_limited_api_definitions(['Include/Python.h']) missing_defs = expected_defs - found_defs @@ -635,6 +601,28 @@ def check_private_names(manifest): f'`{name}` is private (underscore-prefixed) and should be ' + 'removed from the stable ABI list or or marked `abi_only`') +def check_dump(manifest, filename): + """Check that manifest.dump() corresponds to the data. + + Mainly useful when debugging this script. + """ + dumped = tomllib.loads('\n'.join(manifest.dump())) + with filename.open('rb') as file: + from_file = tomllib.load(file) + if dumped != from_file: + print(f'Dump differs from loaded data!', file=sys.stderr) + diff = difflib.unified_diff( + pprint.pformat(dumped).splitlines(), + pprint.pformat(from_file).splitlines(), + '<dumped>', str(filename), + lineterm='', + ) + for line in diff: + print(line, file=sys.stderr) + return False + else: + return True + def main(): parser = argparse.ArgumentParser( description=__doc__, @@ -696,7 +684,16 @@ def main(): run_all_generators = True args.unixy_check = True - with args.file.open() as file: + try: + file = args.file.open('rb') + except FileNotFoundError as err: + if args.file.suffix == '.txt': + # Provide a better error message + suggestion = args.file.with_suffix('.toml') + raise FileNotFoundError( + f'{args.file} not found. Did you mean {suggestion} ?') from err + raise + with file: manifest = parse_manifest(file) check_private_names(manifest) @@ -709,7 +706,7 @@ def main(): if args.dump: for line in manifest.dump(): print(line) - results['dump'] = True + results['dump'] = check_dump(manifest, args.file) for gen in generators: filename = getattr(args, gen.var_name) |