mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 10:48:32 +00:00
Add PYI034 for flake8-pyi
plugin (#4764)
This commit is contained in:
parent
c68686b1de
commit
fcbf5c3fae
14 changed files with 918 additions and 2 deletions
280
crates/ruff/resources/test/fixtures/flake8_pyi/PYI034.py
vendored
Normal file
280
crates/ruff/resources/test/fixtures/flake8_pyi/PYI034.py
vendored
Normal file
|
@ -0,0 +1,280 @@
|
|||
# flags: --extend-ignore=Y023
|
||||
|
||||
import abc
|
||||
import builtins
|
||||
import collections.abc
|
||||
import typing
|
||||
from abc import abstractmethod
|
||||
from collections.abc import AsyncIterable, AsyncIterator, Iterable, Iterator
|
||||
from typing import Any, overload
|
||||
|
||||
import typing_extensions
|
||||
from _typeshed import Self
|
||||
from typing_extensions import final
|
||||
|
||||
|
||||
class Bad(
|
||||
object
|
||||
): # Y040 Do not inherit from "object" explicitly, as it is redundant in Python 3
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> Bad:
|
||||
... # Y034 "__new__" methods usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__new__", e.g. "def __new__(cls, *args: Any, **kwargs: Any) -> Self: ..."
|
||||
|
||||
def __repr__(self) -> str:
|
||||
... # Y029 Defining __repr__ or __str__ in a stub is almost always redundant
|
||||
|
||||
def __str__(self) -> builtins.str:
|
||||
... # Y029 Defining __repr__ or __str__ in a stub is almost always redundant
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
... # Y032 Prefer "object" to "Any" for the second parameter in "__eq__" methods
|
||||
|
||||
def __ne__(self, other: typing.Any) -> typing.Any:
|
||||
... # Y032 Prefer "object" to "Any" for the second parameter in "__ne__" methods
|
||||
|
||||
def __enter__(self) -> Bad:
|
||||
... # Y034 "__enter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__enter__", e.g. "def __enter__(self) -> Self: ..."
|
||||
|
||||
async def __aenter__(self) -> Bad:
|
||||
... # Y034 "__aenter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__aenter__", e.g. "async def __aenter__(self) -> Self: ..."
|
||||
|
||||
def __iadd__(self, other: Bad) -> Bad:
|
||||
... # Y034 "__iadd__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__iadd__", e.g. "def __iadd__(self, other: Bad) -> Self: ..."
|
||||
|
||||
|
||||
class AlsoBad(int, builtins.object):
|
||||
... # Y040 Do not inherit from "object" explicitly, as it is redundant in Python 3
|
||||
|
||||
|
||||
class Good:
|
||||
def __new__(cls: type[Self], *args: Any, **kwargs: Any) -> Self:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def __str__(self) -> str:
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
def __repr__(self) -> str:
|
||||
...
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
...
|
||||
|
||||
def __ne__(self, obj: object) -> int:
|
||||
...
|
||||
|
||||
def __enter__(self: Self) -> Self:
|
||||
...
|
||||
|
||||
async def __aenter__(self: Self) -> Self:
|
||||
...
|
||||
|
||||
def __ior__(self: Self, other: Self) -> Self:
|
||||
...
|
||||
|
||||
|
||||
class Fine:
|
||||
@overload
|
||||
def __new__(cls, foo: int) -> FineSubclass:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> Fine:
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
def __str__(self) -> str:
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
def __repr__(self) -> str:
|
||||
...
|
||||
|
||||
def __eq__(self, other: Any, strange_extra_arg: list[str]) -> Any:
|
||||
...
|
||||
|
||||
def __ne__(self, *, kw_only_other: Any) -> bool:
|
||||
...
|
||||
|
||||
def __enter__(self) -> None:
|
||||
...
|
||||
|
||||
async def __aenter__(self) -> bool:
|
||||
...
|
||||
|
||||
|
||||
class FineSubclass(Fine):
|
||||
...
|
||||
|
||||
|
||||
class StrangeButAcceptable(str):
|
||||
@typing_extensions.overload
|
||||
def __new__(cls, foo: int) -> StrangeButAcceptableSubclass:
|
||||
...
|
||||
|
||||
@typing_extensions.overload
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> StrangeButAcceptable:
|
||||
...
|
||||
|
||||
def __str__(self) -> StrangeButAcceptable:
|
||||
...
|
||||
|
||||
def __repr__(self) -> StrangeButAcceptable:
|
||||
...
|
||||
|
||||
|
||||
class StrangeButAcceptableSubclass(StrangeButAcceptable):
|
||||
...
|
||||
|
||||
|
||||
class FineAndDandy:
|
||||
def __str__(self, weird_extra_arg) -> str:
|
||||
...
|
||||
|
||||
def __repr__(self, weird_extra_arg_with_default=...) -> str:
|
||||
...
|
||||
|
||||
|
||||
@final
|
||||
class WillNotBeSubclassed:
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> WillNotBeSubclassed:
|
||||
...
|
||||
|
||||
def __enter__(self) -> WillNotBeSubclassed:
|
||||
...
|
||||
|
||||
async def __aenter__(self) -> WillNotBeSubclassed:
|
||||
...
|
||||
|
||||
|
||||
# we don't emit an error for these; out of scope for a linter
|
||||
class InvalidButPluginDoesNotCrash:
|
||||
def __new__() -> InvalidButPluginDoesNotCrash:
|
||||
...
|
||||
|
||||
def __enter__() -> InvalidButPluginDoesNotCrash:
|
||||
...
|
||||
|
||||
async def __aenter__() -> InvalidButPluginDoesNotCrash:
|
||||
...
|
||||
|
||||
|
||||
class BadIterator1(Iterator[int]):
|
||||
def __iter__(self) -> Iterator[int]:
|
||||
... # Y034 "__iter__" methods in classes like "BadIterator1" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator1.__iter__", e.g. "def __iter__(self) -> Self: ..."
|
||||
|
||||
|
||||
class BadIterator2(
|
||||
typing.Iterator[int]
|
||||
): # Y022 Use "collections.abc.Iterator[T]" instead of "typing.Iterator[T]" (PEP 585 syntax)
|
||||
def __iter__(self) -> Iterator[int]:
|
||||
... # Y034 "__iter__" methods in classes like "BadIterator2" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator2.__iter__", e.g. "def __iter__(self) -> Self: ..."
|
||||
|
||||
|
||||
class BadIterator3(
|
||||
typing.Iterator[int]
|
||||
): # Y022 Use "collections.abc.Iterator[T]" instead of "typing.Iterator[T]" (PEP 585 syntax)
|
||||
def __iter__(self) -> collections.abc.Iterator[int]:
|
||||
... # Y034 "__iter__" methods in classes like "BadIterator3" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator3.__iter__", e.g. "def __iter__(self) -> Self: ..."
|
||||
|
||||
|
||||
class BadIterator4(Iterator[int]):
|
||||
# Note: *Iterable*, not *Iterator*, returned!
|
||||
def __iter__(self) -> Iterable[int]:
|
||||
... # Y034 "__iter__" methods in classes like "BadIterator4" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator4.__iter__", e.g. "def __iter__(self) -> Self: ..."
|
||||
|
||||
|
||||
class IteratorReturningIterable:
|
||||
def __iter__(self) -> Iterable[str]:
|
||||
... # Y045 "__iter__" methods should return an Iterator, not an Iterable
|
||||
|
||||
|
||||
class BadAsyncIterator(collections.abc.AsyncIterator[str]):
|
||||
def __aiter__(self) -> typing.AsyncIterator[str]:
|
||||
... # Y034 "__aiter__" methods in classes like "BadAsyncIterator" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadAsyncIterator.__aiter__", e.g. "def __aiter__(self) -> Self: ..." # Y022 Use "collections.abc.AsyncIterator[T]" instead of "typing.AsyncIterator[T]" (PEP 585 syntax)
|
||||
|
||||
|
||||
class AsyncIteratorReturningAsyncIterable:
|
||||
def __aiter__(self) -> AsyncIterable[str]:
|
||||
... # Y045 "__aiter__" methods should return an AsyncIterator, not an AsyncIterable
|
||||
|
||||
|
||||
class Abstract(Iterator[str]):
|
||||
@abstractmethod
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def __enter__(self) -> Abstract:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def __aenter__(self) -> Abstract:
|
||||
...
|
||||
|
||||
|
||||
class GoodIterator(Iterator[str]):
|
||||
def __iter__(self: Self) -> Self:
|
||||
...
|
||||
|
||||
|
||||
class GoodAsyncIterator(AsyncIterator[int]):
|
||||
def __aiter__(self: Self) -> Self:
|
||||
...
|
||||
|
||||
|
||||
class DoesNotInheritFromIterator:
|
||||
def __iter__(self) -> DoesNotInheritFromIterator:
|
||||
...
|
||||
|
||||
|
||||
class Unannotated:
|
||||
def __new__(cls, *args, **kwargs):
|
||||
...
|
||||
|
||||
def __iter__(self):
|
||||
...
|
||||
|
||||
def __aiter__(self):
|
||||
...
|
||||
|
||||
async def __aenter__(self):
|
||||
...
|
||||
|
||||
def __repr__(self):
|
||||
...
|
||||
|
||||
def __str__(self):
|
||||
...
|
||||
|
||||
def __eq__(self):
|
||||
...
|
||||
|
||||
def __ne__(self):
|
||||
...
|
||||
|
||||
def __iadd__(self):
|
||||
...
|
||||
|
||||
def __ior__(self):
|
||||
...
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
...
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
...
|
||||
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
...
|
||||
|
||||
|
||||
def __ne__(self, other: Any) -> bool:
|
||||
...
|
||||
|
||||
|
||||
def __imul__(self, other: Any) -> list[str]:
|
||||
...
|
188
crates/ruff/resources/test/fixtures/flake8_pyi/PYI034.pyi
vendored
Normal file
188
crates/ruff/resources/test/fixtures/flake8_pyi/PYI034.pyi
vendored
Normal file
|
@ -0,0 +1,188 @@
|
|||
# flags: --extend-ignore=Y023
|
||||
|
||||
import abc
|
||||
import builtins
|
||||
import collections.abc
|
||||
import typing
|
||||
from abc import abstractmethod
|
||||
from collections.abc import AsyncIterable, AsyncIterator, Iterable, Iterator
|
||||
from typing import Any, overload
|
||||
|
||||
import typing_extensions
|
||||
from _typeshed import Self
|
||||
from typing_extensions import final
|
||||
|
||||
class Bad(
|
||||
object
|
||||
): # Y040 Do not inherit from "object" explicitly, as it is redundant in Python 3
|
||||
def __new__(
|
||||
cls, *args: Any, **kwargs: Any
|
||||
) -> Bad: ... # Y034 "__new__" methods usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__new__", e.g. "def __new__(cls, *args: Any, **kwargs: Any) -> Self: ..."
|
||||
def __repr__(
|
||||
self,
|
||||
) -> str: ... # Y029 Defining __repr__ or __str__ in a stub is almost always redundant
|
||||
def __str__(
|
||||
self,
|
||||
) -> builtins.str: ... # Y029 Defining __repr__ or __str__ in a stub is almost always redundant
|
||||
def __eq__(
|
||||
self, other: Any
|
||||
) -> bool: ... # Y032 Prefer "object" to "Any" for the second parameter in "__eq__" methods
|
||||
def __ne__(
|
||||
self, other: typing.Any
|
||||
) -> typing.Any: ... # Y032 Prefer "object" to "Any" for the second parameter in "__ne__" methods
|
||||
def __enter__(
|
||||
self,
|
||||
) -> Bad: ... # Y034 "__enter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__enter__", e.g. "def __enter__(self) -> Self: ..."
|
||||
async def __aenter__(
|
||||
self,
|
||||
) -> Bad: ... # Y034 "__aenter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__aenter__", e.g. "async def __aenter__(self) -> Self: ..."
|
||||
def __iadd__(
|
||||
self, other: Bad
|
||||
) -> Bad: ... # Y034 "__iadd__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__iadd__", e.g. "def __iadd__(self, other: Bad) -> Self: ..."
|
||||
|
||||
class AlsoBad(
|
||||
int, builtins.object
|
||||
): ... # Y040 Do not inherit from "object" explicitly, as it is redundant in Python 3
|
||||
|
||||
class Good:
|
||||
def __new__(cls: type[Self], *args: Any, **kwargs: Any) -> Self: ...
|
||||
@abstractmethod
|
||||
def __str__(self) -> str: ...
|
||||
@abc.abstractmethod
|
||||
def __repr__(self) -> str: ...
|
||||
def __eq__(self, other: object) -> bool: ...
|
||||
def __ne__(self, obj: object) -> int: ...
|
||||
def __enter__(self: Self) -> Self: ...
|
||||
async def __aenter__(self: Self) -> Self: ...
|
||||
def __ior__(self: Self, other: Self) -> Self: ...
|
||||
|
||||
class Fine:
|
||||
@overload
|
||||
def __new__(cls, foo: int) -> FineSubclass: ...
|
||||
@overload
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> Fine: ...
|
||||
@abc.abstractmethod
|
||||
def __str__(self) -> str: ...
|
||||
@abc.abstractmethod
|
||||
def __repr__(self) -> str: ...
|
||||
def __eq__(self, other: Any, strange_extra_arg: list[str]) -> Any: ...
|
||||
def __ne__(self, *, kw_only_other: Any) -> bool: ...
|
||||
def __enter__(self) -> None: ...
|
||||
async def __aenter__(self) -> bool: ...
|
||||
|
||||
class FineSubclass(Fine): ...
|
||||
|
||||
class StrangeButAcceptable(str):
|
||||
@typing_extensions.overload
|
||||
def __new__(cls, foo: int) -> StrangeButAcceptableSubclass: ...
|
||||
@typing_extensions.overload
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> StrangeButAcceptable: ...
|
||||
def __str__(self) -> StrangeButAcceptable: ...
|
||||
def __repr__(self) -> StrangeButAcceptable: ...
|
||||
|
||||
class StrangeButAcceptableSubclass(StrangeButAcceptable): ...
|
||||
|
||||
class FineAndDandy:
|
||||
def __str__(self, weird_extra_arg) -> str: ...
|
||||
def __repr__(self, weird_extra_arg_with_default=...) -> str: ...
|
||||
|
||||
@final
|
||||
class WillNotBeSubclassed:
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> WillNotBeSubclassed: ...
|
||||
def __enter__(self) -> WillNotBeSubclassed: ...
|
||||
async def __aenter__(self) -> WillNotBeSubclassed: ...
|
||||
|
||||
# we don't emit an error for these; out of scope for a linter
|
||||
class InvalidButPluginDoesNotCrash:
|
||||
def __new__() -> InvalidButPluginDoesNotCrash: ...
|
||||
def __enter__() -> InvalidButPluginDoesNotCrash: ...
|
||||
async def __aenter__() -> InvalidButPluginDoesNotCrash: ...
|
||||
|
||||
class BadIterator1(Iterator[int]):
|
||||
def __iter__(
|
||||
self,
|
||||
) -> Iterator[
|
||||
int
|
||||
]: ... # Y034 "__iter__" methods in classes like "BadIterator1" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator1.__iter__", e.g. "def __iter__(self) -> Self: ..."
|
||||
|
||||
class BadIterator2(
|
||||
typing.Iterator[int]
|
||||
): # Y022 Use "collections.abc.Iterator[T]" instead of "typing.Iterator[T]" (PEP 585 syntax)
|
||||
def __iter__(
|
||||
self,
|
||||
) -> Iterator[
|
||||
int
|
||||
]: ... # Y034 "__iter__" methods in classes like "BadIterator2" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator2.__iter__", e.g. "def __iter__(self) -> Self: ..."
|
||||
|
||||
class BadIterator3(
|
||||
typing.Iterator[int]
|
||||
): # Y022 Use "collections.abc.Iterator[T]" instead of "typing.Iterator[T]" (PEP 585 syntax)
|
||||
def __iter__(
|
||||
self,
|
||||
) -> collections.abc.Iterator[
|
||||
int
|
||||
]: ... # Y034 "__iter__" methods in classes like "BadIterator3" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator3.__iter__", e.g. "def __iter__(self) -> Self: ..."
|
||||
|
||||
class BadIterator4(Iterator[int]):
|
||||
# Note: *Iterable*, not *Iterator*, returned!
|
||||
def __iter__(
|
||||
self,
|
||||
) -> Iterable[
|
||||
int
|
||||
]: ... # Y034 "__iter__" methods in classes like "BadIterator4" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadIterator4.__iter__", e.g. "def __iter__(self) -> Self: ..."
|
||||
|
||||
class IteratorReturningIterable:
|
||||
def __iter__(
|
||||
self,
|
||||
) -> Iterable[
|
||||
str
|
||||
]: ... # Y045 "__iter__" methods should return an Iterator, not an Iterable
|
||||
|
||||
class BadAsyncIterator(collections.abc.AsyncIterator[str]):
|
||||
def __aiter__(
|
||||
self,
|
||||
) -> typing.AsyncIterator[
|
||||
str
|
||||
]: ... # Y034 "__aiter__" methods in classes like "BadAsyncIterator" usually return "self" at runtime. Consider using "typing_extensions.Self" in "BadAsyncIterator.__aiter__", e.g. "def __aiter__(self) -> Self: ..." # Y022 Use "collections.abc.AsyncIterator[T]" instead of "typing.AsyncIterator[T]" (PEP 585 syntax)
|
||||
|
||||
class AsyncIteratorReturningAsyncIterable:
|
||||
def __aiter__(
|
||||
self,
|
||||
) -> AsyncIterable[
|
||||
str
|
||||
]: ... # Y045 "__aiter__" methods should return an AsyncIterator, not an AsyncIterable
|
||||
|
||||
class Abstract(Iterator[str]):
|
||||
@abstractmethod
|
||||
def __iter__(self) -> Iterator[str]: ...
|
||||
@abstractmethod
|
||||
def __enter__(self) -> Abstract: ...
|
||||
@abstractmethod
|
||||
async def __aenter__(self) -> Abstract: ...
|
||||
|
||||
class GoodIterator(Iterator[str]):
|
||||
def __iter__(self: Self) -> Self: ...
|
||||
|
||||
class GoodAsyncIterator(AsyncIterator[int]):
|
||||
def __aiter__(self: Self) -> Self: ...
|
||||
|
||||
class DoesNotInheritFromIterator:
|
||||
def __iter__(self) -> DoesNotInheritFromIterator: ...
|
||||
|
||||
class Unannotated:
|
||||
def __new__(cls, *args, **kwargs): ...
|
||||
def __iter__(self): ...
|
||||
def __aiter__(self): ...
|
||||
async def __aenter__(self): ...
|
||||
def __repr__(self): ...
|
||||
def __str__(self): ...
|
||||
def __eq__(self): ...
|
||||
def __ne__(self): ...
|
||||
def __iadd__(self): ...
|
||||
def __ior__(self): ...
|
||||
|
||||
def __repr__(self) -> str: ...
|
||||
def __str__(self) -> str: ...
|
||||
def __eq__(self, other: Any) -> bool: ...
|
||||
def __ne__(self, other: Any) -> bool: ...
|
||||
def __imul__(self, other: Any) -> list[str]: ...
|
|
@ -436,6 +436,17 @@ where
|
|||
if self.enabled(Rule::AnyEqNeAnnotation) {
|
||||
flake8_pyi::rules::any_eq_ne_annotation(self, name, args);
|
||||
}
|
||||
if self.enabled(Rule::NonSelfReturnType) {
|
||||
flake8_pyi::rules::non_self_return_type(
|
||||
self,
|
||||
stmt,
|
||||
name,
|
||||
decorator_list,
|
||||
returns.as_ref().map(|expr| &**expr),
|
||||
args,
|
||||
stmt.is_async_function_def_stmt(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if self.enabled(Rule::DunderFunctionName) {
|
||||
|
|
|
@ -596,6 +596,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
|||
(Flake8Pyi, "025") => (RuleGroup::Unspecified, Rule::UnaliasedCollectionsAbcSetImport),
|
||||
(Flake8Pyi, "032") => (RuleGroup::Unspecified, Rule::AnyEqNeAnnotation),
|
||||
(Flake8Pyi, "033") => (RuleGroup::Unspecified, Rule::TypeCommentInStub),
|
||||
(Flake8Pyi, "034") => (RuleGroup::Unspecified, Rule::NonSelfReturnType),
|
||||
(Flake8Pyi, "042") => (RuleGroup::Unspecified, Rule::SnakeCaseTypeAlias),
|
||||
(Flake8Pyi, "043") => (RuleGroup::Unspecified, Rule::TSuffixedTypeAlias),
|
||||
(Flake8Pyi, "045") => (RuleGroup::Unspecified, Rule::IterMethodReturnIterable),
|
||||
|
|
|
@ -519,6 +519,7 @@ ruff_macros::register_rules!(
|
|||
rules::flake8_pyi::rules::IterMethodReturnIterable,
|
||||
rules::flake8_pyi::rules::DuplicateUnionMember,
|
||||
rules::flake8_pyi::rules::EllipsisInNonEmptyClassBody,
|
||||
rules::flake8_pyi::rules::NonSelfReturnType,
|
||||
rules::flake8_pyi::rules::CollectionsNamedTuple,
|
||||
rules::flake8_pyi::rules::StringOrBytesTooLong,
|
||||
rules::flake8_pyi::rules::NonEmptyStubBody,
|
||||
|
|
|
@ -20,14 +20,16 @@ mod tests {
|
|||
#[test_case(Rule::AssignmentDefaultInStub, Path::new("PYI015.pyi"))]
|
||||
#[test_case(Rule::BadVersionInfoComparison, Path::new("PYI006.py"))]
|
||||
#[test_case(Rule::BadVersionInfoComparison, Path::new("PYI006.pyi"))]
|
||||
#[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.py"))]
|
||||
#[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.pyi"))]
|
||||
#[test_case(Rule::DocstringInStub, Path::new("PYI021.py"))]
|
||||
#[test_case(Rule::DocstringInStub, Path::new("PYI021.pyi"))]
|
||||
#[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.py"))]
|
||||
#[test_case(Rule::DuplicateUnionMember, Path::new("PYI016.pyi"))]
|
||||
#[test_case(Rule::EllipsisInNonEmptyClassBody, Path::new("PYI013.py"))]
|
||||
#[test_case(Rule::EllipsisInNonEmptyClassBody, Path::new("PYI013.pyi"))]
|
||||
#[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.py"))]
|
||||
#[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.pyi"))]
|
||||
#[test_case(Rule::NonSelfReturnType, Path::new("PYI034.py"))]
|
||||
#[test_case(Rule::NonSelfReturnType, Path::new("PYI034.pyi"))]
|
||||
#[test_case(Rule::IterMethodReturnIterable, Path::new("PYI045.py"))]
|
||||
#[test_case(Rule::IterMethodReturnIterable, Path::new("PYI045.pyi"))]
|
||||
#[test_case(Rule::NumericLiteralTooLong, Path::new("PYI054.py"))]
|
||||
|
|
|
@ -12,6 +12,7 @@ pub(crate) use iter_method_return_iterable::{
|
|||
iter_method_return_iterable, IterMethodReturnIterable,
|
||||
};
|
||||
pub(crate) use non_empty_stub_body::{non_empty_stub_body, NonEmptyStubBody};
|
||||
pub(crate) use non_self_return_type::{non_self_return_type, NonSelfReturnType};
|
||||
pub(crate) use numeric_literal_too_long::{numeric_literal_too_long, NumericLiteralTooLong};
|
||||
pub(crate) use pass_in_class_body::{pass_in_class_body, PassInClassBody};
|
||||
pub(crate) use pass_statement_stub_body::{pass_statement_stub_body, PassStatementStubBody};
|
||||
|
@ -45,6 +46,7 @@ mod duplicate_union_member;
|
|||
mod ellipsis_in_non_empty_class_body;
|
||||
mod iter_method_return_iterable;
|
||||
mod non_empty_stub_body;
|
||||
mod non_self_return_type;
|
||||
mod numeric_literal_too_long;
|
||||
mod pass_in_class_body;
|
||||
mod pass_statement_stub_body;
|
||||
|
|
298
crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs
Normal file
298
crates/ruff/src/rules/flake8_pyi/rules/non_self_return_type.rs
Normal file
|
@ -0,0 +1,298 @@
|
|||
use rustpython_parser::ast::{self, Arguments, Expr, Stmt};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::{identifier_range, map_subscript};
|
||||
use ruff_python_semantic::analyze::visibility::{is_abstract, is_final, is_overload};
|
||||
use ruff_python_semantic::model::SemanticModel;
|
||||
use ruff_python_semantic::scope::ScopeKind;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for methods that are annotated with a fixed return type, which
|
||||
/// should instead be returning `self`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// If methods like `__new__` or `__enter__` are annotated with a fixed return
|
||||
/// type, and the class is subclassed, type checkers will not be able to infer
|
||||
/// the correct return type.
|
||||
///
|
||||
/// For example:
|
||||
/// ```python
|
||||
/// class Shape:
|
||||
/// def set_scale(self, scale: float) -> Shape:
|
||||
/// self.scale = scale
|
||||
/// return self
|
||||
///
|
||||
/// class Circle(Shape):
|
||||
/// def set_radius(self, radius: float) -> Circle:
|
||||
/// self.radius = radius
|
||||
/// return self
|
||||
///
|
||||
/// # This returns `Shape`, not `Circle`.
|
||||
/// Circle().set_scale(0.5)
|
||||
///
|
||||
/// # Thus, this expression is invalid, as `Shape` has no attribute `set_radius`.
|
||||
/// Circle().set_scale(0.5).set_radius(2.7)
|
||||
/// ```
|
||||
///
|
||||
/// Specifically, this check enforces that the return type of the following
|
||||
/// methods is `Self`:
|
||||
///
|
||||
/// 1. In-place binary operations, like `__iadd__`, `__imul__`, etc.
|
||||
/// 1. `__new__`, `__enter__`, and `__aenter__`, if those methods return the
|
||||
/// class name.
|
||||
/// 1. `__iter__` methods that return `Iterator`, despite the class inheriting
|
||||
/// directly from `Iterator`.
|
||||
/// 1. `__aiter__` methods that return `AsyncIterator`, despite the class
|
||||
/// inheriting directly from `AsyncIterator`.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
/// def __new__(cls, *args: Any, **kwargs: Any) -> Bad:
|
||||
/// ...
|
||||
///
|
||||
/// def __enter__(self) -> Bad:
|
||||
/// ...
|
||||
///
|
||||
/// async def __aenter__(self) -> Bad:
|
||||
/// ...
|
||||
///
|
||||
/// def __iadd__(self, other: Bad) -> Bad:
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from typing_extensions import Self
|
||||
///
|
||||
///
|
||||
/// class Foo:
|
||||
/// def __new__(cls, *args: Any, **kwargs: Any) -> Self:
|
||||
/// ...
|
||||
///
|
||||
/// def __enter__(self) -> Self:
|
||||
/// ...
|
||||
///
|
||||
/// async def __aenter__(self) -> Self:
|
||||
/// ...
|
||||
///
|
||||
/// def __iadd__(self, other: Bad) -> Self:
|
||||
/// ...
|
||||
/// ```
|
||||
/// ## References
|
||||
/// - [PEP 673](https://peps.python.org/pep-0673/)
|
||||
#[violation]
|
||||
pub struct NonSelfReturnType {
|
||||
class_name: String,
|
||||
method_name: String,
|
||||
}
|
||||
|
||||
impl Violation for NonSelfReturnType {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let NonSelfReturnType {
|
||||
class_name,
|
||||
method_name,
|
||||
} = self;
|
||||
if matches!(class_name.as_str(), "__new__") {
|
||||
format!("`__new__` methods usually return `self` at runtime")
|
||||
} else {
|
||||
format!("`{method_name}` methods in classes like `{class_name}` usually return `self` at runtime")
|
||||
}
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> Option<String> {
|
||||
Some("Consider using `typing_extensions.Self` as return type".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// PYI034
|
||||
pub(crate) fn non_self_return_type(
|
||||
checker: &mut Checker,
|
||||
stmt: &Stmt,
|
||||
name: &str,
|
||||
decorator_list: &[Expr],
|
||||
returns: Option<&Expr>,
|
||||
args: &Arguments,
|
||||
async_: bool,
|
||||
) {
|
||||
let ScopeKind::Class(class_def) = checker.semantic_model().scope().kind else {
|
||||
return;
|
||||
};
|
||||
|
||||
if args.args.is_empty() && args.posonlyargs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(returns) = returns else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Skip any abstract or overloaded methods.
|
||||
if is_abstract(checker.semantic_model(), decorator_list)
|
||||
|| is_overload(checker.semantic_model(), decorator_list)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if async_ {
|
||||
if name == "__aenter__"
|
||||
&& is_name(returns, &class_def.name)
|
||||
&& !is_final(checker.semantic_model(), &class_def.decorator_list)
|
||||
{
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
NonSelfReturnType {
|
||||
class_name: class_def.name.to_string(),
|
||||
method_name: name.to_string(),
|
||||
},
|
||||
identifier_range(stmt, checker.locator),
|
||||
));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// In-place methods that are expected to return `Self`.
|
||||
if INPLACE_BINOP_METHODS.contains(&name) {
|
||||
if !is_self(returns, checker.semantic_model()) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
NonSelfReturnType {
|
||||
class_name: class_def.name.to_string(),
|
||||
method_name: name.to_string(),
|
||||
},
|
||||
identifier_range(stmt, checker.locator),
|
||||
));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if is_name(returns, &class_def.name) {
|
||||
if matches!(name, "__enter__" | "__new__")
|
||||
&& !is_final(checker.semantic_model(), &class_def.decorator_list)
|
||||
{
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
NonSelfReturnType {
|
||||
class_name: class_def.name.to_string(),
|
||||
method_name: name.to_string(),
|
||||
},
|
||||
identifier_range(stmt, checker.locator),
|
||||
));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
match name {
|
||||
"__iter__" => {
|
||||
if is_iterable(returns, checker.semantic_model())
|
||||
&& is_iterator(&class_def.bases, checker.semantic_model())
|
||||
{
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
NonSelfReturnType {
|
||||
class_name: class_def.name.to_string(),
|
||||
method_name: name.to_string(),
|
||||
},
|
||||
identifier_range(stmt, checker.locator),
|
||||
));
|
||||
}
|
||||
}
|
||||
"__aiter__" => {
|
||||
if is_async_iterable(returns, checker.semantic_model())
|
||||
&& is_async_iterator(&class_def.bases, checker.semantic_model())
|
||||
{
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
NonSelfReturnType {
|
||||
class_name: class_def.name.to_string(),
|
||||
method_name: name.to_string(),
|
||||
},
|
||||
identifier_range(stmt, checker.locator),
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
const INPLACE_BINOP_METHODS: &[&str] = &[
|
||||
"__iadd__",
|
||||
"__isub__",
|
||||
"__imul__",
|
||||
"__imatmul__",
|
||||
"__itruediv__",
|
||||
"__ifloordiv__",
|
||||
"__imod__",
|
||||
"__ipow__",
|
||||
"__ilshift__",
|
||||
"__irshift__",
|
||||
"__iand__",
|
||||
"__ixor__",
|
||||
"__ior__",
|
||||
];
|
||||
|
||||
/// Return `true` if the given expression resolves to the given name.
|
||||
fn is_name(expr: &Expr, name: &str) -> bool {
|
||||
let Expr::Name(ast::ExprName { id, .. }) = expr else {
|
||||
return false;
|
||||
};
|
||||
id.as_str() == name
|
||||
}
|
||||
|
||||
/// Return `true` if the given expression resolves to `typing.Self`.
|
||||
fn is_self(expr: &Expr, model: &SemanticModel) -> bool {
|
||||
model.match_typing_expr(expr, "Self")
|
||||
}
|
||||
|
||||
/// Return `true` if the given class extends `collections.abc.Iterator`.
|
||||
fn is_iterator(bases: &[Expr], model: &SemanticModel) -> bool {
|
||||
bases.iter().any(|expr| {
|
||||
model
|
||||
.resolve_call_path(map_subscript(expr))
|
||||
.map_or(false, |call_path| {
|
||||
matches!(
|
||||
call_path.as_slice(),
|
||||
["typing", "Iterator"] | ["collections", "abc", "Iterator"]
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Return `true` if the given expression resolves to `collections.abc.Iterable`.
|
||||
fn is_iterable(expr: &Expr, model: &SemanticModel) -> bool {
|
||||
model
|
||||
.resolve_call_path(map_subscript(expr))
|
||||
.map_or(false, |call_path| {
|
||||
matches!(
|
||||
call_path.as_slice(),
|
||||
["typing", "Iterable" | "Iterator"]
|
||||
| ["collections", "abc", "Iterable" | "Iterator"]
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Return `true` if the given class extends `collections.abc.AsyncIterator`.
|
||||
fn is_async_iterator(bases: &[Expr], model: &SemanticModel) -> bool {
|
||||
bases.iter().any(|expr| {
|
||||
model
|
||||
.resolve_call_path(map_subscript(expr))
|
||||
.map_or(false, |call_path| {
|
||||
matches!(
|
||||
call_path.as_slice(),
|
||||
["typing", "AsyncIterator"] | ["collections", "abc", "AsyncIterator"]
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Return `true` if the given expression resolves to `collections.abc.AsyncIterable`.
|
||||
fn is_async_iterable(expr: &Expr, model: &SemanticModel) -> bool {
|
||||
model
|
||||
.resolve_call_path(map_subscript(expr))
|
||||
.map_or(false, |call_path| {
|
||||
matches!(
|
||||
call_path.as_slice(),
|
||||
["typing", "AsyncIterable" | "AsyncIterator"]
|
||||
| ["collections", "abc", "AsyncIterable" | "AsyncIterator"]
|
||||
)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
---
|
||||
source: crates/ruff/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI034.pyi:18:9: PYI034 `__new__` methods in classes like `Bad` usually return `self` at runtime
|
||||
|
|
||||
18 | object
|
||||
19 | ): # Y040 Do not inherit from "object" explicitly, as it is redundant in Python 3
|
||||
20 | def __new__(
|
||||
| ^^^^^^^ PYI034
|
||||
21 | cls, *args: Any, **kwargs: Any
|
||||
22 | ) -> Bad: ... # Y034 "__new__" methods usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__new__", e.g. "def __new__(cls, *args: Any, **kwargs: Any) -> Self: ..."
|
||||
|
|
||||
= help: Consider using `typing_extensions.Self` as return type
|
||||
|
||||
PYI034.pyi:33:9: PYI034 `__enter__` methods in classes like `Bad` usually return `self` at runtime
|
||||
|
|
||||
33 | self, other: typing.Any
|
||||
34 | ) -> typing.Any: ... # Y032 Prefer "object" to "Any" for the second parameter in "__ne__" methods
|
||||
35 | def __enter__(
|
||||
| ^^^^^^^^^ PYI034
|
||||
36 | self,
|
||||
37 | ) -> Bad: ... # Y034 "__enter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__enter__", e.g. "def __enter__(self) -> Self: ..."
|
||||
|
|
||||
= help: Consider using `typing_extensions.Self` as return type
|
||||
|
||||
PYI034.pyi:36:15: PYI034 `__aenter__` methods in classes like `Bad` usually return `self` at runtime
|
||||
|
|
||||
36 | self,
|
||||
37 | ) -> Bad: ... # Y034 "__enter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__enter__", e.g. "def __enter__(self) -> Self: ..."
|
||||
38 | async def __aenter__(
|
||||
| ^^^^^^^^^^ PYI034
|
||||
39 | self,
|
||||
40 | ) -> Bad: ... # Y034 "__aenter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__aenter__", e.g. "async def __aenter__(self) -> Self: ..."
|
||||
|
|
||||
= help: Consider using `typing_extensions.Self` as return type
|
||||
|
||||
PYI034.pyi:39:9: PYI034 `__iadd__` methods in classes like `Bad` usually return `self` at runtime
|
||||
|
|
||||
39 | self,
|
||||
40 | ) -> Bad: ... # Y034 "__aenter__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__aenter__", e.g. "async def __aenter__(self) -> Self: ..."
|
||||
41 | def __iadd__(
|
||||
| ^^^^^^^^ PYI034
|
||||
42 | self, other: Bad
|
||||
43 | ) -> Bad: ... # Y034 "__iadd__" methods in classes like "Bad" usually return "self" at runtime. Consider using "typing_extensions.Self" in "Bad.__iadd__", e.g. "def __iadd__(self, other: Bad) -> Self: ..."
|
||||
|
|
||||
= help: Consider using `typing_extensions.Self` as return type
|
||||
|
||||
PYI034.pyi:102:9: PYI034 `__iter__` methods in classes like `BadIterator1` usually return `self` at runtime
|
||||
|
|
||||
102 | class BadIterator1(Iterator[int]):
|
||||
103 | def __iter__(
|
||||
| ^^^^^^^^ PYI034
|
||||
104 | self,
|
||||
105 | ) -> Iterator[
|
||||
|
|
||||
= help: Consider using `typing_extensions.Self` as return type
|
||||
|
||||
PYI034.pyi:111:9: PYI034 `__iter__` methods in classes like `BadIterator2` usually return `self` at runtime
|
||||
|
|
||||
111 | typing.Iterator[int]
|
||||
112 | ): # Y022 Use "collections.abc.Iterator[T]" instead of "typing.Iterator[T]" (PEP 585 syntax)
|
||||
113 | def __iter__(
|
||||
| ^^^^^^^^ PYI034
|
||||
114 | self,
|
||||
115 | ) -> Iterator[
|
||||
|
|
||||
= help: Consider using `typing_extensions.Self` as return type
|
||||
|
||||
PYI034.pyi:120:9: PYI034 `__iter__` methods in classes like `BadIterator3` usually return `self` at runtime
|
||||
|
|
||||
120 | typing.Iterator[int]
|
||||
121 | ): # Y022 Use "collections.abc.Iterator[T]" instead of "typing.Iterator[T]" (PEP 585 syntax)
|
||||
122 | def __iter__(
|
||||
| ^^^^^^^^ PYI034
|
||||
123 | self,
|
||||
124 | ) -> collections.abc.Iterator[
|
||||
|
|
||||
= help: Consider using `typing_extensions.Self` as return type
|
||||
|
||||
PYI034.pyi:128:9: PYI034 `__iter__` methods in classes like `BadIterator4` usually return `self` at runtime
|
||||
|
|
||||
128 | class BadIterator4(Iterator[int]):
|
||||
129 | # Note: *Iterable*, not *Iterator*, returned!
|
||||
130 | def __iter__(
|
||||
| ^^^^^^^^ PYI034
|
||||
131 | self,
|
||||
132 | ) -> Iterable[
|
||||
|
|
||||
= help: Consider using `typing_extensions.Self` as return type
|
||||
|
||||
PYI034.pyi:142:9: PYI034 `__aiter__` methods in classes like `BadAsyncIterator` usually return `self` at runtime
|
||||
|
|
||||
142 | class BadAsyncIterator(collections.abc.AsyncIterator[str]):
|
||||
143 | def __aiter__(
|
||||
| ^^^^^^^^^ PYI034
|
||||
144 | self,
|
||||
145 | ) -> typing.AsyncIterator[
|
||||
|
|
||||
= help: Consider using `typing_extensions.Self` as return type
|
||||
|
||||
|
|
@ -684,12 +684,27 @@ pub fn collect_arg_names<'a>(arguments: &'a Arguments) -> FxHashSet<&'a str> {
|
|||
/// callable.
|
||||
pub fn map_callable(decorator: &Expr) -> &Expr {
|
||||
if let Expr::Call(ast::ExprCall { func, .. }) = decorator {
|
||||
// Ex) `@decorator()`
|
||||
func
|
||||
} else {
|
||||
// Ex) `@decorator`
|
||||
decorator
|
||||
}
|
||||
}
|
||||
|
||||
/// Given an [`Expr`] that can be callable or not (like a decorator, which could
|
||||
/// be used with or without explicit call syntax), return the underlying
|
||||
/// callable.
|
||||
pub fn map_subscript(expr: &Expr) -> &Expr {
|
||||
if let Expr::Subscript(ast::ExprSubscript { value, .. }) = expr {
|
||||
// Ex) `Iterable[T]`
|
||||
value
|
||||
} else {
|
||||
// Ex) `Iterable`
|
||||
expr
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if a statement or expression includes at least one comment.
|
||||
pub fn has_comments<T>(located: &T, locator: &Locator) -> bool
|
||||
where
|
||||
|
|
|
@ -89,6 +89,14 @@ pub fn is_property(
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns `true` if a class is an `final`.
|
||||
pub fn is_final(model: &SemanticModel, decorator_list: &[Expr]) -> bool {
|
||||
decorator_list
|
||||
.iter()
|
||||
.any(|expr| model.match_typing_expr(map_callable(expr), "final"))
|
||||
}
|
||||
|
||||
/// Returns `true` if a function is a "magic method".
|
||||
pub fn is_magic(name: &str) -> bool {
|
||||
name.starts_with("__") && name.ends_with("__")
|
||||
|
|
|
@ -93,6 +93,10 @@ impl<'a> SemanticModel<'a> {
|
|||
return true;
|
||||
}
|
||||
|
||||
if call_path.as_slice() == ["_typeshed", target] {
|
||||
return true;
|
||||
}
|
||||
|
||||
if TYPING_EXTENSIONS.contains(target) {
|
||||
if call_path.as_slice() == ["typing_extensions", target] {
|
||||
return true;
|
||||
|
|
1
ruff.schema.json
generated
1
ruff.schema.json
generated
|
@ -2244,6 +2244,7 @@
|
|||
"PYI03",
|
||||
"PYI032",
|
||||
"PYI033",
|
||||
"PYI034",
|
||||
"PYI04",
|
||||
"PYI042",
|
||||
"PYI043",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue