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

find: support escaping backslash, fix double-bolding, and add basic tests #2589

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
50 changes: 31 additions & 19 deletions sopel/builtins/find.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,11 @@ def kick_cleanup(bot, trigger):
[:,]\s+)? # Followed by optional colon/comma and whitespace
s(?P<sep>/) # The literal s and a separator / as group 2
(?P<old> # Group 3 is the thing to find
(?:\\/|[^/])+ # One or more non-slashes or escaped slashes
(?:\\\\|\\/|[^/])+ # One or more non-slashes or escaped slashes
)
/ # The separator again
(?P<new> # Group 4 is what to replace with
(?:\\/|[^/])* # One or more non-slashes or escaped slashes
(?:\\\\|\\/|[^/])* # One or more non-slashes or escaped slashes
)
(?:/ # Optional separator followed by group 5 (flags)
(?P<flags>\S+)
Expand All @@ -136,11 +136,11 @@ def kick_cleanup(bot, trigger):
[:,]\s+)? # Followed by optional colon/comma and whitespace
s(?P<sep>\|) # The literal s and a separator | as group 2
(?P<old> # Group 3 is the thing to find
(?:\\\||[^|])+ # One or more non-pipe or escaped pipe
(?:\\\\|\\\||[^|])+ # One or more non-pipe or escaped pipe
)
\| # The separator again
(?P<new> # Group 4 is what to replace with
(?:\\\||[^|])* # One or more non-pipe or escaped pipe
(?:\\\\|\\\||[^|])* # One or more non-pipe or escaped pipe
)
(?:\| # Optional separator followed by group 5 (flags)
(?P<flags>\S+)
Expand All @@ -161,14 +161,16 @@ def findandreplace(bot, trigger):
return

sep = trigger.group('sep')
old = trigger.group('old').replace('\\%s' % sep, sep)
escape_sequence_pattern = re.compile(r'\\[\\%s]' % sep)

old = escape_sequence_pattern.sub(decode_escape, trigger.group('old'))
new = trigger.group('new')
me = False # /me command
flags = trigger.group('flags') or ''

# only clean/format the new string if it's non-empty
if new:
new = bold(new.replace('\\%s' % sep, sep))
new = escape_sequence_pattern.sub(decode_escape, new)

# If g flag is given, replace all. Otherwise, replace once.
if 'g' in flags:
Expand All @@ -181,39 +183,49 @@ def findandreplace(bot, trigger):
if 'i' in flags:
regex = re.compile(re.escape(old), re.U | re.I)

def repl(s):
return re.sub(regex, new, s, count == 1)
def repl(line, subst):
return re.sub(regex, subst, line, count == 1)
else:
def repl(s):
return s.replace(old, new, count)
def repl(line, subst):
return line.replace(old, subst, count)

# Look back through the user's lines in the channel until you find a line
# where the replacement works
new_phrase = None
new_line = new_display = None
for line in history:
if line.startswith("\x01ACTION"):
me = True # /me command
line = line[8:]
else:
me = False
replaced = repl(line)
replaced = repl(line, new)
if replaced != line: # we are done
new_phrase = replaced
new_line = replaced
new_display = repl(line, bold(new))
break

if not new_phrase:
if not new_line:
return # Didn't find anything

# Save the new "edited" message.
action = (me and '\x01ACTION ') or '' # If /me message, prepend \x01ACTION
history.appendleft(action + new_phrase) # history is in most-recent-first order
history.appendleft(action + new_line) # history is in most-recent-first order

# output
if not me:
new_phrase = 'meant to say: %s' % new_phrase
new_display = 'meant to say: %s' % new_display
if trigger.group(1):
phrase = '%s thinks %s %s' % (trigger.nick, rnick, new_phrase)
msg = '%s thinks %s %s' % (trigger.nick, rnick, new_display)
else:
phrase = '%s %s' % (trigger.nick, new_phrase)
msg = '%s %s' % (trigger.nick, new_display)

bot.say(msg)


bot.say(phrase)
def decode_escape(match):
print("Substituting %s" % match.group(0))
return {
r'\\': '\\',
r'\|': '|',
r'\/': '/',
}[match.group(0)]
107 changes: 107 additions & 0 deletions test/builtins/test_builtins_find.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Tests for Sopel's ``find`` plugin"""
from __future__ import annotations

import pytest

from sopel.formatting import bold
from sopel.tests import rawlist


TMP_CONFIG = """
[core]
owner = Admin
nick = Sopel
enable =
find
host = irc.libera.chat
"""


@pytest.fixture
def bot(botfactory, configfactory):
settings = configfactory('default.ini', TMP_CONFIG)
return botfactory.preloaded(settings, ['find'])


@pytest.fixture
def irc(bot, ircfactory):
return ircfactory(bot)


@pytest.fixture
def user(userfactory):
return userfactory('User')


@pytest.fixture
def other_user(userfactory):
return userfactory('other_user')


@pytest.fixture
def channel():
return '#testing'


REPLACES_THAT_WORK = (
("A simple line.", r"s/line/message/", f"A simple {bold('message')}."),
("An escaped / line.", r"s/\//slash/", f"An escaped {bold('slash')} line."),
("A piped line.", r"s|line|replacement|", f"A piped {bold('replacement')}."),
("An escaped | line.", r"s|\||pipe|", f"An escaped {bold('pipe')} line."),
("An escaped \\ line.", r"s/\\/backslash/", f"An escaped {bold('backslash')} line."),
("abABab", r"s/b/c/g", "abABab".replace('b', bold('c'))), # g (global) flag
("ABabAB", r"s/b/c/i", f"A{bold('c')}abAB"), # i (case-insensitive) flag
("ABabAB", r"s/b/c/ig", f"A{bold('c')}a{bold('c')}A{bold('c')}"), # both flags
)


@pytest.mark.parametrize('original, command, result', REPLACES_THAT_WORK)
def test_valid_replacements(bot, irc, user, channel, original, command, result):
"""Verify that basic replacement functionality works."""
irc.channel_joined(channel, [user.nick])

irc.say(user, channel, original)
irc.say(user, channel, command)

assert len(bot.backend.message_sent) == 1, (
"The bot should respond with exactly one line.")
assert bot.backend.message_sent == rawlist(
"PRIVMSG %s :%s meant to say: %s" % (channel, user.nick, result),
)


def test_multiple_users(bot, irc, user, other_user, channel):
"""Verify that correcting another user's line works."""
irc.channel_joined(channel, [user.nick, other_user.nick])

irc.say(other_user, channel, 'Some weather we got yesterday')
irc.say(user, channel, '%s: s/yester/to/' % other_user.nick)

assert len(bot.backend.message_sent) == 1, (
"The bot should respond with exactly one line.")
assert bot.backend.message_sent == rawlist(
"PRIVMSG %s :%s thinks %s meant to say: %s" % (
channel, user.nick, other_user.nick,
f"Some weather we got {bold('to')}day",
),
)


def test_replace_the_replacement(bot, irc, user, channel):
"""Verify replacing text that was already replaced."""
irc.channel_joined(channel, [user.nick])

irc.say(user, channel, 'spam')
irc.say(user, channel, 's/spam/eggs/')
irc.say(user, channel, 's/eggs/bacon/')

assert len(bot.backend.message_sent) == 2, (
"The bot should respond twice.")
assert bot.backend.message_sent == rawlist(
"PRIVMSG %s :%s meant to say: %s" % (
channel, user.nick, bold('eggs'),
),
"PRIVMSG %s :%s meant to say: %s" % (
channel, user.nick, bold('bacon'),
),
)
Loading