Files
RustPython/scripts/update_lib/cmd_copy_test.py
T
28393e932d Implement PEP 814 frozendict (#8951)
* Restore consolidated frozendict implementation on current main

Preserve reverse dictionary iterator fixes and the 3.15 frame imports. Keep library refreshes separate pending the runtime dependency sequencing decision.

Assisted-by: OpenAI Codex:model identifier unavailable

* Support scoped canonical test updates in update_lib

Index conditional test classes and methods without crossing namespaces, reject ambiguous targets, and preserve inherited override indentation. Add hash-pinned whole-test copying that verifies execution context and exact source bytes, with focused Python fixtures.

Assisted-by: OpenAI Codex:model identifier unavailable

* Align frozendict copy support and tests with CPython 3.15

Import exact-final copy.py with update_lib; defer Mapping ABC registration to the later library stage. Generate only proven marker corrections, retain observed debug recursion-limit expected failures, and copy the complete canonical builtin-doc discovery test using hash-pinned update_lib copy-test.

Assisted-by: OpenAI Codex:model identifier unavailable

* Preserve decorator rationale during canonical test copying

Reject differing decorator source blocks even when their ASTs match. Cover inline and multiline marker comments, guarded async methods, and CLI no-write behavior.

Assisted-by: OpenAI Codex:model identifier unavailable

* Finish frozendict with borrowed PyAnyDict views

Use the landed PyRef target model for a transparent dict/frozendict header
view, retaining objects explicitly with PyRef<PyAnyDict>. Keep targets
sealed and preserve mutable-only entry points. Borrow frame globals from
the object header rather than movable reference wrappers.

Unify exact-TypeError key diagnostics with final CPython semantics,
including callback failure propagation and no explicit cause. Keep the
Python regressions in builtin_dict and test reference layout/ownership
in Rust. Preserve existing canonical library updates.

Assisted-by: OpenAI Codex:model identifier unavailable

* Align frozendict with CPython and use CPython 3.15 tests

- Name the target type in unhashable key errors through
  PyDict::update_arg (dict_update_arg), replacing the separate
  PyFrozenDict::merge_into copy of the merge logic.
- Split the | operator into dict_or (_PyDict_Or) and frozendict_or.
- Return the argument from frozendict(frozendict) only when the called
  type is frozendict.
- Call through the vectorcall slot in PyCallable::invoke when there
  are no keyword arguments, instead of special-casing frozendict.
- Add ensure_builtins (_PyEval_EnsureBuiltins) for eval/exec, and
  insert __builtins__ for source strings regardless of globals type.
- Read __warningregistry__ and __name__ from stored globals entries and
  accept only None or str for the module name in setup_context.
- Remove the unused PyFrozenDict::from_object.
- Migrate test_dict.py, mapping_tests.py, test_copy.py and
  test_builtin.py to v3.15.0 with update_lib, and add
  test_import/data/syntax_warnings.py.
- Drop builtin_dict.py frozendict tests covered by those modules,
  keeping the descriptor and namespace integration tests.

Assisted-by: Claude Code:claude-opus-5-5

* Update crates/vm/src/builtins/dict.rs

---------

Co-authored-by: Jeong YunWon <jeong@youknowone.org>
Co-authored-by: Jeong, YunWon <69878+youknowone@users.noreply.github.com>
2026-10-14 16:42:56 +09:00

135 lines
5.6 KiB
Python

"""Copy one complete canonical test definition without refreshing its module."""
import argparse
import ast
import hashlib
import json
from pathlib import Path
from update_lib.patch_spec import _iter_class_methods, _iter_module_classes
def _definition(contents: str, qualified_name: str):
parts = qualified_name.split(".")
if len(parts) != 2 or not all(part.isidentifier() for part in parts):
raise ValueError("test name must be ClassName.method_name")
tree = ast.parse(contents)
classes = [node for node in _iter_module_classes(tree) if node.name == parts[0]]
if len(classes) != 1:
raise ValueError(f"Expected one class named {parts[0]}, found {len(classes)}")
methods = [
node for node in _iter_class_methods(classes[0]) if node.name == parts[1]
]
if len(methods) != 1:
raise ValueError(
f"Expected one method named {qualified_name}, found {len(methods)}"
)
method = methods[0]
parents = {
child: parent
for parent in ast.walk(tree)
for child in ast.iter_child_nodes(parent)
}
guards = []
child = method
while child in parents:
parent = parents[child]
if isinstance(parent, ast.If):
branch = "body" if child in parent.body else "orelse"
guards.append((ast.dump(parent.test), branch))
elif not isinstance(parent, (ast.Module, ast.ClassDef)):
raise ValueError("Only module/class scopes and if guards are supported")
child = parent
start = min([method.lineno, *(node.lineno for node in method.decorator_list)]) - 1
return method, guards, start, method.end_lineno
def copy_test(source: str, destination: str, qualified_name: str) -> str:
"""Replace an unambiguous whole definition, preserving all surrounding bytes.
Guard, signature, decorator and indentation differences require a broader
migration rather than silently changing the selected test's execution context.
"""
src, src_guards, src_start, src_end = _definition(source, qualified_name)
dst, dst_guards, dst_start, dst_end = _definition(destination, qualified_name)
if src_guards != dst_guards:
raise ValueError("Source and destination test guards differ")
def signature(node):
return (
ast.dump(node.args),
ast.dump(node.returns) if node.returns is not None else None,
[ast.dump(param) for param in getattr(node, "type_params", [])],
)
if type(src) is not type(dst) or signature(src) != signature(dst):
raise ValueError("Source and destination test signatures differ")
if [ast.dump(node) for node in src.decorator_list] != [
ast.dump(node) for node in dst.decorator_list
]:
raise ValueError(
"Source and destination decorators differ; preserve markers explicitly"
)
if src.col_offset != dst.col_offset:
raise ValueError("Source and destination indentation differs")
source_lines = source.splitlines(keepends=True)
destination_lines = destination.splitlines(keepends=True)
# AST equality omits comments, including project marker rationales. Require
# the complete decorator block to match before replacing the definition.
if (
source_lines[src_start : src.lineno - 1]
!= destination_lines[dst_start : dst.lineno - 1]
):
raise ValueError(
"Source and destination decorator text differs; preserve markers explicitly"
)
definition = source_lines[src_start:src_end]
result = "".join(
destination_lines[:dst_start] + definition + destination_lines[dst_end:]
)
_, _, result_start, result_end = _definition(result, qualified_name)
if result.splitlines(keepends=True)[result_start:result_end] != definition:
raise ValueError("Copied definition is not byte-identical to the source")
return result
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("source", type=Path)
parser.add_argument("destination", type=Path)
parser.add_argument("--test", required=True, help="Exact ClassName.method_name")
parser.add_argument("--source-sha256", required=True)
parser.add_argument("--destination-sha256", required=True)
args = parser.parse_args(argv)
try:
source = args.source.read_bytes()
destination = args.destination.read_bytes()
for label, contents, expected in (
("source", source, args.source_sha256),
("destination", destination, args.destination_sha256),
):
if hashlib.sha256(contents).hexdigest() != expected:
raise ValueError(f"{label} SHA256 does not match the pinned input")
result = copy_test(
source.decode("utf-8"), destination.decode("utf-8"), args.test
).encode("utf-8")
# Recheck the destination immediately before writing so stale invocations
# fail instead of overwriting an intervening edit.
if args.destination.read_bytes() != destination:
raise ValueError("Destination changed during test preparation")
args.destination.write_bytes(result)
except (OSError, UnicodeError, SyntaxError, ValueError) as exc:
parser.exit(1, f"Error: {exc}\n")
print(
json.dumps(
{
"test": args.test,
"source_sha256": args.source_sha256,
"destination_before_sha256": args.destination_sha256,
"destination_after_sha256": hashlib.sha256(result).hexdigest(),
},
sort_keys=True,
)
)
return 0