-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Add --disallow-str-iteration flag #20577
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
ff8281b
9844f81
115c1b7
1684059
eb08cfe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| diff --git a/mypy/typeshed/stdlib/builtins.pyi b/mypy/typeshed/stdlib/builtins.pyi | ||
| index bd425ff3c..61a28e3a9 100644 | ||
| --- a/mypy/typeshed/stdlib/builtins.pyi | ||
| +++ b/mypy/typeshed/stdlib/builtins.pyi | ||
| @@ -1455,6 +1455,8 @@ def input(prompt: object = "", /) -> str: ... | ||
| class _GetItemIterable(Protocol[_T_co]): | ||
| def __getitem__(self, i: int, /) -> _T_co: ... | ||
|
|
||
| +@overload | ||
| +def iter(object: str, /) -> Iterator[str]: ... | ||
| @overload | ||
| def iter(object: SupportsIter[_SupportsNextT_co], /) -> _SupportsNextT_co: ... | ||
| @overload |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5378,6 +5378,10 @@ def analyze_iterable_item_type_without_expression( | |
| echk = self.expr_checker | ||
| iterable: Type | ||
| iterable = get_proper_type(type) | ||
|
|
||
| if self.options.disallow_str_iteration and self.is_str_iteration_type(iterable): | ||
| self.msg.str_iteration_disallowed(context) | ||
|
|
||
| iterator = echk.check_method_call_by_name("__iter__", iterable, [], [], context)[0] | ||
|
|
||
| if ( | ||
|
|
@@ -5390,6 +5394,18 @@ def analyze_iterable_item_type_without_expression( | |
| iterable = echk.check_method_call_by_name("__next__", iterator, [], [], context)[0] | ||
| return iterator, iterable | ||
|
|
||
| def is_str_iteration_type(self, typ: Type) -> bool: | ||
| typ = get_proper_type(typ) | ||
| if isinstance(typ, LiteralType): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wonder if we should do this for subtypes of str too and do something like an |
||
| return isinstance(typ.value, str) | ||
| if isinstance(typ, Instance): | ||
| return typ.type.fullname == "builtins.str" | ||
| if isinstance(typ, UnionType): | ||
| return any(self.is_str_iteration_type(item) for item in typ.relevant_items()) | ||
| if isinstance(typ, TypeVarType): | ||
| return self.is_str_iteration_type(typ.upper_bound) | ||
| return False | ||
|
|
||
| def analyze_range_native_int_type(self, expr: Expression) -> Type | None: | ||
| """Try to infer native int item type from arguments to range(...). | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1136,6 +1136,10 @@ 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_UNPACK) | ||
|
|
||
| def str_iteration_disallowed(self, context: Context) -> None: | ||
| self.fail('Iterating over "str" is disallowed', context) | ||
| self.note("This is because --disallow-str-iteration is enabled", context) | ||
|
|
||
| def type_not_iterable(self, type: Type, context: Context) -> None: | ||
| self.fail(f"{format_type(type, self.options)} object is not iterable", context) | ||
|
|
||
|
|
@@ -3122,6 +3126,15 @@ def get_conflict_protocol_types( | |
| Return them as a list of ('member', 'got', 'expected', 'is_lvalue'). | ||
| """ | ||
| assert right.type.is_protocol | ||
|
|
||
| if left.type.fullname == "builtins.str" and right.type.fullname in ( | ||
| "collections.abc.Collection", | ||
| "typing.Collection", | ||
| ): | ||
| # `str` doesn't conform to the `Collection` protocol, but we don't want to show that as the reason for the error. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't the discrepancy actually on Container? So we should maybe do this for any subclass of Container. |
||
| assert options.disallow_str_iteration | ||
| return [] | ||
|
|
||
| conflicts: list[tuple[str, Type, Type, bool]] = [] | ||
| for member in right.type.protocol_members: | ||
| if member in ("__init__", "__new__"): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| # Builtins stub used in disallow-str-iteration tests. | ||
|
|
||
|
|
||
| from _typeshed import SupportsLenAndGetItem | ||
| from typing import Generic, Iterator, Sequence, Reversible, TypeVar, overload | ||
|
|
||
| _T = TypeVar("_T") | ||
|
|
||
| class object: | ||
| def __init__(self) -> None: pass | ||
|
|
||
| class type: pass | ||
| class int: pass | ||
| class bool(int): pass | ||
| class ellipsis: pass | ||
| class slice: pass | ||
|
|
||
| class str: | ||
| def __iter__(self) -> Iterator[str]: pass | ||
| def __len__(self) -> int: pass | ||
| def __contains__(self, item: object) -> bool: pass | ||
| def __reversed__(self) -> Iterator[str]: pass | ||
| def __getitem__(self, i: int) -> str: pass | ||
|
|
||
| class list(Sequence[_T], Generic[_T]): | ||
| def __iter__(self) -> Iterator[_T]: pass | ||
| def __len__(self) -> int: pass | ||
| def __contains__(self, item: object) -> bool: pass | ||
| def __reversed__(self) -> Iterator[_T]: pass | ||
| @overload | ||
| def __getitem__(self, i: int, /) -> _T: ... | ||
| @overload | ||
| def __getitem__(self, s: slice, /) -> list[_T]: ... | ||
|
|
||
| class tuple(Sequence[_T], Generic[_T]): | ||
| def __iter__(self) -> Iterator[_T]: pass | ||
| def __len__(self) -> int: pass | ||
| def __contains__(self, item: object) -> bool: pass | ||
| def __reversed__(self) -> Iterator[_T]: pass | ||
| @overload | ||
| def __getitem__(self, i: int, /) -> _T: ... | ||
| @overload | ||
| def __getitem__(self, s: slice, /) -> list[_T]: ... | ||
|
|
||
| class dict: pass | ||
|
|
||
| class reversed(Iterator[_T]): | ||
| @overload | ||
| def __new__(cls, sequence: Reversible[_T], /) -> Iterator[_T]: ... # type: ignore[misc] | ||
| @overload | ||
| def __new__(cls, sequence: SupportsLenAndGetItem[_T], /) -> Iterator[_T]: ... # type: ignore[misc] | ||
| def __next__(self) -> _T: ... |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| # Minimal typing fixture for disallow-str-iteration tests. | ||
|
|
||
| from abc import ABCMeta, abstractmethod | ||
|
|
||
| Any = object() | ||
| TypeVar = 0 | ||
| Generic = 0 | ||
| Protocol = 0 | ||
| overload = 0 | ||
|
|
||
| _T = TypeVar("_T") | ||
| _KT = TypeVar("_KT") | ||
| _T_co = TypeVar("_T_co", covariant=True) | ||
| _VT_co = TypeVar("_VT_co", covariant=True) # Value type covariant containers. | ||
| _TC = TypeVar("_TC", bound=type[object]) | ||
|
|
||
| @runtime_checkable | ||
| class Iterable(Protocol[_T_co]): | ||
| @abstractmethod | ||
| def __iter__(self) -> Iterator[_T_co]: ... | ||
|
|
||
| @runtime_checkable | ||
| class Iterator(Iterable[_T_co], Protocol[_T_co]): | ||
| @abstractmethod | ||
| def __next__(self) -> _T_co: ... | ||
| def __iter__(self) -> Iterator[_T_co]: ... | ||
|
|
||
| @runtime_checkable | ||
| class Reversible(Iterable[_T_co], Protocol[_T_co]): | ||
| @abstractmethod | ||
| def __reversed__(self) -> Iterator[_T_co]: ... | ||
|
|
||
| @runtime_checkable | ||
| class Container(Protocol[_T_co]): | ||
| # This is generic more on vibes than anything else | ||
| @abstractmethod | ||
| def __contains__(self, x: object, /) -> bool: ... | ||
|
|
||
| @runtime_checkable | ||
| class Collection(Iterable[_T_co], Container[_T_co], Protocol[_T_co]): | ||
| # Implement Sized (but don't have it as a base class). | ||
| @abstractmethod | ||
| def __len__(self) -> int: ... | ||
|
|
||
| class Sequence(Reversible[_T_co], Collection[_T_co]): | ||
| @overload | ||
| @abstractmethod | ||
| def __getitem__(self, index: int) -> _T_co: ... | ||
| @overload | ||
| @abstractmethod | ||
| def __getitem__(self, index: slice) -> Sequence[_T_co]: ... | ||
| def __contains__(self, value: object) -> bool: ... | ||
| def __iter__(self) -> Iterator[_T_co]: ... | ||
| def __reversed__(self) -> Iterator[_T_co]: ... | ||
|
|
||
| class Mapping(Collection[_KT], Generic[_KT, _VT_co]): | ||
| @abstractmethod | ||
| def __getitem__(self, key: _KT, /) -> _VT_co: ... | ||
| def __contains__(self, key: object, /) -> bool: ... | ||
|
|
||
| def runtime_checkable(cls: _TC) -> _TC: | ||
| return cls |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add an example with
iter(s)to show that's OK.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops, on it