python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.5k stars 2.83k forks source link

Class decorators are not type checked #3135

Open JelleZijlstra opened 7 years ago

JelleZijlstra commented 7 years ago
decorator: int = 3

@decorator  # no error
class A:
    pass

class B:
    pass
NewB = decorator(B)  # E: "int" not callable

Mypy doesn't appear to do any checking of class decorators. Applying the decorator manually works.

JelleZijlstra commented 7 years ago

Guido and I found out about this in https://github.com/python/typeshed/pull/1136#discussion_r109725629. Looks like this can be fixed somewhere around analyze_class_decorator in semanal.py.

ilevkivskyi commented 7 years ago

A use case reported in #3483

from typing import Type

class A:
    pass

def deco(cls) -> Type[A]:
    return A

@deco
class B:
    pass

def func(cls: Type[A]):
    assert cls is A

func(B)

might be non-trivial to implement. I am adding this example here, so that we will not forget about it.

euresti commented 6 years ago

Another possibly easier to solve problem is that the functions called are not type-checked

from typing import TypeVar, Callable
_C = TypeVar('_C', bound=type)
def blah(x:int=17) -> Callable[[_C], _C]: ...

@blah(y=18)
class A:
    pass

blah(y=18)

Or even more fun

x=17

@x(huh=17)
class B:
    pass

x(huh=17)
reinhrst commented 6 years ago

I'm thinking how this could/should be used in combination with Protocols to deal with class decorators adding some methods to a class

class FooBar(Protocol):
   def bar(self) -> int:
     return 1

T = TypeVar("T", bound=Type)
def add_bar(cls: T) -> Intersection[FooBar, T]:
   def bar(self) -> int:
       return 2
  cls.bar = bar

@add_bar
class Foo: pass

Foo().bar()

Now this depends on Intersection which was described in PEP483, but not implemented; I don't see another way of doing this cleanly right now.

erezsh commented 2 years ago

Are there plans to fix this? It's strange that mypy doesn't support a core syntax.

rileyjohngibbs commented 2 years ago

It looks like some of these examples (e.g. the x=17 example above) may have been fixed, so in the interest of having a current, non-working example:

from typing import Any

def foo(_: Any) -> int:
    return 5

@foo
class Foo:
    pass

def test_foo() -> None:
    assert Foo + 7 == 12  # Does not pass typechecking

The error for the final line is: error: Unsupported operand types for + ("Type[Foo]" and "int") [operator].

Function decorators seem to work fine though:

def qux(func: Callable[[], str]) -> Callable[[], int]:
    def wrap() -> int:
        return int(func())
    return wrap

@qux
def five() -> str:
    return "5"

def test_qux() -> None:
    assert five() + 10 == 15  # Passes typechecking
DetachHead commented 1 year ago

It looks like some of these examples (e.g. the x=17 example above) may have been fixed

yeah, class decorators are now typechecked, but the issue now is that they are unable to change the type of the class they're decorating. see https://github.com/python/mypy/issues/11117#issuecomment-1754948638

erezsh commented 1 year ago

yeah, class decorators are now typechecked, but the issue now is that they are unable to change the type of the class they're decorating. see #11117 (comment)

Imho, this is important functionality.

For example, it could be useful for implementers of alternative dataclasses, such as pydantic or sqlmodel. Especially once support for intersection gets into mypy. I believe it would be much better than the current approach, that states:

Mypy does not yet recognize aliases of dataclasses.dataclass, and will probably never recognize dynamically computed decorators.

(taken from https://mypy.readthedocs.io/en/stable/additional_features.html)

It would also obsolete the hacky and limited typing_extensions.dataclass_transform.

Just my two (hopefully correct) cents.

NeilGirdhar commented 1 year ago

It would also obsolete the hacky and limited typing_extensions.dataclass_transform.

I don't think it would. The dataclass_transform marks that a class has fields made with field-makers. Whereas an annotated decorator can tell the type checker that a class can be passed to dataclasses functions like fields and replace.

These are separate concepts. You can have classes with fields that can't be passed to dataclass functions.