Hello community,

here is the log from the commit of package python-limnoria for openSUSE:Factory 
checked in at 2020-10-15 13:50:36
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-limnoria (Old)
 and      /work/SRC/openSUSE:Factory/.python-limnoria.new.3486 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-limnoria"

Thu Oct 15 13:50:36 2020 rev:18 rq:841776 version:2020.10.13

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-limnoria/python-limnoria.changes  
2020-09-06 00:03:00.871272231 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-limnoria.new.3486/python-limnoria.changes    
    2020-10-15 13:50:58.221313675 +0200
@@ -1,0 +2,8 @@
+Wed Oct 14 11:06:32 UTC 2020 - Atri Bhattacharya <[email protected]>
+
+- Update to version 2020-10-13:
+  * RSS: Fix announce removal to work with net+chan-specific
+    config.
+  * It only removed the value from the chan-specific value.
+
+-------------------------------------------------------------------

Old:
----
  limnoria-2020.09.03.tar.gz

New:
----
  limnoria-2020.10.13.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-limnoria.spec ++++++
--- /var/tmp/diff_new_pack.9GgyXH/_old  2020-10-15 13:50:59.013314005 +0200
+++ /var/tmp/diff_new_pack.9GgyXH/_new  2020-10-15 13:50:59.017314007 +0200
@@ -18,9 +18,9 @@
 
 %define skip_python2 1
 %define appname limnoria
-%define srcver 2020-09-03
+%define srcver 2020-10-13
 Name:           python-limnoria
-Version:        2020.09.03
+Version:        2020.10.13
 Release:        0
 Summary:        A modified version of Supybot (an IRC bot and framework)
 License:        BSD-3-Clause

++++++ limnoria-2020.09.03.tar.gz -> limnoria-2020.10.13.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2020-09-03/.travis.yml 
new/Limnoria-master-2020-10-13/.travis.yml
--- old/Limnoria-master-2020-09-03/.travis.yml  2020-08-30 14:52:45.000000000 
+0200
+++ new/Limnoria-master-2020-10-13/.travis.yml  2020-10-10 11:51:56.000000000 
+0200
@@ -15,10 +15,10 @@
 matrix:
   include:
     - python: "3.4"
-      env: WITH_OPT_DEPS=true
+      env: WITH_OPT_DEPS=false
       dist: trusty
     - python: "3.5"
-      env: WITH_OPT_DEPS=true
+      env: WITH_OPT_DEPS=false
       dist: trusty
     - python: "3.6"
       env: WITH_OPT_DEPS=true
@@ -44,7 +44,7 @@
       env: WITH_OPT_DEPS=true
       dist: xenial
 
-  allow_failures:
+#  allow_failures:
     - python: "nightly"
       env: WITH_OPT_DEPS=true
       dist: xenial
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Limnoria-master-2020-09-03/plugins/Autocomplete/README.md 
new/Limnoria-master-2020-10-13/plugins/Autocomplete/README.md
--- old/Limnoria-master-2020-09-03/plugins/Autocomplete/README.md       
2020-08-30 14:52:45.000000000 +0200
+++ new/Limnoria-master-2020-10-13/plugins/Autocomplete/README.md       
2020-10-10 11:51:56.000000000 +0200
@@ -6,4 +6,4 @@
 False unless you know what you are doing).
 
 If you are interested in this feature, please contribute to
-[the discussion](https://github.com/ircv3/ircv3-specifications/pull/415>)
+[the discussion](https://github.com/ircv3/ircv3-specifications/pull/415)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Limnoria-master-2020-09-03/plugins/BadWords/config.py 
new/Limnoria-master-2020-10-13/plugins/BadWords/config.py
--- old/Limnoria-master-2020-09-03/plugins/BadWords/config.py   2020-08-30 
14:52:45.000000000 +0200
+++ new/Limnoria-master-2020-10-13/plugins/BadWords/config.py   2020-10-10 
11:51:56.000000000 +0200
@@ -44,15 +44,27 @@
                          'spaces)'))
         conf.supybot.plugins.BadWords.words.set(words)
 
-class LastModifiedSetOfStrings(registry.SpaceSeparatedSetOfStrings):
+class 
LastModifiedSpaceSeparatedSetOfStrings(registry.SpaceSeparatedSetOfStrings):
     lastModified = 0
     def setValue(self, v):
         self.lastModified = time.time()
         registry.SpaceSeparatedListOfStrings.setValue(self, v)
 
