Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mypy/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
from mypy_extensions import u8

# High-level cache layout format
CACHE_VERSION: Final = 5
CACHE_VERSION: Final = 6

# Type used internally to represent errors:
# (path, line, column, end_line, end_column, severity, message, code)
Expand Down
28 changes: 21 additions & 7 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4437,7 +4437,10 @@ def check_lvalue(
if (
self.options.allow_redefinition_new
and isinstance(lvalue.node, Var)
and lvalue.node.is_inferred
# We allow redefinition for function arguments inside function body.
# Although we normally do this for variables without annotation, users
# don't have a choice to leave a function argument without annotation.
and (lvalue.node.is_inferred or lvalue.node.is_argument)
):
inferred = lvalue.node
self.store_type(lvalue, lvalue_type)
Expand Down Expand Up @@ -4705,8 +4708,15 @@ def infer_rvalue_with_fallback_context(
# There are two cases where we want to try re-inferring r.h.s. in a fallback
# type context. First case is when redefinitions are allowed, and we got
# invalid type when using the preferred (empty) type context.
redefinition_fallback = inferred is not None and not is_valid_inferred_type(
rvalue_type, self.options
redefinition_fallback = (
inferred is not None
and not inferred.is_argument
and not is_valid_inferred_type(rvalue_type, self.options)
)
# For function arguments the preference order is opposite, and we use errors
# during type-checking as the fallback trigger.
argument_redefinition_fallback = (
inferred is not None and inferred.is_argument and local_errors.has_new_errors()
)
# Try re-inferring r.h.s. in empty context for union with explicit annotation,
# and use it results in a narrower type. This helps with various practical
Expand All @@ -4718,7 +4728,8 @@ def infer_rvalue_with_fallback_context(
)

# Skip literal types, as they have special logic (for better errors).
if (redefinition_fallback or union_fallback) and not is_literal_type_like(rvalue_type):
try_fallback = redefinition_fallback or union_fallback or argument_redefinition_fallback
if try_fallback and not is_literal_type_like(rvalue_type):
with (
self.msg.filter_errors(save_filtered_errors=True) as alt_local_errors,
self.local_type_map as alt_type_map,
Expand All @@ -4732,6 +4743,7 @@ def infer_rvalue_with_fallback_context(
and (
# For redefinition fallback we are fine getting not a subtype.
redefinition_fallback
or argument_redefinition_fallback
# Skip Any type, since it is special cased in binder.
or not isinstance(get_proper_type(alt_rvalue_type), AnyType)
and is_proper_subtype(alt_rvalue_type, rvalue_type)
Expand Down Expand Up @@ -4768,8 +4780,8 @@ def check_simple_assignment(
)

# If redefinitions are allowed (i.e. we have --allow-redefinition-new
# and a variable without annotation) then we start with an empty context,
# since this gives somewhat more intuitive behavior. The only exception
# and a variable without annotation) or if a variable has union type we
# try inferring r.h.s. twice with a fallback type context. The only exception
# is TypedDicts, they are often useless without context.
try_fallback = (
inferred is not None or isinstance(get_proper_type(lvalue_type), UnionType)
Expand All @@ -4780,7 +4792,9 @@ def check_simple_assignment(
rvalue, type_context=lvalue_type, always_allow_any=always_allow_any
)
else:
if inferred is not None:
# Prefer full type context for function arguments as this reduces
# false positives, see issue #19918 for discussion.
if inferred is not None and not inferred.is_argument:
preferred = None
fallback = lvalue_type
else:
Expand Down
1 change: 1 addition & 0 deletions mypy/fastparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -1145,6 +1145,7 @@ def make_argument(

var = Var(arg.arg, arg_type)
var.is_inferred = False
var.is_argument = True
argument = Argument(var, arg_type, self.visit(default), kind, pos_only)
argument.set_line(arg.lineno, arg.col_offset, arg.end_lineno, arg.end_col_offset)
return argument
Expand Down
9 changes: 4 additions & 5 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1292,6 +1292,7 @@ class Var(SymbolNode):
"has_explicit_value",
"allow_incompatible_override",
"invalid_partial_type",
"is_argument",
)

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

@property
def name(self) -> str:
Expand Down Expand Up @@ -1415,8 +1418,6 @@ def write(self, data: WriteBuffer) -> None:
write_flags(
data,
[
self.is_self,
self.is_cls,
self.is_initialized_in_class,
self.is_staticmethod,
self.is_classmethod,
Expand Down Expand Up @@ -1454,8 +1455,6 @@ def read(cls, data: ReadBuffer) -> Var:
v.setter_type = setter_type
v._fullname = read_str(data)
(
v.is_self,
v.is_cls,
v.is_initialized_in_class,
v.is_staticmethod,
v.is_classmethod,
Expand All @@ -1475,7 +1474,7 @@ def read(cls, data: ReadBuffer) -> Var:
v.from_module_getattr,
v.has_explicit_value,
v.allow_incompatible_override,
) = read_flags(data, num_flags=21)
) = read_flags(data, num_flags=19)
tag = read_tag(data)
if tag == LITERAL_COMPLEX:
v.final_value = complex(read_float_bare(data), read_float_bare(data))
Expand Down
33 changes: 31 additions & 2 deletions test-data/unit/check-redefine2.test
Original file line number Diff line number Diff line change
Expand Up @@ -562,8 +562,8 @@ def f2() -> None:
def f3(x) -> None:
if int():
x = 0
reveal_type(x) # N: Revealed type is "Any"
reveal_type(x) # N: Revealed type is "Any"
reveal_type(x) # N: Revealed type is "builtins.int"
reveal_type(x) # N: Revealed type is "Any | builtins.int"

[case tetNewRedefineDel]
# flags: --allow-redefinition-new --local-partial-types
Expand Down Expand Up @@ -1295,3 +1295,32 @@ if bool():
else:
x = [Sub(), Sub()]
reveal_type(x[0]) # N: Revealed type is "__main__.Sub"

[case testNewRedefineFunctionArgumentsFullContext]
# flags: --allow-redefinition-new --local-partial-types
from typing import Optional

def process(items: list[Optional[str]]) -> None:
if not items:
items = [None]
reveal_type(items) # N: Revealed type is "builtins.list[builtins.str | None]"
process(items) # OK

[case testNewRedefineFunctionArgumentsEmptyContext]
# flags: --allow-redefinition-new --local-partial-types
def process1(items: list[str]) -> None:
items = [item.split() for item in items]
reveal_type(items) # N: Revealed type is "builtins.list[builtins.list[builtins.str]]"

def process2(items: list[str]) -> None:
if bool():
items = [item.split() for item in items]
reveal_type(items) # N: Revealed type is "builtins.list[builtins.list[builtins.str]]"
reveal_type(items) # N: Revealed type is "builtins.list[builtins.str] | builtins.list[builtins.list[builtins.str]]"

def process3(items: list[str]) -> None:
if bool():
items = []
reveal_type(items) # N: Revealed type is "builtins.list[builtins.str]"
reveal_type(items) # N: Revealed type is "builtins.list[builtins.str]"
[builtins fixtures/primitives.pyi]