PY-58752 Impl inspection for check matching override signature

GitOrigin-RevId: 4520138ac657a514b53f2f939521f0336701be46
This commit is contained in:
Andrey.Matveev
2023-10-10 17:10:47 +03:00
committed by intellij-monorepo-bot
parent 51d3e45887
commit c24752ceb1
11 changed files with 435 additions and 56 deletions

View File

@@ -1,39 +1,83 @@
import _typeshed
import abc
import collections
import sys
import typing
from _collections_abc import dict_items, dict_keys, dict_values
from _typeshed import IdentityFunction
from collections.abc import Iterable
from typing import ( # noqa: Y022,Y027,Y039
from _typeshed import IdentityFunction, Incomplete
from typing import ( # noqa: Y022,Y037,Y038,Y039
IO as IO,
TYPE_CHECKING as TYPE_CHECKING,
Any,
AbstractSet as AbstractSet,
Any as Any,
AnyStr as AnyStr,
AsyncContextManager as AsyncContextManager,
AsyncGenerator as AsyncGenerator,
AsyncIterable as AsyncIterable,
AsyncIterator as AsyncIterator,
Awaitable as Awaitable,
Callable,
BinaryIO as BinaryIO,
Callable as Callable,
ChainMap as ChainMap,
ClassVar as ClassVar,
Collection as Collection,
Container as Container,
ContextManager as ContextManager,
Coroutine as Coroutine,
Counter as Counter,
DefaultDict as DefaultDict,
Deque as Deque,
Mapping,
NewType as NewType,
Dict as Dict,
ForwardRef as ForwardRef,
FrozenSet as FrozenSet,
Generator as Generator,
Generic as Generic,
Hashable as Hashable,
ItemsView as ItemsView,
Iterable as Iterable,
Iterator as Iterator,
KeysView as KeysView,
List as List,
Mapping as Mapping,
MappingView as MappingView,
Match as Match,
MutableMapping as MutableMapping,
MutableSequence as MutableSequence,
MutableSet as MutableSet,
NoReturn as NoReturn,
Sequence,
Optional as Optional,
Pattern as Pattern,
Reversible as Reversible,
Sequence as Sequence,
Set as Set,
Sized as Sized,
SupportsAbs as SupportsAbs,
SupportsBytes as SupportsBytes,
SupportsComplex as SupportsComplex,
SupportsFloat as SupportsFloat,
SupportsInt as SupportsInt,
SupportsRound as SupportsRound,
Text as Text,
TextIO as TextIO,
Tuple as Tuple,
Type as Type,
TypeVar,
Union as Union,
ValuesView as ValuesView,
_Alias,
cast as cast,
no_type_check as no_type_check,
no_type_check_decorator as no_type_check_decorator,
overload as overload,
type_check_only,
)
if sys.version_info >= (3, 10):
from types import UnionType
if sys.version_info >= (3, 9):
from types import GenericAlias
__all__ = [
"Any",
"Buffer",
"ClassVar",
"Concatenate",
"Final",
@@ -43,6 +87,7 @@ __all__ = [
"ParamSpecKwargs",
"Self",
"Type",
"TypeVar",
"TypeVarTuple",
"Unpack",
"Awaitable",
@@ -60,22 +105,31 @@ __all__ = [
"OrderedDict",
"TypedDict",
"SupportsIndex",
"SupportsAbs",
"SupportsRound",
"SupportsBytes",
"SupportsComplex",
"SupportsFloat",
"SupportsInt",
"Annotated",
"assert_never",
"assert_type",
"dataclass_transform",
"deprecated",
"final",
"IntVar",
"is_typeddict",
"Literal",
"NewType",
"overload",
"override",
"Protocol",
"reveal_type",
"runtime",
"runtime_checkable",
"Text",
"TypeAlias",
"TypeAliasType",
"TypeGuard",
"TYPE_CHECKING",
"Never",
@@ -85,13 +139,54 @@ __all__ = [
"clear_overloads",
"get_args",
"get_origin",
"get_original_bases",
"get_overloads",
"get_type_hints",
"AbstractSet",
"AnyStr",
"BinaryIO",
"Callable",
"Collection",
"Container",
"Dict",
"Doc",
"ForwardRef",
"FrozenSet",
"Generator",
"Generic",
"Hashable",
"IO",
"ItemsView",
"Iterable",
"Iterator",
"KeysView",
"List",
"Mapping",
"MappingView",
"Match",
"MutableMapping",
"MutableSequence",
"MutableSet",
"Optional",
"Pattern",
"Reversible",
"Sequence",
"Set",
"Sized",
"TextIO",
"Tuple",
"Union",
"ValuesView",
"cast",
"get_protocol_members",
"is_protocol",
"no_type_check",
"no_type_check_decorator",
]
_T = TypeVar("_T")
_F = TypeVar("_F", bound=Callable[..., Any])
_TC = TypeVar("_TC", bound=Type[object])
_T = typing.TypeVar("_T")
_F = typing.TypeVar("_F", bound=Callable[..., Any])
_TC = typing.TypeVar("_TC", bound=type[object])
# unfortunately we have to duplicate this class definition from typing.pyi or we break pytype
class _SpecialForm:
@@ -105,7 +200,7 @@ class _SpecialForm:
# typing.Protocol and typing_extensions.Protocol so they can properly
# warn users about potential runtime exceptions when using typing.Protocol
# on older versions of Python.
Protocol: _SpecialForm = ...
Protocol: _SpecialForm
def runtime_checkable(cls: _TC) -> _TC: ...
@@ -126,7 +221,8 @@ class _TypedDict(Mapping[str, object], metaclass=abc.ABCMeta):
__required_keys__: ClassVar[frozenset[str]]
__optional_keys__: ClassVar[frozenset[str]]
__total__: ClassVar[bool]
def copy(self: _typeshed.Self) -> _typeshed.Self: ...
__orig_bases__: ClassVar[tuple[Any, ...]]
def copy(self) -> Self: ...
# Using Never so that only calls using mypy plugin hook that specialize the signature
# can go through.
def setdefault(self, k: Never, default: object) -> object: ...
@@ -138,8 +234,16 @@ class _TypedDict(Mapping[str, object], metaclass=abc.ABCMeta):
def values(self) -> dict_values[str, object]: ...
def __delitem__(self, k: Never) -> None: ...
if sys.version_info >= (3, 9):
def __or__(self: _typeshed.Self, __value: _typeshed.Self) -> _typeshed.Self: ...
def __ior__(self: _typeshed.Self, __value: _typeshed.Self) -> _typeshed.Self: ...
@overload
def __or__(self, __value: Self) -> Self: ...
@overload
def __or__(self, __value: dict[str, Any]) -> dict[str, object]: ...
@overload
def __ror__(self, __value: Self) -> Self: ...
@overload
def __ror__(self, __value: dict[str, Any]) -> dict[str, object]: ...
# supposedly incompatible definitions of `__ior__` and `__or__`:
def __ior__(self, __value: Self) -> Self: ... # type: ignore[misc]
# TypedDict is a (non-subscriptable) special form.
TypedDict: object
@@ -148,11 +252,23 @@ OrderedDict = _Alias()
def get_type_hints(
obj: Callable[..., Any],
globalns: dict[str, Any] | None = ...,
localns: dict[str, Any] | None = ...,
include_extras: bool = ...,
globalns: dict[str, Any] | None = None,
localns: dict[str, Any] | None = None,
include_extras: bool = False,
) -> dict[str, Any]: ...
def get_args(tp: Any) -> tuple[Any, ...]: ...
if sys.version_info >= (3, 10):
@overload
def get_origin(tp: UnionType) -> type[UnionType]: ...
if sys.version_info >= (3, 9):
@overload
def get_origin(tp: GenericAlias) -> type: ...
@overload
def get_origin(tp: ParamSpecArgs | ParamSpecKwargs) -> ParamSpec: ...
@overload
def get_origin(tp: Any) -> Any | None: ...
Annotated: _SpecialForm
@@ -163,11 +279,11 @@ class SupportsIndex(Protocol, metaclass=abc.ABCMeta):
@abc.abstractmethod
def __index__(self) -> int: ...
# New things in 3.10
# New and changed things in 3.10
if sys.version_info >= (3, 10):
from typing import (
Concatenate as Concatenate,
ParamSpec as ParamSpec,
NewType as NewType,
ParamSpecArgs as ParamSpecArgs,
ParamSpecKwargs as ParamSpecKwargs,
TypeAlias as TypeAlias,
@@ -175,31 +291,28 @@ if sys.version_info >= (3, 10):
is_typeddict as is_typeddict,
)
else:
@final
class ParamSpecArgs:
__origin__: ParamSpec
@property
def __origin__(self) -> ParamSpec: ...
def __init__(self, origin: ParamSpec) -> None: ...
@final
class ParamSpecKwargs:
__origin__: ParamSpec
@property
def __origin__(self) -> ParamSpec: ...
def __init__(self, origin: ParamSpec) -> None: ...
class ParamSpec:
__name__: str
__bound__: type[Any] | None
__covariant__: bool
__contravariant__: bool
def __init__(
self, name: str, *, bound: None | type[Any] | str = ..., contravariant: bool = ..., covariant: bool = ...
) -> None: ...
@property
def args(self) -> ParamSpecArgs: ...
@property
def kwargs(self) -> ParamSpecKwargs: ...
Concatenate: _SpecialForm
TypeAlias: _SpecialForm
TypeGuard: _SpecialForm
def is_typeddict(tp: object) -> bool: ...
class NewType:
def __init__(self, name: str, tp: Any) -> None: ...
def __call__(self, __x: _T) -> _T: ...
__supertype__: type
# New things in 3.11
# NamedTuples are not new, but the ability to create generic NamedTuples is new in 3.11
if sys.version_info >= (3, 11):
@@ -210,7 +323,6 @@ if sys.version_info >= (3, 11):
NotRequired as NotRequired,
Required as Required,
Self as Self,
TypeVarTuple as TypeVarTuple,
Unpack as Unpack,
assert_never as assert_never,
assert_type as assert_type,
@@ -221,7 +333,7 @@ if sys.version_info >= (3, 11):
)
else:
Self: _SpecialForm
Never: _SpecialForm = ...
Never: _SpecialForm
def reveal_type(__obj: _T) -> _T: ...
def assert_never(__arg: Never) -> Never: ...
def assert_type(__val: _T, __typ: Any) -> _T: ...
@@ -233,38 +345,154 @@ else:
LiteralString: _SpecialForm
Unpack: _SpecialForm
@final
class TypeVarTuple:
__name__: str
def __init__(self, name: str) -> None: ...
def __iter__(self) -> Any: ... # Unpack[Self]
def dataclass_transform(
*,
eq_default: bool = ...,
order_default: bool = ...,
kw_only_default: bool = ...,
field_specifiers: tuple[type[Any] | Callable[..., Any], ...] = ...,
eq_default: bool = True,
order_default: bool = False,
kw_only_default: bool = False,
frozen_default: bool = False,
field_specifiers: tuple[type[Any] | Callable[..., Any], ...] = (),
**kwargs: object,
) -> IdentityFunction: ...
class NamedTuple(tuple[Any, ...]):
if sys.version_info < (3, 8):
_field_types: collections.OrderedDict[str, type]
_field_types: ClassVar[collections.OrderedDict[str, type]]
elif sys.version_info < (3, 9):
_field_types: dict[str, type]
_field_defaults: dict[str, Any]
_fields: tuple[str, ...]
_source: str
_field_types: ClassVar[dict[str, type]]
_field_defaults: ClassVar[dict[str, Any]]
_fields: ClassVar[tuple[str, ...]]
__orig_bases__: ClassVar[tuple[Any, ...]]
@overload
def __init__(self, typename: str, fields: Iterable[tuple[str, Any]] = ...) -> None: ...
@overload
def __init__(self, typename: str, fields: None = ..., **kwargs: Any) -> None: ...
def __init__(self, typename: str, fields: None = None, **kwargs: Any) -> None: ...
@classmethod
def _make(cls: type[_typeshed.Self], iterable: Iterable[Any]) -> _typeshed.Self: ...
def _make(cls, iterable: Iterable[Any]) -> Self: ...
if sys.version_info >= (3, 8):
def _asdict(self) -> dict[str, Any]: ...
else:
def _asdict(self) -> collections.OrderedDict[str, Any]: ...
def _replace(self: _typeshed.Self, **kwargs: Any) -> _typeshed.Self: ...
def _replace(self, **kwargs: Any) -> Self: ...
# New things in 3.xx
# The `default` parameter was added to TypeVar, ParamSpec, and TypeVarTuple (PEP 696)
# The `infer_variance` parameter was added to TypeVar in 3.12 (PEP 695)
# typing_extensions.override (PEP 698)
@final
class TypeVar:
@property
def __name__(self) -> str: ...
@property
def __bound__(self) -> Any | None: ...
@property
def __constraints__(self) -> tuple[Any, ...]: ...
@property
def __covariant__(self) -> bool: ...
@property
def __contravariant__(self) -> bool: ...
@property
def __infer_variance__(self) -> bool: ...
@property
def __default__(self) -> Any | None: ...
def __init__(
self,
name: str,
*constraints: Any,
bound: Any | None = None,
covariant: bool = False,
contravariant: bool = False,
default: Any | None = None,
infer_variance: bool = False,
) -> None: ...
if sys.version_info >= (3, 10):
def __or__(self, right: Any) -> _SpecialForm: ...
def __ror__(self, left: Any) -> _SpecialForm: ...
if sys.version_info >= (3, 11):
def __typing_subst__(self, arg: Incomplete) -> Incomplete: ...
@final
class ParamSpec:
@property
def __name__(self) -> str: ...
@property
def __bound__(self) -> Any | None: ...
@property
def __covariant__(self) -> bool: ...
@property
def __contravariant__(self) -> bool: ...
@property
def __infer_variance__(self) -> bool: ...
@property
def __default__(self) -> Any | None: ...
def __init__(
self,
name: str,
*,
bound: None | type[Any] | str = None,
contravariant: bool = False,
covariant: bool = False,
default: type[Any] | str | None = None,
) -> None: ...
@property
def args(self) -> ParamSpecArgs: ...
@property
def kwargs(self) -> ParamSpecKwargs: ...
@final
class TypeVarTuple:
@property
def __name__(self) -> str: ...
@property
def __default__(self) -> Any | None: ...
def __init__(self, name: str, *, default: Any | None = None) -> None: ...
def __iter__(self) -> Any: ... # Unpack[Self]
def deprecated(__msg: str, *, category: type[Warning] | None = ..., stacklevel: int = 1) -> Callable[[_T], _T]: ...
if sys.version_info >= (3, 12):
from collections.abc import Buffer as Buffer
from types import get_original_bases as get_original_bases
from typing import TypeAliasType as TypeAliasType, override as override
else:
def override(__arg: _F) -> _F: ...
def get_original_bases(__cls: type) -> tuple[Any, ...]: ...
@final
class TypeAliasType:
def __init__(
self, name: str, value: Any, *, type_params: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] = ()
) -> None: ...
@property
def __value__(self) -> Any: ...
@property
def __type_params__(self) -> tuple[TypeVar | ParamSpec | TypeVarTuple, ...]: ...
@property
def __parameters__(self) -> tuple[Any, ...]: ...
@property
def __name__(self) -> str: ...
# It's writable on types, but not on instances of TypeAliasType.
@property
def __module__(self) -> str | None: ... # type: ignore[override]
def __getitem__(self, parameters: Any) -> Any: ...
if sys.version_info >= (3, 10):
def __or__(self, right: Any) -> _SpecialForm: ...
def __ror__(self, left: Any) -> _SpecialForm: ...
@runtime_checkable
class Buffer(Protocol):
# Not actually a Protocol at runtime; see
# https://github.com/python/typeshed/issues/10224 for why we're defining it this way
def __buffer__(self, __flags: int) -> memoryview: ...
if sys.version_info >= (3, 13):
from typing import get_protocol_members as get_protocol_members, is_protocol as is_protocol
else:
def is_protocol(__tp: type) -> bool: ...
def get_protocol_members(__tp: type) -> frozenset[str]: ...
class Doc:
documentation: str
def __init__(self, __documentation: str) -> None: ...
def __hash__(self) -> int: ...
def __eq__(self, other: object) -> bool: ...

View File

@@ -97,6 +97,8 @@ public final class PyNames {
public static final String STATICMETHOD = "staticmethod";
public static final String OVERLOAD = "overload";
public static final String OVERRIDE = "override";
public static final String PROPERTY = "property";
public static final String SETTER = "setter";
public static final String DELETER = "deleter";

View File

@@ -225,6 +225,7 @@
<localInspection language="Python" shortName="PyAbstractClassInspection" suppressId="PyAbstractClass" bundle="messages.PyPsiBundle" key="INSP.NAME.abstract.class" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WEAK WARNING" implementationClass="com.jetbrains.python.inspections.PyAbstractClassInspection"/>
<localInspection language="Python" shortName="PyMissingTypeHintsInspection" suppressId="PyMissingTypeHints" bundle="messages.PyPsiBundle" key="INSP.NAME.missing.type.hints" groupKey="INSP.GROUP.python" enabledByDefault="false" level="WEAK WARNING" implementationClass="com.jetbrains.python.inspections.PyMissingTypeHintsInspection"/>
<localInspection language="Python" shortName="PyOverloadsInspection" suppressId="PyOverloads" bundle="messages.PyPsiBundle" key="INSP.NAME.overloads.in.regular.python.files" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyOverloadsInspection"/>
<localInspection language="Python" shortName="PyOverridesInspection" suppressId="PyOverrides" bundle="messages.PyPsiBundle" key="INSP.NAME.invalid.usages.of.override.decorator" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyOverridesInspection"/>
<localInspection language="Python" shortName="PyProtocolInspection" suppressId="PyProtocol" bundle="messages.PyPsiBundle" key="INSP.NAME.protocol.definition.and.usages" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyProtocolInspection"/>
<localInspection language="Python" shortName="PyTypeHintsInspection" suppressId="PyTypeHints" bundle="messages.PyPsiBundle" key="INSP.NAME.type.hints" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyTypeHintsInspection"/>
<localInspection language="Python" shortName="PyTypedDictInspection" suppressId="PyTypedDict" bundle="messages.PyPsiBundle" key="INSP.NAME.typed.dict" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyTypedDictInspection"/>

View File

@@ -0,0 +1,25 @@
<html>
<body>
<p>Reports when a method decorated with @override doesn't have a matching method in its ancestor classes</p>
<p><b>Example:</b></p>
<pre><code>
from typing import override
class Parent:
def foo(self) -> int:
return 1
def bar(self, x: str) -> str:
return x
class Child(Parent):
@override
def foo(self) -> int:
return 2
@override # Missing super method for override function
def baz(self) -> int:
return 1
</code></pre>
</body>
</html>

View File

@@ -1019,6 +1019,10 @@ INSP.overloads.series.overload.decorated.functions.should.always.be.followed.by.
INSP.overloads.this.method.overload.signature.not.compatible.with.implementation=Signature of this @overload-decorated method is not compatible with the implementation
INSP.overloads.this.function.overload.signature.not.compatible.with.implementation=Signature of this @overload-decorated function is not compatible with the implementation
# PyOverridesInspection
INSP.NAME.invalid.usages.of.override.decorator=Invalid usages of @override decorator
INSP.override.missing.super.method=Missing super method for override
# PyPep8NamingInspection
INSP.NAME.pep8.naming=PEP 8 naming convention violation
INSP.pep8.naming.column.name.excluded.base.classes=Excluded base classes:

View File

@@ -0,0 +1,42 @@
// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.inspections
import com.intellij.codeInspection.LocalInspectionToolSession
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.PsiElementVisitor
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.psi.PyFunction
import com.jetbrains.python.psi.PyKnownDecoratorUtil
import com.jetbrains.python.psi.search.PySuperMethodsSearch
import com.jetbrains.python.psi.types.TypeEvalContext
class PyOverridesInspection : PyInspection() {
override fun buildVisitor(holder: ProblemsHolder,
isOnTheFly: Boolean,
session: LocalInspectionToolSession): PsiElementVisitor =
Visitor(holder, PyInspectionVisitor.getContext(session))
private class Visitor(holder: ProblemsHolder, context: TypeEvalContext) : PyInspectionVisitor(holder, context) {
override fun visitPyFunction(node: PyFunction) {
super.visitPyFunction(node)
if (!PyKnownDecoratorUtil.getKnownDecorators(node, myTypeEvalContext)
.any { it == PyKnownDecoratorUtil.KnownDecorator.TYPING_OVERRIDE ||
it == PyKnownDecoratorUtil.KnownDecorator.TYPING_EXTENSIONS_OVERRIDE }) {
return
}
val overrideDecorator = node.decoratorList?.decorators?.firstOrNull {
"override" == it.qualifiedName?.lastComponent
} ?: return
val superMethods = PySuperMethodsSearch.search(node, myTypeEvalContext).findAll()
if (superMethods.isEmpty()) {
registerProblem(overrideDecorator, PyPsiBundle.message("INSP.override.missing.super.method"))
return
}
}
}
}

View File

@@ -63,6 +63,8 @@ public final class PyKnownDecoratorUtil {
UNITTEST_MOCK_PATCH("unittest.mock.patch"),
TYPING_OVERLOAD("typing." + PyNames.OVERLOAD),
TYPING_OVERRIDE("typing." + PyNames.OVERRIDE),
TYPING_EXTENSIONS_OVERRIDE("typing_extensions." + PyNames.OVERRIDE),
TYPING_RUNTIME("typing.runtime"),
TYPING_RUNTIME_EXT("typing_extensions.runtime"),
TYPING_RUNTIME_CHECKABLE("typing.runtime_checkable"),

View File

@@ -0,0 +1,12 @@
from mod import Parent
from typing_extensions import override
class Child(Parent):
@override
def foo(self, x: str, y: int) -> str:
return 2
<warning descr="Missing super method for override">@override</warning>
def baz(self, x: str, y: int) -> str:
return "1"

View File

@@ -0,0 +1,7 @@
class Parent:
def foo(self, x: str, y: int) -> str:
return 1
def bar(self, x: str, y: int, z: float) -> str:
return x

View File

@@ -0,0 +1,17 @@
from typing_extensions import override
class Parent:
def foo(self) -> int:
return 1
def bar(self, x: str) -> str:
return x
class Child(Parent):
@override
def foo(self) -> int:
return 2
<warning descr="Missing super method for override">@override</warning>
def baz() -> int:
return 1

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.python.inspections;
import com.jetbrains.python.fixtures.PyInspectionTestCase;
import com.jetbrains.python.psi.LanguageLevel;
import org.jetbrains.annotations.NotNull;
public class PyOverridesInspectionTest extends PyInspectionTestCase {
// PY-58752
public void testMissingSuperMethod() {
doTest();
}
// PY-58752
public void testMissingSuperMethodMultifile() {
doMultiFileTest();
}
@NotNull
@Override
protected Class<? extends PyInspection> getInspectionClass() {
return PyOverridesInspection.class;
}
}