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
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-09-30 20:10:19 +09:00
1 parent 74019cf627
commit 536737f4a0
2 files changed
+149 -14

No files matched your search

+27 -11
View File
@@ -150,20 +150,36 @@ impl Comparable for PyWeak {
}
}
/// `__name__` when that lookup yields a string. A missing name or a non-string
/// value is left off the repr.
fn instance_name(obj: &PyObject, vm: &VirtualMachine) -> PyResult<Option<String>> {
let found =
crate::vm::PyMethod::get_special_ex::<false>(obj, identifier!(vm, __name__), vm, true)?;
let Some(crate::vm::PyMethod::Attribute(attr)) = found else {
return Ok(None);
};
Ok(attr
.downcast_ref::<crate::builtins::PyStr>()
.and_then(|name| name.to_str())
.map(str::to_owned))
}
impl Representable for PyWeak {
#[inline]
fn repr_str(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
fn repr_str(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<String> {
let id = zelf.get_id();
Ok(if let Some(o) = zelf.upgrade() {
format!(
"<weakref at {:#x}; to '{}' at {:#x}>",
id,
o.class().name(),
o.get_id(),
)
} else {
format!("<weakref at {id:#x}; dead>")
})
let Some(obj) = zelf.upgrade() else {
return Ok(format!("<weakref at {id:#x}; dead>"));
};
let type_name = obj.class().fully_qualified_name(vm)?;
let obj_id = obj.get_id();
let suffix = match instance_name(&obj, vm)? {
Some(name) => format!(" ({name})"),
None => String::new(),
};
Ok(format!(
"<weakref at {id:#x}; to '{type_name}' at {obj_id:#x}{suffix}>"
))
}
}
+122 -3
View File
@@ -115,13 +115,62 @@ def attr_is_not_inherited(type_, attr):
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:
sig = str(inspect.signature(obj))
# remove function memory addresses
return {
"sig": re.sub(" at 0x[0-9A-Fa-f]+", " at 0xdeadbeef", sig),
"sig": stable_signature(obj),
"doc": doc,
}
except Exception as e:
@@ -322,6 +371,63 @@ def dir_of_mod_or_error(module_name, keep_other=True):
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 _use_pure_zoneinfo(dir_result):
"""Replace the accelerated ZoneInfo with the pure-Python class."""
import zoneinfo._zoneinfo as pure_zoneinfo
cls = pure_zoneinfo.ZoneInfo
name = cls.__name__
for key in list(dir_result):
if key == name or key.startswith(f"{name}."):
del dir_result[key]
dir_result[name] = extra_info(cls)
for attr, info in own_attrs(cls):
dir_result[f"{name}.{attr}"] = info
def gen_modules():
# check name because modules listed have side effects on import,
# e.g. printing something or opening a webpage
@@ -329,6 +435,17 @@ def gen_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):
@@ -348,6 +465,8 @@ def gen_modules():
):
for attr, info in own_attrs(item):
dir_result[f"{item_name}.{attr}"] = info
if mod_name == "zoneinfo":
_use_pure_zoneinfo(dir_result)
modules[mod_name] = dir_result
return modules