Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions mypy/stubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,8 @@ def _verify_static_class_methods(
yield "runtime is a staticmethod but stub is not"
if not isinstance(static_runtime, staticmethod) and stub.is_static:
yield "stub is a staticmethod but runtime is not"
if isinstance(stub, nodes.FuncDef):
yield from _verify_final_method(stub, static_runtime)


def _verify_arg_name(
Expand Down Expand Up @@ -1115,6 +1117,7 @@ def verify_paramspecexpr(
def _verify_readonly_property(stub: nodes.Decorator, runtime: Any) -> Iterator[str]:
assert stub.func.is_property
if isinstance(runtime, property):
yield from _verify_final_method(stub.func, runtime.fget)
return
if inspect.isdatadescriptor(runtime):
# It's enough like a property...
Expand Down Expand Up @@ -1143,6 +1146,11 @@ def _verify_abstract_status(stub: nodes.FuncDef, runtime: Any) -> Iterator[str]:
yield f"is inconsistent, runtime {item_type} is abstract but stub is not"


def _verify_final_method(stub: nodes.FuncDef, runtime: Any) -> Iterator[str]:
if getattr(runtime, "__final__", False) and not stub.is_final:
yield "is decorated with @final at runtime, but not in the stub"


def _resolve_funcitem_from_decorator(dec: nodes.OverloadPart) -> nodes.FuncItem | None:
"""Returns a FuncItem that corresponds to the output of the decorator.

Expand Down
141 changes: 140 additions & 1 deletion mypy/test/teststubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1143,7 +1143,10 @@ def test_not_subclassable(self) -> Iterator[Case]:
def test_has_runtime_final_decorator(self) -> Iterator[Case]:
yield Case(
stub="from typing_extensions import final",
runtime="from typing_extensions import final",
runtime="""
import functools
from typing_extensions import final
""",
error=None,
)
yield Case(
Expand Down Expand Up @@ -1177,6 +1180,142 @@ class C: ...
""",
error="C",
)
yield Case(
stub="""
class D:
@final
def foo(self) -> None: ...
@final
@staticmethod
def bar() -> None: ...
@final
@classmethod
def baz(cls) -> None: ...
@property
@final
def eggs(self) -> int: ...
@final
def ham(self, obj: int) -> int: ...
""",
runtime="""
class D:
@final
def foo(self): pass
@final
@staticmethod
def bar(): pass
@final
@classmethod
def baz(cls): pass
@property
@final
def eggs(self): return 42
@final
@functools.lru_cache()
def ham(self, obj): return obj * 2
""",
error=None,
)
# Stub methods are allowed to have @final even if the runtime doesn't...
yield Case(
stub="""
class E:
@final
def foo(self) -> None: ...
@final
@staticmethod
def bar() -> None: ...
@final
@classmethod
def baz(cls) -> None: ...
@property
@final
def eggs(self) -> int: ...
@final
def ham(self, obj: int) -> int: ...
""",
runtime="""
class E:
def foo(self): pass
@staticmethod
def bar(): pass
@classmethod
def baz(cls): pass
@property
def eggs(self): return 42
@functools.lru_cache()
def ham(self, obj): return obj * 2
""",
error=None,
)
# ...But if the runtime has @final, the stub must have it as well
yield Case(
stub="""
class F:
def foo(self) -> None: ...
""",
runtime="""
class F:
@final
def foo(self): pass
""",
error="F.foo",
)
yield Case(
stub="""
class G:
@staticmethod
def foo() -> None: ...
""",
runtime="""
class G:
@final
@staticmethod
def foo(): pass
""",
error="G.foo",
)
yield Case(
stub="""
class H:
@classmethod
def foo(cls) -> None: ...
""",
runtime="""
class H:
@final
@classmethod
def foo(cls): pass
""",
error="H.foo",
)
yield Case(
stub="""
class I:
@property
def foo(self) -> int: ...
""",
runtime="""
class I:
@property
@final
Comment thread
AlexWaygood marked this conversation as resolved.
def foo(self): return 42
""",
error="I.foo",
)
yield Case(
stub="""
class J:
def foo(self, obj: int) -> int: ...
""",
runtime="""
class J:
@final
@functools.lru_cache()
def foo(self, obj): return obj * 2
Comment thread
AlexWaygood marked this conversation as resolved.
""",
error="J.foo",
)

@collect_cases
def test_name_mangling(self) -> Iterator[Case]:
Expand Down