mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 10:44:36 +08:00
* Record a module's docstring mismatches on their own
They were only kept when the module also had missing items or signature
mismatches, so annotationlib, base64, ntpath and _ctypes reported none.
Whether a module counts as implemented still depends on missing items
and signatures alone.
Assisted-by: Claude Code:claude-opus-5
* Report a builtin type method that differs as a mismatch
A method whose signature or docstring differed was listed under
"builtin items" as if it were missing, with both extra_info dicts as the
reason. That section feeds the website's builtin_items.csv. Differences
now go to mismatched_items and mismatched_doc_items, keyed by type name,
like module items. Nothing reaches this path yet because builtin
signatures are still skipped; without this change, lifting that skip
turns the one "builtin items" line into 455.
Assisted-by: Claude Code:claude-opus-5
* Compare signatures and docstrings of native callables
extra_info() skipped anything inspect treats as builtin since cc2c46b1b
(#2410), when RustPython had no __text_signature__ for builtins. It has
one now, so --signature and --doc cover functions and methods written in
Rust. Modules with a mismatched signature go from 45 to 134, and 160 of
the 710 mismatched items are ones CPython itself cannot parse, which the
--signature listing already skips.
__builtins__ is no longer compared. inspect.signature() evals defaults in
the owning module's namespace and eval() adds __builtins__ there, so
CPython's builtins module gained one after list.index was inspected and
it showed up as missing.
Assisted-by: Claude Code:claude-opus-5
* Scan the members of native classes in each module
A module was only compared one level deep, so a native class counted as
present whatever its methods were. Classes a module owns and that are
not Python-defined are now opened too, and their own members are
compared as "Class.member": 438 classes and 4,833 members, 1,033 of
which have a signature --signature reports.
A class is native when it has no __firstlineno__, which a class
statement records, and no Python functions of its own, which rules out
namedtuple-style classes made by type(). BUILTIN_TYPES moves to module
level so the builtin types are not reported twice, an alias like
array.ArrayType is skipped, and CPython's __annotations_cache__ and
__vectorcalloffset__ are left out.
RustPython follows the CPython list instead of deciding again. A member
only inherited is listed as missing with "(inherited)", as builtin type
methods are. A class missing here, or implemented as something else such
as _sha3's hash functions, is reported by its own line only.
Assisted-by: Claude Code:claude-opus-5
* Report a module that exists but cannot be imported
dir_of_mod_or_error() ran dir() on the exception import_module() returns,
so a failed import looked like a module whose every item is missing:
sqlite3 alone filled 198 of those lines, and CPython's own _ios_support
was compared as a module holding ImportError's attributes. Returning the
exception reaches the "exists but not importable" line that was already
written for it, and drops 529 lines from "stdlib items".
Assisted-by: Claude Code:claude-opus-5
* Compare a module's own docstring
extra_info() reports a docstring for callables, and a module is not one,
so the __doc__ item both sides record was None and a module docstring
never counted as a mismatch. Reading it with getdoc(), the way a
callable's is read, brings 221 module docstrings into the comparison.
sys is the one that differs; base64 and imaplib differ here only
because their Lib copies track 3.14.7 while this run used 3.14.6.
Non-callables keep reporting None. getdoc() on a plain value answers
with its type's docstring, which would turn every string constant into
a mismatch.
Assisted-by: Claude Code:claude-opus-5
* Decide a member's ownership by the class dictionary
attr_is_not_inherited() compared the attribute's value with each base's.
That took an inherited classmethod for the class's own, since a bound
method compares unequal across classes, and dropped an own attribute
whose value matched a base's default None: 394 of CPython's in all,
ast.FunctionDef.returns among them. On the RustPython side it called
AttributeError.name, a class attribute set to None, inherited.
Ownership now means the class dictionary holds the name. An inherited
member is reported only when what it resolves to differs from CPython's
own, so dict_items.__doc__, a slot CPython fills with None that
RustPython resolves to None without one, stays quiet. "(inherited)"
lines drop from 196 to 54, and ast.alias.end_lineno is among the real
gaps that appear.
Assisted-by: Claude Code:claude-fable-5-1
630 lines
20 KiB
Python
Executable File
630 lines
20 KiB
Python
Executable File
#!/usr/bin/env -S python3 -I
|
|
# /// script
|
|
# requires-python = ">=3.14"
|
|
# ///
|
|
|
|
# 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, 14):
|
|
sys.exit(
|
|
f"whats_left.py must be run under CPython 3.14 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):
|
|
if callable(obj):
|
|
doc = inspect.getdoc(obj)
|
|
try:
|
|
sig = str(inspect.signature(obj))
|
|
# remove function memory addresses
|
|
return {
|
|
"sig": re.sub(" at 0x[0-9A-Fa-f]+", " at 0xdeadbeef", sig),
|
|
"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 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
|
|
# 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 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 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 rustpymod[item]["sig"] != cpymod[item]["sig"]
|
|
and not isinstance(cpymod[item]["sig"], Exception)
|
|
]
|
|
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 cpython_value and cpython_value.startswith("ValueError("):
|
|
continue # these items will never match
|
|
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))
|