Fix generated __init__ forward-ref annotations under get_annotations / 3.14 - #1598
Conversation
Stop snapshotting module globals into generated constructors so late forward references resolve like hand-written methods. On 3.14+, attach PEP 649 __annotate__ that re-reads live class annotations, and rewrite __annotate__ closure cells for slots classes.
Merging this PR will degrade performance by 13.4%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | test_create_simple_class_make_class |
3.5 s | 4 s | -13.45% |
| ❌ | test_create_simple_class |
3.7 s | 4.3 s | -13.35% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing MohammedAnasNathani:fix/1596-init-forward-ref-annotations (0c6bf4f) with main (92e12ea)
CI on python-attrs#1598 failed because __annotate__ only re-fetched class annotations and dropped attr.ib(type=...) / string types that never appear on the class. Prefer live class anns, fall back to the full static map, and leave bare strings under VALUE so get_type_hints can eval (and raise NameError for a missing fake module). Also rebind the compiled init to an unindented linecache entry under the stable <attrs generated __init__ ...> filename so inspect.getsource matches docs/doctests without colliding with the factory script cache.
attrs requires 100% coverage; the CI combine job flagged the STRING format static-fallback branches and the no-trailing-newline rebind arm.
Nested _static_value's def line stayed uncovered under combined coverage despite body hits; fold the stringify path into the annotate body so fail-under=100 can pass.
CodSpeed class-creation note (−13.4% on create benches)CodSpeed’s bot flags What the cost is: this fix stops snapshotting What it is not: instance construction is unchanged. This is class-definition microbench only. What I tried and rejected (so we don’t ship a “faster” wrong fix):
Happy to take review direction if maintainers prefer a different tradeoff; I believe the live-globals model is the correct fix for #1596 and the class-create cost is the honest price of that correctness. |
Summary
Fixes #1596.
Generated
__init__/__attrs_init__methods no longer diverge from hand-written constructors when:inspect.signature(..., eval_str=True)/annotationlib.get_annotations(..., format=VALUE).Root causes
There were two independent problems:
1. Snapshot globals (all supported Pythons)
_make_init_scriptused to copysys.modules[cls.__module__].__dict__into the generated function's globals at class-creation time. Names defined later (e.g.Fooafter@define class C: x: Foo) were therefore missing from__init__.__globals__, soeval_str=TrueraisedNameError. Hand-written methods keep a live module__dict__as globals, so they succeed.2. Stale static annotations (Python 3.14+)
attrs set
init.__annotations__to a frozen map ofForwardRefobjects captured at class create. Setting__annotations__clears PEP 649__annotate__. Hand-written methods (and dataclasses) keep an__annotate__that re-reads live class annotations, so VALUE-format evaluation can see the real class once it exists.Fix
__init__/__attrs_init__via a small factory so helpers (NOTHING, converters, validators, …) are closure freevars, while__globals__is the live module__dict__(same model as dataclasses / hand-written code). No more module snapshot.__annotate__only (do not set__annotations__). The annotate function re-fetches field types from the class via afield_arg_mapand reuses static entries forreturn/ converter types.__annotate__callables (and property fget/fset/fdel) so they close over the final class, not the pre-slots original.Tests
tests/test_forward_references.py(3.14+): signatures match hand-written classes, VALUE format resolves to the real type, live globals, slots annotate-cell rewrite, converters stay static.tests/test_annotations.py: 3.13+ regression with a realModuleType+from __future__ import annotationslate ref.tests/test_dunders.py: internal_add_inithelper updated for the new 4-tuple + compile path; initco_filenameis method-specific by design.Verified locally on 3.13 (full suite green aside from pre-existing packaging version parse on dirty trees) and 3.14 (manual
#1596repro + targeted checks; system 3.14 lacks a working pytest install here).Pull Request Checklist
mainbranch..pyi).typing-examples/baseline.pyor, if necessary,typing-examples/mypy.py.attr/__init__.pyi, they've also been re-imported inattrs/__init__.pyi.docs/api.rstby hand.@attr.s()and@attrs.define()have to be added by hand too.versionadded,versionchanged, ordeprecateddirectives.The next version is the second number in the current release + 1.
The first number represents the current year.
So if the current version on PyPI is 26.2.0, the next version is gonna be 26.3.0.
If the next version is the first in the new year, it'll be 27.1.0.
.rstand.mdfiles is written using semantic newlines.changelog.d.