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