1 """Check the stable ABI manifest or generate files from it
2
3 By default, the tool only checks existing files/libraries.
4 Pass --generate to recreate auto-generated files instead.
5
6 For actions that take a FILENAME, the filename can be left out to use a default
7 (relative to the manifest file, as they appear in the CPython codebase).
8 """
9
10 from functools import partial
11 from pathlib import Path
12 import dataclasses
13 import subprocess
14 import sysconfig
15 import argparse
16 import textwrap
17 import tomllib
18 import difflib
19 import pprint
20 import sys
21 import os
22 import os.path
23 import io
24 import re
25 import csv
26
27 SCRIPT_NAME = 'Tools/build/stable_abi.py'
28 MISSING = object()
29
30 EXCLUDED_HEADERS = {
31 "bytes_methods.h",
32 "cellobject.h",
33 "classobject.h",
34 "code.h",
35 "compile.h",
36 "datetime.h",
37 "dtoa.h",
38 "frameobject.h",
39 "genobject.h",
40 "longintrepr.h",
41 "parsetok.h",
42 "pyatomic.h",
43 "pytime.h",
44 "token.h",
45 "ucnhash.h",
46 }
47 MACOS = (sys.platform == "darwin")
48 UNIXY = MACOS or (sys.platform == "linux") # XXX should this be "not Windows"?
49
50
51 # The stable ABI manifest (Misc/stable_abi.toml) exists only to fill the
52 # following dataclasses.
53 # Feel free to change its syntax (and the `parse_manifest` function)
54 # to better serve that purpose (while keeping it human-readable).
55
56 class ESC[4;38;5;81mManifest:
57 """Collection of `ABIItem`s forming the stable ABI/limited API."""
58 def __init__(self):
59 self.contents = dict()
60
61 def add(self, item):
62 if item.name in self.contents:
63 # We assume that stable ABI items do not share names,
64 # even if they're different kinds (e.g. function vs. macro).
65 raise ValueError(f'duplicate ABI item {item.name}')
66 self.contents[item.name] = item
67
68 def select(self, kinds, *, include_abi_only=True, ifdef=None):
69 """Yield selected items of the manifest
70
71 kinds: set of requested kinds, e.g. {'function', 'macro'}
72 include_abi_only: if True (default), include all items of the
73 stable ABI.
74 If False, include only items from the limited API
75 (i.e. items people should use today)
76 ifdef: set of feature macros (e.g. {'HAVE_FORK', 'MS_WINDOWS'}).
77 If None (default), items are not filtered by this. (This is
78 different from the empty set, which filters out all such
79 conditional items.)
80 """
81 for name, item in sorted(self.contents.items()):
82 if item.kind not in kinds:
83 continue
84 if item.abi_only and not include_abi_only:
85 continue
86 if (ifdef is not None
87 and item.ifdef is not None
88 and item.ifdef not in ifdef):
89 continue
90 yield item
91
92 def dump(self):
93 """Yield lines to recreate the manifest file (sans comments/newlines)"""
94 for item in self.contents.values():
95 fields = dataclasses.fields(item)
96 yield f"[{item.kind}.{item.name}]"
97 for field in fields:
98 if field.name in {'name', 'value', 'kind'}:
99 continue
100 value = getattr(item, field.name)
101 if value == field.default:
102 pass
103 elif value is True:
104 yield f" {field.name} = true"
105 elif value:
106 yield f" {field.name} = {value!r}"
107
108
109 itemclasses = {}
110 def itemclass(kind):
111 """Register the decorated class in `itemclasses`"""
112 def decorator(cls):
113 itemclasses[kind] = cls
114 return cls
115 return decorator
116
117 @itemclass('function')
118 @itemclass('macro')
119 @itemclass('data')
120 @itemclass('const')
121 @itemclass('typedef')
122 @dataclasses.dataclass
123 class ESC[4;38;5;81mABIItem:
124 """Information on one item (function, macro, struct, etc.)"""
125
126 name: str
127 kind: str
128 added: str = None
129 abi_only: bool = False
130 ifdef: str = None
131
132 @itemclass('feature_macro')
133 @dataclasses.dataclass(kw_only=True)
134 class ESC[4;38;5;81mFeatureMacro(ESC[4;38;5;149mABIItem):
135 name: str
136 doc: str
137 windows: bool = False
138 abi_only: bool = True
139
140 @itemclass('struct')
141 @dataclasses.dataclass(kw_only=True)
142 class ESC[4;38;5;81mStruct(ESC[4;38;5;149mABIItem):
143 struct_abi_kind: str
144 members: list = None
145
146
147 def parse_manifest(file):
148 """Parse the given file (iterable of lines) to a Manifest"""
149
150 manifest = Manifest()
151
152 data = tomllib.load(file)
153
154 for kind, itemclass in itemclasses.items():
155 for name, item_data in data[kind].items():
156 try:
157 item = itemclass(name=name, kind=kind, **item_data)
158 manifest.add(item)
159 except BaseException as exc:
160 exc.add_note(f'in {kind} {name}')
161 raise
162
163 return manifest
164
165 # The tool can run individual "actions".
166 # Most actions are "generators", which generate a single file from the
167 # manifest. (Checking works by generating a temp file & comparing.)
168 # Other actions, like "--unixy-check", don't work on a single file.
169
170 generators = []
171 def generator(var_name, default_path):
172 """Decorates a file generator: function that writes to a file"""
173 def _decorator(func):
174 func.var_name = var_name
175 func.arg_name = '--' + var_name.replace('_', '-')
176 func.default_path = default_path
177 generators.append(func)
178 return func
179 return _decorator
180
181
182 @generator("python3dll", 'PC/python3dll.c')
183 def gen_python3dll(manifest, args, outfile):
184 """Generate/check the source for the Windows stable ABI library"""
185 write = partial(print, file=outfile)
186 content = f"""\
187 /* Re-export stable Python ABI */
188
189 /* Generated by {SCRIPT_NAME} */
190 """
191 content += r"""
192 #ifdef _M_IX86
193 #define DECORATE "_"
194 #else
195 #define DECORATE
196 #endif
197
198 #define EXPORT_FUNC(name) \
199 __pragma(comment(linker, "/EXPORT:" DECORATE #name "=" PYTHON_DLL_NAME "." #name))
200 #define EXPORT_DATA(name) \
201 __pragma(comment(linker, "/EXPORT:" DECORATE #name "=" PYTHON_DLL_NAME "." #name ",DATA"))
202 """
203 write(textwrap.dedent(content))
204
205 def sort_key(item):
206 return item.name.lower()
207
208 windows_feature_macros = {
209 item.name for item in manifest.select({'feature_macro'}) if item.windows
210 }
211 for item in sorted(
212 manifest.select(
213 {'function'},
214 include_abi_only=True,
215 ifdef=windows_feature_macros),
216 key=sort_key):
217 write(f'EXPORT_FUNC({item.name})')
218
219 write()
220
221 for item in sorted(
222 manifest.select(
223 {'data'},
224 include_abi_only=True,
225 ifdef=windows_feature_macros),
226 key=sort_key):
227 write(f'EXPORT_DATA({item.name})')
228
229 REST_ROLES = {
230 'function': 'function',
231 'data': 'var',
232 'struct': 'type',
233 'macro': 'macro',
234 # 'const': 'const', # all undocumented
235 'typedef': 'type',
236 }
237
238 @generator("doc_list", 'Doc/data/stable_abi.dat')
239 def gen_doc_annotations(manifest, args, outfile):
240 """Generate/check the stable ABI list for documentation annotations"""
241 writer = csv.DictWriter(
242 outfile,
243 ['role', 'name', 'added', 'ifdef_note', 'struct_abi_kind'],
244 lineterminator='\n')
245 writer.writeheader()
246 for item in manifest.select(REST_ROLES.keys(), include_abi_only=False):
247 if item.ifdef:
248 ifdef_note = manifest.contents[item.ifdef].doc
249 else:
250 ifdef_note = None
251 row = {
252 'role': REST_ROLES[item.kind],
253 'name': item.name,
254 'added': item.added,
255 'ifdef_note': ifdef_note}
256 rows = [row]
257 if item.kind == 'struct':
258 row['struct_abi_kind'] = item.struct_abi_kind
259 for member_name in item.members or ():
260 rows.append({
261 'role': 'member',
262 'name': f'{item.name}.{member_name}',
263 'added': item.added})
264 writer.writerows(rows)
265
266 @generator("ctypes_test", 'Lib/test/test_stable_abi_ctypes.py')
267 def gen_ctypes_test(manifest, args, outfile):
268 """Generate/check the ctypes-based test for exported symbols"""
269 write = partial(print, file=outfile)
270 write(textwrap.dedent(f'''\
271 # Generated by {SCRIPT_NAME}
272
273 """Test that all symbols of the Stable ABI are accessible using ctypes
274 """
275
276 import sys
277 import unittest
278 from test.support.import_helper import import_module
279 from _testcapi import get_feature_macros
280
281 feature_macros = get_feature_macros()
282 ctypes_test = import_module('ctypes')
283
284 class TestStableABIAvailability(unittest.TestCase):
285 def test_available_symbols(self):
286
287 for symbol_name in SYMBOL_NAMES:
288 with self.subTest(symbol_name):
289 ctypes_test.pythonapi[symbol_name]
290
291 def test_feature_macros(self):
292 self.assertEqual(
293 set(get_feature_macros()), EXPECTED_FEATURE_MACROS)
294
295 # The feature macros for Windows are used in creating the DLL
296 # definition, so they must be known on all platforms.
297 # If we are on Windows, we check that the hardcoded data matches
298 # the reality.
299 @unittest.skipIf(sys.platform != "win32", "Windows specific test")
300 def test_windows_feature_macros(self):
301 for name, value in WINDOWS_FEATURE_MACROS.items():
302 if value != 'maybe':
303 with self.subTest(name):
304 self.assertEqual(feature_macros[name], value)
305
306 SYMBOL_NAMES = (
307 '''))
308 items = manifest.select(
309 {'function', 'data'},
310 include_abi_only=True,
311 )
312 optional_items = {}
313 for item in items:
314 if item.name in (
315 # Some symbols aren't exported on all platforms.
316 # This is a bug: https://bugs.python.org/issue44133
317 'PyModule_Create2', 'PyModule_FromDefAndSpec2',
318 ):
319 continue
320 if item.ifdef:
321 optional_items.setdefault(item.ifdef, []).append(item.name)
322 else:
323 write(f' "{item.name}",')
324 write(")")
325 for ifdef, names in optional_items.items():
326 write(f"if feature_macros[{ifdef!r}]:")
327 write(f" SYMBOL_NAMES += (")
328 for name in names:
329 write(f" {name!r},")
330 write(" )")
331 write("")
332 feature_macros = list(manifest.select({'feature_macro'}))
333 feature_names = sorted(m.name for m in feature_macros)
334 write(f"EXPECTED_FEATURE_MACROS = set({pprint.pformat(feature_names)})")
335
336 windows_feature_macros = {m.name: m.windows for m in feature_macros}
337 write(f"WINDOWS_FEATURE_MACROS = {pprint.pformat(windows_feature_macros)}")
338
339
340 @generator("testcapi_feature_macros", 'Modules/_testcapi_feature_macros.inc')
341 def gen_testcapi_feature_macros(manifest, args, outfile):
342 """Generate/check the stable ABI list for documentation annotations"""
343 write = partial(print, file=outfile)
344 write(f'// Generated by {SCRIPT_NAME}')
345 write()
346 write('// Add an entry in dict `result` for each Stable ABI feature macro.')
347 write()
348 for macro in manifest.select({'feature_macro'}):
349 name = macro.name
350 write(f'#ifdef {name}')
351 write(f' res = PyDict_SetItemString(result, "{name}", Py_True);')
352 write('#else')
353 write(f' res = PyDict_SetItemString(result, "{name}", Py_False);')
354 write('#endif')
355 write('if (res) {')
356 write(' Py_DECREF(result); return NULL;')
357 write('}')
358 write()
359
360
361 def generate_or_check(manifest, args, path, func):
362 """Generate/check a file with a single generator
363
364 Return True if successful; False if a comparison failed.
365 """
366
367 outfile = io.StringIO()
368 func(manifest, args, outfile)
369 generated = outfile.getvalue()
370 existing = path.read_text()
371
372 if generated != existing:
373 if args.generate:
374 path.write_text(generated)
375 else:
376 print(f'File {path} differs from expected!')
377 diff = difflib.unified_diff(
378 generated.splitlines(), existing.splitlines(),
379 str(path), '<expected>',
380 lineterm='',
381 )
382 for line in diff:
383 print(line)
384 return False
385 return True
386
387
388 def do_unixy_check(manifest, args):
389 """Check headers & library using "Unixy" tools (GCC/clang, binutils)"""
390 okay = True
391
392 # Get all macros first: we'll need feature macros like HAVE_FORK and
393 # MS_WINDOWS for everything else
394 present_macros = gcc_get_limited_api_macros(['Include/Python.h'])
395 feature_macros = set(m.name for m in manifest.select({'feature_macro'}))
396 feature_macros &= present_macros
397
398 # Check that we have all needed macros
399 expected_macros = set(
400 item.name for item in manifest.select({'macro'})
401 )
402 missing_macros = expected_macros - present_macros
403 okay &= _report_unexpected_items(
404 missing_macros,
405 'Some macros from are not defined from "Include/Python.h"'
406 + 'with Py_LIMITED_API:')
407
408 expected_symbols = set(item.name for item in manifest.select(
409 {'function', 'data'}, include_abi_only=True, ifdef=feature_macros,
410 ))
411
412 # Check the static library (*.a)
413 LIBRARY = sysconfig.get_config_var("LIBRARY")
414 if not LIBRARY:
415 raise Exception("failed to get LIBRARY variable from sysconfig")
416 if os.path.exists(LIBRARY):
417 okay &= binutils_check_library(
418 manifest, LIBRARY, expected_symbols, dynamic=False)
419
420 # Check the dynamic library (*.so)
421 LDLIBRARY = sysconfig.get_config_var("LDLIBRARY")
422 if not LDLIBRARY:
423 raise Exception("failed to get LDLIBRARY variable from sysconfig")
424 okay &= binutils_check_library(
425 manifest, LDLIBRARY, expected_symbols, dynamic=False)
426
427 # Check definitions in the header files
428 expected_defs = set(item.name for item in manifest.select(
429 {'function', 'data'}, include_abi_only=False, ifdef=feature_macros,
430 ))
431 found_defs = gcc_get_limited_api_definitions(['Include/Python.h'])
432 missing_defs = expected_defs - found_defs
433 okay &= _report_unexpected_items(
434 missing_defs,
435 'Some expected declarations were not declared in '
436 + '"Include/Python.h" with Py_LIMITED_API:')
437
438 # Some Limited API macros are defined in terms of private symbols.
439 # These are not part of Limited API (even though they're defined with
440 # Py_LIMITED_API). They must be part of the Stable ABI, though.
441 private_symbols = {n for n in expected_symbols if n.startswith('_')}
442 extra_defs = found_defs - expected_defs - private_symbols
443 okay &= _report_unexpected_items(
444 extra_defs,
445 'Some extra declarations were found in "Include/Python.h" '
446 + 'with Py_LIMITED_API:')
447
448 return okay
449
450
451 def _report_unexpected_items(items, msg):
452 """If there are any `items`, report them using "msg" and return false"""
453 if items:
454 print(msg, file=sys.stderr)
455 for item in sorted(items):
456 print(' -', item, file=sys.stderr)
457 return False
458 return True
459
460
461 def binutils_get_exported_symbols(library, dynamic=False):
462 """Retrieve exported symbols using the nm(1) tool from binutils"""
463 # Only look at dynamic symbols
464 args = ["nm", "--no-sort"]
465 if dynamic:
466 args.append("--dynamic")
467 args.append(library)
468 proc = subprocess.run(args, stdout=subprocess.PIPE, universal_newlines=True)
469 if proc.returncode:
470 sys.stdout.write(proc.stdout)
471 sys.exit(proc.returncode)
472
473 stdout = proc.stdout.rstrip()
474 if not stdout:
475 raise Exception("command output is empty")
476
477 for line in stdout.splitlines():
478 # Split line '0000000000001b80 D PyTextIOWrapper_Type'
479 if not line:
480 continue
481
482 parts = line.split(maxsplit=2)
483 if len(parts) < 3:
484 continue
485
486 symbol = parts[-1]
487 if MACOS and symbol.startswith("_"):
488 yield symbol[1:]
489 else:
490 yield symbol
491
492
493 def binutils_check_library(manifest, library, expected_symbols, dynamic):
494 """Check that library exports all expected_symbols"""
495 available_symbols = set(binutils_get_exported_symbols(library, dynamic))
496 missing_symbols = expected_symbols - available_symbols
497 if missing_symbols:
498 print(textwrap.dedent(f"""\
499 Some symbols from the limited API are missing from {library}:
500 {', '.join(missing_symbols)}
501
502 This error means that there are some missing symbols among the
503 ones exported in the library.
504 This normally means that some symbol, function implementation or
505 a prototype belonging to a symbol in the limited API has been
506 deleted or is missing.
507 """), file=sys.stderr)
508 return False
509 return True
510
511
512 def gcc_get_limited_api_macros(headers):
513 """Get all limited API macros from headers.
514
515 Runs the preprocessor over all the header files in "Include" setting
516 "-DPy_LIMITED_API" to the correct value for the running version of the
517 interpreter and extracting all macro definitions (via adding -dM to the
518 compiler arguments).
519
520 Requires Python built with a GCC-compatible compiler. (clang might work)
521 """
522
523 api_hexversion = sys.version_info.major << 24 | sys.version_info.minor << 16
524
525 preprocesor_output_with_macros = subprocess.check_output(
526 sysconfig.get_config_var("CC").split()
527 + [
528 # Prevent the expansion of the exported macros so we can
529 # capture them later
530 "-DSIZEOF_WCHAR_T=4", # The actual value is not important
531 f"-DPy_LIMITED_API={api_hexversion}",
532 "-I.",
533 "-I./Include",
534 "-dM",
535 "-E",
536 ]
537 + [str(file) for file in headers],
538 text=True,
539 )
540
541 return {
542 target
543 for target in re.findall(
544 r"#define (\w+)", preprocesor_output_with_macros
545 )
546 }
547
548
549 def gcc_get_limited_api_definitions(headers):
550 """Get all limited API definitions from headers.
551
552 Run the preprocessor over all the header files in "Include" setting
553 "-DPy_LIMITED_API" to the correct value for the running version of the
554 interpreter.
555
556 The limited API symbols will be extracted from the output of this command
557 as it includes the prototypes and definitions of all the exported symbols
558 that are in the limited api.
559
560 This function does *NOT* extract the macros defined on the limited API
561
562 Requires Python built with a GCC-compatible compiler. (clang might work)
563 """
564 api_hexversion = sys.version_info.major << 24 | sys.version_info.minor << 16
565 preprocesor_output = subprocess.check_output(
566 sysconfig.get_config_var("CC").split()
567 + [
568 # Prevent the expansion of the exported macros so we can capture
569 # them later
570 "-DPyAPI_FUNC=__PyAPI_FUNC",
571 "-DPyAPI_DATA=__PyAPI_DATA",
572 "-DEXPORT_DATA=__EXPORT_DATA",
573 "-D_Py_NO_RETURN=",
574 "-DSIZEOF_WCHAR_T=4", # The actual value is not important
575 f"-DPy_LIMITED_API={api_hexversion}",
576 "-I.",
577 "-I./Include",
578 "-E",
579 ]
580 + [str(file) for file in headers],
581 text=True,
582 stderr=subprocess.DEVNULL,
583 )
584 stable_functions = set(
585 re.findall(r"__PyAPI_FUNC\(.*?\)\s*(.*?)\s*\(", preprocesor_output)
586 )
587 stable_exported_data = set(
588 re.findall(r"__EXPORT_DATA\((.*?)\)", preprocesor_output)
589 )
590 stable_data = set(
591 re.findall(r"__PyAPI_DATA\(.*?\)[\s\*\(]*([^);]*)\)?.*;", preprocesor_output)
592 )
593 return stable_data | stable_exported_data | stable_functions
594
595 def check_private_names(manifest):
596 """Ensure limited API doesn't contain private names
597
598 Names prefixed by an underscore are private by definition.
599 """
600 for name, item in manifest.contents.items():
601 if name.startswith('_') and not item.abi_only:
602 raise ValueError(
603 f'`{name}` is private (underscore-prefixed) and should be '
604 + 'removed from the stable ABI list or or marked `abi_only`')
605
606 def check_dump(manifest, filename):
607 """Check that manifest.dump() corresponds to the data.
608
609 Mainly useful when debugging this script.
610 """
611 dumped = tomllib.loads('\n'.join(manifest.dump()))
612 with filename.open('rb') as file:
613 from_file = tomllib.load(file)
614 if dumped != from_file:
615 print(f'Dump differs from loaded data!', file=sys.stderr)
616 diff = difflib.unified_diff(
617 pprint.pformat(dumped).splitlines(),
618 pprint.pformat(from_file).splitlines(),
619 '<dumped>', str(filename),
620 lineterm='',
621 )
622 for line in diff:
623 print(line, file=sys.stderr)
624 return False
625 else:
626 return True
627
628 def main():
629 parser = argparse.ArgumentParser(
630 description=__doc__,
631 formatter_class=argparse.RawDescriptionHelpFormatter,
632 )
633 parser.add_argument(
634 "file", type=Path, metavar='FILE',
635 help="file with the stable abi manifest",
636 )
637 parser.add_argument(
638 "--generate", action='store_true',
639 help="generate file(s), rather than just checking them",
640 )
641 parser.add_argument(
642 "--generate-all", action='store_true',
643 help="as --generate, but generate all file(s) using default filenames."
644 + " (unlike --all, does not run any extra checks)",
645 )
646 parser.add_argument(
647 "-a", "--all", action='store_true',
648 help="run all available checks using default filenames",
649 )
650 parser.add_argument(
651 "-l", "--list", action='store_true',
652 help="list available generators and their default filenames; then exit",
653 )
654 parser.add_argument(
655 "--dump", action='store_true',
656 help="dump the manifest contents (used for debugging the parser)",
657 )
658
659 actions_group = parser.add_argument_group('actions')
660 for gen in generators:
661 actions_group.add_argument(
662 gen.arg_name, dest=gen.var_name,
663 type=str, nargs="?", default=MISSING,
664 metavar='FILENAME',
665 help=gen.__doc__,
666 )
667 actions_group.add_argument(
668 '--unixy-check', action='store_true',
669 help=do_unixy_check.__doc__,
670 )
671 args = parser.parse_args()
672
673 base_path = args.file.parent.parent
674
675 if args.list:
676 for gen in generators:
677 print(f'{gen.arg_name}: {base_path / gen.default_path}')
678 sys.exit(0)
679
680 run_all_generators = args.generate_all
681
682 if args.generate_all:
683 args.generate = True
684
685 if args.all:
686 run_all_generators = True
687 if UNIXY:
688 args.unixy_check = True
689
690 try:
691 file = args.file.open('rb')
692 except FileNotFoundError as err:
693 if args.file.suffix == '.txt':
694 # Provide a better error message
695 suggestion = args.file.with_suffix('.toml')
696 raise FileNotFoundError(
697 f'{args.file} not found. Did you mean {suggestion} ?') from err
698 raise
699 with file:
700 manifest = parse_manifest(file)
701
702 check_private_names(manifest)
703
704 # Remember results of all actions (as booleans).
705 # At the end we'll check that at least one action was run,
706 # and also fail if any are false.
707 results = {}
708
709 if args.dump:
710 for line in manifest.dump():
711 print(line)
712 results['dump'] = check_dump(manifest, args.file)
713
714 for gen in generators:
715 filename = getattr(args, gen.var_name)
716 if filename is None or (run_all_generators and filename is MISSING):
717 filename = base_path / gen.default_path
718 elif filename is MISSING:
719 continue
720
721 results[gen.var_name] = generate_or_check(manifest, args, filename, gen)
722
723 if args.unixy_check:
724 results['unixy_check'] = do_unixy_check(manifest, args)
725
726 if not results:
727 if args.generate:
728 parser.error('No file specified. Use --help for usage.')
729 parser.error('No check specified. Use --help for usage.')
730
731 failed_results = [name for name, result in results.items() if not result]
732
733 if failed_results:
734 raise Exception(f"""
735 These checks related to the stable ABI did not succeed:
736 {', '.join(failed_results)}
737
738 If you see diffs in the output, files derived from the stable
739 ABI manifest the were not regenerated.
740 Run `make regen-limited-abi` to fix this.
741
742 Otherwise, see the error(s) above.
743
744 The stable ABI manifest is at: {args.file}
745 Note that there is a process to follow when modifying it.
746
747 You can read more about the limited API and its contracts at:
748
749 https://docs.python.org/3/c-api/stable.html
750
751 And in PEP 384:
752
753 https://peps.python.org/pep-0384/
754 """)
755
756
757 if __name__ == "__main__":
758 main()