Skip to content

Commit

Permalink
Merge pull request #2237 from Exirel/tools-memories
Browse files Browse the repository at this point in the history
tools: move memory classes to tools.memories
  • Loading branch information
dgw authored Feb 24, 2022
2 parents 2e25645 + b9a1e0c commit 9944e05
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 214 deletions.
5 changes: 5 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ sopel.tools.identifiers
.. automodule:: sopel.tools.identifiers
:members:

sopel.tools.memories
--------------------

.. automodule:: sopel.tools.memories
:members:

sopel.tools.web
---------------
Expand Down
2 changes: 1 addition & 1 deletion sopel/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def __init__(self, config, daemon=False):
self.memory = tools.SopelMemory()
"""
A thread-safe dict for storage of runtime data to be shared between
plugins. See :class:`sopel.tools.SopelMemory`.
plugins. See :class:`sopel.tools.memories.SopelMemory`.
"""

self.shutdown_methods = []
Expand Down
159 changes: 8 additions & 151 deletions sopel/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,23 @@
from __future__ import annotations

import codecs
from collections import defaultdict
import logging
import os
import re
import sys
import threading
from typing import Callable

from sopel.lifecycle import deprecated # Don't delete; maintains backward compatibility with pre-8.0 API
from ._events import events # NOQA
from .identifiers import Identifier
from . import time, web # NOQA

# shortcuts & backward compatibility with pre-8.0
from .identifiers import Identifier # NOQA
from .memories import ( # NOQA
SopelIdentifierMemory,
SopelMemory,
SopelMemoryWithDefault,
)
from . import time, web # NOQA

IdentifierFactory = Callable[[str], Identifier]

# Can be implementation-dependent
_regex_type = type(re.compile(''))
Expand Down Expand Up @@ -237,151 +239,6 @@ def get_logger(plugin_name):
return logging.getLogger('sopel.externals.%s' % plugin_name)


class SopelMemory(dict):
"""A simple thread-safe ``dict`` implementation.
In order to prevent exceptions when iterating over the values and changing
them at the same time from different threads, we use a blocking lock in
``__setitem__`` and ``__contains__``.
.. versionadded:: 3.1
As ``Willie.WillieMemory``
.. versionchanged:: 4.0
Moved to ``tools.WillieMemory``
.. versionchanged:: 6.0
Renamed from ``WillieMemory`` to ``SopelMemory``
"""
def __init__(self, *args):
dict.__init__(self, *args)
self.lock = threading.Lock()

def __setitem__(self, key, value):
"""Set a key equal to a value.
The dict is locked for other writes while doing so.
"""
self.lock.acquire()
result = dict.__setitem__(self, key, value)
self.lock.release()
return result

def __contains__(self, key):
"""Check if a key is in the dict.
The dict is locked for writes while doing so.
"""
self.lock.acquire()
result = dict.__contains__(self, key)
self.lock.release()
return result

# Needed to make it explicit that we don't care about the `lock` attribute
# when comparing/hashing SopelMemory objects.
__eq__ = dict.__eq__
__ne__ = dict.__ne__
__hash__ = dict.__hash__


class SopelMemoryWithDefault(defaultdict):
"""Same as SopelMemory, but subclasses from collections.defaultdict.
.. versionadded:: 4.3
As ``WillieMemoryWithDefault``
.. versionchanged:: 6.0
Renamed to ``SopelMemoryWithDefault``
"""
def __init__(self, *args):
defaultdict.__init__(self, *args)
self.lock = threading.Lock()

def __setitem__(self, key, value):
"""Set a key equal to a value.
The dict is locked for other writes while doing so.
"""
self.lock.acquire()
result = defaultdict.__setitem__(self, key, value)
self.lock.release()
return result

def __contains__(self, key):
"""Check if a key is in the dict.
The dict is locked for writes while doing so.
"""
self.lock.acquire()
result = defaultdict.__contains__(self, key)
self.lock.release()
return result


class SopelIdentifierMemory(SopelMemory):
"""Special Sopel memory that stores ``Identifier`` as key.
This is a convenient subclass of :class:`SopelMemory` that always casts its
keys as instances of :class:`~.identifiers.Identifier`::
>>> from sopel import tools
>>> memory = tools.SopelIdentifierMemory()
>>> memory['Exirel'] = 'king'
>>> list(memory.items())
[(Identifier('Exirel'), 'king')]
>>> tools.Identifier('exirel') in memory
True
>>> 'exirel' in memory
True
As seen in the example above, it is possible to perform various operations
with both ``Identifier`` and :class:`str` objects, taking advantage of the
case-insensitive behavior of ``Identifier``.
As it works with :class:`~.identifiers.Identifier`, it accepts an
identifier factory. This factory usually comes from a bot instance (see
:meth:`bot.make_identifier()<sopel.irc.AbstractBot.make_identifier>`), like
in the example of a plugin setup function::
def setup(bot):
bot.memory['my_plugin_storage'] = SopelIdentifierMemory(
identifier_factory=bot.make_identifier,
)
.. note::
Internally, it will try to do ``key = self.make_identifier(key)``,
which will raise an exception if it cannot instantiate the key
properly::
>>> memory[1] = 'error'
AttributeError: 'int' object has no attribute 'translate'
.. versionadded:: 7.1
.. versionchanged:: 8.0
The parameter ``identifier_factory`` has been added to properly
transform ``str`` into :class:`~.identifiers.Identifier`. This factory
is stored and accessible through :attr:`make_identifier`.
"""
def __init__(
self,
*args,
identifier_factory: IdentifierFactory = Identifier,
) -> None:
super().__init__(*args)
self.make_identifier = identifier_factory
"""A factory to transform keys into identifiers."""

