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

+3
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

+13-5
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

+9-3
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

+9-2
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

+1-1
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'.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[testoptions]
2+
max_pyver=3.14
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+
...
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[testoptions]
2+
min_pyver=3.14
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
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[testoptions]
2+
max_pyver=3.14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
undefined-variable:5:22:5:31:Undefined.test:Undefined variable 'Undefined':UNDEFINED
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
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.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[testoptions]
2+
max_pyver=3.14

0 commit comments

Comments
 (0)