Skip to content

Commit 8615074

Browse files
committed
Handle deferred evaluation of annotations in Python 3.14
1 parent 9e22638 commit 8615074

14 files changed

+124
-34
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

tests/functional/p/postponed/postponed_evaluation_not_activated.314.txt

Whitespace-only changes.

tests/functional/p/postponed/postponed_evaluation_not_activated.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33

44
class Class:
55
@classmethod
6-
def from_string(cls, source) -> Class: # [undefined-variable]
6+
def from_string(cls, source) -> Class: # <3.14:[undefined-variable]
77
...
88

9-
def validate_b(self, obj: OtherClass) -> bool: # [used-before-assignment]
9+
def validate_b(self, obj: OtherClass) -> bool: # <3.14:[used-before-assignment]
1010
...
1111

1212

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
undefined-variable:12:19:12:26::Undefined variable 'unknown':UNDEFINED
2+
undefined-variable:18:10:18:21:in_method:Undefined variable 'nomoreknown':UNDEFINED
3+
undefined-variable:21:19:21:31::Undefined variable '__revision__':UNDEFINED
4+
undefined-variable:23:8:23:20::Undefined variable '__revision__':UNDEFINED
5+
undefined-variable:27:29:27:37:bad_default:Undefined variable 'unknown2':UNDEFINED
6+
undefined-variable:30:10:30:14:bad_default:Undefined variable 'xxxx':UNDEFINED
7+
undefined-variable:31:4:31:10:bad_default:Undefined variable 'augvar':UNDEFINED
8+
undefined-variable:32:8:32:14:bad_default:Undefined variable 'vardel':UNDEFINED
9+
undefined-variable:34:19:34:31:<lambda>:Undefined variable 'doesnotexist':UNDEFINED
10+
undefined-variable:35:23:35:24:<lambda>:Undefined variable 'z':UNDEFINED
11+
used-before-assignment:38:4:38:9::Using variable 'POUET' before assignment:CONTROL_FLOW
12+
used-before-assignment:43:4:43:10::Using variable 'POUETT' before assignment:CONTROL_FLOW
13+
used-before-assignment:48:4:48:11::Using variable 'POUETTT' before assignment:CONTROL_FLOW
14+
used-before-assignment:56:4:56:9::Using variable 'PLOUF' before assignment:CONTROL_FLOW
15+
used-before-assignment:65:11:65:14:if_branch_test:Using variable 'xxx' before assignment:HIGH
16+
used-before-assignment:91:23:91:32:test_arguments:Using variable 'TestClass' before assignment:HIGH
17+
used-before-assignment:95:16:95:24:TestClass:Using variable 'Ancestor' before assignment:HIGH
18+
used-before-assignment:98:26:98:35:TestClass.MissingAncestor:Using variable 'Ancestor1' before assignment:HIGH
19+
used-before-assignment:105:36:105:41:TestClass.test1.UsingBeforeDefinition:Using variable 'Empty' before assignment:HIGH
20+
undefined-variable:119:10:119:14:Self:Undefined variable 'Self':UNDEFINED
21+
undefined-variable:135:7:135:10::Undefined variable 'BAT':UNDEFINED
22+
undefined-variable:136:4:136:7::Undefined variable 'BAT':UNDEFINED
23+
used-before-assignment:146:31:146:38:KeywordArgument.test1:Using variable 'enabled' before assignment:HIGH
24+
undefined-variable:149:32:149:40:KeywordArgument.test2:Undefined variable 'disabled':UNDEFINED
25+
undefined-variable:154:22:154:25:KeywordArgument.<lambda>:Undefined variable 'arg':UNDEFINED
26+
undefined-variable:166:4:166:13::Undefined variable 'unicode_2':UNDEFINED
27+
undefined-variable:171:4:171:13::Undefined variable 'unicode_3':UNDEFINED
28+
undefined-variable:226:25:226:37:LambdaClass4.<lambda>:Undefined variable 'LambdaClass4':UNDEFINED
29+
undefined-variable:234:25:234:37:LambdaClass5.<lambda>:Undefined variable 'LambdaClass5':UNDEFINED
30+
undefined-variable:291:18:291:24:not_using_loop_variable_accordingly:Undefined variable 'iteree':UNDEFINED
31+
undefined-variable:308:27:308:28:undefined_annotation:Undefined variable 'x':UNDEFINED
32+
used-before-assignment:309:7:309:8:undefined_annotation:Using variable 'x' before assignment:HIGH
33+
undefined-variable:339:11:339:12:decorated3:Undefined variable 'x':UNDEFINED
34+
undefined-variable:344:19:344:20:decorated4:Undefined variable 'y':UNDEFINED
35+
undefined-variable:365:10:365:20:global_var_mixed_assignment:Undefined variable 'GLOBAL_VAR':HIGH

