Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
37 changes: 33 additions & 4 deletions mypy/stubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1313,6 +1313,34 @@ def verify_missing(
yield Error(object_path, "is not present in stub", stub, runtime)


def _unbind_classmethod_alias(
stub_type: mypy.types.Type | None, runtime: MaybeMissing[Any]
) -> mypy.types.Type | None:
"""Drop the implicit first argument of a stub alias that points at a classmethod.

An assignment such as ``d = c`` in a stub refers to the classmethod object, so its
type keeps the ``cls`` argument, whereas reading the same name off the class at
runtime gives a method already bound to it.
"""
if stub_type is None:
return None
if not (inspect.ismethod(runtime) and isinstance(runtime.__self__, type)):
return stub_type
proper_type = mypy.types.get_proper_type(stub_type)
if not isinstance(proper_type, mypy.types.CallableType) or not proper_type.arg_types:
return stub_type
if not (
isinstance(mypy.types.get_proper_type(proper_type.arg_types[0]), mypy.types.TypeType)
or proper_type.arg_names[0] in ("cls", "mcls", "metacls")
):
return stub_type
return proper_type.copy_modified(
arg_types=proper_type.arg_types[1:],
arg_kinds=proper_type.arg_kinds[1:],
arg_names=proper_type.arg_names[1:],
)


@verify.register(nodes.Var)
def verify_var(
stub: nodes.Var, runtime: MaybeMissing[Any], object_path: list[str]
Expand All @@ -1331,23 +1359,24 @@ def verify_var(
yield Error(object_path, "is read-only at runtime but not in the stub", stub, runtime)

runtime_type = get_mypy_type_of_runtime_value(runtime, type_context=stub.type)
stub_type = _unbind_classmethod_alias(stub.type, runtime)
note = ""
if (
runtime_type is not None
and stub.type is not None
and not is_subtype_helper(runtime_type, stub.type)
and stub_type is not None
and not is_subtype_helper(runtime_type, stub_type)
):
should_error = True
# Avoid errors when defining enums, since runtime_type is the enum itself, but we'd
# annotate it with the type of runtime.value
if isinstance(runtime, enum.Enum):
runtime_type = get_mypy_type_of_runtime_value(runtime.value)
if runtime_type is not None and is_subtype_helper(runtime_type, stub.type):
if runtime_type is not None and is_subtype_helper(runtime_type, stub_type):
should_error = False
# We always allow setting the stub value to Ellipsis (...), but use
# _value_ type as a fallback if given. If a member is ... and _value_
# type is given, all runtime types should be assignable to _value_.
proper_type = mypy.types.get_proper_type(stub.type)
proper_type = mypy.types.get_proper_type(stub_type)
if (
isinstance(proper_type, mypy.types.Instance)
and proper_type.type.fullname in mypy.types.ELLIPSIS_TYPE_NAMES
Expand Down
37 changes: 37 additions & 0 deletions mypy/test/teststubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,43 @@ def __new__(cls, *args, **kwargs): pass
""",
error=None,
)
# An alias in the stub refers to the classmethod object, which keeps its "cls"
# argument, while reading the same name off the class at runtime gives a bound
# method.
yield Case(
stub="""
class GoodAlias:
def m(self) -> None: ...
m_alias = m
@classmethod
def c(cls) -> None: ...
c_alias = c
""",
runtime="""
class GoodAlias:
def m(self): pass
m_alias = m
@classmethod
def c(cls): pass
c_alias = c
""",
error=None,
)
yield Case(
stub="""
class BadAlias:
@classmethod
def c(cls) -> None: ...
c_alias: int
""",
runtime="""
class BadAlias:
@classmethod
def c(cls): pass
c_alias = c
""",
error="BadAlias.c_alias",
)

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