jenkins-bot has submitted this change. ( 
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1326256?usp=email )

Change subject: category: Extract bot construction
......................................................................

category: Extract bot construction

Move action-specific bot setup and dispatch into focused helpers while
preserving category option routing.

Bug: T434526
Change-Id: Ie768fe5ce95731f7dc9dd409f8aba73b108afc4e
---
M scripts/category.py
M tests/category_bot_tests.py
2 files changed, 384 insertions(+), 103 deletions(-)

Approvals:
  Xqt: Looks good to me, approved
  jenkins-bot: Verified




diff --git a/scripts/category.py b/scripts/category.py
index 8878707..ac9d451 100755
--- a/scripts/category.py
+++ b/scripts/category.py
@@ -169,6 +169,7 @@
 from itertools import chain
 from operator import methodcaller
 from textwrap import fill
+from typing import Union

 import pywikibot
 from pywikibot import config, i18n, pagegenerators, textlib
@@ -1669,6 +1670,176 @@
     return generator_factory


+def _build_add_bot(
+    settings: _CategorySettings,
+    generator_factory: pagegenerators.GeneratorFactory,
+) -> CategoryAddBot:
+    """Build a bot for the add action."""
+    generator = generator_factory.getCombinedGenerator(preload=True)
+    if not generator:
+        # default for backwards compatibility
+        generator_factory.handle_arg('-links')
+        generator = generator_factory.getCombinedGenerator(preload=True)
+    return CategoryAddBot(
+        generator,
+        newcat=settings.options.get('to'),
+        sort_by_last_name=settings.sort_by_last_name,
+        create=settings.create_pages,
+        comment=settings.summary,
+        follow_redirects=settings.follow_redirects,
+    )
+
+
+def _build_remove_bot(
+    settings: _CategorySettings,
+    generator_factory: pagegenerators.GeneratorFactory,
+) -> CategoryMoveRobot:
+    """Build a bot for the remove action."""
+    if 'from' not in settings.options:
+        settings.options['from'] = pywikibot.input(
+            'Please enter the name of the category that should be removed:')
+    generator = generator_factory.getCombinedGenerator()
+    return CategoryMoveRobot(
+        oldcat=settings.options.get('from'),
+        batch=settings.batch,
+        comment=settings.summary,
+        inplace=settings.inplace,
+        delete_oldcat=settings.delete_empty_cat,
+        title_regex=settings.title_regex,
+        history=settings.history,
+        pagesonly=settings.pagesonly,
+        deletion_comment=settings.use_deletion_summary,
+        generator=generator,
+    )
+
+
+def _resolve_move_categories(settings: _CategorySettings) -> bool:
+    """Prompt until distinct move categories are available."""
+    while True:
+        if 'from' not in settings.options:
+            settings.options['from'] = pywikibot.input(
+                'Please enter the old name of the category:')
+            if not settings.options['from']:
+                return False
+
+        if 'to' not in settings.options:
+            settings.options['to'] = pywikibot.input(
+                'Please enter the new name of the category:')
+
+        if settings.options['from'] != settings.options['to']:
+            return True
+
+        pywikibot.error('-from and -to arguments are equal, please retry.')
+        del settings.options['from']
+        del settings.options['to']
+
+
+def _build_move_bot(
+    settings: _CategorySettings,
+    generator_factory: pagegenerators.GeneratorFactory,
+) -> CategoryMoveRobot | None:
+    """Build a bot for the move action."""
+    if not _resolve_move_categories(settings):
+        return None
+
+    if settings.use_deletion_summary:
+        deletion_comment = (
+            CategoryMoveRobot.DELETION_COMMENT_SAME_AS_EDIT_COMMENT
+        )
+    else:
+        deletion_comment = CategoryMoveRobot.DELETION_COMMENT_AUTOMATIC
+
+    generator = generator_factory.getCombinedGenerator()
+    return CategoryMoveRobot(
+        oldcat=settings.options.get('from'),
+        newcat=settings.options.get('to'),
+        batch=settings.batch,
+        comment=settings.summary,
+        inplace=settings.inplace,
+        delete_oldcat=settings.delete_empty_cat,
+        title_regex=settings.title_regex,
+        history=settings.history,
+        pagesonly=settings.pagesonly,
+        deletion_comment=deletion_comment,
+        wikibase=settings.wikibase,
+        allow_split=settings.allow_split,
+        move_together=settings.move_together,
+        keep_sortkey=settings.keep_sortkey,
+        generator=generator,
+    )
+
+
+def _build_tidy_bot(
+    settings: _CategorySettings,
+    generator_factory: pagegenerators.GeneratorFactory,
+    category_database: CategoryDatabase,
+) -> CategoryTidyRobot:
+    """Build a bot for the tidy action."""
+    return CategoryTidyRobot(
+        settings.options.get('from'), category_database,
+        generator_factory.namespaces, settings.summary)
+
+
+def _build_tree_bot(
+    settings: _CategorySettings,
+    category_database: CategoryDatabase,
+) -> CategoryTreeRobot:
+    """Build a bot for the tree action."""
+    return CategoryTreeRobot(
+        settings.options.get('from'), category_database,
+        settings.options.get('to'), max_depth=settings.depth)
+
+
+def _build_listify_bot(
+    settings: _CategorySettings,
+    generator_factory: pagegenerators.GeneratorFactory,
+) -> CategoryListifyRobot:
+    """Build a bot for the listify action."""
+    return CategoryListifyRobot(
+        settings.options.get('from'), settings.options.get('to'),
+        settings.summary, settings.append, settings.overwrite,
+        settings.showimages, talk_pages=settings.talkpages,
+        recurse=settings.options.get('recurse', False),
+        namespaces=generator_factory.namespaces, **settings.options)
+
+
+def _build_clean_bot(settings: _CategorySettings) -> CleanBot:
+    """Build a bot for the clean action."""
+    return CleanBot(**settings.options)
+
+
+_CategoryBot = Union[BaseBot, CategoryListifyRobot, CategoryTreeRobot]
+
+
+def _build_bot(
+    settings: _CategorySettings,
+    generator_factory: pagegenerators.GeneratorFactory | None,
+    category_database: CategoryDatabase,
+) -> _CategoryBot | None:
+    """Build the bot selected by the category action."""
+    if settings.action == 'add':
+        assert generator_factory is not None
+        return _build_add_bot(settings, generator_factory)
+    if settings.action == 'remove':
+        assert generator_factory is not None
+        return _build_remove_bot(settings, generator_factory)
+    if settings.action == 'move':
+        assert generator_factory is not None
+        return _build_move_bot(settings, generator_factory)
+    if settings.action == 'tidy':
+        assert generator_factory is not None
+        return _build_tidy_bot(
+            settings, generator_factory, category_database)
+    if settings.action == 'tree':
+        return _build_tree_bot(settings, category_database)
+    if settings.action == 'listify':
+        assert generator_factory is not None
+        return _build_listify_bot(settings, generator_factory)
+    if settings.action == 'clean':
+        return _build_clean_bot(settings)
+    return None
+
+
 def main(*args: str) -> None:
     """Process command line arguments and invoke bot.

@@ -1681,110 +1852,19 @@
     suggest_help(unknown_parameters=settings.unknown)

     cat_db = CategoryDatabase(rebuild=settings.rebuild)
+    bot = _build_bot(settings, gen_factory, cat_db)

-    if settings.action == 'add':
-        assert gen_factory is not None
-        gen = gen_factory.getCombinedGenerator(preload=True)
-        if not gen:
-            # default for backwards compatibility
-            gen_factory.handle_arg('-links')
-            gen = gen_factory.getCombinedGenerator(preload=True)
-        bot = CategoryAddBot(gen,
-                             newcat=settings.options.get('to'),
-                             sort_by_last_name=settings.sort_by_last_name,
-                             create=settings.create_pages,
-                             comment=settings.summary,
-                             follow_redirects=settings.follow_redirects)
-    elif settings.action == 'remove':
-        assert gen_factory is not None
-        if 'from' not in settings.options:
-            settings.options['from'] = \
-                pywikibot.input('Please enter the name of the '
-                                'category that should be removed:')
-        gen = gen_factory.getCombinedGenerator()
-        bot = CategoryMoveRobot(oldcat=settings.options.get('from'),
-                                batch=settings.batch,
-                                comment=settings.summary,
-                                inplace=settings.inplace,
-                                delete_oldcat=settings.delete_empty_cat,
-                                title_regex=settings.title_regex,
-                                history=settings.history,
-                                pagesonly=settings.pagesonly,
-                                deletion_comment=(
-                                    settings.use_deletion_summary),
-                                generator=gen)
-    elif settings.action == 'move':
-        assert gen_factory is not None
-        while True:
-            if 'from' not in settings.options:
-                settings.options['from'] = pywikibot.input(
-                    'Please enter the old name of the category:')
-                if not settings.options['from']:
-                    return
+    if suggest_help(missing_action=not settings.action) or bot is None:
+        return

-            if 'to' not in settings.options:
-                settings.options['to'] = pywikibot.input(
-                    'Please enter the new name of the category:')
-
-            if settings.options['from'] != settings.options['to']:
-                break
-
-            pywikibot.error('-from and -to arguments are equal, please retry.')
-            del settings.options['from']
-            del settings.options['to']
-
-        if settings.use_deletion_summary:
-            deletion_comment = \
-                CategoryMoveRobot.DELETION_COMMENT_SAME_AS_EDIT_COMMENT
-        else:
-            deletion_comment = CategoryMoveRobot.DELETION_COMMENT_AUTOMATIC
-        gen = gen_factory.getCombinedGenerator()
-        bot = CategoryMoveRobot(oldcat=settings.options.get('from'),
-                                newcat=settings.options.get('to'),
-                                batch=settings.batch,
-                                comment=settings.summary,
-                                inplace=settings.inplace,
-                                delete_oldcat=settings.delete_empty_cat,
-                                title_regex=settings.title_regex,
-                                history=settings.history,
-                                pagesonly=settings.pagesonly,
-                                deletion_comment=deletion_comment,
-                                wikibase=settings.wikibase,
-                                allow_split=settings.allow_split,
-                                move_together=settings.move_together,
-                                keep_sortkey=settings.keep_sortkey,
-                                generator=gen)
-    elif settings.action == 'tidy':
-        assert gen_factory is not None
-        bot = CategoryTidyRobot(settings.options.get('from'), cat_db,
-                                gen_factory.namespaces, settings.summary)
-    elif settings.action == 'tree':
-        bot = CategoryTreeRobot(settings.options.get('from'), cat_db,
-                                settings.options.get('to'),
-                                max_depth=settings.depth)
-    elif settings.action == 'listify':
-        assert gen_factory is not None
-        bot = CategoryListifyRobot(settings.options.get('from'),
-                                   settings.options.get('to'),
-                                   settings.summary, settings.append,
-                                   settings.overwrite, settings.showimages,
-                                   talk_pages=settings.talkpages,
-                                   recurse=settings.options.get(
-                                       'recurse', False),
-                                   namespaces=gen_factory.namespaces,
-                                   **settings.options)
-    elif settings.action == 'clean':
-        bot = CleanBot(**settings.options)
-
-    if not suggest_help(missing_action=not settings.action):
-        pywikibot.Site().login()
-        try:
-            bot.run()
-        except Error:
-            pywikibot.exception('Fatal error:')
-        finally:
-            if cat_db:
-                cat_db.dump()
+    pywikibot.Site().login()
+    try:
+        bot.run()
+    except Error:
+        pywikibot.exception('Fatal error:')
+    finally:
+        if cat_db:
+            cat_db.dump()


 if __name__ == '__main__':
diff --git a/tests/category_bot_tests.py b/tests/category_bot_tests.py
index de56235..50d1221 100755
--- a/tests/category_bot_tests.py
+++ b/tests/category_bot_tests.py
@@ -9,7 +9,7 @@

 import unittest
 from contextlib import suppress
-from unittest.mock import Mock, patch
+from unittest.mock import Mock, call, patch

 import pywikibot
 from pywikibot.site import BaseSite
@@ -207,6 +207,207 @@
         self.assertEqual(settings.unknown, ['-page:Example'])


+class TestCategoryBotConstruction(TestCase):
+
+    """Test construction of category action bots."""
+
+    net = False
+
+    @patch.object(category, 'CategoryAddBot')
+    def test_build_add_bot(self, bot_class) -> None:
+        """Test add bot construction and its default generator."""
+        generator = object()
+        generator_factory = Mock()
+        generator_factory.getCombinedGenerator.side_effect = [
+            None, generator]
+        settings = category._CategorySettings(
+            action='add',
+            options={'to': 'New'},
+            summary='summary',
+            sort_by_last_name=True,
+            create_pages=True,
+            follow_redirects=True,
+        )
+
+        result = category._build_add_bot(settings, generator_factory)
+
+        self.assertIs(result, bot_class.return_value)
+        self.assertEqual(
+            generator_factory.getCombinedGenerator.call_args_list,
+            [call(preload=True), call(preload=True)],
+        )
+        generator_factory.handle_arg.assert_called_once_with('-links')
+        bot_class.assert_called_once_with(
+            generator,
+            newcat='New',
+            sort_by_last_name=True,
+            create=True,
+            comment='summary',
+            follow_redirects=True,
+        )
+
+    @patch.object(category, 'CategoryMoveRobot')
+    @patch.object(category.pywikibot, 'input', return_value='Old')
+    def test_build_remove_bot(self, input_mock, bot_class) -> None:
+        """Test remove bot construction and its source prompt."""
+        generator = object()
+        generator_factory = Mock()
+        generator_factory.getCombinedGenerator.return_value = generator
+        settings = category._CategorySettings(
+            action='remove',
+            batch=True,
+            summary='summary',
+            inplace=True,
+            delete_empty_cat=False,
+            title_regex='title',
+            history=True,
+            pagesonly=True,
+            use_deletion_summary=False,
+        )
+
+        result = category._build_remove_bot(settings, generator_factory)
+
+        self.assertIs(result, bot_class.return_value)
+        input_mock.assert_called_once_with(
+            'Please enter the name of the category that should be removed:')
+        bot_class.assert_called_once_with(
+            oldcat='Old',
+            batch=True,
+            comment='summary',
+            inplace=True,
+            delete_oldcat=False,
+            title_regex='title',
+            history=True,
+            pagesonly=True,
+            deletion_comment=False,
+            generator=generator,
+        )
+
+    @patch.object(category.pywikibot, 'error')
+    @patch.object(category.pywikibot, 'input', side_effect=['Old', 'New'])
+    def test_resolve_equal_move_categories(
+        self,
+        input_mock,
+        error_mock,
+    ) -> None:
+        """Test prompting again for equal move categories."""
+        settings = category._CategorySettings(
+            action='move', options={'from': 'Same', 'to': 'Same'})
+
+        result = category._resolve_move_categories(settings)
+
+        self.assertTrue(result)
+        self.assertEqual(settings.options, {'from': 'Old', 'to': 'New'})
+        error_mock.assert_called_once_with(
+            '-from and -to arguments are equal, please retry.')
+        self.assertEqual(input_mock.call_count, 2)
+
+        settings = category._CategorySettings(action='move')
+        with patch.object(category.pywikibot, 'input', return_value=''):
+            result = category._resolve_move_categories(settings)
+        self.assertFalse(result)
+
+    def test_build_move_bot(self) -> None:
+        """Test move bot construction."""
+        original_class = category.CategoryMoveRobot
+        bot = Mock()
+        bot_class = Mock(return_value=bot)
+        bot_class.DELETION_COMMENT_SAME_AS_EDIT_COMMENT = (
+            original_class.DELETION_COMMENT_SAME_AS_EDIT_COMMENT
+        )
+        bot_class.DELETION_COMMENT_AUTOMATIC = (
+            original_class.DELETION_COMMENT_AUTOMATIC
+        )
+        generator = object()
+        generator_factory = Mock()
+        generator_factory.getCombinedGenerator.return_value = generator
+        settings = category._CategorySettings(
+            action='move',
+            options={'from': 'Old', 'to': 'New'},
+            batch=True,
+            summary='summary',
+            inplace=True,
+            delete_empty_cat=False,
+            title_regex='title',
+            history=True,
+            pagesonly=True,
+            wikibase=False,
+            allow_split=True,
+            move_together=True,
+            keep_sortkey=True,
+        )
+
+        with patch.object(category, 'CategoryMoveRobot', bot_class):
+            result = category._build_move_bot(settings, generator_factory)
+
+            self.assertIs(result, bot)
+            bot_class.assert_called_once_with(
+                oldcat='Old',
+                newcat='New',
+                batch=True,
+                comment='summary',
+                inplace=True,
+                delete_oldcat=False,
+                title_regex='title',
+                history=True,
+                pagesonly=True,
+                deletion_comment=(
+                    original_class.DELETION_COMMENT_SAME_AS_EDIT_COMMENT),
+                wikibase=False,
+                allow_split=True,
+                move_together=True,
+                keep_sortkey=True,
+                generator=generator,
+            )
+
+            bot_class.reset_mock()
+            settings.use_deletion_summary = False
+            result = category._build_move_bot(settings, generator_factory)
+
+        self.assertIs(result, bot)
+        self.assertEqual(
+            bot_class.call_args.kwargs['deletion_comment'],
+            original_class.DELETION_COMMENT_AUTOMATIC,
+        )
+
+    def test_build_bot_dispatch(self) -> None:
+        """Test dispatching every category action to its builder."""
+        generator_factory = Mock()
+        category_database = Mock()
+        builder_names = {
+            'add': '_build_add_bot',
+            'clean': '_build_clean_bot',
+            'listify': '_build_listify_bot',
+            'move': '_build_move_bot',
+            'remove': '_build_remove_bot',
+            'tidy': '_build_tidy_bot',
+            'tree': '_build_tree_bot',
+        }
+
+        for action, builder_name in builder_names.items():
+            with (
+                self.subTest(action=action),
+                patch.object(category, builder_name) as builder,
+            ):
+                settings = category._CategorySettings(action=action)
+
+                result = category._build_bot(
+                    settings, generator_factory, category_database)
+
+                self.assertIs(result, builder.return_value)
+                if action == 'tidy':
+                    builder.assert_called_once_with(
+                        settings, generator_factory, category_database)
+                elif action == 'tree':
+                    builder.assert_called_once_with(
+                        settings, category_database)
+                elif action in category._GENERATOR_ACTIONS:
+                    builder.assert_called_once_with(
+                        settings, generator_factory)
+                else:
+                    builder.assert_called_once_with(settings)
+
+
 if __name__ == '__main__':
     with suppress(SystemExit):
         unittest.main()

--
To view, visit 
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1326256?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: Ie768fe5ce95731f7dc9dd409f8aba73b108afc4e
Gerrit-Change-Number: 1326256
Gerrit-PatchSet: 3
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]

Reply via email to