Skip to content
Merged
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
95 changes: 84 additions & 11 deletions crates/ty_python_semantic/resources/mdtest/decorators.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,8 @@ def takes_int(x: int) -> int:
# error: [invalid-argument-type]
@takes_int
class Foo: ...

reveal_type(Foo) # revealed: int
```

Using `None` as a decorator is an error:
Expand All @@ -396,6 +398,8 @@ Using `None` as a decorator is an error:
# error: [call-non-callable]
@None
class Bar: ...

reveal_type(Bar) # revealed: <class 'Bar'>
```

A decorator can enforce type constraints on the class being decorated:
Expand Down Expand Up @@ -457,7 +461,7 @@ reveal_type(DataclassThenWrapped) # revealed: WrapBackend
class WrappedThenDataclass:
value: int

reveal_type(WrappedThenDataclass) # revealed: Unknown
reveal_type(WrappedThenDataclass) # revealed: WrapBackend

def int_decorator_factory() -> Callable[[type[object]], int]:
def decorator(cls: type[object]) -> int:
Expand All @@ -470,7 +474,7 @@ def int_decorator_factory() -> Callable[[type[object]], int]:
class IntThenDataclass:
value: int

reveal_type(IntThenDataclass) # revealed: Unknown
reveal_type(IntThenDataclass) # revealed: int

@WrapBackend
class InvalidWrappedBase(1): ... # error: [invalid-base]
Expand All @@ -496,8 +500,8 @@ class OverloadedCacheClient:
return b""
```

Unannotated class decorators are assumed to preserve the class binding. We do not infer returned
classes from decorator bodies:
When a class decorator returns `Unknown`, we preserve the current binding. Unannotated decorators
have an `Unknown` return type because we do not infer returned classes from decorator bodies:

```py
def personify(cls):
Expand Down Expand Up @@ -549,7 +553,13 @@ callable_decorator = CallableDecorator()
class CallableInstanceDecorated: ...

reveal_type(CallableInstanceDecorated) # revealed: <class 'CallableInstanceDecorated'>
```

An explicit return annotation can also produce `Unknown`, for example when a type variable is not
specialized. We preserve the class binding in these cases too, but use the return type when it is
known:

```py
class ExplicitReturnDecorator(Generic[T]):
def __call__(self, cls) -> T:
raise NotImplementedError
Expand All @@ -559,7 +569,7 @@ explicit_return_decorator = ExplicitReturnDecorator()
@explicit_return_decorator
class ExplicitReturnCallableInstanceDecorated: ...

reveal_type(ExplicitReturnCallableInstanceDecorated) # revealed: Unknown
reveal_type(ExplicitReturnCallableInstanceDecorated) # revealed: <class 'ExplicitReturnCallableInstanceDecorated'>

specialized_explicit_return_decorator = ExplicitReturnDecorator[int]()

Expand All @@ -578,28 +588,36 @@ def explicit_return_callable_decorator(cls) -> T:
@explicit_return_callable_decorator
class ExplicitReturnCallableDecorated: ...

reveal_type(ExplicitReturnCallableDecorated) # revealed: Unknown
reveal_type(ExplicitReturnCallableDecorated) # revealed: <class 'ExplicitReturnCallableDecorated'>

def regular_callable_replacement_factory() -> Callable[[type[object]], T]:
raise NotImplementedError

@regular_callable_replacement_factory()
class RegularCallableReplacementDecorated: ...

reveal_type(RegularCallableReplacementDecorated) # revealed: Unknown
reveal_type(RegularCallableReplacementDecorated) # revealed: <class 'RegularCallableReplacementDecorated'>
```

An unknown class decorator still makes the class binding unknown:
An unknown class decorator preserves the class binding while still reporting the unresolved
reference:

```py
# error: [unresolved-reference] "Name `unknown_class_decorator` used when not defined"
@unknown_class_decorator
class UnknownDecorated: ...
class UnknownDecorated:
def method(self, value: int) -> str:
return str(value)

