`@typing.decorator` for annotating decorator-only functions/classes

Since there are things like @typing.final and @typing.deprecated, do you think that it would be useful to have a “marker” @typing.decorator that marks higher-order functions/classes as intended to be used only as decorators?

So that if I had

@typing.decorator
def autorun(f: Callable[..., object]) -> None:
    if f.__module__ == "__main__":
        f()

then

@autorun
def main() -> None:
    print("Hello world!")

would be correct, but

from __main__ import main  # let's assume __main__.main() wasn't decorated with @autorun
autorun(main)

wouldn’t.

In this particular case, it’s critical to ensure autorun() has a decorator nature, because otherwise f.__module__ == "__main__" might be a false positive, as in the last snippet.

functools.wraps could be marked with @typing.decorator in the typeshed, to make people rather use functools.update_wrapper in no-decorator scenarios.

From the technical perspective, the implementation of the typing.decorator marker would be a simple identity function, similarly to @typing.final. The marker would affect static type checkers which then have to validate the AST to ensure that a given function is used only as a decorator.

Specifically, if I marked a function with @typing.decorator

@typing.decorator
def some_decorator_func() -> ...: ...

then only the following usages would be allowed (only from the @typing.decorator rule, we leave the details to return types type safety):

@some_decorator_func
def spam() -> ...: ...
@some_decorator_func(...)
def eggs() -> ...: ...
@some_decorator_func
class Ham:
    ...
@some_decorator_func(...)
class Cheese:
    ...

Similarly in classes,

@typing.decorator
class SomeDecoratorClass:
    ...

would only allow

@SomeDecoratorClass
def spam() -> ...: ...
@SomeDecoratorClass(...)
def eggs() -> ...: ...
@SomeDecoratorClass
class Ham:
    ...
@SomeDecoratorClass(...)
class Cheese:
    ...

Please note that nesting decorators is also implicitly OK here, since what really only matters here is whether the @typing.decorator-marked callable is used as a decorator.

The main goal for typing.decorator is ensuring a decorator receives the decorated construct (class or function) as soon as it’s declared.

It could be dodged though (as most things in Python), for instance with

# The syntax used below requires Python 3.9+

from somewhere import something_else

def decorate(f: Callable[P, R]) -> Callable[P, R]:
    return f

@decorate
@lambda _: something_else
def func() -> ...:
    pass

So it might make sense to require @decorate as the closest-to-target decorator.

Which would mean that

@decorate
@foo
def bar() -> None:
    pass

would be illegal, but

@foo
@decorate
def bar() -> None:
   pass

wouldn’t.

Might make sense for denoting abc.abstractmethod as the innermost decorator :slight_smile:

This seems more like a linting issue than a typing issue. That said, I don’t know how you’d signal to a linter that a function is meant to be a decorator without using the type system, so maybe that’s enough to justify it anyway.

Given so many limitations that @typing.decorator introduces (must be an innermost decorator, most probably can’t be picked dynamically e.g. with @decorate if __debug__ else identity (which would be better off without the decorator syntax)), I’m not sure there isn’t too much probability of @typing.decorator being misused. Maybe there are some trade-offs that could be done to add more flexibility to that idea, or maybe the idea is overly specific and has too few use cases.