pythonpython-3.xtypespython-jedi

Implementing a lazy property that Jedi can understand


I've been using the lazy-property library (https://pypi.org/project/lazy-property/) for a while. It works perfectly, but in my editor these lazy properties don't give any autocompletions.

My setup is Atom, using the ide-python package (https://github.com/lgeiger/ide-python), which is driven by the python-language-server (https://github.com/palantir/python-language-server), which uses Jedi (https://github.com/davidhalter/jedi) for autocompletions.

Basically, this issue should be reproducible in any Jedi-based autocompletion context.

I've been wondering if there's a way the code in lazy-property could be rewritten (maybe using type-hints and whatnot) such that Jedi could understand that the type coming from a lazy-property-decorated method should be the same as if the decorator were absent.

The implementation for this decorator is actually super simple, it's basically just:

class lazy_property(property):
    def __init__(self, method, fget=None, fset=None, fdel=None, doc=None):

        self.method = method
        self.cache_name = f"_{self.method.__name__}"

        doc = doc or method.__doc__
        super().__init__(fget=fget, fset=fset, fdel=fdel, doc=doc)

        update_wrapper(self, method)

    def __get__(self, instance, owner):

        if instance is None:
            return self

        if hasattr(instance, self.cache_name):
            result = getattr(instance, self.cache_name)
        else:
            if self.fget is not None:
                result = self.fget(instance)
            else:
                result = self.method(instance)

            setattr(instance, self.cache_name, result)

        return result

Does anyone have any ideas of how I could refactor this class in order to get Jedi to understand that it should assume that the decorator does not change the typing of the return values?

Any help would be massively appreciated, cheers.


Solution

  • The problem in your case is that Jedi cannot really deal with super().__init__(fget=fget, fset=fset, fdel=fdel, doc=doc). It doesn't really understand what you're doing there. If you write self.fget = fget after that line, Jedi understands your example.

    To dig a bit deeper, Jedi tries to understand if branches. In your case it thinks that the result of result = self.fget(instance) is what it has to infer, because self.fget is not None inferes to True. It inferes to True, because self.fget for property is defined in the typeshed stubs as def fget(self) -> Any: ..., which basically means that it definitely exists. So the stubs basically differ a bit from the reality in your case (they are probably slightly wrong there).

    However please also note that writing cached properties is something that has been done before. One of the best implementations for this is @cached_property in Django, so you may also just copy from there:

    https://docs.djangoproject.com/en/2.2/_modules/django/utils/functional/#cached_property