Skip to content

Commit 1ee91be

Browse files
committed
Merge branch 'backports-7.1' into 7.1.x
Bundled all backported PRs from master to avoid running down Travis CI build credits, since they're strictly rationed now.
2 parents 46f554e + 8fae1ec commit 1ee91be

9 files changed

Lines changed: 246 additions & 17 deletions

File tree

sopel/cli/plugins.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -212,12 +212,15 @@ def handle_list(options):
212212
else:
213213
description['name'] = utils.red(name)
214214
except Exception as error:
215-
label = ('%s' % error) or 'unknown loading exception'
215+
description.update(plugin.get_meta_description())
216+
217+
meta_label = description.get('label') or '(error)'
218+
error_label = ('%s' % error) or 'unknown loading exception'
219+
label = '%s (error: %s)' % (meta_label, error_label)
216220
error_status = 'error'
221+
217222
description.update({
218-
'label': 'Error: %s' % label,
219-
'type': 'unknown',
220-
'source': 'unknown',
223+
'label': label,
221224
'status': error_status,
222225
})
223226
if not no_color:
@@ -268,12 +271,15 @@ def handle_show(options):
268271
description.update(plugin.get_meta_description())
269272
loaded = True
270273
except Exception as error:
271-
label = ('%s' % error) or 'unknown loading exception'
274+
description.update(plugin.get_meta_description())
275+
276+
meta_label = description.get('label') or '(error)'
277+
error_label = ('%s' % error) or 'unknown loading exception'
278+
label = '%s (error: %s)' % (meta_label, error_label)
272279
error_status = 'error'
280+
273281
description.update({
274-
'label': 'Error: %s' % label,
275-
'type': 'unknown',
276-
'source': 'unknown',
282+
'label': label,
277283
'status': error_status,
278284
})
279285

sopel/config/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ def _serialize_boolean(value):
279279
@deprecated(
280280
reason='Use BooleanAttribute instead of ValidatedAttribute with parse=bool',
281281
version='7.1',
282+
warning_in='8.0',
282283
removed_in='9.0',
283284
stack_frame=-2,
284285
)

sopel/modules/announce.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
"""
1010
from __future__ import absolute_import, division, print_function, unicode_literals
1111

12+
import itertools
13+
1214
from sopel import plugin
1315

1416

@@ -18,10 +20,18 @@ def _chunks(items, size):
1820
:param items: the collection of items to chunk
1921
:type items: :term:`iterable`
2022
:param int size: the size of each chunk
23+
:return: a :term:`generator` of chunks
24+
:rtype: :term:`generator` of :class:`tuple`
2125
"""
22-
# from https://stackoverflow.com/a/312464/5991 with modified names for readability
23-
for delim in range(0, len(items), size):
24-
yield items[delim:delim + size]
26+
# This approach is safer than slicing with non-subscriptable types,
27+
# for example `dict_keys` objects
28+
iterator = iter(items)
29+
# TODO: Simplify to assignment expression (`while cond := expr`)
30+
# when dropping Python 3.7
31+
chunk = tuple(itertools.islice(iterator, size))
32+
while chunk:
33+
yield chunk
34+
chunk = tuple(itertools.islice(iterator, size))
2535

2636

2737
@plugin.command('announce')

sopel/modules/reddit.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939

4040
domain = r'https?://(?:www\.|old\.|pay\.|ssl\.|[a-z]{2}\.)?reddit\.com'
4141
subreddit_url = r'%s/r/([\w-]+)/?$' % domain
42-
post_url = r'%s/r/\S+?/comments/([\w-]+)(?:/[\w%%]+)?/?$' % domain
42+
post_url = r'%s/r/\S+?/comments/([\w-]+)(?:/[\w%%]+)?/?' % domain
4343
short_post_url = r'https?://redd\.it/([\w-]+)'
4444
user_url = r'%s/u(?:ser)?/([\w-]+)' % domain
4545
comment_url = r'%s/r/\S+?/comments/\S+?/\S+?/([\w-]+)' % domain
@@ -55,6 +55,7 @@ def setup(bot):
5555
user_agent=USER_AGENT,
5656
client_id='6EiphT6SSQq7FQ',
5757
client_secret=None,
58+
check_for_updates=False,
5859
)
5960

6061

sopel/modules/tell.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
import os
1616
import threading
1717
import time
18+
import unicodedata
1819

19-
from sopel import plugin, tools
20+
from sopel import formatting, plugin, tools
2021
from sopel.config import types
2122
from sopel.tools.time import format_time, get_timezone
2223

@@ -130,6 +131,43 @@ def shutdown(bot):
130131
pass
131132

132133

134+
def _format_safe_lstrip(text):
135+
"""``str.lstrip()`` but without eating IRC formatting.
136+
137+
:param str text: text to clean
138+
:rtype: str
139+
:raises TypeError: if the passed ``text`` is not a string
140+
141+
Stolen and tweaked from the ``choose`` plugin's ``_format_safe()``
142+
function by the person who wrote it.
143+
"""
144+
if not isinstance(text, str):
145+
raise TypeError("A string is required.")
146+
elif not text:
147+
# unnecessary optimization
148+
return ''
149+
150+
start = 0
151+
152+
# strip left
153+
pos = 0
154+
while pos < len(text):
155+
is_whitespace = unicodedata.category(text[pos]) == 'Zs'
156+
is_non_printing = (
157+
text[pos] in formatting.CONTROL_NON_PRINTING and
158+
text[pos] not in formatting.CONTROL_FORMATTING
159+
)
160+
if not is_whitespace and not is_non_printing:
161+
start = pos
162+
break
163+
pos += 1
164+
else:
165+
# skipped everything; string is all whitespace
166+
return ''
167+
168+
return text[start:]
169+
170+
133171
@plugin.command('tell', 'ask')
134172
@plugin.nickname_command('tell', 'ask')
135173
@plugin.example('$nickname, tell dgw he broke something again.')
@@ -143,7 +181,7 @@ def f_remind(bot, trigger):
143181
return
144182

145183
tellee = trigger.group(3).rstrip('.,:;')
146-
msg = trigger.group(2).lstrip(tellee).lstrip()
184+
msg = _format_safe_lstrip(trigger.group(2).split(' ', 1)[1])
147185

148186
if not msg:
149187
bot.reply("%s %s what?" % (verb, tellee))

sopel/modules/translate.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,11 @@ def mangle(bot, trigger):
240240
"""Repeatedly translate the input until it makes absolutely no sense."""
241241
long_lang_list = ['fr', 'de', 'es', 'it', 'no', 'he', 'la', 'ja', 'cy', 'ar', 'yi', 'zh', 'nl', 'ru', 'fi', 'hi', 'af', 'jw', 'mr', 'ceb', 'cs', 'ga', 'sv', 'eo', 'el', 'ms', 'lv']
242242
lang_list = []
243+
243244
for __ in range(0, 8):
244245
lang_list = get_random_lang(long_lang_list, lang_list)
245246
random.shuffle(lang_list)
247+
246248
if trigger.group(2) is None:
247249
try:
248250
phrase = (bot.memory['mangle_lines'][trigger.sender], '')
@@ -251,9 +253,11 @@ def mangle(bot, trigger):
251253
return
252254
else:
253255
phrase = (trigger.group(2).strip(), '')
256+
254257
if phrase[0] == '':
255258
bot.reply("What do you want me to mangle?")
256259
return
260+
257261
for lang in lang_list:
258262
backup = phrase
259263
try:
@@ -274,6 +278,12 @@ def mangle(bot, trigger):
274278
phrase = backup
275279
break
276280

281+
if phrase[0] is None:
282+
# translate() returns (None, None) if an error happens,
283+
# usually because the bot has exceeded a rate limit
284+
bot.reply("Translation rate limit reached. Try again later.")
285+
return
286+
277287
bot.say(phrase[0])
278288

279289

sopel/modules/wikipedia.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def __init__(self, section_name):
3535
self.section_name = section_name
3636

3737
self.citations = False
38+
self.messagebox = False
3839
self.span_depth = 0
3940
self.div_depth = 0
4041

@@ -55,14 +56,35 @@ def handle_starttag(self, tag, attrs):
5556
if attr[0] == 'class' and 'edit' in attr[1]:
5657
self.span_depth += 1
5758

