Skip to content

Commit 536737f

Browse files
authored
Stabilize whats_left signature defaults and weakref repr (#8911)
Mask sys.executable, sys.version_info, the install scheme, and set order in signatures. Compare decimal and zoneinfo with the pure-Python fallback. Print the referent type's module in weakref repr. Assisted-by: Grok:grok-4.7
1 parent 74019cf commit 536737f

2 files changed

Lines changed: 149 additions & 14 deletions

File tree

‎crates/vm/src/builtins/weakref.rs‎

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -150,20 +150,36 @@ impl Comparable for PyWeak {
150150
}
151151
}
152152

153+
/// `__name__` when that lookup yields a string. A missing name or a non-string
154+
/// value is left off the repr.
155+
fn instance_name(obj: &PyObject, vm: &VirtualMachine) -> PyResult<Option<String>> {
156+
let found =
157+
crate::vm::PyMethod::get_special_ex::<false>(obj, identifier!(vm, __name__), vm, true)?;
158+
let Some(crate::vm::PyMethod::Attribute(attr)) = found else {
159+
return Ok(None);
160+
};
161+
Ok(attr
162+
.downcast_ref::<crate::builtins::PyStr>()
163+
.and_then(|name| name.to_str())
164+
.map(str::to_owned))
165+
}
166+
153167
impl Representable for PyWeak {
154168
#[inline]
155-
fn repr_str(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
169+
fn repr_str(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<String> {
156170
let id = zelf.get_id();
157-
Ok(if let Some(o) = zelf.upgrade() {
158-
format!(
159-
"<weakref at {:#x}; to '{}' at {:#x}>",
160-
id,
161-
o.class().name(),
162-
o.get_id(),
163-
)
164-
} else {
165-
format!("<weakref at {id:#x}; dead>")
166-
})
171+
let Some(obj) = zelf.upgrade() else {
172+
return Ok(format!("<weakref at {id:#x}; dead>"));
173+
};
174+
let type_name = obj.class().fully_qualified_name(vm)?;
175+
let obj_id = obj.get_id();
176+
let suffix = match instance_name(&obj, vm)? {
177+
Some(name) => format!(" ({name})"),
178+
None => String::new(),
179+
};
180+
Ok(format!(
181+
"<weakref at {id:#x}; to '{type_name}' at {obj_id:#x}{suffix}>"
182+
))
167183
}
168184
}
169185

‎scripts/whats_left.py‎

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,62 @@ def attr_is_not_inherited(type_, attr):
115115

116116

117117
def extra_info(obj):
118+
import sys
119+
120+
class _OrderedSet:
121+
"""Set repr with sorted elements, so hash order does not change it."""
122+
123+
def __init__(self, items, braces):
124+
self.items = items
125+
self.braces = braces
126+
127+
def __repr__(self):
128+
if not self.items:
129+
return "set()" if self.braces else "frozenset()"
130+
body = ", ".join(sorted(repr(item) for item in self.items))
131+
if self.braces:
132+
return "{" + body + "}"
133+
return "frozenset({" + body + "})"
134+
135+
def canon_default(param):
136+
default = param.default
137+
# The interpreter path, version tuple, and install scheme differ per process.
138+
if isinstance(default, str) and default == sys.executable:
139+
return "<sys.executable>"
140+
if default is sys.version_info:
141+
return "<sys.version_info>"
142+
if param.name == "scheme" and isinstance(default, str):
143+
import sysconfig
144+
145+
if default == sysconfig.get_default_scheme():
146+
return "<sysconfig.get_default_scheme()>"
147+
if isinstance(default, frozenset):
148+
return _OrderedSet(default, False)
149+
if isinstance(default, set):
150+
return _OrderedSet(default, True)
151+
return default
152+
153+
def stable_signature(target):
154+
sig = inspect.signature(target)
155+
params = []
156+
changed = False
157+
for param in sig.parameters.values():
158+
if param.default is not inspect.Parameter.empty:
159+
default = canon_default(param)
160+
if default is not param.default:
161+
param = param.replace(default=default)
162+
changed = True
163+
params.append(param)
164+
if changed:
165+
sig = sig.replace(parameters=params)
166+
# remove function memory addresses
167+
return re.sub(r" at 0x[0-9A-Fa-f]+", " at 0xdeadbeef", str(sig))
168+
118169
if callable(obj):
119170
doc = inspect.getdoc(obj)
120171
try:
121-
sig = str(inspect.signature(obj))
122-
# remove function memory addresses
123172
return {
124-
"sig": re.sub(" at 0x[0-9A-Fa-f]+", " at 0xdeadbeef", sig),
173+
"sig": stable_signature(obj),
125174
"doc": doc,
126175
}
127176
except Exception as e:
@@ -322,13 +371,81 @@ def dir_of_mod_or_error(module_name, keep_other=True):
322371
return result
323372

324373

374+
def _is_decimal_fallback_item(item):
375+
"""Items that belong to the pure decimal module, not an imported helper."""
376+
if inspect.ismodule(item):
377+
return False
378+
declared = getattr(item, "__module__", None)
379+
if declared in {"decimal", "_pydecimal"}:
380+
return True
381+
return declared is None and inspect.getmodule(item) is None
382+
383+
384+
def _expand_class_members(dir_result, module):
385+
for item_name in list(dir_result):
386+
if "." in item_name:
387+
continue
388+
item = getattr(module, item_name)
389+
if (
390+
isinstance(item, type)
391+
and item not in BUILTIN_TYPES
392+
and getattr(item, "__name__", None) == item_name
393+
):
394+
for attr, info in own_attrs(item):
395+
dir_result[f"{item_name}.{attr}"] = info
396+
397+
398+
def _pure_decimal_baseline(wrapper):
399+
"""Signatures and docs from `_pydecimal`, with the wrapper module docstring."""
400+
import _pydecimal
401+
402+
result = {}
403+
for item_name in sorted(set(dir(_pydecimal))):
404+
if item_name == "__builtins__":
405+
continue
406+
if item_name == "__doc__":
407+
result[item_name] = {"sig": None, "doc": inspect.getdoc(wrapper)}
408+
continue
409+
item = getattr(_pydecimal, item_name)
410+
if not _is_decimal_fallback_item(item):
411+
continue
412+
result[item_name] = extra_info(item)
413+
_expand_class_members(result, _pydecimal)
414+
return result
415+
416+
417+
def _use_pure_zoneinfo(dir_result):
418+
"""Replace the accelerated ZoneInfo with the pure-Python class."""
419+
import zoneinfo._zoneinfo as pure_zoneinfo
420+
421+
cls = pure_zoneinfo.ZoneInfo
422+
name = cls.__name__
423+
for key in list(dir_result):
424+
if key == name or key.startswith(f"{name}."):
425+
del dir_result[key]
426+
dir_result[name] = extra_info(cls)
427+
for attr, info in own_attrs(cls):
428+
dir_result[f"{name}.{attr}"] = info
429+
430+
325431
def gen_modules():
326432
# check name because modules listed have side effects on import,
327433
# e.g. printing something or opening a webpage
328434
modules = {}
329435
for mod_name in sorted(scan_modules(), key=name_sort_key):
330436
if mod_name in IGNORED_MODULES:
331437
continue
438+
# decimal's accelerated types are not the implementation under test.
439+
if mod_name == "decimal":
440+
module = import_module(mod_name)
441+
if isinstance(module, Exception):
442+
print(
443+
f"!!! {mod_name} skipped because {type(module).__name__}: {str(module)}",
444+
file=sys.stderr,
445+
)
446+
continue
447+
modules[mod_name] = _pure_decimal_baseline(module)
448+
continue
332449
# when generating CPython list, ignore items defined by other modules
333450
dir_result = dir_of_mod_or_error(mod_name, keep_other=False)
334451
if isinstance(dir_result, Exception):
@@ -348,6 +465,8 @@ def gen_modules():
348465
):
349466
for attr, info in own_attrs(item):
350467
dir_result[f"{item_name}.{attr}"] = info
468+
if mod_name == "zoneinfo":
469+
_use_pure_zoneinfo(dir_result)
351470
modules[mod_name] = dir_result
352471
return modules
353472

0 commit comments

Comments
 (0)