Implement deferred annotations for Python 3.14#17658
Conversation
|
|
This is a PR where I'd appreciate if you @AlexWaygood could take a look at the semantics of the change. I can review the code changes. |
370a67b to
95bd559
Compare
| // then annotations are never evaluated at runtime, | ||
| // so we can treat them as typing-only. | ||
| if semantic.future_annotations_or_stub() { | ||
| if semantic.future_annotations_or_stub() || version.defers_annotations() { |
There was a problem hiding this comment.
If there are no situations where we want to treat PEP-563 differently from PEP-649, should we just have the semantic.future_annotations_or_stub() method itself always return true if the Python version is 3.14 or higher? We could rewrite SemanticModelFlags::new() so that it takes a PythonVersion argument as well as a Path argument, and always sets the FUTURE_ANNOTATIONS flag if the Python version is >= 3.14?
ruff/crates/ruff_python_semantic/src/model.rs
Lines 2574 to 2582 in b7ce694
There was a problem hiding this comment.
I audited existing uses of future_annotations_or_stub() and I can't see any uses where it would be problematic if we had the method return true for either PEP-563 or PEP-649 being activated
There was a problem hiding this comment.
Hmm - I'm torn here. I wanted to keep these separate because the two conditions aren't technically identical. So it may be misleading/surprising for contributors implementing something if semantic.future_annotations_or_stub returns true when we aren't in a stub file and annotations hasn't been imported. (And I figured semantic.future_annotations_or_stub_or_version_defers_annotations was unwieldy 😄 ).
A less compelling reason is that we may want to eventually target situations like the example in the PR summary, in which case it's important to distinguish between the two.
Since it's only a couple call sites, I think I'm gonna keep them separate for now and get this merged, and we can always combine them later.
AlexWaygood
left a comment
There was a problem hiding this comment.
The semantics LGTM here. Thank you! And sorry for the delay -- I wanted to re-read PEP-649 and PEP-749 before reviewing, but they're both long and I kept getting distracted 🙃
I do think there are some other changes we could do as well in this area, but don't need to be done in this PR:
- We should probably at least update the docs for rules that tell you to add
from __future__ import annotationsor that tell you to stringify annotations, to say that this isn't necessary on Python 3.14+ - Certain idioms (some of them pretty common!) that were usable prior to Python 3.14 will no longer be usable; it would be great if we could add diagnostics warning users against doing
cls.__annotations__orcls.__dict__.get("__annotations__", {}). Both are ill-advised on Python 3.14+
|
JelleZijlstra
left a comment
There was a problem hiding this comment.
Feel free to ping me with any questions about this (for context I implemented PEP 649 in CPython).
I am not familiar with how Ruff uses this logic but it generally makes sense to me that it should treat everything as if from __future__ import annotations is present in Python 3.14+.
| @@ -1005,8 +1005,12 @@ impl<'a> Visitor<'a> for Checker<'a> { | |||
|
|
|||
| // Function annotations are always evaluated at runtime, unless future annotations | |||
There was a problem hiding this comment.
This comment is out of date
95bd559 to
71b45cd
Compare
|
Thanks for your help @AlexWaygood and @JelleZijlstra ! I've made a tiny issue recording the idea for a lint rule checking for |
This PR updates the semantic model for Python 3.14 by essentially
equating "run using Python 3.14" with "uses `from __future__ import
annotations`".
While this is not technically correct under the hood, it appears to be
correct for the purposes of our semantic model. That is: from the point
of view of deciding when to parse, bind, etc. annotations, these two
contexts behave the same. More generally these contexts behave the same
unless you are performing some kind of introspection like the following:
Without future import:
```pycon
>>> from annotationlib import get_annotations,Format
>>> def foo()->Bar:...
...
>>> get_annotations(foo,format=Format.FORWARDREF)
{'return': ForwardRef('Bar')}
>>> get_annotations(foo,format=Format.STRING)
{'return': 'Bar'}
>>> get_annotations(foo,format=Format.VALUE)
Traceback (most recent call last):
[...]
NameError: name 'Bar' is not defined
>>> get_annotations(foo)
Traceback (most recent call last):
[...]
NameError: name 'Bar' is not defined
```
With future import:
```
>>> from __future__ import annotations
>>> from annotationlib import get_annotations,Format
>>> def foo()->Bar:...
...
>>> get_annotations(foo,format=Format.FORWARDREF)
{'return': 'Bar'}
>>> get_annotations(foo,format=Format.STRING)
{'return': 'Bar'}
>>> get_annotations(foo,format=Format.VALUE)
{'return': 'Bar'}
>>> get_annotations(foo)
{'return': 'Bar'}
```
(Note: the result of the last call to `get_annotations` in these
examples relies on the fact that, as of this writing, the default value
for `format` is `Format.VALUE`).
If one day we support lint rules targeting code that introspects using
the new `annotationlib`, then it is possible we will need to revisit our
approximation.
Closes astral-sh#15100
This PR updates the semantic model for Python 3.14 by essentially equating "run using Python 3.14" with "uses
from __future__ import annotations".While this is not technically correct under the hood, it appears to be correct for the purposes of our semantic model. That is: from the point of view of deciding when to parse, bind, etc. annotations, these two contexts behave the same. More generally these contexts behave the same unless you are performing some kind of introspection like the following:
Without future import:
With future import:
(Note: the result of the last call to
get_annotationsin these examples relies on the fact that, as of this writing, the default value forformatisFormat.VALUE).If one day we support lint rules targeting code that introspects using the new
annotationlib, then it is possible we will need to revisit our approximation.Closes #15100