FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Compare native signatures and docstrings in whats_left.py (#8751) · RustPython/RustPython@e189ca7 · GitHub

Repository navigation

Commit e189ca7

Browse files
authored
Compare native signatures and docstrings in whats_left.py (#8751)
* 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 cc2c46b (#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
1 parent 27647e1 commit e189ca7

1 file changed

Lines changed: 144 additions & 60 deletions

File tree

‎scripts/whats_left.py‎

Lines changed: 144 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
# This script generates Lib/snippets/whats_left_data.py with these variables defined:
77
# expected_methods - a dictionary mapping builtin objects to their methods
8-
# cpymods - a dictionary mapping module names to their contents
8+
# cpymods - a dictionary mapping module names to their contents, including
9+
# the members of native classes as "Class.member"
910
# libdir - the location of RustPython's Lib/ directory.
1011

1112
#
@@ -24,6 +25,7 @@
2425
import re
2526
import subprocess
2627
import sys
28+
import types
2729
import warnings
2830
from pydoc import ModuleScanner
2931

@@ -109,12 +111,11 @@ def attr_is_not_inherited(type_, attr):
109111
"""
110112
returns True if type_'s attr is not inherited from any of its base classes
111113
"""
112-
bases = type_.__mro__[1:]
113-
return getattr(type_, attr) not in (getattr(base, attr, None) for base in bases)
114+
return attr in type_.__dict__
114115

115116

116117
def extra_info(obj):
117-
if callable(obj) and not inspect._signature_is_builtin(obj):
118+
if callable(obj):
118119
doc = inspect.getdoc(obj)
119120
try:
120121
sig = str(inspect.signature(obj))
@@ -151,36 +152,70 @@ def name_sort_key(name):
151152
return name + "2"
152153

153154

155+
BUILTIN_TYPES = [
156+
bool,
157+
bytearray,
158+
bytes,
159+
complex,
160+
dict,
161+
enumerate,
162+
filter,
163+
float,
164+
frozenset,
165+
int,
166+
list,
167+
map,
168+
memoryview,
169+
range,
170+
set,
171+
slice,
172+
str,
173+
super,
174+
tuple,
175+
object,
176+
zip,
177+
classmethod,
178+
staticmethod,
179+
property,
180+
Exception,
181+
BaseException,
182+
]
183+
184+
185+
# CPython's per-class annotations cache and its vectorcall C API offset
186+
CPYTHON_INTERNAL_ATTRS = {"__annotations_cache__", "__vectorcalloffset__"}
187+
188+
189+
def own_attrs(typ):
190+
attrs = []
191+
for attr in dir(typ):
192+
if attr in CPYTHON_INTERNAL_ATTRS:
193+
continue
194+
# Skip attributes in dir() but not actually accessible (e.g., descriptor that raises)
195+
if not hasattr(typ, attr):
196+
continue
197+
if attr_is_not_inherited(typ, attr):
198+
attrs.append((attr, extra_info(getattr(typ, attr))))
199+
return attrs
200+
201+
202+
def is_native_class(obj):
203+
# A class statement records __firstlineno__. A class made by calling
204+
# type(), like a namedtuple, still has Python functions.
205+
if not isinstance(obj, type) or "__firstlineno__" in obj.__dict__:
206+
return False
207+
for value in obj.__dict__.values():
208+
if isinstance(value, (classmethod, staticmethod)):
209+
value = value.__func__
210+
if isinstance(value, property):
211+
value = value.fget
212+
if isinstance(value, types.FunctionType):
213+
return False
214+
return True
215+
216+
154217
def gen_methods():
155-
types = [
156-
bool,
157-
bytearray,
158-
bytes,
159-
complex,
160-
dict,
161-
enumerate,
162-
filter,
163-
float,
164-
frozenset,
165-
int,
166-
list,
167-
map,
168-
memoryview,
169-
range,
170-
set,
171-
slice,
172-
str,
173-
super,
174-
tuple,
175-
object,
176-
zip,
177-
classmethod,
178-
staticmethod,
179-
property,
180-
Exception,
181-
BaseException,
182-
]
183-
objects = [t.__name__ for t in types]
218+
objects = [t.__name__ for t in BUILTIN_TYPES]
184219
objects.append("type(None)")
185220

186221
iters = [
@@ -202,14 +237,7 @@ def gen_methods():
202237
methods = {}
203238
for typ_code in objects + iters:
204239
typ = eval(typ_code)
205-
attrs = []
206-
for attr in dir(typ):
207-
# Skip attributes in dir() but not actually accessible (e.g., descriptor that raises)
208-
if not hasattr(typ, attr):
209-
continue
210-
if attr_is_not_inherited(typ, attr):
211-
attrs.append((attr, extra_info(getattr(typ, attr))))
212-
methods[typ.__name__] = (typ_code, extra_info(typ), attrs)
240+
methods[typ.__name__] = (typ_code, extra_info(typ), own_attrs(typ))
213241

214242
output = "expected_methods = {\n"
215243
for name in sorted(methods.keys(), key=name_sort_key):
@@ -272,9 +300,21 @@ def is_child(module, item):
272300

273301
def dir_of_mod_or_error(module_name, keep_other=True):
274302
module = import_module(module_name)
303+
if isinstance(module, Exception):
304+
return module
275305
item_names = sorted(set(dir(module)))
276306
result = {}
277307
for item_name in item_names:
308+
# eval() adds __builtins__ to its globals, and inspect.signature() evals
309+
# defaults in the module's namespace, so whether it exists depends on
310+
# what was inspected before.
311+
if item_name == "__builtins__":
312+
continue
313+
if item_name == "__doc__":
314+
# extra_info() reports a docstring for a callable only, and a module
315+
# is not one. getdoc() matches how a callable's is normalized.
316+
result[item_name] = {"sig": None, "doc": inspect.getdoc(module)}
317+
continue
278318
item = getattr(module, item_name)
279319
# don't repeat items imported from other modules
280320
if keep_other or is_child(module, item) or inspect.getmodule(item) is None:
@@ -297,6 +337,17 @@ def gen_modules():
297337
file=sys.stderr,
298338
)
299339
continue
340+
module = import_module(mod_name)
341+
for item_name in list(dir_result):
342+
item = getattr(module, item_name)
343+
# an alias such as array.ArrayType is scanned under its real name
344+
if (
345+
is_native_class(item)
346+
and item not in BUILTIN_TYPES
347+
and item.__name__ == item_name
348+
):
349+
for attr, info in own_attrs(item):
350+
dir_result[f"{item_name}.{attr}"] = info
300351
modules[mod_name] = dir_result
301352
return modules
302353

@@ -352,22 +403,34 @@ def method_incompatibility_reason(typ, method_name, real_method_value):
352403
return ""
353404

354405
is_inherited = not attr_is_not_inherited(typ, method_name)
355-
if is_inherited:
406+
# Inheriting something that reads the same as CPython's own member
407+
# leaves nothing to implement.
408+
if is_inherited and extra_info(getattr(typ, method_name)) != real_method_value:
356409
return "(inherited)"
357410

358-
value = extra_info(getattr(typ, method_name))
359-
if value != real_method_value:
360-
return f"{value} != {real_method_value}"
361-
362411
return None
363412

364413
not_implementeds = {}
414+
mismatched_methods = {}
415+
mismatched_method_docs = {}
365416
for name, (typ, real_value, methods) in expected_methods.items():
366417
missing_methods = {}
367418
for method, real_method_value in methods:
368419
reason = method_incompatibility_reason(typ, method, real_method_value)
369420
if reason is not None:
370421
missing_methods[method] = reason
422+
continue
423+
# A method that exists but differs is a mismatch, not a missing one.
424+
value = extra_info(getattr(typ, method))
425+
item = f"{name}.{method}"
426+
if value["sig"] != real_method_value["sig"]:
427+
mismatched_methods.setdefault(name, []).append(
428+
(item, value["sig"], real_method_value["sig"])
429+
)
430+
if value["doc"] != real_method_value["doc"]:
431+
mismatched_method_docs.setdefault(name, []).append(
432+
(item, value["doc"], real_method_value["doc"])
433+
)
371434
if missing_methods:
372435
not_implementeds[name] = missing_methods
373436

@@ -394,22 +457,44 @@ def method_incompatibility_reason(typ, method_name, real_method_value):
394457
"not_implemented": {},
395458
"failed_to_import": {},
396459
"missing_items": {},
397-
"mismatched_items": {},
398-
"mismatched_doc_items": {},
460+
# builtin types first, keyed by type name, then modules
461+
"mismatched_items": mismatched_methods,
462+
"mismatched_doc_items": mismatched_method_docs,
399463
}
400464
for modname, cpymod in cpymods.items():
401465
rustpymod = rustpymods.get(modname)
402466
if rustpymod is None:
403467
result["not_implemented"][modname] = None
404468
elif isinstance(rustpymod, Exception):
405-
result["failed_to_import"][modname] = rustpymod.__class__.__name__ + str(
406-
rustpymod
469+
result["failed_to_import"][modname] = (
470+
f"{rustpymod.__class__.__name__}: {rustpymod}"
407471
)
408472
else:
473+
module = import_module(modname)
474+
skipped = set()
475+
inherited = set()
476+
for item in cpymod:
477+
cls_name, dot, attr = item.partition(".")
478+
if not dot:
479+
continue
480+
cls = getattr(module, cls_name, None)
481+
if not isinstance(cls, type):
482+
# The class line already reports a missing class or one
483+
# implemented as something else.
484+
skipped.add(item)
485+
elif not hasattr(cls, attr):
486+
continue
487+
elif attr_is_not_inherited(cls, attr):
488+
rustpymod[item] = extra_info(getattr(cls, attr))
489+
elif extra_info(getattr(cls, attr)) == cpymod[item]:
490+
rustpymod[item] = cpymod[item]
491+
else:
492+
inherited.add(item)
409493
implemented_items = sorted(set(cpymod) & set(rustpymod))
410-
mod_missing_items = set(cpymod) - set(rustpymod)
494+
mod_missing_items = set(cpymod) - set(rustpymod) - skipped
411495
mod_missing_items = sorted(
412-
f"{modname}.{item}" for item in mod_missing_items
496+
f"{modname}.{item}" + (" (inherited)" if item in inherited else "")
497+
for item in mod_missing_items
413498
)
414499
mod_mismatched_items = [
415500
(f"{modname}.{item}", rustpymod[item]["sig"], cpymod[item]["sig"])
@@ -422,14 +507,13 @@ def method_incompatibility_reason(typ, method_name, real_method_value):
422507
for item in implemented_items
423508
if rustpymod[item]["doc"] != cpymod[item]["doc"]
424509
]
425-
if mod_missing_items or mod_mismatched_items:
426-
if mod_missing_items:
427-
result["missing_items"][modname] = mod_missing_items
428-
if mod_mismatched_items:
429-
result["mismatched_items"][modname] = mod_mismatched_items
430-
if mod_mismatched_doc_items:
431-
result["mismatched_doc_items"][modname] = mod_mismatched_doc_items
432-
else:
510+
if mod_missing_items:
511+
result["missing_items"][modname] = mod_missing_items
512+
if mod_mismatched_items:
513+
result["mismatched_items"][modname] = mod_mismatched_items
514+
if mod_mismatched_doc_items:
515+
result["mismatched_doc_items"][modname] = mod_mismatched_doc_items
516+
if not (mod_missing_items or mod_mismatched_items):
433517
result["implemented"][modname] = None
434518

435519
result["cpython_modules"] = cpymods

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL