jenkins-bot has submitted this change. ( 
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/632644 )

Change subject: [bugfix] Hold a second dict for backward compatibility in 
_OptionDict
......................................................................

[bugfix] Hold a second dict for backward compatibility in _OptionDict

Deprecated OptionHandler.options contains the options overridden from
defaults. Due to its expensive implementation in is deprecated by a
FutureWarning to be removed soon.

bot.py:
- Use old availableOptions options in OptionHandler.set_option if they
  are set directly and not updated; in such case deprecation of
  classproperty does not work. But show a deprecation warning then.
- Hold the second options dict in _OptionDict for backward compatibility
- update_options is used to update opt options if the deprecated options
  is changed
- Call __getitem__ from __getattr__ to provide attribute access to options.
- Call update_options with __getitem__
- Call __setitem__ from __setattr__ to give attribute access for settings
- Implement __setitem__ to update deprecated options dict
- OptionHandler.__getattribute__ is needed to call update_options

bot_tests.py:
- Add tests for OptionHandler

replace.py:
- replace availableOptions with available_options in redirect.py
- replace all usage of getOption in redirect.py with new opt attributes
  and simplify the option calls which show a good reason for the new
  OptionHandler implementation.

Bug: T264835
Change-Id: I82f1125ffcc7adf758ef5e1fa23b7f029b2eb688
---
M pywikibot/bot.py
M scripts/redirect.py
M tests/bot_tests.py
3 files changed, 142 insertions(+), 60 deletions(-)

Approvals:
  Meno25: Looks good to me, but someone else must approve
  Xqt: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/pywikibot/bot.py b/pywikibot/bot.py
index 079e987..82cf842 100644
--- a/pywikibot/bot.py
+++ b/pywikibot/bot.py
@@ -127,7 +127,7 @@
     PYTHON_VERSION,
 )
 from pywikibot.tools._logging import LoggingFormatter, RotatingFileHandler
-from pywikibot.tools import classproperty
+from pywikibot.tools import classproperty, suppress_warnings
 from pywikibot.tools.formatter import color_format

 # Note: all output goes through python std library "logging" module
@@ -1001,13 +1001,39 @@
     """The option dict which holds the options of OptionHandler."""

     def __init__(self, classname, options):
-        self.classname = classname
+        self._classname = classname
+        self._options = {}
         super().__init__(options)

     def __missing__(self, key):
         raise pywikibot.Error("'{}' is not a valid option for {}."
-                              .format(key, self.classname))
-    __getattr__ = dict.__getitem__
+                              .format(key, self._classname))
+
+    def __getattr__(self, name):
+        """Get item from dict."""
+        return self.__getitem__(name)
+
+    def __setattr__(self, name, value):
+        """Set item or attribute."""
+        if name not in ('_classname', '_options'):
+            self.__setitem__(name, value)
+        else:
+            super().__setattr__(name, value)
+
+    def __getitem__(self, key):
+        """Update options fro backward compatibility and get item."""
+        self.update_options()
+        return super().__getitem__(key)
+
+    def __setitem__(self, key, value):
+        """Set item in dict and deprecated option dict."""
+        if key in self._options:
+            self._options[key] = value
+        super().__setitem__(key, value)
+
+    def update_options(self):
+        """Update dict from deprecated options for backward compatibility."""
+        self.update(self._options)


 _DEPRECATION_MSG = 'Optionhandler.opt.option attribute ' \
@@ -1046,15 +1072,24 @@

     def set_options(self, **options):
         """Set the instance options."""
+        warning = 'pywikibot.bot.OptionHandler.availableOptions'
+        with suppress_warnings(warning.replace('.', r'\.') + ' is deprecated',
+                               category=DeprecationWarning):
+            old_options = self.availableOptions is not self.available_options
+        if old_options:  # old options were set and not updated
+            self.available_options = self.availableOptions
+            issue_deprecation_warning(warning, 'available_options',
+                                      since='20201006')
+
         valid_options = set(self.available_options)
         received_options = set(options)

         # self.opt contains all available options including defaults
         self.opt = _OptionDict(self.__class__.__name__, self.available_options)
         # self.options contains the options overridden from defaults
-        self._options = {opt: options[opt]
-                         for opt in received_options & valid_options}
-        self.opt.update(self._options)
+        self.opt._options = {opt: options[opt]
+                             for opt in received_options & valid_options}
+        self.opt.update(self.opt._options)

         for opt in received_options - valid_options:
             pywikibot.warning('{} is not a valid option. It was ignored.'
@@ -1074,7 +1109,7 @@
     @deprecated(_DEPRECATION_MSG, since='20201006', future_warning=True)
     def options(self):
         """DEPRECATED. Return changed options."""
-        return self._options
+        return self.opt._options

     @options.setter
     @deprecated(_DEPRECATION_MSG, since='20201006', future_warning=True)
@@ -1082,6 +1117,13 @@
         """DEPRECATED. Return changed options."""
         self.set_options(**options)

+    def __getattribute__(self, name):
+        """Update options for backward compatibility of options property."""
+        attr = super().__getattribute__(name)
+        if name == 'opt':
+            attr.update_options()
+        return attr
+

 class BaseBot(OptionHandler):

diff --git a/scripts/redirect.py b/scripts/redirect.py
index de3c865..41e8a18 100755
--- a/scripts/redirect.py
+++ b/scripts/redirect.py
@@ -97,7 +97,7 @@

     """Redirect generator."""

-    availableOptions = {
+    available_options = {
         'fullscan': False,
         'moves': False,
         'namespaces': {0},
@@ -113,15 +113,6 @@
         """Initializer."""
         super().__init__(**kwargs)
         self.site = pywikibot.Site()
-        self.use_api = self.getOption('fullscan')
-        self.use_move_log = self.getOption('moves')
-        self.namespaces = self.getOption('namespaces')
-        self.offset = self.getOption('offset')
-        self.page_title = self.getOption('page')
-        self.api_start = self.getOption('start')
-        self.api_number = self.getOption('limit')
-        self.api_until = self.getOption('until')
-        self.xmlFilename = self.getOption('xml')

         # connect the generator selected by 'action' parameter
         cls = self.__class__
@@ -142,7 +133,7 @@
         a dictionary where the redirect names are the keys and the redirect
         targets are the values.
         """
-        xmlFilename = self.xmlFilename
+        xmlFilename = self.opt.xml
         redict = {}
         # open xml dump and read page titles out of it
         dump = xmlreader.XmlDump(xmlFilename)
@@ -154,9 +145,9 @@
             # always print status message after 10000 pages
             if readPagesCount % 10000 == 0:
                 pywikibot.output('{0} pages read...'.format(readPagesCount))
-            if len(self.namespaces) > 0:
+            if self.opt.namespaces:
                 if pywikibot.Page(self.site, entry.title).namespace() \
-                        not in self.namespaces:
+                        not in self.opt.namespaces:
                     continue
             if alsoGetPageTitles:
                 pageTitles.add(space_to_underscore(pywikibot.Link(entry.title,
@@ -196,15 +187,15 @@
     def get_redirect_pages_via_api(self) -> Generator[pywikibot.Page, None,
                                                       None]:
         """Yield Pages that are redirects."""
-        for ns in self.namespaces:
-            gen = self.site.allpages(start=self.api_start,
+        for ns in self.opt.namespaces:
+            gen = self.site.allpages(start=self.opt.start,
                                      namespace=ns,
                                      filterredir=True)
-            if self.api_number:
-                gen.set_maximum_items(self.api_number)
+            if self.opt.limit:
+                gen.set_maximum_items(self.opt.limit)
             for p in gen:
-                done = (self.api_until
-                        and p.title(with_ns=False) >= self.api_until)
+                done = (self.opt.until
+                        and p.title(with_ns=False) >= self.opt.until)
                 if done:
                     return
                 yield p
@@ -287,17 +278,17 @@
     def retrieve_broken_redirects(self) -> Generator[
             Union[str, pywikibot.Page], None, None]:
         """Retrieve broken redirects."""
-        if self.use_api:
+        if self.opt.fullscan:
             count = 0
             for (pagetitle, type, target, final) \
                     in self.get_redirects_via_api(maxlen=2):
                 if type == 0:
                     yield pagetitle
-                    if self.api_number:
+                    if self.opt.limit:
                         count += 1
-                        if count >= self.api_number:
+                        if count >= self.opt.limit:
                             break
-        elif self.xmlFilename:
+        elif self.opt.xml:
             # retrieve information from XML dump
             pywikibot.output(
                 'Getting a list of all redirects and of all page titles...')
@@ -306,8 +297,8 @@
             for (key, value) in redirs.items():
                 if value not in pageTitles:
                     yield key
-        elif self.page_title:
-            yield self.page_title
+        elif self.opt.page:
+            yield self.opt.page
         else:
             pywikibot.output('Retrieving broken redirect special page...')
             yield from self.site.preloadpages(self.site.broken_redirects())
@@ -315,30 +306,30 @@
     def retrieve_double_redirects(self) -> Generator[
             Union[str, pywikibot.Page], None, None]:
         """Retrieve double redirects."""
-        if self.use_move_log:
+        if self.opt.moves:
             yield from self.get_moved_pages_redirects()
-        elif self.use_api:
+        elif self.opt.fullscan:
             count = 0
             for (pagetitle, type, target, final) \
                     in self.get_redirects_via_api(maxlen=2):
                 if type != 0 and type != 1:
                     yield pagetitle
-                    if self.api_number:
+                    if self.opt.limit:
                         count += 1
-                        if count >= self.api_number:
+                        if count >= self.opt.limit:
                             break
-        elif self.xmlFilename:
+        elif self.opt.xml:
             redict, _ = self.get_redirects_from_dump()
             total = len(redict)
             for num, (key, value) in enumerate(redict.items(), start=1):
                 # check if the value - that is, the redirect target - is a
                 # redirect as well
-                if num > self.offset and value in redict:
+                if num > self.opt.offset and value in redict:
                     pywikibot.output('\nChecking redirect {0} of {1}...'
                                      .format(num, total))
                     yield key
-        elif self.page_title:
-            yield self.page_title
+        elif self.opt.page:
+            yield self.opt.page
         else:
             pywikibot.output('Retrieving double redirect special page...')
             yield from self.site.preloadpages(self.site.double_redirects())
@@ -347,18 +338,18 @@
                                                      None]:
         """Generate redirects to recently-moved pages."""
         # this will run forever, until user interrupts it
-        if self.offset <= 0:
-            self.offset = 1
+        if self.opt.offset <= 0:
+            self.opt.offset = 1
         start = (datetime.datetime.utcnow()
-                 - datetime.timedelta(0, self.offset * 3600))
-        # self.offset hours ago
+                 - datetime.timedelta(0, self.opt.offset * 3600))
+        # self.opt.offset hours ago
         offset_time = start.strftime('%Y%m%d%H%M%S')
         pywikibot.output('Retrieving {} moved pages...'
-                         .format(self.api_number
-                                 if self.api_number is not None else 'all'))
+                         .format(self.opt.limit
+                                 if self.opt.limit is not None else 'all'))
         move_gen = self.site.logevents(logtype='move', start=offset_time)
-        if self.api_number:
-            move_gen.set_maximum_items(self.api_number)
+        if self.opt.limit:
+            move_gen.set_maximum_items(self.opt.limit)
         pywikibot.output('.', newline=False)
         for logentry in move_gen:
             try:
@@ -389,7 +380,7 @@

     def __init__(self, action, **kwargs) -> None:
         """Initializer."""
-        self.availableOptions.update({
+        self.available_options.update({
             'limit': float('inf'),
             'delete': False,
             'sdtemplate': None,
@@ -415,8 +406,8 @@

         @return: A valid speedy deletion template.
         """
-        if self.getOption('delete') and not self.site.has_right('delete'):
-            sd = self.getOption('sdtemplate')
+        if self.opt.delete and not self.site.has_right('delete'):
+            sd = self.opt.sdtemplate
             if not sd and i18n.twhas_key(self.site,
                                          'redirect-broken-redirect-template'):
                 sd = i18n.twtranslate(self.site,
@@ -471,7 +462,7 @@
     def delete_1_broken_redirect(self) -> None:
         """Treat one broken redirect."""
         redir_page = self.current_page
-        done = not self.getOption('delete')
+        done = not self.opt.delete
         try:
             targetPage = redir_page.getRedirectTarget()
         except (pywikibot.CircularRedirect,
@@ -523,7 +514,7 @@
                         .format(targetPage.title(as_link=True),
                                 redir_page.title(as_link=True))):
                     self.delete_redirect(redir_page, 'redirect-remove-broken')
-                elif not (self.getOption('delete') or movedTarget):
+                elif not (self.opt.delete or movedTarget):
                     pywikibot.output(
                         'Cannot fix or delete the broken redirect')
             except pywikibot.IsRedirectPage:
@@ -531,7 +522,7 @@
                     'Redirect target {0} is also a redirect! {1}'.format(
                         targetPage.title(as_link=True),
                         "Won't delete anything."
-                        if self.getOption('delete') else 'Skipping.'))
+                        if self.opt.delete else 'Skipping.'))
             else:
                 # we successfully get the target page, meaning that
                 # it exists and is not a redirect: no reason to touch it.
@@ -539,7 +530,7 @@
                     'Redirect target {0} does exist! {1}'.format(
                         targetPage.title(as_link=True),
                         "Won't delete anything."
-                        if self.getOption('delete') else 'Skipping.'))
+                        if self.opt.delete else 'Skipping.'))

     def fix_1_double_redirect(self) -> None:
         """Treat one double redirect."""
