From 238a35a208375dfac48baffd230a3a0cb6bf8c73 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 20 Aug 2024 11:18:10 +0100 Subject: [PATCH 1/3] Fix Y053 false positives inside `Literal` and `Annotated` slices --- CHANGELOG.md | 6 ++++++ ERRORCODES.md | 2 +- pyi.py | 13 ++++++++++--- tests/defaults.pyi | 8 ++++++++ tests/pep695_py312.pyi | 5 ++++- 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e72ec5b..e39b81e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## Unreleased + +Bugfixes +* Don't emit Y053 for long strings inside `Literal` slices or + metadata strings inside `Annotated` slices. + ## 24.6.0 Bugfixes diff --git a/ERRORCODES.md b/ERRORCODES.md index 5a86af6b..db82bbd2 100644 --- a/ERRORCODES.md +++ b/ERRORCODES.md @@ -66,7 +66,7 @@ The following warnings are currently emitted by default: | Y050 | Prefer `typing_extensions.Never` over `typing.NoReturn` for argument annotations. | Style | Y051 | Y051 detects redundant unions between `Literal` types and builtin supertypes. For example, `Literal[5]` is redundant in the union `int \| Literal[5]`, and `Literal[True]` is redundant in the union `Literal[True] \| bool`. | Redundant code | Y052 | Y052 disallows assignments to constant values where the assignment does not have a type annotation. For example, `x = 0` in the global namespace is ambiguous in a stub, as there are four different types that could be inferred for the variable `x`: `int`, `Final[int]`, `Literal[0]`, or `Final[Literal[0]]`. Enum members are excluded from this check, as are various special assignments such as `__all__` and `__match_args__`. | Correctness -| Y053 | Only string and bytes literals <=50 characters long are permitted. | Style +| Y053 | Only string and bytes literals <=50 characters long are permitted. (There are some exceptions, such as `Literal` subscripts, metadata strings inside `Annotated` subscripts, and strings passed to `@deprecated`.) | Style | Y054 | Only numeric literals with a string representation <=10 characters long are permitted. | Style | Y055 | Unions of the form `type[X] \| type[Y]` can be simplified to `type[X \| Y]`. Similarly, `Union[type[X], type[Y]]` can be simplified to `type[Union[X, Y]]`. | Style | Y056 | Do not call methods such as `.append()`, `.extend()` or `.remove()` on `__all__`. Different type checkers have varying levels of support for calling these methods on `__all__`. Use `+=` instead, which is known to be supported by all major type checkers. | Correctness diff --git a/pyi.py b/pyi.py index 88c7b8a8..c07e3084 100644 --- a/pyi.py +++ b/pyi.py @@ -1240,7 +1240,10 @@ def visit_Call(self, node: ast.Call) -> None: self.error(node, Y031) return elif _is_deprecated(function): - with self.string_literals_allowed.enabled(), self.long_strings_allowed.enabled(): + with ( + self.string_literals_allowed.enabled(), + self.long_strings_allowed.enabled(), + ): for arg in chain(node.args, node.keywords): self.visit(arg) return @@ -1563,7 +1566,8 @@ def _visit_typing_Literal(self, node: ast.Subscript) -> None: suggestion = f"Literal[{new_literal_slice}] | None" self.error(analysis.none_members[0], Y061.format(suggestion=suggestion)) - self.visit(node.slice) + with self.long_strings_allowed.enabled(): + self.visit(node.slice) def _visit_slice_tuple(self, node: ast.Tuple, parent: str | None) -> None: if parent == "Union": @@ -1573,7 +1577,10 @@ def _visit_slice_tuple(self, node: ast.Tuple, parent: str | None) -> None: # Allow literals, except in the first argument if len(node.elts) > 1: self.visit(node.elts[0]) - with self.string_literals_allowed.enabled(): + with ( + self.string_literals_allowed.enabled(), + self.long_strings_allowed.enabled(), + ): for elt in node.elts[1:]: self.visit(elt) else: diff --git a/tests/defaults.pyi b/tests/defaults.pyi index f7c170fd..30826fba 100644 --- a/tests/defaults.pyi +++ b/tests/defaults.pyi @@ -1,6 +1,7 @@ import math import os import sys +from typing import Annotated, Literal, TypeAlias import _typeshed from _typeshed import SupportsRead, SupportsWrite, sentinel @@ -84,3 +85,10 @@ def f40(x: int, /) -> None: ... def f41(x: int, /, y: "int") -> None: ... # Y020 Quoted annotations should never be used in stubs def f42(x: str = "y", /) -> None: ... def f43(x: str = os.pathsep, /) -> None: ... # Y011 Only simple default values allowed for typed arguments + +# Long strings inside `Literal` or `Annotated` slices are okay +def f44(x: Literal["loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong"]) -> None: ... +def f55(x: Annotated[str, "metadataaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"]) -> None: ... + +X: TypeAlias = Literal["llllllllllllllllllllllllllonggggggggggggggggggggggggggggggggggggggggg"] +Y: TypeAlias = Annotated[int, "verrrrrrrrrrrrrrrrrrrry looooooooooooooongggggggggggggggggggggggggggggggggggg"] diff --git a/tests/pep695_py312.pyi b/tests/pep695_py312.pyi index 08327710..5f720a38 100644 --- a/tests/pep695_py312.pyi +++ b/tests/pep695_py312.pyi @@ -3,7 +3,7 @@ import typing from collections.abc import Iterator -from typing import Any, NamedTuple, NoReturn, Protocol, Self, TypedDict +from typing import Any, NamedTuple, NoReturn, Protocol, Self, TypedDict, Annotated, Literal type lowercase_alias = str | int # Y042 Type aliases should use the CamelCase naming convention type _LooksLikeATypeVarT = str | int # Y043 Bad name for a type alias (the "T" suffix implies a TypeVar) @@ -66,3 +66,6 @@ class _UnusedPEP695Protocol[T](Protocol): # Y046 Protocol "_UnusedPEP695Protoco class _UnusedPEP695TypedDict[T](TypedDict): # Y049 TypedDict "_UnusedPEP695TypedDict" is not used x: T + +type X = Literal["Y053 will not be emitted here despite it being a very long string literal, because it is inside a `Literal` slice"] +type Y = Annotated[int, "look at me, very long string literal, but it's okay because it's an `Annotated` metadata string"] From 4ae1a944d922f0a17563d11c5fb8930d06bfd5a9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:18:55 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks --- tests/pep695_py312.pyi | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/pep695_py312.pyi b/tests/pep695_py312.pyi index 5f720a38..6bd0575a 100644 --- a/tests/pep695_py312.pyi +++ b/tests/pep695_py312.pyi @@ -3,7 +3,16 @@ import typing from collections.abc import Iterator -from typing import Any, NamedTuple, NoReturn, Protocol, Self, TypedDict, Annotated, Literal +from typing import ( + Annotated, + Any, + Literal, + NamedTuple, + NoReturn, + Protocol, + Self, + TypedDict, +) type lowercase_alias = str | int # Y042 Type aliases should use the CamelCase naming convention type _LooksLikeATypeVarT = str | int # Y043 Bad name for a type alias (the "T" suffix implies a TypeVar) From 3b68a73c0ea0dc9b1971b148926c49c35d35803b Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 20 Aug 2024 11:21:26 +0100 Subject: [PATCH 3/3] py38 compat --- pyi.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pyi.py b/pyi.py index c07e3084..370d967b 100644 --- a/pyi.py +++ b/pyi.py @@ -1240,10 +1240,7 @@ def visit_Call(self, node: ast.Call) -> None: self.error(node, Y031) return elif _is_deprecated(function): - with ( - self.string_literals_allowed.enabled(), - self.long_strings_allowed.enabled(), - ): + with self.string_literals_allowed.enabled(), self.long_strings_allowed.enabled(): for arg in chain(node.args, node.keywords): self.visit(arg) return @@ -1577,10 +1574,7 @@ def _visit_slice_tuple(self, node: ast.Tuple, parent: str | None) -> None: # Allow literals, except in the first argument if len(node.elts) > 1: self.visit(node.elts[0]) - with ( - self.string_literals_allowed.enabled(), - self.long_strings_allowed.enabled(), - ): + with self.string_literals_allowed.enabled(), self.long_strings_allowed.enabled(): for elt in node.elts[1:]: self.visit(elt) else: