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

irc, bot, builtins: add & use AbstractBot.make_identifier_memory() helper #2552

Merged
merged 7 commits into from
Nov 17, 2023
8 changes: 2 additions & 6 deletions sopel/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,19 +75,15 @@ def __init__(self, config, daemon=False):
self.modeparser = modes.ModeParser()
"""A mode parser used to parse ``MODE`` messages and modestrings."""

self.channels = tools.SopelIdentifierMemory(
identifier_factory=self.make_identifier,
)
self.channels = self.make_identifier_memory()
"""A map of the channels that Sopel is in.

The keys are :class:`~sopel.tools.identifiers.Identifier`\\s of the
channel names, and map to :class:`~sopel.tools.target.Channel` objects
which contain the users in the channel and their permissions.
"""

self.users = tools.SopelIdentifierMemory(
identifier_factory=self.make_identifier,
)
self.users = self.make_identifier_memory()
"""A map of the users that Sopel is aware of.

The keys are :class:`~sopel.tools.identifiers.Identifier`\\s of the
Expand Down
9 changes: 2 additions & 7 deletions sopel/builtins/find.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,11 @@

from sopel import plugin
from sopel.formatting import bold
from sopel.tools import SopelIdentifierMemory


def setup(bot):
if 'find_lines' not in bot.memory:
bot.memory['find_lines'] = SopelIdentifierMemory(
identifier_factory=bot.make_identifier,
)
bot.memory['find_lines'] = bot.make_identifier_memory()


def shutdown(bot):
Expand All @@ -49,9 +46,7 @@ def collectlines(bot, trigger):

# Add a log for the channel and nick, if there isn't already one
if trigger.sender not in bot.memory['find_lines']:
bot.memory['find_lines'][trigger.sender] = SopelIdentifierMemory(
identifier_factory=bot.make_identifier,
)
bot.memory['find_lines'][trigger.sender] = bot.make_identifier_memory()
if trigger.nick not in bot.memory['find_lines'][trigger.sender]:
bot.memory['find_lines'][trigger.sender][trigger.nick] = deque(maxlen=10)

Expand Down
6 changes: 2 additions & 4 deletions sopel/builtins/translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import requests

from sopel import plugin, tools
from sopel import plugin
from sopel.tools import web


Expand All @@ -25,9 +25,7 @@

def setup(bot):
if 'mangle_lines' not in bot.memory:
bot.memory['mangle_lines'] = tools.SopelIdentifierMemory(
identifier_factory=bot.make_identifier,
)
bot.memory['mangle_lines'] = bot.make_identifier_memory()


def shutdown(bot):
Expand Down
4 changes: 1 addition & 3 deletions sopel/builtins/url.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,7 @@ def setup(bot: Sopel):

# Ensure last_seen_url is in memory
if 'last_seen_url' not in bot.memory:
bot.memory['last_seen_url'] = tools.SopelIdentifierMemory(
identifier_factory=bot.make_identifier,
)
bot.memory['last_seen_url'] = bot.make_identifier_memory()

# Initialize shortened_urls as a dict if it doesn't exist.
if 'shortened_urls' not in bot.memory:
Expand Down
37 changes: 35 additions & 2 deletions sopel/irc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@

from sopel import tools, trigger
from sopel.lifecycle import deprecated
from sopel.tools import identifiers
from sopel.tools import identifiers, memories
from .backends import AsyncioBackend, UninitializedBackend
from .capabilities import Capabilities
from .isupport import ISupport
Expand Down Expand Up @@ -227,7 +227,10 @@ def hostmask(self) -> Optional[str]:
# Utility

def make_identifier(self, name: str) -> identifiers.Identifier:
"""Instantiate an Identifier using the bot's context."""
"""Instantiate an Identifier using the bot's context.

.. versionadded:: 8.0
"""
casemapping = {
'ascii': identifiers.ascii_lower,
'rfc1459': identifiers.rfc1459_lower,
Expand All @@ -242,6 +245,36 @@ def make_identifier(self, name: str) -> identifiers.Identifier:
chantypes=chantypes,
)

def make_identifier_memory(self) -> memories.SopelIdentifierMemory:
"""Instantiate a SopelIdentifierMemory using the bot's context.

This is a shortcut for :class:`~.memories.SopelIdentifierMemory`\'s most
common use case, which requires remembering to pass the ``bot``\'s own
:meth:`make_identifier` method so the ``SopelIdentifierMemory`` will
cast its keys to :class:`~.tools.identifiers.Identifier`\\s that are
compatible with what the bot tracks internally and sends with
:class:`~.trigger.Trigger`\\s when a plugin callable runs.

Calling this method is equivalent to the following::

from sopel.tools import memories

memories.SopelIdentifierMemory(
identifier_factory=bot.make_identifier,
)

.. versionadded:: 8.0

.. seealso::

The :mod:`.tools.memories` module describes how to use
:class:`~.tools.memories.SopelIdentifierMemory` and its siblings.

"""
return memories.SopelIdentifierMemory(
identifier_factory=self.make_identifier,
)

def safe_text_length(self, recipient: str) -> int:
"""Estimate a safe text length for an IRC message.

Expand Down
30 changes: 30 additions & 0 deletions test/test_coretasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,36 @@ def test_handle_isupport_casemapping(mockbot):
assert mockbot.nick.lower() == 'test[a]'


def test_handle_isupport_casemapping_identifiermemory(mockbot):
# create a nick that needs casemapping
rfc1459 = 'Test[a]'

# create `SopelIdentifierMemory` w/bot's helper method and add the nick
memory = mockbot.make_identifier_memory()
memory[rfc1459] = rfc1459

# check default behavior (`rfc1459` casemapping)
assert memory['test{a}'] == rfc1459
assert memory['Test[a]'] == rfc1459

# now the bot "connects" to a server using `CASEMAPPING=ascii`
mockbot.on_message(
':irc.example.com 005 Sopel '
'CHANTYPES=# EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLMPQScgimnprstz '
'CHANLIMIT=#:120 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 '
'NETWORK=example STATUSMSG=@+ CALLERID=g CASEMAPPING=ascii '
':are supported by this server')

# CASEMAPPING token change doesn't affect previously existing Identifiers...
assert memory['Test{a}'] == rfc1459
# ...so we have to create a new nick that will casemap differently now
ascii = 'Test[b]'
memory[ascii] = ascii
assert len(memory) == 2
assert memory['test[b]'] == ascii
assert 'test{b}' not in memory


def test_handle_isupport_chantypes(mockbot):
# check default behavior (chantypes allows #, &, +, and !)
assert not mockbot.make_identifier('#channel').is_nick()
Expand Down
15 changes: 15 additions & 0 deletions test/test_irc.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@ def bot(tmpconfig, botfactory):
return botfactory(tmpconfig)


def test_make_identifier(bot):
nick = bot.make_identifier('Test[a]')
assert 'test{a}' == nick


def test_make_identifier_memory(bot):
memory = bot.make_identifier_memory()
memory['Test[a]'] = True
assert memory['test{a}'] is True

memory['test{a}'] = False
assert len(memory) == 1
assert memory['Test[a]'] is False


def prefix_length(bot):
# ':', nick, '!', '~', ident/username, '@', maximum hostname length, <0x20>
return 1 + len(bot.nick) + 1 + 1 + len(bot.user) + 1 + 63 + 1
Expand Down