def __getitem__(self, key):
return super().__getitem__(self.make_identifier(key))

def __contains__(self, key):
return super().__contains__(self.make_identifier(key))

def __setitem__(self, key, value):
super().__setitem__(self.make_identifier(key), value)


def chain_loaders(*lazy_loaders):
"""Chain lazy loaders into one.
Expand Down
167 changes: 167 additions & 0 deletions sopel/tools/memories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
"""Thread-safe memory data-structures for Sopel.
Sopel uses lots of threads to manage rules and jobs and other features, and it
needs to store shared information safely. This class contains various memory
classes that are thread-safe, with some convenience features.
"""
from __future__ import annotations

from collections import defaultdict
import threading
from typing import Callable

from .identifiers import Identifier


IdentifierFactory = Callable[[str], Identifier]


class SopelMemory(dict):
"""A simple thread-safe ``dict`` implementation.
In order to prevent exceptions when iterating over the values and changing
them at the same time from different threads, we use a blocking lock in
``__setitem__`` and ``__contains__``.
.. versionadded:: 3.1
As ``Willie.WillieMemory``
.. versionchanged:: 4.0
Moved to ``tools.WillieMemory``
.. versionchanged:: 6.0
Renamed from ``WillieMemory`` to ``SopelMemory``
.. versionchanged:: 8.0
Moved from ``tools`` to ``tools.memories``
"""
def __init__(self, *args):
dict.__init__(self, *args)
self.lock = threading.Lock()

def __setitem__(self, key, value):
"""Set a key equal to a value.
The dict is locked for other writes while doing so.
"""
self.lock.acquire()
result = dict.__setitem__(self, key, value)
self.lock.release()
return result

def __contains__(self, key):
"""Check if a key is in the dict.
The dict is locked for writes while doing so.
"""
self.lock.acquire()
result = dict.__contains__(self, key)
self.lock.release()
return result

# Needed to make it explicit that we don't care about the `lock` attribute
# when comparing/hashing SopelMemory objects.
__eq__ = dict.__eq__
__ne__ = dict.__ne__
__hash__ = dict.__hash__


class SopelMemoryWithDefault(defaultdict):
"""Same as SopelMemory, but subclasses from collections.defaultdict.
.. versionadded:: 4.3
As ``WillieMemoryWithDefault``
.. versionchanged:: 6.0
Renamed to ``SopelMemoryWithDefault``
.. versionchanged:: 8.0
Moved from ``tools`` to ``tools.memories``
"""
def __init__(self, *args):
defaultdict.__init__(self, *args)
self.lock = threading.Lock()

def __setitem__(self, key, value):
"""Set a key equal to a value.
The dict is locked for other writes while doing so.
"""
self.lock.acquire()
result = defaultdict.__setitem__(self, key, value)
self.lock.release()
return result

def __contains__(self, key):
"""Check if a key is in the dict.
The dict is locked for writes while doing so.
"""
self.lock.acquire()
result = defaultdict.__contains__(self, key)
self.lock.release()
return result


class SopelIdentifierMemory(SopelMemory):
"""Special Sopel memory that stores ``Identifier`` as key.
This is a convenient subclass of :class:`SopelMemory` that always casts its
keys as instances of :class:`~.identifiers.Identifier`::
>>> from sopel import tools
>>> memory = tools.SopelIdentifierMemory()
>>> memory['Exirel'] = 'king'
>>> list(memory.items())
[(Identifier('Exirel'), 'king')]
>>> tools.Identifier('exirel') in memory
True
>>> 'exirel' in memory
True
As seen in the example above, it is possible to perform various operations
with both ``Identifier`` and :class:`str` objects, taking advantage of the
case-insensitive behavior of ``Identifier``.
As it works with :class:`~.identifiers.Identifier`, it accepts an
identifier factory. This factory usually comes from a
:class:`bot instance<sopel.bot.Sopel>`, like in the example of a plugin
setup function::
def setup(bot):
bot.memory['my_plugin_storage'] = SopelIdentifierMemory(
identifier_factory=bot.make_identifier,
)
.. note::
Internally, it will try to do ``key = self.make_identifier(key)``,
which will raise an exception if it cannot instantiate the key
properly::
>>> memory[1] = 'error'
AttributeError: 'int' object has no attribute 'translate'
.. versionadded:: 7.1
.. versionchanged:: 8.0
Moved from ``tools`` to ``tools.memories``.
The parameter ``identifier_factory`` has been added to properly
transform ``str`` into :class:`~.identifiers.Identifier`. This factory
is stored and accessible through :attr:`make_identifier`.
"""
def __init__(
self,
*args,
identifier_factory: IdentifierFactory = Identifier,
) -> None:
super().__init__(*args)
self.make_identifier = identifier_factory
"""A factory to transform keys into identifiers."""

def __getitem__(self, key):
return super().__getitem__(self.make_identifier(key))

def __contains__(self, key):
return super().__contains__(self.make_identifier(key))

def __setitem__(self, key, value):
super().__setitem__(self.make_identifier(key), value)
Loading

0 comments on commit 9944e05

Please sign in to comment.