58-
elif tag == 'div': # We want to skip thumbnail text and the inexplicable table of contents, and as such also need to track div depth
59+
elif tag == 'div':
60+
# We want to skip thumbnail text and the inexplicable table of contents,
61+
# and as such also need to track div depth
5962
if self.div_depth:
6063
self.div_depth += 1
6164
else:
6265
for attr in attrs:
6366
if attr[0] == 'class' and ('thumb' in attr[1] or attr[1] == 'toc'):
6467
self.div_depth += 1
6568

69+
elif tag == 'table':
70+
# Message box templates are what we want to ignore here
71+
for attr in attrs:
72+
if (
73+
attr[0] == 'class'
74+
and any(classname in attr[1].lower() for classname in [
75+
# Most of list from https://en.wikipedia.org/wiki/Template:Mbox_templates_see_also
76+
'ambox', # messageboxes on article pages
77+
'cmbox', # messageboxes on category pages
78+
'imbox', # messageboxes on file (image) pages
79+
'tmbox', # messageboxes on talk pages
80+
'fmbox', # header and footer messageboxes
81+
'ombox', # messageboxes on other types of page
82+
'mbox', # for messageboxes that are used in different namespaces and change their presentation accordingly
83+
'dmbox', # for disambiguation messageboxes
84+
])
85+
):
86+
self.messagebox = True
87+
6688
elif tag == 'ol':
6789
for attr in attrs:
6890
if attr[0] == 'class' and 'references' in attr[1]:
@@ -77,9 +99,11 @@ def handle_endtag(self, tag):
7799
self.span_depth -= 1
78100
if self.div_depth and tag == 'div':
79101
self.div_depth -= 1
102+
if self.messagebox and tag == 'table':
103+
self.messagebox = False
80104

81105
def handle_data(self, data):
82-
if self.consume and not any([self.citations, self.span_depth, self.div_depth]):
106+
if self.consume and not any([self.citations, self.messagebox, self.span_depth, self.div_depth]):
83107
if not (self.is_header and data == self.section_name): # Skip the initial header info only
84108
self.result += data
85109

@@ -225,14 +249,19 @@ def mw_section(server, query, section):
225249
for entry in sections['parse']['sections']:
226250
if entry['anchor'] == section:
227251
section_number = entry['index']
252+
# Needed to handle sections from transcluded pages properly
253+
# e.g. template documentation (usually pulled in from /doc subpage).
254+
# One might expect this prop to be nullable because in most cases it
255+
# will simply repeat the requested page title, but it's always set.
256+
fetch_title = quote(entry['fromtitle'])
228257
break
229258

230259
if not section_number:
231260
return None
232261

233262
snippet_url = ('https://{0}/w/api.php?format=json&redirects'
234263
'&action=parse&page={1}&prop=text'
235-
'&section={2}').format(server, query, section_number)
264+
'&section={2}').format(server, fetch_title, section_number)
236265

237266
data = get(snippet_url).json()
238267

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Tests for Sopel's ``announce`` plugin"""
2+
from __future__ import generator_stop
3+
4+
from sopel.modules import announce
5+
6+
7+
def test_chunks():
8+
"""Test the `_chunks` helper for functionality and compatibility."""
9+
# list input
10+
items = ['list', 'of', 'items', 'to', 'chunk']
11+
r = list(announce._chunks(items, 2))
12+
13+
assert len(r) == 3
14+
assert r[0] == tuple(items[:2])
15+
assert r[1] == tuple(items[2:4])
16+
assert r[2] == tuple(items[4:])
17+
18+
# tuple input
19+
items = ('tuple', 'of', 'items')
20+
r = list(announce._chunks(items, 3))
21+
22+
assert len(r) == 1
23+
assert r[0] == items
24+
25+
# dict keys input
26+
keys = {'one': True, 'two': True, 'three': True}.keys()
27+
items = list(keys)
28+
r = list(announce._chunks(keys, 1))
29+
30+
assert len(r) == 3
31+
for idx in range(len(items)):
32+
assert r[idx] == (items[idx],)

0 commit comments

Comments
 (0)