Skip to content

lru_cache annotation doesn't work as a property #5858

Closed

Description

@takeda

So I originally reported it in typeshed/#2659 but was told this can't be fixed there and it is mypy limitation so repporting it here.


I have following code:

#! /usr/bin/env python

import time
from functools import lru_cache

class MyClass:
	@property
	@lru_cache(1)
	def expensive_operation(self) -> int:
		time.sleep(2)
		return 42

	def another_operation(self) -> int:
		return self.expensive_operation + 1

if __name__ == '__main__':
	o = MyClass()
	print(o.expensive_operation)
	print(o.expensive_operation)
	print(o.another_operation())

It works as expected:

$ ./test.py
42
42
43

Yet, mypy is showing errors:

$ mypy test.py
test.py:7: error: Decorated property not supported
test.py:14: error: Unsupported operand types for + ("_lru_cache_wrapper[int]" and "int")

The lru_cache works just fine when decorated as property, and the returned type should be int and not _lru_cache_wrapper[int].

This is on python 3.7.0 and mypy 0.641.

Activity

  1. changed the title [-]lru_cache annotation doesn't work as property[/-] [+]lru_cache annotation doesn't work as a property[/+] on Oct 30, 2018
  2. probablyodd commented on Dec 1, 2018

    @probablyodd

    Duplicates #1362 and #1608

  3. sashgorokhov commented on Jan 14, 2019

    @sashgorokhov

    @probablyodd well its a bit different. If you set # type: ignore to @property in this case,
    mypy will completely ignore existence of this decorator and treat such methods as common callable methods. Mypy will just say somethingh like "lru_cache" has no attribute "..." if complex object such as dict returned from property method.

    from functools import lru_cache
    
    class Foo:
        @property  # type: ignore
        @lru_cache
        def bar(self) -> dict:
            return dict(key='value')
    
    Foo().bar.get('key')
    
    test.py:11: error: "lru_cache" has no attribute "get"
    

    Edit:

    Possible adhoc solution:

    T = TypeVar('T')
    
    def cache(func: Callable[..., T]) -> T:
        return functools.lru_cache()(func)  # type: ignore
    
    ...
        @property  # type: ignore
        @cache
        def foo(self) -> dict:
            return {}
  4. pgilad commented on Jun 21, 2020

    @pgilad

    Besides @sashgorokhov awesome workaround (that adds some complexity to code) - is there any plan to support this ?

  5. malthunayan commented on Apr 29, 2021

    @malthunayan

    Taking a look at the actual implementation of lru_cache and mypy's excellent documentation on decorator factories, here is another workaround for having type annotations work properly with functool's lru_cache:

    import functools
    from typing import Any, Callable, TypeVar, Union, no_type_check, overload
    
    
    RT = TypeVar("RT", bound=Callable[..., Any])
    
    
    @overload
    def lru_cache(
        max_size: Callable[..., RT], typed: bool = False
    ) -> Callable[..., RT]:
        ...
    
    
    @overload
    def lru_cache(max_size: int, typed: bool = False) -> Callable[[RT], RT]:
        ...
    
    
    @overload
    def lru_cache(
        max_size: Union[Callable[..., RT], int], typed: bool = False
    ) -> Union[Callable[..., RT], Callable[[RT], RT]]:
        ...
    
    
    @no_type_check
    def lru_cache(*args, **kwargs):
        return functools.lru_cache(*args, **kwargs)

    This maintains the signature of whatever callable you are decorating, and you should get proper type annotation checks. Credits goes to @sashgorokhov for inspiring to get this solution with his solution above.

  6. Doggie52 commented on Oct 2, 2021

    @Doggie52

    Is this something that should be adjusted in mypy or is it technically the fault of lru_cache?

    Also @malthunayan I can't seem to get your workaround to work with @lru_cache(maxsize=None).

  7. Doggie52 commented on Oct 2, 2021

    @Doggie52

    Here's my modified workaround - not sure where your max_size is coming from, could have worked in previous Python versions? I'm on 3.8.

    import functools
    from typing import Any, Callable, TypeVar, Union, no_type_check, overload, Optional
    
    RT = TypeVar("RT", bound=Callable[..., Any])
    
    
    @overload
    def lru_cache(
            maxsize: Callable[..., RT], typed: bool = False
    ) -> Callable[..., RT]:
        ...
    
    
    @overload
    def lru_cache(maxsize: Optional[int], typed: bool = False) -> Callable[[RT], RT]:
        ...
    
    
    @overload
    def lru_cache(
            maxsize: Union[Callable[..., RT], Optional[int]], typed: bool = False
    ) -> Union[Callable[..., RT], Callable[[RT], RT]]:
        ...
    
    
    @no_type_check
    def lru_cache(*args, **kwargs):
        return functools.lru_cache(*args, **kwargs)
  8. posita commented on Mar 19, 2022

    @posita
    Contributor

    If there is no plan to fix this soon, then can this be documented properly? This error is implicated by the recommendation in the standard library docs for functools.cached_property for caching properties in classes without proper __dict__s, but there's no hint that such recommendation would frustrate Mypy:

    # test_5858.py
    import statistics
    from functools import cache
    
    class DataSet:
        __slots__ = ("_data",)
    
        def __init__(self, sequence_of_numbers):
            self._data = sequence_of_numbers
    
        @property  # <- error: Decorated property not supported  [misc]
        @cache  # <- results in downstream errors involving type "_lru_cache_wrapper[…]"
        def stdev(self):
            return statistics.stdev(self._data)
    
    f: float = statistics.stdev(range(10))
    f = DataSet(range(10)).stdev
    % python --version
    Python 3.9.10
    % mypy --version
    mypy 0.941
    % mypy --config-file=/dev/null test_5858.py
    /dev/null: No [mypy] section in config file
    test_5858.py:11: error: Decorated property not supported
    test_5858.py:17: error: Incompatible types in assignment (expression has type "_lru_cache_wrapper[Any]", variable has type "float")
    Found 2 errors in 1 file (checked 1 source file)
  9. added a commit that references this issue on Aug 30, 2023
    5783af4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugmypy got something wrongtopic-descriptorsProperties, class vs. instance attributes

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions