Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tools: move memory classes to tools.memories #2237

Merged
merged 2 commits into from
Feb 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
)
Exirel marked this conversation as resolved.
Show resolved Hide resolved
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``
"""
Exirel marked this conversation as resolved.
Show resolved Hide resolved
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``
"""
Exirel marked this conversation as resolved.
Show resolved Hide resolved
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

Exirel marked this conversation as resolved.
Show resolved Hide resolved
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