diff --git a/mypy/checker.py b/mypy/checker.py index eaaebf17b09b..841b3a905c65 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4116,8 +4116,19 @@ def check_multi_assignment( lvalues, rvalue, rvalue_type, context, infer_lvalue_type ) else: + rvalue_literal = rvalue_type if isinstance(rvalue_type, Instance) and rvalue_type.type.fullname == "builtins.str": + if rvalue_type.last_known_value is None: + self.msg.unpacking_strings_disallowed(context) + else: + rvalue_literal = rvalue_type.last_known_value + if ( + isinstance(rvalue_literal, LiteralType) + and isinstance(rvalue_literal.value, str) + and len(lvalues) != len(rvalue_literal.value) + ): self.msg.unpacking_strings_disallowed(context) + self.check_multi_assignment_from_iterable( lvalues, rvalue_type, context, infer_lvalue_type ) @@ -4363,9 +4374,7 @@ def check_multi_assignment_from_iterable( infer_lvalue_type: bool = True, ) -> None: rvalue_type = get_proper_type(rvalue_type) - if self.type_is_iterable(rvalue_type) and isinstance( - rvalue_type, (Instance, CallableType, TypeType, Overloaded) - ): + if self.type_is_iterable(rvalue_type): item_type = self.iterable_item_type(rvalue_type, context) for lv in lvalues: if isinstance(lv, StarExpr): @@ -7803,9 +7812,7 @@ def note( return self.msg.note(msg, context, offset=offset, code=code) - def iterable_item_type( - self, it: Instance | CallableType | TypeType | Overloaded, context: Context - ) -> Type: + def iterable_item_type(self, it: ProperType, context: Context) -> Type: if isinstance(it, Instance): iterable = map_instance_to_supertype(it, self.lookup_typeinfo("typing.Iterable")) item_type = iterable.args[0] diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 8d79545ae123..22854646f934 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -2505,3 +2505,24 @@ a2, b2 = s # E: Unpacking a string is disallowed reveal_type(a2) # N: Revealed type is "builtins.str" reveal_type(b2) # N: Revealed type is "builtins.str" [builtins fixtures/primitives.pyi] + +[case testStringLiteralUnpacking] +from typing import Literal, Final + +def takes_literal(xy: Literal["xy"]): + x, y = xy + reveal_type(x) # N: Revealed type is "builtins.str" + reveal_type(y) # N: Revealed type is "builtins.str" + + x, y, z = xy # E: Unpacking a string is disallowed + reveal_type(z) # N: Revealed type is "builtins.str" + +def last_known_value() -> None: + xy: Final = "xy" + x, y = xy + reveal_type(x) # N: Revealed type is "builtins.str" + reveal_type(y) # N: Revealed type is "builtins.str" + + x, y, z = xy # E: Unpacking a string is disallowed + reveal_type(z) # N: Revealed type is "builtins.str" +[builtins fixtures/primitives.pyi] diff --git a/test-data/unit/check-tuples.test b/test-data/unit/check-tuples.test index cfdd2aacc4d2..b4d86b8007e6 100644 --- a/test-data/unit/check-tuples.test +++ b/test-data/unit/check-tuples.test @@ -515,7 +515,7 @@ a, b = None, None # type: (A, B) a1, b1 = a, a # type: (A, B) # E: Incompatible types in assignment (expression has type "A", variable has type "B") a2, b2 = b, b # type: (A, B) # E: Incompatible types in assignment (expression has type "B", variable has type "A") a3, b3 = a # type: (A, B) # E: "A" object is not iterable -a4, b4 = None # type: (A, B) # E: "None" object is not iterable +a4, b4 = None # type: (A, B) # E: "None" has no attribute "__iter__" (not iterable) a5, b5 = a, b, a # type: (A, B) # E: Too many values to unpack (2 expected, 3 provided) ax, bx = a, b # type: (A, B)