GrahamDumpleton / wrapt

A Python module for decorators, wrappers and monkey patching.
BSD 2-Clause "Simplified" License
2.04k stars 230 forks source link

Possible example for ObjectProxy docs #184

Closed wpietri closed 3 years ago

wpietri commented 3 years ago

I need to wrap the client for a third-party API so I can record all the calls we make and the return values. The API has a lot of methods, so I wanted a way to wrap every call. This took me a while to figure out.

Most of the ObjectProxy examples wrap a function. I've put below an example of it wrapping an object, which hopefully will help people like me who are not as familiar with Python's internals. Feel free to edit as you like. Also glad to submit a pull request if you let me know where you'd like it.

import time
from dataclasses import dataclass, field
from typing import Callable, Tuple, Dict

import wrapt

@dataclass
class CacheKey:
    f: Callable
    args: Tuple
    kwargs: dict

    def __hash__(self) -> int:
        return hash((self.f, self.args, tuple(self.kwargs)))

@dataclass
class CacheValue:
    value: any
    saved_at: float = field(default_factory=time.time)

    def expired(self, lifetime):
        if not lifetime:
            return False
        return self.saved_at + lifetime < time.time()

class CallCache:

    def __init__(self, lifetime=None) -> None:
        self.lifetime = lifetime
        self.data: Dict[CacheKey, CacheValue] = {}
        super().__init__()

    def get(self, method, args, kwargs):
        cache_key = CacheKey(method, args, kwargs)
        if cache_key not in self.data or self.data[cache_key].expired(self.lifetime):
            self.data[cache_key] = CacheValue(method(*args, **kwargs))
        return self.data[cache_key].value

class CachingProxy(wrapt.ObjectProxy):

    def __init__(self, wrapped, expire=None):
        self._self_cache = CallCache(expire)
        super().__init__(wrapped)

    def __getattr__(self, name):
        attr = super().__getattr__(name)
        if isinstance(attr, Callable):
            def wrapper(*args, **kwargs):
                return self._self_cache.get(attr, args, kwargs)
            return wrapper
        else:
            return attr
GrahamDumpleton commented 3 years ago

Might be a good topic for a blog post if you have a blog.

Right now I don't have any plans to expand on the set of examples in the docs and there are much more important topics which still aren't even covered in the docs, such as all the APIs to support monkey patching.

So I'll close this issue out for now, but if I ever get back to expanding on examples in the docs I will be trolling back through issues to identify common use cases that keep coming up, so maybe will revisit this example then. Thanks.