aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Leon Sandøy <[email protected]>2020-05-16 20:22:05 +0200
committerGravatar Leon Sandøy <[email protected]>2020-05-16 20:22:05 +0200
commit588521c82403f6d66693512c6d33272cc370d755 (patch)
treee53f80dfdcac85822331713ddcffa1982df3d764
parentBoilerplate 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__.py3
-rw-r--r--bot/mixins/redis.py56
-rw-r--r--bot/utils/__init__.py4
-rw-r--r--bot/utils/redis.py70
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})"