jenkins-bot has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/core/+/869747 )
Change subject: [IMPR] add some tests and documentation and simplify some code
......................................................................
[IMPR] add some tests and documentation and simplify some code
path detached from 1105d0c
Change-Id: I8f481428da13b7b6fa7796c965e49d2bda24bfba
---
M tests/time_tests.py
M pywikibot/time.py
2 files changed, 78 insertions(+), 13 deletions(-)
Approvals:
RoySmith: Looks good to me, but someone else must approve
Xqt: Looks good to me, approved
jenkins-bot: Verified
diff --git a/pywikibot/time.py b/pywikibot/time.py
index e96a5f7..d6e7854 100644
--- a/pywikibot/time.py
+++ b/pywikibot/time.py
@@ -11,6 +11,7 @@
import math
import re
import types
+from contextlib import suppress
from typing import Type, Union
import pywikibot
@@ -118,8 +119,8 @@
m = re.match(RE_MW, timestr)
if not m:
- msg = "time data '{timestr}' does not match MW format."
- raise ValueError(msg.format(timestr=timestr))
+ raise ValueError(
+ f'time data {timestr!r} does not match MW format.')
return cls.strptime(timestr, cls.mediawikiTSFormat)
@@ -139,8 +140,8 @@
m = re.match(RE_ISO8601, timestr)
if not m:
- msg = "time data '{timestr}' does not match ISO8601 format."
- raise ValueError(msg.format(timestr=timestr))
+ raise ValueError(
+ f'time data {timestr!r} does not match ISO8601 format.')
strpfmt = '%Y-%m-%d{sep}%H:%M:%S'.format(sep=m.group('sep'))
strpstr = timestr[:19]
@@ -205,13 +206,10 @@
]
for handler in handlers:
- try:
+ with suppress(ValueError):
return handler(timestr)
- except ValueError:
- continue
- msg = "time data '{timestr}' does not match any format."
- raise ValueError(msg.format(timestr=timestr))
+ raise ValueError(f'time data {timestr!r} does not match any format.')
def clone(self) -> 'Timestamp':
"""Clone this instance."""
@@ -251,14 +249,43 @@
return cls._from_iso8601(_ts)
@classmethod
- def fromtimestampformat(cls: Type['Timestamp'], ts: Union[str, 'Timestamp']
- ) -> 'Timestamp':
- """Convert a MediaWiki internal timestamp to a Timestamp object."""
+ def fromtimestampformat(cls: Type['Timestamp'],
+ ts: Union[str, 'Timestamp'],
+ strict: bool = False) -> 'Timestamp':
+ """Convert a MediaWiki internal timestamp to a Timestamp object.
+
+ .. versionchanged:: 3.0
+ create a Timestamp if only year, month and day are given.
+ .. versionchanged:: 8.0
+ the *strict* parameter was added which discards missing
+ element tolerance.
+
+ Example
+ -------
+
+ >>> Timestamp.fromtimestampformat('20220705082234')
+ Timestamp(2022, 7, 5, 8, 22, 34)
+ >>> Timestamp.fromtimestampformat('20220927')
+ Timestamp(2022, 9, 27, 0, 0)
+ >>> Timestamp.fromtimestampformat('20221109', strict=True)
+ Traceback (most recent call last):
+ ...
+ ValueError: time data '20221109' does not match MW format.
+
+ :param ts: the timestamp to be converted
+ :param strict: If true, do not ignore missing timestamp elements
+ for hours, minutes or seconds
+ :return: return the *Timestamp* object from given *ts*.
+ :raises ValueError: The timestamp is invalid, e.g. missing or
+ invalid timestamp component.
+ """
# If inadvertently passed a Timestamp object, use replace()
# to create a clone.
if isinstance(ts, cls):
return ts.clone()
- if len(ts) == 8: # year, month and day are given only
+
+ if len(ts) == 8 and not strict:
+ # year, month and day are given only
ts += '000000'
return cls._from_mw(ts)
diff --git a/tests/time_tests.py b/tests/time_tests.py
index ff208f5..7d4f8d8 100755
--- a/tests/time_tests.py
+++ b/tests/time_tests.py
@@ -196,6 +196,33 @@
self.assertEqual(t1, t2)
self.assertEqual(ts1, ts2)
+ tests = [
+ ('202211', None),
+ ('2022112', None),
+ ('20221127', (2022, 11, 27)),
+ ('202211271', None),
+ ('2022112712', None),
+ ('20221127123', None),
+ ('202211271234', None),
+ ('2022112712345', None),
+ ('20221127123456', (2022, 11, 27, 12, 34, 56)),
+ ]
+ for mw_ts, ts in tests:
+ with self.subTest(timestamp=mw_ts):
+ if ts is None:
+ with self.assertRaisesRegex(
+ ValueError,
+ f'time data {mw_ts!r} does not match MW format'):
+ Timestamp.fromtimestampformat(mw_ts)
+ else:
+ self.assertEqual(Timestamp.fromtimestampformat(mw_ts),
+ Timestamp(*ts))
+
+ for mw_ts, ts in tests[1:-1]:
+ with self.subTest(timestamp=mw_ts), self.assertRaisesRegex(
+ ValueError, f'time data {mw_ts!r} does not match MW'):
+ Timestamp.fromtimestampformat(mw_ts, strict=True)
+
def test_add_timedelta(self):
"""Test addin a timedelta to a Timestamp."""
t1 = Timestamp.utcnow()
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/869747
To unsubscribe, or for help writing mail filters, visit https://gerrit.wikimedia.org/r/settings
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: I8f481428da13b7b6fa7796c965e49d2bda24bfba
Gerrit-Change-Number: 869747
Gerrit-PatchSet: 2
Gerrit-Owner: Xqt <info(a)gno.de>
Gerrit-Reviewer: Mpaa <mpaa.wiki(a)gmail.com>
Gerrit-Reviewer: RoySmith <roy(a)panix.com>
Gerrit-Reviewer: Xqt <info(a)gno.de>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged
jenkins-bot has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/core/+/871893 )
Change subject: [fix] Skip invalid link titles in fixing_redirects.py
......................................................................
[fix] Skip invalid link titles in fixing_redirects.py
- If a title contains a replacement character ignore this link from processing
- Raise a more informative exception if replacement char was found in title.
Bug: T324434
Change-Id: I3c802a5790ef6239342b5f2e01a3c5cfa6d2d21d
---
M pywikibot/page/_links.py
M scripts/fixing_redirects.py
2 files changed, 18 insertions(+), 2 deletions(-)
Approvals:
Meno25: Looks good to me, approved
jenkins-bot: Verified
diff --git a/pywikibot/page/_links.py b/pywikibot/page/_links.py
index 1dd4e3f..36ba716 100644
--- a/pywikibot/page/_links.py
+++ b/pywikibot/page/_links.py
@@ -314,7 +314,7 @@
# This code was adapted from Title.php : secureAndSplit()
if '\ufffd' in t:
raise InvalidTitleError(
- '{!r} contains illegal char {!r}'.format(t, '\ufffd'))
+ fr'{t!r} contains illegal replacement char \ufffd')
# Cleanup whitespace
sep = self._source.family.title_delimiter_and_aliases[0]
diff --git a/scripts/fixing_redirects.py b/scripts/fixing_redirects.py
index 98cbc59..defde99 100755
--- a/scripts/fixing_redirects.py
+++ b/scripts/fixing_redirects.py
@@ -84,7 +84,10 @@
# Make sure that next time around we will not find this same hit.
curpos = m.start() + 1
- is_interwikilink = mysite.isInterwikiLink(m['title'])
+ try:
+ is_interwikilink = mysite.isInterwikiLink(m['title'])
+ except InvalidTitleError:
+ continue # skip invalid title
# ignore interwiki links, links in the disabled area
# and links to sections of the same page
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/871893
To unsubscribe, or for help writing mail filters, visit https://gerrit.wikimedia.org/r/settings
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: I3c802a5790ef6239342b5f2e01a3c5cfa6d2d21d
Gerrit-Change-Number: 871893
Gerrit-PatchSet: 2
Gerrit-Owner: Xqt <info(a)gno.de>
Gerrit-Reviewer: D3r1ck01 <xsavitar.wiki(a)aol.com>
Gerrit-Reviewer: Meno25 <meno25mail(a)gmail.com>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged
Xqt has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/core/+/871166 )
Change subject: [IMPR] Assert internal state on Claim modifications
......................................................................
[IMPR] Assert internal state on Claim modifications
Add an assertion to ensure some operations cannot be
performed on a qualifier or a reference (such as
adding qualifiers/references or changing the rank)
and another one to ensure some operations can only
be performed on an existing (saved) claim.
Note that in the second case the operations would
already crash. But now the error messages are more
clear.
Change-Id: If7867b67ac0af72a5be0650ece2813fe1a964077
---
M pywikibot/page/_wikibase.py
1 file changed, 45 insertions(+), 8 deletions(-)
Approvals:
Xqt: Verified; Looks good to me, approved
diff --git a/pywikibot/page/_wikibase.py b/pywikibot/page/_wikibase.py
index 9e857fc..d2af3ac 100644
--- a/pywikibot/page/_wikibase.py
+++ b/pywikibot/page/_wikibase.py
@@ -46,7 +46,7 @@
from pywikibot.page._filepage import FilePage
from pywikibot.page._page import BasePage
from pywikibot.site import DataSite, Namespace
-from pywikibot.tools import cached
+from pywikibot.tools import cached, first_upper
__all__ = (
@@ -271,7 +271,7 @@
data = {}
- # This initializes all data,
+ # This initializes all data
for key, cls in self.DATA_ATTRIBUTES.items():
value = cls.fromJSON(self._content.get(key, {}), self.repo)
setattr(self, key, value)
@@ -1416,8 +1416,8 @@
self._on_item = None # The item it's on
@property
- def on_item(self):
- """Return item this claim is attached to."""
+ def on_item(self) -> Optional[WikibaseEntity]:
+ """Return entity this claim is attached to."""
return self._on_item
@on_item.setter
@@ -1431,6 +1431,16 @@
for source in values:
source.on_item = item
+ def _assert_attached(self) -> None:
+ if self.on_item is None:
+ raise RuntimeError('The claim is not attached to an entity')
+
+ def _assert_mainsnak(self, message: str) -> None:
+ if self.isQualifier:
+ raise RuntimeError(first_upper(message.format('qualifier')))
+ if self.isReference:
+ raise RuntimeError(first_upper(message.format('reference')))
+
def __repr__(self) -> str:
"""Return the representation string."""
return '{cls_name}.fromJSON({}, {})'.format(
@@ -1664,6 +1674,7 @@
:param snaktype: The new snak type ('value', 'somevalue', or
'novalue').
"""
+ self._assert_attached()
if value:
self.setTarget(value)
@@ -1710,10 +1721,13 @@
def setRank(self, rank) -> None:
"""Set the rank of the Claim."""
+ self._assert_mainsnak('Cannot set rank on a {}')
self.rank = rank
def changeRank(self, rank, **kwargs):
"""Change the rank of the Claim and save."""
+ self._assert_mainsnak('Cannot change rank on a {}')
+ self._assert_attached()
self.rank = rank
return self.repo.save_claim(self, **kwargs)
@@ -1747,6 +1761,7 @@
:param claims: the claims to add
:type claims: list of pywikibot.Claim
"""
+ self._assert_mainsnak('Cannot add sources to a {}')
for claim in claims:
if claim.on_item is not None:
raise ValueError(
@@ -1779,6 +1794,8 @@
:param sources: the sources to remove
:type sources: list of pywikibot.Claim
"""
+ self._assert_mainsnak('Cannot remove sources from a {}')
+ self._assert_attached()
data = self.repo.removeSources(self, sources, **kwargs)
self.on_item.latest_revision_id = data['pageinfo']['lastrevid']
for source in sources:
@@ -1792,6 +1809,7 @@
:param qualifier: the qualifier to add
:type qualifier: pywikibot.page.Claim
"""
+ self._assert_mainsnak('Cannot add qualifiers to a {}')
if qualifier.on_item is not None:
raise ValueError(
'The provided Claim instance is already used in an entity')
@@ -1819,8 +1837,10 @@
Remove the qualifiers.
:param qualifiers: the qualifiers to remove
- :type qualifiers: list Claim
+ :type qualifiers: list of pywikibot.Claim
"""
+ self._assert_mainsnak('Cannot remove qualifiers from a {}')
+ self._assert_attached()
data = self.repo.remove_qualifiers(self, qualifiers, **kwargs)
self.on_item.latest_revision_id = data['pageinfo']['lastrevid']
for qualifier in qualifiers:
@@ -1879,9 +1899,7 @@
:param target: qualifier target to check presence of
:return: true if the qualifier was found, false otherwise
"""
- if self.isQualifier or self.isReference:
- raise ValueError('Qualifiers and references cannot have '
- 'qualifiers.')
+ self._assert_mainsnak('{}s cannot have qualifiers')
return any(qualifier.target_equals(target)
for qualifier in self.qualifiers.get(qualifier_id, []))
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/871166
To unsubscribe, or for help writing mail filters, visit https://gerrit.wikimedia.org/r/settings
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: If7867b67ac0af72a5be0650ece2813fe1a964077
Gerrit-Change-Number: 871166
Gerrit-PatchSet: 1
Gerrit-Owner: Matěj Suchánek <matejsuchanek97(a)gmail.com>
Gerrit-Reviewer: Xqt <info(a)gno.de>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged
jenkins-bot has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/core/+/868829 )
Change subject: [textlib] replace addOnly parameter with add_only to be consistent
......................................................................
[textlib] replace addOnly parameter with add_only to be consistent
- replace addOnly parameter with add_only in replaceLanguageLinks
and replaceCategoryLinks to be consistent with replaceCategoryInPlace
and due to PEP8
- deprecate the old variant
- update documentation
- decrease nested flow statements in replaceCategoryLinks
Change-Id: Ic9ed8b374f9a9b6af19621f824723f7184ed3804
---
M pywikibot/textlib.py
1 file changed, 68 insertions(+), 47 deletions(-)
Approvals:
Xqt: Looks good to me, approved
jenkins-bot: Verified
diff --git a/pywikibot/textlib.py b/pywikibot/textlib.py
index 8b252a8..c7d1368 100644
--- a/pywikibot/textlib.py
+++ b/pywikibot/textlib.py
@@ -28,6 +28,7 @@
from pywikibot.tools import (
ModuleDeprecationWrapper,
deprecated,
+ deprecated_args,
first_lower,
first_upper,
)
@@ -904,7 +905,7 @@
# Adding the text
text += '\n' + add
# Reputting the categories
- text = replaceCategoryLinks(text, categories_inside, site, addOnly=True)
+ text = replaceCategoryLinks(text, categories_inside, site, add_only=True)
# Adding the interwiki
return replaceLanguageLinks(text, interwiki_inside, site)
@@ -1145,19 +1146,22 @@
return removeLanguageLinks(text, site, marker)
-def replaceLanguageLinks(oldtext: str, new: dict, site=None,
- addOnly: bool = False, template: bool = False,
+@deprecated_args(addOnly='add_only') # since 8.0
+def replaceLanguageLinks(oldtext: str,
+ new: dict,
+ site: Optional['pywikibot.site.BaseSite'] = None,
+ add_only: bool = False,
+ template: bool = False,
template_subpage: bool = False) -> str:
"""Replace inter-language links in the text with a new set of links.
:param oldtext: The text that needs to be modified.
- :param new: A dict with the Site objects as keys, and Page or Link objects
- as values (i.e., just like the dict returned by getLanguageLinks
- function).
+ :param new: A dict with the Site objects as keys, and Page or Link
+ objects as values (i.e., just like the dict returned by
+ :func:`getLanguageLinks` function).
:param site: The site that the text is from.
- :type site: pywikibot.Site
- :param addOnly: If True, do not remove old language links, only add new
- ones.
+ :param add_only: If True, do not remove old language links, only add
+ new ones.
:param template: Indicates if text belongs to a template page or not.
:param template_subpage: Indicates if text belongs to a template sub-page
or not.
@@ -1171,7 +1175,7 @@
cseparator = site.family.category_text_separator
separatorstripped = separator.strip()
cseparatorstripped = cseparator.strip()
- if addOnly:
+ if add_only:
s2 = oldtext
else:
s2 = removeLanguageLinksAndSeparator(oldtext, site=site, marker=marker,
@@ -1203,7 +1207,7 @@
s2 = removeCategoryLinksAndSeparator(
s2.replace(marker, cseparatorstripped).strip(), site) \
+ separator + s
- newtext = replaceCategoryLinks(s2, cats, site=site, addOnly=True)
+ newtext = replaceCategoryLinks(s2, cats, site=site, add_only=True)
# for Wikitravel's language links position.
# (not supported by rewrite - no API)
elif site.family.name == 'wikitravel':
@@ -1480,19 +1484,21 @@
return text
-def replaceCategoryLinks(oldtext: str, new, site=None,
- addOnly: bool = False) -> str:
+@deprecated_args(addOnly='add_only') # since 8.0
+def replaceCategoryLinks(oldtext: str,
+ new: Iterable,
+ site: Optional['pywikibot.site.BaseSite'] = None,
+ add_only: bool = False) -> str:
"""
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
- which can be either the raw name or [[Category:..]].
- :type new: iterable
+ which can be either the raw name or ``[[Category:..]]``.
:param site: The site that the text is from.
- :type site: pywikibot.Site
- :param addOnly: If addOnly is True, the old category won't be deleted and
- the category(s) given will be added (and they won't replace anything).
+ :param add_only: If add_only is True, the old category won't be
+ deleted and the category(s) given will be added (and they won't
+ replace anything).
:return: The modified text.
"""
# Find a marker that is not already in the text.
@@ -1507,41 +1513,40 @@
iseparator = site.family.interwiki_text_separator
separatorstripped = separator.strip()
iseparatorstripped = iseparator.strip()
- if addOnly:
+ if add_only:
cats_removed_text = oldtext
else:
cats_removed_text = removeCategoryLinksAndSeparator(
oldtext, site=site, marker=marker, separator=separatorstripped)
new_cats = categoryFormat(new, insite=site)
- if new_cats:
- if site.code in site.family.category_attop:
- newtext = new_cats + separator + cats_removed_text
- else:
- # calculate what was after the categories links on the page
- firstafter = cats_removed_text.find(marker)
- if firstafter < 0:
- firstafter = len(cats_removed_text)
- else:
- firstafter += len(marker)
- # Is there text in the 'after' part that means we should keep it
- # after?
- if '</noinclude>' in cats_removed_text[firstafter:]:
- if separatorstripped:
- new_cats = separator + new_cats
- newtext = (cats_removed_text[:firstafter].replace(marker, '')
- + new_cats + cats_removed_text[firstafter:])
- elif site.code in site.family.categories_last:
- newtext = (cats_removed_text.replace(marker, '').strip()
- + separator + new_cats)
- else:
- interwiki = getLanguageLinks(cats_removed_text, insite=site)
- langs_removed_text = removeLanguageLinksAndSeparator(
- cats_removed_text.replace(marker, ''), site, '',
- iseparatorstripped) + separator + new_cats
- newtext = replaceLanguageLinks(
- langs_removed_text, interwiki, site, addOnly=True)
- else:
+ if not new_cats:
newtext = cats_removed_text.replace(marker, '')
+ elif site.code in site.family.category_attop:
+ newtext = new_cats + separator + cats_removed_text
+ else:
+ # calculate what was after the categories links on the page
+ firstafter = cats_removed_text.find(marker)
+ if firstafter < 0:
+ firstafter = len(cats_removed_text)
+ else:
+ firstafter += len(marker)
+ # Is there text in the 'after' part that means we should keep it
+ # after?
+ if '</noinclude>' in cats_removed_text[firstafter:]:
+ if separatorstripped:
+ new_cats = separator + new_cats
+ newtext = (cats_removed_text[:firstafter].replace(marker, '')
+ + new_cats + cats_removed_text[firstafter:])
+ elif site.code in site.family.categories_last:
+ newtext = (cats_removed_text.replace(marker, '').strip()
+ + separator + new_cats)
+ else:
+ interwiki = getLanguageLinks(cats_removed_text, insite=site)
+ langs_removed_text = removeLanguageLinksAndSeparator(
+ cats_removed_text.replace(marker, ''), site, '',
+ iseparatorstripped) + separator + new_cats
+ newtext = replaceLanguageLinks(
+ langs_removed_text, interwiki, site, add_only=True)
# special parts under categories
under_categories = []
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/868829
To unsubscribe, or for help writing mail filters, visit https://gerrit.wikimedia.org/r/settings
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: Ic9ed8b374f9a9b6af19621f824723f7184ed3804
Gerrit-Change-Number: 868829
Gerrit-PatchSet: 1
Gerrit-Owner: Xqt <info(a)gno.de>
Gerrit-Reviewer: Xqt <info(a)gno.de>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged