2

I'm trying to write a framework using decorators and I want the user code to be as clean as possible but also work well with python hints and I have some trouble.

Imagine the next scenario:

framework.py:

from dependency import (getSomeThirdPartyPythonClass, SomeClass)

def my_framework_decorator(function):
    def wrapper():
        dependency : SomeClass = getSomeThirdPartyPythonClass()
        function(function)
    return wrapper

framework_user.py:

from framework import my_framework_decorator

@my_framework_decorator
def user_foo(dependency):
    dependency. # and now I want auto complete of the SomeClass's functions and members
                # from the IDE or from the linter.

I don't want to make the users import from "dependency"

Worth mentioning that I'm working with vscode but tried also pycharm and in both, I'm not getting the wanted result. I played with the setting.json file and the linter attributes but nothing helped.

If you can point for some good linter tool or familiar with a solution to the problem or mention some other elegant approach it will be great.

Muhammad Mohsin Khan
  • 1,444
  • 7
  • 16
  • 23
  • I don't think that's possible, since `user_foo` is still a standalone function. Decorating it with `my_framework_decorator` means `user_foo = my_framework_decorator(user_foo)`, but that doesn't mean the only thing that's ever going to be passed into `user_foo` will be `SomeClass` (at least not from the type checker's / linter's perspective). But your users can do `from framework import SomeClass` instead and then type hint manually. If you want this to be more bundled, you can also set `my_framework_decorator.type = SomeClass` and then users would type hint with `my_framework_decorator.type`. – a_guest Jan 25 '22 at 10:37
  • See https://stackoverflow.com/questions/17862185/how-to-inject-variable-into-scope-with-a-decorator – fedotsoldier Jan 25 '22 at 10:45

1 Answers1

1

In my testing, neither pyright (VS Code) or mypy will infer the arguments of a decorated function according to the decorator.

However, you can still type the decorator, and the type checkers will check it against any types the user provides.

So,

lib.py

from typing import Callable
from dependency import Dog


def my_decorator(function: Callable[[Dog], None]):
    def wrapper():
        argument: Dog = Dog()
        function(argument)

    return wrapper

app.py

from lib import my_decorator


@my_decorator
def foo(animal):
    animal.quack()

As written, pyright and mypy treat animal as Unknown and won't complain here. However, if the user were to type the function arguments, like:

@my_decorator
def foo(animal: Duck):
    animal.quack()

Then we'd get an error:

Pyright: VScode error

mypy:

app.py:7: error: Argument 1 to "my_decorator" has incompatible type "Callable[[Duck], Any]"; expected "Callable[[Dog], None]"

That would prompt the user to add the correct hint, and then they'd get:

autocomplete

...proper autocomplete.


So there is still an advantage to typing the decorator. And, if users are running a type checker in strict mode, this would be enforced.

joerick
  • 16,078
  • 4
  • 53
  • 57
  • I went on this direction eventually. But I used the decorator to describe a functor instead of just function (hope that's good idea). And I have a type definition to "SomeClass" in the functor scope which used in the signature of the wrapped function. something like: class functorFramework: classType = SomeClass def __init__(self, func : Callable[[classType],None]): self.func = func def __call__(self): dependency : classType = getSomeThirdPartyPythonClass() ; self.func(dependency) @functorFramework def func(dep : functorFramework.classType): dep. #getting here autocomplete – Nadav Sagie Jan 27 '22 at 08:32