+class 
LastModifiedCommaSeparatedSetOfStrings(registry.CommaSeparatedSetOfStrings):
+    lastModified = 0
+    def set(self, v):
+        if not v.strip():
+            self.setValue(set())
+        else:
+            super().set(v)
+
+    def setValue(self, v):
+        self.lastModified = time.time()
+        registry.CommaSeparatedListOfStrings.setValue(self, v)
+
 BadWords = conf.registerPlugin('BadWords')
 conf.registerGlobalValue(BadWords, 'words',
-    LastModifiedSetOfStrings([], _("""Determines what words are
+    LastModifiedSpaceSeparatedSetOfStrings([], _("""Determines what words are
     considered to be 'bad' so the bot won't say them.""")))
 conf.registerChannelValue(BadWords,'requireWordBoundaries',
     registry.Boolean(False, _("""Determines whether the bot will require bad
@@ -62,6 +74,9 @@
     false. After changing this setting, the BadWords regexp needs to be
     regenerated by adding/removing a word to the list, or reloading the
     plugin.""")))
+conf.registerGlobalValue(BadWords, 'phrases',
+    LastModifiedCommaSeparatedSetOfStrings([], _("""Comma-separated groups
+    of words that are considered to be 'bad'.""")))
 
 class String256(registry.String):
     def __call__(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Limnoria-master-2020-09-03/plugins/BadWords/plugin.py 
new/Limnoria-master-2020-10-13/plugins/BadWords/plugin.py
--- old/Limnoria-master-2020-09-03/plugins/BadWords/plugin.py   2020-08-30 
14:52:45.000000000 +0200
+++ new/Limnoria-master-2020-10-13/plugins/BadWords/plugin.py   2020-10-10 
11:51:56.000000000 +0200
@@ -53,6 +53,7 @@
         self.filtering = True
         self.lastModified = 0
         self.words = conf.supybot.plugins.BadWords.words
+        self.phrases = conf.supybot.plugins.BadWords.phrases
 
     def callCommand(self, name, irc, msg, *args, **kwargs):
         if ircdb.checkCapability(msg.prefix, 'admin'):
@@ -71,7 +72,7 @@
         self.filtering = True
         # We need to check for bad words here rather than in doPrivmsg because
         # messages don't get to doPrivmsg if the user is ignored.
-        if msg.command == 'PRIVMSG' and self.words():
+        if msg.command == 'PRIVMSG' and (self.words() or self.phrases()):
             channel = msg.channel
             self.updateRegexp(channel, irc.network)
             s = ircutils.stripFormatting(msg.args[1])
@@ -96,12 +97,14 @@
         return msg
 
     def updateRegexp(self, channel, network):
-        if self.lastModified < self.words.lastModified:
-            self.makeRegexp(self.words(), channel, network)
+        if self.lastModified < self.words.lastModified \
+                or self.lastModified < self.phrases.lastModified:
+            self.makeRegexp(self.words() | self.phrases(), channel, network)
             self.lastModified = time.time()
 
     def outFilter(self, irc, msg):
-        if self.filtering and msg.command == 'PRIVMSG' and self.words():
+        if self.filtering and msg.command == 'PRIVMSG' \
+                and (self.words() or self.phrases()):
             channel = msg.channel
             self.updateRegexp(channel, irc.network)
             s = msg.args[1]
@@ -124,7 +127,7 @@
 
         Returns the list of words being censored.
         """
-        L = list(self.words())
+        L = list(self.words() | self.phrases())
         if L:
             self.filtering = False
             utils.sortBy(str.lower, L)
@@ -134,27 +137,40 @@
     list = wrap(list, ['admin'])
 
     @internationalizeDocstring
-    def add(self, irc, msg, args, words):
+    def add(self, irc, msg, args, new_words):
         """<word> [<word> ...]
 
         Adds all <word>s to the list of words being censored.
         """
-        set = self.words()
-        set.update(words)
-        self.words.setValue(set)
+        words = self.words()
+        phrases = self.phrases()
+        for word in new_words:
+            if ' ' in word:
+                phrases.add(word)
+            else:
+                words.add(word)
+
+        self.words.setValue(words)
+        self.phrases.setValue(phrases)
+
         irc.replySuccess()
     add = wrap(add, ['admin', many('something')])
 
     @internationalizeDocstring
-    def remove(self, irc, msg, args, words):
+    def remove(self, irc, msg, args, old_words):
         """<word> [<word> ...]
 
         Removes <word>s from the list of words being censored.
         """
-        set = self.words()
-        for word in words:
-            set.discard(word)
-        self.words.setValue(set)
+        words = self.words()
+        phrases = self.phrases()
+        for word in old_words:
+            words.discard(word)
+            phrases.discard(word)
+        self.words.setValue(words)
+        self.phrases.setValue(phrases)
+
+
         irc.replySuccess()
     remove = wrap(remove, ['admin', many('something')])
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2020-09-03/plugins/BadWords/test.py 
new/Limnoria-master-2020-10-13/plugins/BadWords/test.py
--- old/Limnoria-master-2020-09-03/plugins/BadWords/test.py     2020-08-30 
14:52:45.000000000 +0200
+++ new/Limnoria-master-2020-10-13/plugins/BadWords/test.py     2020-10-10 
11:51:56.000000000 +0200
@@ -76,7 +76,8 @@
         self.assertNotError('badwords list')
         self.assertNotError('badwords add shit')
         self.assertNotError('badwords add ass')
-        self.assertResponse('badwords list', 'ass and shit')
+        self.assertNotError('badwords add "fuck you"')
+        self.assertResponse('badwords list', 'ass, fuck you, and shit')
 
 # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2020-09-03/plugins/Config/plugin.py 
new/Limnoria-master-2020-10-13/plugins/Config/plugin.py
--- old/Limnoria-master-2020-09-03/plugins/Config/plugin.py     2020-08-30 
14:52:45.000000000 +0200
+++ new/Limnoria-master-2020-10-13/plugins/Config/plugin.py     2020-10-10 
11:51:56.000000000 +0200
@@ -37,7 +37,6 @@
 import supybot.world as world
 import supybot.ircdb as ircdb
 from supybot.commands import *
-from supybot.utils.iter import all
 import supybot.ircutils as ircutils
 import supybot.registry as registry
 import supybot.callbacks as callbacks
@@ -172,7 +171,9 @@
                 vname = ':' + vname
             if getattr(v, '_channelValue', False):
                 vname = '#' + vname
-            if v._added and not all(irc.isChannel, v._added):
+            if v._added and not all(
+                    irc.isChannel(child) or child.startswith(':')
+                    for child in v._added):
                 vname = '@' + vname
             L.append(vname)
         utils.sortBy(str.lower, L)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2020-09-03/plugins/Config/test.py 
new/Limnoria-master-2020-10-13/plugins/Config/test.py
--- old/Limnoria-master-2020-09-03/plugins/Config/test.py       2020-08-30 
14:52:45.000000000 +0200
+++ new/Limnoria-master-2020-10-13/plugins/Config/test.py       2020-10-10 
11:51:56.000000000 +0200
@@ -39,7 +39,7 @@
 
 class ConfigTestCase(ChannelPluginTestCase):
     # We add utilities so there's something in supybot.plugins.
-    plugins = ('Config', 'User', 'Utilities')
+    plugins = ('Config', 'User', 'Utilities', 'Web')
 
     prefix1 = '[email protected]'
     prefix2 = '[email protected]'
@@ -437,6 +437,77 @@
         self.assertFalse(
             conf.supybot.reply.whenAddressedBy.strings.get(':test')._wasSet)
 
+    def testResetRegexpChannel(self):
+        """Specifically tests resetting a Regexp value, as they have an extra
+        internal state that needs to be reset; see the comment in plugin.py"""
+        self.assertResponse(
+            'config plugins.Web.nonSnarfingRegexp',
+            'Global:  ; #test @ test:  '
+        )
+        self.assertResponse(
+            'config plugins.Web.nonSnarfingRegexp m/foo/',
+            'The operation succeeded.'
+        )
+        self.assertResponse(
+            'config channel plugins.Web.nonSnarfingRegexp m/bar/',
+            'The operation succeeded.'
+        )
+        self.assertResponse(
+            'config plugins.Web.nonSnarfingRegexp',
+            'Global: m/foo/; #test @ test: m/bar/'
+        )
+        self.assertResponse(
+            'config reset channel plugins.Web.nonSnarfingRegexp',
+            'The operation succeeded.'
+        )
+        self.assertResponse('config plugins.Web.nonSnarfingRegexp',
+            'Global: m/foo/; #test @ test: m/foo/'
+        )
+        self.assertResponse(
+            'config plugins.Web.nonSnarfingRegexp ""',
+            'The operation succeeded.'
+        )
+        self.assertResponse(
+            'config plugins.Web.nonSnarfingRegexp',
+            'Global:  ; #test @ test:  '
+        )
+
+    def testResetRegexpNetwork(self):
+        """Specifically tests resetting a Regexp value, as they have an extra
+        internal state that needs to be reset; see the comment in plugin.py"""
+        self.assertResponse(
+            'config plugins.Web.nonSnarfingRegexp',
+            'Global:  ; #test @ test:  '
+        )
+        self.assertResponse(
+            'config plugins.Web.nonSnarfingRegexp m/foo/',
+            'The operation succeeded.'
+        )
+        self.assertResponse(
+            'config network plugins.Web.nonSnarfingRegexp m/bar/',
+            'The operation succeeded.'
+        )
+        self.assertResponse(
+            'config plugins.Web.nonSnarfingRegexp',
+            'Global: m/foo/; #test @ test: m/bar/'
+        )
+        self.assertResponse(
+            'config reset network plugins.Web.nonSnarfingRegexp',
+            'The operation succeeded.'
+        )
+        self.assertResponse('config plugins.Web.nonSnarfingRegexp',
+            'Global: m/foo/; #test @ test: m/foo/'
+        )
+        self.assertResponse(
+            'config plugins.Web.nonSnarfingRegexp ""',
+            'The operation succeeded.'
+        )
+        self.assertResponse(
+            'config plugins.Web.nonSnarfingRegexp',
+            'Global:  ; #test @ test:  '
+        )
+
+
 
 # vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2020-09-03/plugins/Math/test.py 
new/Limnoria-master-2020-10-13/plugins/Math/test.py
--- old/Limnoria-master-2020-09-03/plugins/Math/test.py 2020-08-30 
14:52:45.000000000 +0200
+++ new/Limnoria-master-2020-10-13/plugins/Math/test.py 2020-10-10 
11:51:56.000000000 +0200
@@ -137,7 +137,8 @@
 
     def testCalcMemoryError(self):
         self.assertRegexp('calc ' + '('*10000,
-            '(too much recursion'  # cpython
+            '(too much recursion'  # cpython < 3.10
+            '|too many nested parentheses'  # cpython >= 3.10
             '|parenthesis is never closed)'  # pypy
         )
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2020-09-03/plugins/Misc/plugin.py 
new/Limnoria-master-2020-10-13/plugins/Misc/plugin.py
--- old/Limnoria-master-2020-09-03/plugins/Misc/plugin.py       2020-08-30 
14:52:45.000000000 +0200
+++ new/Limnoria-master-2020-10-13/plugins/Misc/plugin.py       2020-10-10 
11:51:56.000000000 +0200
@@ -168,8 +168,7 @@
                 # echo [] will get us an empty token set, but there's no need
                 # to log this in that case anyway, it being a nested command.
                 self.log.info('Not replying to %s in %s, not a command.',
-                    tokens[0], channel
-                    if channel != irc.nick else _('private'))
+                    tokens[0], channel or _('private'))
             if irc.nested:
                 bracketConfig = conf.supybot.commands.nested.brackets
                 brackets = bracketConfig.getSpecific(irc.network, channel)()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2020-09-03/plugins/RSS/plugin.py 
new/Limnoria-master-2020-10-13/plugins/RSS/plugin.py
--- old/Limnoria-master-2020-09-03/plugins/RSS/plugin.py        2020-08-30 
14:52:45.000000000 +0200
+++ new/Limnoria-master-2020-10-13/plugins/RSS/plugin.py        2020-10-10 
11:51:56.000000000 +0200
@@ -527,10 +527,17 @@
             message isn't sent in the channel itself.
             """
             announce = conf.supybot.plugins.RSS.announce
-            S = announce.get(channel)()
-            for feed in feeds:
-                S.discard(feed)
-            announce.get(channel).setValue(S)
+
+            def remove_from_var(var):
+                S = var()
+                for feed in feeds:
+                    S.discard(feed)
+                var.setValue(S)
+
+            remove_from_var(announce.get(channel))
+            remove_from_var(announce.getSpecific(
+                channel=channel, network=irc.network))
+
             irc.replySuccess()
         remove = wrap(remove, [('checkChannelCapability', 'op'),
                                many(first('url', 'feedName'))])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2020-09-03/plugins/RSS/test.py 
new/Limnoria-master-2020-10-13/plugins/RSS/test.py
--- old/Limnoria-master-2020-09-03/plugins/RSS/test.py  2020-08-30 
14:52:45.000000000 +0200
+++ new/Limnoria-master-2020-10-13/plugins/RSS/test.py  2020-10-10 
11:51:56.000000000 +0200
@@ -28,8 +28,12 @@
 # POSSIBILITY OF SUCH DAMAGE.
 ###
 
+import functools
+from unittest.mock import patch
 import sys
+
 import feedparser
+
 from supybot.test import *
 import supybot.conf as conf
 import supybot.utils.minisix as minisix
@@ -53,12 +57,26 @@
 </rss>
 """
 
-def constant(content):
-    if minisix.PY3:
-        content = content.encode()
-    def f(*args, **kwargs):
-        return minisix.io.BytesIO(content)
-    return f
+
+class MockResponse:
+    headers = {}
+    url = ''
+    def read(self):
+        return self._data.encode()
+
+    def close(self):
+        pass
+
+def mock_urllib(f):
+    mock = MockResponse()
+
+    @functools.wraps(f)
+    def newf(self):
+        with patch("urllib.request.OpenerDirector.open", return_value=mock):
+            f(self, mock)
+
+    return newf
+
 
 url = 'http://www.advogato.org/rss/articles.xml'
 class RSSTestCase(ChannelPluginTestCase):
@@ -82,9 +100,9 @@
         finally:
             self.assertNotError('rss remove xkcd')
 
-    def testInitialAnnounceNewest(self):
-        old_open = feedparser._open_resource
-        feedparser._open_resource = constant(xkcd_new)
+    @mock_urllib
+    def testInitialAnnounceNewest(self, mock):
+        mock._data = xkcd_new
         timeFastForward(1.1)
         try:
             with conf.supybot.plugins.RSS.sortFeedItems.context('newestFirst'):
@@ -95,11 +113,10 @@
         finally:
             self._feedMsg('rss announce remove xkcd')
             self._feedMsg('rss remove xkcd')
-            feedparser._open_resource = old_open
 
-    def testInitialAnnounceOldest(self):
-        old_open = feedparser._open_resource
-        feedparser._open_resource = constant(xkcd_new)
+    @mock_urllib
+    def testInitialAnnounceOldest(self, mock):
+        mock._data = xkcd_new
         timeFastForward(1.1)
         try:
             with conf.supybot.plugins.RSS.initialAnnounceHeadlines.context(1):
@@ -110,11 +127,10 @@
         finally:
             self._feedMsg('rss announce remove xkcd')
             self._feedMsg('rss remove xkcd')
-            feedparser._open_resource = old_open
 
-    def testNoInitialAnnounce(self):
-        old_open = feedparser._open_resource
-        feedparser._open_resource = constant(xkcd_old)
+    @mock_urllib
+    def testNoInitialAnnounce(self, mock):
+        mock._data = xkcd_old
         timeFastForward(1.1)
         try:
             with conf.supybot.plugins.RSS.initialAnnounceHeadlines.context(0):
@@ -124,11 +140,10 @@
         finally:
             self._feedMsg('rss announce remove xkcd')
             self._feedMsg('rss remove xkcd')
-            feedparser._open_resource = old_open
 
-    def testAnnounce(self):
-        old_open = feedparser._open_resource
-        feedparser._open_resource = constant(xkcd_old)
+    @mock_urllib
+    def testAnnounce(self, mock):
+        mock._data = xkcd_old
         timeFastForward(1.1)
         try:
             self.assertError('rss announce add xkcd')
@@ -139,7 +154,7 @@
                 with conf.supybot.plugins.RSS.waitPeriod.context(1):
                     timeFastForward(1.1)
                     self.assertNoResponse(' ', timeout=0.1)
-                    feedparser._open_resource = constant(xkcd_new)
+                    mock._data = xkcd_new
                     self.assertNoResponse(' ', timeout=0.1)
                     timeFastForward(1.1)
                     self.assertRegexp(' ', 'Chaos')
@@ -148,11 +163,10 @@
         finally:
             self._feedMsg('rss announce remove xkcd')
             self._feedMsg('rss remove xkcd')
-            feedparser._open_resource = old_open
 
-    def testMaxAnnounces(self):
-        old_open = feedparser._open_resource
-        feedparser._open_resource = constant(xkcd_old)
+    @mock_urllib
+    def testMaxAnnounces(self, mock):
+        mock._data = xkcd_old
         timeFastForward(1.1)
         try:
             self.assertError('rss announce add xkcd')
@@ -164,7 +178,7 @@
                     with 
conf.supybot.plugins.RSS.maximumAnnounceHeadlines.context(1):
                         timeFastForward(1.1)
                         self.assertNoResponse(' ', timeout=0.1)
-                        feedparser._open_resource = constant(xkcd_new)
+                        mock._data = xkcd_new
                         log.debug('set return value to: %r', xkcd_new)
                         self.assertNoResponse(' ', timeout=0.1)
                         timeFastForward(1.1)
@@ -173,11 +187,10 @@
         finally:
             self._feedMsg('rss announce remove xkcd')
             self._feedMsg('rss remove xkcd')
-            feedparser._open_resource = old_open
 
-    def testAnnounceAnonymous(self):
-        old_open = feedparser._open_resource
-        feedparser._open_resource = constant(xkcd_old)
+    @mock_urllib
+    def testAnnounceAnonymous(self, mock):
+        mock._data = xkcd_old
         timeFastForward(1.1)
         try:
             self.assertNotError('rss announce add http://xkcd.com/rss.xml')
@@ -185,18 +198,17 @@
             with conf.supybot.plugins.RSS.waitPeriod.context(1):
                 timeFastForward(1.1)
                 self.assertNoResponse(' ', timeout=0.1)
-                feedparser._open_resource = constant(xkcd_new)
+                mock._data = xkcd_new
                 self.assertNoResponse(' ', timeout=0.1)
                 timeFastForward(1.1)
                 self.assertRegexp(' ', 'Telescopes')
         finally:
             self._feedMsg('rss announce remove http://xkcd.com/rss.xml')
             self._feedMsg('rss remove http://xkcd.com/rss.xml')
-            feedparser._open_resource = old_open
 
-    def testAnnounceReload(self):
-        old_open = feedparser._open_resource
-        feedparser._open_resource = constant(xkcd_old)
+    @mock_urllib
+    def testAnnounceReload(self, mock):
+        mock._data = xkcd_old
         timeFastForward(1.1)
         try:
             with conf.supybot.plugins.RSS.waitPeriod.context(1):
@@ -210,29 +222,27 @@
         finally:
             self._feedMsg('rss announce remove xkcd')
             self._feedMsg('rss remove xkcd')
-            feedparser._open_resource = old_open
 
-    def testReload(self):
-        old_open = feedparser._open_resource
-        feedparser._open_resource = constant(xkcd_old)
+    @mock_urllib
+    def testReload(self, mock):
+        mock._data = xkcd_old
         timeFastForward(1.1)
         try:
             with conf.supybot.plugins.RSS.waitPeriod.context(1):
                 self.assertNotError('rss add xkcd http://xkcd.com/rss.xml')
                 self.assertNotError('rss announce add xkcd')
                 self.assertRegexp(' ', 'Snake Facts')
-                feedparser._open_resource = constant(xkcd_new)
+                mock._data = xkcd_new
                 self.assertNotError('reload RSS')
                 self.assertRegexp(' ', 'Telescopes')
         finally:
             self._feedMsg('rss announce remove xkcd')
             self._feedMsg('rss remove xkcd')
-            feedparser._open_resource = old_open
 
-    def testReloadNoDelay(self):
+    @mock_urllib
+    def testReloadNoDelay(self, mock):
         # https://github.com/ProgVal/Limnoria/issues/922
-        old_open = feedparser._open_resource
-        feedparser._open_resource = constant(xkcd_old)
+        mock._data = xkcd_old
         timeFastForward(1.1)
         try:
             with conf.supybot.plugins.RSS.waitPeriod.context(1):
@@ -243,11 +253,10 @@
         finally:
             self._feedMsg('rss announce remove xkcd')
             self._feedMsg('rss remove xkcd')
-            feedparser._open_resource = old_open
 
-    def testReannounce(self):
-        old_open = feedparser._open_resource
-        feedparser._open_resource = constant(xkcd_old)
+    @mock_urllib
+    def testReannounce(self, mock):
+        mock._data = xkcd_old
         timeFastForward(1.1)
         try:
             self.assertError('rss announce add xkcd')
@@ -260,7 +269,7 @@
                         timeFastForward(1.1)
                         self.assertNoResponse(' ', timeout=0.1)
                         self._feedMsg('rss announce remove xkcd')
-                        feedparser._open_resource = constant(xkcd_new)
+                        mock._data = xkcd_new
                         timeFastForward(1.1)
                         self.assertNoResponse(' ', timeout=0.1)
                         self.assertNotError('rss announce add xkcd')
@@ -271,11 +280,10 @@
         finally:
             self._feedMsg('rss announce remove xkcd')
             self._feedMsg('rss remove xkcd')
-            feedparser._open_resource = old_open
 
-    def testFeedSpecificFormat(self):
-        old_open = feedparser._open_resource
-        feedparser._open_resource = constant(xkcd_old)
+    @mock_urllib
+    def testFeedSpecificFormat(self, mock):
+        mock._data = xkcd_old
         timeFastForward(1.1)
         try:
             self.assertNotError('rss add xkcd http://xkcd.com/rss.xml')
@@ -287,11 +295,10 @@
         finally:
             self._feedMsg('rss remove xkcd')
             self._feedMsg('rss remove xkcdsec')
-            feedparser._open_resource = old_open
 
-    def testFeedSpecificWaitPeriod(self):
-        old_open = feedparser._open_resource
-        feedparser._open_resource = constant(xkcd_old)
+    @mock_urllib
+    def testFeedSpecificWaitPeriod(self, mock):
+        mock._data = xkcd_old
         timeFastForward(1.1)
         try:
             self.assertNotError('rss add xkcd1 http://xkcd.com/rss.xml')
@@ -304,7 +311,7 @@
                 with 
conf.supybot.plugins.RSS.feeds.xkcd1.waitPeriod.context(1):
                     timeFastForward(1.1)
                     self.assertNoResponse(' ', timeout=0.1)
-                    feedparser._open_resource = constant(xkcd_new)
+                    mock._data = xkcd_new
                     self.assertNoResponse(' ', timeout=0.1)
                     timeFastForward(1.1)
                     self.assertRegexp(' ', 'xkcd1.*Chaos')
@@ -317,30 +324,23 @@
             self._feedMsg('rss remove xkcd1')
             self._feedMsg('rss announce remove xkcd2')
             self._feedMsg('rss remove xkcd2')
-            feedparser._open_resource = old_open
 
-    def testDescription(self):
+    @mock_urllib
+    def testDescription(self, mock):
         timeFastForward(1.1)
         with conf.supybot.plugins.RSS.format.context('$description'):
-            old_open = feedparser._open_resource
-            feedparser._open_resource = constant(xkcd_new)
-            try:
-                self.assertRegexp('rss http://xkcd.com/rss.xml',
-                        'On the other hand, the refractor\'s')
-            finally:
-                feedparser._open_resource = old_open
+            mock._data = xkcd_new
+            self.assertRegexp('rss http://xkcd.com/rss.xml',
+                    'On the other hand, the refractor\'s')
 
-    def testBadlyFormedFeedWithNoItems(self):
+    @mock_urllib
+    def testBadlyFormedFeedWithNoItems(self, mock):
         # This combination will cause the RSS command to show the last parser
         # error.
-        old_open = feedparser._open_resource
         timeFastForward(1.1)
-        feedparser._open_resource = constant(not_well_formed)
-        try:
-            self.assertRegexp('rss http://example.com/',
-                              'Parser error')
-        finally:
-            feedparser._open_resource = old_open
+        mock._data = not_well_formed
+        self.assertRegexp('rss http://example.com/',
+                          'Parser error')
 
     if network:
         timeout = 5  # Note this applies also to the above tests
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Limnoria-master-2020-09-03/plugins/Scheduler/plugin.py 
new/Limnoria-master-2020-10-13/plugins/Scheduler/plugin.py
--- old/Limnoria-master-2020-09-03/plugins/Scheduler/plugin.py  2020-08-30 
14:52:45.000000000 +0200
+++ new/Limnoria-master-2020-10-13/plugins/Scheduler/plugin.py  2020-10-10 
11:51:56.000000000 +0200
@@ -90,7 +90,12 @@
                         # though we'll never know for sure, because other
                         # plugins can schedule stuff, too.
                         n = int(name)
-                    self._add(network, event['msg'], event['time'], 
event['command'], n)
+                    # Here we use event.get() method instead of event[]
+                    # This is to maintain compatibility with older bots
+                    # lacking 'is_reminder' in their event dicts
+                    is_reminder = event.get('is_reminder', False)
+                    self._add(network, event['msg'], event['time'], 
event['command'],
+                              is_reminder, n)
                 elif event['type'] == 'repeat': # repeating event
                     now = time.time()
                     first_run = event.get('first_run')
@@ -145,13 +150,27 @@
             self.Proxy(irc, msg, tokens)
         return f
 
-    def _add(self, network, msg, t, command, name=None):
-        f = self._makeCommandFunction(network, msg, command)
+    def _makeReminderFunction(self, network, msg, text):
+        """Makes a function suitable for scheduling text"""
+        def f():
+            # If the network isn't available, pick any other one
+            irc = world.getIrc(network) or world.ircs[0]
+            replyIrc = callbacks.ReplyIrcProxy(irc, msg)
+            replyIrc.reply(_('Reminder: %s') % text, msg=msg, prefixNick=True)
+            del self.events[str(f.eventId)]
+        return f
+
+    def _add(self, network, msg, t, command, is_reminder=False, name=None):
+        if is_reminder:
+            f = self._makeReminderFunction(network, msg, command)
+        else:
+            f = self._makeCommandFunction(network, msg, command)
         id = schedule.addEvent(f, t, name)
         f.eventId = id
         self.events[str(id)] = {'command':command,
+                                'is_reminder':is_reminder,
                                 'msg':msg,
-                                'network': network,
+                                'network':network,
                                 'time':t,
                                 'type':'single'}
         return id
@@ -172,6 +191,19 @@
     add = wrap(add, ['positiveInt', 'text'])
 
     @internationalizeDocstring
+    def remind(self, irc, msg, args, seconds, text):
+        """ <seconds> <text>
+
+        Sets a reminder with string <text> to run <seconds> seconds in the
+        future. For example, 'scheduler remind [seconds 30m] "Hello World"'
+        will return '<nick> Reminder: Hello World' 30 minutes after being set.
+        """
+        t = time.time() + seconds
+        id = self._add(irc.network, msg, t, text, is_reminder=True)
+        irc.replySuccess(format(_('Reminder Event #%i added.'), id))
+    remind = wrap(remind, ['positiveInt', 'text'])
+
+    @internationalizeDocstring
     def remove(self, irc, msg, args, id):
         """<id>
 
@@ -200,7 +232,7 @@
         assert id == name
         self.events[name] = {'command':command,
                              'msg':msg,
-                             'network': network,
+                             'network':network,
                              'time':seconds,
                              'type':'repeat',
                              'first_run': first_run,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2020-09-03/plugins/Scheduler/test.py 
new/Limnoria-master-2020-10-13/plugins/Scheduler/test.py
--- old/Limnoria-master-2020-09-03/plugins/Scheduler/test.py    2020-08-30 
14:52:45.000000000 +0200
+++ new/Limnoria-master-2020-10-13/plugins/Scheduler/test.py    2020-10-10 
11:51:56.000000000 +0200
@@ -74,6 +74,21 @@
         timeFastForward(5)
         self.assertNoResponse(' ', timeout=1)
 
+    def testRemind(self):
+        self.assertNotError('scheduler remind 5 testRemind')
+        self.assertResponse(
+            'scheduler list',
+            '3 (in 4 seconds): "testRemind"')
+        timeFastForward(3)
+        self.assertNoResponse(' ', timeout=1)
+        timeFastForward(3)
+        self.assertResponse(' ', 'Reminder: testRemind')
+        timeFastForward(5)
+        self.assertNoResponse(' ', timeout=1)
+        self.assertResponse(
+            'scheduler list', 'There are currently no scheduled commands.')
+
+
     def testRepeat(self):
         self.assertRegexp('scheduler repeat repeater 5 echo testRepeat',
             'testRepeat')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2020-09-03/src/ircdb.py 
new/Limnoria-master-2020-10-13/src/ircdb.py
--- old/Limnoria-master-2020-09-03/src/ircdb.py 2020-08-30 14:52:45.000000000 
+0200
+++ new/Limnoria-master-2020-10-13/src/ircdb.py 2020-10-10 11:51:56.000000000 
+0200
@@ -1172,8 +1172,8 @@
     try:
         id = users.getUserId(hostmask)
         user = users.getUser(id)
-        if user._checkCapability('owner'):
-            # Owners shouldn't ever be ignored.
+        if user._checkCapability('trusted'):
+            # Trusted users (including owners) shouldn't ever be ignored.
             return False
         elif user.ignore:
             log.debug('Ignoring %s due to their IrcUser ignore flag.', 
hostmask)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2020-09-03/src/irclib.py 
new/Limnoria-master-2020-10-13/src/irclib.py
--- old/Limnoria-master-2020-09-03/src/irclib.py        2020-08-30 
14:52:45.000000000 +0200
+++ new/Limnoria-master-2020-10-13/src/irclib.py        2020-10-10 
11:51:56.000000000 +0200
@@ -301,6 +301,58 @@
 # status of various modes (especially ops/halfops/voices) in channels, etc.
 ###
 class ChannelState(utils.python.Object):
+    """Represents the known state of an IRC channel.
+
+    .. attribute:: topic
+
+        The topic of a channel (possibly the empty stringĂ 
+
+        :type: str
+
+    .. attribute:: created
+
+        Timestamp of the channel creation, according to the server.
+
+        :type: int
+
+    .. attribute:: ops
+
+        Set of the nicks of all the operators of the channel.
+
+        :type: ircutils.IrcSet[str]
+
+    .. attribute:: halfops
+
+        Set of the nicks of all the half-operators of the channel.
+
+        :type: ircutils.IrcSet[str]
+
+    .. attribute:: voices
+
+        Set of the nicks of all the voiced users of the channel.
+
+        :type: ircutils.IrcSet[str]
+
+    .. attribute:: users
+
+        Set of the nicks of all the users in the channel.
+
+        :type: ircutils.IrcSet[str]
+
+    .. attribute:: bans
+
+        Set of the all the banmasks set in the channel.
+
+        :type: ircutils.IrcSet[str]
+
+    .. attribute:: modes
+
+        Dict of all the modes set in the channel, with they value, if any.
+        This excludes the following modes: ovhbeq
+
+        :type: Dict[str, Optional[str]]
+    """
+
     __slots__ = ('users', 'ops', 'halfops', 'bans',
                  'voices', 'topic', 'modes', 'created')
     def __init__(self):
@@ -314,16 +366,27 @@
         self.modes = {}
 
     def isOp(self, nick):
+        """Returns whether the given nick is an op."""
         return nick in self.ops
+
     def isOpPlus(self, nick):
+        """Returns whether the given nick is an op."""
         return nick in self.ops
+
     def isVoice(self, nick):
+        """Returns whether the given nick is voiced."""
         return nick in self.voices
+
     def isVoicePlus(self, nick):
+        """Returns whether the given nick is voiced, an halfop, or an op."""
         return nick in self.voices or nick in self.halfops or nick in self.ops
+
     def isHalfop(self, nick):
+        """Returns whether the given nick is an halfop."""
         return nick in self.halfops
+
     def isHalfopPlus(self, nick):
+        """Returns whether the given nick is an halfop, or an op."""
         return nick in self.halfops or nick in self.ops
 
     def addUser(self, user):
@@ -412,7 +475,11 @@
             ret = ret and getattr(self, name) == getattr(other, name)
         return ret
 
+
 Batch = collections.namedtuple('Batch', 'type arguments messages')
+"""Represents a batch of messages, see
+<https://ircv3.net/specs/extensions/batch-3.2>"""
+
 
 class IrcStateFsm(object):
     '''Finite State Machine keeping track of what part of the connection
@@ -421,6 +488,8 @@
 
     @enum.unique
     class States(enum.Enum):
+        """Enumeration of all the states of an IRC connection."""
+
         UNINITIALIZED = 10
         '''Nothing received yet (except server notices)'''
 
@@ -515,6 +584,8 @@
         self._transition(irc, msg, self.States.INIT_MOTD, [
             self.States.INIT_CAP_NEGOTIATION,
             self.States.INIT_WAITING_MOTD,
+            self.States.CONNECTED,
+            self.States.CONNECTED_SASL,
         ])
 
     def on_end_motd(self, irc, msg):
@@ -522,7 +593,9 @@
         self._transition(irc, msg, self.States.CONNECTED, [
             self.States.INIT_CAP_NEGOTIATION,
             self.States.INIT_WAITING_MOTD,
-            self.States.INIT_MOTD
+            self.States.INIT_MOTD,
+            self.States.CONNECTED,
+            self.States.CONNECTED_SASL,
         ])
 
     def on_shutdown(self, irc, msg):
@@ -530,8 +603,84 @@
 
 class IrcState(IrcCommandDispatcher, log.Firewalled):
     """Maintains state of the Irc connection.  Should also become smarter.
+
+    .. attribute:: fsm
+
+        A finite-state machine representing the current state of the IRC
+        connection: various steps while connecting, then remains in the
+        CONNECTED state (or CONNECTED_SASL when doing SASL in the middle of a
+        connection).
+
+        :type: IrcStateFsm
+
+    .. attribute:: capabilities_req
+
+        Set of all capabilities requested from the server.
+        See <https://ircv3.net/specs/core/capability-negotiation>
+
+        :type: Set[str]
+
+    .. attribute:: capabilities_acq
+
+        Set of all capabilities requested from and acknowledged by the
+        server. See <https://ircv3.net/specs/core/capability-negotiation>
+
+        :type: Set[str]
+
+    .. attribute:: capabilities_nak
+
+        Set of all capabilities requested from and refused by the server.
+        This should always be empty unless the bot, a plugin, or the server is
+        misbehaving. See <https://ircv3.net/specs/core/capability-negotiation>
+
+        :type: Set[str]
+
+    .. attribute:: capabilities_ls
+
+        Stores all the capabilities advertised by the server, as well as their
+        value, if any.
+
+        :type: Dict[str, Optional[str]]
+
+    .. attribute:: ircd
+
+        Identification string of the software running the server we are
+        connected to. See
+        <https://defs.ircdocs.horse/defs/numerics.html#rpl-myinfo-004>
+
+        :type: str
+
+    .. attribute:: supported
+
+        Stores the value of ISUPPORT sent when connecting.
+        See <https://defs.ircdocs.horse/defs/isupport.html> for the list of
+        keys.
+
+        :type: utils.InsensitivePreservingDict[str, Any]
+
+    .. attribute:: history
+
+        History of messages received from the network. Automatically discards
+        messages so it doesn't exceed
+        ``supybot.protocols.irc.maxHistoryLength``.
+
+        :type: RingBuffer[ircmsgs.IrcMsg]
+
+    .. attribute:: channels
+
+        Store channel states.
+
+        :type: ircutils.IrcDict[str, ChannelState]
+
+    .. attribute:: nickToHostmask
+
+        Stores the last hostmask of a seen nick.
+
+        :type: ircutils.IrcDict[str, str]
     """
     __firewalled__ = {'addMsg': None}
+
+
     def __init__(self, history=None, supported=None,
                  nicksToHostmasks=None, channels=None,
                  capabilities_req=None,
@@ -895,6 +1044,76 @@
     """The base class for an IRC connection.
 
     Handles PING commands already.
+
+    .. attribute:: zombie
+
+        Whether or not this object represents a living IRC connection.
+
+        :type: bool
+
+    .. attribute:: network
+
+        The name of the network this object is connected to.
+
+        :type: str
+
+    .. attribute:: startedAt
+
+        When this connection was (re)started.
+
+        :type: float
+
+    .. attribute:: callbacks
+
+        List of all callbacks (ie. plugins) currently loaded
+
+        :type: List[IrcCallback]
+
+    .. attribute:: queue
+
+        Queue of messages waiting to be sent. Plugins should use the
+        ``queueMsg`` method instead of accessing this directly.
+
+        :type: IrcMsgQueue
+
+    .. attribute:: fastqueue
+
+        Same as ``queue``, but for messages with high priority. Plugins should
+        use the ``sendMsg`` method instead of accessing this directly (or
+        `queueMsg` if the message isn't high priority).
+
+        :type: smallqueue
+
+    .. attribute:: driver
+
+        Driver of the IRC connection (normally, a
+        :py:class:`supybot.drivers.Socket.SocketDriver` object).
+        Plugins normally do not need to access this.
+
+    .. attribute:: startedSync
+
+        When joining a channel, a ``'#channel': time.time()`` entry is added
+        to this dict, which is then removed when the join is completed.
+        Plugins should not change this value, it is automatically handled when
+        they send a JOIN.
+
+        :type: ircutils.IrcDict[str, float]
+
+    .. attribute:: monitoring
+
+        A dict with nicks as keys and the number of plugins monitoring this
+        nick as value.
+        Plugins should not access this directly, and should use the ``monitor``
+        and ``unmonitor`` methods instead.
+
+        :type: ircutils.IrcDict[str, int]
+
+    .. attribute:: state
+
+        An :py:class:`supybot.irclib.IrcState` object, which stores all the
+        known information about the connection with the IRC network.
+
+        :type: supybot.irclib.IrcState
     """
     __firewalled__ = {'die': None,
                       'feedMsg': None,
@@ -930,6 +1149,9 @@
         return ircutils.isChannel(s, **kw)
 
     def isNick(self, s):
+        """Returns whether the given argument is a valid nick on this
+        network.
+        """
         kw = {}
         if 'nicklen' in self.state.supported:
             kw['nicklen'] = self.state.supported['nicklen']
@@ -1291,6 +1513,12 @@
         'userhost-in-names', 'invite-notify', 'server-time',
         'chghost', 'batch', 'away-notify', 'message-tags',
         'msgid', 'setname', 'labeled-response', 'echo-message'])
+    """IRCv3 capabilities requested when they are available.
+
+    echo-message is special-cased to be requested only with labeled-response.
+
+    To check if a capability was negotiated, use `irc.state.capabilities_ack`.
+    """
 
     def _queueConnectMessages(self):
         if self.zombie:
@@ -1358,7 +1586,10 @@
                     self._maybeStartSasl(msg)
                 else:
                     pass # Already in the middle of a SASL auth
-            else:
+            elif self.state.fsm.state != IrcStateFsm.States.CONNECTED:
+                # If we are still in the initial cap negotiation (ie. if this
+                # is not in response to a 'CAP NEW'), send a CAP END so the
+                # server sends us the MOTD
                 self.endCapabilityNegociation(msg)
         else:
             log.debug('Waiting for ACK/NAK of capabilities: %r',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2020-09-03/src/registry.py 
new/Limnoria-master-2020-10-13/src/registry.py
--- old/Limnoria-master-2020-09-03/src/registry.py      2020-08-30 
14:52:45.000000000 +0200
+++ new/Limnoria-master-2020-10-13/src/registry.py      2020-10-10 
11:51:56.000000000 +0200
@@ -749,14 +749,15 @@
 
 class Regexp(Value):
     """Value must be a valid regular expression."""
-    __slots__ = ('sr', 'value', '__parent')
     errormsg = _('Value must be a valid regular expression, not %r.')
-    def __init__(self, *args, **kwargs):
-        kwargs['setDefault'] = False
-        self.sr = ''
-        self.value = None
-        self.__parent = super(Regexp, self)
-        self.__parent.__init__(*args, **kwargs)
+
+    def __init__(self, default, *args, **kwargs):
+        # We're not supposed to do convertions here, BUT this is needed
+        # when the value is set programmatically because the value
+        # plugins set (a string) is not the same as the one they get
+        # (a compiled pattern object)
+        default = self._convertFromString(default)
+        super().__init__(default, *args, **kwargs)
 
     def error(self, e):
         s = 'Value must be a regexp of the form m/.../ or /.../. %s' % e
@@ -764,29 +765,48 @@
         e.value = self
         raise e
 
+    def _convertFromString(self, s):
+        if s:
+            # We need to preserve the original string, as it's shown in
+            # the user interface and the config file.
+            # It might be tempting to set the original string as an
+            # attribute, but doing so would result in inconsistent states
+            # for childs of this variable, should they be reset, or the
+            # value of there parent change.
+            return (s, utils.str.perlReToPythonRe(s))
+        else:
+            return None
+
     def set(self, s):
         try:
-            if s:
-                self.setValue(utils.str.perlReToPythonRe(s), sr=s)
-            else:
-                self.setValue(None)
+            v = self._convertFromString(s)
         except ValueError as e:
             self.error(e)
+        else:
+            super().set(v)
 
-    def setValue(self, v, sr=None):
-        if v is None:
-            self.sr = ''
-            self.__parent.setValue(None)
-        elif sr is not None:
-            self.sr = sr
-            self.__parent.setValue(v)
+    def setValue(self, v):
+        """Don't call this function directly from plugins, it is subject
+        to change without notice."""
+        if v is not None and (not isinstance(v, tuple) or len(v) != 2):
+            raise InvalidRegistryValue(
+                'Can\'t setValue a regexp, there would be an inconsistency '
+                'between the regexp and the recorded string value. '
+                'Use .set() instead.')
+
+        super().setValue(v)
+
+    def __call__(self):
+        if self.value is None:
+            return None
         else:
-            raise InvalidRegistryValue('Can\'t setValue a regexp, there would 
be an inconsistency '\
-                  'between the regexp and the recorded string value.')
+            return self.value[1]
 
     def __str__(self):
-        self() # Gotta update if we've been reloaded.
-        return self.sr
+        if self.value is None:
+            return ''
+        else:
+            return self.value[0]
 
 class SeparatedListOf(Value):
     __slots__ = ()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2020-09-03/src/setup.py 
new/Limnoria-master-2020-10-13/src/setup.py
--- old/Limnoria-master-2020-09-03/src/setup.py 2020-08-30 14:52:45.000000000 
+0200
+++ new/Limnoria-master-2020-10-13/src/setup.py 2020-10-10 11:51:56.000000000 
+0200
@@ -70,6 +70,7 @@
             if os.path.isfile(readme_path):
                 with open(readme_path, 'r') as fd:
                     kwargs['long_description'] = fd.read()
+                    kwargs['long_description_content_type'] = 'text/markdown'
 
         module_name = kwargs['name'].replace('-', '_')
         kwargs.setdefault('packages', [module_name])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2020-09-03/test/test_irclib.py 
new/Limnoria-master-2020-10-13/test/test_irclib.py
--- old/Limnoria-master-2020-09-03/test/test_irclib.py  2020-08-30 
14:52:45.000000000 +0200
+++ new/Limnoria-master-2020-10-13/test/test_irclib.py  2020-10-10 
11:51:56.000000000 +0200
@@ -45,6 +45,38 @@
 rawmsgs = []
 
 
+class CapNegMixin:
+    """Utilities for handling the capability negotiation."""
+
+    def startCapNegociation(self, caps='sasl'):
+        m = self.irc.takeMsg()
+        self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m)
+        self.assertTrue(m.args == ('LS', '302'), 'Expected CAP LS 302, got 
%r.' % m)
+
+        m = self.irc.takeMsg()
+        self.assertTrue(m.command == 'NICK', 'Expected NICK, got %r.' % m)
+
+        m = self.irc.takeMsg()
+        self.assertTrue(m.command == 'USER', 'Expected USER, got %r.' % m)
+
+        self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP',
+            args=('*', 'LS', caps)))
+
+        if caps:
+            m = self.irc.takeMsg()
+            self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m)
+            self.assertEqual(m.args[0], 'REQ', m)
+            self.assertEqual(m.args[1], 'sasl')
+
+            self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP',
+                args=('*', 'ACK', 'sasl')))
+
+    def endCapNegociation(self):
+        m = self.irc.takeMsg()
+        self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m)
+        self.assertEqual(m.args, ('END',), m)
+
+
 class IrcCommandDispatcherTestCase(SupyTestCase):
     class DispatchedClass(irclib.IrcCommandDispatcher):
         def doPrivmsg():
@@ -473,7 +505,8 @@
         st = irclib.IrcState()
         self.assert_(st.addMsg(self.irc, ircmsgs.IrcMsg('MODE foo +i')) or 1)
 
-class IrcCapsTestCase(SupyTestCase):
+
+class IrcCapsTestCase(SupyTestCase, CapNegMixin):
     def testReqLineLength(self):
         self.irc = irclib.Irc('test')
 
@@ -572,6 +605,74 @@
         m = self.irc.takeMsg()
         self.assertIsNone(m)
 
+    def testCapNew(self):
+        self.irc = irclib.Irc('test')
+
+        self.assertEqual(self.irc.sasl_current_mechanism, None)
+        self.assertEqual(self.irc.sasl_next_mechanisms, [])
+
+        self.startCapNegociation(caps='')
+
+        self.endCapNegociation()
+
+        while self.irc.takeMsg():
+            pass
+
+        self.irc.feedMsg(ircmsgs.IrcMsg(command='422')) # ERR_NOMOTD
+
+        m = self.irc.takeMsg()
+        self.assertIsNone(m)
+
+        self.irc.feedMsg(ircmsgs.IrcMsg(
+            command='CAP', args=['*', 'NEW', 'account-notify']))
+
+        m = self.irc.takeMsg()
+        self.assertEqual(m,
+            ircmsgs.IrcMsg(command='CAP', args=['REQ', 'account-notify']))
+
+        self.irc.feedMsg(ircmsgs.IrcMsg(
+            command='CAP', args=['*', 'ACK', 'account-notify']))
+
+        self.assertIn('account-notify', self.irc.state.capabilities_ack)
+
+    def testCapNewEchomessageLabeledResponse(self):
+        self.irc = irclib.Irc('test')
+
+        self.assertEqual(self.irc.sasl_current_mechanism, None)
+        self.assertEqual(self.irc.sasl_next_mechanisms, [])
+
+        self.startCapNegociation(caps='')
+
+        self.endCapNegociation()
+
+        while self.irc.takeMsg():
+            pass
+
+        self.irc.feedMsg(ircmsgs.IrcMsg(command='422')) # ERR_NOMOTD
+
+        m = self.irc.takeMsg()
+        self.assertIsNone(m)
+
+        self.irc.feedMsg(ircmsgs.IrcMsg(
+            command='CAP', args=['*', 'NEW', 'echo-message']))
+
+        m = self.irc.takeMsg()
+        self.assertIsNone(m)
+
+        self.irc.feedMsg(ircmsgs.IrcMsg(
+            command='CAP', args=['*', 'NEW', 'labeled-response']))
+
+        m = self.irc.takeMsg()
+        self.assertEqual(m,
+            ircmsgs.IrcMsg(
+                command='CAP', args=['REQ', 'echo-message labeled-response']))
+
+        self.irc.feedMsg(ircmsgs.IrcMsg(
+            command='CAP', args=['*', 'ACK', 'echo-message labeled-response']))
+
+        self.assertIn('echo-message', self.irc.state.capabilities_ack)
+        self.assertIn('labeled-response', self.irc.state.capabilities_ack)
+
 
 class StsTestCase(SupyTestCase):
     def setUp(self):
@@ -757,6 +858,17 @@
         self.irc.feedMsg(msg2)
         self.assertEqual(list(self.irc.state.history), [msg1, msg2])
 
+    def testMultipleMotd(self):
+        self.irc.reset()
+
+        self.irc.feedMsg(ircmsgs.IrcMsg(command='422'))
+
+        self.irc.feedMsg(ircmsgs.IrcMsg(command='422'))
+
+        self.irc.feedMsg(ircmsgs.IrcMsg(command='375', args=['nick']))
+        self.irc.feedMsg(ircmsgs.IrcMsg(command='372', args=['nick', 'some 
message']))
+        self.irc.feedMsg(ircmsgs.IrcMsg(command='376', args=['nick']))
+
     def testMsgChannel(self):
         self.irc.reset()
 
@@ -856,38 +968,10 @@
             self.irc.removeCallback(c.name())
         self.assertEqual(c.batch, irclib.Batch('netjoin', (), [m1, m2, m3, 
m4]))
 
-class SaslTestCase(SupyTestCase):
+class SaslTestCase(SupyTestCase, CapNegMixin):
     def setUp(self):
         pass
 
-    def startCapNegociation(self, caps='sasl'):
-        m = self.irc.takeMsg()
-        self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m)
-        self.assertTrue(m.args == ('LS', '302'), 'Expected CAP LS 302, got 
%r.' % m)
-
-        m = self.irc.takeMsg()
-        self.assertTrue(m.command == 'NICK', 'Expected NICK, got %r.' % m)
-
-        m = self.irc.takeMsg()
-        self.assertTrue(m.command == 'USER', 'Expected USER, got %r.' % m)
-
-        self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP',
-            args=('*', 'LS', caps)))
-
-        if caps:
-            m = self.irc.takeMsg()
-            self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m)
-            self.assertEqual(m.args[0], 'REQ', m)
-            self.assertEqual(m.args[1], 'sasl')
-
-            self.irc.feedMsg(ircmsgs.IrcMsg(command='CAP',
-                args=('*', 'ACK', 'sasl')))
-
-    def endCapNegociation(self):
-        m = self.irc.takeMsg()
-        self.assertTrue(m.command == 'CAP', 'Expected CAP, got %r.' % m)
-        self.assertEqual(m.args, ('END',), m)
-
     def testPlain(self):
         try:
             conf.supybot.networks.test.sasl.username.setValue('jilles')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Limnoria-master-2020-09-03/test/test_registry.py 
new/Limnoria-master-2020-10-13/test/test_registry.py
--- old/Limnoria-master-2020-09-03/test/test_registry.py        2020-08-30 
14:52:45.000000000 +0200
+++ new/Limnoria-master-2020-10-13/test/test_registry.py        2020-10-10 
11:51:56.000000000 +0200
@@ -182,9 +182,24 @@
         self.assertTrue(v().match('foo'))
         v.set('')
         self.assertEqual(v(), None)
+
+    def testRegexpSetValue(self):
+        v = registry.Regexp(None, 'help')
+        self.assertRaises(registry.InvalidRegistryValue,
+                          v.setValue, r'foo')
         self.assertRaises(registry.InvalidRegistryValue,
                           v.setValue, re.compile(r'foo'))
 
+    def testRegexpDefaultString(self):
+        v = registry.Regexp('m/foo/', 'help')
+        self.assertEqual(v(), re.compile('foo'))
+
+        v = registry.Regexp('', 'help')
+        self.assertEqual(v(), None)
+
+        v = registry.Regexp(None, 'help')
+        self.assertEqual(v(), None)
+
     def testBackslashesKeys(self):
         conf.supybot.reply.whenAddressedBy.strings.get(':foo').set('=/*')
         filename = conf.supybot.directories.conf.dirize('backslashes1.conf')


Reply via email to