Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit eb41eb1

Browse files
authored
Let --allow-redefinition-new widen global in function with None init (#21285)
Now this infers an optional type, for backward compat with `--allow-redefinition-old`: ```py # mypy: allow-redefinition-new x = None def foo() -> None: global x x = 1 # Ok # Type of "x" is "int | None" here ``` There are a few non-trivial things. The implementation propagates the widened type to the global scope, since otherwise `x` would have type `None` at top level after the function. We also only allow widening if the original type is `None`, to make reasoning about this easier. Otherwise the type could be inferred from any number of functions. Also this is sufficient to maintain backward compatibility. I used coding agent assist with small incremental changes and careful manual reviews. See #21276 for additional context.
1 parent f4db3cd commit eb41eb1

4 files changed

Lines changed: 200 additions & 13 deletions

File tree

mypy/checker.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,9 @@ class TypeChecker(NodeVisitor[None], TypeCheckerSharedApi, SplittingVisitor):
424424
# Short names of Var nodes whose previous inferred type has been widened via assignment.
425425
# NOTE: The names might not be unique, they are only for debugging purposes.
426426
widened_vars: list[str]
427+
# Global variables widened inside a function body, to be propagated to
428+
# the module-level binder after the function is type checked (with --allow-redefinition-new).
429+
_globals_widened_in_func: list[tuple[NameExpr, Type]]
427430
globals: SymbolTable
428431
modules: dict[str, MypyFile]
429432
# Nodes that couldn't be checked because some types weren't available. We'll run
@@ -488,6 +491,7 @@ def __init__(
488491
self.tscope = Scope()
489492
self.scope = CheckerScope(tree)
490493
self.binder = ConditionalTypeBinder(options)
494+
self.globals_binder = self.binder
491495
self.globals = tree.names
492496
self.return_types = []
493497
self.dynamic_funcs = []
@@ -496,6 +500,7 @@ def __init__(
496500
self.var_decl_frames = {}
497501
self.deferred_nodes = []
498502
self.widened_vars = []
503+
self._globals_widened_in_func = []
499504
self._type_maps = [{}]
500505
self.module_refs = set()
501506
self.pass_num = 0
@@ -1616,6 +1621,16 @@ def check_func_def(
16161621

16171622
self.return_types.pop()
16181623

1624+
# Propagate any global variable widenings directly to the
1625+
# module-level binder (skipping any intermediate class binders).
1626+
if self._globals_widened_in_func:
1627+
for lvalue, widened_type in self._globals_widened_in_func:
1628+
self.globals_binder.put(lvalue, widened_type)
1629+
lit = literal_hash(lvalue)
1630+
if lit is not None:
1631+
self.globals_binder.declarations[lit] = widened_type
1632+
self._globals_widened_in_func = []
1633+
16191634
self.binder = old_binder
16201635

16211636
def check_funcdef_item(
@@ -4897,13 +4912,18 @@ def check_simple_assignment(
48974912
not self.refers_to_different_scope(lvalue)
48984913
and not isinstance(inferred.type, PartialType)
48994914
and not is_proper_subtype(new_inferred, inferred.type)
4915+
and self.can_widen_in_scope(lvalue, inferred.type)
49004916
):
49014917
lvalue_type = make_simplified_union([inferred.type, new_inferred])
49024918
# Widen the type to the union of original and new type.
49034919
if not inferred.is_index_var:
49044920
# Skip index variables as they are reset on each loop.
49054921
self.widened_vars.append(inferred.name)
49064922
self.set_inferred_type(inferred, lvalue, lvalue_type)
4923+
if lvalue.kind == GDEF and self.scope.top_level_function() is not None:
4924+
# Widening a global inside a function -- record for
4925+
# propagation to the module-level binder afterwards.
4926+
self._globals_widened_in_func.append((lvalue, lvalue_type))
49074927
self.binder.put(lvalue, rvalue_type)
49084928
# TODO: A bit hacky, maybe add a binder method that does put and
49094929
# updates declaration?
@@ -4932,14 +4952,30 @@ def refers_to_different_scope(self, name: NameExpr) -> bool:
49324952
if name.kind == LDEF:
49334953
# TODO: Consider reference to outer function as a different scope?
49344954
return False
4935-
elif self.scope.top_level_function() is not None:
4955+
elif self.scope.top_level_function() is not None and name.kind != GDEF:
49364956
# A non-local reference from within a function must refer to a different scope
49374957
return True
49384958
elif name.kind == GDEF and name.fullname.rpartition(".")[0] != self.tree.fullname:
49394959
# Reference to global definition from another module
49404960
return True
49414961
return False
49424962

4963+
def can_widen_in_scope(self, name: NameExpr, orig_type: Type) -> bool:
4964+
"""Can a variable type be widened via assignment in the current scope?
4965+
4966+
Globals can only be widened from within a function if the original type
4967+
is None (backward compat with partial type handling of `x = None`).
4968+
4969+
See test cases testNewRedefineGlobalVariableNoneInit[1-4], for example.
4970+
"""
4971+
if (
4972+
name.kind == GDEF
4973+
and self.scope.top_level_function() is not None
4974+
and not isinstance(get_proper_type(orig_type), NoneType)
4975+
):
4976+
return False
4977+
return True
4978+
49434979
def check_member_assignment(
49444980
self,
49454981
lvalue: MemberExpr,
@@ -8337,7 +8373,16 @@ def visit_nonlocal_decl(self, o: NonlocalDecl, /) -> None:
83378373
return None
83388374

83398375
def visit_global_decl(self, o: GlobalDecl, /) -> None:
8340-
return None
8376+
if self.options.allow_redefinition_new:
8377+
# Add names to binder, since their types could be widened
8378+
for name in o.names:
8379+
sym = self.globals.get(name)
8380+
if sym and isinstance(sym.node, Var) and sym.node.type is not None:
8381+
n = NameExpr(name)
8382+
n.node = sym.node
8383+
n.kind = GDEF
8384+
n.fullname = sym.node.fullname
8385+
self.binder.assign_type(n, sym.node.type, sym.node.type)
83418386

83428387

83438388
class TypeCheckerAsSemanticAnalyzer(SemanticAnalyzerCoreInterface):

test-data/unit/check-inference.test

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4350,20 +4350,22 @@ def g() -> None:
43504350
x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int | None")
43514351
reveal_type(x) # N: Revealed type is "builtins.int | None"
43524352

4353+
reveal_type(x) # N: Revealed type is "builtins.int | None"
4354+
43534355
[case testGlobalVariableNoneInitMultipleFuncsRedefine]
43544356
# flags: --allow-redefinition-new --local-partial-types
43554357

4356-
# Widening this is intentionally prohibited (for now).
4358+
# Widening None is supported, as a special case
43574359
x = None
43584360

43594361
def f() -> None:
43604362
global x
43614363
reveal_type(x) # N: Revealed type is "None"
4362-
x = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "None")
4363-
reveal_type(x) # N: Revealed type is "None"
4364+
x = 1
4365+
reveal_type(x) # N: Revealed type is "builtins.int"
43644366

43654367
def g() -> None:
43664368
global x
4367-
reveal_type(x) # N: Revealed type is "None"
4368-
x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "None")
4369-
reveal_type(x) # N: Revealed type is "None"
4369+
reveal_type(x) # N: Revealed type is "None | builtins.int"
4370+
x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int | None")
4371+
reveal_type(x) # N: Revealed type is "None | builtins.int"

test-data/unit/check-redefine2.test

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,17 +168,76 @@ def f2() -> None:
168168

169169
reveal_type(x) # N: Revealed type is "builtins.int | builtins.str"
170170

171-
[case testNewRedefineGlobalVariableNoneInit]
171+
[case testNewRedefineGlobalVariableNoneInit1]
172172
# flags: --allow-redefinition-new --local-partial-types
173173
x = None
174174

175175
def f() -> None:
176176
global x
177177
reveal_type(x) # N: Revealed type is "None"
178-
x = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "None")
179-
reveal_type(x) # N: Revealed type is "None"
178+
x = 1
179+
reveal_type(x) # N: Revealed type is "builtins.int"
180+
181+
reveal_type(x) # N: Revealed type is "None | builtins.int"
182+
183+
[case testNewRedefineGlobalVariableNoneInit2]
184+
# flags: --allow-redefinition-new --local-partial-types
185+
x = None
186+
187+
def deco(f): return f
188+
189+
@deco
190+
def f() -> None:
191+
global x
192+
if int():
193+
x = 1
194+
reveal_type(x) # N: Revealed type is "None | builtins.int"
195+
196+
reveal_type(x) # N: Revealed type is "None | builtins.int"
197+
198+
[case testNewRedefineGlobalVariableNoneInit3]
199+
# flags: --allow-redefinition-new --local-partial-types
200+
from typing import overload
201+
202+
x = None
203+
204+
class C:
205+
@overload
206+
def f(self) -> None: ...
207+
@overload
208+
def f(self, n: int) -> None: ...
209+
210+
def f(self, n: int = 0) -> None:
211+
global x
212+
x = n
213+
a = [x]
214+
x = None
215+
reveal_type(x) # N: Revealed type is "None"
216+
217+
reveal_type(x) # N: Revealed type is "None | builtins.int"
218+
219+
[case testNewRedefineGlobalVariableNoneInit4]
220+
# flags: --allow-redefinition-new --local-partial-types
221+
x = None
222+
223+
def f() -> None:
224+
def nested() -> None:
225+
global x
226+
x = 1
180227

181-
reveal_type(x) # N: Revealed type is "None"
228+
nested()
229+
230+
reveal_type(x) # N: Revealed type is "None | builtins.int"
231+
232+
[case testNewRedefineGlobalVariableWithUnsupportedType]
233+
# flags: --allow-redefinition-new --local-partial-types
234+
x = 1
235+
236+
def f() -> None:
237+
global x
238+
x = "a" # E: Incompatible types in assignment (expression has type "str", variable has type "int")
239+
240+
reveal_type(x) # N: Revealed type is "builtins.int"
182241

183242
[case testNewRedefineParameterTypes]
184243
# flags: --allow-redefinition-new --local-partial-types
@@ -641,6 +700,7 @@ def f5() -> None:
641700
continue
642701
x = ""
643702
reveal_type(x) # N: Revealed type is "builtins.str"
703+
644704
[case testNewRedefineWhileLoopSimple]
645705
# flags: --allow-redefinition-new --local-partial-types
646706
def f() -> None:

test-data/unit/fine-grained.test

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6743,7 +6743,7 @@ class D:
67436743
class D:
67446744
y: int
67456745
[out]
6746-
b2.py:7: error: Argument 1 to "f" has incompatible type "D"; expected "P2" (diff)
6746+
b2.py:7: error: Argument 1 to "f" has incompatible type "D"; expected "P2"
67476747
b1.py:7: error: Argument 1 to "f" has incompatible type "D"; expected "P1"
67486748

67496749
[case testProtocolsInvalidateByRemovingBase]
@@ -11682,3 +11682,83 @@ def f() -> str: ...
1168211682
[out]
1168311683
==
1168411684
main:4: error: Incompatible return value type (got "str | None", expected "int | None")
11685+
11686+
[case testGlobalNoneWidenedInFuncWithRedefinition1]
11687+
import m
11688+
reveal_type(m.x)
11689+
11690+
[file m.py]
11691+
# mypy: allow-redefinition-new
11692+
import m2
11693+
11694+
x = None
11695+
11696+
def foo() -> None:
11697+
global x
11698+
x = m2.bar()
11699+
11700+
[file m2.py]
11701+
def bar() -> int: return 0
11702+
11703+
[file m2.py.2]
11704+
def bar() -> str: return "a"
11705+
11706+
[out]
11707+
main:2: note: Revealed type is "None | builtins.int"
11708+
==
11709+
main:2: note: Revealed type is "None | builtins.str"
11710+
11711+
[case testGlobalNoneWidenedInFuncWithRedefinition2]
11712+
import m
11713+
reveal_type(m.x)
11714+
11715+
[file m.py]
11716+
# mypy: allow-redefinition-new
11717+
import m2
11718+
11719+
x = None
11720+
11721+
def deco(f): return f
11722+
11723+
class C:
11724+
@deco
11725+
def foo(self) -> None:
11726+
global x
11727+
x = m2.bar()
11728+
11729+
[file m2.py]
11730+
def bar() -> int: return 0
11731+
11732+
[file m2.py.2]
11733+
def bar() -> str: return "a"
11734+
11735+
[out]
11736+
main:2: note: Revealed type is "None | builtins.int"
11737+
==
11738+
main:2: note: Revealed type is "None | builtins.str"
11739+
11740+
[case testGlobalNoneWidenedInFuncWithRedefinition3]
11741+
import m
11742+
reveal_type(m.x)
11743+
11744+
[file m.py]
11745+
# mypy: allow-redefinition-new
11746+
import m2
11747+
11748+
x = None
11749+
11750+
def foo() -> None:
11751+
def nested(self) -> None:
11752+
global x
11753+
x = m2.bar()
11754+
11755+
[file m2.py]
11756+
def bar() -> int: return 0
11757+
11758+
[file m2.py.2]
11759+
def bar() -> str: return "a"
11760+
11761+
[out]
11762+
main:2: note: Revealed type is "None | builtins.int"
11763+
==
11764+
main:2: note: Revealed type is "None | builtins.str"

0 commit comments

Comments
 (0)