Skip to content

Commit

Permalink
memoize decorator now works with classes
Browse files Browse the repository at this point in the history
  • Loading branch information
cevans87 committed Feb 27, 2019
1 parent a066a17 commit 5a86e1f
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 3 deletions.
9 changes: 9 additions & 0 deletions README.md
Expand Up @@ -32,6 +32,15 @@ Python 3.7+ async-enabled decorators and tools including
other nine calls in this example wait for the result.
await asyncio.gather(*[foo(1) for _ in range(10)])

- Classes may be memoized.
@memoize
Class Foo:
def init(self, _): ...

Foo(1) # Instance is actually created.
Foo(1) # Instance not created. Previously-cached instance returned.
Foo(2) # Instance is actually created.

- Calls to foo(1), foo(bar=1), and foo(1, baz='baz') are equivalent and only cached once
@memoize
def foo(bar, baz='baz'): ...
Expand Down
5 changes: 4 additions & 1 deletion atools/decorator_mixin.py
@@ -1,5 +1,6 @@
from __future__ import annotations
from functools import wraps
import inspect
from types import FunctionType
from typing import Any, Awaitable, Callable, Dict, Optional, Tuple, Type, Union

Expand Down Expand Up @@ -27,7 +28,9 @@ def __call__(cls, _decoratee: Optional[Decoratee] = None, **kwargs) -> Decorated

decorator = super().__call__(_decoratee, **kwargs)

if not isinstance(_decoratee, FunctionType):
if inspect.isclass(decorator):
decorated = decorator
elif inspect.isclass(_decoratee):
# _decoratee is a class. Our decorator should have already done its work.
decorated = _decoratee
else:
Expand Down
32 changes: 31 additions & 1 deletion atools/memoize_decorator.py
Expand Up @@ -282,6 +282,15 @@ async def foo(bar) -> Any: ...
other nine calls in this example wait for the result.
await asyncio.gather(*[foo(1) for _ in range(10)])
- Classes may be memoized.
@memoize
Class Foo:
def init(self, _): ...
Foo(1) # Instance is actually created.
Foo(1) # Instance not created. Previously-cached instance returned.
Foo(2) # Instance is actually created.
- Calls to foo(1), foo(bar=1), and foo(1, baz='baz') are equivalent and only cached once
@memoize
def foo(bar, baz='baz'): ...
Expand Down Expand Up @@ -346,6 +355,27 @@ def foo(self): -> Any: ...
a.bar(1) # Foo.bar(a, 1) is actually called cached and again.
"""

def __new__(
cls,
fn: Fn,
*,
size: Optional[int] = None,
duration: Optional[Union[int, timedelta]] = None,
):
if not inspect.isclass(fn):
return super().__new__(cls)

class WrappedMeta(type(fn)):
# noinspection PyMethodParameters
@memoize
def __call__(cls, *args, **kwargs):
return super().__call__(*args, **kwargs)

class Wrapped(fn, metaclass=WrappedMeta):
pass

return type(fn.__name__, (Wrapped,), {})

def __init__(
self,
fn: Fn,
Expand All @@ -355,7 +385,7 @@ def __init__(
) -> None:
if inspect.iscoroutinefunction(fn):
self._memo = _MemoizeAsync(fn, size=size, duration=duration)
else:
elif not inspect.isclass(fn):
self._memo = _MemoizeSync(fn, size=size, duration=duration)

def __call__(self, *args, **kwargs) -> Any:
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -2,7 +2,7 @@

setup(
name='atools',
version='0.4.1',
version='0.5.0',
packages=['', 'atools'],
python_requires='>=3.7',
url='https://github.com/cevans87/atools',
Expand Down
30 changes: 30 additions & 0 deletions test/test_memoize_decorator.py
Expand Up @@ -355,6 +355,36 @@ def test_async_no_event_loop_does_not_raise(self) -> None:
async def foo() -> None:
...

def test_memoizes_class(self) -> None:
body = MagicMock()

class Bar:
...

@memoize
class Foo(Bar):
def __init__(self, foo):
body(foo)

self.assertIs(Foo(0), Foo(0))
body.assert_called_once_with(0)
self.assertIsNot(Foo(0), Foo(1))

def test_memoizes_class_with_metaclass(self) -> None:
body = MagicMock()

class FooMeta(type):
pass

@memoize
class Foo(metaclass=FooMeta):
def __init__(self, foo):
body(foo)

self.assertIs(Foo(0), Foo(0))
body.assert_called_once_with(0)
self.assertIsNot(Foo(0), Foo(1))


if __name__ == '__main__':
unittest.main(verbosity=2)

0 comments on commit 5a86e1f

Please sign in to comment.