Skip to content

Commit

Permalink
Stubtest: verify stub methods or properties are decorated with `@fina…
Browse files Browse the repository at this point in the history
…l` if they are decorated with `@final` at runtime (#14951)

This implements most of #14924. The only thing it _doesn't_ implement is
verification for overloaded methods decorated with `@final` -- I tried
working on that, but hit #14950.
  • Loading branch information
AlexWaygood committed Mar 24, 2023
1 parent 2c6e43e commit 01a088b
Show file tree
Hide file tree
Showing 2 changed files with 260 additions and 14 deletions.
54 changes: 41 additions & 13 deletions mypy/stubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,8 +529,21 @@ def verify_typeinfo(
yield from verify(stub_to_verify, runtime_attr, object_path + [entry])


def _static_lookup_runtime(object_path: list[str]) -> MaybeMissing[Any]:
static_runtime = importlib.import_module(object_path[0])
for entry in object_path[1:]:
try:
static_runtime = inspect.getattr_static(static_runtime, entry)
except AttributeError:
# This can happen with mangled names, ignore for now.
# TODO: pass more information about ancestors of nodes/objects to verify, so we don't
# have to do this hacky lookup. Would be useful in several places.
return MISSING
return static_runtime


def _verify_static_class_methods(
stub: nodes.FuncBase, runtime: Any, object_path: list[str]
stub: nodes.FuncBase, runtime: Any, static_runtime: MaybeMissing[Any], object_path: list[str]
) -> Iterator[str]:
if stub.name in ("__new__", "__init_subclass__", "__class_getitem__"):
# Special cased by Python, so don't bother checking
Expand All @@ -545,16 +558,8 @@ def _verify_static_class_methods(
yield "stub is a classmethod but runtime is not"
return

# Look the object up statically, to avoid binding by the descriptor protocol
static_runtime = importlib.import_module(object_path[0])
for entry in object_path[1:]:
try:
static_runtime = inspect.getattr_static(static_runtime, entry)
except AttributeError:
# This can happen with mangled names, ignore for now.
# TODO: pass more information about ancestors of nodes/objects to verify, so we don't
# have to do this hacky lookup. Would be useful in a couple other places too.
return
if static_runtime is MISSING:
return

if isinstance(static_runtime, classmethod) and not stub.is_class:
yield "runtime is a classmethod but stub is not"
Expand Down Expand Up @@ -945,11 +950,16 @@ def verify_funcitem(
if not callable(runtime):
return

# Look the object up statically, to avoid binding by the descriptor protocol
static_runtime = _static_lookup_runtime(object_path)

if isinstance(stub, nodes.FuncDef):
for error_text in _verify_abstract_status(stub, runtime):
yield Error(object_path, error_text, stub, runtime)
for error_text in _verify_final_method(stub, runtime, static_runtime):
yield Error(object_path, error_text, stub, runtime)

for message in _verify_static_class_methods(stub, runtime, object_path):
for message in _verify_static_class_methods(stub, runtime, static_runtime, object_path):
yield Error(object_path, "is inconsistent, " + message, stub, runtime)

signature = safe_inspect_signature(runtime)
Expand Down Expand Up @@ -1063,9 +1073,15 @@ def verify_overloadedfuncdef(
for msg in _verify_abstract_status(first_part.func, runtime):
yield Error(object_path, msg, stub, runtime)

for message in _verify_static_class_methods(stub, runtime, object_path):
# Look the object up statically, to avoid binding by the descriptor protocol
static_runtime = _static_lookup_runtime(object_path)

for message in _verify_static_class_methods(stub, runtime, static_runtime, object_path):
yield Error(object_path, "is inconsistent, " + message, stub, runtime)

# TODO: Should call _verify_final_method here,
# but overloaded final methods in stubs cause a stubtest crash: see #14950

signature = safe_inspect_signature(runtime)
if not signature:
return
Expand Down Expand Up @@ -1126,6 +1142,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, MISSING)
return
if inspect.isdatadescriptor(runtime):
# It's enough like a property...
Expand Down Expand Up @@ -1154,6 +1171,17 @@ 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, static_runtime: MaybeMissing[Any]
) -> Iterator[str]:
if stub.is_final:
return
if getattr(runtime, "__final__", False) or (
static_runtime is not MISSING and getattr(static_runtime, "__final__", False)
):
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
220 changes: 219 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,221 @@ class C: ...
""",
error="C",
)
yield Case(
stub="""
class D:
@final
def foo(self) -> None: ...
@final
@staticmethod
def bar() -> None: ...
@staticmethod
@final
def bar2() -> None: ...
@final
@classmethod
def baz(cls) -> None: ...
@classmethod
@final
def baz2(cls) -> None: ...
@property
@final
def eggs(self) -> int: ...
@final
@property
def eggs2(self) -> int: ...
@final
def ham(self, obj: int) -> int: ...
""",
runtime="""
class D:
@final
def foo(self): pass
@final
@staticmethod
def bar(): pass
@staticmethod
@final
def bar2(): pass
@final
@classmethod
def baz(cls): pass
@classmethod
@final
def baz2(cls): pass
@property
@final
def eggs(self): return 42
@final
@property
def eggs2(self): pass
@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: ...
@staticmethod
@final
def bar2() -> None: ...
@final
@classmethod
def baz(cls) -> None: ...
@classmethod
@final
def baz2(cls) -> None: ...
@property
@final
def eggs(self) -> int: ...
@final
@property
def eggs2(self) -> int: ...
@final
def ham(self, obj: int) -> int: ...
""",
runtime="""
class E:
def foo(self): pass
@staticmethod
def bar(): pass
@staticmethod
def bar2(): pass
@classmethod
def baz(cls): pass
@classmethod
def baz2(cls): pass
@property
def eggs(self): return 42
@property
def eggs2(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:
@staticmethod
def foo() -> None: ...
""",
runtime="""
class H:
@staticmethod
@final
def foo(): pass
""",
error="H.foo",
)
yield Case(
stub="""
class I:
@classmethod
def foo(cls) -> None: ...
""",
runtime="""
class I:
@final
@classmethod
def foo(cls): pass
""",
error="I.foo",
)
yield Case(
stub="""
class J:
@classmethod
def foo(cls) -> None: ...
""",
runtime="""
class J:
@classmethod
@final
def foo(cls): pass
""",
error="J.foo",
)
yield Case(
stub="""
class K:
@property
def foo(self) -> int: ...
""",
runtime="""
class K:
@property
@final
def foo(self): return 42
""",
error="K.foo",
)
# This test wouldn't pass,
# because the runtime can't set __final__ on instances of builtins.property,
# so stubtest has non way of knowing that the runtime was decorated with @final:
#
# yield Case(
# stub="""
# class K2:
# @property
# def foo(self) -> int: ...
# """,
# runtime="""
# class K2:
# @final
# @property
# def foo(self): return 42
# """,
# error="K2.foo",
# )
yield Case(
stub="""
class L:
def foo(self, obj: int) -> int: ...
""",
runtime="""
class L:
@final
@functools.lru_cache()
def foo(self, obj): return obj * 2
""",
error="L.foo",
)

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

0 comments on commit 01a088b

Please sign in to comment.