reveal_type(UnknownDecorated) # revealed: Unknown
reveal_type(UnknownDecorated) # revealed: <class 'UnknownDecorated'>
reveal_type(UnknownDecorated()) # revealed: UnknownDecorated
reveal_type(UnknownDecorated().method(1)) # revealed: str
UnknownDecorated().method("a") # error: [invalid-argument-type]
```

An unannotated class decorator preserves the result of earlier decorators:
If an earlier decorator replaces the class with an instance, an `Unknown` return type preserves that
instance's type. This applies both to unannotated decorators and to decorators whose return
annotations evaluate to `Unknown`:

```py
def unannotated_identity(cls):
Expand All @@ -610,6 +628,12 @@ def unannotated_identity(cls):
class WrappedThenUnannotated: ...

reveal_type(WrappedThenUnannotated) # revealed: WrapBackend

@explicit_return_decorator
@WrapBackend
class WrappedThenUnknown: ...

reveal_type(WrappedThenUnknown) # revealed: WrapBackend
```

Metadata decorators still apply above an unannotated class-preserving decorator:
Expand All @@ -627,6 +651,55 @@ class DeprecatedThenUnannotated: ...
DeprecatedThenUnannotated() # error: [deprecated] "use OtherClass"
```

## Unknown return annotations on class decorators

An unresolved return annotation produces `Unknown`. This preserves the class binding and allows
outer metadata decorators to apply to the class:

```py
from dataclasses import dataclass

def decorator(cls: type) -> Missing: # error: [unresolved-reference]
return cls

@dataclass
@decorator
class C:
value: int

reveal_type(C) # revealed: <class 'C'>
reveal_type(C(1).value) # revealed: int
C("a") # error: [invalid-argument-type]
```

## Explicitly dynamic class decorators

Unlike `Unknown`, an explicit `Any` return type replaces the class binding:

```py
from typing import Any

def decorator(cls: type) -> Any:
return cls

@decorator
class C: ...

reveal_type(C) # revealed: Any
```

An explicit `type[Any]` return type also replaces the binding:

```py
def class_decorator(cls: type) -> type[Any]:
return cls

@class_decorator
class D: ...

reveal_type(D) # revealed: type[Any]
```

## Preserving the original class object

If a class decorator returns the original class object, we preserve the class binding so it can
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,28 @@ class Child(Base[T]): ...
child: Child[int]
```

## Unknown decorators on generic bases

An unresolved decorator preserves the class binding and its generic context. A subclass can forward
a type variable to the decorated base and be specialized without a cascading error.

```py
from typing import Generic, TypeVar
from ty_extensions._internal import generic_context

T = TypeVar("T")

# error: [unresolved-reference] "Name `unknown_decorator` used when not defined"
@unknown_decorator
class Base(Generic[T]): ...

reveal_type(generic_context(Base)) # revealed: ty_extensions._internal.GenericContext[T@Base]

class Child(Base[T]): ...

child: Child[int]
```

## Specializing classes with unavailable generic context

When an earlier error prevents ty from determining a class's generic context, specializing the class
Expand Down Expand Up @@ -217,30 +239,6 @@ class Parser(typing.Generic[T]): ...
parser: Parser[int] # error: [invalid-type-form] "Non-generic class `Parser` cannot be specialized in a type expression"
```

### Decorated generic bases

An unresolved decorator obscures the generic context of a base class. Specializing a subclass that
forwards a type variable to that base currently produces a cascading error.

```py
from typing import Generic, TypeVar
from ty_extensions._internal import generic_context

T = TypeVar("T")

# error: [unresolved-reference] "Name `unknown_decorator` used when not defined"
@unknown_decorator
class Base(Generic[T]): ...

reveal_type(generic_context(Base)) # revealed: None

class Child(Base[T]): ...

# TODO: Avoid this cascading error when the base's generic context is unavailable.
# error: [invalid-type-form] "Non-generic class `Child` cannot be specialized in a type expression"
child: Child[int]
```

### Unresolved generic bases

```py
Expand Down
Loading
Loading