tests/functional/u/undefined/undefined_variable.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ def onclick(event):
252252
from datetime import datetime
253253

254254

255-
def func_should_fail(_dt: datetime): # [used-before-assignment]
255+
def func_should_fail(_dt: datetime): # <3.14:[used-before-assignment]
256256
pass
257257

258258

@@ -374,13 +374,13 @@ def global_var_mixed_assignment():
374374

375375

376376
class RepeatedReturnAnnotations:
377-
def x(self, o: RepeatedReturnAnnotations) -> bool: # [undefined-variable]
377+
def x(self, o: RepeatedReturnAnnotations) -> bool: # <3.14:[undefined-variable]
378378
pass
379-
def y(self) -> RepeatedReturnAnnotations: # [undefined-variable]
379+
def y(self) -> RepeatedReturnAnnotations: # <3.14:[undefined-variable]
380380
pass
381-
def z(self) -> RepeatedReturnAnnotations: # [undefined-variable]
381+
def z(self) -> RepeatedReturnAnnotations: # <3.14:[undefined-variable]
382382
pass
383383

384384
class A:
385-
def say_hello(self) -> __module__: # [undefined-variable]
385+
def say_hello(self) -> __module__: # <3.14:[undefined-variable]
386386
...
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
undefined-variable:33:34:33:39:Undefined1.InnerScope.test_undefined:Undefined variable 'Undef':UNDEFINED
2+
undefined-variable:36:25:36:28:Undefined1.InnerScope.test1:Undefined variable 'ABC':UNDEFINED
3+
undefined-variable:51:28:51:32:FalsePositive342.test_bad:Undefined variable 'trop':UNDEFINED
4+
undefined-variable:54:31:54:36:FalsePositive342.test_bad1:Undefined variable 'trop1':UNDEFINED
5+
undefined-variable:57:31:57:36:FalsePositive342.test_bad2:Undefined variable 'trop2':UNDEFINED
6+
undefined-variable:63:0:63:9:Bad:Undefined variable 'ABCMet':UNDEFINED
7+
undefined-variable:66:0:66:15:SecondBad:Undefined variable 'ab':UNDEFINED
8+
undefined-variable:97:53:97:61:InheritingClass:Undefined variable 'variable':UNDEFINED
9+
undefined-variable:103:0:103:15:Inheritor:Undefined variable 'DefinedTooLate':UNDEFINED

tests/functional/u/undefined/undefined_variable_py30.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
class Undefined:
66
""" test various annotation problems. """
77

8-
def test(self)->Undefined: # [undefined-variable]
8+
def test(self)->Undefined: # <3.14:[undefined-variable]
99
""" used Undefined, which is Undefined in this scope. """
1010

1111
Undefined = True
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
used-before-assignment:17:14:17:17:test_fail.wrap:Using variable 'cnt' before assignment:HIGH
2+
used-before-assignment:26:14:26:17:test_fail2.wrap:Using variable 'cnt' before assignment:HIGH
3+
used-before-assignment:90:10:90:18:type_annotation_never_gets_value_despite_nonlocal:Using variable 'some_num' before assignment:HIGH
4+
used-before-assignment:96:14:96:18:inner_function_lacks_access_to_outer_args.inner:Using variable 'args' before assignment:HIGH
5+
used-before-assignment:117:18:117:21:nonlocal_in_outer_frame_fail.outer.inner:Using variable 'num' before assignment:HIGH
6+
possibly-used-before-assignment:149:20:149:28:nonlocal_in_distant_outer_frame_fail.outer.intermediate.inner:Possibly using variable 'callback' before assignment:CONTROL_FLOW
7+
used-before-assignment:163:14:163:17:nonlocal_after_bad_usage_fail.inner:Using variable 'num' before assignment:HIGH

