diff --git a/mypy/binder.py b/mypy/binder.py index 384bdca728b2..d3482d1dad4f 100644 --- a/mypy/binder.py +++ b/mypy/binder.py @@ -7,8 +7,9 @@ from typing_extensions import TypeAlias as _TypeAlias from mypy.erasetype import remove_instance_last_known_values -from mypy.literals import Key, literal, literal_hash, subkeys +from mypy.literals import Key, extract_var_from_literal_hash, literal, literal_hash, subkeys from mypy.nodes import Expression, IndexExpr, MemberExpr, NameExpr, RefExpr, TypeInfo, Var +from mypy.options import Options from mypy.subtypes import is_same_type, is_subtype from mypy.typeops import make_simplified_union from mypy.types import ( @@ -39,6 +40,7 @@ class CurrentType(NamedTuple): class Frame: """A Frame represents a specific point in the execution of a program. + It carries information about the current types of expressions at that point, arising either from assignments to those expressions or the result of isinstance checks and other type narrowing @@ -97,7 +99,7 @@ class A: # This maps an expression to a list of bound types for every item in the union type. type_assignments: Assigns | None = None - def __init__(self) -> None: + def __init__(self, options: Options) -> None: # Each frame gets an increasing, distinct id. self.next_id = 1 @@ -131,6 +133,11 @@ def __init__(self) -> None: self.break_frames: list[int] = [] self.continue_frames: list[int] = [] + # If True, initial assignment to a simple variable (e.g. "x", but not "x.y") + # is added to the binder. This allows more precise narrowing and more + # flexible inference of variable types (--allow-redefinition-new). + self.bind_all = options.allow_redefinition_new + def _get_id(self) -> int: self.next_id += 1 return self.next_id @@ -226,12 +233,20 @@ def update_from_options(self, frames: list[Frame]) -> bool: for key in keys: current_value = self._get(key) resulting_values = [f.types.get(key, current_value) for f in frames] - if any(x is None for x in resulting_values): + # Keys can be narrowed using two different semantics. The new semantics + # is enabled for plain variables when bind_all is true, and it allows + # variable types to be widened using subsequent assignments. This is + # tricky to support for instance attributes (primarily due to deferrals), + # so we don't use it for them. + old_semantics = not self.bind_all or extract_var_from_literal_hash(key) is None + if old_semantics and any(x is None for x in resulting_values): # We didn't know anything about key before # (current_value must be None), and we still don't # know anything about key in at least one possible frame. continue + resulting_values = [x for x in resulting_values if x is not None] + if all_reachable and all( x is not None and not x.from_assignment for x in resulting_values ): @@ -278,7 +293,11 @@ def update_from_options(self, frames: list[Frame]) -> bool: # still equivalent to such type). if isinstance(type, UnionType): type = collapse_variadic_union(type) - if isinstance(type, ProperType) and isinstance(type, UnionType): + if ( + old_semantics + and isinstance(type, ProperType) + and isinstance(type, UnionType) + ): # Simplify away any extra Any's that were added to the declared # type when popping a frame. simplified = UnionType.make_union( diff --git a/mypy/build.py b/mypy/build.py index f6272ed808cf..355ba861385e 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -2240,8 +2240,10 @@ def semantic_analysis_pass1(self) -> None: # TODO: Do this while constructing the AST? self.tree.names = SymbolTable() if not self.tree.is_stub: - # Always perform some low-key variable renaming - self.tree.accept(LimitedVariableRenameVisitor()) + if not self.options.allow_redefinition_new: + # Perform some low-key variable renaming when assignments can't + # widen inferred types + self.tree.accept(LimitedVariableRenameVisitor()) if options.allow_redefinition: # Perform more renaming across the AST to allow variable redefinitions self.tree.accept(VariableRenameVisitor()) diff --git a/mypy/checker.py b/mypy/checker.py index b8d5bbd4fa2d..59d7ba5c02f4 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -316,6 +316,9 @@ class TypeChecker(NodeVisitor[None], CheckerPluginInterface): # Vars for which partial type errors are already reported # (to avoid logically duplicate errors with different error context). partial_reported: set[Var] + # Short names of Var nodes whose previous inferred type has been widened via assignment. + # NOTE: The names might not be unique, they are only for debugging purposes. + widened_vars: list[str] globals: SymbolTable modules: dict[str, MypyFile] # Nodes that couldn't be checked because some types weren't available. We'll run @@ -376,7 +379,7 @@ def __init__( self.plugin = plugin self.tscope = Scope() self.scope = CheckerScope(tree) - self.binder = ConditionalTypeBinder() + self.binder = ConditionalTypeBinder(options) self.globals = tree.names self.return_types = [] self.dynamic_funcs = [] @@ -384,6 +387,7 @@ def __init__( self.partial_reported = set() self.var_decl_frames = {} self.deferred_nodes = [] + self.widened_vars = [] self._type_maps = [{}] self.module_refs = set() self.pass_num = 0 @@ -430,7 +434,7 @@ def reset(self) -> None: # TODO: verify this is still actually worth it over creating new checkers self.partial_reported.clear() self.module_refs.clear() - self.binder = ConditionalTypeBinder() + self.binder = ConditionalTypeBinder(self.options) self._type_maps[1:] = [] self._type_maps[0].clear() self.temp_type_map = None @@ -523,6 +527,7 @@ def check_second_pass( return True def check_partial(self, node: DeferredNodeType | FineGrainedDeferredNodeType) -> None: + self.widened_vars = [] if isinstance(node, MypyFile): self.check_top_level(node) else: @@ -592,6 +597,10 @@ def accept_loop( # Check for potential decreases in the number of partial types so as not to stop the # iteration too early: partials_old = sum(len(pts.map) for pts in self.partial_types) + # Check if assignment widened the inferred type of a variable; in this case we + # need to iterate again (we only do one extra iteration, since this could go + # on without bound otherwise) + widened_old = len(self.widened_vars) # Disable error types that we cannot safely identify in intermediate iteration steps: warn_unreachable = self.options.warn_unreachable @@ -599,6 +608,7 @@ def accept_loop( self.options.warn_unreachable = False self.options.enabled_error_codes.discard(codes.REDUNDANT_EXPR) + iter = 1 while True: with self.binder.frame_context(can_skip=True, break_frame=2, continue_frame=1): if on_enter_body is not None: @@ -606,9 +616,24 @@ def accept_loop( self.accept(body) partials_new = sum(len(pts.map) for pts in self.partial_types) - if (partials_new == partials_old) and not self.binder.last_pop_changed: + widened_new = len(self.widened_vars) + # Perform multiple iterations if something changed that might affect + # inferred types. Also limit the number of iterations. The limits are + # somewhat arbitrary, but they were chosen to 1) avoid slowdown from + # multiple iterations in common cases and 2) support common, valid use + # cases. Limits are needed since otherwise we could infer infinitely + # complex types. + if ( + (partials_new == partials_old) + and (not self.binder.last_pop_changed or iter > 3) + and (widened_new == widened_old or iter > 1) + ): break partials_old = partials_new + widened_old = widened_new + iter += 1 + if iter == 20: + raise RuntimeError("Too many iterations when checking a loop") # If necessary, reset the modified options and make up for the postponed error checks: self.options.warn_unreachable = warn_unreachable @@ -1189,7 +1214,7 @@ def check_func_def( original_typ = typ for item, typ in expanded: old_binder = self.binder - self.binder = ConditionalTypeBinder() + self.binder = ConditionalTypeBinder(self.options) with self.binder.top_frame_context(): defn.expanded.append(item) @@ -1377,6 +1402,17 @@ def check_func_def( new_frame = self.binder.push_frame() new_frame.types[key] = narrowed_type self.binder.declarations[key] = old_binder.declarations[key] + + if self.options.allow_redefinition_new and not self.is_stub: + # Add formal argument types to the binder. + for arg in defn.arguments: + # TODO: Add these directly using a fast path (possibly "put") + v = arg.variable + if v.type is not None: + n = NameExpr(v.name) + n.node = v + self.binder.assign_type(n, v.type, v.type) + with self.scope.push_function(defn): # We suppress reachability warnings for empty generator functions # (return; yield) which have a "yield" that's unreachable by definition @@ -2562,7 +2598,7 @@ def visit_class_def(self, defn: ClassDef) -> None: self.fail(message_registry.CANNOT_INHERIT_FROM_FINAL.format(base.name), defn) with self.tscope.class_scope(defn.info), self.enter_partial_types(is_class=True): old_binder = self.binder - self.binder = ConditionalTypeBinder() + self.binder = ConditionalTypeBinder(self.options) with self.binder.top_frame_context(): with self.scope.push_class(defn.info): self.accept(defn.defs) @@ -3220,7 +3256,9 @@ def check_assignment( return var = lvalue_type.var - if is_valid_inferred_type(rvalue_type, is_lvalue_final=var.is_final): + if is_valid_inferred_type( + rvalue_type, self.options, is_lvalue_final=var.is_final + ): partial_types = self.find_partial_types(var) if partial_types is not None: if not self.current_node_deferred: @@ -3266,7 +3304,8 @@ def check_assignment( # unpleasant, and a generalization of this would # be an improvement! if ( - is_literal_none(rvalue) + not self.options.allow_redefinition_new + and is_literal_none(rvalue) and isinstance(lvalue, NameExpr) and lvalue.kind == LDEF and isinstance(lvalue.node, Var) @@ -3286,7 +3325,12 @@ def check_assignment( lvalue_type = make_optional_type(lvalue_type) self.set_inferred_type(lvalue.node, lvalue, lvalue_type) - rvalue_type = self.check_simple_assignment(lvalue_type, rvalue, context=rvalue) + rvalue_type, lvalue_type = self.check_simple_assignment( + lvalue_type, rvalue, context=rvalue, inferred=inferred, lvalue=lvalue + ) + # The above call may update inferred variable type. Prevent further + # inference. + inferred = None # Special case: only non-abstract non-protocol classes can be assigned to # variables with explicit type Type[A], where A is protocol or abstract. @@ -3319,6 +3363,9 @@ def check_assignment( and lvalue_type is not None ): lvalue.node.type = remove_instance_last_known_values(lvalue_type) + elif self.options.allow_redefinition_new and lvalue_type is not None: + # TODO: Can we use put() here? + self.binder.assign_type(lvalue, lvalue_type, lvalue_type) elif index_lvalue: self.check_indexed_assignment(index_lvalue, rvalue, lvalue) @@ -3400,7 +3447,9 @@ def try_infer_partial_generic_type_from_assignment( rvalue_type = self.expr_checker.accept(rvalue) rvalue_type = get_proper_type(rvalue_type) if isinstance(rvalue_type, Instance): - if rvalue_type.type == typ.type and is_valid_inferred_type(rvalue_type): + if rvalue_type.type == typ.type and is_valid_inferred_type( + rvalue_type, self.options + ): var.type = rvalue_type del partial_types[var] elif isinstance(rvalue_type, AnyType): @@ -4284,6 +4333,12 @@ def check_lvalue(self, lvalue: Lvalue) -> tuple[Type | None, IndexExpr | None, V self.store_type(lvalue, lvalue_type) elif isinstance(lvalue, NameExpr): lvalue_type = self.expr_checker.analyze_ref_expr(lvalue, lvalue=True) + if ( + self.options.allow_redefinition_new + and isinstance(lvalue.node, Var) + and lvalue.node.is_inferred + ): + inferred = lvalue.node self.store_type(lvalue, lvalue_type) elif isinstance(lvalue, (TupleExpr, ListExpr)): types = [ @@ -4324,14 +4379,19 @@ def infer_variable_type( if isinstance(init_type, DeletedType): self.msg.deleted_as_rvalue(init_type, context) elif ( - not is_valid_inferred_type(init_type, is_lvalue_final=name.is_final) + not is_valid_inferred_type( + init_type, + self.options, + is_lvalue_final=name.is_final, + is_lvalue_member=isinstance(lvalue, MemberExpr), + ) and not self.no_partial_types ): # We cannot use the type of the initialization expression for full type # inference (it's not specific enough), but we might be able to give # partial type which will be made more specific later. A partial type # gets generated in assignment like 'x = []' where item type is not known. - if not self.infer_partial_type(name, lvalue, init_type): + if name.name != "_" and not self.infer_partial_type(name, lvalue, init_type): self.msg.need_annotation_for_var(name, context, self.options.python_version) self.set_inference_error_fallback_type(name, lvalue, init_type) elif ( @@ -4351,10 +4411,16 @@ def infer_variable_type( init_type = strip_type(init_type) self.set_inferred_type(name, lvalue, init_type) + if self.options.allow_redefinition_new: + self.binder.assign_type(lvalue, init_type, init_type) def infer_partial_type(self, name: Var, lvalue: Lvalue, init_type: Type) -> bool: init_type = get_proper_type(init_type) - if isinstance(init_type, NoneType): + if isinstance(init_type, NoneType) and ( + isinstance(lvalue, MemberExpr) or not self.options.allow_redefinition_new + ): + # When using --allow-redefinition-new, None types aren't special + # when inferring simple variable types. partial_type = PartialType(None, name) elif isinstance(init_type, Instance): fullname = init_type.type.fullname @@ -4484,17 +4550,64 @@ def check_simple_assignment( rvalue_name: str = "expression", *, notes: list[str] | None = None, - ) -> Type: + lvalue: Expression | None = None, + inferred: Var | None = None, + ) -> tuple[Type, Type | None]: if self.is_stub and isinstance(rvalue, EllipsisExpr): # '...' is always a valid initializer in a stub. - return AnyType(TypeOfAny.special_form) + return AnyType(TypeOfAny.special_form), lvalue_type else: always_allow_any = lvalue_type is not None and not isinstance( get_proper_type(lvalue_type), AnyType ) + if inferred is None or is_typeddict_type_context(lvalue_type): + type_context = lvalue_type + else: + type_context = None rvalue_type = self.expr_checker.accept( - rvalue, lvalue_type, always_allow_any=always_allow_any + rvalue, type_context=type_context, always_allow_any=always_allow_any ) + if ( + lvalue_type is not None + and type_context is None + and not is_valid_inferred_type(rvalue_type, self.options) + ): + # Inference in an empty type context didn't produce a valid type, so + # try using lvalue type as context instead. + rvalue_type = self.expr_checker.accept( + rvalue, type_context=lvalue_type, always_allow_any=always_allow_any + ) + if not is_valid_inferred_type(rvalue_type, self.options) and inferred is not None: + self.msg.need_annotation_for_var( + inferred, context, self.options.python_version + ) + rvalue_type = rvalue_type.accept(SetNothingToAny()) + + if ( + isinstance(lvalue, NameExpr) + and inferred is not None + and inferred.type is not None + and not inferred.is_final + ): + new_inferred = remove_instance_last_known_values(rvalue_type) + if not is_same_type(inferred.type, new_inferred): + # Should we widen the inferred type or the lvalue? Variables defined + # at module level or class bodies can't be widened in functions, or + # in another module. + if not self.refers_to_different_scope(lvalue): + lvalue_type = make_simplified_union([inferred.type, new_inferred]) + if not is_same_type(lvalue_type, inferred.type) and not isinstance( + inferred.type, PartialType + ): + # Widen the type to the union of original and new type. + self.widened_vars.append(inferred.name) + self.set_inferred_type(inferred, lvalue, lvalue_type) + self.binder.put(lvalue, rvalue_type) + # TODO: A bit hacky, maybe add a binder method that does put and + # updates declaration? + lit = literal_hash(lvalue) + if lit is not None: + self.binder.declarations[lit] = lvalue_type if ( isinstance(get_proper_type(lvalue_type), UnionType) # Skip literal types, as they have special logic (for better errors). @@ -4514,7 +4627,7 @@ def check_simple_assignment( not local_errors.has_new_errors() # Skip Any type, since it is special cased in binder. and not isinstance(get_proper_type(alt_rvalue_type), AnyType) - and is_valid_inferred_type(alt_rvalue_type) + and is_valid_inferred_type(alt_rvalue_type, self.options) and is_proper_subtype(alt_rvalue_type, rvalue_type) ): rvalue_type = alt_rvalue_type @@ -4534,7 +4647,19 @@ def check_simple_assignment( f"{lvalue_name} has type", notes=notes, ) - return rvalue_type + return rvalue_type, lvalue_type + + def refers_to_different_scope(self, name: NameExpr) -> bool: + if name.kind == LDEF: + # TODO: Consider reference to outer function as a different scope? + return False + elif self.scope.top_level_function() is not None: + # A non-local reference from within a function must refer to a different scope + return True + elif name.kind == GDEF and name.fullname.rpartition(".")[0] != self.tree.fullname: + # Reference to global definition from another module + return True + return False def check_member_assignment( self, @@ -4561,7 +4686,7 @@ def check_member_assignment( if (isinstance(instance_type, FunctionLike) and instance_type.is_type_obj()) or isinstance( instance_type, TypeType ): - rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context) + rvalue_type, _ = self.check_simple_assignment(attribute_type, rvalue, context) return rvalue_type, attribute_type, True with self.msg.filter_errors(filter_deprecated=True): @@ -4572,7 +4697,7 @@ def check_member_assignment( if not isinstance(attribute_type, Instance): # TODO: support __set__() for union types. - rvalue_type = self.check_simple_assignment(attribute_type, rvalue, context) + rvalue_type, _ = self.check_simple_assignment(attribute_type, rvalue, context) return rvalue_type, attribute_type, use_binder mx = MemberContext( @@ -4591,7 +4716,7 @@ def check_member_assignment( # the return type of __get__. This doesn't match the python semantics, # (which allow you to override the descriptor with any value), but preserves # the type of accessing the attribute (even after the override). - rvalue_type = self.check_simple_assignment(get_type, rvalue, context) + rvalue_type, _ = self.check_simple_assignment(get_type, rvalue, context) return rvalue_type, get_type, use_binder dunder_set = attribute_type.type.get_method("__set__") @@ -4667,7 +4792,7 @@ def check_member_assignment( # and '__get__' type is narrower than '__set__', then we invoke the binder to narrow type # by this assignment. Technically, this is not safe, but in practice this is # what a user expects. - rvalue_type = self.check_simple_assignment(set_type, rvalue, context) + rvalue_type, _ = self.check_simple_assignment(set_type, rvalue, context) infer = is_subtype(rvalue_type, get_type) and is_subtype(get_type, set_type) return rvalue_type if infer else set_type, get_type, infer @@ -4697,6 +4822,19 @@ def check_indexed_assignment( if isinstance(res_type, UninhabitedType) and not res_type.ambiguous: self.binder.unreachable() + def replace_partial_type( + self, var: Var, new_type: Type, partial_types: dict[Var, Context] + ) -> None: + """Replace the partial type of var with a non-partial type.""" + var.type = new_type + del partial_types[var] + if self.options.allow_redefinition_new: + # When using --allow-redefinition-new, binder tracks all types of + # simple variables. + n = NameExpr(var.name) + n.node = var + self.binder.assign_type(n, new_type, new_type) + def try_infer_partial_type_from_indexed_assignment( self, lvalue: IndexExpr, rvalue: Expression ) -> None: @@ -4724,8 +4862,8 @@ def try_infer_partial_type_from_indexed_assignment( key_type = self.expr_checker.accept(lvalue.index) value_type = self.expr_checker.accept(rvalue) if ( - is_valid_inferred_type(key_type) - and is_valid_inferred_type(value_type) + is_valid_inferred_type(key_type, self.options) + and is_valid_inferred_type(value_type, self.options) and not self.current_node_deferred and not ( typename == "collections.defaultdict" @@ -4733,8 +4871,8 @@ def try_infer_partial_type_from_indexed_assignment( and not is_equivalent(value_type, var.type.value_type) ) ): - var.type = self.named_generic_type(typename, [key_type, value_type]) - del partial_types[var] + new_type = self.named_generic_type(typename, [key_type, value_type]) + self.replace_partial_type(var, new_type, partial_types) def type_requires_usage(self, typ: Type) -> tuple[str, ErrorCode] | None: """Some types require usage in all cases. The classic example is @@ -5058,8 +5196,13 @@ def visit_try_without_finally(self, s: TryStmt, try_frame: bool) -> None: # try/except block. source = var.name if isinstance(var.node, Var): - var.node.type = DeletedType(source=source) - self.binder.cleanse(var) + new_type = DeletedType(source=source) + var.node.type = new_type + if self.options.allow_redefinition_new: + # TODO: Should we use put() here? + self.binder.assign_type(var, new_type, new_type) + if not self.options.allow_redefinition_new: + self.binder.cleanse(var) if s.else_body: self.accept(s.else_body) @@ -5448,11 +5591,13 @@ def visit_match_stmt(self, s: MatchStmt) -> None: # Create a dummy subject expression to handle cases where a match statement's subject # is not a literal value. This lets us correctly narrow types and check exhaustivity # This is hack! - id = s.subject.callee.fullname if isinstance(s.subject.callee, RefExpr) else "" - name = "dummy-match-" + id - v = Var(name) - named_subject = NameExpr(name) - named_subject.node = v + if s.subject_dummy is None: + id = s.subject.callee.fullname if isinstance(s.subject.callee, RefExpr) else "" + name = "dummy-match-" + id + v = Var(name) + s.subject_dummy = NameExpr(name) + s.subject_dummy.node = v + named_subject = s.subject_dummy else: named_subject = s.subject @@ -8473,7 +8618,9 @@ def _find_inplace_method(inst: Instance, method: str, operator: str) -> str | No return None -def is_valid_inferred_type(typ: Type, is_lvalue_final: bool = False) -> bool: +def is_valid_inferred_type( + typ: Type, options: Options, is_lvalue_final: bool = False, is_lvalue_member: bool = False +) -> bool: """Is an inferred type valid and needs no further refinement? Examples of invalid types include the None type (when we are not assigning @@ -8492,7 +8639,7 @@ def is_valid_inferred_type(typ: Type, is_lvalue_final: bool = False) -> bool: # type could either be NoneType or an Optional type, depending on # the context. This resolution happens in leave_partial_types when # we pop a partial types scope. - return is_lvalue_final + return is_lvalue_final or (not is_lvalue_member and options.allow_redefinition_new) elif isinstance(proper_type, UninhabitedType): return False return not typ.accept(InvalidInferredTypes()) @@ -9096,3 +9243,10 @@ def _ambiguous_enum_variants(types: list[Type]) -> set[str]: else: result.add("") return result + + +def is_typeddict_type_context(lvalue_type: Type | None) -> bool: + if lvalue_type is None: + return False + lvalue_proper = get_proper_type(lvalue_type) + return isinstance(lvalue_proper, TypedDictType) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 1017009ce7ab..80471a04469c 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -1117,8 +1117,7 @@ def try_infer_partial_type(self, e: CallExpr) -> None: typ = self.try_infer_partial_value_type_from_call(e, callee.name, var) # Var may be deleted from partial_types in try_infer_partial_value_type_from_call if typ is not None and var in partial_types: - var.type = typ - del partial_types[var] + self.chk.replace_partial_type(var, typ, partial_types) elif isinstance(callee.expr, IndexExpr) and isinstance(callee.expr.base, RefExpr): # Call 'x[y].method(...)'; may infer type of 'x' if it's a partial defaultdict. if callee.expr.analyzed is not None: @@ -1136,12 +1135,12 @@ def try_infer_partial_type(self, e: CallExpr) -> None: if value_type is not None: # Infer key type. key_type = self.accept(index) - if mypy.checker.is_valid_inferred_type(key_type): + if mypy.checker.is_valid_inferred_type(key_type, self.chk.options): # Store inferred partial type. assert partial_type.type is not None typename = partial_type.type.fullname - var.type = self.chk.named_generic_type(typename, [key_type, value_type]) - del partial_types[var] + new_type = self.chk.named_generic_type(typename, [key_type, value_type]) + self.chk.replace_partial_type(var, new_type, partial_types) def get_partial_var(self, ref: RefExpr) -> tuple[Var, dict[Var, Context]] | None: var = ref.node @@ -1176,7 +1175,7 @@ def try_infer_partial_value_type_from_call( and e.arg_kinds == [ARG_POS] ): item_type = self.accept(e.args[0]) - if mypy.checker.is_valid_inferred_type(item_type): + if mypy.checker.is_valid_inferred_type(item_type, self.chk.options): return self.chk.named_generic_type(typename, [item_type]) elif ( typename in self.container_args @@ -1188,7 +1187,7 @@ def try_infer_partial_value_type_from_call( arg_typename = arg_type.type.fullname if arg_typename in self.container_args[typename][methodname]: if all( - mypy.checker.is_valid_inferred_type(item_type) + mypy.checker.is_valid_inferred_type(item_type, self.chk.options) for item_type in arg_type.args ): return self.chk.named_generic_type(typename, list(arg_type.args)) @@ -5787,6 +5786,14 @@ def check_for_comp(self, e: GeneratorExpr | DictionaryComprehension) -> None: _, sequence_type = self.chk.analyze_async_iterable_item_type(sequence) else: _, sequence_type = self.chk.analyze_iterable_item_type(sequence) + if ( + isinstance(get_proper_type(sequence_type), UninhabitedType) + and isinstance(index, NameExpr) + and index.name == "_" + ): + # To preserve backward compatibility, avoid inferring Never for "_" + sequence_type = AnyType(TypeOfAny.special_form) + self.chk.analyze_index_variables(index, sequence_type, True, e) for condition in conditions: self.accept(condition) @@ -5830,7 +5837,7 @@ def visit_conditional_expr(self, e: ConditionalExpr, allow_none_return: bool = F else_map, e.else_expr, context=ctx, allow_none_return=allow_none_return ) - if not mypy.checker.is_valid_inferred_type(if_type): + if not mypy.checker.is_valid_inferred_type(if_type, self.chk.options): # Analyze the right branch disregarding the left branch. else_type = full_context_else_type # we want to keep the narrowest value of else_type for union'ing the branches diff --git a/mypy/main.py b/mypy/main.py index 77d8cefe9866..ad836a5ddc19 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -93,6 +93,13 @@ def main( stdout, stderr, options.hide_error_codes, hide_success=bool(options.output) ) + if options.allow_redefinition_new and not options.local_partial_types: + fail( + "error: --local-partial-types must be enabled if using --allow-redefinition-new", + stderr, + options, + ) + if options.install_types and (stdout is not sys.stdout or stderr is not sys.stderr): # Since --install-types performs user input, we want regular stdout and stderr. fail("error: --install-types not supported in this mode of running mypy", stderr, options) @@ -856,7 +863,15 @@ def add_invertible_flag( "--allow-redefinition", default=False, strict_flag=False, - help="Allow unconditional variable redefinition with a new type", + help="Allow restricted, unconditional variable redefinition with a new type", + group=strictness_group, + ) + + add_invertible_flag( + "--allow-redefinition-new", + default=False, + strict_flag=False, + help=argparse.SUPPRESS, # This is still very experimental group=strictness_group, ) diff --git a/mypy/nodes.py b/mypy/nodes.py index 6487ee4b745c..678c28f14684 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -1073,7 +1073,8 @@ def fullname(self) -> str: return self._fullname def __repr__(self) -> str: - return f"" + name = self.fullname or self.name + return f"" def accept(self, visitor: NodeVisitor[T]) -> T: return visitor.visit_var(self) @@ -1637,11 +1638,12 @@ def accept(self, visitor: StatementVisitor[T]) -> T: class MatchStmt(Statement): - __slots__ = ("subject", "patterns", "guards", "bodies") + __slots__ = ("subject", "subject_dummy", "patterns", "guards", "bodies") __match_args__ = ("subject", "patterns", "guards", "bodies") subject: Expression + subject_dummy: NameExpr | None patterns: list[Pattern] guards: list[Expression | None] bodies: list[Block] @@ -1656,6 +1658,7 @@ def __init__( super().__init__() assert len(patterns) == len(guards) == len(bodies) self.subject = subject + self.subject_dummy = None self.patterns = patterns self.guards = guards self.bodies = bodies diff --git a/mypy/options.py b/mypy/options.py index c1047657dd77..27b583722568 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -22,6 +22,7 @@ class BuildType: PER_MODULE_OPTIONS: Final = { # Please keep this list sorted "allow_redefinition", + "allow_redefinition_new", "allow_untyped_globals", "always_false", "always_true", @@ -219,6 +220,10 @@ def __init__(self) -> None: # and the same nesting level as the initialization self.allow_redefinition = False + # Allow flexible variable redefinition with an arbitrary type, in different + # blocks and and at different nesting levels + self.allow_redefinition_new = False + # Prohibit equality, identity, and container checks for non-overlapping types. # This makes 1 == '1', 1 in ['1'], and 1 is '1' errors. self.strict_equality = False diff --git a/mypy/plugins/functools.py b/mypy/plugins/functools.py index c435dde7fde7..25a8c83007ba 100644 --- a/mypy/plugins/functools.py +++ b/mypy/plugins/functools.py @@ -276,7 +276,7 @@ def handle_partial_with_callee(ctx: mypy.plugin.FunctionContext, callee: Type) - for i, actuals in enumerate(formal_to_actual): if len(bound.arg_types) == len(fn_type.arg_types): arg_type = bound.arg_types[i] - if not mypy.checker.is_valid_inferred_type(arg_type): + if not mypy.checker.is_valid_inferred_type(arg_type, ctx.api.options): arg_type = fn_type.arg_types[i] # bit of a hack else: # TODO: I assume that bound and fn_type have the same arguments. It appears this isn't @@ -301,7 +301,7 @@ def handle_partial_with_callee(ctx: mypy.plugin.FunctionContext, callee: Type) - partial_names.append(fn_type.arg_names[i]) ret_type = bound.ret_type - if not mypy.checker.is_valid_inferred_type(ret_type): + if not mypy.checker.is_valid_inferred_type(ret_type, ctx.api.options): ret_type = fn_type.ret_type # same kind of hack as above partially_applied = fn_type.copy_modified( diff --git a/mypy/semanal.py b/mypy/semanal.py index a0cfdcce1e33..e301ac1e5120 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -658,6 +658,13 @@ def refresh_partial( def refresh_top_level(self, file_node: MypyFile) -> None: """Reanalyze a stale module top-level in fine-grained incremental mode.""" + if self.options.allow_redefinition_new and not self.options.local_partial_types: + n = TempNode(AnyType(TypeOfAny.special_form)) + n.line = 1 + n.column = 0 + n.end_line = 1 + n.end_column = 0 + self.fail("--local-partial-types must be enabled if using --allow-redefinition-new", n) self.recurse_into_functions = False self.add_implicit_module_attrs(file_node) for d in file_node.defs: @@ -4335,8 +4342,10 @@ def analyze_name_lvalue( else: lvalue.fullname = lvalue.name if self.is_func_scope(): - if unmangle(name) == "_": + if unmangle(name) == "_" and not self.options.allow_redefinition_new: # Special case for assignment to local named '_': always infer 'Any'. + # This isn't needed with --allow-redefinition-new, since arbitrary + # types can be assigned to '_' anyway. typ = AnyType(TypeOfAny.special_form) self.store_declared_types(lvalue, typ) if is_final and self.is_final_redefinition(kind, name): diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 0c7e67e5444d..26ef6cb589ed 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -6855,3 +6855,14 @@ from .lib import NT [builtins fixtures/tuple.pyi] [out] [out2] + +[case testNewRedefineAffectsCache] +# flags: --local-partial-types --allow-redefinition-new +# flags2: --local-partial-types +# flags3: --local-partial-types --allow-redefinition-new +x = 0 +if int(): + x = "" +[out] +[out2] +main:6: error: Incompatible types in assignment (expression has type "str", variable has type "int") diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 016f50552a5f..92fde5615816 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -2552,3 +2552,41 @@ def f(t: T) -> None: case T([K() as k]): reveal_type(k) # N: Revealed type is "Tuple[builtins.int, fallback=__main__.K]" [builtins fixtures/tuple.pyi] + +[case testNewRedefineMatchBasics] +# flags: --allow-redefinition-new --local-partial-types + +def f1(x: int | str | list[bytes]) -> None: + match x: + case int(): + reveal_type(x) # N: Revealed type is "builtins.int" + case str(y): + reveal_type(y) # N: Revealed type is "builtins.str" + case [y]: + reveal_type(y) # N: Revealed type is "builtins.bytes" + reveal_type(y) # N: Revealed type is "Union[builtins.str, builtins.bytes]" + +[case testNewRedefineLoopWithMatch] +# flags: --allow-redefinition-new --local-partial-types + +def f1() -> None: + while True: + x = object() + match x: + case str(y): + pass + case int(): + pass + if int(): + continue + +def f2() -> None: + for x in [""]: + match str(): + case "a": + y = "" + case "b": + y = 1 + return + reveal_type(y) # N: Revealed type is "builtins.str" +[builtins fixtures/list.pyi] diff --git a/test-data/unit/check-redefine2.test b/test-data/unit/check-redefine2.test new file mode 100644 index 000000000000..238b64399ce4 --- /dev/null +++ b/test-data/unit/check-redefine2.test @@ -0,0 +1,1193 @@ +-- Test cases for the redefinition of variable with a different type (new version). + +[case testNewRedefineLocalWithDifferentType] +# flags: --allow-redefinition-new --local-partial-types +def f() -> None: + x = 0 + reveal_type(x) # N: Revealed type is "builtins.int" + x = '' + reveal_type(x) # N: Revealed type is "builtins.str" + +[case testNewRedefineConditionalLocalWithDifferentType] +# flags: --allow-redefinition-new --local-partial-types +def f() -> None: + if int(): + x = 0 + reveal_type(x) # N: Revealed type is "builtins.int" + else: + x = '' + reveal_type(x) # N: Revealed type is "builtins.str" + +[case testNewRedefineMergeConditionalLocal1] +# flags: --allow-redefinition-new --local-partial-types +def f1() -> None: + if int(): + x = 0 + else: + x = '' + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + +def f2() -> None: + if int(): + x = 0 + else: + x = None + reveal_type(x) # N: Revealed type is "Union[builtins.int, None]" + +[case testNewRedefineMergeConditionalLocal2] +# flags: --allow-redefinition-new --local-partial-types +def nested_ifs() -> None: + if int(): + if int(): + x = 0 + else: + x = '' + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + else: + if int(): + x = None + else: + x = b"" + reveal_type(x) # N: Revealed type is "Union[None, builtins.bytes]" + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str, None, builtins.bytes]" + +[case testNewRedefineUninitializedCodePath1] +# flags: --allow-redefinition-new --local-partial-types +def f1() -> None: + if int(): + x = 0 + reveal_type(x) # N: Revealed type is "builtins.int" + x = "" + reveal_type(x) # N: Revealed type is "builtins.str" + +[case testNewRedefineUninitializedCodePath2] +# flags: --allow-redefinition-new --local-partial-types +from typing import Union + +def f1() -> None: + if int(): + x: Union[int, str] = 0 + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + x = "" + reveal_type(x) # N: Revealed type is "builtins.str" + +[case testNewRedefineUninitializedCodePath3] +# flags: --allow-redefinition-new --local-partial-types +from typing import Union + +def f1() -> None: + if int(): + x = 0 + elif int(): + x = "" + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + +[case testNewRedefineUninitializedCodePath4] +# flags: --allow-redefinition-new --local-partial-types +from typing import Union + +def f1() -> None: + if int(): + x: Union[int, str] = 0 + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + +[case testNewRedefineUninitializedCodePath5] +# flags: --allow-redefinition-new --local-partial-types +from typing import Union + +def f1() -> None: + x = 0 + if int(): + x = "" + reveal_type(x) # N: Revealed type is "builtins.str" + x = None + reveal_type(x) # N: Revealed type is "Union[builtins.int, None]" + +[case testNewRedefineUninitializedCodePath6] +# flags: --allow-redefinition-new --local-partial-types +from typing import Union + +x: Union[str, None] + +def f1() -> None: + if x is not None: + reveal_type(x) # N: Revealed type is "builtins.str" + reveal_type(x) # N: Revealed type is "Union[builtins.str, None]" + +[case testNewRedefineGlobalVariableSimple] +# flags: --allow-redefinition-new --local-partial-types +if int(): + x = 0 + reveal_type(x) # N: Revealed type is "builtins.int" +else: + x = "" + reveal_type(x) # N: Revealed type is "builtins.str" +reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + +def f1() -> None: + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + +def f2() -> None: + global x + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + x = 0 + reveal_type(x) # N: Revealed type is "builtins.int" + +reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + +[case testNewRedefineGlobalVariableNoneInit] +# flags: --allow-redefinition-new --local-partial-types +x = None + +def f() -> None: + global x + reveal_type(x) # N: Revealed type is "None" + x = 1 # E: Incompatible types in assignment (expression has type "int", variable has type "None") + reveal_type(x) # N: Revealed type is "None" + +reveal_type(x) # N: Revealed type is "None" + +[case testNewRedefineParameterTypes] +# flags: --allow-redefinition-new --local-partial-types +from typing import Optional + +def f1(x: Optional[str] = None) -> None: + reveal_type(x) # N: Revealed type is "Union[builtins.str, None]" + if x is None: + x = "" + reveal_type(x) # N: Revealed type is "builtins.str" + +def f2(*args: str, **kwargs: int) -> None: + reveal_type(args) # N: Revealed type is "builtins.tuple[builtins.str, ...]" + reveal_type(kwargs) # N: Revealed type is "builtins.dict[builtins.str, builtins.int]" + +class C: + def m(self) -> None: + reveal_type(self) # N: Revealed type is "__main__.C" +[builtins fixtures/dict.pyi] + + +[case testNewRedefineClassBody] +# flags: --allow-redefinition-new --local-partial-types +class C: + if int(): + x = 0 + reveal_type(x) # N: Revealed type is "builtins.int" + else: + x = "" + reveal_type(x) # N: Revealed type is "builtins.str" + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + +reveal_type(C.x) # N: Revealed type is "Union[builtins.int, builtins.str]" + +[case testNewRedefineNestedFunctionBasics] +# flags: --allow-redefinition-new --local-partial-types +def f1() -> None: + if int(): + x = 0 + else: + x = "" + + def nested() -> None: + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + +def f2() -> None: + if int(): + x = 0 + else: + x = "" + + def nested() -> None: + nonlocal x + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + x = 0 + reveal_type(x) # N: Revealed type is "builtins.int" + + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + +[case testNewRedefineLambdaBasics] +# flags: --allow-redefinition-new --local-partial-types +def f1() -> None: + x = 0 + if int(): + x = None + f = lambda: reveal_type(x) # N: Revealed type is "Union[builtins.int, None]" + reveal_type(f) # N: Revealed type is "def () -> Union[builtins.int, None]" + if x is None: + x = "" + f = lambda: reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + reveal_type(f) # N: Revealed type is "def () -> Union[builtins.int, builtins.str]" + +[case testNewRedefineAssignmentExpression] +# flags: --allow-redefinition-new --local-partial-types +def f1() -> None: + if x := int(): + reveal_type(x) # N: Revealed type is "builtins.int" + elif x := str(): + reveal_type(x) # N: Revealed type is "builtins.str" + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + +def f2() -> None: + if x := int(): + reveal_type(x) # N: Revealed type is "builtins.int" + elif x := str(): + reveal_type(x) # N: Revealed type is "builtins.str" + else: + pass + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + +def f3() -> None: + if (x := int()) or (x := str()): + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + +[case testNewRedefineOperatorAssignment] +# flags: --allow-redefinition-new --local-partial-types +class D: pass +class C: + def __add__(self, x: C) -> D: ... + +c = C() +if int(): + c += C() + reveal_type(c) # N: Revealed type is "__main__.D" +reveal_type(c) # N: Revealed type is "Union[__main__.C, __main__.D]" + +[case testNewRedefineImportFrom-xfail] +# flags: --allow-redefinition-new --local-partial-types +if int(): + from m import x +else: + # TODO: This could be useful to allow + from m import y as x # E: Incompatible import of "x" (imported name has type "str", local name has type "int") +reveal_type(x) # N: Revealed type is "builtins.int" + +if int(): + from m import y +else: + y = 1 +reveal_type(y) # N: Revealed type is "Union[builtins.str, builtins.int]" + +[file m.py] +x = 1 +y = "" + +[case testNewRedefineImport] +# flags: --allow-redefinition-new --local-partial-types +if int(): + import m +else: + import m2 as m # E: Name "m" already defined (by an import) +m.x +m.y # E: Module has no attribute "y" + +[file m.py] +x = 1 + +[file m2.py] +y = "" +[builtins fixtures/module.pyi] + +[case testNewRedefineOptionalTypesSimple] +# flags: --allow-redefinition-new --local-partial-types +def f1() -> None: + x = None + if int(): + x = "" + reveal_type(x) # N: Revealed type is "Union[None, builtins.str]" + +def f2() -> None: + if int(): + x = None + elif int(): + x = "" + else: + x = 1 + reveal_type(x) # N: Revealed type is "Union[None, builtins.str, builtins.int]" + +def f3() -> None: + if int(): + x = None + else: + x = "" + reveal_type(x) # N: Revealed type is "Union[None, builtins.str]" + +def f4() -> None: + x = None + reveal_type(x) # N: Revealed type is "None" + +y = None +if int(): + y = 1 +reveal_type(y) # N: Revealed type is "Union[None, builtins.int]" + +if int(): + z = None +elif int(): + z = 1 +else: + z = "" +reveal_type(z) # N: Revealed type is "Union[None, builtins.int, builtins.str]" + +[case testNewRedefinePartialTypeForInstanceVariable] +# flags: --allow-redefinition-new --local-partial-types +class C1: + def __init__(self) -> None: + self.x = None + if int(): + self.x = 1 + reveal_type(self.x) # N: Revealed type is "builtins.int" + reveal_type(self.x) # N: Revealed type is "Union[builtins.int, None]" + +reveal_type(C1().x) # N: Revealed type is "Union[builtins.int, None]" + +class C2: + def __init__(self) -> None: + self.x = [] + for i in [1, 2]: + self.x.append(i) + reveal_type(self.x) # N: Revealed type is "builtins.list[builtins.int]" + +reveal_type(C2().x) # N: Revealed type is "builtins.list[builtins.int]" + +class C3: + def __init__(self) -> None: + self.x = None + if int(): + self.x = 1 + else: + self.x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "Optional[int]") + reveal_type(self.x) # N: Revealed type is "Union[builtins.int, None]" + +reveal_type(C3().x) # N: Revealed type is "Union[builtins.int, None]" + +class C4: + def __init__(self) -> None: + self.x = [] + if int(): + self.x = [""] + reveal_type(self.x) # N: Revealed type is "builtins.list[builtins.str]" + +reveal_type(C4().x) # N: Revealed type is "builtins.list[builtins.str]" +[builtins fixtures/list.pyi] + +[case testNewRedefinePartialGenericTypes] +# flags: --allow-redefinition-new --local-partial-types +def f1() -> None: + a = [] + a.append(1) + reveal_type(a) # N: Revealed type is "builtins.list[builtins.int]" + +def f2() -> None: + a = [] + a.append(1) + reveal_type(a) # N: Revealed type is "builtins.list[builtins.int]" + a = [""] + reveal_type(a) # N: Revealed type is "builtins.list[builtins.str]" + +def f3() -> None: + a = [] + a.append(1) + reveal_type(a) # N: Revealed type is "builtins.list[builtins.int]" + a = [] + reveal_type(a) # N: Revealed type is "builtins.list[builtins.int]" + +def f4() -> None: + a = [] + a.append(1) + reveal_type(a) # N: Revealed type is "builtins.list[builtins.int]" + # Partial types are currently not supported on reassignment + a = [] + a.append("x") # E: Argument 1 to "append" of "list" has incompatible type "str"; expected "int" + reveal_type(a) # N: Revealed type is "builtins.list[builtins.int]" + +def f5() -> None: + if int(): + a = [] + a.append(1) + reveal_type(a) # N: Revealed type is "builtins.list[builtins.int]" + else: + b = [""] + a = b + reveal_type(a) # N: Revealed type is "builtins.list[builtins.str]" + reveal_type(a) # N: Revealed type is "Union[builtins.list[builtins.int], builtins.list[builtins.str]]" + +def f6() -> None: + a = [] + a.append(1) + reveal_type(a) # N: Revealed type is "builtins.list[builtins.int]" + b = [""] + a = b + reveal_type(a) # N: Revealed type is "builtins.list[builtins.str]" +[builtins fixtures/list.pyi] + +[case testNewRedefineFinalLiteral] +# flags: --allow-redefinition-new --local-partial-types +from typing import Final, Literal + +x: Final = "foo" +reveal_type(x) # N: Revealed type is "Literal['foo']?" +a: Literal["foo"] = x + +class B: + x: Final = "bar" + a: Literal["bar"] = x +reveal_type(B.x) # N: Revealed type is "Literal['bar']?" +[builtins fixtures/tuple.pyi] + +[case testNewRedefineAnnotatedVariable] +# flags: --allow-redefinition-new --local-partial-types +from typing import Optional + +def f1() -> None: + x: int = 0 + if int(): + x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") + reveal_type(x) # N: Revealed type is "builtins.int" + reveal_type(x) # N: Revealed type is "builtins.int" + +def f2(x: Optional[str]) -> None: + if x is not None: + reveal_type(x) # N: Revealed type is "builtins.str" + else: + x = "" + reveal_type(x) # N: Revealed type is "builtins.str" + +def f3() -> None: + a: list[Optional[str]] = [""] + reveal_type(a) # N: Revealed type is "builtins.list[Union[builtins.str, None]]" + a = [""] + reveal_type(a) # N: Revealed type is "builtins.list[Union[builtins.str, None]]" + +class C: + x: Optional[str] + + def f(self) -> None: + if self.x is not None: + reveal_type(self.x) # N: Revealed type is "builtins.str" + else: + self.x = "" + reveal_type(self.x) # N: Revealed type is "builtins.str" + +[case testNewRedefineAnyType1] +# flags: --allow-redefinition-new --local-partial-types +def a(): pass + +def f1() -> None: + if int(): + x = "" + else: + x = a() + reveal_type(x) # N: Revealed type is "Any" + reveal_type(x) # N: Revealed type is "Union[builtins.str, Any]" + x = 1 + reveal_type(x) # N: Revealed type is "builtins.int" + +def f2() -> None: + if int(): + x = a() + else: + x = "" + reveal_type(x) # N: Revealed type is "builtins.str" + reveal_type(x) # N: Revealed type is "Union[Any, builtins.str]" + x = 1 + reveal_type(x) # N: Revealed type is "builtins.int" + +def f3() -> None: + x = 1 + x = a() + reveal_type(x) # N: Revealed type is "Any" + x = "" + reveal_type(x) # N: Revealed type is "builtins.str" + +def f4() -> None: + x = a() + x = 1 + reveal_type(x) # N: Revealed type is "builtins.int" + x = a() + reveal_type(x) # N: Revealed type is "Any" + +def f5() -> None: + x = a() + if int(): + x = 1 + reveal_type(x) # N: Revealed type is "builtins.int" + elif int(): + x = "" + reveal_type(x) # N: Revealed type is "builtins.str" + reveal_type(x) # N: Revealed type is "Union[Any, builtins.int, builtins.str]" + +def f6() -> None: + x = a() + if int(): + x = 1 + else: + x = "" + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + +def f7() -> None: + x: int + x = a() + reveal_type(x) # N: Revealed type is "builtins.int" + +[case testNewRedefineAnyType2] +# flags: --allow-redefinition-new --local-partial-types +from typing import Any + +def f1() -> None: + x: Any + x = int() + reveal_type(x) # N: Revealed type is "Any" + +def f2() -> None: + x: Any + if int(): + x = 0 + reveal_type(x) # N: Revealed type is "Any" + else: + x = "" + reveal_type(x) # N: Revealed type is "Any" + reveal_type(x) # N: Revealed type is "Any" + +def f3(x) -> None: + if int(): + x = 0 + reveal_type(x) # N: Revealed type is "Any" + reveal_type(x) # N: Revealed type is "Any" + +[case tetNewRedefineDel] +# flags: --allow-redefinition-new --local-partial-types +def f1() -> None: + x = "" + reveal_type(x) # N: Revealed type is "builtins.str" + del x + reveal_type(x) # N: Revealed type is "" + x = 0 + reveal_type(x) # N: Revealed type is "builtins.int" + +def f2() -> None: + if int(): + x = 0 + del x + else: + x = "" + reveal_type(x) # N: Revealed type is "builtins.str" + +def f3() -> None: + if int(): + x = 0 + else: + x = "" + del x + reveal_type(x) # N: Revealed type is "builtins.int" + +def f4() -> None: + while int(): + if int(): + x: int = 0 + else: + del x + reveal_type(x) # N: Revealed type is "builtins.int" + +def f5() -> None: + while int(): + if int(): + x = 0 + else: + del x + continue + x = "" + reveal_type(x) # N: Revealed type is "builtins.str" +[case testNewRedefineWhileLoopSimple] +# flags: --allow-redefinition-new --local-partial-types +def f() -> None: + while int(): + x = "" + reveal_type(x) # N: Revealed type is "builtins.str" + x = 0 + reveal_type(x) # N: Revealed type is "builtins.int" + reveal_type(x) # N: Revealed type is "builtins.int" + while int(): + x = None + reveal_type(x) # N: Revealed type is "None" + x = b"" + reveal_type(x) # N: Revealed type is "builtins.bytes" + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.bytes]" + x = [1] + reveal_type(x) # N: Revealed type is "builtins.list[builtins.int]" + +[case testNewRedefineWhileLoopOptional] +# flags: --allow-redefinition-new --local-partial-types +def f1() -> None: + x = None + while int(): + if int(): + x = "" + reveal_type(x) # N: Revealed type is "Union[None, builtins.str]" + +def f2() -> None: + x = None + while int(): + reveal_type(x) # N: Revealed type is "None" \ + # N: Revealed type is "Union[None, builtins.str]" + if int(): + x = "" + reveal_type(x) # N: Revealed type is "Union[None, builtins.str]" + +[case testNewRedefineWhileLoopPartialType] +# flags: --allow-redefinition-new --local-partial-types +def f1() -> None: + x = [] + while int(): + x.append(1) + reveal_type(x) # N: Revealed type is "builtins.list[builtins.int]" +[builtins fixtures/list.pyi] + +[case testNewRedefineWhileLoopComplex1] +# flags: --allow-redefinition-new --local-partial-types + +def f1() -> None: + while True: + try: + pass + except Exception as e: + continue +[builtins fixtures/exception.pyi] + +[case testNewRedefineWhileLoopComplex2] +# flags: --allow-redefinition-new --local-partial-types + +class C: + def __enter__(self) -> str: ... + def __exit__(self, *args) -> str: ... + +def f1() -> None: + while True: + with C() as x: + continue + +def f2() -> None: + while True: + from m import y + if int(): + continue + +[file m.py] +y = "" +[builtins fixtures/tuple.pyi] + +[case testNewRedefineReturn] +# flags: --allow-redefinition-new --local-partial-types +def f1() -> None: + if int(): + x = 0 + return + else: + x = "" + reveal_type(x) # N: Revealed type is "builtins.str" + +def f2() -> None: + if int(): + x = "" + else: + x = 0 + return + reveal_type(x) # N: Revealed type is "builtins.str" + +[case testNewRedefineBreakAndContinue] +# flags: --allow-redefinition-new --local-partial-types +def b() -> None: + while int(): + x = "" + if int(): + x = 1 + break + reveal_type(x) # N: Revealed type is "builtins.str" + x = None + reveal_type(x) # N: Revealed type is "Union[builtins.int, None]" + +def c() -> None: + x = 0 + while int(): + reveal_type(x) # N: Revealed type is "builtins.int" \ + # N: Revealed type is "Union[builtins.int, builtins.str, None]" + if int(): + x = "" + continue + else: + x = None + reveal_type(x) # N: Revealed type is "None" + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str, None]" + +[case testNewRedefineUnderscore] +# flags: --allow-redefinition-new --local-partial-types +def f() -> None: + if int(): + _ = 0 + reveal_type(_) # N: Revealed type is "builtins.int" + else: + _ = "" + reveal_type(_) # N: Revealed type is "builtins.str" + reveal_type(_) # N: Revealed type is "Union[builtins.int, builtins.str]" + +[case testNewRedefineWithStatement] +# flags: --allow-redefinition-new --local-partial-types +class C: + def __enter__(self) -> int: ... + def __exit__(self, x, y, z): ... +class D: + def __enter__(self) -> str: ... + def __exit__(self, x, y, z): ... + +def f1() -> None: + with C() as x: + reveal_type(x) # N: Revealed type is "builtins.int" + with D() as x: + reveal_type(x) # N: Revealed type is "builtins.str" + +def f2() -> None: + if int(): + with C() as x: + reveal_type(x) # N: Revealed type is "builtins.int" + else: + with D() as x: + reveal_type(x) # N: Revealed type is "builtins.str" + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + +[case testNewRedefineTryStatement] +# flags: --allow-redefinition-new --local-partial-types +class E(Exception): pass + +def g(): ... + +def f1() -> None: + try: + x = 1 + g() + x = "" + reveal_type(x) # N: Revealed type is "builtins.str" + except RuntimeError as e: + reveal_type(e) # N: Revealed type is "builtins.RuntimeError" + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + except E as e: + reveal_type(e) # N: Revealed type is "__main__.E" + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + reveal_type(e) # N: Revealed type is "" + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + +def f2() -> None: + try: + x = 1 + if int(): + x = "" + return + except Exception: + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + return + reveal_type(x) # N: Revealed type is "builtins.int" + +def f3() -> None: + try: + x = 1 + if int(): + x = "" + return + finally: + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" \ + # N: Revealed type is "builtins.int" + reveal_type(x) # N: Revealed type is "builtins.int" + +def f4() -> None: + while int(): + try: + x = 1 + if int(): + x = "" + break + if int(): + while int(): + if int(): + x = None + break + finally: + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str, None]" \ + # N: Revealed type is "Union[builtins.int, None]" + reveal_type(x) # N: Revealed type is "Union[builtins.int, None]" +[builtins fixtures/exception.pyi] + +[case testNewRedefineRaiseStatement] +# flags: --allow-redefinition-new --local-partial-types +def f1() -> None: + if int(): + x = "" + elif int(): + x = None + raise Exception() + else: + x = 1 + reveal_type(x) # N: Revealed type is "Union[builtins.str, builtins.int]" + +def f2() -> None: + try: + x = 1 + if int(): + x = "" + raise Exception() + reveal_type(x) # N: Revealed type is "builtins.int" + except Exception: + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" +[builtins fixtures/exception.pyi] + + +[case testNewRedefineMultipleAssignment] +# flags: --allow-redefinition-new --local-partial-types +def f1() -> None: + x, y = 1, "" + reveal_type(x) # N: Revealed type is "builtins.int" + reveal_type(y) # N: Revealed type is "builtins.str" + x, y = None, 2 + reveal_type(x) # N: Revealed type is "None" + reveal_type(y) # N: Revealed type is "builtins.int" + +def f2() -> None: + if int(): + x, y = 1, "" + reveal_type(x) # N: Revealed type is "builtins.int" + reveal_type(y) # N: Revealed type is "builtins.str" + else: + x, y = None, 2 + reveal_type(x) # N: Revealed type is "None" + reveal_type(y) # N: Revealed type is "builtins.int" + reveal_type(x) # N: Revealed type is "Union[builtins.int, None]" + reveal_type(y) # N: Revealed type is "Union[builtins.str, builtins.int]" + +[case testNewRedefineForLoopBasics] +# flags: --allow-redefinition-new --local-partial-types +def f1() -> None: + for x in [1]: + reveal_type(x) # N: Revealed type is "builtins.int" + for x in [""]: + reveal_type(x) # N: Revealed type is "builtins.str" + +def f2() -> None: + if int(): + for x, y in [(1, "x")]: + reveal_type(x) # N: Revealed type is "builtins.int" + reveal_type(y) # N: Revealed type is "builtins.str" + else: + for x, y in [(None, 1)]: + reveal_type(x) # N: Revealed type is "None" + reveal_type(y) # N: Revealed type is "builtins.int" + + reveal_type(x) # N: Revealed type is "Union[builtins.int, None]" + reveal_type(y) # N: Revealed type is "Union[builtins.str, builtins.int]" +[builtins fixtures/for.pyi] + +[case testNewRedefineForLoop1] +# flags: --allow-redefinition-new --local-partial-types +def l() -> list[int]: + return [] + +def f1() -> None: + x = "" + for x in l(): + reveal_type(x) # N: Revealed type is "builtins.int" + reveal_type(x) # N: Revealed type is "Union[builtins.str, builtins.int]" + +def f2() -> None: + for x in [1, 2]: + x = [x] + reveal_type(x) # N: Revealed type is "builtins.list[builtins.int]" + +def f3() -> None: + for x in [1, 2]: + if int(): + x = "x" + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" +[builtins fixtures/for.pyi] + +[case testNewRedefineForLoop2] +# flags: --allow-redefinition-new --local-partial-types +from typing import Any + +def f(a: Any) -> None: + for d in a: + if isinstance(d["x"], str): + return +[builtins fixtures/isinstance.pyi] + +[case testNewRedefineForStatementIndexNarrowing] +# flags: --allow-redefinition-new --local-partial-types +from typing import TypedDict + +class X(TypedDict): + hourly: int + daily: int + +x: X +for a in ("hourly", "daily"): + reveal_type(a) # N: Revealed type is "Union[Literal['hourly']?, Literal['daily']?]" + reveal_type(x[a]) # N: Revealed type is "builtins.int" + reveal_type(a.upper()) # N: Revealed type is "builtins.str" + c = a + reveal_type(c) # N: Revealed type is "builtins.str" + a = "monthly" + reveal_type(a) # N: Revealed type is "builtins.str" + a = "yearly" + reveal_type(a) # N: Revealed type is "builtins.str" + a = 1 + reveal_type(a) # N: Revealed type is "builtins.int" +reveal_type(a) # N: Revealed type is "builtins.int" + +b: str +for b in ("hourly", "daily"): + reveal_type(b) # N: Revealed type is "builtins.str" + reveal_type(b.upper()) # N: Revealed type is "builtins.str" +[builtins fixtures/for.pyi] +[typing fixtures/typing-full.pyi] + +[case testNewRedefineForLoopIndexWidening] +# flags: --allow-redefinition-new --local-partial-types + +def f1() -> None: + for x in [1]: + reveal_type(x) # N: Revealed type is "builtins.int" + x = "" + reveal_type(x) # N: Revealed type is "builtins.str" + reveal_type(x) # N: Revealed type is "builtins.str" + +def f2() -> None: + for x in [1]: + reveal_type(x) # N: Revealed type is "builtins.int" + if int(): + break + x = "" + reveal_type(x) # N: Revealed type is "builtins.str" + reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + +def f3() -> None: + if int(): + for x in [1]: + x = "" + reveal_type(x) # N: Revealed type is "builtins.str" + +[case testNewRedefineVariableAnnotatedInLoop] +# flags: --allow-redefinition-new --local-partial-types --enable-error-code=redundant-expr +from typing import Optional + +def f1() -> None: + e: Optional[str] = None + for x in ["a"]: + if e is None and int(): + e = x + continue + elif e is not None and int(): + break + reveal_type(e) # N: Revealed type is "Union[builtins.str, None]" + reveal_type(e) # N: Revealed type is "Union[builtins.str, None]" + +def f2(e: Optional[str]) -> None: + for x in ["a"]: + if e is None and int(): + e = x + continue + elif e is not None and int(): + break + reveal_type(e) # N: Revealed type is "Union[builtins.str, None]" + reveal_type(e) # N: Revealed type is "Union[builtins.str, None]" + +[case testNewRedefineLoopAndPartialTypesSpecialCase] +# flags: --allow-redefinition-new --local-partial-types +def f() -> list[str]: + a = [] # type: ignore + o = [] + for line in ["x"]: + if int(): + continue + if int(): + a = [] + if int(): + a.append(line) + else: + o.append(line) + return o +[builtins fixtures/list.pyi] + +[case testNewRedefineFinalVariable] +# flags: --allow-redefinition-new --local-partial-types +from typing import Final + +x: Final = "foo" +x = 1 # E: Cannot assign to final name "x" \ + # E: Incompatible types in assignment (expression has type "int", variable has type "str") + +class C: + y: Final = "foo" + y = 1 # E: Cannot assign to final name "y" \ + # E: Incompatible types in assignment (expression has type "int", variable has type "str") + +[case testNewRedefineEnableUsingComment] +# flags: --local-partial-types +import a +import b + +[file a.py] +# mypy: allow-redefinition-new +if int(): + x = 0 +else: + x = "" +reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + +[file b.py] +if int(): + x = 0 +else: + x = "" # E: Incompatible types in assignment (expression has type "str", variable has type "int") +reveal_type(x) # N: Revealed type is "builtins.int" + +[case testNewRedefineWithoutLocalPartialTypes] +import a +import b + +[file a.py] +# mypy: local-partial-types, allow-redefinition-new +x = 0 +if int(): + x = "" + +[file b.py] +# mypy: allow-redefinition-new +x = 0 +if int(): + x = "" + +[out] +tmp/b.py:1: error: --local-partial-types must be enabled if using --allow-redefinition-new + +[case testNewRedefineNestedLoopInfiniteExpansion] +# flags: --allow-redefinition-new --local-partial-types +def a(): ... + +def f() -> None: + while int(): + x = a() + + while int(): + x = [x] + + reveal_type(x) # N: Revealed type is "Union[Any, builtins.list[Any], builtins.list[Union[Any, builtins.list[Any]]], builtins.list[Union[Any, builtins.list[Any], builtins.list[Union[Any, builtins.list[Any]]]]], builtins.list[Union[Any, builtins.list[Any], builtins.list[Union[Any, builtins.list[Any]]], builtins.list[Union[Any, builtins.list[Any], builtins.list[Union[Any, builtins.list[Any]]]]]]]]" + +[case testNewRedefinePartialNoneEmptyList] +# flags: --allow-redefinition-new --local-partial-types +def func() -> None: + l = None + + if int(): + l = [] # E: Need type annotation for "l" + l.append(1) + reveal_type(l) # N: Revealed type is "Union[None, builtins.list[Any]]" +[builtins fixtures/list.pyi] + +[case testNewRedefineNarrowingSpecialCase] +# flags: --allow-redefinition-new --local-partial-types --warn-unreachable +from typing import Any, Union + +def get() -> Union[tuple[Any, Any], tuple[None, None]]: ... + +def f() -> None: + x, _ = get() + reveal_type(x) # N: Revealed type is "Union[Any, None]" + if x and int(): + reveal_type(x) # N: Revealed type is "Any" + reveal_type(x) # N: Revealed type is "Union[Any, None]" + if x and int(): + reveal_type(x) # N: Revealed type is "Any" +[builtins fixtures/tuple.pyi] + +[case testNewRedefinePartialTypeForUnderscore] +# flags: --allow-redefinition-new --local-partial-types + +def t() -> tuple[int]: + return (42,) + +def f1() -> None: + # Underscore is slightly special to preserve backward compatibility + x, *_ = t() + reveal_type(x) # N: Revealed type is "builtins.int" + +def f2() -> None: + x, *y = t() # E: Need type annotation for "y" (hint: "y: List[] = ...") + +def f3() -> None: + x, _ = 1, [] + +def f4() -> None: + a, b = 1, [] # E: Need type annotation for "b" (hint: "b: List[] = ...") +[builtins fixtures/tuple.pyi] + +[case testNewRedefineUseInferredTypedDictTypeForContext] +# flags: --allow-redefinition-new --local-partial-types +from typing import TypedDict + +class TD(TypedDict): + x: int + +def f() -> None: + td = TD(x=1) + if int(): + td = {"x": 5} + reveal_type(td) # N: Revealed type is "TypedDict('__main__.TD', {'x': builtins.int})" +[typing fixtures/typing-typeddict.pyi] + +[case testNewRedefineEmptyGeneratorUsingUnderscore] +# flags: --allow-redefinition-new --local-partial-types +def f() -> None: + gen = (_ for _ in ()) + reveal_type(gen) # N: Revealed type is "typing.Generator[Any, None, None]" +[builtins fixtures/tuple.pyi] + +[case testNewRedefineCannotWidenImportedVariable] +# flags: --allow-redefinition-new --local-partial-types +import a +import b +reveal_type(a.x) # N: Revealed type is "builtins.str" + +[file a.py] +from b import x +if int(): + x = None # E: Incompatible types in assignment (expression has type "None", variable has type "str") + +[file b.py] +x = "a" + +[case testNewRedefineCannotWidenGlobalOrClassVariableWithMemberRef] +# flags: --allow-redefinition-new --local-partial-types +from typing import ClassVar +import a + +a.x = None # E: Incompatible types in assignment (expression has type "None", variable has type "str") +reveal_type(a.x) # N: Revealed type is "builtins.str" + +class C: + x = "" + y: ClassVar[str] = "" + +C.x = None # E: Incompatible types in assignment (expression has type "None", variable has type "str") +reveal_type(C.x) # N: Revealed type is "builtins.str" +C.y = None # E: Incompatible types in assignment (expression has type "None", variable has type "str") +reveal_type(C.y) # N: Revealed type is "builtins.str" + +[file a.py] +x = "a" + +[case testNewRedefineWidenGlobalInInitModule] +# flags: --allow-redefinition-new --local-partial-types +import pkg + +[file pkg/__init__.py] +x = 0 +if int(): + x = "" +reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]"