Skip to content

Commit 32635df

Browse files
committed
Handle deferred evaluation of annotations in Python 3.14
1 parent 1257474 commit 32635df

30 files changed

+936
-39
lines changed

doc/whatsnew/fragments/10149.feature

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Handle deferred evaluation of annotations in Python 3.14.
2+
3+
Closes #10149

pylint/checkers/typecheck.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -976,8 +976,15 @@ class TypeChecker(BaseChecker):
976976
def open(self) -> None:
977977
py_version = self.linter.config.py_version
978978
self._py310_plus = py_version >= (3, 10)
979+
self._py314_plus = py_version >= (3, 14)
980+
self._postponed_evaluation_enabled = False
979981
self._mixin_class_rgx = self.linter.config.mixin_class_rgx
980982

983+
def visit_module(self, node: nodes.Module) -> None:
984+
self._postponed_evaluation_enabled = (
985+
self._py314_plus or is_postponed_evaluation_enabled(node)
986+
)
987+
981988
@cached_property
982989
def _compiled_generated_members(self) -> tuple[Pattern[str], ...]:
983990
# do this lazily since config not fully initialized in __init__
@@ -1066,7 +1073,7 @@ def visit_attribute(
10661073
):
10671074
return
10681075

1069-
if is_postponed_evaluation_enabled(node) and is_node_in_type_annotation_context(
1076+
if self._postponed_evaluation_enabled and is_node_in_type_annotation_context(
10701077
node
10711078
):
10721079
return
@@ -1950,9 +1957,10 @@ def _detect_unsupported_alternative_union_syntax(self, node: nodes.BinOp) -> Non
19501957
if self._py310_plus: # 310+ supports the new syntax
19511958
return
19521959

1953-
if isinstance(
1954-
node.parent, TYPE_ANNOTATION_NODES_TYPES
1955-
) and not is_postponed_evaluation_enabled(node):
1960+
if (
1961+
isinstance(node.parent, TYPE_ANNOTATION_NODES_TYPES)
1962+
and not self._postponed_evaluation_enabled
1963+
):
19561964
# Use in type annotations only allowed if
19571965
# postponed evaluation is enabled.
19581966
self._check_unsupported_alternative_union_syntax(node)
@@ -1974,7 +1982,7 @@ def _detect_unsupported_alternative_union_syntax(self, node: nodes.BinOp) -> Non
19741982
# Make sure to filter context if postponed evaluation is enabled
19751983
# and parent is allowed node type.
19761984
allowed_nested_syntax = False
1977-
if is_postponed_evaluation_enabled(node):
1985+
if self._postponed_evaluation_enabled:
19781986
parent_node = node.parent
19791987
while True:
19801988
if isinstance(parent_node, TYPE_ANNOTATION_NODES_TYPES):

pylint/checkers/variables.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,6 +1300,10 @@ def __init__(self, linter: PyLinter) -> None:
13001300
] = {}
13011301
self._postponed_evaluation_enabled = False
13021302

1303+
def open(self) -> None:
1304+
py_version = self.linter.config.py_version
1305+
self._py314_plus = py_version >= (3, 14)
1306+
13031307
@utils.only_required_for_messages(
13041308
"unbalanced-dict-unpacking",
13051309
)
@@ -1363,7 +1367,9 @@ def visit_module(self, node: nodes.Module) -> None:
13631367
checks globals doesn't overrides builtins.
13641368
"""
13651369
self._to_consume = [NamesConsumer(node, "module")]
1366-
self._postponed_evaluation_enabled = is_postponed_evaluation_enabled(node)
1370+
self._postponed_evaluation_enabled = (
1371+
self._py314_plus or is_postponed_evaluation_enabled(node)
1372+
)
13671373

13681374
for name, stmts in node.locals.items():
13691375
if utils.is_builtin(name):
@@ -2489,8 +2495,8 @@ def _is_only_type_assignment(
24892495
parent = parent_scope.parent
24902496
return True
24912497

2492-
@staticmethod
24932498
def _is_first_level_self_reference(
2499+
self,
24942500
node: nodes.Name,
24952501
defstmt: nodes.ClassDef,
24962502
found_nodes: list[nodes.NodeNG],
@@ -2502,7 +2508,7 @@ def _is_first_level_self_reference(
25022508
# Check if used as type annotation
25032509
# Break if postponed evaluation is enabled
25042510
if utils.is_node_in_type_annotation_context(node):
2505-
if not utils.is_postponed_evaluation_enabled(node):
2511+
if not self._postponed_evaluation_enabled:
25062512
return (VariableVisitConsumerAction.CONTINUE, None)
25072513
return (VariableVisitConsumerAction.RETURN, None)
25082514
# Check if used as default value by calling the class

pylint/extensions/typing.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ def open(self) -> None:
186186
self._py39_plus = py_version >= (3, 9)
187187
self._py310_plus = py_version >= (3, 10)
188188
self._py313_plus = py_version >= (3, 13)
189+
self._py314_plus = py_version >= (3, 14)
190+
self._postponed_evaluation_enabled = False
189191

190192
self._should_check_typing_alias = self._py39_plus or (
191193
self._py37_plus and self.linter.config.runtime_typing is False
@@ -197,6 +199,11 @@ def open(self) -> None:
197199
self._should_check_noreturn = py_version < (3, 7, 2)
198200
self._should_check_callable = py_version < (3, 9, 2)
199201

202+
def visit_module(self, node: nodes.Module) -> None:
203+
self._postponed_evaluation_enabled = (
204+
self._py314_plus or is_postponed_evaluation_enabled(node)
205+
)
206+
200207
def _msg_postponed_eval_hint(self, node: nodes.NodeNG) -> str:
201208
"""Message hint if postponed evaluation isn't enabled."""
202209
if self._py310_plus or "annotations" in node.root().future_imports:
@@ -474,7 +481,7 @@ def _check_broken_noreturn(self, node: nodes.Name | nodes.Attribute) -> None:
474481
return
475482