@@ -575,7 +566,7 @@
                     .format(str(e)[10:]))
                 break
             except pywikibot.NoPage:
-                if self.getOption('always'):
+                if self.opt.always:
                     pywikibot.output(
                         "Skipping: Redirect target {} doesn't exist."
                         .format(newRedir.title(as_link=True)))
@@ -611,7 +602,7 @@
                         'Redirect target {0} forms a redirect loop.'
                         .format(targetPage.title(as_link=True)))
                     break  # FIXME: doesn't work. edits twice!
-                    if self.getOption('delete'):
+                    if self.opt.delete:
                         # Delete the two redirects
                         # TODO: Check whether pages aren't vandalized
                         # and (maybe) do not have a version history
@@ -656,7 +647,7 @@

     def treat(self, page) -> None:
         """Treat a page."""
-        if self._treat_counter >= self.getOption('limit'):
+        if self._treat_counter >= self.opt.limit:
             pywikibot.output('\nNumber of pages reached the limit. '
                              'Script terminated.')
             self.generator.close()
diff --git a/tests/bot_tests.py b/tests/bot_tests.py
index 6ee2b36..5772099 100644
--- a/tests/bot_tests.py
+++ b/tests/bot_tests.py
@@ -71,7 +71,7 @@
         """Set and patch the current bot."""
         assert value._save_page != self.bot_save, 'bot may not be patched.'
         self._bot = value
