Files
RustPython/scripts/whats_left.py
T
youknowdotandJeong, YunWon 06007c81fa Begin the Python 3.15 target and essential-library migration (#8950)
* Target Python 3.15 using the final upstream reference

Update active version and comparison configuration to Python 3.15.0. Preserve the alpha implementation status, actual bytecode format, and existing libraries pending separate migration. Align generated library-upgrade workflow pins without changing permissions or actions.

Assisted-by: OpenAI Codex:model identifier unavailable

* Adapt essential snippet contracts to Python 3.15 final

Preserve existing coverage while following verified final behavior for script metadata, builtin signatures and loading, AST fixture construction, memoryview diagnostics, mmap search bounds, and struct format errors. The unchanged final comparator exposes the earlier expectations; focused native baseline failures establish the required implementation changes.

Assisted-by: OpenAI Codex:model identifier unavailable

* Keep final-reference regression paths and remove expired sysconfig argument

Preserve reverse multiplication syntax and native exact overflow diagnostics. Accept the two source-proven CPython 3.15 specialization messages only in affected reverse str/bytes/tuple cases. Use an actually out-of-range Windows datetime errno fixture. Remove the sysconfig check_home argument at its existing canonical 3.15 removal test boundary.

Assisted-by: OpenAI Codex:model identifier unavailable

* Keep README wording unchanged in Python 3.15 transition

Assisted-by: OpenAI Codex:model identifier unavailable

* Match Python 3.15 repetition specialization and keep universal tests

Use exact sequence-type guards and preserve generic fallback diagnostics. Validate both cold and warmed expressions without implementation-specific assertions. Reject stale CPython source checkouts and cross-version library caches.

Assisted-by: OpenAI Codex:model identifier unavailable

* Limit in-place specialization additions to exact sequence repetition

Assisted-by: OpenAI Codex:model identifier unavailable

* Keep subclass regression scoped to repetition dispatch

Assisted-by: OpenAI Codex:model identifier unavailable

* Provision final CPython 3.15 with pinned uv builds

Assisted-by: OpenAI Codex:model identifier unavailable

* Enforce Python 3.15 AST constructor requirements

Reject retired missing-field and arbitrary-keyword forms while preserving defaults and explicit placeholders. Sync only matching final CPython constructor test hunks.

Assisted-by: OpenAI Codex:model identifier unavailable

* Keep seeded Python ahead of its Windows DLL directory

Assisted-by: OpenAI Codex:model identifier unavailable

* Keep opcode metadata tied to the implemented bytecode format

The interpreter target and reference tests use Python 3.15.0, but the current Rust opcode enum retains the 3.14 bytecode format. Generate metadata from its matching source rather than importing unsupported instruction families.

Assisted-by: OpenAI Codex:model identifier unavailable

* Align CSV, logarithm and decimal formatting with Python 3.15

Propagate dialect lookup errors except AttributeError, retry integer logarithms after float overflow, and update the decimal conversion diagnostic. Exercise the final reference contracts without implementation-specific branches.

Assisted-by: OpenAI Codex:model identifier unavailable

* Bind the logarithm index result before borrowing its integer

Assisted-by: OpenAI Codex:model identifier unavailable

* Align required canonical test expectations with Python 3.15

Take only demonstrated contract changes from CPython v3.15.0, d220409a13b77de7a6c30abe16ea90db01528edc: removed module __cached__, Struct format exception classes, and decimal-format diagnostic. Preserve unrelated tests and expectations.

Assisted-by: OpenAI Codex:model identifier unavailable

* Track pending frozendict pickle support during the 3.15 transition

Keep the two existing frozendict pickle tests running as expected failures until #8951 supplies the builtin. Unexpected success will signal removal of these temporary markers.

Assisted-by: OpenAI Codex:model identifier unavailable

* Support final Python 3.15 Windows negative timestamps

Use FILETIME for pre-epoch conversions and preserve precise Windows timestamp errors.

Assisted-by: OpenAI Codex:model identifier unavailable

* Derive historical Windows timezone offsets from wall time

FILETIME reports historical DST as unknown, so calculate the actual offset rather than selecting standard-time bias.

Assisted-by: OpenAI Codex:model identifier unavailable

* Identify decimal formatting arguments in Python 3.15 errors

Track consumed positional arguments, including star width and precision, and preserve mapping key representations in decimal conversion diagnostics.

Assisted-by: OpenAI Codex:model identifier unavailable

* Reuse mapping format keys for lookup and error context

Assisted-by: OpenAI Codex:model identifier unavailable

* Borrow mapping diagnostic keys as Python objects

Assisted-by: OpenAI Codex:model identifier unavailable

* Avoid empty components in the reference Python library path

Assisted-by: OpenAI Codex:model identifier unavailable

* Isolate RustPython bytecode caches during the 3.15 transition

Assisted-by: OpenAI Codex:model identifier unavailable

* Sync final Windows timestamp test expectations

Import only the final CPython platform-limit guards and mktime roundtrip expectation required by the existing Windows CI suite.

Assisted-by: OpenAI Codex:model identifier unavailable

* Restore official Python setup for 3.15 final

Use the existing pinned setup-python action now that its official catalog supplies CPython 3.15.0. Remove the temporary standalone provisioning action and its sparse-checkout entry while retaining interpreter outputs and requested versions.

Assisted-by: OpenAI Codex:model identifier unavailable

* Align merged runtime contracts with Python 3.15 final

Restrict mappingproxy ordering, use final codec registry normalization, and prefer index conversion before float conversion for sleep. Retain universal Python assertions and import the matching final codec/time canonical test hunks plus the non-ASCII codec-name deprecation warning.

Upstream source: CPython d220409a13b77de7a6c30abe16ea90db01528edc. The existing codec normalization implementation and bytecode contract remain unchanged.

Assisted-by: OpenAI Codex:model identifier unavailable

* Limit Windows launcher version check to its interpreter

Exclude only the CPython py.exe version-identity case from RustPython CI. The installed launcher reports its own final version, while RustPython correctly remains alpha. Preserve the other 45 launcher cases and all RustPython version assertions without modifying Lib.

Assisted-by: OpenAI Codex:model identifier unavailable

---------

Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>
2026-10-11 12:57:49 +09:00

736 lines
24 KiB
Python
Executable File

#!/usr/bin/env -S python3 -I
# /// script
# requires-python = ">=3.15.0"
# ///
# This script generates Lib/snippets/whats_left_data.py with these variables defined:
# expected_methods - a dictionary mapping builtin objects to their methods
# cpymods - a dictionary mapping module names to their contents, including
# the members of native classes as "Class.member"
# libdir - the location of RustPython's Lib/ directory.
#
# TODO: include this:
# which finds all modules it has available and
# creates a Python dictionary mapping module names to their contents, which is
# in turn used to generate a second Python script that also finds which modules
# it has available and compares that against the first dictionary we generated.
# We then run this second generated script with RustPython.
import argparse
import inspect
import json
import os
import platform
import re
import subprocess
import sys
import types
import warnings
from pydoc import ModuleScanner
if not sys.flags.isolated:
print("running without -I option.")
print("python -I scripts/whats_left.py")
exit(1)
GENERATED_FILE = "extra_tests/not_impl.py"
implementation = platform.python_implementation()
if implementation != "CPython":
sys.exit(f"whats_left.py must be run under CPython, got {implementation} instead")
if sys.version_info[:2] < (3, 15):
sys.exit(
f"whats_left.py must be run under CPython 3.15 or newer, got {implementation} {sys.version} instead. If you have uv, try `uv run python -I scripts/whats_left.py` to select a proper Python interpreter easier."
)
def parse_args():
parser = argparse.ArgumentParser(description="Process some integers.")
parser.add_argument(
"--signature",
action="store_true",
help="print functions whose signatures don't match CPython's",
)
parser.add_argument(
"--doc",
action="store_true",
help="print elements whose __doc__ don't match CPython's",
)
parser.add_argument(
"--json",
action="store_true",
help="print output as JSON (instead of line by line)",
)
parser.add_argument(
"--no-default-features",
action="store_true",
help="disable default features when building RustPython",
)
parser.add_argument(
"--features",
action="append",
help="which features to enable when building RustPython (default: [])",
default=[],
)
parser.add_argument(
"--rustpython",
help="use this RustPython executable instead of building one with cargo",
)
args = parser.parse_args()
return args
args = parse_args()
# CPython specific modules (mostly consisting of templates/tests)
CPYTHON_SPECIFIC_MODS = {
"xxmodule",
"xxsubtype",
"xxlimited",
"_xxtestfuzz",
"_testbuffer",
"_testcapi",
"_testimportmultiple",
"_testinternalcapi",
"_testmultiphase",
"_testlimitedcapi",
}
IGNORED_MODULES = {"this", "antigravity"} | CPYTHON_SPECIFIC_MODS
sys.path = [
path
for path in sys.path
if ("site-packages" not in path and "dist-packages" not in path)
]
def attr_is_not_inherited(type_, attr):
"""
returns True if type_'s attr is not inherited from any of its base classes
"""
return attr in type_.__dict__
def extra_info(obj):
import sys
class _OrderedSet:
"""Set repr with sorted elements, so hash order does not change it."""
def __init__(self, items, braces):
self.items = items
self.braces = braces
def __repr__(self):
if not self.items:
return "set()" if self.braces else "frozenset()"
body = ", ".join(sorted(repr(item) for item in self.items))
if self.braces:
return "{" + body + "}"
return "frozenset({" + body + "})"
def canon_default(param):
default = param.default
# The interpreter path, version tuple, and install scheme differ per process.
if isinstance(default, str) and default == sys.executable:
return "<sys.executable>"
if default is sys.version_info:
return "<sys.version_info>"
if param.name == "scheme" and isinstance(default, str):
import sysconfig
if default == sysconfig.get_default_scheme():
return "<sysconfig.get_default_scheme()>"
if isinstance(default, frozenset):
return _OrderedSet(default, False)
if isinstance(default, set):
return _OrderedSet(default, True)
return default
def stable_signature(target):
sig = inspect.signature(target)
params = []
changed = False
for param in sig.parameters.values():
if param.default is not inspect.Parameter.empty:
default = canon_default(param)
if default is not param.default:
param = param.replace(default=default)
changed = True
params.append(param)
if changed:
sig = sig.replace(parameters=params)
# remove function memory addresses
return re.sub(r" at 0x[0-9A-Fa-f]+", " at 0xdeadbeef", str(sig))
if callable(obj):
doc = inspect.getdoc(obj)
try:
return {
"sig": stable_signature(obj),
"doc": doc,
}
except Exception as e:
exception = repr(e)
# CPython uses ' RustPython uses "
if exception.replace('"', "'").startswith("ValueError('no signature found"):
return {
"sig": "ValueError('no signature found')",
"doc": doc,
}
return {
"sig": exception,
"doc": doc,
}
return {
"sig": None,
"doc": None,
}
def name_sort_key(name):
if name == "builtins":
return ""
if name[0] == "_":
return name[1:] + "1"
return name + "2"
BUILTIN_TYPES = [
bool,
bytearray,
bytes,
complex,
dict,
enumerate,
filter,
float,
frozenset,
int,
list,
map,
memoryview,
range,
set,
slice,
str,
super,
tuple,
object,
zip,
classmethod,
staticmethod,
property,
Exception,
BaseException,
]
# CPython's per-class annotations cache and its vectorcall C API offset
CPYTHON_INTERNAL_ATTRS = {"__annotations_cache__", "__vectorcalloffset__"}
def own_attrs(typ):
attrs = []
for attr in dir(typ):
if attr in CPYTHON_INTERNAL_ATTRS:
continue
# Skip attributes in dir() but not actually accessible (e.g., descriptor that raises)
if not hasattr(typ, attr):
continue
if attr_is_not_inherited(typ, attr):
attrs.append((attr, extra_info(getattr(typ, attr))))
return attrs
def is_native_class(obj):
# A class statement records __firstlineno__. A class made by calling
# type(), like a namedtuple, still has Python functions.
if not isinstance(obj, type) or "__firstlineno__" in obj.__dict__:
return False
for value in obj.__dict__.values():
if isinstance(value, (classmethod, staticmethod)):
value = value.__func__
if isinstance(value, property):
value = value.fget
if isinstance(value, types.FunctionType):
return False
return True
def gen_methods():
objects = [t.__name__ for t in BUILTIN_TYPES]
objects.append("type(None)")
iters = [
"type(bytearray().__iter__())",
"type(bytes().__iter__())",
"type(dict().__iter__())",
"type(dict().values().__iter__())",
"type(dict().items().__iter__())",
"type(dict().values())",
"type(dict().items())",
"type(set().__iter__())",
"type(list().__iter__())",
"type(range(0).__iter__())",
"type(str().__iter__())",
"type(tuple().__iter__())",
"type(memoryview(bytearray(b'0')).__iter__())",
]
methods = {}
for typ_code in objects + iters:
typ = eval(typ_code)
methods[typ.__name__] = (typ_code, extra_info(typ), own_attrs(typ))
output = "expected_methods = {\n"
for name in sorted(methods.keys(), key=name_sort_key):
typ_code, extra, attrs = methods[name]
output += f" '{name}': ({typ_code}, {extra!r}, [\n"
for attr, attr_extra in attrs:
output += f" ({attr!r}, {attr_extra!r}),\n"
output += " ]),\n"
if typ_code != objects[-1]:
output += "\n"
output += "}\n\n"
return output
def scan_modules():
"""taken from the source code of help('modules')
https://github.com/python/cpython/blob/63298930fb531ba2bb4f23bc3b915dbf1e17e9e1/Lib/pydoc.py#L2178"""
modules = {}
def callback(path, modname, desc, modules=modules):
if modname and modname[-9:] == ".__init__":
modname = modname[:-9] + " (package)"
if modname.find(".") < 0:
modules[modname] = 1
def onerror(modname):
callback(None, modname, None)
with warnings.catch_warnings():
# ignore warnings from importing deprecated modules
warnings.simplefilter("ignore")
ModuleScanner().run(callback, onerror=onerror)
return list(modules.keys())
def import_module(module_name):
import io
from contextlib import redirect_stdout
# Importing modules causes ('Constant String', 2, None, 4) and
# "Hello world!" to be printed to stdout.
f = io.StringIO()
with warnings.catch_warnings(), redirect_stdout(f):
# ignore warnings caused by importing deprecated modules
warnings.filterwarnings("ignore", category=DeprecationWarning)
try:
module = __import__(module_name)
except Exception as e:
return e
return module
def is_child(module, item):
import inspect
item_mod = inspect.getmodule(item)
return item_mod is module
def dir_of_mod_or_error(module_name, keep_other=True):
module = import_module(module_name)
if isinstance(module, Exception):
return module
item_names = sorted(set(dir(module)))
result = {}
for item_name in item_names:
# eval() adds __builtins__ to its globals, and inspect.signature() evals
# defaults in the module's namespace, so whether it exists depends on
# what was inspected before.
if item_name == "__builtins__":
continue
if item_name == "__doc__":
# extra_info() reports a docstring for a callable only, and a module
# is not one. getdoc() matches how a callable's is normalized.
result[item_name] = {"sig": None, "doc": inspect.getdoc(module)}
continue
item = getattr(module, item_name)
# don't repeat items imported from other modules
if keep_other or is_child(module, item) or inspect.getmodule(item) is None:
result[item_name] = extra_info(item)
return result
def _is_decimal_fallback_item(item):
"""Items that belong to the pure decimal module, not an imported helper."""
if inspect.ismodule(item):
return False
declared = getattr(item, "__module__", None)
if declared in {"decimal", "_pydecimal"}:
return True
return declared is None and inspect.getmodule(item) is None
def _expand_class_members(dir_result, module):
for item_name in list(dir_result):
if "." in item_name:
continue
item = getattr(module, item_name)
if (
isinstance(item, type)
and item not in BUILTIN_TYPES
and getattr(item, "__name__", None) == item_name
):
for attr, info in own_attrs(item):
dir_result[f"{item_name}.{attr}"] = info
def _pure_decimal_baseline(wrapper):
"""Signatures and docs from `_pydecimal`, with the wrapper module docstring."""
import _pydecimal
result = {}
for item_name in sorted(set(dir(_pydecimal))):
if item_name == "__builtins__":
continue
if item_name == "__doc__":
result[item_name] = {"sig": None, "doc": inspect.getdoc(wrapper)}
continue
item = getattr(_pydecimal, item_name)
if not _is_decimal_fallback_item(item):
continue
result[item_name] = extra_info(item)
_expand_class_members(result, _pydecimal)
return result
def gen_modules():
# check name because modules listed have side effects on import,
# e.g. printing something or opening a webpage
modules = {}
for mod_name in sorted(scan_modules(), key=name_sort_key):
if mod_name in IGNORED_MODULES:
continue
# decimal's accelerated types are not the implementation under test.
if mod_name == "decimal":
module = import_module(mod_name)
if isinstance(module, Exception):
print(
f"!!! {mod_name} skipped because {type(module).__name__}: {str(module)}",
file=sys.stderr,
)
continue
modules[mod_name] = _pure_decimal_baseline(module)
continue
# when generating CPython list, ignore items defined by other modules
dir_result = dir_of_mod_or_error(mod_name, keep_other=False)
if isinstance(dir_result, Exception):
print(
f"!!! {mod_name} skipped because {type(dir_result).__name__}: {str(dir_result)}",
file=sys.stderr,
)
continue
module = import_module(mod_name)
for item_name in list(dir_result):
item = getattr(module, item_name)
# an alias such as array.ArrayType is scanned under its real name
if (
is_native_class(item)
and item not in BUILTIN_TYPES
and item.__name__ == item_name
):
for attr, info in own_attrs(item):
dir_result[f"{item_name}.{attr}"] = info
modules[mod_name] = dir_result
return modules
output = """\
# WARNING: THIS IS AN AUTOMATICALLY GENERATED FILE
# EDIT extra_tests/not_impl_gen.sh, NOT THIS FILE.
# RESULTS OF THIS TEST DEPEND ON THE CPYTHON
# VERSION AND PYTHON ENVIRONMENT USED
# TO RUN not_impl_mods_gen.py
"""
output += gen_methods()
output += f"""
cpymods = {gen_modules()!r}
libdir = {os.path.abspath("Lib/").encode("utf8")!r}
"""
# Copy the source code of functions we will reuse in the generated script
REUSED = [
attr_is_not_inherited,
extra_info,
dir_of_mod_or_error,
import_module,
is_child,
]
for fn in REUSED:
output += "".join(inspect.getsourcelines(fn)[0]) + "\n\n"
# Prevent missing variable linter errors from compare()
expected_methods = {}
cpymods = {}
libdir = ""
# This function holds the source code that will be run under RustPython
def compare():
import inspect
import io
import json
import os
import platform
import re
import sys
import warnings
from contextlib import redirect_stdout
def sig_mismatch(rustpy_sig, cpython_sig):
# A signature CPython cannot produce is never a mismatch; having one is fine.
if cpython_sig is None or cpython_sig.startswith("ValueError("):
return False
return rustpy_sig != cpython_sig
def method_incompatibility_reason(typ, method_name, real_method_value):
has_method = hasattr(typ, method_name)
if not has_method:
return ""
is_inherited = not attr_is_not_inherited(typ, method_name)
# Inheriting something that reads the same as CPython's own member
# leaves nothing to implement.
if is_inherited and extra_info(getattr(typ, method_name)) != real_method_value:
return "(inherited)"
return None
not_implementeds = {}
mismatched_methods = {}
mismatched_method_docs = {}
for name, (typ, real_value, methods) in expected_methods.items():
missing_methods = {}
for method, real_method_value in methods:
reason = method_incompatibility_reason(typ, method, real_method_value)
if reason is not None:
missing_methods[method] = reason
continue
# A method that exists but differs is a mismatch, not a missing one.
value = extra_info(getattr(typ, method))
item = f"{name}.{method}"
if sig_mismatch(value["sig"], real_method_value["sig"]):
mismatched_methods.setdefault(name, []).append(
(item, value["sig"], real_method_value["sig"])
)
if value["doc"] != real_method_value["doc"]:
mismatched_method_docs.setdefault(name, []).append(
(item, value["doc"], real_method_value["doc"])
)
if missing_methods:
not_implementeds[name] = missing_methods
if platform.python_implementation() == "CPython":
if not_implementeds:
sys.exit(
f"ERROR: CPython should have all the methods but missing: {not_implementeds}"
)
mod_names = [
name.decode()
for name, ext in map(os.path.splitext, os.listdir(libdir))
if ext == b".py" or os.path.isdir(os.path.join(libdir, name))
]
mod_names += list(sys.builtin_module_names)
# Remove easter egg modules
mod_names = sorted(set(mod_names) - {"this", "antigravity"})
rustpymods = {mod: dir_of_mod_or_error(mod) for mod in mod_names}
result = {
"cpython_modules": {},
"implemented": {},
"not_implemented": {},
"failed_to_import": {},
"missing_items": {},
# builtin types first, keyed by type name, then modules
"mismatched_items": mismatched_methods,
"mismatched_doc_items": mismatched_method_docs,
}
for modname, cpymod in cpymods.items():
rustpymod = rustpymods.get(modname)
if rustpymod is None:
result["not_implemented"][modname] = None
elif isinstance(rustpymod, Exception):
result["failed_to_import"][modname] = (
f"{rustpymod.__class__.__name__}: {rustpymod}"
)
else:
module = import_module(modname)
skipped = set()
inherited = set()
for item in cpymod:
cls_name, dot, attr = item.partition(".")
if not dot:
continue
cls = getattr(module, cls_name, None)
if not isinstance(cls, type):
# The class line already reports a missing class or one
# implemented as something else.
skipped.add(item)
elif not hasattr(cls, attr):
continue
elif attr_is_not_inherited(cls, attr):
rustpymod[item] = extra_info(getattr(cls, attr))
elif extra_info(getattr(cls, attr)) == cpymod[item]:
rustpymod[item] = cpymod[item]
else:
inherited.add(item)
implemented_items = sorted(set(cpymod) & set(rustpymod))
mod_missing_items = set(cpymod) - set(rustpymod) - skipped
mod_missing_items = sorted(
f"{modname}.{item}" + (" (inherited)" if item in inherited else "")
for item in mod_missing_items
)
mod_mismatched_items = [
(f"{modname}.{item}", rustpymod[item]["sig"], cpymod[item]["sig"])
for item in implemented_items
if sig_mismatch(rustpymod[item]["sig"], cpymod[item]["sig"])
]
mod_mismatched_doc_items = [
(f"{modname}.{item}", rustpymod[item]["doc"], cpymod[item]["doc"])
for item in implemented_items
if rustpymod[item]["doc"] != cpymod[item]["doc"]
]
if mod_missing_items:
result["missing_items"][modname] = mod_missing_items
if mod_mismatched_items:
result["mismatched_items"][modname] = mod_mismatched_items
if mod_mismatched_doc_items:
result["mismatched_doc_items"][modname] = mod_mismatched_doc_items
if not (mod_missing_items or mod_mismatched_items):
result["implemented"][modname] = None
result["cpython_modules"] = cpymods
result["not_implementeds"] = not_implementeds
print(json.dumps(result))
def remove_one_indent(s):
indent = " "
return s[len(indent) :] if s.startswith(indent) else s
compare_src = inspect.getsourcelines(compare)[0][1:]
output += "".join(remove_one_indent(line) for line in compare_src)
with open(GENERATED_FILE, "w", encoding="utf-8") as f:
f.write(output + "\n")
def resolve_rustpython(path):
if os.path.isfile(path):
return path
if os.name == "nt" and not path.lower().endswith(".exe"):
exe = path + ".exe"
if os.path.isfile(exe):
return exe
sys.exit(f"RustPython executable not found: {path}")
if args.rustpython:
rustpython_run_command = [resolve_rustpython(args.rustpython), GENERATED_FILE]
else:
cargo_build_command = ["cargo", "build", "--release"]
if args.no_default_features:
cargo_build_command.append("--no-default-features")
joined_features = ",".join(args.features)
if args.features:
cargo_build_command.extend(["--features", joined_features])
subprocess.run(cargo_build_command, check=True)
rustpython_run_command = ["cargo", "run", "--release"]
if args.no_default_features:
rustpython_run_command.append("--no-default-features")
if args.features:
rustpython_run_command.extend(["--features", joined_features])
rustpython_run_command.extend(["-q", "--", GENERATED_FILE])
result = subprocess.run(
rustpython_run_command,
env={**os.environ.copy(), "RUSTPYTHONPATH": "Lib"},
text=True,
capture_output=True,
)
# The last line should be json output, the rest of the lines can contain noise
# because importing certain modules can print stuff to stdout/stderr
print(result.stderr, file=sys.stderr)
result = json.loads(result.stdout.splitlines()[-1])
if args.json:
print(json.dumps(result))
sys.exit()
# missing entire modules
print("# modules")
for modname in result["not_implemented"]:
print(modname, "(entire module)")
for modname, exception in result["failed_to_import"].items():
print(f"{modname} (exists but not importable: {exception})")
# missing from builtins
print("\n# builtin items")
for module, missing_methods in result["not_implementeds"].items():
for method, reason in missing_methods.items():
print(f"{module}.{method}" + (f" {reason}" if reason else ""))
# missing from modules
print("\n# stdlib items")
for modname, missing in result["missing_items"].items():
for item in missing:
print(item)
if args.signature:
print("\n# mismatching signatures (warnings)")
for modname, mismatched in result["mismatched_items"].items():
for i, (item, rustpy_value, cpython_value) in enumerate(mismatched):
if rustpy_value is None or rustpy_value.startswith("ValueError("):
rustpy_value = f" {rustpy_value}"
print(f"{item}{rustpy_value}")
if cpython_value is None:
cpython_value = f" {cpython_value}"
print(f"{' ' * len(item)}{cpython_value}")
if i < len(mismatched) - 1:
print()
if args.doc:
print("\n# mismatching `__doc__`s (warnings)")
for modname, mismatched in result["mismatched_doc_items"].items():
for item, rustpy_doc, cpython_doc in mismatched:
print(f"{item} {repr(rustpy_doc)} != {repr(cpython_doc)}")
print()
print("# summary")
for error_type, modules in result.items():
print("# ", error_type, len(modules))