From ed7360c3316ab249790af8c931a6b24fe3da3f61 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sat, 26 Mar 2022 18:37:28 +0000 Subject: [PATCH 1/2] Add special-casing for `__match_args__` fields --- CHANGELOG.md | 8 ++++++++ pyi.py | 12 ++++++++++-- tests/attribute_annotations.pyi | 21 ++++++++++++++++++++- tests/quotes.pyi | 3 +++ 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee522c1a..4fb063ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## Unreleased + +Behaviour changes: +* Add special-casing so that string literals are allowed in the context of + `__match_args__` assignments inside a class definition. +* Add special-casing so that arbitrary values can be assigned to a variable in + a stub file if the variable is annotated with `Final`. + ## 22.2.0 Bugfixes: diff --git a/pyi.py b/pyi.py index f09fe89a..1324eda5 100644 --- a/pyi.py +++ b/pyi.py @@ -301,6 +301,7 @@ def _is_object(node: ast.expr, name: str, *, from_: Container[str]) -> bool: _is_Any = partial(_is_object, name="Any", from_={"typing"}) _is_overload = partial(_is_object, name="overload", from_={"typing"}) _is_final = partial(_is_object, name="final", from_=_TYPING_MODULES) +_is_Final = partial(_is_object, name="Final", from_=_TYPING_MODULES) _is_Self = partial(_is_object, name="Self", from_=({"_typeshed"} | _TYPING_MODULES)) @@ -622,7 +623,10 @@ def visit_Assign(self, node: ast.Assign) -> None: else: self.error(node, Y017) target_name = None - if target_name == "__all__": + is_special_assignment = ( + target_name == "__match_args__" and self.in_class.active + ) or (target_name == "__all__" and not self.in_class.active) + if is_special_assignment: with self.string_literals_allowed.enabled(): self.generic_visit(node) else: @@ -648,7 +652,7 @@ def visit_Assign(self, node: ast.Assign) -> None: # We avoid triggering Y026 for calls and = ... because there are various # unusual cases where assignment to the result of a call is legitimate # in stubs. - if target_name != "__all__" and not isinstance( + if not is_special_assignment and not isinstance( assignment, (ast.Ellipsis, ast.Call) ): self.error(node, Y026) @@ -694,6 +698,10 @@ def visit_Expr(self, node: ast.Expr) -> None: self.generic_visit(node) def visit_AnnAssign(self, node: ast.AnnAssign) -> None: + if _is_Final(node.annotation): + with self.string_literals_allowed.enabled(): + self.generic_visit(node) + return if _is_name(node.target, "__all__") and not self.in_class.active: with self.string_literals_allowed.enabled(): self.generic_visit(node) diff --git a/tests/attribute_annotations.pyi b/tests/attribute_annotations.pyi index 962d6072..07c50513 100644 --- a/tests/attribute_annotations.pyi +++ b/tests/attribute_annotations.pyi @@ -1,4 +1,6 @@ -from typing import TypeAlias +import typing +import typing_extensions +from typing import Final, TypeAlias field1: int field2: int = ... @@ -13,6 +15,15 @@ field8 = False # Y015 Bad default value. Use "field8 = ..." instead of "field8 field9 = None # Y026 Use typing_extensions.TypeAlias for type aliases field10: TypeAlias = None +# Tests for Final +field11: Final = 1 +field12: Final = "foo" +field13: Final = b"foo" +field14: Final = True +field15: Final = ('a', 'b', 'c') +field16: typing.Final = "foo" +field17: typing_extensions.Final = "foo" + class Foo: field1: int field2: int = ... @@ -22,3 +33,11 @@ class Foo: field6 = 0 # Y015 Bad default value. Use "field6 = ..." instead of "field6 = 0" field7 = b"" # Y015 Bad default value. Use "field7 = ..." instead of "field7 = b''" field8 = False # Y015 Bad default value. Use "field8 = ..." instead of "field8 = False" + # Tests for Final + field9: Final = 1 + field10: Final = "foo" + field11: Final = b"foo" + field12: Final = True + field13: Final = ('a', 'b', 'c') + field14: typing.Final = "foo" + field15: typing_extensions.Final = "foo" diff --git a/tests/quotes.pyi b/tests/quotes.pyi index 2211bd48..838f927c 100644 --- a/tests/quotes.pyi +++ b/tests/quotes.pyi @@ -4,6 +4,7 @@ from typing import Annotated, Literal, TypeAlias, TypeVar import typing_extensions __all__ = ["f", "g"] +__match_args__ = ('foo',) # Y026 Use typing_extensions.TypeAlias for type aliases # Y020 Quoted annotations should never be used in stubs def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs @@ -18,3 +19,5 @@ Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used i class Child(list["int"]): # Y020 Quoted annotations should never be used in stubs """Documented and guaranteed useful.""" # Y021 Docstrings should not be included in stubs + __all__ = ('foo',) # Y026 Use typing_extensions.TypeAlias for type aliases # Y020 Quoted annotations should never be used in stubs + __match_args__ = ('foo', 'bar') From a1bc0470f080692e45a4dfb7968b50d86331614b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 26 Mar 2022 18:38:38 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks --- tests/attribute_annotations.pyi | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/attribute_annotations.pyi b/tests/attribute_annotations.pyi index 07c50513..ab1a9f7c 100644 --- a/tests/attribute_annotations.pyi +++ b/tests/attribute_annotations.pyi @@ -1,7 +1,8 @@ import typing -import typing_extensions from typing import Final, TypeAlias +import typing_extensions + field1: int field2: int = ... field3 = ... # type: int # Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int")