Resolve the __new__ class alias to the defining class when it exists - #590
dchaudhari7177 wants to merge 1 commit into
Conversation
Instrumenting __new__() injects '<ClassName> = <first arg>' so a forward reference naming the defining class resolves while the class is still being created -- which is when __new__() first runs for an Enum (agronholm#398). The first argument is the subclass on a subclass construction, so that alias made the class's own name mean the subclass for the whole method body. An abstract-base guard like 'if cls is Base: raise' then rejected every subclass, and a forward-reference argument annotation was checked against the subclass, rejecting a legitimate instance of the base class. Both are silent and both go away if the annotation is deleted, so the trap is re-armed by anyone who adds it back. The alias now reads globals().get('<ClassName>', <first arg>), so it is the real class whenever the class exists and falls back to the argument only in the window agronholm#398 is about. The annotation is compiled as a bare name reference, so the binding has to stay in the function's scope; what changes is its value. Closes agronholm#578
|
Closing as a duplicate — I missed #581 and #583, both of which predate this by over a week and cover #578. Sorry for the noise. #583 is also the better approach. It stops the transformer compiling the self-naming forward reference to a bare `Name` at all, keeping it a `ForwardRef` for the memo to resolve, so the alias goes away instead of being corrected. This PR only changed what the alias was bound to, which leaves the binding shadowing an enclosing-scope class of the same name. One data point from here that may be worth having on #583: the annotation being compiled to a bare name is exactly why the alias could not simply be moved into the memo's locals — I tried that first and it reintroduced #398's `NameError` for an `IntEnum` whose `new` is annotated `-> "E"`. Since #583 routes through `ForwardRef` resolution instead, that case is worth an explicit test if it does not have one. |
Changes
Fixes #578.
Instrumenting
__new__()injects<ClassName> = <first arg>at the top of the body so that a forward reference naming the defining class resolves while the class is still being created — the window #398 is about, since__new__()first runs for anEnumbefore the class name exists.The first argument is the subclass on a subclass construction, so that alias made the class's own name mean the subclass for the whole method body:
An abstract-base guard (
if cls is Base: raise TypeError(...)) therefore rejected every subclass once the module was instrumented, and a forward-reference argument annotation was checked against the subclass, so a legitimate instance of the base class was rejected with a message naming a class the annotation never mentioned.Both symptoms are silent and both disappear if you delete the annotation, so the trap is re-armed by whoever adds it back.
The alias now reads:
so it is the real class whenever the class exists, and falls back to the first argument only in the not-yet-defined window. The binding has to stay in the function's scope — the transformer compiles the forward reference to a bare
Name, not to a string the memo evaluates — so what changes is its value, not its existence. My first attempt moved the name into the memo's locals instead and reintroduced #398'sNameError, which is what confirmed that.Unchanged on purpose: a class that is not a module global (nested in a class or a function) is not in
globals(), so it still falls back to the argument. That is the current behaviour, and narrowing it further would need a different mechanism.Checklist
tests/) added which would fail without your patchdocs/, in case of behavior changes or new features) — no documented behaviour changes; this restores what the docs already implydocs/versionhistory.rst).Verification
Two behaviour tests in
tests/test_importhook.pyover a newSelfNamingNew/SubclassOfSelfNamingNewpair intests/dummymodule.py: one asserts the class's own name is the base class while the first argument is the subclass, the other passes a genuine base-class instance to the subclass's__new__. Both fail onmasterwith the patch reverted:test_new_with_selfandtest_new_with_explicit_class_nameintests/test_transformer.pyare updated for the new generated line; #398's own path (anIntEnumwith a__new__annotated-> "E") was checked by hand against both the old and the new code and still works.Full suite: 533 passed, 9 skipped, 9 xfailed. The 4 failures in
tests/test_pytest_plugin.pyare pre-existing on an unmodified checkout — they come frompytest-asyncio's unsetasyncio_default_fixture_loop_scopewarning being raised as an error, and are unrelated to this change.ruff0.15.20 (the version.pre-commit-config.yamlpins) check and format clean;mypyclean on the changed module.