diff options
| author | 2020-05-16 20:22:05 +0200 | |
|---|---|---|
| committer | 2020-05-16 20:22:05 +0200 | |
| commit | 588521c82403f6d66693512c6d33272cc370d755 (patch) | |
| tree | e53f80dfdcac85822331713ddcffa1982df3d764 | |
| parent | Boilerplate for the RedisCacheMixin (diff) | |
Refactor - no more mixins!
It was brought to my attention that we may need several caches per Cog
for some of our Cogs. This means that the original approach of having
this be a mixin is a little bit problematic.
Instead, RedisDict will be instantiated directly inside the class you
want it in. By leveraging __set_name__, we can create a namespace
containing both the class name and the variable name without the user
having to provide anything.
For example, if you create an attribute MyClass.cache = RedisDict(),
this will be using the redis namespace 'MyClass.cache.' before anything
you store in it.
With this approach, it is also possible to instantiate a RedisDict with
a custom namespace by simply passing it into the constructor.
- RedisDict("firedog") will create items with the 'firedog.your_item'
prefix.
- If there are multiple RedisDicts using the same namespace, an
underscore will be appended to the namespace, such that the second
RedisDict("firedog") will actually create items in the
'firedog_.your_item' namespace.
This is also possible to use outside of classes, so long as you provide
a custom namespace when you instantiate it.
Custom namespaces will always take precedence over automatic
'Class.attribute_name' ones.
| -rw-r--r-- | bot/mixins/__init__.py | 3 | ||||
| -rw-r--r-- | bot/mixins/redis.py | 56 | ||||
| -rw-r--r-- | bot/utils/__init__.py | 4 | ||||
| -rw-r--r-- | bot/utils/redis.py | 70 |
4 files changed, 74 insertions, 59 deletions
diff --git a/bot/mixins/__init__.py b/bot/mixins/__init__.py deleted file mode 100644 index ff1f0c50d..000000000 --- a/bot/mixins/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .redis import RedisCacheMixin - -__all__ = ['RedisCacheMixin'] diff --git a/bot/mixins/redis.py b/bot/mixins/redis.py deleted file mode 100644 index f19108576..000000000 --- a/bot/mixins/redis.py +++ /dev/null @@ -1,56 +0,0 @@ -import redis as redis_py - -redis = redis_py.Redis(host="redis") - - -class RedisDict(dict): - """ - A dictionary interface for a Redis database. - - Objects created by this class should mostly behave like a normal dictionary, - but will store all the data in our Redis database for persistence between restarts. - - There are, however, a few limitations to what kinds of data types can be - stored on Redis, so this is a little bit more limited than a regular dict. - """ - - def __init__(self, namespace: str = "global"): - """Initialize the RedisDict with the right namespace.""" - # TODO: Make namespace collision impossible! - # Append a number or something if it exists already. - self.namespace = namespace - - # redis.mset({"firedog": "donkeykong"}) - # - # print(redis.get("firedog").decode("utf-8") - - -class RedisCacheMixin: - """ - A mixin which adds a cls.cache parameter which can be used for persistent caching. - - This adds a dictionary-like object called cache which can be treated like a regular dictionary, - but which can only store simple data types like ints, strings, and floats. - - To use it, simply subclass it into your class like this: - - class MyCog(Cog, RedisCacheMixin): - def some_command(self): - # You can now do this! - self.cache['some_data'] = some_data - - All the data stored in this cache will probably be available permanently, even if the bot restarts or - is updated. However, Redis is not meant to be used for reliable, permanent storage. It may be cleared - from time to time, so please only use it for caching data that you can afford to lose. - - If it's really important that your data should never disappear, please use our postgres database instead. - """ - - def __init_subclass__(cls, **kwargs): - """ - Initialize the cache when subclass is created. - - When this mixin is subclassed, we create a cache using the subclass name as the namespace. - This is to prevent collisions between subclasses. - """ - cls.cache = RedisDict(cls.__name__) diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py index 9b32e515d..7ae2db8fe 100644 --- a/bot/utils/__init__.py +++ b/bot/utils/__init__.py @@ -2,6 +2,10 @@ from abc import ABCMeta from discord.ext.commands import CogMeta +from bot.utils.redis import RedisDict + +__all__ = ['RedisDict', 'CogABCMeta'] + class CogABCMeta(CogMeta, ABCMeta): """Metaclass for ABCs meant to be implemented as Cogs.""" diff --git a/bot/utils/redis.py b/bot/utils/redis.py new file mode 100644 index 000000000..8b33e8977 --- /dev/null +++ b/bot/utils/redis.py @@ -0,0 +1,70 @@ +from collections.abc import MutableMapping +from typing import Optional + +import redis as redis_py + +redis = redis_py.Redis(host="redis") + + +class RedisDict(MutableMapping): + """ + A dictionary interface for a Redis database. + + Objects created by this class should mostly behave like a normal dictionary, + but will store all the data in our Redis database for persistence between restarts. + + Redis is limited to simple types, so to allow you to store collections like lists + and dictionaries, we JSON deserialize every value. That means that it will not be possible + to store complex objects, only stuff like strings, numbers, and collections of strings and numbers. + + TODO: Implement these: + __delitem__ + __getitem__ + __setitem__ + __iter__ + __len__ + clear (just use DEL and the hash goes) + copy (convert to dict maybe?) + pop + popitem + setdefault + update + + TODO: TEST THESE + .get + .items + .keys + .values + .__eg__ + .__ne__ + """ + + namespaces = [] + + def _set_namespace(self, namespace: str) -> None: + """Try to set the namespace, but do not permit collisions.""" + while namespace in self.namespaces: + namespace = namespace + "_" + + self.namespaces.append(namespace) + self.namespace = namespace + + def __init__(self, namespace: Optional[str] = None) -> None: + """Initialize the RedisDict with the right namespace.""" + super().__init__() + self.has_custom_namespace = namespace is not None + self._set_namespace(namespace) + + def __set_name__(self, owner: object, attribute_name: str) -> None: + """ + Set the namespace to Class.attribute_name. + + Called automatically when this class is constructed inside a class as an attribute, as long as + no custom namespace is provided to the constructor. + """ + if not self.has_custom_namespace: + self._set_namespace(f"{owner.__name__}.{attribute_name}") + + def __repr__(self) -> str: + """Return a beautiful representation of this object instance.""" + return f"RedisDict(namespace={self.namespace!r})" |