@deprecated meets metaclasses, sibling bases, and the class header - #626
Conversation
4adcda6 to
861fa97
Compare
test.test_warnings 25 -> 23 bad, taking DeprecatedTests one by one.
THE METACLASS EATS ITS KEYWORDS FIRST. CPython evaluates ``class
Foo(metaclass=MyMeta, cls='haha')'' as MyMeta('Foo', bases, ns, cls='haha'):
the header keywords are the metaclass's to bind or forward, and
__init_subclass__ receives only what its __new__ passes on to type.__new__.
Grail had it upside down twice over -- the keywords never reached the
metaclass (its __new__ got defaults), and the hook chain got all of them
(object's terminal raised "takes no keyword arguments").
___grailDispatchMetaclass___ now passes the stashed header keywords to the
metaclass __new__/__init__, and the hook runner subtracts what the __new__'s
signature consumes: named and keyword-only parameters by name, and a
**kwargs catch-all consumes EVERYTHING REMAINING -- measured against
CPython, where a forwarding-shaped metaclass that delegates with
``super().__new__(mcs, name, bases, attrs)'' leaves the hook chain
empty-handed and a hook requiring a keyword raises; the first reading
("unconsumed keywords flow through") was plausible and wrong. The signature
is read through __wrapped__ first, so a DECORATED __new__ -- @deprecated's
own wrapper -- reads the wrapped function's parameters, exactly as
inspect.signature would. Fixes test_do_not_shadow_user_arguments.
A SIBLING BASE'S HOOK GETS THE KEYWORDS. The vendored _py_warnings now
carries the post-3.14.0 upstream fix: @deprecated's builtin-case wrapper
delegates cooperatively -- super(arg, cls).__init_subclass__(**kw) -- instead
of forwarding into the captured builtin, which raises the moment a sibling
base's hook was meant to receive the keywords. The vendored TEST corpus is
newer than 3.14.0 and pins exactly this
(test_existing_init_subclass_in_sibling_base); the reference interpreter
itself fails the scenario, so the fixture carries its one sibling check as
the inverse of the usual XFAIL -- the corpus is AHEAD of CPython 3.14.0
there, and the fixture header says so.
The other base order needs Grail's super() to find an ASSIGNED hook: a
Python object in the attribute store, which the MRO walk over compiled
method dictionaries slid straight past to object's terminal no-op.
_lookupMethodAndSideFirstOf: now probes the assigned store -- scoped to the
__init_subclass__ family, wary of the 180-test lesson its own comments
record -- and answers a third pair shape, { hook. #assigned }, which both
consumers hand to the same runner class creation uses.
TYPE.__NEW__ VALIDATES ITS NAMESPACE ARGUMENT, as CPython's does. Not
decorative: routing the header keywords exposed a latent mis-forward -- a
class-body __new__ with a REQUIRED extra parameter compiles fixed-arity on
the class's INSTANCE side, and its varargs forwarder's virtual self-send,
run against a class receiver, resolves up the metaclass chain to the builtin
with the arguments shifted one left. That arrived as "NoneType does not
understand #isEmpty", uncatchable, and turned test_super's
test___classcell___overwrite from a pass into a crash; it is now the
TypeError CPython's own argument checking raises, which that test expects.
The mis-forward itself is the same instance-side-method-against-class-
receiver disease the __init_subclass__ DNU repair covers, in a costume that
FINDS a wrong method rather than failing to find one; fixing the forwarder's
dispatch is its own change.
Attempted and REVERTED: mapping the ExecBlock family and UnboundMethod to
CPython's 'function' in ___pythonBuiltinTypeName___ (for
test_only_strings_allowed's error message). Five SUnit tests pin the
current names, the overlay suite and test_super coupled to them, and the
mapping's own comment had said so all along. That change needs its own PR
updating the pinned tests with it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… era
CI moved to CPython 3.14.7, which SHIPS the cooperative-delegation fix the
vendored _py_warnings carries -- so the sibling check passed there and the
hardcoded XFAIL scored XPASS, failing the fixture gate, while 3.14.0
locally still needs the XFAIL. The fixture now detects the fix from the
running interpreter's own source ('super(arg, cls)' in deprecated's body)
and places the check accordingly. Detected from the SOURCE, not from the
check's outcome: classifying by result would relabel any real regression as
expected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
861fa97 to
23c9eca
Compare
|
Rebased onto current Two things needed fixing beyond the mechanical rebase: The The scoreboard row needed measuring, not merging. Re-verified on the new base: curated suite 0 regressions, SUnit 5677/5677 (4.0) and 2827 + 2850 across CI's shard groups (3.7.5, clean on first cold runs), fixture gate 2632 OK / 34 XFAIL. (One procedural note: the first force-push after the conflict resolution never received a |
Stacked on #625.
test.test_warnings25 → 23 bad, takingDeprecatedTestsone by one.The metaclass eats its keywords first —
test_do_not_shadow_user_argumentsCPython evaluates
class Foo(metaclass=MyMeta, cls='haha')asMyMeta('Foo', bases, ns, cls='haha'): the header keywords are the metaclass's to bind or forward, and__init_subclass__receives only what its__new__passes on totype.__new__. Grail had it upside down twice over — the keywords never reached the metaclass (its__new__got defaults), and the hook chain got all of them (object's terminal raised "takes no keyword arguments"). This broke the undecorated shape too, not just@deprecated's.___grailDispatchMetaclass___now passes the stashed header keywords to the metaclass__new__/__init__, and the hook runner subtracts what the__new__'s signature consumes:**kwargscatch-all consumes everything remaining — measured, not guessed: CPython leaves the hook chain empty-handed there (a hook requiring a keyword raises), and my first reading — "unconsumed keywords flow through" — was plausible and wrong. The fixture records both the rule and the correction.The signature is read through
__wrapped__first, so a decorated__new__—@deprecated's own wrapper,(cls, *args, **kwargs)— reads the wrapped function's parameters, exactly asinspect.signaturewould.A sibling base's hook gets the keywords —
test_existing_init_subclass_in_sibling_baseTwo findings here, one per base order:
class C(A, B, x=42)(A@deprecated, B's hook takesx): the vendored_py_warningscaptured A's pre-wrap hook —object's builtin — and forwarded the keywords into it, which raises. Upstream CPython fixed this after 3.14.0: the builtin-case wrapper now delegates cooperatively,super(arg, cls).__init_subclass__(**kw). The vendored copy carries that fix — and the vendored test corpus is newer than 3.14.0 and pins it. I verified the scenario fails on the reference interpreter itself (3.14.0's own suite doesn't even contain this test), so the fixture carries its sibling check as the inverse of the usual XFAIL: the corpus is ahead of CPython 3.14.0 there, and the fixture header says so explicitly.class D(B, A, x=42): B's hook runs first, and its zero-argumentsuper().__init_subclass__()must find A's assigned wrapper on D's MRO — a Python object in the attribute store, which_lookupMethodAndSideFirstOf:'s walk over compiled method dictionaries slid straight past toobject's terminal no-op, dropping the deprecation warning. The walk now probes the assigned store — scoped to the__init_subclass__family, wary of the 180-test lesson its own comments record — and answers a third pair shape,{ hook. #assigned }, which both consumers hand to the same runner class creation uses.type.__new__validates its namespace argumentRouting the header keywords exposed a latent mis-forward: a class-body
__new__with a required extra parameter compiles fixed-arity on the class's instance side, and its varargs forwarder's virtual self-send — run against a class receiver — resolves up the metaclass chain to the builtin with the arguments shifted one left (nsreceives the extra parameter's value). That arrived asNoneType does not understand #isEmpty, uncatchable, and turnedtest_super'stest___classcell___overwritefrom a pass into a crash — caught by the regression gate on the first full run.type.__new__now raises theTypeErrorCPython's own argument checking raises (argument 3 must be dict, not …), which is what that test asserts — all four of its subtests pass. The mis-forward itself is the same instance-side-method-against-class-receiver disease #625 covers, in a costume that finds a wrong method rather than failing to find one; fixing the forwarder's dispatch is its own change, and the guard's comment names it.Attempted and reverted, honestly
Mapping the ExecBlock family and
UnboundMethodto CPython's'function'in___pythonBuiltinTypeName___(fortest_only_strings_allowed's error message) fixed that test and broke five name-pinning SUnit tests, the overlay suite, andtest_super— the mapping's own comment had warned exactly this. Reverted; it needs its own PR that updates the pinned tests with it.Deferred with named causes (not attempted)
test_property— stacked decorators under@property: the parse-time property fast-path fires ondecoratorList includes: #propertyregardless of what sits beneath, orphaning the inner decorator's wrapper while reads hit the native accessor;test_class— needsC.__new__ is object.__new__(method identity for inherited dunder reads; Grail's class-sideBoundMethods aren't interned);test_inspect—inspect.iscoroutinefunctionis False even for a plain async def; the foundation is missing, not the wrapping.Verification
test.test_warningst=187 f=18 e=5 s=29)test.test_superNew fixture
tests/python/deprecated_cooperative.py— 5 checks + 1 inverse-XFAIL, metaclass checks validated against CPython 3.14.0, sibling checks against the upstream fix's semantics and Grail. Wired asDeprecatedCooperativeTestCase(3/3).Two honest notes. An
install.gswiring slip put the new test'sinputline beforePythonTestCaseloads, which took one full verification run to STERROR-everything before the ordering was fixed — the "0 regressions" above is from the clean re-run. And single one-off failures appeared inFrameEqualityTestCaseandTracebackTestCaseacross runs — both live-frame/sys._getframetests, both 3/3 in isolation and clean on re-runs, same family as the one-offs flagged in #594 and #623.🤖 Generated with Claude Code