tests/functional/u/used/used_before_assignment_nonlocal.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,17 @@ def wrap():
2626
cnt = cnt + 1 # [used-before-assignment]
2727
wrap()
2828

29-
def test_fail3(arg: test_fail4): # [used-before-assignment]
29+
def test_fail3(arg: test_fail4): # <3.14:[used-before-assignment]
3030
""" Depends on `test_fail4`, in argument annotation. """
3131
return arg
32-
# +1: [used-before-assignment, used-before-assignment]
32+
# +1:<3.14: [used-before-assignment, used-before-assignment]
3333
def test_fail4(*args: test_fail5, **kwargs: undefined):
3434
""" Depends on `test_fail5` and `undefined` in
3535
variable and named arguments annotations.
3636
"""
3737
return args, kwargs
3838

39-
def test_fail5()->undefined1: # [used-before-assignment]
39+
def test_fail5()->undefined1: # <3.14:[used-before-assignment]
4040
""" Depends on `undefined1` in function return annotation. """
4141

4242
def undefined():
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
undefined-variable:79:20:79:27:MyClass.incorrect_default_method:Undefined variable 'MyClass':UNDEFINED
2+
possibly-used-before-assignment:171:14:171:20:TypeCheckingMultiBranch.defined_in_elif_branch:Possibly using variable 'bisect' before assignment:INFERENCE
3+
possibly-used-before-assignment:172:15:172:23:TypeCheckingMultiBranch.defined_in_elif_branch:Possibly using variable 'calendar' before assignment:INFERENCE
4+
used-before-assignment:175:14:175:22:TypeCheckingMultiBranch.defined_in_else_branch:Using variable 'zoneinfo' before assignment:INFERENCE
5+
used-before-assignment:176:14:176:20:TypeCheckingMultiBranch.defined_in_else_branch:Using variable 'pprint' before assignment:INFERENCE
6+
used-before-assignment:177:14:177:25:TypeCheckingMultiBranch.defined_in_else_branch:Using variable 'collections' before assignment:INFERENCE
7+
possibly-used-before-assignment:181:14:181:19:TypeCheckingMultiBranch.defined_in_nested_if_else:Possibly using variable 'heapq' before assignment:INFERENCE
8+
used-before-assignment:185:14:185:19:TypeCheckingMultiBranch.defined_in_try_except:Using variable 'types' before assignment:INFERENCE
9+
used-before-assignment:186:14:186:18:TypeCheckingMultiBranch.defined_in_try_except:Using variable 'copy' before assignment:INFERENCE
10+
used-before-assignment:187:14:187:21:TypeCheckingMultiBranch.defined_in_try_except:Using variable 'numbers' before assignment:INFERENCE
11+
used-before-assignment:188:15:188:20:TypeCheckingMultiBranch.defined_in_try_except:Using variable 'array' before assignment:INFERENCE
12+
used-before-assignment:191:14:191:19:TypeCheckingMultiBranch.defined_in_loops:Using variable 'email' before assignment:INFERENCE
13+
used-before-assignment:192:14:192:21:TypeCheckingMultiBranch.defined_in_loops:Using variable 'mailbox' before assignment:INFERENCE
14+
used-before-assignment:193:14:193:23:TypeCheckingMultiBranch.defined_in_loops:Using variable 'mimetypes' before assignment:INFERENCE
15+
used-before-assignment:197:14:197:22:TypeCheckingMultiBranch.defined_in_with:Using variable 'binascii' before assignment:INFERENCE

tests/functional/u/used/used_before_assignment_typing.py

