Skip to content

Commit c35cbd6

Browse files
authored
Allow redefinitions for function arguments (#20853)
Fixes #19918 Implementation is straightforward, but logic behind is essentially a compromise. Both empty context and full type context for redefinition have pros and cons. We select full context first for function arguments because: * It avoids false positives in at least some of the "covariant normalization" patterns. For example `list[str | None]` to `list[str]` _in a branch_ will infer `list[str | None]` instead of `list[str | None] | list[str]`. Unconditional normalization like `list[str | None]` to `list[str]` still doesn't work, but it doesn't work for empty context either. * Conceptually function arguments are in between annotated and annotated variables, in the sense that user doesn't really have an option to leave them unannotated if they want the redefinition semantics. So we allow redefinition but use type context behavior like for annotated variables. cc @JukkaL
1 parent 0bf9b14 commit c35cbd6

File tree

5 files changed

+58
-15
lines changed

5 files changed

+58
-15
lines changed

mypy/cache.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
from mypy_extensions import u8
7070

7171
# High-level cache layout format
72-
CACHE_VERSION: Final = 5
72+
CACHE_VERSION: Final = 6
7373

7474
# Type used internally to represent errors:
7575
# (path, line, column, end_line, end_column, severity, message, code)

mypy/checker.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4442,7 +4442,10 @@ def check_lvalue(
44424442
if (
44434443
self.options.allow_redefinition_new
44444444
and isinstance(lvalue.node, Var)
4445-
and lvalue.node.is_inferred
4445+
# We allow redefinition for function arguments inside function body.
4446+
# Although we normally do this for variables without annotation, users
4447+
# don't have a choice to leave a function argument without annotation.
4448+
and (lvalue.node.is_inferred or lvalue.node.is_argument)
44464449
):
44474450
inferred = lvalue.node
44484451
self.store_type(lvalue, lvalue_type)
@@ -4710,8 +4713,15 @@ def infer_rvalue_with_fallback_context(
47104713
# There are two cases where we want to try re-inferring r.h.s. in a fallback
47114714
# type context. First case is when redefinitions are allowed, and we got
47124715
# invalid type when using the preferred (empty) type context.
4713-
redefinition_fallback = inferred is not None and not is_valid_inferred_type(
4714-
rvalue_type, self.options
4716+
redefinition_fallback = (
4717+
inferred is not None
4718+
and not inferred.is_argument
4719+
and not is_valid_inferred_type(rvalue_type, self.options)
4720+
)
4721+
# For function arguments the preference order is opposite, and we use errors
4722+
# during type-checking as the fallback trigger.
4723+
argument_redefinition_fallback = (
4724+
inferred is not None and inferred.is_argument and local_errors.has_new_errors()
47154725
)
47164726
# Try re-inferring r.h.s. in empty context for union with explicit annotation,
47174727
# and use it results in a narrower type. This helps with various practical
@@ -4723,7 +4733,8 @@ def infer_rvalue_with_fallback_context(
47234733
)
47244734

47254735
# Skip literal types, as they have special logic (for better errors).
4726-
if (redefinition_fallback or union_fallback) and not is_literal_type_like(rvalue_type):
4736+
try_fallback = redefinition_fallback or union_fallback or argument_redefinition_fallback
4737+
if try_fallback and not is_literal_type_like(rvalue_type):
47274738
with (
47284739
self.msg.filter_errors(save_filtered_errors=True) as alt_local_errors,
47294740
self.local_type_map as alt_type_map,
@@ -4737,6 +4748,7 @@ def infer_rvalue_with_fallback_context(
47374748
and (
47384749
# For redefinition fallback we are fine getting not a subtype.
47394750
redefinition_fallback
4751+
or argument_redefinition_fallback
47404752
# Skip Any type, since it is special cased in binder.
47414753
or not isinstance(get_proper_type(alt_rvalue_type), AnyType)
47424754
and is_proper_subtype(alt_rvalue_type, rvalue_type)
@@ -4773,8 +4785,8 @@ def check_simple_assignment(
47734785
)
47744786

47754787
# If redefinitions are allowed (i.e. we have --allow-redefinition-new
4776-
# and a variable without annotation) then we start with an empty context,
4777-
# since this gives somewhat more intuitive behavior. The only exception
4788+
# and a variable without annotation) or if a variable has union type we
4789+
# try inferring r.h.s. twice with a fallback type context. The only exception
47784790
# is TypedDicts, they are often useless without context.
47794791
try_fallback = (
47804792
inferred is not None or isinstance(get_proper_type(lvalue_type), UnionType)
@@ -4785,7 +4797,9 @@ def check_simple_assignment(
47854797
rvalue, type_context=lvalue_type, always_allow_any=always_allow_any
47864798
)
47874799
else:
4788-
if inferred is not None:
4800+
# Prefer full type context for function arguments as this reduces
4801+
# false positives, see issue #19918 for discussion.
4802+
if inferred is not None and not inferred.is_argument:
47894803
preferred = None
47904804
fallback = lvalue_type
47914805
else:

mypy/fastparse.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1145,6 +1145,7 @@ def make_argument(
11451145

11461146
var = Var(arg.arg, arg_type)
11471147
var.is_inferred = False
1148+
var.is_argument = True
11481149
argument = Argument(var, arg_type, self.visit(default), kind, pos_only)
11491150
argument.set_line(arg.lineno, arg.col_offset, arg.end_lineno, arg.end_col_offset)
11501151
return argument

mypy/nodes.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1292,6 +1292,7 @@ class Var(SymbolNode):
12921292
"has_explicit_value",
12931293
"allow_incompatible_override",
12941294
"invalid_partial_type",
1295+
"is_argument",
12951296
)
12961297

12971298
__match_args__ = ("name", "type", "final_value")
@@ -1352,6 +1353,8 @@ def __init__(self, name: str, type: mypy.types.Type | None = None) -> None:
13521353
# If True, this means we didn't manage to infer full type and fall back to
13531354
# something like list[Any]. We may decide to not use such types as context.
13541355
self.invalid_partial_type = False
1356+
# Is it a variable symbol for a function argument?
1357+
self.is_argument = False
13551358

13561359
@property
13571360
def name(self) -> str:
@@ -1415,8 +1418,6 @@ def write(self, data: WriteBuffer) -> None:
14151418
write_flags(
14161419
data,
14171420
[
1418-
self.is_self,
1419-
self.is_cls,
14201421
self.is_initialized_in_class,
14211422
self.is_staticmethod,
14221423
self.is_classmethod,
@@ -1454,8 +1455,6 @@ def read(cls, data: ReadBuffer) -> Var:
14541455
v.setter_type = setter_type
14551456
v._fullname = read_str(data)
14561457
(
1457-
v.is_self,
1458-
v.is_cls,
14591458
v.is_initialized_in_class,
14601459
v.is_staticmethod,
14611460
v.is_classmethod,
@@ -1475,7 +1474,7 @@ def read(cls, data: ReadBuffer) -> Var:
14751474
v.from_module_getattr,
14761475
v.has_explicit_value,
14771476
v.allow_incompatible_override,
1478-
) = read_flags(data, num_flags=21)
1477+
) = read_flags(data, num_flags=19)
14791478
tag = read_tag(data)
14801479
if tag == LITERAL_COMPLEX:
14811480
v.final_value = complex(read_float_bare(data), read_float_bare(data))

test-data/unit/check-redefine2.test

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -562,8 +562,8 @@ def f2() -> None:
562562
def f3(x) -> None:
563563
if int():
564564
x = 0
565-
reveal_type(x) # N: Revealed type is "Any"
566-
reveal_type(x) # N: Revealed type is "Any"
565+
reveal_type(x) # N: Revealed type is "builtins.int"
566+
reveal_type(x) # N: Revealed type is "Any | builtins.int"
567567

568568
[case tetNewRedefineDel]
569569
# flags: --allow-redefinition-new --local-partial-types
@@ -1310,3 +1310,32 @@ if bool():
13101310
else:
13111311
x = [Sub(), Sub()]
13121312
reveal_type(x[0]) # N: Revealed type is "__main__.Sub"
1313+
1314+
[case testNewRedefineFunctionArgumentsFullContext]
1315+
# flags: --allow-redefinition-new --local-partial-types
1316+
from typing import Optional
1317+
1318+
def process(items: list[Optional[str]]) -> None:
1319+
if not items:
1320+
items = [None]
1321+
reveal_type(items) # N: Revealed type is "builtins.list[builtins.str | None]"
1322+
process(items) # OK
1323+
1324+
[case testNewRedefineFunctionArgumentsEmptyContext]
1325+
# flags: --allow-redefinition-new --local-partial-types
1326+
def process1(items: list[str]) -> None:
1327+
items = [item.split() for item in items]
1328+
reveal_type(items) # N: Revealed type is "builtins.list[builtins.list[builtins.str]]"
1329+
1330+
def process2(items: list[str]) -> None:
1331+
if bool():
1332+
items = [item.split() for item in items]
1333+
reveal_type(items) # N: Revealed type is "builtins.list[builtins.list[builtins.str]]"
1334+
reveal_type(items) # N: Revealed type is "builtins.list[builtins.str] | builtins.list[builtins.list[builtins.str]]"
1335+
1336+
def process3(items: list[str]) -> None:
1337+
if bool():
1338+
items = []
1339+
reveal_type(items) # N: Revealed type is "builtins.list[builtins.str]"
1340+
reveal_type(items) # N: Revealed type is "builtins.list[builtins.str]"
1341+
[builtins fixtures/primitives.pyi]

0 commit comments

Comments
 (0)