-        self._bot.options['always'] = True
+        self._bot.opt.always = True
         self._original = self._bot._save_page
         self._bot._save_page = self.bot_save
         self._old_counter = self._bot._save_counter
@@ -357,6 +357,55 @@
         self.bot.run()


+class Options(pywikibot.bot.OptionHandler):
+
+    """A derived OptionHandler class."""
+
+    available_options = {
+        'foo': 'bar',
+        'bar': 42,
+        'baz': False
+    }
+
+
+class TestOptionHandler(TestCase):
+
+    """OptionHandler test class."""
+
+    dry = True
+
+    def setUp(self):
+        """Setup tests."""
+        self.option_handler = Options(baz=True)
+        super().setUp()
+
+    def test_opt_values(self):
+        """Test OptionHandler."""
+        oh = self.option_handler
+        self.assertEqual(oh.opt.foo, 'bar')
+        self.assertEqual(oh.opt.bar, 42)
+        self.assertTrue(oh.opt.baz)
+        self.assertEqual(oh.opt.foo, oh.opt['foo'])
+        oh.opt.baz = 'Hey'
+        self.assertEqual(oh.opt.baz, 'Hey')
+        self.assertEqual(oh.opt['baz'], 'Hey')
+        self.assertNotIn('baz', oh.opt.__dict__)
+        with suppress_warnings(r'pywikibot\.bot\.OptionHandler\.options'):
+            self.assertEqual(oh.options['baz'], 'Hey')
+
+    def test_options(self):
+        """Test deprecated option attribute."""
+        oh = self.option_handler
+        with suppress_warnings(r'pywikibot\.bot\.OptionHandler\.options'):
+            self.assertNotIn('bar', oh.options)
+            self.assertIn('baz', oh.options)
+            self.assertTrue(oh.options['baz'])
+            self.assertEqual(oh.opt['baz'], oh.options['baz'])
+            oh.options['baz'] = False
+            self.assertFalse(oh.options['baz'])
+            self.assertFalse(oh.opt.baz)
+
+
 if __name__ == '__main__':  # pragma: no cover
     with suppress(SystemExit):
         unittest.main()

--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/632644
To unsubscribe, or for help writing mail filters, visit 
https://gerrit.wikimedia.org/r/settings

Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: I82f1125ffcc7adf758ef5e1fa23b7f029b2eb688
Gerrit-Change-Number: 632644
Gerrit-PatchSet: 4
Gerrit-Owner: Xqt <[email protected]>
Gerrit-Reviewer: D3r1ck01 <[email protected]>
Gerrit-Reviewer: Meno25 <[email protected]>
Gerrit-Reviewer: Xqt <[email protected]>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged
_______________________________________________
Pywikibot-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/pywikibot-commits

Reply via email to