jenkins-bot has submitted this change. (
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1323821?usp=email )
Change subject: replace: Reduce complexity of main
......................................................................
replace: Reduce complexity of main
Split argument parsing, replacement and fix setup, summary handling,
compilation, and generator construction into focused helpers. Add offline
regression coverage for the extracted behavior.
Bug: T402153
Change-Id: I40adc89d1fb8edbc185561ce6e8d11cab03e9917
---
M scripts/replace.py
M tests/replacebot_tests.py
2 files changed, 519 insertions(+), 215 deletions(-)
Approvals:
Xqt: Looks good to me, approved
jenkins-bot: Verified
diff --git a/scripts/replace.py b/scripts/replace.py
index 02da024..c36c44b 100755
--- a/scripts/replace.py
+++ b/scripts/replace.py
@@ -170,6 +170,7 @@
import re
from collections.abc import Generator, Sequence
from contextlib import suppress
+from dataclasses import dataclass
from pathlib import Path
from typing import Any
@@ -816,7 +817,41 @@
"""
-def handle_exceptions(*args: str) -> tuple[list[str], dict[str, str]]:
+@dataclass
+class _ReplaceConfig:
+
+ """Configuration parsed from the command line."""
+
+ bot_options: dict[str, Any]
+ replacement_args: list[str]
+ fix_names: list[str]
+ exceptions: dict[str, list[str]]
+ edit_summary: str | bool
+ preload: bool
+ regex: bool
+ flags: int
+ xml_filename: str | None
+ xml_start: str | None
+ sql_query: str | None
+
+
+_SCRIPT_OPTION_VALUES = {
+ '-regex': ('regex', True),
+ '-manualinput': ('manual_input', True),
+ '-nopreload': ('preload', False),
+}
+_BOT_VALUE_OPTIONS = {
+ '-sleep': ('sleep', float),
+ '-addcat': ('addcat', str),
+}
+_REGEX_FLAGS = {
+ '-nocase': re.IGNORECASE,
+ '-dotall': re.DOTALL,
+ '-multiline': re.MULTILINE,
+}
+
+
+def handle_exceptions(*args: str) -> tuple[list[str], dict[str, list[str]]]:
"""Handle exceptions args to ignore pages which contain certain texts.
.. version-added:: 7.0
@@ -909,6 +944,312 @@
return pagegenerators.MySQLPageGenerator(sql)
+def _parse_args(
+ args: Sequence[str],
+ generator_factory: pagegenerators.GeneratorFactory,
+) -> _ReplaceConfig | None:
+ """Parse command-line arguments for the replace script."""
+ bot_options = {}
+ edit_summary = ''
+ replacement_args = []
+ file_replacements: list[str] | None = []
+ fix_names = []
+ sql_query: str | None = None
+ flags = 0
+ script_options = {
+ 'regex': False,
+ 'manual_input': False,
+ 'preload': True,
+ }
+ xml_options = {
+ '-xml': (
+ 'xml_filename', i18n.input, 'pywikibot-enter-xml-filename'),
+ '-xmlstart': (
+ 'xml_start', pywikibot.input,
+ 'Please enter the dumped article to start with:'),
+ }
+ xml_values: dict[str, str | None] = {
+ 'xml_filename': None,
+ 'xml_start': None,
+ }
+
+ local_args = pywikibot.handle_args(args)
+ local_args = generator_factory.handle_args(local_args)
+ local_args, exceptions = handle_exceptions(*local_args)
+
+ for arg in local_args:
+ option, _, value = arg.partition(':')
+ if option in _SCRIPT_OPTION_VALUES:
+ name, option_value = _SCRIPT_OPTION_VALUES[option]
+ script_options[name] = option_value
+ elif option in xml_options:
+ name, input_function, prompt = xml_options[option]
+ xml_values[name] = value or input_function(prompt)
+ elif option == '-mysqlquery':
+ sql_query = value
+ elif option == '-fix':
+ fix_names.append(value)
+ elif option in _BOT_VALUE_OPTIONS:
+ name, converter = _BOT_VALUE_OPTIONS[option]
+ bot_options[name] = converter(value)
+ elif option in ('-allowoverlap', '-always', '-quiet', '-recursive'):
+ bot_options[option[1:]] = True
+ elif option in _REGEX_FLAGS:
+ flags |= _REGEX_FLAGS[option]
+ elif option == '-summary':
+ edit_summary = value
+ elif option == '-automaticsummary':
+ edit_summary = True
+ elif option == '-pairsfile':
+ file_replacements = handle_pairsfile(value)
+ else:
+ replacement_args.append(arg)
+
+ if file_replacements is None:
+ return None
+
+ if len(replacement_args) % 2:
+ pywikibot.error('Incomplete command line pattern replacement pair:\n'
+ f'{replacement_args}')
+ return None
+
+ replacement_args += file_replacements
+ if (not (replacement_args or fix_names)
+ or script_options['manual_input']):
+ replacement_args += handle_manual()
+
+ return _ReplaceConfig(
+ bot_options=bot_options,
+ replacement_args=replacement_args,
+ fix_names=fix_names,
+ exceptions=exceptions,
+ edit_summary=edit_summary,
+ preload=script_options['preload'],
+ regex=script_options['regex'],
+ flags=flags,
+ xml_filename=xml_values['xml_filename'],
+ xml_start=xml_values['xml_start'],
+ sql_query=sql_query,
+ )
+
+
+def _build_commandline_replacements(
+ replacement_args: list[str],
+ site,
+ edit_summary: str | bool,
+) -> tuple[list[Replacement], str | None]:
+ """Create replacements and an example summary from command-line pairs."""
+ replacements = []
+ single_summary = None
+ automatic_summary = not edit_summary or edit_summary is True
+
+ for old, new in batched(replacement_args, 2):
+ replacement = Replacement(old, new)
+ if automatic_summary and not single_summary:
+ single_summary = i18n.twtranslate(
+ site,
+ 'replace-replacing',
+ {'description': f' (-{replacement.old} +{replacement.new})'}
+ )
+ replacements.append(replacement)
+
+ return replacements, single_summary
+
+
+def _get_fix(fix_name: str) -> dict[str, Any] | None:
+ """Return a validated predefined fix or ``None`` on error."""
+ try:
+ fix = fixes.fixes[fix_name]
+ except KeyError:
+ pywikibot.info('Available predefined fixes are: {}'
+ .format(', '.join(fixes.fixes.keys())))
+ if not fixes.user_fixes_loaded:
+ pywikibot.info(f'The user fixes file could not be found: '
+ f'{fixes.filename}')
+ return None
+
+ if not isinstance(fix, dict):
+ pywikibot.error(
+ f'fixes[{fix_name!r}] is a {type(fix).__name__}, not a dict')
+ if type(fix) is tuple:
+ pywikibot.info('Maybe a trailing comma in your user-fixes.py?')
+ pywikibot.debug(fix)
+ return None
+
+ return fix
+
+
+def _get_fix_summary(fix: dict[str, Any], site) -> str | None:
+ """Return the translated summary for a predefined fix."""
+ if 'msg' not in fix:
+ return None
+ if isinstance(fix['msg'], str):
+ return i18n.twtranslate(site, str(fix['msg']))
+ return i18n.translate(site, fix['msg'], fallback=True)
+
+
+def _build_fix_set(
+ fix_name: str,
+ fix: dict[str, Any],
+ set_summary: str | None,
+) -> tuple[ReplacementList, list[str]]:
+ """Create a replacement set and collect its missing summaries."""
+ replacement_set = ReplacementList(
+ fix.get('regex'),
+ fix.get('exceptions'),
+ fix.get('nocase'),
+ set_summary,
+ name=fix_name,
+ )
+ missing_summaries = []
+
+ for index, replacement in enumerate(fix['replacements'], start=1):
+ summary = None if len(replacement) < 3 else replacement[2]
+ if not set_summary and not summary:
+ missing_summaries.append(
+ f'"{fix_name}" (replacement #{index})')
+ if chars.contains_invisible(replacement[0]):
+ pywikibot.warning(
+ 'The old string '
+ f'"{chars.replace_invisible(replacement[0])}"'
+ ' contains formatting characters like U+200E'
+ )
+ if (not callable(replacement[1])
+ and chars.contains_invisible(replacement[1])):
+ pywikibot.warning(
+ 'The new string '
+ f'"{chars.replace_invisible(replacement[1])}"'
+ ' contains formatting characters like U+200E')
+ replacement_set.append(ReplacementListEntry(
+ old=replacement[0],
+ new=replacement[1],
+ fix_set=replacement_set,
+ edit_summary=summary,
+ ))
+
+ return replacement_set, missing_summaries
+
+
+def _merge_exceptions(
+ replacement_set: ReplacementList,
+ exceptions: dict[str, list[str]],
+) -> None:
+ """Merge exceptions from a predefined fix into script exceptions."""
+ if replacement_set._exceptions is None:
+ return
+
+ for key, values in replacement_set._exceptions.items():
+ if key in exceptions:
+ exceptions[key] = list(set(exceptions[key]) | set(values))
+ else:
+ exceptions[key] = values
+
+
+def _load_fixes(
+ fix_names: list[str],
+ site,
+ generator_factory: pagegenerators.GeneratorFactory,
+ exceptions: dict[str, list[str]],
+) -> tuple[list[ReplacementListEntry], list[str]] | None:
+ """Load predefined fixes and collect their missing summaries."""
+ replacements = []
+ missing_summaries = []
+ generators_given = bool(generator_factory.gens)
+
+ for fix_name in fix_names:
+ fix = _get_fix(fix_name)
+ if fix is None:
+ return None
+ if not fix['replacements']:
+ pywikibot.warning(f'No replacements defined for fix {fix_name!r}')
+ continue
+
+ set_summary = _get_fix_summary(fix, site)
+ if not generators_given and 'generator' in fix:
+ generator_args = fix['generator']
+ if isinstance(generator_args, str):
+ generator_factory.handle_arg(generator_args)
+ else:
+ generator_factory.handle_args(generator_args)
+
+ replacement_set, missing_fix_summaries = _build_fix_set(
+ fix_name, fix, set_summary)
+ if replacement_set:
+ replacements.extend(replacement_set)
+ _merge_exceptions(replacement_set, exceptions)
+
+ if len(fix['replacements']) == len(missing_fix_summaries):
+ missing_summaries.append(f'"{fix_name}" (all replacements)')
+ else:
+ missing_summaries += missing_fix_summaries
+
+ return replacements, missing_summaries
+
+
+def _resolve_summary(
+ edit_summary: str | bool,
+ single_summary: str | None,
+ missing_fix_summaries: list[str],
+) -> str | bool:
+ """Display summary information and return the edit summary to use."""
+ if ((not edit_summary or edit_summary is True)
+ and (missing_fix_summaries or single_summary)):
+ if single_summary:
+ pywikibot.info('The summary message for the command line '
+ 'replacements will be something like: '
+ + single_summary)
+ if missing_fix_summaries:
+ pywikibot.info('The summary will not be used when the fix has '
+ 'one defined but the following fix(es) do(es) '
+ 'not have a summary defined: {}'
+ .format(', '.join(missing_fix_summaries)))
+ if edit_summary is not True:
+ return pywikibot.input(
+ 'Press Enter to use this automatic message, or enter a '
+ 'description of the\nchanges your bot will make:')
+ return ''
+
+ return edit_summary
+
+
+def _compile_replacements(
+ replacements: list[ReplacementBase],
+ exceptions: dict[str, Any],
+ regex: bool,
+ flags: int,
+) -> None:
+ """Compile replacements and exceptions before generator creation."""
+ for replacement in replacements:
+ replacement.compile(regex, flags)
+ precompile_exceptions(exceptions, regex, flags)
+
+
+def _build_generator(
+ generator_factory: pagegenerators.GeneratorFactory,
+ replacements: list[ReplacementBase],
+ exceptions: dict[str, Any],
+ site,
+ *,
+ xml_filename: str | None,
+ xml_start: str | None,
+ sql_query: str | None,
+ preload: bool,
+):
+ """Create and combine the configured page generators."""
+ generator = None
+ if xml_filename:
+ generator = XmlDumpReplacePageGenerator(
+ xml_filename, xml_start, replacements, exceptions, site)
+ elif sql_query is not None:
+ # Only -excepttext option is considered by the query. Other
+ # exceptions are taken into account by the ReplaceRobot.
+ generator = handle_sql(
+ sql_query, replacements, exceptions['text-contains'])
+
+ return generator_factory.getCombinedGenerator(
+ generator, preload=preload)
+
+
def main(*args: str) -> None:
"""Process command line arguments and invoke bot.
@@ -919,226 +1260,41 @@
:param args: command line arguments
"""
- options = {}
- gen = None
- # summary message
- edit_summary = ''
- # Array which will collect commandline parameters.
- # First element is original text, second element is replacement text.
- preload = True # preload pages
- commandline_replacements = []
- file_replacements = []
- # A list of 2-tuples of original text and replacement text.
- replacements = []
-
- # Should the elements of 'replacements' and 'exceptions' be interpreted
- # as regular expressions?
- regex = False
- # Predefined fixes from dictionary 'fixes' (see above).
- fixes_set = []
- # the dump's path, either absolute or relative, which will be used
- # if -xml flag is present
- xmlFilename = None
- xmlStart = None
- sql_query: str | None = None
- # Set the default regular expression flags
- flags = 0
- # Request manual replacements even if replacements are already defined
- manual_input = False
-
- # Read commandline parameters.
- genFactory = pagegenerators.GeneratorFactory(
+ generator_factory = pagegenerators.GeneratorFactory(
disabled_options=['mysqlquery'])
- local_args = pywikibot.handle_args(args)
- local_args = genFactory.handle_args(local_args)
- local_args, exceptions = handle_exceptions(*local_args)
-
- for arg in local_args:
- opt, _, value = arg.partition(':')
- if opt == '-regex':
- regex = True
- elif opt == '-xmlstart':
- xmlStart = value or pywikibot.input(
- 'Please enter the dumped article to start with:')
- elif opt == '-xml':
- xmlFilename = value or i18n.input('pywikibot-enter-xml-filename')
- elif opt == '-mysqlquery':
- sql_query = value
- elif opt == '-fix':
- fixes_set.append(value)
- elif opt == '-sleep':
- options['sleep'] = float(value)
- elif opt in ('-allowoverlap', '-always', '-quiet', '-recursive'):
- options[opt[1:]] = True
- elif opt == '-nocase':
- flags |= re.IGNORECASE
- elif opt == '-dotall':
- flags |= re.DOTALL
- elif opt == '-multiline':
- flags |= re.MULTILINE
- elif opt == '-addcat':
- options['addcat'] = value
- elif opt == '-summary':
- edit_summary = value
- elif opt == '-automaticsummary':
- edit_summary = True
- elif opt == '-manualinput':
- manual_input = True
- elif opt == '-pairsfile':
- file_replacements = handle_pairsfile(value)
- elif opt == '-nopreload':
- preload = False
- else:
- commandline_replacements.append(arg)
-
- if file_replacements is None:
+ config = _parse_args(args, generator_factory)
+ if config is None:
return
- if len(commandline_replacements) % 2:
- pywikibot.error('Incomplete command line pattern replacement pair:\n'
- f'{commandline_replacements}')
- return
-
- commandline_replacements += file_replacements
- if not (commandline_replacements or fixes_set) or manual_input:
- commandline_replacements += handle_manual()
-
- # The summary stored here won't be actually used but is only an example
site = pywikibot.Site()
- single_summary = (
- 'Not needed' if edit_summary and edit_summary is not True else None
+ replacements, single_summary = _build_commandline_replacements(
+ config.replacement_args, site, config.edit_summary)
+
+ fix_result = _load_fixes(
+ config.fix_names, site, generator_factory, config.exceptions)
+ if fix_result is None:
+ return
+ fix_replacements, missing_fix_summaries = fix_result
+ replacements.extend(fix_replacements)
+
+ edit_summary = _resolve_summary(
+ config.edit_summary, single_summary, missing_fix_summaries)
+ _compile_replacements(
+ replacements, config.exceptions, config.regex, config.flags)
+ generator = _build_generator(
+ generator_factory,
+ replacements,
+ config.exceptions,
+ site,
+ xml_filename=config.xml_filename,
+ xml_start=config.xml_start,
+ sql_query=config.sql_query,
+ preload=config.preload,
)
- for old, new in batched(commandline_replacements, 2):
- replacement = Replacement(old, new)
- if not single_summary:
- single_summary = i18n.twtranslate(
- site,
- 'replace-replacing',
- {'description': f' (-{replacement.old} +{replacement.new})'}
- )
- replacements.append(replacement)
- # Perform one of the predefined actions.
- missing_fixes_summaries = [] # which a fixes/replacements miss a summary
- generators_given = bool(genFactory.gens)
- for fix_name in fixes_set:
- try:
- fix = fixes.fixes[fix_name]
- except KeyError:
- pywikibot.info('Available predefined fixes are: {}'
- .format(', '.join(fixes.fixes.keys())))
- if not fixes.user_fixes_loaded:
- pywikibot.info(f'The user fixes file could not be found: '
- f'{fixes.filename}')
- return
-
- if not isinstance(fix, dict):
- pywikibot.error(
- f'fixes[{fix_name!r}] is a {type(fix).__name__}, not a dict')
- if type(fix) is tuple:
- pywikibot.info('Maybe a trailing comma in your user-fixes.py?')
- pywikibot.debug(fix)
- return
-
- if not fix['replacements']:
- pywikibot.warning(f'No replacements defined for fix {fix_name!r}')
- continue
- if 'msg' in fix:
- if isinstance(fix['msg'], str):
- set_summary = i18n.twtranslate(site, str(fix['msg']))
- else:
- set_summary = i18n.translate(site, fix['msg'], fallback=True)
- else:
- set_summary = None
- if not generators_given and 'generator' in fix:
- gen_args = fix['generator']
- if isinstance(gen_args, str):
- genFactory.handle_arg(gen_args)
- else:
- genFactory.handle_args(gen_args)
- replacement_set = ReplacementList(fix.get('regex'),
- fix.get('exceptions'),
- fix.get('nocase'),
- set_summary,
- name=fix_name)
- # Whether some replacements have a summary, if so only show which
- # have none, otherwise just mention the complete fix
- missing_fix_summaries = []
- for index, replacement in enumerate(fix['replacements'], start=1):
- summary = None if len(replacement) < 3 else replacement[2]
- if not set_summary and not summary:
- missing_fix_summaries.append(
- f'"{fix_name}" (replacement #{index})')
- if chars.contains_invisible(replacement[0]):
- pywikibot.warning(
- 'The old string '
- f'"{chars.replace_invisible(replacement[0])}"'
- ' contains formatting characters like U+200E'
- )
- if (not callable(replacement[1])
- and chars.contains_invisible(replacement[1])):
- pywikibot.warning(
- 'The new string '
- f'"{chars.replace_invisible(replacement[1])}"'
- ' contains formatting characters like U+200E')
- replacement_set.append(ReplacementListEntry(
- old=replacement[0],
- new=replacement[1],
- fix_set=replacement_set,
- edit_summary=summary,
- ))
-
- # Exceptions specified via 'fix' shall be merged to those via CLI.
- if replacement_set:
- replacements.extend(replacement_set)
- if replacement_set._exceptions is not None:
- for k, v in replacement_set._exceptions.items():
- if k in exceptions:
- exceptions[k] = list(set(exceptions[k]) | set(v))
- else:
- exceptions[k] = v
-
- if len(fix['replacements']) == len(missing_fix_summaries):
- missing_fixes_summaries.append(
- f'"{fix_name}" (all replacements)')
- else:
- missing_fixes_summaries += missing_fix_summaries
-
- if ((not edit_summary or edit_summary is True)
- and (missing_fixes_summaries or single_summary)):
- if single_summary:
- pywikibot.info('The summary message for the command line '
- 'replacements will be something like: '
- + single_summary)
- if missing_fixes_summaries:
- pywikibot.info('The summary will not be used when the fix has '
- 'one defined but the following fix(es) do(es) '
- 'not have a summary defined: {}'
- .format(', '.join(missing_fixes_summaries)))
- if edit_summary is not True:
- edit_summary = pywikibot.input(
- 'Press Enter to use this automatic message, or enter a '
- 'description of the\nchanges your bot will make:')
- else:
- edit_summary = ''
-
- # Pre-compile all regular expressions here to save time later
- for replacement in replacements:
- replacement.compile(regex, flags)
-
- precompile_exceptions(exceptions, regex, flags)
-
- if xmlFilename:
- gen = XmlDumpReplacePageGenerator(xmlFilename, xmlStart,
- replacements, exceptions, site)
- elif sql_query is not None:
- # Only -excepttext option is considered by the query. Other
- # exceptions are taken into account by the ReplaceRobot
- gen = handle_sql(sql_query, replacements, exceptions['text-contains'])
-
- gen = genFactory.getCombinedGenerator(gen, preload=preload)
- bot = ReplaceRobot(gen, replacements, exceptions, site=site,
- summary=edit_summary, **options)
+ bot = ReplaceRobot(
+ generator, replacements, config.exceptions, site=site,
+ summary=edit_summary, **config.bot_options)
site.login()
bot.run()
diff --git a/tests/replacebot_tests.py b/tests/replacebot_tests.py
index 4550d1d..49b23c7 100755
--- a/tests/replacebot_tests.py
+++ b/tests/replacebot_tests.py
@@ -7,13 +7,16 @@
"""Tests for the replace script and ReplaceRobot class."""
from __future__ import annotations
+import re
import unittest
from contextlib import suppress
+from unittest.mock import MagicMock, call, patch
import pywikibot
from pywikibot import fixes
from scripts import replace
from tests import join_data_path
+from tests.aspects import TestCase
from tests.bot_tests import TWNBotTestCase
from tests.utils import empty_sites
@@ -303,6 +306,151 @@
'"no-msg-callable" (all replacements)',
], pywikibot.bot.ui.pop_output())
+
+class TestReplaceHelpers(TestCase):
+
+ """Test helpers used by :func:`replace.main`."""
+
+ net = False
+
+ def setUp(self) -> None:
+ """Clear output from previous helper tests."""
+ super().setUp()
+ pywikibot.bot.ui.clear()
+
+ @patch.object(replace.pywikibot, 'handle_args',
+ side_effect=lambda args: args)
+ def test_parse_args(self, handle_args) -> None:
+ """Test parsing script-specific arguments."""
+ generator_factory = MagicMock()
+ generator_factory.handle_args.side_effect = lambda args: args
+
+ options = replace._parse_args((
+ '-regex', '-nocase', '-dotall', '-multiline', '-sleep:1.5',
+ '-always', '-quiet', '-recursive', '-allowoverlap',
+ '-addcat:Test', '-summary:summary', '-nopreload',
+ '-xml:dump.xml', '-xmlstart:Start', '-mysqlquery:query',
+ '-fix:no-msg', '-excepttitle:Skip', '1', '2',
+ ), generator_factory)
+
+ self.assertIsNotNone(options)
+ self.assertEqual(options.bot_options, {
+ 'sleep': 1.5,
+ 'always': True,
+ 'quiet': True,
+ 'recursive': True,
+ 'allowoverlap': True,
+ 'addcat': 'Test',
+ })
+ self.assertEqual(options.replacement_args, ['1', '2'])
+ self.assertEqual(options.fix_names, ['no-msg'])
+ self.assertEqual(options.exceptions['title'], ['Skip'])
+ self.assertEqual(options.edit_summary, 'summary')
+ self.assertFalse(options.preload)
+ self.assertTrue(options.regex)
+ self.assertEqual(
+ options.flags, re.IGNORECASE | re.DOTALL | re.MULTILINE)
+ self.assertEqual(options.xml_filename, 'dump.xml')
+ self.assertEqual(options.xml_start, 'Start')
+ self.assertEqual(options.sql_query, 'query')
+ handle_args.assert_called_once()
+
+ def test_manual_summary_skips_translation(self) -> None:
+ """Test that an explicit summary does not load i18n messages."""
+ with patch.object(replace.i18n, 'twtranslate') as twtranslate:
+ replacements, summary = replace._build_commandline_replacements(
+ ['1', '2'], MagicMock(), 'summary')
+
+ self.assertLength(replacements, 1)
+ self.assertIsNone(summary)
+ twtranslate.assert_not_called()
+
+ def test_fix_exceptions_are_merged(self) -> None:
+ """Test merging command-line and fix exceptions."""
+ exceptions = {key: [] for key in replace.EXC_KEYS.values()}
+ exceptions['title'].append('Command line')
+ generator_factory = MagicMock()
+ generator_factory.gens = []
+
+ result = replace._load_fixes(
+ ['no-msg-title-exceptions'], MagicMock(), generator_factory,
+ exceptions)
+
+ self.assertIsNotNone(result)
+ replacements, missing_summaries = result
+ self.assertLength(replacements, 1)
+ self.assertEqual(set(exceptions['title']),
+ {'Command line', 'Declined'})
+ self.assertEqual(exceptions['require-title'], ['Allowed'])
+ self.assertEqual(missing_summaries, [
+ '"no-msg-title-exceptions" (all replacements)',
+ ])
+
+ def test_fix_generator_precedence(self) -> None:
+ """Test that a command-line generator suppresses fix generators."""
+ fix = {'generator': '-page:Fix', 'replacements': [('1', '2')]}
+ exceptions = {key: [] for key in replace.EXC_KEYS.values()}
+
+ with patch.dict(fixes.fixes, {'with-generator': fix}):
+ generator_factory = MagicMock()
+ generator_factory.gens = [object()]
+ replace._load_fixes(
+ ['with-generator'], MagicMock(), generator_factory,
+ exceptions)
+ generator_factory.handle_arg.assert_not_called()
+
+ generator_factory = MagicMock()
+ generator_factory.gens = []
+ replace._load_fixes(
+ ['with-generator'], MagicMock(), generator_factory,
+ exceptions)
+ generator_factory.handle_arg.assert_called_once_with('-page:Fix')
+
+ def test_build_generator(self) -> None:
+ """Test XML, SQL, and combined generator construction."""
+ generator_factory = MagicMock()
+ generator_factory.getCombinedGenerator.side_effect = (
+ lambda generator, preload: generator)
+ replacements = [MagicMock()]
+ exceptions = {key: [] for key in replace.EXC_KEYS.values()}
+ xml_generator = object()
+ sql_generator = object()
+
+ with (
+ patch.object(replace, 'XmlDumpReplacePageGenerator',
+ return_value=xml_generator) as xml,
+ patch.object(replace, 'handle_sql',
+ return_value=sql_generator) as sql,
+ ):
+ result = replace._build_generator(
+ generator_factory, replacements, exceptions, MagicMock(),
+ xml_filename='dump.xml', xml_start='Start',
+ sql_query='query', preload=False)
+ self.assertIs(result, xml_generator)
+ xml.assert_called_once()
+ sql.assert_not_called()
+
+ result = replace._build_generator(
+ generator_factory, replacements, exceptions, MagicMock(),
+ xml_filename=None, xml_start=None, sql_query='query',
+ preload=True)
+ self.assertIs(result, sql_generator)
+ sql.assert_called_once_with(
+ 'query', replacements, exceptions['text-contains'])
+
+ result = replace._build_generator(
+ generator_factory, replacements, exceptions, MagicMock(),
+ xml_filename=None, xml_start=None, sql_query=None,
+ preload=True)
+ self.assertIsNone(result)
+
+ self.assertEqual(
+ generator_factory.getCombinedGenerator.call_args_list, [
+ call(xml_generator, preload=False),
+ call(sql_generator, preload=True),
+ call(None, preload=True),
+ ])
+
def test_pairs_file(self) -> None:
"""Test handle_pairsfile."""
result = replace.handle_pairsfile('non existing file')
--
To view, visit
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1323821?usp=email
To unsubscribe, or for help writing mail filters, visit
https://gerrit.wikimedia.org/r/settings?usp=email
Gerrit-MessageType: merged
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: I40adc89d1fb8edbc185561ce6e8d11cab03e9917
Gerrit-Change-Number: 1323821
Gerrit-PatchSet: 2
Gerrit-Owner: Mahveotm <[email protected]>
Gerrit-Reviewer: Xqt <[email protected]>
Gerrit-Reviewer: jenkins-bot
_______________________________________________
Pywikibot-commits mailing list -- [email protected]
To unsubscribe send an email to [email protected]