476483
if in_type_checking_block(node) or (
477-
is_postponed_evaluation_enabled(node)
484+
self._postponed_evaluation_enabled
478485
and is_node_in_type_annotation_context(node)
479486
):
480487
return
@@ -511,7 +518,7 @@ def _check_broken_callable(self, node: nodes.Name | nodes.Attribute) -> None:
511518
def _broken_callable_location(self, node: nodes.Name | nodes.Attribute) -> bool:
512519
"""Check if node would be a broken location for collections.abc.Callable."""
513520
if in_type_checking_block(node) or (
514-
is_postponed_evaluation_enabled(node)
521+
self._postponed_evaluation_enabled
515522
and is_node_in_type_annotation_context(node)
516523
):
517524
return False

pylint/testutils/functional/find_functional_tests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
from pylint.testutils.functional.test_file import FunctionalTestFile
1212

13-
REASONABLY_DISPLAYABLE_VERTICALLY = 49
13+
REASONABLY_DISPLAYABLE_VERTICALLY = 55
1414
"""'Wet finger' number of files that are reasonable to display by an IDE.
1515
1616
'Wet finger' as in 'in my settings there are precisely this many'.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[testoptions]
2+
max_pyver=3.14
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# pylint: disable=missing-docstring,unused-argument,pointless-statement
2+
# pylint: disable=too-few-public-methods
3+
4+
class Class:
5+
@classmethod
6+
def from_string(cls, source) -> Class:
7+
...
8+
9+
def validate_b(self, obj: OtherClass) -> bool:
10+
...
11+
12+
13+
class OtherClass:
14+
...
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[testoptions]
2+
min_pyver=3.14
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# pylint: disable=missing-docstring
2+
class Undefined:
3+
""" test various annotation problems. """
4+
5+
def test(self) -> Undefined: # [undefined-variable]
6+
""" used Undefined, which is Undefined in this scope. """
7+
8+
Undefined = True
9+
10+
def test1(self) -> Undefined:
11+
""" This Undefined exists at local scope. """
12+
13+
def test2(self):
14+
""" This should not emit. """
15+
def func() -> Undefined:
16+
""" empty """
17+
return 2
18+
return func
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[testoptions]
2+
max_pyver=3.14
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
undefined-variable:5:22:5:31:Undefined.test:Undefined variable 'Undefined':UNDEFINED
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# pylint: disable=missing-docstring
2+
class Undefined:
3+
""" test various annotation problems. """
4+
5+
def test(self) -> Undefined:
6+
""" used Undefined, which is Undefined in this scope. """
7+
8+
Undefined = True
9+
10+
def test1(self) -> Undefined:
11+
""" This Undefined exists at local scope. """
12+
13+
def test2(self):
14+
""" This should not emit. """
15+
def func() -> Undefined:
16+
""" empty """
17+
return 2
18+
return func
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[testoptions]
2+
min_pyver=3.14

tests/functional/u/undefined/undefined_variable_deferred_annotations_py314.txt

Whitespace-only changes.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[testoptions]
2+
max_pyver=3.14

0 commit comments

Comments
 (0)