+13-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Tests for used-before-assignment for typing related issues"""
22
# pylint: disable=missing-function-docstring,ungrouped-imports,invalid-name
3-
3+
# pylint: disable=line-too-long
44

55
from typing import List, NamedTuple, Optional, TYPE_CHECKING
66

@@ -66,12 +66,12 @@ class MyClass:
6666
"""Type annotation or default values for first level methods can't refer to their own class"""
6767

6868
def incorrect_typing_method(
69-
self, other: MyClass # [undefined-variable]
69+
self, other: MyClass # <3.14:[undefined-variable]
7070
) -> bool:
7171
return self == other
7272

7373
def incorrect_nested_typing_method(
74-
self, other: List[MyClass] # [undefined-variable]
74+
self, other: List[MyClass] # <3.14:[undefined-variable]
7575
) -> bool:
7676
return self == other[0]
7777

@@ -137,7 +137,7 @@ class MyFourthClass: # pylint: disable=too-few-public-methods
137137
"""Class to test conditional imports guarded by TYPE_CHECKING two levels
138138
up then used in function annotation. See https://github.com/pylint-dev/pylint/issues/7539"""
139139

140-
def is_close(self, comparator: math.isclose, first, second): # [used-before-assignment]
140+
def is_close(self, comparator: math.isclose, first, second): # <3.14:[used-before-assignment]
141141
"""Conditional imports guarded are only valid for variable annotations."""
142142
comparator(first, second)
143143

@@ -150,7 +150,7 @@ class VariableAnnotationsGuardedByTypeChecking: # pylint: disable=too-few-publi
150150
and https://github.com/pylint-dev/pylint/issues/7882
151151
"""
152152

153-
still_an_error: datetime.date # [used-before-assignment]
153+
still_an_error: datetime.date # <3.14:[used-before-assignment]
154154

155155
def print_date(self, date) -> None:
156156
date: datetime.date = date
@@ -167,33 +167,33 @@ class ConditionalImportGuardedWhenUsed: # pylint: disable=too-few-public-method
167167

168168
class TypeCheckingMultiBranch: # pylint: disable=too-few-public-methods,unused-variable
169169
"""Test for defines in TYPE_CHECKING if/elif/else branching"""
170-
def defined_in_elif_branch(self) -> calendar.Calendar: # [possibly-used-before-assignment]
170+
def defined_in_elif_branch(self) -> calendar.Calendar: # <3.14:[possibly-used-before-assignment]
171171
print(bisect) # [possibly-used-before-assignment]
172-
return calendar.Calendar()
172+
return calendar.Calendar() # >=3.14:[possibly-used-before-assignment]
173173

174174
def defined_in_else_branch(self) -> urlopen:
175175
print(zoneinfo) # [used-before-assignment]
176176
print(pprint()) # [used-before-assignment]
177177
print(collections()) # [used-before-assignment]
178178
return urlopen
179179

180-
def defined_in_nested_if_else(self) -> heapq: # [possibly-used-before-assignment]
181-
print(heapq)
180+
def defined_in_nested_if_else(self) -> heapq: # <3.14:[possibly-used-before-assignment]
181+
print(heapq) # >=3.14:[possibly-used-before-assignment]
182182
return heapq
183183

184-
def defined_in_try_except(self) -> array: # [used-before-assignment]
184+
def defined_in_try_except(self) -> array: # <3.14:[used-before-assignment]
185185
print(types) # [used-before-assignment]
186186
print(copy) # [used-before-assignment]
187187
print(numbers) # [used-before-assignment]
188-
return array
188+
return array # >=3.14:[used-before-assignment]
189189

190-
def defined_in_loops(self) -> json: # [used-before-assignment]
190+
def defined_in_loops(self) -> json: # <3.14:[used-before-assignment]
191191
print(email) # [used-before-assignment]
192192
print(mailbox) # [used-before-assignment]
193193
print(mimetypes) # [used-before-assignment]
194194
return json
195195

196-
def defined_in_with(self) -> base64: # [used-before-assignment]
196+
def defined_in_with(self) -> base64: # <3.14:[used-before-assignment]
197197
print(binascii) # [used-before-assignment]
198198
return base64
199199

0 commit comments

Comments
 (0)