From 5de3236d37d24d533f5fabf9de9b3d1e79a267e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Such=C3=A1nek?= Date: Mon, 28 Oct 2024 18:11:31 +0100 Subject: [PATCH 01/95] [FEAT] Add BasePage.get_revision BasePage.getOldVersion returns just the revision text. For advanced scripts, like patrolling bots, it's useful to analyze the revision's metadata as well. Change-Id: I6f9f7136d0d484535917e4d12c18fa78db883775 --- pywikibot/page/_basepage.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/pywikibot/page/_basepage.py b/pywikibot/page/_basepage.py index 1d675fe091..1d50f3a466 100644 --- a/pywikibot/page/_basepage.py +++ b/pywikibot/page/_basepage.py @@ -452,16 +452,27 @@ def _getInternals(self): self._getexception = IsRedirectPageError(self) raise self._getexception + def get_revision( + self, oldid: int, *, force: bool = False, content: bool = False + ) -> pywikibot.page.Revision: + """Return an old revision of this page. + + :param oldid: The revid of the revision desired. + :param content: if True, retrieve the content of the revision + (default False) + """ + if force or oldid not in self._revisions \ + or (content and self._revisions[oldid].text is None): + self.site.loadrevisions(self, content=content, revids=oldid) + return self._revisions[oldid] + @remove_last_args(['get_redirect']) def getOldVersion(self, oldid, force: bool = False) -> str: """Return text of an old revision of this page. :param oldid: The revid of the revision desired. """ - if force or oldid not in self._revisions \ - or self._revisions[oldid].text is None: - self.site.loadrevisions(self, content=True, revids=oldid) - return self._revisions[oldid].text + return self.get_revision(oldid, content=True, force=force).text def permalink(self, oldid=None, percent_encoded: bool = True, with_protocol: bool = False) -> str: From 38aeb043cd51f52528e21c2fcfd1990513c0b031 Mon Sep 17 00:00:00 2001 From: xqt Date: Wed, 30 Oct 2024 11:23:40 +0100 Subject: [PATCH 02/95] [9.6] Prepare next release Change-Id: I31f5e1dd3b64165a39cbc6f51a858f56e15706bb --- HISTORY.rst | 14 ++++++++++++++ ROADMAP.rst | 11 ++--------- pywikibot/__metadata__.py | 2 +- scripts/__init__.py | 2 +- scripts/pyproject.toml | 2 +- 5 files changed, 19 insertions(+), 12 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 8278f995c4..79ec950ed0 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,20 @@ Release History =============== +9.5.0 +----- + +* Add support for tcywikisource and tcywiktionary (:phab:`T378473`, :phab:`T378465`) +* i18n-updates +* Update invisible chars in :mod:`tools.chars` from unicode 16.0.0 +* Rename :meth:`DataSite.getPropertyType()` + to :meth:`DataSite.get_property_type()` +* provide XXXI with :func:`date.romanNumToInt` and :func:`date.intToRomanNum` functions +* No longer raise :exc:`exceptions.UnsupportedPageError` within :meth:`data.api.PageGenerator.result` (:phab:`T377651`) +* Extract messages with strong tag from xtools as error message in + :meth:`Page.authorship()` (:phab:`T376815`) + + 9.4.1 ----- *15 October 2024* diff --git a/ROADMAP.rst b/ROADMAP.rst index cd3def1497..d4206323d8 100644 --- a/ROADMAP.rst +++ b/ROADMAP.rst @@ -1,15 +1,8 @@ Current Release Changes ======================= -* Add support for tcywikisource and tcywiktionary (:phab:`T378473`, :phab:`T378465`) -* i18n-updates -* Update invisible chars in :mod:`tools.chars` from unicode 16.0.0 -* Rename :meth:`DataSite.getPropertyType()` - to :meth:`DataSite.get_property_type()` -* provide XXXI with :func:`date.romanNumToInt` and :func:`date.intToRomanNum` functions -* No longer raise :exc:`exceptions.UnsupportedPageError` within :meth:`data.api.PageGenerator.result` (:phab:`T377651`) -* Extract messages with strong tag from xtools as error message in - :meth:`Page.authorship()` (:phab:`T376815`) +* (no changes yet) + Current Deprecations ==================== diff --git a/pywikibot/__metadata__.py b/pywikibot/__metadata__.py index d067c1c401..607d2da452 100644 --- a/pywikibot/__metadata__.py +++ b/pywikibot/__metadata__.py @@ -12,6 +12,6 @@ from time import strftime -__version__ = '9.5.0' +__version__ = '9.6.0.dev0' __url__ = 'https://www.mediawiki.org/wiki/Manual:Pywikibot' __copyright__ = f'2003-{strftime("%Y")}, Pywikibot team' diff --git a/scripts/__init__.py b/scripts/__init__.py index 109f6044cf..96c14f7134 100644 --- a/scripts/__init__.py +++ b/scripts/__init__.py @@ -34,7 +34,7 @@ from pathlib import Path -__version__ = '9.5.0' +__version__ = '9.6.0' #: defines the entry point for pywikibot-scripts package base_dir = Path(__file__).parent diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml index 0db85408ae..51c254ddc8 100644 --- a/scripts/pyproject.toml +++ b/scripts/pyproject.toml @@ -7,7 +7,7 @@ package-dir = {"pywikibot_scripts" = "scripts"} [project] name = "pywikibot-scripts" -version = "9.5.0" +version = "9.6.0" authors = [ {name = "xqt", email = "info@gno.de"}, From fa9500e70fdc297e07875cd3b90b17162b8aca05 Mon Sep 17 00:00:00 2001 From: xqt Date: Thu, 31 Oct 2024 11:24:30 +0100 Subject: [PATCH 03/95] [bugfix] strip newlines from pairsfile lines Bug: T378647 Change-Id: I7c07398dfdcc80d3a0a45b4151438819c4d96cec --- scripts/replace.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/replace.py b/scripts/replace.py index 5b04af1201..d17bb8a00b 100755 --- a/scripts/replace.py +++ b/scripts/replace.py @@ -848,8 +848,7 @@ def handle_pairsfile(filename: str) -> list[str] | None: 'Please enter the filename to read replacements from:') try: - with Path(filename).open(encoding='utf-8') as f: - replacements = f.readlines() + replacements = Path(filename).read_text(encoding='utf-8').splitlines() if not replacements: raise OSError(f'{filename} is empty.') except OSError as e: From ccf3f8c81b6f0825ff62419f317da1aae5590010 Mon Sep 17 00:00:00 2001 From: xqt Date: Thu, 31 Oct 2024 13:44:45 +0100 Subject: [PATCH 04/95] [test] Test replace.handle_pairs() Check the following conditions: - non existing file - odd lines - file with BOM - file without BOM Use utf-8-sig for encoding to ignore the byte order mark when reading. The other way with lstrip() does not work. Bug: T378647 Change-Id: Ic81f6a9485e125906358c3f2c08e1248169a791a --- .pre-commit-config.yaml | 1 + scripts/replace.py | 8 ++++---- tests/data/pairsfile BOM.txt | 4 ++++ tests/data/pairsfile.txt | 4 ++++ tests/replacebot_tests.py | 25 +++++++++++++++++++++++++ 5 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 tests/data/pairsfile BOM.txt create mode 100644 tests/data/pairsfile.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1199d403e7..670c48555d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,6 +29,7 @@ repos: - id: destroyed-symlinks - id: end-of-file-fixer - id: fix-byte-order-marker + exclude: '^tests/data/' - id: fix-encoding-pragma args: - --remove diff --git a/scripts/replace.py b/scripts/replace.py index d17bb8a00b..0a1daa644d 100755 --- a/scripts/replace.py +++ b/scripts/replace.py @@ -848,20 +848,20 @@ def handle_pairsfile(filename: str) -> list[str] | None: 'Please enter the filename to read replacements from:') try: - replacements = Path(filename).read_text(encoding='utf-8').splitlines() - if not replacements: + # use utf-8-sig to ignore BOM + content = Path(filename).read_text(encoding='utf-8-sig') + if not content: raise OSError(f'{filename} is empty.') except OSError as e: pywikibot.error(f'Error loading {filename}: {e}') return None + replacements = content.splitlines() if len(replacements) % 2: pywikibot.error(f'{filename} contains an incomplete pattern ' f'replacement pair:\n{replacements}') return None - # Strip BOM from first line - replacements[0].lstrip('\uFEFF') return replacements diff --git a/tests/data/pairsfile BOM.txt b/tests/data/pairsfile BOM.txt new file mode 100644 index 0000000000..8096d66d3b --- /dev/null +++ b/tests/data/pairsfile BOM.txt @@ -0,0 +1,4 @@ +Category:Notice Board Quests +Категория:Задания с Доски объявлений +Windbluff Tower Key +Ключ от Крепости Ветров diff --git a/tests/data/pairsfile.txt b/tests/data/pairsfile.txt new file mode 100644 index 0000000000..927e6086db --- /dev/null +++ b/tests/data/pairsfile.txt @@ -0,0 +1,4 @@ +Category:Notice Board Quests +Категория:Задания с Доски объявлений +Windbluff Tower Key +Ключ от Крепости Ветров diff --git a/tests/replacebot_tests.py b/tests/replacebot_tests.py index f7abe982c8..4b22ed1610 100755 --- a/tests/replacebot_tests.py +++ b/tests/replacebot_tests.py @@ -303,6 +303,31 @@ def test_fix_callable(self): '"no-msg-callable" (all replacements)', ], pywikibot.bot.ui.pop_output()) + def test_pairs_file(self): + """Test handle_pairsfile.""" + result = replace.handle_pairsfile('non existing file') + self.assertIsNone(result) + self.assertIn("No such file or directory: 'non existing file'", + pywikibot.bot.ui.pop_output()[0]) + + result = replace.handle_pairsfile('tests/data/pagelist-lines.txt') + self.assertIsNone(result) + self.assertIn('pagelist-lines.txt contains an incomplete pattern ' + "replacement pair:\n['file', 'bracket', ", + pywikibot.bot.ui.pop_output()[0]) + + # check file with and without BOM + for variant in ('', ' BOM'): + result = replace.handle_pairsfile( + f'tests/data/pairsfile{variant}.txt') + self.assertIsEmpty(pywikibot.bot.ui.pop_output()) + self.assertEqual(result, [ + 'Category:Notice Board Quests', + 'Категория:Задания с Доски объявлений', + 'Windbluff Tower Key', + 'Ключ от Крепости Ветров' + ]) + if __name__ == '__main__': with suppress(SystemExit): From 19c188676769f1ed73b097e88ebad61108e0dd02 Mon Sep 17 00:00:00 2001 From: xqt Date: Thu, 31 Oct 2024 14:26:49 +0100 Subject: [PATCH 05/95] [i18n] copy pywikibot translations from i18n repository ignore this file from pre-commit check because it is used when making a distribution package. Change-Id: I27169de038848e31af41d3e8deb2443b3a45a0c9 --- .pre-commit-config.yaml | 1 + pywikibot/scripts/i18n/pywikibot/en.json | 48 ++++++++++++------------ 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 670c48555d..4d19b0f6ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,6 +40,7 @@ repos: - --autofix - --indent=4 - --no-ensure-ascii + exclude: pywikibot/scripts/i18n/pywikibot/ - id: trailing-whitespace args: - --markdown-linebreak-ext=rst diff --git a/pywikibot/scripts/i18n/pywikibot/en.json b/pywikibot/scripts/i18n/pywikibot/en.json index dc41df353a..1421f9bad8 100644 --- a/pywikibot/scripts/i18n/pywikibot/en.json +++ b/pywikibot/scripts/i18n/pywikibot/en.json @@ -1,26 +1,26 @@ { - "@metadata": { - "authors": [ - "Daniel Herding", - "Huji", - "Siebrand", - "Revi", - "Russell Blau", - "Xqt" - ] - }, - "pywikibot-bot-prefix": "Bot:", - "pywikibot-cosmetic-changes": "; cosmetic changes", - "pywikibot-enter-category-name": "Please enter the category name:", - "pywikibot-enter-file-links-processing": "Links to which file page should be processed?", - "pywikibot-enter-finished-browser": "Press Enter when finished in browser.", - "pywikibot-enter-namespace-number": "Please enter a namespace by its number:", - "pywikibot-enter-new-text": "Please enter the new text:", - "pywikibot-enter-page-processing": "Which page should be processed?", - "pywikibot-enter-xml-filename": "Please enter the XML dump's filename:", - "pywikibot-fixes-fckeditor": "Bot: Fixing rich-editor html", - "pywikibot-fixes-html": "Bot: Converting/fixing HTML", - "pywikibot-fixes-isbn": "Bot: Formatting ISBN", - "pywikibot-fixes-syntax": "Bot: Fixing wiki syntax", - "pywikibot-touch": "Pywikibot touch edit" + "@metadata": { + "authors": [ + "Daniel Herding", + "Huji", + "Siebrand", + "Revi", + "Russell Blau", + "Xqt" + ] + }, + "pywikibot-bot-prefix": "Bot:", + "pywikibot-cosmetic-changes": "; cosmetic changes", + "pywikibot-enter-category-name": "Please enter the category name:", + "pywikibot-enter-file-links-processing": "Links to which file page should be processed?", + "pywikibot-enter-finished-browser": "Press Enter when finished in browser.", + "pywikibot-enter-namespace-number": "Please enter a namespace by its number:", + "pywikibot-enter-new-text": "Please enter the new text:", + "pywikibot-enter-page-processing": "Which page should be processed?", + "pywikibot-enter-xml-filename": "Please enter the XML dump's filename:", + "pywikibot-fixes-fckeditor": "Bot: Fixing rich-editor html", + "pywikibot-fixes-html": "Bot: Converting/fixing HTML", + "pywikibot-fixes-isbn": "Bot: Formatting ISBN", + "pywikibot-fixes-syntax": "Bot: Fixing wiki syntax", + "pywikibot-touch": "Pywikibot touch edit" } From 3f3ddb68908acaa15a0c311f67372d2c311c7505 Mon Sep 17 00:00:00 2001 From: Xqt Date: Thu, 31 Oct 2024 19:32:22 +0000 Subject: [PATCH 06/95] [lint] remove flake8-no-u-prefixed-strings This test is already done with pyupgrade Change-Id: I9cd314f31eb4fc3271bfc0dbcb876835df9eec55 Signed-off-by: Xqt --- .pre-commit-config.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 670c48555d..6d72a745d2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -91,5 +91,4 @@ repos: - flake8-quotes>=3.3.2 - flake8-raise - flake8-tuple>=0.4.1 - - flake8-no-u-prefixed-strings>=0.2 - pep8-naming>=0.13.3 From bbe4c794bb04f6faac4df294e454aa1fd5c15023 Mon Sep 17 00:00:00 2001 From: JJMC89 Date: Thu, 31 Oct 2024 18:31:20 -0700 Subject: [PATCH 07/95] remove deprecated fix-encoding-pragma hook Change-Id: I88cf9f03c744ff0ee6ba5fc787d8c240d19b1ccb --- .pre-commit-config.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6d72a745d2..528bb18525 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,9 +30,6 @@ repos: - id: end-of-file-fixer - id: fix-byte-order-marker exclude: '^tests/data/' - - id: fix-encoding-pragma - args: - - --remove - id: forbid-new-submodules - id: mixed-line-ending - id: pretty-format-json From f95f22cf3103088c55ddaedd1e3aabb0be40a062 Mon Sep 17 00:00:00 2001 From: xqt Date: Thu, 31 Oct 2024 18:13:33 +0100 Subject: [PATCH 08/95] [lint] use ruff selecting D instead of deprecated flake8 pydocstyle Change-Id: I4055ebdb5ac9582d1b3d4e684a7e4907131d57a8 --- .pre-commit-config.yaml | 8 ++++++-- pyproject.toml | 9 +++++++++ tox.ini | 16 +++------------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 528bb18525..48d32f7786 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,6 +48,12 @@ repos: - id: python-check-blanket-type-ignore - id: python-check-mock-methods - id: python-use-type-annotations + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.7.1 + hooks: + - id: ruff + args: + - --fix - repo: https://github.com/asottile/pyupgrade rev: v3.19.0 hooks: @@ -79,10 +85,8 @@ repos: additional_dependencies: # Due to incompatibilities between packages the order matters. - darglint2 - - pydocstyle==6.3.0 # deprecated and no longer maintained - flake8-bugbear!=24.1.17 - flake8-comprehensions>=3.13.0 - - flake8-docstrings>=1.4.0 - flake8-mock-x2 - flake8-print>=5.0.0 - flake8-quotes>=3.3.2 diff --git a/pyproject.toml b/pyproject.toml index 24e9b904ff..38eb436f8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -145,3 +145,12 @@ enable_error_code = [ "ignore-without-code", ] ignore_missing_imports = true + +[tool.ruff.lint] +select = ["D"] +ignore = ["D105", "D203", "D211", "D212", "D213", "D214", "D401", "D404", "D406", "D407", "D412", "D413", "D416", "D417"] + +[tool.ruff.lint.per-file-ignores] +"pywikibot/families/*" = ["D102"] +"scripts/dataextend.py" = ["D101", "D102"] +"tests/ui_tests.py" = ["D102", "D103"] diff --git a/tox.ini b/tox.ini index 8f69dff930..e98f12e62d 100644 --- a/tox.ini +++ b/tox.ini @@ -118,8 +118,6 @@ deps = [flake8] # The following are intentionally ignored, possibly pending consensus -# D105: Missing docstring in magic method -# D211: No blank lines allowed before class docstring # E704: multiple statements on one line (def) # FI1: __future__ import "x" missing # H101: TODO format @@ -132,15 +130,10 @@ deps = # R100: raise in except handler without from # W503: line break before binary operator; against current PEP 8 recommendation -# Errors occurred after upgrade to pydocstyle 2.0.0 (T164142) -# D401: First line should be in imperative mood; try rephrasing -# D412: No blank lines allowed between a section header and its content -# D413: Missing blank line after last section - # DARXXX: Darglint docstring issues to be solved # DAR000: T368849 -ignore = B007,D105,D211,D401,D413,D412,DAR000,DAR003,DAR101,DAR102,DAR201,DAR202,DAR301,DAR401,DAR402,DAR501,E704,H101,H231,H232,H233,H234,H235,H236,H237,H238,H301,H306,H404,H405,H903,R100,W503 +ignore = B007,DAR000,DAR003,DAR101,DAR102,DAR201,DAR202,DAR301,DAR401,DAR402,DAR501,E704,H101,H231,H232,H233,H234,H235,H236,H237,H238,H301,H306,H404,H405,H903,R100,W503 enable-extensions = H203,H204,H205,N818 count = True @@ -166,7 +159,6 @@ per-file-ignores = pywikibot/date.py: N802, N803, N806, N816 pywikibot/editor.py: N803, N806 pywikibot/exceptions.py: H501 - pywikibot/families/*: D102 pywikibot/family.py: N802, N803, N806, N815 pywikibot/fixes.py: E241 pywikibot/interwiki_graph.py: N802, N803, N806 @@ -191,7 +183,7 @@ per-file-ignores = scripts/clean_sandbox.py: N816 scripts/commonscat.py: N802, N806, N816 scripts/cosmetic_changes.py: N816 - scripts/dataextend.py: C901, D101, D102, E501 + scripts/dataextend.py: C901, E501 scripts/harvest_template.py: N802, N816 scripts/interwiki.py: N802, N803, N806, N816 scripts/imagetransfer.py: N803, N806, N816 @@ -227,7 +219,7 @@ per-file-ignores = tests/tools_formatter_tests.py: N802 tests/tools_tests.py: N802 tests/ui_options_tests.py: N802 - tests/ui_tests.py: D102, D103, N802 + tests/ui_tests.py: N802 tests/wikibase_edit_tests.py: N802 tests/wikibase_tests.py: N802 tests/xmlreader_tests.py: N802 @@ -242,8 +234,6 @@ classmethod-decorators = classmethod,classproperty [pycodestyle] exclude = .tox,.git,./*.egg,build,./scripts/i18n/* -# see explanations above -ignore = D105,D211 [pytest] minversion = 7.0.1 From 2124a486515077c90ee4adb9b78dd9e98c2e8b0b Mon Sep 17 00:00:00 2001 From: xqt Date: Fri, 1 Nov 2024 11:46:38 +0100 Subject: [PATCH 09/95] [doc] Fix docstrings - [203] Add blank line before class docstring - [212] Start Multi-line docstring summary at the first line Change-Id: I06485009d487b356975c9873dffbc54f32aefb58 --- pyproject.toml | 2 +- pywikibot/__init__.py | 3 +- pywikibot/_wbtypes.py | 86 +++---- pywikibot/bot.py | 69 ++---- pywikibot/bot_choice.py | 12 +- pywikibot/comms/eventstreams.py | 3 +- pywikibot/comms/http.py | 15 +- pywikibot/config.py | 3 +- pywikibot/cosmetic_changes.py | 18 +- pywikibot/data/api/__init__.py | 3 +- pywikibot/data/api/_generators.py | 21 +- pywikibot/data/api/_optionset.py | 9 +- pywikibot/data/api/_paraminfo.py | 3 +- pywikibot/data/api/_requests.py | 24 +- pywikibot/data/memento.py | 3 +- pywikibot/data/sparql.py | 23 +- pywikibot/data/superset.py | 1 + pywikibot/data/wikistats.py | 6 +- pywikibot/date.py | 24 +- pywikibot/diff.py | 3 +- pywikibot/editor.py | 3 +- pywikibot/exceptions.py | 15 +- pywikibot/families/wikiquote_family.py | 3 +- pywikibot/family.py | 21 +- pywikibot/flow.py | 3 +- pywikibot/i18n.py | 21 +- pywikibot/interwiki_graph.py | 9 +- pywikibot/logentries.py | 33 +-- pywikibot/login.py | 33 +-- pywikibot/page/_basepage.py | 108 +++------ pywikibot/page/_category.py | 9 +- pywikibot/page/_collections.py | 38 ++-- pywikibot/page/_filepage.py | 27 +-- pywikibot/page/_links.py | 81 +++---- pywikibot/page/_page.py | 3 +- pywikibot/page/_user.py | 48 ++-- pywikibot/page/_wikibase.py | 210 ++++++------------ pywikibot/pagegenerators/__init__.py | 6 +- pywikibot/pagegenerators/_factory.py | 15 +- pywikibot/pagegenerators/_filters.py | 33 +-- pywikibot/pagegenerators/_generators.py | 55 ++--- pywikibot/proofreadpage.py | 6 +- pywikibot/scripts/generate_family_file.py | 5 +- pywikibot/scripts/generate_user_files.py | 9 +- pywikibot/scripts/login.py | 3 +- pywikibot/site/_apisite.py | 51 ++--- pywikibot/site/_basesite.py | 15 +- pywikibot/site/_datasite.py | 63 ++---- pywikibot/site/_extensions.py | 57 ++--- pywikibot/site/_generators.py | 9 +- pywikibot/site/_interwikimap.py | 9 +- pywikibot/site/_namespace.py | 24 +- pywikibot/site/_obsoletesites.py | 1 + pywikibot/site/_siteinfo.py | 15 +- pywikibot/site_detect.py | 3 +- pywikibot/specialbots/_upload.py | 6 +- pywikibot/textlib.py | 40 ++-- pywikibot/throttle.py | 1 + pywikibot/time.py | 15 +- pywikibot/titletranslate.py | 3 +- pywikibot/tools/__init__.py | 21 +- pywikibot/tools/_deprecate.py | 12 +- pywikibot/tools/collections.py | 3 +- pywikibot/tools/djvu.py | 22 +- pywikibot/tools/formatter.py | 3 +- pywikibot/tools/itertools.py | 6 +- pywikibot/tools/threading.py | 1 + pywikibot/userinterfaces/gui.py | 9 +- .../userinterfaces/terminal_interface.py | 3 +- .../userinterfaces/terminal_interface_base.py | 9 +- pywikibot/userinterfaces/transliteration.py | 3 +- pywikibot/version.py | 6 +- pywikibot/xmlreader.py | 3 +- scripts/add_text.py | 6 +- scripts/archivebot.py | 6 +- scripts/basic.py | 6 +- scripts/blockpageschecker.py | 6 +- scripts/category.py | 24 +- scripts/category_graph.py | 4 +- scripts/category_redirect.py | 3 +- scripts/change_pagelang.py | 3 +- scripts/checkimages.py | 15 +- scripts/claimit.py | 6 +- scripts/commons_information.py | 3 +- scripts/commonscat.py | 6 +- scripts/coordinate_import.py | 12 +- scripts/cosmetic_changes.py | 6 +- scripts/create_isbn_edition.py | 3 +- scripts/data_ingestion.py | 24 +- scripts/dataextend.py | 3 +- scripts/delete.py | 12 +- scripts/delinker.py | 3 +- scripts/djvutext.py | 9 +- scripts/download_dump.py | 3 +- scripts/fixing_redirects.py | 6 +- scripts/harvest_template.py | 6 +- scripts/illustrate_wikidata.py | 3 +- scripts/image.py | 6 +- scripts/imagetransfer.py | 6 +- scripts/interwiki.py | 54 ++--- scripts/interwikidata.py | 6 +- scripts/listpages.py | 6 +- scripts/maintenance/cache.py | 3 +- scripts/maintenance/make_i18n_dict.py | 6 +- scripts/misspelling.py | 12 +- scripts/movepages.py | 3 +- scripts/newitem.py | 6 +- scripts/noreferences.py | 9 +- scripts/nowcommons.py | 6 +- scripts/pagefromfile.py | 6 +- scripts/parser_function_count.py | 3 +- scripts/patrol.py | 6 +- scripts/protect.py | 9 +- scripts/redirect.py | 12 +- scripts/reflinks.py | 6 +- scripts/replace.py | 12 +- scripts/replicate_wiki.py | 3 +- scripts/revertbot.py | 3 +- scripts/solve_disambiguation.py | 12 +- scripts/speedy_delete.py | 4 +- scripts/template.py | 6 +- scripts/templatecount.py | 18 +- scripts/touch.py | 3 +- scripts/transferbot.py | 6 +- scripts/transwikiimport.py | 3 +- scripts/unusedfiles.py | 3 +- scripts/upload.py | 3 +- scripts/watchlist.py | 3 +- scripts/weblinkchecker.py | 21 +- tests/api_tests.py | 3 +- tests/archivebot_tests.py | 3 +- tests/aspects.py | 42 ++-- tests/basepage.py | 3 +- tests/bot_tests.py | 6 +- tests/category_bot_tests.py | 1 + tests/cosmetic_changes_tests.py | 3 +- tests/edit_tests.py | 1 + tests/fixing_redirects_tests.py | 1 + tests/flow_tests.py | 1 + tests/http_tests.py | 9 +- tests/link_tests.py | 3 +- tests/logentries_tests.py | 3 +- tests/login_tests.py | 8 +- tests/noreferences_tests.py | 1 + tests/page_tests.py | 8 +- tests/pagegenerators_tests.py | 6 +- tests/proofreadpage_tests.py | 1 + tests/pwb_tests.py | 6 +- tests/reflinks_tests.py | 3 +- tests/site_detect_tests.py | 3 +- tests/site_generators_tests.py | 2 + tests/site_login_logout_tests.py | 1 + tests/site_tests.py | 1 + tests/sparql_tests.py | 8 + tests/superset_tests.py | 2 + tests/textlib_tests.py | 4 +- tests/tools_deprecate_tests.py | 5 +- tests/ui_tests.py | 3 +- tests/upload_tests.py | 3 +- tests/uploadbot_tests.py | 3 +- tests/utils.py | 18 +- tests/wikibase_edit_tests.py | 9 +- tests/wikibase_tests.py | 73 +++--- 163 files changed, 786 insertions(+), 1445 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 38eb436f8c..1b959f4e44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -148,7 +148,7 @@ ignore_missing_imports = true [tool.ruff.lint] select = ["D"] -ignore = ["D105", "D203", "D211", "D212", "D213", "D214", "D401", "D404", "D406", "D407", "D412", "D413", "D416", "D417"] +ignore = ["D105", "D211", "D213", "D214", "D401", "D404", "D406", "D407", "D412", "D413", "D416", "D417"] [tool.ruff.lint.per-file-ignores] "pywikibot/families/*" = ["D102"] diff --git a/pywikibot/__init__.py b/pywikibot/__init__.py index 2889125969..7cd07b5855 100644 --- a/pywikibot/__init__.py +++ b/pywikibot/__init__.py @@ -272,8 +272,7 @@ def Site(code: str | None = None, # noqa: N802 def showDiff(oldtext: str, # noqa: N802 newtext: str, context: int = 0) -> None: - """ - Output a string showing the differences between oldtext and newtext. + """Output a string showing the differences between oldtext and newtext. The differences are highlighted (only on compatible systems) to show which changes were made. diff --git a/pywikibot/_wbtypes.py b/pywikibot/_wbtypes.py index 66374df215..ee06fcc126 100644 --- a/pywikibot/_wbtypes.py +++ b/pywikibot/_wbtypes.py @@ -101,8 +101,7 @@ def __init__(self, lat: float, lon: float, alt: float | None = None, site: DataSite | None = None, globe_item: ItemPageStrNoneType = None, primary: bool = False) -> None: - """ - Represent a geo coordinate. + """Represent a geo coordinate. :param lat: Latitude :param lon: Longitude @@ -150,8 +149,7 @@ def entity(self) -> str: return self._entity def toWikibase(self) -> dict[str, Any]: - """ - Export the data to a JSON object for the Wikibase API. + """Export the data to a JSON object for the Wikibase API. FIXME: Should this be in the DataSite object? @@ -167,8 +165,7 @@ def toWikibase(self) -> dict[str, Any]: @classmethod def fromWikibase(cls, data: dict[str, Any], site: DataSite | None = None) -> Coordinate: - """ - Constructor to create an object from Wikibase's JSON output. + """Constructor to create an object from Wikibase's JSON output. :param data: Wikibase JSON :param site: The Wikibase site @@ -230,8 +227,7 @@ def precision(self, value: float) -> None: self._precision = value def precisionToDim(self) -> int | None: - """ - Convert precision from Wikibase to GeoData's dim and return the latter. + """Convert precision from Wikibase to GeoData's dim. dim is calculated if the Coordinate doesn't have a dimension, and precision is set. When neither dim nor precision are set, ValueError @@ -273,8 +269,7 @@ def precisionToDim(self) -> int | None: def get_globe_item(self, repo: DataSite | None = None, lazy_load: bool = False) -> pywikibot.ItemPage: - """ - Return the ItemPage corresponding to the globe. + """Return the ItemPage corresponding to the globe. Note that the globe need not be in the same data repository as the Coordinate itself. @@ -720,8 +715,7 @@ def toTimestr(self, force_iso: bool = False) -> str: self.hour, self.minute, self.second) def toTimestamp(self, timezone_aware: bool = False) -> Timestamp: - """ - Convert the data to a pywikibot.Timestamp. + """Convert the data to a pywikibot.Timestamp. .. versionchanged:: 8.0.1 *timezone_aware* parameter was added. @@ -764,8 +758,7 @@ def toWikibase(self) -> dict[str, Any]: @classmethod def fromWikibase(cls, data: dict[str, Any], site: DataSite | None = None) -> WbTime: - """ - Create a WbTime from the JSON data given by the Wikibase API. + """Create a WbTime from the JSON data given by the Wikibase API. :param data: Wikibase JSON :param site: The Wikibase site. If not provided, retrieves the data @@ -784,8 +777,7 @@ class WbQuantity(WbRepresentation): @staticmethod def _require_errors(site: DataSite | None) -> bool: - """ - Check if Wikibase site is so old it requires error bounds to be given. + """Check if Wikibase site is old and requires error bounds to be given. If no site item is supplied it raises a warning and returns True. @@ -800,8 +792,7 @@ def _require_errors(site: DataSite | None) -> bool: @staticmethod def _todecimal(value: ToDecimalType) -> Decimal | None: - """ - Convert a string to a Decimal for use in WbQuantity. + """Convert a string to a Decimal for use in WbQuantity. None value is returned as is. @@ -815,8 +806,7 @@ def _todecimal(value: ToDecimalType) -> Decimal | None: @staticmethod def _fromdecimal(value: Decimal | None) -> str | None: - """ - Convert a Decimal to a string representation suitable for WikiBase. + """Convert a Decimal to a string representation suitable for WikiBase. None value is returned as is. @@ -830,8 +820,7 @@ def __init__( error: ToDecimalType | tuple[ToDecimalType, ToDecimalType] = None, site: DataSite | None = None, ) -> None: - """ - Create a new WbQuantity object. + """Create a new WbQuantity object. :param amount: number representing this quantity :param unit: the Wikibase item for the unit or the entity URI of this @@ -878,8 +867,7 @@ def unit(self) -> str: def get_unit_item(self, repo: DataSite | None = None, lazy_load: bool = False) -> pywikibot.ItemPage: - """ - Return the ItemPage corresponding to the unit. + """Return the ItemPage corresponding to the unit. Note that the unit need not be in the same data repository as the WbQuantity itself. @@ -901,8 +889,7 @@ def get_unit_item(self, repo: DataSite | None = None, return self._unit def toWikibase(self) -> dict[str, Any]: - """ - Convert the data to a JSON object for the Wikibase API. + """Convert the data to a JSON object for the Wikibase API. :return: Wikibase JSON """ @@ -916,8 +903,7 @@ def toWikibase(self) -> dict[str, Any]: @classmethod def fromWikibase(cls, data: dict[str, Any], site: DataSite | None = None) -> WbQuantity: - """ - Create a WbQuantity from the JSON data given by the Wikibase API. + """Create a WbQuantity from the JSON data given by the Wikibase API. :param data: Wikibase JSON :param site: The Wikibase site @@ -934,13 +920,13 @@ def fromWikibase(cls, data: dict[str, Any], class WbMonolingualText(WbRepresentation): + """A Wikibase monolingual text representation.""" _items = ('text', 'language') def __init__(self, text: str, language: str) -> None: - """ - Create a new WbMonolingualText object. + """Create a new WbMonolingualText object. :param text: text string :param language: language code of the string @@ -951,8 +937,7 @@ def __init__(self, text: str, language: str) -> None: self.language = language def toWikibase(self) -> dict[str, Any]: - """ - Convert the data to a JSON object for the Wikibase API. + """Convert the data to a JSON object for the Wikibase API. :return: Wikibase JSON """ @@ -964,8 +949,7 @@ def toWikibase(self) -> dict[str, Any]: @classmethod def fromWikibase(cls, data: dict[str, Any], site: DataSite | None = None) -> WbMonolingualText: - """ - Create a WbMonolingualText from the JSON data given by Wikibase API. + """Create a WbMonolingualText from the JSON data given by Wikibase API. :param data: Wikibase JSON :param site: The Wikibase site @@ -974,6 +958,7 @@ def fromWikibase(cls, data: dict[str, Any], class WbDataPage(WbRepresentation): + """An abstract Wikibase representation for data pages. .. warning:: Perhaps a temporary implementation until :phab:`T162336` @@ -986,8 +971,7 @@ class WbDataPage(WbRepresentation): @classmethod @abc.abstractmethod def _get_data_site(cls, repo_site: DataSite) -> APISite: - """ - Return the site serving as a repository for a given data type. + """Return the site serving as a repository for a given data type. .. note:: implemented in the extended class. @@ -998,8 +982,7 @@ def _get_data_site(cls, repo_site: DataSite) -> APISite: @classmethod @abc.abstractmethod def _get_type_specifics(cls, site: DataSite) -> dict[str, Any]: - """ - Return the specifics for a given data type. + """Return the specifics for a given data type. .. note:: Must be implemented in the extended class. @@ -1017,8 +1000,7 @@ def _get_type_specifics(cls, site: DataSite) -> dict[str, Any]: @staticmethod def _validate(page: pywikibot.Page, data_site: BaseSite, ending: str, label: str) -> None: - """ - Validate the provided page against general and type specific rules. + """Validate the provided page against general and type specific rules. :param page: Page containing the data. :param data_site: The site serving as a repository for the given @@ -1073,8 +1055,7 @@ def __hash__(self) -> int: return hash(self.toWikibase()) def toWikibase(self) -> str: - """ - Convert the data to the value required by the Wikibase API. + """Convert the data to the value required by the Wikibase API. :return: title of the data page incl. namespace """ @@ -1097,12 +1078,12 @@ def fromWikibase(cls, page_name: str, site: DataSite | None) -> WbDataPage: class WbGeoShape(WbDataPage): + """A Wikibase geo-shape representation.""" @classmethod def _get_data_site(cls, site: DataSite) -> APISite: - """ - Return the site serving as a geo-shape repository. + """Return the site serving as a geo-shape repository. :param site: The Wikibase site """ @@ -1110,8 +1091,7 @@ def _get_data_site(cls, site: DataSite) -> APISite: @classmethod def _get_type_specifics(cls, site: DataSite) -> dict[str, Any]: - """ - Return the specifics for WbGeoShape. + """Return the specifics for WbGeoShape. :param site: The Wikibase site """ @@ -1124,12 +1104,12 @@ def _get_type_specifics(cls, site: DataSite) -> dict[str, Any]: class WbTabularData(WbDataPage): + """A Wikibase tabular-data representation.""" @classmethod def _get_data_site(cls, site: DataSite) -> APISite: - """ - Return the site serving as a tabular-data repository. + """Return the site serving as a tabular-data repository. :param site: The Wikibase site """ @@ -1137,8 +1117,7 @@ def _get_data_site(cls, site: DataSite) -> APISite: @classmethod def _get_type_specifics(cls, site: DataSite) -> dict[str, Any]: - """ - Return the specifics for WbTabularData. + """Return the specifics for WbTabularData. :param site: The Wikibase site """ @@ -1151,6 +1130,7 @@ def _get_type_specifics(cls, site: DataSite) -> dict[str, Any]: class WbUnknown(WbRepresentation): + """A Wikibase representation for unknown data type. This will prevent the bot from breaking completely when a new type @@ -1166,8 +1146,7 @@ class WbUnknown(WbRepresentation): _items = ('json',) def __init__(self, json: dict[str, Any], warning: str = '') -> None: - """ - Create a new WbUnknown object. + """Create a new WbUnknown object. :param json: Wikibase JSON :param warning: a warning message which is shown once if @@ -1192,8 +1171,7 @@ def toWikibase(self) -> dict[str, Any]: @classmethod def fromWikibase(cls, data: dict[str, Any], site: DataSite | None = None) -> WbUnknown: - """ - Create a WbUnknown from the JSON data given by the Wikibase API. + """Create a WbUnknown from the JSON data given by the Wikibase API. :param data: Wikibase JSON :param site: The Wikibase site diff --git a/pywikibot/bot.py b/pywikibot/bot.py index 59780d3469..c5227e7a1b 100644 --- a/pywikibot/bot.py +++ b/pywikibot/bot.py @@ -646,8 +646,7 @@ def input_list_choice(question: str, answers: AnswerType, default: int | str | None = None, force: bool = False) -> str: - """ - Ask the user the question and return one of the valid answers. + """Ask the user the question and return one of the valid answers. :param question: The question asked without trailing spaces. :param answers: The valid answers each containing a full length answer. @@ -674,8 +673,7 @@ def calledModuleName() -> str: def handle_args(args: Iterable[str] | None = None, do_help: bool = True) -> list[str]: - """ - Handle global command line arguments and return the rest as a list. + """Handle global command line arguments and return the rest as a list. Takes the command line arguments as strings, processes all :ref:`global parameters` such as ``-lang`` or @@ -908,8 +906,7 @@ def suggest_help(missing_parameters: Sequence[str] | None = None, missing_action: bool = False, additional_text: str = '', missing_dependencies: Sequence[str] | None = None) -> bool: - """ - Output error message to use -help with additional text before it. + """Output error message to use -help with additional text before it. :param missing_parameters: A list of parameters which are missing. :param missing_generator: Whether a generator is missing. @@ -952,8 +949,7 @@ def suggest_help(missing_parameters: Sequence[str] | None = None, def writeToCommandLogFile() -> None: - """ - Save name of the called module along with all params to logs/commands.log. + """Save name of the called module along with all params to logfile. This can be used by user later to track errors or report bugs. """ @@ -1258,8 +1254,7 @@ def user_confirm(self, question: str) -> bool: def userPut(self, page: pywikibot.page.BasePage, oldtext: str, newtext: str, **kwargs: Any) -> bool: - """ - Save a new revision of a page, with user confirmation as required. + """Save a new revision of a page, with user confirmation as required. Print differences, ask user for confirmation, and puts the page if needed. @@ -1294,8 +1289,7 @@ def userPut(self, page: pywikibot.page.BasePage, oldtext: str, def _save_page(self, page: pywikibot.page.BasePage, func: Callable[..., Any], *args: Any, **kwargs: Any) -> bool: - """ - Helper function to handle page save-related option error handling. + """Helper function to handle page save-related option error handling. .. note:: Do no use it directly. Use :meth:`userPut` instead. @@ -1598,8 +1592,7 @@ def run(self) -> None: # a site previously defined class Bot(BaseBot): - """ - Generic bot subclass for multiple sites. + """Generic bot subclass for multiple sites. If possible the MultipleSitesBot or SingleSiteBot classes should be used instead which specifically handle multiple or single sites. @@ -1627,8 +1620,7 @@ def site(self) -> BaseSite | None: @site.setter def site(self, site: BaseSite | None) -> None: - """ - Set the Site that the bot is using. + """Set the Site that the bot is using. When Bot.run() is managing the generator and site property, this is set each time a page is on a site different from the previous page. @@ -1682,8 +1674,7 @@ def init_page(self, item: Any) -> pywikibot.page.BasePage: class SingleSiteBot(BaseBot): - """ - A bot only working on one site and ignoring the others. + """A bot only working on one site and ignoring the others. If no site is given from the start it'll use the first page's site. Any page after the site has been defined and is not on the defined site will be @@ -1693,8 +1684,7 @@ class SingleSiteBot(BaseBot): def __init__(self, site: BaseSite | bool | None = True, **kwargs: Any) -> None: - """ - Create a SingleSiteBot instance. + """Create a SingleSiteBot instance. :param site: If True it'll be set to the configured site using pywikibot.Site. @@ -1746,8 +1736,7 @@ def skip_page(self, page: pywikibot.page.BasePage) -> bool: class MultipleSitesBot(BaseBot): - """ - A bot class working on multiple sites. + """A bot class working on multiple sites. The bot should accommodate for that case and not store site specific information on only one site. @@ -1814,8 +1803,7 @@ def set_options(self, **kwargs: Any) -> None: class CurrentPageBot(BaseBot): - """ - A bot which automatically sets 'current_page' on each treat(). + """A bot which automatically sets 'current_page' on each treat(). This class should be always used together with either the MultipleSitesBot or SingleSiteBot class as there is no site management in this class. @@ -1838,8 +1826,7 @@ def put_current(self, new_text: str, ignore_save_related_errors: bool | None = None, ignore_server_errors: bool | None = None, **kwargs: Any) -> bool: - """ - Call :py:obj:`Bot.userPut` but use the current page. + """Call :py:obj:`Bot.userPut` but use the current page. It compares the new_text to the current page text. @@ -1865,8 +1852,7 @@ def put_current(self, new_text: str, class AutomaticTWSummaryBot(CurrentPageBot): - """ - A class which automatically defines ``summary`` for ``put_current``. + """A class which automatically defines ``summary`` for ``put_current``. The class must defined a ``summary_key`` string which contains the i18n key for :py:obj:`i18n.twtranslate`. It can also @@ -2002,8 +1988,7 @@ def skip_page(self, page: pywikibot.page.BasePage) -> bool: class WikidataBot(Bot, ExistingPageBot): - """ - Generic Wikidata Bot to be subclassed. + """Generic Wikidata Bot to be subclassed. Source claims (P143) can be created for specific sites @@ -2041,8 +2026,7 @@ def __init__(self, **kwargs: Any) -> None: f'{self.site} is not connected to a data repository') def cacheSources(self) -> None: - """ - Fetch the sources from the list on Wikidata. + """Fetch the sources from the list on Wikidata. It is stored internally and reused by getSource() """ @@ -2055,8 +2039,7 @@ def cacheSources(self) -> None: self.repo, family[source_lang]) def get_property_by_name(self, property_name: str) -> str: - """ - Find given property and return its ID. + """Find given property and return its ID. Method first uses site.search() and if the property isn't found, then asks user to provide the property ID. @@ -2078,8 +2061,7 @@ def user_edit_entity(self, entity: pywikibot.page.WikibasePage, ignore_save_related_errors: bool | None = None, ignore_server_errors: bool | None = None, **kwargs: Any) -> bool: - """ - Edit entity with data provided, with user confirmation as required. + """Edit entity with data provided, with user confirmation as required. :param entity: page to be edited :param data: data to be saved, or None if the diff should be created @@ -2119,8 +2101,7 @@ def user_add_claim(self, item: pywikibot.page.ItemPage, claim: pywikibot.page.Claim, source: BaseSite | None = None, bot: bool = True, **kwargs: Any) -> bool: - """ - Add a claim to an item, with user confirmation as required. + """Add a claim to an item, with user confirmation as required. :param item: page to be edited :param claim: claim to be saved @@ -2149,8 +2130,7 @@ def user_add_claim(self, item: pywikibot.page.ItemPage, return self._save_page(item, item.addClaim, claim, bot=bot, **kwargs) def getSource(self, site: BaseSite) -> pywikibot.page.Claim | None: - """ - Create a Claim usable as a source for Wikibase statements. + """Create a Claim usable as a source for Wikibase statements. :param site: site that is the source of assertions. @@ -2170,8 +2150,7 @@ def user_add_claim_unless_exists( source: BaseSite | None = None, logger_callback: Callable[[str], Any] = pwb_logging.log, **kwargs: Any) -> bool: - """ - Decorator of :py:obj:`user_add_claim`. + """Decorator of :py:obj:`user_add_claim`. Before adding a new claim, it checks if we can add it, using provided filters. @@ -2261,8 +2240,7 @@ def create_item_for_page(self, page: pywikibot.page.BasePage, summary: str | None = None, **kwargs: Any ) -> pywikibot.page.ItemPage | None: - """ - Create an ItemPage with the provided page as the sitelink. + """Create an ItemPage with the provided page as the sitelink. :param page: the page for which the item will be created :param data: additional data to be included in the new item (optional). @@ -2345,8 +2323,7 @@ def treat_page(self) -> None: def treat_page_and_item(self, page: pywikibot.page.BasePage, item: pywikibot.page.ItemPage) -> None: - """ - Treat page together with its item (if it exists). + """Treat page together with its item (if it exists). Must be implemented in subclasses. """ diff --git a/pywikibot/bot_choice.py b/pywikibot/bot_choice.py index 2548a8b3ab..b7dd803544 100644 --- a/pywikibot/bot_choice.py +++ b/pywikibot/bot_choice.py @@ -46,8 +46,7 @@ class Option(ABC): - """ - A basic option for input_choice. + """A basic option for input_choice. The following methods need to be implemented: @@ -97,8 +96,7 @@ def stop(self) -> bool: return self._stop def handled(self, value: str) -> Option | None: - """ - Return the Option object that applies to the given value. + """Return the Option object that applies to the given value. If this Option object doesn't know which applies it returns None. """ @@ -167,8 +165,7 @@ class StandardOption(Option): """An option with a description and shortcut and returning the shortcut.""" def __init__(self, option: str, shortcut: str, **kwargs: Any) -> None: - """ - Initializer. + """Initializer. :param option: option string :param shortcut: Shortcut of the option @@ -217,8 +214,7 @@ def out(self) -> str: class NestedOption(OutputOption, StandardOption): - """ - An option containing other options. + """An option containing other options. It will return True in test if this option applies but False if a sub option applies while handle returns the sub option. diff --git a/pywikibot/comms/eventstreams.py b/pywikibot/comms/eventstreams.py index 5bc6530771..5f46511f1f 100644 --- a/pywikibot/comms/eventstreams.py +++ b/pywikibot/comms/eventstreams.py @@ -195,8 +195,7 @@ def url(self): since=f'?since={self._since}' if self._since else '') def set_maximum_items(self, value: int) -> None: - """ - Set the maximum number of items to be retrieved from the stream. + """Set the maximum number of items to be retrieved from the stream. If not called, most queries will continue as long as there is more data to be retrieved from the stream. diff --git a/pywikibot/comms/http.py b/pywikibot/comms/http.py index edda981c33..ceb6d6288c 100644 --- a/pywikibot/comms/http.py +++ b/pywikibot/comms/http.py @@ -1,5 +1,4 @@ -""" -Basic HTTP access interface. +"""Basic HTTP access interface. This module handles communication between the bot and the HTTP threads. @@ -162,8 +161,7 @@ def get_value(self, key, args, kwargs): def user_agent_username(username=None): - """ - Reduce username to a representation permitted in HTTP headers. + """Reduce username to a representation permitted in HTTP headers. To achieve that, this function: 1) replaces spaces (' ') with '_' @@ -251,8 +249,7 @@ def request(site: pywikibot.site.BaseSite, uri: str | None = None, headers: dict | None = None, **kwargs) -> requests.Response: - """ - Request to Site with default error handling and response decoding. + """Request to Site with default error handling and response decoding. See :py:obj:`requests.Session.request` for additional parameters. @@ -309,8 +306,7 @@ def get_authentication(uri: str) -> tuple[str, str] | None: def error_handling_callback(response): - """ - Raise exceptions and log alerts. + """Raise exceptions and log alerts. :param response: Response returned by Session.request(). :type response: :py:obj:`requests.Response` @@ -365,8 +361,7 @@ def error_handling_callback(response): def fetch(uri: str, method: str = 'GET', headers: dict | None = None, default_error_handling: bool = True, use_fake_user_agent: bool | str = False, **kwargs): - """ - HTTP request. + """HTTP request. See :py:obj:`requests.Session.request` for parameters. diff --git a/pywikibot/config.py b/pywikibot/config.py index b5c9032f5c..1e45f5b989 100644 --- a/pywikibot/config.py +++ b/pywikibot/config.py @@ -1,5 +1,4 @@ -""" -Module to define and load pywikibot configuration default and user preferences. +"""Module to define pywikibot configuration default and user preferences. User preferences are loaded from a python file called `user-config.py`, which may be located in directory specified by the environment variable diff --git a/pywikibot/cosmetic_changes.py b/pywikibot/cosmetic_changes.py index 8df7cd4322..c2337fe5fa 100644 --- a/pywikibot/cosmetic_changes.py +++ b/pywikibot/cosmetic_changes.py @@ -1,5 +1,4 @@ -""" -This module can do slight modifications to tidy a wiki page's source code. +"""This module can do slight modifications to tidy a wiki page's source code. The changes are not supposed to change the look of the rendered wiki page. @@ -330,8 +329,7 @@ def change(self, text: str) -> bool | str: return new_text def fixSelfInterwiki(self, text: str) -> str: - """ - Interwiki links to the site itself are displayed like local links. + """Interwiki links to the site itself are displayed like local links. Remove their language code prefix. """ @@ -767,8 +765,7 @@ def removeUselessSpaces(self, text: str) -> str: return text def removeNonBreakingSpaceBeforePercent(self, text: str) -> str: - """ - Remove a non-breaking space between number and percent sign. + """Remove a non-breaking space between number and percent sign. Newer MediaWiki versions automatically place a non-breaking space in front of a percent sign, so it is no longer required to place it @@ -779,8 +776,7 @@ def removeNonBreakingSpaceBeforePercent(self, text: str) -> str: return text def cleanUpSectionHeaders(self, text: str) -> str: - """ - Add a space between the equal signs and the section title. + """Add a space between the equal signs and the section title. Example:: @@ -805,8 +801,7 @@ def cleanUpSectionHeaders(self, text: str) -> str: ['comment', 'math', 'nowiki', 'pre']) def putSpacesInLists(self, text: str) -> str: - """ - Add a space between the * or # and the text. + """Add a space between the * or # and the text. .. note:: This space is recommended in the syntax help on the English, German and French Wikipedias. It might be that it @@ -1069,8 +1064,7 @@ def fixArabicLetters(self, text: str) -> str: return text def commonsfiledesc(self, text: str) -> str: - """ - Clean up file descriptions on Wikimedia Commons. + """Clean up file descriptions on Wikimedia Commons. It works according to [1] and works only on pages in the file namespace on Wikimedia Commons. diff --git a/pywikibot/data/api/__init__.py b/pywikibot/data/api/__init__.py index c6475673af..ca065a31ea 100644 --- a/pywikibot/data/api/__init__.py +++ b/pywikibot/data/api/__init__.py @@ -46,8 +46,7 @@ def _invalidate_superior_cookies(family) -> None: - """ - Clear cookies for site's second level domain. + """Clear cookies for site's second level domain. The http module takes care of all the cookie stuff. This is a workaround for requests bug, see :phab:`T224712` diff --git a/pywikibot/data/api/_generators.py b/pywikibot/data/api/_generators.py index c4fef30368..006f8d024d 100644 --- a/pywikibot/data/api/_generators.py +++ b/pywikibot/data/api/_generators.py @@ -95,8 +95,7 @@ def __init__( data_name: str = 'data', **kwargs ) -> None: - """ - Initialize an APIGenerator object. + """Initialize an APIGenerator object. kwargs are used to create a Request object; see that object's documentation for values. @@ -123,8 +122,7 @@ def __init__( self.request[self.limit_name] = self.query_increment def set_query_increment(self, value: int) -> None: - """ - Set the maximum number of items to be retrieved per API query. + """Set the maximum number of items to be retrieved per API query. If not called, the default is config.step. @@ -137,8 +135,7 @@ def set_query_increment(self, value: int) -> None: .format(type(self).__name__, self.query_increment)) def set_maximum_items(self, value: int | str | None) -> None: - """ - Set the maximum number of items to be retrieved from the wiki. + """Set the maximum number of items to be retrieved from the wiki. If not called, most queries will continue as long as there is more data to be retrieved from the API. @@ -157,8 +154,7 @@ def set_maximum_items(self, value: int | str | None) -> None: @property def generator(self): - """ - Submit request and iterate the response. + """Submit request and iterate the response. Continues response as needed until limit (if defined) is reached. @@ -764,8 +760,7 @@ class PropertyGenerator(QueryGenerator): """ def __init__(self, prop: str, **kwargs) -> None: - """ - Initializer. + """Initializer. Required and optional parameters are as for ``Request``, except that action=query is assumed and prop is required. @@ -844,8 +839,7 @@ class ListGenerator(QueryGenerator): """ def __init__(self, listaction: str, **kwargs) -> None: - """ - Initializer. + """Initializer. Required and optional parameters are as for ``Request``, except that action=query is assumed and listaction is required. @@ -988,8 +982,7 @@ def _update_coordinates(page, coordinates) -> None: def update_page(page: pywikibot.Page, pagedict: dict[str, Any], props: Iterable[str] | None = None) -> None: - """ - Update attributes of Page object *page*, based on query data in *pagedict*. + """Update attributes of *page*, based on query data in *pagedict*. :param page: object to be updated :param pagedict: the contents of a *page* element of a query diff --git a/pywikibot/data/api/_optionset.py b/pywikibot/data/api/_optionset.py index d31f0e4ed8..75019c74e0 100644 --- a/pywikibot/data/api/_optionset.py +++ b/pywikibot/data/api/_optionset.py @@ -17,8 +17,7 @@ class OptionSet(MutableMapping): - """ - A class to store a set of options which can be either enabled or not. + """A class to store a set of options which can be either enabled or not. If it is instantiated with the associated site, module and parameter it will only allow valid names as options. If instantiated 'lazy loaded' it @@ -95,8 +94,7 @@ def _set_site(self, site, module: str, param: str, self._site_set = True def from_dict(self, dictionary): - """ - Load options from the dict. + """Load options from the dict. The options are not cleared before. If changes have been made previously, but only the dict values should be applied it needs to be @@ -153,8 +151,7 @@ def __setitem__(self, name, value): raise ValueError(f'Invalid value "{value}"') def __getitem__(self, name) -> bool | None: - """ - Return whether the option is enabled. + """Return whether the option is enabled. :return: If the name has been set it returns whether it is enabled. Otherwise it returns None. If the site has been set it raises a diff --git a/pywikibot/data/api/_paraminfo.py b/pywikibot/data/api/_paraminfo.py index b7cb846b30..7d95660e9d 100644 --- a/pywikibot/data/api/_paraminfo.py +++ b/pywikibot/data/api/_paraminfo.py @@ -139,8 +139,7 @@ def fetch(self, modules: Iterable | str) -> None: self._fetch(modules) def _fetch(self, modules: set | frozenset) -> None: - """ - Fetch paraminfo for multiple modules without initializing beforehand. + """Get paraminfo for multiple modules without initializing beforehand. :param modules: API modules to load and which haven't been loaded yet. """ diff --git a/pywikibot/data/api/_requests.py b/pywikibot/data/api/_requests.py index 0e84593b73..66797496d4 100644 --- a/pywikibot/data/api/_requests.py +++ b/pywikibot/data/api/_requests.py @@ -147,8 +147,7 @@ def __init__(self, site=None, retry_wait: int | None = None, use_get: bool | None = None, parameters=_PARAM_DEFAULT, **kwargs) -> None: - """ - Create a new Request instance with the given parameters. + """Create a new Request instance with the given parameters. The parameters for the request can be defined via either the 'parameters' parameter or the keyword arguments. The keyword arguments @@ -284,8 +283,7 @@ def _warn_kwargs(cls) -> None: @classmethod def clean_kwargs(cls, kwargs: dict) -> dict: - """ - Convert keyword arguments into new parameters mode. + """Convert keyword arguments into new parameters mode. If there are no other arguments in kwargs apart from the used arguments by the class' initializer it'll just return kwargs and otherwise remove @@ -331,8 +329,7 @@ def clean_kwargs(cls, kwargs: dict) -> dict: return kwargs def _format_value(self, value): - """ - Format the MediaWiki API request parameter. + """Format the MediaWiki API request parameter. Converts from Python datatypes to MediaWiki API parameter values. @@ -404,8 +401,7 @@ def iteritems(self): return iter(self.items()) def _add_defaults(self): - """ - Add default parameters to the API request. + """Add default parameters to the API request. This method will only add them once. """ @@ -449,8 +445,7 @@ def _add_defaults(self): self.__defaulted = True # skipcq: PTC-W0037 def _encoded_items(self) -> dict[str, str | bytes]: - """ - Build a dict of params with minimal encoding needed for the site. + """Build a dict of params with minimal encoding needed for the site. This helper method only prepares params for serialisation or transmission, so it only encodes values which are not ASCII, @@ -496,8 +491,7 @@ def _encoded_items(self) -> dict[str, str | bytes]: return params def _http_param_string(self): - """ - Return the parameters as a HTTP URL query fragment. + """Return the parameters as a HTTP URL query fragment. URL encodes the parameters provided by _encoded_items() @@ -615,8 +609,7 @@ def _use_get(self): @classmethod def _build_mime_request(cls, params: dict, mime_params: dict) -> tuple[dict, bytes]: - """ - Construct a MIME multipart form post. + """Construct a MIME multipart form post. :param params: HTTP request params :param mime_params: HTTP request parts which must be sent in the body @@ -1187,8 +1180,7 @@ def create_simple(cls, req_site, **kwargs): @classmethod def _get_cache_dir(cls) -> Path: - """ - Return the base directory path for cache entries. + """Return the base directory path for cache entries. The directory will be created if it does not already exist. diff --git a/pywikibot/data/memento.py b/pywikibot/data/memento.py index 9433e4282d..1a26d5b70d 100644 --- a/pywikibot/data/memento.py +++ b/pywikibot/data/memento.py @@ -224,8 +224,7 @@ def is_memento(uri: str, response: requests.Response | None = None, session: requests.Session | None = None, timeout: int | None = None) -> bool: - """ - Determines if the URI given is indeed a Memento. + """Determines if the URI given is indeed a Memento. The simple case is to look for a Memento-Datetime header in the request, but not all archives are Memento-compliant yet. diff --git a/pywikibot/data/sparql.py b/pywikibot/data/sparql.py index 69e26e8dac..addf5215f1 100644 --- a/pywikibot/data/sparql.py +++ b/pywikibot/data/sparql.py @@ -28,6 +28,7 @@ class SparqlQuery(WaitingMixin): + """SPARQL Query class. This class allows to run SPARQL queries against any SPARQL endpoint. @@ -42,8 +43,7 @@ def __init__(self, entity_url: str | None = None, repo=None, max_retries: int | None = None, retry_wait: float | None = None) -> None: - """ - Create endpoint. + """Create endpoint. :param endpoint: SPARQL endpoint URL :param entity_url: URL prefix for any entities returned in a query. @@ -89,8 +89,7 @@ def __init__(self, self.retry_wait = retry_wait def get_last_response(self): - """ - Return last received response. + """Return last received response. :return: Response object from last request or None """ @@ -101,8 +100,7 @@ def select(self, full_data: bool = False, headers: dict[str, str] | None = None ) -> list[dict[str, str]] | None: - """ - Run SPARQL query and return the result. + """Run SPARQL query and return the result. The response is assumed to be in format defined by: https://www.w3.org/TR/2013/REC-sparql11-results-json-20130321/ @@ -184,8 +182,7 @@ def query(self, query: str, headers: dict[str, str] | None = None): def ask(self, query: str, headers: dict[str, str] | None = None) -> bool: - """ - Run SPARQL ASK query and return boolean result. + """Run SPARQL ASK query and return boolean result. :param query: Query text """ @@ -195,8 +192,7 @@ def ask(self, query: str, return data['boolean'] def get_items(self, query, item_name: str = 'item', result_type=set): - """ - Retrieve items which satisfy given query. + """Retrieve items which satisfy given query. Items are returned as Wikibase IDs. @@ -216,6 +212,7 @@ def get_items(self, query, item_name: str = 'item', result_type=set): class SparqlNode: + """Base class for SPARQL nodes.""" def __init__(self, value) -> None: @@ -227,6 +224,7 @@ def __str__(self) -> str: class URI(SparqlNode): + """Representation of URI result type.""" def __init__(self, data: dict, entity_url, **kwargs) -> None: @@ -235,8 +233,7 @@ def __init__(self, data: dict, entity_url, **kwargs) -> None: self.entity_url = entity_url def getID(self): # noqa: N802 - """ - Get ID of Wikibase object identified by the URI. + """Get ID of Wikibase object identified by the URI. :return: ID of Wikibase object, e.g. Q1234 """ @@ -249,6 +246,7 @@ def __repr__(self) -> str: class Literal(SparqlNode): + """Representation of RDF literal result type.""" def __init__(self, data: dict, **kwargs) -> None: @@ -266,6 +264,7 @@ def __repr__(self) -> str: class Bnode(SparqlNode): + """Representation of blank node.""" def __init__(self, data: dict, **kwargs) -> None: diff --git a/pywikibot/data/superset.py b/pywikibot/data/superset.py index 6e223df1b4..101b96d89e 100644 --- a/pywikibot/data/superset.py +++ b/pywikibot/data/superset.py @@ -23,6 +23,7 @@ class SupersetQuery(WaitingMixin): + """Superset Query class. This class allows to run SQL queries against wikimedia superset diff --git a/pywikibot/data/wikistats.py b/pywikibot/data/wikistats.py index 23b2547869..4c6a0c08fa 100644 --- a/pywikibot/data/wikistats.py +++ b/pywikibot/data/wikistats.py @@ -20,8 +20,7 @@ class WikiStats: - """ - Light wrapper around WikiStats data, caching responses and data. + """Light wrapper around WikiStats data, caching responses and data. The methods accept a Pywikibot family name as the WikiStats table name, mapping the names before calling the WikiStats API. @@ -114,8 +113,7 @@ def get_dict(self, table: str) -> dict: def sorted(self, table: str, key: str, reverse: bool | None = None) -> list: - """ - Reverse numerical sort of data. + """Reverse numerical sort of data. :param table: name of table of data :param key: data table key diff --git a/pywikibot/date.py b/pywikibot/date.py index 62f64908ad..3a97e7579f 100644 --- a/pywikibot/date.py +++ b/pywikibot/date.py @@ -116,8 +116,7 @@ def dh_noConv(value: int, pattern: str, limit: Callable[[int], bool]) -> str: def dh_dayOfMnth(value: int, pattern: str) -> str: - """ - Helper for decoding a single integer value. + """Helper for decoding a single integer value. The single integer should be <=31, no conversion, no rounding (used in days of month). @@ -127,8 +126,7 @@ def dh_dayOfMnth(value: int, pattern: str) -> str: def dh_mnthOfYear(value: int, pattern: str) -> str: - """ - Helper for decoding a single integer value. + """Helper for decoding a single integer value. The value should be >=1000, no conversion, no rounding (used in month of the year) @@ -137,8 +135,7 @@ def dh_mnthOfYear(value: int, pattern: str) -> str: def dh_decAD(value: int, pattern: str) -> str: - """ - Helper for decoding a single integer value. + """Helper for decoding a single integer value. It should be no conversion, round to decimals (used in decades) """ @@ -147,8 +144,7 @@ def dh_decAD(value: int, pattern: str) -> str: def dh_decBC(value: int, pattern: str) -> str: - """ - Helper for decoding a single integer value. + """Helper for decoding a single integer value. It should be no conversion, round to decimals (used in decades) """ @@ -276,8 +272,7 @@ def _(value: str, ind: int, match: str) -> int: def alwaysTrue(x: Any) -> bool: - """ - Return True, always. + """Return True, always. Used for multiple value selection function to accept all other values. @@ -1963,8 +1958,7 @@ def _format_limit_dom(days: int) -> tuple[Callable[[int], bool], int, int]: def getAutoFormat(lang: str, title: str, ignoreFirstLetterCase: bool = True ) -> tuple[str | None, str | None]: - """ - Return first matching formatted date value. + """Return first matching formatted date value. :param lang: language code :param title: value to format @@ -2022,8 +2016,7 @@ def formatYear(lang: str, year: int) -> str: def apply_month_delta(date: datetime.date, month_delta: int = 1, add_overlap: bool = False) -> datetime.date: - """ - Add or subtract months from the date. + """Add or subtract months from the date. By default if the new month has less days then the day of the date it chooses the last day in the new month. For example a date in the March 31st @@ -2055,8 +2048,7 @@ def apply_month_delta(date: datetime.date, month_delta: int = 1, def get_month_delta(date1: datetime.date, date2: datetime.date) -> int: - """ - Return the difference between two dates in months. + """Return the difference between two dates in months. It does only work on calendars with 12 months per year, and where the months are consecutive and non-negative numbers. diff --git a/pywikibot/diff.py b/pywikibot/diff.py index 49f5f72051..c5c1ab3c1c 100644 --- a/pywikibot/diff.py +++ b/pywikibot/diff.py @@ -43,8 +43,7 @@ def __init__(self, a: str | Sequence[str], b: str | Sequence[str], grouped_opcode: Sequence[tuple[str, int, int, int, int]] ) -> None: - """ - Initializer. + """Initializer. :param a: sequence of lines :param b: sequence of lines diff --git a/pywikibot/editor.py b/pywikibot/editor.py index 7b470780c1..ec42bdd0c8 100644 --- a/pywikibot/editor.py +++ b/pywikibot/editor.py @@ -101,8 +101,7 @@ def _concat(command: Sequence[str]) -> str: def edit(self, text: str, jumpIndex: int | None = None, highlight: str | None = None) -> str | None: - """ - Call the editor and thus allows the user to change the text. + """Call the editor and thus allows the user to change the text. Halts the thread's operation until the editor is closed. diff --git a/pywikibot/exceptions.py b/pywikibot/exceptions.py index ec403e20df..de528b4611 100644 --- a/pywikibot/exceptions.py +++ b/pywikibot/exceptions.py @@ -262,8 +262,7 @@ class UploadError(APIError): def __init__(self, code: str, message: str, file_key: str | None = None, offset: int | bool = 0) -> None: - """ - Create a new UploadError instance. + """Create a new UploadError instance. :param file_key: The file_key of the uploaded file to reuse it later. If no key is known or it is an incomplete file it may be None. @@ -282,8 +281,7 @@ def message(self) -> str: class PageRelatedError(Error): - """ - Abstract Exception, used when the exception concerns a particular Page. + """Abstract Exception, used when the exception concerns a particular Page. This class should be used when the Exception concerns a particular Page, and when a generic message can be written once for all. @@ -295,8 +293,7 @@ class PageRelatedError(Error): def __init__(self, page: pywikibot.page.BasePage, message: str | None = None) -> None: - """ - Initializer. + """Initializer. :param page: Page that caused the exception """ @@ -479,8 +476,7 @@ class CircularRedirectError(PageRelatedError): class InterwikiRedirectPageError(PageRelatedError): - """ - Page is a redirect to another site. + """Page is a redirect to another site. This is considered invalid in Pywikibot. See bug :phab:`T75184`. @@ -699,8 +695,7 @@ class NoWikibaseEntityError(WikiBaseError): """This entity doesn't exist.""" def __init__(self, entity: pywikibot.page.WikibaseEntity) -> None: - """ - Initializer. + """Initializer. :param entity: Wikibase entity """ diff --git a/pywikibot/families/wikiquote_family.py b/pywikibot/families/wikiquote_family.py index a1e2e2f9ef..1a032a039b 100644 --- a/pywikibot/families/wikiquote_family.py +++ b/pywikibot/families/wikiquote_family.py @@ -70,8 +70,7 @@ class Family(family.SubdomainFamily, family.WikimediaFamily): } def encodings(self, code): - """ - Return a list of historical encodings for a specific language. + """Return a list of historical encodings for a specific language. :param code: site code """ diff --git a/pywikibot/family.py b/pywikibot/family.py index 6cb4ece163..673f1715a8 100644 --- a/pywikibot/family.py +++ b/pywikibot/family.py @@ -482,8 +482,7 @@ def protocol(self, code: str) -> str: return 'https' def verify_SSL_certificate(self, code: str) -> bool: - """ - Return whether a HTTPS certificate should be verified. + """Return whether a HTTPS certificate should be verified. .. versionadded:: 5.3 renamed from ignore_certificate_error @@ -535,8 +534,7 @@ def _hostname(self, code, protocol=None): return protocol, host def base_url(self, code: str, uri: str, protocol=None) -> str: - """ - Prefix uri with port and hostname. + """Prefix uri with port and hostname. :param code: The site code :param uri: The absolute path after the hostname @@ -712,8 +710,7 @@ def isPublic(self, code) -> bool: return True def post_get_convert(self, site, getText): - """ - Do a conversion on the retrieved text from the Wiki. + """Do a conversion on the retrieved text from the Wiki. For example a :wiki:`X-conversion in Esperanto `. @@ -721,8 +718,7 @@ def post_get_convert(self, site, getText): return getText def pre_put_convert(self, site, putText): - """ - Do a conversion on the text to insert on the Wiki. + """Do a conversion on the text to insert on the Wiki. For example a :wiki:`X-conversion in Esperanto `. @@ -731,8 +727,7 @@ def pre_put_convert(self, site, putText): @property def obsolete(self) -> types.MappingProxyType[str, str | None]: - """ - Old codes that are not part of the family. + """Old codes that are not part of the family. Interwiki replacements override removals for the same code. @@ -744,8 +739,7 @@ def obsolete(self) -> types.MappingProxyType[str, str | None]: @classproperty def domains(cls) -> set[str]: - """ - Get list of unique domain names included in this family. + """Get list of unique domain names included in this family. These domains may also exist in another family. """ @@ -1165,8 +1159,7 @@ def globes(self, code): def AutoFamily(name: str, url: str) -> SingleSiteFamily: - """ - Family that automatically loads the site configuration. + """Family that automatically loads the site configuration. :param name: Name for the family :param url: API endpoint URL of the wiki diff --git a/pywikibot/flow.py b/pywikibot/flow.py index 1e1db84a58..614800d456 100644 --- a/pywikibot/flow.py +++ b/pywikibot/flow.py @@ -423,8 +423,7 @@ def __init__(self, page: Topic, uuid: str) -> None: @classmethod def fromJSON(cls, page: Topic, post_uuid: str, # noqa: N802 data: dict[str, Any]) -> Post: - """ - Create a Post object using the data returned from the API call. + """Create a Post object using the data returned from the API call. :param page: A Flow topic :param post_uuid: The UUID of the post diff --git a/pywikibot/i18n.py b/pywikibot/i18n.py index f3ac0fb866..e07f0ddc3a 100644 --- a/pywikibot/i18n.py +++ b/pywikibot/i18n.py @@ -368,8 +368,7 @@ def set_messages_package(package_name: str) -> None: def messages_available() -> bool: - """ - Return False if there are no i18n messages available. + """Return False if there are no i18n messages available. To determine if messages are available, it looks for the package name set using :py:obj:`set_messages_package` for a message bundle called @@ -438,8 +437,7 @@ def _get_bundle(lang: str, dirname: str) -> dict[str, str]: def _get_translation(lang: str, twtitle: str) -> str | None: - """ - Return message of certain twtitle if exists. + """Return message of certain twtitle if exists. For internal use, don't use it directly. """ @@ -515,8 +513,7 @@ def replace_plural(match: Match[str]) -> str: class _PluralMappingAlias(abc.Mapping): - """ - Aliasing class to allow non mappings in _extract_plural. + """Aliasing class to allow non mappings in _extract_plural. That function only uses __getitem__ so this is only implemented here. """ @@ -699,8 +696,7 @@ def twtranslate( only_plural: bool = False, bot_prefix: bool = False ) -> str | None: - r""" - Translate a message using JSON files in messages_package_name. + r"""Translate a message using JSON files in messages_package_name. fallback parameter must be True for i18n and False for L10N or testing purposes. @@ -823,8 +819,7 @@ def twtranslate( def twhas_key(source: str | pywikibot.site.BaseSite, twtitle: str) -> bool: - """ - Check if a message has a translation in the specified language code. + """Check if a message has a translation in the specified language code. The translations are retrieved from i18n., based on the callers import table. @@ -842,8 +837,7 @@ def twhas_key(source: str | pywikibot.site.BaseSite, twtitle: str) -> bool: def twget_keys(twtitle: str) -> list[str]: - """ - Return all language codes for a special message. + """Return all language codes for a special message. :param twtitle: The TranslateWiki string title, in - format @@ -938,8 +932,7 @@ def input(twtitle: str, parameters: Mapping[str, int] | None = None, password: bool = False, fallback_prompt: str | None = None) -> str: - """ - Ask the user a question, return the user's answer. + """Ask the user a question, return the user's answer. The prompt message is retrieved via :py:obj:`twtranslate` and uses the config variable 'userinterface_lang'. diff --git a/pywikibot/interwiki_graph.py b/pywikibot/interwiki_graph.py index 3efacb522a..e19c8a546c 100644 --- a/pywikibot/interwiki_graph.py +++ b/pywikibot/interwiki_graph.py @@ -23,8 +23,7 @@ class GraphSavingThread(threading.Thread): - """ - Threaded graph renderer. + """Threaded graph renderer. Rendering a graph can take extremely long. We use multithreading because of that. @@ -176,8 +175,7 @@ def saveGraphFile(self) -> None: thread.start() def createGraph(self) -> None: - """ - Create graph of the interwiki links. + """Create graph of the interwiki links. For more info see https://meta.wikimedia.org/wiki/Interwiki_graphs """ @@ -202,8 +200,7 @@ def createGraph(self) -> None: def getFilename(page: pywikibot.page.Page, extension: str | None = None) -> str: - """ - Create a filename that is unique for the page. + """Create a filename that is unique for the page. :param page: page used to create the new filename :param extension: file extension diff --git a/pywikibot/logentries.py b/pywikibot/logentries.py index 8442df3594..642e974982 100644 --- a/pywikibot/logentries.py +++ b/pywikibot/logentries.py @@ -104,8 +104,7 @@ def params(self) -> dict[str, Any]: @cached def page(self) -> int | pywikibot.page.Page: - """ - Page on which action was performed. + """Page on which action was performed. :return: page on action was performed """ @@ -140,8 +139,7 @@ def page(self) -> pywikibot.page.User: class BlockEntry(LogEntry): - """ - Block or unblock log entry. + """Block or unblock log entry. It might contain a block or unblock depending on the action. The duration, expiry and flags are not available on unblock log entries. @@ -161,8 +159,7 @@ def __init__(self, apidata: dict[str, Any], self._blockid = int(self['title'][pos + 1:]) def page(self) -> int | pywikibot.page.Page: - """ - Return the blocked account or IP. + """Return the blocked account or IP. :return: the Page object of username or IP if this block action targets a username or IP, or the blockid if this log reflects @@ -176,8 +173,7 @@ def page(self) -> int | pywikibot.page.Page: @cached def flags(self) -> list[str]: - """ - Return a list of (str) flags associated with the block entry. + """Return a list of (str) flags associated with the block entry. It raises an Error if the entry is an unblocking log entry. @@ -190,8 +186,7 @@ def flags(self) -> list[str]: @cached def duration(self) -> datetime.timedelta | None: - """ - Return a datetime.timedelta representing the block duration. + """Return a datetime.timedelta representing the block duration. :return: datetime.timedelta, or None if block is indefinite. """ @@ -299,8 +294,7 @@ def auto(self) -> bool: class LogEntryFactory: - """ - LogEntry Factory. + """LogEntry Factory. Only available method is create() """ @@ -315,8 +309,7 @@ class LogEntryFactory: def __init__(self, site: pywikibot.site.BaseSite, logtype: str | None = None) -> None: - """ - Initializer. + """Initializer. :param site: The site on which the log entries are created. :param logtype: The log type of the log entries, if known in advance. @@ -333,8 +326,7 @@ def __init__(self, site: pywikibot.site.BaseSite, self._creator = lambda data: logclass(data, self._site) def create(self, logdata: dict[str, Any]) -> LogEntry: - """ - Instantiate the LogEntry object representing logdata. + """Instantiate the LogEntry object representing logdata. :param logdata: returned by the api @@ -343,8 +335,7 @@ def create(self, logdata: dict[str, Any]) -> LogEntry: return self._creator(logdata) def get_valid_entry_class(self, logtype: str) -> LogEntry: - """ - Return the class corresponding to the @logtype string parameter. + """Return the class corresponding to the @logtype string parameter. :return: specified subclass of LogEntry :raise KeyError: logtype is not valid @@ -356,8 +347,7 @@ def get_valid_entry_class(self, logtype: str) -> LogEntry: @classmethod def get_entry_class(cls, logtype: str) -> LogEntry: - """ - Return the class corresponding to the @logtype string parameter. + """Return the class corresponding to the @logtype string parameter. :return: specified subclass of LogEntry @@ -384,8 +374,7 @@ def get_entry_class(cls, logtype: str) -> LogEntry: return cls._logtypes[logtype] def _create_from_data(self, logdata: dict[str, Any]) -> LogEntry: - """ - Check for logtype from data, and creates the correct LogEntry. + """Check for logtype from data, and creates the correct LogEntry. :param logdata: log entry data """ diff --git a/pywikibot/login.py b/pywikibot/login.py index c109a21171..2c9c8c6d27 100644 --- a/pywikibot/login.py +++ b/pywikibot/login.py @@ -46,8 +46,7 @@ class _PasswordFileWarning(UserWarning): class LoginStatus(IntEnum): - """ - Enum for Login statuses. + """Enum for Login statuses. >>> LoginStatus.NOT_ATTEMPTED LoginStatus(-3) @@ -80,8 +79,7 @@ class LoginManager: def __init__(self, password: str | None = None, site: pywikibot.site.BaseSite | None = None, user: str | None = None) -> None: - """ - Initializer. + """Initializer. All parameters default to defaults in user-config. @@ -114,8 +112,7 @@ def __init__(self, password: str | None = None, self.readPassword() def check_user_exists(self) -> None: - """ - Check that the username exists on the site. + """Check that the username exists on the site. .. seealso:: :api:`Users` @@ -148,8 +145,7 @@ def check_user_exists(self) -> None: .format(main_username, self.site)) def botAllowed(self) -> bool: - """ - Check whether the bot is listed on a specific page. + """Check whether the bot is listed on a specific page. This allows bots to comply with the policy on the respective wiki. """ @@ -264,8 +260,7 @@ def readPassword(self) -> None: } def login(self, retry: bool = False, autocreate: bool = False) -> bool: - """ - Attempt to log into the server. + """Attempt to log into the server. .. seealso:: :api:`Login` @@ -481,8 +476,7 @@ class BotPassword: """BotPassword object for storage in password file.""" def __init__(self, suffix: str, password: str) -> None: - """ - Initializer. + """Initializer. BotPassword function by using a separate password paired with a suffixed username of the form @. @@ -499,8 +493,7 @@ def __init__(self, suffix: str, password: str) -> None: self.password = password def login_name(self, username: str) -> str: - """ - Construct the login name from the username and suffix. + """Construct the login name from the username and suffix. :param user: username (without suffix) """ @@ -517,8 +510,7 @@ class OauthLoginManager(LoginManager): def __init__(self, password: str | None = None, site: pywikibot.site.BaseSite | None = None, user: str | None = None) -> None: - """ - Initializer. + """Initializer. All parameters default to defaults in user-config. @@ -543,8 +535,7 @@ def __init__(self, password: str | None = None, self._access_token: tuple[str, str] | None = None def login(self, retry: bool = False, force: bool = False) -> bool: - """ - Attempt to log into the server. + """Attempt to log into the server. .. seealso:: :api:`Login` @@ -582,8 +573,7 @@ def login(self, retry: bool = False, force: bool = False) -> bool: @property def consumer_token(self) -> tuple[str, str]: - """ - Return OAuth consumer key token and secret token. + """Return OAuth consumer key token and secret token. .. seealso:: :api:`Tokens` """ @@ -591,8 +581,7 @@ def consumer_token(self) -> tuple[str, str]: @property def access_token(self) -> tuple[str, str] | None: - """ - Return OAuth access key token and secret token. + """Return OAuth access key token and secret token. .. seealso:: :api:`Tokens` """ diff --git a/pywikibot/page/_basepage.py b/pywikibot/page/_basepage.py index 1d675fe091..85d255a68c 100644 --- a/pywikibot/page/_basepage.py +++ b/pywikibot/page/_basepage.py @@ -60,8 +60,7 @@ class BasePage(ComparableMixin): - """ - BasePage: Base object for a MediaWiki page. + """BasePage: Base object for a MediaWiki page. This object only implements internally methods that do not require reading from or writing to the wiki. All other methods are delegated @@ -79,8 +78,7 @@ class BasePage(ComparableMixin): ) def __init__(self, source, title: str = '', ns=0) -> None: - """ - Instantiate a Page object. + """Instantiate a Page object. Three calling formats are supported: @@ -144,8 +142,7 @@ def site(self): return self._link.site def version(self): - """ - Return MediaWiki version number of the page site. + """Return MediaWiki version number of the page site. This is needed to use @need_version() decorator for methods of Page objects. @@ -163,8 +160,7 @@ def data_repository(self): return self.site.data_repository() def namespace(self) -> Namespace: - """ - Return the namespace of the page. + """Return the namespace of the page. :return: namespace of the page """ @@ -172,8 +168,7 @@ def namespace(self) -> Namespace: @property def content_model(self): - """ - Return the content model for this page. + """Return the content model for this page. If it cannot be reliably determined via the API, None is returned. @@ -194,8 +189,7 @@ def depth(self) -> int: @property def pageid(self) -> int: - """ - Return pageid of the page. + """Return pageid of the page. :return: pageid or 0 if page does not exist """ @@ -218,8 +212,7 @@ def title( insite=None, without_brackets: bool = False ) -> str: - """ - Return the title of this Page, as a string. + """Return the title of this Page, as a string. :param underscore: (not used with as_link) if true, replace all ' ' characters with '_' @@ -314,8 +307,7 @@ def __repr__(self) -> str: return f'{self.__class__.__name__}({self.title()!r})' def _cmpkey(self): - """ - Key for comparison of Page objects. + """Key for comparison of Page objects. Page objects are "equal" if and only if they are on the same site and have the same normalized title, including section if any. @@ -325,8 +317,7 @@ def _cmpkey(self): return (self.site, self.namespace(), self.title()) def __hash__(self): - """ - A stable identifier to be used as a key in hash-tables. + """A stable identifier to be used as a key in hash-tables. This relies on the fact that the string representation of an instance cannot change after the construction. @@ -340,8 +331,7 @@ def full_url(self): @cached def autoFormat(self): - """ - Return :py:obj:`date.getAutoFormat` dictName and value, if any. + """Return :py:obj:`date.getAutoFormat` dictName and value, if any. Value can be a year, date, etc., and dictName is 'YearBC', 'Year_December', or another dictionary name. Please note that two @@ -411,8 +401,7 @@ def get(self, force: bool = False, get_redirect: bool = False) -> str: return text def has_content(self) -> bool: - """ - Page has been loaded. + """Page has been loaded. Not existing pages are considered loaded. @@ -428,8 +417,7 @@ def _latest_cached_revision(self): return None def _getInternals(self): - """ - Helper function for get(). + """Helper function for get(). Stores latest revision in self if it doesn't contain it, doesn't think. * Raises exceptions from previous runs. @@ -493,8 +481,7 @@ def latest_revision_id(self): @latest_revision_id.deleter def latest_revision_id(self) -> None: - """ - Remove the latest revision id set for this Page. + """Remove the latest revision id set for this Page. All internal cached values specifically for the latest revision of this page are cleared. @@ -608,8 +595,7 @@ def text(self) -> None: del self._raw_extracted_templates def preloadText(self) -> str: - """ - The text returned by EditFormPreloadText. + """The text returned by EditFormPreloadText. See API module "info". @@ -702,8 +688,7 @@ def extract(self, variant: str = 'plain', *, return '\n'.join(text_lines[:min(lines, len(text_lines))]) def properties(self, force: bool = False) -> dict: - """ - Return the properties of the page. + """Return the properties of the page. :param force: force updating from the live site """ @@ -713,8 +698,7 @@ def properties(self, force: bool = False) -> dict: return self._pageprops def defaultsort(self, force: bool = False) -> str | None: - """ - Extract value of the {{DEFAULTSORT:}} magic word from the page. + """Extract value of the {{DEFAULTSORT:}} magic word from the page. :param force: force updating from the live site """ @@ -765,8 +749,7 @@ def isIpEdit(self) -> bool: @cached def lastNonBotUser(self) -> str | None: - """ - Return name or IP address of last human/non-bot user to edit page. + """Return name or IP address of last human/non-bot user to edit page. Determine the most recent human editor out of the last revisions. If it was not able to retrieve a human user, returns None. @@ -887,8 +870,7 @@ def isTalkPage(self): return ns >= 0 and ns % 2 == 1 def toggleTalkPage(self) -> pywikibot.Page | None: - """ - Return other member of the article-talk page pair for this Page. + """Return other member of the article-talk page pair for this Page. If self is a talk page, returns the associated content page; otherwise, returns the associated talk page. The returned page need @@ -986,8 +968,7 @@ def getReferences(self, namespaces=None, total: int | None = None, content: bool = False) -> Iterable[pywikibot.Page]: - """ - Return an iterator all pages that refer to or embed the page. + """Return an iterator all pages that refer to or embed the page. If you need a full list of referring pages, use ``pages = list(s.getReferences())`` @@ -1026,8 +1007,7 @@ def backlinks(self, namespaces=None, total: int | None = None, content: bool = False) -> Iterable[pywikibot.Page]: - """ - Return an iterator for pages that link to this page. + """Return an iterator for pages that link to this page. :param follow_redirects: if True, also iterate pages that link to a redirect pointing to the page. @@ -1052,8 +1032,7 @@ def embeddedin(self, namespaces=None, total: int | None = None, content: bool = False) -> Iterable[pywikibot.Page]: - """ - Return an iterator for pages that embed this page as a template. + """Return an iterator for pages that embed this page as a template. :param filter_redirects: if True, only iterate redirects; if False, omit redirects; if None, do not filter @@ -1078,8 +1057,7 @@ def redirects( total: int | None = None, content: bool = False ) -> Iterable[pywikibot.Page]: - """ - Return an iterable of redirects to this page. + """Return an iterable of redirects to this page. :param filter_fragments: If True, only return redirects with fragments. If False, only return redirects without fragments. If None, return @@ -1161,8 +1139,7 @@ def has_permission(self, action: str = 'edit') -> bool: return self.site.page_can_be_edited(self, action) def botMayEdit(self) -> bool: - """ - Determine whether the active bot is allowed to edit the page. + """Determine whether the active bot is allowed to edit the page. This will be True if the page doesn't contain {{bots}} or {{nobots}} or any other template from edit_restricted_templates list @@ -1458,8 +1435,7 @@ def put(self, newtext: str, **kwargs) def watch(self, unwatch: bool = False) -> bool: - """ - Add or remove this page to/from bot account's watchlist. + """Add or remove this page to/from bot account's watchlist. :param unwatch: True to unwatch, False (default) to watch. :return: True if successful, False otherwise. @@ -1474,8 +1450,7 @@ def clear_cache(self) -> None: delattr(self, attr) def purge(self, **kwargs) -> bool: - """ - Purge the server's cache for this page. + """Purge the server's cache for this page. :keyword redirects: Automatically resolve redirects. :type redirects: bool @@ -1570,8 +1545,7 @@ def interwiki( self, expand: bool = True, ) -> Generator[pywikibot.page.Link, None, None]: - """ - Yield interwiki links in the page text, excluding language links. + """Yield interwiki links in the page text, excluding language links. :param expand: if True (default), include interwiki links found in templates transcluded onto this page; if False, only iterate @@ -1602,8 +1576,7 @@ def langlinks( self, include_obsolete: bool = False, ) -> list[pywikibot.Link]: - """ - Return a list of all inter-language Links on this page. + """Return a list of all inter-language Links on this page. :param include_obsolete: if true, return even Link objects whose site is obsolete @@ -1733,8 +1706,7 @@ def imagelinks( total: int | None = None, content: bool = False, ) -> Iterable[pywikibot.FilePage]: - """ - Iterate FilePage objects for images displayed on this Page. + """Iterate FilePage objects for images displayed on this Page. :param total: iterate no more than this number of pages in total :param content: if True, retrieve the content of the current version @@ -1749,8 +1721,7 @@ def categories( total: int | None = None, content: bool = False, ) -> Iterable[pywikibot.Page]: - """ - Iterate categories that the article is in. + """Iterate categories that the article is in. :param with_sort_key: if True, include the sort key in each Category. :param total: iterate no more than this number of pages in total @@ -1775,8 +1746,7 @@ def categories( return self.site.pagecategories(self, total=total, content=content) def extlinks(self, total: int | None = None) -> Iterable[str]: - """ - Iterate all external URLs (not interwiki links) from this page. + """Iterate all external URLs (not interwiki links) from this page. :param total: iterate no more than this number of pages in total :return: a generator that yields str objects containing URLs. @@ -1784,8 +1754,7 @@ def extlinks(self, total: int | None = None) -> Iterable[str]: return self.site.page_extlinks(self, total=total) def coordinates(self, primary_only: bool = False): - """ - Return a list of Coordinate objects for points on the page. + """Return a list of Coordinate objects for points on the page. Uses the MediaWiki extension GeoData. @@ -1805,8 +1774,7 @@ def coordinates(self, primary_only: bool = False): return list(self._coords) def page_image(self): - """ - Return the most appropriate image on the page. + """Return the most appropriate image on the page. Uses the MediaWiki extension PageImages. @@ -1907,8 +1875,7 @@ def getVersionHistoryTable(self, def contributors(self, total: int | None = None, starttime=None, endtime=None): - """ - Compile contributors of this page with edit counts. + """Compile contributors of this page with edit counts. :param total: iterate no more than this number of revisions in total :param starttime: retrieve revisions starting at this Timestamp @@ -1967,8 +1934,7 @@ def move(self, movetalk: bool = True, noredirect: bool = False, movesubpages: bool = True) -> pywikibot.page.Page: - """ - Move this page to a new title. + """Move this page to a new title. .. versionchanged:: 7.2 The *movesubpages* parameter was added @@ -1997,8 +1963,7 @@ def delete( *, deletetalk: bool = False ) -> int: - """ - Delete the page from the wiki. Requires administrator status. + """Delete the page from the wiki. Requires administrator status. .. versionchanged:: 7.1 keyword only parameter *deletetalk* was added. @@ -2229,8 +2194,7 @@ def change_category(self, old_cat, new_cat, in_place: bool = True, include: list[str] | None = None, show_diff: bool = False) -> bool: - """ - Remove page from oldCat and add it to newCat. + """Remove page from oldCat and add it to newCat. .. versionadded:: 7.0 The `show_diff` parameter diff --git a/pywikibot/page/_category.py b/pywikibot/page/_category.py index 9bda6cd6e4..95b0993ccf 100644 --- a/pywikibot/page/_category.py +++ b/pywikibot/page/_category.py @@ -22,8 +22,7 @@ class Category(Page): """A page in the Category: namespace.""" def __init__(self, source, title: str = '', sort_key=None) -> None: - """ - Initializer. + """Initializer. All parameters are the same as for Page() Initializer. """ @@ -138,8 +137,7 @@ def articles(self, *, recurse: int | bool = False, total: int | None = None, **kwargs: Any) -> Generator[Page, None, None]: - """ - Yield all articles in the current category. + """Yield all articles in the current category. Yields all pages in the category that are not subcategories. Duplicates are filtered. To enable duplicates use :meth:`members` @@ -289,8 +287,7 @@ def newest_pages( self, total: int | None = None ) -> Generator[Page, None, None]: - """ - Return pages in a category ordered by the creation date. + """Return pages in a category ordered by the creation date. If two or more pages are created at the same time, the pages are returned in the order they were added to the category. The most diff --git a/pywikibot/page/_collections.py b/pywikibot/page/_collections.py index ee0edebd6b..17c0ad826e 100644 --- a/pywikibot/page/_collections.py +++ b/pywikibot/page/_collections.py @@ -25,8 +25,7 @@ class BaseDataDict(MutableMapping): - """ - Base structure holding data for a Wikibase entity. + """Base structure holding data for a Wikibase entity. Data are mappings from a language to a value. It will be specialised in subclasses. @@ -82,8 +81,7 @@ def normalizeKey(key) -> str: class LanguageDict(BaseDataDict): - """ - A structure holding language data for a Wikibase entity. + """A structure holding language data for a Wikibase entity. Language data are mappings from a language to a string. It can be labels, descriptions and others. @@ -133,8 +131,7 @@ def toJSON(self, diffto: dict | None = None) -> dict: class AliasesDict(BaseDataDict): - """ - A structure holding aliases for a Wikibase entity. + """A structure holding aliases for a Wikibase entity. It is a mapping from a language to a list of strings. """ @@ -200,6 +197,7 @@ def toJSON(self, diffto: dict | None = None) -> dict: class ClaimCollection(MutableMapping): + """A structure holding claims for a Wikibase entity.""" def __init__(self, repo) -> None: @@ -319,11 +317,11 @@ def set_on_item(self, item) -> None: class SiteLinkCollection(MutableMapping): + """A structure holding SiteLinks for a Wikibase item.""" def __init__(self, repo, data=None) -> None: - """ - Initializer. + """Initializer. :param repo: the Wikibase site on which badges are defined :type repo: pywikibot.site.DataSite @@ -346,8 +344,7 @@ def fromJSON(cls, data, repo): @staticmethod def getdbName(site): - """ - Helper function to obtain a dbName for a Site. + """Helper function to obtain a dbName for a Site. :param site: The site to look up. :type site: pywikibot.site.BaseSite or str @@ -357,8 +354,7 @@ def getdbName(site): return site def __getitem__(self, key): - """ - Get the SiteLink with the given key. + """Get the SiteLink with the given key. :param key: site key as Site instance or db key :type key: pywikibot.Site or str @@ -380,8 +376,7 @@ def __setitem__( key: str | pywikibot.site.APISite, val: str | dict[str, Any] | pywikibot.page.SiteLink, ) -> None: - """ - Set the SiteLink for a given key. + """Set the SiteLink for a given key. This only sets the value given as str, dict or SiteLink. If a str or dict is given the SiteLink object is created later in @@ -424,8 +419,7 @@ def _extract_json(cls, obj): @classmethod def normalizeData(cls, data) -> dict: - """ - Helper function to expand data into the Wikibase API structure. + """Helper function to expand data into the Wikibase API structure. :param data: Data to normalize :type data: list or dict @@ -456,8 +450,7 @@ def normalizeData(cls, data) -> dict: return norm_data def toJSON(self, diffto: dict | None = None) -> dict: - """ - Create JSON suitable for Wikibase API. + """Create JSON suitable for Wikibase API. When diffto is provided, JSON representing differences to the provided data is created. @@ -501,8 +494,7 @@ class SubEntityCollection(MutableSequence): """Ordered collection of sub-entities indexed by their ids.""" def __init__(self, repo, data=None): - """ - Initializer. + """Initializer. :param repo: Wikibase site :type repo: pywikibot.site.DataSite @@ -570,8 +562,7 @@ def fromJSON(cls, data, repo): @classmethod def normalizeData(cls, data: list) -> dict: - """ - Helper function to expand data into the Wikibase API structure. + """Helper function to expand data into the Wikibase API structure. :param data: Data to normalize :type data: list @@ -581,8 +572,7 @@ def normalizeData(cls, data: list) -> dict: raise NotImplementedError # TODO def toJSON(self, diffto: dict | None = None) -> dict: - """ - Create JSON suitable for Wikibase API. + """Create JSON suitable for Wikibase API. When diffto is provided, JSON representing differences to the provided data is created. diff --git a/pywikibot/page/_filepage.py b/pywikibot/page/_filepage.py index d91c1819d2..d8b1b303d4 100644 --- a/pywikibot/page/_filepage.py +++ b/pywikibot/page/_filepage.py @@ -74,8 +74,7 @@ def __init__(self, source, title: str = '', *, ) def _load_file_revisions(self, imageinfo) -> None: - """ - Store an Image revision of FilePage (a FileInfo object) in local cache. + """Save a file revision of FilePage (a FileInfo object) in local cache. Metadata shall be added lazily to the revision already present in cache. @@ -97,8 +96,7 @@ def _load_file_revisions(self, imageinfo) -> None: @property def latest_file_info(self): - """ - Retrieve and store information of latest Image rev. of FilePage. + """Retrieve and store information of latest Image rev. of FilePage. At the same time, the whole history of Image is fetched and cached in self._file_revisions @@ -112,8 +110,7 @@ def latest_file_info(self): @property def oldest_file_info(self): - """ - Retrieve and store information of oldest Image rev. of FilePage. + """Retrieve and store information of oldest Image rev. of FilePage. At the same time, the whole history of Image is fetched and cached in self._file_revisions @@ -126,8 +123,7 @@ def oldest_file_info(self): return self._file_revisions[oldest_ts] def get_file_info(self, ts) -> dict: - """ - Retrieve and store information of a specific Image rev. of FilePage. + """Retrieve and store information of a specific Image rev. of FilePage. This function will load also metadata. It is also used as a helper in FileInfo to load metadata lazily. @@ -142,8 +138,7 @@ def get_file_info(self, ts) -> dict: return self._file_revisions[ts] def get_file_history(self) -> dict: - """ - Return the file's version history. + """Return the file's version history. :return: dictionary with: key: timestamp of the entry @@ -285,8 +280,7 @@ def file_is_used(self) -> bool: return bool(list(self.using_pages(total=1))) def upload(self, source: str, **kwargs) -> bool: - """ - Upload this file to the wiki. + """Upload this file to the wiki. keyword arguments are from site.upload() method. @@ -425,8 +419,7 @@ def download(self, return False def globalusage(self, total=None): - """ - Iterate all global usage for this page. + """Iterate all global usage for this page. .. seealso:: :meth:`using_pages` @@ -438,8 +431,7 @@ def globalusage(self, total=None): return self.site.globalusage(self, total=total) def data_item(self): - """ - Convenience function to get the associated Wikibase item of the file. + """Function to get the associated Wikibase item of the file. If WikibaseMediaInfo extension is available (e.g., on Commons), the method returns the associated mediainfo entity. Otherwise, @@ -460,8 +452,7 @@ def data_item(self): class FileInfo: - """ - A structure holding imageinfo of latest rev. of FilePage. + """A structure holding imageinfo of latest rev. of FilePage. All keys of API imageinfo dictionary are mapped to FileInfo attributes. Attributes can be retrieved both as self['key'] or self.key. diff --git a/pywikibot/page/_links.py b/pywikibot/page/_links.py index 4373132d06..335067415f 100644 --- a/pywikibot/page/_links.py +++ b/pywikibot/page/_links.py @@ -34,8 +34,7 @@ class BaseLink(ComparableMixin): - """ - A MediaWiki link (local or interwiki). + """A MediaWiki link (local or interwiki). Has the following attributes: @@ -49,8 +48,7 @@ class BaseLink(ComparableMixin): _items = ('title', 'namespace', '_sitekey') def __init__(self, title: str, namespace=None, site=None) -> None: - """ - Initializer. + """Initializer. :param title: the title of the page linked to (str); does not include namespace or section @@ -86,8 +84,7 @@ def __repr__(self) -> str: return f"pywikibot.page.{type(self).__name__}({', '.join(attrs)})" def lookup_namespace(self): - """ - Look up the namespace given the provided namespace id or name. + """Look up the namespace given the provided namespace id or name. :rtype: pywikibot.Namespace """ @@ -112,8 +109,7 @@ def lookup_namespace(self): @property def site(self): - """ - Return the site of the link. + """Return the site of the link. :rtype: pywikibot.Site """ @@ -123,8 +119,7 @@ def site(self): @property def namespace(self): - """ - Return the namespace of the link. + """Return the namespace of the link. :rtype: pywikibot.Namespace """ @@ -140,8 +135,7 @@ def canonical_title(self) -> str: return self.title def ns_title(self, onsite=None): - """ - Return full page title, including namespace. + """Return full page title, including namespace. :param onsite: site object if specified, present title using onsite local namespace, @@ -169,8 +163,7 @@ def ns_title(self, onsite=None): return self.title def astext(self, onsite=None) -> str: - """ - Return a text representation of the link. + """Return a text representation of the link. :param onsite: if specified, present as a (possibly interwiki) link from the given site; otherwise, present as an internal link on @@ -192,8 +185,7 @@ def astext(self, onsite=None) -> str: return f'[[{self.site.sitename}:{title}]]' def _cmpkey(self): - """ - Key for comparison of BaseLink objects. + """Key for comparison of BaseLink objects. BaseLink objects are "equal" if and only if they are on the same site and have the same normalized title. @@ -212,8 +204,7 @@ def __hash__(self): @classmethod def fromPage(cls, page): # noqa: N802 - """ - Create a BaseLink to a Page. + """Create a BaseLink to a Page. :param page: target pywikibot.page.Page :type page: pywikibot.page.Page @@ -229,8 +220,7 @@ def fromPage(cls, page): # noqa: N802 class Link(BaseLink): - """ - A MediaWiki wikitext link (local or interwiki). + """A MediaWiki wikitext link (local or interwiki). Constructs a Link object based on a wikitext link and a source site. @@ -258,8 +248,7 @@ class Link(BaseLink): ) def __init__(self, text, source=None, default_namespace=0) -> None: - """ - Initializer. + """Initializer. :param text: the link text (everything appearing between [[ and ]] on a wiki page) @@ -332,8 +321,7 @@ def __init__(self, text, source=None, default_namespace=0) -> None: self._text = source.title(with_section=False) + self._text def parse_site(self) -> tuple: - """ - Parse only enough text to determine which site the link points to. + """Parse only enough text to determine which site the link points to. This method does not parse anything after the first ":"; links with multiple interwiki prefixes (such as "wikt:fr:Parlais") need @@ -372,8 +360,7 @@ def parse_site(self) -> tuple: return (fam.name, code) # text before : doesn't match any known prefix def parse(self): - """ - Parse wikitext of the link. + """Parse wikitext of the link. Called internally when accessing attributes. """ @@ -493,8 +480,7 @@ def parse(self): @property def site(self): - """ - Return the site of the link. + """Return the site of the link. :rtype: pywikibot.Site """ @@ -504,8 +490,7 @@ def site(self): @property def namespace(self): - """ - Return the namespace of the link. + """Return the namespace of the link. :rtype: pywikibot.Namespace """ @@ -535,8 +520,7 @@ def anchor(self) -> str: return self._anchor def astext(self, onsite=None): - """ - Return a text representation of the link. + """Return a text representation of the link. :param onsite: if specified, present as a (possibly interwiki) link from the given site; otherwise, present as an internal link on @@ -551,8 +535,7 @@ def astext(self, onsite=None): return text def _cmpkey(self): - """ - Key for comparison of Link objects. + """Key for comparison of Link objects. Link objects are "equal" if and only if they are on the same site and have the same normalized title, including section if any. @@ -563,8 +546,7 @@ def _cmpkey(self): @classmethod def fromPage(cls, page, source=None): # noqa: N802 - """ - Create a Link to a Page. + """Create a Link to a Page. :param page: target Page :type page: pywikibot.page.Page @@ -587,8 +569,7 @@ def fromPage(cls, page, source=None): # noqa: N802 @classmethod def langlinkUnsafe(cls, lang, title, source): # noqa: N802 - """ - Create a "lang:title" Link linked from source. + """Create a "lang:title" Link linked from source. Assumes that the lang & title come clean, no checks are made. @@ -628,8 +609,7 @@ def langlinkUnsafe(cls, lang, title, source): # noqa: N802 @classmethod def create_separated(cls, link, source, default_namespace=0, section=None, label=None): - """ - Create a new instance but overwrite section or label. + """Create a new instance but overwrite section or label. The returned Link instance is already parsed. @@ -661,8 +641,7 @@ def create_separated(cls, link, source, default_namespace=0, section=None, class SiteLink(BaseLink): - """ - A single sitelink in a Wikibase item. + """A single sitelink in a Wikibase item. Extends BaseLink by the following attribute: @@ -675,8 +654,7 @@ class SiteLink(BaseLink): _items = ('_sitekey', '_rawtitle', 'badges') def __init__(self, title, site=None, badges=None) -> None: - """ - Initializer. + """Initializer. :param title: the title of the linked page including namespace :type title: str @@ -699,8 +677,7 @@ def __init__(self, title, site=None, badges=None) -> None: @staticmethod def _parse_namespace(title, site=None): - """ - Parse enough of a title with a ':' to determine the namespace. + """Parse enough of a title with a ':' to determine the namespace. :param site: the Site object for the wiki linked to. Can be provided as either a Site instance or a db key, defaults to pywikibot.Site(). @@ -727,8 +704,7 @@ def _parse_namespace(title, site=None): @property def badges(self): - """ - Return a list of all badges associated with the link. + """Return a list of all badges associated with the link. :rtype: [pywikibot.ItemPage] """ @@ -740,8 +716,7 @@ def fromJSON( # noqa: N802 data: dict[str, Any], site: pywikibot.site.DataSite | None = None, ) -> SiteLink: - """ - Create a SiteLink object from JSON returned in the API call. + """Create a SiteLink object from JSON returned in the API call. :param data: JSON containing SiteLink data :param site: The Wikibase site @@ -754,8 +729,7 @@ def fromJSON( # noqa: N802 return sl def toJSON(self) -> dict[str, str | list[str]]: # noqa: N802 - """ - Convert the SiteLink to a JSON object for the Wikibase API. + """Convert the SiteLink to a JSON object for the Wikibase API. :return: Wikibase JSON """ @@ -807,8 +781,7 @@ def toJSON(self) -> dict[str, str | list[str]]: # noqa: N802 def html2unicode(text: str, ignore=None, exceptions=None) -> str: - """ - Replace HTML entities with equivalent unicode. + """Replace HTML entities with equivalent unicode. :param ignore: HTML entities to ignore :param ignore: list of int diff --git a/pywikibot/page/_page.py b/pywikibot/page/_page.py index 991a98563b..48c74c8c6f 100644 --- a/pywikibot/page/_page.py +++ b/pywikibot/page/_page.py @@ -184,8 +184,7 @@ def set_redirect_target( self.save(**kwargs) def get_best_claim(self, prop: str): - """ - Return the first best Claim for this page. + """Return the first best Claim for this page. Return the first 'preferred' ranked Claim specified by Wikibase property or the first 'normal' one otherwise. diff --git a/pywikibot/page/_user.py b/pywikibot/page/_user.py index 01ed5e6194..49e76e80b5 100644 --- a/pywikibot/page/_user.py +++ b/pywikibot/page/_user.py @@ -26,15 +26,13 @@ class User(Page): - """ - A class that represents a Wiki user. + """A class that represents a Wiki user. This class also represents the Wiki page User: """ def __init__(self, source, title: str = '') -> None: - """ - Initializer for a User object. + """Initializer for a User object. All parameters are the same as for Page() Initializer. """ @@ -56,8 +54,7 @@ def __init__(self, source, title: str = '') -> None: @property def username(self) -> str: - """ - The username. + """The username. Convenience method that returns the title of the page with namespace prefix omitted, which is the username. @@ -67,8 +64,7 @@ def username(self) -> str: return self.title(with_ns=False) def isRegistered(self, force: bool = False) -> bool: # noqa: N802 - """ - Determine if the user is registered on the site. + """Determine if the user is registered on the site. It is possible to have a page named User:xyz and not have a corresponding user with username xyz. @@ -91,8 +87,7 @@ def is_CIDR(self) -> bool: # noqa: N802 return is_ip_network(self.username) def getprops(self, force: bool = False) -> dict: - """ - Return a properties about the user. + """Return a properties about the user. :param force: if True, forces reloading the data from API """ @@ -109,8 +104,7 @@ def getprops(self, force: bool = False) -> dict: def registration(self, force: bool = False) -> pywikibot.Timestamp | None: - """ - Fetch registration date for this user. + """Fetch registration date for this user. :param force: if True, forces reloading the data from API """ @@ -121,8 +115,7 @@ def registration(self, return None def editCount(self, force: bool = False) -> int: # noqa: N802 - """ - Return edit count for a registered user. + """Return edit count for a registered user. Always returns 0 for 'anonymous' users. @@ -162,16 +155,14 @@ def is_locked(self, force: bool = False) -> bool: return self.site.is_locked(self.username, force) def isEmailable(self, force: bool = False) -> bool: # noqa: N802 - """ - Determine whether emails may be send to this user through MediaWiki. + """Determine whether emails may be send to this user through MediaWiki. :param force: if True, forces reloading the data from API """ return not self.isAnonymous() and 'emailable' in self.getprops(force) def groups(self, force: bool = False) -> list: - """ - Return a list of groups to which this user belongs. + """Return a list of groups to which this user belongs. The list of groups may be empty. @@ -199,8 +190,7 @@ def rights(self, force: bool = False) -> list: return self.getprops(force).get('rights', []) def getUserPage(self, subpage: str = '') -> Page: # noqa: N802 - """ - Return a Page object relative to this user's main page. + """Return a Page object relative to this user's main page. :param subpage: subpage part to be appended to the main page title (optional) @@ -216,8 +206,7 @@ def getUserPage(self, subpage: str = '') -> Page: # noqa: N802 return Page(Link(self.title() + subpage, self.site)) def getUserTalkPage(self, subpage: str = '') -> Page: # noqa: N802 - """ - Return a Page object relative to this user's main talk page. + """Return a Page object relative to this user's main talk page. :param subpage: subpage part to be appended to the main talk page title (optional) @@ -234,8 +223,7 @@ def getUserTalkPage(self, subpage: str = '') -> Page: # noqa: N802 self.site, default_namespace=3)) def send_email(self, subject: str, text: str, ccme: bool = False) -> bool: - """ - Send an email to this user via MediaWiki's email interface. + """Send an email to this user via MediaWiki's email interface. :param subject: the subject header of the mail :param text: mail body @@ -266,8 +254,7 @@ def send_email(self, subject: str, text: str, ccme: bool = False) -> bool: and maildata['emailuser']['result'] == 'Success') def block(self, *args, **kwargs): - """ - Block user. + """Block user. Refer :py:obj:`APISite.blockuser` method for parameters. @@ -282,8 +269,7 @@ def block(self, *args, **kwargs): raise def unblock(self, reason: str | None = None) -> None: - """ - Remove the block for the user. + """Remove the block for the user. :param reason: Reason for the unblock. """ @@ -424,8 +410,7 @@ def deleted_contributions( yield page, Revision(**contrib) def uploadedImages(self, total: int = 10): # noqa: N802 - """ - Yield tuples describing files uploaded by this user. + """Yield tuples describing files uploaded by this user. Each tuple is composed of a pywikibot.Page, the timestamp (str in ISO8601 format), comment (str) and a bool for pageid > 0. @@ -443,8 +428,7 @@ def uploadedImages(self, total: int = 10): # noqa: N802 @property def is_thankable(self) -> bool: - """ - Determine if the user has thanks notifications enabled. + """Determine if the user has thanks notifications enabled. .. note:: This doesn't accurately determine if thanks is enabled for user. diff --git a/pywikibot/page/_wikibase.py b/pywikibot/page/_wikibase.py index 7382c3c12c..bea70cd926 100644 --- a/pywikibot/page/_wikibase.py +++ b/pywikibot/page/_wikibase.py @@ -1,5 +1,4 @@ -""" -Objects representing various types of Wikibase pages and structures. +"""Objects representing various types of Wikibase pages and structures. This module also includes objects: @@ -78,8 +77,7 @@ class WikibaseEntity: - """ - The base interface for Wikibase entities. + """The base interface for Wikibase entities. Each entity is identified by a data repository it belongs to and an identifier. @@ -100,8 +98,7 @@ class WikibaseEntity: DATA_ATTRIBUTES: dict[str, Any] = {} def __init__(self, repo, id_: str | None = None) -> None: - """ - Initializer. + """Initializer. :param repo: Entity repository. :type repo: DataSite @@ -122,8 +119,7 @@ def __repr__(self) -> str: @classmethod def is_valid_id(cls, entity_id: str) -> bool: - """ - Whether the string can be a valid id of the entity type. + """Whether the string can be a valid id of the entity type. :param entity_id: The ID to test. """ @@ -147,8 +143,7 @@ def _initialize_empty(self): setattr(self, key, cls.new_empty(self.repo)) def _defined_by(self, singular: bool = False) -> dict[str, str]: - """ - Internal function to provide the API parameters to identify the entity. + """Function to provide the API parameters to identify the entity. An empty dict is returned if the entity has not been created yet. @@ -165,8 +160,7 @@ def _defined_by(self, singular: bool = False) -> dict[str, str]: return params def getID(self, numeric: bool = False) -> int | str: - """ - Get the identifier of this entity. + """Get the identifier of this entity. :param numeric: Strip the first letter and return an int """ @@ -175,16 +169,14 @@ def getID(self, numeric: bool = False) -> int | str: return self.id def get_data_for_new_entity(self) -> dict: - """ - Return data required for creation of a new entity. + """Return data required for creation of a new entity. Override it if you need. """ return {} def toJSON(self, diffto: dict | None = None) -> dict: - """ - Create JSON suitable for Wikibase API. + """Create JSON suitable for Wikibase API. When diffto is provided, JSON representing differences to the provided data is created. @@ -206,8 +198,7 @@ def toJSON(self, diffto: dict | None = None) -> dict: @classmethod def _normalizeData(cls, data: dict) -> dict: - """ - Helper function to expand data into the Wikibase API structure. + """Helper function to expand data into the Wikibase API structure. :param data: The dict to normalize :return: The dict with normalized data @@ -220,8 +211,7 @@ def _normalizeData(cls, data: dict) -> dict: @property def latest_revision_id(self) -> int | None: - """ - Get the revision identifier for the most recent revision of the entity. + """Get the revision id for the most recent revision of the entity. :rtype: int or None if it cannot be determined :raise NoWikibaseEntityError: if the entity doesn't exist @@ -252,8 +242,7 @@ def exists(self) -> bool: return 'missing' not in self._content def get(self, force: bool = False) -> dict: - """ - Fetch all entity data and cache it. + """Fetch all entity data and cache it. :param force: override caching :raise NoWikibaseEntityError: if this entity doesn't exist @@ -360,8 +349,7 @@ def editEntity( target_ref.hash = ref_stat['hash'] def concept_uri(self) -> str: - """ - Return the full concept URI. + """Return the full concept URI. :raise NoWikibaseEntityError: if this entity's id is not known """ @@ -408,8 +396,7 @@ def _assert_has_id(self): self.id = 'M' + str(self.file.pageid) def _defined_by(self, singular: bool = False) -> dict: - """ - Internal function to provide the API parameters to identify the entity. + """Function to provide the API parameters to identify the entity. .. versionadded:: 8.5 @@ -591,8 +578,7 @@ def removeClaims(self, claims, **kwargs) -> None: class WikibasePage(BasePage, WikibaseEntity): - """ - Mixin base class for Wikibase entities which are also pages (eg. items). + """Mixin base class for Wikibase entities which are also pages (eg. items). There should be no need to instantiate this directly. """ @@ -600,8 +586,7 @@ class WikibasePage(BasePage, WikibaseEntity): _cache_attrs = (*BasePage._cache_attrs, '_content') def __init__(self, site, title: str = '', **kwargs) -> None: - """ - Initializer. + """Initializer. If title is provided, either ns or entity_type must also be provided, and will be checked against the title parsed using the Page @@ -697,8 +682,7 @@ def __init__(self, site, title: str = '', **kwargs) -> None: self._link.title) def namespace(self) -> int: - """ - Return the number of the namespace of the entity. + """Return the number of the namespace of the entity. :return: Namespace id """ @@ -715,8 +699,7 @@ def exists(self) -> bool: return 'missing' not in self._content def botMayEdit(self) -> bool: - """ - Return whether bots may edit this page. + """Return whether bots may edit this page. Because there is currently no system to mark a page that it shouldn't be edited by bots on Wikibase pages it always returns True. The content @@ -728,8 +711,7 @@ def botMayEdit(self) -> bool: return True def get(self, force: bool = False, *args, **kwargs) -> dict: - """ - Fetch all page data, and cache it. + """Fetch all page data, and cache it. :param force: override caching :raise NotImplementedError: a value in args or kwargs @@ -763,8 +745,7 @@ def get(self, force: bool = False, *args, **kwargs) -> dict: @property def latest_revision_id(self) -> int: - """ - Get the revision identifier for the most recent revision of the entity. + """Get the revision id for the most recent revision of the entity. :rtype: int :raise pywikibot.exceptions.NoPageError: if the entity doesn't exist @@ -886,8 +867,7 @@ def set_redirect_target( save: bool = True, **kwargs ): - """ - Set target of a redirect for a Wikibase page. + """Set target of a redirect for a Wikibase page. Has not been implemented in the Wikibase API yet, except for ItemPage. """ @@ -895,8 +875,7 @@ def set_redirect_target( @allow_asynchronous def addClaim(self, claim, bot: bool = True, **kwargs): - """ - Add a claim to the entity. + """Add a claim to the entity. :param claim: The claim to add :type claim: pywikibot.page.Claim @@ -919,8 +898,7 @@ def addClaim(self, claim, bot: bool = True, **kwargs): claim.on_item = self def removeClaims(self, claims, **kwargs) -> None: - """ - Remove the claims from the entity. + """Remove the claims from the entity. :param claims: list of claims to be removed :type claims: list or pywikibot.Claim @@ -960,8 +938,7 @@ class ItemPage(WikibasePage): } def __init__(self, site, title=None, ns=None) -> None: - """ - Initializer. + """Initializer. :param site: data repository :type site: pywikibot.site.DataSite @@ -989,8 +966,7 @@ def __init__(self, site, title=None, ns=None) -> None: assert self.id == self._link.title def _defined_by(self, singular: bool = False) -> dict: - """ - Internal function to provide the API parameters to identify the item. + """Function to provide the API parameters to identify the item. The API parameters may be 'id' if the ItemPage has one, or 'site'&'title' if instantiated via ItemPage.fromPage with @@ -1036,8 +1012,7 @@ def _defined_by(self, singular: bool = False) -> dict: return params def title(self, **kwargs): - """ - Return ID as title of the ItemPage. + """Return ID as title of the ItemPage. If the ItemPage was lazy-loaded via ItemPage.fromPage, this method will fetch the Wikibase item ID for the page, potentially raising @@ -1070,8 +1045,7 @@ def title(self, **kwargs): return super().title(**kwargs) def getID(self, numeric: bool = False, force: bool = False): - """ - Get the entity identifier. + """Get the entity identifier. :param numeric: Strip the first letter and return an int :param force: Force an update of new data @@ -1082,8 +1056,7 @@ def getID(self, numeric: bool = False, force: bool = False): @classmethod def fromPage(cls, page, lazy_load: bool = False): - """ - Get the ItemPage for a Page that links to it. + """Get the ItemPage for a Page that links to it. :param page: Page to look for corresponding data item :type page: pywikibot.page.Page @@ -1122,8 +1095,7 @@ def fromPage(cls, page, lazy_load: bool = False): @classmethod def from_entity_uri(cls, site, uri: str, lazy_load: bool = False): - """ - Get the ItemPage from its entity uri. + """Get the ItemPage from its entity uri. :param site: The Wikibase site for the item. :type site: pywikibot.site.DataSite @@ -1160,8 +1132,7 @@ def get( *args, **kwargs ) -> dict[str, Any]: - """ - Fetch all item data, and cache it. + """Fetch all item data, and cache it. :param force: override caching :param get_redirect: return the item content, do not follow the @@ -1211,8 +1182,7 @@ def getRedirectTarget(self, *, ignore_section: bool = True): return self.__class__(target.site, target.title(), target.namespace()) def iterlinks(self, family=None): - """ - Iterate through all the sitelinks. + """Iterate through all the sitelinks. :param family: string/Family object which represents what family of links to iterate @@ -1269,8 +1239,7 @@ def setSitelink(self, sitelink: SITELINK_TYPE, **kwargs) -> None: self.setSitelinks([sitelink], **kwargs) def removeSitelink(self, site: LANGUAGE_IDENTIFIER, **kwargs) -> None: - """ - Remove a sitelink. + """Remove a sitelink. A site can either be a Site object, or it can be a dbName. """ @@ -1278,8 +1247,7 @@ def removeSitelink(self, site: LANGUAGE_IDENTIFIER, **kwargs) -> None: def removeSitelinks(self, sites: list[LANGUAGE_IDENTIFIER], **kwargs ) -> None: - """ - Remove sitelinks. + """Remove sitelinks. Sites should be a list, with values either being Site objects, or dbNames. @@ -1303,8 +1271,7 @@ def setSitelinks(self, sitelinks: list[SITELINK_TYPE], **kwargs) -> None: self.editEntity(data, **kwargs) def mergeInto(self, item, **kwargs) -> None: - """ - Merge the item into another item. + """Merge the item into another item. :param item: The item to merge into :type item: pywikibot.page.ItemPage @@ -1370,8 +1337,7 @@ def isRedirectPage(self): class Property: - """ - A Wikibase property. + """A Wikibase property. While every Wikibase property has a Page on the data repository, this object is for when the property is used as part of another concept @@ -1420,8 +1386,7 @@ class Property: } def __init__(self, site, id: str, datatype: str | None = None) -> None: - """ - Initializer. + """Initializer. :param site: data repository :type site: pywikibot.site.DataSite @@ -1459,8 +1424,7 @@ def type(self) -> str: return self.repo.get_property_type(self) def getID(self, numeric: bool = False): - """ - Get the identifier of this property. + """Get the identifier of this property. :param numeric: Strip the first letter and return an int """ @@ -1494,8 +1458,7 @@ class PropertyPage(WikibasePage, Property): } def __init__(self, source, title=None, datatype=None) -> None: - """ - Initializer. + """Initializer. :param source: data repository property is on :type source: pywikibot.site.DataSite @@ -1523,8 +1486,7 @@ def __init__(self, source, title=None, datatype=None) -> None: Property.__init__(self, source, self.id) def get(self, force: bool = False, *args, **kwargs) -> dict: - """ - Fetch the property entity, and cache it. + """Fetch the property entity, and cache it. :param force: override caching :raise NotImplementedError: a value in args or kwargs @@ -1552,8 +1514,7 @@ def newClaim(self, *args, **kwargs) -> Claim: **kwargs) def getID(self, numeric: bool = False): - """ - Get the identifier of this property. + """Get the identifier of this property. :param numeric: Strip the first letter and return an int """ @@ -1571,8 +1532,7 @@ def get_data_for_new_entity(self): class Claim(Property): - """ - A Claim on a Wikibase entity. + """A Claim on a Wikibase entity. Claims are standard claims as well as references and qualifiers. """ @@ -1614,8 +1574,7 @@ def __init__( rank: str = 'normal', **kwargs ) -> None: - """ - Initializer. + """Initializer. Defined by the "snak" value, supplemented by site + pid @@ -1733,8 +1692,7 @@ def same_as( return True def copy(self): - """ - Create an independent copy of this object. + """Create an independent copy of this object. :rtype: pywikibot.page.Claim """ @@ -1806,8 +1764,7 @@ def fromJSON(cls, site, data: dict[str, Any]) -> Claim: @classmethod def referenceFromJSON(cls, site, data) -> dict: - """ - Create a dict of claims from reference JSON returned in the API call. + """Create a dict of claims from reference JSON fetched in the API call. Reference objects are represented a bit differently, and require some more handling. @@ -1830,8 +1787,7 @@ def referenceFromJSON(cls, site, data) -> dict: @classmethod def qualifierFromJSON(cls, site, data): - """ - Create a Claim for a qualifier from JSON. + """Create a Claim for a qualifier from JSON. Qualifier objects are represented a bit differently like references, but I'm not @@ -1892,8 +1848,7 @@ def toJSON(self) -> dict: return data def setTarget(self, value): - """ - Set the target value in the local object. + """Set the target value in the local object. :param value: The new target value. :type value: object @@ -1912,8 +1867,7 @@ def changeTarget( snaktype: str = 'value', **kwargs ) -> None: - """ - Set the target value in the data repository. + """Set the target value in the data repository. :param value: The new target value. :type value: object @@ -1931,8 +1885,7 @@ def changeTarget( self.on_item.latest_revision_id = data['pageinfo']['lastrevid'] def getTarget(self): - """ - Return the target value of this Claim. + """Return the target value of this Claim. None is returned if no target is set @@ -1941,16 +1894,14 @@ def getTarget(self): return self.target def getSnakType(self) -> str: - """ - Return the type of snak. + """Return the type of snak. :return: str ('value', 'somevalue' or 'novalue') """ return self.snaktype def setSnakType(self, value): - """ - Set the type of snak. + """Set the type of snak. :param value: Type of snak :type value: str ('value', 'somevalue', or 'novalue') @@ -1978,8 +1929,7 @@ def changeRank(self, rank, **kwargs): return self.on_item.repo.save_claim(self, **kwargs) def changeSnakType(self, value=None, **kwargs) -> None: - """ - Save the new snak value. + """Save the new snak value. TODO: Is this function really needed? """ @@ -1992,8 +1942,7 @@ def getSources(self) -> list: return self.sources def addSource(self, claim, **kwargs) -> None: - """ - Add the claim as a source. + """Add the claim as a source. :param claim: the claim to add :type claim: pywikibot.Claim @@ -2001,8 +1950,7 @@ def addSource(self, claim, **kwargs) -> None: self.addSources([claim], **kwargs) def addSources(self, claims, **kwargs): - """ - Add the claims as one source. + """Add the claims as one source. :param claims: the claims to add :type claims: list of pywikibot.Claim @@ -2026,8 +1974,7 @@ def addSources(self, claims, **kwargs): self.sources.append(source) def removeSource(self, source, **kwargs) -> None: - """ - Remove the source. Call removeSources(). + """Remove the source. Call removeSources(). :param source: the source to remove :type source: pywikibot.Claim @@ -2035,8 +1982,7 @@ def removeSource(self, source, **kwargs) -> None: self.removeSources([source], **kwargs) def removeSources(self, sources, **kwargs) -> None: - """ - Remove the sources. + """Remove the sources. :param sources: the sources to remove :type sources: list of pywikibot.Claim @@ -2071,8 +2017,7 @@ def addQualifier(self, qualifier, **kwargs): self.qualifiers[qualifier.getID()] = [qualifier] def removeQualifier(self, qualifier, **kwargs) -> None: - """ - Remove the qualifier. Call removeQualifiers(). + """Remove the qualifier. Call removeQualifiers(). :param qualifier: the qualifier to remove :type qualifier: pywikibot.page.Claim @@ -2080,8 +2025,7 @@ def removeQualifier(self, qualifier, **kwargs) -> None: self.removeQualifiers([qualifier], **kwargs) def removeQualifiers(self, qualifiers, **kwargs) -> None: - """ - Remove the qualifiers. + """Remove the qualifiers. :param qualifiers: the qualifiers to remove :type qualifiers: list of pywikibot.Claim @@ -2095,8 +2039,7 @@ def removeQualifiers(self, qualifiers, **kwargs) -> None: qualifier.on_item = None def target_equals(self, value) -> bool: - """ - Check whether the Claim's target is equal to specified value. + """Check whether the Claim's target is equal to specified value. The function checks for: @@ -2137,8 +2080,7 @@ def target_equals(self, value) -> bool: return self.target == value def has_qualifier(self, qualifier_id: str, target) -> bool: - """ - Check whether Claim contains specified qualifier. + """Check whether Claim contains specified qualifier. :param qualifier_id: id of the qualifier :param target: qualifier target to check presence of @@ -2177,8 +2119,7 @@ def _formatValue(self) -> dict: return value def _formatDataValue(self) -> dict: - """ - Format the target into the proper JSON datavalue that Wikibase wants. + """Format the target into the proper JSON datavalue for Wikibase. :return: Wikibase API representation with type and value. """ @@ -2224,8 +2165,7 @@ class LexemePage(WikibasePage): } def __init__(self, site, title=None) -> None: - """ - Initializer. + """Initializer. :param site: data repository :type site: pywikibot.site.DataSite @@ -2251,8 +2191,7 @@ def get_data_for_new_entity(self): raise NotImplementedError # TODO def toJSON(self, diffto: dict | None = None) -> dict: - """ - Create JSON suitable for Wikibase API. + """Create JSON suitable for Wikibase API. When diffto is provided, JSON representing differences to the provided data is created. @@ -2271,8 +2210,7 @@ def toJSON(self, diffto: dict | None = None) -> dict: return data def get(self, force=False, get_redirect=False, *args, **kwargs): - """ - Fetch all lexeme data, and cache it. + """Fetch all lexeme data, and cache it. :param force: override caching :type force: bool @@ -2308,8 +2246,7 @@ def get(self, force=False, get_redirect=False, *args, **kwargs): @classmethod def _normalizeData(cls, data: dict) -> dict: - """ - Helper function to expand data into the Wikibase API structure. + """Helper function to expand data into the Wikibase API structure. :param data: The dict to normalize :return: the altered dict from parameter data. @@ -2326,8 +2263,7 @@ def _normalizeData(cls, data: dict) -> dict: @allow_asynchronous def add_form(self, form, **kwargs): - """ - Add a form to the lexeme. + """Add a form to the lexeme. :param form: The form to add :type form: Form @@ -2356,8 +2292,7 @@ def add_form(self, form, **kwargs): self.latest_revision_id = data['lastrevid'] def remove_form(self, form, **kwargs) -> None: - """ - Remove a form from the lexeme. + """Remove a form from the lexeme. :param form: The form to remove :type form: pywikibot.LexemeForm @@ -2371,8 +2306,7 @@ def remove_form(self, form, **kwargs) -> None: # todo: senses def mergeInto(self, lexeme, **kwargs): - """ - Merge the lexeme into another lexeme. + """Merge the lexeme into another lexeme. :param lexeme: The lexeme to merge into :type lexeme: LexemePage @@ -2437,8 +2371,7 @@ def on_lexeme(self): @allow_asynchronous def addClaim(self, claim, **kwargs): - """ - Add a claim to the form. + """Add a claim to the form. :param claim: The claim to add :type claim: Claim @@ -2458,8 +2391,7 @@ def addClaim(self, claim, **kwargs): claim.on_item = self def removeClaims(self, claims, **kwargs) -> None: - """ - Remove the claims from the form. + """Remove the claims from the form. :param claims: list of claims to be removed :type claims: list or pywikibot.Claim @@ -2511,8 +2443,7 @@ def _normalizeData(cls, data): return new_data def get(self, force: bool = False) -> dict: - """ - Fetch all form data, and cache it. + """Fetch all form data, and cache it. :param force: override caching @@ -2532,8 +2463,7 @@ def get(self, force: bool = False) -> dict: return data def edit_elements(self, data: dict, **kwargs) -> None: - """ - Update form elements. + """Update form elements. :param data: Data to be saved """ diff --git a/pywikibot/pagegenerators/__init__.py b/pywikibot/pagegenerators/__init__.py index 64c8089644..b6a0abf215 100644 --- a/pywikibot/pagegenerators/__init__.py +++ b/pywikibot/pagegenerators/__init__.py @@ -567,8 +567,7 @@ def PageClassGenerator(generator: Iterable[pywikibot.page.Page] ) -> Generator[pywikibot.page.Page, None, None]: - """ - Yield pages from another generator as Page subclass objects. + """Yield pages from another generator as Page subclass objects. The page class type depends on the page namespace. Objects may be Category, FilePage, Userpage or Page. @@ -714,8 +713,7 @@ def PreloadingEntityGenerator( generator: Iterable[pywikibot.page.WikibaseEntity], groupsize: int = 50, ) -> Generator[pywikibot.page.WikibaseEntity, None, None]: - """ - Yield preloaded pages taken from another generator. + """Yield preloaded pages taken from another generator. Function basically is copied from above, but for Wikibase entities. diff --git a/pywikibot/pagegenerators/_factory.py b/pywikibot/pagegenerators/_factory.py index 0e2c135db9..b61104717c 100644 --- a/pywikibot/pagegenerators/_factory.py +++ b/pywikibot/pagegenerators/_factory.py @@ -91,8 +91,7 @@ def __init__(self, site: BaseSite | None = None, positional_arg_name: str | None = None, enabled_options: Iterable[str] | None = None, disabled_options: Iterable[str] | None = None) -> None: - """ - Initializer. + """Initializer. :param site: Site for generator results :param positional_arg_name: generator to use for positional args, @@ -160,8 +159,7 @@ def _validate_options(self, @property def site(self) -> pywikibot.site.BaseSite: - """ - Generator site. + """Generator site. The generator site should not be accessed until after the global arguments have been handled, otherwise the default Site may be changed @@ -177,8 +175,7 @@ def site(self) -> pywikibot.site.BaseSite: @property def namespaces(self) -> frozenset[pywikibot.site.Namespace]: - """ - List of Namespace parameters. + """List of Namespace parameters. Converts int or string namespaces to Namespace objects and change the storage to immutable once it has been accessed. @@ -317,8 +314,7 @@ def getCombinedGenerator(self, # noqa: N802 def getCategory(self, category: str # noqa: N802 ) -> tuple[pywikibot.Category, str | None]: - """ - Return Category and start as defined by category. + """Return Category and start as defined by category. :param category: category name with start parameter """ @@ -347,8 +343,7 @@ def getCategoryGen(self, category: str, # noqa: N802 recurse: int | bool = False, content: bool = False, gen_func: Callable | None = None) -> Any: - """ - Return generator based on Category defined by category and gen_func. + """Return generator based on Category defined by category and gen_func. :param category: category name with start parameter :param recurse: if not False or 0, also iterate articles in diff --git a/pywikibot/pagegenerators/_filters.py b/pywikibot/pagegenerators/_filters.py index 5ee3a71c5f..a9b3203799 100644 --- a/pywikibot/pagegenerators/_filters.py +++ b/pywikibot/pagegenerators/_filters.py @@ -50,8 +50,7 @@ def NamespaceFilterPageGenerator( | Sequence[str | Namespace], site: BaseSite | None = None, ) -> Generator[pywikibot.page.BasePage, None, None]: - """ - A generator yielding pages from another generator in given namespaces. + """A generator yielding pages from another generator in given namespaces. If a site is provided, the namespaces are validated using the namespaces of that site, otherwise the namespaces are validated using the default @@ -87,8 +86,7 @@ def PageTitleFilterPageGenerator( generator: Iterable[pywikibot.page.BasePage], ignore_list: dict[str, dict[str, str]], ) -> Generator[pywikibot.page.BasePage, None, None]: - """ - Yield only those pages are not listed in the ignore list. + """Yield only those pages are not listed in the ignore list. :param ignore_list: family names are mapped to dictionaries in which language codes are mapped to lists of page titles. Each title must @@ -115,8 +113,7 @@ def RedirectFilterPageGenerator( no_redirects: bool = True, show_filtered: bool = False, ) -> Generator[pywikibot.page.BasePage, None, None]: - """ - Yield pages from another generator that are redirects or not. + """Yield pages from another generator that are redirects or not. :param no_redirects: Exclude redirects if True, else only include redirects. @@ -150,8 +147,7 @@ def __filter_match(cls, prop: str, claim: str, qualifiers: dict[str, str]) -> bool: - """ - Return true if the page contains the claim given. + """Return true if the page contains the claim given. :param page: the page to check :return: true if page contains the claim, false otherwise @@ -188,8 +184,7 @@ def filter( qualifiers: dict[str, str] | None = None, negate: bool = False, ) -> Generator[pywikibot.page.WikibasePage, None, None]: - """ - Yield all ItemPages which contain certain claim in a property. + """Yield all ItemPages which contain certain claim in a property. :param prop: property id to check :param claim: value of the property to check. Can be exact value (for @@ -213,8 +208,7 @@ def SubpageFilterGenerator(generator: Iterable[pywikibot.page.BasePage], max_depth: int = 0, show_filtered: bool = False ) -> Generator[pywikibot.page.BasePage, None, None]: - """ - Generator which filters out subpages based on depth. + """Generator which filters out subpages based on depth. It looks at the namespace of each page and checks if that namespace has subpages enabled. If so, pages with forward slashes ('/') are excluded. @@ -325,8 +319,7 @@ def QualityFilterPageGenerator( generator: Iterable[pywikibot.page.BasePage], quality: list[int], ) -> Generator[pywikibot.page.BasePage, None, None]: - """ - Wrap a generator to filter pages according to quality levels. + """Wrap a generator to filter pages according to quality levels. This is possible only for pages with content_model 'proofread-page'. In all the other cases, no filter is applied. @@ -347,8 +340,7 @@ def CategoryFilterPageGenerator( generator: Iterable[pywikibot.page.BasePage], category_list: Sequence[pywikibot.page.Category], ) -> Generator[pywikibot.page.BasePage, None, None]: - """ - Wrap a generator to filter pages by categories specified. + """Wrap a generator to filter pages by categories specified. :param generator: A generator object :param category_list: categories used to filter generated pages @@ -377,8 +369,7 @@ def EdittimeFilterPageGenerator( first_edit_end: datetime.datetime | None = None, show_filtered: bool = False, ) -> Generator[pywikibot.page.BasePage, None, None]: - """ - Wrap a generator to filter pages outside last or first edit range. + """Wrap a generator to filter pages outside last or first edit range. :param generator: A generator object :param last_edit_start: Only yield pages last edited after this time @@ -438,8 +429,7 @@ def UserEditFilterGenerator( max_revision_depth: int | None = None, show_filtered: bool = False ) -> Generator[pywikibot.page.BasePage, None, None]: - """ - Generator which will yield Pages modified by username. + """Generator which will yield Pages modified by username. It only looks at the last editors given by max_revision_depth. If timestamp is set in MediaWiki format JJJJMMDDhhmmss, older edits are @@ -473,8 +463,7 @@ def WikibaseItemFilterPageGenerator( has_item: bool = True, show_filtered: bool = False, ) -> Generator[pywikibot.page.BasePage, None, None]: - """ - A wrapper generator used to exclude if page has a Wikibase item or not. + """A wrapper generator used to exclude if page has a Wikibase item or not. :param generator: Generator to wrap. :param has_item: Exclude pages without an item if True, or only diff --git a/pywikibot/pagegenerators/_generators.py b/pywikibot/pagegenerators/_generators.py index 2956b358c7..d15ca69607 100644 --- a/pywikibot/pagegenerators/_generators.py +++ b/pywikibot/pagegenerators/_generators.py @@ -84,8 +84,7 @@ def PrefixingPageGenerator(prefix: str, total: int | None = None, content: bool = False ) -> Iterable[pywikibot.page.Page]: - """ - Prefixed Page generator. + """Prefixed Page generator. :param prefix: The prefix of the pages. :param namespace: Namespace to retrieve pages from @@ -125,8 +124,7 @@ def LogeventsPageGenerator(logtype: str | None = None, end: Timestamp | None = None, reverse: bool = False ) -> Generator[pywikibot.page.Page, None, None]: - """ - Generate Pages for specified modes of logevents. + """Generate Pages for specified modes of logevents. :param logtype: Mode of logs to retrieve :param user: User of logs retrieved @@ -176,8 +174,7 @@ def RecentChangesPageGenerator( Iterable[pywikibot.Page]]) = None, **kwargs: Any ) -> Generator[pywikibot.Page, None, None]: - """ - Generate pages that are in the recent changes list, including duplicates. + """Generate recent changes pages, including duplicates. For keyword parameters refer :meth:`APISite.recentchanges() `. @@ -414,8 +411,7 @@ def TextIOPageGenerator(source: str | None = None, def PagesFromTitlesGenerator(iterable: Iterable[str], site: BaseSite | None = None ) -> Generator[pywikibot.page.Page, None, None]: - """ - Generate pages from the titles (strings) yielded by iterable. + """Generate pages from the titles (strings) yielded by iterable. :param site: Site for generator results. """ @@ -480,8 +476,7 @@ def UserContributionsGenerator(username: str, def NewimagesPageGenerator(total: int | None = None, site: BaseSite | None = None ) -> Generator[pywikibot.page.Page, None, None]: - """ - New file generator. + """New file generator. :param total: Maximum number of pages to retrieve in total :param site: Site for generator results. @@ -494,8 +489,7 @@ def NewimagesPageGenerator(total: int | None = None, def WikibaseItemGenerator(gen: Iterable[pywikibot.page.Page] ) -> Generator[pywikibot.page.ItemPage, None, None]: - """ - A wrapper generator used to yield Wikibase items of another generator. + """A wrapper generator used to yield Wikibase items of another generator. :param gen: Generator to wrap. :return: Wrapped generator @@ -516,8 +510,7 @@ def AncientPagesPageGenerator( total: int = 100, site: BaseSite | None = None ) -> Generator[pywikibot.page.Page, None, None]: - """ - Ancient page generator. + """Ancient page generator. :param total: Maximum number of pages to retrieve in total :param site: Site for generator results. @@ -686,8 +679,7 @@ def DeadendPagesPageGenerator( def LongPagesPageGenerator(total: int = 100, site: BaseSite | None = None ) -> Generator[pywikibot.page.Page, None, None]: - """ - Long page generator. + """Long page generator. :param total: Maximum number of pages to retrieve in total :param site: Site for generator results. @@ -700,8 +692,7 @@ def LongPagesPageGenerator(total: int = 100, def ShortPagesPageGenerator(total: int = 100, site: BaseSite | None = None ) -> Generator[pywikibot.page.Page, None, None]: - """ - Short page generator. + """Short page generator. :param total: Maximum number of pages to retrieve in total :param site: Site for generator results. @@ -785,8 +776,7 @@ def SearchPageGenerator( def LiveRCPageGenerator(site: BaseSite | None = None, total: int | None = None ) -> Generator[pywikibot.page.Page, None, None]: - """ - Yield pages from a socket.io RC stream. + """Yield pages from a socket.io RC stream. Generates pages based on the EventStreams Server-Sent-Event (SSE) recent changes stream. @@ -816,6 +806,7 @@ def LiveRCPageGenerator(site: BaseSite | None = None, class GoogleSearchPageGenerator(GeneratorWrapper): + """Page generator using Google search results. To use this generator, you need to install the package 'google': @@ -834,8 +825,7 @@ class GoogleSearchPageGenerator(GeneratorWrapper): def __init__(self, query: str | None = None, site: BaseSite | None = None) -> None: - """ - Initializer. + """Initializer. :param site: Site for generator results. """ @@ -896,8 +886,7 @@ def generator(self) -> Generator[pywikibot.page.Page, None, None]: def MySQLPageGenerator(query: str, site: BaseSite | None = None, verbose: bool | None = None ) -> Generator[pywikibot.page.Page, None, None]: - """ - Yield a list of pages based on a MySQL query. + """Yield a list of pages based on a MySQL query. The query should return two columns, page namespace and page title pairs from some table. An example query that yields all ns0 pages might look @@ -1055,6 +1044,7 @@ def SupersetPageGenerator(query: str, class XMLDumpPageGenerator(abc.Iterator): # type: ignore[type-arg] + """Xml iterator that yields Page objects. .. versionadded:: 7.2 @@ -1133,8 +1123,7 @@ def __init__(self, *args, **kwargs): def YearPageGenerator(start: int = 1, end: int = 2050, site: BaseSite | None = None ) -> Generator[pywikibot.page.Page, None, None]: - """ - Year page generator. + """Year page generator. :param site: Site for generator results. """ @@ -1153,8 +1142,7 @@ def YearPageGenerator(start: int = 1, end: int = 2050, def DayPageGenerator(start_month: int = 1, end_month: int = 12, site: BaseSite | None = None, year: int = 2000 ) -> Generator[pywikibot.page.Page, None, None]: - """ - Day page generator. + """Day page generator. :param site: Site for generator results. :param year: considering leap year. @@ -1238,8 +1226,7 @@ def WikibaseSearchItemPageGenerator( total: int | None = None, site: BaseSite | None = None, ) -> Generator[pywikibot.page.ItemPage, None, None]: - """ - Generate pages that contain the provided text. + """Generate pages that contain the provided text. :param text: Text to look for. :param language: Code of the language to search in. If not specified, @@ -1259,6 +1246,7 @@ def WikibaseSearchItemPageGenerator( class PetScanPageGenerator(GeneratorWrapper): + """Queries PetScan to generate pages. .. seealso:: https://petscan.wmflabs.org/ @@ -1275,8 +1263,7 @@ def __init__( site: BaseSite | None = None, extra_options: dict[Any, Any] | None = None ) -> None: - """ - Initializer. + """Initializer. :param categories: List of category names to retrieve pages from :param subset_combination: Combination mode. @@ -1298,8 +1285,7 @@ def __init__( def buildQuery(self, categories: Sequence[str], subset_combination: bool, namespaces: Iterable[int | pywikibot.site.Namespace] | None, extra_options: dict[Any, Any] | None) -> dict[str, Any]: - """ - Get the querystring options to query PetScan. + """Get the querystring options to query PetScan. :param categories: List of categories (as strings) :param subset_combination: Combination mode. @@ -1370,6 +1356,7 @@ def generator(self) -> Generator[pywikibot.page.Page, None, None]: class PagePilePageGenerator(GeneratorWrapper): + """Queries PagePile to generate pages. .. seealso:: https://pagepile.toolforge.org/ diff --git a/pywikibot/proofreadpage.py b/pywikibot/proofreadpage.py index 727527dff3..06f4146182 100644 --- a/pywikibot/proofreadpage.py +++ b/pywikibot/proofreadpage.py @@ -58,6 +58,7 @@ def _bs4_soup(*args: Any, **kwargs: Any) -> None: class TagAttr: + """Tag attribute of . Represent a single attribute. It is used internally in @@ -164,6 +165,7 @@ def __repr__(self): class TagAttrDesc: + """A descriptor tag. .. versionadded:: 8.0 @@ -192,6 +194,7 @@ def __delete__(self, obj): class PagesTagParser(collections.abc.Container): + """Parser for tag ````. .. seealso:: @@ -1091,8 +1094,7 @@ def _parse_redlink(href: str) -> str | None: return None def save(self, *args: Any, **kwargs: Any) -> None: # See Page.save(). - """ - Save page after validating the content. + """Save page after validating the content. Trying to save any other content fails silently with a parameterless INDEX_TEMPLATE being saved. diff --git a/pywikibot/scripts/generate_family_file.py b/pywikibot/scripts/generate_family_file.py index 066d4bf4ea..3ec90561ba 100755 --- a/pywikibot/scripts/generate_family_file.py +++ b/pywikibot/scripts/generate_family_file.py @@ -32,7 +32,7 @@ If the url scheme is missing, ``https`` will be used. """ # -# (C) Pywikibot team, 2010-2023 +# (C) Pywikibot team, 2010-2024 # # Distributed under the terms of the MIT license. # @@ -64,8 +64,7 @@ def __init__(self, name: str | None = None, dointerwiki: str | None = None, verify: str | None = None) -> None: - """ - Parameters are optional. If not given the script asks for the values. + """Parameters are optional. If missing the script asks for the values. :param url: an url from where the family settings are loaded :param name: the family name without "_family.py" tail. diff --git a/pywikibot/scripts/generate_user_files.py b/pywikibot/scripts/generate_user_files.py index 2864f4beb0..f0e0a98898 100755 --- a/pywikibot/scripts/generate_user_files.py +++ b/pywikibot/scripts/generate_user_files.py @@ -114,8 +114,7 @@ def get_site_and_lang( default_username: str | None = None, force: bool = False ) -> tuple[str, str, str]: - """ - Ask the user for the family, site code and username. + """Ask the user for the family, site code and username. :param default_family: The default family which should be chosen. :param default_lang: The default site code which should be chosen, @@ -353,8 +352,7 @@ def create_user_config( main_username: str, force: bool = False ): - """ - Create a user-config.py in base_dir. + """Create a user-config.py in base_dir. Create a user-password.py if necessary. """ @@ -497,8 +495,7 @@ def ask_for_dir_change(force) -> tuple[bool, bool]: def main(*args: str) -> None: - """ - Process command line arguments and generate user-config. + """Process command line arguments and generate user-config. If args is an empty list, sys.argv is used. diff --git a/pywikibot/scripts/login.py b/pywikibot/scripts/login.py index ce3f29f5a3..75c6088224 100755 --- a/pywikibot/scripts/login.py +++ b/pywikibot/scripts/login.py @@ -122,8 +122,7 @@ def login_one_site(code, family, oauth, logout, autocreate): def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/pywikibot/site/_apisite.py b/pywikibot/site/_apisite.py index 2a3ec9cf1d..0d6f64f848 100644 --- a/pywikibot/site/_apisite.py +++ b/pywikibot/site/_apisite.py @@ -155,8 +155,7 @@ def __setstate__(self, state: dict[str, Any]) -> None: self._tokens = TokenWallet(self) def interwiki(self, prefix: str) -> BaseSite: - """ - Return the site for a corresponding interwiki prefix. + """Return the site for a corresponding interwiki prefix. :raises pywikibot.exceptions.SiteDefinitionError: if the url given in the interwiki table doesn't match any of the existing families. @@ -165,8 +164,7 @@ def interwiki(self, prefix: str) -> BaseSite: return self._interwikimap[prefix].site def interwiki_prefix(self, site: BaseSite) -> list[str]: - """ - Return the interwiki prefixes going to that site. + """Return the interwiki prefixes going to that site. The interwiki prefixes are ordered first by length (shortest first) and then alphabetically. :py:obj:`interwiki(prefix)` is not @@ -186,8 +184,7 @@ def interwiki_prefix(self, site: BaseSite) -> list[str]: return sorted(prefixes, key=lambda p: (len(p), p)) def local_interwiki(self, prefix: str) -> bool: - """ - Return whether the interwiki prefix is local. + """Return whether the interwiki prefix is local. A local interwiki prefix is handled by the target site like a normal link. So if that link also contains an interwiki link it does follow @@ -289,8 +286,7 @@ def _generator( @staticmethod def _request_class(kwargs: dict[str, Any]) -> type[api.Request]: - """ - Get the appropriate class. + """Get the appropriate class. Inside this class kwargs use the parameters mode but QueryGenerator may use the old kwargs mode. @@ -459,8 +455,7 @@ def _relogin(self) -> None: self.login() def logout(self) -> None: - """ - Logout of the site and load details for the logged out user. + """Logout of the site and load details for the logged out user. Also logs out of the global account if linked to the user. @@ -797,8 +792,7 @@ def is_locked(self, return 'locked' in self.get_globaluserinfo(user, force) def get_searched_namespaces(self, force: bool = False) -> set[Namespace]: - """ - Retrieve the default searched namespaces for the user. + """Retrieve the default searched namespaces for the user. If no user is logged in, it returns the namespaces used by default. Otherwise it returns the user preferences. It caches the last result @@ -1125,8 +1119,7 @@ def expand_text( return req.submit()['expandtemplates']['wikitext'] def getcurrenttimestamp(self) -> str: - """ - Return the server time as a MediaWiki timestamp string. + """Return the server time as a MediaWiki timestamp string. It calls :py:obj:`server_time` first so it queries the server to get the current server time. @@ -1136,8 +1129,7 @@ def getcurrenttimestamp(self) -> str: return self.server_time().totimestampformat() def server_time(self) -> pywikibot.Timestamp: - """ - Return a Timestamp object representing the current server time. + """Return a Timestamp object representing the current server time. It uses the 'time' property of the siteinfo 'general'. It'll force a reload before returning the time. @@ -1293,8 +1285,7 @@ def image_repository(self) -> BaseSite | None: return None def data_repository(self) -> pywikibot.site.DataSite | None: - """ - Return the data repository connected to this site. + """Return the data repository connected to this site. :return: The data repository if one is connected or None otherwise. """ @@ -1337,8 +1328,7 @@ def page_from_repository( self, item: str ) -> pywikibot.page.Page | None: - """ - Return a Page for this site object specified by Wikibase item. + """Return a Page for this site object specified by Wikibase item. Usage: @@ -2747,8 +2737,7 @@ def undelete( } def protection_types(self) -> set[str]: - """ - Return the protection types available on this site. + """Return the protection types available on this site. **Example:** @@ -2764,8 +2753,7 @@ def protection_types(self) -> set[str]: @need_version('1.27.3') def protection_levels(self) -> set[str]: - """ - Return the protection levels available on this site. + """Return the protection levels available on this site. **Example:** @@ -2862,8 +2850,7 @@ def blockuser( reblock: bool = False, allowusertalk: bool = False ) -> dict[str, Any]: - """ - Block a user for certain amount of time and for a certain reason. + """Block a user for certain amount of time and for a certain reason. .. seealso:: :api:`Block` @@ -2908,8 +2895,7 @@ def unblockuser( user: pywikibot.page.User, reason: str | None = None ) -> dict[str, Any]: - """ - Remove the block for the user. + """Remove the block for the user. .. seealso:: :api:`Block` @@ -2957,8 +2943,7 @@ def purgepages( converttitles: bool = False, redirects: bool = False ) -> bool: - """ - Purge the server's cache for one or multiple pages. + """Purge the server's cache for one or multiple pages. :param pages: list of Page objects :param redirects: Automatically resolve redirects. @@ -3054,8 +3039,7 @@ def upload( return Uploader(self, filepage, **kwargs).upload() def get_property_names(self, force: bool = False) -> list[str]: - """ - Get property names for pages_with_property(). + """Get property names for pages_with_property(). .. seealso:: :api:`Pagepropnames` @@ -3072,8 +3056,7 @@ def compare( diff: _CompType, difftype: str = 'table' ) -> str: - """ - Corresponding method to the 'action=compare' API action. + """Corresponding method to the 'action=compare' API action. .. hint:: Use :func:`diff.html_comparator` function to parse result. diff --git a/pywikibot/site/_basesite.py b/pywikibot/site/_basesite.py index 570f23c724..bf65bd21dd 100644 --- a/pywikibot/site/_basesite.py +++ b/pywikibot/site/_basesite.py @@ -37,8 +37,7 @@ class BaseSite(ComparableMixin): """Site methods that are independent of the communication interface.""" def __init__(self, code: str, fam=None, user=None) -> None: - """ - Initializer. + """Initializer. :param code: the site's language code :type code: str @@ -122,8 +121,7 @@ def family(self): @property def code(self): - """ - The identifying code for this Site equal to the wiki prefix. + """The identifying code for this Site equal to the wiki prefix. By convention, this is usually an ISO language code, but it does not have to be. @@ -296,8 +294,7 @@ def pagename2codes(self) -> list[str]: return ['PAGENAMEE'] def lock_page(self, page, block: bool = True): - """ - Lock page for writing. Must be called before writing any page. + """Lock page for writing. Must be called before writing any page. We don't want different threads trying to write to the same page at the same time, even to different sections. @@ -317,8 +314,7 @@ def lock_page(self, page, block: bool = True): self._locked_pages.add(title) def unlock_page(self, page) -> None: - """ - Unlock page. Call as soon as a write operation has completed. + """Unlock page. Call as soon as a write operation has completed. :param page: the page to be locked :type page: pywikibot.Page @@ -385,8 +381,7 @@ def redirect_regex(self) -> Pattern[str]: re.IGNORECASE | re.DOTALL) def sametitle(self, title1: str, title2: str) -> bool: - """ - Return True if title1 and title2 identify the same wiki page. + """Return True if title1 and title2 identify the same wiki page. title1 and title2 may be unequal but still identify the same page, if they use different aliases for the same namespace. diff --git a/pywikibot/site/_datasite.py b/pywikibot/site/_datasite.py index b69562dd65..802f5fe309 100644 --- a/pywikibot/site/_datasite.py +++ b/pywikibot/site/_datasite.py @@ -50,8 +50,7 @@ def __init__(self, *args, **kwargs) -> None: } def get_repo_for_entity_type(self, entity_type: str) -> DataSite: - """ - Get the data repository for the entity type. + """Get the data repository for the entity type. When no foreign repository is defined for the entity type, the method returns this repository itself even if it does not @@ -86,8 +85,7 @@ def _cache_entity_namespaces(self) -> None: break def get_namespace_for_entity_type(self, entity_type): - """ - Return namespace for given entity type. + """Return namespace for given entity type. :return: corresponding namespace :rtype: Namespace @@ -103,8 +101,7 @@ def get_namespace_for_entity_type(self, entity_type): @property def item_namespace(self): - """ - Return namespace for items. + """Return namespace for items. :return: item namespace :rtype: Namespace @@ -115,8 +112,7 @@ def item_namespace(self): @property def property_namespace(self): - """ - Return namespace for properties. + """Return namespace for properties. :return: property namespace :rtype: Namespace @@ -127,8 +123,7 @@ def property_namespace(self): return self._property_namespace def get_entity_for_entity_id(self, entity_id): - """ - Return a new instance for given entity id. + """Return a new instance for given entity id. :raises pywikibot.exceptions.NoWikibaseEntityError: there is no entity with the id @@ -145,8 +140,7 @@ def get_entity_for_entity_id(self, entity_id): @property @need_version('1.28-wmf.3') def sparql_endpoint(self): - """ - Return the sparql endpoint url, if any has been set. + """Return the sparql endpoint url, if any has been set. :return: sparql endpoint url :rtype: str|None @@ -156,8 +150,7 @@ def sparql_endpoint(self): @property @need_version('1.28-wmf.23') def concept_base_uri(self): - """ - Return the base uri for concepts/entities. + """Return the base uri for concepts/entities. :return: concept base uri :rtype: str @@ -182,8 +175,7 @@ def tabular_data_repository(self): return None def loadcontent(self, identification, *props): - """ - Fetch the current content of a Wikibase item. + """Fetch the current content of a Wikibase item. This is called loadcontent since wbgetentities does not support fetching old @@ -451,8 +443,7 @@ def save_claim(self, summary: str | None = None, bot: bool = True, tags: str | None = None): - """ - Save the whole claim to the wikibase site. + """Save the whole claim to the wikibase site. .. versionchanged:: 9.4 *tags* parameter was added @@ -776,8 +767,7 @@ def mergeItems(self, @need_extension('WikibaseLexeme') def mergeLexemes(self, from_lexeme, to_lexeme, summary=None, *, bot: bool = True) -> dict: - """ - Merge two lexemes together. + """Merge two lexemes together. :param from_lexeme: Lexeme to merge from :type from_lexeme: pywikibot.LexemePage @@ -802,8 +792,7 @@ def mergeLexemes(self, from_lexeme, to_lexeme, summary=None, *, @need_right('item-redirect') def set_redirect_target(self, from_item, to_item, bot: bool = True): - """ - Make a redirect to another item. + """Make a redirect to another item. :param to_item: title of target item. :type to_item: pywikibot.ItemPage @@ -823,8 +812,7 @@ def set_redirect_target(self, from_item, to_item, bot: bool = True): def search_entities(self, search: str, language: str, total: int | None = None, **kwargs): - """ - Search for pages or properties that contain the given text. + """Search for pages or properties that contain the given text. :param search: Text to find. :param language: Language to search in. @@ -858,8 +846,7 @@ def parsevalue(self, datatype: str, values: list[str], options: dict[str, Any] | None = None, language: str | None = None, validate: bool = False) -> list[Any]: - """ - Send data values to the wikibase parser for interpretation. + """Send data values to the wikibase parser for interpretation. .. versionadded:: 7.5 .. seealso:: `wbparsevalue API @@ -913,8 +900,7 @@ def parsevalue(self, datatype: str, values: list[str], @need_right('edit') def _wbset_action(self, itemdef, action: str, action_data, **kwargs) -> dict: - """ - Execute wbset{action} on a Wikibase entity. + """Execute wbset{action} on a Wikibase entity. Supported actions are: wbsetaliases, wbsetdescription, wbsetlabel and wbsetsitelink @@ -1028,16 +1014,14 @@ def prepare_data(action, data): return req.submit() def wbsetaliases(self, itemdef, aliases, **kwargs): - """ - Set aliases for a single Wikibase entity. + """Set aliases for a single Wikibase entity. See self._wbset_action() for parameters """ return self._wbset_action(itemdef, 'wbsetaliases', aliases, **kwargs) def wbsetdescription(self, itemdef, description, **kwargs): - """ - Set description for a single Wikibase entity. + """Set description for a single Wikibase entity. See self._wbset_action() """ @@ -1045,16 +1029,14 @@ def wbsetdescription(self, itemdef, description, **kwargs): **kwargs) def wbsetlabel(self, itemdef, label, **kwargs): - """ - Set label for a single Wikibase entity. + """Set label for a single Wikibase entity. See self._wbset_action() for parameters """ return self._wbset_action(itemdef, 'wbsetlabel', label, **kwargs) def wbsetsitelink(self, itemdef, sitelink, **kwargs): - """ - Set, remove or modify a sitelink on a Wikibase item. + """Set, remove or modify a sitelink on a Wikibase item. See self._wbset_action() for parameters """ @@ -1064,8 +1046,7 @@ def wbsetsitelink(self, itemdef, sitelink, **kwargs): @need_extension('WikibaseLexeme') def add_form(self, lexeme, form, *, bot: bool = True, baserevid=None) -> dict: - """ - Add a form. + """Add a form. :param lexeme: Lexeme to modify :type lexeme: pywikibot.LexemePage @@ -1091,8 +1072,7 @@ def add_form(self, lexeme, form, *, bot: bool = True, @need_right('edit') @need_extension('WikibaseLexeme') def remove_form(self, form, *, bot: bool = True, baserevid=None) -> dict: - """ - Remove a form. + """Remove a form. :param form: Form to be removed :type form: pywikibot.LexemeForm @@ -1116,8 +1096,7 @@ def remove_form(self, form, *, bot: bool = True, baserevid=None) -> dict: @need_extension('WikibaseLexeme') def edit_form_elements(self, form, data, *, bot: bool = True, baserevid=None) -> dict: - """ - Edit lexeme form elements. + """Edit lexeme form elements. :param form: Form :type form: pywikibot.LexemeForm diff --git a/pywikibot/site/_extensions.py b/pywikibot/site/_extensions.py index 00b15a1b11..77a8344032 100644 --- a/pywikibot/site/_extensions.py +++ b/pywikibot/site/_extensions.py @@ -190,8 +190,7 @@ class PageImagesMixin: @need_extension('PageImages') def loadpageimage(self, page) -> None: - """ - Load [[mw:Extension:PageImages]] info. + """Load [[mw:Extension:PageImages]] info. :param page: The page for which to obtain the image :type page: pywikibot.Page @@ -399,8 +398,7 @@ class FlowMixin: @need_extension('Flow') @deprecated(since='9.4.0') def load_board(self, page): - """ - Retrieve the data for a Flow board. + """Retrieve the data for a Flow board. :param page: A Flow board :type page: Board @@ -425,8 +423,7 @@ def load_topiclist(self, offset_id: str | None = None, reverse: bool = False, include_offset: bool = False) -> dict[str, Any]: - """ - Retrieve the topiclist of a Flow board. + """Retrieve the topiclist of a Flow board. .. versionchanged:: 8.0 All parameters except *page* are keyword only parameters. @@ -460,8 +457,7 @@ def load_topiclist(self, @need_extension('Flow') @deprecated(since='9.4.0') def load_topic(self, page, content_format: str): - """ - Retrieve the data for a Flow topic. + """Retrieve the data for a Flow topic. :param page: A Flow topic :type page: Topic @@ -479,8 +475,7 @@ def load_topic(self, page, content_format: str): @need_extension('Flow') @deprecated(since='9.4.0') def load_post_current_revision(self, page, post_id, content_format: str): - """ - Retrieve the data for a post to a Flow topic. + """Retrieve the data for a post to a Flow topic. :param page: A Flow topic :type page: Topic @@ -501,8 +496,7 @@ def load_post_current_revision(self, page, post_id, content_format: str): @need_extension('Flow') @deprecated(since='9.4.0') def create_new_topic(self, page, title, content, content_format): - """ - Create a new topic on a Flow board. + """Create a new topic on a Flow board. :param page: A Flow board :type page: Board @@ -550,8 +544,7 @@ def reply_to_post(self, page, reply_to_uuid: str, content: str, @need_extension('Flow') @deprecated(since='9.4.0') def lock_topic(self, page, lock, reason): - """ - Lock or unlock a Flow topic. + """Lock or unlock a Flow topic. :param page: A Flow topic :type page: Topic @@ -575,8 +568,7 @@ def lock_topic(self, page, lock, reason): @need_extension('Flow') @deprecated(since='9.4.0') def moderate_topic(self, page, state, reason): - """ - Moderate a Flow topic. + """Moderate a Flow topic. :param page: A Flow topic :type page: Topic @@ -598,8 +590,7 @@ def moderate_topic(self, page, state, reason): @need_extension('Flow') @deprecated(since='9.4.0') def summarize_topic(self, page, summary): - """ - Add summary to Flow topic. + """Add summary to Flow topic. :param page: A Flow topic :type page: Topic @@ -623,8 +614,7 @@ def summarize_topic(self, page, summary): @need_extension('Flow') @deprecated(since='9.4.0') def delete_topic(self, page, reason): - """ - Delete a Flow topic. + """Delete a Flow topic. :param page: A Flow topic :type page: Topic @@ -639,8 +629,7 @@ def delete_topic(self, page, reason): @need_extension('Flow') @deprecated(since='9.4.0') def hide_topic(self, page, reason): - """ - Hide a Flow topic. + """Hide a Flow topic. :param page: A Flow topic :type page: Topic @@ -655,8 +644,7 @@ def hide_topic(self, page, reason): @need_extension('Flow') @deprecated(since='9.4.0') def suppress_topic(self, page, reason): - """ - Suppress a Flow topic. + """Suppress a Flow topic. :param page: A Flow topic :type page: Topic @@ -671,8 +659,7 @@ def suppress_topic(self, page, reason): @need_extension('Flow') @deprecated(since='9.4.0') def restore_topic(self, page, reason): - """ - Restore a Flow topic. + """Restore a Flow topic. :param page: A Flow topic :type page: Topic @@ -687,8 +674,7 @@ def restore_topic(self, page, reason): @need_extension('Flow') @deprecated(since='9.4.0') def moderate_post(self, post, state, reason): - """ - Moderate a Flow post. + """Moderate a Flow post. :param post: A Flow post :type post: Post @@ -713,8 +699,7 @@ def moderate_post(self, post, state, reason): @need_extension('Flow') @deprecated(since='9.4.0') def delete_post(self, post, reason): - """ - Delete a Flow post. + """Delete a Flow post. :param post: A Flow post :type post: Post @@ -729,8 +714,7 @@ def delete_post(self, post, reason): @need_extension('Flow') @deprecated(since='9.4.0') def hide_post(self, post, reason): - """ - Hide a Flow post. + """Hide a Flow post. :param post: A Flow post :type post: Post @@ -745,8 +729,7 @@ def hide_post(self, post, reason): @need_extension('Flow') @deprecated(since='9.4.0') def suppress_post(self, post, reason): - """ - Suppress a Flow post. + """Suppress a Flow post. :param post: A Flow post :type post: Post @@ -761,8 +744,7 @@ def suppress_post(self, post, reason): @need_extension('Flow') @deprecated(since='9.4.0') def restore_post(self, post, reason): - """ - Restore a Flow post. + """Restore a Flow post. :param post: A Flow post :type post: Post @@ -780,8 +762,7 @@ class UrlShortenerMixin: @need_extension('UrlShortener') def create_short_link(self, url): - """ - Return a shortened link. + """Return a shortened link. Note that on Wikimedia wikis only metawiki supports this action, and this wiki can process links to all WM domains. diff --git a/pywikibot/site/_generators.py b/pywikibot/site/_generators.py index 8b1221dedc..5c08ebf963 100644 --- a/pywikibot/site/_generators.py +++ b/pywikibot/site/_generators.py @@ -66,8 +66,7 @@ def load_pages_from_pageids( self, pageids: str | Iterable[int | str], ) -> Generator[pywikibot.Page, None, None]: - """ - Return a page generator from pageids. + """Return a page generator from pageids. Pages are iterated in the same order than in the underlying pageids. @@ -387,8 +386,7 @@ def pagereferences( total: int | None = None, content: bool = False, ) -> Iterable[pywikibot.Page]: - """ - Convenience method combining pagebacklinks and page_embeddedin. + """Convenience method combining pagebacklinks and page_embeddedin. :param namespaces: If present, only return links from the namespaces in this list. @@ -1791,8 +1789,7 @@ def alldeletedrevisions( total: int | None = None, **kwargs, ) -> Generator[dict[str, Any], None, None]: - """ - Yield all deleted revisions. + """Yield all deleted revisions. .. seealso:: :api:`Alldeletedrevisions` .. warning:: *user* keyword argument must be given together with diff --git a/pywikibot/site/_interwikimap.py b/pywikibot/site/_interwikimap.py index 8711194846..aa4da13eb5 100644 --- a/pywikibot/site/_interwikimap.py +++ b/pywikibot/site/_interwikimap.py @@ -35,8 +35,7 @@ class _InterwikiMap: """A representation of the interwiki map of a site.""" def __init__(self, site) -> None: - """ - Create an empty uninitialized interwiki map for the given site. + """Create an empty uninitialized interwiki map for the given site. :param site: Given site for which interwiki map is to be created :type site: pywikibot.site.APISite @@ -62,8 +61,7 @@ def _iw_sites(self): return self._map def __getitem__(self, prefix): - """ - Return the site, locality and url for the requested prefix. + """Return the site, locality and url for the requested prefix. :param prefix: Interwiki prefix :type prefix: Dictionary key @@ -81,8 +79,7 @@ def __getitem__(self, prefix): .format(prefix, type(self._iw_sites[prefix].site))) def get_by_url(self, url: str) -> set[str]: - """ - Return a set of prefixes applying to the URL. + """Return a set of prefixes applying to the URL. :param url: URL for the interwiki """ diff --git a/pywikibot/site/_namespace.py b/pywikibot/site/_namespace.py index 1d707442ef..b1d0568053 100644 --- a/pywikibot/site/_namespace.py +++ b/pywikibot/site/_namespace.py @@ -71,8 +71,7 @@ def __new__(cls, name, bases, dic): class Namespace(Iterable, ComparableMixin, metaclass=MetaNamespace): - """ - Namespace site data object. + """Namespace site data object. This is backwards compatible with the structure of entries in site._namespaces which were a list of:: @@ -303,8 +302,7 @@ def builtin_namespaces(cls, case: str = 'first-letter'): @staticmethod def normalize_name(name): - """ - Remove an optional colon before and after name. + """Remove an optional colon before and after name. TODO: reject illegal characters. """ @@ -329,8 +327,7 @@ def normalize_name(name): class NamespacesDict(Mapping): - """ - An immutable dictionary containing the Namespace instances. + """An immutable dictionary containing the Namespace instances. It adds a deprecation message when called as the 'namespaces' property of APISite was callable. @@ -350,8 +347,7 @@ def __iter__(self): return iter(self._namespaces) def __getitem__(self, key: Namespace | int | str) -> Namespace: - """ - Get the namespace with the given key. + """Get the namespace with the given key. :param key: namespace key """ @@ -369,8 +365,7 @@ def __getitem__(self, key: Namespace | int | str) -> Namespace: return super().__getitem__(key) def __getattr__(self, attr: Namespace | int | str) -> Namespace: - """ - Get the namespace with the given key. + """Get the namespace with the given key. :param attr: namespace key """ @@ -390,8 +385,7 @@ def __len__(self) -> int: return len(self._namespaces) def lookup_name(self, name: str) -> Namespace | None: - """ - Find the Namespace for a name also checking aliases. + """Find the Namespace for a name also checking aliases. :param name: Name of the namespace. """ @@ -401,8 +395,7 @@ def lookup_name(self, name: str) -> Namespace | None: return self.lookup_normalized_name(name.lower()) def lookup_normalized_name(self, name: str) -> Namespace | None: - """ - Find the Namespace for a name also checking aliases. + """Find the Namespace for a name also checking aliases. The name has to be normalized and must be lower case. @@ -411,8 +404,7 @@ def lookup_normalized_name(self, name: str) -> Namespace | None: return self._namespace_names.get(name) def resolve(self, identifiers) -> list[Namespace]: - """ - Resolve namespace identifiers to obtain Namespace objects. + """Resolve namespace identifiers to obtain Namespace objects. Identifiers may be any value for which int() produces a valid namespace id, except bool, or any string which Namespace.lookup_name diff --git a/pywikibot/site/_obsoletesites.py b/pywikibot/site/_obsoletesites.py index a165bad4e8..bc99cc4121 100644 --- a/pywikibot/site/_obsoletesites.py +++ b/pywikibot/site/_obsoletesites.py @@ -18,6 +18,7 @@ class RemovedSite(BaseSite): class ClosedSite(APISite): + """Site closed to read-only mode.""" def _closed_error(self, notice: str = '') -> None: diff --git a/pywikibot/site/_siteinfo.py b/pywikibot/site/_siteinfo.py index d0e92e1e6a..5471959113 100644 --- a/pywikibot/site/_siteinfo.py +++ b/pywikibot/site/_siteinfo.py @@ -20,8 +20,7 @@ class Siteinfo(Container): - """ - A 'dictionary' like container for siteinfo. + """A 'dictionary' like container for siteinfo. This class queries the server to get the requested siteinfo property. Optionally it can cache this directly in the instance so that later @@ -90,8 +89,7 @@ def _post_process(prop, data) -> None: data[p] = p in data def _get_siteinfo(self, prop, expiry) -> dict: - """ - Retrieve a siteinfo property. + """Retrieve a siteinfo property. All properties which the site doesn't support contain the default value. Because pre-1.12 no data was @@ -179,8 +177,7 @@ def _is_expired(cache_date, expire): return cache_date + expire < pywikibot.Timestamp.nowutc() def _get_general(self, key: str, expiry): - """ - Return a siteinfo property which is loaded by default. + """Return a siteinfo property which is loaded by default. The property 'general' will be queried if it wasn't yet or it's forced. Additionally all uncached default properties are queried. This way @@ -229,8 +226,7 @@ def get( cache: bool = True, expiry: datetime.datetime | float | bool = False ) -> Any: - """ - Return a siteinfo property. + """Return a siteinfo property. It will never throw an APIError if it only stated, that the siteinfo property doesn't exist. Instead it will use the default value. @@ -325,8 +321,7 @@ def is_recognised(self, key: str) -> bool | None: return None if time is None else bool(time) def get_requested_time(self, key: str): - """ - Return when 'key' was successfully requested from the server. + """Return when 'key' was successfully requested from the server. If the property is actually in the siprop 'general' it returns the last request from the 'general' siprop. diff --git a/pywikibot/site_detect.py b/pywikibot/site_detect.py index 5d3a61cbe8..cbaa93faa3 100644 --- a/pywikibot/site_detect.py +++ b/pywikibot/site_detect.py @@ -39,8 +39,7 @@ class MWSite: """Minimal wiki site class.""" def __init__(self, fromurl, **kwargs) -> None: - """ - Initializer. + """Initializer. :raises pywikibot.exceptions.ServerError: a server error occurred while loading the site diff --git a/pywikibot/specialbots/_upload.py b/pywikibot/specialbots/_upload.py index c10f519639..c3396edb8e 100644 --- a/pywikibot/specialbots/_upload.py +++ b/pywikibot/specialbots/_upload.py @@ -375,16 +375,14 @@ def abort_on_warn(self, warn_code): return self.aborts is True or warn_code in self.aborts def ignore_on_warn(self, warn_code: str): - """ - Determine if the warning message should be ignored. + """Determine if the warning message should be ignored. :param warn_code: The warning message """ return self.ignore_warning is True or warn_code in self.ignore_warning def upload_file(self, file_url: str) -> str | None: - """ - Upload the image at file_url to the target wiki. + """Upload the image at file_url to the target wiki. .. seealso:: :api:`Upload` diff --git a/pywikibot/textlib.py b/pywikibot/textlib.py index 4ab11ca2f3..b25ce1c1fd 100644 --- a/pywikibot/textlib.py +++ b/pywikibot/textlib.py @@ -112,8 +112,7 @@ def to_local_digits(phrase: str | int, lang: str) -> str: - """ - Change Latin digits based on language to localized version. + """Change Latin digits based on language to localized version. Be aware that this function only works for several languages, and that it returns an unchanged string if an unsupported language is given. @@ -380,8 +379,7 @@ def replaceExcept(text: str, marker: str = '', site: pywikibot.site.BaseSite | None = None, count: int = 0) -> str: - """ - Return text with *old* replaced by *new*, ignoring specified types of text. + """Return text with *old* replaced by *new*, ignoring specified text types. Skip occurrences of *old* within *exceptions*; e.g. within nowiki tags or HTML comments. If *caseInsensitive* is true, then use case @@ -500,8 +498,7 @@ def removeDisabledParts(text: str, include: Container | None = None, site: pywikibot.site.BaseSite | None = None ) -> str: - """ - Return text without portions where wiki markup is disabled. + """Return text without portions where wiki markup is disabled. Parts that will be removed by default are: @@ -619,8 +616,7 @@ def handle_endtag(self, tag) -> None: def isDisabled(text: str, index: int, tags=None) -> bool: - """ - Return True if text[index] is disabled, e.g. by a comment or nowiki tags. + """Return True if text[index] is disabled, e.g. by a comment or nowiki tag. For the tags parameter, see :py:obj:`removeDisabledParts`. """ @@ -643,8 +639,7 @@ def findmarker(text: str, startwith: str = '@@', def expandmarker(text: str, marker: str = '', separator: str = '') -> str: - """ - Return a marker expanded whitespace and the separator. + """Return a marker expanded whitespace and the separator. It searches for the first occurrence of the marker and gets the combination of the separator and whitespace directly before it. @@ -1251,8 +1246,7 @@ def removeLanguageLinks(text: str, site=None, marker: str = '') -> str: def removeLanguageLinksAndSeparator(text: str, site=None, marker: str = '', separator: str = '') -> str: - """ - Return text with inter-language links and preceding separators removed. + """Return text with inter-language links and preceding separators removed. If a link to an unknown language is encountered, a warning is printed. @@ -1532,8 +1526,7 @@ def removeCategoryLinks(text: str, site=None, marker: str = '') -> str: def removeCategoryLinksAndSeparator(text: str, site=None, marker: str = '', separator: str = '') -> str: - """ - Return text with category links and preceding separators removed. + """Return text with category links and preceding separators removed. :param text: The text that needs to be modified. :param site: The site that the text is coming from. @@ -1556,8 +1549,7 @@ def removeCategoryLinksAndSeparator(text: str, site=None, marker: str = '', def replaceCategoryInPlace(oldtext, oldcat, newcat, site=None, add_only: bool = False) -> str: - """ - Replace old category with new one and return the modified text. + """Replace old category with new one and return the modified text. :param oldtext: Content of the old category :param oldcat: pywikibot.Category object of the old category @@ -1613,8 +1605,7 @@ def replaceCategoryLinks(oldtext: str, new: Iterable, site: pywikibot.site.BaseSite | None = None, add_only: bool = False) -> str: - """ - Replace all existing category links with new category links. + """Replace all existing category links with new category links. :param oldtext: The text that needs to be replaced. :param new: Should be a list of Category objects or strings @@ -1866,8 +1857,7 @@ def explicit(param): def extract_templates_and_params_regex_simple(text: str): - """ - Extract top-level templates with params using only a simple regex. + """Extract top-level templates with params using only a simple regex. This function uses only a single regex, and returns an entry for each template called at the top-level of the wikitext. @@ -1922,8 +1912,7 @@ def glue_template_and_params(template_and_params) -> str: # -------------------------- def does_text_contain_section(pagetext: str, section: str) -> bool: - """ - Determine whether the page text contains the given section title. + """Determine whether the page text contains the given section title. It does not care whether a section string may contain spaces or underlines. Both will match. @@ -1963,6 +1952,7 @@ def reformat_ISBNs(text: str, match_func) -> str: class TimeStripperPatterns(NamedTuple): + """Hold precompiled timestamp patterns for :class:`TimeStripper`. Attribute order is important to avoid mismatch when searching. @@ -2143,8 +2133,7 @@ def _last_match_and_replace(self, m = all_matches[-1] def marker(m: Match[str]): - """ - Replace exactly the same number of matched characters. + """Replace exactly the same number of matched characters. Same number of chars shall be replaced, in order to be able to compare pos for matches reliably (absolute pos of a match @@ -2186,8 +2175,7 @@ def _valid_date_dict_positions(dateDict) -> bool: return not min_pos < time_pos < max_pos def timestripper(self, line: str) -> pywikibot.Timestamp | None: - """ - Find timestamp in line and convert it to time zone aware datetime. + """Find timestamp in line and convert it to time zone aware datetime. All the following items must be matched, otherwise None is returned: -. year, month, hour, time, day, minute, tzinfo diff --git a/pywikibot/throttle.py b/pywikibot/throttle.py index d50dafb873..20680a80c2 100644 --- a/pywikibot/throttle.py +++ b/pywikibot/throttle.py @@ -33,6 +33,7 @@ class ProcEntry(NamedTuple): + """ProcEntry namedtuple.""" module_id: str diff --git a/pywikibot/time.py b/pywikibot/time.py index 002ce516f6..4f4e002dbd 100644 --- a/pywikibot/time.py +++ b/pywikibot/time.py @@ -318,8 +318,7 @@ def fromtimestampformat(cls, return cls._from_mw(ts) def isoformat(self, sep: str = 'T') -> str: # type: ignore[override] - """ - Convert object to an ISO 8601 timestamp accepted by MediaWiki. + """Convert object to an ISO 8601 timestamp accepted by MediaWiki. datetime.datetime.isoformat does not postfix the ISO formatted date with a 'Z' unless a timezone is included, which causes MediaWiki @@ -332,8 +331,7 @@ def totimestampformat(self) -> str: return self.strftime(self.mediawikiTSFormat) def posix_timestamp(self) -> float: - """ - Convert object to a POSIX timestamp. + """Convert object to a POSIX timestamp. See Note in datetime.timestamp(). @@ -504,8 +502,7 @@ def now(cls, tz=None) -> Timestamp: class TZoneFixedOffset(datetime.tzinfo): - """ - Class building tzinfo objects for fixed-offset time zones. + """Class building tzinfo objects for fixed-offset time zones. :param offset: a number indicating fixed offset in minutes east from UTC :param name: a string with name of the timezone @@ -541,8 +538,7 @@ def str2timedelta( string: str, timestamp: datetime.datetime | None = None, ) -> datetime.timedelta: - """ - Return a timedelta for a shorthand duration. + """Return a timedelta for a shorthand duration. :param string: a string defining a time period: @@ -575,8 +571,7 @@ def str2timedelta( def parse_duration(string: str) -> tuple[str, int]: - """ - Return the key and duration extracted from the string. + """Return the key and duration extracted from the string. :param string: a string defining a time period diff --git a/pywikibot/titletranslate.py b/pywikibot/titletranslate.py index ace5183e67..4004fb84e8 100644 --- a/pywikibot/titletranslate.py +++ b/pywikibot/titletranslate.py @@ -17,8 +17,7 @@ def translate( removebrackets: bool = False, site=None ) -> list[pywikibot.Link]: - """ - Return a list of links to pages on other sites based on hints. + """Return a list of links to pages on other sites based on hints. Entries for single page titles list those pages. Page titles for entries such as "all:" or "xyz:" or "20:" are first built from the page title of diff --git a/pywikibot/tools/__init__.py b/pywikibot/tools/__init__.py index 00ce4c35c5..641e14a4fc 100644 --- a/pywikibot/tools/__init__.py +++ b/pywikibot/tools/__init__.py @@ -159,8 +159,7 @@ def has_module(module: str, version: str | None = None) -> bool: class classproperty: # noqa: N801 - """ - Descriptor class to access a class method as a property. + """Descriptor class to access a class method as a property. This class may be used as a decorator:: @@ -297,8 +296,7 @@ def __ne__(self, other): def first_lower(string: str) -> str: - """ - Return a string with the first character uncapitalized. + """Return a string with the first character uncapitalized. Empty strings are supported. The original string is not changed. @@ -313,8 +311,7 @@ def first_lower(string: str) -> str: def first_upper(string: str) -> str: - """ - Return a string with the first character capitalized. + """Return a string with the first character capitalized. Empty strings are supported. The original string is not changed. @@ -419,8 +416,7 @@ def normalize_username(username) -> str | None: @total_ordering class MediaWikiVersion: - """ - Version object to allow comparing 'wmf' versions with normal ones. + """Version object to allow comparing 'wmf' versions with normal ones. The version mainly consist of digits separated by periods. After that is a suffix which may only be 'wmf', 'alpha', @@ -449,8 +445,7 @@ class MediaWikiVersion: r'(\d+(?:\.\d+)+)(-?wmf\.?(\d+)|alpha|beta(\d+)|-?rc\.?(\d+)|.*)?') def __init__(self, version_str: str) -> None: - """ - Initializer. + """Initializer. :param version_str: version to parse """ @@ -527,8 +522,7 @@ def __lt__(self, other: Any) -> bool: def open_archive(filename: str, mode: str = 'rb', use_extension: bool = True): - """ - Open a file and uncompress it if needed. + """Open a file and uncompress it if needed. This function supports bzip2, gzip, 7zip, lzma, and xz as compression containers. It uses the packages available in the @@ -632,8 +626,7 @@ def open_archive(filename: str, mode: str = 'rb', use_extension: bool = True): def merge_unique_dicts(*args, **kwargs): - """ - Return a merged dict and make sure that the original dicts keys are unique. + """Return a merged dict and make sure that the original keys are unique. The positional arguments are the dictionaries to be merged. It is also possible to define an additional dict using the keyword diff --git a/pywikibot/tools/_deprecate.py b/pywikibot/tools/_deprecate.py index bff2a18c8a..c1e6512871 100644 --- a/pywikibot/tools/_deprecate.py +++ b/pywikibot/tools/_deprecate.py @@ -109,8 +109,7 @@ def get_wrapper_depth(wrapper): def add_full_name(obj): - """ - A decorator to add __full_name__ to the function being decorated. + """A decorator to add __full_name__ to the function being decorated. This should be done for all decorators used in pywikibot, as any decorator that does not add __full_name__ will prevent other @@ -522,8 +521,7 @@ def wrapper(*args: Any, **kwargs: Any) -> Any: def remove_last_args(arg_names): - """ - Decorator to declare all args additionally provided deprecated. + """Decorator to declare all args additionally provided deprecated. All positional arguments appearing after the normal arguments are marked deprecated. It marks also all keyword arguments present in arg_names as @@ -591,8 +589,7 @@ def redirect_func(target, *, class_name: str | None = None, since: str = '', future_warning: bool = True): - """ - Return a function which can be used to redirect to 'target'. + """Return a function which can be used to redirect to 'target'. It also acts like marking that function deprecated and copies all parameters. @@ -675,8 +672,7 @@ def add_deprecated_attr(self, name: str, replacement: Any = None, *, warning_message: str | None = None, since: str = '', future_warning: bool = True): - """ - Add the name to the local deprecated names dict. + """Add the name to the local deprecated names dict. .. versionchanged:: 7.0 ``since`` parameter must be a release number, not a timestamp. diff --git a/pywikibot/tools/collections.py b/pywikibot/tools/collections.py index cd4d9a9a2d..3a1570f870 100644 --- a/pywikibot/tools/collections.py +++ b/pywikibot/tools/collections.py @@ -148,8 +148,7 @@ class CombinedError(KeyError, IndexError): class EmptyDefault(str, Mapping): - """ - A default for a not existing siteinfo property. + """A default for a not existing siteinfo property. It should be chosen if there is no better default known. It acts like an empty collections, so it can be iterated through it safely if treated as a diff --git a/pywikibot/tools/djvu.py b/pywikibot/tools/djvu.py index 43b1af3459..24b76b44ae 100644 --- a/pywikibot/tools/djvu.py +++ b/pywikibot/tools/djvu.py @@ -15,8 +15,7 @@ def _call_cmd(args, lib: str = 'djvulibre') -> tuple: - """ - Tiny wrapper around subprocess.Popen(). + """Tiny wrapper around subprocess.Popen(). :param args: same as Popen() :type args: str or typing.Sequence[string] @@ -52,8 +51,7 @@ class DjVuFile: """ def __init__(self, file: str) -> None: - """ - Initializer. + """Initializer. :param file: filename (including path) to djvu file """ @@ -108,8 +106,7 @@ def wrapper(obj, *args, **kwargs): @check_cache def number_of_images(self, force: bool = False): - """ - Return the number of images in the djvu file. + """Return the number of images in the djvu file. :param force: if True, refresh the cached data """ @@ -122,8 +119,7 @@ def number_of_images(self, force: bool = False): @check_page_number def page_info(self, n: int, force: bool = False): - """ - Return a tuple (id, (size, dpi)) for page n of djvu file. + """Return a tuple (id, (size, dpi)) for page n of djvu file. :param n: page n of djvu file :param force: if True, refresh the cached data @@ -134,9 +130,9 @@ def page_info(self, n: int, force: bool = False): @check_cache def _get_page_info(self, force: bool = False): - """ - Return a dict of tuples (id, (size, dpi)) for all pages of djvu file. + """Return a dict of tuples for all pages of djvu file. + The tuples consist of (id, (size, dpi)). :param force: if True, refresh the cached data """ if not hasattr(self, '_page_info'): @@ -181,8 +177,7 @@ def get_most_common_info(self): @check_cache def has_text(self, force: bool = False): - """ - Test if the djvu file has a text-layer. + """Test if the djvu file has a text-layer. :param force: if True, refresh the cached data """ @@ -212,8 +207,7 @@ def _remove_control_chars(data): @check_page_number @check_cache def get_page(self, n: int, force: bool = False): - """ - Get page n for djvu file. + """Get page n for djvu file. :param n: page n of djvu file :param force: if True, refresh the cached data diff --git a/pywikibot/tools/formatter.py b/pywikibot/tools/formatter.py index 59c3aced71..fa2f3df133 100644 --- a/pywikibot/tools/formatter.py +++ b/pywikibot/tools/formatter.py @@ -69,8 +69,7 @@ def output(self) -> None: @deprecated('New color format pattern like <>colored text<>', since='7.2.0') def color_format(text: str, *args, **kwargs) -> str: - r""" - Do ``str.format`` without having to worry about colors. + r"""Do ``str.format`` without having to worry about colors. It is automatically adding \\03 in front of color fields so it's unnecessary to add them manually. Any other \\03 in the text is diff --git a/pywikibot/tools/itertools.py b/pywikibot/tools/itertools.py index 41fc5cac61..df3fffddb0 100644 --- a/pywikibot/tools/itertools.py +++ b/pywikibot/tools/itertools.py @@ -65,8 +65,7 @@ def itergroup(iterable, def islice_with_ellipsis(iterable, *args, marker: str = '…'): - """ - Generator which yields the first n elements of the iterable. + """Generator which yields the first n elements of the iterable. If more elements are available and marker is True, it returns an extra string marker as continuation mark. @@ -216,8 +215,7 @@ def roundrobin_generators(*iterables) -> Generator[Any, None, None]: def filter_unique(iterable, container=None, key=None, add=None): - """ - Yield unique items from an iterable, omitting duplicates. + """Yield unique items from an iterable, omitting duplicates. By default, to provide uniqueness, it puts the generated items into a set created as a local variable. It only yields items which are not diff --git a/pywikibot/tools/threading.py b/pywikibot/tools/threading.py index 7ea4590def..b4820ff30b 100644 --- a/pywikibot/tools/threading.py +++ b/pywikibot/tools/threading.py @@ -22,6 +22,7 @@ class RLock: + """Context manager which implements extended reentrant lock objects. This RLock is implicit derived from threading.RLock but provides a diff --git a/pywikibot/userinterfaces/gui.py b/pywikibot/userinterfaces/gui.py index 43774f3450..6a18d84f4d 100644 --- a/pywikibot/userinterfaces/gui.py +++ b/pywikibot/userinterfaces/gui.py @@ -219,8 +219,7 @@ def replace_event(self, event=None) -> str: return 'break' def find_all(self, s): - """ - Highlight all occurrences of string s, and select the first one. + """Highlight all occurrences of string s, and select the first one. If the string has already been highlighted, jump to the next occurrence after the current selection. (You cannot go backwards using the @@ -394,8 +393,7 @@ def __init__(self, parent=None, **kwargs) -> None: def edit(self, text: str, jumpIndex: int | None = None, # noqa: N803 highlight: str | None = None) -> str | None: - """ - Provide user with editor to modify text. + """Provide user with editor to modify text. :param text: the text to be edited :param jumpIndex: position at which to put the caret @@ -441,8 +439,7 @@ def config_dialog(self, event=None) -> None: ConfigDialog(self, 'Settings') def pressedOK(self) -> None: # noqa: N802 - """ - Perform OK operation. + """Perform OK operation. Called when user pushes the OK button. Saves the buffer into a variable, and closes the window. diff --git a/pywikibot/userinterfaces/terminal_interface.py b/pywikibot/userinterfaces/terminal_interface.py index 6c3d240ce1..ce0b5c85e5 100644 --- a/pywikibot/userinterfaces/terminal_interface.py +++ b/pywikibot/userinterfaces/terminal_interface.py @@ -1,5 +1,4 @@ -""" -Platform independent terminal interface module. +"""Platform independent terminal interface module. It imports the appropriate operating system specific implementation. """ diff --git a/pywikibot/userinterfaces/terminal_interface_base.py b/pywikibot/userinterfaces/terminal_interface_base.py index c905a246fa..335eca2afe 100644 --- a/pywikibot/userinterfaces/terminal_interface_base.py +++ b/pywikibot/userinterfaces/terminal_interface_base.py @@ -70,8 +70,7 @@ class UI(ABUIC): split_col_pat = re.compile(r'(\w+);?(\w+)?') def __init__(self) -> None: - """ - Initialize the UI. + """Initialize the UI. This caches the std-streams locally so any attempts to monkey-patch the streams later will not work. @@ -145,8 +144,7 @@ def encounter_color(self, color, target_stream): @classmethod def divide_color(cls, color): - """ - Split color label in a tuple. + """Split color label in a tuple. Received color is a string like 'fg_color;bg_color' or 'fg_color'. Returned values are (fg_color, bg_color) or (fg_color, None). @@ -347,8 +345,7 @@ def input(self, question: str, password: bool = False, default: str | None = '', force: bool = False) -> str: - """ - Ask the user a question and return the answer. + """Ask the user a question and return the answer. Works like raw_input(), but returns a unicode string instead of ASCII. diff --git a/pywikibot/userinterfaces/transliteration.py b/pywikibot/userinterfaces/transliteration.py index 4ad1d85606..7101aac8b4 100644 --- a/pywikibot/userinterfaces/transliteration.py +++ b/pywikibot/userinterfaces/transliteration.py @@ -1105,8 +1105,7 @@ class Transliterator: """Class to transliterating text.""" def __init__(self, encoding: str) -> None: - """ - Initialize the transliteration mapping. + """Initialize the transliteration mapping. :param encoding: the encoding available. Any transliterated character which can't be mapped, will be removed from the mapping. diff --git a/pywikibot/version.py b/pywikibot/version.py index 3d33a29438..67cd656953 100644 --- a/pywikibot/version.py +++ b/pywikibot/version.py @@ -304,8 +304,7 @@ def getversion_onlinerepo(path: str = 'branches/master') -> str: def get_module_filename(module) -> str | None: - """ - Retrieve filename from an imported pywikibot module. + """Retrieve filename from an imported pywikibot module. It uses the __file__ attribute of the module. If it's file extension ends with py and another character the last character is discarded when the py @@ -327,8 +326,7 @@ def get_module_filename(module) -> str | None: def get_module_mtime(module): - """ - Retrieve the modification time from an imported module. + """Retrieve the modification time from an imported module. :param module: The module instance. :type module: module diff --git a/pywikibot/xmlreader.py b/pywikibot/xmlreader.py index 2788ff741e..a1994cffbf 100644 --- a/pywikibot/xmlreader.py +++ b/pywikibot/xmlreader.py @@ -256,8 +256,7 @@ def _fetch_revs(self, elem: Element, with_id=False) -> Iterator[RawRev]: @staticmethod def parse_restrictions(restrictions: str) -> tuple[str | None, str | None]: - """ - Parse the characters within a restrictions tag. + """Parse the characters within a restrictions tag. Returns strings representing user groups allowed to edit and to move a page, where None means there are no restrictions. diff --git a/scripts/add_text.py b/scripts/add_text.py index 216ca023ea..9e84fd1cfc 100755 --- a/scripts/add_text.py +++ b/scripts/add_text.py @@ -176,8 +176,7 @@ def treat_page(self) -> None: def main(*argv: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. @@ -199,8 +198,7 @@ def main(*argv: str) -> None: def parse(argv: Sequence[str], generator_factory: pagegenerators.GeneratorFactory ) -> dict[str, bool | str]: - """ - Parses our arguments and provide a dictionary with their values. + """Parses our arguments and provide a dictionary with their values. :param argv: input arguments to be parsed :param generator_factory: factory that will determine what pages to diff --git a/scripts/archivebot.py b/scripts/archivebot.py index 3a2b22085c..70cb5465f3 100755 --- a/scripts/archivebot.py +++ b/scripts/archivebot.py @@ -287,8 +287,7 @@ def calc_md5_hexdigest(txt, salt) -> str: class DiscussionThread: - """ - An object representing a discussion thread on a page. + """An object representing a discussion thread on a page. It represents something that is of the form:: @@ -906,8 +905,7 @@ def show_md5_key(calc, salt, site) -> bool: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/basic.py b/scripts/basic.py index 446554440f..5d9483f7c1 100755 --- a/scripts/basic.py +++ b/scripts/basic.py @@ -75,8 +75,7 @@ class BasicBot( AutomaticTWSummaryBot, # Automatically defines summary; needs summary_key ): - """ - An incomplete sample bot. + """An incomplete sample bot. :ivar summary_key: Edit summary message key. The message that should be used is placed on /i18n subdirectory. The file containing these @@ -129,8 +128,7 @@ def treat_page(self) -> None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/blockpageschecker.py b/scripts/blockpageschecker.py index 02f5562aa1..0a2c6d9ae6 100755 --- a/scripts/blockpageschecker.py +++ b/scripts/blockpageschecker.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -""" -A bot to remove stale protection templates from pages that are not protected. +"""A bot to remove stale protection templates from unprotected pages. Very often sysops block the pages for a set time but then they forget to remove the warning! This script is useful if you want to remove those @@ -438,8 +437,7 @@ def understand_block(): def main(*args: str) -> None: - """ - Process command line arguments and perform task. + """Process command line arguments and perform task. If args is an empty list, sys.argv is used. diff --git a/scripts/category.py b/scripts/category.py index 83ff20b4c1..9798d27877 100755 --- a/scripts/category.py +++ b/scripts/category.py @@ -239,8 +239,7 @@ def determine_type_target( self, page: pywikibot.Page ) -> pywikibot.Page | None: - """ - Return page to be categorized by type. + """Return page to be categorized by type. :param page: Existing, missing or redirect page to be processed. :return: Page to be categorized. @@ -284,8 +283,7 @@ def determine_type_target( return None def determine_template_target(self, page) -> pywikibot.Page: - """ - Return template page to be categorized. + """Return template page to be categorized. Categories for templates can be included in section of template doc page. @@ -716,8 +714,7 @@ def __init__(self, oldcat, self.move_comment = move_comment if move_comment else self.comment def run(self) -> None: - """ - The main bot function that does all the work. + """The main bot function that does all the work. For readability it is split into several helper functions: - _movecat() @@ -824,8 +821,7 @@ def _delete(self, moved_page, moved_talk) -> None: self.counter['delete talk'] += 1 def _change(self, gen) -> None: - """ - Private function to move category contents. + """Private function to move category contents. Do not use this function from outside the class. @@ -1052,8 +1048,8 @@ def run(self) -> None: class CategoryTidyRobot(Bot, CategoryPreprocess): - """ - Robot to move members of a category into sub- or super-categories. + + """Robot to move members of a category into sub- or super-categories. Specify the category title on the command line. The robot will pick up the page, look for all sub- and super-categories, and show @@ -1098,8 +1094,7 @@ def move_to_category(self, member: pywikibot.Page, original_cat: pywikibot.Category, current_cat: pywikibot.Category) -> None: - """ - Ask whether to move it to one of the sub- or super-categories. + """Ask whether to move it to one of the sub- or super-categories. Given a page in the original_cat category, ask the user whether to move it to one of original_cat's sub- or super-categories. @@ -1113,6 +1108,7 @@ def move_to_category(self, :param current_cat: a category which is questioned. """ class CatContextOption(ContextOption): + """An option to show more and more context and categories.""" @property @@ -1137,6 +1133,7 @@ def out(self) -> str: return text class CatIntegerOption(IntegerOption): + """An option allowing a range of integers.""" @staticmethod @@ -1499,8 +1496,7 @@ def treat(self, child) -> None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/category_graph.py b/scripts/category_graph.py index fd46d3042a..77c6b012a6 100755 --- a/scripts/category_graph.py +++ b/scripts/category_graph.py @@ -67,6 +67,7 @@ class CategoryGraphBot(SingleSiteBot): + """Bot to create graph of the category structure.""" @staticmethod @@ -216,8 +217,7 @@ def run(self) -> None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/category_redirect.py b/scripts/category_redirect.py index 6f1b1e510b..462f36525f 100755 --- a/scripts/category_redirect.py +++ b/scripts/category_redirect.py @@ -560,8 +560,7 @@ def teardown(self) -> None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/change_pagelang.py b/scripts/change_pagelang.py index fc7ee8239f..587fce7ec1 100755 --- a/scripts/change_pagelang.py +++ b/scripts/change_pagelang.py @@ -124,8 +124,7 @@ def treat(self, page) -> None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/checkimages.py b/scripts/checkimages.py index 6dd9bedccd..60abaf6ebe 100755 --- a/scripts/checkimages.py +++ b/scripts/checkimages.py @@ -781,8 +781,7 @@ def load_hidden_templates(self) -> None: def important_image( list_given: list[tuple[float, pywikibot.FilePage]] ) -> pywikibot.FilePage: - """ - Get tuples of image and time, return the most used or oldest image. + """Get tuples of image and time, return the most used or oldest image. .. versionchanged:: 7.2 itertools.zip_longest is used to stop `using_pages` as soon as @@ -1165,8 +1164,7 @@ def mini_template_check(self, template) -> bool: return False def template_in_list(self) -> None: - """ - Check if template is in list. + """Check if template is in list. The problem is the calls to the MediaWiki system because they can be pretty slow. While searching in a list of objects is really fast, so @@ -1184,8 +1182,7 @@ def template_in_list(self) -> None: break def smart_detection(self) -> tuple[str, bool]: - """ - Detect templates. + """Detect templates. The bot instead of checking if there's a simple template in the image's description, checks also if that template is a license or @@ -1325,8 +1322,7 @@ def skip_images(self, skip_number, limit) -> bool: @staticmethod def wait(generator, wait_time) -> Generator[pywikibot.FilePage]: - """ - Skip the images uploaded before x seconds. + """Skip the images uploaded before x seconds. Let the users to fix the image's problem alone in the first x seconds. """ @@ -1538,8 +1534,7 @@ def check_step(self) -> None: def main(*args: str) -> bool: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/claimit.py b/scripts/claimit.py index 3f1ba9e490..68724c9ba0 100755 --- a/scripts/claimit.py +++ b/scripts/claimit.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -""" -A script that adds claims to Wikidata items based on a list of pages. +"""A script that adds claims to Wikidata items based on a list of pages. These command line parameters can be used to specify which pages to work on: @@ -99,8 +98,7 @@ def treat_page_and_item(self, page, item) -> None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/commons_information.py b/scripts/commons_information.py index 7da3aa6763..794408a2bb 100755 --- a/scripts/commons_information.py +++ b/scripts/commons_information.py @@ -296,8 +296,7 @@ def treat_page(self) -> None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/commonscat.py b/scripts/commonscat.py index b71a5597a5..c2ce6786f8 100755 --- a/scripts/commonscat.py +++ b/scripts/commonscat.py @@ -280,8 +280,7 @@ def skipPage(page) -> bool: return False def treat_page(self) -> None: - """ - Add CommonsCat template to page. + """Add CommonsCat template to page. Take a page. Go to all the interwiki page looking for a commonscat template. When all the interwiki's links are checked and a proper @@ -531,8 +530,7 @@ def checkCommonscatLink(self, name: str = ''): def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/coordinate_import.py b/scripts/coordinate_import.py index c802a700f6..e161127a0e 100755 --- a/scripts/coordinate_import.py +++ b/scripts/coordinate_import.py @@ -76,8 +76,7 @@ def __init__(self, **kwargs) -> None: self.create_missing_item = self.opt.create def has_coord_qualifier(self, claims) -> str | None: - """ - Check if self.prop is used as property for a qualifier. + """Check if self.prop is used as property for a qualifier. :param claims: the Wikibase claims to check in :type claims: dict @@ -91,8 +90,7 @@ def has_coord_qualifier(self, claims) -> str | None: return None def item_has_coordinates(self, item) -> bool: - """ - Check if the item has coordinates. + """Check if the item has coordinates. :return: whether the item has coordinates """ @@ -124,8 +122,7 @@ def treat_page_and_item(self, page, item) -> None: self.try_import_coordinates_from_page(page, item) def try_import_coordinates_from_page(self, page, item) -> bool: - """ - Try import coordinate from the given page to the given item. + """Try import coordinate from the given page to the given item. :return: whether any coordinates were found and the import was successful @@ -153,8 +150,7 @@ def try_import_coordinates_from_page(self, page, item) -> bool: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/cosmetic_changes.py b/scripts/cosmetic_changes.py index a916aef7f7..346d721309 100755 --- a/scripts/cosmetic_changes.py +++ b/scripts/cosmetic_changes.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -""" -This module can do slight modifications to tidy a wiki page's source code. +"""This module can do slight modifications to tidy a wiki page's source code. The changes are not supposed to change the look of the rendered wiki page. @@ -91,8 +90,7 @@ def treat_page(self) -> None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/create_isbn_edition.py b/scripts/create_isbn_edition.py index 427b7e1894..75bda4960d 100755 --- a/scripts/create_isbn_edition.py +++ b/scripts/create_isbn_edition.py @@ -763,8 +763,7 @@ def show_final_information(number, doi): def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/data_ingestion.py b/scripts/data_ingestion.py index 0ee55dd3a6..97933af969 100755 --- a/scripts/data_ingestion.py +++ b/scripts/data_ingestion.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -r""" -A generic bot to do data ingestion (batch uploading) of photos or other files. +r"""A generic bot to do data ingestion (batch uploading) of photos or other files. In addition it installs related metadata. The uploading is primarily from a url to a wiki-site. @@ -120,8 +119,7 @@ class Photo(pywikibot.FilePage): def __init__(self, url: str, metadata: dict[str, Any], site: pywikibot.site.APISite | None = None) -> None: - """ - Initializer. + """Initializer. :param url: URL of photo :param metadata: metadata about the photo that can be referred to @@ -144,8 +142,7 @@ def __init__(self, url: str, metadata: dict[str, Any], super().__init__(site, self.get_title('%(_filename)s.%(_ext)s')) def download_photo(self) -> BinaryIO: - """ - Download the photo and store it in an io.BytesIO object. + """Download the photo and store it in an io.BytesIO object. TODO: Add exception handling """ @@ -155,8 +152,7 @@ def download_photo(self) -> BinaryIO: return self.contents def find_duplicate_images(self) -> list[str]: - """ - Find duplicates of the photo. + """Find duplicates of the photo. Calculates the SHA1 hash and asks the MediaWiki API for a list of duplicates. @@ -170,8 +166,7 @@ def find_duplicate_images(self) -> list[str]: sha1=base64.b16encode(hash_object.digest()))] def get_title(self, fmt: str) -> str: - """ - Populate format string with %(name)s entries using metadata. + """Populate format string with %(name)s entries using metadata. .. note:: this does not clean the title, so it may be unusable as a MediaWiki page title, and cause an API exception when used. @@ -215,8 +210,7 @@ class DataIngestionBot(pywikibot.Bot): """Data ingestion bot.""" def __init__(self, titlefmt: str, pagefmt: str, **kwargs) -> None: - """ - Initializer. + """Initializer. :param titlefmt: Title format :param pagefmt: Page format @@ -252,8 +246,7 @@ def treat(self, page) -> None: @classmethod def parse_configuration_page(cls, configuration_page) -> dict[str, str]: - """ - Parse a Page which contains the configuration. + """Parse a Page which contains the configuration. :param configuration_page: page with configuration :type configuration_page: :py:obj:`pywikibot.Page` @@ -282,8 +275,7 @@ def parse_configuration_page(cls, configuration_page) -> dict[str, str]: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/dataextend.py b/scripts/dataextend.py index 1a802cb229..2cdabc38c2 100755 --- a/scripts/dataextend.py +++ b/scripts/dataextend.py @@ -17728,8 +17728,7 @@ def findmixedrefs(self, html: str): def main(*args: tuple[str, ...]) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/delete.py b/scripts/delete.py index 977e3e5d00..05b4e3cf07 100755 --- a/scripts/delete.py +++ b/scripts/delete.py @@ -78,8 +78,7 @@ class PageWithRefs(Page): - """ - A subclass of Page with convenience methods for reference checking. + """A subclass of Page with convenience methods for reference checking. Supports the same interface as Page, with some added methods. """ @@ -99,8 +98,7 @@ def get_ref_table(self, *args, **kwargs) -> RefTable: @property def ref_table(self) -> RefTable: - """ - Build link reference table lazily. + """Build link reference table lazily. This property gives a default table without any parameter set for getReferences(), whereas self.get_ref_table() is able to accept @@ -111,8 +109,7 @@ def ref_table(self) -> RefTable: return self._ref_table def namespaces_with_ref_to_page(self, namespaces=None) -> set[Namespace]: - """ - Check if current page has links from pages in namepaces. + """Check if current page has links from pages in namepaces. If namespaces is None, all namespaces are checked. Returns a set with namespaces where a ref to page is present. @@ -148,8 +145,7 @@ def __init__(self, summary: str, **kwargs) -> None: self.generator = (PageWithRefs(p) for p in self.generator) def display_references(self) -> None: - """ - Display pages that link to the current page, sorted per namespace. + """Display pages that link to the current page, sorted per namespace. Number of pages to display per namespace is provided by: - self.opt.isorphan diff --git a/scripts/delinker.py b/scripts/delinker.py index a2b3c90f70..aa23d20a0e 100755 --- a/scripts/delinker.py +++ b/scripts/delinker.py @@ -222,8 +222,7 @@ def teardown(self): def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/djvutext.py b/scripts/djvutext.py index 6e8d30596e..91898e2c60 100755 --- a/scripts/djvutext.py +++ b/scripts/djvutext.py @@ -56,8 +56,7 @@ class DjVuTextBot(SingleSiteBot): - """ - A bot that uploads text-layer from djvu files to Page:namespace. + """A bot that uploads text-layer from djvu files to Page:namespace. Works only on sites with Proofread Page extension installed. @@ -77,8 +76,7 @@ def __init__( pages: tuple | None = None, **kwargs ) -> None: - """ - Initializer. + """Initializer. :param djvu: djvu from where to fetch the text layer :type djvu: DjVuFile object @@ -137,8 +135,7 @@ def treat(self, page) -> None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/download_dump.py b/scripts/download_dump.py index 46a5890f17..6436b93235 100755 --- a/scripts/download_dump.py +++ b/scripts/download_dump.py @@ -186,8 +186,7 @@ def convert_from_bytes(total_bytes): def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/fixing_redirects.py b/scripts/fixing_redirects.py index d1aea725f7..a505c4e6fa 100755 --- a/scripts/fixing_redirects.py +++ b/scripts/fixing_redirects.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -""" -Correct all redirect links in featured pages or only one page of each wiki. +"""Correct all redirect links in featured pages or only one page of each wiki. Can be used with: @@ -200,8 +199,7 @@ def treat_page(self) -> None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/harvest_template.py b/scripts/harvest_template.py index e495cc207e..30ca6c46fa 100755 --- a/scripts/harvest_template.py +++ b/scripts/harvest_template.py @@ -280,8 +280,7 @@ def template_link_target(item: pywikibot.ItemPage, return linked_item def _get_option_with_fallback(self, handler, option) -> Any: - """ - Compare bot's (global) and provided (local) options. + """Compare bot's (global) and provided (local) options. .. seealso:: :class:`OptionHandler` """ @@ -510,8 +509,7 @@ def handle_commonsmedia( def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/illustrate_wikidata.py b/scripts/illustrate_wikidata.py index 67ca848596..135c1e0cfe 100755 --- a/scripts/illustrate_wikidata.py +++ b/scripts/illustrate_wikidata.py @@ -81,8 +81,7 @@ def treat_page_and_item(self, page, item) -> None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/image.py b/scripts/image.py index 3bd14f6c44..c3d9d9e403 100755 --- a/scripts/image.py +++ b/scripts/image.py @@ -65,8 +65,7 @@ class ImageRobot(ReplaceBot): def __init__(self, generator, old_image: str, new_image: str = '', **kwargs) -> None: - """ - Initializer. + """Initializer. :param generator: the pages to work on :type generator: iterable @@ -121,8 +120,7 @@ def __init__(self, generator, old_image: str, def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/imagetransfer.py b/scripts/imagetransfer.py index 829e766cbb..c6f7f3b70b 100755 --- a/scripts/imagetransfer.py +++ b/scripts/imagetransfer.py @@ -202,8 +202,7 @@ def __init__(self, **kwargs) -> None: self.opt.target = pywikibot.Site(self.opt.target) def transfer_image(self, sourceImagePage) -> None: - """ - Download image and its description, and upload it to another site. + """Download image and its description, and upload it to another site. :return: the filename which was used to upload the image """ @@ -365,8 +364,7 @@ def transfer_allowed(self, image) -> bool: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/interwiki.py b/scripts/interwiki.py index 6b02cd5a17..7cff604166 100755 --- a/scripts/interwiki.py +++ b/scripts/interwiki.py @@ -579,8 +579,7 @@ def readOptions(self, option: str) -> bool: class Subject(interwiki_graph.Subject): - """ - Class to follow the progress of a single 'subject'. + """Class to follow the progress of a single 'subject'. (i.e. a page with all its translations) @@ -675,8 +674,7 @@ def __init__(self, origin=None, hints=None, conf=None) -> None: self.workonme = True def getFoundDisambig(self, site): - """ - Return the first disambiguation found. + """Return the first disambiguation found. If we found a disambiguation on the given site while working on the subject, this method returns it. If several ones have been found, the @@ -690,8 +688,7 @@ def getFoundDisambig(self, site): return None def getFoundNonDisambig(self, site): - """ - Return the first non-disambiguation found. + """Return the first non-disambiguation found. If we found a non-disambiguation on the given site while working on the subject, this method returns it. If several ones have been found, the @@ -708,8 +705,7 @@ def getFoundNonDisambig(self, site): return None def getFoundInCorrectNamespace(self, site): - """ - Return the first page in the extended namespace. + """Return the first page in the extended namespace. If we found a page that has the expected namespace on the given site while working on the subject, this method returns it. If several ones @@ -755,8 +751,7 @@ def translate(self, hints=None, keephintedsites: bool = False) -> None: self.hintedsites.add(page.site) def openSites(self): - """ - Iterator. + """Iterator. Yields (site, count) pairs: * site is a site where we still have work to do on @@ -797,8 +792,7 @@ def makeForcedStop(self, counter) -> None: self.forcedStop = True def addIfNew(self, page, counter, linkingPage) -> bool: - """ - Add the pagelink given to the todo list, if it hasn't been seen yet. + """Add the pagelink given to the todo list, if it hasn't been seen yet. If it is added, update the counter accordingly. @@ -847,8 +841,7 @@ def get_alternative( return pywikibot.Page(site, title) if title else None def namespaceMismatch(self, linkingPage, linkedPage, counter) -> bool: - """ - Check whether or not the given page has a different namespace. + """Check whether or not the given page has a different namespace. Returns True if the namespaces are different and the user has selected not to follow the linked page. @@ -917,8 +910,7 @@ def namespaceMismatch(self, linkingPage, linkedPage, counter) -> bool: return False def disambigMismatch(self, page, counter): - """ - Check whether the given page has a different disambiguation status. + """Check whether the given page has a different disambiguation status. Returns a tuple (skip, alternativePage). @@ -1248,8 +1240,7 @@ def check_page(self, page, counter) -> None: break def batchLoaded(self, counter) -> None: - """ - Notify that the promised batch of pages was loaded. + """Notify that the promised batch of pages was loaded. This is called by a worker to tell us that the promised batch of pages was loaded. @@ -1416,8 +1407,7 @@ def assemble(self): return result def finish(self): - """ - Round up the subject, making any necessary changes. + """Round up the subject, making any necessary changes. This should be called exactly once after the todo list has gone empty. @@ -1745,12 +1735,11 @@ def replaceLinks(self, page, newPages) -> bool: @staticmethod def reportBacklinks(new, updatedSites) -> None: - """ - Report missing back links. This will be called from finish() if needed. - - updatedSites is a list that contains all sites we changed, to avoid - reporting of missing backlinks for pages we already fixed + """Report missing back links. + This will be called from :meth:`finish` if needed. *updatedSites* + is a list that contains all sites that are changed, to avoid + reporting of missing backlinks for already fixed pages. """ # use sets because searching an element is faster than in lists expectedPages = set(new.values()) @@ -1801,8 +1790,7 @@ def reportBacklinks(new, updatedSites) -> None: class InterwikiBot: - """ - A class keeping track of a list of subjects. + """A class keeping track of a list of subjects. It controls which pages are queried from which languages when. """ @@ -1830,8 +1818,7 @@ def add(self, page, hints=None) -> None: self.plus(site, count) def setPageGenerator(self, pageGenerator, number=None, until=None) -> None: - """ - Add a generator of subjects. + """Add a generator of subjects. Once the list of subjects gets too small, this generator is called to produce more Pages. @@ -1910,8 +1897,7 @@ def firstSubject(self) -> Subject | None: return self.subjects[0] if self.subjects else None def maxOpenSite(self): - """ - Return the site that has the most open queries plus the number. + """Return the site that has the most open queries plus the number. If there is nothing left, return None. Only sites that are todo for the first Subject are returned. @@ -1965,8 +1951,7 @@ def selectQuerySite(self): return self.maxOpenSite() def oneQuery(self) -> bool: - """ - Perform one step in the solution process. + """Perform one step in the solution process. Returns True if pages could be preloaded, or false otherwise. @@ -2117,8 +2102,7 @@ def botMayEdit(page) -> bool: def page_empty_check(page) -> bool: - """ - Return True if page should be skipped as it is almost empty. + """Return True if page should be skipped as it is almost empty. Pages in content namespaces are considered empty if they contain less than 50 characters, and other pages are considered empty if they are not diff --git a/scripts/interwikidata.py b/scripts/interwikidata.py index 018c606ba0..328c011bb1 100755 --- a/scripts/interwikidata.py +++ b/scripts/interwikidata.py @@ -146,8 +146,7 @@ def create_item(self) -> pywikibot.ItemPage: return item def handle_complicated(self) -> bool: - """ - Handle pages when they have interwiki conflict. + """Handle pages when they have interwiki conflict. When this method returns True it means conflict has resolved and it's okay to clean old interwiki links. @@ -232,8 +231,7 @@ def try_to_merge(self, item) -> pywikibot.ItemPage | bool | None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/listpages.py b/scripts/listpages.py index 64688ffddb..b052b2138a 100755 --- a/scripts/listpages.py +++ b/scripts/listpages.py @@ -136,8 +136,7 @@ class Formatter: fmt_need_lang = [k for k, v in fmt_options.items() if 'trs_title' in v] def __init__(self, page, outputlang=None, default: str = '******') -> None: - """ - Initializer. + """Initializer. :param page: the page to be formatted. :type page: Page object. @@ -271,8 +270,7 @@ def teardown(self) -> None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/maintenance/cache.py b/scripts/maintenance/cache.py index e3cbb68f16..929d5d88d5 100755 --- a/scripts/maintenance/cache.py +++ b/scripts/maintenance/cache.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -r""" -This script runs commands on each entry in the API caches. +r"""This script runs commands on each entry in the API caches. Syntax: diff --git a/scripts/maintenance/make_i18n_dict.py b/scripts/maintenance/make_i18n_dict.py index 5c22a3b20c..b8f361c6d0 100755 --- a/scripts/maintenance/make_i18n_dict.py +++ b/scripts/maintenance/make_i18n_dict.py @@ -112,8 +112,7 @@ def read(self, oldmsg, newmsg=None): print('WARNING: "en" key missing for message ' + newmsg) def run(self, quiet=False): - """ - Run the bot, read the messages from source and print the dict. + """Run the bot, read the messages from source and print the dict. :param quiet: print the result if False :type quiet: bool @@ -124,8 +123,7 @@ def run(self, quiet=False): self.print_all() def to_json(self, quiet=True): - """ - Run the bot and create json files. + """Run the bot and create json files. :param quiet: Print the result if False :type quiet: bool diff --git a/scripts/misspelling.py b/scripts/misspelling.py index ef3a50d674..12edc75ed7 100755 --- a/scripts/misspelling.py +++ b/scripts/misspelling.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -""" -This script fixes links that contain common spelling mistakes. +"""This script fixes links that contain common spelling mistakes. This is only possible on wikis that have a template for these misspellings. @@ -105,8 +104,7 @@ def generator(self) -> Generator[pywikibot.Page]: yield from pagegenerators.PreloadingGenerator(chain(*generators)) def findAlternatives(self, page) -> bool: - """ - Append link target to a list of alternative links. + """Append link target to a list of alternative links. Overrides the BaseDisambigBot method. @@ -142,8 +140,7 @@ def findAlternatives(self, page) -> bool: return False def setSummaryMessage(self, page, *args, **kwargs) -> None: - """ - Setup the summary message. + """Setup the summary message. Overrides the BaseDisambigBot method. """ @@ -154,8 +151,7 @@ def setSummaryMessage(self, page, *args, **kwargs) -> None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/movepages.py b/scripts/movepages.py index 266e369fab..8086cd9d56 100755 --- a/scripts/movepages.py +++ b/scripts/movepages.py @@ -212,8 +212,7 @@ def treat_page(self) -> None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/newitem.py b/scripts/newitem.py index 6a05b84be0..a3301fbec9 100755 --- a/scripts/newitem.py +++ b/scripts/newitem.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -""" -This script creates new items on Wikidata based on certain criteria. +"""This script creates new items on Wikidata based on certain criteria. * When was the (Wikipedia) page created? * When was the last edit on the page? @@ -182,8 +181,7 @@ def treat_page_and_item(self, page, item) -> None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/noreferences.py b/scripts/noreferences.py index a13a1dcb0c..4c373e6d7d 100755 --- a/scripts/noreferences.py +++ b/scripts/noreferences.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -""" -This script adds a missing references section to pages. +"""This script adds a missing references section to pages. It goes over multiple pages, searches for pages where is missing although a tag is present, and in that case adds a new @@ -620,8 +619,7 @@ def lacksReferences(self, text) -> bool: return True def addReferences(self, oldText) -> str: - """ - Add a references tag into an existing section where it fits into. + """Add a references tag into an existing section where it fits into. If there is no such section, creates a new section containing the references tag. Also repair malformed references tags. @@ -807,8 +805,7 @@ def treat_page(self) -> None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/nowcommons.py b/scripts/nowcommons.py index e8bda2ceb7..02f60d7a1c 100755 --- a/scripts/nowcommons.py +++ b/scripts/nowcommons.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -r""" -Script to delete files that are also present on Wikimedia Commons. +r"""Script to delete files that are also present on Wikimedia Commons. Do not run this script on Wikimedia Commons itself. It works based on a given array of templates defined below. @@ -386,8 +385,7 @@ def teardown(self): def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/pagefromfile.py b/scripts/pagefromfile.py index 5cba3d1fac..2e0e09d0ad 100755 --- a/scripts/pagefromfile.py +++ b/scripts/pagefromfile.py @@ -103,8 +103,7 @@ def __init__(self, offset) -> None: class PageFromFileRobot(SingleSiteBot, CurrentPageBot): - """ - Responsible for writing pages to the wiki. + """Responsible for writing pages to the wiki. Titles and contents are given by a PageFromFileReader. @@ -281,8 +280,7 @@ def find_page(self, text) -> tuple[int, str, str]: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/parser_function_count.py b/scripts/parser_function_count.py index 09a6aabfbb..da298bb0eb 100755 --- a/scripts/parser_function_count.py +++ b/scripts/parser_function_count.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -""" -Used to find expensive templates that are subject to be converted to Lua. +"""Used to find expensive templates that are subject to be converted to Lua. It counts parser functions and then orders templates by number of these and uploads the first n titles or alternatively templates having count()>n. diff --git a/scripts/patrol.py b/scripts/patrol.py index ec0259b6e1..dd9d40f1e8 100755 --- a/scripts/patrol.py +++ b/scripts/patrol.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -""" -The bot is meant to mark the edits based on info obtained by whitelist. +"""The bot is meant to mark the edits based on info obtained by whitelist. This bot obtains a list of recent changes and newpages and marks the edits as patrolled based on a whitelist. @@ -83,8 +82,7 @@ class PatrolBot(BaseBot): } def __init__(self, site=None, **kwargs) -> None: - """ - Initializer. + """Initializer. :keyword ask: If True, confirm each patrol action :keyword whitelist: page title for whitelist (optional) diff --git a/scripts/protect.py b/scripts/protect.py index b9440d24eb..a6681af28b 100755 --- a/scripts/protect.py +++ b/scripts/protect.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -""" -This script can be used to protect and unprotect pages en masse. +"""This script can be used to protect and unprotect pages en masse. Of course, you will need an admin account on the relevant wiki. These command line parameters can be used to specify which pages to work on: @@ -87,8 +86,7 @@ class ProtectionRobot(SingleSiteBot, ConfigParserBot, CurrentPageBot): } def __init__(self, protections, **kwargs) -> None: - """ - Create a new ProtectionRobot. + """Create a new ProtectionRobot. :param protections: protections as a dict with "type": "level" :type protections: dict @@ -150,8 +148,7 @@ def check_protection_level(operation, level, levels, default=None) -> str: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/redirect.py b/scripts/redirect.py index 1e0cb11ab2..aeee362e90 100755 --- a/scripts/redirect.py +++ b/scripts/redirect.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -""" -Script to resolve double redirects, and to delete broken redirects. +"""Script to resolve double redirects, and to delete broken redirects. Requires access to MediaWiki's maintenance pages or to a XML dump file. Delete function requires adminship. @@ -139,8 +138,7 @@ def get_redirects_from_dump( self, alsoGetPageTitles: bool = False ) -> tuple[dict[str, str], set[str]]: - """ - Extract redirects from dump. + """Extract redirects from dump. Load a local XML dump file, look at all pages which have the redirect flag set, and find out where they're pointing at. Return @@ -230,8 +228,7 @@ def get_redirects_via_api( self, maxlen: int = 8 ) -> Generator[tuple[str, int | None, str, str | None]]: - r""" - Return a generator that yields tuples of data about redirect Pages. + r"""Return a generator that yields tuples of data about redirect Pages. .. versionchanged:: 7.0 only yield tuple if type of redirect is not 1 (normal redirect) @@ -693,8 +690,7 @@ def treat(self, page) -> None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/reflinks.py b/scripts/reflinks.py index 95b719e67d..2f7f10e5a5 100755 --- a/scripts/reflinks.py +++ b/scripts/reflinks.py @@ -265,8 +265,7 @@ def transform(self, ispdf: bool = False) -> None: # TODO : remove HTML when both opening and closing tags are included def avoid_uppercase(self) -> None: - """ - Convert to title()-case if title is 70% uppercase characters. + """Convert to title()-case if title is 70% uppercase characters. Skip title that has less than 6 characters. """ @@ -744,8 +743,7 @@ def treat(self, page) -> None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/replace.py b/scripts/replace.py index 0a1daa644d..c4fc4a6d02 100755 --- a/scripts/replace.py +++ b/scripts/replace.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -r""" -This bot will make direct text replacements. +r"""This bot will make direct text replacements. It will retrieve information on which pages might need changes either from an XML dump or a text file, or only change a single page. @@ -325,8 +324,7 @@ def get_inside_exceptions(self): class ReplacementList(list): - """ - A list of replacements which all share some properties. + """A list of replacements which all share some properties. The shared properties are: * use_regex @@ -412,8 +410,7 @@ def get_inside_exceptions(self): class XmlDumpReplacePageGenerator: - """ - Iterator that will yield Pages that might contain text to replace. + """Iterator that will yield Pages that might contain text to replace. These pages will be retrieved from a local XML dump file. @@ -606,8 +603,7 @@ def isTextExcepted(self, text, exceptions=None) -> bool: return False def apply_replacements(self, original_text, applied, page=None): - """ - Apply all replacements to the given text. + """Apply all replacements to the given text. :rtype: str, set """ diff --git a/scripts/replicate_wiki.py b/scripts/replicate_wiki.py index 4f11c209cd..d77ee59b44 100755 --- a/scripts/replicate_wiki.py +++ b/scripts/replicate_wiki.py @@ -226,8 +226,7 @@ def check_page(self, pagename) -> None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/revertbot.py b/scripts/revertbot.py index 94b1136162..bf2f442910 100755 --- a/scripts/revertbot.py +++ b/scripts/revertbot.py @@ -155,8 +155,7 @@ def revert(self, item) -> str | bool: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/solve_disambiguation.py b/scripts/solve_disambiguation.py index 2f309d897d..2b5775243e 100755 --- a/scripts/solve_disambiguation.py +++ b/scripts/solve_disambiguation.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -""" -Script to help a human solve disambiguations by presenting a set of options. +"""Script to help a human solve disambiguations by presenting a set of options. Specify the disambiguation page on the command line. @@ -441,8 +440,7 @@ def __iter__(self) -> Generator[pywikibot.Page]: class PrimaryIgnoreManager: - """ - Primary ignore manager. + """Primary ignore manager. If run with the -primary argument, reads from a file which pages should not be worked on; these are the ones where the user pressed n last time. @@ -630,8 +628,7 @@ def __init__(self, *args, **kwargs) -> None: self.dn_template_str = i18n.translate(self.site, dn_template) def checkContents(self, text: str) -> str | None: # noqa: N802 - """ - Check if the text matches any of the ignore regexes. + """Check if the text matches any of the ignore regexes. :param text: wikitext of a page :return: None if none of the regular expressions @@ -1221,8 +1218,7 @@ def treat(self, page) -> None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/speedy_delete.py b/scripts/speedy_delete.py index 1c753ed8a5..e7f1935ecb 100755 --- a/scripts/speedy_delete.py +++ b/scripts/speedy_delete.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -""" -Help sysops to quickly check and/or delete pages listed for speedy deletion. +"""Help sysops to quickly check and/or delete pages listed for speedy deletion. This bot trawls through candidates for speedy deletion in a fast and semi-automated fashion. It displays the contents of each page @@ -38,6 +37,7 @@ class SpeedyBot(SingleSiteBot, ExistingPageBot): + """Bot to delete pages which are tagged as speedy deletion. This bot will load a list of pages from the category of candidates for diff --git a/scripts/template.py b/scripts/template.py index d76108ace7..770fc42a84 100755 --- a/scripts/template.py +++ b/scripts/template.py @@ -134,8 +134,7 @@ class TemplateRobot(ReplaceBot): } def __init__(self, generator, templates: dict, **kwargs) -> None: - """ - Initializer. + """Initializer. :param generator: the pages to work on :type generator: iterable @@ -209,8 +208,7 @@ def __init__(self, generator, templates: dict, **kwargs) -> None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/templatecount.py b/scripts/templatecount.py index cd4d744a01..266acc286f 100755 --- a/scripts/templatecount.py +++ b/scripts/templatecount.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -""" -Display the list of pages transcluding a given list of templates. +"""Display the list of pages transcluding a given list of templates. It can also be used to simply count the number of pages (rather than listing each individually). @@ -49,8 +48,7 @@ class TemplateCountRobot: @classmethod def count_templates(cls, templates, namespaces) -> None: - """ - Display number of transclusions for a list of templates. + """Display number of transclusions for a list of templates. Displays the number of transcluded page in the given 'namespaces' for each template given by 'templates' list. @@ -75,8 +73,7 @@ def count_templates(cls, templates, namespaces) -> None: @classmethod def list_templates(cls, templates, namespaces) -> None: - """ - Display transcluded pages for a list of templates. + """Display transcluded pages for a list of templates. Displays each transcluded page in the given 'namespaces' for each template given by 'templates' list. @@ -103,8 +100,7 @@ def list_templates(cls, templates, namespaces) -> None: @classmethod def template_dict(cls, templates, namespaces) -> dict[ str, list[pywikibot.Page]]: - """ - Create a dict of templates and its transcluded pages. + """Create a dict of templates and its transcluded pages. The names of the templates are the keys, and lists of pages transcluding templates in the given namespaces are the values. @@ -119,8 +115,7 @@ def template_dict(cls, templates, namespaces) -> dict[ @staticmethod def template_dict_generator(templates, namespaces) -> Generator[ tuple[str, list[pywikibot.Page]], None, None]: - """ - Yield transclusions of each template in 'templates'. + """Yield transclusions of each template in 'templates'. For each template in 'templates', yield a tuple (template, transclusions), where 'transclusions' is a list of all pages @@ -140,8 +135,7 @@ def template_dict_generator(templates, namespaces) -> Generator[ def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/touch.py b/scripts/touch.py index 9004e634ee..2793c36c2e 100755 --- a/scripts/touch.py +++ b/scripts/touch.py @@ -137,8 +137,7 @@ def purgepages(self, flush=False): def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/transferbot.py b/scripts/transferbot.py index 6c8b3b67c9..252bde8041 100755 --- a/scripts/transferbot.py +++ b/scripts/transferbot.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -r""" -This script transfers pages from a source wiki to a target wiki. +r"""This script transfers pages from a source wiki to a target wiki. It also copies edit history to a subpage. @@ -61,8 +60,7 @@ def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/transwikiimport.py b/scripts/transwikiimport.py index dc493cc1ab..7a8a18e343 100755 --- a/scripts/transwikiimport.py +++ b/scripts/transwikiimport.py @@ -161,8 +161,7 @@ def api_query(site, params: dict[str, str]): def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/unusedfiles.py b/scripts/unusedfiles.py index 82feda7f30..0618b5193c 100755 --- a/scripts/unusedfiles.py +++ b/scripts/unusedfiles.py @@ -162,8 +162,7 @@ def append_text(self, page, apptext): def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/upload.py b/scripts/upload.py index 07d9aec5fe..9432928060 100755 --- a/scripts/upload.py +++ b/scripts/upload.py @@ -112,8 +112,7 @@ def get_chunk_size(match) -> int: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/watchlist.py b/scripts/watchlist.py index f6c1df66d3..d5a2e1c901 100755 --- a/scripts/watchlist.py +++ b/scripts/watchlist.py @@ -122,8 +122,7 @@ def refresh_new() -> None: def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/scripts/weblinkchecker.py b/scripts/weblinkchecker.py index bd58cafb7b..c5c4842105 100755 --- a/scripts/weblinkchecker.py +++ b/scripts/weblinkchecker.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -""" -This bot is used for checking external links found at the wiki. +"""This bot is used for checking external links found at the wiki. It checks several pages at once, with a limit set by the config variable max_external_links, which defaults to 50. @@ -190,8 +189,7 @@ def weblinks_from_text( without_bracketed: bool = False, only_bracketed: bool = False ): - """ - Yield web links from text. + """Yield web links from text. Only used as text predicate for XmlDumpPageGenerator to speed up generator. @@ -323,8 +321,7 @@ def run(self): class History: - """ - Store previously found dead links. + """Store previously found dead links. The URLs are dictionary keys, and values are lists of tuples where each tuple represents one time the URL was @@ -419,8 +416,7 @@ def set_dead_link(self, url, error, page, weblink_dead_days) -> None: self.history_dict[url] = [(page.title(), now, error)] def set_link_alive(self, url) -> bool: - """ - Record that the link is now alive. + """Record that the link is now alive. If link was previously found dead, remove it from the .dat file. @@ -441,8 +437,7 @@ def save(self) -> None: class DeadLinkReportThread(threading.Thread): - """ - A Thread that is responsible for posting error reports on talk pages. + """A Thread that is responsible for posting error reports on talk pages. There is only one DeadLinkReportThread, and it is using a semaphore to make sure that two LinkCheckerThreads cannot access the queue at the same time. @@ -536,8 +531,7 @@ def run(self) -> None: class WeblinkCheckerRobot(SingleSiteBot, ExistingPageBot): - """ - Bot which will search for dead weblinks. + """Bot which will search for dead weblinks. It uses several LinkCheckThreads at once to process pages from generator. """ @@ -640,8 +634,7 @@ def RepeatPageGenerator(): # noqa: N802 def main(*args: str) -> None: - """ - Process command line arguments and invoke bot. + """Process command line arguments and invoke bot. If args is an empty list, sys.argv is used. diff --git a/tests/api_tests.py b/tests/api_tests.py index 14e56c9962..8c21f4d594 100755 --- a/tests/api_tests.py +++ b/tests/api_tests.py @@ -779,8 +779,7 @@ def test_internals(self): class TestLazyLoginBase(TestCase): - """ - Test that it tries to login when read API access is denied. + """Test that it tries to login when read API access is denied. Because there is no such family configured it creates an AutoFamily and BaseSite on it's own. It's testing against steward.wikimedia.org. diff --git a/tests/archivebot_tests.py b/tests/archivebot_tests.py index 479e57b16b..49f2c780fd 100755 --- a/tests/archivebot_tests.py +++ b/tests/archivebot_tests.py @@ -163,8 +163,7 @@ def test_archivebot(self, code=None): class TestArchiveBotAfterDateUpdate(TestCase): - """ - Test archivebot script on failures on Wikipedia sites. + """Test archivebot script on failures on Wikipedia sites. If failure is due to updated date format on wiki, test pages with new format only. diff --git a/tests/aspects.py b/tests/aspects.py index 7a6da4e6d7..6fefe0cb39 100644 --- a/tests/aspects.py +++ b/tests/aspects.py @@ -1,5 +1,4 @@ -""" -Test aspects to allow fine grained control over what tests are executed. +"""Test aspects to allow fine grained control over what tests are executed. Several parts of the test infrastructure are implemented as mixins, such as API result caching and excessive test durations. @@ -241,8 +240,7 @@ def assertPageTitlesCountEqual( def assertAPIError(self, code, info=None, callable_obj=None, *args, regex=None, **kwargs): - """ - Assert that a specific APIError wrapped around :py:obj:`assertRaises`. + """Assert that a specific APIError wrapped around :exc:`assertRaises`. If no callable object is defined and it returns a context manager, that context manager will return the underlying context manager used by @@ -438,8 +436,7 @@ class CheckHostnameMixin(TestCaseBase): @classmethod def setUpClass(cls): - """ - Set up the test class. + """Set up the test class. Prevent tests running if the host is down. """ @@ -579,8 +576,7 @@ def setUpClass(cls): f'{cls.__name__}: Not able to login to {site}') def setUp(self): - """ - Set up the test case. + """Set up the test case. Login to the site if it is not logged in. """ @@ -892,8 +888,7 @@ class TestCase(TestCaseBase, metaclass=MetaTestCaseClass): @classmethod def setUpClass(cls): - """ - Set up the test class. + """Set up the test class. Prefetch the Site object for each of the sites the test class has declared are needed. @@ -1058,8 +1053,7 @@ def add_patch(decorated): return add_patch def patch(self, obj, attr_name, replacement): - """ - Patch the obj's attribute with the replacement. + """Patch the obj's attribute with the replacement. It will be reset after each ``tearDown``. """ @@ -1105,8 +1099,7 @@ class DefaultSiteTestCase(TestCase): @classmethod def override_default_site(cls, site): - """ - Override the default site. + """Override the default site. :param site: site tests should use :type site: BaseSite @@ -1200,8 +1193,7 @@ class WikibaseTestCase(TestCase): @classmethod def setUpClass(cls): - """ - Set up the test class. + """Set up the test class. Checks that all sites are configured with a Wikibase repository, with Site.has_data_repository() returning True, and all sites @@ -1249,8 +1241,7 @@ class WikibaseClientTestCase(WikibaseTestCase): @classmethod def setUpClass(cls): - """ - Set up the test class. + """Set up the test class. Checks that all sites are configured as a Wikibase client, with Site.has_data_repository returning True. @@ -1285,8 +1276,7 @@ class DefaultWikidataClientTestCase(DefaultWikibaseClientTestCase): @classmethod def setUpClass(cls): - """ - Set up the test class. + """Set up the test class. Require the data repository is wikidata.org. """ @@ -1446,8 +1436,7 @@ def _build_message(cls, return msg def assertDeprecationParts(self, deprecated=None, instead=None): - """ - Assert that a deprecation warning happened. + """Assert that a deprecation warning happened. To simplify deprecation tests it just requires the to separated parts and forwards the result to :py:obj:`assertDeprecation`. @@ -1465,8 +1454,7 @@ def assertDeprecationParts(self, deprecated=None, instead=None): self.assertDeprecation(self._build_message(deprecated, instead)) def assertDeprecation(self, msg=None): - """ - Assert that a deprecation warning happened. + """Assert that a deprecation warning happened. :param msg: Either the specific message or None to allow any generic message. When set to ``INSTEAD`` it only counts those supplying an @@ -1497,8 +1485,7 @@ def assertDeprecation(self, msg=None): def assertOneDeprecationParts(self, deprecated=None, instead=None, count=1): - """ - Assert that exactly one deprecation message happened and reset. + """Assert that exactly one deprecation message happened and reset. It uses the same arguments as :py:obj:`assertDeprecationParts`. """ @@ -1564,8 +1551,7 @@ def tearDown(self): class HttpbinTestCase(TestCase): - """ - Custom test case class, which allows dry httpbin tests with pytest-httpbin. + """Custom test case class, which allows dry httpbin tests. Test cases, which use httpbin, need to inherit this class. """ diff --git a/tests/basepage.py b/tests/basepage.py index 50880971c9..21ced3026e 100644 --- a/tests/basepage.py +++ b/tests/basepage.py @@ -25,8 +25,7 @@ def setUp(self): class BasePageLoadRevisionsCachingTestBase(BasePageTestBase): - """ - Test site.loadrevisions() caching. + """Test site.loadrevisions() caching. This test class monkey patches site.loadrevisions, which will cause the pickling tests in site_tests and page_tests to fail, if it diff --git a/tests/bot_tests.py b/tests/bot_tests.py index 0f5cb99a53..7641abb72c 100755 --- a/tests/bot_tests.py +++ b/tests/bot_tests.py @@ -40,8 +40,7 @@ class TestBotTreatExit: """Mixin to provide handling for treat and exit.""" def _treat(self, pages, post_treat=None): - """ - Get tests which are executed on each treat. + """Get tests which are executed on each treat. It uses pages as an iterator and compares the page given to the page returned by pages iterator. It checks that the bot's _site and site @@ -71,8 +70,7 @@ def treat(page): return treat def _treat_page(self, pages=True, post_treat=None): - """ - Adjust to CurrentPageBot signature. + """Adjust to CurrentPageBot signature. It uses almost the same logic as _treat but returns a wrapper function which itself calls the function returned by _treat. diff --git a/tests/category_bot_tests.py b/tests/category_bot_tests.py index 0ce04ce8f9..efc28ce373 100755 --- a/tests/category_bot_tests.py +++ b/tests/category_bot_tests.py @@ -68,6 +68,7 @@ def _runtest_strip_cfd_templates(self, template_start, template_end): class TestPreprocessingCategory(TestCase): + """Test determining template or type categorization target.""" family = 'wikipedia' diff --git a/tests/cosmetic_changes_tests.py b/tests/cosmetic_changes_tests.py index 251d38b8f3..8714183abf 100755 --- a/tests/cosmetic_changes_tests.py +++ b/tests/cosmetic_changes_tests.py @@ -434,8 +434,7 @@ def test_translate_magic_words(self): @unittest.expectedFailure def test_translateMagicWords_fail(self): - """ - Test translateMagicWords method. + """Test translateMagicWords method. The current implementation doesn't check whether the magic word is inside a template. diff --git a/tests/edit_tests.py b/tests/edit_tests.py index 7fb89e9e6a..9f3e3515c4 100755 --- a/tests/edit_tests.py +++ b/tests/edit_tests.py @@ -72,6 +72,7 @@ def test_appendtext(self): class TestSiteMergeHistory(TestCase): + """Test history merge action.""" family = 'wikipedia' diff --git a/tests/fixing_redirects_tests.py b/tests/fixing_redirects_tests.py index a60b121fc9..d1597a23a0 100755 --- a/tests/fixing_redirects_tests.py +++ b/tests/fixing_redirects_tests.py @@ -13,6 +13,7 @@ class TestFixingRedirects(TestCase): + """Test fixing redirects.""" family = 'wikipedia' diff --git a/tests/flow_tests.py b/tests/flow_tests.py index aa718b07cd..62e19f5734 100755 --- a/tests/flow_tests.py +++ b/tests/flow_tests.py @@ -239,6 +239,7 @@ def test_invalid_data(self): class TestFlowTopic(TestCase): + """Test Topic functions.""" family = 'wikipedia' diff --git a/tests/http_tests.py b/tests/http_tests.py index 5139c11e90..f94250fb85 100755 --- a/tests/http_tests.py +++ b/tests/http_tests.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Tests for http module.""" # -# (C) Pywikibot team, 2014-2023 +# (C) Pywikibot team, 2014-2024 # # Distributed under the terms of the MIT license. # @@ -520,8 +520,7 @@ def test_no_params(self): self.assertEqual(r.json()['args'], {}) def test_unencoded_params(self): - """ - Test fetch method with unencoded parameters to be encoded internally. + """Test fetch method with unencoded parameters to be encoded inside. HTTPBin returns the args in their urldecoded form, so what we put in should be the same as what we get out. @@ -537,8 +536,7 @@ def test_unencoded_params(self): self.assertEqual(r.json()['args'], {'fish&chips': 'delicious'}) def test_encoded_params(self): - """ - Test fetch method with encoded parameters to be re-encoded internally. + """Test fetch method with encoded parameters to be re-encoded inside. HTTPBin returns the args in their urldecoded form, so what we put in should be the same as what we get out. @@ -555,6 +553,7 @@ def test_encoded_params(self): class DataBodyParameterTestCase(HttpbinTestCase): + """Test data and body params of fetch/request methods are equivalent.""" maxDiff = None diff --git a/tests/link_tests.py b/tests/link_tests.py index 65a26ee17d..8134cd65f8 100755 --- a/tests/link_tests.py +++ b/tests/link_tests.py @@ -59,8 +59,7 @@ def test(self): class TestLink(DefaultDrySiteTestCase): - """ - Test parsing links with DrySite. + """Test parsing links with DrySite. The DrySite is using the builtin namespaces which behaviour is controlled in this repository so namespace aware tests do work, even when the actual diff --git a/tests/logentries_tests.py b/tests/logentries_tests.py index 2e9247423d..2aa90ce959 100755 --- a/tests/logentries_tests.py +++ b/tests/logentries_tests.py @@ -26,8 +26,7 @@ class TestLogentriesBase(TestCase): - """ - Base class for log entry tests. + """Base class for log entry tests. It uses the German Wikipedia for a current representation of the log entries and the test Wikipedia for the future representation. diff --git a/tests/login_tests.py b/tests/login_tests.py index f27d0e3a52..0e95d182e9 100755 --- a/tests/login_tests.py +++ b/tests/login_tests.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -""" -Tests for LoginManager classes. +"""Tests for LoginManager classes. e.g. used to test password-file based login. """ @@ -21,12 +20,14 @@ class FakeFamily: + """Mock.""" name = '~FakeFamily' class FakeSite: + """Mock.""" code = '~FakeCode' @@ -37,6 +38,7 @@ class FakeSite: class FakeConfig: + """Mock.""" usernames = defaultdict(dict) @@ -46,6 +48,7 @@ class FakeConfig: @mock.patch('pywikibot.Site', FakeSite) @mock.patch('pywikibot.login.config', FakeConfig) class TestOfflineLoginManager(DefaultDrySiteTestCase): + """Test offline operation of login.LoginManager.""" dry = True @@ -82,6 +85,7 @@ def test_star_family(self): @mock.patch('pywikibot.Site', FakeSite) class TestPasswordFile(DefaultDrySiteTestCase): + """Test parsing password files.""" def patch(self, name): diff --git a/tests/noreferences_tests.py b/tests/noreferences_tests.py index d64d2d6125..80ed3b6cb8 100755 --- a/tests/noreferences_tests.py +++ b/tests/noreferences_tests.py @@ -13,6 +13,7 @@ class TestAddingReferences(TestCase): + """Test adding references to section.""" family = 'wikipedia' diff --git a/tests/page_tests.py b/tests/page_tests.py index 716bb8c13e..6b9c294371 100755 --- a/tests/page_tests.py +++ b/tests/page_tests.py @@ -597,8 +597,7 @@ def test_depth(self): self.assertEqual(page_d3.depth, 3) def test_page_image(self): - """ - Test ``Page.page_image`` function. + """Test ``Page.page_image`` function. Since we are not sure what the wiki will return, we mainly test types """ @@ -943,8 +942,7 @@ def test_revisions_time_interval_true(self): class TestPageRedirects(TestCase): - """ - Test redirects. + """Test redirects. This is using the pages 'User:Legoktm/R1', 'User:Legoktm/R2' and 'User:Legoktm/R3' on the English Wikipedia. 'R1' is redirecting to 'R2', @@ -1237,6 +1235,7 @@ def test_invalid_entities(self): class TestPermalink(TestCase): + """Test that permalink links are correct.""" family = 'wikipedia' @@ -1261,6 +1260,7 @@ def test_permalink(self): class TestShortLink(TestCase): + """Test that short link management is correct.""" login = True diff --git a/tests/pagegenerators_tests.py b/tests/pagegenerators_tests.py index 4e4205383c..e3ba1ed4db 100755 --- a/tests/pagegenerators_tests.py +++ b/tests/pagegenerators_tests.py @@ -815,8 +815,7 @@ class TestItemClaimFilterPageGenerator(WikidataTestCase): """Test item claim filter page generator generator.""" def _simple_claim_test(self, prop, claim, qualifiers, valid, negate=False): - """ - Test given claim on sample (India) page. + """Test given claim on sample (India) page. :param prop: the property to check :param claim: the claim the property should contain @@ -860,8 +859,7 @@ def test_invalid_qualifiers(self): False) def test_nonexisting_qualifiers(self): - """ - Test ItemClaimFilterPageGenerator on sample page. + """Test ItemClaimFilterPageGenerator on sample page. The item does not have the searched qualifiers. """ diff --git a/tests/proofreadpage_tests.py b/tests/proofreadpage_tests.py index c6811d81be..063b289ff9 100755 --- a/tests/proofreadpage_tests.py +++ b/tests/proofreadpage_tests.py @@ -29,6 +29,7 @@ class TestPagesTagParser(TestCase): + """Test TagAttr class.""" net = False diff --git a/tests/pwb_tests.py b/tests/pwb_tests.py index 130ee4a927..5bfbd952e9 100755 --- a/tests/pwb_tests.py +++ b/tests/pwb_tests.py @@ -48,8 +48,7 @@ def _do_check(self, name): return (direct, vpwb) def test_env(self): - """ - Test external environment of pywikibot. + """Test external environment of pywikibot. Make sure the environment is not contaminated, and is the same as the environment we get when directly running a script. @@ -57,8 +56,7 @@ def test_env(self): self._do_check('print_env') def test_locals(self): - """ - Test internal environment of pywikibot. + """Test internal environment of pywikibot. Make sure the environment is not contaminated, and is the same as the environment we get when directly running a script. diff --git a/tests/reflinks_tests.py b/tests/reflinks_tests.py index ed87e5ed05..4c5553369b 100755 --- a/tests/reflinks_tests.py +++ b/tests/reflinks_tests.py @@ -83,8 +83,7 @@ def test_start_variants(self): class TestReferencesBotConstructor(ScriptMainTestCase): - """ - Test reflinks with run() removed. + """Test reflinks with run() removed. These tests can't verify the order of the pages in the XML as the constructor is given a preloading generator. diff --git a/tests/site_detect_tests.py b/tests/site_detect_tests.py index ea9852605b..715fb24515 100755 --- a/tests/site_detect_tests.py +++ b/tests/site_detect_tests.py @@ -29,8 +29,7 @@ class SiteDetectionTestCase(TestCase): net = True def assertSite(self, url: str): - """ - Assert a MediaWiki site can be loaded from the url. + """Assert a MediaWiki site can be loaded from the url. :param url: Url of tested site :raises AssertionError: Site under url is not MediaWiki powered diff --git a/tests/site_generators_tests.py b/tests/site_generators_tests.py index 96550295b2..3f17ffc999 100755 --- a/tests/site_generators_tests.py +++ b/tests/site_generators_tests.py @@ -38,6 +38,7 @@ class TestSiteGenerators(DefaultSiteTestCase): + """Test cases for Site methods.""" cached = True @@ -709,6 +710,7 @@ def test_unconnected(self): class TestSiteGeneratorsUsers(DefaultSiteTestCase): + """Test cases for Site methods with users.""" cached = True diff --git a/tests/site_login_logout_tests.py b/tests/site_login_logout_tests.py index c4805c8fd1..beb1770402 100755 --- a/tests/site_login_logout_tests.py +++ b/tests/site_login_logout_tests.py @@ -58,6 +58,7 @@ def test_login_logout(self): class TestClearCookies(TestCase): + """Test cookies are cleared after logout.""" login = True diff --git a/tests/site_tests.py b/tests/site_tests.py index 83f0c7de2e..9e56f9caa1 100755 --- a/tests/site_tests.py +++ b/tests/site_tests.py @@ -294,6 +294,7 @@ def test_ratelimit(self): class TestLockingPage(DefaultSiteTestCase): + """Test cases for lock/unlock a page within threads.""" cached = True diff --git a/tests/sparql_tests.py b/tests/sparql_tests.py index 01cc2434f3..5ea73464b6 100755 --- a/tests/sparql_tests.py +++ b/tests/sparql_tests.py @@ -88,6 +88,7 @@ class Container: + """Simple test container for return values.""" def __init__(self, value): @@ -100,6 +101,7 @@ def json(self): class TestSparql(WikidataTestCase): + """Test SPARQL queries.""" @patch.object(sparql.http, 'fetch') @@ -184,6 +186,7 @@ def testQueryAsk(self, mock_method): class TestCommonsQueryService(TestCase): + """Test Commons Query Service auth.""" family = 'commons' @@ -211,9 +214,11 @@ def testLoginAndOauthPermisson(self): class Shared: + """Shared test placeholder.""" class SparqlNodeTests(TestCase): + """Tests encoding issues.""" net = False @@ -233,6 +238,7 @@ def test__str__returnsStringType(self): class LiteralTests(Shared.SparqlNodeTests): + """Tests for sparql.Literal.""" net = False @@ -241,6 +247,7 @@ class LiteralTests(Shared.SparqlNodeTests): class BnodeTests(Shared.SparqlNodeTests): + """Tests for sparql.Bnode.""" net = False @@ -248,6 +255,7 @@ class BnodeTests(Shared.SparqlNodeTests): class URITests(Shared.SparqlNodeTests): + """Tests for sparql.URI.""" net = False diff --git a/tests/superset_tests.py b/tests/superset_tests.py index 744b430280..6712c856ca 100755 --- a/tests/superset_tests.py +++ b/tests/superset_tests.py @@ -21,6 +21,7 @@ class TestSupersetWithoutAuth(TestCase): + """Test Superset without auth.""" family = 'meta' @@ -46,6 +47,7 @@ def test_init(self): class TestSupersetWithAuth(TestCase): + """Test Superset with auth.""" login = True diff --git a/tests/textlib_tests.py b/tests/textlib_tests.py index 5acd7568a8..bdd979a1be 100755 --- a/tests/textlib_tests.py +++ b/tests/textlib_tests.py @@ -189,8 +189,7 @@ def test_add_text(self): class TestCategoryRearrangement(DefaultDrySiteTestCase): - """ - Ensure that sorting keys are not being lost. + """Ensure that sorting keys are not being lost. Tests .getCategoryLinks() and .replaceCategoryLinks(), with both a newline and an empty string as separators. @@ -966,6 +965,7 @@ def test_replace_interwiki_links(self): class TestReplaceLinksNonDry(TestCase): + """Test the replace_links function in textlib non-dry.""" family = 'wikipedia' diff --git a/tests/tools_deprecate_tests.py b/tests/tools_deprecate_tests.py index 335f3b87ea..3d25942ac0 100755 --- a/tests/tools_deprecate_tests.py +++ b/tests/tools_deprecate_tests.py @@ -122,8 +122,7 @@ def deprecated_func_docstring_arg(foo=None): @deprecated def deprecated_func_docstring_arg2(foo=None): - """ - DEPRECATED. + """DEPRECATED. :param foo: Foo. DEPRECATED. """ @@ -327,7 +326,7 @@ def test_deprecated_function_multiline_docstring(self): Python 3.13 strips the doc string, see https://docs.python.org/3.13/whatsnew/3.13.html#other-language-changes """ - doc = '\n DEPRECATED.\n\n :param foo: Foo. DEPRECATED.\n ' + doc = 'DEPRECATED.\n\n :param foo: Foo. DEPRECATED.\n ' if PYTHON_VERSION < (3, 13): self.assertEqual(deprecated_func_docstring_arg2.__doc__, doc) else: diff --git a/tests/ui_tests.py b/tests/ui_tests.py index d688b0c1b3..dab9f0236c 100755 --- a/tests/ui_tests.py +++ b/tests/ui_tests.py @@ -495,8 +495,7 @@ class FakeUnixTest(FakeUIColorizedTestBase, FakeUITest): class FakeWin32Test(FakeUIColorizedTestBase, FakeUITest): - """ - Test case to allow doing colorized Win32 tests in any environment. + """Test case to allow doing colorized Win32 tests in any environment. This only patches the ctypes import in the terminal_interface_win32 module. As the Win32CtypesUI is using the std-streams from another diff --git a/tests/upload_tests.py b/tests/upload_tests.py index 66cd0e8f4e..8e90d05649 100755 --- a/tests/upload_tests.py +++ b/tests/upload_tests.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -""" -Site upload test. +"""Site upload test. These tests write to the wiki. """ diff --git a/tests/uploadbot_tests.py b/tests/uploadbot_tests.py index 7bffec2a18..9c286ba964 100755 --- a/tests/uploadbot_tests.py +++ b/tests/uploadbot_tests.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -""" -UploadRobot test. +"""UploadRobot test. These tests write to the wiki. """ diff --git a/tests/utils.py b/tests/utils.py index 25b4547914..3e5e73fbed 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -31,8 +31,7 @@ def expected_failure_if(expect): - """ - Unit test decorator to expect failure under conditions. + """Unit test decorator to expect failure under conditions. :param expect: Flag to check if failure is expected :type expect: bool @@ -75,8 +74,7 @@ def entered_loop(iterable): class WarningSourceSkipContextManager(warnings.catch_warnings): - """ - Warning context manager that adjusts source of warning. + """Warning context manager that adjusts source of warning. The source of the warning will be moved further down the stack to skip a list of objects that have been monkey @@ -84,8 +82,7 @@ class WarningSourceSkipContextManager(warnings.catch_warnings): """ def __init__(self, skip_list): - """ - Initializer. + """Initializer. :param skip_list: List of objects to be skipped. The source of any warning that matches the skip_list won't be adjusted. @@ -96,8 +93,7 @@ def __init__(self, skip_list): @property def skip_list(self): - """ - Return list of filename and line ranges to skip. + """Return list of filename and line ranges to skip. :rtype: list of (obj, str, int, int) """ @@ -105,8 +101,7 @@ def skip_list(self): @skip_list.setter def skip_list(self, value): - """ - Set list of objects to be skipped. + """Set list of objects to be skipped. :param value: List of objects to be skipped :type value: list of object or (obj, str, int, int) @@ -173,8 +168,7 @@ def detailed_show_warning(*args, **kwargs): class AssertAPIErrorContextManager: - """ - Context manager to assert certain APIError exceptions. + """Context manager to assert certain APIError exceptions. This is build similar to the :py:obj:`unittest.TestCase.assertError` implementation which creates a context manager. It then calls diff --git a/tests/wikibase_edit_tests.py b/tests/wikibase_edit_tests.py index d9069d6b15..338db402d5 100755 --- a/tests/wikibase_edit_tests.py +++ b/tests/wikibase_edit_tests.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -""" -Tests for editing Wikibase items. +"""Tests for editing Wikibase items. Tests which should fail should instead be in the TestWikibaseSaveTest class in edit_failiure_tests.py @@ -242,8 +241,7 @@ class TestWikibaseMakeClaim(WikibaseTestCase): @staticmethod def _clean_item(repo, prop: str): - """ - Return an item without any existing claims of the given property. + """Return an item without any existing claims of the given property. :param repo: repository to fetch item from :type repo: pywikibot.site.DataSite @@ -617,8 +615,7 @@ class TestWikibaseAddClaimToExisting(WikibaseTestCase): @staticmethod def _clean_item_temp(repo, prop: str): - """ - Return an item without any existing claims of the given property. + """Return an item without any existing claims of the given property. :param repo: repository to fetch item from :type repo: pywikibot.site.DataSite diff --git a/tests/wikibase_tests.py b/tests/wikibase_tests.py index 0a1f9c29b1..413ab3b363 100755 --- a/tests/wikibase_tests.py +++ b/tests/wikibase_tests.py @@ -175,8 +175,7 @@ def test_Coordinate_entity_uri_globe(self): class TestWikibaseCoordinateNonDry(WbRepresentationTestCase): - """ - Test Wikibase Coordinate data type (non-dry). + """Test Wikibase Coordinate data type (non-dry). These can be moved to TestWikibaseCoordinate once DrySite has been bumped to the appropriate version. @@ -779,8 +778,7 @@ def test_WbQuantity_unit_fromWikibase(self): class TestWbQuantityNonDry(WbRepresentationTestCase): - """ - Test Wikibase WbQuantity data type (non-dry). + """Test Wikibase WbQuantity data type (non-dry). These can be moved to TestWbQuantity once DrySite has been bumped to the appropriate version. @@ -944,6 +942,7 @@ def test_WbMonolingualText_errors(self): class TestWikibaseParser(WikidataTestCase): + """Test passing various datatypes to wikibase parser.""" def test_wbparse_strings(self): @@ -992,8 +991,7 @@ def test_wbparse_raises_valueerror(self): class TestWbGeoShapeNonDry(WbRepresentationTestCase): - """ - Test Wikibase WbGeoShape data type (non-dry). + """Test Wikibase WbGeoShape data type (non-dry). These require non dry tests due to the page.exists() call. """ @@ -1068,8 +1066,7 @@ def test_WbGeoShape_error_on_wrong_page_type(self): class TestWbTabularDataNonDry(WbRepresentationTestCase): - """ - Test Wikibase WbTabularData data type (non-dry). + """Test Wikibase WbTabularData data type (non-dry). These require non dry tests due to the page.exists() call. """ @@ -1220,8 +1217,7 @@ class MyItemPage(ItemPage): class TestItemLoad(WikidataTestCase): - """ - Test item creation. + """Test item creation. Tests for item creation include: 1. by Q id @@ -1307,8 +1303,7 @@ def test_load_item_set_id(self): self.assertEqual(item.title(), 'Q60') def test_reuse_item_set_id(self): - """ - Test modifying item.id attribute. + """Test modifying item.id attribute. Some scripts are using item.id = 'Q60' semantics, which does work but modifying item.id does not currently work, and this test @@ -1334,8 +1329,7 @@ def test_reuse_item_set_id(self): # self.assertTrue(item.labels['en'].lower().endswith('main page')) def test_empty_item(self): - """ - Test empty wikibase item. + """Test empty wikibase item. should not raise an error as the constructor only requires the site parameter, with the title parameter defaulted to None. @@ -1365,8 +1359,7 @@ def test_item_invalid_titles(self): ItemPage(wikidata, '') def test_item_untrimmed_title(self): - """ - Test intrimmed titles of wikibase items. + """Test intrimmed titles of wikibase items. Spaces in the title should not cause an error. """ @@ -1540,8 +1533,7 @@ def _test_fromPage_noitem(self, link): ItemPage.fromPage(page) def test_fromPage_redirect(self): - """ - Test item from redirect page. + """Test item from redirect page. A redirect should not have a wikidata item. """ @@ -1549,8 +1541,7 @@ def test_fromPage_redirect(self): self._test_fromPage_noitem(link) def test_fromPage_missing(self): - """ - Test item from deleted page. + """Test item from deleted page. A deleted page should not have a wikidata item. """ @@ -1558,8 +1549,7 @@ def test_fromPage_missing(self): self._test_fromPage_noitem(link) def test_fromPage_noitem(self): - """ - Test item from new page. + """Test item from new page. A new created page should not have a wikidata item yet. """ @@ -1754,8 +1744,7 @@ class TestClaim(WikidataTestCase): """Test Claim object functionality.""" def test_claim_eq_simple(self): - """ - Test comparing two claims. + """Test comparing two claims. If they have the same property and value, they are equal. """ @@ -1768,8 +1757,7 @@ def test_claim_eq_simple(self): self.assertEqual(claim2, claim1) def test_claim_eq_simple_different_value(self): - """ - Test comparing two claims. + """Test comparing two claims. If they have the same property and different values, they are not equal. @@ -1783,8 +1771,7 @@ def test_claim_eq_simple_different_value(self): self.assertNotEqual(claim2, claim1) def test_claim_eq_simple_different_rank(self): - """ - Test comparing two claims. + """Test comparing two claims. If they have the same property and value and different ranks, they are equal. @@ -1799,8 +1786,7 @@ def test_claim_eq_simple_different_rank(self): self.assertEqual(claim2, claim1) def test_claim_eq_simple_different_snaktype(self): - """ - Test comparing two claims. + """Test comparing two claims. If they have the same property and different snaktypes, they are not equal. @@ -1814,8 +1800,7 @@ def test_claim_eq_simple_different_snaktype(self): self.assertNotEqual(claim2, claim1) def test_claim_eq_simple_different_property(self): - """ - Test comparing two claims. + """Test comparing two claims. If they have the same value and different properties, they are not equal. @@ -1829,8 +1814,7 @@ def test_claim_eq_simple_different_property(self): self.assertNotEqual(claim2, claim1) def test_claim_eq_with_qualifiers(self): - """ - Test comparing two claims. + """Test comparing two claims. If they have the same property, value and qualifiers, they are equal. """ @@ -1849,8 +1833,7 @@ def test_claim_eq_with_qualifiers(self): self.assertEqual(claim2, claim1) def test_claim_eq_with_different_qualifiers(self): - """ - Test comparing two claims. + """Test comparing two claims. If they have the same property and value and different qualifiers, they are not equal. @@ -1870,8 +1853,7 @@ def test_claim_eq_with_different_qualifiers(self): self.assertNotEqual(claim2, claim1) def test_claim_eq_one_without_qualifiers(self): - """ - Test comparing two claims. + """Test comparing two claims. If they have the same property and value and one of them has no qualifiers while the other one does, they are not equal. @@ -1888,8 +1870,7 @@ def test_claim_eq_one_without_qualifiers(self): self.assertNotEqual(claim2, claim1) def test_claim_eq_with_different_sources(self): - """ - Test comparing two claims. + """Test comparing two claims. If they have the same property and value and different sources, they are equal. @@ -1909,8 +1890,7 @@ def test_claim_eq_with_different_sources(self): self.assertEqual(claim2, claim1) def test_claim_copy_is_equal(self): - """ - Test making a copy of a claim. + """Test making a copy of a claim. The copy of a claim should be always equal to the claim. """ @@ -1927,8 +1907,7 @@ def test_claim_copy_is_equal(self): self.assertEqual(claim, copy) def test_claim_copy_is_equal_qualifier(self): - """ - Test making a copy of a claim. + """Test making a copy of a claim. The copy of a qualifier should be always equal to the qualifier. """ @@ -1941,8 +1920,7 @@ def test_claim_copy_is_equal_qualifier(self): self.assertTrue(copy.isQualifier) def test_claim_copy_is_equal_source(self): - """ - Test making a copy of a claim. + """Test making a copy of a claim. The copy of a source should be always equal to the source. """ @@ -2204,8 +2182,7 @@ class TestNamespaces(WikidataTestCase): """Test cases to test namespaces of Wikibase entities.""" def test_empty_wikibase_page(self): - """ - Test empty wikibase page. + """Test empty wikibase page. As a base class it should be able to instantiate it with minimal arguments From 6952e476f8153cee234cdbfe8219c9b329962dab Mon Sep 17 00:00:00 2001 From: xqt Date: Fri, 1 Nov 2024 15:16:46 +0100 Subject: [PATCH 10/95] [bugfix] extract linktrail for hr-wiki Replace r'\p{L}' pattern to r'[^\W\d_]' unless we sometime have regex package mandatory with Pywikibot Bug: T378787 Change-Id: I6247df4a4052e3376b159151c77bcafd701660de --- pywikibot/site/_apisite.py | 3 +++ tests/site_tests.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pywikibot/site/_apisite.py b/pywikibot/site/_apisite.py index 2a3ec9cf1d..adbdd6f852 100644 --- a/pywikibot/site/_apisite.py +++ b/pywikibot/site/_apisite.py @@ -872,6 +872,9 @@ def linktrail(self) -> str: if linktrail == '/^()(.*)$/sD': # empty linktrail return '' + # T378787 + linktrail = linktrail.replace(r'\p{L}', r'[^\W\d_]') + match = re.search(r'\((?:\:\?|\?\:)?\[(?P.+?)\]' r'(?P(\|.)*)\)?\+\)', linktrail) if not match: diff --git a/tests/site_tests.py b/tests/site_tests.py index 83f0c7de2e..c01b7c394c 100755 --- a/tests/site_tests.py +++ b/tests/site_tests.py @@ -1015,7 +1015,8 @@ def test_has_linktrail(self): small_wikis = self.site.family.languages_by_size[-size:] great_wikis = self.site.family.languages_by_size[:-size] great_wikis = random.sample(great_wikis, size) - for code in sorted(small_wikis + great_wikis): + # Also test for 'hr' which failed due to T378787 + for code in {'hr', *small_wikis, *great_wikis}: site = pywikibot.Site(code, self.family) with self.subTest(site=site): self.assertIsInstance(site.linktrail(), str) From aacff5c0970e3a82706b8f5f51d2117cf45cc646 Mon Sep 17 00:00:00 2001 From: xqt Date: Sat, 2 Nov 2024 13:15:59 +0100 Subject: [PATCH 11/95] Replace unnecessary escape sequences Change-Id: Ieee852169e3e4cbf5446a01f05bea7c435d99aa4 --- tests/mediawikiversion_tests.py | 2 +- tests/time_tests.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/mediawikiversion_tests.py b/tests/mediawikiversion_tests.py index f5c2f15870..f85e440097 100755 --- a/tests/mediawikiversion_tests.py +++ b/tests/mediawikiversion_tests.py @@ -97,7 +97,7 @@ def test_invalid_versions(self): MediaWikiVersion('1.missing') with self.assertRaisesRegex( AssertionError, - 'Found \"wmf\" in \"wmf-1\"'): + 'Found "wmf" in "wmf-1"'): MediaWikiVersion('1.33wmf-1') def test_generator(self): diff --git a/tests/time_tests.py b/tests/time_tests.py index 3deaf37432..c317c7c0d5 100755 --- a/tests/time_tests.py +++ b/tests/time_tests.py @@ -119,7 +119,7 @@ def test_set_from_string_posix(self): def test_set_from_string_invalid(self): """Test failure creating instance from invalid string.""" for timestr, _posix in self.test_results['INVALID']: - regex = "time data \'[^\']*?\' does not match" + regex = "time data '[^']*?' does not match" with self.subTest(timestr), \ self.assertRaisesRegex(ValueError, regex): Timestamp.set_timestamp(timestr) From a02b970fc8a70377cf9828f1194a321c89bd8bd3 Mon Sep 17 00:00:00 2001 From: xqt Date: Sat, 2 Nov 2024 12:46:22 +0100 Subject: [PATCH 12/95] [RET] remove unnecessary return None and elif after break Change-Id: I3dcda6fa94c8586240d338d572dbf182f9a3753b --- scripts/archivebot.py | 2 +- tests/interwikidata_tests.py | 2 +- tests/tools_deprecate_tests.py | 4 ++-- tests/utils.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/archivebot.py b/scripts/archivebot.py index 70cb5465f3..7d9b8838c1 100755 --- a/scripts/archivebot.py +++ b/scripts/archivebot.py @@ -1019,7 +1019,7 @@ def signal_handler(signum, frame): pywikibot.info(f'{canceled} done') break - elif not process_page(pg, *botargs): + if not process_page(pg, *botargs): break diff --git a/tests/interwikidata_tests.py b/tests/interwikidata_tests.py index 45ae584920..4780a65fdf 100755 --- a/tests/interwikidata_tests.py +++ b/tests/interwikidata_tests.py @@ -31,7 +31,7 @@ def create_item(self): def try_to_add(self): """Prevent adding sitelinks to items.""" - return None + return class TestInterwikidataBot(SiteAttributeTestCase): diff --git a/tests/tools_deprecate_tests.py b/tests/tools_deprecate_tests.py index 3d25942ac0..76183959db 100755 --- a/tests/tools_deprecate_tests.py +++ b/tests/tools_deprecate_tests.py @@ -144,7 +144,7 @@ def deprecated_func_arg3(foo=None): @remove_last_args(['foo', 'bar']) def deprecated_all(): """Test remove_last_args with all args removed.""" - return None + return @remove_last_args(['bar']) @@ -232,7 +232,7 @@ def deprecated_instance_method_and_arg2(self, foo): @remove_last_args(['foo', 'bar']) def deprecated_all(self): """Deprecating positional parameters.""" - return None + return @remove_last_args(['bar']) def deprecated_all2(self, foo): diff --git a/tests/utils.py b/tests/utils.py index 3e5e73fbed..c80e3311b3 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -297,7 +297,7 @@ def is_cached(self, key: str) -> bool: def is_recognised(self, key): """Return None.""" - return None + return def get_requested_time(self, key): """Return False.""" From 92419f5bcc8cce3d63a445154960a6d1c51eab32 Mon Sep 17 00:00:00 2001 From: xqt Date: Sat, 2 Nov 2024 13:23:26 +0100 Subject: [PATCH 13/95] [RSE] fix unnecessary parentheses on raised exception Change-Id: Ib72369a7ee29b13b1cf0597851f44e7fe17ca3ef --- pywikibot/userinterfaces/terminal_interface_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywikibot/userinterfaces/terminal_interface_base.py b/pywikibot/userinterfaces/terminal_interface_base.py index 335eca2afe..800148b3bd 100644 --- a/pywikibot/userinterfaces/terminal_interface_base.py +++ b/pywikibot/userinterfaces/terminal_interface_base.py @@ -407,7 +407,7 @@ def _input_reraise_cntl_c(self, password): else: text = self._raw_input() except KeyboardInterrupt: - raise QuitKeyboardInterrupt() + raise QuitKeyboardInterrupt except UnicodeDecodeError: return None # wrong terminal encoding, T258143 return text From 192c430137d478f6c4ac96227f470e2e60f6a686 Mon Sep 17 00:00:00 2001 From: xqt Date: Sat, 2 Nov 2024 13:42:09 +0100 Subject: [PATCH 14/95] Convert format strings to f-strings Change-Id: Ie72ee5c4d04c3b149b87a36e41a2cd0e32717f33 --- pywikibot/__init__.py | 9 ++- pywikibot/_wbtypes.py | 5 +- pywikibot/bot.py | 34 ++++----- pywikibot/bot_choice.py | 17 ++--- pywikibot/comms/eventstreams.py | 12 ++-- pywikibot/comms/http.py | 7 +- pywikibot/config.py | 22 +++--- pywikibot/cosmetic_changes.py | 22 +++--- pywikibot/data/api/_generators.py | 59 +++++++-------- pywikibot/data/api/_paraminfo.py | 9 +-- pywikibot/data/memento.py | 6 +- pywikibot/date.py | 12 ++-- pywikibot/diff.py | 17 ++--- pywikibot/family.py | 9 ++- pywikibot/flow.py | 3 +- pywikibot/i18n.py | 27 +++---- pywikibot/interwiki_graph.py | 5 +- pywikibot/logentries.py | 8 +-- pywikibot/logging.py | 4 +- pywikibot/login.py | 67 ++++++++--------- pywikibot/page/_basepage.py | 37 ++++------ pywikibot/page/_collections.py | 11 ++- pywikibot/page/_filepage.py | 4 +- pywikibot/page/_links.py | 33 +++++---- pywikibot/page/_revision.py | 7 +- pywikibot/page/_toolforge.py | 5 +- pywikibot/page/_wikibase.py | 24 ++++--- pywikibot/pagegenerators/_filters.py | 9 +-- pywikibot/pagegenerators/_generators.py | 5 +- pywikibot/proofreadpage.py | 29 ++++---- pywikibot/scripts/generate_family_file.py | 10 +-- pywikibot/scripts/generate_user_files.py | 21 +++--- pywikibot/scripts/login.py | 21 +++--- pywikibot/scripts/wrapper.py | 32 ++++----- pywikibot/site/_apisite.py | 21 +++--- pywikibot/site/_basesite.py | 30 ++++---- pywikibot/site/_datasite.py | 12 ++-- pywikibot/site/_extensions.py | 5 +- pywikibot/site/_generators.py | 5 +- pywikibot/site/_interwikimap.py | 6 +- pywikibot/site/_namespace.py | 23 +++--- pywikibot/site/_upload.py | 29 ++++---- pywikibot/site_detect.py | 15 ++-- pywikibot/specialbots/_upload.py | 2 +- pywikibot/textlib.py | 71 +++++++++---------- pywikibot/time.py | 8 +-- pywikibot/titletranslate.py | 5 +- pywikibot/tools/__init__.py | 12 ++-- pywikibot/tools/collections.py | 6 +- pywikibot/tools/djvu.py | 5 +- pywikibot/tools/itertools.py | 6 +- pywikibot/tools/threading.py | 3 +- pywikibot/userinterfaces/buffer_interface.py | 7 +- pywikibot/userinterfaces/gui.py | 7 +- .../userinterfaces/terminal_interface_base.py | 15 ++-- pywikibot/version.py | 8 ++- tests/link_tests.py | 2 +- 57 files changed, 449 insertions(+), 456 deletions(-) diff --git a/pywikibot/__init__.py b/pywikibot/__init__.py index 7cd07b5855..2375309712 100644 --- a/pywikibot/__init__.py +++ b/pywikibot/__init__.py @@ -244,8 +244,8 @@ def Site(code: str | None = None, # noqa: N802 debug(f"Instantiated {interface.__name__} object '{_sites[key]}'") if _sites[key].code != code: - warn('Site {} instantiated using different code "{}"' - .format(_sites[key], code), UserWarning, 2) + warn(f'Site {_sites[key]} instantiated using different code ' + f'"{code}"', UserWarning, 2) return _sites[key] @@ -337,9 +337,8 @@ def remaining() -> tuple[int, datetime.timedelta]: num, sec = remaining() if num > 0 and sec.total_seconds() > _config.noisysleep: - output('<>Waiting for {num} pages to be put. ' - 'Estimated time remaining: {sec}<>' - .format(num=num, sec=sec)) + output(f'<>Waiting for {num} pages to be put. ' + f'Estimated time remaining: {sec}<>') exit_queue = None if _putthread is not threading.current_thread(): diff --git a/pywikibot/_wbtypes.py b/pywikibot/_wbtypes.py index ee06fcc126..19317be18e 100644 --- a/pywikibot/_wbtypes.py +++ b/pywikibot/_wbtypes.py @@ -1032,9 +1032,8 @@ def _validate(page: pywikibot.Page, data_site: BaseSite, ending: str, # check should be enough. if not page.title().startswith('Data:') \ or not page.title().endswith(ending): - raise ValueError( - "Page must be in 'Data:' namespace and end in '{}' " - 'for {}.'.format(ending, label)) + raise ValueError(f"Page must be in 'Data:' namespace and end in " + f"'{ending}' for {label}.") def __init__(self, page: pywikibot.Page, diff --git a/pywikibot/bot.py b/pywikibot/bot.py index c5227e7a1b..15d425e4bf 100644 --- a/pywikibot/bot.py +++ b/pywikibot/bot.py @@ -296,8 +296,8 @@ def set_interface(module_name: str) -> None: """ global ui - ui_module = __import__('pywikibot.userinterfaces.{}_interface' - .format(module_name), fromlist=['UI']) + ui_module = __import__(f'pywikibot.userinterfaces.{module_name}_interface', + fromlist=['UI']) ui = ui_module.UI() assert ui is not None atexit.register(ui.flush) @@ -1327,8 +1327,8 @@ def _save_page(self, page: pywikibot.page.BasePage, pywikibot.info( f'Skipping {page.title()} because of edit conflict') elif isinstance(e, SpamblacklistError): - pywikibot.info('Cannot change {} because of blacklist ' - 'entry {}'.format(page.title(), e.url)) + pywikibot.info(f'Cannot change {page.title()} because of ' + f'blacklist entry {e.url}') elif isinstance(e, LockedPageError): pywikibot.info(f'Skipping {page.title()} (locked page)') else: @@ -1390,8 +1390,8 @@ def exit(self) -> None: for op, count in self.counter.items(): if not count or op == 'read': continue - pywikibot.info('{} operation time: {:.1f} seconds' - .format(op.capitalize(), write_seconds / count)) + pywikibot.info(f'{op.capitalize()} operation time: ' + f'{write_seconds / count:.1f} seconds') # exc_info contains exception from self.run() while terminating exc_info = sys.exc_info() @@ -1454,8 +1454,8 @@ def treat(self, page: Any) -> None: :class:`page.BasePage`. For other page types the :attr:`treat_page_type` must be set. """ - raise NotImplementedError('Method {}.treat() not implemented.' - .format(self.__class__.__name__)) + raise NotImplementedError( + f'Method {type(self).__name__}.treat() not implemented.') def setup(self) -> None: """Some initial setup before :meth:`run` operation starts. @@ -1814,8 +1814,8 @@ class CurrentPageBot(BaseBot): def treat_page(self) -> None: """Process one page (Abstract method).""" - raise NotImplementedError('Method {}.treat_page() not implemented.' - .format(self.__class__.__name__)) + raise NotImplementedError( + f'Method {type(self).__name__}.treat_page() not implemented.') def treat(self, page: pywikibot.page.BasePage) -> None: """Set page to current page and treat that page.""" @@ -1874,8 +1874,9 @@ def summary_parameters(self) -> dict[str, str]: def summary_parameters(self, value: dict[str, str]) -> None: """Set the i18n dictionary.""" if not isinstance(value, dict): - raise TypeError('"value" must be a dict but {} was found.' - .format(type(value).__name__)) + raise TypeError( + f'"value" must be a dict but {type(value).__name__} was found.' + ) self._summary_parameters = value @summary_parameters.deleter @@ -2250,8 +2251,8 @@ def create_item_for_page(self, page: pywikibot.page.BasePage, :return: pywikibot.ItemPage or None """ if not summary: - summary = 'Bot: New item with sitelink from {}'.format( - page.title(as_link=True, insite=self.repo)) + summary = ('Bot: New item with sitelink from ' + f'{page.title(as_link=True, insite=self.repo)}') if data is None: data = {} @@ -2327,9 +2328,8 @@ def treat_page_and_item(self, page: pywikibot.page.BasePage, Must be implemented in subclasses. """ - raise NotImplementedError('Method {}.treat_page_and_item() not ' - 'implemented.' - .format(self.__class__.__name__)) + raise NotImplementedError(f'Method {type(self).__name__}.' + 'treat_page_and_item() not implemented.') set_interface(config.userinterface) diff --git a/pywikibot/bot_choice.py b/pywikibot/bot_choice.py index b7dd803544..eacc8ae188 100644 --- a/pywikibot/bot_choice.py +++ b/pywikibot/bot_choice.py @@ -181,9 +181,8 @@ def format(self, default: str | None = None) -> str: if self.shortcut == default: shortcut = self.shortcut.upper() if index >= 0: - return '{}[{}]{}'.format( - self.option[:index], shortcut, - self.option[index + len(self.shortcut):]) + return (f'{self.option[:index]}[{shortcut}]' + f'{self.option[index + len(self.shortcut):]}') return f'{self.option} [{shortcut}]' def result(self, value: str) -> Any: @@ -592,11 +591,9 @@ def out(self) -> str: """Highlighted output section of the text.""" start = max(0, self.start - self.context) end = min(len(self.text), self.end + self.context) - return '{}<<{color}>>{}<>{}'.format( - self.text[start:self.start], - self.text[self.start:self.end], - self.text[self.end:end], - color=self.color) + return (f'{self.text[start:self.start]}<<{self.color}>>' + f'{self.text[self.start:self.end]}<>' + f'{self.text[self.end:end]}') class UnhandledAnswer(Exception): # noqa: N818 @@ -775,8 +772,8 @@ def handle_link(self) -> Any: if self._new is False: question += 'be unlinked?' else: - question += 'target to <>{}<>?'.format( - self._new.canonical_title()) + question += (f'target to <>' + f'{self._new.canonical_title()}<>?') choice = pywikibot.input_choice(question, choices, default=self._default, diff --git a/pywikibot/comms/eventstreams.py b/pywikibot/comms/eventstreams.py index 5f46511f1f..a8a83cf8e4 100644 --- a/pywikibot/comms/eventstreams.py +++ b/pywikibot/comms/eventstreams.py @@ -186,8 +186,8 @@ def url(self): :raises NotImplementedError: no stream types specified """ if self._streams is None: - raise NotImplementedError('No streams specified for class {}' - .format(self.__class__.__name__)) + raise NotImplementedError( + f'No streams specified for class {type(self).__name__}') return '{host}{path}/{streams}{since}'.format( host=self._site.eventstreams_host(), path=self._site.eventstreams_path(), @@ -205,8 +205,8 @@ def set_maximum_items(self, value: int) -> None: """ if value is not None: self._total = int(value) - debug('{}: Set limit (maximum_items) to {}.' - .format(self.__class__.__name__, self._total)) + debug(f'{type(self).__name__}: Set limit (maximum_items) to ' + f'{self._total}.') def register_filter(self, *args, **kwargs): """Register a filter. @@ -363,8 +363,8 @@ def generator(self): else: warning(f'Unknown event {event.event} occurred.') - debug('{}: Stopped iterating due to exceeding item limit.' - .format(self.__class__.__name__)) + debug(f'{type(self).__name__}: Stopped iterating due to exceeding item' + ' limit.') del self.source diff --git a/pywikibot/comms/http.py b/pywikibot/comms/http.py index ceb6d6288c..71d629b7d1 100644 --- a/pywikibot/comms/http.py +++ b/pywikibot/comms/http.py @@ -400,7 +400,7 @@ def assign_fake_user_agent(use_fake_user_agent, uri): if use_fake_user_agent and isinstance(use_fake_user_agent, str): return use_fake_user_agent # Custom UA. raise ValueError('Invalid parameter: ' - 'use_fake_user_agent={}'.format(use_fake_user_agent)) + f'use_fake_user_agent={use_fake_user_agent}') def assign_user_agent(user_agent_format_string): if not user_agent_format_string or '{' in user_agent_format_string: @@ -554,8 +554,9 @@ def _try_decode(content: bytes, encoding: str | None) -> str | None: if header_codecs and charset_codecs and header_codecs != charset_codecs: pywikibot.warning( - 'Encoding "{}" requested but "{}" received in the ' - 'response header.'.format(charset, header_encoding)) + f'Encoding "{charset}" requested but "{header_encoding}" received' + ' in the response header.' + ) _encoding = _try_decode(response.content, header_encoding) \ or _try_decode(response.content, charset) diff --git a/pywikibot/config.py b/pywikibot/config.py index 1e45f5b989..c2089459f4 100644 --- a/pywikibot/config.py +++ b/pywikibot/config.py @@ -397,9 +397,10 @@ def exists(directory: str) -> bool: if __no_user_config is None: assert get_base_dir.__doc__ is not None exc_text += ( - '\nPlease check that {0} is stored in the correct location.' - '\nDirectory where {0} is searched is determined as follows:' - '\n\n '.format(config_file) + f'\nPlease check that {config_file} is stored in the correct' + ' location.' + f'\nDirectory where {config_file} is searched is determined as' + ' follows:\n\n ' ) + get_base_dir.__doc__ raise RuntimeError(exc_text) @@ -997,9 +998,10 @@ def _assert_types( DEPRECATED_VARIABLE = ( - '"{{}}" present in our {} is no longer a supported configuration variable ' - 'and should be removed. Please inform the maintainers if you depend on it.' - .format(user_config_file)) + f'"{{}}" present in our {user_config_file} is no longer a supported' + ' configuration variable and should be removed. Please inform the' + ' maintainers if you depend on it.' +) def _check_user_config_types( @@ -1025,10 +1027,10 @@ def _check_user_config_types( warn('\n' + fill(DEPRECATED_VARIABLE.format(name)), _ConfigurationDeprecationWarning) elif name not in _future_variables: - warn('\n' + fill('Configuration variable "{}" is defined in ' - 'your {} but unknown. It can be a misspelled ' - 'one or a variable that is no longer ' - 'supported.'.format(name, user_config_file)), + warn('\n' + fill(f'Configuration variable "{name}" is defined ' + f'in your {user_config_file} but unknown. It' + ' can be a misspelled one or a variable that' + ' is no longer supported.'), UserWarning) diff --git a/pywikibot/cosmetic_changes.py b/pywikibot/cosmetic_changes.py index c2337fe5fa..effc1cdac4 100644 --- a/pywikibot/cosmetic_changes.py +++ b/pywikibot/cosmetic_changes.py @@ -318,8 +318,9 @@ def change(self, text: str) -> bool | str: new_text = self._change(text) except Exception as e: if self.ignore == CANCEL.PAGE: - pywikibot.warning('Skipped "{}", because an error occurred.' - .format(self.title)) + pywikibot.warning( + f'Skipped "{self.title}", because an error occurred.' + ) pywikibot.error(e) return False raise @@ -334,8 +335,9 @@ def fixSelfInterwiki(self, text: str) -> str: Remove their language code prefix. """ if not self.talkpage and pywikibot.calledModuleName() != 'interwiki': - interwikiR = re.compile(r'\[\[(?: *:)? *{} *: *([^\[\]\n]*)\]\]' - .format(self.site.code)) + interwikiR = re.compile( + rf'\[\[(?: *:)? *{self.site.code} *: *([^\[\]\n]*)\]\]' + ) text = interwikiR.sub(r'[[\1]]', text) return text @@ -640,8 +642,8 @@ def handleOneLink(match: Match[str]) -> str: # instead of a pipelink elif (firstcase_label.startswith(firstcase_title) and trailR.sub('', label[len(titleWithSection):]) == ''): - newLink = '[[{}]]{}'.format(label[:len(titleWithSection)], - label[len(titleWithSection):]) + newLink = (f'[[{label[:len(titleWithSection)]}]]' + f'{label[len(titleWithSection):]}') else: # Try to capitalize the first letter of the title. @@ -891,9 +893,8 @@ def replace_link(match: Match[str]) -> str: # Match first a non space in the title to prevent that multiple # spaces at the end without title will be matched by it - title_regex = (r'(?P[^{sep}]+?)' - r'(\s+(?P[^\s].*?))' - .format(sep=separator)) + title_regex = (rf'(?P<link>[^{separator}]+?)' + r'(\s+(?P<title>[^\s].*?))') url_regex = fr'\[\[?{url}?\s*\]\]?' text = textlib.replaceExcept( text, @@ -1043,8 +1044,7 @@ def fixArabicLetters(self, text: str) -> str: faChrs = 'ءاآأإئؤبپتثجچحخدذرزژسشصضطظعغفقکگلمنوهیةيك' + digits['fa'] # not to let bot edits in latin content - exceptions.append(re.compile('[^{fa}] *?"*? *?, *?[^{fa}]' - .format(fa=faChrs))) + exceptions.append(re.compile(f'[^{faChrs}] *?"*? *?, *?[^{faChrs}]')) text = textlib.replaceExcept(text, ',', '،', exceptions, site=self.site) if self.site.code == 'ckb': diff --git a/pywikibot/data/api/_generators.py b/pywikibot/data/api/_generators.py index 006f8d024d..1a316e94b2 100644 --- a/pywikibot/data/api/_generators.py +++ b/pywikibot/data/api/_generators.py @@ -131,8 +131,8 @@ def set_query_increment(self, value: int) -> None: """ self.query_increment = int(value) self.request[self.limit_name] = self.query_increment - pywikibot.debug('{}: Set query_increment to {}.' - .format(type(self).__name__, self.query_increment)) + pywikibot.debug(f'{type(self).__name__}: Set query_increment to ' + f'{self.query_increment}.') def set_maximum_items(self, value: int | str | None) -> None: """Set the maximum number of items to be retrieved from the wiki. @@ -147,10 +147,10 @@ def set_maximum_items(self, value: int | str | None) -> None: self.limit = int(value) if self.query_increment and self.limit < self.query_increment: self.request[self.limit_name] = self.limit - pywikibot.debug('{}: Set request item limit to {}' - .format(type(self).__name__, self.limit)) - pywikibot.debug('{}: Set limit (maximum_items) to {}.' - .format(type(self).__name__, self.limit)) + pywikibot.debug(f'{type(self).__name__}: Set request item ' + f'limit to {self.limit}') + pywikibot.debug(f'{type(self).__name__}: Set limit ' + f'(maximum_items) to {self.limit}.') @property def generator(self): @@ -176,14 +176,15 @@ def generator(self): yield item n += 1 if self.limit is not None and n >= self.limit: - pywikibot.debug('{}: Stopped iterating due to ' - 'exceeding item limit.' - .format(type(self).__name__)) + pywikibot.debug( + f'{type(self).__name__}: Stopped iterating due to' + ' exceeding item limit.' + ) return offset += n_items else: - pywikibot.debug('{}: Stopped iterating due to empty list in ' - 'response.'.format(type(self).__name__)) + pywikibot.debug(f'{type(self).__name__}: Stopped iterating' + ' due to empty list in response.') break @@ -238,8 +239,8 @@ def __init__(self, **kwargs) -> None: self.modules = parameters[modtype].split('|') break else: - raise Error('{}: No query module name found in arguments.' - .format(self.__class__.__name__)) + raise Error(f'{type(self).__name__}: No query module name found' + ' in arguments.') parameters['indexpageids'] = True # always ask for list of pageids self.continue_name = 'continue' @@ -266,10 +267,11 @@ def __init__(self, **kwargs) -> None: self.limited_module = module limited_modules.remove(module) break - pywikibot.log('{}: multiple requested query modules support limits' - "; using the first such module '{}' of {!r}" - .format(self.__class__.__name__, self.limited_module, - self.modules)) + pywikibot.log( + f'{type(self).__name__}: multiple requested query modules' + ' support limits; using the first such module ' + f"{self.limited_module}' of {self.modules!r}" + ) # Set limits for all remaining limited modules to max value. # Default values will only cause more requests and make the query @@ -379,8 +381,9 @@ def set_query_increment(self, value) -> None: self.query_limit = limit else: self.query_limit = min(self.api_limit, limit) - pywikibot.debug('{}: Set query_limit to {}.' - .format(type(self).__name__, self.query_limit)) + pywikibot.debug( + f'{type(self).__name__}: Set query_limit to {self.query_limit}.' + ) def set_maximum_items(self, value: int | str | None) -> None: """Set the maximum number of items to be retrieved from the wiki. @@ -410,8 +413,9 @@ def _update_limit(self) -> None: limit = int(param['max']) if self.api_limit is None or limit < self.api_limit: self.api_limit = limit - pywikibot.debug('{}: Set query_limit to {}.' - .format(type(self).__name__, self.api_limit)) + pywikibot.debug( + f'{type(self).__name__}: Set query_limit to {self.api_limit}.' + ) def support_namespace(self) -> bool: """Check if namespace is a supported parameter on this query. @@ -446,8 +450,8 @@ def set_namespace(self, namespaces): param = self.site._paraminfo.parameter('query+' + self.limited_module, 'namespace') if not param: - pywikibot.warning('{} module does not support a namespace ' - 'parameter'.format(self.limited_module)) + pywikibot.warning(f'{self.limited_module} module does not support' + ' a namespace parameter') warn('set_namespace() will be modified to raise TypeError ' 'when namespace parameter is not supported. ' 'It will be a Breaking Change, please update your code ' @@ -468,8 +472,8 @@ def set_namespace(self, namespaces): if 'multi' not in param and len(namespaces) != 1: if self._check_result_namespace is NotImplemented: - raise TypeError('{} module does not support multiple ' - 'namespaces'.format(self.limited_module)) + raise TypeError(f'{self.limited_module} module does not' + ' support multiple namespaces') self._namespaces = set(namespaces) namespaces = None @@ -608,9 +612,8 @@ def generator(self): self.data = self.request.submit() if not self.data or not isinstance(self.data, dict): - pywikibot.debug( - '{}: stopped iteration because no dict retrieved from api.' - .format(type(self).__name__)) + pywikibot.debug(f'{type(self).__name__}: stopped iteration' + ' because no dict retrieved from api.') break if 'query' in self.data and self.resultkey in self.data['query']: diff --git a/pywikibot/data/api/_paraminfo.py b/pywikibot/data/api/_paraminfo.py index 7d95660e9d..b4d7788582 100644 --- a/pywikibot/data/api/_paraminfo.py +++ b/pywikibot/data/api/_paraminfo.py @@ -193,10 +193,11 @@ def module_generator(): if len(missing_modules) == 1 and len(normalized_result) == 1: # Okay it's possible to recover normalized_result = next(iter(normalized_result.values())) - pywikibot.warning('The module "{0[name]}" ("{0[path]}") ' - 'was returned as path even though "{1}" ' - 'was requested'.format(normalized_result, - missing_modules[0])) + pywikibot.warning( + f'The module "{normalized_result["name"]}" ' + f'("{normalized_result["path"]}") was returned as path ' + f'even though "{missing_modules[0]}" was requested' + ) normalized_result['path'] = missing_modules[0] normalized_result['name'] = missing_modules[0].rsplit('+')[0] normalized_result = {missing_modules[0]: normalized_result} diff --git a/pywikibot/data/memento.py b/pywikibot/data/memento.py index 1a26d5b70d..9bd5110dda 100644 --- a/pywikibot/data/memento.py +++ b/pywikibot/data/memento.py @@ -9,7 +9,7 @@ # Parts of MementoClient class codes are # licensed under the BSD open source software license. # -# (C) Pywikibot team, 2015-2023 +# (C) Pywikibot team, 2015-2024 # # Distributed under the terms of the MIT license. # @@ -179,8 +179,8 @@ def get_native_timegate_uri(self, ) except (requests.exceptions.ConnectTimeout, requests.exceptions.ConnectionError): # pragma: no cover - warning('Could not connect to URI {}, returning no native ' - 'URI-G'.format(original_uri)) + warning(f'Could not connect to URI {original_uri}, returning' + 'no native URI-G') return None debug('Request headers sent to search for URI-G: ' diff --git a/pywikibot/date.py b/pywikibot/date.py index 3a97e7579f..17f917c943 100644 --- a/pywikibot/date.py +++ b/pywikibot/date.py @@ -508,8 +508,9 @@ def dh(value: int, pattern: str, encf: encf_type, decf: decf_type, if isinstance(params, (tuple, list)): assert len(params) == len(decoders), ( - 'parameter count ({}) does not match decoder count ({})' - .format(len(params), len(decoders))) + f'parameter count ({len(params)}) does not match decoder count ' + f'({len(decoders)})' + ) # convert integer parameters into their textual representation str_params = tuple(_make_parameter(decoders[i], param) for i, param in enumerate(params)) @@ -1859,9 +1860,10 @@ def makeMonthNamedList(lang: str, pattern: str = '%s', # for all other days formats[dayMnthFmts[i]]['br'] = eval( 'lambda m: multi(m, [' - '(lambda v: dh_dayOfMnth(v, "%dañ {mname}"), lambda p: p == 1), ' - '(lambda v: dh_dayOfMnth(v, "%d {mname}"), alwaysTrue)])' - .format(mname=brMonthNames[i])) + f'(lambda v: dh_dayOfMnth(v, "%dañ {brMonthNames[i]}"),' + ' lambda p: p == 1), ' + f'(lambda v: dh_dayOfMnth(v, "%d {brMonthNames[i]}"), alwaysTrue)])' + ) # # Month of the Year: "en:May 1976" diff --git a/pywikibot/diff.py b/pywikibot/diff.py index c5c1ab3c1c..d1df913040 100644 --- a/pywikibot/diff.py +++ b/pywikibot/diff.py @@ -62,22 +62,16 @@ def __init__(self, a: str | Sequence[str], '+': 'lightgreen', '-': 'lightred', } - self.diff = list(self.create_diff()) self.diff_plain_text = ''.join(self.diff) self.diff_text = ''.join(self.format_diff()) - first, last = self.group[0], self.group[-1] self.a_rng = (first[1], last[2]) self.b_rng = (first[3], last[4]) - self.header = self.get_header() - self.diff_plain_text = '{hunk.header}\n{hunk.diff_plain_text}' \ - .format(hunk=self) + self.diff_plain_text = f'{self.header}\n{self.diff_plain_text}' self.diff_text = str(self.diff_text) - self.reviewed = self.PENDING - self.pre_context = 0 self.post_context = 0 @@ -91,7 +85,7 @@ def get_header_text(a_rng: tuple[int, int], b_rng: tuple[int, int], """Provide header for any ranges.""" a_rng = _format_range_unified(*a_rng) b_rng = _format_range_unified(*b_rng) - return '{0} -{1} +{2} {0}'.format(affix, a_rng, b_rng) + return f'{affix} -{a_rng} +{b_rng} {affix}' def create_diff(self) -> Iterable[str]: """Generator of diff text for this hunk, without formatting. @@ -382,9 +376,10 @@ def extend_context(start: int, end: int) -> str: context_range = self._get_context_range(hunks) - output = '<<aqua>>{}<<default>>\n{}'.format( - Hunk.get_header_text(*context_range), - extend_context(context_range[0][0], hunks[0].a_rng[0])) + output = ( + f'<<aqua>>{Hunk.get_header_text(*context_range)}<<default>>\n' + f'{extend_context(context_range[0][0], hunks[0].a_rng[0])}' + ) previous_hunk = None for hunk in hunks: if previous_hunk: diff --git a/pywikibot/family.py b/pywikibot/family.py index 673f1715a8..b96bf1402c 100644 --- a/pywikibot/family.py +++ b/pywikibot/family.py @@ -377,17 +377,16 @@ def load(fam: str | None = None): # codes can accept also underscore/dash. if not all(x in NAME_CHARACTERS for x in cls.name): warnings.warn( - 'Name of family {} must be ASCII letters and digits ' - '[a-zA-Z0-9]'.format(cls.name), + f'Name of family {cls.name} must be ASCII letters and digits' + ' [a-zA-Z0-9]', FamilyMaintenanceWarning, stacklevel=2, ) for code in cls.langs: if not all(x in CODE_CHARACTERS for x in code): warnings.warn( - 'Family {} code {} must be ASCII lowercase letters and ' - 'digits [a-z0-9] or underscore/dash [_-]' - .format(cls.name, code), + f'Family {cls.name} code {code} must be ASCII lowercase' + ' letters and digits [a-z0-9] or underscore/dash [_-]', FamilyMaintenanceWarning, stacklevel=2, ) diff --git a/pywikibot/flow.py b/pywikibot/flow.py index 614800d456..e5b136563b 100644 --- a/pywikibot/flow.py +++ b/pywikibot/flow.py @@ -109,7 +109,8 @@ def get(self, force: bool = False, get_redirect: bool = False if get_redirect or force: raise NotImplementedError( "Neither 'force' nor 'get_redirect' parameter is implemented " - 'in {}.get()'.format(self.__class__.__name__)) + f'in {self.__class__.__name__}.get()' + ) # TODO: Return more useful data return getattr(self, '_data', {}) diff --git a/pywikibot/i18n.py b/pywikibot/i18n.py index e07f0ddc3a..0c683af039 100644 --- a/pywikibot/i18n.py +++ b/pywikibot/i18n.py @@ -464,8 +464,8 @@ def replace_plural(match: Match[str]) -> str: variants = match[2] num = parameters[selector] if not isinstance(num, int): - raise ValueError("'{}' must be a number, not a {} ({})" - .format(selector, num, type(num).__name__)) + raise ValueError(f"'{selector}' must be a number, not a {num} " + f'({type(num).__name__})') plural_entries = [] specific_entries = {} @@ -635,8 +635,9 @@ def translate(code: str | pywikibot.site.BaseSite, return trans if not isinstance(parameters, Mapping): - raise ValueError('parameters should be a mapping, not {}' - .format(type(parameters).__name__)) + raise ValueError( + f'parameters should be a mapping, not {type(parameters).__name__}' + ) # else we check for PLURAL variants trans = _extract_plural(code, trans, parameters) @@ -776,10 +777,10 @@ def twtranslate( return fallback_prompt raise pywikibot.exceptions.TranslationError( - 'Unable to load messages package {} for bundle {}' - '\nIt can happen due to lack of i18n submodule or files. ' - 'See {}/i18n' - .format(_messages_package_name, twtitle, __url__)) + f'Unable to load messages package {_messages_package_name} for ' + f' bundle {twtitle}\nIt can happen due to lack of i18n submodule ' + f'or files. See {__url__}/i18n' + ) # if source is a site then use its lang attribute, otherwise it's a str lang = getattr(source, 'lang', source) @@ -810,8 +811,9 @@ def twtranslate( trans = _extract_plural(alt, trans, parameters) if parameters is not None and not isinstance(parameters, Mapping): - raise ValueError('parameters should be a mapping, not {}' - .format(type(parameters).__name__)) + raise ValueError( + f'parameters should be a mapping, not {type(parameters).__name__}' + ) if not only_plural and parameters: trans = trans % parameters @@ -949,8 +951,9 @@ def input(twtitle: str, prompt = fallback_prompt else: raise pywikibot.exceptions.TranslationError( - 'Unable to load messages package {} for bundle {}' - .format(_messages_package_name, twtitle)) + f'Unable to load messages package {_messages_package_name} for ' + f'bundle {twtitle}' + ) return pywikibot.input(prompt, password) diff --git a/pywikibot/interwiki_graph.py b/pywikibot/interwiki_graph.py index e19c8a546c..7925679679 100644 --- a/pywikibot/interwiki_graph.py +++ b/pywikibot/interwiki_graph.py @@ -110,9 +110,8 @@ def addNode(self, page: pywikibot.page.Page) -> None: """Add a node for page.""" assert self.graph is not None node = pydot.Node(self.getLabel(page), shape='rectangle') - node.set_URL('"http://{}{}"' - .format(page.site.hostname(), - page.site.get_address(page.title(as_url=True)))) + node.set_URL(f'"http://{page.site.hostname()}' + f'{page.site.get_address(page.title(as_url=True))}"') node.set_style('filled') node.set_fillcolor('white') node.set_fontsize('11') diff --git a/pywikibot/logentries.py b/pywikibot/logentries.py index 642e974982..9b25276368 100644 --- a/pywikibot/logentries.py +++ b/pywikibot/logentries.py @@ -39,8 +39,8 @@ def __init__(self, apidata: dict[str, Any], self.site = site expected_type = self._expected_type if expected_type is not None and expected_type != self.type(): - raise Error('Wrong log type! Expecting {}, received {} instead.' - .format(expected_type, self.type())) + raise Error(f'Wrong log type! Expecting {expected_type}, received ' + f'{self.type()} instead.') def __missing__(self, key: str) -> None: """Debug when the key is missing. @@ -79,8 +79,8 @@ def __hash__(self) -> int: def __eq__(self, other: Any) -> bool: """Compare if self is equal to other.""" if not isinstance(other, LogEntry): - pywikibot.debug("'{}' cannot be compared with '{}'" - .format(type(self).__name__, type(other).__name__)) + pywikibot.debug(f"'{type(self).__name__}' cannot be compared with " + f"'{type(other).__name__}'") return False return self.logid() == other.logid() and self.site == other.site diff --git a/pywikibot/logging.py b/pywikibot/logging.py index 02bbce90a0..ef028cea8a 100644 --- a/pywikibot/logging.py +++ b/pywikibot/logging.py @@ -138,8 +138,8 @@ def logoutput(msg: Any, f'keyword argument "{key}={arg}"', since='7.2.0') if key in kwargs: - warning('{!r} is given as keyword argument {!r} already; ignoring ' - '{!r}'.format(key, arg, kwargs[key])) + warning(f'{key!r} is given as keyword argument {arg!r} already; ' + f'ignoring {kwargs[key]!r}') else: kwargs[key] = arg diff --git a/pywikibot/login.py b/pywikibot/login.py index 2c9c8c6d27..b670af633a 100644 --- a/pywikibot/login.py +++ b/pywikibot/login.py @@ -100,12 +100,13 @@ def __init__(self, password: str | None = None, user = code_to_usr.get(site.code) or code_to_usr['*'] except KeyError: raise NoUsernameError( - 'ERROR: ' - 'username for {site.family.name}:{site.code} is undefined.' - '\nIf you have a username for that site, please add a ' - 'line to user config file (user-config.py) as follows:\n' - "usernames['{site.family.name}']['{site.code}'] = " - "'myUsername'".format(site=site)) + f'ERROR: username for {site.family.name}:{site.code} is' + ' undefined.\nIf you have a username for that site,' + ' please add a line to user config file (user-config.py)' + ' as follows:\n' + f"usernames['{site.family.name}']['{site.code}'] =" + " 'myUsername'" + ) self.password = password self.login_name = self.username = user if getattr(config, 'password_file', ''): @@ -122,11 +123,10 @@ def check_user_exists(self) -> None: # convert any Special:BotPassword usernames to main account equivalent main_username = self.username if '@' in self.username: - warn( - 'When using BotPasswords it is recommended that you store ' - 'your login credentials in a password_file instead. See ' - '{}/BotPasswords for instructions and more information.' - .format(__url__)) + warn('When using BotPasswords it is recommended that you store' + ' your login credentials in a password_file instead. See ' + f'{__url__}/BotPasswords for instructions and more' + ' information.') main_username = self.username.partition('@')[0] try: @@ -134,15 +134,16 @@ def check_user_exists(self) -> None: user = next(data, {'name': None}) except APIError as e: if e.code == 'readapidenied': - pywikibot.warning("Could not check user '{}' exists on {}" - .format(main_username, self.site)) + pywikibot.warning(f"Could not check user '{main_username}' " + f'exists on {self.site}') return raise if user['name'] != main_username: # Report the same error as server error code NotExists - raise NoUsernameError("Username '{}' does not exist on {}" - .format(main_username, self.site)) + raise NoUsernameError( + f"Username '{main_username}' does not exist on {self.site}" + ) def botAllowed(self) -> bool: """Check whether the bot is listed on a specific page. @@ -280,9 +281,10 @@ def login(self, retry: bool = False, autocreate: bool = False) -> bool: # As we don't want the password to appear on the screen, we set # password = True self.password = pywikibot.input( - 'Password for user {name} on {site} (no characters will be ' - 'shown):'.format(name=self.login_name, site=self.site), - password=True) + f'Password for user {self.login_name} on {self.site}' + ' (no characters will be shown):', + password=True + ) else: pywikibot.info(f'Logging in to {self.site} as {self.login_name}') @@ -293,8 +295,8 @@ def login(self, retry: bool = False, autocreate: bool = False) -> bool: # TODO: investigate other unhandled API codes if error_code in self._api_error: - error_msg = 'Username {!r} {} on {}'.format( - self.login_name, self._api_error[error_code], self.site) + error_msg = (f'Username {self.login_name!r} ' + f'{self._api_error[error_code]} on {self.site}') if error_code in ('Failed', 'FAIL'): error_msg += f'.\n{e.info}' raise NoUsernameError(error_msg) @@ -527,10 +529,11 @@ def __init__(self, password: str | None = None, assert password is not None and user is not None super().__init__(password=None, site=site, user=None) if self.password: - pywikibot.warn('Password exists in password file for {login.site}:' - '{login.username}. Password is unnecessary and ' - 'should be removed if OAuth enabled.' - .format(login=self)) + pywikibot.warn( + f'Password exists in password file for {self.site}: ' + f'{self.username}. Password is unnecessary and should be' + ' removed if OAuth enabled.' + ) self._consumer_token = (user, password) self._access_token: tuple[str, str] | None = None @@ -544,9 +547,8 @@ def login(self, retry: bool = False, force: bool = False) -> bool: :param force: force to re-authenticate """ if self.access_token is None or force: - pywikibot.info( - 'Logging in to {site} via OAuth consumer {key}' - .format(key=self.consumer_token[0], site=self.site)) + pywikibot.info(f'Logging in to {self.site} via OAuth consumer ' + f'{self.consumer_token[0]}') consumer_token = mwoauth.ConsumerToken(*self.consumer_token) handshaker = mwoauth.Handshaker( self.site.base_url(self.site.path()), consumer_token) @@ -554,9 +556,10 @@ def login(self, retry: bool = False, force: bool = False) -> bool: redirect, request_token = handshaker.initiate() pywikibot.stdout('Authenticate via web browser..') webbrowser.open(redirect) - pywikibot.stdout('If your web browser does not open ' - 'automatically, please point it to: {}' - .format(redirect)) + pywikibot.stdout( + 'If your web browser does not open automatically, please ' + f'point it to: {redirect}' + ) request_qs = pywikibot.input('Response query string: ') access_token = handshaker.complete(request_token, request_qs) self._access_token = (access_token.key, access_token.secret) @@ -567,8 +570,8 @@ def login(self, retry: bool = False, force: bool = False) -> bool: return self.login(retry=True, force=force) return False else: - pywikibot.info('Logged in to {site} via consumer {key}' - .format(key=self.consumer_token[0], site=self.site)) + pywikibot.info(f'Logged in to {self.site} via consumer ' + f'{self.consumer_token[0]}') return True @property diff --git a/pywikibot/page/_basepage.py b/pywikibot/page/_basepage.py index 85d255a68c..27ca737cbb 100644 --- a/pywikibot/page/_basepage.py +++ b/pywikibot/page/_basepage.py @@ -129,9 +129,8 @@ def __init__(self, source, title: str = '', ns=0) -> None: self._link = source self._revisions = {} else: - raise Error( - "Invalid argument type '{}' in Page initializer: {}" - .format(type(source), source)) + raise Error(f"Invalid argument type '{type(source)}' in Page " + f'initializer: {source}') @property def site(self): @@ -257,8 +256,7 @@ def title( or self.site.code != target_code)): if self.site.family.name not in ( target_family, self.site.code): - title = '{site.family.name}:{site.code}:{title}'.format( - site=self.site, title=title) + title = f'{self.site.family.name}:{self.site.code}:{title}' else: # use this form for sites like commons, where the # code is the same as the family name @@ -672,9 +670,8 @@ def extract(self, variant: str = 'plain', *, extract = shorten(extract, chars, break_long_words=False, placeholder='…') else: - raise ValueError( - 'variant parameter must be "plain", "html" or "wiki", not "{}"' - .format(variant)) + raise ValueError('variant parameter must be "plain", "html" or ' + f'"wiki", not "{variant}"') if not lines: return extract @@ -847,9 +844,9 @@ def isCategoryRedirect(self) -> bool: self._catredirect = p.title() else: pywikibot.warning( - 'Category redirect target {} on {} is not a ' - 'category'.format(p.title(as_link=True), - self.title(as_link=True))) + f'Category redirect target {p.title(as_link=True)}' + f' on {self.title(as_link=True)} is not a category' + ) else: pywikibot.warning( 'No target found for category redirect on ' @@ -1533,9 +1530,8 @@ def linkedPages( f'keyword argument "{key}={arg}"', since='7.0.0') if key in kwargs: - pywikibot.warning('{!r} is given as keyword argument {!r} ' - 'already; ignoring {!r}' - .format(key, arg, kwargs[key])) + pywikibot.warning(f'{key!r} is given as keyword argument ' + f'{arg!r} already; ignoring {kwargs[key]!r}') else: kwargs[key] = arg @@ -1867,8 +1863,8 @@ def getVersionHistoryTable(self, result += '! oldid || date/time || username || edit summary\n' for entry in self.revisions(reverse=reverse, total=total): result += '|----\n' - result += ('| {r.revid} || {r.timestamp} || {r.user} || ' - '<nowiki>{r.comment}</nowiki>\n'.format(r=entry)) + result += (f'| {entry.revid} || {entry.timestamp} || {entry.user} ' + f'|| <nowiki>{entry.comment}</nowiki>\n') result += '|}\n' return result @@ -2233,12 +2229,9 @@ def change_category(self, old_cat, new_cat, return False if old_cat not in cats: - if self.namespace() != 10: - pywikibot.error( - f'{self} is not in category {old_cat.title()}!') - else: - pywikibot.info('{} is not in category {}, skipping...' - .format(self, old_cat.title())) + pywikibot.info( + f'{self} is not in category {old_cat.title()}, skipping...' + ) return False # This prevents the bot from adding new_cat if it is already present. diff --git a/pywikibot/page/_collections.py b/pywikibot/page/_collections.py index 17c0ad826e..5f6822f11b 100644 --- a/pywikibot/page/_collections.py +++ b/pywikibot/page/_collections.py @@ -444,7 +444,7 @@ def normalizeData(cls, data) -> dict: if not isinstance(json, dict): raise ValueError( "Couldn't determine the site and title of the value: " - '{!r}'.format(json)) + f'{json!r}') db_name = json['site'] norm_data[db_name] = json return norm_data @@ -511,11 +511,10 @@ def __init__(self, repo, data=None): def _validate_isinstance(self, obj): if not isinstance(obj, self.type_class): raise TypeError( - '{} should only hold instances of {}, ' - 'instance of {} was provided' - .format(self.__class__.__name__, - self.type_class.__name__, - obj.__class__.__name__)) + f'{type(self).__name__} should only hold instances of ' + f'{self.type_class.__name__}, instance of ' + f'{type(obj).__name__} was provided' + ) def __getitem__(self, index): if isinstance(index, str): diff --git a/pywikibot/page/_filepage.py b/pywikibot/page/_filepage.py index d8b1b303d4..703d178707 100644 --- a/pywikibot/page/_filepage.py +++ b/pywikibot/page/_filepage.py @@ -155,8 +155,8 @@ def getImagePageHtml(self) -> str: # noqa: N802 same FilePage object, the page will only be downloaded once. """ if not hasattr(self, '_imagePageHtml'): - path = '{}/index.php?title={}'.format(self.site.scriptpath(), - self.title(as_url=True)) + path = (f'{self.site.scriptpath()}/index.php?' + f'title={self.title(as_url=True)}') self._imagePageHtml = http.request(self.site, path).text return self._imagePageHtml diff --git a/pywikibot/page/_links.py b/pywikibot/page/_links.py index 335067415f..fe24dacf93 100644 --- a/pywikibot/page/_links.py +++ b/pywikibot/page/_links.py @@ -6,7 +6,7 @@ its contents. """ # -# (C) Pywikibot team, 2008-2023 +# (C) Pywikibot team, 2008-2024 # # Distributed under the terms of the MIT license. # @@ -155,8 +155,9 @@ def ns_title(self, onsite=None): break else: raise InvalidTitleError( - 'No corresponding title found for namespace {} on {}.' - .format(self.namespace, onsite)) + 'No corresponding title found for namespace ' + f'{self.namespace} on {onsite}.' + ) if self.namespace != Namespace.MAIN: return f'{name}:{self.title}' @@ -308,9 +309,10 @@ def __init__(self, text, source=None, default_namespace=0) -> None: # Cleanup whitespace sep = self._source.family.title_delimiter_and_aliases[0] t = re.sub( - '[{}\xa0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]+' - .format(self._source.family.title_delimiter_and_aliases), - sep, t) + f'[{self._source.family.title_delimiter_and_aliases}' + '\xa0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]+', + sep, t + ) # Strip spaces at both ends t = t.strip() # Remove left-to-right and right-to-left markers. @@ -395,16 +397,18 @@ def parse(self): break # text before : doesn't match any known prefix except SiteDefinitionError as e: raise SiteDefinitionError( - '{} is not a local page on {}, and the interwiki ' - 'prefix {} is not supported by Pywikibot!\n{}' - .format(self._text, self._site, prefix, e)) + f'{self._text} is not a local page on {self._site}, and ' + f'the interwiki prefix {prefix} is not supported by ' + f'Pywikibot!\n{e}' + ) else: if first_other_site: if not self._site.local_interwiki(prefix): raise InvalidTitleError( - '{} links to a non local site {} via an ' - 'interwiki link to {}.'.format( - self._text, newsite, first_other_site)) + f'{self._text} links to a non local site ' + f'{newsite} via an interwiki link to ' + f'{first_other_site}.' + ) elif newsite != self._source: first_other_site = newsite self._site = newsite @@ -435,8 +439,9 @@ def parse(self): next_ns = t[:t.index(':')] if self._site.namespaces.lookup_name(next_ns): raise InvalidTitleError( - "The (non-)talk page of '{}' is a valid title " - 'in another namespace.'.format(self._text)) + f"The (non-)talk page of '{self._text}' is a valid" + ' title in another namespace.' + ) # Reject illegal characters. m = Link.illegal_titles_pattern.search(t) diff --git a/pywikibot/page/_revision.py b/pywikibot/page/_revision.py index 27d077cc2f..8d9075cd68 100644 --- a/pywikibot/page/_revision.py +++ b/pywikibot/page/_revision.py @@ -1,6 +1,6 @@ """Object representing page revision.""" # -# (C) Pywikibot team, 2008-2022 +# (C) Pywikibot team, 2008-2024 # # Distributed under the terms of the MIT license. # @@ -95,5 +95,6 @@ def __str__(self) -> str: def __missing__(self, key): """Provide backward compatibility for exceptions.""" # raise AttributeError instead of KeyError for backward compatibility - raise AttributeError("'{}' object has no attribute '{}'" - .format(self.__class__.__name__, key)) + raise AttributeError( + f"'{type(self).__name__}' object has no attribute '{key}'" + ) diff --git a/pywikibot/page/_toolforge.py b/pywikibot/page/_toolforge.py index 29b22e9595..1d7aa8aa81 100644 --- a/pywikibot/page/_toolforge.py +++ b/pywikibot/page/_toolforge.py @@ -50,9 +50,8 @@ def _check_wh_supported(self): 'main_authors method is implemented for wikipedia family only') if self.site.code not in self.WIKIBLAME_CODES: - raise NotImplementedError( - 'main_authors method is not implemented for wikipedia:{}' - .format(self.site.code)) + raise NotImplementedError('main_authors method is not implemented ' + f'for wikipedia:{self.site.code}') if self.namespace() != pywikibot.site.Namespace.MAIN: raise NotImplementedError( diff --git a/pywikibot/page/_wikibase.py b/pywikibot/page/_wikibase.py index bea70cd926..c6d860f625 100644 --- a/pywikibot/page/_wikibase.py +++ b/pywikibot/page/_wikibase.py @@ -113,8 +113,8 @@ def __init__(self, repo, id_: str | None = None) -> None: def __repr__(self) -> str: if self.id != '-1': - return 'pywikibot.page.{}({!r}, {!r})'.format( - self.__class__.__name__, self.repo, self.id) + return (f'pywikibot.page.{type(self).__name__}' + f'({self.repo!r}, {self.id!r})') return f'pywikibot.page.{self.__class__.__name__}({self.repo!r})' @classmethod @@ -135,8 +135,9 @@ def __getattr__(self, name): return getattr(self, name) return self.get()[name] - raise AttributeError("'{}' object has no attribute '{}'" - .format(self.__class__.__name__, name)) + raise AttributeError( + f"'{type(self).__name__}' object has no attribute '{name}'" + ) def _initialize_empty(self): for key, cls in self.DATA_ATTRIBUTES.items(): @@ -609,9 +610,8 @@ def __init__(self, site, title: str = '', **kwargs) -> None: if not isinstance(site, pywikibot.site.DataSite): raise TypeError('site must be a pywikibot.site.DataSite object') if title and ('ns' not in kwargs and 'entity_type' not in kwargs): - pywikibot.debug('{}.__init__: {} title {!r} specified without ' - 'ns or entity_type' - .format(type(self).__name__, site, title)) + pywikibot.debug(f'{type(self).__name__}.__init__: {site} title ' + f'{title!r} specified without ns or entity_type') self._namespace = None @@ -658,8 +658,9 @@ def __init__(self, site, title: str = '', **kwargs) -> None: if self._namespace: if self._link.namespace != self._namespace.id: - raise ValueError("'{}' is not in the namespace {}" - .format(title, self._namespace.id)) + raise ValueError( + f"'{title}' is not in the namespace {self._namespace.id}" + ) else: # Neither ns or entity_type was provided. # Use the _link to determine entity type. @@ -723,8 +724,9 @@ def get(self, force: bool = False, *args, **kwargs) -> dict: """ if args or kwargs: raise NotImplementedError( - '{}.get does not implement var args: {!r} and {!r}'.format( - self.__class__.__name__, args, kwargs)) + f'{type(self).__name__}.get does not implement var args: ' + f'{args!r} and {kwargs!r}' + ) # todo: this variable is specific to ItemPage lazy_loading_id = not hasattr(self, 'id') and hasattr(self, '_site') diff --git a/pywikibot/pagegenerators/_filters.py b/pywikibot/pagegenerators/_filters.py index a9b3203799..381f598d29 100644 --- a/pywikibot/pagegenerators/_filters.py +++ b/pywikibot/pagegenerators/_filters.py @@ -1,6 +1,6 @@ """Page filter generators provided by the pagegenerators module.""" # -# (C) Pywikibot team, 2008-2022 +# (C) Pywikibot team, 2008-2024 # # Distributed under the terms of the MIT license. # @@ -387,11 +387,8 @@ def to_be_yielded(edit: _Edit, edit_time = rev.timestamp # type: ignore[attr-defined] - msg = '{prefix} edit on {page} was on {time}.\n' \ - 'Too {{when}}. Skipping.' \ - .format(prefix=type(edit).__name__, - page=page, - time=edit_time.isoformat()) + msg = (f'{type(edit).__name__} edit on {page} was on ' + f'{edit_time.isoformat()}.\nToo {{when}}. Skipping.') if edit_time < edit.edit_start: _output_if(show_filtered, msg.format(when='old')) diff --git a/pywikibot/pagegenerators/_generators.py b/pywikibot/pagegenerators/_generators.py index d15ca69607..e50cc546c8 100644 --- a/pywikibot/pagegenerators/_generators.py +++ b/pywikibot/pagegenerators/_generators.py @@ -143,9 +143,8 @@ def LogeventsPageGenerator(logtype: str | None = None, try: yield entry.page() except KeyError as e: - pywikibot.warning('LogeventsPageGenerator: ' - 'failed to load page for {!r}; skipping' - .format(entry.data)) + pywikibot.warning('LogeventsPageGenerator: failed to load page ' + f'for {entry.data!r}; skipping') pywikibot.error(e) diff --git a/pywikibot/proofreadpage.py b/pywikibot/proofreadpage.py index 06f4146182..a4526036bc 100644 --- a/pywikibot/proofreadpage.py +++ b/pywikibot/proofreadpage.py @@ -442,13 +442,13 @@ def __init__(self, source: PageSourceType, title: str = '') -> None: site = source super().__init__(source, title) if self.namespace() != site.proofread_page_ns: - raise ValueError('Page {} must belong to {} namespace' - .format(self.title(), site.proofread_page_ns)) + raise ValueError(f'Page {self.title()} must belong to ' + f'{site.proofread_page_ns} namespace') # Ensure that constants are in line with Extension values. level_list = list(self.site.proofread_levels) if level_list != self.PROOFREAD_LEVELS: - raise ValueError('QLs do not match site values: {} != {}' - .format(level_list, self.PROOFREAD_LEVELS)) + raise ValueError(f'QLs do not match site values: {level_list} != ' + f'{self.PROOFREAD_LEVELS}') self._base, self._base_ext, self._num = self._parse_title() self._multi_page = self._base_ext in self._MULTI_PAGE_EXT @@ -587,8 +587,8 @@ def ql(self) -> int: @decompose def ql(self, value: int) -> None: if value not in self.site.proofread_levels: - raise ValueError('Not valid QL value: {} (legal values: {})' - .format(value, list(self.site.proofread_levels))) + raise ValueError(f'Not valid QL value: {value} (legal values: ' + f'{list(self.site.proofread_levels)})') # TODO: add logic to validate ql value change, considering # site.proofread_levels. self._full_header.ql = value @@ -611,8 +611,10 @@ def status(self) -> str | None: try: return self.site.proofread_levels[self.ql] except KeyError: - pywikibot.warning('Not valid status set for {}: quality level = {}' - .format(self.title(as_link=True), self.ql)) + pywikibot.warning( + f'Not valid status set for {self.title(as_link=True)}: ' + f'quality level = {self.ql}' + ) return None def without_text(self) -> None: @@ -1047,8 +1049,8 @@ def __init__(self, source: PageSourceType, title: str = '') -> None: site = source super().__init__(source, title) if self.namespace() != site.proofread_index_ns: - raise ValueError('Page {} must belong to {} namespace' - .format(self.title(), site.proofread_index_ns)) + raise ValueError(f'Page {self.title()} must belong to ' + f'{site.proofread_index_ns} namespace') self._all_page_links = {} @@ -1181,7 +1183,8 @@ def _get_page_mappings(self) -> None: if not self._soup.find_all('a', attrs=attrs): raise ValueError( 'Missing class="qualityN prp-pagequality-N" or ' - 'class="new" in: {}.'.format(self)) + f'class="new" in: {self}.' + ) page_cnt = 0 for a_tag in self._soup.find_all('a', attrs=attrs): @@ -1267,8 +1270,8 @@ def page_gen( end = self.num_pages if not 1 <= start <= end <= self.num_pages: - raise ValueError('start={}, end={} are not in valid range (1, {})' - .format(start, end, self.num_pages)) + raise ValueError(f'start={start}, end={end} are not in valid ' + f'range (1, {self.num_pages})') # All but 'Without Text' if filter_ql is None: diff --git a/pywikibot/scripts/generate_family_file.py b/pywikibot/scripts/generate_family_file.py index 3ec90561ba..c2b2e00681 100755 --- a/pywikibot/scripts/generate_family_file.py +++ b/pywikibot/scripts/generate_family_file.py @@ -118,8 +118,8 @@ def get_params(self) -> bool: # pragma: no cover return False if any(x not in NAME_CHARACTERS for x in self.name): - print('ERROR: Name of family "{}" must be ASCII letters and ' - 'digits [a-zA-Z0-9]'.format(self.name)) + print(f'ERROR: Name of family "{self.name}" must be ASCII letters' + ' and digits [a-zA-Z0-9]') return False return True @@ -155,9 +155,9 @@ def run(self) -> None: self.wikis[w.lang] = w print('\n==================================' - '\nAPI url: {w.api}' - '\nMediaWiki version: {w.version}' - '\n==================================\n'.format(w=w)) + f'\nAPI url: {w.api}' + f'\nMediaWiki version: {w.version}' + '\n==================================\n') self.getlangs(w) self.getapis() diff --git a/pywikibot/scripts/generate_user_files.py b/pywikibot/scripts/generate_user_files.py index f0e0a98898..022471a6ce 100755 --- a/pywikibot/scripts/generate_user_files.py +++ b/pywikibot/scripts/generate_user_files.py @@ -86,12 +86,12 @@ def change_base_dir(): # config would find that file return new_base - msg = fill("""WARNING: Your user files will be created in the directory + msg = fill(f"""WARNING: Your user files will be created in the directory '{new_base}' you have chosen. To access these files, you will either have to use the argument "-dir:{new_base}" every time you run the bot, or set the environment variable "PYWIKIBOT_DIR" equal to this directory name in your operating system. See your operating system documentation for how to -set environment variables.""".format(new_base=new_base), width=76) +set environment variables.""", width=76) pywikibot.info(msg) if pywikibot.input_yn('Is this OK?', default=False, automatic_quit=False): return new_base @@ -166,8 +166,8 @@ def get_site_and_lang( mycode = pywikibot.input(message, default=default_lang, force=force) if known_langs and mycode and mycode not in known_langs \ and not pywikibot.input_yn( - fill('The site code {!r} is not in the list of known sites. ' - 'Do you want to continue?'.format(mycode)), + fill(f'The site code {mycode!r} is not in the list of known' + ' sites. Do you want to continue?'), default=False, automatic_quit=False): mycode = None @@ -382,16 +382,16 @@ def create_user_config( botpasswords = [] userset = {user.name for user in userlist} for username in userset: - if pywikibot.input_yn('Do you want to add a BotPassword for {}?' - .format(username), force=force, default=False): + if pywikibot.input_yn('Do you want to add a BotPassword for ' + f'{username}?', force=force, default=False): if msg: pywikibot.info(msg) msg = None message = f'BotPassword\'s "bot name" for {username}' botpasswordname = pywikibot.input(message, force=force) - message = 'BotPassword\'s "password" for "{}" ' \ + message = f'BotPassword\'s "password" for "{botpasswordname}" ' \ '(no characters will be shown)' \ - .format(botpasswordname) + botpasswordpass = pywikibot.input(message, force=force, password=True) if botpasswordname and botpasswordpass: @@ -403,8 +403,9 @@ def create_user_config( f"# usernames['{main_family}']['{main_code}'] = 'MyUsername'") else: usernames = '\n'.join( - "usernames['{user.family}']['{user.code}'] = '{user.name}'" - .format(user=user) for user in userlist) + f"usernames['{user.family}']['{user.code}'] = '{user.name}'" + for user in userlist + ) # Arbitrarily use the first key as default settings main_family, main_code = userlist[0].family, userlist[0].code botpasswords = '\n'.join( diff --git a/pywikibot/scripts/login.py b/pywikibot/scripts/login.py index 75c6088224..e47f01f1ab 100755 --- a/pywikibot/scripts/login.py +++ b/pywikibot/scripts/login.py @@ -44,7 +44,7 @@ moved to :mod:`pywikibot.scripts` folder """ # -# (C) Pywikibot team, 2003-2023 +# (C) Pywikibot team, 2003-2024 # # Distributed under the terms of the MIT license. # @@ -83,11 +83,11 @@ def _oauth_login(site) -> None: else: oauth_token = login_manager.consumer_token + login_manager.access_token pywikibot.info( - 'Logged in on {site} as {username} via OAuth consumer {consumer}\n' - 'NOTE: To use OAuth, you need to copy the following line to your ' - 'user config file:\n authenticate[{hostname!r}] = {oauth_token}' - .format(site=site, username=site.username(), consumer=consumer_key, - hostname=site.hostname(), oauth_token=oauth_token)) + f'Logged in on {site} as {site.username()} via OAuth consumer ' + f'{consumer_key}\nNOTE: To use OAuth, you need to copy the' + ' following line to your user config file:\n' + f'authenticate[{site.hostname()!r}] = {oauth_token}' + ) def login_one_site(code, family, oauth, logout, autocreate): @@ -95,9 +95,8 @@ def login_one_site(code, family, oauth, logout, autocreate): try: site = pywikibot.Site(code, family) except SiteDefinitionError: - pywikibot.error('{}:{} is not a valid site, ' - 'please remove it from your user-config' - .format(family, code)) + pywikibot.error(f'{family}:{code} is not a valid site, ' + 'please remove it from your user-config') return if oauth: @@ -172,5 +171,5 @@ def main(*args: str) -> None: start = datetime.datetime.now() with suppress(KeyboardInterrupt): main() - pywikibot.info('\nExecution time: {} seconds' - .format((datetime.datetime.now() - start).seconds)) + pywikibot.info('\nExecution time: ' + f'{(datetime.datetime.now() - start).seconds} seconds') diff --git a/pywikibot/scripts/wrapper.py b/pywikibot/scripts/wrapper.py index fb64afd088..3a16b28396 100755 --- a/pywikibot/scripts/wrapper.py +++ b/pywikibot/scripts/wrapper.py @@ -77,25 +77,26 @@ def check_pwb_versions(package: str): wikibot_version = Version(pwb.__version__) if scripts_version.release > wikibot_version.release: # pragma: no cover - print('WARNING: Pywikibot version {} is behind scripts package ' - 'version {}.\nYour Pywikibot may need an update or be ' - 'misconfigured.\n'.format(wikibot_version, scripts_version)) + print(f'WARNING: Pywikibot version {wikibot_version} is behind ' + f'scripts package version {scripts_version}.\n' + 'Your Pywikibot may need an update or be misconfigured.\n') # calculate previous minor release if wikibot_version.minor > 0: # pragma: no cover - prev_wikibot = Version('{v.major}.{}.{v.micro}' - .format(wikibot_version.minor - 1, - v=wikibot_version)) + prev_wikibot = Version( + f'{wikibot_version.major}.{wikibot_version.minor - 1}.' + f'{wikibot_version.micro}' + ) if scripts_version.release < prev_wikibot.release: - print('WARNING: Scripts package version {} is behind legacy ' - 'Pywikibot version {} and current version {}\nYour scripts ' - 'may need an update or be misconfigured.\n' - .format(scripts_version, prev_wikibot, wikibot_version)) + print(f'WARNING: Scripts package version {scripts_version} is ' + f'behind legacy Pywikibot version {prev_wikibot} and ' + f'current version {wikibot_version}\n' + 'Your scripts may need an update or be misconfigured.\n') elif scripts_version.release < wikibot_version.release: # pragma: no cover - print('WARNING: Scripts package version {} is behind current version ' - '{}\nYour scripts may need an update or be misconfigured.\n' - .format(scripts_version, wikibot_version)) + print(f'WARNING: Scripts package version {scripts_version} is behind ' + f'current version {wikibot_version}\n' + 'Your scripts may need an update or be misconfigured.\n') del Version @@ -359,9 +360,8 @@ def find_alternates(filename, script_paths): script = similar_scripts[0] wait_time = config.pwb_autostart_waittime info('NOTE: Starting the most similar script ' - '<<lightyellow>>{}.py<<default>>\n' - ' in {} seconds; type CTRL-C to stop.' - .format(script, wait_time)) + f'<<lightyellow>>{script}.py<<default>>\n' + f' in {wait_time} seconds; type CTRL-C to stop.') try: sleep(wait_time) # Wait a bit to let it be cancelled except KeyboardInterrupt: diff --git a/pywikibot/site/_apisite.py b/pywikibot/site/_apisite.py index 9e819e3b8b..81c3a85e5a 100644 --- a/pywikibot/site/_apisite.py +++ b/pywikibot/site/_apisite.py @@ -717,8 +717,8 @@ def get_globaluserinfo(self, elif isinstance(user, int): param = {'guiid': user} else: - raise TypeError("Inappropriate argument type of 'user' ({})" - .format(type(user).__name__)) + raise TypeError("Inappropriate argument type of 'user' " + f'({type(user).__name__})') if force or user not in self._globaluserinfo: param.update( @@ -1564,9 +1564,10 @@ def page_can_be_edited( :raises ValueError: invalid action parameter """ if action not in self.siteinfo.get('restrictions')['types']: - raise ValueError('{}.page_can_be_edited(): Invalid value "{}" for ' - '"action" parameter' - .format(self.__class__.__name__, action)) + raise ValueError( + f'{type(self).__name__}.page_can_be_edited(): ' + f'Invalid value "{action}" for "action" parameter' + ) prot_rights = { '': action, 'autoconfirmed': 'editsemiprotected', @@ -2494,8 +2495,9 @@ def movepage( # TODO: Check for talkmove-error messages if 'talkmove-error-code' in result['move']: pywikibot.warning( - 'movepage: Talk page {} not moved' - .format(page.toggleTalkPage().title(as_link=True))) + 'movepage: Talk page ' + f'{page.toggleTalkPage().title(as_link=True)} not moved' + ) return pywikibot.Page(page, newtitle) # catalog of rollback errors for use in error messages @@ -2619,8 +2621,9 @@ def delete( """ if oldimage and isinstance(page, pywikibot.page.BasePage) \ and not isinstance(page, pywikibot.FilePage): - raise TypeError("'page' must be a FilePage not a '{}'" - .format(page.__class__.__name__)) + raise TypeError( + f"'page' must be a FilePage not a '{page.__class__.__name__}'" + ) token = self.tokens['csrf'] params = { diff --git a/pywikibot/site/_basesite.py b/pywikibot/site/_basesite.py index bf65bd21dd..fb3029687e 100644 --- a/pywikibot/site/_basesite.py +++ b/pywikibot/site/_basesite.py @@ -72,8 +72,8 @@ def __init__(self, code: str, fam=None, user=None) -> None: else: # no such language anymore self.obsolete = True - pywikibot.log('Site {} instantiated and marked "obsolete" ' - 'to prevent access'.format(self)) + pywikibot.log(f'Site {self} instantiated and marked "obsolete"' + ' to prevent access') elif self.__code not in self.languages(): if self.__family.name in self.__family.langs \ and len(self.__family.langs) == 1: @@ -82,11 +82,11 @@ def __init__(self, code: str, fam=None, user=None) -> None: and code == pywikibot.config.mylang: pywikibot.config.mylang = self.__code warn('Global configuration variable "mylang" changed to ' - '"{}" while instantiating site {}' - .format(self.__code, self), UserWarning) + f'"{self.__code}" while instantiating site {self}', + UserWarning) else: - error_msg = ("Language '{}' does not exist in family {}" - .format(self.__code, self.__family.name)) + error_msg = (f"Language '{self.__code}' does not exist in " + f'family {self.__family.name}') raise UnknownSiteError(error_msg) self._username = normalize_username(user) @@ -150,15 +150,14 @@ def doc_subpage(self) -> tuple: # should it just raise an Exception and fail? # this will help to check the dictionary ... except KeyError: - warn('Site {} has no language defined in ' - 'doc_subpages dict in {}_family.py file' - .format(self, self.family.name), - FamilyMaintenanceWarning, 2) + warn(f'Site {self} has no language defined in ' + f'doc_subpages dict in {self.family.name}_family.py ' + 'file', FamilyMaintenanceWarning, 2) # doc_subpages not defined in x_family.py file except AttributeError: doc = () # default - warn('Site {} has no doc_subpages dict in {}_family.py file' - .format(self, self.family.name), + warn(f'Site {self} has no doc_subpages dict in ' + f'{self.family.name}_family.py file', FamilyMaintenanceWarning, 2) return doc @@ -332,10 +331,9 @@ def disambcategory(self): try: item = self.family.disambcatname[repo.code] except KeyError: - raise Error( - 'No {repo} qualifier found for disambiguation category ' - 'name in {fam}_family file'.format(repo=repo_name, - fam=self.family.name)) + raise Error(f'No {repo_name} qualifier found for' + ' disambiguation category name in ' + f'{self.family.name}_family file') dp = pywikibot.ItemPage(repo, item) try: diff --git a/pywikibot/site/_datasite.py b/pywikibot/site/_datasite.py index 802f5fe309..c8221532e1 100644 --- a/pywikibot/site/_datasite.py +++ b/pywikibot/site/_datasite.py @@ -95,9 +95,9 @@ def get_namespace_for_entity_type(self, entity_type): if entity_type in self._entity_namespaces: return self._entity_namespaces[entity_type] raise EntityTypeUnknownError( - '{!r} does not support entity type "{}" ' - "or it doesn't have its own namespace" - .format(self, entity_type)) + f'{self!r} does not support entity type "{entity_type}" ' + " or it doesn't have its own namespace" + ) @property def item_namespace(self): @@ -893,7 +893,7 @@ def parsevalue(self, datatype: str, values: list[str], if 'value' not in result_hash: # There should be an APIError occurred already raise RuntimeError("Unexpected missing 'value' in query data:" - '\n{}'.format(result_hash)) + f'\n{result_hash}') results.append(result_hash['value']) return results @@ -1007,8 +1007,8 @@ def prepare_data(action, data): if arg in ['summary', 'tags']: params[arg] = kwargs[arg] else: - warn('Unknown parameter {} for action {}, ignored' - .format(arg, action), UserWarning, 2) + warn(f'Unknown parameter {arg} for action {action}, ignored', + UserWarning, 2) req = self.simple_request(**params) return req.submit() diff --git a/pywikibot/site/_extensions.py b/pywikibot/site/_extensions.py index 77a8344032..4b6243aaa7 100644 --- a/pywikibot/site/_extensions.py +++ b/pywikibot/site/_extensions.py @@ -246,9 +246,8 @@ def globalusage(self, page, total=None): try: gu_site = pywikibot.Site(url=entry['url']) except SiteDefinitionError: - pywikibot.warning( - 'Site could not be defined for global' - ' usage for {}: {}.'.format(page, entry)) + pywikibot.warning('Site could not be defined for global ' + f'usage for {page}: {entry}.') continue gu_page = pywikibot.Page(gu_site, entry['title']) yield gu_page diff --git a/pywikibot/site/_generators.py b/pywikibot/site/_generators.py index 5c08ebf963..ba5d464a98 100644 --- a/pywikibot/site/_generators.py +++ b/pywikibot/site/_generators.py @@ -1984,8 +1984,9 @@ def patrol( if err.code in self._patrol_errors: raise Error(self._patrol_errors[err.code] .format_map(errdata)) - pywikibot.debug("protect: Unexpected error code '{}' received." - .format(err.code)) + pywikibot.debug( + f"protect: Unexpected error code '{err.code}' received." + ) raise yield result['patrol'] diff --git a/pywikibot/site/_interwikimap.py b/pywikibot/site/_interwikimap.py index aa4da13eb5..f62accf113 100644 --- a/pywikibot/site/_interwikimap.py +++ b/pywikibot/site/_interwikimap.py @@ -1,6 +1,6 @@ """Objects representing interwiki map of MediaWiki site.""" # -# (C) Pywikibot team, 2015-2022 +# (C) Pywikibot team, 2015-2024 # # Distributed under the terms of the MIT license. # @@ -75,8 +75,8 @@ def __getitem__(self, prefix): return self._iw_sites[prefix] if isinstance(self._iw_sites[prefix].site, Exception): raise self._iw_sites[prefix].site - raise TypeError('_iw_sites[{}] is wrong type: {}' - .format(prefix, type(self._iw_sites[prefix].site))) + raise TypeError(f'_iw_sites[{prefix}] is wrong type: ' + f'{type(self._iw_sites[prefix].site)}') def get_by_url(self, url: str) -> set[str]: """Return a set of prefixes applying to the URL. diff --git a/pywikibot/site/_namespace.py b/pywikibot/site/_namespace.py index b1d0568053..dd282cb916 100644 --- a/pywikibot/site/_namespace.py +++ b/pywikibot/site/_namespace.py @@ -1,6 +1,6 @@ """Objects representing Namespaces of MediaWiki site.""" # -# (C) Pywikibot team, 2008-2023 +# (C) Pywikibot team, 2008-2024 # # Distributed under the terms of the MIT license. # @@ -273,14 +273,10 @@ def __repr__(self) -> str: else: kwargs = '' - return '{}(id={}, custom_name={!r}, canonical_name={!r}, ' \ - 'aliases={!r}{})' \ - .format(self.__class__.__name__, - self.id, - self.custom_name, - self.canonical_name, - self.aliases, - kwargs) + return (f'{self.__class__.__name__}(id={self.id}, ' + f'custom_name={self.custom_name!r}, ' + f'canonical_name={self.canonical_name!r}, ' + f'aliases={self.aliases!r}{kwargs})') @staticmethod def default_case(id, default_case=None): @@ -355,8 +351,8 @@ def __getitem__(self, key: Namespace | int | str) -> Namespace: try: return self._namespaces[key] except KeyError: - raise KeyError('{} is not a known namespace. Maybe you should ' - 'clear the api cache.'.format(key)) + raise KeyError(f'{key} is not a known namespace. Maybe you' + ' should clear the api cache.') namespace = self.lookup_name(key) if namespace: @@ -438,8 +434,9 @@ def resolve(self, identifiers) -> list[Namespace]: for ns in identifiers] if NotImplemented in result: - raise TypeError('identifiers contains inappropriate types: {!r}' - .format(identifiers)) + raise TypeError( + f'identifiers contains inappropriate types: {identifiers!r}' + ) # Namespace.lookup_name returns None if the name is not recognised if None in result: diff --git a/pywikibot/site/_upload.py b/pywikibot/site/_upload.py index 7adc5fc7e8..d5320eb4b4 100644 --- a/pywikibot/site/_upload.py +++ b/pywikibot/site/_upload.py @@ -199,9 +199,9 @@ def ignore_warnings(warnings): if (offset is not False and offset is not True and offset > file_size): raise ValueError( - 'For the file key "{}" the offset was set to {} ' - 'while the file is only {} bytes large.' - .format(file_key, offset, file_size)) + f'For the file key "{file_key}" the offset was set to ' + f'{offset} while the file is only {file_size} bytes large.' + ) if verify_stash or offset is True: if not file_key: @@ -338,10 +338,10 @@ def ignore_warnings(warnings): # every time ApiError. if offset != new_offset: pywikibot.log( - 'Old offset: {}; Returned ' - 'offset: {}; Chunk size: {}' - .format(offset, new_offset, - len(chunk))) + f'Old offset: {offset}; Returned ' + f'offset: {new_offset}; Chunk size: ' + f'{len(chunk)}' + ) pywikibot.warning('Attempting to correct ' 'automatically from ' 'offset mismatch error.') @@ -390,11 +390,11 @@ def ignore_warnings(warnings): if 'offset' in data: new_offset = int(data['offset']) if offset + len(chunk) != new_offset: - pywikibot.log('Old offset: {}; Returned ' - 'offset: {}; Chunk size: {}' - .format(offset, - new_offset, - len(chunk))) + pywikibot.log( + f'Old offset: {offset}; Returned ' + f'offset: {new_offset}; Chunk size: ' + f'{len(chunk)}' + ) pywikibot.warning('Unexpected offset.') offset = new_offset else: @@ -427,9 +427,8 @@ def ignore_warnings(warnings): else: # upload by URL if not self.site.has_right('upload_by_url'): - raise Error( - "User '{}' is not authorized to upload by URL on site {}." - .format(self.site.user(), self)) + raise Error(f"User '{self.site.user()}' is not authorized to " + f'upload by URL on site {self}.') final_request = self.site.simple_request( action='upload', filename=file_page_title, url=self.url, comment=self.comment, text=self.text, token=token) diff --git a/pywikibot/site_detect.py b/pywikibot/site_detect.py index cbaa93faa3..940139ee0d 100644 --- a/pywikibot/site_detect.py +++ b/pywikibot/site_detect.py @@ -239,10 +239,10 @@ def set_api_url(self, url) -> None: if not new_parsed_url.scheme or not new_parsed_url.netloc: new_parsed_url = urlparse( - '{}://{}{}'.format( - new_parsed_url.scheme or self.url.scheme, - new_parsed_url.netloc or self.url.netloc, - new_parsed_url.path)) + f'{new_parsed_url.scheme or self.url.scheme}://' + f'{new_parsed_url.netloc or self.url.netloc}' + f'{new_parsed_url.path}' + ) else: if self._parsed_url: # allow upgrades to https, but not downgrades @@ -255,12 +255,11 @@ def set_api_url(self, url) -> None: or self._parsed_url.netloc in new_parsed_url.netloc): return - assert new_parsed_url == self._parsed_url, '{} != {}'.format( - self._parsed_url, new_parsed_url) + assert new_parsed_url == self._parsed_url, \ + f'{self._parsed_url} != {new_parsed_url}' self._parsed_url = new_parsed_url - self.server = '{url.scheme}://{url.netloc}'.format( - url=self._parsed_url) + self.server = f'{self._parsed_url.scheme}://{self._parsed_url.netloc}' self.scriptpath = self._parsed_url.path def handle_starttag(self, tag, attrs) -> None: diff --git a/pywikibot/specialbots/_upload.py b/pywikibot/specialbots/_upload.py index c3396edb8e..3654d394cb 100644 --- a/pywikibot/specialbots/_upload.py +++ b/pywikibot/specialbots/_upload.py @@ -216,7 +216,7 @@ def _handle_warning(self, warning: str) -> bool | None: return None if self.aborts is not True else False def _handle_warnings(self, warnings): - messages = '\n'.join('{0.code}: {0.info}'.format(warning) + messages = '\n'.join(f'{warning.code}: {warning.info}' for warning in sorted(warnings, key=lambda w: w.code)) if len(warnings) > 1: diff --git a/pywikibot/textlib.py b/pywikibot/textlib.py index b25ce1c1fd..dc4d46e418 100644 --- a/pywikibot/textlib.py +++ b/pywikibot/textlib.py @@ -235,10 +235,10 @@ def ignore_case(string: str) -> str: def _tag_pattern(tag_name: str) -> str: """Return a tag pattern for the given tag name.""" return ( - r'<{0}(?:>|\s+[^>]*(?<!/)>)' # start tag + rf'<{ignore_case(tag_name)}(?:>|\s+[^>]*(?<!/)>)' # start tag r'[\s\S]*?' # contents - r'</{0}\s*>' # end tag - .format(ignore_case(tag_name))) + rf'</{ignore_case(tag_name)}\s*>' # end tag + ) def _tag_regex(tag_name: str): @@ -732,9 +732,9 @@ def replace_callable(link, text, groups, rng): def check_classes(replacement): """Normalize the replacement into a list.""" if not isinstance(replacement, (pywikibot.Page, pywikibot.Link)): - raise ValueError('The replacement must be None, False, ' - 'a sequence, a Link or a str but ' - 'is "{}"'.format(type(replacement))) + raise ValueError('The replacement must be None, False, a' + ' sequence, a Link or a str but is ' + f'"{type(replacement)}"') def title_section(link) -> str: title = link.title @@ -743,8 +743,8 @@ def title_section(link) -> str: return title if not isinstance(site, pywikibot.site.BaseSite): - raise ValueError('The "site" argument must be a BaseSite not {}.' - .format(type(site).__name__)) + raise ValueError('The "site" argument must be a BaseSite not ' + f'{type(site).__name__}.') if isinstance(replace, Sequence): if len(replace) != 2: @@ -753,8 +753,8 @@ def title_section(link) -> str: replace_list = [to_link(replace[0]), replace[1]] if not isinstance(replace_list[0], pywikibot.Link): raise ValueError( - 'The original value must be either str, Link or Page ' - 'but is "{}"'.format(type(replace_list[0]))) + 'The original value must be either str, Link or Page but is ' + f'"{type(replace_list[0])}"') if replace_list[1] is not False and replace_list[1] is not None: if isinstance(replace_list[1], str): replace_list[1] = pywikibot.Page(site, replace_list[1]) @@ -764,7 +764,8 @@ def title_section(link) -> str: linktrail = site.linktrail() link_pattern = re.compile( r'\[\[(?P<title>.*?)(#(?P<section>.*?))?(\|(?P<label>.*?))?\]\]' - r'(?P<linktrail>{})'.format(linktrail)) + rf'(?P<linktrail>{linktrail})' + ) extended_label_pattern = re.compile(fr'(.*?\]\])({linktrail})') linktrail = re.compile(linktrail) curpos = 0 @@ -1234,8 +1235,8 @@ def removeLanguageLinks(text: str, site=None, marker: str = '') -> str: + list(site.family.obsolete.keys())) if not languages: return text - interwikiR = re.compile(r'\[\[({})\s?:[^\[\]\n]*\]\][\s]*' - .format(languages), re.IGNORECASE) + interwikiR = re.compile(rf'\[\[({languages})\s?:[^\[\]\n]*\]\][\s]*', + re.IGNORECASE) text = replaceExcept(text, interwikiR, '', ['comment', 'math', 'nowiki', 'pre', 'syntaxhighlight'], @@ -1467,8 +1468,9 @@ def getCategoryLinks(text: str, site=None, # and HTML comments text = removeDisabledParts(text, include=include or []) catNamespace = '|'.join(site.namespaces.CATEGORY) - R = re.compile(r'\[\[\s*(?P<namespace>{})\s*:\s*(?P<rest>.+?)\]\]' - .format(catNamespace), re.I) + R = re.compile( + rf'\[\[\s*(?P<namespace>{catNamespace})\s*:\s*(?P<rest>.+?)\]\]', re.I + ) for match in R.finditer(text): match_rest = match['rest'] if expand_text and '{{' in match_rest: @@ -1510,8 +1512,7 @@ def removeCategoryLinks(text: str, site=None, marker: str = '') -> str: if site is None: site = pywikibot.Site() catNamespace = '|'.join(site.namespaces.CATEGORY) - categoryR = re.compile(r'\[\[\s*({})\s*:.*?\]\]\s*' - .format(catNamespace), re.I) + categoryR = re.compile(rf'\[\[\s*({catNamespace})\s*:.*?\]\]\s*', re.I) text = replaceExcept(text, categoryR, '', ['comment', 'includeonly', 'math', 'nowiki', 'pre', 'syntaxhighlight'], @@ -1568,13 +1569,12 @@ def replaceCategoryInPlace(oldtext, oldcat, newcat, site=None, # title might contain regex special characters title = case_escape(site.namespaces[14].case, title, underscore=True) - categoryR = re.compile(r'\[\[\s*({})\s*:\s*{}[\s\u200e\u200f]*' - r'((?:\|[^]]+)?\]\])' - .format(catNamespace, title), re.I) + categoryR = re.compile( + rf'\[\[\s*({catNamespace})\s*:\s*{title}[\s\u200e\u200f]*' + r'((?:\|[^]]+)?\]\])', re.I) categoryRN = re.compile( - r'^[^\S\n]*\[\[\s*({})\s*:\s*{}[\s\u200e\u200f]*' - r'((?:\|[^]]+)?\]\])[^\S\n]*\n' - .format(catNamespace, title), re.I | re.M) + rf'^[^\S\n]*\[\[\s*({catNamespace})\s*:\s*{title}[\s\u200e\u200f]*' + r'((?:\|[^]]+)?\]\])[^\S\n]*\n', re.I | re.M) exceptions = ['comment', 'math', 'nowiki', 'pre', 'syntaxhighlight'] if newcat is None: # First go through and try the more restrictive regex that removes @@ -1587,16 +1587,16 @@ def replaceCategoryInPlace(oldtext, oldcat, newcat, site=None, elif add_only: text = replaceExcept( oldtext, categoryR, - '{}\n{}'.format( - oldcat.title(as_link=True, allow_interwiki=False), - newcat.title(as_link=True, allow_interwiki=False)), - exceptions, site=site) + f'{oldcat.title(as_link=True, allow_interwiki=False)}\n' + f'{newcat.title(as_link=True, allow_interwiki=False)}', + exceptions, site=site + ) else: - text = replaceExcept(oldtext, categoryR, - '[[{}:{}\\2' - .format(site.namespace(14), - newcat.title(with_ns=False)), - exceptions, site=site) + text = replaceExcept( + oldtext, categoryR, + f'[[{site.namespace(14)}:{newcat.title(with_ns=False)}\\2', + exceptions, site=site + ) return text @@ -1756,10 +1756,9 @@ def compileLinkR(withoutBracketed: bool = False, onlyBracketed: bool = False): # not allowed inside links. For example, in this wiki text: # ''Please see https://www.example.org.'' # .'' shouldn't be considered as part of the link. - regex = r'(?P<url>http[s]?://[^{notInside}]*?[^{notAtEnd}]' \ - r'(?=[{notAtEnd}]*\'\')|http[s]?://[^{notInside}]*' \ - r'[^{notAtEnd}])'.format(notInside=notInside, - notAtEnd=notAtEnd) + regex = rf'(?P<url>http[s]?://[^{notInside}]*?[^{notAtEnd}]' \ + rf'(?=[{notAtEnd}]*\'\')|http[s]?://[^{notInside}]*' \ + rf'[^{notAtEnd}])' if withoutBracketed: regex = r'(?<!\[)' + regex diff --git a/pywikibot/time.py b/pywikibot/time.py index 4f4e002dbd..e8910c871f 100644 --- a/pywikibot/time.py +++ b/pywikibot/time.py @@ -527,11 +527,9 @@ def dst(self, dt: datetime.datetime | None) -> datetime.timedelta: def __repr__(self) -> str: """Return the internal representation of the timezone.""" - return '{}({}, {})'.format( - self.__class__.__name__, - self._offset.days * 86400 + self._offset.seconds, - self._name - ) + return (f'{type(self).__name__}' + f'({self._offset.days * 86400 + self._offset.seconds}, ' + f'{self._name})') def str2timedelta( diff --git a/pywikibot/titletranslate.py b/pywikibot/titletranslate.py index 4004fb84e8..e349c74390 100644 --- a/pywikibot/titletranslate.py +++ b/pywikibot/titletranslate.py @@ -68,9 +68,8 @@ def translate( sitelang = page.site.lang dict_name, value = date.getAutoFormat(sitelang, page.title()) if dict_name: - pywikibot.info( - 'TitleTranslate: {} was recognized as {} with value {}' - .format(page.title(), dict_name, value)) + pywikibot.info(f'TitleTranslate: {page.title()} was recognized as ' + f'{dict_name} with value {value}') for entry_lang, entry in date.formats[dict_name].items(): if entry_lang not in site.languages(): continue diff --git a/pywikibot/tools/__init__.py b/pywikibot/tools/__init__.py index 641e14a4fc..3a782b75c2 100644 --- a/pywikibot/tools/__init__.py +++ b/pywikibot/tools/__init__.py @@ -150,8 +150,8 @@ def has_module(module: str, version: str | None = None) -> bool: module_version = packaging.version.Version(metadata_version) if module_version < required_version: - warn('Module version {} is lower than requested version {}' - .format(module_version, required_version), ImportWarning) + warn(f'Module version {module_version} is lower than requested ' + f'version {required_version}', ImportWarning) return False return True @@ -491,8 +491,8 @@ def from_generator(generator: str) -> MediaWikiVersion: prefix = 'MediaWiki ' if not generator.startswith(prefix): - raise ValueError('Generator string ({!r}) must start with ' - '"{}"'.format(generator, prefix)) + raise ValueError(f'Generator string ({generator!r}) must start ' + f'with "{prefix}"') return MediaWikiVersion(generator[len(prefix):]) @@ -513,8 +513,8 @@ def __lt__(self, other: Any) -> bool: if isinstance(other, str): other = MediaWikiVersion(other) elif not isinstance(other, MediaWikiVersion): - raise TypeError("Comparison between 'MediaWikiVersion' and '{}' " - 'unsupported'.format(type(other).__name__)) + raise TypeError(f"Comparison between 'MediaWikiVersion' and " + f"'{type(other).__name__}' unsupported") if self.version != other.version: return self.version < other.version diff --git a/pywikibot/tools/collections.py b/pywikibot/tools/collections.py index 3a1570f870..5ef1cb9c12 100644 --- a/pywikibot/tools/collections.py +++ b/pywikibot/tools/collections.py @@ -1,6 +1,6 @@ """Collections datatypes.""" # -# (C) Pywikibot team, 2014-2023 +# (C) Pywikibot team, 2014-2024 # # Distributed under the terms of the MIT license. # @@ -270,8 +270,8 @@ def send(self, value: Any) -> Any: :raises TypeError: generator property is not a generator """ if not isinstance(self.generator, GeneratorType): - raise TypeError('generator property is not a generator but {}' - .format(type(self.generator).__name__)) + raise TypeError('generator property is not a generator but ' + f'{type(self.generator).__name__}') if not hasattr(self, '_started_gen'): # start the generator self._started_gen = self.generator diff --git a/pywikibot/tools/djvu.py b/pywikibot/tools/djvu.py index 24b76b44ae..aa5fc7aef1 100644 --- a/pywikibot/tools/djvu.py +++ b/pywikibot/tools/djvu.py @@ -98,9 +98,8 @@ def wrapper(obj, *args, **kwargs): n = args[0] force = kwargs.get('force', False) if not 1 <= n <= obj.number_of_images(force=force): - raise ValueError('Page {} not in file {} [{}-{}]' - .format(int(n), obj.file, int(n), - int(obj.number_of_images()))) + raise ValueError(f'Page {int(n)} not in file {obj.file} ' + f'[{int(n)}-{int(obj.number_of_images())}]') return fn(obj, *args, **kwargs) return wrapper diff --git a/pywikibot/tools/itertools.py b/pywikibot/tools/itertools.py index df3fffddb0..e5911ba265 100644 --- a/pywikibot/tools/itertools.py +++ b/pywikibot/tools/itertools.py @@ -4,7 +4,7 @@ in :mod:`backports` """ # -# (C) Pywikibot team, 2008-2023 +# (C) Pywikibot team, 2008-2024 # # Distributed under the terms of the MIT license. # @@ -140,8 +140,8 @@ def intersect_generators(*iterables, allow_duplicates: bool = False): # If any iterable is empty, no pages are going to be returned for source in iterables: if not source: - debug('At least one iterable ({!r}) is empty and execution was ' - 'skipped immediately.'.format(source)) + debug(f'At least one iterable ({source!r}) is empty and execution' + ' was skipped immediately.') return # Item is cached to check that it is found n_gen times diff --git a/pywikibot/tools/threading.py b/pywikibot/tools/threading.py index b4820ff30b..75395614e7 100644 --- a/pywikibot/tools/threading.py +++ b/pywikibot/tools/threading.py @@ -66,7 +66,8 @@ def __repr__(self) -> str: """Representation of tools.RLock instance.""" return repr(self._lock).replace( '_thread.RLock', - '{cls.__module__}.{cls.__class__.__name__}'.format(cls=self)) + f'{self.__module__}.{type(self).__name__}' + ) @property def count(self): diff --git a/pywikibot/userinterfaces/buffer_interface.py b/pywikibot/userinterfaces/buffer_interface.py index 97d7237682..2f508c7656 100644 --- a/pywikibot/userinterfaces/buffer_interface.py +++ b/pywikibot/userinterfaces/buffer_interface.py @@ -3,7 +3,7 @@ .. versionadded:: 6.4 """ # -# (C) Pywikibot team, 2021-2022 +# (C) Pywikibot team, 2021-2024 # # Distributed under the terms of the MIT license. # @@ -75,9 +75,8 @@ def pop_output(self): elif isinstance(record, logging.LogRecord): output.append(record.getMessage()) else: - raise ValueError( - 'BUG: buffer can only contain logs and strings, had {}' - .format(type(record).__name__)) + raise ValueError('Buffer can only contain logs and strings, ' + f'had {type(record).__name__}') return output diff --git a/pywikibot/userinterfaces/gui.py b/pywikibot/userinterfaces/gui.py index 6a18d84f4d..7cd42132e8 100644 --- a/pywikibot/userinterfaces/gui.py +++ b/pywikibot/userinterfaces/gui.py @@ -13,7 +13,7 @@ .. seealso:: :mod:`editor` """ # -# (C) Pywikibot team, 2003-2023 +# (C) Pywikibot team, 2003-2024 # # Distributed under the terms of the MIT license. # @@ -465,9 +465,8 @@ def __init__(self, photo_description, photo, filename) -> None: self.root = tkinter.Tk() # "%dx%d%+d%+d" % (width, height, xoffset, yoffset) - self.root.geometry('{}x{}+10-10' - .format(int(pywikibot.config.tkhorsize), - int(pywikibot.config.tkvertsize))) + self.root.geometry(f'{int(pywikibot.config.tkhorsize)}x' + f'{int(pywikibot.config.tkvertsize)}+10-10') self.root.title(filename) self.photo_description = photo_description diff --git a/pywikibot/userinterfaces/terminal_interface_base.py b/pywikibot/userinterfaces/terminal_interface_base.py index 800148b3bd..4f68dfe6d6 100644 --- a/pywikibot/userinterfaces/terminal_interface_base.py +++ b/pywikibot/userinterfaces/terminal_interface_base.py @@ -139,8 +139,8 @@ def init_handlers( def encounter_color(self, color, target_stream): """Abstract method to handle the next color encountered.""" - raise NotImplementedError('The {} class does not support ' - 'colors.'.format(self.__class__.__name__)) + raise NotImplementedError(f'The {type(self).__name__} class does not' + ' support colors.') @classmethod def divide_color(cls, color): @@ -180,9 +180,8 @@ def _write(self, text: str, target_stream) -> None: out, err = self.stdout.name, self.stderr.name except AttributeError: out, err = self.stdout, self.stderr - raise OSError( - 'Target stream {} is neither stdin ({}) nor stderr ({})' - .format(target_stream.name, out, err)) + raise OSError(f'Target stream {target_stream.name} is neither ' + f'stdin ({out}) nor stderr ({err})') def support_color(self, target_stream) -> bool: """Return whether the target stream does support colors.""" @@ -479,8 +478,8 @@ def output_option(option, before_question) -> None: for i, option in enumerate(options): if not isinstance(option, Option): if len(option) != 2: - raise ValueError('Option #{} does not consist of an ' - 'option and shortcut.'.format(i)) + raise ValueError(f'Option #{i} does not consist of an ' + 'option and shortcut.') options[i] = StandardOption(*option) # TODO: Test for uniquity @@ -625,7 +624,7 @@ def emit(self, record) -> None: self.UI.output(msg, targetStream=self.stream) -class MaxLevelFilter(): +class MaxLevelFilter: """Filter that only passes records at or below a specific level. diff --git a/pywikibot/version.py b/pywikibot/version.py index 67cd656953..4daf5a5108 100644 --- a/pywikibot/version.py +++ b/pywikibot/version.py @@ -409,9 +409,11 @@ def package_versions( path = _file info['path'] = path - assert path not in paths, \ - 'Path {} of the package {} is in defined paths as {}' \ - .format(path, name, paths[path]) + assert path not in paths, ( + f'Path {path} of the package {name} is in defined paths as ' + f'{paths[path]}' + ) + paths[path] = name if '__version__' in package.__dict__: diff --git a/tests/link_tests.py b/tests/link_tests.py index 8134cd65f8..256347efc3 100755 --- a/tests/link_tests.py +++ b/tests/link_tests.py @@ -163,7 +163,7 @@ def generate_has_no_title_exc_regex(text): # A link is invalid if their (non-)talk page would be in another # namespace than the link's "other" namespace (['Talk:File:Example.svg'], - r'The \(non-\)talk page of (u|)\'Talk:File:Example.svg\'' + r"The \(non-\)talk page of 'Talk:File:Example.svg'" r' is a valid title in another namespace.'), (['.', '..', './Sandbox', '../Sandbox', 'Foo/./Sandbox', From aaf0bced5104a7f6acd0cf0b54ed000f071b997f Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sat, 2 Nov 2024 18:26:19 +0100 Subject: [PATCH 15/95] [doc] Capitalize TODOs Change-Id: I242bc5a1a6ce9bcde2d19ea6076861ebe7a76f1c --- pywikibot/page/_wikibase.py | 10 +++++----- scripts/coordinate_import.py | 2 +- scripts/harvest_template.py | 4 ++-- scripts/interwiki.py | 4 ++-- scripts/interwikidata.py | 2 +- scripts/parser_function_count.py | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pywikibot/page/_wikibase.py b/pywikibot/page/_wikibase.py index bea70cd926..047c690c22 100644 --- a/pywikibot/page/_wikibase.py +++ b/pywikibot/page/_wikibase.py @@ -726,7 +726,7 @@ def get(self, force: bool = False, *args, **kwargs) -> dict: '{}.get does not implement var args: {!r} and {!r}'.format( self.__class__.__name__, args, kwargs)) - # todo: this variable is specific to ItemPage + # TODO: this variable is specific to ItemPage lazy_loading_id = not hasattr(self, 'id') and hasattr(self, '_site') try: data = WikibaseEntity.get(self, force=force) @@ -735,7 +735,7 @@ def get(self, force: bool = False, *args, **kwargs) -> dict: p = pywikibot.Page(self._site, self._title) if not p.exists(): raise NoPageError(p) - # todo: raise a nicer exception here (T87345) + # TODO: raise a nicer exception here (T87345) raise NoPageError(self) if 'pageid' in self._content: @@ -1509,7 +1509,7 @@ def get(self, force: bool = False, *args, **kwargs) -> dict: def newClaim(self, *args, **kwargs) -> Claim: """Helper function to create a new claim object for this property.""" - # todo: raise when self.id is -1 + # TODO: raise when self.id is -1 return Claim(self.site, self.getID(), *args, datatype=self.type, **kwargs) @@ -2095,7 +2095,7 @@ def _formatValue(self) -> dict: :return: JSON value """ - # todo: eventually unify the following two groups + # TODO: eventually unify the following two groups if self.type in ('wikibase-item', 'wikibase-property'): value = {'entity-type': self.getTarget().entity_type, 'numeric-id': self.getTarget().getID(numeric=True)} @@ -2303,7 +2303,7 @@ def remove_form(self, form, **kwargs) -> None: form.on_lexeme = None form.id = '-1' - # todo: senses + # TODO: senses def mergeInto(self, lexeme, **kwargs): """Merge the lexeme into another lexeme. diff --git a/scripts/coordinate_import.py b/scripts/coordinate_import.py index e161127a0e..3886ce63be 100755 --- a/scripts/coordinate_import.py +++ b/scripts/coordinate_import.py @@ -139,7 +139,7 @@ def try_import_coordinates_from_page(self, page, item) -> bool: pywikibot.info( f'Adding {coordinate.lat}, {coordinate.lon} to {item.title()}') - # todo: handle exceptions using self.user_add_claim + # TODO: handle exceptions using self.user_add_claim try: item.addClaim(newclaim) except CoordinateGlobeUnknownError as e: diff --git a/scripts/harvest_template.py b/scripts/harvest_template.py index 30ca6c46fa..91d2248e71 100755 --- a/scripts/harvest_template.py +++ b/scripts/harvest_template.py @@ -329,9 +329,9 @@ def treat_field(self, if not field or field not in self.fields: return - # todo: extend the list of tags to ignore + # TODO: extend the list of tags to ignore value = textlib.removeDisabledParts( - # todo: eventually we may want to import the references + # TODO: eventually we may want to import the references value, tags=['ref'], site=site).strip() if not value: diff --git a/scripts/interwiki.py b/scripts/interwiki.py index 7cff604166..8389d97d53 100755 --- a/scripts/interwiki.py +++ b/scripts/interwiki.py @@ -650,7 +650,7 @@ def __init__(self, origin=None, hints=None, conf=None) -> None: super().__init__(origin) - # todo is a list of all pages that still need to be analyzed. + # TODO is a list of all pages that still need to be analyzed. # Mark the origin page as todo. self.todo = SizedKeyCollection('site') if origin: @@ -1274,7 +1274,7 @@ def batchLoaded(self, counter) -> None: counter.minus(page.site) # Now check whether any interwiki links should be added to the - # todo list. + # TODO list. self.check_page(page, counter) # These pages are no longer 'in progress' diff --git a/scripts/interwikidata.py b/scripts/interwikidata.py index 328c011bb1..5187f42aee 100755 --- a/scripts/interwikidata.py +++ b/scripts/interwikidata.py @@ -212,7 +212,7 @@ def try_to_merge(self, item) -> pywikibot.ItemPage | bool | None: """Merge two items.""" wd_data = self.get_items() if not wd_data: - # todo: add links to item + # TODO: add links to item return None if len(wd_data) > 1: diff --git a/scripts/parser_function_count.py b/scripts/parser_function_count.py index da298bb0eb..5900508a6d 100755 --- a/scripts/parser_function_count.py +++ b/scripts/parser_function_count.py @@ -58,7 +58,7 @@ from pywikibot.bot import ExistingPageBot, SingleSiteBot -# Todo: +# TODO: # * Using xml and xmlstart # * Using categories # * Error handling for uploading (anyway, that's the last action, it's only From 11b979d22871b29f3b461a0eb7e7b621b6d2711a Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sat, 2 Nov 2024 18:53:21 +0100 Subject: [PATCH 16/95] [IMPR] Code improvements - decrease nested statements - simplify imports - use maximum() function to determine maximum - remove emtry comments Change-Id: I7f95850f83ab822e0448f0766e40c9ecab4995bb --- pywikibot/bot.py | 45 ++++++++++++++-------------- pywikibot/bot_choice.py | 13 ++++---- pywikibot/diff.py | 7 ++--- pywikibot/pagegenerators/_filters.py | 7 ++--- pywikibot/site/_datasite.py | 15 +++++----- pywikibot/site/_upload.py | 21 +++++++------ pywikibot/site_detect.py | 27 ++++++++--------- pywikibot/specialbots/_upload.py | 2 +- scripts/category.py | 19 ++++++------ scripts/checkimages.py | 31 ++++++++++--------- scripts/interwikidata.py | 5 ++-- scripts/maintenance/cache.py | 7 ++--- scripts/pagefromfile.py | 5 ++-- scripts/patrol.py | 3 +- scripts/solve_disambiguation.py | 29 +++++++++--------- scripts/template.py | 7 ++--- scripts/transwikiimport.py | 13 ++++---- scripts/welcome.py | 2 -- tests/cache_tests.py | 2 +- tests/sparql_tests.py | 2 +- 20 files changed, 122 insertions(+), 140 deletions(-) diff --git a/pywikibot/bot.py b/pywikibot/bot.py index 15d425e4bf..010005c320 100644 --- a/pywikibot/bot.py +++ b/pywikibot/bot.py @@ -2284,32 +2284,31 @@ def treat_page(self) -> None: item = pywikibot.ItemPage.fromPage(page) except NoPageError: item = None + elif isinstance(page, pywikibot.ItemPage): + item = page + page = None else: - if isinstance(page, pywikibot.ItemPage): - item = page + # FIXME: Hack because 'is_data_repository' doesn't work if + # site is the APISite. See T85483 + assert page is not None + data_site = page.site.data_repository() + if (data_site.family == page.site.family + and data_site.code == page.site.code): + is_item = page.namespace() == data_site.item_namespace.id + else: + is_item = False + if is_item: + item = pywikibot.ItemPage(data_site, page.title()) page = None else: - # FIXME: Hack because 'is_data_repository' doesn't work if - # site is the APISite. See T85483 - assert page is not None - data_site = page.site.data_repository() - if (data_site.family == page.site.family - and data_site.code == page.site.code): - is_item = page.namespace() == data_site.item_namespace.id - else: - is_item = False - if is_item: - item = pywikibot.ItemPage(data_site, page.title()) - page = None - else: - try: - item = pywikibot.ItemPage.fromPage(page) - except NoPageError: - item = None - if self.use_from_page is False: - _error(f'{page} is not in the item namespace but must' - ' be an item.') - return + try: + item = pywikibot.ItemPage.fromPage(page) + except NoPageError: + item = None + if self.use_from_page is False: + _error(f'{page} is not in the item namespace but must' + ' be an item.') + return assert not (page is None and item is None) diff --git a/pywikibot/bot_choice.py b/pywikibot/bot_choice.py index eacc8ae188..9ddc71d1da 100644 --- a/pywikibot/bot_choice.py +++ b/pywikibot/bot_choice.py @@ -350,14 +350,13 @@ def handle(self) -> Any: kwargs['label'] += '#' + self.replacer._new.section else: kwargs['label'] = self.replacer._new.anchor + elif self.replacer.current_link.anchor is None: + kwargs['label'] = self.replacer.current_groups['title'] + if self.replacer.current_groups['section']: + kwargs['label'] += '#' \ + + self.replacer.current_groups['section'] else: - if self.replacer.current_link.anchor is None: - kwargs['label'] = self.replacer.current_groups['title'] - if self.replacer.current_groups['section']: - kwargs['label'] += '#' \ - + self.replacer.current_groups['section'] - else: - kwargs['label'] = self.replacer.current_link.anchor + kwargs['label'] = self.replacer.current_link.anchor return pywikibot.Link.create_separated( self.replacer._new.canonical_title(), self.replacer._new.site, **kwargs) diff --git a/pywikibot/diff.py b/pywikibot/diff.py index d1df913040..005da215d5 100644 --- a/pywikibot/diff.py +++ b/pywikibot/diff.py @@ -189,10 +189,9 @@ def color_line(self, line: str, line_ref: str | None = None) -> str: apply_color = 'default;' + self.bg_colors[color] char_tagged = f'<<{apply_color}>>{char}' color_closed = False - else: - if char_ref == ' ': - char_tagged = f'<<default>>{char}' - color_closed = True + elif char_ref == ' ': + char_tagged = f'<<default>>{char}' + color_closed = True colored_line += char_tagged if not color_closed: diff --git a/pywikibot/pagegenerators/_filters.py b/pywikibot/pagegenerators/_filters.py index 381f598d29..660ec16f15 100644 --- a/pywikibot/pagegenerators/_filters.py +++ b/pywikibot/pagegenerators/_filters.py @@ -222,10 +222,9 @@ def SubpageFilterGenerator(generator: Iterable[pywikibot.page.BasePage], for page in generator: if page.depth <= max_depth: yield page - else: - if show_filtered: - pywikibot.info( - f'Page {page} is a subpage that is too deep. Skipping.') + elif show_filtered: + pywikibot.info( + f'Page {page} is a subpage that is too deep. Skipping.') class RegexFilter: diff --git a/pywikibot/site/_datasite.py b/pywikibot/site/_datasite.py index c8221532e1..d4a645bca1 100644 --- a/pywikibot/site/_datasite.py +++ b/pywikibot/site/_datasite.py @@ -221,15 +221,14 @@ def preload_entities( ident = p._defined_by() for key in ident: req[key].append(ident[key]) + elif (p.site == self + and p.namespace() in self._entity_namespaces.values()): + req['ids'].append(p.title(with_ns=False)) else: - if p.site == self and p.namespace() in ( - self._entity_namespaces.values()): - req['ids'].append(p.title(with_ns=False)) - else: - assert p.site.has_data_repository, \ - 'Site must have a data repository' - req['sites'].append(p.site.dbName()) - req['titles'].append(p._link._text) + assert p.site.has_data_repository, \ + 'Site must have a data repository' + req['sites'].append(p.site.dbName()) + req['titles'].append(p._link._text) req = self.simple_request(action='wbgetentities', **req) data = req.submit() diff --git a/pywikibot/site/_upload.py b/pywikibot/site/_upload.py index d5320eb4b4..0386764aed 100644 --- a/pywikibot/site/_upload.py +++ b/pywikibot/site/_upload.py @@ -413,17 +413,16 @@ def ignore_warnings(warnings): raise Error('Unrecognized result: {result}' .format_map(data)) - else: # not chunked upload - if file_key: - final_request['filekey'] = file_key - else: - file_contents = f.read() - filetype = (mimetypes.guess_type(self.filename)[0] - or 'application/octet-stream') - final_request.mime = { - 'file': (file_contents, filetype.split('/'), - {'filename': mime_filename}) - } + elif file_key: + final_request['filekey'] = file_key + else: + file_contents = f.read() + filetype = (mimetypes.guess_type(self.filename)[0] + or 'application/octet-stream') + final_request.mime = { + 'file': (file_contents, filetype.split('/'), + {'filename': mime_filename}) + } else: # upload by URL if not self.site.has_right('upload_by_url'): diff --git a/pywikibot/site_detect.py b/pywikibot/site_detect.py index 940139ee0d..728f9e26ad 100644 --- a/pywikibot/site_detect.py +++ b/pywikibot/site_detect.py @@ -243,20 +243,19 @@ def set_api_url(self, url) -> None: f'{new_parsed_url.netloc or self.url.netloc}' f'{new_parsed_url.path}' ) - else: - if self._parsed_url: - # allow upgrades to https, but not downgrades - if self._parsed_url.scheme == 'https' \ - and new_parsed_url.scheme != self._parsed_url.scheme: - return - - # allow http://www.brickwiki.info/ vs http://brickwiki.info/ - if (new_parsed_url.netloc in self._parsed_url.netloc - or self._parsed_url.netloc in new_parsed_url.netloc): - return - - assert new_parsed_url == self._parsed_url, \ - f'{self._parsed_url} != {new_parsed_url}' + elif self._parsed_url: + # allow upgrades to https, but not downgrades + if self._parsed_url.scheme == 'https' \ + and new_parsed_url.scheme != self._parsed_url.scheme: + return + + # allow http://www.brickwiki.info/ vs http://brickwiki.info/ + if (new_parsed_url.netloc in self._parsed_url.netloc + or self._parsed_url.netloc in new_parsed_url.netloc): + return + + assert new_parsed_url == self._parsed_url, \ + f'{self._parsed_url} != {new_parsed_url}' self._parsed_url = new_parsed_url self.server = f'{self._parsed_url.scheme}://{self._parsed_url.netloc}' diff --git a/pywikibot/specialbots/_upload.py b/pywikibot/specialbots/_upload.py index 3654d394cb..69abe2ac90 100644 --- a/pywikibot/specialbots/_upload.py +++ b/pywikibot/specialbots/_upload.py @@ -19,10 +19,10 @@ import requests import pywikibot -import pywikibot.comms.http as http from pywikibot import config from pywikibot.backports import Callable from pywikibot.bot import BaseBot, QuitKeyboardInterrupt +from pywikibot.comms import http from pywikibot.exceptions import APIError, FatalServerError, NoPageError diff --git a/scripts/category.py b/scripts/category.py index 9798d27877..8d601e6b2a 100755 --- a/scripts/category.py +++ b/scripts/category.py @@ -700,17 +700,16 @@ def __init__(self, oldcat, elif deletion_comment == self.DELETION_COMMENT_SAME_AS_EDIT_COMMENT: # Use the edit comment as the deletion comment. self.deletion_comment = self.comment + # Deletion comment is set to internationalized default. + elif self.newcat: + # Category is moved. + self.deletion_comment = i18n.twtranslate(self.site, + 'category-was-moved', + template_vars) else: - # Deletion comment is set to internationalized default. - if self.newcat: - # Category is moved. - self.deletion_comment = i18n.twtranslate(self.site, - 'category-was-moved', - template_vars) - else: - # Category is deleted. - self.deletion_comment = i18n.twtranslate( - self.site, 'category-was-disbanded') + # Category is deleted. + self.deletion_comment = i18n.twtranslate( + self.site, 'category-was-disbanded') self.move_comment = move_comment if move_comment else self.comment def run(self) -> None: diff --git a/scripts/checkimages.py b/scripts/checkimages.py index 60abaf6ebe..9f4741c9f2 100755 --- a/scripts/checkimages.py +++ b/scripts/checkimages.py @@ -1262,22 +1262,21 @@ def smart_detection(self) -> tuple[str, bool]: else: pywikibot.info('Skipping the file...') self.some_problem = False - else: - if not self.seems_ok and self.license_found: - rep_text_license_fake = ((self.list_entry - + "seems to have a ''fake license''," - ' license detected:' - ' <nowiki>%s</nowiki>') % - (self.image_name, self.license_found)) - print_with_time_zone( - f'{self.image_name} seems to have a fake license: ' - f'{self.license_found}, reporting...') - self.report_image(self.image_name, - rep_text=rep_text_license_fake, - addings=False) - elif self.license_found: - pywikibot.info(f'[[{self.image_name}]] seems ok, license ' - f'found: {{{{{self.license_found}}}}}...') + elif not self.seems_ok and self.license_found: + rep_text_license_fake = ((self.list_entry + + "seems to have a ''fake license''," + ' license detected:' + ' <nowiki>%s</nowiki>') % + (self.image_name, self.license_found)) + print_with_time_zone( + f'{self.image_name} seems to have a fake license: ' + f'{self.license_found}, reporting...') + self.report_image(self.image_name, + rep_text=rep_text_license_fake, + addings=False) + elif self.license_found: + pywikibot.info(f'[[{self.image_name}]] seems ok, license ' + f'found: {{{{{self.license_found}}}}}...') return (self.license_found, self.white_templates_found) @staticmethod diff --git a/scripts/interwikidata.py b/scripts/interwikidata.py index 5187f42aee..23767dd066 100755 --- a/scripts/interwikidata.py +++ b/scripts/interwikidata.py @@ -106,9 +106,8 @@ def treat_page(self) -> None: item = self.try_to_add() if self.opt.create and item is None: item = self.create_item() - else: - if self.opt.merge: - item = self.try_to_merge(item) + elif self.opt.merge: + item = self.try_to_merge(item) if item and self.opt.clean: self.current_item = item diff --git a/scripts/maintenance/cache.py b/scripts/maintenance/cache.py index 929d5d88d5..45674ffe41 100755 --- a/scripts/maintenance/cache.py +++ b/scripts/maintenance/cache.py @@ -444,11 +444,10 @@ def main(): if output: sys.exit('Only one output may be defined.') output = '' + elif not cache_paths: + cache_paths = [arg] else: - if not cache_paths: - cache_paths = [arg] - else: - cache_paths.append(arg) + cache_paths.append(arg) if not cache_paths: folders = ('apicache', 'apicache-py2', 'apicache-py3') diff --git a/scripts/pagefromfile.py b/scripts/pagefromfile.py index 2e0e09d0ad..1217bbfdce 100755 --- a/scripts/pagefromfile.py +++ b/scripts/pagefromfile.py @@ -169,9 +169,8 @@ def treat_page(self) -> None: else: pywikibot.info(f'Page {title} already exists, not adding!') return - else: - if self.opt.autosummary: - comment = config.default_edit_summary = '' + elif self.opt.autosummary: + comment = config.default_edit_summary = '' self.put_current(contents, summary=comment, minor=self.opt.minor, diff --git a/scripts/patrol.py b/scripts/patrol.py index dd9d40f1e8..410c1ea3c1 100755 --- a/scripts/patrol.py +++ b/scripts/patrol.py @@ -323,8 +323,7 @@ def treat(self, page): else: verbose_output('Skipped') - if rcid > self.highest_rcid: - self.highest_rcid = rcid + self.highest_rcid = max(rcid, self.highest_rcid) self.last_rcid = rcid diff --git a/scripts/solve_disambiguation.py b/scripts/solve_disambiguation.py index 2b5775243e..367086e5fb 100755 --- a/scripts/solve_disambiguation.py +++ b/scripts/solve_disambiguation.py @@ -1163,22 +1163,21 @@ def setSummaryMessage( {'from': page.title(), 'to': targets, 'count': len(new_targets)}) + elif unlink_counter and not new_targets: + self.summary = i18n.twtranslate( + self.site, 'solve_disambiguation-links-removed', + {'from': page.title(), + 'count': unlink_counter}) + elif dn and not new_targets: + self.summary = i18n.twtranslate( + self.site, 'solve_disambiguation-adding-dn-template', + {'from': page.title()}) else: - if unlink_counter and not new_targets: - self.summary = i18n.twtranslate( - self.site, 'solve_disambiguation-links-removed', - {'from': page.title(), - 'count': unlink_counter}) - elif dn and not new_targets: - self.summary = i18n.twtranslate( - self.site, 'solve_disambiguation-adding-dn-template', - {'from': page.title()}) - else: - self.summary = i18n.twtranslate( - self.site, 'solve_disambiguation-links-resolved', - {'from': page.title(), - 'to': targets, - 'count': len(new_targets)}) + self.summary = i18n.twtranslate( + self.site, 'solve_disambiguation-links-resolved', + {'from': page.title(), + 'to': targets, + 'count': len(new_targets)}) def teardown(self) -> None: """Write ignoring pages to a file.""" diff --git a/scripts/template.py b/scripts/template.py index 770fc42a84..cc91a6ab98 100755 --- a/scripts/template.py +++ b/scripts/template.py @@ -255,10 +255,9 @@ def main(*args: str) -> None: skip = True elif arg.startswith('-timestamp:'): timestamp = arg[len('-timestamp:'):] - else: - if not gen_factory.handle_arg(arg): - template_name = pywikibot.Page(site, arg, ns=10) - template_names.append(template_name.title(with_ns=False)) + elif not gen_factory.handle_arg(arg): + template_name = pywikibot.Page(site, arg, ns=10) + template_names.append(template_name.title(with_ns=False)) if not template_names: pywikibot.bot.suggest_help(missing_parameters=['templates']) diff --git a/scripts/transwikiimport.py b/scripts/transwikiimport.py index 7a8a18e343..67bcfb59a1 100755 --- a/scripts/transwikiimport.py +++ b/scripts/transwikiimport.py @@ -289,13 +289,12 @@ def main(*args: str) -> None: ' exists)' ) continue - else: - if not targetpage.botMayEdit(): - pywikibot.warning( - f'Target page {targetpage.title(as_link=True)} is not' - ' editable by bots' - ) - continue + elif not targetpage.botMayEdit(): + pywikibot.warning( + f'Target page {targetpage.title(as_link=True)} is not' + ' editable by bots' + ) + continue params['interwikipage'] = fromtitle api_query(tosite, params) diff --git a/scripts/welcome.py b/scripts/welcome.py index 18a4be13c4..430da3a6cd 100755 --- a/scripts/welcome.py +++ b/scripts/welcome.py @@ -429,8 +429,6 @@ 'zh': '<small>(via ~~~)</small>', } -# -# LOGPAGE_HEADER = { '_default': '{|border="2" cellpadding="4" cellspacing="0" style="margin: ' '0.5em 0.5em 0.5em 1em; padding: 0.5em; background: #bfcda5; ' diff --git a/tests/cache_tests.py b/tests/cache_tests.py index 837c51ecba..b02bf40bf8 100755 --- a/tests/cache_tests.py +++ b/tests/cache_tests.py @@ -9,9 +9,9 @@ import unittest -import scripts.maintenance.cache as cache from pywikibot.login import LoginStatus from pywikibot.site import BaseSite +from scripts.maintenance import cache from tests import join_cache_path from tests.aspects import TestCase diff --git a/tests/sparql_tests.py b/tests/sparql_tests.py index 5ea73464b6..243ad7b97e 100755 --- a/tests/sparql_tests.py +++ b/tests/sparql_tests.py @@ -13,7 +13,7 @@ from unittest.mock import patch import pywikibot -import pywikibot.data.sparql as sparql +from pywikibot.data import sparql from pywikibot.exceptions import NoUsernameError from tests.aspects import TestCase, WikidataTestCase from tests.utils import skipping From 78733ed0547ba8ccf0e22cae6d0f00b780ed12c9 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sat, 2 Nov 2024 19:28:10 +0100 Subject: [PATCH 17/95] [IMPR] use long regex flags which are more readable Change-Id: I2946c23d2ac0f0c31f2c9a7310cf478a90a8812b --- pywikibot/comms/http.py | 2 +- pywikibot/cosmetic_changes.py | 4 ++-- pywikibot/pagegenerators/_filters.py | 2 +- pywikibot/textlib.py | 12 ++++++------ pywikibot/tools/__init__.py | 2 +- scripts/category.py | 4 ++-- scripts/category_redirect.py | 2 +- scripts/checkimages.py | 2 +- scripts/commonscat.py | 2 +- scripts/solve_disambiguation.py | 2 +- scripts/upload.py | 2 +- scripts/welcome.py | 2 +- 12 files changed, 19 insertions(+), 19 deletions(-) diff --git a/pywikibot/comms/http.py b/pywikibot/comms/http.py index 71d629b7d1..71345d6128 100644 --- a/pywikibot/comms/http.py +++ b/pywikibot/comms/http.py @@ -457,7 +457,7 @@ def assign_user_agent(user_agent_format_string): # Extract charset (from content-type header) CHARSET_RE = re.compile( r'charset\s*=\s*(?P<q>[\'"]?)(?P<charset>[^\'",;>/]+)(?P=q)', - flags=re.I, + flags=re.IGNORECASE, ) diff --git a/pywikibot/cosmetic_changes.py b/pywikibot/cosmetic_changes.py index effc1cdac4..cf3df33299 100644 --- a/pywikibot/cosmetic_changes.py +++ b/pywikibot/cosmetic_changes.py @@ -529,7 +529,7 @@ def replace_magicword(match: Match[str]) -> str: exceptions = ['comment', 'nowiki', 'pre', 'syntaxhighlight'] regex = re.compile( FILE_LINK_REGEX % '|'.join(self.site.namespaces[6]), - flags=re.X) + flags=re.VERBOSE) return textlib.replaceExcept( text, regex, replace_magicword, exceptions) @@ -729,7 +729,7 @@ def removeEmptySections(self, text: str) -> str: if self.site.code in skip_templates: for template in skip_templates[self.site.code]: skip_regexes.append( - re.compile(r'\{\{\s*%s\s*\}\}' % template, re.I)) + re.compile(r'\{\{\s*%s\s*\}\}' % template, re.IGNORECASE)) # empty lists skip_regexes.append(re.compile(r'(?m)^[\*#] *$')) diff --git a/pywikibot/pagegenerators/_filters.py b/pywikibot/pagegenerators/_filters.py index 660ec16f15..85e811a733 100644 --- a/pywikibot/pagegenerators/_filters.py +++ b/pywikibot/pagegenerators/_filters.py @@ -291,7 +291,7 @@ def titlefilter(cls, quantifier = 'any' elif quantifier is True: quantifier = 'none' - reg = cls.__precompile(regex, re.I) + reg = cls.__precompile(regex, re.IGNORECASE) for page in generator: title = page.title(with_ns=not ignore_namespace) if cls.__filter_match(reg, title, quantifier): diff --git a/pywikibot/textlib.py b/pywikibot/textlib.py index dc4d46e418..d8e67f9878 100644 --- a/pywikibot/textlib.py +++ b/pywikibot/textlib.py @@ -1571,10 +1571,10 @@ def replaceCategoryInPlace(oldtext, oldcat, newcat, site=None, title = case_escape(site.namespaces[14].case, title, underscore=True) categoryR = re.compile( rf'\[\[\s*({catNamespace})\s*:\s*{title}[\s\u200e\u200f]*' - r'((?:\|[^]]+)?\]\])', re.I) + r'((?:\|[^]]+)?\]\])', re.IGNORECASE) categoryRN = re.compile( rf'^[^\S\n]*\[\[\s*({catNamespace})\s*:\s*{title}[\s\u200e\u200f]*' - r'((?:\|[^]]+)?\]\])[^\S\n]*\n', re.I | re.M) + r'((?:\|[^]]+)?\]\])[^\S\n]*\n', re.IGNORECASE | re.MULTILINE) exceptions = ['comment', 'math', 'nowiki', 'pre', 'syntaxhighlight'] if newcat is None: # First go through and try the more restrictive regex that removes @@ -1621,7 +1621,7 @@ def replaceCategoryLinks(oldtext: str, if site is None: site = pywikibot.Site() if re.search(r'\{\{ *(' + r'|'.join(site.getmagicwords('defaultsort')) - + r')', oldtext, flags=re.I): + + r')', oldtext, flags=re.IGNORECASE): separator = '\n' else: separator = site.family.category_text_separator @@ -1668,15 +1668,15 @@ def replaceCategoryLinks(oldtext: str, if site.sitename == 'wikipedia:de': personendaten = re.compile(r'\{\{ *Personendaten.*?\}\}', - re.I | re.DOTALL) + re.IGNORECASE | re.DOTALL) under_categories.append(personendaten) if site.sitename == 'wikipedia:yi': - stub = re.compile(r'\{\{.*?שטומף *\}\}', re.I) + stub = re.compile(r'\{\{.*?שטומף *\}\}', re.IGNORECASE) under_categories.append(stub) if site.family.name == 'wikipedia' and site.code in ('simple', 'en'): - stub = re.compile(r'\{\{.*?stub *\}\}', re.I) + stub = re.compile(r'\{\{.*?stub *\}\}', re.IGNORECASE) under_categories.append(stub) if under_categories: diff --git a/pywikibot/tools/__init__.py b/pywikibot/tools/__init__.py index 3a782b75c2..e23a89c863 100644 --- a/pywikibot/tools/__init__.py +++ b/pywikibot/tools/__init__.py @@ -217,7 +217,7 @@ def __init__( the start of the path to the warning module must match. (case-sensitive) """ - self.message_match = re.compile(message, re.I).match + self.message_match = re.compile(message, re.IGNORECASE).match self.category = category self.filename_match = re.compile(filename).match super().__init__(record=True) diff --git a/scripts/category.py b/scripts/category.py index 8d601e6b2a..12ac25e042 100755 --- a/scripts/category.py +++ b/scripts/category.py @@ -197,7 +197,7 @@ CFD_TEMPLATE_REGEX = re.compile(r'<!--\s*BEGIN CFD TEMPLATE\s*-->.*?' r'<!--\s*END CFD TEMPLATE\s*-->\n?', - flags=re.I | re.M | re.S) + flags=re.IGNORECASE | re.MULTILINE | re.DOTALL) cfd_templates = { 'wikipedia': { @@ -531,7 +531,7 @@ def treat(self, page) -> None: if self.includeonly == ['includeonly']: tagname = 'includeonly' tagnameregexp = re.compile(fr'(.*)(<\/{tagname}>)', - re.I | re.DOTALL) + re.IGNORECASE | re.DOTALL) categorytitle = catpl.title( as_link=True, allow_interwiki=False) if tagnameregexp.search(text): diff --git a/scripts/category_redirect.py b/scripts/category_redirect.py index 462f36525f..aadb8bfc5a 100755 --- a/scripts/category_redirect.py +++ b/scripts/category_redirect.py @@ -339,7 +339,7 @@ def setup_soft_redirect(self): template='|'.join(item.replace(' ', '[ _]+') for item in self.template_list), catns=self.site.namespace(14)), - re.I | re.X) + re.IGNORECASE | re.VERBOSE) nonemptypages = [] catpages = set() diff --git a/scripts/checkimages.py b/scripts/checkimages.py index 9f4741c9f2..ca695d8571 100755 --- a/scripts/checkimages.py +++ b/scripts/checkimages.py @@ -1358,7 +1358,7 @@ def is_tagged(self) -> bool: if '{{' in i: regex_pattern = re.compile( r'\{\{(?:template)?%s ?(?:\||\r?\n|\}|<|/) ?' - % i.split('{{')[1].replace(' ', '[ _]'), re.I) + % i.split('{{')[1].replace(' ', '[ _]'), re.IGNORECASE) result = regex_pattern.findall(self.image_check_text) if result: return True diff --git a/scripts/commonscat.py b/scripts/commonscat.py index c2ce6786f8..9074deb8f1 100755 --- a/scripts/commonscat.py +++ b/scripts/commonscat.py @@ -492,7 +492,7 @@ def checkCommonscatLink(self, name: str = ''): r'(?P<newcat1>[^\|\}]+)(\|[^\}]+)?\]\]|' r'Robot: Changing Category:(.+) ' r'to Category:(?P<newcat2>.+)') - m = re.search(regex, logcomment, flags=re.I) + m = re.search(regex, logcomment, flags=re.IGNORECASE) if not m: pywikibot.info( diff --git a/scripts/solve_disambiguation.py b/scripts/solve_disambiguation.py index 367086e5fb..3da88c5dc3 100755 --- a/scripts/solve_disambiguation.py +++ b/scripts/solve_disambiguation.py @@ -674,7 +674,7 @@ def setup(self) -> None: \[\[ (?P<title> [^\[\]\|#]*) (?P<section> \#[^\]\|]*)? (\|(?P<label> [^\]]*))? \]\] - (?P<linktrail>{linktrail})""", flags=re.X) + (?P<linktrail>{linktrail})""", flags=re.VERBOSE) @staticmethod def firstlinks(page) -> Generator[str]: diff --git a/scripts/upload.py b/scripts/upload.py index 9432928060..a64ef92c1f 100755 --- a/scripts/upload.py +++ b/scripts/upload.py @@ -83,7 +83,7 @@ CHUNK_SIZE_REGEX = re.compile( - r'-chunked(?::(\d+(?:\.\d+)?)[ \t]*(k|ki|m|mi)?b?)?', re.I) + r'-chunked(?::(\d+(?:\.\d+)?)[ \t]*(k|ki|m|mi)?b?)?', re.IGNORECASE) def get_chunk_size(match) -> int: diff --git a/scripts/welcome.py b/scripts/welcome.py index 430da3a6cd..4d86298b0a 100755 --- a/scripts/welcome.py +++ b/scripts/welcome.py @@ -752,7 +752,7 @@ def define_sign(self, force: bool = False) -> list[str]: return self._random_signature sign_text = '' - creg = re.compile(r'^\* ?(.*?)$', re.M) + creg = re.compile(r'^\* ?(.*?)$', re.MULTILINE) if not globalvar.sign_file_name: sign_page_name = i18n.translate(self.site, RANDOM_SIGN) if not sign_page_name: From 32ea148d268f27db48dbf5536552539ea7c3ed45 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sun, 3 Nov 2024 14:01:00 +0100 Subject: [PATCH 18/95] [bugfix] import tkinter and there objects within try statement Some Python distributions have tkinter but the underlying _tkinter implementation is missing. Thus just import tkinter does not raise the exception. Bug: T378894 Change-Id: I5d2a37bb93fcb4b45f0dff323f96fd9fa8722d88 --- pywikibot/userinterfaces/gui.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/pywikibot/userinterfaces/gui.py b/pywikibot/userinterfaces/gui.py index 7cd42132e8..64bcbcfaf0 100644 --- a/pywikibot/userinterfaces/gui.py +++ b/pywikibot/userinterfaces/gui.py @@ -23,28 +23,29 @@ from pywikibot.tools import PYTHON_VERSION +# Some Python distributions have tkinter but the underlying _tkinter +# implementation is missing. Thus just import tkinter does not raise +# the exception. Therefore try to import _tkinter. +# Note: idlelib also needs tkinter. try: - import idlelib + import _tkinter # noqa: F401 except ImportError as e: - idlelib = e - ConfigDialog = ReplaceDialog = SearchDialog = object() - idleConf = MultiCallCreator = object() # noqa: N816 + idlelib = tkinter = e + Frame = simpledialog = ScrolledText = object + ConfigDialog = ReplaceDialog = SearchDialog = object + idleConf = MultiCallCreator = object # noqa: N816 else: + import tkinter + from tkinter import Frame, simpledialog + from tkinter.scrolledtext import ScrolledText + + import idlelib from idlelib import replace as ReplaceDialog # noqa: N812 from idlelib import search as SearchDialog # noqa: N812 from idlelib.config import idleConf from idlelib.configdialog import ConfigDialog from idlelib.multicall import MultiCallCreator -try: - import tkinter -except ImportError as e: - tkinter = e - Frame = simpledialog = ScrolledText = object -else: - from tkinter import Frame, simpledialog - from tkinter.scrolledtext import ScrolledText - __all__ = ('EditBoxWindow', 'TextEditor', 'Tkdialog') From 2f678b61e6b8c032c3a5f3e4e6a600f6284d9ac7 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sun, 3 Nov 2024 16:15:12 +0100 Subject: [PATCH 19/95] [doc] Update ROADMAP.rst and CHANGELOG.rst Change-Id: I3cab77c8b96abf2571e1332555663f6ce729bebf --- HISTORY.rst | 2 +- ROADMAP.rst | 3 ++- docs/api_ref/pywikibot.site.rst | 2 +- scripts/CHANGELOG.rst | 8 ++++++++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 79ec950ed0..0c5a7b7515 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -666,7 +666,7 @@ Release History * Raise InvalidTitleError instead of unspecific ValueError in ProofreadPage (:phab:`T308016`) * Preload pages if GeneratorFactory.articlenotfilter_list is not empty; also set attribute ``is_preloading``. * ClaimCollection.toJSON() should not ignore new claim (:phab:`T308245`) -* use linktrail via siteinfo and remove `update_linkrtrails` maintenance script +* use linktrail via siteinfo and remove `update_linktrails` maintenance script * Print counter statistic for all counters (:phab:`T307834`) * Use proofreadpagesinindex query module * Prioritize -namespaces options in `pagegenerators.handle_args` (:phab:`T222519`) diff --git a/ROADMAP.rst b/ROADMAP.rst index d4206323d8..6d48c5d260 100644 --- a/ROADMAP.rst +++ b/ROADMAP.rst @@ -1,7 +1,8 @@ Current Release Changes ======================= -* (no changes yet) +* Extract :meth:`APISite.linktrail()<pywikibot.site._apisite.APISite.linktrail>` + for hr-wiki (:phab:`T378787`) Current Deprecations diff --git a/docs/api_ref/pywikibot.site.rst b/docs/api_ref/pywikibot.site.rst index 347430b6f8..1d8b3a2d6b 100644 --- a/docs/api_ref/pywikibot.site.rst +++ b/docs/api_ref/pywikibot.site.rst @@ -27,7 +27,7 @@ .. seealso:: :meth:`family.Family.linktrail` .. deprecated:: 7.3 Only supported as :class:`APISite<pywikibot.site._apisite.APISite>` - method. Use :meth:`APISite.linktrail + method. Use :meth:`APISite.linktrail() <pywikibot.site._apisite.APISite.linktrail>` :rtype: str diff --git a/scripts/CHANGELOG.rst b/scripts/CHANGELOG.rst index cf28d26c0c..e8953aa0d2 100644 --- a/scripts/CHANGELOG.rst +++ b/scripts/CHANGELOG.rst @@ -1,6 +1,14 @@ Scripts Changelog ================= +9.6.0 +----- + +replace +^^^^^^^ + +* Strip newlines from pairsfile lines (:phab:`T378647`) + 9.5.0 ----- From 84c616aaea847f1a66c1aacb9251109dbf607c88 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sun, 3 Nov 2024 17:50:41 +0100 Subject: [PATCH 20/95] Deprecate dataextend.py script Bug: T377066 Change-Id: Ida64477422d664ccdf87c904ef404b778198035c --- docs/scripts/archive.rst | 11 ++++++----- scripts/CHANGELOG.rst | 5 +++++ scripts/dataextend.py | 2 ++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/scripts/archive.rst b/docs/scripts/archive.rst index 577986095b..5630b3a564 100644 --- a/docs/scripts/archive.rst +++ b/docs/scripts/archive.rst @@ -3,12 +3,13 @@ Outdated core scripts ********************* This list contains outdated scripts from :term:`core` banch which -aren't supported any longer. They are either archived or deleted. +aren't supported any longer. They aredeleted from repository. -Feel free to reactivate any script at any time by creating a Phabricator -task: :phab:`Recovery request -<maniphest/task/edit/form/1/?projects=pywikibot,pywikibot-scripts&title=Recover -Pywikibot%20script:%20>` +.. hint:: + Feel free to reactivate any script at any time by creating a + Phabricator task: :phab:`Recovery request + <maniphest/task/edit/form/1/?projects=pywikibot,pywikibot-scripts&title=Recover + Pywikibot%20script:%20>` .. seealso:: :ref:`Outdated compat scripts` diff --git a/scripts/CHANGELOG.rst b/scripts/CHANGELOG.rst index e8953aa0d2..7503323781 100644 --- a/scripts/CHANGELOG.rst +++ b/scripts/CHANGELOG.rst @@ -4,6 +4,11 @@ Scripts Changelog 9.6.0 ----- +dataextend +^^^^^^^^^^ + +* The script is deprecated and will be removed from script package with Pywikibot 10. + replace ^^^^^^^ diff --git a/scripts/dataextend.py b/scripts/dataextend.py index 2cdabc38c2..76df0c0036 100755 --- a/scripts/dataextend.py +++ b/scripts/dataextend.py @@ -52,6 +52,8 @@ included. .. versionadded:: 7.2 +.. deprecated:: 9.6 + will be removed with Pywikibot 10. """ # # (C) Pywikibot team, 2020-2024 From 9bab7d95dcee31450d9055e5cfb307986e3dc532 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Tue, 5 Nov 2024 12:04:52 +0100 Subject: [PATCH 21/95] [fix] retry SparqlQuery.query on internal server error (500) Bug: T378788 Change-Id: I498d8b306c73edebc2f4c17f200508dde26c428e --- pywikibot/data/sparql.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pywikibot/data/sparql.py b/pywikibot/data/sparql.py index addf5215f1..d217e8b6de 100644 --- a/pywikibot/data/sparql.py +++ b/pywikibot/data/sparql.py @@ -15,7 +15,7 @@ from pywikibot.backports import removeprefix from pywikibot.comms import http from pywikibot.data import WaitingMixin -from pywikibot.exceptions import Error, NoUsernameError +from pywikibot.exceptions import Error, NoUsernameError, ServerError try: @@ -140,6 +140,8 @@ def query(self, query: str, headers: dict[str, str] | None = None): .. versionchanged:: 8.5 :exc:`exceptions.NoUsernameError` is raised if the response looks like the user is not logged in. + .. versionchanged:: 9.6 + retry on internal server error (500). :param query: Query text :raises NoUsernameError: User not logged in @@ -154,9 +156,14 @@ def query(self, query: str, headers: dict[str, str] | None = None): while True: try: self.last_response = http.fetch(url, headers=headers) - break except Timeout: - self.wait() + pass + except ServerError as e: + if not e.unicode.startswith('500'): + raise + else: + break + self.wait() try: return self.last_response.json() From b59e87b70b15c36f527e7c707f731ef0ea806ee1 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Tue, 5 Nov 2024 15:17:19 +0100 Subject: [PATCH 22/95] [test] fix message of TestReplacementsMain.test_pairs_file for pypy3.7 Bug: T379074 Change-Id: I8121dce147507bffc043ac3f51936786d37efb47 --- tests/replacebot_tests.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/replacebot_tests.py b/tests/replacebot_tests.py index 4b22ed1610..193c952af7 100755 --- a/tests/replacebot_tests.py +++ b/tests/replacebot_tests.py @@ -307,8 +307,10 @@ def test_pairs_file(self): """Test handle_pairsfile.""" result = replace.handle_pairsfile('non existing file') self.assertIsNone(result) - self.assertIn("No such file or directory: 'non existing file'", - pywikibot.bot.ui.pop_output()[0]) + + msg = pywikibot.bot.ui.pop_output()[0] + self.assertIn('No such file or directory:', msg) + self.assertIn('non existing file', msg) result = replace.handle_pairsfile('tests/data/pagelist-lines.txt') self.assertIsNone(result) From 40ba9b51c74a8e962933521fd22b2c1a951a5c39 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Tue, 5 Nov 2024 16:21:07 +0100 Subject: [PATCH 23/95] [doc] update ROADMAP.rst and fix spelling mistake Change-Id: If84b9aaebe999539d92d560034592678f8460155 --- ROADMAP.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ROADMAP.rst b/ROADMAP.rst index 6d48c5d260..7bb2dcbebd 100644 --- a/ROADMAP.rst +++ b/ROADMAP.rst @@ -1,6 +1,7 @@ Current Release Changes ======================= +* Retry :meth:`data.sparql.SparqlQuery.query` on internal server error (500) (:phab:`T378788`) * Extract :meth:`APISite.linktrail()<pywikibot.site._apisite.APISite.linktrail>` for hr-wiki (:phab:`T378787`) @@ -75,7 +76,7 @@ Pending removal in Pywikibot 10 * 7.3.0: Old color escape sequences like ``\03{color}`` is deprecated in favour of new color format like <<color>> * 7.3.0: ``linktrail`` method of :class:`family.Family` is deprecated; use :meth:`APISite.linktrail() <pywikibot.site._apisite.APISite.linktrail>` instead -* 7.2.0: Positional arguments *decoder*, *layer* and *newline* for :mod:`logging` functions where dropped; keyword +* 7.2.0: Positional arguments *decoder*, *layer* and *newline* for :mod:`logging` functions were dropped; keyword arguments must be used instead. * 7.2.0: ``tb`` parameter of :func:`exception()<pywikibot.logging.exception>` function was renamed to ``exc_info`` * 7.2.0: XMLDumpOldPageGenerator is deprecated in favour of a ``content`` parameter of From c5a9f64eb5e4794131082ad4c405e3364adb84c5 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Tue, 5 Nov 2024 16:45:44 +0100 Subject: [PATCH 24/95] [doc] Update HISTORY.rst Change-Id: I0615daee67327c7f2dc26cbb797598ef67f1a027 --- HISTORY.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/HISTORY.rst b/HISTORY.rst index 0c5a7b7515..eafd659af3 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,7 @@ Release History 9.5.0 ----- +*30 October 2024* * Add support for tcywikisource and tcywiktionary (:phab:`T378473`, :phab:`T378465`) * i18n-updates From 5eeea21b23dd0ed646ea627b4a31afd040d7a952 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Wed, 6 Nov 2024 13:30:54 +0100 Subject: [PATCH 25/95] [tests] Split wikibase_tests (step1) Change-Id: I566880adfa93ac06e5f8fc3288f617ae22bcdae1 --- tests/{wikibase_tests.py => _wikibase_tests.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{wikibase_tests.py => _wikibase_tests.py} (100%) diff --git a/tests/wikibase_tests.py b/tests/_wikibase_tests.py similarity index 100% rename from tests/wikibase_tests.py rename to tests/_wikibase_tests.py From 27e4c89d730f2cd6e8f90d7d87fb4940d1c61f30 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Wed, 6 Nov 2024 13:33:32 +0100 Subject: [PATCH 26/95] [tests] Split wikibase_tests (step2) Change-Id: I3e80e453c8d5998ae1bc48e160f7814620218265 --- .../{_wikibase_tests.py => wbtypes_tests.py} | 0 tests/wikibase_tests.py | 2517 +++++++++++++++++ 2 files changed, 2517 insertions(+) rename tests/{_wikibase_tests.py => wbtypes_tests.py} (100%) mode change 100755 => 100644 create mode 100755 tests/wikibase_tests.py diff --git a/tests/_wikibase_tests.py b/tests/wbtypes_tests.py old mode 100755 new mode 100644 similarity index 100% rename from tests/_wikibase_tests.py rename to tests/wbtypes_tests.py diff --git a/tests/wikibase_tests.py b/tests/wikibase_tests.py new file mode 100755 index 0000000000..413ab3b363 --- /dev/null +++ b/tests/wikibase_tests.py @@ -0,0 +1,2517 @@ +#!/usr/bin/env python3 +"""Tests for the Wikidata parts of the page module.""" +# +# (C) Pywikibot team, 2008-2024 +# +# Distributed under the terms of the MIT license. +# +from __future__ import annotations + +import copy +import datetime +import json +import operator +import unittest +from contextlib import suppress +from decimal import Decimal + +import pywikibot +from pywikibot import pagegenerators +from pywikibot.exceptions import ( + InvalidTitleError, + IsNotRedirectPageError, + IsRedirectPageError, + NoPageError, + UnknownExtensionError, + WikiBaseError, +) +from pywikibot.page import ItemPage, Page, PropertyPage, WikibasePage +from pywikibot.site import Namespace, NamespacesDict +from pywikibot.tools import MediaWikiVersion, suppress_warnings +from tests import WARN_SITE_CODE, join_pages_path +from tests.aspects import TestCase, WikidataTestCase +from tests.basepage import ( + BasePageLoadRevisionsCachingTestBase, + BasePageMethodsTestBase, +) + + +# fetch a page which is very likely to be unconnected, which doesn't have +# a generator, and unit tests may be used to test old versions of pywikibot +def _get_test_unconnected_page(site): + """Get unconnected page from site for tests.""" + gen = pagegenerators.NewpagesPageGenerator(site=site, total=10, + namespaces=[1]) + for page in gen: + if not page.properties().get('wikibase_item'): + return page + return None # pragma: no cover + + +class WbRepresentationTestCase(WikidataTestCase): + + """Test methods inherited or extended from _WbRepresentation.""" + + def _test_hashable(self, representation): + """Test that the representation is hashable.""" + list_of_dupes = [representation, representation] + self.assertLength(set(list_of_dupes), 1) + + +class TestLoadRevisionsCaching(BasePageLoadRevisionsCachingTestBase, + WikidataTestCase): + + """Test site.loadrevisions() caching.""" + + def setUp(self): + """Setup test.""" + self._page = ItemPage(self.get_repo(), 'Q15169668') + super().setUp() + + def test_page_text(self): + """Test site.loadrevisions() with Page.text.""" + with suppress_warnings(WARN_SITE_CODE, category=UserWarning): + self._test_page_text() + + +class TestGeneral(WikidataTestCase): + + """General Wikibase tests.""" + + @classmethod + def setUpClass(cls): + """Setup test class.""" + super().setUpClass() + enwiki = pywikibot.Site('en', 'wikipedia') + cls.mainpage = pywikibot.Page(pywikibot.page.Link('Main Page', enwiki)) + + def testWikibase(self): + """Wikibase tests.""" + repo = self.get_repo() + item_namespace = repo.namespaces[0] + self.assertEqual(item_namespace.defaultcontentmodel, 'wikibase-item') + item = ItemPage.fromPage(self.mainpage) + self.assertIsInstance(item, ItemPage) + self.assertEqual(item.getID(), 'Q5296') + self.assertEqual(item.title(), 'Q5296') + self.assertIn('en', item.labels) + self.assertTrue( + item.labels['en'].lower().endswith('main page'), + msg=f"\nitem.labels['en'] of item Q5296 is {item.labels['en']!r}") + self.assertIn('en', item.aliases) + self.assertIn('home page', (a.lower() for a in item.aliases['en'])) + self.assertEqual(item.namespace(), 0) + item2 = ItemPage(repo, 'q5296') + self.assertEqual(item2.getID(), 'Q5296') + item2.get() + self.assertTrue(item2.labels['en'].lower().endswith('main page')) + prop = PropertyPage(repo, 'Property:P21') + self.assertEqual(prop.type, 'wikibase-item') + self.assertEqual(prop.namespace(), 120) + claim = pywikibot.Claim(repo, 'p21') + regex = r' is not type .+\.$' + with self.assertRaisesRegex(ValueError, regex): + claim.setTarget(value='test') + claim.setTarget(ItemPage(repo, 'q1')) + self.assertEqual(claim._formatValue(), {'entity-type': 'item', + 'numeric-id': 1}) + + def test_cmp(self): + """Test WikibasePage comparison.""" + self.assertEqual(ItemPage.fromPage(self.mainpage), + ItemPage(self.get_repo(), 'q5296')) + + +class TestWikibaseCoordinate(WbRepresentationTestCase): + + """Test Wikibase Coordinate data type.""" + + dry = True + + def test_Coordinate_WbRepresentation_methods(self): + """Test inherited or extended methods from _WbRepresentation.""" + repo = self.get_repo() + coord = pywikibot.Coordinate( + site=repo, lat=12.0, lon=13.0, precision=0, + globe='moon') + self._test_hashable(coord) + + def test_Coordinate_dim(self): + """Test Coordinate dimension.""" + repo = self.get_repo() + x = pywikibot.Coordinate(site=repo, lat=12.0, lon=13.0, precision=5.0) + self.assertEqual(x.precisionToDim(), 544434) + self.assertIsInstance(x.precisionToDim(), int) + y = pywikibot.Coordinate(site=repo, lat=12.0, lon=13.0, dim=54444) + self.assertEqual(y.precision, 0.500005084017101) + self.assertIsInstance(y.precision, float) + z = pywikibot.Coordinate(site=repo, lat=12.0, lon=13.0) + regex = r'^No values set for dim or precision$' + with self.assertRaisesRegex(ValueError, regex): + z.precisionToDim() + + def test_Coordinate_plain_globe(self): + """Test setting Coordinate globe from a plain-text value.""" + repo = self.get_repo() + coord = pywikibot.Coordinate( + site=repo, lat=12.0, lon=13.0, precision=0, + globe='moon') + self.assertEqual(coord.toWikibase(), + {'latitude': 12.0, 'longitude': 13.0, + 'altitude': None, 'precision': 0, + 'globe': 'http://www.wikidata.org/entity/Q405'}) + + def test_Coordinate_entity_uri_globe(self): + """Test setting Coordinate globe from an entity uri.""" + repo = self.get_repo() + coord = pywikibot.Coordinate( + site=repo, lat=12.0, lon=13.0, precision=0, + globe_item='http://www.wikidata.org/entity/Q123') + self.assertEqual(coord.toWikibase(), + {'latitude': 12.0, 'longitude': 13.0, + 'altitude': None, 'precision': 0, + 'globe': 'http://www.wikidata.org/entity/Q123'}) + + +class TestWikibaseCoordinateNonDry(WbRepresentationTestCase): + + """Test Wikibase Coordinate data type (non-dry). + + These can be moved to TestWikibaseCoordinate once DrySite has been bumped + to the appropriate version. + """ + + def test_Coordinate_item_globe(self): + """Test setting Coordinate globe from an ItemPage.""" + repo = self.get_repo() + coord = pywikibot.Coordinate( + site=repo, lat=12.0, lon=13.0, precision=0, + globe_item=ItemPage(repo, 'Q123')) + self.assertEqual(coord.toWikibase(), + {'latitude': 12.0, 'longitude': 13.0, + 'altitude': None, 'precision': 0, + 'globe': 'http://www.wikidata.org/entity/Q123'}) + + def test_Coordinate_get_globe_item_from_uri(self): + """Test getting globe item from Coordinate with entity uri globe.""" + repo = self.get_repo() + q = pywikibot.Coordinate( + site=repo, lat=12.0, lon=13.0, precision=0, + globe_item='http://www.wikidata.org/entity/Q123') + self.assertEqual(q.get_globe_item(), ItemPage(repo, 'Q123')) + + def test_Coordinate_get_globe_item_from_itempage(self): + """Test getting globe item from Coordinate with ItemPage globe.""" + repo = self.get_repo() + globe = ItemPage(repo, 'Q123') + q = pywikibot.Coordinate( + site=repo, lat=12.0, lon=13.0, precision=0, globe_item=globe) + self.assertEqual(q.get_globe_item(), ItemPage(repo, 'Q123')) + + def test_Coordinate_get_globe_item_from_plain_globe(self): + """Test getting globe item from Coordinate with plain text globe.""" + repo = self.get_repo() + q = pywikibot.Coordinate( + site=repo, lat=12.0, lon=13.0, precision=0, globe='moon') + self.assertEqual(q.get_globe_item(), ItemPage(repo, 'Q405')) + + def test_Coordinate_get_globe_item_provide_repo(self): + """Test getting globe item from Coordinate, providing repo.""" + repo = self.get_repo() + q = pywikibot.Coordinate( + site=repo, lat=12.0, lon=13.0, precision=0, + globe_item='http://www.wikidata.org/entity/Q123') + self.assertEqual(q.get_globe_item(repo), ItemPage(repo, 'Q123')) + + def test_Coordinate_get_globe_item_different_repo(self): + """Test getting globe item in different repo from Coordinate.""" + repo = self.get_repo() + test_repo = pywikibot.Site('test', 'wikidata') + q = pywikibot.Coordinate( + site=repo, lat=12.0, lon=13.0, precision=0, + globe_item='http://test.wikidata.org/entity/Q123') + self.assertEqual(q.get_globe_item(test_repo), + ItemPage(test_repo, 'Q123')) + + def test_Coordinate_equality(self): + """Test Coordinate equality with different globe representations.""" + repo = self.get_repo() + a = pywikibot.Coordinate( + site=repo, lat=12.0, lon=13.0, precision=0.1, + globe='moon') + b = pywikibot.Coordinate( + site=repo, lat=12.0, lon=13.0, precision=0.1, + globe_item='http://www.wikidata.org/entity/Q405') + c = pywikibot.Coordinate( + site=repo, lat=12.0, lon=13.0, precision=0.1, + globe_item=ItemPage(repo, 'Q405')) + d = pywikibot.Coordinate( + site=repo, lat=12.0, lon=13.0, precision=0.1, + globe_item='http://test.wikidata.org/entity/Q405') + self.assertEqual(a, b) + self.assertEqual(b, c) + self.assertEqual(c, a) + self.assertNotEqual(a, d) + self.assertNotEqual(b, d) + self.assertNotEqual(c, d) + + +class TestWbTime(WbRepresentationTestCase): + + """Test Wikibase WbTime data type.""" + + dry = True + + def test_WbTime_WbRepresentation_methods(self): + """Test inherited or extended methods from _WbRepresentation.""" + repo = self.get_repo() + t = pywikibot.WbTime(site=repo, year=2010, month=0, day=0, hour=12, + minute=43) + self._test_hashable(t) + + def test_WbTime_timestr(self): + """Test timestr functions of WbTime.""" + repo = self.get_repo() + t = pywikibot.WbTime(site=repo, year=2010, month=0, day=0, hour=12, + minute=43) + self.assertEqual(t.toTimestr(), '+00000002010-00-00T12:43:00Z') + self.assertEqual(t.toTimestr(force_iso=True), '+2010-01-01T12:43:00Z') + + t = pywikibot.WbTime(site=repo, year=2010, hour=12, minute=43) + self.assertEqual(t.toTimestr(), '+00000002010-01-01T12:43:00Z') + self.assertEqual(t.toTimestr(force_iso=True), '+2010-01-01T12:43:00Z') + + t = pywikibot.WbTime(site=repo, year=-2010, hour=12, minute=43) + self.assertEqual(t.toTimestr(), '-00000002010-01-01T12:43:00Z') + self.assertEqual(t.toTimestr(force_iso=True), '-2010-01-01T12:43:00Z') + + def test_WbTime_fromTimestr(self): + """Test WbTime creation from UTC date/time string.""" + repo = self.get_repo() + t = pywikibot.WbTime.fromTimestr('+00000002010-01-01T12:43:00Z', + site=repo) + self.assertEqual(t, pywikibot.WbTime(site=repo, year=2010, hour=12, + minute=43, precision=14)) + + def test_WbTime_zero_month(self): + """Test WbTime creation from date/time string with zero month.""" + # ensures we support formats in T123888 / T107870 + repo = self.get_repo() + t = pywikibot.WbTime.fromTimestr('+00000002010-00-00T12:43:00Z', + site=repo) + self.assertEqual(t, pywikibot.WbTime(site=repo, year=2010, month=0, + day=0, hour=12, minute=43, + precision=14)) + + def test_WbTime_skip_params_precision(self): + """Test skipping units (such as day, month) when creating WbTimes.""" + repo = self.get_repo() + t = pywikibot.WbTime(year=2020, day=2, site=repo) + self.assertEqual(t, pywikibot.WbTime(year=2020, month=1, day=2, + site=repo)) + self.assertEqual(t.precision, pywikibot.WbTime.PRECISION['day']) + t2 = pywikibot.WbTime(year=2020, hour=5, site=repo) + self.assertEqual(t2, pywikibot.WbTime(year=2020, month=1, day=1, + hour=5, site=repo)) + self.assertEqual(t2.precision, pywikibot.WbTime.PRECISION['hour']) + t3 = pywikibot.WbTime(year=2020, minute=5, site=repo) + self.assertEqual(t3, pywikibot.WbTime(year=2020, month=1, day=1, + hour=0, minute=5, site=repo)) + self.assertEqual(t3.precision, pywikibot.WbTime.PRECISION['minute']) + t4 = pywikibot.WbTime(year=2020, second=5, site=repo) + self.assertEqual(t4, pywikibot.WbTime(year=2020, month=1, day=1, + hour=0, minute=0, second=5, + site=repo)) + self.assertEqual(t4.precision, pywikibot.WbTime.PRECISION['second']) + t5 = pywikibot.WbTime(year=2020, month=2, hour=5, site=repo) + self.assertEqual(t5, pywikibot.WbTime(year=2020, month=2, day=1, + hour=5, site=repo)) + self.assertEqual(t5.precision, pywikibot.WbTime.PRECISION['hour']) + t6 = pywikibot.WbTime(year=2020, month=2, minute=5, site=repo) + self.assertEqual(t6, pywikibot.WbTime(year=2020, month=2, day=1, + hour=0, minute=5, site=repo)) + self.assertEqual(t6.precision, pywikibot.WbTime.PRECISION['minute']) + t7 = pywikibot.WbTime(year=2020, month=2, second=5, site=repo) + self.assertEqual(t7, pywikibot.WbTime(year=2020, month=2, day=1, + hour=0, minute=0, second=5, + site=repo)) + self.assertEqual(t7.precision, pywikibot.WbTime.PRECISION['second']) + t8 = pywikibot.WbTime(year=2020, day=2, hour=5, site=repo) + self.assertEqual(t8, pywikibot.WbTime(year=2020, month=1, day=2, + hour=5, site=repo)) + self.assertEqual(t8.precision, pywikibot.WbTime.PRECISION['hour']) + t9 = pywikibot.WbTime(year=2020, month=3, day=2, minute=5, site=repo) + self.assertEqual(t9, pywikibot.WbTime(year=2020, month=3, day=2, + hour=0, minute=5, site=repo)) + self.assertEqual(t9.precision, pywikibot.WbTime.PRECISION['minute']) + + def test_WbTime_normalization(self): + """Test WbTime normalization.""" + repo = self.get_repo() + # flake8 is being annoying, so to reduce line length, I'll make + # some aliases here + decade = pywikibot.WbTime.PRECISION['decade'] + century = pywikibot.WbTime.PRECISION['century'] + millenia = pywikibot.WbTime.PRECISION['millenia'] + t = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, + minute=43, second=12) + t2 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, + minute=43, second=12, + precision=pywikibot.WbTime.PRECISION['second']) + t3 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, + minute=43, second=12, + precision=pywikibot.WbTime.PRECISION['minute']) + t4 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, + minute=43, second=12, + precision=pywikibot.WbTime.PRECISION['hour']) + t5 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, + minute=43, second=12, + precision=pywikibot.WbTime.PRECISION['day']) + t6 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, + minute=43, second=12, + precision=pywikibot.WbTime.PRECISION['month']) + t7 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, + minute=43, second=12, + precision=pywikibot.WbTime.PRECISION['year']) + t8 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, + minute=43, second=12, + precision=decade) + t9 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, + minute=43, second=12, + precision=century) + t10 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, + minute=43, second=12, + precision=millenia) + t11 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, + minute=43, second=12, timezone=-300, + precision=pywikibot.WbTime.PRECISION['day']) + t12 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, + minute=43, second=12, timezone=300, + precision=pywikibot.WbTime.PRECISION['day']) + t13 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, + minute=43, second=12, timezone=-300, + precision=pywikibot.WbTime.PRECISION['hour']) + self.assertEqual(t.normalize(), t) + self.assertEqual(t2.normalize(), t.normalize()) + self.assertEqual(t3.normalize(), + pywikibot.WbTime(site=repo, year=2010, month=1, + day=1, hour=12, minute=43)) + self.assertEqual(t4.normalize(), + pywikibot.WbTime(site=repo, year=2010, + month=1, day=1, hour=12)) + self.assertEqual(t5.normalize(), + pywikibot.WbTime(site=repo, year=2010, + month=1, day=1)) + self.assertEqual(t6.normalize(), + pywikibot.WbTime(site=repo, year=2010, + month=1)) + self.assertEqual( + t7.normalize(), pywikibot.WbTime(site=repo, year=2010)) + self.assertEqual(t8.normalize(), + pywikibot.WbTime(site=repo, year=2010, + precision=decade)) + self.assertEqual(t9.normalize(), + pywikibot.WbTime(site=repo, year=2100, + precision=century)) + self.assertEqual(t9.normalize(), + pywikibot.WbTime(site=repo, year=2010, + precision=century).normalize()) + self.assertEqual(t10.normalize(), + pywikibot.WbTime(site=repo, year=3000, + precision=millenia)) + self.assertEqual(t10.normalize(), + pywikibot.WbTime(site=repo, year=2010, + precision=millenia).normalize()) + t11_normalized = t11.normalize() + t12_normalized = t12.normalize() + self.assertEqual(t11_normalized.timezone, 0) + self.assertEqual(t12_normalized.timezone, 0) + self.assertNotEqual(t11, t12) + self.assertEqual(t11_normalized, t12_normalized) + self.assertEqual(t13.normalize().timezone, -300) + + def test_WbTime_normalization_very_low_precision(self): + """Test WbTime normalization with very low precision.""" + repo = self.get_repo() + # flake8 is being annoying, so to reduce line length, I'll make + # some aliases here + year_10000 = pywikibot.WbTime.PRECISION['10000'] + year_100000 = pywikibot.WbTime.PRECISION['100000'] + year_1000000 = pywikibot.WbTime.PRECISION['1000000'] + year_10000000 = pywikibot.WbTime.PRECISION['10000000'] + year_100000000 = pywikibot.WbTime.PRECISION['100000000'] + year_1000000000 = pywikibot.WbTime.PRECISION['1000000000'] + t = pywikibot.WbTime(site=repo, year=-3124684989, + precision=year_10000) + t2 = pywikibot.WbTime(site=repo, year=-3124684989, + precision=year_100000) + t3 = pywikibot.WbTime(site=repo, year=-3124684989, + precision=year_1000000) + t4 = pywikibot.WbTime(site=repo, year=-3124684989, + precision=year_10000000) + t5 = pywikibot.WbTime(site=repo, year=-3124684989, + precision=year_100000000) + t6 = pywikibot.WbTime(site=repo, year=-3124684989, + precision=year_1000000000) + self.assertEqual(t.normalize(), + pywikibot.WbTime(site=repo, year=-3124680000, + precision=year_10000)) + self.assertEqual(t2.normalize(), + pywikibot.WbTime(site=repo, year=-3124700000, + precision=year_100000)) + self.assertEqual(t3.normalize(), + pywikibot.WbTime(site=repo, year=-3125000000, + precision=year_1000000)) + self.assertEqual(t4.normalize(), + pywikibot.WbTime(site=repo, year=-3120000000, + precision=year_10000000)) + self.assertEqual(t5.normalize(), + pywikibot.WbTime(site=repo, year=-3100000000, + precision=year_100000000)) + self.assertEqual(t6.normalize(), + pywikibot.WbTime(site=repo, year=-3000000000, + precision=year_1000000000)) + + def test_WbTime_timestamp(self): + """Test timestamp functions of WbTime.""" + repo = self.get_repo() + timestamp = pywikibot.Timestamp.fromISOformat('2010-01-01T12:43:00Z') + t = pywikibot.WbTime(site=repo, year=2010, month=0, day=0, hour=12, + minute=43) + self.assertEqual(t.toTimestamp(), timestamp) + + # Roundtrip fails as Timestamp and WbTime interpret month=0 differently + self.assertNotEqual( + t, pywikibot.WbTime.fromTimestamp(timestamp, site=repo)) + + t = pywikibot.WbTime(site=repo, year=2010, hour=12, minute=43) + self.assertEqual(t.toTimestamp(), timestamp) + + t = pywikibot.WbTime(site=repo, year=-2010, hour=12, minute=43) + regex = r'^You cannot turn BC dates into a Timestamp$' + with self.assertRaisesRegex(ValueError, regex): + t.toTimestamp() + + t = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, + minute=43, second=0) + self.assertEqual(t.toTimestamp(), timestamp) + self.assertEqual( + t, pywikibot.WbTime.fromTimestamp(timestamp, site=repo)) + timezone = datetime.timezone(datetime.timedelta(hours=-5)) + ts = pywikibot.Timestamp(2020, 1, 1, 12, 43, 0, tzinfo=timezone) + t = pywikibot.WbTime.fromTimestamp(ts, site=repo, copy_timezone=True) + self.assertEqual(t.timezone, -5 * 60) + t = pywikibot.WbTime.fromTimestamp(ts, site=repo, copy_timezone=True, + timezone=60) + self.assertEqual(t.timezone, 60) + + ts1 = pywikibot.Timestamp( + year=2022, month=12, day=21, hour=13, + tzinfo=datetime.timezone(datetime.timedelta(hours=-5))) + t1 = pywikibot.WbTime.fromTimestamp(ts1, timezone=-300, site=repo) + self.assertIsNotNone(t1.toTimestamp(timezone_aware=True).tzinfo) + self.assertIsNone(t1.toTimestamp(timezone_aware=False).tzinfo) + self.assertEqual(t1.toTimestamp(timezone_aware=True), ts1) + self.assertNotEqual(t1.toTimestamp(timezone_aware=False), ts1) + + def test_WbTime_errors(self): + """Test WbTime precision errors.""" + repo = self.get_repo() + regex = r'^no year given$' + with self.assertRaisesRegex(ValueError, regex): + pywikibot.WbTime(site=repo, precision=15) + with self.assertRaisesRegex(ValueError, regex): + pywikibot.WbTime(site=repo, precision='invalid_precision') + regex = r'^Invalid precision: "15"$' + with self.assertRaisesRegex(ValueError, regex): + pywikibot.WbTime(site=repo, year=2020, precision=15) + regex = r'^Invalid precision: "invalid_precision"$' + with self.assertRaisesRegex(ValueError, regex): + pywikibot.WbTime(site=repo, year=2020, + precision='invalid_precision') + + def test_comparison(self): + """Test WbTime comparison.""" + repo = self.get_repo() + t1 = pywikibot.WbTime(site=repo, year=2010, hour=12, minute=43) + t2 = pywikibot.WbTime(site=repo, year=-2005, hour=16, minute=45) + self.assertEqual(t1.precision, pywikibot.WbTime.PRECISION['minute']) + self.assertEqual(t1, t1) + self.assertGreaterEqual(t1, t1) + self.assertGreaterEqual(t1, t2) + self.assertGreater(t1, t2) + self.assertEqual(t1.year, 2010) + self.assertEqual(t2.year, -2005) + self.assertEqual(t1.month, 1) + self.assertEqual(t2.month, 1) + self.assertEqual(t1.day, 1) + self.assertEqual(t2.day, 1) + self.assertEqual(t1.hour, 12) + self.assertEqual(t2.hour, 16) + self.assertEqual(t1.minute, 43) + self.assertEqual(t2.minute, 45) + self.assertEqual(t1.second, 0) + self.assertEqual(t2.second, 0) + self.assertEqual(t1.toTimestr(), '+00000002010-01-01T12:43:00Z') + self.assertEqual(t2.toTimestr(), '-00000002005-01-01T16:45:00Z') + self.assertRaises(ValueError, pywikibot.WbTime, site=repo, + precision=15) + self.assertRaises(ValueError, pywikibot.WbTime, site=repo, + precision='invalid_precision') + self.assertIsInstance(t1.toTimestamp(), pywikibot.Timestamp) + self.assertRaises(ValueError, t2.toTimestamp) + + def test_comparison_types(self): + """Test WbTime comparison with different types.""" + repo = self.get_repo() + t1 = pywikibot.WbTime(site=repo, year=2010, hour=12, minute=43) + t2 = pywikibot.WbTime(site=repo, year=-2005, hour=16, minute=45) + self.assertGreater(t1, t2) + self.assertRaises(TypeError, operator.lt, t1, 5) + self.assertRaises(TypeError, operator.gt, t1, 5) + self.assertRaises(TypeError, operator.le, t1, 5) + self.assertRaises(TypeError, operator.ge, t1, 5) + + def test_comparison_timezones(self): + """Test comparisons with timezones.""" + repo = self.get_repo() + ts1 = pywikibot.Timestamp( + year=2022, month=12, day=21, hour=13, + tzinfo=datetime.timezone(datetime.timedelta(hours=-5))) + ts2 = pywikibot.Timestamp( + year=2022, month=12, day=21, hour=17, + tzinfo=datetime.timezone.utc) + self.assertGreater(ts1.timestamp(), ts2.timestamp()) + + t1 = pywikibot.WbTime.fromTimestamp(ts1, timezone=-300, site=repo) + t2 = pywikibot.WbTime.fromTimestamp(ts2, timezone=0, site=repo) + self.assertGreater(t1, t2) + + def test_comparison_timezones_equal(self): + """Test when two WbTime's have equal instants but not the same tz.""" + repo = self.get_repo() + ts1 = pywikibot.Timestamp( + year=2023, month=12, day=21, hour=13, + tzinfo=datetime.timezone(datetime.timedelta(hours=-5))) + ts2 = pywikibot.Timestamp( + year=2023, month=12, day=21, hour=18, + tzinfo=datetime.timezone.utc) + self.assertEqual(ts1.timestamp(), ts2.timestamp()) + + t1 = pywikibot.WbTime.fromTimestamp(ts1, timezone=-300, site=repo) + t2 = pywikibot.WbTime.fromTimestamp(ts2, timezone=0, site=repo) + self.assertGreaterEqual(t1, t2) + self.assertGreaterEqual(t2, t1) + self.assertNotEqual(t1, t2) + self.assertNotEqual(t2, t1) + # Ignore H205: We specifically want to test the operator + self.assertFalse(t1 > t2) # noqa: H205 + self.assertFalse(t2 > t1) # noqa: H205 + self.assertFalse(t1 < t2) # noqa: H205 + self.assertFalse(t2 < t1) # noqa: H205 + + def test_comparison_equal_instant(self): + """Test the equal_instant method.""" + repo = self.get_repo() + + ts1 = pywikibot.Timestamp( + year=2023, month=12, day=21, hour=13, + tzinfo=datetime.timezone(datetime.timedelta(hours=-5))) + ts2 = pywikibot.Timestamp( + year=2023, month=12, day=21, hour=18, + tzinfo=datetime.timezone.utc) + ts3 = pywikibot.Timestamp( + year=2023, month=12, day=21, hour=19, + tzinfo=datetime.timezone(datetime.timedelta(hours=1))) + ts4 = pywikibot.Timestamp( + year=2023, month=12, day=21, hour=13, + tzinfo=datetime.timezone(datetime.timedelta(hours=-6))) + + self.assertEqual(ts1.timestamp(), ts2.timestamp()) + self.assertEqual(ts1.timestamp(), ts3.timestamp()) + self.assertEqual(ts2.timestamp(), ts3.timestamp()) + self.assertNotEqual(ts1.timestamp(), ts4.timestamp()) + self.assertNotEqual(ts2.timestamp(), ts4.timestamp()) + self.assertNotEqual(ts3.timestamp(), ts4.timestamp()) + + t1 = pywikibot.WbTime.fromTimestamp(ts1, timezone=-300, site=repo) + t2 = pywikibot.WbTime.fromTimestamp(ts2, timezone=0, site=repo) + t3 = pywikibot.WbTime.fromTimestamp(ts3, timezone=60, site=repo) + t4 = pywikibot.WbTime.fromTimestamp(ts4, timezone=-360, site=repo) + + self.assertTrue(t1.equal_instant(t2)) + self.assertTrue(t1.equal_instant(t3)) + self.assertTrue(t2.equal_instant(t3)) + self.assertFalse(t1.equal_instant(t4)) + self.assertFalse(t2.equal_instant(t4)) + self.assertFalse(t3.equal_instant(t4)) + + +class TestWbQuantity(WbRepresentationTestCase): + + """Test Wikibase WbQuantity data type.""" + + dry = True + + def test_WbQuantity_WbRepresentation_methods(self): + """Test inherited or extended methods from _WbRepresentation.""" + repo = self.get_repo() + q = pywikibot.WbQuantity(amount=1234, error=1, site=repo) + self._test_hashable(q) + + def test_WbQuantity_integer(self): + """Test WbQuantity for integer value.""" + repo = self.get_repo() + q = pywikibot.WbQuantity(amount=1234, error=1, site=repo) + self.assertEqual(q.toWikibase(), + {'amount': '+1234', 'lowerBound': '+1233', + 'upperBound': '+1235', 'unit': '1'}) + q = pywikibot.WbQuantity(amount=5, error=(2, 3), site=repo) + self.assertEqual(q.toWikibase(), + {'amount': '+5', 'lowerBound': '+2', + 'upperBound': '+7', 'unit': '1'}) + q = pywikibot.WbQuantity(amount=0, error=(0, 0), site=repo) + self.assertEqual(q.toWikibase(), + {'amount': '+0', 'lowerBound': '+0', + 'upperBound': '+0', 'unit': '1'}) + q = pywikibot.WbQuantity(amount=-5, error=(2, 3), site=repo) + self.assertEqual(q.toWikibase(), + {'amount': '-5', 'lowerBound': '-8', + 'upperBound': '-3', 'unit': '1'}) + + def test_WbQuantity_float_27(self): + """Test WbQuantity for float value.""" + repo = self.get_repo() + q = pywikibot.WbQuantity(amount=0.044405586, error=0.0, site=repo) + q_dict = {'amount': '+0.044405586', 'lowerBound': '+0.044405586', + 'upperBound': '+0.044405586', 'unit': '1'} + self.assertEqual(q.toWikibase(), q_dict) + + def test_WbQuantity_scientific(self): + """Test WbQuantity for scientific notation.""" + repo = self.get_repo() + q = pywikibot.WbQuantity(amount='1.3e-13', error='1e-14', site=repo) + q_dict = {'amount': '+1.3e-13', 'lowerBound': '+1.2e-13', + 'upperBound': '+1.4e-13', 'unit': '1'} + self.assertEqual(q.toWikibase(), q_dict) + + def test_WbQuantity_decimal(self): + """Test WbQuantity for decimal value.""" + repo = self.get_repo() + q = pywikibot.WbQuantity(amount=Decimal('0.044405586'), + error=Decimal('0.0'), site=repo) + q_dict = {'amount': '+0.044405586', 'lowerBound': '+0.044405586', + 'upperBound': '+0.044405586', 'unit': '1'} + self.assertEqual(q.toWikibase(), q_dict) + + def test_WbQuantity_string(self): + """Test WbQuantity for decimal notation.""" + repo = self.get_repo() + q = pywikibot.WbQuantity(amount='0.044405586', error='0', site=repo) + q_dict = {'amount': '+0.044405586', 'lowerBound': '+0.044405586', + 'upperBound': '+0.044405586', 'unit': '1'} + self.assertEqual(q.toWikibase(), q_dict) + + def test_WbQuantity_formatting_bound(self): + """Test WbQuantity formatting with bounds.""" + repo = self.get_repo() + q = pywikibot.WbQuantity(amount='0.044405586', error='0', site=repo) + self.assertEqual(str(q), + '{{\n' + ' "amount": "+{val}",\n' + ' "lowerBound": "+{val}",\n' + ' "unit": "1",\n' + ' "upperBound": "+{val}"\n' + '}}'.format(val='0.044405586')) + self.assertEqual(repr(q), + 'WbQuantity(amount={val}, ' + 'upperBound={val}, lowerBound={val}, ' + 'unit=1)'.format(val='0.044405586')) + + def test_WbQuantity_self_equality(self): + """Test WbQuantity equality.""" + repo = self.get_repo() + q = pywikibot.WbQuantity(amount='0.044405586', error='0', site=repo) + self.assertEqual(q, q) + + def test_WbQuantity_fromWikibase(self): + """Test WbQuantity.fromWikibase() instantiating.""" + repo = self.get_repo() + q = pywikibot.WbQuantity.fromWikibase({'amount': '+0.0229', + 'lowerBound': '0', + 'upperBound': '1', + 'unit': '1'}, + site=repo) + # note that the bounds are inputted as INT but are returned as FLOAT + self.assertEqual(q.toWikibase(), + {'amount': '+0.0229', 'lowerBound': '+0.0000', + 'upperBound': '+1.0000', 'unit': '1'}) + + def test_WbQuantity_errors(self): + """Test WbQuantity error handling.""" + regex = r'^no amount given$' + with self.assertRaisesRegex(ValueError, regex): + pywikibot.WbQuantity(amount=None, error=1) + + def test_WbQuantity_entity_unit(self): + """Test WbQuantity with entity uri unit.""" + repo = self.get_repo() + q = pywikibot.WbQuantity(amount=1234, error=1, site=repo, + unit='http://www.wikidata.org/entity/Q712226') + self.assertEqual(q.toWikibase(), + {'amount': '+1234', 'lowerBound': '+1233', + 'upperBound': '+1235', + 'unit': 'http://www.wikidata.org/entity/Q712226'}) + + def test_WbQuantity_unit_fromWikibase(self): + """Test WbQuantity recognising unit from Wikibase output.""" + repo = self.get_repo() + q = pywikibot.WbQuantity.fromWikibase({ + 'amount': '+1234', 'lowerBound': '+1233', 'upperBound': '+1235', + 'unit': 'http://www.wikidata.org/entity/Q712226'}, + site=repo) + self.assertEqual(q.toWikibase(), + {'amount': '+1234', 'lowerBound': '+1233', + 'upperBound': '+1235', + 'unit': 'http://www.wikidata.org/entity/Q712226'}) + + +class TestWbQuantityNonDry(WbRepresentationTestCase): + + """Test Wikibase WbQuantity data type (non-dry). + + These can be moved to TestWbQuantity once DrySite has been bumped to + the appropriate version. + """ + + def setUp(self): + """Override setup to store repo and it's version.""" + super().setUp() + self.repo = self.get_repo() + self.version = self.repo.mw_version + + def test_WbQuantity_unbound(self): + """Test WbQuantity for value without bounds.""" + if self.version < MediaWikiVersion('1.29.0-wmf.2'): + self.skipTest('Wiki version must be 1.29.0-wmf.2 or newer to ' + 'support unbound uncertainties.') + q = pywikibot.WbQuantity(amount=1234.5, site=self.repo) + self.assertEqual(q.toWikibase(), + {'amount': '+1234.5', 'unit': '1', + 'upperBound': None, 'lowerBound': None}) + + def test_WbQuantity_formatting_unbound(self): + """Test WbQuantity formatting without bounds.""" + if self.version < MediaWikiVersion('1.29.0-wmf.2'): + self.skipTest('Wiki version must be 1.29.0-wmf.2 or newer to ' + 'support unbound uncertainties.') + q = pywikibot.WbQuantity(amount='0.044405586', site=self.repo) + self.assertEqual(str(q), + '{{\n' + ' "amount": "+{val}",\n' + ' "lowerBound": null,\n' + ' "unit": "1",\n' + ' "upperBound": null\n' + '}}'.format(val='0.044405586')) + self.assertEqual(repr(q), + 'WbQuantity(amount={val}, ' + 'upperBound=None, lowerBound=None, ' + 'unit=1)'.format(val='0.044405586')) + + def test_WbQuantity_fromWikibase_unbound(self): + """Test WbQuantity.fromWikibase() instantiating without bounds.""" + if self.version < MediaWikiVersion('1.29.0-wmf.2'): + self.skipTest('Wiki version must be 1.29.0-wmf.2 or newer to ' + 'support unbound uncertainties.') + q = pywikibot.WbQuantity.fromWikibase({'amount': '+0.0229', + 'unit': '1'}, + site=self.repo) + self.assertEqual(q.toWikibase(), + {'amount': '+0.0229', 'lowerBound': None, + 'upperBound': None, 'unit': '1'}) + + def test_WbQuantity_ItemPage_unit(self): + """Test WbQuantity with ItemPage unit.""" + if self.version < MediaWikiVersion('1.28-wmf.23'): + self.skipTest('Wiki version must be 1.28-wmf.23 or newer to ' + 'expose wikibase-conceptbaseuri.') + + q = pywikibot.WbQuantity(amount=1234, error=1, + unit=pywikibot.ItemPage(self.repo, 'Q712226')) + self.assertEqual(q.toWikibase(), + {'amount': '+1234', 'lowerBound': '+1233', + 'upperBound': '+1235', + 'unit': 'http://www.wikidata.org/entity/Q712226'}) + + def test_WbQuantity_equality(self): + """Test WbQuantity equality with different unit representations.""" + if self.version < MediaWikiVersion('1.28-wmf.23'): + self.skipTest('Wiki version must be 1.28-wmf.23 or newer to ' + 'expose wikibase-conceptbaseuri.') + + a = pywikibot.WbQuantity( + amount=1234, error=1, + unit=pywikibot.ItemPage(self.repo, 'Q712226')) + b = pywikibot.WbQuantity( + amount=1234, error=1, + unit='http://www.wikidata.org/entity/Q712226') + c = pywikibot.WbQuantity( + amount=1234, error=1, + unit='http://test.wikidata.org/entity/Q712226') + d = pywikibot.WbQuantity( + amount=1234, error=2, + unit='http://www.wikidata.org/entity/Q712226') + self.assertEqual(a, b) + self.assertNotEqual(a, c) + self.assertNotEqual(b, c) + self.assertNotEqual(b, d) + + def test_WbQuantity_get_unit_item(self): + """Test getting unit item from WbQuantity.""" + repo = self.get_repo() + q = pywikibot.WbQuantity(amount=1234, error=1, site=repo, + unit='http://www.wikidata.org/entity/Q123') + self.assertEqual(q.get_unit_item(), + ItemPage(repo, 'Q123')) + + def test_WbQuantity_get_unit_item_provide_repo(self): + """Test getting unit item from WbQuantity, providing repo.""" + repo = self.get_repo() + q = pywikibot.WbQuantity(amount=1234, error=1, + unit='http://www.wikidata.org/entity/Q123') + self.assertEqual(q.get_unit_item(repo), + ItemPage(repo, 'Q123')) + + def test_WbQuantity_get_unit_item_different_repo(self): + """Test getting unit item in different repo from WbQuantity.""" + repo = self.get_repo() + test_repo = pywikibot.Site('test', 'wikidata') + q = pywikibot.WbQuantity(amount=1234, error=1, site=repo, + unit='http://test.wikidata.org/entity/Q123') + self.assertEqual(q.get_unit_item(test_repo), + ItemPage(test_repo, 'Q123')) + + +class TestWbMonolingualText(WbRepresentationTestCase): + + """Test Wikibase WbMonolingualText data type.""" + + dry = True + + def test_WbMonolingualText_WbRepresentation_methods(self): + """Test inherited or extended methods from _WbRepresentation.""" + q = pywikibot.WbMonolingualText( + text='Test that basics work', language='en') + self._test_hashable(q) + + def test_WbMonolingualText_string(self): + """Test WbMonolingualText string.""" + q = pywikibot.WbMonolingualText(text='Test that basics work', + language='en') + q_dict = {'text': 'Test that basics work', 'language': 'en'} + self.assertEqual(q.toWikibase(), q_dict) + + def test_WbMonolingualText_unicode(self): + """Test WbMonolingualText unicode.""" + q = pywikibot.WbMonolingualText(text='Testa det här', language='sv') + q_dict = {'text': 'Testa det här', 'language': 'sv'} + self.assertEqual(q.toWikibase(), q_dict) + + def test_WbMonolingualText_equality(self): + """Test WbMonolingualText equality.""" + q = pywikibot.WbMonolingualText(text='Thou shall test this!', + language='en-gb') + self.assertEqual(q, q) + + def test_WbMonolingualText_fromWikibase(self): + """Test WbMonolingualText.fromWikibase() instantiating.""" + q = pywikibot.WbMonolingualText.fromWikibase({'text': 'Test this!', + 'language': 'en'}) + self.assertEqual(q.toWikibase(), + {'text': 'Test this!', 'language': 'en'}) + + def test_WbMonolingualText_errors(self): + """Test WbMonolingualText error handling.""" + regex = r'^text and language cannot be empty$' + with self.assertRaisesRegex(ValueError, regex): + pywikibot.WbMonolingualText(text='', language='sv') + with self.assertRaisesRegex(ValueError, regex): + pywikibot.WbMonolingualText(text='Test this!', language='') + with self.assertRaisesRegex(ValueError, regex): + pywikibot.WbMonolingualText(text=None, language='sv') + + +class TestWikibaseParser(WikidataTestCase): + + """Test passing various datatypes to wikibase parser.""" + + def test_wbparse_strings(self): + """Test that strings return unchanged.""" + test_list = ['test string', 'second test'] + parsed_strings = self.site.parsevalue('string', test_list) + self.assertEqual(parsed_strings, test_list) + + def test_wbparse_time(self): + """Test parsing of a time value.""" + parsed_date = self.site.parsevalue( + 'time', ['1994-02-08'], {'precision': 9})[0] + self.assertEqual(parsed_date['time'], '+1994-02-08T00:00:00Z') + self.assertEqual(parsed_date['precision'], 9) + + def test_wbparse_quantity(self): + """Test parsing of quantity values.""" + parsed_quantities = self.site.parsevalue( + 'quantity', + ['1.90e-9+-0.20e-9', '1000000.00000000054321+-0', '-123+-1', + '2.70e34+-1e32']) + self.assertEqual(parsed_quantities[0]['amount'], '+0.00000000190') + self.assertEqual(parsed_quantities[0]['upperBound'], '+0.00000000210') + self.assertEqual(parsed_quantities[0]['lowerBound'], '+0.00000000170') + self.assertEqual(parsed_quantities[1]['amount'], + '+1000000.00000000054321') + self.assertEqual(parsed_quantities[1]['upperBound'], + '+1000000.00000000054321') + self.assertEqual(parsed_quantities[1]['lowerBound'], + '+1000000.00000000054321') + self.assertEqual(parsed_quantities[2]['amount'], '-123') + self.assertEqual(parsed_quantities[2]['upperBound'], '-122') + self.assertEqual(parsed_quantities[2]['lowerBound'], '-124') + self.assertEqual(parsed_quantities[3]['amount'], + '+27000000000000000000000000000000000') + self.assertEqual(parsed_quantities[3]['upperBound'], + '+27100000000000000000000000000000000') + self.assertEqual(parsed_quantities[3]['lowerBound'], + '+26900000000000000000000000000000000') + + def test_wbparse_raises_valueerror(self): + """Test invalid value condition.""" + with self.assertRaises(ValueError): + self.site.parsevalue('quantity', ['Not a quantity']) + + +class TestWbGeoShapeNonDry(WbRepresentationTestCase): + + """Test Wikibase WbGeoShape data type (non-dry). + + These require non dry tests due to the page.exists() call. + """ + + def setUp(self): + """Setup tests.""" + self.commons = pywikibot.Site('commons') + self.page = Page(self.commons, 'Data:Lyngby Hovedgade.map') + super().setUp() + + def test_WbGeoShape_WbRepresentation_methods(self): + """Test inherited or extended methods from _WbRepresentation.""" + q = pywikibot.WbGeoShape(self.page) + self._test_hashable(q) + + def test_WbGeoShape_page(self): + """Test WbGeoShape page.""" + q = pywikibot.WbGeoShape(self.page) + q_val = 'Data:Lyngby Hovedgade.map' + self.assertEqual(q.toWikibase(), q_val) + + def test_WbGeoShape_page_and_site(self): + """Test WbGeoShape from page and site.""" + q = pywikibot.WbGeoShape(self.page, self.get_repo()) + q_val = 'Data:Lyngby Hovedgade.map' + self.assertEqual(q.toWikibase(), q_val) + + def test_WbGeoShape_equality(self): + """Test WbGeoShape equality.""" + q = pywikibot.WbGeoShape(self.page, self.get_repo()) + self.assertEqual(q, q) + + def test_WbGeoShape_fromWikibase(self): + """Test WbGeoShape.fromWikibase() instantiating.""" + repo = self.get_repo() + q = pywikibot.WbGeoShape.fromWikibase( + 'Data:Lyngby Hovedgade.map', repo) + self.assertEqual(q.toWikibase(), 'Data:Lyngby Hovedgade.map') + + def test_WbGeoShape_error_on_non_page(self): + """Test WbGeoShape error handling when given a non-page.""" + regex = r'^Page .+? must be a pywikibot\.Page object not a' + with self.assertRaisesRegex(ValueError, regex): + pywikibot.WbGeoShape('A string', self.get_repo()) + + def test_WbGeoShape_error_on_non_exitant_page(self): + """Test WbGeoShape error handling of a non-existant page.""" + page = Page(self.commons, 'Non-existant page... really') + regex = r'^Page \[\[.+?\]\] must exist\.$' + with self.assertRaisesRegex(ValueError, regex): + pywikibot.WbGeoShape(page, self.get_repo()) + + def test_WbGeoShape_error_on_wrong_site(self): + """Test WbGeoShape error handling of a page on non-filerepo site.""" + repo = self.get_repo() + page = Page(repo, 'Q123') + regex = r'^Page must be on the geo-shape repository site\.$' + with self.assertRaisesRegex(ValueError, regex): + pywikibot.WbGeoShape(page, self.get_repo()) + + def test_WbGeoShape_error_on_wrong_page_type(self): + """Test WbGeoShape error handling of a non-map page.""" + non_data_page = Page(self.commons, 'File:Foo.jpg') + non_map_page = Page(self.commons, 'Data:TemplateData/TemplateData.tab') + regex = (r"^Page must be in 'Data:' namespace and end in '\.map' " + r'for geo-shape\.$') + with self.assertRaisesRegex(ValueError, regex): + pywikibot.WbGeoShape(non_data_page, self.get_repo()) + with self.assertRaisesRegex(ValueError, regex): + pywikibot.WbGeoShape(non_map_page, self.get_repo()) + + +class TestWbTabularDataNonDry(WbRepresentationTestCase): + + """Test Wikibase WbTabularData data type (non-dry). + + These require non dry tests due to the page.exists() call. + """ + + def setUp(self): + """Setup tests.""" + self.commons = pywikibot.Site('commons') + self.page = Page(self.commons, 'Data:Bea.gov/GDP by state.tab') + super().setUp() + + def test_WbTabularData_WbRepresentation_methods(self): + """Test inherited or extended methods from _WbRepresentation.""" + q = pywikibot.WbTabularData(self.page) + self._test_hashable(q) + + def test_WbTabularData_page(self): + """Test WbTabularData page.""" + q = pywikibot.WbTabularData(self.page) + q_val = 'Data:Bea.gov/GDP by state.tab' + self.assertEqual(q.toWikibase(), q_val) + + def test_WbTabularData_page_and_site(self): + """Test WbTabularData from page and site.""" + q = pywikibot.WbTabularData(self.page, self.get_repo()) + q_val = 'Data:Bea.gov/GDP by state.tab' + self.assertEqual(q.toWikibase(), q_val) + + def test_WbTabularData_equality(self): + """Test WbTabularData equality.""" + q = pywikibot.WbTabularData(self.page, self.get_repo()) + self.assertEqual(q, q) + + def test_WbTabularData_fromWikibase(self): + """Test WbTabularData.fromWikibase() instantiating.""" + repo = self.get_repo() + q = pywikibot.WbTabularData.fromWikibase( + 'Data:Bea.gov/GDP by state.tab', repo) + self.assertEqual(q.toWikibase(), 'Data:Bea.gov/GDP by state.tab') + + def test_WbTabularData_error_on_non_page(self): + """Test WbTabularData error handling when given a non-page.""" + regex = r'^Page .+? must be a pywikibot\.Page object not a' + with self.assertRaisesRegex(ValueError, regex): + pywikibot.WbTabularData('A string', self.get_repo()) + + def test_WbTabularData_error_on_non_exitant_page(self): + """Test WbTabularData error handling of a non-existant page.""" + page = Page(self.commons, 'Non-existant page... really') + regex = r'^Page \[\[.+?\]\] must exist\.$' + with self.assertRaisesRegex(ValueError, regex): + pywikibot.WbTabularData(page, self.get_repo()) + + def test_WbTabularData_error_on_wrong_site(self): + """Test WbTabularData error handling of a page on non-filerepo site.""" + repo = self.get_repo() + page = Page(repo, 'Q123') + regex = r'^Page must be on the tabular-data repository site\.$' + with self.assertRaisesRegex(ValueError, regex): + pywikibot.WbTabularData(page, self.get_repo()) + + def test_WbTabularData_error_on_wrong_page_type(self): + """Test WbTabularData error handling of a non-map page.""" + non_data_page = Page(self.commons, 'File:Foo.jpg') + non_map_page = Page(self.commons, 'Data:Lyngby Hovedgade.map') + regex = (r"^Page must be in 'Data:' namespace and end in '\.tab' " + r'for tabular-data\.$') + with self.assertRaisesRegex(ValueError, regex): + pywikibot.WbTabularData(non_data_page, self.get_repo()) + with self.assertRaisesRegex(ValueError, regex): + pywikibot.WbTabularData(non_map_page, self.get_repo()) + + +class TestWbUnknown(WbRepresentationTestCase): + + """Test Wikibase WbUnknown data type.""" + + dry = True + + def test_WbUnknown_WbRepresentation_methods(self): + """Test inherited or extended methods from _WbRepresentation.""" + q_dict = {'text': 'Test that basics work', 'language': 'en'} + q = pywikibot.WbUnknown(q_dict) + self._test_hashable(q) + + def test_WbUnknown_string(self): + """Test WbUnknown string.""" + q_dict = {'text': 'Test that basics work', 'language': 'en'} + q = pywikibot.WbUnknown(q_dict) + self.assertEqual(q.toWikibase(), q_dict) + + def test_WbUnknown_equality(self): + """Test WbUnknown equality.""" + q_dict = {'text': 'Thou shall test this!', 'language': 'unknown'} + q = pywikibot.WbUnknown(q_dict) + self.assertEqual(q, q) + + def test_WbUnknown_fromWikibase(self): + """Test WbUnknown.fromWikibase() instantiating.""" + q = pywikibot.WbUnknown.fromWikibase({'text': 'Test this!', + 'language': 'en'}) + self.assertEqual(q.toWikibase(), + {'text': 'Test this!', 'language': 'en'}) + + +class TestLoadUnknownType(WikidataTestCase): + + """Test unknown datatypes being loaded as WbUnknown.""" + + dry = True + + def setUp(self): + """Setup test.""" + super().setUp() + wikidata = self.get_repo() + self.wdp = ItemPage(wikidata, 'Q60') + self.wdp.id = 'Q60' + with open(join_pages_path('Q60_unknown_datatype.wd')) as f: + self.wdp._content = json.load(f) + + def test_load_unknown(self): + """Ensure unknown value is loaded but raises a warning.""" + self.wdp.get() + unknown_value = self.wdp.claims['P99999'][0].getTarget() + self.assertIsInstance(unknown_value, pywikibot.WbUnknown) + self.assertEqual(unknown_value.warning, + 'foo-unknown-bar datatype is not supported yet.') + + +class TestItemPageExtensibility(TestCase): + + """Test ItemPage extensibility.""" + + family = 'wikipedia' + code = 'en' + + dry = True + + def test_ItemPage_extensibility(self): + """Test ItemPage extensibility.""" + class MyItemPage(ItemPage): + + """Dummy ItemPage subclass.""" + + page = pywikibot.Page(self.site, 'foo') + self.assertIsInstance(MyItemPage.fromPage(page, lazy_load=True), + MyItemPage) + + +class TestItemLoad(WikidataTestCase): + + """Test item creation. + + Tests for item creation include: + 1. by Q id + 2. ItemPage.fromPage(page) + 3. ItemPage.fromPage(page_with_props_loaded) + 4. ItemPage.from_entity_uri(site, uri) + + Test various invalid scenarios: + 1. invalid Q ids + 2. invalid pages to fromPage + 3. missing pages to fromPage + 4. unconnected pages to fromPage + """ + + sites = { + 'wikidata': { + 'family': 'wikidata', + 'code': 'wikidata', + }, + 'enwiki': { + 'family': 'wikipedia', + 'code': 'en', + } + } + + @classmethod + def setUpClass(cls): + """Setup test class.""" + super().setUpClass() + cls.site = cls.get_site('enwiki') + + def setUp(self): + """Setup test.""" + super().setUp() + self.nyc = pywikibot.Page(pywikibot.page.Link('New York City', + self.site)) + + def test_item_normal(self): + """Test normal wikibase item.""" + wikidata = self.get_repo() + item = ItemPage(wikidata, 'Q60') + self.assertEqual(item._link._title, 'Q60') + self.assertEqual(item._defined_by(), {'ids': 'Q60'}) + self.assertEqual(item.id, 'Q60') + self.assertFalse(hasattr(item, '_title')) + self.assertFalse(hasattr(item, '_site')) + self.assertEqual(item.title(), 'Q60') + self.assertEqual(item.getID(), 'Q60') + self.assertEqual(item.getID(numeric=True), 60) + self.assertFalse(hasattr(item, '_content')) + item.get() + self.assertTrue(hasattr(item, '_content')) + + def test_item_lazy_initialization(self): + """Test that Wikibase items are properly initialized lazily.""" + wikidata = self.get_repo() + item = ItemPage(wikidata, 'Q60') + attrs = ['_content', 'labels', 'descriptions', 'aliases', + 'claims', 'sitelinks'] + for attr in attrs: + with self.subTest(attr=attr, note='before loading'): + # hasattr() loads the attributes; use item.__dict__ for tests + self.assertNotIn(attr, item.__dict__) + + item.labels # trigger loading + for attr in attrs: + with self.subTest(attr=attr, note='after loading'): + self.assertIn(attr, item.__dict__) + + def test_load_item_set_id(self): + """Test setting item.id attribute on empty item.""" + wikidata = self.get_repo() + item = ItemPage(wikidata, '-1') + self.assertEqual(item._link._title, '-1') + item.id = 'Q60' + self.assertFalse(hasattr(item, '_content')) + self.assertEqual(item.getID(), 'Q60') + self.assertFalse(hasattr(item, '_content')) + item.get() + self.assertTrue(hasattr(item, '_content')) + self.assertIn('en', item.labels) + self.assertEqual(item.labels['en'], 'New York City') + self.assertEqual(item.title(), 'Q60') + + def test_reuse_item_set_id(self): + """Test modifying item.id attribute. + + Some scripts are using item.id = 'Q60' semantics, which does work + but modifying item.id does not currently work, and this test + highlights that it breaks silently. + """ + wikidata = self.get_repo() + item = ItemPage(wikidata, 'Q60') + item.get() + self.assertEqual(item.labels['en'], 'New York City') + + # When the id attribute is modified, the ItemPage goes into + # an inconsistent state. + item.id = 'Q5296' + # The title is updated correctly + self.assertEqual(item.title(), 'Q5296') + + # This del has no effect on the test; it is here to demonstrate that + # it doesn't help to clear this piece of saved state. + del item._content + # The labels are not updated; assertion showing undesirable behaviour: + self.assertEqual(item.labels['en'], 'New York City') + # TODO: This is the assertion that this test should be using: + # self.assertTrue(item.labels['en'].lower().endswith('main page')) + + def test_empty_item(self): + """Test empty wikibase item. + + should not raise an error as the constructor only requires + the site parameter, with the title parameter defaulted to None. + """ + wikidata = self.get_repo() + item = ItemPage(wikidata) + self.assertEqual(item._link._title, '-1') + self.assertLength(item.labels, 0) + self.assertLength(item.descriptions, 0) + self.assertLength(item.aliases, 0) + self.assertLength(item.claims, 0) + self.assertLength(item.sitelinks, 0) + + def test_item_invalid_titles(self): + """Test invalid titles of wikibase items.""" + wikidata = self.get_repo() + + regex = r"^'.+' is not a valid .+ page title$" + for title in ['null', 'NULL', 'None', + '-2', '1', '0', '+1', 'Q0', + 'Q0.5', 'Q', 'Q-1', 'Q+1']: + with self.assertRaisesRegex(InvalidTitleError, regex): + ItemPage(wikidata, title) + + regex = r"^Item's title cannot be empty$" + with self.assertRaisesRegex(InvalidTitleError, regex): + ItemPage(wikidata, '') + + def test_item_untrimmed_title(self): + """Test intrimmed titles of wikibase items. + + Spaces in the title should not cause an error. + """ + wikidata = self.get_repo() + item = ItemPage(wikidata, ' Q60 ') + self.assertEqual(item._link._title, 'Q60') + self.assertEqual(item.title(), 'Q60') + item.get() + + def test_item_missing(self): + """Test nmissing item.""" + wikidata = self.get_repo() + # this item has never existed + item = ItemPage(wikidata, 'Q7') + self.assertEqual(item._link._title, 'Q7') + self.assertEqual(item.title(), 'Q7') + self.assertFalse(hasattr(item, '_content')) + self.assertEqual(item.id, 'Q7') + self.assertEqual(item.getID(), 'Q7') + numeric_id = item.getID(numeric=True) + self.assertIsInstance(numeric_id, int) + self.assertEqual(numeric_id, 7) + self.assertFalse(hasattr(item, '_content')) + regex = r"^Page .+ doesn't exist\.$" + with self.assertRaisesRegex(NoPageError, regex): + item.get() + self.assertTrue(hasattr(item, '_content')) + self.assertEqual(item.id, 'Q7') + self.assertEqual(item.getID(), 'Q7') + self.assertEqual(item._link._title, 'Q7') + self.assertEqual(item.title(), 'Q7') + with self.assertRaisesRegex(NoPageError, regex): + item.get() + self.assertTrue(hasattr(item, '_content')) + self.assertEqual(item._link._title, 'Q7') + self.assertEqual(item.getID(), 'Q7') + self.assertEqual(item.title(), 'Q7') + + def test_item_never_existed(self): + """Test non-existent item.""" + wikidata = self.get_repo() + # this item has not been created + item = ItemPage(wikidata, 'Q9999999999999999999') + self.assertFalse(item.exists()) + self.assertEqual(item.getID(), 'Q9999999999999999999') + regex = r"^Page .+ doesn't exist\.$" + with self.assertRaisesRegex(NoPageError, regex): + item.get() + + def test_fromPage_noprops(self): + """Test item from page without properties.""" + page = self.nyc + item = ItemPage.fromPage(page) + self.assertEqual(item._link._title, '-1') + self.assertTrue(hasattr(item, 'id')) + self.assertTrue(hasattr(item, '_content')) + self.assertEqual(item.title(), 'Q60') + self.assertTrue(hasattr(item, '_content')) + self.assertEqual(item.id, 'Q60') + self.assertEqual(item.getID(), 'Q60') + self.assertEqual(item.getID(numeric=True), 60) + item.get() + self.assertTrue(item.exists()) + + def test_fromPage_noprops_with_section(self): + """Test item from page with section.""" + page = pywikibot.Page(self.nyc.site, self.nyc.title() + '#foo') + item = ItemPage.fromPage(page) + self.assertEqual(item._link._title, '-1') + self.assertTrue(hasattr(item, 'id')) + self.assertTrue(hasattr(item, '_content')) + self.assertEqual(item.title(), 'Q60') + self.assertTrue(hasattr(item, '_content')) + self.assertEqual(item.id, 'Q60') + self.assertEqual(item.getID(), 'Q60') + self.assertEqual(item.getID(numeric=True), 60) + item.get() + self.assertTrue(item.exists()) + + def test_fromPage_props(self): + """Test item from page with properties.""" + page = self.nyc + # fetch page properties + page.properties() + item = ItemPage.fromPage(page) + self.assertEqual(item._link._title, 'Q60') + self.assertEqual(item.id, 'Q60') + self.assertFalse(hasattr(item, '_content')) + self.assertEqual(item.title(), 'Q60') + self.assertFalse(hasattr(item, '_content')) + self.assertEqual(item.id, 'Q60') + self.assertEqual(item.getID(), 'Q60') + self.assertEqual(item.getID(numeric=True), 60) + self.assertFalse(hasattr(item, '_content')) + item.get() + self.assertTrue(hasattr(item, '_content')) + self.assertTrue(item.exists()) + item2 = ItemPage.fromPage(page) + self.assertTrue(item is item2) + + def test_fromPage_lazy(self): + """Test item from page with lazy_load.""" + page = pywikibot.Page(pywikibot.page.Link('New York City', self.site)) + item = ItemPage.fromPage(page, lazy_load=True) + self.assertEqual(item._defined_by(), + {'sites': 'enwiki', 'titles': 'New York City'}) + self.assertEqual(item._link._title, '-1') + self.assertFalse(hasattr(item, 'id')) + self.assertFalse(hasattr(item, '_content')) + self.assertEqual(item.title(), 'Q60') + self.assertTrue(hasattr(item, '_content')) + self.assertEqual(item.id, 'Q60') + self.assertEqual(item.getID(), 'Q60') + self.assertEqual(item.getID(numeric=True), 60) + item.get() + self.assertTrue(item.exists()) + + def _test_fromPage_noitem(self, link): + """Helper function to test a page without an associated item. + + It tests two of the ways to fetch an item: + 1. the Page already has props, which should contain an item id if + present, and that item id is used to instantiate the item, and + 2. the page doesn't have props, in which case the site&titles is + used to lookup the item id, but that lookup occurs after + instantiation, during the first attempt to use the data item. + """ + for props in [True, False]: + for method in ['title', 'get', 'getID', 'exists']: + page = pywikibot.Page(link) + if props: + page.properties() + + item = ItemPage.fromPage(page, lazy_load=True) + + self.assertFalse(hasattr(item, 'id')) + self.assertTrue(hasattr(item, '_title')) + self.assertTrue(hasattr(item, '_site')) + self.assertFalse(hasattr(item, '_content')) + + self.assertEqual(item._link._title, '-1') + # the method 'exists' does not raise an exception + if method == 'exists': + self.assertFalse(item.exists()) + else: + regex = r"^Page .+ doesn't exist\.$" + with self.assertRaisesRegex(NoPageError, regex): + getattr(item, method)() + + # The invocation above of a fetching method shouldn't change + # the local item, but it does! The title changes to '-1'. + # + # However when identifying the item for 'en:Test page' + # (a deleted page), the exception handling is smarter, and no + # local data is modified in this scenario. This case is + # separately tested in test_fromPage_missing_lazy. + if link.title != 'Test page': + self.assertEqual(item._link._title, '-1') + + self.assertTrue(hasattr(item, '_content')) + + self.assertFalse(item.exists()) + + page = pywikibot.Page(link) + if props: + page.properties() + + # by default, fromPage should always raise the same exception + regex = r"^Page .+ doesn't exist\.$" + with self.assertRaisesRegex(NoPageError, regex): + ItemPage.fromPage(page) + + def test_fromPage_redirect(self): + """Test item from redirect page. + + A redirect should not have a wikidata item. + """ + link = pywikibot.page.Link('Main page', self.site) + self._test_fromPage_noitem(link) + + def test_fromPage_missing(self): + """Test item from deleted page. + + A deleted page should not have a wikidata item. + """ + link = pywikibot.page.Link('Test page', self.site) + self._test_fromPage_noitem(link) + + def test_fromPage_noitem(self): + """Test item from new page. + + A new created page should not have a wikidata item yet. + """ + page = _get_test_unconnected_page(self.site) + link = page._link + self._test_fromPage_noitem(link) + + def test_fromPage_missing_lazy(self): + """Test lazy loading of item from nonexistent source page.""" + # this is a deleted page, and should not have a wikidata item + link = pywikibot.page.Link('Test page', self.site) + page = pywikibot.Page(link) + # ItemPage.fromPage should raise an exception when not lazy loading + # and that exception should refer to the source title 'Test page' + # not the Item being created. + with self.assertRaisesRegex(NoPageError, 'Test page'): + ItemPage.fromPage(page, lazy_load=False) + + item = ItemPage.fromPage(page, lazy_load=True) + + # Now verify that delay loading will result in the desired semantics. + # It should not raise NoPageError on the wikibase item which has a + # title like '-1' or 'Null', as that is useless to determine the cause + # without a full debug log. + # It should raise NoPageError on the source page, with title 'Test + # page' as that is what the bot operator needs to see in the log + # output. + with self.assertRaisesRegex(NoPageError, 'Test page'): + item.get() + + def test_from_entity_uri(self): + """Test ItemPage.from_entity_uri.""" + repo = self.get_repo() + entity_uri = 'http://www.wikidata.org/entity/Q124' + self.assertEqual(ItemPage.from_entity_uri(repo, entity_uri), + ItemPage(repo, 'Q124')) + + def test_from_entity_uri_not_a_data_repo(self): + """Test ItemPage.from_entity_uri with a non-Wikibase site.""" + repo = self.site + entity_uri = 'http://www.wikidata.org/entity/Q124' + regex = r' is not a data repository\.$' + with self.assertRaisesRegex(TypeError, regex): + ItemPage.from_entity_uri(repo, entity_uri) + + def test_from_entity_uri_wrong_repo(self): + """Test ItemPage.from_entity_uri with unexpected item repo.""" + repo = self.get_repo() + entity_uri = 'http://test.wikidata.org/entity/Q124' + regex = (r'^The supplied data repository \(.+\) does not ' + r'correspond to that of the item \(.+\)$') + with self.assertRaisesRegex(ValueError, regex): + ItemPage.from_entity_uri(repo, entity_uri) + + def test_from_entity_uri_invalid_title(self): + """Test ItemPage.from_entity_uri with an invalid item title format.""" + repo = self.get_repo() + entity_uri = 'http://www.wikidata.org/entity/Nonsense' + regex = r"^'.+' is not a valid .+ page title$" + with self.assertRaisesRegex(InvalidTitleError, regex): + ItemPage.from_entity_uri(repo, entity_uri) + + def test_from_entity_uri_no_item(self): + """Test ItemPage.from_entity_uri with non-existent item.""" + repo = self.get_repo() + entity_uri = 'http://www.wikidata.org/entity/Q999999999999999999' + regex = r"^Page .+ doesn't exist\.$" + with self.assertRaisesRegex(NoPageError, regex): + ItemPage.from_entity_uri(repo, entity_uri) + + def test_from_entity_uri_no_item_lazy(self): + """Test ItemPage.from_entity_uri with lazy loaded non-existent item.""" + repo = self.get_repo() + entity_uri = 'http://www.wikidata.org/entity/Q999999999999999999' + expected_item = ItemPage(repo, 'Q999999999999999999') + self.assertEqual( + ItemPage.from_entity_uri(repo, entity_uri, lazy_load=True), + expected_item) + + self.assertFalse(expected_item.exists()) # ensure actually missing + + +class TestRedirects(WikidataTestCase): + + """Test redirect and non-redirect items.""" + + def test_normal_item(self): + """Test normal item.""" + wikidata = self.get_repo() + item = ItemPage(wikidata, 'Q1') + self.assertFalse(item.isRedirectPage()) + self.assertTrue(item.exists()) + regex = r'^Page .+ is not a redirect page\.$' + with self.assertRaisesRegex(IsNotRedirectPageError, regex): + item.getRedirectTarget() + + def test_redirect_item(self): + """Test redirect item.""" + wikidata = self.get_repo() + item = ItemPage(wikidata, 'Q10008448') + item.get(get_redirect=True) + target = ItemPage(wikidata, 'Q8422626') + # tests after get operation + self.assertTrue(item.isRedirectPage()) + self.assertTrue(item.exists()) + self.assertEqual(item.getRedirectTarget(), target) + self.assertIsInstance(item.getRedirectTarget(), ItemPage) + regex = r'^Page .+ is a redirect page\.$' + with self.assertRaisesRegex(IsRedirectPageError, regex): + item.get() + + def test_redirect_item_without_get(self): + """Test redirect item without explicit get operation.""" + wikidata = self.get_repo() + item = pywikibot.ItemPage(wikidata, 'Q10008448') + self.assertTrue(item.exists()) + self.assertTrue(item.isRedirectPage()) + target = pywikibot.ItemPage(wikidata, 'Q8422626') + self.assertEqual(item.getRedirectTarget(), target) + + +class TestPropertyPage(WikidataTestCase): + + """Test PropertyPage.""" + + def test_property_empty_property(self): + """Test creating a PropertyPage without a title and datatype.""" + wikidata = self.get_repo() + regex = r'^"datatype" is required for new property\.$' + with self.assertRaisesRegex(TypeError, regex): + PropertyPage(wikidata) + + def test_property_empty_title(self): + """Test creating a PropertyPage without a title.""" + wikidata = self.get_repo() + regex = r"^Property's title cannot be empty$" + with self.assertRaisesRegex(InvalidTitleError, regex): + PropertyPage(wikidata, title='') + + def test_globe_coordinate(self): + """Test a coordinate PropertyPage has the correct type.""" + wikidata = self.get_repo() + property_page = PropertyPage(wikidata, 'P625') + self.assertEqual(property_page.type, 'globe-coordinate') + + claim = pywikibot.Claim(wikidata, 'P625') + self.assertEqual(claim.type, 'globe-coordinate') + + def test_get(self): + """Test PropertyPage.get() method.""" + wikidata = self.get_repo() + property_page = PropertyPage(wikidata, 'P625') + property_page.get() + self.assertEqual(property_page.type, 'globe-coordinate') + + def test_new_claim(self): + """Test that PropertyPage.newClaim uses cached datatype.""" + wikidata = self.get_repo() + property_page = PropertyPage(wikidata, 'P625') + property_page.get() + claim = property_page.newClaim() + self.assertEqual(claim.type, 'globe-coordinate') + + # Now verify that it isn't fetching the type from the property + # data in the repo by setting the cache to the incorrect type + # and checking that it is the cached value that is used. + property_page._type = 'wikibase-item' + claim = property_page.newClaim() + self.assertEqual(claim.type, 'wikibase-item') + + def test_as_target(self): + """Test that PropertyPage can be used as a value.""" + wikidata = self.get_repo() + property_page = PropertyPage(wikidata, 'P1687') + claim = property_page.newClaim() + claim.setTarget(property_page) + self.assertEqual(claim.type, 'wikibase-property') + self.assertEqual(claim.target, property_page) + + @unittest.expectedFailure + def test_exists(self): + """Test the exists method of PropertyPage.""" + wikidata = self.get_repo() + property_page = PropertyPage(wikidata, 'P1687') + self.assertTrue(property_page.exists()) + # Retry with cached _content. + self.assertTrue(property_page.exists()) + + +class TestClaim(WikidataTestCase): + + """Test Claim object functionality.""" + + def test_claim_eq_simple(self): + """Test comparing two claims. + + If they have the same property and value, they are equal. + """ + wikidata = self.get_repo() + claim1 = pywikibot.Claim(wikidata, 'P31') + claim1.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) + claim2 = pywikibot.Claim(wikidata, 'P31') + claim2.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) + self.assertEqual(claim1, claim2) + self.assertEqual(claim2, claim1) + + def test_claim_eq_simple_different_value(self): + """Test comparing two claims. + + If they have the same property and different values, + they are not equal. + """ + wikidata = self.get_repo() + claim1 = pywikibot.Claim(wikidata, 'P31') + claim1.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) + claim2 = pywikibot.Claim(wikidata, 'P31') + claim2.setTarget(pywikibot.ItemPage(wikidata, 'Q1')) + self.assertNotEqual(claim1, claim2) + self.assertNotEqual(claim2, claim1) + + def test_claim_eq_simple_different_rank(self): + """Test comparing two claims. + + If they have the same property and value and different ranks, + they are equal. + """ + wikidata = self.get_repo() + claim1 = pywikibot.Claim(wikidata, 'P31') + claim1.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) + claim1.setRank('preferred') + claim2 = pywikibot.Claim(wikidata, 'P31') + claim2.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) + self.assertEqual(claim1, claim2) + self.assertEqual(claim2, claim1) + + def test_claim_eq_simple_different_snaktype(self): + """Test comparing two claims. + + If they have the same property and different snaktypes, + they are not equal. + """ + wikidata = self.get_repo() + claim1 = pywikibot.Claim(wikidata, 'P31') + claim1.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) + claim2 = pywikibot.Claim(wikidata, 'P31') + claim2.setSnakType('novalue') + self.assertNotEqual(claim1, claim2) + self.assertNotEqual(claim2, claim1) + + def test_claim_eq_simple_different_property(self): + """Test comparing two claims. + + If they have the same value and different properties, + they are not equal. + """ + wikidata = self.get_repo() + claim1 = pywikibot.Claim(wikidata, 'P31') + claim1.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) + claim2 = pywikibot.Claim(wikidata, 'P21') + claim2.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) + self.assertNotEqual(claim1, claim2) + self.assertNotEqual(claim2, claim1) + + def test_claim_eq_with_qualifiers(self): + """Test comparing two claims. + + If they have the same property, value and qualifiers, they are equal. + """ + wikidata = self.get_repo() + claim1 = pywikibot.Claim(wikidata, 'P31') + claim1.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) + qualifier1 = pywikibot.Claim(wikidata, 'P214', is_qualifier=True) + qualifier1.setTarget('foo') + claim1.addQualifier(qualifier1) + claim2 = pywikibot.Claim(wikidata, 'P31') + claim2.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) + qualifier2 = pywikibot.Claim(wikidata, 'P214', is_qualifier=True) + qualifier2.setTarget('foo') + claim2.addQualifier(qualifier2) + self.assertEqual(claim1, claim2) + self.assertEqual(claim2, claim1) + + def test_claim_eq_with_different_qualifiers(self): + """Test comparing two claims. + + If they have the same property and value and different qualifiers, + they are not equal. + """ + wikidata = self.get_repo() + claim1 = pywikibot.Claim(wikidata, 'P31') + claim1.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) + qualifier1 = pywikibot.Claim(wikidata, 'P214', is_qualifier=True) + qualifier1.setTarget('foo') + claim1.addQualifier(qualifier1) + claim2 = pywikibot.Claim(wikidata, 'P31') + claim2.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) + qualifier2 = pywikibot.Claim(wikidata, 'P214', is_qualifier=True) + qualifier2.setTarget('bar') + claim2.addQualifier(qualifier2) + self.assertNotEqual(claim1, claim2) + self.assertNotEqual(claim2, claim1) + + def test_claim_eq_one_without_qualifiers(self): + """Test comparing two claims. + + If they have the same property and value and one of them has + no qualifiers while the other one does, they are not equal. + """ + wikidata = self.get_repo() + claim1 = pywikibot.Claim(wikidata, 'P31') + claim1.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) + qualifier = pywikibot.Claim(wikidata, 'P214', is_qualifier=True) + qualifier.setTarget('foo') + claim1.addQualifier(qualifier) + claim2 = pywikibot.Claim(wikidata, 'P31') + claim2.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) + self.assertNotEqual(claim1, claim2) + self.assertNotEqual(claim2, claim1) + + def test_claim_eq_with_different_sources(self): + """Test comparing two claims. + + If they have the same property and value and different sources, + they are equal. + """ + wikidata = self.get_repo() + claim1 = pywikibot.Claim(wikidata, 'P31') + claim1.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) + source1 = pywikibot.Claim(wikidata, 'P143', is_reference=True) + source1.setTarget(pywikibot.ItemPage(wikidata, 'Q328')) + claim1.addSource(source1) + claim2 = pywikibot.Claim(wikidata, 'P31') + claim2.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) + source2 = pywikibot.Claim(wikidata, 'P143', is_reference=True) + source2.setTarget(pywikibot.ItemPage(wikidata, 'Q48183')) + claim2.addSource(source2) + self.assertEqual(claim1, claim2) + self.assertEqual(claim2, claim1) + + def test_claim_copy_is_equal(self): + """Test making a copy of a claim. + + The copy of a claim should be always equal to the claim. + """ + wikidata = self.get_repo() + claim = pywikibot.Claim(wikidata, 'P31') + claim.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) + qualifier = pywikibot.Claim(wikidata, 'P214', is_qualifier=True) + qualifier.setTarget('foo') + source = pywikibot.Claim(wikidata, 'P143', is_reference=True) + source.setTarget(pywikibot.ItemPage(wikidata, 'Q328')) + claim.addQualifier(qualifier) + claim.addSource(source) + copy = claim.copy() + self.assertEqual(claim, copy) + + def test_claim_copy_is_equal_qualifier(self): + """Test making a copy of a claim. + + The copy of a qualifier should be always equal to the qualifier. + """ + wikidata = self.get_repo() + qualifier = pywikibot.Claim(wikidata, 'P214', is_qualifier=True) + qualifier.setTarget('foo') + copy = qualifier.copy() + self.assertEqual(qualifier, copy) + self.assertTrue(qualifier.isQualifier) + self.assertTrue(copy.isQualifier) + + def test_claim_copy_is_equal_source(self): + """Test making a copy of a claim. + + The copy of a source should be always equal to the source. + """ + wikidata = self.get_repo() + source = pywikibot.Claim(wikidata, 'P143', is_reference=True) + source.setTarget(pywikibot.ItemPage(wikidata, 'Q328')) + copy = source.copy() + self.assertEqual(source, copy) + self.assertTrue(source.isReference) + self.assertTrue(copy.isReference) + + +class TestClaimSetValue(WikidataTestCase): + + """Test setting claim values.""" + + def test_set_website(self): + """Test setting claim of url type.""" + wikidata = self.get_repo() + claim = pywikibot.Claim(wikidata, 'P856') + self.assertEqual(claim.type, 'url') + target = 'https://en.wikipedia.org/' + claim.setTarget(target) + self.assertEqual(claim.target, target) + + def test_set_WbMonolingualText(self): + """Test setting claim of monolingualtext type.""" + wikidata = self.get_repo() + claim = pywikibot.Claim(wikidata, 'P1450') + self.assertEqual(claim.type, 'monolingualtext') + target = pywikibot.WbMonolingualText(text='Test this!', language='en') + claim.setTarget(target) + self.assertEqual(claim.target, target) + + def test_set_WbQuantity(self): + """Test setting claim of quantity type.""" + wikidata = self.get_repo() + claim = pywikibot.Claim(wikidata, 'P1106') + self.assertEqual(claim.type, 'quantity') + target = pywikibot.WbQuantity( + amount=1234, error=1, unit='http://www.wikidata.org/entity/Q11573') + claim.setTarget(target) + self.assertEqual(claim.target, target) + + def test_set_math(self): + """Test setting claim of math type.""" + wikidata = self.get_repo() + claim = pywikibot.Claim(wikidata, 'P2535') + self.assertEqual(claim.type, 'math') + target = 'a^2 + b^2 = c^2' + claim.setTarget(target) + self.assertEqual(claim.target, target) + + def test_set_identifier(self): + """Test setting claim of external-id type.""" + wikidata = self.get_repo() + claim = pywikibot.Claim(wikidata, 'P214') + self.assertEqual(claim.type, 'external-id') + target = 'Any string is a valid identifier' + claim.setTarget(target) + self.assertEqual(claim.target, target) + + def test_set_date(self): + """Test setting claim of time type.""" + wikidata = self.get_repo() + claim = pywikibot.Claim(wikidata, 'P569') + self.assertEqual(claim.type, 'time') + claim.setTarget(pywikibot.WbTime( + year=2001, month=1, day=1, site=wikidata)) + self.assertEqual(claim.target.year, 2001) + self.assertEqual(claim.target.month, 1) + self.assertEqual(claim.target.day, 1) + + def test_set_musical_notation(self): + """Test setting claim of musical-notation type.""" + wikidata = self.get_repo() + claim = pywikibot.Claim(wikidata, 'P6604') + self.assertEqual(claim.type, 'musical-notation') + target = "\\relative c' { c d e f | g2 g | a4 a a a | g1 |}" + claim.setTarget(target) + self.assertEqual(claim.target, target) + + def test_set_incorrect_target_value(self): + """Test setting claim of the incorrect value.""" + wikidata = self.get_repo() + date_claim = pywikibot.Claim(wikidata, 'P569') + regex = r' is not type .+\.$' + with self.assertRaisesRegex(ValueError, regex): + date_claim.setTarget('foo') + url_claim = pywikibot.Claim(wikidata, 'P856') + with self.assertRaisesRegex(ValueError, regex): + url_claim.setTarget(pywikibot.WbTime(2001, site=wikidata)) + mono_claim = pywikibot.Claim(wikidata, 'P1450') + with self.assertRaisesRegex(ValueError, regex): + mono_claim.setTarget('foo') + quantity_claim = pywikibot.Claim(wikidata, 'P1106') + with self.assertRaisesRegex(ValueError, regex): + quantity_claim.setTarget('foo') + + +class TestItemBasePageMethods(WikidataTestCase, BasePageMethodsTestBase): + + """Test behavior of ItemPage methods inherited from BasePage.""" + + def setUp(self): + """Setup tests.""" + self._page = ItemPage(self.get_repo(), 'Q60') + super().setUp() + + def test_basepage_methods(self): + """Test ItemPage methods inherited from superclass BasePage.""" + self._test_invoke() + self._test_no_wikitext() + + def test_item_is_hashable(self): + """Ensure that ItemPages are hashable.""" + list_of_dupes = [self._page, self._page] + self.assertLength(set(list_of_dupes), 1) + + +class TestPageMethodsWithItemTitle(WikidataTestCase, BasePageMethodsTestBase): + + """Test behavior of Page methods for wikibase item.""" + + def setUp(self): + """Setup tests.""" + self._page = pywikibot.Page(self.site, 'Q60') + super().setUp() + + def test_basepage_methods(self): + """Test Page methods inherited from superclass BasePage with Q60.""" + self._test_invoke() + self._test_no_wikitext() + + +class TestLinks(WikidataTestCase): + + """Test cases to test links stored in Wikidata. + + Uses a stored data file for the wikibase item. + However wikibase creates site objects for each sitelink, and the unit test + directly creates a Site for 'wikipedia:af' to use in a comparison. + """ + + sites = { + 'wikidata': { + 'family': 'wikidata', + 'code': 'wikidata', + }, + 'afwiki': { + 'family': 'wikipedia', + 'code': 'af', + } + } + + def setUp(self): + """Setup Tests.""" + super().setUp() + self.wdp = ItemPage(self.get_repo(), 'Q60') + self.wdp.id = 'Q60' + with open(join_pages_path('Q60_only_sitelinks.wd')) as f: + self.wdp._content = json.load(f) + self.wdp.get() + + def test_iterlinks_page_object(self): + """Test iterlinks for page objects.""" + page = next(pg for pg in self.wdp.iterlinks() if pg.site.code == 'af') + self.assertEqual(page, pywikibot.Page(self.get_site('afwiki'), + 'New York Stad')) + + def test_iterlinks_filtering(self): + """Test iterlinks for a given family.""" + wikilinks = list(self.wdp.iterlinks('wikipedia')) + wvlinks = list(self.wdp.iterlinks('wikivoyage')) + + self.assertLength(wikilinks, 3) + self.assertLength(wvlinks, 2) + + +class TestWriteNormalizeData(TestCase): + + """Test cases for routines that normalize data for writing to Wikidata. + + Exercises ItemPage._normalizeData with data that is not normalized + and data which is already normalized. + """ + + net = False + + def setUp(self): + """Setup tests.""" + super().setUp() + self.data_out = { + 'labels': {'en': {'language': 'en', 'value': 'Foo'}}, + 'descriptions': {'en': {'language': 'en', 'value': 'Desc'}}, + 'aliases': {'en': [ + {'language': 'en', 'value': 'Bah'}, + {'language': 'en', 'value': 'Bar', 'remove': ''}, + ]}, + } + + def test_normalize_data(self): + """Test _normalizeData() method.""" + data_in = { + 'labels': {'en': 'Foo'}, + 'descriptions': {'en': 'Desc'}, + 'aliases': {'en': [ + 'Bah', + {'language': 'en', 'value': 'Bar', 'remove': ''}, + ]}, + } + + response = ItemPage._normalizeData(data_in) + self.assertEqual(response, self.data_out) + + def test_normalized_data(self): + """Test _normalizeData() method for normalized data.""" + response = ItemPage._normalizeData( + copy.deepcopy(self.data_out)) + self.assertEqual(response, self.data_out) + + +class TestPreloadingEntityGenerator(TestCase): + + """Test preloading item generator.""" + + sites = { + 'wikidata': { + 'family': 'wikidata', + 'code': 'wikidata', + }, + 'enwiki': { + 'family': 'wikipedia', + 'code': 'en', + } + } + + def test_non_item_gen(self): + """Test PreloadingEntityGenerator with getReferences().""" + site = self.get_site('wikidata') + page = pywikibot.Page(site, 'Property:P31') + ref_gen = page.getReferences(follow_redirects=False, total=5) + gen = pagegenerators.PreloadingEntityGenerator(ref_gen) + for item in gen: + self.assertIsInstance(item, ItemPage) + + def test_foreign_page_item_gen(self): + """Test PreloadingEntityGenerator with connected pages.""" + site = self.get_site('enwiki') + page_gen = [pywikibot.Page(site, 'Main Page'), + pywikibot.Page(site, 'New York City')] + gen = pagegenerators.PreloadingEntityGenerator(page_gen) + for item in gen: + self.assertIsInstance(item, ItemPage) + + +class TestNamespaces(WikidataTestCase): + + """Test cases to test namespaces of Wikibase entities.""" + + def test_empty_wikibase_page(self): + """Test empty wikibase page. + + As a base class it should be able to instantiate + it with minimal arguments + """ + wikidata = self.get_repo() + page = WikibasePage(wikidata) + regex = r' object has no attribute ' + with self.assertRaisesRegex(AttributeError, regex): + page.namespace() + page = WikibasePage(wikidata, title='') + with self.assertRaisesRegex(AttributeError, regex): + page.namespace() + + page = WikibasePage(wikidata, ns=0) + self.assertEqual(page.namespace(), 0) + page = WikibasePage(wikidata, entity_type='item') + self.assertEqual(page.namespace(), 0) + + page = WikibasePage(wikidata, ns=120) + self.assertEqual(page.namespace(), 120) + page = WikibasePage(wikidata, title='', ns=120) + self.assertEqual(page.namespace(), 120) + page = WikibasePage(wikidata, entity_type='property') + self.assertEqual(page.namespace(), 120) + + # mismatch in namespaces + regex = r'^Namespace ".+" is not valid for Wikibase entity type ".+"$' + with self.assertRaisesRegex(ValueError, regex): + WikibasePage(wikidata, ns=0, entity_type='property') + with self.assertRaisesRegex(ValueError, regex): + WikibasePage(wikidata, ns=120, entity_type='item') + + def test_wikibase_link_namespace(self): + """Test the title resolved to a namespace correctly.""" + wikidata = self.get_repo() + # title without any namespace clues (ns or entity_type) + # should verify the Link namespace is appropriate + page = WikibasePage(wikidata, title='Q6') + self.assertEqual(page.namespace(), 0) + page = WikibasePage(wikidata, title='Property:P60') + self.assertEqual(page.namespace(), 120) + + def test_wikibase_namespace_selection(self): + """Test various ways to correctly specify the namespace.""" + wikidata = self.get_repo() + + page = ItemPage(wikidata, 'Q60') + self.assertEqual(page.namespace(), 0) + page.get() + + page = ItemPage(wikidata, title='Q60') + self.assertEqual(page.namespace(), 0) + page.get() + + page = WikibasePage(wikidata, title='Q60', ns=0) + self.assertEqual(page.namespace(), 0) + page.get() + + page = WikibasePage(wikidata, title='Q60', + entity_type='item') + self.assertEqual(page.namespace(), 0) + page.get() + + page = PropertyPage(wikidata, 'Property:P6') + self.assertEqual(page.namespace(), 120) + page.get() + + page = PropertyPage(wikidata, 'P6') + self.assertEqual(page.namespace(), 120) + page.get() + + page = WikibasePage(wikidata, title='Property:P6') + self.assertEqual(page.namespace(), 120) + page.get() + + page = WikibasePage(wikidata, title='P6', ns=120) + self.assertEqual(page.namespace(), 120) + page.get() + + page = WikibasePage(wikidata, title='P6', + entity_type='property') + self.assertEqual(page.namespace(), 120) + page.get() + + def test_wrong_namespaces(self): + """Test incorrect namespaces for Wikibase entities.""" + wikidata = self.get_repo() + # All subclasses of WikibasePage raise a ValueError + # if the namespace for the page title is not correct + regex = r': Namespace ".+" is not valid$' + with self.assertRaisesRegex(ValueError, regex): + WikibasePage(wikidata, title='Wikidata:Main Page') + regex = r"^'.+' is not in the namespace " + with self.assertRaisesRegex(ValueError, regex): + ItemPage(wikidata, 'File:Q1') + with self.assertRaisesRegex(ValueError, regex): + PropertyPage(wikidata, 'File:P60') + + def test_item_unknown_namespace(self): + """Test unknown namespaces for Wikibase entities.""" + # The 'Invalid:' is not a known namespace, so is parsed to be + # part of the title in namespace 0 + # TODO: These items have inappropriate titles, which should + # raise an error. + wikidata = self.get_repo() + regex = r"^'.+' is not a valid item page title$" + with self.assertRaisesRegex(InvalidTitleError, regex): + ItemPage(wikidata, 'Invalid:Q1') + + +class TestAlternateNamespaces(WikidataTestCase): + + """Test cases to test namespaces of Wikibase entities.""" + + cached = False + dry = True + + @classmethod + def setUpClass(cls): + """Setup test class.""" + super().setUpClass() + + cls.get_repo()._namespaces = NamespacesDict({ + 90: Namespace(id=90, + case='first-letter', + canonical_name='Item', + defaultcontentmodel='wikibase-item'), + 92: Namespace(id=92, + case='first-letter', + canonical_name='Prop', + defaultcontentmodel='wikibase-property') + }) + + def test_alternate_item_namespace(self): + """Test alternate item namespace.""" + item = ItemPage(self.repo, 'Q60') + self.assertEqual(item.namespace(), 90) + self.assertEqual(item.id, 'Q60') + self.assertEqual(item.title(), 'Item:Q60') + self.assertEqual(item._defined_by(), {'ids': 'Q60'}) + + item = ItemPage(self.repo, 'Item:Q60') + self.assertEqual(item.namespace(), 90) + self.assertEqual(item.id, 'Q60') + self.assertEqual(item.title(), 'Item:Q60') + self.assertEqual(item._defined_by(), {'ids': 'Q60'}) + + def test_alternate_property_namespace(self): + """Test alternate property namespace.""" + prop = PropertyPage(self.repo, 'P21') + self.assertEqual(prop.namespace(), 92) + self.assertEqual(prop.id, 'P21') + self.assertEqual(prop.title(), 'Prop:P21') + self.assertEqual(prop._defined_by(), {'ids': 'P21'}) + + prop = PropertyPage(self.repo, 'Prop:P21') + self.assertEqual(prop.namespace(), 92) + self.assertEqual(prop.id, 'P21') + self.assertEqual(prop.title(), 'Prop:P21') + self.assertEqual(prop._defined_by(), {'ids': 'P21'}) + + +class TestOwnClient(TestCase): + + """Test that a data repository family can be its own client.""" + + sites = { + # The main Wikidata is its own client. + 'wikidata': { + 'family': 'wikidata', + 'code': 'wikidata', + 'item': 'Q32119', + }, + # test.wikidata is also + 'wikidatatest': { + 'family': 'wikidata', + 'code': 'test', + 'item': 'Q33', + }, + } + + def test_own_client(self, key): + """Test that a data repository family can be its own client.""" + site = self.get_site(key) + page = self.get_mainpage(site) + item = ItemPage.fromPage(page) + self.assertEqual(page.site, site) + self.assertEqual(item.site, site) + + def test_page_from_repository(self, key): + """Test that page_from_repository method works for wikibase too.""" + site = self.get_site(key) + page = site.page_from_repository('Q5296') + self.assertEqual(page, self.get_mainpage(site)) + + def test_redirect_from_repository(self, key): + """Test page_from_repository method with redirects.""" + site = self.get_site(key) + item = self.sites[key]['item'] + with self.assertRaisesRegex( + IsRedirectPageError, + fr'{self.sites[key]["item"]}\]\] is a redirect'): + site.page_from_repository(item) + + +class TestUnconnectedClient(TestCase): + + """Test clients not connected to a data repository.""" + + sites = { + # Wikispecies is not supported by Wikidata yet. + 'species': { + 'family': 'species', + 'code': 'species', + 'page_title': 'Main Page', + }, + # fr.wiktionary is not supported by Wikidata yet. + 'frwikt': { + 'family': 'wiktionary', + 'code': 'fr', + 'page_title': 'and', + }, + } + + dry = True + + def test_not_supported_family(self, key): + """Test that family without a data repository causes error.""" + site = self.get_site(key) + + self.wdp = pywikibot.Page(site, self.sites[key]['page_title']) + regex = r' has no data repository$' + with self.assertRaisesRegex(WikiBaseError, regex): + ItemPage.fromPage(self.wdp) + with self.assertRaisesRegex(WikiBaseError, regex): + self.wdp.data_item() + + def test_has_data_repository(self, key): + """Test that site has no data repository.""" + site = self.get_site(key) + self.assertFalse(site.has_data_repository) + + def test_page_from_repository_fails(self, key): + """Test that page_from_repository method fails.""" + site = self.get_site(key) + dummy_item = 'Q1' + regex = r'^Wikibase is not implemented for .+\.$' + with self.assertRaisesRegex(UnknownExtensionError, regex): + site.page_from_repository(dummy_item) + + +class TestJSON(WikidataTestCase): + + """Test cases to test toJSON() functions.""" + + def setUp(self): + """Setup test.""" + super().setUp() + wikidata = self.get_repo() + self.wdp = ItemPage(wikidata, 'Q60') + self.wdp.id = 'Q60' + with open(join_pages_path('Q60.wd')) as f: + self.wdp._content = json.load(f) + self.wdp.get() + del self.wdp._content['id'] + del self.wdp._content['type'] + del self.wdp._content['lastrevid'] + del self.wdp._content['pageid'] + + def test_itempage_json(self): + """Test itempage json.""" + old = json.dumps(self.wdp._content, indent=2, sort_keys=True) + new = json.dumps(self.wdp.toJSON(), indent=2, sort_keys=True) + + self.assertEqual(old, new) + + def test_json_diff(self): + """Test json diff.""" + del self.wdp.labels['en'] + self.wdp.aliases['de'].append('New York') + self.wdp.aliases['de'].append('foo') + self.wdp.aliases['de'].remove('NYC') + del self.wdp.aliases['nl'] + del self.wdp.claims['P213'] + del self.wdp.sitelinks['afwiki'] + self.wdp.sitelinks['nlwiki']._badges = set() + expected = { + 'labels': { + 'en': { + 'language': 'en', + 'value': '' + } + }, + 'aliases': { + 'de': [ + {'language': 'de', 'value': 'City of New York'}, + {'language': 'de', 'value': 'The Big Apple'}, + {'language': 'de', 'value': 'New York'}, + {'language': 'de', 'value': 'New York'}, + {'language': 'de', 'value': 'foo'}, + ], + 'nl': [ + {'language': 'nl', 'value': 'New York', 'remove': ''}, + ], + }, + 'claims': { + 'P213': [ + { + 'id': 'Q60$0427a236-4120-7d00-fa3e-e23548d4c02d', + 'remove': '' + } + ] + }, + 'sitelinks': { + 'afwiki': { + 'site': 'afwiki', + 'title': '', + }, + 'nlwiki': { + 'site': 'nlwiki', + 'title': 'New York City', + 'badges': [''] + } + } + } + diff = self.wdp.toJSON(diffto=self.wdp._content) + self.assertEqual(diff, expected) + + +if __name__ == '__main__': + with suppress(SystemExit): + unittest.main() From da9245562e7a2fd53094785c7bfd0a00f9d63d95 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Wed, 6 Nov 2024 13:38:14 +0100 Subject: [PATCH 27/95] [tests] Split wikibase_tests (step3) derive wbtypes_tests.py Change-Id: I33bd325c2b24152ed808e0b311626d07acfb2f05 --- tests/wbtypes_tests.py | 1488 +-------------------------------------- tests/wikibase_tests.py | 1018 +------------------------- tox.ini | 1 + 3 files changed, 6 insertions(+), 2501 deletions(-) mode change 100644 => 100755 tests/wbtypes_tests.py diff --git a/tests/wbtypes_tests.py b/tests/wbtypes_tests.py old mode 100644 new mode 100755 index 413ab3b363..e3cf190b51 --- a/tests/wbtypes_tests.py +++ b/tests/wbtypes_tests.py @@ -7,45 +7,16 @@ # from __future__ import annotations -import copy import datetime -import json import operator import unittest from contextlib import suppress from decimal import Decimal import pywikibot -from pywikibot import pagegenerators -from pywikibot.exceptions import ( - InvalidTitleError, - IsNotRedirectPageError, - IsRedirectPageError, - NoPageError, - UnknownExtensionError, - WikiBaseError, -) -from pywikibot.page import ItemPage, Page, PropertyPage, WikibasePage -from pywikibot.site import Namespace, NamespacesDict -from pywikibot.tools import MediaWikiVersion, suppress_warnings -from tests import WARN_SITE_CODE, join_pages_path -from tests.aspects import TestCase, WikidataTestCase -from tests.basepage import ( - BasePageLoadRevisionsCachingTestBase, - BasePageMethodsTestBase, -) - - -# fetch a page which is very likely to be unconnected, which doesn't have -# a generator, and unit tests may be used to test old versions of pywikibot -def _get_test_unconnected_page(site): - """Get unconnected page from site for tests.""" - gen = pagegenerators.NewpagesPageGenerator(site=site, total=10, - namespaces=[1]) - for page in gen: - if not page.properties().get('wikibase_item'): - return page - return None # pragma: no cover +from pywikibot.page import ItemPage, Page +from pywikibot.tools import MediaWikiVersion +from tests.aspects import WikidataTestCase class WbRepresentationTestCase(WikidataTestCase): @@ -58,70 +29,6 @@ def _test_hashable(self, representation): self.assertLength(set(list_of_dupes), 1) -class TestLoadRevisionsCaching(BasePageLoadRevisionsCachingTestBase, - WikidataTestCase): - - """Test site.loadrevisions() caching.""" - - def setUp(self): - """Setup test.""" - self._page = ItemPage(self.get_repo(), 'Q15169668') - super().setUp() - - def test_page_text(self): - """Test site.loadrevisions() with Page.text.""" - with suppress_warnings(WARN_SITE_CODE, category=UserWarning): - self._test_page_text() - - -class TestGeneral(WikidataTestCase): - - """General Wikibase tests.""" - - @classmethod - def setUpClass(cls): - """Setup test class.""" - super().setUpClass() - enwiki = pywikibot.Site('en', 'wikipedia') - cls.mainpage = pywikibot.Page(pywikibot.page.Link('Main Page', enwiki)) - - def testWikibase(self): - """Wikibase tests.""" - repo = self.get_repo() - item_namespace = repo.namespaces[0] - self.assertEqual(item_namespace.defaultcontentmodel, 'wikibase-item') - item = ItemPage.fromPage(self.mainpage) - self.assertIsInstance(item, ItemPage) - self.assertEqual(item.getID(), 'Q5296') - self.assertEqual(item.title(), 'Q5296') - self.assertIn('en', item.labels) - self.assertTrue( - item.labels['en'].lower().endswith('main page'), - msg=f"\nitem.labels['en'] of item Q5296 is {item.labels['en']!r}") - self.assertIn('en', item.aliases) - self.assertIn('home page', (a.lower() for a in item.aliases['en'])) - self.assertEqual(item.namespace(), 0) - item2 = ItemPage(repo, 'q5296') - self.assertEqual(item2.getID(), 'Q5296') - item2.get() - self.assertTrue(item2.labels['en'].lower().endswith('main page')) - prop = PropertyPage(repo, 'Property:P21') - self.assertEqual(prop.type, 'wikibase-item') - self.assertEqual(prop.namespace(), 120) - claim = pywikibot.Claim(repo, 'p21') - regex = r' is not type .+\.$' - with self.assertRaisesRegex(ValueError, regex): - claim.setTarget(value='test') - claim.setTarget(ItemPage(repo, 'q1')) - self.assertEqual(claim._formatValue(), {'entity-type': 'item', - 'numeric-id': 1}) - - def test_cmp(self): - """Test WikibasePage comparison.""" - self.assertEqual(ItemPage.fromPage(self.mainpage), - ItemPage(self.get_repo(), 'q5296')) - - class TestWikibaseCoordinate(WbRepresentationTestCase): """Test Wikibase Coordinate data type.""" @@ -941,54 +848,6 @@ def test_WbMonolingualText_errors(self): pywikibot.WbMonolingualText(text=None, language='sv') -class TestWikibaseParser(WikidataTestCase): - - """Test passing various datatypes to wikibase parser.""" - - def test_wbparse_strings(self): - """Test that strings return unchanged.""" - test_list = ['test string', 'second test'] - parsed_strings = self.site.parsevalue('string', test_list) - self.assertEqual(parsed_strings, test_list) - - def test_wbparse_time(self): - """Test parsing of a time value.""" - parsed_date = self.site.parsevalue( - 'time', ['1994-02-08'], {'precision': 9})[0] - self.assertEqual(parsed_date['time'], '+1994-02-08T00:00:00Z') - self.assertEqual(parsed_date['precision'], 9) - - def test_wbparse_quantity(self): - """Test parsing of quantity values.""" - parsed_quantities = self.site.parsevalue( - 'quantity', - ['1.90e-9+-0.20e-9', '1000000.00000000054321+-0', '-123+-1', - '2.70e34+-1e32']) - self.assertEqual(parsed_quantities[0]['amount'], '+0.00000000190') - self.assertEqual(parsed_quantities[0]['upperBound'], '+0.00000000210') - self.assertEqual(parsed_quantities[0]['lowerBound'], '+0.00000000170') - self.assertEqual(parsed_quantities[1]['amount'], - '+1000000.00000000054321') - self.assertEqual(parsed_quantities[1]['upperBound'], - '+1000000.00000000054321') - self.assertEqual(parsed_quantities[1]['lowerBound'], - '+1000000.00000000054321') - self.assertEqual(parsed_quantities[2]['amount'], '-123') - self.assertEqual(parsed_quantities[2]['upperBound'], '-122') - self.assertEqual(parsed_quantities[2]['lowerBound'], '-124') - self.assertEqual(parsed_quantities[3]['amount'], - '+27000000000000000000000000000000000') - self.assertEqual(parsed_quantities[3]['upperBound'], - '+27100000000000000000000000000000000') - self.assertEqual(parsed_quantities[3]['lowerBound'], - '+26900000000000000000000000000000000') - - def test_wbparse_raises_valueerror(self): - """Test invalid value condition.""" - with self.assertRaises(ValueError): - self.site.parsevalue('quantity', ['Not a quantity']) - - class TestWbGeoShapeNonDry(WbRepresentationTestCase): """Test Wikibase WbGeoShape data type (non-dry). @@ -1171,1347 +1030,6 @@ def test_WbUnknown_fromWikibase(self): {'text': 'Test this!', 'language': 'en'}) -class TestLoadUnknownType(WikidataTestCase): - - """Test unknown datatypes being loaded as WbUnknown.""" - - dry = True - - def setUp(self): - """Setup test.""" - super().setUp() - wikidata = self.get_repo() - self.wdp = ItemPage(wikidata, 'Q60') - self.wdp.id = 'Q60' - with open(join_pages_path('Q60_unknown_datatype.wd')) as f: - self.wdp._content = json.load(f) - - def test_load_unknown(self): - """Ensure unknown value is loaded but raises a warning.""" - self.wdp.get() - unknown_value = self.wdp.claims['P99999'][0].getTarget() - self.assertIsInstance(unknown_value, pywikibot.WbUnknown) - self.assertEqual(unknown_value.warning, - 'foo-unknown-bar datatype is not supported yet.') - - -class TestItemPageExtensibility(TestCase): - - """Test ItemPage extensibility.""" - - family = 'wikipedia' - code = 'en' - - dry = True - - def test_ItemPage_extensibility(self): - """Test ItemPage extensibility.""" - class MyItemPage(ItemPage): - - """Dummy ItemPage subclass.""" - - page = pywikibot.Page(self.site, 'foo') - self.assertIsInstance(MyItemPage.fromPage(page, lazy_load=True), - MyItemPage) - - -class TestItemLoad(WikidataTestCase): - - """Test item creation. - - Tests for item creation include: - 1. by Q id - 2. ItemPage.fromPage(page) - 3. ItemPage.fromPage(page_with_props_loaded) - 4. ItemPage.from_entity_uri(site, uri) - - Test various invalid scenarios: - 1. invalid Q ids - 2. invalid pages to fromPage - 3. missing pages to fromPage - 4. unconnected pages to fromPage - """ - - sites = { - 'wikidata': { - 'family': 'wikidata', - 'code': 'wikidata', - }, - 'enwiki': { - 'family': 'wikipedia', - 'code': 'en', - } - } - - @classmethod - def setUpClass(cls): - """Setup test class.""" - super().setUpClass() - cls.site = cls.get_site('enwiki') - - def setUp(self): - """Setup test.""" - super().setUp() - self.nyc = pywikibot.Page(pywikibot.page.Link('New York City', - self.site)) - - def test_item_normal(self): - """Test normal wikibase item.""" - wikidata = self.get_repo() - item = ItemPage(wikidata, 'Q60') - self.assertEqual(item._link._title, 'Q60') - self.assertEqual(item._defined_by(), {'ids': 'Q60'}) - self.assertEqual(item.id, 'Q60') - self.assertFalse(hasattr(item, '_title')) - self.assertFalse(hasattr(item, '_site')) - self.assertEqual(item.title(), 'Q60') - self.assertEqual(item.getID(), 'Q60') - self.assertEqual(item.getID(numeric=True), 60) - self.assertFalse(hasattr(item, '_content')) - item.get() - self.assertTrue(hasattr(item, '_content')) - - def test_item_lazy_initialization(self): - """Test that Wikibase items are properly initialized lazily.""" - wikidata = self.get_repo() - item = ItemPage(wikidata, 'Q60') - attrs = ['_content', 'labels', 'descriptions', 'aliases', - 'claims', 'sitelinks'] - for attr in attrs: - with self.subTest(attr=attr, note='before loading'): - # hasattr() loads the attributes; use item.__dict__ for tests - self.assertNotIn(attr, item.__dict__) - - item.labels # trigger loading - for attr in attrs: - with self.subTest(attr=attr, note='after loading'): - self.assertIn(attr, item.__dict__) - - def test_load_item_set_id(self): - """Test setting item.id attribute on empty item.""" - wikidata = self.get_repo() - item = ItemPage(wikidata, '-1') - self.assertEqual(item._link._title, '-1') - item.id = 'Q60' - self.assertFalse(hasattr(item, '_content')) - self.assertEqual(item.getID(), 'Q60') - self.assertFalse(hasattr(item, '_content')) - item.get() - self.assertTrue(hasattr(item, '_content')) - self.assertIn('en', item.labels) - self.assertEqual(item.labels['en'], 'New York City') - self.assertEqual(item.title(), 'Q60') - - def test_reuse_item_set_id(self): - """Test modifying item.id attribute. - - Some scripts are using item.id = 'Q60' semantics, which does work - but modifying item.id does not currently work, and this test - highlights that it breaks silently. - """ - wikidata = self.get_repo() - item = ItemPage(wikidata, 'Q60') - item.get() - self.assertEqual(item.labels['en'], 'New York City') - - # When the id attribute is modified, the ItemPage goes into - # an inconsistent state. - item.id = 'Q5296' - # The title is updated correctly - self.assertEqual(item.title(), 'Q5296') - - # This del has no effect on the test; it is here to demonstrate that - # it doesn't help to clear this piece of saved state. - del item._content - # The labels are not updated; assertion showing undesirable behaviour: - self.assertEqual(item.labels['en'], 'New York City') - # TODO: This is the assertion that this test should be using: - # self.assertTrue(item.labels['en'].lower().endswith('main page')) - - def test_empty_item(self): - """Test empty wikibase item. - - should not raise an error as the constructor only requires - the site parameter, with the title parameter defaulted to None. - """ - wikidata = self.get_repo() - item = ItemPage(wikidata) - self.assertEqual(item._link._title, '-1') - self.assertLength(item.labels, 0) - self.assertLength(item.descriptions, 0) - self.assertLength(item.aliases, 0) - self.assertLength(item.claims, 0) - self.assertLength(item.sitelinks, 0) - - def test_item_invalid_titles(self): - """Test invalid titles of wikibase items.""" - wikidata = self.get_repo() - - regex = r"^'.+' is not a valid .+ page title$" - for title in ['null', 'NULL', 'None', - '-2', '1', '0', '+1', 'Q0', - 'Q0.5', 'Q', 'Q-1', 'Q+1']: - with self.assertRaisesRegex(InvalidTitleError, regex): - ItemPage(wikidata, title) - - regex = r"^Item's title cannot be empty$" - with self.assertRaisesRegex(InvalidTitleError, regex): - ItemPage(wikidata, '') - - def test_item_untrimmed_title(self): - """Test intrimmed titles of wikibase items. - - Spaces in the title should not cause an error. - """ - wikidata = self.get_repo() - item = ItemPage(wikidata, ' Q60 ') - self.assertEqual(item._link._title, 'Q60') - self.assertEqual(item.title(), 'Q60') - item.get() - - def test_item_missing(self): - """Test nmissing item.""" - wikidata = self.get_repo() - # this item has never existed - item = ItemPage(wikidata, 'Q7') - self.assertEqual(item._link._title, 'Q7') - self.assertEqual(item.title(), 'Q7') - self.assertFalse(hasattr(item, '_content')) - self.assertEqual(item.id, 'Q7') - self.assertEqual(item.getID(), 'Q7') - numeric_id = item.getID(numeric=True) - self.assertIsInstance(numeric_id, int) - self.assertEqual(numeric_id, 7) - self.assertFalse(hasattr(item, '_content')) - regex = r"^Page .+ doesn't exist\.$" - with self.assertRaisesRegex(NoPageError, regex): - item.get() - self.assertTrue(hasattr(item, '_content')) - self.assertEqual(item.id, 'Q7') - self.assertEqual(item.getID(), 'Q7') - self.assertEqual(item._link._title, 'Q7') - self.assertEqual(item.title(), 'Q7') - with self.assertRaisesRegex(NoPageError, regex): - item.get() - self.assertTrue(hasattr(item, '_content')) - self.assertEqual(item._link._title, 'Q7') - self.assertEqual(item.getID(), 'Q7') - self.assertEqual(item.title(), 'Q7') - - def test_item_never_existed(self): - """Test non-existent item.""" - wikidata = self.get_repo() - # this item has not been created - item = ItemPage(wikidata, 'Q9999999999999999999') - self.assertFalse(item.exists()) - self.assertEqual(item.getID(), 'Q9999999999999999999') - regex = r"^Page .+ doesn't exist\.$" - with self.assertRaisesRegex(NoPageError, regex): - item.get() - - def test_fromPage_noprops(self): - """Test item from page without properties.""" - page = self.nyc - item = ItemPage.fromPage(page) - self.assertEqual(item._link._title, '-1') - self.assertTrue(hasattr(item, 'id')) - self.assertTrue(hasattr(item, '_content')) - self.assertEqual(item.title(), 'Q60') - self.assertTrue(hasattr(item, '_content')) - self.assertEqual(item.id, 'Q60') - self.assertEqual(item.getID(), 'Q60') - self.assertEqual(item.getID(numeric=True), 60) - item.get() - self.assertTrue(item.exists()) - - def test_fromPage_noprops_with_section(self): - """Test item from page with section.""" - page = pywikibot.Page(self.nyc.site, self.nyc.title() + '#foo') - item = ItemPage.fromPage(page) - self.assertEqual(item._link._title, '-1') - self.assertTrue(hasattr(item, 'id')) - self.assertTrue(hasattr(item, '_content')) - self.assertEqual(item.title(), 'Q60') - self.assertTrue(hasattr(item, '_content')) - self.assertEqual(item.id, 'Q60') - self.assertEqual(item.getID(), 'Q60') - self.assertEqual(item.getID(numeric=True), 60) - item.get() - self.assertTrue(item.exists()) - - def test_fromPage_props(self): - """Test item from page with properties.""" - page = self.nyc - # fetch page properties - page.properties() - item = ItemPage.fromPage(page) - self.assertEqual(item._link._title, 'Q60') - self.assertEqual(item.id, 'Q60') - self.assertFalse(hasattr(item, '_content')) - self.assertEqual(item.title(), 'Q60') - self.assertFalse(hasattr(item, '_content')) - self.assertEqual(item.id, 'Q60') - self.assertEqual(item.getID(), 'Q60') - self.assertEqual(item.getID(numeric=True), 60) - self.assertFalse(hasattr(item, '_content')) - item.get() - self.assertTrue(hasattr(item, '_content')) - self.assertTrue(item.exists()) - item2 = ItemPage.fromPage(page) - self.assertTrue(item is item2) - - def test_fromPage_lazy(self): - """Test item from page with lazy_load.""" - page = pywikibot.Page(pywikibot.page.Link('New York City', self.site)) - item = ItemPage.fromPage(page, lazy_load=True) - self.assertEqual(item._defined_by(), - {'sites': 'enwiki', 'titles': 'New York City'}) - self.assertEqual(item._link._title, '-1') - self.assertFalse(hasattr(item, 'id')) - self.assertFalse(hasattr(item, '_content')) - self.assertEqual(item.title(), 'Q60') - self.assertTrue(hasattr(item, '_content')) - self.assertEqual(item.id, 'Q60') - self.assertEqual(item.getID(), 'Q60') - self.assertEqual(item.getID(numeric=True), 60) - item.get() - self.assertTrue(item.exists()) - - def _test_fromPage_noitem(self, link): - """Helper function to test a page without an associated item. - - It tests two of the ways to fetch an item: - 1. the Page already has props, which should contain an item id if - present, and that item id is used to instantiate the item, and - 2. the page doesn't have props, in which case the site&titles is - used to lookup the item id, but that lookup occurs after - instantiation, during the first attempt to use the data item. - """ - for props in [True, False]: - for method in ['title', 'get', 'getID', 'exists']: - page = pywikibot.Page(link) - if props: - page.properties() - - item = ItemPage.fromPage(page, lazy_load=True) - - self.assertFalse(hasattr(item, 'id')) - self.assertTrue(hasattr(item, '_title')) - self.assertTrue(hasattr(item, '_site')) - self.assertFalse(hasattr(item, '_content')) - - self.assertEqual(item._link._title, '-1') - # the method 'exists' does not raise an exception - if method == 'exists': - self.assertFalse(item.exists()) - else: - regex = r"^Page .+ doesn't exist\.$" - with self.assertRaisesRegex(NoPageError, regex): - getattr(item, method)() - - # The invocation above of a fetching method shouldn't change - # the local item, but it does! The title changes to '-1'. - # - # However when identifying the item for 'en:Test page' - # (a deleted page), the exception handling is smarter, and no - # local data is modified in this scenario. This case is - # separately tested in test_fromPage_missing_lazy. - if link.title != 'Test page': - self.assertEqual(item._link._title, '-1') - - self.assertTrue(hasattr(item, '_content')) - - self.assertFalse(item.exists()) - - page = pywikibot.Page(link) - if props: - page.properties() - - # by default, fromPage should always raise the same exception - regex = r"^Page .+ doesn't exist\.$" - with self.assertRaisesRegex(NoPageError, regex): - ItemPage.fromPage(page) - - def test_fromPage_redirect(self): - """Test item from redirect page. - - A redirect should not have a wikidata item. - """ - link = pywikibot.page.Link('Main page', self.site) - self._test_fromPage_noitem(link) - - def test_fromPage_missing(self): - """Test item from deleted page. - - A deleted page should not have a wikidata item. - """ - link = pywikibot.page.Link('Test page', self.site) - self._test_fromPage_noitem(link) - - def test_fromPage_noitem(self): - """Test item from new page. - - A new created page should not have a wikidata item yet. - """ - page = _get_test_unconnected_page(self.site) - link = page._link - self._test_fromPage_noitem(link) - - def test_fromPage_missing_lazy(self): - """Test lazy loading of item from nonexistent source page.""" - # this is a deleted page, and should not have a wikidata item - link = pywikibot.page.Link('Test page', self.site) - page = pywikibot.Page(link) - # ItemPage.fromPage should raise an exception when not lazy loading - # and that exception should refer to the source title 'Test page' - # not the Item being created. - with self.assertRaisesRegex(NoPageError, 'Test page'): - ItemPage.fromPage(page, lazy_load=False) - - item = ItemPage.fromPage(page, lazy_load=True) - - # Now verify that delay loading will result in the desired semantics. - # It should not raise NoPageError on the wikibase item which has a - # title like '-1' or 'Null', as that is useless to determine the cause - # without a full debug log. - # It should raise NoPageError on the source page, with title 'Test - # page' as that is what the bot operator needs to see in the log - # output. - with self.assertRaisesRegex(NoPageError, 'Test page'): - item.get() - - def test_from_entity_uri(self): - """Test ItemPage.from_entity_uri.""" - repo = self.get_repo() - entity_uri = 'http://www.wikidata.org/entity/Q124' - self.assertEqual(ItemPage.from_entity_uri(repo, entity_uri), - ItemPage(repo, 'Q124')) - - def test_from_entity_uri_not_a_data_repo(self): - """Test ItemPage.from_entity_uri with a non-Wikibase site.""" - repo = self.site - entity_uri = 'http://www.wikidata.org/entity/Q124' - regex = r' is not a data repository\.$' - with self.assertRaisesRegex(TypeError, regex): - ItemPage.from_entity_uri(repo, entity_uri) - - def test_from_entity_uri_wrong_repo(self): - """Test ItemPage.from_entity_uri with unexpected item repo.""" - repo = self.get_repo() - entity_uri = 'http://test.wikidata.org/entity/Q124' - regex = (r'^The supplied data repository \(.+\) does not ' - r'correspond to that of the item \(.+\)$') - with self.assertRaisesRegex(ValueError, regex): - ItemPage.from_entity_uri(repo, entity_uri) - - def test_from_entity_uri_invalid_title(self): - """Test ItemPage.from_entity_uri with an invalid item title format.""" - repo = self.get_repo() - entity_uri = 'http://www.wikidata.org/entity/Nonsense' - regex = r"^'.+' is not a valid .+ page title$" - with self.assertRaisesRegex(InvalidTitleError, regex): - ItemPage.from_entity_uri(repo, entity_uri) - - def test_from_entity_uri_no_item(self): - """Test ItemPage.from_entity_uri with non-existent item.""" - repo = self.get_repo() - entity_uri = 'http://www.wikidata.org/entity/Q999999999999999999' - regex = r"^Page .+ doesn't exist\.$" - with self.assertRaisesRegex(NoPageError, regex): - ItemPage.from_entity_uri(repo, entity_uri) - - def test_from_entity_uri_no_item_lazy(self): - """Test ItemPage.from_entity_uri with lazy loaded non-existent item.""" - repo = self.get_repo() - entity_uri = 'http://www.wikidata.org/entity/Q999999999999999999' - expected_item = ItemPage(repo, 'Q999999999999999999') - self.assertEqual( - ItemPage.from_entity_uri(repo, entity_uri, lazy_load=True), - expected_item) - - self.assertFalse(expected_item.exists()) # ensure actually missing - - -class TestRedirects(WikidataTestCase): - - """Test redirect and non-redirect items.""" - - def test_normal_item(self): - """Test normal item.""" - wikidata = self.get_repo() - item = ItemPage(wikidata, 'Q1') - self.assertFalse(item.isRedirectPage()) - self.assertTrue(item.exists()) - regex = r'^Page .+ is not a redirect page\.$' - with self.assertRaisesRegex(IsNotRedirectPageError, regex): - item.getRedirectTarget() - - def test_redirect_item(self): - """Test redirect item.""" - wikidata = self.get_repo() - item = ItemPage(wikidata, 'Q10008448') - item.get(get_redirect=True) - target = ItemPage(wikidata, 'Q8422626') - # tests after get operation - self.assertTrue(item.isRedirectPage()) - self.assertTrue(item.exists()) - self.assertEqual(item.getRedirectTarget(), target) - self.assertIsInstance(item.getRedirectTarget(), ItemPage) - regex = r'^Page .+ is a redirect page\.$' - with self.assertRaisesRegex(IsRedirectPageError, regex): - item.get() - - def test_redirect_item_without_get(self): - """Test redirect item without explicit get operation.""" - wikidata = self.get_repo() - item = pywikibot.ItemPage(wikidata, 'Q10008448') - self.assertTrue(item.exists()) - self.assertTrue(item.isRedirectPage()) - target = pywikibot.ItemPage(wikidata, 'Q8422626') - self.assertEqual(item.getRedirectTarget(), target) - - -class TestPropertyPage(WikidataTestCase): - - """Test PropertyPage.""" - - def test_property_empty_property(self): - """Test creating a PropertyPage without a title and datatype.""" - wikidata = self.get_repo() - regex = r'^"datatype" is required for new property\.$' - with self.assertRaisesRegex(TypeError, regex): - PropertyPage(wikidata) - - def test_property_empty_title(self): - """Test creating a PropertyPage without a title.""" - wikidata = self.get_repo() - regex = r"^Property's title cannot be empty$" - with self.assertRaisesRegex(InvalidTitleError, regex): - PropertyPage(wikidata, title='') - - def test_globe_coordinate(self): - """Test a coordinate PropertyPage has the correct type.""" - wikidata = self.get_repo() - property_page = PropertyPage(wikidata, 'P625') - self.assertEqual(property_page.type, 'globe-coordinate') - - claim = pywikibot.Claim(wikidata, 'P625') - self.assertEqual(claim.type, 'globe-coordinate') - - def test_get(self): - """Test PropertyPage.get() method.""" - wikidata = self.get_repo() - property_page = PropertyPage(wikidata, 'P625') - property_page.get() - self.assertEqual(property_page.type, 'globe-coordinate') - - def test_new_claim(self): - """Test that PropertyPage.newClaim uses cached datatype.""" - wikidata = self.get_repo() - property_page = PropertyPage(wikidata, 'P625') - property_page.get() - claim = property_page.newClaim() - self.assertEqual(claim.type, 'globe-coordinate') - - # Now verify that it isn't fetching the type from the property - # data in the repo by setting the cache to the incorrect type - # and checking that it is the cached value that is used. - property_page._type = 'wikibase-item' - claim = property_page.newClaim() - self.assertEqual(claim.type, 'wikibase-item') - - def test_as_target(self): - """Test that PropertyPage can be used as a value.""" - wikidata = self.get_repo() - property_page = PropertyPage(wikidata, 'P1687') - claim = property_page.newClaim() - claim.setTarget(property_page) - self.assertEqual(claim.type, 'wikibase-property') - self.assertEqual(claim.target, property_page) - - @unittest.expectedFailure - def test_exists(self): - """Test the exists method of PropertyPage.""" - wikidata = self.get_repo() - property_page = PropertyPage(wikidata, 'P1687') - self.assertTrue(property_page.exists()) - # Retry with cached _content. - self.assertTrue(property_page.exists()) - - -class TestClaim(WikidataTestCase): - - """Test Claim object functionality.""" - - def test_claim_eq_simple(self): - """Test comparing two claims. - - If they have the same property and value, they are equal. - """ - wikidata = self.get_repo() - claim1 = pywikibot.Claim(wikidata, 'P31') - claim1.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) - claim2 = pywikibot.Claim(wikidata, 'P31') - claim2.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) - self.assertEqual(claim1, claim2) - self.assertEqual(claim2, claim1) - - def test_claim_eq_simple_different_value(self): - """Test comparing two claims. - - If they have the same property and different values, - they are not equal. - """ - wikidata = self.get_repo() - claim1 = pywikibot.Claim(wikidata, 'P31') - claim1.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) - claim2 = pywikibot.Claim(wikidata, 'P31') - claim2.setTarget(pywikibot.ItemPage(wikidata, 'Q1')) - self.assertNotEqual(claim1, claim2) - self.assertNotEqual(claim2, claim1) - - def test_claim_eq_simple_different_rank(self): - """Test comparing two claims. - - If they have the same property and value and different ranks, - they are equal. - """ - wikidata = self.get_repo() - claim1 = pywikibot.Claim(wikidata, 'P31') - claim1.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) - claim1.setRank('preferred') - claim2 = pywikibot.Claim(wikidata, 'P31') - claim2.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) - self.assertEqual(claim1, claim2) - self.assertEqual(claim2, claim1) - - def test_claim_eq_simple_different_snaktype(self): - """Test comparing two claims. - - If they have the same property and different snaktypes, - they are not equal. - """ - wikidata = self.get_repo() - claim1 = pywikibot.Claim(wikidata, 'P31') - claim1.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) - claim2 = pywikibot.Claim(wikidata, 'P31') - claim2.setSnakType('novalue') - self.assertNotEqual(claim1, claim2) - self.assertNotEqual(claim2, claim1) - - def test_claim_eq_simple_different_property(self): - """Test comparing two claims. - - If they have the same value and different properties, - they are not equal. - """ - wikidata = self.get_repo() - claim1 = pywikibot.Claim(wikidata, 'P31') - claim1.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) - claim2 = pywikibot.Claim(wikidata, 'P21') - claim2.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) - self.assertNotEqual(claim1, claim2) - self.assertNotEqual(claim2, claim1) - - def test_claim_eq_with_qualifiers(self): - """Test comparing two claims. - - If they have the same property, value and qualifiers, they are equal. - """ - wikidata = self.get_repo() - claim1 = pywikibot.Claim(wikidata, 'P31') - claim1.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) - qualifier1 = pywikibot.Claim(wikidata, 'P214', is_qualifier=True) - qualifier1.setTarget('foo') - claim1.addQualifier(qualifier1) - claim2 = pywikibot.Claim(wikidata, 'P31') - claim2.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) - qualifier2 = pywikibot.Claim(wikidata, 'P214', is_qualifier=True) - qualifier2.setTarget('foo') - claim2.addQualifier(qualifier2) - self.assertEqual(claim1, claim2) - self.assertEqual(claim2, claim1) - - def test_claim_eq_with_different_qualifiers(self): - """Test comparing two claims. - - If they have the same property and value and different qualifiers, - they are not equal. - """ - wikidata = self.get_repo() - claim1 = pywikibot.Claim(wikidata, 'P31') - claim1.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) - qualifier1 = pywikibot.Claim(wikidata, 'P214', is_qualifier=True) - qualifier1.setTarget('foo') - claim1.addQualifier(qualifier1) - claim2 = pywikibot.Claim(wikidata, 'P31') - claim2.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) - qualifier2 = pywikibot.Claim(wikidata, 'P214', is_qualifier=True) - qualifier2.setTarget('bar') - claim2.addQualifier(qualifier2) - self.assertNotEqual(claim1, claim2) - self.assertNotEqual(claim2, claim1) - - def test_claim_eq_one_without_qualifiers(self): - """Test comparing two claims. - - If they have the same property and value and one of them has - no qualifiers while the other one does, they are not equal. - """ - wikidata = self.get_repo() - claim1 = pywikibot.Claim(wikidata, 'P31') - claim1.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) - qualifier = pywikibot.Claim(wikidata, 'P214', is_qualifier=True) - qualifier.setTarget('foo') - claim1.addQualifier(qualifier) - claim2 = pywikibot.Claim(wikidata, 'P31') - claim2.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) - self.assertNotEqual(claim1, claim2) - self.assertNotEqual(claim2, claim1) - - def test_claim_eq_with_different_sources(self): - """Test comparing two claims. - - If they have the same property and value and different sources, - they are equal. - """ - wikidata = self.get_repo() - claim1 = pywikibot.Claim(wikidata, 'P31') - claim1.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) - source1 = pywikibot.Claim(wikidata, 'P143', is_reference=True) - source1.setTarget(pywikibot.ItemPage(wikidata, 'Q328')) - claim1.addSource(source1) - claim2 = pywikibot.Claim(wikidata, 'P31') - claim2.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) - source2 = pywikibot.Claim(wikidata, 'P143', is_reference=True) - source2.setTarget(pywikibot.ItemPage(wikidata, 'Q48183')) - claim2.addSource(source2) - self.assertEqual(claim1, claim2) - self.assertEqual(claim2, claim1) - - def test_claim_copy_is_equal(self): - """Test making a copy of a claim. - - The copy of a claim should be always equal to the claim. - """ - wikidata = self.get_repo() - claim = pywikibot.Claim(wikidata, 'P31') - claim.setTarget(pywikibot.ItemPage(wikidata, 'Q5')) - qualifier = pywikibot.Claim(wikidata, 'P214', is_qualifier=True) - qualifier.setTarget('foo') - source = pywikibot.Claim(wikidata, 'P143', is_reference=True) - source.setTarget(pywikibot.ItemPage(wikidata, 'Q328')) - claim.addQualifier(qualifier) - claim.addSource(source) - copy = claim.copy() - self.assertEqual(claim, copy) - - def test_claim_copy_is_equal_qualifier(self): - """Test making a copy of a claim. - - The copy of a qualifier should be always equal to the qualifier. - """ - wikidata = self.get_repo() - qualifier = pywikibot.Claim(wikidata, 'P214', is_qualifier=True) - qualifier.setTarget('foo') - copy = qualifier.copy() - self.assertEqual(qualifier, copy) - self.assertTrue(qualifier.isQualifier) - self.assertTrue(copy.isQualifier) - - def test_claim_copy_is_equal_source(self): - """Test making a copy of a claim. - - The copy of a source should be always equal to the source. - """ - wikidata = self.get_repo() - source = pywikibot.Claim(wikidata, 'P143', is_reference=True) - source.setTarget(pywikibot.ItemPage(wikidata, 'Q328')) - copy = source.copy() - self.assertEqual(source, copy) - self.assertTrue(source.isReference) - self.assertTrue(copy.isReference) - - -class TestClaimSetValue(WikidataTestCase): - - """Test setting claim values.""" - - def test_set_website(self): - """Test setting claim of url type.""" - wikidata = self.get_repo() - claim = pywikibot.Claim(wikidata, 'P856') - self.assertEqual(claim.type, 'url') - target = 'https://en.wikipedia.org/' - claim.setTarget(target) - self.assertEqual(claim.target, target) - - def test_set_WbMonolingualText(self): - """Test setting claim of monolingualtext type.""" - wikidata = self.get_repo() - claim = pywikibot.Claim(wikidata, 'P1450') - self.assertEqual(claim.type, 'monolingualtext') - target = pywikibot.WbMonolingualText(text='Test this!', language='en') - claim.setTarget(target) - self.assertEqual(claim.target, target) - - def test_set_WbQuantity(self): - """Test setting claim of quantity type.""" - wikidata = self.get_repo() - claim = pywikibot.Claim(wikidata, 'P1106') - self.assertEqual(claim.type, 'quantity') - target = pywikibot.WbQuantity( - amount=1234, error=1, unit='http://www.wikidata.org/entity/Q11573') - claim.setTarget(target) - self.assertEqual(claim.target, target) - - def test_set_math(self): - """Test setting claim of math type.""" - wikidata = self.get_repo() - claim = pywikibot.Claim(wikidata, 'P2535') - self.assertEqual(claim.type, 'math') - target = 'a^2 + b^2 = c^2' - claim.setTarget(target) - self.assertEqual(claim.target, target) - - def test_set_identifier(self): - """Test setting claim of external-id type.""" - wikidata = self.get_repo() - claim = pywikibot.Claim(wikidata, 'P214') - self.assertEqual(claim.type, 'external-id') - target = 'Any string is a valid identifier' - claim.setTarget(target) - self.assertEqual(claim.target, target) - - def test_set_date(self): - """Test setting claim of time type.""" - wikidata = self.get_repo() - claim = pywikibot.Claim(wikidata, 'P569') - self.assertEqual(claim.type, 'time') - claim.setTarget(pywikibot.WbTime( - year=2001, month=1, day=1, site=wikidata)) - self.assertEqual(claim.target.year, 2001) - self.assertEqual(claim.target.month, 1) - self.assertEqual(claim.target.day, 1) - - def test_set_musical_notation(self): - """Test setting claim of musical-notation type.""" - wikidata = self.get_repo() - claim = pywikibot.Claim(wikidata, 'P6604') - self.assertEqual(claim.type, 'musical-notation') - target = "\\relative c' { c d e f | g2 g | a4 a a a | g1 |}" - claim.setTarget(target) - self.assertEqual(claim.target, target) - - def test_set_incorrect_target_value(self): - """Test setting claim of the incorrect value.""" - wikidata = self.get_repo() - date_claim = pywikibot.Claim(wikidata, 'P569') - regex = r' is not type .+\.$' - with self.assertRaisesRegex(ValueError, regex): - date_claim.setTarget('foo') - url_claim = pywikibot.Claim(wikidata, 'P856') - with self.assertRaisesRegex(ValueError, regex): - url_claim.setTarget(pywikibot.WbTime(2001, site=wikidata)) - mono_claim = pywikibot.Claim(wikidata, 'P1450') - with self.assertRaisesRegex(ValueError, regex): - mono_claim.setTarget('foo') - quantity_claim = pywikibot.Claim(wikidata, 'P1106') - with self.assertRaisesRegex(ValueError, regex): - quantity_claim.setTarget('foo') - - -class TestItemBasePageMethods(WikidataTestCase, BasePageMethodsTestBase): - - """Test behavior of ItemPage methods inherited from BasePage.""" - - def setUp(self): - """Setup tests.""" - self._page = ItemPage(self.get_repo(), 'Q60') - super().setUp() - - def test_basepage_methods(self): - """Test ItemPage methods inherited from superclass BasePage.""" - self._test_invoke() - self._test_no_wikitext() - - def test_item_is_hashable(self): - """Ensure that ItemPages are hashable.""" - list_of_dupes = [self._page, self._page] - self.assertLength(set(list_of_dupes), 1) - - -class TestPageMethodsWithItemTitle(WikidataTestCase, BasePageMethodsTestBase): - - """Test behavior of Page methods for wikibase item.""" - - def setUp(self): - """Setup tests.""" - self._page = pywikibot.Page(self.site, 'Q60') - super().setUp() - - def test_basepage_methods(self): - """Test Page methods inherited from superclass BasePage with Q60.""" - self._test_invoke() - self._test_no_wikitext() - - -class TestLinks(WikidataTestCase): - - """Test cases to test links stored in Wikidata. - - Uses a stored data file for the wikibase item. - However wikibase creates site objects for each sitelink, and the unit test - directly creates a Site for 'wikipedia:af' to use in a comparison. - """ - - sites = { - 'wikidata': { - 'family': 'wikidata', - 'code': 'wikidata', - }, - 'afwiki': { - 'family': 'wikipedia', - 'code': 'af', - } - } - - def setUp(self): - """Setup Tests.""" - super().setUp() - self.wdp = ItemPage(self.get_repo(), 'Q60') - self.wdp.id = 'Q60' - with open(join_pages_path('Q60_only_sitelinks.wd')) as f: - self.wdp._content = json.load(f) - self.wdp.get() - - def test_iterlinks_page_object(self): - """Test iterlinks for page objects.""" - page = next(pg for pg in self.wdp.iterlinks() if pg.site.code == 'af') - self.assertEqual(page, pywikibot.Page(self.get_site('afwiki'), - 'New York Stad')) - - def test_iterlinks_filtering(self): - """Test iterlinks for a given family.""" - wikilinks = list(self.wdp.iterlinks('wikipedia')) - wvlinks = list(self.wdp.iterlinks('wikivoyage')) - - self.assertLength(wikilinks, 3) - self.assertLength(wvlinks, 2) - - -class TestWriteNormalizeData(TestCase): - - """Test cases for routines that normalize data for writing to Wikidata. - - Exercises ItemPage._normalizeData with data that is not normalized - and data which is already normalized. - """ - - net = False - - def setUp(self): - """Setup tests.""" - super().setUp() - self.data_out = { - 'labels': {'en': {'language': 'en', 'value': 'Foo'}}, - 'descriptions': {'en': {'language': 'en', 'value': 'Desc'}}, - 'aliases': {'en': [ - {'language': 'en', 'value': 'Bah'}, - {'language': 'en', 'value': 'Bar', 'remove': ''}, - ]}, - } - - def test_normalize_data(self): - """Test _normalizeData() method.""" - data_in = { - 'labels': {'en': 'Foo'}, - 'descriptions': {'en': 'Desc'}, - 'aliases': {'en': [ - 'Bah', - {'language': 'en', 'value': 'Bar', 'remove': ''}, - ]}, - } - - response = ItemPage._normalizeData(data_in) - self.assertEqual(response, self.data_out) - - def test_normalized_data(self): - """Test _normalizeData() method for normalized data.""" - response = ItemPage._normalizeData( - copy.deepcopy(self.data_out)) - self.assertEqual(response, self.data_out) - - -class TestPreloadingEntityGenerator(TestCase): - - """Test preloading item generator.""" - - sites = { - 'wikidata': { - 'family': 'wikidata', - 'code': 'wikidata', - }, - 'enwiki': { - 'family': 'wikipedia', - 'code': 'en', - } - } - - def test_non_item_gen(self): - """Test PreloadingEntityGenerator with getReferences().""" - site = self.get_site('wikidata') - page = pywikibot.Page(site, 'Property:P31') - ref_gen = page.getReferences(follow_redirects=False, total=5) - gen = pagegenerators.PreloadingEntityGenerator(ref_gen) - for item in gen: - self.assertIsInstance(item, ItemPage) - - def test_foreign_page_item_gen(self): - """Test PreloadingEntityGenerator with connected pages.""" - site = self.get_site('enwiki') - page_gen = [pywikibot.Page(site, 'Main Page'), - pywikibot.Page(site, 'New York City')] - gen = pagegenerators.PreloadingEntityGenerator(page_gen) - for item in gen: - self.assertIsInstance(item, ItemPage) - - -class TestNamespaces(WikidataTestCase): - - """Test cases to test namespaces of Wikibase entities.""" - - def test_empty_wikibase_page(self): - """Test empty wikibase page. - - As a base class it should be able to instantiate - it with minimal arguments - """ - wikidata = self.get_repo() - page = WikibasePage(wikidata) - regex = r' object has no attribute ' - with self.assertRaisesRegex(AttributeError, regex): - page.namespace() - page = WikibasePage(wikidata, title='') - with self.assertRaisesRegex(AttributeError, regex): - page.namespace() - - page = WikibasePage(wikidata, ns=0) - self.assertEqual(page.namespace(), 0) - page = WikibasePage(wikidata, entity_type='item') - self.assertEqual(page.namespace(), 0) - - page = WikibasePage(wikidata, ns=120) - self.assertEqual(page.namespace(), 120) - page = WikibasePage(wikidata, title='', ns=120) - self.assertEqual(page.namespace(), 120) - page = WikibasePage(wikidata, entity_type='property') - self.assertEqual(page.namespace(), 120) - - # mismatch in namespaces - regex = r'^Namespace ".+" is not valid for Wikibase entity type ".+"$' - with self.assertRaisesRegex(ValueError, regex): - WikibasePage(wikidata, ns=0, entity_type='property') - with self.assertRaisesRegex(ValueError, regex): - WikibasePage(wikidata, ns=120, entity_type='item') - - def test_wikibase_link_namespace(self): - """Test the title resolved to a namespace correctly.""" - wikidata = self.get_repo() - # title without any namespace clues (ns or entity_type) - # should verify the Link namespace is appropriate - page = WikibasePage(wikidata, title='Q6') - self.assertEqual(page.namespace(), 0) - page = WikibasePage(wikidata, title='Property:P60') - self.assertEqual(page.namespace(), 120) - - def test_wikibase_namespace_selection(self): - """Test various ways to correctly specify the namespace.""" - wikidata = self.get_repo() - - page = ItemPage(wikidata, 'Q60') - self.assertEqual(page.namespace(), 0) - page.get() - - page = ItemPage(wikidata, title='Q60') - self.assertEqual(page.namespace(), 0) - page.get() - - page = WikibasePage(wikidata, title='Q60', ns=0) - self.assertEqual(page.namespace(), 0) - page.get() - - page = WikibasePage(wikidata, title='Q60', - entity_type='item') - self.assertEqual(page.namespace(), 0) - page.get() - - page = PropertyPage(wikidata, 'Property:P6') - self.assertEqual(page.namespace(), 120) - page.get() - - page = PropertyPage(wikidata, 'P6') - self.assertEqual(page.namespace(), 120) - page.get() - - page = WikibasePage(wikidata, title='Property:P6') - self.assertEqual(page.namespace(), 120) - page.get() - - page = WikibasePage(wikidata, title='P6', ns=120) - self.assertEqual(page.namespace(), 120) - page.get() - - page = WikibasePage(wikidata, title='P6', - entity_type='property') - self.assertEqual(page.namespace(), 120) - page.get() - - def test_wrong_namespaces(self): - """Test incorrect namespaces for Wikibase entities.""" - wikidata = self.get_repo() - # All subclasses of WikibasePage raise a ValueError - # if the namespace for the page title is not correct - regex = r': Namespace ".+" is not valid$' - with self.assertRaisesRegex(ValueError, regex): - WikibasePage(wikidata, title='Wikidata:Main Page') - regex = r"^'.+' is not in the namespace " - with self.assertRaisesRegex(ValueError, regex): - ItemPage(wikidata, 'File:Q1') - with self.assertRaisesRegex(ValueError, regex): - PropertyPage(wikidata, 'File:P60') - - def test_item_unknown_namespace(self): - """Test unknown namespaces for Wikibase entities.""" - # The 'Invalid:' is not a known namespace, so is parsed to be - # part of the title in namespace 0 - # TODO: These items have inappropriate titles, which should - # raise an error. - wikidata = self.get_repo() - regex = r"^'.+' is not a valid item page title$" - with self.assertRaisesRegex(InvalidTitleError, regex): - ItemPage(wikidata, 'Invalid:Q1') - - -class TestAlternateNamespaces(WikidataTestCase): - - """Test cases to test namespaces of Wikibase entities.""" - - cached = False - dry = True - - @classmethod - def setUpClass(cls): - """Setup test class.""" - super().setUpClass() - - cls.get_repo()._namespaces = NamespacesDict({ - 90: Namespace(id=90, - case='first-letter', - canonical_name='Item', - defaultcontentmodel='wikibase-item'), - 92: Namespace(id=92, - case='first-letter', - canonical_name='Prop', - defaultcontentmodel='wikibase-property') - }) - - def test_alternate_item_namespace(self): - """Test alternate item namespace.""" - item = ItemPage(self.repo, 'Q60') - self.assertEqual(item.namespace(), 90) - self.assertEqual(item.id, 'Q60') - self.assertEqual(item.title(), 'Item:Q60') - self.assertEqual(item._defined_by(), {'ids': 'Q60'}) - - item = ItemPage(self.repo, 'Item:Q60') - self.assertEqual(item.namespace(), 90) - self.assertEqual(item.id, 'Q60') - self.assertEqual(item.title(), 'Item:Q60') - self.assertEqual(item._defined_by(), {'ids': 'Q60'}) - - def test_alternate_property_namespace(self): - """Test alternate property namespace.""" - prop = PropertyPage(self.repo, 'P21') - self.assertEqual(prop.namespace(), 92) - self.assertEqual(prop.id, 'P21') - self.assertEqual(prop.title(), 'Prop:P21') - self.assertEqual(prop._defined_by(), {'ids': 'P21'}) - - prop = PropertyPage(self.repo, 'Prop:P21') - self.assertEqual(prop.namespace(), 92) - self.assertEqual(prop.id, 'P21') - self.assertEqual(prop.title(), 'Prop:P21') - self.assertEqual(prop._defined_by(), {'ids': 'P21'}) - - -class TestOwnClient(TestCase): - - """Test that a data repository family can be its own client.""" - - sites = { - # The main Wikidata is its own client. - 'wikidata': { - 'family': 'wikidata', - 'code': 'wikidata', - 'item': 'Q32119', - }, - # test.wikidata is also - 'wikidatatest': { - 'family': 'wikidata', - 'code': 'test', - 'item': 'Q33', - }, - } - - def test_own_client(self, key): - """Test that a data repository family can be its own client.""" - site = self.get_site(key) - page = self.get_mainpage(site) - item = ItemPage.fromPage(page) - self.assertEqual(page.site, site) - self.assertEqual(item.site, site) - - def test_page_from_repository(self, key): - """Test that page_from_repository method works for wikibase too.""" - site = self.get_site(key) - page = site.page_from_repository('Q5296') - self.assertEqual(page, self.get_mainpage(site)) - - def test_redirect_from_repository(self, key): - """Test page_from_repository method with redirects.""" - site = self.get_site(key) - item = self.sites[key]['item'] - with self.assertRaisesRegex( - IsRedirectPageError, - fr'{self.sites[key]["item"]}\]\] is a redirect'): - site.page_from_repository(item) - - -class TestUnconnectedClient(TestCase): - - """Test clients not connected to a data repository.""" - - sites = { - # Wikispecies is not supported by Wikidata yet. - 'species': { - 'family': 'species', - 'code': 'species', - 'page_title': 'Main Page', - }, - # fr.wiktionary is not supported by Wikidata yet. - 'frwikt': { - 'family': 'wiktionary', - 'code': 'fr', - 'page_title': 'and', - }, - } - - dry = True - - def test_not_supported_family(self, key): - """Test that family without a data repository causes error.""" - site = self.get_site(key) - - self.wdp = pywikibot.Page(site, self.sites[key]['page_title']) - regex = r' has no data repository$' - with self.assertRaisesRegex(WikiBaseError, regex): - ItemPage.fromPage(self.wdp) - with self.assertRaisesRegex(WikiBaseError, regex): - self.wdp.data_item() - - def test_has_data_repository(self, key): - """Test that site has no data repository.""" - site = self.get_site(key) - self.assertFalse(site.has_data_repository) - - def test_page_from_repository_fails(self, key): - """Test that page_from_repository method fails.""" - site = self.get_site(key) - dummy_item = 'Q1' - regex = r'^Wikibase is not implemented for .+\.$' - with self.assertRaisesRegex(UnknownExtensionError, regex): - site.page_from_repository(dummy_item) - - -class TestJSON(WikidataTestCase): - - """Test cases to test toJSON() functions.""" - - def setUp(self): - """Setup test.""" - super().setUp() - wikidata = self.get_repo() - self.wdp = ItemPage(wikidata, 'Q60') - self.wdp.id = 'Q60' - with open(join_pages_path('Q60.wd')) as f: - self.wdp._content = json.load(f) - self.wdp.get() - del self.wdp._content['id'] - del self.wdp._content['type'] - del self.wdp._content['lastrevid'] - del self.wdp._content['pageid'] - - def test_itempage_json(self): - """Test itempage json.""" - old = json.dumps(self.wdp._content, indent=2, sort_keys=True) - new = json.dumps(self.wdp.toJSON(), indent=2, sort_keys=True) - - self.assertEqual(old, new) - - def test_json_diff(self): - """Test json diff.""" - del self.wdp.labels['en'] - self.wdp.aliases['de'].append('New York') - self.wdp.aliases['de'].append('foo') - self.wdp.aliases['de'].remove('NYC') - del self.wdp.aliases['nl'] - del self.wdp.claims['P213'] - del self.wdp.sitelinks['afwiki'] - self.wdp.sitelinks['nlwiki']._badges = set() - expected = { - 'labels': { - 'en': { - 'language': 'en', - 'value': '' - } - }, - 'aliases': { - 'de': [ - {'language': 'de', 'value': 'City of New York'}, - {'language': 'de', 'value': 'The Big Apple'}, - {'language': 'de', 'value': 'New York'}, - {'language': 'de', 'value': 'New York'}, - {'language': 'de', 'value': 'foo'}, - ], - 'nl': [ - {'language': 'nl', 'value': 'New York', 'remove': ''}, - ], - }, - 'claims': { - 'P213': [ - { - 'id': 'Q60$0427a236-4120-7d00-fa3e-e23548d4c02d', - 'remove': '' - } - ] - }, - 'sitelinks': { - 'afwiki': { - 'site': 'afwiki', - 'title': '', - }, - 'nlwiki': { - 'site': 'nlwiki', - 'title': 'New York City', - 'badges': [''] - } - } - } - diff = self.wdp.toJSON(diffto=self.wdp._content) - self.assertEqual(diff, expected) - - if __name__ == '__main__': with suppress(SystemExit): unittest.main() diff --git a/tests/wikibase_tests.py b/tests/wikibase_tests.py index 413ab3b363..8829eeb459 100755 --- a/tests/wikibase_tests.py +++ b/tests/wikibase_tests.py @@ -8,12 +8,9 @@ from __future__ import annotations import copy -import datetime import json -import operator import unittest from contextlib import suppress -from decimal import Decimal import pywikibot from pywikibot import pagegenerators @@ -25,9 +22,9 @@ UnknownExtensionError, WikiBaseError, ) -from pywikibot.page import ItemPage, Page, PropertyPage, WikibasePage +from pywikibot.page import ItemPage, PropertyPage, WikibasePage from pywikibot.site import Namespace, NamespacesDict -from pywikibot.tools import MediaWikiVersion, suppress_warnings +from pywikibot.tools import suppress_warnings from tests import WARN_SITE_CODE, join_pages_path from tests.aspects import TestCase, WikidataTestCase from tests.basepage import ( @@ -48,16 +45,6 @@ def _get_test_unconnected_page(site): return None # pragma: no cover -class WbRepresentationTestCase(WikidataTestCase): - - """Test methods inherited or extended from _WbRepresentation.""" - - def _test_hashable(self, representation): - """Test that the representation is hashable.""" - list_of_dupes = [representation, representation] - self.assertLength(set(list_of_dupes), 1) - - class TestLoadRevisionsCaching(BasePageLoadRevisionsCachingTestBase, WikidataTestCase): @@ -122,825 +109,6 @@ def test_cmp(self): ItemPage(self.get_repo(), 'q5296')) -class TestWikibaseCoordinate(WbRepresentationTestCase): - - """Test Wikibase Coordinate data type.""" - - dry = True - - def test_Coordinate_WbRepresentation_methods(self): - """Test inherited or extended methods from _WbRepresentation.""" - repo = self.get_repo() - coord = pywikibot.Coordinate( - site=repo, lat=12.0, lon=13.0, precision=0, - globe='moon') - self._test_hashable(coord) - - def test_Coordinate_dim(self): - """Test Coordinate dimension.""" - repo = self.get_repo() - x = pywikibot.Coordinate(site=repo, lat=12.0, lon=13.0, precision=5.0) - self.assertEqual(x.precisionToDim(), 544434) - self.assertIsInstance(x.precisionToDim(), int) - y = pywikibot.Coordinate(site=repo, lat=12.0, lon=13.0, dim=54444) - self.assertEqual(y.precision, 0.500005084017101) - self.assertIsInstance(y.precision, float) - z = pywikibot.Coordinate(site=repo, lat=12.0, lon=13.0) - regex = r'^No values set for dim or precision$' - with self.assertRaisesRegex(ValueError, regex): - z.precisionToDim() - - def test_Coordinate_plain_globe(self): - """Test setting Coordinate globe from a plain-text value.""" - repo = self.get_repo() - coord = pywikibot.Coordinate( - site=repo, lat=12.0, lon=13.0, precision=0, - globe='moon') - self.assertEqual(coord.toWikibase(), - {'latitude': 12.0, 'longitude': 13.0, - 'altitude': None, 'precision': 0, - 'globe': 'http://www.wikidata.org/entity/Q405'}) - - def test_Coordinate_entity_uri_globe(self): - """Test setting Coordinate globe from an entity uri.""" - repo = self.get_repo() - coord = pywikibot.Coordinate( - site=repo, lat=12.0, lon=13.0, precision=0, - globe_item='http://www.wikidata.org/entity/Q123') - self.assertEqual(coord.toWikibase(), - {'latitude': 12.0, 'longitude': 13.0, - 'altitude': None, 'precision': 0, - 'globe': 'http://www.wikidata.org/entity/Q123'}) - - -class TestWikibaseCoordinateNonDry(WbRepresentationTestCase): - - """Test Wikibase Coordinate data type (non-dry). - - These can be moved to TestWikibaseCoordinate once DrySite has been bumped - to the appropriate version. - """ - - def test_Coordinate_item_globe(self): - """Test setting Coordinate globe from an ItemPage.""" - repo = self.get_repo() - coord = pywikibot.Coordinate( - site=repo, lat=12.0, lon=13.0, precision=0, - globe_item=ItemPage(repo, 'Q123')) - self.assertEqual(coord.toWikibase(), - {'latitude': 12.0, 'longitude': 13.0, - 'altitude': None, 'precision': 0, - 'globe': 'http://www.wikidata.org/entity/Q123'}) - - def test_Coordinate_get_globe_item_from_uri(self): - """Test getting globe item from Coordinate with entity uri globe.""" - repo = self.get_repo() - q = pywikibot.Coordinate( - site=repo, lat=12.0, lon=13.0, precision=0, - globe_item='http://www.wikidata.org/entity/Q123') - self.assertEqual(q.get_globe_item(), ItemPage(repo, 'Q123')) - - def test_Coordinate_get_globe_item_from_itempage(self): - """Test getting globe item from Coordinate with ItemPage globe.""" - repo = self.get_repo() - globe = ItemPage(repo, 'Q123') - q = pywikibot.Coordinate( - site=repo, lat=12.0, lon=13.0, precision=0, globe_item=globe) - self.assertEqual(q.get_globe_item(), ItemPage(repo, 'Q123')) - - def test_Coordinate_get_globe_item_from_plain_globe(self): - """Test getting globe item from Coordinate with plain text globe.""" - repo = self.get_repo() - q = pywikibot.Coordinate( - site=repo, lat=12.0, lon=13.0, precision=0, globe='moon') - self.assertEqual(q.get_globe_item(), ItemPage(repo, 'Q405')) - - def test_Coordinate_get_globe_item_provide_repo(self): - """Test getting globe item from Coordinate, providing repo.""" - repo = self.get_repo() - q = pywikibot.Coordinate( - site=repo, lat=12.0, lon=13.0, precision=0, - globe_item='http://www.wikidata.org/entity/Q123') - self.assertEqual(q.get_globe_item(repo), ItemPage(repo, 'Q123')) - - def test_Coordinate_get_globe_item_different_repo(self): - """Test getting globe item in different repo from Coordinate.""" - repo = self.get_repo() - test_repo = pywikibot.Site('test', 'wikidata') - q = pywikibot.Coordinate( - site=repo, lat=12.0, lon=13.0, precision=0, - globe_item='http://test.wikidata.org/entity/Q123') - self.assertEqual(q.get_globe_item(test_repo), - ItemPage(test_repo, 'Q123')) - - def test_Coordinate_equality(self): - """Test Coordinate equality with different globe representations.""" - repo = self.get_repo() - a = pywikibot.Coordinate( - site=repo, lat=12.0, lon=13.0, precision=0.1, - globe='moon') - b = pywikibot.Coordinate( - site=repo, lat=12.0, lon=13.0, precision=0.1, - globe_item='http://www.wikidata.org/entity/Q405') - c = pywikibot.Coordinate( - site=repo, lat=12.0, lon=13.0, precision=0.1, - globe_item=ItemPage(repo, 'Q405')) - d = pywikibot.Coordinate( - site=repo, lat=12.0, lon=13.0, precision=0.1, - globe_item='http://test.wikidata.org/entity/Q405') - self.assertEqual(a, b) - self.assertEqual(b, c) - self.assertEqual(c, a) - self.assertNotEqual(a, d) - self.assertNotEqual(b, d) - self.assertNotEqual(c, d) - - -class TestWbTime(WbRepresentationTestCase): - - """Test Wikibase WbTime data type.""" - - dry = True - - def test_WbTime_WbRepresentation_methods(self): - """Test inherited or extended methods from _WbRepresentation.""" - repo = self.get_repo() - t = pywikibot.WbTime(site=repo, year=2010, month=0, day=0, hour=12, - minute=43) - self._test_hashable(t) - - def test_WbTime_timestr(self): - """Test timestr functions of WbTime.""" - repo = self.get_repo() - t = pywikibot.WbTime(site=repo, year=2010, month=0, day=0, hour=12, - minute=43) - self.assertEqual(t.toTimestr(), '+00000002010-00-00T12:43:00Z') - self.assertEqual(t.toTimestr(force_iso=True), '+2010-01-01T12:43:00Z') - - t = pywikibot.WbTime(site=repo, year=2010, hour=12, minute=43) - self.assertEqual(t.toTimestr(), '+00000002010-01-01T12:43:00Z') - self.assertEqual(t.toTimestr(force_iso=True), '+2010-01-01T12:43:00Z') - - t = pywikibot.WbTime(site=repo, year=-2010, hour=12, minute=43) - self.assertEqual(t.toTimestr(), '-00000002010-01-01T12:43:00Z') - self.assertEqual(t.toTimestr(force_iso=True), '-2010-01-01T12:43:00Z') - - def test_WbTime_fromTimestr(self): - """Test WbTime creation from UTC date/time string.""" - repo = self.get_repo() - t = pywikibot.WbTime.fromTimestr('+00000002010-01-01T12:43:00Z', - site=repo) - self.assertEqual(t, pywikibot.WbTime(site=repo, year=2010, hour=12, - minute=43, precision=14)) - - def test_WbTime_zero_month(self): - """Test WbTime creation from date/time string with zero month.""" - # ensures we support formats in T123888 / T107870 - repo = self.get_repo() - t = pywikibot.WbTime.fromTimestr('+00000002010-00-00T12:43:00Z', - site=repo) - self.assertEqual(t, pywikibot.WbTime(site=repo, year=2010, month=0, - day=0, hour=12, minute=43, - precision=14)) - - def test_WbTime_skip_params_precision(self): - """Test skipping units (such as day, month) when creating WbTimes.""" - repo = self.get_repo() - t = pywikibot.WbTime(year=2020, day=2, site=repo) - self.assertEqual(t, pywikibot.WbTime(year=2020, month=1, day=2, - site=repo)) - self.assertEqual(t.precision, pywikibot.WbTime.PRECISION['day']) - t2 = pywikibot.WbTime(year=2020, hour=5, site=repo) - self.assertEqual(t2, pywikibot.WbTime(year=2020, month=1, day=1, - hour=5, site=repo)) - self.assertEqual(t2.precision, pywikibot.WbTime.PRECISION['hour']) - t3 = pywikibot.WbTime(year=2020, minute=5, site=repo) - self.assertEqual(t3, pywikibot.WbTime(year=2020, month=1, day=1, - hour=0, minute=5, site=repo)) - self.assertEqual(t3.precision, pywikibot.WbTime.PRECISION['minute']) - t4 = pywikibot.WbTime(year=2020, second=5, site=repo) - self.assertEqual(t4, pywikibot.WbTime(year=2020, month=1, day=1, - hour=0, minute=0, second=5, - site=repo)) - self.assertEqual(t4.precision, pywikibot.WbTime.PRECISION['second']) - t5 = pywikibot.WbTime(year=2020, month=2, hour=5, site=repo) - self.assertEqual(t5, pywikibot.WbTime(year=2020, month=2, day=1, - hour=5, site=repo)) - self.assertEqual(t5.precision, pywikibot.WbTime.PRECISION['hour']) - t6 = pywikibot.WbTime(year=2020, month=2, minute=5, site=repo) - self.assertEqual(t6, pywikibot.WbTime(year=2020, month=2, day=1, - hour=0, minute=5, site=repo)) - self.assertEqual(t6.precision, pywikibot.WbTime.PRECISION['minute']) - t7 = pywikibot.WbTime(year=2020, month=2, second=5, site=repo) - self.assertEqual(t7, pywikibot.WbTime(year=2020, month=2, day=1, - hour=0, minute=0, second=5, - site=repo)) - self.assertEqual(t7.precision, pywikibot.WbTime.PRECISION['second']) - t8 = pywikibot.WbTime(year=2020, day=2, hour=5, site=repo) - self.assertEqual(t8, pywikibot.WbTime(year=2020, month=1, day=2, - hour=5, site=repo)) - self.assertEqual(t8.precision, pywikibot.WbTime.PRECISION['hour']) - t9 = pywikibot.WbTime(year=2020, month=3, day=2, minute=5, site=repo) - self.assertEqual(t9, pywikibot.WbTime(year=2020, month=3, day=2, - hour=0, minute=5, site=repo)) - self.assertEqual(t9.precision, pywikibot.WbTime.PRECISION['minute']) - - def test_WbTime_normalization(self): - """Test WbTime normalization.""" - repo = self.get_repo() - # flake8 is being annoying, so to reduce line length, I'll make - # some aliases here - decade = pywikibot.WbTime.PRECISION['decade'] - century = pywikibot.WbTime.PRECISION['century'] - millenia = pywikibot.WbTime.PRECISION['millenia'] - t = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, - minute=43, second=12) - t2 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, - minute=43, second=12, - precision=pywikibot.WbTime.PRECISION['second']) - t3 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, - minute=43, second=12, - precision=pywikibot.WbTime.PRECISION['minute']) - t4 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, - minute=43, second=12, - precision=pywikibot.WbTime.PRECISION['hour']) - t5 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, - minute=43, second=12, - precision=pywikibot.WbTime.PRECISION['day']) - t6 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, - minute=43, second=12, - precision=pywikibot.WbTime.PRECISION['month']) - t7 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, - minute=43, second=12, - precision=pywikibot.WbTime.PRECISION['year']) - t8 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, - minute=43, second=12, - precision=decade) - t9 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, - minute=43, second=12, - precision=century) - t10 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, - minute=43, second=12, - precision=millenia) - t11 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, - minute=43, second=12, timezone=-300, - precision=pywikibot.WbTime.PRECISION['day']) - t12 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, - minute=43, second=12, timezone=300, - precision=pywikibot.WbTime.PRECISION['day']) - t13 = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, - minute=43, second=12, timezone=-300, - precision=pywikibot.WbTime.PRECISION['hour']) - self.assertEqual(t.normalize(), t) - self.assertEqual(t2.normalize(), t.normalize()) - self.assertEqual(t3.normalize(), - pywikibot.WbTime(site=repo, year=2010, month=1, - day=1, hour=12, minute=43)) - self.assertEqual(t4.normalize(), - pywikibot.WbTime(site=repo, year=2010, - month=1, day=1, hour=12)) - self.assertEqual(t5.normalize(), - pywikibot.WbTime(site=repo, year=2010, - month=1, day=1)) - self.assertEqual(t6.normalize(), - pywikibot.WbTime(site=repo, year=2010, - month=1)) - self.assertEqual( - t7.normalize(), pywikibot.WbTime(site=repo, year=2010)) - self.assertEqual(t8.normalize(), - pywikibot.WbTime(site=repo, year=2010, - precision=decade)) - self.assertEqual(t9.normalize(), - pywikibot.WbTime(site=repo, year=2100, - precision=century)) - self.assertEqual(t9.normalize(), - pywikibot.WbTime(site=repo, year=2010, - precision=century).normalize()) - self.assertEqual(t10.normalize(), - pywikibot.WbTime(site=repo, year=3000, - precision=millenia)) - self.assertEqual(t10.normalize(), - pywikibot.WbTime(site=repo, year=2010, - precision=millenia).normalize()) - t11_normalized = t11.normalize() - t12_normalized = t12.normalize() - self.assertEqual(t11_normalized.timezone, 0) - self.assertEqual(t12_normalized.timezone, 0) - self.assertNotEqual(t11, t12) - self.assertEqual(t11_normalized, t12_normalized) - self.assertEqual(t13.normalize().timezone, -300) - - def test_WbTime_normalization_very_low_precision(self): - """Test WbTime normalization with very low precision.""" - repo = self.get_repo() - # flake8 is being annoying, so to reduce line length, I'll make - # some aliases here - year_10000 = pywikibot.WbTime.PRECISION['10000'] - year_100000 = pywikibot.WbTime.PRECISION['100000'] - year_1000000 = pywikibot.WbTime.PRECISION['1000000'] - year_10000000 = pywikibot.WbTime.PRECISION['10000000'] - year_100000000 = pywikibot.WbTime.PRECISION['100000000'] - year_1000000000 = pywikibot.WbTime.PRECISION['1000000000'] - t = pywikibot.WbTime(site=repo, year=-3124684989, - precision=year_10000) - t2 = pywikibot.WbTime(site=repo, year=-3124684989, - precision=year_100000) - t3 = pywikibot.WbTime(site=repo, year=-3124684989, - precision=year_1000000) - t4 = pywikibot.WbTime(site=repo, year=-3124684989, - precision=year_10000000) - t5 = pywikibot.WbTime(site=repo, year=-3124684989, - precision=year_100000000) - t6 = pywikibot.WbTime(site=repo, year=-3124684989, - precision=year_1000000000) - self.assertEqual(t.normalize(), - pywikibot.WbTime(site=repo, year=-3124680000, - precision=year_10000)) - self.assertEqual(t2.normalize(), - pywikibot.WbTime(site=repo, year=-3124700000, - precision=year_100000)) - self.assertEqual(t3.normalize(), - pywikibot.WbTime(site=repo, year=-3125000000, - precision=year_1000000)) - self.assertEqual(t4.normalize(), - pywikibot.WbTime(site=repo, year=-3120000000, - precision=year_10000000)) - self.assertEqual(t5.normalize(), - pywikibot.WbTime(site=repo, year=-3100000000, - precision=year_100000000)) - self.assertEqual(t6.normalize(), - pywikibot.WbTime(site=repo, year=-3000000000, - precision=year_1000000000)) - - def test_WbTime_timestamp(self): - """Test timestamp functions of WbTime.""" - repo = self.get_repo() - timestamp = pywikibot.Timestamp.fromISOformat('2010-01-01T12:43:00Z') - t = pywikibot.WbTime(site=repo, year=2010, month=0, day=0, hour=12, - minute=43) - self.assertEqual(t.toTimestamp(), timestamp) - - # Roundtrip fails as Timestamp and WbTime interpret month=0 differently - self.assertNotEqual( - t, pywikibot.WbTime.fromTimestamp(timestamp, site=repo)) - - t = pywikibot.WbTime(site=repo, year=2010, hour=12, minute=43) - self.assertEqual(t.toTimestamp(), timestamp) - - t = pywikibot.WbTime(site=repo, year=-2010, hour=12, minute=43) - regex = r'^You cannot turn BC dates into a Timestamp$' - with self.assertRaisesRegex(ValueError, regex): - t.toTimestamp() - - t = pywikibot.WbTime(site=repo, year=2010, month=1, day=1, hour=12, - minute=43, second=0) - self.assertEqual(t.toTimestamp(), timestamp) - self.assertEqual( - t, pywikibot.WbTime.fromTimestamp(timestamp, site=repo)) - timezone = datetime.timezone(datetime.timedelta(hours=-5)) - ts = pywikibot.Timestamp(2020, 1, 1, 12, 43, 0, tzinfo=timezone) - t = pywikibot.WbTime.fromTimestamp(ts, site=repo, copy_timezone=True) - self.assertEqual(t.timezone, -5 * 60) - t = pywikibot.WbTime.fromTimestamp(ts, site=repo, copy_timezone=True, - timezone=60) - self.assertEqual(t.timezone, 60) - - ts1 = pywikibot.Timestamp( - year=2022, month=12, day=21, hour=13, - tzinfo=datetime.timezone(datetime.timedelta(hours=-5))) - t1 = pywikibot.WbTime.fromTimestamp(ts1, timezone=-300, site=repo) - self.assertIsNotNone(t1.toTimestamp(timezone_aware=True).tzinfo) - self.assertIsNone(t1.toTimestamp(timezone_aware=False).tzinfo) - self.assertEqual(t1.toTimestamp(timezone_aware=True), ts1) - self.assertNotEqual(t1.toTimestamp(timezone_aware=False), ts1) - - def test_WbTime_errors(self): - """Test WbTime precision errors.""" - repo = self.get_repo() - regex = r'^no year given$' - with self.assertRaisesRegex(ValueError, regex): - pywikibot.WbTime(site=repo, precision=15) - with self.assertRaisesRegex(ValueError, regex): - pywikibot.WbTime(site=repo, precision='invalid_precision') - regex = r'^Invalid precision: "15"$' - with self.assertRaisesRegex(ValueError, regex): - pywikibot.WbTime(site=repo, year=2020, precision=15) - regex = r'^Invalid precision: "invalid_precision"$' - with self.assertRaisesRegex(ValueError, regex): - pywikibot.WbTime(site=repo, year=2020, - precision='invalid_precision') - - def test_comparison(self): - """Test WbTime comparison.""" - repo = self.get_repo() - t1 = pywikibot.WbTime(site=repo, year=2010, hour=12, minute=43) - t2 = pywikibot.WbTime(site=repo, year=-2005, hour=16, minute=45) - self.assertEqual(t1.precision, pywikibot.WbTime.PRECISION['minute']) - self.assertEqual(t1, t1) - self.assertGreaterEqual(t1, t1) - self.assertGreaterEqual(t1, t2) - self.assertGreater(t1, t2) - self.assertEqual(t1.year, 2010) - self.assertEqual(t2.year, -2005) - self.assertEqual(t1.month, 1) - self.assertEqual(t2.month, 1) - self.assertEqual(t1.day, 1) - self.assertEqual(t2.day, 1) - self.assertEqual(t1.hour, 12) - self.assertEqual(t2.hour, 16) - self.assertEqual(t1.minute, 43) - self.assertEqual(t2.minute, 45) - self.assertEqual(t1.second, 0) - self.assertEqual(t2.second, 0) - self.assertEqual(t1.toTimestr(), '+00000002010-01-01T12:43:00Z') - self.assertEqual(t2.toTimestr(), '-00000002005-01-01T16:45:00Z') - self.assertRaises(ValueError, pywikibot.WbTime, site=repo, - precision=15) - self.assertRaises(ValueError, pywikibot.WbTime, site=repo, - precision='invalid_precision') - self.assertIsInstance(t1.toTimestamp(), pywikibot.Timestamp) - self.assertRaises(ValueError, t2.toTimestamp) - - def test_comparison_types(self): - """Test WbTime comparison with different types.""" - repo = self.get_repo() - t1 = pywikibot.WbTime(site=repo, year=2010, hour=12, minute=43) - t2 = pywikibot.WbTime(site=repo, year=-2005, hour=16, minute=45) - self.assertGreater(t1, t2) - self.assertRaises(TypeError, operator.lt, t1, 5) - self.assertRaises(TypeError, operator.gt, t1, 5) - self.assertRaises(TypeError, operator.le, t1, 5) - self.assertRaises(TypeError, operator.ge, t1, 5) - - def test_comparison_timezones(self): - """Test comparisons with timezones.""" - repo = self.get_repo() - ts1 = pywikibot.Timestamp( - year=2022, month=12, day=21, hour=13, - tzinfo=datetime.timezone(datetime.timedelta(hours=-5))) - ts2 = pywikibot.Timestamp( - year=2022, month=12, day=21, hour=17, - tzinfo=datetime.timezone.utc) - self.assertGreater(ts1.timestamp(), ts2.timestamp()) - - t1 = pywikibot.WbTime.fromTimestamp(ts1, timezone=-300, site=repo) - t2 = pywikibot.WbTime.fromTimestamp(ts2, timezone=0, site=repo) - self.assertGreater(t1, t2) - - def test_comparison_timezones_equal(self): - """Test when two WbTime's have equal instants but not the same tz.""" - repo = self.get_repo() - ts1 = pywikibot.Timestamp( - year=2023, month=12, day=21, hour=13, - tzinfo=datetime.timezone(datetime.timedelta(hours=-5))) - ts2 = pywikibot.Timestamp( - year=2023, month=12, day=21, hour=18, - tzinfo=datetime.timezone.utc) - self.assertEqual(ts1.timestamp(), ts2.timestamp()) - - t1 = pywikibot.WbTime.fromTimestamp(ts1, timezone=-300, site=repo) - t2 = pywikibot.WbTime.fromTimestamp(ts2, timezone=0, site=repo) - self.assertGreaterEqual(t1, t2) - self.assertGreaterEqual(t2, t1) - self.assertNotEqual(t1, t2) - self.assertNotEqual(t2, t1) - # Ignore H205: We specifically want to test the operator - self.assertFalse(t1 > t2) # noqa: H205 - self.assertFalse(t2 > t1) # noqa: H205 - self.assertFalse(t1 < t2) # noqa: H205 - self.assertFalse(t2 < t1) # noqa: H205 - - def test_comparison_equal_instant(self): - """Test the equal_instant method.""" - repo = self.get_repo() - - ts1 = pywikibot.Timestamp( - year=2023, month=12, day=21, hour=13, - tzinfo=datetime.timezone(datetime.timedelta(hours=-5))) - ts2 = pywikibot.Timestamp( - year=2023, month=12, day=21, hour=18, - tzinfo=datetime.timezone.utc) - ts3 = pywikibot.Timestamp( - year=2023, month=12, day=21, hour=19, - tzinfo=datetime.timezone(datetime.timedelta(hours=1))) - ts4 = pywikibot.Timestamp( - year=2023, month=12, day=21, hour=13, - tzinfo=datetime.timezone(datetime.timedelta(hours=-6))) - - self.assertEqual(ts1.timestamp(), ts2.timestamp()) - self.assertEqual(ts1.timestamp(), ts3.timestamp()) - self.assertEqual(ts2.timestamp(), ts3.timestamp()) - self.assertNotEqual(ts1.timestamp(), ts4.timestamp()) - self.assertNotEqual(ts2.timestamp(), ts4.timestamp()) - self.assertNotEqual(ts3.timestamp(), ts4.timestamp()) - - t1 = pywikibot.WbTime.fromTimestamp(ts1, timezone=-300, site=repo) - t2 = pywikibot.WbTime.fromTimestamp(ts2, timezone=0, site=repo) - t3 = pywikibot.WbTime.fromTimestamp(ts3, timezone=60, site=repo) - t4 = pywikibot.WbTime.fromTimestamp(ts4, timezone=-360, site=repo) - - self.assertTrue(t1.equal_instant(t2)) - self.assertTrue(t1.equal_instant(t3)) - self.assertTrue(t2.equal_instant(t3)) - self.assertFalse(t1.equal_instant(t4)) - self.assertFalse(t2.equal_instant(t4)) - self.assertFalse(t3.equal_instant(t4)) - - -class TestWbQuantity(WbRepresentationTestCase): - - """Test Wikibase WbQuantity data type.""" - - dry = True - - def test_WbQuantity_WbRepresentation_methods(self): - """Test inherited or extended methods from _WbRepresentation.""" - repo = self.get_repo() - q = pywikibot.WbQuantity(amount=1234, error=1, site=repo) - self._test_hashable(q) - - def test_WbQuantity_integer(self): - """Test WbQuantity for integer value.""" - repo = self.get_repo() - q = pywikibot.WbQuantity(amount=1234, error=1, site=repo) - self.assertEqual(q.toWikibase(), - {'amount': '+1234', 'lowerBound': '+1233', - 'upperBound': '+1235', 'unit': '1'}) - q = pywikibot.WbQuantity(amount=5, error=(2, 3), site=repo) - self.assertEqual(q.toWikibase(), - {'amount': '+5', 'lowerBound': '+2', - 'upperBound': '+7', 'unit': '1'}) - q = pywikibot.WbQuantity(amount=0, error=(0, 0), site=repo) - self.assertEqual(q.toWikibase(), - {'amount': '+0', 'lowerBound': '+0', - 'upperBound': '+0', 'unit': '1'}) - q = pywikibot.WbQuantity(amount=-5, error=(2, 3), site=repo) - self.assertEqual(q.toWikibase(), - {'amount': '-5', 'lowerBound': '-8', - 'upperBound': '-3', 'unit': '1'}) - - def test_WbQuantity_float_27(self): - """Test WbQuantity for float value.""" - repo = self.get_repo() - q = pywikibot.WbQuantity(amount=0.044405586, error=0.0, site=repo) - q_dict = {'amount': '+0.044405586', 'lowerBound': '+0.044405586', - 'upperBound': '+0.044405586', 'unit': '1'} - self.assertEqual(q.toWikibase(), q_dict) - - def test_WbQuantity_scientific(self): - """Test WbQuantity for scientific notation.""" - repo = self.get_repo() - q = pywikibot.WbQuantity(amount='1.3e-13', error='1e-14', site=repo) - q_dict = {'amount': '+1.3e-13', 'lowerBound': '+1.2e-13', - 'upperBound': '+1.4e-13', 'unit': '1'} - self.assertEqual(q.toWikibase(), q_dict) - - def test_WbQuantity_decimal(self): - """Test WbQuantity for decimal value.""" - repo = self.get_repo() - q = pywikibot.WbQuantity(amount=Decimal('0.044405586'), - error=Decimal('0.0'), site=repo) - q_dict = {'amount': '+0.044405586', 'lowerBound': '+0.044405586', - 'upperBound': '+0.044405586', 'unit': '1'} - self.assertEqual(q.toWikibase(), q_dict) - - def test_WbQuantity_string(self): - """Test WbQuantity for decimal notation.""" - repo = self.get_repo() - q = pywikibot.WbQuantity(amount='0.044405586', error='0', site=repo) - q_dict = {'amount': '+0.044405586', 'lowerBound': '+0.044405586', - 'upperBound': '+0.044405586', 'unit': '1'} - self.assertEqual(q.toWikibase(), q_dict) - - def test_WbQuantity_formatting_bound(self): - """Test WbQuantity formatting with bounds.""" - repo = self.get_repo() - q = pywikibot.WbQuantity(amount='0.044405586', error='0', site=repo) - self.assertEqual(str(q), - '{{\n' - ' "amount": "+{val}",\n' - ' "lowerBound": "+{val}",\n' - ' "unit": "1",\n' - ' "upperBound": "+{val}"\n' - '}}'.format(val='0.044405586')) - self.assertEqual(repr(q), - 'WbQuantity(amount={val}, ' - 'upperBound={val}, lowerBound={val}, ' - 'unit=1)'.format(val='0.044405586')) - - def test_WbQuantity_self_equality(self): - """Test WbQuantity equality.""" - repo = self.get_repo() - q = pywikibot.WbQuantity(amount='0.044405586', error='0', site=repo) - self.assertEqual(q, q) - - def test_WbQuantity_fromWikibase(self): - """Test WbQuantity.fromWikibase() instantiating.""" - repo = self.get_repo() - q = pywikibot.WbQuantity.fromWikibase({'amount': '+0.0229', - 'lowerBound': '0', - 'upperBound': '1', - 'unit': '1'}, - site=repo) - # note that the bounds are inputted as INT but are returned as FLOAT - self.assertEqual(q.toWikibase(), - {'amount': '+0.0229', 'lowerBound': '+0.0000', - 'upperBound': '+1.0000', 'unit': '1'}) - - def test_WbQuantity_errors(self): - """Test WbQuantity error handling.""" - regex = r'^no amount given$' - with self.assertRaisesRegex(ValueError, regex): - pywikibot.WbQuantity(amount=None, error=1) - - def test_WbQuantity_entity_unit(self): - """Test WbQuantity with entity uri unit.""" - repo = self.get_repo() - q = pywikibot.WbQuantity(amount=1234, error=1, site=repo, - unit='http://www.wikidata.org/entity/Q712226') - self.assertEqual(q.toWikibase(), - {'amount': '+1234', 'lowerBound': '+1233', - 'upperBound': '+1235', - 'unit': 'http://www.wikidata.org/entity/Q712226'}) - - def test_WbQuantity_unit_fromWikibase(self): - """Test WbQuantity recognising unit from Wikibase output.""" - repo = self.get_repo() - q = pywikibot.WbQuantity.fromWikibase({ - 'amount': '+1234', 'lowerBound': '+1233', 'upperBound': '+1235', - 'unit': 'http://www.wikidata.org/entity/Q712226'}, - site=repo) - self.assertEqual(q.toWikibase(), - {'amount': '+1234', 'lowerBound': '+1233', - 'upperBound': '+1235', - 'unit': 'http://www.wikidata.org/entity/Q712226'}) - - -class TestWbQuantityNonDry(WbRepresentationTestCase): - - """Test Wikibase WbQuantity data type (non-dry). - - These can be moved to TestWbQuantity once DrySite has been bumped to - the appropriate version. - """ - - def setUp(self): - """Override setup to store repo and it's version.""" - super().setUp() - self.repo = self.get_repo() - self.version = self.repo.mw_version - - def test_WbQuantity_unbound(self): - """Test WbQuantity for value without bounds.""" - if self.version < MediaWikiVersion('1.29.0-wmf.2'): - self.skipTest('Wiki version must be 1.29.0-wmf.2 or newer to ' - 'support unbound uncertainties.') - q = pywikibot.WbQuantity(amount=1234.5, site=self.repo) - self.assertEqual(q.toWikibase(), - {'amount': '+1234.5', 'unit': '1', - 'upperBound': None, 'lowerBound': None}) - - def test_WbQuantity_formatting_unbound(self): - """Test WbQuantity formatting without bounds.""" - if self.version < MediaWikiVersion('1.29.0-wmf.2'): - self.skipTest('Wiki version must be 1.29.0-wmf.2 or newer to ' - 'support unbound uncertainties.') - q = pywikibot.WbQuantity(amount='0.044405586', site=self.repo) - self.assertEqual(str(q), - '{{\n' - ' "amount": "+{val}",\n' - ' "lowerBound": null,\n' - ' "unit": "1",\n' - ' "upperBound": null\n' - '}}'.format(val='0.044405586')) - self.assertEqual(repr(q), - 'WbQuantity(amount={val}, ' - 'upperBound=None, lowerBound=None, ' - 'unit=1)'.format(val='0.044405586')) - - def test_WbQuantity_fromWikibase_unbound(self): - """Test WbQuantity.fromWikibase() instantiating without bounds.""" - if self.version < MediaWikiVersion('1.29.0-wmf.2'): - self.skipTest('Wiki version must be 1.29.0-wmf.2 or newer to ' - 'support unbound uncertainties.') - q = pywikibot.WbQuantity.fromWikibase({'amount': '+0.0229', - 'unit': '1'}, - site=self.repo) - self.assertEqual(q.toWikibase(), - {'amount': '+0.0229', 'lowerBound': None, - 'upperBound': None, 'unit': '1'}) - - def test_WbQuantity_ItemPage_unit(self): - """Test WbQuantity with ItemPage unit.""" - if self.version < MediaWikiVersion('1.28-wmf.23'): - self.skipTest('Wiki version must be 1.28-wmf.23 or newer to ' - 'expose wikibase-conceptbaseuri.') - - q = pywikibot.WbQuantity(amount=1234, error=1, - unit=pywikibot.ItemPage(self.repo, 'Q712226')) - self.assertEqual(q.toWikibase(), - {'amount': '+1234', 'lowerBound': '+1233', - 'upperBound': '+1235', - 'unit': 'http://www.wikidata.org/entity/Q712226'}) - - def test_WbQuantity_equality(self): - """Test WbQuantity equality with different unit representations.""" - if self.version < MediaWikiVersion('1.28-wmf.23'): - self.skipTest('Wiki version must be 1.28-wmf.23 or newer to ' - 'expose wikibase-conceptbaseuri.') - - a = pywikibot.WbQuantity( - amount=1234, error=1, - unit=pywikibot.ItemPage(self.repo, 'Q712226')) - b = pywikibot.WbQuantity( - amount=1234, error=1, - unit='http://www.wikidata.org/entity/Q712226') - c = pywikibot.WbQuantity( - amount=1234, error=1, - unit='http://test.wikidata.org/entity/Q712226') - d = pywikibot.WbQuantity( - amount=1234, error=2, - unit='http://www.wikidata.org/entity/Q712226') - self.assertEqual(a, b) - self.assertNotEqual(a, c) - self.assertNotEqual(b, c) - self.assertNotEqual(b, d) - - def test_WbQuantity_get_unit_item(self): - """Test getting unit item from WbQuantity.""" - repo = self.get_repo() - q = pywikibot.WbQuantity(amount=1234, error=1, site=repo, - unit='http://www.wikidata.org/entity/Q123') - self.assertEqual(q.get_unit_item(), - ItemPage(repo, 'Q123')) - - def test_WbQuantity_get_unit_item_provide_repo(self): - """Test getting unit item from WbQuantity, providing repo.""" - repo = self.get_repo() - q = pywikibot.WbQuantity(amount=1234, error=1, - unit='http://www.wikidata.org/entity/Q123') - self.assertEqual(q.get_unit_item(repo), - ItemPage(repo, 'Q123')) - - def test_WbQuantity_get_unit_item_different_repo(self): - """Test getting unit item in different repo from WbQuantity.""" - repo = self.get_repo() - test_repo = pywikibot.Site('test', 'wikidata') - q = pywikibot.WbQuantity(amount=1234, error=1, site=repo, - unit='http://test.wikidata.org/entity/Q123') - self.assertEqual(q.get_unit_item(test_repo), - ItemPage(test_repo, 'Q123')) - - -class TestWbMonolingualText(WbRepresentationTestCase): - - """Test Wikibase WbMonolingualText data type.""" - - dry = True - - def test_WbMonolingualText_WbRepresentation_methods(self): - """Test inherited or extended methods from _WbRepresentation.""" - q = pywikibot.WbMonolingualText( - text='Test that basics work', language='en') - self._test_hashable(q) - - def test_WbMonolingualText_string(self): - """Test WbMonolingualText string.""" - q = pywikibot.WbMonolingualText(text='Test that basics work', - language='en') - q_dict = {'text': 'Test that basics work', 'language': 'en'} - self.assertEqual(q.toWikibase(), q_dict) - - def test_WbMonolingualText_unicode(self): - """Test WbMonolingualText unicode.""" - q = pywikibot.WbMonolingualText(text='Testa det här', language='sv') - q_dict = {'text': 'Testa det här', 'language': 'sv'} - self.assertEqual(q.toWikibase(), q_dict) - - def test_WbMonolingualText_equality(self): - """Test WbMonolingualText equality.""" - q = pywikibot.WbMonolingualText(text='Thou shall test this!', - language='en-gb') - self.assertEqual(q, q) - - def test_WbMonolingualText_fromWikibase(self): - """Test WbMonolingualText.fromWikibase() instantiating.""" - q = pywikibot.WbMonolingualText.fromWikibase({'text': 'Test this!', - 'language': 'en'}) - self.assertEqual(q.toWikibase(), - {'text': 'Test this!', 'language': 'en'}) - - def test_WbMonolingualText_errors(self): - """Test WbMonolingualText error handling.""" - regex = r'^text and language cannot be empty$' - with self.assertRaisesRegex(ValueError, regex): - pywikibot.WbMonolingualText(text='', language='sv') - with self.assertRaisesRegex(ValueError, regex): - pywikibot.WbMonolingualText(text='Test this!', language='') - with self.assertRaisesRegex(ValueError, regex): - pywikibot.WbMonolingualText(text=None, language='sv') - - class TestWikibaseParser(WikidataTestCase): """Test passing various datatypes to wikibase parser.""" @@ -989,188 +157,6 @@ def test_wbparse_raises_valueerror(self): self.site.parsevalue('quantity', ['Not a quantity']) -class TestWbGeoShapeNonDry(WbRepresentationTestCase): - - """Test Wikibase WbGeoShape data type (non-dry). - - These require non dry tests due to the page.exists() call. - """ - - def setUp(self): - """Setup tests.""" - self.commons = pywikibot.Site('commons') - self.page = Page(self.commons, 'Data:Lyngby Hovedgade.map') - super().setUp() - - def test_WbGeoShape_WbRepresentation_methods(self): - """Test inherited or extended methods from _WbRepresentation.""" - q = pywikibot.WbGeoShape(self.page) - self._test_hashable(q) - - def test_WbGeoShape_page(self): - """Test WbGeoShape page.""" - q = pywikibot.WbGeoShape(self.page) - q_val = 'Data:Lyngby Hovedgade.map' - self.assertEqual(q.toWikibase(), q_val) - - def test_WbGeoShape_page_and_site(self): - """Test WbGeoShape from page and site.""" - q = pywikibot.WbGeoShape(self.page, self.get_repo()) - q_val = 'Data:Lyngby Hovedgade.map' - self.assertEqual(q.toWikibase(), q_val) - - def test_WbGeoShape_equality(self): - """Test WbGeoShape equality.""" - q = pywikibot.WbGeoShape(self.page, self.get_repo()) - self.assertEqual(q, q) - - def test_WbGeoShape_fromWikibase(self): - """Test WbGeoShape.fromWikibase() instantiating.""" - repo = self.get_repo() - q = pywikibot.WbGeoShape.fromWikibase( - 'Data:Lyngby Hovedgade.map', repo) - self.assertEqual(q.toWikibase(), 'Data:Lyngby Hovedgade.map') - - def test_WbGeoShape_error_on_non_page(self): - """Test WbGeoShape error handling when given a non-page.""" - regex = r'^Page .+? must be a pywikibot\.Page object not a' - with self.assertRaisesRegex(ValueError, regex): - pywikibot.WbGeoShape('A string', self.get_repo()) - - def test_WbGeoShape_error_on_non_exitant_page(self): - """Test WbGeoShape error handling of a non-existant page.""" - page = Page(self.commons, 'Non-existant page... really') - regex = r'^Page \[\[.+?\]\] must exist\.$' - with self.assertRaisesRegex(ValueError, regex): - pywikibot.WbGeoShape(page, self.get_repo()) - - def test_WbGeoShape_error_on_wrong_site(self): - """Test WbGeoShape error handling of a page on non-filerepo site.""" - repo = self.get_repo() - page = Page(repo, 'Q123') - regex = r'^Page must be on the geo-shape repository site\.$' - with self.assertRaisesRegex(ValueError, regex): - pywikibot.WbGeoShape(page, self.get_repo()) - - def test_WbGeoShape_error_on_wrong_page_type(self): - """Test WbGeoShape error handling of a non-map page.""" - non_data_page = Page(self.commons, 'File:Foo.jpg') - non_map_page = Page(self.commons, 'Data:TemplateData/TemplateData.tab') - regex = (r"^Page must be in 'Data:' namespace and end in '\.map' " - r'for geo-shape\.$') - with self.assertRaisesRegex(ValueError, regex): - pywikibot.WbGeoShape(non_data_page, self.get_repo()) - with self.assertRaisesRegex(ValueError, regex): - pywikibot.WbGeoShape(non_map_page, self.get_repo()) - - -class TestWbTabularDataNonDry(WbRepresentationTestCase): - - """Test Wikibase WbTabularData data type (non-dry). - - These require non dry tests due to the page.exists() call. - """ - - def setUp(self): - """Setup tests.""" - self.commons = pywikibot.Site('commons') - self.page = Page(self.commons, 'Data:Bea.gov/GDP by state.tab') - super().setUp() - - def test_WbTabularData_WbRepresentation_methods(self): - """Test inherited or extended methods from _WbRepresentation.""" - q = pywikibot.WbTabularData(self.page) - self._test_hashable(q) - - def test_WbTabularData_page(self): - """Test WbTabularData page.""" - q = pywikibot.WbTabularData(self.page) - q_val = 'Data:Bea.gov/GDP by state.tab' - self.assertEqual(q.toWikibase(), q_val) - - def test_WbTabularData_page_and_site(self): - """Test WbTabularData from page and site.""" - q = pywikibot.WbTabularData(self.page, self.get_repo()) - q_val = 'Data:Bea.gov/GDP by state.tab' - self.assertEqual(q.toWikibase(), q_val) - - def test_WbTabularData_equality(self): - """Test WbTabularData equality.""" - q = pywikibot.WbTabularData(self.page, self.get_repo()) - self.assertEqual(q, q) - - def test_WbTabularData_fromWikibase(self): - """Test WbTabularData.fromWikibase() instantiating.""" - repo = self.get_repo() - q = pywikibot.WbTabularData.fromWikibase( - 'Data:Bea.gov/GDP by state.tab', repo) - self.assertEqual(q.toWikibase(), 'Data:Bea.gov/GDP by state.tab') - - def test_WbTabularData_error_on_non_page(self): - """Test WbTabularData error handling when given a non-page.""" - regex = r'^Page .+? must be a pywikibot\.Page object not a' - with self.assertRaisesRegex(ValueError, regex): - pywikibot.WbTabularData('A string', self.get_repo()) - - def test_WbTabularData_error_on_non_exitant_page(self): - """Test WbTabularData error handling of a non-existant page.""" - page = Page(self.commons, 'Non-existant page... really') - regex = r'^Page \[\[.+?\]\] must exist\.$' - with self.assertRaisesRegex(ValueError, regex): - pywikibot.WbTabularData(page, self.get_repo()) - - def test_WbTabularData_error_on_wrong_site(self): - """Test WbTabularData error handling of a page on non-filerepo site.""" - repo = self.get_repo() - page = Page(repo, 'Q123') - regex = r'^Page must be on the tabular-data repository site\.$' - with self.assertRaisesRegex(ValueError, regex): - pywikibot.WbTabularData(page, self.get_repo()) - - def test_WbTabularData_error_on_wrong_page_type(self): - """Test WbTabularData error handling of a non-map page.""" - non_data_page = Page(self.commons, 'File:Foo.jpg') - non_map_page = Page(self.commons, 'Data:Lyngby Hovedgade.map') - regex = (r"^Page must be in 'Data:' namespace and end in '\.tab' " - r'for tabular-data\.$') - with self.assertRaisesRegex(ValueError, regex): - pywikibot.WbTabularData(non_data_page, self.get_repo()) - with self.assertRaisesRegex(ValueError, regex): - pywikibot.WbTabularData(non_map_page, self.get_repo()) - - -class TestWbUnknown(WbRepresentationTestCase): - - """Test Wikibase WbUnknown data type.""" - - dry = True - - def test_WbUnknown_WbRepresentation_methods(self): - """Test inherited or extended methods from _WbRepresentation.""" - q_dict = {'text': 'Test that basics work', 'language': 'en'} - q = pywikibot.WbUnknown(q_dict) - self._test_hashable(q) - - def test_WbUnknown_string(self): - """Test WbUnknown string.""" - q_dict = {'text': 'Test that basics work', 'language': 'en'} - q = pywikibot.WbUnknown(q_dict) - self.assertEqual(q.toWikibase(), q_dict) - - def test_WbUnknown_equality(self): - """Test WbUnknown equality.""" - q_dict = {'text': 'Thou shall test this!', 'language': 'unknown'} - q = pywikibot.WbUnknown(q_dict) - self.assertEqual(q, q) - - def test_WbUnknown_fromWikibase(self): - """Test WbUnknown.fromWikibase() instantiating.""" - q = pywikibot.WbUnknown.fromWikibase({'text': 'Test this!', - 'language': 'en'}) - self.assertEqual(q.toWikibase(), - {'text': 'Test this!', 'language': 'en'}) - - class TestLoadUnknownType(WikidataTestCase): """Test unknown datatypes being loaded as WbUnknown.""" diff --git a/tox.ini b/tox.ini index e98f12e62d..627dea7ad8 100644 --- a/tox.ini +++ b/tox.ini @@ -220,6 +220,7 @@ per-file-ignores = tests/tools_tests.py: N802 tests/ui_options_tests.py: N802 tests/ui_tests.py: N802 + tests/wbtypes_tests.py: N802 tests/wikibase_edit_tests.py: N802 tests/wikibase_tests.py: N802 tests/xmlreader_tests.py: N802 From 1c6d8c37b8409e74b22db38effb12a6058e9cc85 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Thu, 7 Nov 2024 09:39:15 +0100 Subject: [PATCH 28/95] [doc] Fix doc strings about todo collection Change-Id: I073eaa978a785d999a5f6f0a8b0a25ff39e5b0c1 --- scripts/interwiki.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/scripts/interwiki.py b/scripts/interwiki.py index 8389d97d53..bb298a3072 100755 --- a/scripts/interwiki.py +++ b/scripts/interwiki.py @@ -650,8 +650,8 @@ def __init__(self, origin=None, hints=None, conf=None) -> None: super().__init__(origin) - # TODO is a list of all pages that still need to be analyzed. - # Mark the origin page as todo. + # self.todo is a collection of all pages that still need to be + # analyzed. Mark the origin page as todo. self.todo = SizedKeyCollection('site') if origin: self.todo.append(origin) @@ -725,7 +725,7 @@ def getFoundInCorrectNamespace(self, site): return None def translate(self, hints=None, keephintedsites: bool = False) -> None: - """Add the given translation hints to the todo list.""" + """Add the given translation hints to the todo collection.""" if self.conf.same and self.origin: if hints: hints += ['all:'] @@ -763,7 +763,8 @@ def whatsNextPageBatch(self, site) -> list[pywikibot.Page]: """Return the next page batch. By calling this method, you 'promise' this instance that you - will preload all the *site* Pages that are in the todo list. + will preload all the *site* Pages that are in the todo + collection. :return: This routine will return a list of pages that can be treated. @@ -792,7 +793,7 @@ def makeForcedStop(self, counter) -> None: self.forcedStop = True def addIfNew(self, page, counter, linkingPage) -> bool: - """Add the pagelink given to the todo list, if it hasn't been seen yet. + """Add the *page* to the todo collection, if it hasn't been seen yet. If it is added, update the counter accordingly. @@ -1100,7 +1101,7 @@ def redir_checked(self, page, counter): return True def check_page(self, page, counter) -> None: - """Check whether any iw links should be added to the todo list.""" + """Check whether iw links should be added to the todo collection.""" try: ok = page.exists() except InvalidPageError as e: # T357953 @@ -1274,7 +1275,7 @@ def batchLoaded(self, counter) -> None: counter.minus(page.site) # Now check whether any interwiki links should be added to the - # TODO list. + # self.todo collection. self.check_page(page, counter) # These pages are no longer 'in progress' @@ -1409,8 +1410,8 @@ def assemble(self): def finish(self): """Round up the subject, making any necessary changes. - This should be called exactly once after the todo list has gone empty. - + This should be called exactly once after the todo collection has + gone empty. """ if not self.isDone(): raise Exception('Bugcheck: finish called before done') @@ -1579,7 +1580,7 @@ def replaceLinks(self, page, newPages) -> bool: if pltmp != page: pywikibot.error( f'{page} is not in the list of new links! Found {pltmp}.') - raise SaveError('BUG: sanity check failed') + raise SaveError('sanity check failed') # Avoid adding an iw link back to itself del new[page.site] From c929026882dd954efa5e60b0e6769bf353290fb2 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Thu, 7 Nov 2024 10:09:14 +0100 Subject: [PATCH 29/95] [tests] add 'wbtypes' to library_test_modules set Change-Id: I306830efea955bcea1a2414a96623792f60e2059 --- tests/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/__init__.py b/tests/__init__.py index 8eb9a7a4a5..2c51f5f988 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -136,6 +136,7 @@ def create_path_func(base_func, subpath): 'uploadbot', 'user', 'version', + 'wbtypes', 'wikibase', 'wikibase_edit', 'wikiblame', From 1a04c0949e94133a79c002577aa957ef99ccd6cc Mon Sep 17 00:00:00 2001 From: Translation updater bot <l10n-bot@translatewiki.net> Date: Thu, 7 Nov 2024 13:17:12 +0100 Subject: [PATCH 30/95] Update git submodules * Update scripts/i18n from branch 'master' to 0ea66067be2482aee6634cf7f0c95ce4033c7cde - Localisation updates from https://translatewiki.net. Change-Id: I7a1cda8a2c26da121205b00c0f426331ab0e3527 --- scripts/i18n | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/i18n b/scripts/i18n index 3d6b057230..0ea66067be 160000 --- a/scripts/i18n +++ b/scripts/i18n @@ -1 +1 @@ -Subproject commit 3d6b0572304d23c803e4b9769676f39d770fae2e +Subproject commit 0ea66067be2482aee6634cf7f0c95ce4033c7cde From 4437d70a158f8efd33ead986e912d021d409b928 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Fri, 8 Nov 2024 15:22:31 +0100 Subject: [PATCH 31/95] Announce support of MediaWiki < 1.23 is to be dropped Bug: T378984 Change-Id: Iae91bac7fd2f01f625afee06b1e814f4fbf674d0 --- pywikibot/site/_apisite.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pywikibot/site/_apisite.py b/pywikibot/site/_apisite.py index 81c3a85e5a..3802d23b29 100644 --- a/pywikibot/site/_apisite.py +++ b/pywikibot/site/_apisite.py @@ -1246,6 +1246,14 @@ def version(self) -> str: pywikibot.error(msg) raise + if MediaWikiVersion(version) < '1.31': + warn('\n' + + fill(f'Support of MediaWiki {version} will be dropped. ' + 'It is recommended to use MediaWiki 1.31 or above. ' + 'You may use every Pywikibot 9.X for older MediaWiki ' + 'versions. See T378984 for further information.'), + FutureWarning) + if MediaWikiVersion(version) < '1.27': raise RuntimeError(f'Pywikibot "{pywikibot.__version__}" does not ' f'support MediaWiki "{version}".\n' From 276f60502d51b0d2883ba3409de01f4b89166cf6 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Fri, 8 Nov 2024 14:53:26 +0100 Subject: [PATCH 32/95] [IMPR] Show a warning if Pywikibot is running with Python 3.7 Also use timeout with TestPwb.test_argv Bug: T379227 Change-Id: Idf55843b5540bec81fca2a4fe462602b5e56a664 --- ROADMAP.rst | 2 ++ docs/index.rst | 3 ++- pywikibot/__init__.py | 11 ++++++++++- tests/pwb_tests.py | 2 +- tests/utils.py | 4 ++++ 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/ROADMAP.rst b/ROADMAP.rst index 7bb2dcbebd..7ef2387e50 100644 --- a/ROADMAP.rst +++ b/ROADMAP.rst @@ -1,6 +1,7 @@ Current Release Changes ======================= +* Python 3.7 support will be discontinued and probably this is the last version supporting it * Retry :meth:`data.sparql.SparqlQuery.query` on internal server error (500) (:phab:`T378788`) * Extract :meth:`APISite.linktrail()<pywikibot.site._apisite.APISite.linktrail>` for hr-wiki (:phab:`T378787`) @@ -66,6 +67,7 @@ Current Deprecations Pending removal in Pywikibot 10 ------------------------------- +* 9.6.0: Python 3.7 support is deprecated and will be dropped with Pywikibot 10 * 9.1.0: :func:`version.svn_rev_info` and :func:`version.getversion_svn` will be removed. SVN is no longer supported. (:phab:`T362484`) * 7.7.0: :mod:`tools.threading` classes should no longer imported from :mod:`tools` diff --git a/docs/index.rst b/docs/index.rst index 7929f582d9..ea1163c8c7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,7 +21,8 @@ system that has a compatible version of Python installed. To check whether you have Python installed and to find its version, just type ``python`` at the CMD or shell prompt. -Python 3.7 or higher is currently required to run the bot. +Python 3.7 or higher is currently required to run the bot but Python 3.8 or +higher is recommended. Python 3.7 support will be dropped with Pywikibot 10 soon. Pywikibot and this documentation are licensed under the :ref:`MIT license`; diff --git a/pywikibot/__init__.py b/pywikibot/__init__.py index 2375309712..6819a7f905 100644 --- a/pywikibot/__init__.py +++ b/pywikibot/__init__.py @@ -59,7 +59,7 @@ ) from pywikibot.site import BaseSite as _BaseSite from pywikibot.time import Timestamp -from pywikibot.tools import normalize_username +from pywikibot.tools import PYTHON_VERSION, normalize_username if TYPE_CHECKING: @@ -87,6 +87,15 @@ _sites: dict[str, APISite] = {} +if PYTHON_VERSION < (3, 8): + __version = sys.version.split(maxsplit=1)[0] + warn(f""" + + Python {__version} will be dropped soon with Pywikibot 10. + It is recommended to use Python 3.8 or above. + See phab: T379227 for further information. +""", FutureWarning) # adjust this line no in utils.execute() + @cache def _code_fam_from_url(url: str, name: str | None = None diff --git a/tests/pwb_tests.py b/tests/pwb_tests.py index 5bfbd952e9..abf0293c85 100755 --- a/tests/pwb_tests.py +++ b/tests/pwb_tests.py @@ -83,7 +83,7 @@ def test_argv(self): script_opts = ['-help'] command = [script_path, *script_opts] without_global_args = execute_pwb(command) - with_no_global_args = execute_pwb(['-maxlag:5', *command]) + with_no_global_args = execute_pwb(['-maxlag:5', *command], timeout=10) self.assertEqual(without_global_args['stdout'], with_no_global_args['stdout']) self.assertEqual(without_global_args['stdout'].rstrip(), diff --git a/tests/utils.py b/tests/utils.py index c80e3311b3..89b92bb43c 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -23,6 +23,7 @@ from pywikibot.exceptions import APIError from pywikibot.login import LoginStatus from pywikibot.site import Namespace +from pywikibot.tools import PYTHON_VERSION from pywikibot.tools.collections import EMPTY_DEFAULT from tests import _pwb_py @@ -473,6 +474,9 @@ def execute(command: list[str], *, data_in=None, timeout=None): :param command: executable to run and arguments to use """ + if PYTHON_VERSION < (3, 8): + command.insert(1, '-W ignore::FutureWarning:pywikibot:97') + env = os.environ.copy() # Prevent output by test package; e.g. 'max_retries reduced from x to y' From 8b50db64643295965dd364fb5bc311ee03f2ba0f Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sat, 9 Nov 2024 10:36:55 +0100 Subject: [PATCH 33/95] [tests: Allow some scripts to fail Bug: T379455 Change-Id: I61422b2b2be22c13bcd617416086386f39a7f729 --- tests/script_tests.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/script_tests.py b/tests/script_tests.py index 7bd674ea1c..6a420a2034 100755 --- a/tests/script_tests.py +++ b/tests/script_tests.py @@ -368,6 +368,7 @@ class TestScriptSimulate(DefaultSiteTestCase, PwbTestCase, 'category_redirect', 'claimit', 'clean_sandbox', + 'commons_information', # T379455 'coordinate_import', 'delinker', 'disambredir', @@ -446,6 +447,8 @@ class TestScriptGenerator(DefaultSiteTestCase, PwbTestCase, _allowed_failures = { 'basic', 'delete', # T368859 + 'fixing_redirects', # T379455 + 'illustrate_wikidata', # T379455 'imagetransfer', # T368859 'newitem', 'nowcommons', From 6ca7fb585480cbbc376fe0fe24494c150bd7b116 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sat, 9 Nov 2024 11:09:33 +0100 Subject: [PATCH 34/95] tests: timeout is not required for T379227 Bug: T379227 Change-Id: I655ef2207433b60832e8b004e813a00f1067c7ed --- tests/pwb_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pwb_tests.py b/tests/pwb_tests.py index abf0293c85..5bfbd952e9 100755 --- a/tests/pwb_tests.py +++ b/tests/pwb_tests.py @@ -83,7 +83,7 @@ def test_argv(self): script_opts = ['-help'] command = [script_path, *script_opts] without_global_args = execute_pwb(command) - with_no_global_args = execute_pwb(['-maxlag:5', *command], timeout=10) + with_no_global_args = execute_pwb(['-maxlag:5', *command]) self.assertEqual(without_global_args['stdout'], with_no_global_args['stdout']) self.assertEqual(without_global_args['stdout'].rstrip(), From d10ed91e6fc40da1a22e97e2feca82c25783a53c Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sat, 9 Nov 2024 12:59:48 +0100 Subject: [PATCH 35/95] [tests] Ignore quotes in user name The user name can be quotes by double qotes like in 'user': "Wicci'o'Bot" and this may cause the failure. Bug: T379456 Change-Id: Ia171f2a106f04f2cfd862d4e4b9b6e6980e3393d --- pywikibot/comms/eventstreams.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywikibot/comms/eventstreams.py b/pywikibot/comms/eventstreams.py index a8a83cf8e4..35c02db231 100644 --- a/pywikibot/comms/eventstreams.py +++ b/pywikibot/comms/eventstreams.py @@ -93,7 +93,7 @@ class EventStreams(GeneratorWrapper): 'server_url': 'https://www.wikidata.org', ... 'type': 'edit', - 'user': '...', + 'user': ..., 'wiki': 'wikidatawiki'} >>> del stream From e663f8f6815eef6070a7c282c6289ce50aa95b99 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Fri, 1 Nov 2024 09:45:48 +0100 Subject: [PATCH 36/95] [tests] replace daglint2 tests with docsig Also fix some bugs. Bug: T378801 Change-Id: I0b45581c43aded9187ffb81cd7ef268e3cfe137a --- .pre-commit-config.yaml | 8 +++++++- pywikibot/data/api/_requests.py | 2 +- pywikibot/login.py | 2 +- pywikibot/page/_links.py | 11 +++++------ pywikibot/page/_wikibase.py | 2 -- pywikibot/pagegenerators/_generators.py | 2 +- pywikibot/textlib.py | 2 +- tox.ini | 5 +---- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a8ee50386a..a2b1c160d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -77,6 +77,13 @@ repos: hooks: - id: isort exclude: '^pwb\.py$' + - repo: https://github.com/jshwi/docsig + rev: v0.64.0 + hooks: + - id: docsig + args: + - "-dSIG101,SIG202,SIG203,SIG301,SIG302,SIG401,SIG402,SIG404,SIG501,SIG502,SIG503,SIG505" + exclude: ^(tests|scripts) - repo: https://github.com/PyCQA/flake8 rev: 7.1.1 hooks: @@ -85,7 +92,6 @@ repos: - --doctests additional_dependencies: # Due to incompatibilities between packages the order matters. - - darglint2 - flake8-bugbear!=24.1.17 - flake8-comprehensions>=3.13.0 - flake8-mock-x2 diff --git a/pywikibot/data/api/_requests.py b/pywikibot/data/api/_requests.py index 66797496d4..d0893f647b 100644 --- a/pywikibot/data/api/_requests.py +++ b/pywikibot/data/api/_requests.py @@ -613,7 +613,7 @@ def _build_mime_request(cls, params: dict, :param params: HTTP request params :param mime_params: HTTP request parts which must be sent in the body - :type mime_params: dict of (content, keytype, headers) # noqa: DAR103 + :type mime_params: dict of (content, keytype, headers) :return: HTTP request headers and body """ # construct a MIME message containing all API key/values diff --git a/pywikibot/login.py b/pywikibot/login.py index b670af633a..c4357736bd 100644 --- a/pywikibot/login.py +++ b/pywikibot/login.py @@ -497,7 +497,7 @@ def __init__(self, suffix: str, password: str) -> None: def login_name(self, username: str) -> str: """Construct the login name from the username and suffix. - :param user: username (without suffix) + :param username: username (without suffix) """ return f'{username}@{self.suffix}' diff --git a/pywikibot/page/_links.py b/pywikibot/page/_links.py index fe24dacf93..bf5e323c58 100644 --- a/pywikibot/page/_links.py +++ b/pywikibot/page/_links.py @@ -573,17 +573,15 @@ def fromPage(cls, page, source=None): # noqa: N802 return link @classmethod - def langlinkUnsafe(cls, lang, title, source): # noqa: N802 + def langlinkUnsafe(cls, lang: str, title: str, source): # noqa: N802 """Create a "lang:title" Link linked from source. Assumes that the lang & title come clean, no checks are made. :param lang: target site code (language) - :type lang: str :param title: target Page - :type title: str :param source: Link from site source - :param source: Site + :type source: Site :rtype: pywikibot.page.Link """ @@ -785,11 +783,12 @@ def toJSON(self) -> dict[str, str | list[str]]: # noqa: N802 } -def html2unicode(text: str, ignore=None, exceptions=None) -> str: +def html2unicode(text: str, + ignore: list[int] | None = None, + exceptions=None) -> str: """Replace HTML entities with equivalent unicode. :param ignore: HTML entities to ignore - :param ignore: list of int """ if ignore is None: ignore = [] diff --git a/pywikibot/page/_wikibase.py b/pywikibot/page/_wikibase.py index eb91dcd8e0..34e3f58817 100644 --- a/pywikibot/page/_wikibase.py +++ b/pywikibot/page/_wikibase.py @@ -1214,8 +1214,6 @@ def getSitelink(self, site, force: bool = False) -> str: :param site: Site to find the linked page of. :type site: pywikibot.Site or database name :param force: override caching - :param get_redirect: return the item content, do not follow the - redirect, do not raise an exception. :raise IsRedirectPageError: instance is a redirect page :raise NoSiteLinkError: site is not in :attr:`sitelinks` """ diff --git a/pywikibot/pagegenerators/_generators.py b/pywikibot/pagegenerators/_generators.py index e50cc546c8..ffe6752ecb 100644 --- a/pywikibot/pagegenerators/_generators.py +++ b/pywikibot/pagegenerators/_generators.py @@ -155,7 +155,7 @@ def NewpagesPageGenerator(site: BaseSite | None = None, """Iterate Page objects for all new titles in a single namespace. :param site: Site for generator results. - :param namespace: namespace to retrieve pages from + :param namespaces: namespace to retrieve pages from :param total: Maxmium number of pages to retrieve in total """ # API does not (yet) have a newpages function, so this tries to duplicate diff --git a/pywikibot/textlib.py b/pywikibot/textlib.py index d8e67f9878..b6966c8f99 100644 --- a/pywikibot/textlib.py +++ b/pywikibot/textlib.py @@ -1394,7 +1394,7 @@ def interwikiFormat(links: dict, insite=None) -> str: :param links: interwiki links to be formatted :type links: dict with the Site objects as keys, and Page - or Link objects as values. # noqa: DAR103 + or Link objects as values. :param insite: site the interwiki links will be formatted for (defaulting to the current site). :type insite: BaseSite diff --git a/tox.ini b/tox.ini index 627dea7ad8..4c83cfae36 100644 --- a/tox.ini +++ b/tox.ini @@ -130,10 +130,7 @@ deps = # R100: raise in except handler without from # W503: line break before binary operator; against current PEP 8 recommendation -# DARXXX: Darglint docstring issues to be solved -# DAR000: T368849 - -ignore = B007,DAR000,DAR003,DAR101,DAR102,DAR201,DAR202,DAR301,DAR401,DAR402,DAR501,E704,H101,H231,H232,H233,H234,H235,H236,H237,H238,H301,H306,H404,H405,H903,R100,W503 +ignore = B007,E704,H101,H231,H232,H233,H234,H235,H236,H237,H238,H301,H306,H404,H405,H903,R100,W503 enable-extensions = H203,H204,H205,N818 count = True From 2d0d5dd94d7e690f7d17d491dea743f764ce6bb6 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sat, 9 Nov 2024 16:43:51 +0100 Subject: [PATCH 37/95] tests: autoupdate pre-commit - update ruff-pre-commit Change-Id: I8cf6f4ca35655ac61d4b340b7543df6d98612931 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a2b1c160d8..c526fa256f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,7 +50,7 @@ repos: - id: python-check-mock-methods - id: python-use-type-annotations - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.1 + rev: v0.7.3 hooks: - id: ruff args: From a8e61ee28dd721f8e5c979a4c86ef9946411865e Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sat, 9 Nov 2024 17:42:38 +0100 Subject: [PATCH 38/95] doc: update CONTENT.rst file Change-Id: If140ea225d063d2c0b7d434c9f93c00eb60b0a13 --- CONTENT.rst | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/CONTENT.rst b/CONTENT.rst index 124b051d53..50936affff 100644 --- a/CONTENT.rst +++ b/CONTENT.rst @@ -10,8 +10,6 @@ The contents of the package +---------------------------+-----------------------------------------------------------+ | CONTENT.rst | This Content description file | +---------------------------+-----------------------------------------------------------+ - | dev-requirements.txt | PIP requirements file for development dependencies | - +---------------------------+-----------------------------------------------------------+ | Dockerfile | Assemble a Docker image, install all dependencies via pip | +---------------------------+-----------------------------------------------------------+ | Dockerfile-dev | Docker image including development dependencies | @@ -20,18 +18,22 @@ The contents of the package +---------------------------+-----------------------------------------------------------+ | LICENSE | Reference to the MIT license | +---------------------------+-----------------------------------------------------------+ - | make_dist.py | Script to create a Pywikibot distribution | - +---------------------------+-----------------------------------------------------------+ | MANIFEST.in | Setup file for package data | +---------------------------+-----------------------------------------------------------+ + | README.rst | Short info string used by Pywikibot Nightlies | + +---------------------------+-----------------------------------------------------------+ + | ROADMAP.rst | PyPI version roadmap file | + +---------------------------+-----------------------------------------------------------+ + | dev-requirements.txt | PIP requirements file for development dependencies | + +---------------------------+-----------------------------------------------------------+ + | make_dist.py | Script to create a Pywikibot distribution | + +---------------------------+-----------------------------------------------------------+ | pwb.py | Caller script for pwb wrapper script | +---------------------------+-----------------------------------------------------------+ - | README.rst | Short info string used by Pywikibot Nightlies | + | pyproject.toml | Configuration file used by packaging tools and tests | +---------------------------+-----------------------------------------------------------+ | requirements.txt | General PIP requirements file | +---------------------------+-----------------------------------------------------------+ - | ROADMAP.rst | PyPI version roadmap file | - +---------------------------+-----------------------------------------------------------+ | setup.py | Installer script for Pywikibot framework | +---------------------------+-----------------------------------------------------------+ | tox.ini | Tests config file | @@ -44,6 +46,8 @@ The contents of the package +---------------------------+-----------------------------------------------------------+ | Directories | +===========================+===========================================================+ + | docs | Documentation files | + +---------------------------+-----------------------------------------------------------+ | pywikibot | Contains some libraries and control files | +---------------------------+-----------------------------------------------------------+ | scripts | Contains all bots and utility scripts | From 83197e14a6c64cb49938a53d4d0eaec1cfe06531 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sun, 10 Nov 2024 09:49:35 +0100 Subject: [PATCH 39/95] tests: fix TestItemLoad tests after the label was changed Bug: T379484 Change-Id: Ia01ac9c18d8c453e1800c3bc67397bfe304dc9a7 --- tests/wikibase_tests.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/wikibase_tests.py b/tests/wikibase_tests.py index 8829eeb459..dfb45183e2 100755 --- a/tests/wikibase_tests.py +++ b/tests/wikibase_tests.py @@ -285,7 +285,8 @@ def test_load_item_set_id(self): item.get() self.assertTrue(hasattr(item, '_content')) self.assertIn('en', item.labels) - self.assertEqual(item.labels['en'], 'New York City') + # label could change + self.assertIn(item.labels['en'], ['New York', 'New York City']) self.assertEqual(item.title(), 'Q60') def test_reuse_item_set_id(self): @@ -295,10 +296,12 @@ def test_reuse_item_set_id(self): but modifying item.id does not currently work, and this test highlights that it breaks silently. """ + # label could change + label = ['New York', 'New York City'] wikidata = self.get_repo() item = ItemPage(wikidata, 'Q60') item.get() - self.assertEqual(item.labels['en'], 'New York City') + self.assertIn(item.labels['en'], label) # When the id attribute is modified, the ItemPage goes into # an inconsistent state. @@ -310,9 +313,7 @@ def test_reuse_item_set_id(self): # it doesn't help to clear this piece of saved state. del item._content # The labels are not updated; assertion showing undesirable behaviour: - self.assertEqual(item.labels['en'], 'New York City') - # TODO: This is the assertion that this test should be using: - # self.assertTrue(item.labels['en'].lower().endswith('main page')) + self.assertIn(item.labels['en'], label) def test_empty_item(self): """Test empty wikibase item. From 1e37bd943cb76bab42bb638157fa919f89088307 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sun, 10 Nov 2024 09:59:50 +0100 Subject: [PATCH 40/95] tests: update pre-commit docsig Change-Id: Ie3fa91d7084c34b7c4cd700731bf070cc29f04d1 --- .pre-commit-config.yaml | 4 +--- pyproject.toml | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c526fa256f..861c9f5240 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -78,11 +78,9 @@ repos: - id: isort exclude: '^pwb\.py$' - repo: https://github.com/jshwi/docsig - rev: v0.64.0 + rev: v0.64.1 hooks: - id: docsig - args: - - "-dSIG101,SIG202,SIG203,SIG301,SIG302,SIG401,SIG402,SIG404,SIG501,SIG502,SIG503,SIG505" exclude: ^(tests|scripts) - repo: https://github.com/PyCQA/flake8 rev: 7.1.1 diff --git a/pyproject.toml b/pyproject.toml index 1b959f4e44..bde14aa29b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -126,6 +126,22 @@ Download = "https://www.pywikibot.org" Changelog = "https://doc.wikimedia.org/pywikibot/master/changelog.html" Tracker = "https://phabricator.wikimedia.org/tag/pywikibot/" +[tool.docsig] +disable = [ + "SIG101", + "SIG202", + "SIG203", + "SIG301", + "SIG302", + "SIG401", + "SIG402", + "SIG404", + "SIG501", + "SIG502", + "SIG503", + "SIG505", +] + [tool.isort] py_version = 37 From 7292dcfc7043d30edb222b5658ded165413e824d Mon Sep 17 00:00:00 2001 From: Geertivp <geertivp@gmail.com> Date: Sun, 10 Nov 2024 12:59:22 +0100 Subject: [PATCH 41/95] doc: update documentation for create_isbn_edition.py patch extracted from diff to https://github.com/geertivp/Pywikibot/blob/main/create_isbn_edition.py. with the following changes - move documentation to the end to avoid two Documentation labels - move algorithm description to main() function because it is not related to bot users - remove TODO part which is T379488 now . remove unimplemented part which is T379489 now - use sphinx roles Signed-off-by: xqt <info@gno.de> Bug: T314942 Change-Id: Iaa0caf1d4dcba46d376741edd97269ec5ed1b12e --- scripts/create_isbn_edition.py | 312 +++++++++++++++++++++++---------- 1 file changed, 221 insertions(+), 91 deletions(-) diff --git a/scripts/create_isbn_edition.py b/scripts/create_isbn_edition.py index 75bda4960d..3afac24652 100755 --- a/scripts/create_isbn_edition.py +++ b/scripts/create_isbn_edition.py @@ -1,22 +1,25 @@ #!/usr/bin/env python3 -r"""Pywikibot script to load ISBN related data into Wikidata. +r"""Pywikibot client to load ISBN linked data into Wikidata. Pywikibot script to get ISBN data from a digital library, and create or amend the related Wikidata item for edition (with the -:samp:`P212={ISBN number}` as unique external ID). +:samp:`P212, {ISBN number}` as unique external ID). Use digital libraries to get ISBN data in JSON format, and integrate the results into Wikidata. +.. note:: + ISBN data should only be used for editions, and not for written works. + Then the resulting item number can be used e.g. to generate Wikipedia references using template ``Cite_Q``. -**Parameters**: +**Parameters:** All parameters are optional: .. code:: text - *P1:* digital library (default goob "-") + *P1:* digital library (default wiki "-") bnf Catalogue General (France) bol Bol.com @@ -27,64 +30,83 @@ mcues Ministerio de Cultura (Spain) openl OpenLibrary.org porbase urn.porbase.org Portugal - sbn Servizio Bibliotecario Nazionale + sbn Servizio Bibliotecario Nazionale (Italy) wiki wikipedia.org - worldcat WorldCat + worldcat WorldCat (wc) *P2:* ISO 639-1 language code. Default LANG; e.g. en, nl, fr, de, es, it, etc. *P3 P4...:* P/Q pairs to add additional claims (repeated) e.g. P921 Q107643461 (main subject: database management - linked to P2163 Fast ID) - - *stdin:* ISBN numbers (International standard book number) + linked to P2163, Fast ID 888037) - Free text (e.g. Wikipedia references list, or publication list) - is accepted. Identification is done via an ISBN regex expression. + *stdin:* List of ISBN numbers (International standard book + number, version 10 or 13). Free text (e.g. + Wikipedia references list, or publication list) is + accepted. Identification is done via an ISBN regex + expression. **Functionality:** - * The ISBN number is used as a primary key (P212 where no duplicates - are allowed. The item update is not performed when there is no - unique match - * Statements are added or merged incrementally; existing data is not - overwritten. - * Authors and publishers are searched to get their item number - (ambiguous items are skipped) - * Book title and subtitle are separated with '.', ':', or '-' - * This script can be run incrementally with the same parameters - Caveat: Take into account the Wikidata Query database replication - delay. Wait for minimum 5 minutes to avoid creating duplicate - objects. + * Both ISBN-10 and ISBN-13 numbers are accepted as input. + * Only ISBN-13 numbers are stored. ISBN-10 numbers are only used for + identification purposes; they are not stored. + * The ISBN number is used as a primary key; no two items can have + the same P212 ISBN number. The item update is not performed when + there is no unique match. Only editions are updated or created. + * Individual statements are added or merged incrementally; existing + data is not overwritten. + * Authors and publishers are searched to get their item number; + unknown of ambiguous items are skipped. + * Book title and subtitle are separated with either '.', ':', or '-' + in that order. + * Detect author, illustrator, writer preface, afterwork instances. + * Add profession "author" to individual authors. + * This script can be run incrementally. + +**Examples:** + Default library (Google Books), language (LANG), no additional + statements: + + pwb create_isbn_edition.py 9789042925564 + + Wikimedia, language English, main subject: database management: + + pwb create_isbn_edition.py wiki en P921 Q107643461 978-0-596-10089-6 **Data quality:** + * ISBN numbers (P212) are only assigned to editions. + * A written work should not have an ISBN number (P212). + * For targets of P629 *(edition of)* amend "is an Q47461344 + *(written work)* instance" and "inverse P747 *(work has edition)*" + statements * Use https://query.wikidata.org/querybuilder/ to identify P212 duplicates. Merge duplicate items before running the script again. - * The following properties should only be used for written works + * The following properties should only be used for written works, + not for editions: * P5331: OCLC work ID (editions should only have P243) * P8383: Goodreads-identificatiecode for work (editions should only have P2969) -**Examples:** - Default library (Google Books), language (LANG), no additional - statements: +**Return status:** + The following status codes are returned to the shell:: - pwb create_isbn_edition.py 9789042925564 + 3 Invalid or missing parameter + 4 Library not installed + 12 Item does not exist + 20 Network error - Wikimedia, language Dutch, main subject: database management: - - pwb create_isbn_edition.py wiki en P921 Q107643461 978-0-596-10089-6 - -**Standard ISBN properties:** +**Standard ISBN properties for editions:** :: - P31:Q3331189: instance of edition + P31:Q3331189: instance of edition (mandatory statement) P50: author P123: publisher - P212: canonical ISBN number (lookup via Wikidata Query) - P407: language of work - (Qnumber linked to ISO 639-1 language code) + P212: canonical ISBN number (with dashes; searchable + via Wikidata Query) + P407: language of work (Qnumber linked to ISO 639-1 + language code) P577: date of publication (year) P1476: book title P1680: subtitle @@ -92,65 +114,50 @@ **Other ISBN properties:** :: - P291: place of publication P921: main subject (inverse lookup from external Fast ID P2163) P629: work for edition P747: edition of work - P1104: number of pages **Qualifiers:** :: + P248: Source + P813: Retrieval date P1545: (author) sequence number **External identifiers:** :: - P213: ISNI ID P243: OCLC ID - P496: ORCID iD - P675: Google Books-identificatiecode P1036: Dewey Decimal Classification P2163: Fast ID (inverse lookup via Wikidata Query) -> P921: main subject + + (not implemented) P2969: Goodreads-identificatiecode (only for written works) P5331: OCLC work ID (editions should only have P243) + + (not implemented) P8383: Goodreads-identificatiecode for work (editions should only have P2969) + P213: ISNI ID + P496: ORCID ID + P675: Google Books-identificatiecode -**Author:** - Geert Van Pamel (User:Geertivp), 2022-08-04, +**Unavailable properties from digital library:** + :: - Licensed under MIT amd GNU General Public License v3.0 + (not implemented by isbnlib) + P98: Editor + P110: Illustrator/photographer + P291: place of publication + P1104: number of pages + ?: edition format (hardcover, paperback) -**Documentation:** - * :wiki:`ISBN` - * :wiki:`List_of_ISO_639-1_codes` - * https://www.geeksforgeeks.org/searching-books-with-python/ - * https://www.freecodecamp.org/news/python-json-how-to-convert-a-string-to-json/ - * https://pypi.org/project/isbnlib/ - * https://buildmedia.readthedocs.org/media/pdf/isbnlib/v3.4.5/isbnlib.pdf - * https://isbntools.readthedocs.io/en/latest/info.html - * https://www.wikidata.org/wiki/Property:P212 - * https://www.wikidata.org/wiki/Wikidata:WikiProject_Books - * WikiProject Books: https://www.wikidata.org/wiki/Q21831105 - * https://www.wikidata.org/wiki/Wikidata:List_of_properties/work - * https://www.wikidata.org/wiki/Template:Book_properties - * https://www.wikidata.org/wiki/Template:Bibliographic_properties - * http://classify.oclc.org/classify2/ClassifyDemo - * https://www.wikidata.org/wiki/Wikidata:WikiProject_Source_MetaData - * https://www.wikidata.org/wiki/Help:Sources - * https://www.wikidata.org/wiki/Q22696135 - * https://meta.wikimedia.org/wiki/Community_Wishlist_Survey_2021/Wikidata/Bibliographical_references/sources_for_wikidataitems - * https://doc.wikimedia.org/pywikibot/master/api_ref/pywikibot.html - * https://doc.wikimedia.org/pywikibot/master/ - * https://docs.python.org/3/howto/logging.html - * https://wikitech.wikimedia.org/wiki/Portal:Toolforge - * http://www.isbn.org/standards/home/isbn/international/hyphenation-instructions.asp - * https://www.wikidata.org/wiki/Wikidata:Pywikibot\_-_Python_3_Tutorial/Setting_qualifiers - * https://www.wikidata.org/wiki/Wikidata:Pywikibot\_-_Python_3_Tutorial/Setting_statements +**Author:** + Geert Van Pamel (User:Geertivp), MIT License, 2022-08-04, **Prerequisites:** In addition to Pywikibot the following ISBN lib package is mandatory; @@ -173,20 +180,72 @@ **Restrictions:** * Better use the ISO 639-1 language code parameter as a default. The - language code is not always available from the digital library. - * SPARQL queries run on a replicated database. + language code is not always available from the digital library; + therefore we need a default. + * Publisher unknown: + * Missing P31:Q2085381 statement, missing subclass in script + * Missing alias + * Create publisher + * Unknown author: create author as a person + +**Known Problems:** + * Unknown ISBN, e.g. 9789400012820 + * If there is no ISBN data available for an edition either returns + no output (goob = Google Books), or an error message (wiki, openl). + The script is taking care of both. Try another library instance. + * Only 6 specific ISBN attributes are listed by the webservice(s), + missing are e.g.: place of publication, number of pages + * Some digital libraries have more registrations than others. + * Some digital libraries have data quality problems. + * Not all ISBN atttributes have data values (authors, publisher, + date of publication), language can be missing at the digital + library. + * How to add still more digital libraries? + + * This would require an additional isbnlib module + * Does the KBR has a public ISBN service (Koninklijke Bibliotheek + van België)? + * The script uses multiple webservice calls; script might take time, + but it is automated. + * Need to manually amend ISBN items that have no author, publisher, + or other required data + * You could use another digital library + * Which other services to use? + * BibTex service is currently unavailable + * Filter for work properties: https://www.wikidata.org/wiki/Q63413107 + + :: + + ['9781282557246', '9786612557248', '9781847196057', '9781847196040'] + P5331: OCLC identification code for work 793965595; should only + have P243) + P8383: Goodreads identification code for work 13957943; should + only have P2969) + * ERROR: an HTTP error has ocurred e.g. (503) Service Unavailable + * error: externally-managed-environment + + ``isbnlib-kb`` cannot be installed via :code:`pip install` command. + It raises ``error: externally-managed-environment`` because this + environment is externally managed. + + To install Python packages system-wide, try :samp:`apt install + python3-{xyz}`, where *xyz* is the package you are trying to + install. + + If you wish to install a non-Debian-packaged Python package, + create a virtual environment using + :code:`python3 -m venv path/to/venv`. Then use + :code:`path/to/venv/bin/python` and :code:`path/to/venv/bin/pip`. + Make sure you have ``python3-full`` installed. + + If you wish to install a non-Debian packaged Python application, + it may be easiest to use :samp:`pipx install {xyz}`, which will + manage a virtual environment for you. Make sure you have ``pipx`` + installed. + + .. seealso:: See :pylib:`venv` for more information about virtual + environments. - .. important:: - Replication delay: wait 5 minutes before retry, otherwise there - is a risk for creating duplicates. - -**Algorithm:** - #. Get parameters - #. Validate parameters - #. Get ISBN data - #. Convert ISBN data - #. Get additional data - #. Register ISBN data into Wikidata (create or amend items or claims) **Environment:** The python script can run on the following platforms: @@ -196,47 +255,91 @@ * Toolforge Portal * PAWS - LANG: ISO 639-1 language code + LANG: default ISO 639-1 language code **Applications:** - Generate a book reference. Example for (wp.en): + Generate a book reference. Example for wp.en only: .. code:: wikitext {{Cite Q|Q63413107}} + Use the Visual editor reference with Qnumber. + .. seealso:: + - https://www.wikidata.org/wiki/Wikidata:WikiProject_Books + - https://www.wikidata.org/wiki/Q21831105 (WikiProject Books) - https://meta.wikimedia.org/wiki/WikiCite + - https://phabricator.wikimedia.org/tag/wikicite/ - https://www.wikidata.org/wiki/Q21831105 (WikiCite) - https://www.wikidata.org/wiki/Q22321052 (Cite_Q) - https://www.mediawiki.org/wiki/Global_templates - https://www.wikidata.org/wiki/Wikidata:WikiProject_Source_MetaData - - https://phabricator.wikimedia.org/tag/wikicite/ - https://meta.wikimedia.org/wiki/WikiCite/Shared_Citations + - https://www.wikidata.org/wiki/Q36524 (Authority control) + - https://meta.wikimedia.org/wiki/Community_Wishlist_Survey_2021/Wikidata/Bibliographical_references/sources_for_wikidataitems **Wikidata Query:** * List of editions about musicians: https://w.wiki/5aaz * List of editions having ISBN number: https://w.wiki/5akq **Related projects:** - * :phab:`T314942` (this script) + * :phab:`T314942` * :phab:`T282719` * :phab:`T214802` * :phab:`T208134` * :phab:`T138911` * :phab:`T20814` * :wiki:`User:Citation_bot` - * https://meta.wikimedia.org/wiki/Community_Wishlist_Survey_2021/Wikidata/Bibliographical_references/sources_for_wikidataitems * https://zenodo.org/record/55004#.YvwO4hTP1D8 **Other systems:** - * wiki:`bibliographic_database` + * https://isbn.org/ISBN_converter + * :wiki:`bibliographic_database` * https://www.titelbank.nl/pls/ttb/f?p=103:4012:::NO::P4012_TTEL_ID:3496019&cs=19BB8084860E3314502A1F777F875FE61 + * https://isbndb.com/apidocs/v2 + * https://isbndb.com/book/9780404150006 + +**Documentation:** + * :wiki:`ISBN` + * https://pypi.org/project/isbnlib/ + * https://buildmedia.readthedocs.org/media/pdf/isbnlib/v3.4.5/isbnlib.pdf + * https://www.wikidata.org/wiki/Property:P212 + * http://www.isbn.org/standards/home/isbn/international/hyphenation-instructions.asp + * https://isbntools.readthedocs.io/en/latest/info.html + * :wiki:`List_of_ISO_639-1_codes` + + * https://www.wikidata.org/wiki/Wikidata:List_of_properties/work + * https://www.wikidata.org/wiki/Template:Book_properties + * https://www.wikidata.org/wiki/Template:Bibliographic_properties + * https://www.wikidata.org/wiki/Wikidata:WikiProject_Source_MetaData + * https://www.wikidata.org/wiki/Help:Sources + * https://www.wikidata.org/wiki/Q22696135 (Wikidata references module) + + * https://www.geeksforgeeks.org/searching-books-with-python/ + * http://classify.oclc.org/classify2/ClassifyDemo + * :mod:`pywikibot` + * :api:`Search` + * https://www.mediawiki.org/wiki/Wikibase/API + + * :wiki:`Wikipedia:Book_sources` + * :wiki:`https://en.wikipedia.org/wiki/Wikipedia:ISBN` + * https://www.boek.nl/nur + * https://isbnlib.readthedocs.io/_/downloads/en/latest/pdf/ + * https://www.wikidata.org/wiki/Special:BookSources/978-94-014-9746-6 + + * **Goodreads:** + + - https://github.com/akkana/scripts/blob/master/bookfind.py + - https://www.kaggle.com/code/hoshi7/goodreads-analysis-and-recommending-books?scriptVersionId=18346227 + - https://help.goodreads.com/s/question/0D51H00005FzcX1SAJ/how-can-i-search-by-isbn + - https://help.goodreads.com/s/article/Librarian-Manual-ISBN-10-ISBN-13-and-ASINS + - https://www.goodreads.com/book/show/203964185-de-nieuwe-wereldeconomie .. versionadded:: 7.7 -""" # noqa: E501, W505, W605 +""" # noqa: E501, W505 # # (C) Pywikibot team, 2022-2024 # @@ -767,6 +870,33 @@ def main(*args: str) -> None: If args is an empty list, sys.argv is used. + **Algorithm:** + + :: + + Get parameters from shell + Validate parameters + Get ISBN data + Convert ISBN data: + Reverse names when Lastname, Firstname + Get additional data + Register ISBN data into Wikidata: + Add source reference when creating the item: + (digital library instance, retrieval date) + Create or amend items or claims: + Number the authors in order of appearence + Check data consistency + Correct data quality problems: + OCLC Work ID for Written work + Written work instance statement + Inverse relationship written work -> edition + Move/register OCLC work ID to/with written work + Manually corrections: + Create missing (referenced) items + (authors, publishers, written works, main subject/FAST ID) + Resolve ambiguous values + + :param args: command line arguments """ global booklib From 061031f8015570200332a6990444430982479a12 Mon Sep 17 00:00:00 2001 From: Tol <tol@tol.sh> Date: Tue, 12 Nov 2024 04:46:02 +0000 Subject: [PATCH 42/95] Implement param with_sort_key in Page.categories() In pywikibot.page._pages.BasePage.categories(): When with_sort_key, pass it to self.site.pagecategories() instead of throwing NotImplementedError. In pywikibot.site._generators.GeneratorsMixin.pagecategories(): If with_sort_key: - Use pywikibot.data.api.PropertyGenerator instead of PageGenerator - Create Category items (with sortkey) from the titles in the API response - If content (load category page content) then pass them through self.preloadpages() Bug: T75561 Change-Id: Ie62c921029492fe3ea86cf90c483764131aae23a --- pywikibot/page/_basepage.py | 7 ++----- pywikibot/site/_generators.py | 26 ++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/pywikibot/page/_basepage.py b/pywikibot/page/_basepage.py index 05941b948e..91e8c7b575 100644 --- a/pywikibot/page/_basepage.py +++ b/pywikibot/page/_basepage.py @@ -1737,10 +1737,6 @@ def categories( :return: a generator that yields Category objects. :rtype: generator """ - # FIXME: bug T75561: with_sort_key is ignored by Site.pagecategories - if with_sort_key: - raise NotImplementedError('with_sort_key is not implemented') - # Data might have been preloaded # Delete cache if content is needed and elements have no content if hasattr(self, '_categories'): @@ -1750,7 +1746,8 @@ def categories( else: return itertools.islice(self._categories, total) - return self.site.pagecategories(self, total=total, content=content) + return self.site.pagecategories(self, with_sort_key=with_sort_key, + total=total, content=content) def extlinks(self, total: int | None = None) -> Iterable[str]: """Iterate all external URLs (not interwiki links) from this page. diff --git a/pywikibot/site/_generators.py b/pywikibot/site/_generators.py index ba5d464a98..4d1ecc6aff 100644 --- a/pywikibot/site/_generators.py +++ b/pywikibot/site/_generators.py @@ -451,11 +451,11 @@ def pagelinks( g_content=content, redirects=follow_redirects, **plargs) - # Sortkey doesn't work with generator def pagecategories( self, page: pywikibot.Page, *, + with_sort_key: bool = False, total: int | None = None, content: bool = False, ) -> Iterable[pywikibot.Page]: @@ -463,6 +463,7 @@ def pagecategories( .. seealso:: :api:`Categories` + :param with_sort_key: if True, include the sort key in each Category :param content: if True, load the current content of each iterated page (default False); note that this means the contents of the category description page, not the pages contained in the category @@ -473,9 +474,26 @@ def pagecategories( else: clargs['titles'] = page.title( with_section=False).encode(self.encoding()) - return self._generator(api.PageGenerator, - type_arg='categories', total=total, - g_content=content, **clargs) + if with_sort_key: + page_dict = next(iter(self._generator( + api.PropertyGenerator, + type_arg='categories', + total=total, + clprop='sortkey|timestamp|hidden', + **clargs))) + cats = ( + pywikibot.Category(self, + cat_dict['title'], + sort_key=cat_dict['sortkeyprefix'] or None) + for cat_dict in page_dict.get('categories', []) + ) + return self.preloadpages(cats) if content else cats + else: + return self._generator(api.PageGenerator, + type_arg='categories', + total=total, + g_content=content, + **clargs) def pageimages( self, From 865d14eb6c2b6358c6470210921e906dae57c358 Mon Sep 17 00:00:00 2001 From: Translation updater bot <l10n-bot@translatewiki.net> Date: Thu, 14 Nov 2024 15:04:40 +0100 Subject: [PATCH 43/95] Update git submodules * Update scripts/i18n from branch 'master' to 5224712a07f0c72d2f19a91c2f96ecd7f52eddb4 - Localisation updates from https://translatewiki.net. Change-Id: Icd9a565bc2004e5c7045cc99783370168345e9e9 --- scripts/i18n | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/i18n b/scripts/i18n index 0ea66067be..5224712a07 160000 --- a/scripts/i18n +++ b/scripts/i18n @@ -1 +1 @@ -Subproject commit 0ea66067be2482aee6634cf7f0c95ce4033c7cde +Subproject commit 5224712a07f0c72d2f19a91c2f96ecd7f52eddb4 From 2dededfcd482f042b39d80e2625c7efca0508884 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sun, 17 Nov 2024 16:19:43 +0100 Subject: [PATCH 44/95] doc: Update ROADMAP.rst and documentation of other files Change-Id: I1d5211aa9f8ddade6bbfbb35459994336de7b74b --- ROADMAP.rst | 2 ++ pywikibot/page/_basepage.py | 22 ++++++++++++++++++---- pywikibot/site/_generators.py | 30 +++++++++++++++++++----------- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/ROADMAP.rst b/ROADMAP.rst index 7ef2387e50..f76a3c3cb9 100644 --- a/ROADMAP.rst +++ b/ROADMAP.rst @@ -1,7 +1,9 @@ Current Release Changes ======================= +* Implement param *with_sort_key* in :meth:`page.BasePage.categories` (:phab:`T75561`) * Python 3.7 support will be discontinued and probably this is the last version supporting it +* Add :meth:`page.BasePage.get_revision` method * Retry :meth:`data.sparql.SparqlQuery.query` on internal server error (500) (:phab:`T378788`) * Extract :meth:`APISite.linktrail()<pywikibot.site._apisite.APISite.linktrail>` for hr-wiki (:phab:`T378787`) diff --git a/pywikibot/page/_basepage.py b/pywikibot/page/_basepage.py index 91e8c7b575..335f0e3aac 100644 --- a/pywikibot/page/_basepage.py +++ b/pywikibot/page/_basepage.py @@ -443,6 +443,10 @@ def get_revision( ) -> pywikibot.page.Revision: """Return an old revision of this page. + .. versionadded:: 9.6 + .. seealso:: :meth:`getOldVersion` + + :param oldid: The revid of the revision desired. :param content: if True, retrieve the content of the revision (default False) @@ -456,6 +460,8 @@ def get_revision( def getOldVersion(self, oldid, force: bool = False) -> str: """Return text of an old revision of this page. + .. seealso:: :meth:`get_revision` + :param oldid: The revid of the revision desired. """ return self.get_revision(oldid, content=True, force=force).text @@ -1730,12 +1736,20 @@ def categories( ) -> Iterable[pywikibot.Page]: """Iterate categories that the article is in. - :param with_sort_key: if True, include the sort key in each Category. + .. versionchanged:: 2.0 + *with_sort_key* parameter is not supported and a + NotImplementedError is raised if set. + .. versionchanged:: 9.6 + *with_sort_key* parameter is supported. + .. seealso:: :meth:`Site.pagecategories() + <pywikibot.site._generators.GeneratorsMixin.pagecategories>` + + :param with_sort_key: if True, include the sort key in + each Category. :param total: iterate no more than this number of pages in total - :param content: if True, retrieve the content of the current version - of each category description page (default False) + :param content: if True, retrieve the content of the current + version of each category description page (default False) :return: a generator that yields Category objects. - :rtype: generator """ # Data might have been preloaded # Delete cache if content is needed and elements have no content diff --git a/pywikibot/site/_generators.py b/pywikibot/site/_generators.py index 4d1ecc6aff..0de2182a9c 100644 --- a/pywikibot/site/_generators.py +++ b/pywikibot/site/_generators.py @@ -461,12 +461,19 @@ def pagecategories( ) -> Iterable[pywikibot.Page]: """Iterate categories to which page belongs. - .. seealso:: :api:`Categories` + .. versionadded:: 9.6 + the *with_sort_key* parameter. - :param with_sort_key: if True, include the sort key in each Category - :param content: if True, load the current content of each iterated page - (default False); note that this means the contents of the - category description page, not the pages contained in the category + .. seealso:: + - :meth:`page.BasePage.categories` + - :api:`Categories` + + :param with_sort_key: if True, include the sort key in each + Category + :param content: if True, load the current content of each + iterated page default False); note that this means the + contents of the category description page, not the pages + contained in the category """ clargs: dict[str, Any] = {} if hasattr(page, '_pageid'): @@ -474,6 +481,7 @@ def pagecategories( else: clargs['titles'] = page.title( with_section=False).encode(self.encoding()) + if with_sort_key: page_dict = next(iter(self._generator( api.PropertyGenerator, @@ -488,12 +496,12 @@ def pagecategories( for cat_dict in page_dict.get('categories', []) ) return self.preloadpages(cats) if content else cats - else: - return self._generator(api.PageGenerator, - type_arg='categories', - total=total, - g_content=content, - **clargs) + + return self._generator(api.PageGenerator, + type_arg='categories', + total=total, + g_content=content, + **clargs) def pageimages( self, From 861f073cdf34399c2cbff94f3f0745b0649bd1b2 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Mon, 18 Nov 2024 12:54:47 +0100 Subject: [PATCH 45/95] [tests] Add tests for BasePage.categories(with_sort_key=True) also update documentation and remove timestamp|hidden clprop because they are not used and their property can be found by other methods. Bug: T73561 Change-Id: If975307630924763ab062365b5a918b00e45be4b --- pywikibot/page/_basepage.py | 2 ++ pywikibot/site/_generators.py | 5 ++++- tests/page_tests.py | 25 +++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/pywikibot/page/_basepage.py b/pywikibot/page/_basepage.py index 335f0e3aac..28ad6db0f7 100644 --- a/pywikibot/page/_basepage.py +++ b/pywikibot/page/_basepage.py @@ -1743,6 +1743,8 @@ def categories( *with_sort_key* parameter is supported. .. seealso:: :meth:`Site.pagecategories() <pywikibot.site._generators.GeneratorsMixin.pagecategories>` + .. note:: This method also yields categories which are + transcluded. :param with_sort_key: if True, include the sort key in each Category. diff --git a/pywikibot/site/_generators.py b/pywikibot/site/_generators.py index 0de2182a9c..d77d675f25 100644 --- a/pywikibot/site/_generators.py +++ b/pywikibot/site/_generators.py @@ -468,6 +468,9 @@ def pagecategories( - :meth:`page.BasePage.categories` - :api:`Categories` + .. note:: This method also yields categories which are + transcluded. + :param with_sort_key: if True, include the sort key in each Category :param content: if True, load the current content of each @@ -487,7 +490,7 @@ def pagecategories( api.PropertyGenerator, type_arg='categories', total=total, - clprop='sortkey|timestamp|hidden', + clprop='sortkey', **clargs))) cats = ( pywikibot.Category(self, diff --git a/tests/page_tests.py b/tests/page_tests.py index 6b9c294371..30bb9b1e62 100755 --- a/tests/page_tests.py +++ b/tests/page_tests.py @@ -619,6 +619,31 @@ def test_page_image(self): mainpage.page_image() +class TestPageCategories(TestCase): + + """Categories tests class.""" + + family = 'wikipedia' + code = 'en' + + def testCategories(self): + """Test BasePage.categories() with sort keys.""" + mainhelp = pywikibot.Page(self.site, 'Help:Contents') + cat_help = pywikibot.Category(self.site, 'Category:Help') + + for with_sort_key in (False, True): + with self.subTest(with_sort_key=with_sort_key): + cats = list(mainhelp.categories(with_sort_key=with_sort_key)) + self.assertLength(cats, 4) + self.assertIn(cat_help, cats) + for p in cats: + self.assertIsInstance(p, pywikibot.Category) + if with_sort_key: + self.assertEqual(p.sortKey, 'Contents') + else: + self.assertIsNone(p.sortKey) + + class TestPageCoordinates(TestCase): """Test Page Object using German Wikipedia.""" From 3370dad6df97678256aba497b3b6f27e5d29e8dd Mon Sep 17 00:00:00 2001 From: Geertivp <geertivp@gmail.com> Date: Sun, 10 Nov 2024 20:04:30 +0100 Subject: [PATCH 46/95] [IMPR] update create_isbn_edition.py patch extracted from diff to https://github.com/geertivp/Pywikibot/blob/main/create_isbn_edition.py. with the following changes - keep add_claims function - move the outer try/exception loop to amend_isbn_edition call in main() - code cleanups Signed-off-by: xqt <info@gno.de> Bug: T314942 Change-Id: I34fb2f78f9e383cf8891d80ad96e796a93450273 --- scripts/create_isbn_edition.py | 1425 ++++++++++++++++++++++++-------- 1 file changed, 1078 insertions(+), 347 deletions(-) diff --git a/scripts/create_isbn_edition.py b/scripts/create_isbn_edition.py index 3afac24652..fca0e41a24 100755 --- a/scripts/create_isbn_edition.py +++ b/scripts/create_isbn_edition.py @@ -339,6 +339,8 @@ - https://www.goodreads.com/book/show/203964185-de-nieuwe-wereldeconomie .. versionadded:: 7.7 +.. versionchanged:: 9.6 + several implementation improvements """ # noqa: E501, W505 # # (C) Pywikibot team, 2022-2024 @@ -349,14 +351,16 @@ import os # Operating system import re # Regular expressions (very handy!) -from itertools import islice +import sys # System calls +from contextlib import suppress +from datetime import date from pprint import pformat from typing import Any import pywikibot # API interface to Wikidata -from pywikibot import pagegenerators as pg # Wikidata Query interface from pywikibot.config import verbose_output as verbose from pywikibot.data import api +from pywikibot.tools import first_upper try: @@ -369,258 +373,798 @@ except ImportError as e: unidecode = e -# Initialisation -booklib = 'goob' # Default digital library +# Global variables +# Module name (using the Pywikibot package) +pgmlic = 'MIT License' +creator = 'User:Geertivp' + +MAINLANG = 'en:mul' +MULANG = 'mul' + +# Exit on fatal error (can be disabled with -p; please take care) +exitfatal = True +exitstat = 0 # (default) Exit status + +# Wikibase properties +INSTANCEPROP = 'P31' +AUTHORPROP = 'P50' +EDITORPROP = 'P98' +PROFESSIONPROP = 'P106' +ILLUSTRATORPROP = 'P110' +PUBLISHERPROP = 'P123' +# STYLEPROP = 'P135' +# GENREPROP = 'P136' +# BASEDONPROP = 'P144' +# PREVSERIALPROP = 'P155' +# NEXTSERIALPROP = 'P156' +# PRIZEPROP = 'P166' +# SERIALPROP = 'P179' +# COLLIONPROP = 'P195' +ISBNPROP = 'P212' +ISNIIDPROP = 'P213' +# BNFIDPROP = 'P268' +PLACEPUBPROP = 'P291' +OCLDIDPROP = 'P243' +REFPROP = 'P248' +# EDITIONIDPROP = 'P393' +EDITIONLANGPROP = 'P407' +WIKILANGPROP = 'P424' +# ORIGCOUNTRYPROP = 'P495' +ORCIDIDPROP = 'P496' +PUBYEARPROP = 'P577' +WRITTENWORKPROP = 'P629' +# OPENLIBIDPROP = 'P648' +# TRANSLATORPROP = 'P655' +# PERSONPROP = 'P674' +GOOGLEBOOKIDPROP = 'P675' +# INTARCHIDPROP = 'P724' +EDITIONPROP = 'P747' +# CONTRIBUTORPROP = 'P767' +REFDATEPROP = 'P813' +# STORYLOCPROP = 'P840' +# PRINTEDBYPROP = 'P872' +MAINSUBPROP = 'P921' +# INSPIREDBYPROP = 'P941' +ISBN10PROP = 'P957' +# SUDOCIDPROP = 'P1025' +DEWCLASIDPROP = 'P1036' +# EULIDPROP = 'P1084' +# LIBTHINGIDPROP = 'P1085' +NUMBEROFPAGESPROP = 'P1104' +# LCOCLCCNIDPROP = 'P1144' +# LIBCONGRESSIDPROP = 'P1149' +# BNIDPROP = 'P1143' +# UDCPROP = 'P1190' +# DNBIDPROP = 'P1292' +DESCRIBEDBYPROP = 'P1343' +EDITIONTITLEPROP = 'P1476' +SEQNRPROP = 'P1545' +EDITIONSUBTITLEPROP = 'P1680' +# ASSUMEDAUTHORPROP = 'P1779' +# RSLBOOKIDPROP = 'P1815' +# RSLEDIDPROP = 'P1973' +# GUTENBERGIDPROP = 'P2034' +FASTIDPROP = 'P2163' +# NUMPARTSPROP = 'P2635' +PREFACEBYPROP = 'P2679' +AFTERWORDBYPROP = 'P2680' +GOODREADSIDPROP = 'P2969' +# CZLIBIDPROP = 'P3184' +# BABELIOIDPROP = 'P3631' +# ESTCIDPROP = 'P3939' +OCLCWORKIDPROP = 'P5331' +# K10IDPROP = 'P6721' +# CREATIVEWORKTYPE = 'P7937' +LIBCONGEDPROP = 'P8360' +GOODREADSWORKIDPROP = 'P8383' + +# Instances +AUTHORINSTANCE = 'Q482980' +ILLUSTRATORINSTANCE = 'Q15296811' +WRITERINSTANCE = 'Q36180' + +authorprop_list = { + AUTHORPROP, + EDITORPROP, + ILLUSTRATORPROP, + PREFACEBYPROP, + AFTERWORDBYPROP, +} + +# Profession author required +author_profession = { + AUTHORINSTANCE, + ILLUSTRATORINSTANCE, + WRITERINSTANCE, +} + +# List of digital library synonyms +bookliblist = { + '-': 'wiki', + 'dnl': 'dnb', + 'google': 'goob', + 'gb': 'goob', + 'isbn': 'isbndb', + 'kbn': 'kb', + 'wc': 'worldcat', + 'wcat': 'worldcat', + 'wikipedia': 'wiki', + 'wp': 'wiki', +} + +# List of of digital libraries +# You can better run the script repeatedly with difference library sources. +# Content and completeness differs amongst libraryies. +bib_source = { + # database ID - item number - label - default language + 'bnf': ('Q193563', 'Catalogue General (France)', 'fr'), + 'bol': ('Q609913', 'Bol.Com', 'en'), + 'dnb': ('Q27302', 'Deutsche National Library', 'de'), + 'goob': ('Q206033', 'Google Books', 'en'), + # A (paying) api key is needed + 'isbndb': ('Q117793433', 'isbndb.com', 'en'), + 'kb': ('Q1526131', 'Koninklijke Bibliotheek (Nederland)', 'nl'), + # Not implemented in Belgium + # 'kbr': ('Q383931', 'Koninklijke Bibliotheek (België)', 'nl'), + 'loc': ('Q131454', 'Library of Congress (US)', 'en'), + 'mcues': ('Q750403', 'Ministerio de Cultura (Spain)', 'es'), + 'openl': ('Q1201876', 'OpenLibrary.org', 'en'), + 'porbase': ('Q51882885', 'Portugal (urn.porbase.org)', 'pt'), + 'sbn': ('Q576951', 'Servizio Bibliotecario Nazionale (Italië)', 'it'), + 'wiki': ('Q121093616', 'Wikipedia.org', 'en'), + 'worldcat': ('Q76630151', 'WorldCat (worldcat2)', 'en'), +} + +# Remap obsolete or non-standard language codes +langcode = { + 'dut': 'nl', + 'eng': 'en', + 'frans': 'fr', + 'fre': 'fr', + 'iw': 'he', + 'nld': 'nl', +} + +# Statement property target validation rules +propreqobjectprop = { + # Main subject statement requires an object with FAST ID property + MAINSUBPROP: {FASTIDPROP}, +} # ISBN number: 10 or 13 digits with optional dashes (-) -ISBNRE = re.compile(r'[0-9-]{10,17}') +# or DOI number with 10-prefix +ISBNRE = re.compile(r'[0-9–-]{10,17}') +NAMEREVRE = re.compile(r',(\s*.*)*$') # Reverse lastname, firstname PROPRE = re.compile(r'P[0-9]+') # Wikidata P-number QSUFFRE = re.compile(r'Q[0-9]+') # Wikidata Q-number +# Remove trailing () suffix (keep only the base label) +SUFFRE = re.compile(r'\s*[(].*[)]$') -# Other statements are added via command line parameters +# Required statement for edition +# Additional statements can be added via command line parameters target = { - 'P31': 'Q3331189', # Is an instance of an edition + INSTANCEPROP: 'Q3331189', # Is an instance of an edition + # other statements to add } -# Statement property and instance validation rules +# Instance validation rules for properties propreqinst = { - 'P50': 'Q5', # Author requires human - # Publisher requires publisher - 'P123': {'Q2085381', 'Q1114515', 'Q1320047'}, + AUTHORPROP: {'Q5'}, # Author requires human # Edition language requires at least one of (living, natural) language - 'P407': {'Q34770', 'Q33742', 'Q1288568'}, + EDITIONLANGPROP: {'Q34770', 'Q33742', 'Q1288568'}, + # Is an instance of an edition + INSTANCEPROP: {'Q24017414'}, + # Publisher requires type of publisher + PUBLISHERPROP: {'Q41298', 'Q479716', 'Q1114515', 'Q1320047', 'Q2085381'}, + # Written work (requires list) + WRITTENWORKPROP: ['Q47461344', 'Q7725634'], } -mainlang = os.getenv('LANG', 'en')[:2] # Default description language +# Wikidata transaction comment +transcmt = '#pwb Create ISBN edition' + + +def fatal_error(errcode, errtext): + """A fatal error has occurred. + + Print the error message, and exit with an error code. + """ + global exitstat -# Connect to database -transcmt = '#pwb Create ISBN edition' # Wikidata transaction comment + exitstat = max(exitstat, errcode) + pywikibot.critical(errtext) + if exitfatal: # unless we ignore fatal errors + sys.exit(exitstat) + else: + pywikibot.warning('Proceed after fatal error') + + +def get_item_header(header: str | list[str]) -> str: + """Get the item header (label, description, alias in user language). + + :param header: item label, description, or alias language list + :return: label, description, or alias in the first available language + """ + # Return one of the preferred labels + for lang in main_languages: + if lang in header: + return header[lang] + + # Return any other available label + for lang in header: + return header[lang] + + return '-' + + +def get_item_header_lang(header: str | list[str], lang: str) -> str: + """Get the item header (label, description, alias in user language). + + :param header: item label, description, or alias language list + :param lang: language code + :return: label, description, or alias in the first available language + """ + # Try to get any explicit language code + if lang in header: + return header[lang] + + return get_item_header(header) + + +def get_item_page(qnumber) -> pywikibot.ItemPage: + """Get the item; handle redirects.""" + if isinstance(qnumber, str): + item = pywikibot.ItemPage(repo, qnumber) + try: + item.get() + except pywikibot.exceptions.IsRedirectPageError: + # Resolve a single redirect error + item = item.getRedirectTarget() + label = get_item_header(item.labels) + pywikibot.warning( + f'Item {label} ({qnumber}) redirects to {item.getID()}') + qnumber = item.getID() + else: + item = qnumber + qnumber = item.getID() + + while item.isRedirectPage(): + # Should fix the sitelinks + item = item.getRedirectTarget() + label = get_item_header(item.labels) + pywikibot.warning( + f'Item {label} ({qnumber}) redirects to {item.getID()}') + qnumber = item.getID() + + return item + + +def get_language_preferences() -> list[str]: + """Get the list of preferred languages. + + Uses environment variables LANG, LC_ALL, and LANGUAGE, 'en' is + always appended. + + .. seealso:: + - :wiki:`List_of_ISO_639-1_codes + + :Return: List of ISO 639-1 language codes with strings delimited by + ':'. + """ + # See also: + # https://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html + mainlang = os.getenv('LANGUAGE', + os.getenv('LC_ALL', + os.getenv('LANG', MAINLANG))).split(':') + main_languages = [lang.split('_')[0] for lang in mainlang] + + # Cleanup language list (remove non-ISO codes) + for lang in main_languages: + if len(lang) > 3: + main_languages.remove(lang) + + for lang in MAINLANG.split(':'): + if lang not in main_languages: + main_languages.append(lang) + + return main_languages + + +def item_is_in_list(statement_list: list, itemlist: list[str]) -> str: + """Verify if statement list contains at least one item from the itemlist. + + param statement_list: Statement list + param itemlist: List of values (string) + return: Matching or empty string + """ + for seq in statement_list: + with suppress(AttributeError): # Ignore NoneType error + isinlist = seq.getTarget().getID() + if isinlist in itemlist: + return isinlist + return '' + + +def item_has_label(item, label: str) -> str: + """Verify if the item has a label. + + :param item: Item + :param label: Item label + :return: Matching string + """ + label = unidecode(label).casefold() + for lang in item.labels: + if unidecode(item.labels[lang]).casefold() == label: + return item.labels[lang] + + for lang in item.aliases: + for seq in item.aliases[lang]: + if unidecode(seq).casefold() == label: + return seq + + return '' # Must return "False" when no label + + +def is_in_value_list(statement_list: list, valuelist: list[str]) -> bool: + """Verify if statement list contains at least one value from the valuelist. + + :param statement_list: Statement list of values + :param valuelist: List of values + :return: True when match, False otherwise + """ + for seq in statement_list: + if seq.getTarget() in valuelist: + return True + return False -def is_in_list(statement_list, checklist: list[str]) -> bool: - """Verify if statement list contains at least one item from the checklist. +def get_canon_name(baselabel: str) -> str: + """Get standardised name. - :param statement_list: Statement list - :param checklist: List of values - :Returns: True when match + :param baselabel: input label """ - return any(seq.getTarget().getID() in checklist for seq in statement_list) + suffix = SUFFRE.search(baselabel) # Remove () suffix, if any + if suffix: + baselabel = baselabel[:suffix.start()] # Get canonical form + colonloc = baselabel.find(':') + commaloc = NAMEREVRE.search(baselabel) -def get_item_list(item_name: str, instance_id): + # Reorder "lastname, firstname" and concatenate with space + if colonloc < 0 and commaloc: + baselabel = (baselabel[commaloc.start() + 1:] + + ' ' + baselabel[:commaloc.start()]) + baselabel = baselabel.replace(',', ' ') # Remove remaining "," + + # Remove redundant spaces + baselabel = ' '.join(baselabel.split()) + return baselabel + + +def get_item_list(item_name: str, + instance_id: str | set[str] | list[str]) -> list[str]: """Get list of items by name, belonging to an instance (list). + Normally there should have one single best match. The caller should + take care of homonyms. + + .. seealso:: + https://www.wikidata.org/w/api.php?action=help&modules=wbsearchentities + :param item_name: Item name (case sensitive) - :param instance_id: Instance ID (string, set, or list) - :Returns: Set of items (Q-numbers) + :param instance_id: Instance ID + :return: Set of items (Q-numbers) """ + pywikibot.debug(f'Search label: {item_name.encode("utf-8")}') item_list = set() # Empty set + # TODO: try to us search_entities instead? params = { 'action': 'wbsearchentities', 'format': 'json', 'type': 'item', - 'strictlanguage': False, # All languages are searched, but labels are in native language + 'strictlanguage': False, 'language': mainlang, + 'uselang': mainlang, # (primary) Search language 'search': item_name, # Get item list from label + 'limit': 20, # Should be reasonable value } request = api.Request(site=repo, parameters=params) result = request.submit() + pywikibot.debug(result) if 'search' in result: + # Ignore accents and case + item_name_canon = unidecode(item_name).casefold() + + # Loop though items for res in result['search']: - item = pywikibot.ItemPage(repo, res['id']) - item.get(get_redirect=True) - if 'P31' in item.claims: - for seq in item.claims['P31']: # Loop through instances - # Matching instance - if seq.getTarget().getID() in instance_id: - for lang in item.labels: # Search all languages - # Ignore label case and accents - if (unidecode(item_name.lower()) - == unidecode(item.labels[lang].lower())): - item_list.add(item.getID()) # Label math - for lang in item.aliases: - # Case sensitive for aliases - if item_name in item.aliases[lang]: - item_list.add(item.getID()) # Alias match - return item_list - - -def amend_isbn_edition(isbn_number: str) -> None: - """Amend ISBN registration. - - Amend Wikidata, by registering the ISBN-13 data via P212, - depending on the data obtained from the digital library. - - :param isbn_number: ISBN number (10 or 13 digits with optional hyphens) + item = get_item_page(res['id']) + + # Matching instance + if INSTANCEPROP not in item.claims \ + or not item_is_in_list(item.claims[INSTANCEPROP], instance_id): + continue + + # Search all languages, ignore label case and accents + for lang in item.labels: + if (item_name_canon + == unidecode(item.labels[lang].casefold())): + item_list.add(item.getID()) # Label math + break + + for lang in item.aliases: + for seq in item.aliases[lang]: + if item_name_canon == unidecode(seq).casefold(): + item_list.add(item.getID()) # Alias match + break + + pywikibot.log(item_list) + # Convert set to list + return list(item_list) + + +def get_item_with_prop_value(prop: str, propval: str) -> list[str]: + """Get list of items that have a property/value statement. + + .. seealso:: :api:`Search` + + :param prop: Property ID + :param propval: Property value + :return: List of items (Q-numbers) + """ + srsearch = f'{prop}:{propval}' + pywikibot.debug(f'Search statement: {srsearch}') + item_name_canon = unidecode(propval).casefold() + item_list = set() + # TODO: use APISite.search instead? + params = { + 'action': 'query', # Statement search + 'list': 'search', + 'srsearch': srsearch, + 'srwhat': 'text', + 'format': 'json', + 'srlimit': 50, # Should be reasonable value + } + request = api.Request(site=repo, parameters=params) + result = request.submit() + # https://www.wikidata.org/w/api.php?action=query&list=search&srwhat=text&srsearch=P212:978-94-028-1317-3 + # https://www.wikidata.org/w/index.php?search=P212:978-94-028-1317-3 + + if 'query' in result and 'search' in result['query']: + # Loop though items + for row in result['query']['search']: + qnumber = row['title'] + item = get_item_page(qnumber) + + if prop not in item.claims: + continue + + for seq in item.claims[prop]: + if unidecode(seq.getTarget()).casefold() == item_name_canon: + item_list.add(item.getID()) # Found match + break + + # Convert set to list + pywikibot.log(item_list) + return sorted(item_list) + + +def amend_isbn_edition(isbn_number: str) -> int: + """Amend ISBN registration in Wikidata. + + It is registering the ISBN-13 data via P212, depending on the data + obtained from the digital library. + + :param isbn_number: ISBN number (10 or 13 digits with optional + hyphens) + :return: Return status which is: + + * 0: Amended (found or created) + * 1: Not found + * 2: Ambiguous + * 3: Other error """ isbn_number = isbn_number.strip() if not isbn_number: - return # Do nothing when the ISBN number is missing + return 3 # Do nothing when the ISBN number is missing + # Some digital library services raise failure try: isbn_data = isbnlib.meta(isbn_number, service=booklib) - # {'ISBN-13': '9789042925564', - # 'Title': 'De Leuvense Vaart - Van De Vaartkom Tot Wijgmaal. ' - # 'Aspecten Uit De Industriele Geschiedenis Van Leuven', - # 'Authors': ['A. Cresens'], - # 'Publisher': 'Peeters Pub & Booksellers', - # 'Year': '2012', - # 'Language': 'nl'} + # { + # 'ISBN-13': '9789042925564', + # 'Title': 'De Leuvense Vaart - Van De Vaartkom Tot Wijgmaal. ' + # 'Aspecten Uit De Industriele Geschiedenis Van Leuven', + # 'Authors': ['A. Cresens'], + # 'Publisher': 'Peeters Pub & Booksellers', + # 'Year': '2012', + # 'Language': 'nl', + # } + except isbnlib._exceptions.NotRecognizedServiceError as error: + fatal_error(4, f'{error}\n pip install isbnlib-xxx') + except isbnlib._exceptions.NotValidISBNError as error: + pywikibot.error(error) + return 1 except Exception as error: # When the book is unknown the function returns - pywikibot.error(error) - return + pywikibot.error(f'{isbn_number} not found\n{error}') + return 1 - if len(isbn_data) < 6: - pywikibot.error(f'Unknown or incomplete digital library registration ' - f'for {isbn_number}') - return + # Others return an empty result + if not isbn_data: + pywikibot.error( + f'Unknown ISBN book number {isbnlib.mask(isbn_number)}') + return 1 # Show the raw results + # Can be very useful in troubleshooting if verbose: pywikibot.info('\n' + pformat(isbn_data)) - add_claims(isbn_data) + return add_claims(isbn_data) -def add_claims(isbn_data: dict[str, Any]) -> None: # noqa: C901 +def add_claims(isbn_data: dict[str, Any]) -> int: # noqa: C901 """Inspect isbn_data and add claims if possible.""" + global proptyx + # targetx is not global (to allow for language specific editions) + # Get the book language from the ISBN book reference booklang = mainlang # Default language if isbn_data['Language']: - booklang = isbn_data['Language'].strip() - if booklang == 'iw': # Obsolete codes - booklang = 'he' - lang_list = list(get_item_list(booklang, propreqinst['P407'])) + # Get the book language from the ISBN book number + # Can overwrite the default language + booklang = isbn_data['Language'].strip().lower() + + # Replace obsolete or non-standard codes + if booklang in langcode: + booklang = langcode[booklang] + + # Get Wikidata language code + lang_list = get_item_list(booklang, propreqinst[EDITIONLANGPROP]) + + # Hardcoded parameter + if 'Q3504110' in lang_list: # Somebody insisted on this disturbing value + lang_list.remove('Q3504110') # Remove duplicate "En" language + + if not lang_list: + # Can' t store unknown language (need to update mapping table...) + pywikibot.error(f'Unknown language {booklang}'.format(booklang)) + return 3 + + if len(lang_list) != 1: + # Ambiguous language + pywikibot.warning(f'Ambiguous language {booklang}') + return 3 + + # Set edition language item number + target[EDITIONLANGPROP] = lang_list[0] + + # Require short Wikipedia language code + if len(booklang) > 3: + # Get best ranked language item + lang = get_item_page(lang_list[0]) + + # Get official language code + if WIKILANGPROP in lang.claims: + booklang = lang.claims[WIKILANGPROP][0].getTarget() - if not lang_list: - pywikibot.warning('Unknown language ' + booklang) - return + # Get edition title + edition_title = isbn_data['Title'].strip() - if len(lang_list) != 1: - pywikibot.warning('Ambiguous language ' + booklang) - return + # Split (sub)title with first matching delimiter + # By priority of parsing strings: + for seq in ['|', '. ', ' - ', ': ', '; ', ', ']: + titles = edition_title.split(seq) + if len(titles) > 1: + break - target['P407'] = lang_list[0] + if verbose: # Print (sub)title(s) + pywikibot.info('\n' + pformat(titles)) # Get formatted ISBN number isbn_number = isbn_data['ISBN-13'] # Numeric format isbn_fmtd = isbnlib.mask(isbn_number) # Canonical format pywikibot.info(isbn_fmtd) # First one - # Get (sub)title when there is a dot - titles = isbn_data['Title'].split('. ') # goob is using a '.' - if len(titles) == 1: - titles = isbn_data['Title'].split(': ') # Extract subtitle - if len(titles) == 1: - titles = isbn_data['Title'].split(' - ') # Extract subtitle + # Get main title and subtitle objectname = titles[0].strip() - subtitle = '' + subtitle = '' # If there was no delimiter, there is no subtitle if len(titles) > 1: - subtitle = titles[1].strip() - - # pywikibot.info book titles - pywikibot.debug(objectname) - pywikibot.debug(subtitle) # Optional - - # print subsequent subtitles, when available - for title in islice(titles, 2, None): - # Not stored in Wikidata... - pywikibot.debug(title.strip()) - - # Search the ISBN number in Wikidata both canonical and numeric - # P212 should have canonical hyphenated format - isbn_query = f"""# Get ISBN number -SELECT ?item WHERE {{ - VALUES ?isbn_number {{ - "{isbn_fmtd}" - "{isbn_number}" - }} - ?item wdt:P212 ?isbn_number. -}} -""" - - pywikibot.info(isbn_query) - generator = pg.WikidataSPARQLPageGenerator(isbn_query, site=repo) - - # Main loop for all DISTINCT items - rescnt = 0 - for rescnt, item in enumerate(generator, start=1): - qnumber = item.getID() - pywikibot.warning(f'Found item: {qnumber}') + # Redundant "subtitles" are ignored + subtitle = first_upper(titles[1].strip()) + + # Get formatted ISBN number + isbn_number = isbn_data['ISBN-13'] # Numeric format + isbn_fmtd = isbnlib.mask(isbn_number) # Canonical format (with "-") + pywikibot.log(isbn_fmtd) + + # Search the ISBN number both in canonical and numeric format + qnumber_list = get_item_with_prop_value(ISBNPROP, isbn_fmtd) + qnumber_list += get_item_with_prop_value(ISBNPROP, isbn_number) + + # Get addional data from the digital library + # This could fail with + # ISBNLibHTTPError('403 Are you making many requests?') + # Handle ISBN classification + # pwb create_isbn_edition - de P407 Q188 978-3-8376-5645-9 Q113460204 + # { + # 'owi': '11103651812', + # 'oclc': '1260160983', + # 'lcc': 'TK5105.8882', + # 'ddc': '300', + # 'fast': { + # '1175035': 'Wikis (Computer science)', + # '1795979': 'Wikipedia', + # '1122877': 'Social sciences' + # } + # } + isbn_classify = {} + try: + isbn_classify = isbnlib.classify(isbn_number) + except Exception as error: + pywikibot.error(f'Classify error, {error}') + else: + pywikibot.info('\n' + pformat(isbn_classify)) + + # Note that only older works have an ISBN10 number + isbn10_number = '' + isbn10_fmtd = '' + + # Take care of ISBNLibHTTPError + # (classify is more important than obsolete ISBN-10) + # ISBNs were not used before 1966 + # Since 2007, new ISBNs are only issued in the ISBN-13 format + if isbn_fmtd.startswith('978-'): + try: + # Returns empty string for non-978 numbers + isbn10_number = isbnlib.to_isbn10(isbn_number) + if isbn10_number: + isbn10_fmtd = isbnlib.mask(isbn10_number) + pywikibot.info(f'ISBN 10: {isbn10_fmtd}') + qnumber_list += get_item_with_prop_value(ISBN10PROP, + isbn10_fmtd) + qnumber_list += get_item_with_prop_value(ISBN10PROP, + isbn10_number) + except Exception as error: + pywikibot.error(f'ISBN 10 error, {error}') + + qnumber_list = sorted(set(qnumber_list)) # Get unique values # Create or amend the item - if rescnt == 1: - item.get(get_redirect=True) # Update item - elif not rescnt: - label = {booklang: objectname} + + if not qnumber_list: + # Create the edition + label = {MULANG: objectname} item = pywikibot.ItemPage(repo) # Create item item.editEntity({'labels': label}, summary=transcmt) + qnumber = item.getID() # Get new item number + status = 'Created' + elif len(qnumber_list) == 1: + qnumber = qnumber_list[0] + item = get_item_page(qnumber) qnumber = item.getID() - pywikibot.warning(f'Creating item: {qnumber}') + + # Update item only if edition, or instance is missing + if (INSTANCEPROP in item.claims + and not item_is_in_list(item.claims[INSTANCEPROP], + [target[INSTANCEPROP]])): + pywikibot.error( + f'Item {qnumber} {isbn_fmtd} is not an edition; not updated') + return 3 + + # Add missing book label for book language + if MULANG not in item.labels: + item.labels[MULANG] = objectname + item.editEntity({'labels': item.labels}, summary=transcmt) + status = 'Found' else: - pywikibot.critical(f'Ambiguous ISBN number {isbn_fmtd}') - return + pywikibot.error(f'Ambiguous ISBN number {isbn_fmtd}, ' + f'{qnumber_list} not updated') + return 2 - # Add all P/Q values - # Make sure that labels are known in the native language - pywikibot.debug(target) + pywikibot.warning(f'{status} item: P212:{isbn_fmtd} ({qnumber}) ' + f'language {booklang} ({target[EDITIONLANGPROP]}) ' + f'{get_item_header_lang(item.labels, booklang)}') - # Register statements + # Register missing statements + pywikibot.debug(target) for propty in target: if propty not in item.claims: if propty not in proptyx: proptyx[propty] = pywikibot.PropertyPage(repo, propty) - targetx[propty] = pywikibot.ItemPage(repo, target[propty]) - try: - pywikibot.warning( - f'Add {proptyx[propty].labels[booklang]} ' - f'({propty}): {targetx[propty].labels[booklang]} ' - f'({target[propty]})' - ) - except: # noqa: B001, E722, H201 - pywikibot.warning(f'Add {propty}:{target[propty]}') + # Target could get overwritten locally + targetx[propty] = pywikibot.ItemPage(repo, target[propty]) claim = pywikibot.Claim(repo, propty) claim.setTarget(targetx[propty]) - item.addClaim(claim, bot=True, summary=transcmt) + item.addClaim(claim, bot=wdbotflag, summary=transcmt) + pywikibot.warning( + f'Add {get_item_header_lang(proptyx[propty].labels, booklang)}' + f':{get_item_header_lang(targetx[propty].labels, booklang)} ' + f'({propty}:{target[propty]})' + ) + + # Set source reference + if booklib in bib_sourcex: + # A source reference can be only used once + # Expected error: + # "The provided Claim instance is already used in an entity" + # TODO: This error is sometimes raised without reason + try: + claim.addSources(booklib_ref, summary=transcmt) + except ValueError as error: + pywikibot.error(f'Source reference error, {error}') + + if (DESCRIBEDBYPROP not in item.claims + or not item_is_in_list(item.claims[DESCRIBEDBYPROP], + [bib_source[booklib][0]])): + claim = pywikibot.Claim(repo, DESCRIBEDBYPROP) + claim.setTarget(bib_sourcex[booklib]) + item.addClaim(claim, bot=wdbotflag, summary=transcmt) + pywikibot.warning( + f'Add described by:{booklib} - {bib_source[booklib][1]} ' + f'({DESCRIBEDBYPROP}:{bib_source[booklib][0]})' + ) - # Set formatted ISBN number - if 'P212' not in item.claims: - pywikibot.warning(f'Add ISBN number (P212): {isbn_fmtd}') - claim = pywikibot.Claim(repo, 'P212') + if ISBNPROP not in item.claims: + # Create formatted ISBN-13 number + claim = pywikibot.Claim(repo, ISBNPROP) claim.setTarget(isbn_fmtd) - item.addClaim(claim, bot=True, summary=transcmt) + item.addClaim(claim, bot=wdbotflag, summary=transcmt) + pywikibot.warning(f'Add ISBN number ({ISBNPROP}) {isbn_fmtd}') + else: + for seq in item.claims[ISBNPROP]: + # Update unformatted to formatted ISBN-13 + if seq.getTarget() == isbn_number: + seq.changeTarget(isbn_fmtd, bot=wdbotflag, summary=transcmt) + pywikibot.warning( + f'Set formatted ISBN number ({ISBNPROP}): {isbn_fmtd}') + + if not isbn10_fmtd: + pass + elif ISBN10PROP in item.claims: + for seq in item.claims[ISBN10PROP]: + # Update unformatted to formatted ISBN-10 + if seq.getTarget() == isbn10_number: + seq.changeTarget(isbn10_fmtd, bot=wdbotflag, summary=transcmt) + pywikibot.warning('Set formatted ISBN-10 number ' + f'({ISBN10PROP}): {isbn10_fmtd}') + else: + # Create ISBN-10 number + claim = pywikibot.Claim(repo, ISBN10PROP) + claim.setTarget(isbn10_fmtd) + item.addClaim(claim, bot=wdbotflag, summary=transcmt) + pywikibot.warning(f'Add ISBN-10 number ({ISBN10PROP}) {isbn10_fmtd}') # Title - if 'P1476' not in item.claims: - pywikibot.warning(f'Add Title (P1476): {objectname}') - claim = pywikibot.Claim(repo, 'P1476') + if EDITIONTITLEPROP not in item.claims: + claim = pywikibot.Claim(repo, EDITIONTITLEPROP) claim.setTarget( pywikibot.WbMonolingualText(text=objectname, language=booklang)) - item.addClaim(claim, bot=True, summary=transcmt) + item.addClaim(claim, bot=wdbotflag, summary=transcmt) + pywikibot.warning(f'Add Title ({EDITIONTITLEPROP}) {objectname}') # Subtitle - if subtitle and 'P1680' not in item.claims: - pywikibot.warning(f'Add Subtitle (P1680): {subtitle}') - claim = pywikibot.Claim(repo, 'P1680') + if subtitle and EDITIONSUBTITLEPROP not in item.claims: + claim = pywikibot.Claim(repo, EDITIONSUBTITLEPROP) claim.setTarget( pywikibot.WbMonolingualText(text=subtitle, language=booklang)) - item.addClaim(claim, bot=True, summary=transcmt) + item.addClaim(claim, bot=wdbotflag, summary=transcmt) + pywikibot.warning(f'Add Subtitle ({EDITIONSUBTITLEPROP}): {subtitle}') # Date of publication pub_year = isbn_data['Year'] - if pub_year and 'P577' not in item.claims: - pywikibot.warning( - f"Add Year of publication (P577): {isbn_data['Year']}") - claim = pywikibot.Claim(repo, 'P577') + if pub_year and PUBYEARPROP not in item.claims: + claim = pywikibot.Claim(repo, PUBYEARPROP) claim.setTarget(pywikibot.WbTime(year=int(pub_year), precision='year')) - item.addClaim(claim, bot=True, summary=transcmt) + item.addClaim(claim, bot=wdbotflag, summary=transcmt) + pywikibot.warning( + f'Add Year of publication ({PUBYEARPROP}): {isbn_data["Year"]}') - # Get the author list + # Set the author list author_cnt = 0 for author_name in isbn_data['Authors']: author_name = author_name.strip() @@ -628,131 +1172,220 @@ def add_claims(isbn_data: dict[str, Any]) -> None: # noqa: C901 continue author_cnt += 1 - author_list = list(get_item_list(author_name, propreqinst['P50'])) + # Reorder "lastname, firstname" and concatenate with space + author_name = get_canon_name(author_name) + author_list = get_item_list(author_name, propreqinst[AUTHORPROP]) if len(author_list) == 1: add_author = True - if 'P50' in item.claims: - for seq in item.claims['P50']: - if seq.getTarget().getID() in author_list: + author_item = get_item_page(author_list[0]) + + if (PROFESSIONPROP not in author_item.claims + or not item_is_in_list(author_item.claims[PROFESSIONPROP], + author_profession)): + # Add profession:author statement + claim = pywikibot.Claim(repo, PROFESSIONPROP) + claim.setTarget(target_author) + author_item.addClaim( + claim, bot=wdbotflag, + summary=f'{transcmt} {PROFESSIONPROP}:{AUTHORINSTANCE}') + pywikibot.warning( + 'Add profession:author ' + f'({PROFESSIONPROP}:{AUTHORINSTANCE}) to ' + f'{author_name} ({author_list[0]})' + ) + + # Possibly found as author? + # Possibly found as editor? + # Possibly found as illustrator/photographer? + for prop in authorprop_list: + if prop not in item.claims: + continue + + for claim in item.claims[prop]: + book_author = claim.getTarget() + if book_author.getID() == author_list[0]: + # Add missing sequence number + if SEQNRPROP not in claim.qualifiers: + qualifier = pywikibot.Claim(repo, SEQNRPROP) + qualifier.setTarget(str(author_cnt)) + claim.addQualifier(qualifier, bot=wdbotflag, + summary=transcmt) add_author = False break - if add_author: - pywikibot.warning(f'Add author {author_cnt} (P50): ' - f'{author_name} ({author_list[0]})') - claim = pywikibot.Claim(repo, 'P50') - claim.setTarget(pywikibot.ItemPage(repo, author_list[0])) - item.addClaim(claim, bot=True, summary=transcmt) + elif item_has_label(book_author, author_name): + pywikibot.warning( + f'Edition has conflicting author ({prop}) ' + f'{author_name} ({book_author.getID()})' + ) + add_author = False + break - qualifier = pywikibot.Claim(repo, 'P1545') + if add_author: + claim = pywikibot.Claim(repo, AUTHORPROP) + claim.setTarget(author_item) + item.addClaim(claim, bot=wdbotflag, summary=transcmt) + pywikibot.warning(f'Add author {author_cnt}:{author_name} ' + f'({AUTHORPROP}:{author_list[0]})') + + # Add sequence number + qualifier = pywikibot.Claim(repo, SEQNRPROP) qualifier.setTarget(str(author_cnt)) - claim.addQualifier(qualifier, summary=transcmt) - elif not author_list: - pywikibot.warning(f'Unknown author: {author_name}') + claim.addQualifier(qualifier, bot=wdbotflag, summary=transcmt) + elif author_list: + pywikibot.error(f'Ambiguous author: {author_name}') else: - pywikibot.warning(f'Ambiguous author: {author_name}') + pywikibot.error(f'Unknown author: {author_name}') - # Get the publisher + # Set the publisher publisher_name = isbn_data['Publisher'].strip() if publisher_name: - publisher_list = list( - get_item_list(publisher_name, propreqinst['P123'])) + publisher_list = get_item_list(get_canon_name(publisher_name), + propreqinst[PUBLISHERPROP]) if len(publisher_list) == 1: - if 'P123' not in item.claims: - pywikibot.warning(f'Add publisher (P123): {publisher_name} ' - f'({publisher_list[0]})') - claim = pywikibot.Claim(repo, 'P123') - claim.setTarget(pywikibot.ItemPage(repo, publisher_list[0])) - item.addClaim(claim, bot=True, summary=transcmt) - elif not publisher_list: - pywikibot.warning('Unknown publisher: ' + publisher_name) + if (PUBLISHERPROP not in item.claims + or not item_is_in_list(item.claims[PUBLISHERPROP], + publisher_list)): + claim = pywikibot.Claim(repo, PUBLISHERPROP) + claim.setTarget(get_item_page(publisher_list[0])) + item.addClaim(claim, bot=wdbotflag, summary=transcmt) + pywikibot.warning(f'Add publisher: {publisher_name} ' + f'({PUBLISHERPROP}:{publisher_list[0]})') + elif publisher_list: + pywikibot.error(f'Ambiguous publisher: {publisher_name} ' + f'({publisher_list})') else: - pywikibot.warning('Ambiguous publisher: ' + publisher_name) + pywikibot.error(f'Unknown publisher: {publisher_name}') - # Get addional data from the digital library - isbn_cover = isbnlib.cover(isbn_number) - isbn_editions = isbnlib.editions(isbn_number, service='merge') - isbn_doi = isbnlib.doi(isbn_number) - isbn_info = isbnlib.info(isbn_number) - - if verbose: - pywikibot.info() - pywikibot.info(isbn_info) - pywikibot.info(isbn_doi) - pywikibot.info(isbn_editions) + # Amend Written work relationship (one to many relationship) + if WRITTENWORKPROP in item.claims: + work = item.claims[WRITTENWORKPROP][0].getTarget() + if len(item.claims[WRITTENWORKPROP]) > 1: # Many to many (error) + pywikibot.error(f'Written work {work.getID()} is not unique') + else: + # Enhance data quality for Written work + if ISBNPROP in work.claims: + pywikibot.error(f'Written work {work.getID()} must not have' + ' an ISBN number') + + # Add written work instance + if (INSTANCEPROP not in work.claims + or not item_is_in_list(work.claims[INSTANCEPROP], + propreqinst[WRITTENWORKPROP])): + claim = pywikibot.Claim(repo, INSTANCEPROP) + claim.setTarget(get_item_page(propreqinst[WRITTENWORKPROP][0])) + work.addClaim(claim, bot=wdbotflag, summary=transcmt) + pywikibot.warning( + f'Add is a:written work instance ({INSTANCEPROP}:' + f'{propreqinst[WRITTENWORKPROP][0]}) ' + f'to written work {work.getID()}' + ) - # Book cover images - pywikibot.info(pformat(isbn_cover)) + # Check if inverse relationship to "edition of" exists + if (EDITIONPROP not in work.claims + or not item_is_in_list(work.claims[EDITIONPROP], + [qnumber])): + claim = pywikibot.Claim(repo, EDITIONPROP) + claim.setTarget(item) + work.addClaim(claim, bot=wdbotflag, summary=transcmt) + pywikibot.warning( + f'Add edition statement ({EDITIONPROP}:{qnumber}) to ' + f'written work {work.getID()}' + ) - # Handle ISBN classification - isbn_classify = isbnlib.classify(isbn_number) - pywikibot.debug(pformat(isbn_classify)) - - # ./create_isbn_edition.py '978-3-8376-5645-9' - de P407 Q188 - # Q113460204 - # {'owi': '11103651812', 'oclc': '1260160983', 'lcc': 'TK5105.8882', - # 'ddc': '300', 'fast': {'1175035': 'Wikis (Computer science)', - # '1795979': 'Wikipedia', - # '1122877': 'Social sciences'}} - - # Set the OCLC ID - if 'oclc' in isbn_classify and 'P243' not in item.claims: - pywikibot.warning(f"Add OCLC ID (P243): {isbn_classify['oclc']}") - claim = pywikibot.Claim(repo, 'P243') + # We need to first set the OCLC ID + # Because OCLC Work ID can be in conflict for edition + if 'oclc' in isbn_classify and OCLDIDPROP not in item.claims: + claim = pywikibot.Claim(repo, OCLDIDPROP) claim.setTarget(isbn_classify['oclc']) - item.addClaim(claim, bot=True, summary=transcmt) - - # OCLC ID and OCLC work ID should not be both assigned - if 'P243' in item.claims and 'P5331' in item.claims: - if 'P629' in item.claims: - oclcwork = item.claims['P5331'][0] # OCLC Work should be unique - # Get the OCLC Work ID from the edition - oclcworkid = oclcwork.getTarget() + item.addClaim(claim, bot=wdbotflag, summary=transcmt) + pywikibot.warning( + f'Add OCLC ID ({OCLDIDPROP}) {isbn_classify["oclc"]}') + + # OCLC ID and OCLC Work ID should not be both assigned + # Move OCLC Work ID to work if possible + if OCLDIDPROP in item.claims and OCLCWORKIDPROP in item.claims: + # Check if OCLC Work is available + oclcwork = item.claims[OCLCWORKIDPROP][0] # OCLC Work should be unique + # Get the OCLC Work ID from the edition + oclcworkid = oclcwork.getTarget() + + # Keep OCLC Work ID in edition if ambiguous + if len(item.claims[OCLCWORKIDPROP]) > 1: + pywikibot.error( + 'OCLC Work ID {work.getID()} is not unique; not moving') + elif WRITTENWORKPROP in item.claims: # Edition should belong to only one single work - work = item.claims['P629'][0].getTarget() - # There doesn't exist a moveClaim method? + work = item.claims[WRITTENWORKPROP][0].getTarget() pywikibot.warning( f'Move OCLC Work ID {oclcworkid} to work {work.getID()}') - # Keep current OCLC Work ID if present - if 'P5331' not in work.claims: - claim = pywikibot.Claim(repo, 'P5331') + + # Keep OCLC Work ID in edition if mismatch or ambiguity + if len(item.claims[WRITTENWORKPROP]) > 1: + pywikibot.error( + f'Written Work {work.getID()} is not unique; not moving') + elif OCLCWORKIDPROP not in work.claims: + claim = pywikibot.Claim(repo, OCLCWORKIDPROP) claim.setTarget(oclcworkid) - work.addClaim(claim, bot=True, summary=transcmt) - # OCLC Work ID does not belong to edition - item.removeClaims(oclcwork, bot=True, summary=transcmt) + work.addClaim(claim, bot=wdbotflag, + summary='#pwb Move OCLC Work ID') + pywikibot.warning( + f'Move OCLC Work ID ({OCLCWORKIDPROP}) {oclcworkid} to ' + f'written work {work.getID()}' + ) + + # OCLC Work ID does not belong to edition + item.removeClaims(oclcwork, bot=wdbotflag, + summary='#pwb Move OCLC Work ID') + elif is_in_value_list(work.claims[OCLCWORKIDPROP], oclcworkid): + # OCLC Work ID does not belong to edition + item.removeClaims(oclcwork, bot=wdbotflag, + summary='#pwb Remove redundant OCLC Work ID') + else: + pywikibot.error( + f'OCLC Work ID mismatch {oclcworkid} - ' + f'{work.claims[OCLCWORKIDPROP][0].getTarget()}; not moving' + ) else: - pywikibot.error('OCLC Work ID {} conflicts with OCLC ID {} and no ' - 'work available' - .format(item.claims['P5331'][0].getTarget(), - item.claims['P243'][0].getTarget())) + pywikibot.error(f'OCLC Work ID {oclcworkid} conflicts with OCLC ' + f'ID {item.claims[OCLDIDPROP][0].getTarget()} and' + ' no work available') # OCLC work ID should not be registered for editions, only for works if 'owi' not in isbn_classify: pass - elif 'P629' in item.claims: # Get the work related to the edition + elif WRITTENWORKPROP in item.claims: + # Get the work related to the edition # Edition should only have one single work - work = item.claims['P629'][0].getTarget() - if 'P5331' not in work.claims: # Assign the OCLC work ID if missing - pywikibot.warning( - f"Add OCLC work ID (P5331): {isbn_classify['owi']} to work " - f'{work.getID()}') - claim = pywikibot.Claim(repo, 'P5331') + # Assign the OCLC work ID if missing in work + work = item.claims[WRITTENWORKPROP][0].getTarget() + if (OCLCWORKIDPROP not in work.claims + or not is_in_value_list(work.claims[OCLCWORKIDPROP], + isbn_classify['owi'])): + claim = pywikibot.Claim(repo, OCLCWORKIDPROP) claim.setTarget(isbn_classify['owi']) - work.addClaim(claim, bot=True, summary=transcmt) - elif 'P243' in item.claims: - pywikibot.warning('OCLC Work ID {} ignored because of OCLC ID {}' - .format(isbn_classify['owi'], - item.claims['P243'][0].getTarget())) - # Assign the OCLC work ID only if there is no work, and no OCLC ID - # for edition - elif 'P5331' not in item.claims: + work.addClaim(claim, bot=wdbotflag, summary=transcmt) + pywikibot.warning( + f'Add OCLC work ID ({OCLCWORKIDPROP}) {isbn_classify["owi"]} ' + f'to work {work.getID()}' + ) + elif OCLDIDPROP in item.claims: pywikibot.warning( - f"Add OCLC work ID (P5331): {isbn_classify['owi']} to edition") - claim = pywikibot.Claim(repo, 'P5331') + f'OCLC Work ID {isbn_classify["owi"]} ignored because of OCLC ID' + f'{item.claims[OCLDIDPROP][0].getTarget()}' + ) + elif (OCLCWORKIDPROP not in item.claims + or not is_in_value_list(item.claims[OCLCWORKIDPROP], + isbn_classify['owi'])): + # Assign the OCLC work ID only if there is no work, and no OCLC ID for + # edition + claim = pywikibot.Claim(repo, OCLCWORKIDPROP) claim.setTarget(isbn_classify['owi']) - item.addClaim(claim, bot=True, summary=transcmt) + item.addClaim(claim, bot=wdbotflag, summary=transcmt) + pywikibot.warning(f'Add OCLC work ID ({OCLCWORKIDPROP}) ' + f'{isbn_classify["owi"]} to edition') # Reverse logic for moving OCLC ID and P212 (ISBN) from work to # edition is more difficult because of 1:M relationship... @@ -765,22 +1398,22 @@ def add_claims(isbn_data: dict[str, Any]) -> None: # noqa: C901 # registered for editions; should rather use P2969 # Library of Congress Classification (works and editions) - if 'lcc' in isbn_classify and 'P8360' not in item.claims: - pywikibot.warning( - 'Add Library of Congress Classification for edition (P8360): ' - f"{isbn_classify['lcc']}") - claim = pywikibot.Claim(repo, 'P8360') + if 'lcc' in isbn_classify and LIBCONGEDPROP not in item.claims: + claim = pywikibot.Claim(repo, LIBCONGEDPROP) claim.setTarget(isbn_classify['lcc']) - item.addClaim(claim, bot=True, summary=transcmt) - - # Dewey Decimale Classificatie - if 'ddc' in isbn_classify and 'P1036' not in item.claims: + item.addClaim(claim, bot=wdbotflag, summary=transcmt) pywikibot.warning( - f"Add Dewey Decimale Classificatie (P1036): {isbn_classify['ddc']}" + 'Add Library of Congress Classification for edition ' + f'({LIBCONGEDPROP}) {isbn_classify["lcc"]}' ) - claim = pywikibot.Claim(repo, 'P1036') + + # Dewey Decimale Classificatie + if 'ddc' in isbn_classify and DEWCLASIDPROP not in item.claims: + claim = pywikibot.Claim(repo, DEWCLASIDPROP) claim.setTarget(isbn_classify['ddc']) - item.addClaim(claim, bot=True, summary=transcmt) + item.addClaim(claim, bot=wdbotflag, summary=transcmt) + pywikibot.warning(f'Add Dewey Decimale Classificatie ({DEWCLASIDPROP})' + f' {isbn_classify["ddc"]}') # Register Fast ID using P921 (main subject) through P2163 (Fast ID) # https://www.wikidata.org/wiki/Q3294867 @@ -793,76 +1426,75 @@ def add_claims(isbn_data: dict[str, Any]) -> None: # noqa: C901 # Corresponding to P921 (Wikidata main subject) if 'fast' in isbn_classify: for fast_id in isbn_classify['fast']: - - # Get the main subject - main_subject_query = f"""# Search the main subject -SELECT ?item WHERE {{ - ?item wdt:P2163 "{fast_id}". -}} -""" - - pywikibot.info(main_subject_query) - generator = pg.WikidataSPARQLPageGenerator(main_subject_query, - site=repo) - - # Main loop for all DISTINCT items - rescnt = 0 - for rescnt, main_subject in enumerate(generator, start=1): - qmain_subject = main_subject.getID() - try: - main_subject_label = main_subject.labels[booklang] - pywikibot.info(f'Found main subject {main_subject_label} ' - f'({qmain_subject}) for Fast ID {fast_id}') - except: # noqa: B001, E722, H201 - main_subject_label = '' - pywikibot.info(f'Found main subject ({qmain_subject}) for ' - f'Fast ID {fast_id}') - pywikibot.error(f'Missing label for item {qmain_subject}') - - # Create or amend P921 statement - if not rescnt: - pywikibot.error( - f'Main subject not found for Fast ID {fast_id}') - elif rescnt == 1: - add_main_subject = True - if 'P921' in item.claims: # Check for duplicates - for seq in item.claims['P921']: - if seq.getTarget().getID() == qmain_subject: - add_main_subject = False - break - - if add_main_subject: - pywikibot.warning( - f'Add main subject (P921) {main_subject_label} ' - f'({qmain_subject})') - claim = pywikibot.Claim(repo, 'P921') - claim.setTarget(main_subject) - item.addClaim(claim, bot=True, summary=transcmt) + # Get the main subject item number + qmain_subject = get_item_with_prop_value(FASTIDPROP, fast_id) + main_subject_label = isbn_classify['fast'][fast_id].lower() + + if len(qmain_subject) == 1: + # Get main subject and label + main_subject = get_item_page(qmain_subject[0]) + main_subject_label = get_item_header(main_subject.labels) + + if (MAINSUBPROP in item.claims + and item_is_in_list(item.claims[MAINSUBPROP], + qmain_subject)): + pywikibot.log( + f'Skipping main subject ({MAINSUBPROP}): ' + f'{main_subject_label} ({qmain_subject[0]})' + ) else: - pywikibot.info(f'Skipping main subject ' - f'{main_subject_label} ({qmain_subject})') + claim = pywikibot.Claim(repo, MAINSUBPROP) + claim.setTarget(main_subject) + # Add main subject + item.addClaim(claim, bot=wdbotflag, summary=transcmt) + pywikibot.warning( + f'Add main subject:{main_subject_label} ' + f'({MAINSUBPROP}:{qmain_subject[0]})' + ) + elif qmain_subject: + pywikibot.error(f'Ambiguous main subject for Fast ID {fast_id}' + f' - {main_subject_label}') else: - pywikibot.error( - f'Ambiguous main subject for Fast ID {fast_id}') - show_final_information(isbn_number, isbn_doi) + pywikibot.error(f'Main subject not found for Fast ID {fast_id}' + f' - {main_subject_label}') + + show_final_information(isbn_number) + return 0 -def show_final_information(number, doi): - """Print additional information.""" +def show_final_information(isbn_number: str) -> None: + """Print additional information. + + Get optional information.Could generate too many transactions errors; + so the process might stop at the first error. + """ # Book description - description = isbnlib.desc(number) - if description: + isbn_description = isbnlib.desc(isbn_number) + if isbn_description: pywikibot.info() - pywikibot.info(description) + pywikibot.info(isbn_description) - try: - bibtex_metadata = isbnlib.doi2tex(doi) - except Exception as error: - # Currently does not work (service not available) - pywikibot.error(error) # Data not available - pywikibot.warning('BibTex unavailable') - else: - pywikibot.info(bibtex_metadata) + # ISBN info + isbn_info = isbnlib.info(isbn_number) + if isbn_info: + pywikibot.info(isbn_info) + + # DOI number + isbn_doi = isbnlib.doi(isbn_number) + if isbn_doi: + pywikibot.info(isbn_doi) + + # ISBN editions + isbn_editions = isbnlib.editions(isbn_number, service='merge') + if isbn_editions: + pywikibot.info(isbn_editions) + + # Book cover images + isbn_cover = isbnlib.cover(isbn_number) + for seq in isbn_cover: + pywikibot.info(f'{seq}: {isbn_cover[seq]}') + + # BibTex currently does not work (service not available); code was removed. def main(*args: str) -> None: @@ -899,67 +1531,166 @@ def main(*args: str) -> None: :param args: command line arguments """ + global bib_sourcex global booklib + global booklib_ref + global exitstat global mainlang - global repo + global main_languages global proptyx + global repo + global target_author global targetx + global wdbotflag + + # check dependencies + for module in (isbnlib, unidecode): + if isinstance(module, ImportError): + raise module # Get optional parameters local_args = pywikibot.handle_args(*args) + # Connect to databases # Login to Wikibase instance # Required for wikidata object access (item, property, statement) repo = pywikibot.Site('wikidata') + repo.login() + + # Get language list + main_languages = get_language_preferences() + + # Get all program parameters + pywikibot.info(f'{pywikibot.calledModuleName()}, ' + f'{pywikibot.__version__}, {pgmlic}, {creator}') + + # This script requires a bot flag + wdbotflag = 'bot' in pywikibot.User(repo, repo.user()).groups() + + # Prebuilt targets + target_author = pywikibot.ItemPage(repo, AUTHORINSTANCE) + + # Get today's date + today = date.today() + date_ref = pywikibot.WbTime(year=int(today.strftime('%Y')), + month=int(today.strftime('%m')), + day=int(today.strftime('%d')), + precision='day') # Get the digital library + booklib = 'wiki' if local_args: booklib = local_args.pop(0) - if booklib == '-': - booklib = 'goob' + booklib = bookliblist.get(booklib, booklib) + + # Get ItemPage for digital library sources + bib_sourcex = {seq: get_item_page(bib_source[seq][0]) + for seq in bib_source} + + if booklib in bib_sourcex: + references = pywikibot.Claim(repo, REFPROP) + references.setTarget(bib_sourcex[booklib]) + + # Set retrieval date + retrieved = pywikibot.Claim(repo, REFDATEPROP) + retrieved.setTarget(date_ref) + booklib_ref = [references, retrieved] + + # Register source and retrieval date + mainlang = bib_source[booklib][2] + else: + # Unknown bib reference - show implemented codes + for seq in bib_source: + pywikibot.info(f'{seq.ljust(10)}{bib_source[seq][2].ljust(4)}' + f'{bib_source[seq][1]}') + fatal_error(3, f'Unknown Digital library ({REFPROP}) {booklib}') + + # Get optional parameters (all are optional) # Get the native language # The language code is only required when P/Q parameters are added, - # or different from the LANG code + # or different from the environment LANG code if local_args: mainlang = local_args.pop(0) + if mainlang not in main_languages: + main_languages.insert(0, mainlang) + + pywikibot.info( + f'Refers to Digital library: {bib_source[booklib][1]} ' + f'({REFPROP}:{bib_source[booklib][0]}), language {mainlang}' + ) + # Get additional P/Q parameters while local_args: - inpar = PROPRE.findall(local_args.pop(0).upper())[0] - target[inpar] = QSUFFRE.findall(local_args.pop(0).upper())[0] + inpar = local_args.pop(0).upper() + inprop = PROPRE.findall(inpar)[0] + + if ':-' in inpar: + target[inprop] = '-' + else: + if ':Q' not in inpar: + inpar = local_args.pop(0).upper() + try: + target[inprop] = QSUFFRE.findall(inpar)[0] + except IndexError: + target[inprop] = '-' + break # Validate P/Q list proptyx = {} targetx = {} - # Validate the propery/instance pair + # Validate and encode the propery/instance pair for propty in target: if propty not in proptyx: proptyx[propty] = pywikibot.PropertyPage(repo, propty) - targetx[propty] = pywikibot.ItemPage(repo, target[propty]) - targetx[propty].get(get_redirect=True) - if propty in propreqinst and ( - 'P31' not in targetx[propty].claims or not is_in_list( - targetx[propty].claims['P31'], propreqinst[propty])): - pywikibot.critical(f'{targetx[propty].labels[mainlang]} ' - f'({target[propty]}) is not a language') - return - - # check dependencies - for module in (isbnlib, unidecode): - if isinstance(module, ImportError): - raise module + if target[propty] != '-': + targetx[propty] = get_item_page(target[propty]) + pywikibot.info(f'Add {get_item_header(proptyx[propty].labels)}:' + f'{get_item_header(targetx[propty].labels)} ' + f'({propty}:{target[propty]})') + + # Check the instance type for P/Q pairs (critical) + if (propty in propreqinst + and (INSTANCEPROP not in targetx[propty].claims + or not item_is_in_list(targetx[propty].claims[INSTANCEPROP], + propreqinst[propty]))): + pywikibot.critical( + f'{get_item_header(targetx[propty].labels)} ({target[propty]})' + f' is not one of instance type {propreqinst[propty]} for ' + f'statement {get_item_header(proptyx[propty].labels)} ' + f'({propty})' + ) + sys.exit(3) + + # Verify that the target of a statement has a certain property + # (warning) + if (propty in propreqobjectprop + and not item_is_in_list(targetx[propty].claims, + propreqobjectprop[propty])): + pywikibot.error( + f'{get_item_header(targetx[propty].labels)} ({target[propty]})' + f' does not have property {propreqobjectprop[propty]} for ' + f'statement {get_item_header(proptyx[propty].labels)} ' + f'({propty})' + ) # Get list of item numbers # Typically the Appendix list of references of e.g. a Wikipedia page # containing ISBN numbers - inputfile = pywikibot.input('Get list of item numbers') - # Extract all ISBN numbers - itemlist = sorted(set(ISBNRE.findall(inputfile))) + # Extract all ISBN numbers from local args + itemlist = sorted(arg for arg in local_args if ISBNRE.fullmatch(arg)) for isbn_number in itemlist: # Process the next edition - amend_isbn_edition(isbn_number) + try: + exitstat = amend_isbn_edition(isbn_number) + except isbnlib.dev._exceptions.ISBNLibHTTPError: + pywikibot.exception() + except pywikibot.exceptions.Error as e: + pywikibot.error(e) + + sys.exit(exitstat) if __name__ == '__main__': From 7e036ad3d022794b808b6d82fb0c3288651d2dd7 Mon Sep 17 00:00:00 2001 From: Translation updater bot <l10n-bot@translatewiki.net> Date: Mon, 18 Nov 2024 13:18:37 +0100 Subject: [PATCH 47/95] Update git submodules * Update scripts/i18n from branch 'master' to 342e547ae7874288def3d99a13f9f875d160ee7c - Localisation updates from https://translatewiki.net. Change-Id: I29b93d48f70e3a262c9955b9ab50299f3af27e7a --- scripts/i18n | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/i18n b/scripts/i18n index 5224712a07..342e547ae7 160000 --- a/scripts/i18n +++ b/scripts/i18n @@ -1 +1 @@ -Subproject commit 5224712a07f0c72d2f19a91c2f96ecd7f52eddb4 +Subproject commit 342e547ae7874288def3d99a13f9f875d160ee7c From 9d28f8cb6a92ea4758e425476b521a111a69859f Mon Sep 17 00:00:00 2001 From: Geertivp <geertivp@gmail.com> Date: Mon, 18 Nov 2024 17:06:02 +0100 Subject: [PATCH 48/95] Enhance searching the item number from the ISBN number Get list of Wikidata items (more efficient; more details available). Enhanced error messages. Show the required isbnlib libraries. Bug: T314942 Change-Id: I6f34e5757fa6e2dd06adb31d9d6136a3e7846c6e Signed-off-by: xqt <info@gno.de> --- scripts/create_isbn_edition.py | 192 +++++++++++++++++++-------------- 1 file changed, 112 insertions(+), 80 deletions(-) diff --git a/scripts/create_isbn_edition.py b/scripts/create_isbn_edition.py index fca0e41a24..1a33d05f5e 100755 --- a/scripts/create_isbn_edition.py +++ b/scripts/create_isbn_edition.py @@ -171,8 +171,8 @@ .. code:: shell - pip install isbnlib-bol pip install isbnlib-bnf + pip install isbnlib-bol pip install isbnlib-dnb pip install isbnlib-kb pip install isbnlib-loc @@ -245,7 +245,25 @@ .. seealso:: See :pylib:`venv` for more information about virtual environments. + .. note:: If you believe this is a mistake, please contact your + Python installation or OS distribution provider. You can + override this, at the risk of breaking your Python installation + or OS, by passing ``--break-system-packages`` to ``pip``. + .. hint:: See :pep:`668` for the detailed specification. + + You need to install a local python environment: + + - https://pip.pypa.io/warnings/venv + - :python:`tutorial/venv` + + .. code-block:: bash + sudo -s + apt install python3-full + python3 -m venv /opt/python + /opt/python/bin/pip install pywikibot + /opt/python/bin/pip install isbnlib-kb + /opt/python/bin/python ../userscripts/create_isbn_edition.py kb **Environment:** The python script can run on the following platforms: @@ -471,7 +489,7 @@ AFTERWORDBYPROP, } -# Profession author required +# Profession author instances author_profession = { AUTHORINSTANCE, ILLUSTRATORINSTANCE, @@ -496,23 +514,31 @@ # You can better run the script repeatedly with difference library sources. # Content and completeness differs amongst libraryies. bib_source = { - # database ID - item number - label - default language - 'bnf': ('Q193563', 'Catalogue General (France)', 'fr'), - 'bol': ('Q609913', 'Bol.Com', 'en'), - 'dnb': ('Q27302', 'Deutsche National Library', 'de'), - 'goob': ('Q206033', 'Google Books', 'en'), + # database ID: item number, label, default language, package + 'bnf': ('Q193563', 'Catalogue General (France)', 'fr', 'isbnlib-bnf'), + 'bol': ('Q609913', 'Bol.Com', 'en', 'isbnlib-bol'), + 'dnb': ('Q27302', 'Deutsche National Library', 'de', 'isbnlib-dnb'), + 'goob': ('Q206033', 'Google Books', 'en', 'isbnlib'), # lib # A (paying) api key is needed - 'isbndb': ('Q117793433', 'isbndb.com', 'en'), - 'kb': ('Q1526131', 'Koninklijke Bibliotheek (Nederland)', 'nl'), + 'isbndb': ('Q117793433', 'isbndb.com', 'en', 'isbnlib'), + 'kb': ('Q1526131', 'Koninklijke Bibliotheek (Nederland)', 'nl', + 'isbnlib-kb'), # Not implemented in Belgium - # 'kbr': ('Q383931', 'Koninklijke Bibliotheek (België)', 'nl'), - 'loc': ('Q131454', 'Library of Congress (US)', 'en'), - 'mcues': ('Q750403', 'Ministerio de Cultura (Spain)', 'es'), - 'openl': ('Q1201876', 'OpenLibrary.org', 'en'), - 'porbase': ('Q51882885', 'Portugal (urn.porbase.org)', 'pt'), - 'sbn': ('Q576951', 'Servizio Bibliotecario Nazionale (Italië)', 'it'), - 'wiki': ('Q121093616', 'Wikipedia.org', 'en'), - 'worldcat': ('Q76630151', 'WorldCat (worldcat2)', 'en'), + # 'kbr': ('Q383931', 'Koninklijke Bibliotheek (België)', 'nl', 'isbnlib'), + 'loc': ('Q131454', 'Library of Congress (US)', 'en', 'isbnlib-loc'), + 'mcues': ('Q750403', 'Ministerio de Cultura (Spain)', 'es', + 'isbnlib-mcues'), + 'openl': ('Q1201876', 'OpenLibrary.org', 'en', 'isbnlib'), # lib + 'porbase': ('Q51882885', 'Portugal (urn.porbase.org)', 'pt', + 'isbnlib-porbase'), + 'sbn': ('Q576951', 'Servizio Bibliotecario Nazionale (Italië)', 'it', + 'isbnlib-sbn'), + 'wiki': ('Q121093616', 'Wikipedia.org', 'en', 'isbnlib'), # lib + 'worldcat': ('Q76630151', 'WorldCat (worldcat2)', 'en', + 'isbnlib-worldcat2'), + # isbnlib-oclc + # https://github.com/swissbib + # others to be added } # Remap obsolete or non-standard language codes @@ -742,7 +768,7 @@ def get_canon_name(baselabel: str) -> str: def get_item_list(item_name: str, - instance_id: str | set[str] | list[str]) -> list[str]: + instance_id: str | set[str] | list[str]) -> set[str]: """Get list of items by name, belonging to an instance (list). Normally there should have one single best match. The caller should @@ -753,7 +779,7 @@ def get_item_list(item_name: str, :param item_name: Item name (case sensitive) :param instance_id: Instance ID - :return: Set of items (Q-numbers) + :return: Set of items """ pywikibot.debug(f'Search label: {item_name.encode("utf-8")}') item_list = set() # Empty set @@ -796,15 +822,14 @@ def get_item_list(item_name: str, for lang in item.aliases: for seq in item.aliases[lang]: if item_name_canon == unidecode(seq).casefold(): - item_list.add(item.getID()) # Alias match + item_list.add(item) # Alias match break pywikibot.log(item_list) - # Convert set to list - return list(item_list) + return item_list -def get_item_with_prop_value(prop: str, propval: str) -> list[str]: +def get_item_with_prop_value(prop: str, propval: str) -> set[str]: """Get list of items that have a property/value statement. .. seealso:: :api:`Search` @@ -842,12 +867,11 @@ def get_item_with_prop_value(prop: str, propval: str) -> list[str]: for seq in item.claims[prop]: if unidecode(seq.getTarget()).casefold() == item_name_canon: - item_list.add(item.getID()) # Found match + item_list.add(item) # Found match break - # Convert set to list pywikibot.log(item_list) - return sorted(item_list) + return item_list def amend_isbn_edition(isbn_number: str) -> int: @@ -869,8 +893,11 @@ def amend_isbn_edition(isbn_number: str) -> int: if not isbn_number: return 3 # Do nothing when the ISBN number is missing + pywikibot.info() + # Some digital library services raise failure try: + # Get ISBN basic data isbn_data = isbnlib.meta(isbn_number, service=booklib) # { # 'ISBN-13': '9789042925564', @@ -910,8 +937,9 @@ def add_claims(isbn_data: dict[str, Any]) -> int: # noqa: C901 global proptyx # targetx is not global (to allow for language specific editions) - # Get the book language from the ISBN book reference - booklang = mainlang # Default language + # Set default language from book library + # Mainlang was set to default digital library language code + booklang = mainlang if isbn_data['Language']: # Get the book language from the ISBN book number # Can overwrite the default language @@ -925,8 +953,7 @@ def add_claims(isbn_data: dict[str, Any]) -> int: # noqa: C901 lang_list = get_item_list(booklang, propreqinst[EDITIONLANGPROP]) # Hardcoded parameter - if 'Q3504110' in lang_list: # Somebody insisted on this disturbing value - lang_list.remove('Q3504110') # Remove duplicate "En" language + lang_list -= {'Q3504110'} # Remove duplicate "En" language if not lang_list: # Can' t store unknown language (need to update mapping table...) @@ -935,20 +962,19 @@ def add_claims(isbn_data: dict[str, Any]) -> int: # noqa: C901 if len(lang_list) != 1: # Ambiguous language - pywikibot.warning(f'Ambiguous language {booklang}') + pywikibot.warning(f'Ambiguous language {booklang}\n' + f'[lang_item.getID() for lang_item in lang_list]') return 3 # Set edition language item number - target[EDITIONLANGPROP] = lang_list[0] + lang_item = lang_list.pop() + target[EDITIONLANGPROP] = lang_item.getID() # Require short Wikipedia language code if len(booklang) > 3: - # Get best ranked language item - lang = get_item_page(lang_list[0]) - # Get official language code - if WIKILANGPROP in lang.claims: - booklang = lang.claims[WIKILANGPROP][0].getTarget() + if WIKILANGPROP in lang_item.claims: + booklang = lang_item.claims[WIKILANGPROP][0].getTarget() # Get edition title edition_title = isbn_data['Title'].strip() @@ -982,7 +1008,7 @@ def add_claims(isbn_data: dict[str, Any]) -> int: # noqa: C901 # Search the ISBN number both in canonical and numeric format qnumber_list = get_item_with_prop_value(ISBNPROP, isbn_fmtd) - qnumber_list += get_item_with_prop_value(ISBNPROP, isbn_number) + qnumber_list.update(get_item_with_prop_value(ISBNPROP, isbn_number)) # Get addional data from the digital library # This could fail with @@ -1023,27 +1049,24 @@ def add_claims(isbn_data: dict[str, Any]) -> int: # noqa: C901 if isbn10_number: isbn10_fmtd = isbnlib.mask(isbn10_number) pywikibot.info(f'ISBN 10: {isbn10_fmtd}') - qnumber_list += get_item_with_prop_value(ISBN10PROP, - isbn10_fmtd) - qnumber_list += get_item_with_prop_value(ISBN10PROP, - isbn10_number) + qnumber_list.update( + get_item_with_prop_value(ISBN10PROP, isbn10_fmtd)) + qnumber_list.update( + get_item_with_prop_value(ISBN10PROP, isbn10_number)) except Exception as error: pywikibot.error(f'ISBN 10 error, {error}') - qnumber_list = sorted(set(qnumber_list)) # Get unique values - # Create or amend the item if not qnumber_list: # Create the edition label = {MULANG: objectname} item = pywikibot.ItemPage(repo) # Create item - item.editEntity({'labels': label}, summary=transcmt) + item.editLabels(label, summary=transcmt, bot=wdbotflag) qnumber = item.getID() # Get new item number status = 'Created' elif len(qnumber_list) == 1: - qnumber = qnumber_list[0] - item = get_item_page(qnumber) + item = qnumber_list.pop() qnumber = item.getID() # Update item only if edition, or instance is missing @@ -1057,16 +1080,18 @@ def add_claims(isbn_data: dict[str, Any]) -> int: # noqa: C901 # Add missing book label for book language if MULANG not in item.labels: item.labels[MULANG] = objectname - item.editEntity({'labels': item.labels}, summary=transcmt) + item.editLabels(item.labels, summary=transcmt, bot=wdbotflag) status = 'Found' else: - pywikibot.error(f'Ambiguous ISBN number {isbn_fmtd}, ' - f'{qnumber_list} not updated') + pywikibot.error( + f'Ambiguous ISBN number {isbn_fmtd}, ' + f'{[item.getID() for item in qnumber_list]} not updated' + ) return 2 - pywikibot.warning(f'{status} item: P212:{isbn_fmtd} ({qnumber}) ' + pywikibot.warning(f'{status} item {qnumber}: P212: {isbn_fmtd} ' f'language {booklang} ({target[EDITIONLANGPROP]}) ' - f'{get_item_header_lang(item.labels, booklang)}') + f'{objectname}') # Register missing statements pywikibot.debug(target) @@ -1178,7 +1203,7 @@ def add_claims(isbn_data: dict[str, Any]) -> int: # noqa: C901 if len(author_list) == 1: add_author = True - author_item = get_item_page(author_list[0]) + author_item = author_list.pop() if (PROFESSIONPROP not in author_item.claims or not item_is_in_list(author_item.claims[PROFESSIONPROP], @@ -1186,14 +1211,10 @@ def add_claims(isbn_data: dict[str, Any]) -> int: # noqa: C901 # Add profession:author statement claim = pywikibot.Claim(repo, PROFESSIONPROP) claim.setTarget(target_author) - author_item.addClaim( - claim, bot=wdbotflag, - summary=f'{transcmt} {PROFESSIONPROP}:{AUTHORINSTANCE}') - pywikibot.warning( - 'Add profession:author ' - f'({PROFESSIONPROP}:{AUTHORINSTANCE}) to ' - f'{author_name} ({author_list[0]})' - ) + author_item.addClaim(claim, bot=wdbotflag, summary=transcmt) + pywikibot.warning('Add profession: author ' + f'({PROFESSIONPROP}:{AUTHORINSTANCE}) to ' + f'{author_name} ({author_item.getID()})') # Possibly found as author? # Possibly found as editor? @@ -1204,7 +1225,7 @@ def add_claims(isbn_data: dict[str, Any]) -> int: # noqa: C901 for claim in item.claims[prop]: book_author = claim.getTarget() - if book_author.getID() == author_list[0]: + if book_author == author_item: # Add missing sequence number if SEQNRPROP not in claim.qualifiers: qualifier = pywikibot.Claim(repo, SEQNRPROP) @@ -1227,35 +1248,43 @@ def add_claims(isbn_data: dict[str, Any]) -> int: # noqa: C901 claim.setTarget(author_item) item.addClaim(claim, bot=wdbotflag, summary=transcmt) pywikibot.warning(f'Add author {author_cnt}:{author_name} ' - f'({AUTHORPROP}:{author_list[0]})') + f'({AUTHORPROP}:{author_item.getID()})') # Add sequence number qualifier = pywikibot.Claim(repo, SEQNRPROP) qualifier.setTarget(str(author_cnt)) claim.addQualifier(qualifier, bot=wdbotflag, summary=transcmt) elif author_list: - pywikibot.error(f'Ambiguous author: {author_name}') + pywikibot.error( + f'Ambiguous author: {author_name}' + f'({[author_item.getID() for author_item in author_list]})' + ) else: pywikibot.error(f'Unknown author: {author_name}') # Set the publisher publisher_name = isbn_data['Publisher'].strip() if publisher_name: - publisher_list = get_item_list(get_canon_name(publisher_name), + publisher_list = get_item_list(publisher_name, propreqinst[PUBLISHERPROP]) if len(publisher_list) == 1: + publisher_item = publisher_list.pop() if (PUBLISHERPROP not in item.claims or not item_is_in_list(item.claims[PUBLISHERPROP], - publisher_list)): + [publisher_item.getID()])): claim = pywikibot.Claim(repo, PUBLISHERPROP) - claim.setTarget(get_item_page(publisher_list[0])) + claim.setTarget(publisher_item) item.addClaim(claim, bot=wdbotflag, summary=transcmt) - pywikibot.warning(f'Add publisher: {publisher_name} ' - f'({PUBLISHERPROP}:{publisher_list[0]})') + pywikibot.warning( + f'Add publisher: {publisher_name} ' + f'({PUBLISHERPROP}:{publisher_item.getID()})' + ) elif publisher_list: - pywikibot.error(f'Ambiguous publisher: {publisher_name} ' - f'({publisher_list})') + pywikibot.error( + f'Ambiguous publisher: {publisher_name} ' + f'({[p_item.getID() for p_item in publisher_list]})' + ) else: pywikibot.error(f'Unknown publisher: {publisher_name}') @@ -1432,19 +1461,18 @@ def add_claims(isbn_data: dict[str, Any]) -> int: # noqa: C901 if len(qmain_subject) == 1: # Get main subject and label - main_subject = get_item_page(qmain_subject[0]) - main_subject_label = get_item_header(main_subject.labels) + main_subject_label = get_item_header(qmain_subject[0].labels) if (MAINSUBPROP in item.claims - and item_is_in_list(item.claims[MAINSUBPROP], - qmain_subject)): + and item_is_in_list(item.claims[MAINSUBPROP], + [qmain_subject[0].getID()])): pywikibot.log( f'Skipping main subject ({MAINSUBPROP}): ' f'{main_subject_label} ({qmain_subject[0]})' ) else: claim = pywikibot.Claim(repo, MAINSUBPROP) - claim.setTarget(main_subject) + claim.setTarget(qmain_subject[0]) # Add main subject item.addClaim(claim, bot=wdbotflag, summary=transcmt) pywikibot.warning( @@ -1479,7 +1507,8 @@ def show_final_information(isbn_number: str) -> None: if isbn_info: pywikibot.info(isbn_info) - # DOI number + # DOI number -- No warranty that the document number really exists on + # https:/doi.org isbn_doi = isbnlib.doi(isbn_number) if isbn_doi: pywikibot.info(isbn_doi) @@ -1588,6 +1617,7 @@ def main(*args: str) -> None: for seq in bib_source} if booklib in bib_sourcex: + # Register source references = pywikibot.Claim(repo, REFPROP) references.setTarget(bib_sourcex[booklib]) @@ -1596,13 +1626,15 @@ def main(*args: str) -> None: retrieved.setTarget(date_ref) booklib_ref = [references, retrieved] - # Register source and retrieval date + # Get default language from book library mainlang = bib_source[booklib][2] else: # Unknown bib reference - show implemented codes for seq in bib_source: - pywikibot.info(f'{seq.ljust(10)}{bib_source[seq][2].ljust(4)}' - f'{bib_source[seq][1]}') + pywikibot.info( + f'{seq.ljust(10)}{bib_source[seq][2].ljust(4)}' + f'{bib_source[seq][3].ljust(20)}{bib_source[seq][1]}' + ) fatal_error(3, f'Unknown Digital library ({REFPROP}) {booklib}') # Get optional parameters (all are optional) From 593dba194392f8155310a1f39339c2a5b32269d8 Mon Sep 17 00:00:00 2001 From: Xqt <info@gno.de> Date: Mon, 18 Nov 2024 21:13:57 +0000 Subject: [PATCH 49/95] call handle_args first to provide help message Change-Id: I2d1766019d670f9b070336d9a84857c0d727c9b6 Signed-off-by: Xqt <info@gno.de> --- scripts/create_isbn_edition.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/create_isbn_edition.py b/scripts/create_isbn_edition.py index 1a33d05f5e..14d0d3ca39 100755 --- a/scripts/create_isbn_edition.py +++ b/scripts/create_isbn_edition.py @@ -1572,14 +1572,14 @@ def main(*args: str) -> None: global targetx global wdbotflag + # Get optional parameters + local_args = pywikibot.handle_args(*args) + # check dependencies for module in (isbnlib, unidecode): if isinstance(module, ImportError): raise module - # Get optional parameters - local_args = pywikibot.handle_args(*args) - # Connect to databases # Login to Wikibase instance # Required for wikidata object access (item, property, statement) From 818c56e9b3c3a50d000c1836d362f4243d3798fc Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Tue, 19 Nov 2024 14:43:52 +0100 Subject: [PATCH 50/95] [bugfix] Upcast to FilePage in PageGenerator.result() Upcast to FilePage in api.PageGenerator.result if pagedata has imageinfo contents even if the file extension is invalid. Bug: T379513 Change-Id: Ieb5f3c802569a13c0ed96ee5290aa4de799e1b8b --- pywikibot/data/api/_generators.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pywikibot/data/api/_generators.py b/pywikibot/data/api/_generators.py index 1a316e94b2..fdf941adfe 100644 --- a/pywikibot/data/api/_generators.py +++ b/pywikibot/data/api/_generators.py @@ -727,9 +727,12 @@ def result(self, pagedata: dict[str, Any]) -> pywikibot.Page: of object. .. versionchanged:: 9.5 - no longer raise :exc:`exceptions.UnsupportedPageError` but + No longer raise :exc:`exceptions.UnsupportedPageError` but return a generic :class:`pywikibot.Page` obect. The exception is raised when getting the content for example. + .. versionchanged:: 9.6 + Upcast to :class:`page.FilePage` if *pagedata* has + ``imageinfo`` contents even if the file extension is invalid. """ p = pywikibot.Page(self.site, pagedata['title'], pagedata['ns']) ns = pagedata['ns'] @@ -738,7 +741,8 @@ def result(self, pagedata: dict[str, Any]) -> pywikibot.Page: p = pywikibot.User(p) elif ns == Namespace.FILE: with suppress(ValueError): - p = pywikibot.FilePage(p) + p = pywikibot.FilePage( + p, ignore_extension='imageinfo' in pagedata) elif ns == Namespace.CATEGORY: p = pywikibot.Category(p) From 17ed3ca38b3a955ee2c46a1752e61ab5f740e1ea Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Tue, 19 Nov 2024 14:50:05 +0100 Subject: [PATCH 51/95] [IMPR] Show a warning if ignore_extension was set and the extension is invalid. Change-Id: I23a58ed419faee25109b243ecc30f2b570dbe8c4 --- pywikibot/page/_filepage.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/pywikibot/page/_filepage.py b/pywikibot/page/_filepage.py index 703d178707..3e3fd88c21 100644 --- a/pywikibot/page/_filepage.py +++ b/pywikibot/page/_filepage.py @@ -43,9 +43,15 @@ def __init__(self, source, title: str = '', *, """Initializer. .. versionchanged:: 8.4 - check for valid extensions. + Check for valid extensions. .. versionchanged:: 9.3 - *ignore_extension* parameter was added + Added the optional *ignore_extension* parameter. + .. versionchanged:: 9.6 + Show a warning if *ignore_extension* was set and the + extension is invalid. + .. seealso:: + :meth:`Site.file_extensions + <pywikibot.site._apisite.APISite.file_extensions>` :param source: the source of the page :type source: pywikibot.page.BaseLink (or subclass), @@ -62,16 +68,15 @@ def __init__(self, source, title: str = '', *, if self.namespace() != 6: raise ValueError(f"'{self.title()}' is not in the file namespace!") - if ignore_extension: - return - title = self.title(with_ns=False, with_section=False) _, sep, extension = title.rpartition('.') if not sep or extension.lower() not in self.site.file_extensions: - raise ValueError( - f'{title!r} does not have a valid extension ' - f'({", ".join(self.site.file_extensions)}).' - ) + msg = (f'{title!r} does not have a valid extension\n' + f'({", ".join(self.site.file_extensions)}).') + if not ignore_extension: + raise ValueError(msg) + + pywikibot.warning(msg) def _load_file_revisions(self, imageinfo) -> None: """Save a file revision of FilePage (a FileInfo object) in local cache. From 52255d7ce5c8a20bcf63a082cfade44aa40ab6d5 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Tue, 19 Nov 2024 16:50:25 +0100 Subject: [PATCH 52/95] tests: ignore PyJWT version 2.10.0 with mwoauth Bug: T380270 Change-Id: Iaeaf3e5fcf61993efdd270d9b1ba746c64296da8 --- .github/workflows/oauth_tests-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/oauth_tests-ci.yml b/.github/workflows/oauth_tests-ci.yml index 0a53aa95cd..bf85fab70d 100644 --- a/.github/workflows/oauth_tests-ci.yml +++ b/.github/workflows/oauth_tests-ci.yml @@ -86,6 +86,8 @@ jobs: pip install coverage pip install "importlib_metadata ; python_version < '3.8'" pip install mwparserfromhell + # PyJWT added due to T380270 + pip install "PyJWT != 2.10.0" pip install mwoauth pip install packaging pip install requests From d00c515661a6231467ecead6717dce06b7c3313c Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Tue, 19 Nov 2024 17:03:20 +0100 Subject: [PATCH 53/95] Update requirements Bug: T380270 Change-Id: I3cb27622241efc7151c16c2a9069eb48da37c2e9 --- .github/workflows/oauth_tests-ci.yml | 2 +- requirements.txt | 2 ++ setup.py | 5 ++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/oauth_tests-ci.yml b/.github/workflows/oauth_tests-ci.yml index bf85fab70d..e0b9105c15 100644 --- a/.github/workflows/oauth_tests-ci.yml +++ b/.github/workflows/oauth_tests-ci.yml @@ -87,7 +87,7 @@ jobs: pip install "importlib_metadata ; python_version < '3.8'" pip install mwparserfromhell # PyJWT added due to T380270 - pip install "PyJWT != 2.10.0" + pip install "PyJWT != 2.10.0 ; python_version > '3.8'" pip install mwoauth pip install packaging pip install requests diff --git a/requirements.txt b/requirements.txt index 6c3d89de7f..df401ab5a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,6 +32,8 @@ wikitextparser>=0.47.5 # OAuth support # mwoauth 0.2.4 is needed because it supports getting identity information # about the user +# Due to T380270 PyJWT must be set +PyJWT != 2.10.0; python_version > '3.8' mwoauth>=0.2.4,!=0.3.1 # interwiki_graph.py module and category_graph.py script: diff --git a/setup.py b/setup.py index 3932956328..3a56fbc6b1 100755 --- a/setup.py +++ b/setup.py @@ -49,7 +49,10 @@ 'Pillow>=8.1.2, != 10.0, != 10.1; python_version < "3.13"', 'Pillow>=10.4; python_version >= "3.13"', ], - 'mwoauth': ['mwoauth!=0.3.1,>=0.2.4'], + 'mwoauth': [ + 'PyJWT != 2.10.0; python_version > "3.8"', # T380270 + 'mwoauth!=0.3.1,>=0.2.4', + ], 'html': ['beautifulsoup4>=4.7.1'], 'http': ['fake-useragent>=1.4.0'], } From 01db35cd9b9179b4bbf96cbcd4ad99a5a8e47270 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Tue, 19 Nov 2024 19:07:12 +0100 Subject: [PATCH 54/95] [IMPR] decrease nested flow statements in create_isbn_edition.py Change-Id: I6cae4dd409b513a8b6e22942ac5259ed55ed1bee --- scripts/create_isbn_edition.py | 47 +++++++++++++++++----------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/scripts/create_isbn_edition.py b/scripts/create_isbn_edition.py index 14d0d3ca39..d803761330 100755 --- a/scripts/create_isbn_edition.py +++ b/scripts/create_isbn_edition.py @@ -782,8 +782,7 @@ def get_item_list(item_name: str, :return: Set of items """ pywikibot.debug(f'Search label: {item_name.encode("utf-8")}') - item_list = set() # Empty set - # TODO: try to us search_entities instead? + # TODO: try to use search_entities instead? params = { 'action': 'wbsearchentities', 'format': 'json', @@ -799,31 +798,33 @@ def get_item_list(item_name: str, result = request.submit() pywikibot.debug(result) - if 'search' in result: - # Ignore accents and case - item_name_canon = unidecode(item_name).casefold() + if 'search' not in result: + return set() - # Loop though items - for res in result['search']: - item = get_item_page(res['id']) + # Ignore accents and case + item_name_canon = unidecode(item_name).casefold() - # Matching instance - if INSTANCEPROP not in item.claims \ - or not item_is_in_list(item.claims[INSTANCEPROP], instance_id): - continue + item_list = set() + # Loop though items + for res in result['search']: + item = get_item_page(res['id']) - # Search all languages, ignore label case and accents - for lang in item.labels: - if (item_name_canon - == unidecode(item.labels[lang].casefold())): - item_list.add(item.getID()) # Label math - break + # Matching instance + if INSTANCEPROP not in item.claims \ + or not item_is_in_list(item.claims[INSTANCEPROP], instance_id): + continue - for lang in item.aliases: - for seq in item.aliases[lang]: - if item_name_canon == unidecode(seq).casefold(): - item_list.add(item) # Alias match - break + # Search all languages, ignore label case and accents + for lang in item.labels: + if item_name_canon == unidecode(item.labels[lang].casefold()): + item_list.add(item.getID()) # Label math + break + + for lang in item.aliases: + for seq in item.aliases[lang]: + if item_name_canon == unidecode(seq).casefold(): + item_list.add(item) # Alias match + break pywikibot.log(item_list) return item_list From c8b185f7fed7640fd46021d557928bf8d265c4a7 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Wed, 20 Nov 2024 16:58:20 +0100 Subject: [PATCH 55/95] [IMPR] use search_entities method in get_item_list function Change-Id: I7d033572a0595b73bd8f0f5892a939a2c13e46e9 --- scripts/create_isbn_edition.py | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/scripts/create_isbn_edition.py b/scripts/create_isbn_edition.py index d803761330..c566fbc783 100755 --- a/scripts/create_isbn_edition.py +++ b/scripts/create_isbn_edition.py @@ -781,32 +781,12 @@ def get_item_list(item_name: str, :param instance_id: Instance ID :return: Set of items """ - pywikibot.debug(f'Search label: {item_name.encode("utf-8")}') - # TODO: try to use search_entities instead? - params = { - 'action': 'wbsearchentities', - 'format': 'json', - 'type': 'item', - # All languages are searched, but labels are in native language - 'strictlanguage': False, - 'language': mainlang, - 'uselang': mainlang, # (primary) Search language - 'search': item_name, # Get item list from label - 'limit': 20, # Should be reasonable value - } - request = api.Request(site=repo, parameters=params) - result = request.submit() - pywikibot.debug(result) - - if 'search' not in result: - return set() - # Ignore accents and case item_name_canon = unidecode(item_name).casefold() item_list = set() - # Loop though items - for res in result['search']: + # Loop though items, total should be reasonable value + for res in repo.search_entities(item_name, mainlang, total=20): item = get_item_page(res['id']) # Matching instance From 1f7b49be3cfcb787a997888301ffdc2bd74a9d7d Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Thu, 21 Nov 2024 07:04:11 +0100 Subject: [PATCH 56/95] [IMPR] return a boolean with item_is_in_list and item_has_label Simplify the code: both functions are used with their boolean result, it is not necessary to return the item found. Change-Id: Ie7e38f8fa041e91b8cca7c7dec813b1236beb2ae --- scripts/create_isbn_edition.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/scripts/create_isbn_edition.py b/scripts/create_isbn_edition.py index d803761330..79d5d859a0 100755 --- a/scripts/create_isbn_edition.py +++ b/scripts/create_isbn_edition.py @@ -696,39 +696,39 @@ def get_language_preferences() -> list[str]: return main_languages -def item_is_in_list(statement_list: list, itemlist: list[str]) -> str: +def item_is_in_list(statement_list: list, itemlist: list[str]) -> bool: """Verify if statement list contains at least one item from the itemlist. param statement_list: Statement list param itemlist: List of values (string) - return: Matching or empty string + return: Whether the item matches """ for seq in statement_list: with suppress(AttributeError): # Ignore NoneType error isinlist = seq.getTarget().getID() if isinlist in itemlist: - return isinlist - return '' + return True + return False -def item_has_label(item, label: str) -> str: +def item_has_label(item, label: str) -> bool: """Verify if the item has a label. :param item: Item :param label: Item label - :return: Matching string + :return: Whether the item has a label """ label = unidecode(label).casefold() for lang in item.labels: if unidecode(item.labels[lang]).casefold() == label: - return item.labels[lang] + return True for lang in item.aliases: for seq in item.aliases[lang]: if unidecode(seq).casefold() == label: - return seq + return True - return '' # Must return "False" when no label + return False def is_in_value_list(statement_list: list, valuelist: list[str]) -> bool: From d231b97289e31eb0baa571483ad8a1bd085d4c35 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Fri, 22 Nov 2024 15:58:23 +0100 Subject: [PATCH 57/95] [IMPR] use page.autoFormat instead of date.getAutoFormat in titletranslate.py - use page.autoFormat() instead of date.getAutoFormat() in titletranslate.translate() - rename local variable x with link for better readability - use site.getSite() method to get a site with other code - raise RuntimeError instead of AssertionError if neither page nor site parameter is given - hint is usually given as list of literals Change-Id: I731c667fb74364da65cd9618b57036e951e7bc14 --- pywikibot/titletranslate.py | 56 ++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/pywikibot/titletranslate.py b/pywikibot/titletranslate.py index e349c74390..019c73b506 100644 --- a/pywikibot/titletranslate.py +++ b/pywikibot/titletranslate.py @@ -12,25 +12,37 @@ def translate( page=None, - hints=(), + hints: list[str] | None = None, auto: bool = True, removebrackets: bool = False, site=None ) -> list[pywikibot.Link]: """Return a list of links to pages on other sites based on hints. - Entries for single page titles list those pages. Page titles for entries - such as "all:" or "xyz:" or "20:" are first built from the page title of - 'page' and then listed. When 'removebrackets' is True, a trailing pair of - brackets and the text between them is removed from the page title. - If 'auto' is true, known year and date page titles are autotranslated - to all known target languages and inserted into the list. - """ - result = set() + Entries for single page titles list those pages. Page titles for + entries such as "all:" or "xyz:" or "20:" are first built from the + page title of 'page' and then listed. - assert page or site + .. versionchanged:: 9.6 + Raise ``RuntimeError`` instead of ``AssertionError`` if neither + *page* nor *site* parameter is given. + + :param auto: If true, known year and date page titles are + autotranslated to all known target languages and inserted into + the list. + :param removebrackets: If True, a trailing pair of brackets and the + text between them is removed from the page title. + :raises RuntimeError: Either page or site parameter must be given. + """ + if not page and not site: + raise RuntimeError( + 'Either page or site parameter must be given with translate()') site = site or page.site + result = set() + + if hints is None: + hints = [] for h in hints: # argument may be given as -hint:xy where xy is a language code @@ -54,10 +66,10 @@ def translate( if newcode in site.languages(): if newcode != site.code: ns = page.namespace() if page else 0 - x = pywikibot.Link(newname, - site.getSite(code=newcode), - default_namespace=ns) - result.add(x) + link = pywikibot.Link(newname, + site.getSite(code=newcode), + default_namespace=ns) + result.add(link) elif config.verbose_output: pywikibot.info(f'Ignoring unknown language code {newcode}') @@ -65,19 +77,17 @@ def translate( # existing interwiki links. if auto and page: # search inside all dictionaries for this link - sitelang = page.site.lang - dict_name, value = date.getAutoFormat(sitelang, page.title()) + dict_name, value = page.autoFormat() if dict_name: pywikibot.info(f'TitleTranslate: {page.title()} was recognized as ' f'{dict_name} with value {value}') for entry_lang, entry in date.formats[dict_name].items(): if entry_lang not in site.languages(): continue - if entry_lang != sitelang: - newname = entry(value) - x = pywikibot.Link( - newname, - pywikibot.Site(code=entry_lang, - fam=site.family)) - result.add(x) + + if entry_lang != page.site.lang: + link = pywikibot.Link(entry(value), + site.getSite(entry_lang)) + result.add(link) + return list(result) From 4f46379860731234fcc372ead326109e70343961 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Fri, 22 Nov 2024 17:16:16 +0100 Subject: [PATCH 58/95] [IMPR] Add a new Site property codes - add Site.codes property to give a list of site codes - deprecate Site.languages() which returns a list of site codes - update languages() usages Bug: T380606 Change-Id: I2c4ceb0b9fe98f58972fc79b4d2076be20f24d36 --- pywikibot/site/_basesite.py | 24 +++++++++++++++++++----- pywikibot/titletranslate.py | 4 ++-- tests/dry_api_tests.py | 5 +++-- tests/site_tests.py | 12 ++++++------ 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/pywikibot/site/_basesite.py b/pywikibot/site/_basesite.py index fb3029687e..86ee9f83e2 100644 --- a/pywikibot/site/_basesite.py +++ b/pywikibot/site/_basesite.py @@ -74,7 +74,7 @@ def __init__(self, code: str, fam=None, user=None) -> None: self.obsolete = True pywikibot.log(f'Site {self} instantiated and marked "obsolete"' ' to prevent access') - elif self.__code not in self.languages(): + elif self.__code not in self.codes: if self.__family.name in self.__family.langs \ and len(self.__family.langs) == 1: self.__code = self.__family.name @@ -231,13 +231,27 @@ def __hash__(self): """Return hash value of instance.""" return hash(repr(self)) - def languages(self): - """Return list of all valid language codes for this site's Family.""" - return list(self.family.langs.keys()) + @deprecated('codes', since='9.6') + def languages(self) -> list[str]: + """Return list of all valid site codes for this site's Family. + + .. deprecated:: 9.6 + Use :meth:`codes` instead. + """ + return sorted(self.codes) + + @property + def codes(self) -> set[str]: + """Return set of all valid site codes for this site's Family. + + .. versionadded:: 9.6 + .. seealso:: :attr:`family.Family.codes` + """ + return set(self.family.langs.keys()) def validLanguageLinks(self): # noqa: N802 """Return list of language codes to be used in interwiki links.""" - return [lang for lang in self.languages() + return [lang for lang in sorted(self.codes) if self.namespaces.lookup_normalized_name(lang) is None] def _interwiki_urls(self, only_article_suffixes: bool = False): diff --git a/pywikibot/titletranslate.py b/pywikibot/titletranslate.py index e349c74390..7214304928 100644 --- a/pywikibot/titletranslate.py +++ b/pywikibot/titletranslate.py @@ -51,7 +51,7 @@ def translate( codes = site.family.language_groups.get(codes, codes.split(',')) for newcode in codes: - if newcode in site.languages(): + if newcode in site.codes: if newcode != site.code: ns = page.namespace() if page else 0 x = pywikibot.Link(newname, @@ -71,7 +71,7 @@ def translate( pywikibot.info(f'TitleTranslate: {page.title()} was recognized as ' f'{dict_name} with value {value}') for entry_lang, entry in date.formats[dict_name].items(): - if entry_lang not in site.languages(): + if entry_lang not in site.codes: continue if entry_lang != sitelang: newname = entry(value) diff --git a/tests/dry_api_tests.py b/tests/dry_api_tests.py index a49c66c160..76746251b4 100755 --- a/tests/dry_api_tests.py +++ b/tests/dry_api_tests.py @@ -169,8 +169,9 @@ def version(self): def protocol(self): return 'http' - def languages(self): - return ['mock'] + @property + def codes(self): + return {'mock'} def user(self): return self._user diff --git a/tests/site_tests.py b/tests/site_tests.py index 7a0505bd42..9d133ffdd9 100755 --- a/tests/site_tests.py +++ b/tests/site_tests.py @@ -104,12 +104,12 @@ def test_constructors(self): pywikibot.site.APISite.fromDBName(dbname, site), pywikibot.Site(sitename)) - def test_language_methods(self): - """Test cases for languages() and related methods.""" + def test_codes_property(self): + """Test cases for codes property and related methods.""" mysite = self.get_site() - langs = mysite.languages() - self.assertIsInstance(langs, list) - self.assertIn(mysite.code, langs) + codes = mysite.codes + self.assertIsInstance(codes, set) + self.assertIn(mysite.code, codes) self.assertIsInstance(mysite.obsolete, bool) ipf = mysite.interwiki_putfirst() if ipf: # no languages use this anymore, keep it for foreign families @@ -118,7 +118,7 @@ def test_language_methods(self): self.assertIsNone(ipf) for item in mysite.validLanguageLinks(): - self.assertIn(item, langs) + self.assertIn(item, codes) self.assertIsNone(self.site.namespaces.lookup_name(item)) def test_namespace_methods(self): From b591bead09dffc81ca911c0215ef87828bdf223c Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sat, 23 Nov 2024 18:22:44 +0100 Subject: [PATCH 59/95] [cleanup] remove Sphinx autodoc workaround for Family classproperty-s This workaround does not solves the missing doc string with class properties (any longer) Change-Id: Ifd181805257c8e484eb3e4bce21077afa3f2aa5a --- docs/conf.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 522a1b273c..da75c999ed 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -579,35 +579,10 @@ def pywikibot_script_docstring_fixups(app, what, name, obj, options, lines): length = 0 -def pywikibot_family_classproperty_getattr(obj, name, *defargs): - """Custom getattr() to get classproperty instances.""" - from sphinx.util.inspect import safe_getattr - - from pywikibot.family import Family - from pywikibot.tools import classproperty - - if not isinstance(obj, type) or not issubclass(obj, Family): - return safe_getattr(obj, name, *defargs) - - for base_class in obj.__mro__: - try: - prop = base_class.__dict__[name] - except KeyError: - continue - - if not isinstance(prop, classproperty): - return safe_getattr(obj, name, *defargs) - - return prop - - return safe_getattr(obj, name, *defargs) - - def setup(app): """Implicit Sphinx extension hook.""" app.connect('autodoc-process-docstring', pywikibot_docstring_fixups) app.connect('autodoc-process-docstring', pywikibot_script_docstring_fixups) - app.add_autodoc_attrgetter(type, pywikibot_family_classproperty_getattr) autoclass_content = 'both' From f0bb2adaa720c68e46f958a988ba07e85d5b4c9b Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sat, 23 Nov 2024 18:39:28 +0100 Subject: [PATCH 60/95] cleanup: move rstcheck settings from .rstcheck.cfg to pyproject.toml Change-Id: Iac897b24d99216b7ee270a5b96d90217b2ceeb3b --- .rstcheck.cfg | 4 ---- pyproject.toml | 7 +++++++ 2 files changed, 7 insertions(+), 4 deletions(-) delete mode 100644 .rstcheck.cfg diff --git a/.rstcheck.cfg b/.rstcheck.cfg deleted file mode 100644 index 918cfcf59a..0000000000 --- a/.rstcheck.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[rstcheck] -ignore_directives=automodule,autoclass,autofunction,tabs -ignore_messages=(Undefined substitution referenced: "(release|today|version)") -ignore_roles=api,phab,pylib,source,wiki diff --git a/pyproject.toml b/pyproject.toml index bde14aa29b..b6f595bca6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,6 +162,13 @@ enable_error_code = [ ] ignore_missing_imports = true + +[tool.rstcheck] +ignore_directives = ["automodule", "autoclass", "autofunction", "tabs"] +ignore_messages = '(Undefined substitution referenced: "(release|today|version)")' +ignore_roles = ["api", "phab", "pylib", "source", "wiki"] + + [tool.ruff.lint] select = ["D"] ignore = ["D105", "D211", "D213", "D214", "D401", "D404", "D406", "D407", "D412", "D413", "D416", "D417"] From 79c6a17909be32164e2c369532614540ab4ab85b Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sat, 23 Nov 2024 18:58:20 +0100 Subject: [PATCH 61/95] cleanup: move .coveragerc settings to pyproject.toml Change-Id: Ibb30a2998eac18de0df2f512b15e09807a47f3d4 --- .coveragerc | 23 ----------------------- pyproject.toml | 27 +++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 23 deletions(-) delete mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 2efa48eb6f..0000000000 --- a/.coveragerc +++ /dev/null @@ -1,23 +0,0 @@ -[report] -ignore_errors = True -skip_empty = True - -exclude_lines = - # Have to re-enable the standard pragma - pragma: no cover - except ImportError - except KeyboardInterrupt - except OSError - except \w*ServerError - except SyntaxError - raise ImportError - raise NotImplementedError - raise unittest\.SkipTest - self\.skipTest - if __name__ == '__main__': - if .+PYWIKIBOT_TEST_\w+.+: - if self\.mw_version < .+: - if TYPE_CHECKING: - @(abc\.)?abstractmethod - @deprecated\([^\)]+\) - @unittest\.skip diff --git a/pyproject.toml b/pyproject.toml index b6f595bca6..df9b092db9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -126,6 +126,33 @@ Download = "https://www.pywikibot.org" Changelog = "https://doc.wikimedia.org/pywikibot/master/changelog.html" Tracker = "https://phabricator.wikimedia.org/tag/pywikibot/" + +[tool.coverage.report] +ignore_errors = true +skip_empty = true + +exclude_lines = [ + # Have to re-enable the standard pragma + "pragma: no cover", + "except ImportError", + "except KeyboardInterrupt", + "except OSError", + "except \\w*ServerError", + "except SyntaxError", + "raise ImportError", + "raise NotImplementedError", + "raise unittest\\.SkipTest", + "self\\.skipTest", + "if __name__ == '__main__':", + "if .+PYWIKIBOT_TEST_\\w+.+:", + "if self\\.mw_version < .+:", + "if TYPE_CHECKING:", + "@(abc\\.)?abstractmethod", + "@deprecated\\([^\\)]+\\)", + "@unittest\\.skip", +] + + [tool.docsig] disable = [ "SIG101", From 840f01cf81c34b2c2178640306013ca32fb31845 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sun, 24 Nov 2024 10:30:59 +0100 Subject: [PATCH 62/95] R1723: remove unnecessary elif used after break Change-Id: I4e26fa6b60dc8475e9c6af63d0bef6e8a8094434 --- scripts/create_isbn_edition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/create_isbn_edition.py b/scripts/create_isbn_edition.py index d803761330..e33c5f2625 100755 --- a/scripts/create_isbn_edition.py +++ b/scripts/create_isbn_edition.py @@ -1236,7 +1236,7 @@ def add_claims(isbn_data: dict[str, Any]) -> int: # noqa: C901 add_author = False break - elif item_has_label(book_author, author_name): + if item_has_label(book_author, author_name): pywikibot.warning( f'Edition has conflicting author ({prop}) ' f'{author_name} ({book_author.getID()})' From 14d7ef20d507ad51dbc6afa16fb10b5c9b50c076 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sun, 24 Nov 2024 11:42:39 +0100 Subject: [PATCH 63/95] tests: update pre-commit hooks Change-Id: I1530c399e2b0e9d9a5c3d8cd851a75679178df96 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 861c9f5240..c020770bfd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,7 +50,7 @@ repos: - id: python-check-mock-methods - id: python-use-type-annotations - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.3 + rev: v0.8.0 hooks: - id: ruff args: From a1fdc40a794e6a10103a5c2f66b2a98e8f3c9109 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sun, 24 Nov 2024 13:29:17 +0100 Subject: [PATCH 64/95] Code improvements - avoid redefinition of local variables within for loops - use HTTPStatus enum which is more readable - use dict.items() if keys and values are used - simplify comparison for multiple values Change-Id: I43834038aabaa6ae921b0ae7d742e8c9478ef062 --- pywikibot/bot.py | 11 ++++++----- pywikibot/cosmetic_changes.py | 5 ++--- pywikibot/data/superset.py | 11 ++++++----- pywikibot/page/_page.py | 4 ++-- pywikibot/page/_toolforge.py | 3 ++- pywikibot/page/_wikibase.py | 4 ++-- pywikibot/site/_datasite.py | 8 ++++---- pywikibot/site/_generators.py | 4 ++-- pywikibot/tools/__init__.py | 4 ++-- tests/site_tests.py | 2 +- 10 files changed, 29 insertions(+), 27 deletions(-) diff --git a/pywikibot/bot.py b/pywikibot/bot.py index 010005c320..6e9efa8476 100644 --- a/pywikibot/bot.py +++ b/pywikibot/bot.py @@ -2042,17 +2042,18 @@ def cacheSources(self) -> None: def get_property_by_name(self, property_name: str) -> str: """Find given property and return its ID. - Method first uses site.search() and if the property isn't found, then - asks user to provide the property ID. + Method first uses site.search() and if the property isn't found, + then asks user to provide the property ID. :param property_name: property to find """ ns = self.repo.property_namespace for page in self.repo.search(property_name, total=1, namespaces=ns): - page = pywikibot.PropertyPage(self.repo, page.title()) + prop = pywikibot.PropertyPage(self.repo, page.title()) pywikibot.info( - f'Assuming that {property_name} property is {page.id}.') - return page.id + f'Assuming that {property_name} property is {prop.id}.') + return prop.id + return pywikibot.input( f'Property {property_name} was not found. Please enter the ' f'property ID (e.g. P123) of it:').upper() diff --git a/pywikibot/cosmetic_changes.py b/pywikibot/cosmetic_changes.py index cf3df33299..76fdeefc83 100644 --- a/pywikibot/cosmetic_changes.py +++ b/pywikibot/cosmetic_changes.py @@ -462,9 +462,8 @@ def translateAndCapitalizeNamespaces(self, text: str) -> str: else 'User talk'] # lowerspaced and underscored namespaces for i, item in enumerate(namespaces): - item = item.replace(' ', '[ _]') - item = f'[{item[0]}{item[0].lower()}]' + item[1:] - namespaces[i] = item + ns = item.replace(' ', '[ _]') + namespaces[i] = f'[{ns[0]}{ns[0].lower()}]{ns[1:]}' namespaces.append(first_lower(final_ns)) if final_ns and namespaces: if (self.site.sitename == 'wikipedia:pt' diff --git a/pywikibot/data/superset.py b/pywikibot/data/superset.py index 101b96d89e..7fd0e14e3f 100644 --- a/pywikibot/data/superset.py +++ b/pywikibot/data/superset.py @@ -9,6 +9,7 @@ # from __future__ import annotations +from http import HTTPStatus from textwrap import fill from typing import TYPE_CHECKING, Any @@ -91,9 +92,9 @@ def login(self) -> bool: self.last_response = http.fetch(url) # Handle error cases - if self.last_response.status_code == 200: + if self.last_response.status_code == HTTPStatus.OK: self.connected = True - elif self.last_response.status_code == 401: + elif self.last_response.status_code == HTTPStatus.UNAUTHORIZED: self.connected = False raise NoUsernameError(fill( 'User not logged in. You need to log in to ' @@ -124,7 +125,7 @@ def get_csrf_token(self) -> str: url = f'{self.superset_url}/api/v1/security/csrf_token/' self.last_response = http.fetch(url) - if self.last_response.status_code == 200: + if self.last_response.status_code == HTTPStatus.OK: return self.last_response.json()['result'] status_code = self.last_response.status_code @@ -147,12 +148,12 @@ def get_database_id_by_schema_name(self, schema_name: str) -> int: url += f'/api/v1/database/{database_id}/schemas/?q=(force:!f)' self.last_response = http.fetch(url) - if self.last_response.status_code == 200: + if self.last_response.status_code == HTTPStatus.OK: schemas = self.last_response.json()['result'] if schema_name in schemas: return database_id - elif self.last_response.status_code == 404: + elif self.last_response.status_code == HTTPStatus.NOT_FOUND: break else: status_code = self.last_response.status_code diff --git a/pywikibot/page/_page.py b/pywikibot/page/_page.py index 48c74c8c6f..199d4d642c 100644 --- a/pywikibot/page/_page.py +++ b/pywikibot/page/_page.py @@ -112,9 +112,9 @@ def templatesWithParams( # noqa: N802 positional.append(intkeys[i]) continue - for k in intkeys: + for k, v in intkeys.items(): if k < 1 or k >= i: - named[str(k)] = intkeys[k] + named[str(k)] = v break for item in named.items(): diff --git a/pywikibot/page/_toolforge.py b/pywikibot/page/_toolforge.py index 1d7aa8aa81..19a2782c67 100644 --- a/pywikibot/page/_toolforge.py +++ b/pywikibot/page/_toolforge.py @@ -11,6 +11,7 @@ import collections import re +from http import HTTPStatus from typing import TYPE_CHECKING import pywikibot @@ -183,7 +184,7 @@ def authorship( url = baseurl.format(url=url) r = pywikibot.comms.http.fetch(url) - if r.status_code != 200: + if r.status_code != HTTPStatus.OK: r.raise_for_status() result: list[list[str]] = [] diff --git a/pywikibot/page/_wikibase.py b/pywikibot/page/_wikibase.py index 34e3f58817..4324705877 100644 --- a/pywikibot/page/_wikibase.py +++ b/pywikibot/page/_wikibase.py @@ -1616,8 +1616,8 @@ def on_item(self, item) -> None: qualifier.on_item = item for source in self.sources: for values in source.values(): - for source in values: - source.on_item = item + for val in values: + val.on_item = item def _assert_attached(self) -> None: if self.on_item is None: diff --git a/pywikibot/site/_datasite.py b/pywikibot/site/_datasite.py index d4a645bca1..85684857fa 100644 --- a/pywikibot/site/_datasite.py +++ b/pywikibot/site/_datasite.py @@ -345,9 +345,9 @@ def editEntity(self, params['token'] = self.tokens['csrf'] - for arg in kwargs: + for arg, param in kwargs.items(): if arg in ['clear', 'summary', 'tags']: - params[arg] = kwargs[arg] + params[arg] = param elif arg != 'baserevid': warn(f'Unknown wbeditentity parameter {arg} ignored', UserWarning, 2) @@ -1002,9 +1002,9 @@ def prepare_data(action, data): }) params.update(prepare_data(action, action_data)) - for arg in kwargs: + for arg, param in kwargs.items(): if arg in ['summary', 'tags']: - params[arg] = kwargs[arg] + params[arg] = param else: warn(f'Unknown parameter {arg} for action {action}, ignored', UserWarning, 2) diff --git a/pywikibot/site/_generators.py b/pywikibot/site/_generators.py index d77d675f25..8f5c7f707a 100644 --- a/pywikibot/site/_generators.py +++ b/pywikibot/site/_generators.py @@ -209,9 +209,9 @@ def preloadpages( # This checks to see if there is a normalized title in # the response that corresponds to the canonical form # used in the query. - for key in cache: + for key, value in cache.items(): if self.sametitle(key, pagedata['title']): - cache[pagedata['title']] = cache[key] + cache[pagedata['title']] = value break else: pywikibot.warning( diff --git a/pywikibot/tools/__init__.py b/pywikibot/tools/__init__.py index e23a89c863..298f0950c6 100644 --- a/pywikibot/tools/__init__.py +++ b/pywikibot/tools/__init__.py @@ -581,9 +581,9 @@ def open_archive(filename: str, mode: str = 'rb', use_extension: bool = True): with open(filename, 'rb') as f: magic_number = f.read(8) - for pattern in extension_map: + for pattern, ext in extension_map.items(): if magic_number.startswith(pattern): - extension = extension_map[pattern] + extension = ext break else: extension = '' diff --git a/tests/site_tests.py b/tests/site_tests.py index 7a0505bd42..b10ef12fe0 100755 --- a/tests/site_tests.py +++ b/tests/site_tests.py @@ -196,7 +196,7 @@ def test_messages(self): self.assertLength(mysite.mediawiki_messages(months, lang1), 12) self.assertLength(mysite.mediawiki_messages(months, lang2), 12) familyname = mysite.family.name - if lang1 != lang2 and lang1 != familyname and lang2 != familyname: + if lang1 not in (lang2, familyname) and lang2 != familyname: self.assertNotEqual(mysite.mediawiki_messages(months, lang1), mysite.mediawiki_messages(months, lang2)) From f29ae9eaf4ff0b78d2db4da65dbbe91e9e9a6078 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sun, 24 Nov 2024 13:47:44 +0100 Subject: [PATCH 65/95] IMPR: use target.items() to get key/value pairs in create_isbn_edition.py Change-Id: I625d0cfd60da54c0b65d277637f757c97217ca99 --- scripts/create_isbn_edition.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/scripts/create_isbn_edition.py b/scripts/create_isbn_edition.py index e33c5f2625..eeafa91b40 100755 --- a/scripts/create_isbn_edition.py +++ b/scripts/create_isbn_edition.py @@ -935,7 +935,6 @@ def amend_isbn_edition(isbn_number: str) -> int: def add_claims(isbn_data: dict[str, Any]) -> int: # noqa: C901 """Inspect isbn_data and add claims if possible.""" - global proptyx # targetx is not global (to allow for language specific editions) # Set default language from book library @@ -1096,13 +1095,13 @@ def add_claims(isbn_data: dict[str, Any]) -> int: # noqa: C901 # Register missing statements pywikibot.debug(target) - for propty in target: + for propty, title in target.items(): if propty not in item.claims: if propty not in proptyx: proptyx[propty] = pywikibot.PropertyPage(repo, propty) # Target could get overwritten locally - targetx[propty] = pywikibot.ItemPage(repo, target[propty]) + targetx[propty] = pywikibot.ItemPage(repo, title) claim = pywikibot.Claim(repo, propty) claim.setTarget(targetx[propty]) @@ -1110,7 +1109,7 @@ def add_claims(isbn_data: dict[str, Any]) -> int: # noqa: C901 pywikibot.warning( f'Add {get_item_header_lang(proptyx[propty].labels, booklang)}' f':{get_item_header_lang(targetx[propty].labels, booklang)} ' - f'({propty}:{target[propty]})' + f'({propty}:{title})' ) # Set source reference @@ -1675,14 +1674,14 @@ def main(*args: str) -> None: targetx = {} # Validate and encode the propery/instance pair - for propty in target: + for propty, title in target.items(): if propty not in proptyx: proptyx[propty] = pywikibot.PropertyPage(repo, propty) - if target[propty] != '-': - targetx[propty] = get_item_page(target[propty]) + if title != '-': + targetx[propty] = get_item_page(title) pywikibot.info(f'Add {get_item_header(proptyx[propty].labels)}:' f'{get_item_header(targetx[propty].labels)} ' - f'({propty}:{target[propty]})') + f'({propty}:{title})') # Check the instance type for P/Q pairs (critical) if (propty in propreqinst @@ -1690,7 +1689,7 @@ def main(*args: str) -> None: or not item_is_in_list(targetx[propty].claims[INSTANCEPROP], propreqinst[propty]))): pywikibot.critical( - f'{get_item_header(targetx[propty].labels)} ({target[propty]})' + f'{get_item_header(targetx[propty].labels)} ({title})' f' is not one of instance type {propreqinst[propty]} for ' f'statement {get_item_header(proptyx[propty].labels)} ' f'({propty})' @@ -1703,7 +1702,7 @@ def main(*args: str) -> None: and not item_is_in_list(targetx[propty].claims, propreqobjectprop[propty])): pywikibot.error( - f'{get_item_header(targetx[propty].labels)} ({target[propty]})' + f'{get_item_header(targetx[propty].labels)} ({title})' f' does not have property {propreqobjectprop[propty]} for ' f'statement {get_item_header(proptyx[propty].labels)} ' f'({propty})' From 24539b79c87dca8b9aff39acc4419a789cb097b8 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sun, 24 Nov 2024 14:39:15 +0100 Subject: [PATCH 66/95] IMPR: Do not redefine local variable within loop Change-Id: I48a017a86556a0f115c033c69e03b149789e4486 --- pywikibot/site/_siteinfo.py | 8 ++++---- pywikibot/userinterfaces/terminal_interface_base.py | 12 ++++++------ scripts/coordinate_import.py | 6 +++--- scripts/protect.py | 6 +++--- scripts/weblinkchecker.py | 4 ++-- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pywikibot/site/_siteinfo.py b/pywikibot/site/_siteinfo.py index 5471959113..b720aab2bb 100644 --- a/pywikibot/site/_siteinfo.py +++ b/pywikibot/site/_siteinfo.py @@ -142,15 +142,15 @@ def warn_handler(mod, message) -> bool: "one property is unknown: '{}'" .format("', '".join(props))) results = {} - for prop in props: - results.update(self._get_siteinfo(prop, expiry)) + for p in props: + results.update(self._get_siteinfo(p, expiry)) return results raise result = {} if invalid_properties: - for prop in invalid_properties: - result[prop] = (EMPTY_DEFAULT, False) + for invalid_prop in invalid_properties: + result[invalid_prop] = (EMPTY_DEFAULT, False) pywikibot.log("Unable to get siprop(s) '{}'" .format("', '".join(invalid_properties))) diff --git a/pywikibot/userinterfaces/terminal_interface_base.py b/pywikibot/userinterfaces/terminal_interface_base.py index 4f68dfe6d6..b123ff8c4d 100644 --- a/pywikibot/userinterfaces/terminal_interface_base.py +++ b/pywikibot/userinterfaces/terminal_interface_base.py @@ -213,8 +213,8 @@ def _print(self, text, target_stream) -> None: # match.split() includes every regex group; for each matched color # fg_col:b_col, fg_col and bg_col are added to the resulting list. len_text_parts = len(text_parts[::4]) - for index, (text, next_color) in enumerate(zip(text_parts[::4], - text_parts[1::4])): + for index, (txt, next_color) in enumerate(zip(text_parts[::4], + text_parts[1::4])): current_color = color_stack[-1] if next_color == 'previous': if len(color_stack) > 1: # keep the last element in the stack @@ -226,15 +226,15 @@ def _print(self, text, target_stream) -> None: if current_color != next_color: colored_line = True if colored_line and not colorized: - if '\n' in text: # Normal end of line - text = text.replace('\n', ' ***\n', 1) + if '\n' in txt: # Normal end of line + txt = txt.replace('\n', ' ***\n', 1) colored_line = False elif index == len_text_parts - 1: # Or end of text - text += ' ***' + txt += ' ***' colored_line = False # print the text up to the tag. - self._write(text, target_stream) + self._write(txt, target_stream) if current_color != next_color and colorized: # set the new color, but only if they change diff --git a/scripts/coordinate_import.py b/scripts/coordinate_import.py index 3886ce63be..a0cdba8521 100755 --- a/scripts/coordinate_import.py +++ b/scripts/coordinate_import.py @@ -113,9 +113,9 @@ def treat_page_and_item(self, page, item) -> None: return if page is None: # running over items, search in linked pages - for page in item.iterlinks(): - if page.site.has_extension('GeoData') \ - and self.try_import_coordinates_from_page(page, item): + for p in item.iterlinks(): + if p.site.has_extension('GeoData') \ + and self.try_import_coordinates_from_page(p, item): break return diff --git a/scripts/protect.py b/scripts/protect.py index a6681af28b..8150148d27 100755 --- a/scripts/protect.py +++ b/scripts/protect.py @@ -128,15 +128,15 @@ def check_protection_level(operation, level, levels, default=None) -> str: first_char = [] default_char = None num = 1 - for level in levels: - for c in level: + for lev in levels: + for c in lev: if c not in first_char: first_char.append(c) break else: first_char.append(str(num)) num += 1 - if level == default: + if lev == default: default_char = first_char[-1] choice = pywikibot.input_choice( diff --git a/scripts/weblinkchecker.py b/scripts/weblinkchecker.py index c5c4842105..14174b868b 100755 --- a/scripts/weblinkchecker.py +++ b/scripts/weblinkchecker.py @@ -369,10 +369,10 @@ def log(self, url, error, containing_page, archive_url) -> None: error_report = f'* {url} ([{archive_url} archive])\n' else: error_report = f'* {url}\n' - for (page_title, date, error) in self.history_dict[url]: + for (page_title, date, err) in self.history_dict[url]: # ISO 8601 formulation iso_date = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(date)) - error_report += f'** In [[{page_title}]] on {iso_date}, {error}\n' + error_report += f'** In [[{page_title}]] on {iso_date}, {err}\n' pywikibot.info('** Logging link for deletion.') txtfilename = pywikibot.config.datafilepath( 'deadlinks', From 3941f18757c563c15bd4bd996e91ca944d416e5f Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sun, 24 Nov 2024 15:08:47 +0100 Subject: [PATCH 67/95] cleanup: sys.maxunicode is 0x10ffff since Python 3.3 due to PEP393 Change-Id: I1a7b2dd299a2f0a591dfca965a827eedd3c87719 --- pywikibot/tools/chars.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/pywikibot/tools/chars.py b/pywikibot/tools/chars.py index a9aef1b4ec..de011c0f83 100644 --- a/pywikibot/tools/chars.py +++ b/pywikibot/tools/chars.py @@ -1,13 +1,12 @@ """Character based helper functions (not wiki-dependent).""" # -# (C) Pywikibot team, 2015-2023 +# (C) Pywikibot team, 2015-2024 # # Distributed under the terms of the MIT license. # from __future__ import annotations import re -import sys from contextlib import suppress from urllib.parse import unquote @@ -30,14 +29,7 @@ def contains_invisible(text): def replace_invisible(text): """Replace invisible characters by '<codepoint>'.""" def replace(match) -> str: - match = match.group() - if sys.maxunicode < 0x10ffff and len(match) == 2: - mask = (1 << 10) - 1 - assert ord(match[0]) & ~mask == 0xd800 - assert ord(match[1]) & ~mask == 0xdc00 - codepoint = (ord(match[0]) & mask) << 10 | (ord(match[1]) & mask) - else: - codepoint = ord(match) + codepoint = ord(match.group()) return f'<{codepoint:x}>' return INVISIBLE_REGEX.sub(replace, text) From 8142065a7138d0a7604c0b98092e871bfd76d8ed Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sun, 24 Nov 2024 15:34:04 +0100 Subject: [PATCH 68/95] IMPR: use key/value pairs instead of dict for YearAD defaultdict Change-Id: I5af09dec12b00408ef732f8d52159640d8084f3b --- pywikibot/date.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/pywikibot/date.py b/pywikibot/date.py index 17f917c943..b2f40e9e59 100644 --- a/pywikibot/date.py +++ b/pywikibot/date.py @@ -775,26 +775,26 @@ def _period_with_pattern(period: str, pattern: str): 'zh': lambda v: dh_number(v, '%d'), }, - 'YearAD': defaultdict(lambda: dh_simpleYearAD, **{ - 'bn': lambda v: dh_yearAD(v, '%B'), - 'fa': lambda v: dh_yearAD(v, '%F (میلادی)'), - 'gan': lambda v: dh_yearAD(v, '%d年'), - 'gu': lambda v: dh_yearAD(v, '%G'), - 'hi': lambda v: dh_yearAD(v, '%H'), - 'hr': lambda v: dh_yearAD(v, '%d.'), - 'ja': lambda v: dh_yearAD(v, '%d年'), - 'jbo': lambda v: dh_yearAD(v, '%dmoi nanca'), - 'kn': lambda v: dh_yearAD(v, '%K'), - 'ko': lambda v: dh_yearAD(v, '%d년'), - 'ksh': lambda v: dh_yearAD(v, 'Joohr %d'), - 'mr': lambda v: dh_yearAD(v, 'ई.स. %H'), - 'nan': lambda v: dh_yearAD(v, '%d nî'), - 'ru': lambda v: dh_yearAD(v, '%d год'), - # 2005 => 'พ.ศ. 2548' - 'th': lambda v: dh_yearAD(v, 'พ.ศ. %T'), - 'ur': lambda v: dh_yearAD(v, '%dء'), - 'zh': lambda v: dh_yearAD(v, '%d年'), - }), + 'YearAD': defaultdict( + lambda: dh_simpleYearAD, + bn=lambda v: dh_yearAD(v, '%B'), + fa=lambda v: dh_yearAD(v, '%F (میلادی)'), + gan=lambda v: dh_yearAD(v, '%d年'), + gu=lambda v: dh_yearAD(v, '%G'), + hi=lambda v: dh_yearAD(v, '%H'), + hr=lambda v: dh_yearAD(v, '%d.'), + ja=lambda v: dh_yearAD(v, '%d年'), + jbo=lambda v: dh_yearAD(v, '%dmoi nanca'), + kn=lambda v: dh_yearAD(v, '%K'), + ko=lambda v: dh_yearAD(v, '%d년'), + ksh=lambda v: dh_yearAD(v, 'Joohr %d'), + mr=lambda v: dh_yearAD(v, 'ई.स. %H'), + nan=lambda v: dh_yearAD(v, '%d nî'), + ru=lambda v: dh_yearAD(v, '%d год'), + th=lambda v: dh_yearAD(v, 'พ.ศ. %T'), + ur=lambda v: dh_yearAD(v, '%dء'), + zh=lambda v: dh_yearAD(v, '%d年'), + ), 'YearBC': { 'af': lambda v: dh_yearBC(v, '%d v.C.'), From d57b95212c84ecb31dca8f79a947ef265426f896 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sun, 24 Nov 2024 18:00:28 +0100 Subject: [PATCH 69/95] IMPR: use list comprehensions instead of for loops Change-Id: I83654f9f51c6834e07f7ae2e2d0bb26ebfbac014 --- pywikibot/page/_page.py | 3 +-- scripts/upload.py | 5 +++-- tests/__init__.py | 12 ++++++------ tests/pagegenerators_tests.py | 6 ++++-- tests/uploadbot_tests.py | 5 +++-- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/pywikibot/page/_page.py b/pywikibot/page/_page.py index 199d4d642c..6e5f277b39 100644 --- a/pywikibot/page/_page.py +++ b/pywikibot/page/_page.py @@ -117,8 +117,7 @@ def templatesWithParams( # noqa: N802 named[str(k)] = v break - for item in named.items(): - positional.append('{}={}'.format(*item)) + positional += [f'{key}={value}' for key, value in named.items()] result.append((pywikibot.Page(link, self.site), positional)) return result diff --git a/scripts/upload.py b/scripts/upload.py index a64ef92c1f..c766801012 100755 --- a/scripts/upload.py +++ b/scripts/upload.py @@ -222,8 +222,9 @@ def main(*args: str) -> None: if not recursive: # Do not visit any subdirectories directory_info[1][:] = [] - for dir_file in directory_info[2]: - file_list.append(os.path.join(directory_info[0], dir_file)) + + file_list += [os.path.join(directory_info[0], dir_file) + for dir_file in directory_info[2]] url = file_list else: url = [url] diff --git a/tests/__init__.py b/tests/__init__.py index 2c51f5f988..3fc5d44c53 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -239,12 +239,12 @@ def collector(loader=unittest.loader.defaultTestLoader): discovered = loader.loadTestsFromName(module_class_name) enabled_tests = [] for cls in discovered: - for test_func in cls: - if test_func._testMethodName not in disabled_tests[module]: - enabled_tests.append( - module_class_name + '.' - + test_func.__class__.__name__ + '.' - + test_func._testMethodName) + enabled_tests += [ + f'{module_class_name}.{type(test_func).__name__}.' + f'{test_func._testMethodName}' + for test_func in cls + if test_func._testMethodName not in disabled_tests[module] + ] test_list.extend(enabled_tests) else: diff --git a/tests/pagegenerators_tests.py b/tests/pagegenerators_tests.py index e3ba1ed4db..c7cee26e72 100755 --- a/tests/pagegenerators_tests.py +++ b/tests/pagegenerators_tests.py @@ -546,8 +546,10 @@ def _run_test(self, start_month=1, end_month=12, year=2000): expected = [] for month in range(start_month, end_month + 1): - for day in range(1, calendar.monthrange(year, month)[1] + 1): - expected.append(date.format_date(month, day, self.site)) + expected += [ + date.format_date(month, day, self.site) + for day in range(1, calendar.monthrange(year, month)[1] + 1) + ] self.assertPageTitlesEqual(gen2, expected) diff --git a/tests/uploadbot_tests.py b/tests/uploadbot_tests.py index 9c286ba964..4c7e943883 100755 --- a/tests/uploadbot_tests.py +++ b/tests/uploadbot_tests.py @@ -39,8 +39,9 @@ def test_png_list(self): """Test uploading a list of pngs using upload.py.""" image_list = [] for directory_info in os.walk(join_images_path()): - for dir_file in directory_info[2]: - image_list.append(os.path.join(directory_info[0], dir_file)) + image_list += [os.path.join(directory_info[0], dir_file) + for dir_file in directory_info[2]] + bot = UploadRobot(url=image_list, target_site=self.get_site(), **self.params) bot.run() From ee7d2f419cf140603e5ae2d3ecb899f798c192f3 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sun, 24 Nov 2024 15:29:36 +0100 Subject: [PATCH 70/95] IMPR: avoid using join statements with literal constants Change-Id: Icc546daed6c0f9b928b7ef6f34cd30604f0c1974 --- pywikibot/textlib.py | 6 ++--- .../userinterfaces/terminal_interface_base.py | 8 +++--- scripts/archivebot.py | 2 +- scripts/revertbot.py | 2 +- tests/category_bot_tests.py | 23 +++++----------- tests/cosmetic_changes_tests.py | 26 +++++++++---------- tests/site_detect_tests.py | 7 +++-- 7 files changed, 31 insertions(+), 43 deletions(-) diff --git a/pywikibot/textlib.py b/pywikibot/textlib.py index b6966c8f99..00890478c1 100644 --- a/pywikibot/textlib.py +++ b/pywikibot/textlib.py @@ -1112,10 +1112,8 @@ def extract_sections( cat_regex, interwiki_regex = get_regexes(['category', 'interwiki'], site) langlink_pattern = interwiki_regex.pattern.replace(':?', '') last_section_content = sections[-1].content if sections else header - footer = re.search( - r'({})*\Z'.format(r'|'.join((langlink_pattern, - cat_regex.pattern, r'\s'))), - last_section_content).group().lstrip() + footer = re.search(fr'({langlink_pattern}|{cat_regex.pattern}|\s)*\Z', + last_section_content).group().lstrip() if footer: if sections: diff --git a/pywikibot/userinterfaces/terminal_interface_base.py b/pywikibot/userinterfaces/terminal_interface_base.py index b123ff8c4d..c32a1136f3 100644 --- a/pywikibot/userinterfaces/terminal_interface_base.py +++ b/pywikibot/userinterfaces/terminal_interface_base.py @@ -317,10 +317,10 @@ def stream_output(self, text, targetStream=None) -> None: # transliteration was successful. The replacement # could consist of multiple letters. # mark the transliterated letters in yellow. - transliteratedText = ''.join((transliteratedText, - '<<lightyellow>>', - transliterated, - '<<previous>>')) + transliteratedText = ( + f'{transliteratedText}' + f'<<lightyellow>>{transliterated}<<previous>>' + ) # memorize if we replaced a single letter by multiple # letters. if transliterated: diff --git a/scripts/archivebot.py b/scripts/archivebot.py index 7d9b8838c1..732b3623fc 100755 --- a/scripts/archivebot.py +++ b/scripts/archivebot.py @@ -422,7 +422,7 @@ def load_page(self) -> None: header, threads, footer = extract_sections(text, self.site) header = header.replace(marker, '') if header and footer: - self.header = '\n\n'.join((header.rstrip(), footer, '')) + self.header = f'{header.rstrip()}\n\n{footer}\n\n' else: self.header = header + footer diff --git a/scripts/revertbot.py b/scripts/revertbot.py index bf2f442910..53bcaadc2d 100755 --- a/scripts/revertbot.py +++ b/scripts/revertbot.py @@ -103,7 +103,7 @@ def local_timestamp(self, ts) -> str: year = formatYear(self.site.lang, ts.year) date = format_date(ts.month, ts.day, self.site) *_, time = str(ts).strip('Z').partition('T') - return ' '.join((date, year, time)) + return f'{date} {year} {time}' def revert(self, item) -> str | bool: """Revert a single item.""" diff --git a/tests/category_bot_tests.py b/tests/category_bot_tests.py index efc28ce373..13ba895f72 100755 --- a/tests/category_bot_tests.py +++ b/tests/category_bot_tests.py @@ -47,22 +47,13 @@ def test_strip_cfd_templates_without_spaces_in_comments(self): def _runtest_strip_cfd_templates(self, template_start, template_end): """Run a CFD template stripping test, given CFD start/end templates.""" bot = CategoryMoveRobot(oldcat='Old', newcat='New') - bot.newcat.text = '\n'.join(( - 'Preamble', - template_start, - 'Random text inside template', - 'Even another template: {{cfr-speedy}}', - template_end, - 'Footer stuff afterwards', - '', - '[[Category:Should remain]]' - )) - expected = '\n'.join(( - 'Preamble', - 'Footer stuff afterwards', - '', - '[[Category:Should remain]]' - )) + bot.newcat.text = ( + f'Preamble\n{template_start}\nRandom text inside template\n' + f'Even another template: {{{{cfr-speedy}}}}\n{template_end}\n' + f'Footer stuff afterwards\n\n[[Category:Should remain]]' + ) + expected = ('Preamble\nFooter stuff afterwards\n\n' + '[[Category:Should remain]]') bot._strip_cfd_templates(commit=False) self.assertEqual(bot.newcat.text, expected) diff --git a/tests/cosmetic_changes_tests.py b/tests/cosmetic_changes_tests.py index 8714183abf..538cda2d12 100755 --- a/tests/cosmetic_changes_tests.py +++ b/tests/cosmetic_changes_tests.py @@ -581,31 +581,31 @@ def test_invalid_isbn(self): # Invalid characters with self.assertRaisesRegex( ValidationError, - '|'.join((self.ISBN_DIGITERROR_RE, - self.ISBN_INVALIDERROR_RE, - self.ISBN_INVALIDLENGTHERROR_RE))): + f'{self.ISBN_DIGITERROR_RE}|{self.ISBN_INVALIDERROR_RE}|' + f'{self.ISBN_INVALIDLENGTHERROR_RE}' + ): self.cct.fix_ISBN('ISBN 0975229LOL') # Invalid checksum with self.assertRaisesRegex( ValidationError, - '|'.join((self.ISBN_CHECKSUMERROR_RE, - self.ISBN_INVALIDERROR_RE, - self.ISBN_INVALIDLENGTHERROR_RE, - self.ISBN_INVALIDCHECKERROR_RE))): + f'{self.ISBN_CHECKSUMERROR_RE}|{self.ISBN_INVALIDERROR_RE}|' + f'{self.ISBN_INVALIDLENGTHERROR_RE}|' + f'{self.ISBN_INVALIDCHECKERROR_RE}' + ): self.cct.fix_ISBN('ISBN 0975229801') # Invalid length with self.assertRaisesRegex( ValidationError, - '|'.join((self.ISBN_DIGITERROR_RE, - self.ISBN_INVALIDERROR_RE, - self.ISBN_INVALIDLENGTHERROR_RE))): + f'{self.ISBN_DIGITERROR_RE}|{self.ISBN_INVALIDERROR_RE}|' + f'{self.ISBN_INVALIDLENGTHERROR_RE}' + ): self.cct.fix_ISBN('ISBN 09752298') # X in the middle with self.assertRaisesRegex( ValidationError, - '|'.join((self.ISBN_INVALIDCHARERROR_RE, - self.ISBN_INVALIDERROR_RE, - self.ISBN_INVALIDLENGTHERROR_RE))): + f'{self.ISBN_INVALIDCHARERROR_RE}|{self.ISBN_INVALIDERROR_RE}|' + f'{self.ISBN_INVALIDLENGTHERROR_RE}' + ): self.cct.fix_ISBN('ISBN 09752X9801') def test_ignore_invalid_isbn(self): diff --git a/tests/site_detect_tests.py b/tests/site_detect_tests.py index 715fb24515..3c18a1a6bb 100755 --- a/tests/site_detect_tests.py +++ b/tests/site_detect_tests.py @@ -170,10 +170,9 @@ class PrivateWikiTestCase(PatchingTestCase): # the user-supplied URL. We need to return enough data for # site_detect.WikiHTMLPageParser to determine the server # version and the API URL. - WEBPATH: ''.join(( - '<meta name="generator" content="', _generator, - '"/>\n<link rel="EditURI" type="application/rsd+xml" ' - 'href="', _apiurl, '?action=rsd"/>')), + WEBPATH: f'<meta name="generator" content="{_generator}"/>\n' + f'<link rel="EditURI" type="application/rsd+xml" ' + f'href="{_apiurl}?action=rsd"/>', APIPATH: '{"error":{"code":"readapidenied"}}', } From 93409b173a2290846031549c7a7a97d11b11ba37 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sun, 24 Nov 2024 18:40:19 +0100 Subject: [PATCH 71/95] tests: install tomli for oauth_tests-ci Change-Id: I7c183a45080da19c71f24d4ca1b0c80a67fa98b3 --- .github/workflows/oauth_tests-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/oauth_tests-ci.yml b/.github/workflows/oauth_tests-ci.yml index e0b9105c15..43d6f24d94 100644 --- a/.github/workflows/oauth_tests-ci.yml +++ b/.github/workflows/oauth_tests-ci.yml @@ -91,6 +91,7 @@ jobs: pip install mwoauth pip install packaging pip install requests + pip install "tomli >= 2.02; python_version < '3.11" - name: Generate family files if: ${{ matrix.family == 'wpbeta' }} From 8fde0bfb33faa1d0815f26ad7aed141b85ba28cd Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sun, 24 Nov 2024 18:45:18 +0100 Subject: [PATCH 72/95] tests: fix for tomli install Change-Id: I93d9a6831d89dbc835810d45a5149cd696ebce42 --- .github/workflows/oauth_tests-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/oauth_tests-ci.yml b/.github/workflows/oauth_tests-ci.yml index 43d6f24d94..76fd7a2bef 100644 --- a/.github/workflows/oauth_tests-ci.yml +++ b/.github/workflows/oauth_tests-ci.yml @@ -91,7 +91,7 @@ jobs: pip install mwoauth pip install packaging pip install requests - pip install "tomli >= 2.02; python_version < '3.11" + pip install "tomli >= 2.02; python_version < '3.11'" - name: Generate family files if: ${{ matrix.family == 'wpbeta' }} From f050f8025c94fdbae6bc409781257d544a75b890 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sun, 24 Nov 2024 18:55:35 +0100 Subject: [PATCH 73/95] tests: fix for tomli dependency Change-Id: I0ab3ea3150db86aa8b6eb4bc0039730cd085b679 --- .github/workflows/oauth_tests-ci.yml | 2 +- docs/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/oauth_tests-ci.yml b/.github/workflows/oauth_tests-ci.yml index 76fd7a2bef..5900c59dc9 100644 --- a/.github/workflows/oauth_tests-ci.yml +++ b/.github/workflows/oauth_tests-ci.yml @@ -91,7 +91,7 @@ jobs: pip install mwoauth pip install packaging pip install requests - pip install "tomli >= 2.02; python_version < '3.11'" + pip install "tomli >= 2.0.2; python_version < '3.11'" - name: Generate family files if: ${{ matrix.family == 'wpbeta' }} diff --git a/docs/requirements.txt b/docs/requirements.txt index ed1375865f..090e200042 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,5 +6,5 @@ rstcheck >=6.2.4 sphinxext-opengraph >= 0.9.1 sphinx-copybutton >= 0.5.2 sphinx-tabs >= 3.4.7 -tomli >= 2.02; python_version < '3.11' +tomli >= 2.0.2; python_version < '3.11' furo >= 2024.8.6 From 284eae4376877d4ea54c23acd3a9075a216548f0 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Mon, 25 Nov 2024 08:28:46 +0100 Subject: [PATCH 74/95] tests: use tomli for test running with Python < 3.11 Bug: T380697 Change-Id: I6968a9b79cd4122af1070209f01e0a8d00c35ddb --- .github/workflows/doctest.yml | 1 + .github/workflows/login_tests-ci.yml | 1 + .github/workflows/oauth_tests-ci.yml | 2 +- dev-requirements.txt | 3 +++ docs/requirements.txt | 2 +- 5 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/doctest.yml b/.github/workflows/doctest.yml index 615fb557e1..e14c36b09c 100644 --- a/.github/workflows/doctest.yml +++ b/.github/workflows/doctest.yml @@ -60,6 +60,7 @@ jobs: python -m pip install --upgrade pip pip --version pip install coverage + pip install "tomli; python_version < '3.11'" pip install "importlib_metadata ; python_version < '3.8'" pip install mwparserfromhell pip install packaging diff --git a/.github/workflows/login_tests-ci.yml b/.github/workflows/login_tests-ci.yml index d4f3a06b5a..168790acaf 100644 --- a/.github/workflows/login_tests-ci.yml +++ b/.github/workflows/login_tests-ci.yml @@ -86,6 +86,7 @@ jobs: python -m pip install --upgrade pip pip --version pip install coverage + pip install "tomli; python_version < '3.11'" pip install "importlib_metadata ; python_version < '3.8'" pip install mwparserfromhell pip install packaging diff --git a/.github/workflows/oauth_tests-ci.yml b/.github/workflows/oauth_tests-ci.yml index 5900c59dc9..9435725830 100644 --- a/.github/workflows/oauth_tests-ci.yml +++ b/.github/workflows/oauth_tests-ci.yml @@ -84,6 +84,7 @@ jobs: python -m pip install --upgrade pip pip --version pip install coverage + pip install "tomli; python_version < '3.11'" pip install "importlib_metadata ; python_version < '3.8'" pip install mwparserfromhell # PyJWT added due to T380270 @@ -91,7 +92,6 @@ jobs: pip install mwoauth pip install packaging pip install requests - pip install "tomli >= 2.0.2; python_version < '3.11'" - name: Generate family files if: ${{ matrix.family == 'wpbeta' }} diff --git a/dev-requirements.txt b/dev-requirements.txt index a87313f96d..60cc9e917e 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -10,6 +10,9 @@ pytest-xvfb>=3.0.0 pre-commit coverage>=5.2.1 +# required for coverage (T380697) +tomli>=2.0.1; python_version < "3.11" + # optional but needed for tests fake-useragent diff --git a/docs/requirements.txt b/docs/requirements.txt index 090e200042..0dbffe905c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,5 +6,5 @@ rstcheck >=6.2.4 sphinxext-opengraph >= 0.9.1 sphinx-copybutton >= 0.5.2 sphinx-tabs >= 3.4.7 -tomli >= 2.0.2; python_version < '3.11' +tomli >= 2.0.1; python_version < '3.11' furo >= 2024.8.6 From 3c9dce3d5f497ce33cf049d4b0f6d363c22f20ba Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Mon, 25 Nov 2024 12:50:19 +0100 Subject: [PATCH 75/95] tests: test TestInterwikidataBot.test_main for all sites Change-Id: I8fe175d80701003e12195aac0bbacb125165f0e4 --- tests/interwikidata_tests.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/interwikidata_tests.py b/tests/interwikidata_tests.py index 4780a65fdf..51b0f1cd91 100755 --- a/tests/interwikidata_tests.py +++ b/tests/interwikidata_tests.py @@ -13,7 +13,7 @@ import pywikibot from pywikibot import Link from scripts import interwikidata -from tests.aspects import SiteAttributeTestCase +from tests.aspects import AlteredDefaultSiteTestCase, SiteAttributeTestCase from tests.utils import empty_sites @@ -34,7 +34,7 @@ def try_to_add(self): return -class TestInterwikidataBot(SiteAttributeTestCase): +class TestInterwikidataBot(AlteredDefaultSiteTestCase, SiteAttributeTestCase): """Test Interwikidata.""" @@ -53,10 +53,13 @@ class TestInterwikidataBot(SiteAttributeTestCase): }, } - def test_main(self): + def test_main(self, key): """Test main function interwikidata.py.""" - # The default site is used here - if pywikibot.Site().has_data_repository: + site = self.get_site(key) + pywikibot.config.family = site.family + pywikibot.config.mylang = site.code + + if site.has_data_repository: with empty_sites(): # The main function return None. self.assertIsNone(interwikidata.main()) From 13b4fe8c820c406e9832913025d253d209dd61fc Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Mon, 25 Nov 2024 17:50:25 +0100 Subject: [PATCH 76/95] tests: use coverage < 7.6.2 or > 7.6.8 with pypy 3.10 Bug: T380732 Change-Id: I7b16584fe796c8cd0db9891dfe121b64f4794be8 --- .github/workflows/pywikibot-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pywikibot-ci.yml b/.github/workflows/pywikibot-ci.yml index cc2fb97cba..7aae45c79e 100644 --- a/.github/workflows/pywikibot-ci.yml +++ b/.github/workflows/pywikibot-ci.yml @@ -89,6 +89,8 @@ jobs: run: | python -m pip install --upgrade pip pip --version + # T380732 + pip install "coverage !=7.6.2,!=7.6.3,!=7.6.4,!=7.6.5,!=7.6.6,!=7.6.7,!=7.6.8; implementation_name=='pypy' and python_version=='3.10'" if [ -f dev-requirements.txt ]; then pip install -r dev-requirements.txt; fi if [ -f requirements.txt ]; then pip install -r requirements.txt; fi pip install wikitextparser From 61aa6eb9da456b2f1540c3a5a5f906129accfbe8 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Mon, 25 Nov 2024 18:37:18 +0100 Subject: [PATCH 77/95] cleanup: remove unused 'error' parameter from History.log() method Bug: T380693 Change-Id: Iade518b3d24c8bdf470b891f1bc8c9bc569e4099 --- scripts/weblinkchecker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/weblinkchecker.py b/scripts/weblinkchecker.py index 14174b868b..daaa880263 100755 --- a/scripts/weblinkchecker.py +++ b/scripts/weblinkchecker.py @@ -363,7 +363,7 @@ def __init__(self, report_thread, site=None) -> None: # no saved history exists yet, or history dump broken self.history_dict = {} - def log(self, url, error, containing_page, archive_url) -> None: + def log(self, url, containing_page, archive_url) -> None: """Log an error report to a text file in the deadlinks subdirectory.""" if archive_url: error_report = f'* {url} ([{archive_url} archive])\n' @@ -411,7 +411,7 @@ def set_dead_link(self, url, error, page, weblink_dead_days) -> None: pywikibot.warning( f'get_closest_memento_url({url}) failed: {e}') archive_url = None - self.log(url, error, page, archive_url) + self.log(url, page, archive_url) else: self.history_dict[url] = [(page.title(), now, error)] From a3125f81b159ff5126bee9c9ddac1232678b86b8 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Mon, 25 Nov 2024 19:59:45 +0100 Subject: [PATCH 78/95] tests: Skip gui_tests if a RuntimeError occures with tkinter Bug: T380732 Change-Id: I2c9176f0af694d9cdc828730b04156876f010357 --- tests/gui_tests.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/gui_tests.py b/tests/gui_tests.py index 3d15388693..dd1e7bcc55 100755 --- a/tests/gui_tests.py +++ b/tests/gui_tests.py @@ -84,11 +84,17 @@ def setUpModule(): # pypy3 has a tkinter module which just raises importError if _tkinter # is not installed; thus require_modules does not work for it. + # pypy3.10 has a version mismatch, see T380732. try: import tkinter except ImportError as e: raise unittest.SkipTest(e) + try: + tkinter.Tk() + except RuntimeError as e: + raise unittest.SkipTest(f'Skipping due to T380732 - {e}') + from pywikibot.userinterfaces.gui import EditBoxWindow, Tkdialog From eeec6d89177706546c54702dedcd7f2225085ff0 Mon Sep 17 00:00:00 2001 From: Xqt <info@gno.de> Date: Tue, 26 Nov 2024 04:01:02 +0000 Subject: [PATCH 79/95] Tests: more verbosity with pytest Change-Id: I136ec4872559270e8d335bd1d56451e5279eeda4 Signed-off-by: Xqt <info@gno.de> --- .github/workflows/pywikibot-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pywikibot-ci.yml b/.github/workflows/pywikibot-ci.yml index 7aae45c79e..4b1ce541a1 100644 --- a/.github/workflows/pywikibot-ci.yml +++ b/.github/workflows/pywikibot-ci.yml @@ -140,7 +140,7 @@ jobs: if [ ${{matrix.site || 0}} != 'wikisource:zh' ]; then coverage run -m unittest discover -vv -p \"*_tests.py\"; else - pytest --cov=.; + pytest -vv --cov=.; fi - name: Show coverage statistics From a21843bed61211ae17c13eafd63214287399eab0 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Tue, 26 Nov 2024 10:09:15 +0100 Subject: [PATCH 80/95] tests: destroy tkinter dialog window after creation Bug: T380732 Change-Id: Ie60c8560b3e3cc4d45da90c69f90ebf51c17ea68 --- .github/workflows/pywikibot-ci.yml | 2 +- tests/gui_tests.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pywikibot-ci.yml b/.github/workflows/pywikibot-ci.yml index 4b1ce541a1..1c50929f05 100644 --- a/.github/workflows/pywikibot-ci.yml +++ b/.github/workflows/pywikibot-ci.yml @@ -140,7 +140,7 @@ jobs: if [ ${{matrix.site || 0}} != 'wikisource:zh' ]; then coverage run -m unittest discover -vv -p \"*_tests.py\"; else - pytest -vv --cov=.; + pytest -v --cov=.; fi - name: Show coverage statistics diff --git a/tests/gui_tests.py b/tests/gui_tests.py index dd1e7bcc55..91cb9759b4 100755 --- a/tests/gui_tests.py +++ b/tests/gui_tests.py @@ -91,9 +91,11 @@ def setUpModule(): raise unittest.SkipTest(e) try: - tkinter.Tk() + dialog = tkinter.Tk() except RuntimeError as e: raise unittest.SkipTest(f'Skipping due to T380732 - {e}') + else: + dialog.destroy() from pywikibot.userinterfaces.gui import EditBoxWindow, Tkdialog From 4a0bdfc051d144325dbcd74a2c05268563238019 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Wed, 20 Nov 2024 17:47:20 +0100 Subject: [PATCH 81/95] [IMPR] use Site.search() in get_item_with_prop_value Change-Id: Ia1c0c8ced9926ae48ca6fd7c6704150492035d5f --- scripts/create_isbn_edition.py | 44 ++++++++++++---------------------- 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/scripts/create_isbn_edition.py b/scripts/create_isbn_edition.py index 274627daf9..a802484fe8 100755 --- a/scripts/create_isbn_edition.py +++ b/scripts/create_isbn_edition.py @@ -377,7 +377,6 @@ import pywikibot # API interface to Wikidata from pywikibot.config import verbose_output as verbose -from pywikibot.data import api from pywikibot.tools import first_upper @@ -813,7 +812,8 @@ def get_item_list(item_name: str, def get_item_with_prop_value(prop: str, propval: str) -> set[str]: """Get list of items that have a property/value statement. - .. seealso:: :api:`Search` + .. seealso:: :meth:`Site.search() + <pywikibot.site._generators.GeneratorsMixin.search>` :param prop: Property ID :param propval: Property value @@ -823,33 +823,19 @@ def get_item_with_prop_value(prop: str, propval: str) -> set[str]: pywikibot.debug(f'Search statement: {srsearch}') item_name_canon = unidecode(propval).casefold() item_list = set() - # TODO: use APISite.search instead? - params = { - 'action': 'query', # Statement search - 'list': 'search', - 'srsearch': srsearch, - 'srwhat': 'text', - 'format': 'json', - 'srlimit': 50, # Should be reasonable value - } - request = api.Request(site=repo, parameters=params) - result = request.submit() - # https://www.wikidata.org/w/api.php?action=query&list=search&srwhat=text&srsearch=P212:978-94-028-1317-3 - # https://www.wikidata.org/w/index.php?search=P212:978-94-028-1317-3 - - if 'query' in result and 'search' in result['query']: - # Loop though items - for row in result['query']['search']: - qnumber = row['title'] - item = get_item_page(qnumber) - - if prop not in item.claims: - continue - - for seq in item.claims[prop]: - if unidecode(seq.getTarget()).casefold() == item_name_canon: - item_list.add(item) # Found match - break + + # Loop though items + for row in repo.search(srsearch, where='text', total=50): + qnumber = row['title'] + item = get_item_page(qnumber) + + if prop not in item.claims: + continue + + for seq in item.claims[prop]: + if unidecode(seq.getTarget()).casefold() == item_name_canon: + item_list.add(item) # Found match + break pywikibot.log(item_list) return item_list From beb62c3ac042ef3e0db6bb455f5f8ff94aea5feb Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Wed, 27 Nov 2024 16:18:03 +0100 Subject: [PATCH 82/95] tests: autoupdate docsig release Change-Id: Ic5be3dc7301703a28829e0df04c2cde9beb42a73 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c020770bfd..536482a4f5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -78,7 +78,7 @@ repos: - id: isort exclude: '^pwb\.py$' - repo: https://github.com/jshwi/docsig - rev: v0.64.1 + rev: v0.65.0 hooks: - id: docsig exclude: ^(tests|scripts) From fa47da3ee507098a9437dbad2eeae97b62da2533 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sat, 30 Nov 2024 11:12:52 +0100 Subject: [PATCH 83/95] tests: fix spelling mistake Change-Id: I7e84f5d06a5f9a7b93241375fee83530c1b5cdc2 --- tests/oauth_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/oauth_tests.py b/tests/oauth_tests.py index a39c528832..bbdb8e3b05 100755 --- a/tests/oauth_tests.py +++ b/tests/oauth_tests.py @@ -92,7 +92,7 @@ def test_edit(self): self.assertIn(ts, t) -class TestOauthLoginManger(DefaultSiteTestCase, OAuthSiteTestCase): +class TestOauthLoginManager(DefaultSiteTestCase, OAuthSiteTestCase): """Test OAuth login manager.""" From 9c8ba1f6e18d947f03980a27eb1e0c5020ef884e Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sat, 30 Nov 2024 14:33:28 +0100 Subject: [PATCH 84/95] tests: Update PyJWT requirements - PyJWT < 2.10.0 is required due to https://github.com/jpadilla/pyjwt/issues/1017 - increase leeway for mwoauth.identify function because the default value of 10.0 seconds is too low sometimes - add __repr__ method for LoginManager Bug: T380270 Change-Id: Ie7fadb26f5a3947343feb302f58098d266af2fee --- .github/workflows/oauth_tests-ci.yml | 3 ++- pywikibot/login.py | 17 +++++++++++++---- requirements.txt | 2 +- setup.py | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.github/workflows/oauth_tests-ci.yml b/.github/workflows/oauth_tests-ci.yml index 9435725830..2af22aa8e0 100644 --- a/.github/workflows/oauth_tests-ci.yml +++ b/.github/workflows/oauth_tests-ci.yml @@ -84,11 +84,12 @@ jobs: python -m pip install --upgrade pip pip --version pip install coverage + # tomli required for coverage due to T380697 pip install "tomli; python_version < '3.11'" pip install "importlib_metadata ; python_version < '3.8'" pip install mwparserfromhell # PyJWT added due to T380270 - pip install "PyJWT != 2.10.0 ; python_version > '3.8'" + pip install "PyJWT != 2.10.0, != 2.10.1 ; python_version > '3.8'" pip install mwoauth pip install packaging pip install requests diff --git a/pywikibot/login.py b/pywikibot/login.py index c4357736bd..1a714e16c7 100644 --- a/pywikibot/login.py +++ b/pywikibot/login.py @@ -592,7 +592,12 @@ def access_token(self) -> tuple[str, str] | None: @property def identity(self) -> dict[str, Any] | None: - """Get identifying information about a user via an authorized token.""" + """Get identifying information about a user via an authorized token. + + .. versionchanged:: 9.6 + *leeway* parameter for ``mwoauth.identify`` function was + increased to 30.0 seconds. + """ if self.access_token is None: pywikibot.error('Access token not set') return None @@ -601,8 +606,12 @@ def identity(self) -> dict[str, Any] | None: access_token = mwoauth.AccessToken(*self.access_token) try: identity = mwoauth.identify(self.site.base_url(self.site.path()), - consumer_token, access_token) - return identity + consumer_token, + access_token, + leeway=30.0) except Exception as e: pywikibot.error(e) - return None + else: + return identity + + return None diff --git a/requirements.txt b/requirements.txt index df401ab5a6..4193d61c53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,7 +33,7 @@ wikitextparser>=0.47.5 # mwoauth 0.2.4 is needed because it supports getting identity information # about the user # Due to T380270 PyJWT must be set -PyJWT != 2.10.0; python_version > '3.8' +PyJWT != 2.10.0, != 2.10.1; python_version > '3.8' mwoauth>=0.2.4,!=0.3.1 # interwiki_graph.py module and category_graph.py script: diff --git a/setup.py b/setup.py index 3a56fbc6b1..3f3953eafb 100755 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ 'Pillow>=10.4; python_version >= "3.13"', ], 'mwoauth': [ - 'PyJWT != 2.10.0; python_version > "3.8"', # T380270 + 'PyJWT != 2.10.0, != 2.10.1; python_version > "3.8"', # T380270 'mwoauth!=0.3.1,>=0.2.4', ], 'html': ['beautifulsoup4>=4.7.1'], From 5ed63fd958b69e0ee881f55adc109b7b8a6a5a60 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sat, 30 Nov 2024 17:07:22 +0100 Subject: [PATCH 85/95] Tests: no verbosity with pytest verbosity flag leads replacebot_tests and ui_tests to fail Bug: T381199 Change-Id: Ic39053f9a6a4b625acf8a216027993c4fea6f216 --- .github/workflows/pywikibot-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pywikibot-ci.yml b/.github/workflows/pywikibot-ci.yml index 1c50929f05..7aae45c79e 100644 --- a/.github/workflows/pywikibot-ci.yml +++ b/.github/workflows/pywikibot-ci.yml @@ -140,7 +140,7 @@ jobs: if [ ${{matrix.site || 0}} != 'wikisource:zh' ]; then coverage run -m unittest discover -vv -p \"*_tests.py\"; else - pytest -v --cov=.; + pytest --cov=.; fi - name: Show coverage statistics From 50de04ac92af1dea1a9e4ff078c05c1ff6737272 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Sun, 1 Dec 2024 18:46:42 +0100 Subject: [PATCH 86/95] doc: Enable docstring with classproperty methods - modify docstring of classproperty methods to show a preleading classproperty and add rtype if present. - return the modified decorator class when Sphinx is running. Bug: T380628 Change-Id: Ie44c9f89d34e7cf4aec332daf8cbed9b30ac0dc8 --- pywikibot/tools/__init__.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pywikibot/tools/__init__.py b/pywikibot/tools/__init__.py index 298f0950c6..7375d0de65 100644 --- a/pywikibot/tools/__init__.py +++ b/pywikibot/tools/__init__.py @@ -177,12 +177,20 @@ def bar(cls): # a class property method """ def __init__(self, cls_method) -> None: - """Hold the class method.""" + """Initializer: hold the class method and documentation.""" self.method = cls_method - self.__doc__ = self.method.__doc__ + self.__annotations__ = self.method.__annotations__ + self.__doc__ = (':class:`classproperty<tools.classproperty>` ' + f'{self.method.__doc__}') + rtype = self.__annotations__.get('return') + if rtype: + self.__doc__ += f'\n\n:rtype: {rtype}' def __get__(self, instance, owner): """Get the attribute of the owner class by its method.""" + if SPHINX_RUNNING: + return self + return self.method(owner) From c96c50ab340bfc9ca101ecc270afde513cd36e97 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Mon, 2 Dec 2024 09:57:26 +0100 Subject: [PATCH 87/95] doc: Remove duplicate family.Family entry Change-Id: I355ee2ad8d5d3e91b406721e3efeae77c520495d --- docs/api_ref/family.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api_ref/family.rst b/docs/api_ref/family.rst index 9717a19f8d..314d96a6f7 100644 --- a/docs/api_ref/family.rst +++ b/docs/api_ref/family.rst @@ -4,6 +4,7 @@ .. automodule:: family :synopsis: Objects representing MediaWiki families + :exclude-members: Family .. autoclass:: Family From bf2c1c778d3e18cf92dbd456041a4cca05b002bc Mon Sep 17 00:00:00 2001 From: Meno25 <meno25mail@gmail.com> Date: Mon, 2 Dec 2024 12:32:51 +0000 Subject: [PATCH 88/95] Add support for new wiki * idwikivoyage Bug: T381082 Change-Id: Ie097cf346093a7057776738cc002d477daa82102 --- pywikibot/families/wikivoyage_family.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pywikibot/families/wikivoyage_family.py b/pywikibot/families/wikivoyage_family.py index bc541fc2b2..9f0b9424ac 100644 --- a/pywikibot/families/wikivoyage_family.py +++ b/pywikibot/families/wikivoyage_family.py @@ -18,8 +18,8 @@ class Family(family.SubdomainFamily, family.WikimediaFamily): codes = { 'bn', 'cs', 'de', 'el', 'en', 'eo', 'es', 'fa', 'fi', 'fr', 'he', 'hi', - 'it', 'ja', 'nl', 'pl', 'ps', 'pt', 'ro', 'ru', 'shn', 'sv', 'tr', - 'uk', 'vi', 'zh', + 'id', 'it', 'ja', 'nl', 'pl', 'ps', 'pt', 'ro', 'ru', 'shn', 'sv', + 'tr', 'uk', 'vi', 'zh', } category_redirect_templates = { From 548635aa561bd9289019ceb33f7cf73f96c485f9 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Mon, 2 Dec 2024 16:48:22 +0100 Subject: [PATCH 89/95] doc: adjust classproperty docstrings for Python 3.12 and below Bug: T381279 Change-Id: I22706940b1fc4f8ab401313495ff923cc6e9a634 --- pywikibot/tools/__init__.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pywikibot/tools/__init__.py b/pywikibot/tools/__init__.py index 7375d0de65..d36f52c092 100644 --- a/pywikibot/tools/__init__.py +++ b/pywikibot/tools/__init__.py @@ -180,11 +180,19 @@ def __init__(self, cls_method) -> None: """Initializer: hold the class method and documentation.""" self.method = cls_method self.__annotations__ = self.method.__annotations__ - self.__doc__ = (':class:`classproperty<tools.classproperty>` ' - f'{self.method.__doc__}') + doc = self.method.__doc__ + self.__doc__ = f':class:`classproperty<tools.classproperty>` {doc}' + rtype = self.__annotations__.get('return') if rtype: - self.__doc__ += f'\n\n:rtype: {rtype}' + lines = doc.splitlines() + + if len(lines) > 2 and PYTHON_VERSION < (3, 13): + spaces = ' ' * re.search('[^ ]', lines[2]).start() + else: + spaces = '' + + self.__doc__ += f'\n{spaces}:rtype: {rtype}' def __get__(self, instance, owner): """Get the attribute of the owner class by its method.""" From f83ab2850b218c743bc7059e352275d89da357eb Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Mon, 2 Dec 2024 17:10:44 +0100 Subject: [PATCH 90/95] tests: Skip wikiblame tests due to T381262 Bug: T381262 Change-Id: Id2eef3e62f719e662c1dc917b6614a0a27628bbd --- pywikibot/page/_toolforge.py | 12 ++++++------ tests/wikiblame_tests.py | 2 ++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/pywikibot/page/_toolforge.py b/pywikibot/page/_toolforge.py index 19a2782c67..cfe2f9ebe9 100644 --- a/pywikibot/page/_toolforge.py +++ b/pywikibot/page/_toolforge.py @@ -74,9 +74,9 @@ def main_authors(self) -> collections.Counter[str, int]: >>> import pywikibot >>> site = pywikibot.Site('wikipedia:eu') >>> page = pywikibot.Page(site, 'Python (informatika)') - >>> auth = page.main_authors() - >>> auth.most_common(1) - [('Ksarasola', 82)] + >>> auth = page.main_authors() # doctest: +SKIP + >>> auth.most_common(1) # doctest: +SKIP + [('Ksarasola', 82)] # doctest: +SKIP .. important:: Only implemented for main namespace pages and only wikipedias of :attr:`WIKIBLAME_CODES` are supported. @@ -126,9 +126,9 @@ def authorship( >>> import pywikibot >>> site = pywikibot.Site('wikipedia:en') >>> page = pywikibot.Page(site, 'Pywikibot') - >>> auth = page.authorship() - >>> auth - {'1234qwer1234qwer4': (68, 100.0)} + >>> auth = page.authorship() # doctest: +SKIP + >>> auth # doctest: +SKIP + {'1234qwer1234qwer4': (68, 100.0)} # doctest: +SKIP .. important:: Only implemented for main namespace pages and only wikipedias of :attr:`WIKIBLAME_CODES` are supported. diff --git a/tests/wikiblame_tests.py b/tests/wikiblame_tests.py index 0d5057caec..306cb5d1e8 100644 --- a/tests/wikiblame_tests.py +++ b/tests/wikiblame_tests.py @@ -36,6 +36,7 @@ def test_exceptions(self): page.authorship() @require_modules('wikitextparser') + @unittest.expectedFailure # T381262 def test_main_authors(self): """Test main_authors() method.""" page = pywikibot.Page(self.site, 'Python (programmeertaal)') @@ -48,6 +49,7 @@ def test_main_authors(self): self.assertIsInstance(values[1], float) @require_modules('wikitextparser') + @unittest.expectedFailure # T381262 def test_restrictions(self): """Test main_authors() method with restrictions.""" page = pywikibot.Page(pywikibot.Site('wikipedia:en'), 'Python') From 62a9418169e41d08abca8f410faa29d330f67c0e Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Mon, 2 Dec 2024 18:03:14 +0100 Subject: [PATCH 91/95] doc: Update ROADMAP.rst Change-Id: Ic495bdcbd29272d168cfed14f6ebad33db5c7536 --- ROADMAP.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ROADMAP.rst b/ROADMAP.rst index f76a3c3cb9..b706a44562 100644 --- a/ROADMAP.rst +++ b/ROADMAP.rst @@ -1,6 +1,18 @@ Current Release Changes ======================= +* Add support for idwikivoyage (:phab:`T381082`) +* Add docstrings of :class:`tools.classproperty` methods (:phab:`T380628`) +* Site property :attr:`BaseSite.codes<pywikibot.site._basesite.BaseSite.codes>` was added (:phab:`T380606`) +* Increase *leeway* parameter of :meth:`login.OauthLoginManager.identity` (:phab:`T380270`) +* Show a warning if *ignore_extension* parameter of :class:`pywikibot.FilePage` was set and the extension is invalid +* Remove old code of Python 3.2 or older in :func:`tools.chars.replace_invisible` due to :pep:`393` +* use :meth:`BasePage.autoFormat()<page.BasePage.autoFormat>` instead of :func:`date.getAutoFormat` in + :mod:`titletranslate` +* Upcast :class:`pywikibot.Page` to :class:`pywikibot.FilePage` in :meth:`PageGenerator.result() + <data.api.PageGenerator.result>` if ``imageinfo`` is given (:phab:`T379513`) +* Update oauth requirements +* i18n-updates * Implement param *with_sort_key* in :meth:`page.BasePage.categories` (:phab:`T75561`) * Python 3.7 support will be discontinued and probably this is the last version supporting it * Add :meth:`page.BasePage.get_revision` method @@ -12,6 +24,8 @@ Current Release Changes Current Deprecations ==================== +* 9.6.0: :meth:`BaseSite.languages()<pywikibot.site._basesite.BaseSite.languages>` will be removed in favour of + :attr:`BaseSite.codes<pywikibot.site._basesite.BaseSite.codes>` * 9.5.0: :meth:`DataSite.getPropertyType()<pywikibot.site._datasite.DataSite.getPropertyType>` will be removed in favour of :meth:`DataSite.get_property_type()<pywikibot.site._datasite.DataSite.get_property_type>` * 9.4.0: :mod:`flow` support is deprecated and will be removed (:phab:`T371180`) From 58bb6889ce415f5968d2a01e19689fd465219192 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Mon, 2 Dec 2024 17:20:20 +0100 Subject: [PATCH 92/95] doc: single line docstrings needs an additional line feed Bug: T381279 Change-Id: Ifb72012068ebdf3eefd5c0ef418854257c3b041f --- pywikibot/tools/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywikibot/tools/__init__.py b/pywikibot/tools/__init__.py index d36f52c092..ba49aa0681 100644 --- a/pywikibot/tools/__init__.py +++ b/pywikibot/tools/__init__.py @@ -192,7 +192,7 @@ def __init__(self, cls_method) -> None: else: spaces = '' - self.__doc__ += f'\n{spaces}:rtype: {rtype}' + self.__doc__ += f'\n\n{spaces}:rtype: {rtype}' def __get__(self, instance, owner): """Get the attribute of the owner class by its method.""" From 093ccad5d2d3154cbdad498086350f6473b1ba8a Mon Sep 17 00:00:00 2001 From: Xqt <info@gno.de> Date: Mon, 2 Dec 2024 19:33:15 +0000 Subject: [PATCH 93/95] Revert "tests: Skip wikiblame tests due to T381262" This reverts commit f83ab2850b218c743bc7059e352275d89da357eb. Reason for revert: solved upstream Bug: T381262 Change-Id: I74ec5d56f20521751123118133cf6db505790ff5 --- pywikibot/page/_toolforge.py | 12 ++++++------ tests/wikiblame_tests.py | 2 -- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/pywikibot/page/_toolforge.py b/pywikibot/page/_toolforge.py index cfe2f9ebe9..19a2782c67 100644 --- a/pywikibot/page/_toolforge.py +++ b/pywikibot/page/_toolforge.py @@ -74,9 +74,9 @@ def main_authors(self) -> collections.Counter[str, int]: >>> import pywikibot >>> site = pywikibot.Site('wikipedia:eu') >>> page = pywikibot.Page(site, 'Python (informatika)') - >>> auth = page.main_authors() # doctest: +SKIP - >>> auth.most_common(1) # doctest: +SKIP - [('Ksarasola', 82)] # doctest: +SKIP + >>> auth = page.main_authors() + >>> auth.most_common(1) + [('Ksarasola', 82)] .. important:: Only implemented for main namespace pages and only wikipedias of :attr:`WIKIBLAME_CODES` are supported. @@ -126,9 +126,9 @@ def authorship( >>> import pywikibot >>> site = pywikibot.Site('wikipedia:en') >>> page = pywikibot.Page(site, 'Pywikibot') - >>> auth = page.authorship() # doctest: +SKIP - >>> auth # doctest: +SKIP - {'1234qwer1234qwer4': (68, 100.0)} # doctest: +SKIP + >>> auth = page.authorship() + >>> auth + {'1234qwer1234qwer4': (68, 100.0)} .. important:: Only implemented for main namespace pages and only wikipedias of :attr:`WIKIBLAME_CODES` are supported. diff --git a/tests/wikiblame_tests.py b/tests/wikiblame_tests.py index 306cb5d1e8..0d5057caec 100644 --- a/tests/wikiblame_tests.py +++ b/tests/wikiblame_tests.py @@ -36,7 +36,6 @@ def test_exceptions(self): page.authorship() @require_modules('wikitextparser') - @unittest.expectedFailure # T381262 def test_main_authors(self): """Test main_authors() method.""" page = pywikibot.Page(self.site, 'Python (programmeertaal)') @@ -49,7 +48,6 @@ def test_main_authors(self): self.assertIsInstance(values[1], float) @require_modules('wikitextparser') - @unittest.expectedFailure # T381262 def test_restrictions(self): """Test main_authors() method with restrictions.""" page = pywikibot.Page(pywikibot.Site('wikipedia:en'), 'Python') From 7eae438e79b3036e52bfff1f38f97209cb047f1a Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Tue, 3 Dec 2024 10:29:44 +0100 Subject: [PATCH 94/95] doc: update CHANGELOG.rst Change-Id: Id75fc7ebc35c497216f31f301ac7c006cef07e69 --- scripts/CHANGELOG.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/scripts/CHANGELOG.rst b/scripts/CHANGELOG.rst index 7503323781..e39cd84ccd 100644 --- a/scripts/CHANGELOG.rst +++ b/scripts/CHANGELOG.rst @@ -4,6 +4,17 @@ Scripts Changelog 9.6.0 ----- +* i18n updates + +create_isbn_edition +^^^^^^^^^^^^^^^^^^^ + +* Use :meth:`DataSite.search()<pywikibot.site._generators.GeneratorsMixin.search>` in ``get_item_with_prop_value`` +* Use :meth:`DataSite.search_entities()<pywikibot.site._datasite.DataSite.search_entities>` method in + ``get_item_list`` function +* Call :func:`pywikibot.handle_args` first to provide help message +* Enhance searching the item number from the ISBN number, code improvements (:phab:`T314942`) + dataextend ^^^^^^^^^^ @@ -14,6 +25,11 @@ replace * Strip newlines from pairsfile lines (:phab:`T378647`) +weblinkchecker +^^^^^^^^^^^^^^ + +* Remove unused *error* parameter from ``History.log()`` method (:phab:`T380693`) + 9.5.0 ----- From 83076529347a24f304a348aa0cca9420850d6972 Mon Sep 17 00:00:00 2001 From: xqt <info@gno.de> Date: Tue, 3 Dec 2024 10:37:49 +0100 Subject: [PATCH 95/95] [9.6] Publish Pywikibot 9.6 Change-Id: I1712e9460fccb5e53377400216b67c70c3fddc7d --- pywikibot/__metadata__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywikibot/__metadata__.py b/pywikibot/__metadata__.py index 607d2da452..73d3b8f068 100644 --- a/pywikibot/__metadata__.py +++ b/pywikibot/__metadata__.py @@ -12,6 +12,6 @@ from time import strftime -__version__ = '9.6.0.dev0' +__version__ = '9.6.0' __url__ = 'https://www.mediawiki.org/wiki/Manual:Pywikibot' __copyright__ = f'2003-{strftime("%Y")}, Pywikibot team'