Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stubtest] support @type_check_only decorator #16422

Merged
merged 5 commits into from
Nov 11, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,7 @@ class FuncBase(Node):
"is_static", # Uses "@staticmethod" (explicit or implicit)
"is_final", # Uses "@final"
"is_explicit_override", # Uses "@override"
"is_type_check_only", # Uses "@type_check_only"
"_fullname",
)

Expand All @@ -530,6 +531,7 @@ def __init__(self) -> None:
self.is_static = False
self.is_final = False
self.is_explicit_override = False
self.is_type_check_only = False
# Name with module prefix
self._fullname = ""

Expand Down Expand Up @@ -2866,6 +2868,7 @@ class is generic then it will be a type constructor of higher kind.
"type_var_tuple_suffix",
"self_type",
"dataclass_transform_spec",
"is_type_check_only",
)

_fullname: str # Fully qualified name
Expand Down Expand Up @@ -3016,6 +3019,9 @@ class is generic then it will be a type constructor of higher kind.
# Added if the corresponding class is directly decorated with `typing.dataclass_transform`
dataclass_transform_spec: DataclassTransformSpec | None

# Is set to `True` when class is decorated with `@typing.type_check_only`
is_type_check_only: bool

FLAGS: Final = [
"is_abstract",
"is_enum",
Expand Down Expand Up @@ -3072,6 +3078,7 @@ def __init__(self, names: SymbolTable, defn: ClassDef, module_name: str) -> None
self.metadata = {}
self.self_type = None
self.dataclass_transform_spec = None
self.is_type_check_only = False

def add_type_vars(self) -> None:
self.has_type_var_tuple_type = False
Expand Down
6 changes: 6 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@
REVEAL_TYPE_NAMES,
TPDICT_NAMES,
TYPE_ALIAS_NAMES,
TYPE_CHECK_ONLY_NAMES,
TYPED_NAMEDTUPLE_NAMES,
AnyType,
CallableType,
Expand Down Expand Up @@ -1568,6 +1569,9 @@ def visit_decorator(self, dec: Decorator) -> None:
removed.append(i)
else:
self.fail("@final cannot be used with non-method functions", d)
elif refers_to_fullname(d, TYPE_CHECK_ONLY_NAMES):
# TODO: support `@overload` funcs.
dec.func.is_type_check_only = True
elif isinstance(d, CallExpr) and refers_to_fullname(
d.callee, DATACLASS_TRANSFORM_NAMES
):
Expand Down Expand Up @@ -1868,6 +1872,8 @@ def analyze_class_decorator(self, defn: ClassDef, decorator: Expression) -> None
self.fail("@runtime_checkable can only be used with protocol classes", defn)
elif decorator.fullname in FINAL_DECORATOR_NAMES:
defn.info.is_final = True
elif refers_to_fullname(decorator, TYPE_CHECK_ONLY_NAMES):
defn.info.is_type_check_only = True
elif isinstance(decorator, CallExpr) and refers_to_fullname(
decorator.callee, DATACLASS_TRANSFORM_NAMES
):
Expand Down
27 changes: 27 additions & 0 deletions mypy/stubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,19 @@ def _verify_metaclass(
def verify_typeinfo(
stub: nodes.TypeInfo, runtime: MaybeMissing[type[Any]], object_path: list[str]
) -> Iterator[Error]:
if stub.is_type_check_only:
# This type only exists in stubs, we only check that the runtime part
# is missing. Other checks are not required.
if not isinstance(runtime, Missing):
yield Error(
object_path,
'is marked as "@type_check_only", but also exists at runtime',
stub,
runtime,
stub_desc=repr(stub),
)
return

if isinstance(runtime, Missing):
yield Error(object_path, "is not present at runtime", stub, runtime, stub_desc=repr(stub))
return
Expand Down Expand Up @@ -1066,6 +1079,7 @@ def verify_var(
def verify_overloadedfuncdef(
stub: nodes.OverloadedFuncDef, runtime: MaybeMissing[Any], object_path: list[str]
) -> Iterator[Error]:
# TODO: support `@type_check_only` decorator
if isinstance(runtime, Missing):
yield Error(object_path, "is not present at runtime", stub, runtime)
return
Expand Down Expand Up @@ -1253,6 +1267,19 @@ def apply_decorator_to_funcitem(
def verify_decorator(
stub: nodes.Decorator, runtime: MaybeMissing[Any], object_path: list[str]
) -> Iterator[Error]:
if stub.func.is_type_check_only:
# This function only exists in stubs, we only check that the runtime part
# is missing. Other checks are not required.
if not isinstance(runtime, Missing):
yield Error(
object_path,
'is marked as "@type_check_only", but also exists at runtime',
stub,
runtime,
stub_desc=repr(stub),
)
return

if isinstance(runtime, Missing):
yield Error(object_path, "is not present at runtime", stub, runtime)
return
Expand Down
45 changes: 45 additions & 0 deletions mypy/test/teststubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class Sequence(Iterable[_T_co]): ...
class Tuple(Sequence[_T_co]): ...
class NamedTuple(tuple[Any, ...]): ...
def overload(func: _T) -> _T: ...
def type_check_only(func: _T) -> _T: ...
"""

stubtest_builtins_stub = """
Expand Down Expand Up @@ -2027,6 +2028,50 @@ def some(self) -> int: ...
error=None,
)

@collect_cases
def test_type_check_only(self) -> Iterator[Case]:
yield Case(
stub="from typing import type_check_only, overload",
runtime="from typing import overload",
error=None,
)
# You can have public types that are only defined in stubs
# with `@type_check_only`:
yield Case(
stub="""
@type_check_only
class A1: ...
""",
runtime="",
error=None,
)
# Having `@type_check_only` on a type that existist in runtime is an error
sobolevn marked this conversation as resolved.
Show resolved Hide resolved
yield Case(
stub="""
@type_check_only
class A2: ...
""",
runtime="class A2: ...",
error="A2",
)
# The same is true for functions:
yield Case(
stub="""
@type_check_only
def func1() -> None: ...
""",
runtime="",
error=None,
)
yield Case(
stub="""
@type_check_only
def func2() -> None: ...
""",
runtime="def func2() -> None: ...",
error="func2",
)


def remove_color_code(s: str) -> str:
return re.sub("\\x1b.*?m", "", s) # this works!
Expand Down
3 changes: 3 additions & 0 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@
# Supported @final decorator names.
FINAL_DECORATOR_NAMES: Final = ("typing.final", "typing_extensions.final")

# Supported @type_check_only names.
TYPE_CHECK_ONLY_NAMES: Final = ("typing.type_check_only", "typing_extensions.type_check_only")

# Supported Literal type names.
LITERAL_TYPE_NAMES: Final = ("typing.Literal", "typing_extensions.Literal")

Expand Down
40 changes: 40 additions & 0 deletions test-data/unit/check-classes.test
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I fully understand what the point is of adding the tests you've added in this file, since they all seem to be asserting undesirable behaviour

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not undersirable, it just a status-quo. It is expected to be working like this for now. We don't have any other expected behavior for now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and the status quo is undesirable -- ideally, mypy would fully support @type_check_only. If somebody came along and implemented the feature in the future, I think it would be highly confusing if existing tests started failing as a result of the feature being implemented

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, we now know that these tests work and can add them later.
I am actually interested in working on this further :)

Original file line number Diff line number Diff line change
Expand Up @@ -8012,3 +8012,43 @@ class C:
def f(self) -> None:
__module__ # E: Name "__module__" is not defined
__qualname__ # E: Name "__qualname__" is not defined

[case testTypeCheckOnlyInStub]
# TODO: Ideally, in the future this import should raise an error:
# because, we are importing a thing that does not exist in runtime.
# But, not for now: maybe we will have a new error code for this.
from foo import A, b

a: A
reveal_type(a.x) # N: Revealed type is "builtins.int"
reveal_type(b) # N: Revealed type is "def ()"
[file foo.pyi]
from typing import type_check_only

@type_check_only
class A:
x: int

@type_check_only
def b() -> None: ...
[builtins fixtures/bool.pyi]
[typing fixtures/typing-full.pyi]


[case testTypeCheckOnlyInRuntime]
# Right now there should be no difference between runtime and stub,
# but in the future it might change.
from typing import type_check_only

@type_check_only
class A:
x: int

@type_check_only
def b() -> None: ...

a: A
reveal_type(a.x) # N: Revealed type is "builtins.int"
reveal_type(b) # N: Revealed type is "def ()"
[builtins fixtures/bool.pyi]
[typing fixtures/typing-full.pyi]
3 changes: 3 additions & 0 deletions test-data/unit/fixtures/typing-full.pyi
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the changes to the fixtures anymore, after the latest push?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is typing-full, it won't hurt.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, makes sense

Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,6 @@ def override(__arg: T) -> T: ...

# Was added in 3.11
def reveal_type(__obj: T) -> T: ...

# Only exists in type checking time:
def type_check_only(__func_or_class: T) -> T: ...