From 12cc9315b6a7dcee73d5c9bf49b713f580eef043 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sat, 24 Jun 2023 12:52:44 -0700 Subject: [PATCH 1/2] Make str unpacking check opt-in Fixes #13823. See also #6406 --- docs/source/error_code_list2.rst | 21 +++++++++++++++++++++ mypy/checker.py | 6 +++++- mypy/errorcodes.py | 3 +++ mypy/messages.py | 6 +++++- test-data/unit/check-expressions.test | 13 +++++++++++++ test-data/unit/check-unions.test | 17 +---------------- 6 files changed, 48 insertions(+), 18 deletions(-) diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index 11f463f93018..5bbf06a9a5f5 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -416,3 +416,24 @@ Example: # The following will not generate an error on either # Python 3.8, or Python 3.9 42 + "testing..." # type: ignore + + +.. _code-str-unpacking: + +Check that ``str`` is explicitly unpacked [str-unpacking] +--------------------------------------------------------- + +It can sometimes be surprising that ``str`` is iterable, especially when unpacking. + +Example: + +.. code-block:: python + + # Use "mypy --enable-error-code str-unpacking ..." + + def print_dict(d: dict[str, str]) -> int: + # We meant to do d.items(), but instead we're unpacking the str keys of d + + # Error: Unpacking a string is disallowed + for k, v in d: + print(k, v) diff --git a/mypy/checker.py b/mypy/checker.py index 1026376cce63..69925f9becc5 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3491,7 +3491,11 @@ def check_multi_assignment( self.check_multi_assignment_from_union( lvalues, rvalue, rvalue_type, context, infer_lvalue_type ) - elif isinstance(rvalue_type, Instance) and rvalue_type.type.fullname == "builtins.str": + elif ( + self.msg.errors.is_error_code_enabled(codes.STR_UNPACKING) + and isinstance(rvalue_type, Instance) + and rvalue_type.type.fullname == "builtins.str" + ): self.msg.unpacking_strings_disallowed(context) else: self.check_multi_assignment_from_iterable( diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index e87b04b6f473..868f8f39fccb 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -203,6 +203,9 @@ def __hash__(self) -> int: "General", default_enabled=False, ) +STR_UNPACKING: Final[ErrorCode] = ErrorCode( + "str-unpacking", "Warn about expressions that unpack str", "General", default_enabled=False +) NAME_MATCH: Final = ErrorCode( "name-match", "Check that type definition has consistent naming", "General" ) diff --git a/mypy/messages.py b/mypy/messages.py index 9d703a1a974a..3029156ef1ca 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1108,7 +1108,11 @@ def wrong_number_values_to_unpack( ) def unpacking_strings_disallowed(self, context: Context) -> None: - self.fail("Unpacking a string is disallowed", context) + self.fail( + "Unpacking a string is disallowed", + context, + code=codes.STR_UNPACKING, + ) def type_not_iterable(self, type: Type, context: Context) -> None: self.fail(f"{format_type(type, self.options)} object is not iterable", context) diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 1fa551f6a2e4..94ed809b28b8 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -2330,3 +2330,16 @@ T = TypeVar("T") x: int x + T # E: Unsupported operand types for + ("int" and "object") T() # E: "object" not callable + +[case testStringDisallowedUnpacking] +# flags: --enable-error-code str-unpacking +from typing import Dict + +d: Dict[str, str] + +for a, b in d: # E: Unpacking a string is disallowed + pass + +s = "foo" +a, b = s # E: Unpacking a string is disallowed +[builtins fixtures/dict.pyi] diff --git a/test-data/unit/check-unions.test b/test-data/unit/check-unions.test index 65d5c1abc7e8..777383e5d1d4 100644 --- a/test-data/unit/check-unions.test +++ b/test-data/unit/check-unions.test @@ -721,26 +721,11 @@ reveal_type(d) # N: Revealed type is "builtins.list[builtins.int]" from typing import Union bad: Union[int, str] -x, y = bad # E: "int" object is not iterable \ - # E: Unpacking a string is disallowed +x, y = bad # E: "int" object is not iterable reveal_type(x) # N: Revealed type is "Any" reveal_type(y) # N: Revealed type is "Any" [out] -[case testStringDisallowedUnpacking] -from typing import Dict - -d: Dict[str, str] - -for a, b in d: # E: Unpacking a string is disallowed - pass - -s = "foo" -a, b = s # E: Unpacking a string is disallowed - -[builtins fixtures/dict.pyi] -[out] - [case testUnionAlwaysTooMany] from typing import Union, Tuple bad: Union[Tuple[int, int, int], Tuple[str, str, str]] From 5955f2bf0a371d49a72aa74d335352b955e119fc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 24 Jun 2023 19:54:00 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/messages.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/mypy/messages.py b/mypy/messages.py index 3029156ef1ca..b1db7574fdbd 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -1108,11 +1108,7 @@ def wrong_number_values_to_unpack( ) def unpacking_strings_disallowed(self, context: Context) -> None: - self.fail( - "Unpacking a string is disallowed", - context, - code=codes.STR_UNPACKING, - ) + self.fail("Unpacking a string is disallowed", context, code=codes.STR_UNPACKING) def type_not_iterable(self, type: Type, context: Context) -> None: self.fail(f"{format_type(type, self.options)} object is not iterable", context)