jenkins-bot has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/core/+/942811 )
Change subject: [IMPR] use welcome messages from WelcomeBot within CheckImagesBot
......................................................................
[IMPR] use welcome messages from WelcomeBot within CheckImagesBot
use welcome messages from WelcomeBot within CheckImagesBot to prevent
code duplication and different L10N entries.
welcome:
- move WelcomeBot.check_managed_sites method to get_welcome_text
function to be reused by checkimages.py
- use capital letters for localizing dictionaries
- rename netext dict to WELCOME
- update L10N for WELCOME from checkimages.py
checkimages.py:
- import get_welcome_text from welcome.py script
- remove EMPTY dict
- rewrite put_mex_in_talk() method to use welcome.get_welcome_text()
Change-Id: I374f125837be190a268ef090ffbdbfb19ee14a12
---
M scripts/checkimages.py
M scripts/welcome.py
2 files changed, 100 insertions(+), 90 deletions(-)
Approvals:
Meno25: Looks good to me, but someone else must approve
Xqt: Looks good to me, approved
jenkins-bot: Verified
diff --git a/scripts/checkimages.py b/scripts/checkimages.py
index 9cf45b3..a5fb81a 100755
--- a/scripts/checkimages.py
+++ b/scripts/checkimages.py
@@ -1,9 +1,8 @@
#!/usr/bin/env python3
-"""
-Script to check recently uploaded files.
+"""Script to check recently uploaded files.
-This script checks if a file description is present and if there are other
-problems in the image's description.
+This script checks if a file description is present and if there are
+other problems in the image's description.
This script will have to be configured for each language. Please submit
translations as addition to the Pywikibot framework.
@@ -68,13 +67,14 @@
* Text= This is the template that the bot will use when it will report the
image's problem.
-Todo
-----
-* Clean the code, some passages are pretty difficult to understand.
-* Add the "catch the language" function for commons.
-* Fix and reorganise the new documentation
-* Add a report for the image tagged.
+.. todo::
+ * Clean the code, some passages are pretty difficult to understand.
+ * Add the "catch the language" function for commons.
+ * Fix and reorganise the new documentation
+ * Add a report for the image tagged.
+.. versionchanged:: 8.4
+ Welcome messages are imported from :mod:`scripts.welcome` script.
"""
#
# (C) Pywikibot team, 2006-2023
@@ -107,6 +107,7 @@
from pywikibot.family import Family
from pywikibot.site import Namespace
+from scripts.welcome import get_welcome_text
###############################################################################
# <--------------------------- Change only below! --------------------------->#
@@ -172,32 +173,6 @@
'zh': ['{{no source', '{{unknown', '{{No license'],
}
-# When the bot find that the usertalk is empty is not pretty to put only the
-# no source without the welcome, isn't it?
-EMPTY = {
- 'commons': '{{subst:welcome}}\n~~~~\n',
- 'meta': '{{subst:Welcome}}\n~~~~\n',
- 'ar': '{{subst:أهلا ومرحبا}}\n~~~~\n',
- 'arz': '{{subst:اهلا و سهلا}}\n~~~~\n',
- 'de': '{{subst:willkommen}} ~~~~',
- 'en': '{{subst:welcome}}\n~~~~\n',
- 'fa': '{{subst:خوشامدید|%s}}',
- 'fr': '{{Bienvenue nouveau\n~~~~\n',
- 'ga': '{{subst:Fáilte}} - ~~~~\n',
- 'hr': '{{subst:dd}}--~~~~\n',
- 'hu': '{{subst:Üdvözlet|~~~~}}\n',
- 'it': '<!-- inizio template di benvenuto -->\n{{subst:Benvebot}}\n~~~~\n'
- '<!-- fine template di benvenuto -->',
- 'ja': '{{subst:Welcome/intro}}\n{{subst:welcome|--~~~~}}\n',
- 'ko': '{{환영}}--~~~~\n',
- 'ru': '{{subst:Приветствие}}\n~~~~\n',
- 'sd': '{{ڀليڪار}}\n~~~~\n',
- 'sr': '{{dd}}--~~~~\n',
- 'ta': '{{welcome}}\n~~~~\n',
- 'ur': '{{خوش آمدید}}\n~~~~\n',
- 'zh': '{{subst:welcome|sign=~~~~}}',
-}
-
# if the file has an unknown extension it will be tagged with this template.
# In reality, there aren't unknown extension, they are only not allowed...
DELETE_IMMEDIATELY = {
@@ -672,9 +647,11 @@
return True
def put_mex_in_talk(self) -> None:
- """Function to put the warning in talk page of the uploader."""
- commento2 = i18n.twtranslate(self.site.lang,
- 'checkimages-source-notice-comment')
+ """Function to put the warning in talk page of the uploader.
+
+ When the bot find that the usertalk is empty it adds the welcome
+ message first. The messages are imported from welcome.py script.
+ """
email_page_name = i18n.translate(self.site, EMAIL_PAGE_WITH_TEXT)
email_subj = i18n.translate(self.site, EMAIL_SUBJECT)
if self.notification2:
@@ -683,10 +660,11 @@
self.notification2 = self.notification
second_text = False
+ curr_text = None
# Getting the talk page's history, to check if there is another
# advise...
try:
- testoattuale = self.talk_page.get()
+ curr_text = self.talk_page.get()
history = list(self.talk_page.revisions(total=10))
latest_user = history[0]['user']
pywikibot.info(
@@ -699,21 +677,19 @@
except IsRedirectPageError:
pywikibot.info(
'The user talk is a redirect, trying to get the right talk...')
- try:
- self.talk_page = self.talk_page.getRedirectTarget()
- testoattuale = self.talk_page.get()
- except NoPageError:
- testoattuale = i18n.translate(self.site, EMPTY)
+ self.talk_page = self.talk_page.getRedirectTarget()
+ if self.talk_page.exists():
+ curr_text = self.talk_page.get()
except NoPageError:
pywikibot.info('The user page is blank')
- testoattuale = i18n.translate(self.site, EMPTY)
- if self.comm_talk:
- commentox = self.comm_talk
- else:
- commentox = commento2
+ if curr_text is None:
+ try:
+ curr_text = get_welcome_text(self.site) % '~~~~'
+ except KeyError:
+ curr_text = ''
- new_text = f'{testoattuale}\n\n'
+ new_text = f'{curr_text}\n\n'
if second_text:
new_text += f'{self.notification2}'
else:
@@ -725,8 +701,10 @@
pywikibot.info('Maximum notifications reached, skip.')
return
+ summary = self.comm_talk or i18n.twtranslate(
+ self.site.lang, 'checkimages-source-notice-comment')
try:
- self.talk_page.put(new_text, summary=commentox, minor=False)
+ self.talk_page.put(new_text, summary=summary, minor=False)
except PageSaveRelatedError as e:
if not self.ignore_save_related_errors:
raise
diff --git a/scripts/welcome.py b/scripts/welcome.py
index 917d5d7..80a6a67 100755
--- a/scripts/welcome.py
+++ b/scripts/welcome.py
@@ -185,8 +185,8 @@
# been eliminated.
# FIXME: Not all language/project combinations have been defined yet.
# Add the following strings to customise for a language:
-# logbook, netext, report_page, bad_pag, report_text, random_sign,
-# whitelist_pg, final_new_text_additions, logpage_header if
+# LOGBOOK, WELCOME, REPORT_PAGE, BAD_PAGE, REPORT_TEXT, RANDOM_SIGN,
+# WHITELIST, FINAL_NEW_TEXT_ADDITIONS, LOGPAGE_HEADER if
# different from Wikipedia entry
############################################################################
@@ -194,7 +194,7 @@
# The page where the bot will save the log (e.g. Wikipedia:Welcome log).
#
# ATTENTION: Projects not listed won't write a log to the wiki.
-logbook = {
+LOGBOOK = {
'ar': 'Project:سجل الترحيب',
'fr': ('Wikipedia:Prise de décision/'
'Accueil automatique des nouveaux par un robot/log'),
@@ -211,8 +211,9 @@
# The text for the welcome message (e.g. {{welcome}}) and %s at the end
# that is your signature (the bot has a random parameter to add different
# sign, so in this way it will change according to your parameters).
-netext = {
+WELCOME = {
'commons': '{{subst:welcome}} %s',
+ 'meta': '{{subst:Welcome}} %s',
'wikipedia': {
'am': '{{subst:Welcome}} %s',
'ar': '{{subst:أهلا ومرحبا}} %s',
@@ -223,19 +224,23 @@
'bn': '{{subst:স্বাগতম/বট}} %s',
'bs': '{{Dobrodošlica}} %s',
'da': '{{velkommen|%s}}',
+ 'de': '{{subst:willkommen}} %s',
'en': '{{subst:welcome}} %s',
- 'fa': '{{جا:خوشامد}} %s',
- 'fr': '{{subst:Discussion Projet:Aide/Bienvenue}} %s',
+ 'fa': '{{subst:خوشامدید|%s}}',
+ 'fr': '{{Bienvenue nouveau}} %s',
'ga': '{{subst:fáilte}} %s',
'gom': '{{subst:welcome}} %s',
'gor': '{{subst:Welcome}} %s',
'he': '{{ס:ברוך הבא}} %s',
'hr': '{{subst:dd}} %s',
+ 'hu': '{{subst:Üdvözlet|%s}}\n',
'id': '{{subst:sdbot2}}\n%s',
- 'it': '<!-- inizio template di benvenuto -->\n{{subst:Benvebot}}\n%s',
+ 'it': '<!-- inizio template di benvenuto -->\n{{subst:Benvebot}}\n%s'
+ '<!-- fine template di benvenuto -->',
'ja': '{{subst:Welcome/intro}}\n{{subst:welcome|%s}}',
'ka': '{{ახალი მომხმარებელი}}--%s',
'kn': '{{subst:ಸುಸ್ವಾಗತ}} %s',
+ 'ko': '{{환영}}--%s\n',
'ml': '{{ബദൽ:സ്വാഗതം/bot}} %s',
'my': '{{subst:welcome}} %s',
'nap': '{{Bemmenuto}}%s',
@@ -244,11 +249,12 @@
'pdc': '{{subst:Wilkum}}%s',
'pt': '{{subst:bem vindo}} %s',
'roa-tara': '{{Bovègne}} %s',
- 'ru': '{{Hello}} %s',
- 'sd': '{{subst:ڀليڪار}} %s',
+ 'ru': '{{subst:Приветствие}}\n%s\n',
+ 'sd': '{{ڀليڪار}}\n%s\n',
'shn': '{{subst:ႁပ်ႉတွၼ်ႈၽူႈၸႂ်ႉတိုဝ်း}} %s',
'sq': '{{subst:tung}} %s',
- 'sr': '{{Добродошлица}} %s',
+ 'sr': '{{dd}}--%s\n',
+ 'ta': '{{welcome}}\n%s\n',
'ur': '{{نقل:خوش آمدید}}%s',
'vec': '{{subst:Benvegnù|%s}}',
'vo': '{{benokömö}} %s',
@@ -292,7 +298,7 @@
},
}
# The page where the bot will report users with a possibly bad username.
-report_page = {
+REPORT_PAGE = {
'commons': ("Project:Administrators'noticeboard/User problems/Usernames"
'to be checked'),
'wikipedia': {
@@ -318,7 +324,7 @@
}
# The page where the bot reads the real-time bad words page
# (this parameter is optional).
-bad_pag = {
+BAD_PAGE = {
'commons': 'Project:Welcome log/Bad_names',
'wikipedia': {
'am': 'User:Beria/Bad_names',
@@ -341,7 +347,7 @@
# The text for reporting a possibly bad username
# e.g. *[[Talk_page:Username|Username]]).
-report_text = {
+REPORT_TEXT = {
'commons': '\n*{{user3|%s}}' + timeselected,
'wikipedia': {
'am': '\n*[[User talk:%s]]' + timeselected,
@@ -365,7 +371,7 @@
}
# Set where you load your list of signatures that the bot will load if you use
# the random argument (this parameter is optional).
-random_sign = {
+RANDOM_SIGN = {
'am': 'User:Beria/Signatures',
'ar': 'Project:سجل الترحيب/توقيعات',
'ba': 'Ҡатнашыусы:Salamat bot/Ярҙам',
@@ -385,7 +391,7 @@
}
# The page where the bot reads the real-time whitelist page.
# (this parameter is optional).
-whitelist_pg = {
+WHITELIST = {
'ar': 'Project:سجل الترحيب/قائمة بيضاء',
'en': 'User:Filnik/whitelist',
'ga': 'Project:Log fáilte/Bánliosta',
@@ -395,14 +401,14 @@
# Text after the {{welcome}} template, if you want to add something
# Default (en): nothing.
-final_new_text_additions = {
+FINAL_NEW_TEXT_ADDITIONS = {
'it': '\n<!-- fine template di benvenuto -->',
'zh': '<small>(via ~~~)</small>',
}
#
#
-logpage_header = {
+LOGPAGE_HEADER = {
'_default': '{|border="2" cellpadding="4" cellspacing="0" style="margin: '
'0.5em 0.5em 0.5em 1em; padding: 0.5em; background: #bfcda5; '
'border: 1px #b6fd2c solid; border-collapse: collapse; '
@@ -459,6 +465,20 @@
quiet = False # Users without contributions aren't displayed
+def get_welcome_text(site: pywikibot.site.BaseSite) -> str:
+ """Check that site is managed by the script and return the message.
+
+ :raises KeyError: site is not in WELCOME dict
+ """
+ msg = i18n.translate(site, WELCOME)
+ if not msg:
+ script = pywikibot.calledModuleName()
+ welcome = 'welcome.' if script != 'welcome' else ''
+ raise KeyError(f'{script}.py is not localized for site {site} in '
+ f'{welcome}WELCOME dict.')
+ return msg
+
+
class WelcomeBot(SingleSiteBot):
"""Bot to add welcome messages on User pages."""
@@ -466,26 +486,17 @@
def __init__(self, **kwargs) -> None:
"""Initializer."""
super().__init__(**kwargs)
- self.check_managed_sites()
+ self.welcome_text = get_welcome_text(self.site)
self.bname: Dict[str, str] = {}
self.welcomed_users: List[str] = []
- self.log_name = i18n.translate(self.site, logbook)
+ self.log_name = i18n.translate(self.site, LOGBOOK)
if not self.log_name:
globalvar.make_welcome_log = False
if globalvar.random_sign:
self.define_sign(True)
- def check_managed_sites(self) -> None:
- """Check that site is managed by welcome.py."""
- # Raises KeyError if site is not in netext dict.
- site_netext = i18n.translate(self.site, netext)
- if site_netext is None:
- raise KeyError(f'welcome.py is not localized for site {self.site}'
- ' in netext dict.')
- self.welcome_text = site_netext
-
def bad_name_filer(self, name, force: bool = False) -> bool:
"""Check for bad names."""
if not globalvar.filt_bad_name:
@@ -532,8 +543,7 @@
# blacklist from wikipage
badword_page = pywikibot.Page(self.site,
- i18n.translate(self.site,
- bad_pag))
+ i18n.translate(self.site, BAD_PAGE))
list_loaded = []
if badword_page.exists():
pywikibot.info(
@@ -548,7 +558,7 @@
if not hasattr(self, '_whitelist') or force:
# initialize whitelist
whitelist_default = ['emiliano']
- wtlpg = i18n.translate(self.site, whitelist_pg)
+ wtlpg = i18n.translate(self.site, WHITELIST)
list_white = []
if wtlpg:
whitelist_page = pywikibot.Page(self.site, wtlpg)
@@ -612,8 +622,7 @@
# name in queue is max, put detail to report page
pywikibot.info('Updating badname accounts to report page...')
rep_page = pywikibot.Page(self.site,
- i18n.translate(self.site,
- report_page))
+ i18n.translate(self.site, REPORT_PAGE))
if rep_page.exists():
text_get = rep_page.get()
else:
@@ -630,8 +639,7 @@
pywikibot.info(f'{username} is already in the report page.')
else:
# Adding the log.
- rep_text += i18n.translate(self.site,
- report_text) % username
+ rep_text += i18n.translate(self.site, REPORT_TEXT) % username
if self.site.code == 'it':
rep_text = f'{rep_text}{self.bname[username]}}}}}'
@@ -663,7 +671,7 @@
self.show_status()
pywikibot.info(
'Log page is not exist, getting information for page creation')
- text = i18n.translate(self.site, logpage_header,
+ text = i18n.translate(self.site, LOGPAGE_HEADER,
fallback=i18n.DEFAULT_FALLBACK)
text += '\n!' + self.site.namespace(2)
text += '\n!' + str.capitalize(
@@ -729,7 +737,7 @@
sign_text = ''
creg = re.compile(r'^\* ?(.*?)$', re.M)
if not globalvar.sign_file_name:
- sign_page_name = i18n.translate(self.site, random_sign)
+ sign_page_name = i18n.translate(self.site, RANDOM_SIGN)
if not sign_page_name:
self.show_status(Msg.WARN)
pywikibot.info(f"{self.site} doesn't allow random signature,"
@@ -817,7 +825,7 @@
elif self.site.sitename != 'wikinews:it':
welcome_text = welcome_text % globalvar.default_sign
- final_text = i18n.translate(self.site, final_new_text_additions)
+ final_text = i18n.translate(self.site, FINAL_NEW_TEXT_ADDITIONS)
if final_text:
welcome_text += final_text
welcome_comment = i18n.twtranslate(self.site, 'welcome-welcome')
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/942811
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: I374f125837be190a268ef090ffbdbfb19ee14a12
Gerrit-Change-Number: 942811
Gerrit-PatchSet: 1
Gerrit-Owner: Xqt <info(a)gno.de>
Gerrit-Reviewer: D3r1ck01 <dalangi-ctr(a)wikimedia.org>
Gerrit-Reviewer: Meno25 <meno25mail(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/+/942668 )
Change subject: [IMPR] use inline re.IGNORECASE flag in textlib.case_escape function
......................................................................
[IMPR] use inline re.IGNORECASE flag in textlib.case_escape function
- use inline re.IGNORECASE flag for the first letter of string argument
- add underscore parameter to detect interchangeable and collapsible
spaces/underscores in string
- use underscore parameter within scripts
Bug: T308265
Change-Id: I58df8260db97c45cde6e959ada7e5a8acc959d79
---
M pywikibot/textlib.py
M scripts/image.py
M scripts/delinker.py
3 files changed, 31 insertions(+), 15 deletions(-)
Approvals:
Matěj Suchánek: Looks good to me, approved
jenkins-bot: Verified
diff --git a/pywikibot/textlib.py b/pywikibot/textlib.py
index 39f8d50..b8ac31f 100644
--- a/pywikibot/textlib.py
+++ b/pywikibot/textlib.py
@@ -163,19 +163,24 @@
return phrase
-def case_escape(case: str, string: str) -> str:
+def case_escape(case: str, string: str, *, underscore: bool = False) -> str:
"""Return an escaped regex pattern which depends on 'first-letter' case.
.. versionadded:: 7.0
+ .. versionchanged:: 8.4
+ Added the optional *underscore* parameter.
- :param case: if `case` is 'first-letter' the regex contains an
- upper/lower case set for the first letter
+ :param case: if `case` is 'first-letter', the regex contains an
+ inline re.IGNORECASE flag for the first letter
+ :param underscore: if True, expand the regex to detect spaces and
+ underscores which are interchangeable and collapsible
"""
- first = string[0]
- if first.isalpha() and case == 'first-letter':
- pattern = f'[{first.upper()}{first.lower()}]{re.escape(string[1:])}'
+ if case == 'first-letter':
+ pattern = f'(?i:{string[:1]}){re.escape(string[1:])}'
else:
pattern = re.escape(string)
+ if underscore:
+ pattern = re.sub(r'_|\\ ', '[_ ]+', pattern)
return pattern
@@ -1557,9 +1562,7 @@
return oldtext
# title might contain regex special characters
- title = case_escape(site.namespaces[14].case, title)
- # spaces and underscores in page titles are interchangeable and collapsible
- title = title.replace(r'\ ', '[ _]+').replace(r'\_', '[ _]+')
+ 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)
diff --git a/scripts/delinker.py b/scripts/delinker.py
index 6282cd6..4d3d0b4 100755
--- a/scripts/delinker.py
+++ b/scripts/delinker.py
@@ -100,9 +100,9 @@
"""Set page to current page and delink that page."""
# use image_regex from image.py
namespace = file_page.site.namespaces[6]
- escaped = case_escape(namespace.case, file_page.title(with_ns=False))
- # Be careful, spaces and _ have been converted to '\ ' and '\_'
- escaped = re.sub('\\\\[_ ]', '[_ ]', escaped)
+ escaped = case_escape(namespace.case,
+ file_page.title(with_ns=False),
+ underscore=True)
self.image_regex = re.compile(
r'\[\[ *(?:{})\s*:\s*{} *(?P<parameters>\|'
r'(?:[^\[\]]|\[\[[^\]]+\]\]|\[[^\]]+\])*|) *\]\]'
diff --git a/scripts/image.py b/scripts/image.py
index 1b5d05d..167fa78 100755
--- a/scripts/image.py
+++ b/scripts/image.py
@@ -85,10 +85,8 @@
param)
namespace = self.site.namespaces[6]
- escaped = case_escape(namespace.case, self.old_image)
+ escaped = case_escape(namespace.case, self.old_image, underscore=True)
- # Be careful, spaces and _ have been converted to '\ ' and '\_'
- escaped = re.sub('\\\\[_ ]', '[_ ]', escaped)
if not self.opt.loose or not self.new_image:
image_regex = re.compile(
r'\[\[ *(?:{})\s*:\s*{} *(?P<parameters>\|'
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/942668
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: I58df8260db97c45cde6e959ada7e5a8acc959d79
Gerrit-Change-Number: 942668
Gerrit-PatchSet: 2
Gerrit-Owner: Xqt <info(a)gno.de>
Gerrit-Reviewer: D3r1ck01 <dalangi-ctr(a)wikimedia.org>
Gerrit-Reviewer: Matěj Suchánek <matejsuchanek97(a)gmail.com>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged
jenkins-bot has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/core/+/942624 )
Change subject: [IMPR] use urllib.parse.unquote() for tools.chars.url2string() function
......................................................................
[IMPR] use urllib.parse.unquote() for tools.chars.url2string() function
Simplify tools.chars.url2string() function by using
urllib.parse.unquote() instead of urllib.parse.unquote_to_bytes and
encoding/decoding strings for it.
Change-Id: I49bf4fec45f6f67ddab75f7248b8b1a9eadc6d8a
---
M pywikibot/tools/chars.py
1 file changed, 30 insertions(+), 9 deletions(-)
Approvals:
Matěj Suchánek: Looks good to me, approved
jenkins-bot: Verified
diff --git a/pywikibot/tools/chars.py b/pywikibot/tools/chars.py
index c64c84d..47bfb5a 100644
--- a/pywikibot/tools/chars.py
+++ b/pywikibot/tools/chars.py
@@ -8,7 +8,7 @@
import sys
from contextlib import suppress
from typing import Union
-from urllib.parse import unquote_to_bytes
+from urllib.parse import unquote
from pywikibot.backports import Iterable
from pywikibot.tools._unidata import _category_cf
@@ -98,10 +98,22 @@
encodings: Union[str, Iterable[str]] = 'utf-8') -> str:
"""Convert URL-encoded text to unicode using several encoding.
- Uses the first encoding that doesn't cause an error.
+ Uses the first encoding that doesn't cause an error. Raises the
+ first exception if all encodings fails.
+
+ For a single *encodings* string this function is equvalent to
+ :samp:`urllib.parse.unquote(title, encodings, errors='strict')`
+
+ .. versionchanged:: 8.4
+ Ignore *LookupError* and try other encodings.
+
+ .. seealso:: :python:`urllib.parse.unquote
+ <library/urllib.parse.html#urllib.parse.unquote>`
**Example:**
+ >>> url2string('abc%20def')
+ 'abc def'
>>> url2string('/El%20Ni%C3%B1o/')
'/El Niño/'
>>> url2string('/El%20Ni%C3%B1o/', 'ascii')
@@ -118,19 +130,15 @@
:raise LookupError: unknown encoding
"""
if isinstance(encodings, str):
- encodings = [encodings]
+ return unquote(title, encodings, errors='strict')
first_exception = None
for enc in encodings:
try:
- t = title.encode(enc)
- t = unquote_to_bytes(t)
- result = t.decode(enc)
- except UnicodeError as e:
+ return unquote(title, enc, errors='strict')
+ except (UnicodeError, LookupError) as e:
if not first_exception:
first_exception = e
- else:
- return result
# Couldn't convert, raise the first exception
raise first_exception
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/942624
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: I49bf4fec45f6f67ddab75f7248b8b1a9eadc6d8a
Gerrit-Change-Number: 942624
Gerrit-PatchSet: 4
Gerrit-Owner: Xqt <info(a)gno.de>
Gerrit-Reviewer: Matěj Suchánek <matejsuchanek97(a)gmail.com>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged
jenkins-bot has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/core/+/942603 )
Change subject: [IMPR] Convert URL-encoded characters also for links outside main namespace
......................................................................
[IMPR] Convert URL-encoded characters also for links outside main namespace
As found by T342470 the CosmeticChangesToolkit.cleanUpLinks() does not
convert URL-encoded characters outside main namespace or for interwiki
links. This patch solved this issue.
Bug: T342470
Change-Id: Ie9f8fc503df842ad45fe44eefc57449c0473cd29
---
M pywikibot/cosmetic_changes.py
1 file changed, 28 insertions(+), 12 deletions(-)
Approvals:
Meno25: Looks good to me, approved
jenkins-bot: Verified
diff --git a/pywikibot/cosmetic_changes.py b/pywikibot/cosmetic_changes.py
index ffd43a5..bf3e112 100644
--- a/pywikibot/cosmetic_changes.py
+++ b/pywikibot/cosmetic_changes.py
@@ -501,32 +501,38 @@
"""Tidy up wikilinks found in a string.
This function will:
- * Replace underscores with spaces
+ * Replace underscores with spaces
* Move leading and trailing spaces out of the wikilink and into the
surrounding text
-
* Convert URL-encoded characters into Unicode-encoded characters
-
* Move trailing characters out of the link and make the link without
using a pipe, if possible
-
* Capitalize the article title of the link, if appropriate
+ .. versionchanged:: 8.4
+ Convert URL-encoded characters if a link is an interwiki link
+ or different from main namespace.
+
:param text: string to perform the clean-up on
:return: text with tidied wikilinks
"""
# helper function which works on one link and either returns it
# unmodified, or returns a replacement.
def handleOneLink(match: Match[str]) -> str:
- titleWithSection = match['titleWithSection']
+ # Convert URL-encoded characters to str
+ titleWithSection = url2string(match['titleWithSection'],
+ encodings=self.site.encodings())
label = match['label']
trailingChars = match['linktrail']
newline = match['newline']
+ # entire link but convert URL-encoded text
+ oldlink = url2string(match.group(),
+ encodings=self.site.encodings())
is_interwiki = self.site.isInterwikiLink(titleWithSection)
if is_interwiki:
- return match.group()
+ return oldlink
# The link looks like this:
# [[page_title|link_text]]trailing_chars
@@ -538,7 +544,7 @@
except InvalidTitleError:
in_main_namespace = False
if not in_main_namespace:
- return match.group()
+ return oldlink
# Replace underlines by spaces, also multiple underlines
titleWithSection = re.sub('_+', ' ', titleWithSection)
@@ -560,13 +566,9 @@
titleWithSection = titleWithSection.rstrip()
hadTrailingSpaces = len(titleWithSection) != titleLength
- # Convert URL-encoded characters to str
- titleWithSection = url2string(titleWithSection,
- encodings=self.site.encodings())
-
if not titleWithSection:
# just skip empty links.
- return match.group()
+ return match.groups()
# Remove unnecessary initial and final spaces from label.
# Please note that some editors prefer spaces around pipes.
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/942603
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: Ie9f8fc503df842ad45fe44eefc57449c0473cd29
Gerrit-Change-Number: 942603
Gerrit-PatchSet: 1
Gerrit-Owner: Xqt <info(a)gno.de>
Gerrit-Reviewer: Meno25 <meno25mail(a)gmail.com>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged
jenkins-bot has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/i18n/+/942454 )
Change subject: [i18n] Additional translations for checkimages
......................................................................
[i18n] Additional translations for checkimages
Change-Id: Ib92dd114119efbb3f8dcac5abed6d125ddda840f
---
M checkimages/en.json
M checkimages/qqq.json
2 files changed, 13 insertions(+), 0 deletions(-)
Approvals:
Xqt: Looks good to me, approved
jenkins-bot: Verified
diff --git a/checkimages/en.json b/checkimages/en.json
index 421142b..3431526 100644
--- a/checkimages/en.json
+++ b/checkimages/en.json
@@ -11,6 +11,8 @@
"checkimages-doubles-file-comment": "Bot: File already on Commons, may be deleted",
"checkimages-doubles-head": "Duplicate file",
"checkimages-doubles-talk-comment": "Bot: Notify that the file already exists on Commons",
+ "checkimages-forced-mode": "('''forced mode''')",
+ "checkimages-has-duplicates": "has the following duplicates%(force)s:",
"checkimages-log-comment": "Bot: Updating the log",
"checkimages-no-license-head": "Image without license",
"checkimages-source-tag-comment": "Bot: Marking newly uploaded untagged file",
diff --git a/checkimages/qqq.json b/checkimages/qqq.json
index 33e9e57..d324fca 100644
--- a/checkimages/qqq.json
+++ b/checkimages/qqq.json
@@ -10,6 +10,8 @@
"checkimages-doubles-file-comment": "Edit summary used by the bot while it reports a problem in the file page",
"checkimages-doubles-head": "Head of the report given to the uploader",
"checkimages-doubles-talk-comment": "Edit summary used by the bot while it reports the problem in the uploader's talk page",
+ "checkimages-forced-mode": "Report is generated in force mode",
+ "checkimages-has-duplicates": "Report that an image has several duplicates",
"checkimages-log-comment": "Edit summary for the checkimages' report",
"checkimages-no-license-head": "The header of a report if an image has no license",
"checkimages-source-tag-comment": "Edit summary for untagged user talk notice",
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/i18n/+/942454
To unsubscribe, or for help writing mail filters, visit https://gerrit.wikimedia.org/r/settings
Gerrit-Project: pywikibot/i18n
Gerrit-Branch: master
Gerrit-Change-Id: Ib92dd114119efbb3f8dcac5abed6d125ddda840f
Gerrit-Change-Number: 942454
Gerrit-PatchSet: 1
Gerrit-Owner: Xqt <info(a)gno.de>
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/+/938812 )
Change subject: [bugfix] Add 'yue' code_aliases to wikipedia_family.py
......................................................................
[bugfix] Add 'yue' code_aliases to wikipedia_family.py
- call a __post_init__() class method in Family.__new__ class if present
- raise a RuntimeError if the __post_init__() method is not a class method
- update deprecation message when using Family.__init__()
- add a __post_init__() class method to wikipedia_family.py and
wiktionary_family.py to add the 'yue'/'zh-yue' alias to cls.code_aliases
- add __init__() and __post_init__ descriptions to documentation
Bug: T341960
Change-Id: Ie8dbb0b5f3c9a40cd1f3bf61e64f7e8f1bbf1076
---
M docs/api_ref/family.rst
M pywikibot/family.py
M pywikibot/families/wiktionary_family.py
M pywikibot/families/wikipedia_family.py
4 files changed, 97 insertions(+), 4 deletions(-)
Approvals:
Xqt: Looks good to me, approved
jenkins-bot: Verified
diff --git a/docs/api_ref/family.rst b/docs/api_ref/family.rst
index 042b337..3a61f22 100644
--- a/docs/api_ref/family.rst
+++ b/docs/api_ref/family.rst
@@ -4,3 +4,48 @@
.. automodule:: family
:synopsis: Objects representing MediaWiki families
+
+ .. autoclass:: Family
+
+ .. method:: __init__()
+
+ Initializer
+
+ .. deprecated:: 3.0.20180710
+ Use :meth:`__post_init__` instead.
+ .. versionchanged:: 8.3
+ A FutureWarning is printed instead of a ``NotImplementedWarning``.
+ The deprecation may be removed in a future release and a
+ ``RuntimeError`` will be thrown instead.
+
+ .. method:: __post_init__()
+ :classmethod:
+
+ Post-init processing for Family class.
+
+ The allocator will call this class method after the Family class was
+ created and no :meth:`__init__()` method is used and ``__post_init__()``
+ is defined in your Family subclass. This can be used for example to
+ expand Family attribute lists.
+
+ .. warning:: The ``__post_init__()`` classmethod cannot be inherited
+ from a superclass. The current family file class is considered
+ only.
+
+ .. caution:: Never modify the current attributes directly; always use
+ a copy. Otherwise the base class is modified which leads to
+ unwanted side-effects.
+
+ **Example:**
+
+ .. code-block:: Python
+
+ @classmethod
+ def __post_init__(cls):
+ """Add 'yue' code alias."""
+ aliases = cls.code_aliases.copy()
+ aliases['yue'] = 'zh-yue'
+ cls.code_aliases = aliases
+
+ .. versionadded:: 8.3
+
diff --git a/pywikibot/families/wikipedia_family.py b/pywikibot/families/wikipedia_family.py
index a972c83..e7a542c 100644
--- a/pywikibot/families/wikipedia_family.py
+++ b/pywikibot/families/wikipedia_family.py
@@ -216,6 +216,16 @@
'de': ('Archiv',),
}
+ @classmethod
+ def __post_init__(cls):
+ """Add 'yue' code alias due to :phab:`T341960`.
+
+ .. versionadded:: 8.3
+ """
+ aliases = cls.code_aliases.copy()
+ aliases['yue'] = 'zh-yue'
+ cls.code_aliases = aliases
+
def encodings(self, code):
"""Return a list of historical encodings for a specific site."""
# Historic compatibility
diff --git a/pywikibot/families/wiktionary_family.py b/pywikibot/families/wiktionary_family.py
index eb92bc2..5478747 100644
--- a/pywikibot/families/wiktionary_family.py
+++ b/pywikibot/families/wiktionary_family.py
@@ -87,3 +87,13 @@
'ar': ('/شرح', '/doc'),
'sr': ('/док', ),
}
+
+ @classmethod
+ def __post_init__(cls):
+ """Add 'zh-yue' code alias due to :phab:`T341960`.
+
+ .. versionadded:: 8.3
+ """
+ aliases = cls.code_aliases.copy()
+ aliases['zh-yue'] = 'yue'
+ cls.code_aliases = aliases
diff --git a/pywikibot/family.py b/pywikibot/family.py
index 6a1e26e..9660c83 100644
--- a/pywikibot/family.py
+++ b/pywikibot/family.py
@@ -5,6 +5,7 @@
# Distributed under the terms of the MIT license.
#
import collections
+import inspect
import logging
import string
import sys
@@ -13,6 +14,7 @@
import warnings
from importlib import import_module
from itertools import chain
+from textwrap import fill
from os.path import basename, dirname, splitext
from typing import Optional
@@ -55,9 +57,8 @@
"""Allocator."""
# any Family class defined in this file are abstract
if cls in globals().values():
- raise TypeError(
- 'Abstract Family class {} cannot be instantiated; '
- 'subclass it instead'.format(cls.__name__))
+ raise TypeError(f'Abstract Family class {cls.__name__} cannot be'
+ ' instantiated; subclass it instead')
# Override classproperty
cls.instance = super().__new__(cls)
@@ -67,13 +68,23 @@
if '__init__' in cls.__dict__:
# Initializer deprecated. Families should be immutable and any
# instance / class modification should go to allocator (__new__).
- cls.__init__ = deprecated(cls.__init__)
+ cls.__init__ = deprecated(instead='__post_init__() classmethod',
+ since='3.0.20180710')(cls.__init__)
# Invoke initializer immediately and make initializer no-op.
# This is to avoid repeated initializer invocation on repeated
# invocations of the metaclass's __call__.
cls.instance.__init__()
cls.__init__ = lambda self: None # no-op
+ elif '__post_init__' not in cls.__dict__:
+ pass
+ elif inspect.ismethod(cls.__post_init__): # classmethod check
+ cls.__post_init__()
+ else:
+ raise RuntimeError(fill(
+ f'__post_init__() method of {cls.__module__}.{cls.__name__}'
+ ' class or its superclass must be a classmethod. Please check'
+ ' your family file.', width=66))
return cls.instance
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/938812
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: Ie8dbb0b5f3c9a40cd1f3bf61e64f7e8f1bbf1076
Gerrit-Change-Number: 938812
Gerrit-PatchSet: 6
Gerrit-Owner: Xqt <info(a)gno.de>
Gerrit-Reviewer: Meno25 <meno25mail(a)gmail.com>
Gerrit-Reviewer: Xqt <info(a)gno.de>
Gerrit-Reviewer: Zhuyifei1999 <zhuyifei1999(a)gmail.com>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged
jenkins-bot has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/i18n/+/940107 )
Change subject: Localisation updates from https://translatewiki.net.
......................................................................
Localisation updates from https://translatewiki.net.
Change-Id: Ia10f173ee03a8b4638b00a917e15449ef7f25d1f
---
M category/gl.json
M unusedfiles/gl.json
M pywikibot/gl.json
M replicate_wiki/sr.json
A replicate_wiki/gl.json
M unprotect/gl.json
M redirect/gl.json
7 files changed, 37 insertions(+), 11 deletions(-)
Approvals:
L10n-bot: Looks good to me, approved
jenkins-bot: Verified
diff --git a/category/gl.json b/category/gl.json
index f0eecd8..a2ce1d7 100644
--- a/category/gl.json
+++ b/category/gl.json
@@ -8,7 +8,7 @@
},
"category-adding": "Bot: Engado a categoría \"[[:Category:%(newcat)s|%(newcat)s]]\"",
"category-also-in": "(tamén en %(alsocat)s)",
- "category-clean": "Bot: Elimino a categoría %(category)s que xa está en %(child)s",
+ "category-clean": "Bot: Elimino a categoría %(category)s porque xa está dentro de %(child)s",
"category-listifying": "Bot: Listando a partir de %(fromcat)s ({{PLURAL:%(num)d|1 entrada|%(num)d entradas}})",
"category-removing": "Bot: Elimino desde \"%(oldcat)s\"",
"category-renamed": "Bot: Traslado desde \"%(oldcat)s\". Autores: %(authors)s",
diff --git a/pywikibot/gl.json b/pywikibot/gl.json
index 37dcf55..c44a59d 100644
--- a/pywikibot/gl.json
+++ b/pywikibot/gl.json
@@ -7,7 +7,7 @@
]
},
"pywikibot-bot-prefix": "Robot:",
- "pywikibot-cosmetic-changes": "; cambios estética",
+ "pywikibot-cosmetic-changes": "; cambios estéticos",
"pywikibot-enter-category-name": "Insira o nome da categoría:",
"pywikibot-enter-file-links-processing": "As ligazóns a que páxina de ficheiro se deben procesar?",
"pywikibot-enter-finished-browser": "Prema na tecla \"Intro\" cando remate no navegador.",
diff --git a/redirect/gl.json b/redirect/gl.json
index 16b1f7f..13dfeb1 100644
--- a/redirect/gl.json
+++ b/redirect/gl.json
@@ -9,6 +9,6 @@
"redirect-fix-broken-moved": "Arranxo a redirección rota cara á páxina de destino trasladada \"%(to)s\"",
"redirect-fix-double": "Arranxo a redirección dobre cara a \"%(to)s\"",
"redirect-fix-loop": "Arranxo a redirección en bucle cara a \"%(to)s\"",
- "redirect-remove-broken": "Redirección cara a unha páxina eliminada ou en branco",
+ "redirect-remove-broken": "Redirección cara a unha páxina eliminada ou que non existe",
"redirect-remove-loop": "O destino da redirección crea un bucle"
}
diff --git a/replicate_wiki/gl.json b/replicate_wiki/gl.json
new file mode 100644
index 0000000..40997a1
--- /dev/null
+++ b/replicate_wiki/gl.json
@@ -0,0 +1,12 @@
+{
+ "@metadata": {
+ "authors": [
+ "Toliño"
+ ]
+ },
+ "replicate_wiki-headline": "Páxinas que difiren da orixinal",
+ "replicate_wiki-missing-users": "Administradores do orixinal que faltan aquí",
+ "replicate_wiki-same-pages": "Todas as páxinas importantes son iguais",
+ "replicate_wiki-same-users": "Todos os usuarios do orixinal tamén están presentes neste wiki",
+ "replicate_wiki-summary": "Bot: Sincronización wiki desde %(source)s"
+}
diff --git a/replicate_wiki/sr.json b/replicate_wiki/sr.json
index 98a5d14..9c610ec 100644
--- a/replicate_wiki/sr.json
+++ b/replicate_wiki/sr.json
@@ -4,6 +4,9 @@
"Milicevic01"
]
},
+ "replicate_wiki-headline": "Странице које се разликују од оригинала",
+ "replicate_wiki-missing-users": "Админи из оригинала којих нема овде",
"replicate_wiki-same-pages": "Све важне странице су исте",
+ "replicate_wiki-same-users": "Сви корисници из оригинала су такође присутни на овом викију",
"replicate_wiki-summary": "Бот: усклађено са %(source)s."
}
diff --git a/unprotect/gl.json b/unprotect/gl.json
index 986bc4d..e244a89 100644
--- a/unprotect/gl.json
+++ b/unprotect/gl.json
@@ -1,12 +1,13 @@
{
"@metadata": {
"authors": [
- "Elisardojm"
+ "Elisardojm",
+ "Toliño"
]
},
- "unprotect-category": "Bot: desprotección de tódalas páxinas da categoría %(cat)s",
- "unprotect-images": "Bot: desprotección de tódalas imaxes da páxina %(page)s",
- "unprotect-links": "Bot: desprotección de tódalas páxinas ligadas desde %(page)s",
- "unprotect-ref": "Bot: desprotección de tódalas páxinas que ligan cara a %(page)s",
- "unprotect-simple": "Bot: desprotección dunha lista de ficheiros"
+ "unprotect-category": "Bot: Desprotexo tódalas páxinas da categoría %(cat)s",
+ "unprotect-images": "Bot: Desprotexo tódalas imaxes da páxina %(page)s",
+ "unprotect-links": "Bot: Desprotexo tódalas páxinas ligadas desde %(page)s",
+ "unprotect-ref": "Bot: Desprotexo tódalas páxinas que ligan cara a %(page)s",
+ "unprotect-simple": "Bot: Desprotexo unha lista de ficheiros"
}
diff --git a/unusedfiles/gl.json b/unusedfiles/gl.json
index 6867cd9..f41fc2e 100644
--- a/unusedfiles/gl.json
+++ b/unusedfiles/gl.json
@@ -1,8 +1,9 @@
{
"@metadata": {
"authors": [
- "Elisardojm"
+ "Elisardojm",
+ "Toliño"
]
},
- "unusedfiles-comment": "Bot: marcando o ficheiro como orfo"
+ "unusedfiles-comment": "Bot: Marco o ficheiro como orfo"
}
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/i18n/+/940107
To unsubscribe, or for help writing mail filters, visit https://gerrit.wikimedia.org/r/settings
Gerrit-Project: pywikibot/i18n
Gerrit-Branch: master
Gerrit-Change-Id: Ia10f173ee03a8b4638b00a917e15449ef7f25d1f
Gerrit-Change-Number: 940107
Gerrit-PatchSet: 1
Gerrit-Owner: L10n-bot <l10n-bot(a)translatewiki.net>
Gerrit-Reviewer: L10n-bot <l10n-bot(a)translatewiki.net>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged
Xqt has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/core/+/938434 )
Change subject: [IMPR] Move wbtypes to _wbtypes.py (part 3)
......................................................................
[IMPR] Move wbtypes to _wbtypes.py (part 3)
Change-Id: If932fc115b6699de01acd56ed015349ee98c6fbe
---
M pywikibot/__init__.py
M pywikibot/_wbtypes.py
M tox.ini
D pywikibot/__wbtypes.py
4 files changed, 174 insertions(+), 1,647 deletions(-)
Approvals:
Xqt: Verified; Looks good to me, approved
diff --git a/pywikibot/__init__.py b/pywikibot/__init__.py
index f8e3564..3e14eb0 100644
--- a/pywikibot/__init__.py
+++ b/pywikibot/__init__.py
@@ -6,15 +6,13 @@
#
import atexit
import datetime
-import math
import re
import sys
import threading
from contextlib import suppress
-from decimal import Decimal
from queue import Queue
from time import sleep as time_sleep
-from typing import Any, Optional, Type, Union
+from typing import Any, Optional, Union
from urllib.parse import urlparse
from warnings import warn
@@ -31,8 +29,16 @@
__url__,
__version__,
)
-from pywikibot._wbtypes import WbRepresentation as _WbRepresentation
-from pywikibot.backports import ( # skipcq: PY-W2000
+from pywikibot._wbtypes import (
+ Coordinate,
+ WbGeoShape,
+ WbMonolingualText,
+ WbQuantity,
+ WbTabularData,
+ WbTime,
+ WbUnknown,
+)
+from pywikibot.backports import (
Callable,
Dict,
List,
@@ -66,13 +72,9 @@
stdout,
warning,
)
-from pywikibot.site import APISite, BaseSite, DataSite
+from pywikibot.site import APISite, BaseSite
from pywikibot.time import Timestamp
-from pywikibot.tools import normalize_username, remove_last_args
-
-
-ItemPageStrNoneType = Union[str, 'ItemPage', None]
-ToDecimalType = Union[int, float, str, 'Decimal', None]
+from pywikibot.tools import normalize_username
__all__ = (
'__copyright__', '__description__', '__download_url__', '__license__',
@@ -90,1094 +92,10 @@
)
# argvu is set by pywikibot.bot when it's imported
-
if not hasattr(sys.modules[__name__], 'argvu'):
argvu: List[str] = []
-
-class Coordinate(_WbRepresentation):
-
- """Class for handling and storing Coordinates."""
-
- _items = ('lat', 'lon', 'entity')
-
- def __init__(self, lat: float, lon: float, alt: Optional[float] = None,
- precision: Optional[float] = None,
- globe: Optional[str] = None, typ: str = '',
- name: str = '', dim: Optional[int] = None,
- site: Optional[DataSite] = None,
- globe_item: ItemPageStrNoneType = None,
- primary: bool = False) -> None:
- """
- Represent a geo coordinate.
-
- :param lat: Latitude
- :param lon: Longitude
- :param alt: Altitude
- :param precision: precision
- :param globe: Which globe the point is on
- :param typ: The type of coordinate point
- :param name: The name
- :param dim: Dimension (in meters)
- :param site: The Wikibase site
- :param globe_item: The Wikibase item for the globe, or the entity URI
- of this Wikibase item. Takes precedence over 'globe'
- if present.
- :param primary: True for a primary set of coordinates
- """
- self.lat = lat
- self.lon = lon
- self.alt = alt
- self._precision = precision
- self._entity = globe_item
- self.type = typ
- self.name = name
- self._dim = dim
- self.site = site or Site().data_repository()
- self.primary = primary
-
- if globe:
- globe = globe.lower()
- elif not globe_item:
- globe = self.site.default_globe()
- self.globe = globe
-
- @property
- def entity(self) -> str:
- """Return the entity uri of the globe."""
- if not self._entity:
- if self.globe not in self.site.globes():
- raise exceptions.CoordinateGlobeUnknownError(
- f'{self.globe} is not supported in Wikibase yet.')
- return self.site.globes()[self.globe]
-
- if isinstance(self._entity, ItemPage):
- return self._entity.concept_uri()
-
- return self._entity
-
- def toWikibase(self) -> Dict[str, Any]:
- """
- Export the data to a JSON object for the Wikibase API.
-
- FIXME: Should this be in the DataSite object?
-
- :return: Wikibase JSON
- """
- return {'latitude': self.lat,
- 'longitude': self.lon,
- 'altitude': self.alt,
- 'globe': self.entity,
- 'precision': self.precision,
- }
-
- @classmethod
- def fromWikibase(cls: Type['Coordinate'], data: Dict[str, Any],
- site: Optional[DataSite] = None) -> 'Coordinate':
- """
- Constructor to create an object from Wikibase's JSON output.
-
- :param data: Wikibase JSON
- :param site: The Wikibase site
- """
- if site is None:
- site = Site().data_repository()
-
- globe = None
-
- if data['globe']:
- globes = {entity: name for name, entity in site.globes().items()}
- globe = globes.get(data['globe'])
-
- return cls(data['latitude'], data['longitude'],
- data['altitude'], data['precision'],
- globe, site=site, globe_item=data['globe'])
-
- @property
- def precision(self) -> Optional[float]:
- """
- Return the precision of the geo coordinate.
-
- The precision is calculated if the Coordinate does not have a
- precision, and self._dim is set.
-
- When no precision and no self._dim exists, None is returned.
-
- The biggest error (in degrees) will be given by the longitudinal error;
- the same error in meters becomes larger (in degrees) further up north.
- We can thus ignore the latitudinal error.
-
- The longitudinal can be derived as follows:
-
- In small angle approximation (and thus in radians):
-
- M{Δλ ≈ Δpos / r_φ}, where r_φ is the radius of earth at the given
- latitude.
- Δλ is the error in longitude.
-
- M{r_φ = r cos φ}, where r is the radius of earth, φ the latitude
-
- Therefore::
-
- precision = math.degrees(
- self._dim/(radius*math.cos(math.radians(self.lat))))
- """
- if self._dim is None and self._precision is None:
- return None
- if self._precision is None and self._dim is not None:
- radius = 6378137 # TODO: Support other globes
- self._precision = math.degrees(
- self._dim / (radius * math.cos(math.radians(self.lat))))
- return self._precision
-
- @precision.setter
- def precision(self, value: float) -> None:
- self._precision = value
-
- def precisionToDim(self) -> Optional[int]:
- """
- Convert precision from Wikibase to GeoData's dim and return the latter.
-
- dim is calculated if the Coordinate doesn't have a dimension, and
- precision is set. When neither dim nor precision are set, ValueError
- is thrown.
-
- Carrying on from the earlier derivation of precision, since
- precision = math.degrees(dim/(radius*math.cos(math.radians(self.lat))))
- we get::
-
- dim = math.radians(
- precision)*radius*math.cos(math.radians(self.lat))
-
- But this is not valid, since it returns a float value for dim which is
- an integer. We must round it off to the nearest integer.
-
- Therefore::
-
- dim = int(round(math.radians(
- precision)*radius*math.cos(math.radians(self.lat))))
- """
- if self._dim is None and self._precision is None:
- raise ValueError('No values set for dim or precision')
- if self._dim is None and self._precision is not None:
- radius = 6378137
- self._dim = int(
- round(
- math.radians(self._precision) * radius * math.cos(
- math.radians(self.lat))
- )
- )
- return self._dim
-
- def get_globe_item(self, repo: Optional[DataSite] = None,
- lazy_load: bool = False) -> 'ItemPage':
- """
- Return the ItemPage corresponding to the globe.
-
- Note that the globe need not be in the same data repository as the
- Coordinate itself.
-
- A successful lookup is stored as an internal value to avoid the need
- for repeated lookups.
-
- :param repo: the Wikibase site for the globe, if different from that
- provided with the Coordinate.
- :param lazy_load: Do not raise NoPage if ItemPage does not exist.
- :return: pywikibot.ItemPage
- """
- if isinstance(self._entity, ItemPage):
- return self._entity
-
- repo = repo or self.site
- return ItemPage.from_entity_uri(repo, self.entity, lazy_load)
-
-
-class WbTime(_WbRepresentation):
-
- """A Wikibase time representation.
-
- Make a WbTime object from the current time:
-
- .. code-block:: python
-
- current_ts = pywikibot.Timestamp.now()
- wbtime = pywikibot.WbTime.fromTimestamp(current_ts)
-
- For converting python datetime objects to WbTime objects, see
- :class:`pywikibot.Timestamp` and :meth:`fromTimestamp`.
- """
-
- PRECISION = {'1000000000': 0,
- '100000000': 1,
- '10000000': 2,
- '1000000': 3,
- '100000': 4,
- '10000': 5,
- 'millenia': 6,
- 'century': 7,
- 'decade': 8,
- 'year': 9,
- 'month': 10,
- 'day': 11,
- 'hour': 12,
- 'minute': 13,
- 'second': 14
- }
-
- FORMATSTR = '{0:+012d}-{1:02d}-{2:02d}T{3:02d}:{4:02d}:{5:02d}Z'
-
- _items = ('year', 'month', 'day', 'hour', 'minute', 'second',
- 'precision', 'before', 'after', 'timezone', 'calendarmodel')
-
- _month_offset = {
- 1: 0,
- 2: 31, # Jan -> Feb: 31 days
- 3: 59, # Feb -> Mar: 28 days, plus 31 days in Jan -> Feb
- 4: 90, # Mar -> Apr: 31 days, plus 59 days in Jan -> Mar
- 5: 120, # Apr -> May: 30 days, plus 90 days in Jan -> Apr
- 6: 151, # May -> Jun: 31 days, plus 120 days in Jan -> May
- 7: 181, # Jun -> Jul: 30 days, plus 151 days in Jan -> Jun
- 8: 212, # Jul -> Aug: 31 days, plus 181 days in Jan -> Jul
- 9: 243, # Aug -> Sep: 31 days, plus 212 days in Jan -> Aug
- 10: 273, # Sep -> Oct: 30 days, plus 243 days in Jan -> Sep
- 11: 304, # Oct -> Nov: 31 days, plus 273 days in Jan -> Oct
- 12: 334, # Nov -> Dec: 30 days, plus 304 days in Jan -> Nov
- }
-
- def __init__(self,
- year: Optional[int] = None,
- month: Optional[int] = None,
- day: Optional[int] = None,
- hour: Optional[int] = None,
- minute: Optional[int] = None,
- second: Optional[int] = None,
- precision: Union[int, str, None] = None,
- before: int = 0,
- after: int = 0,
- timezone: int = 0,
- calendarmodel: Optional[str] = None,
- site: Optional[DataSite] = None) -> None:
- """Create a new WbTime object.
-
- The precision can be set by the Wikibase int value (0-14) or by
- a human readable string, e.g., ``hour``. If no precision is
- given, it is set according to the given time units.
-
- Timezone information is given in three different ways depending
- on the time:
-
- * Times after the implementation of UTC (1972): as an offset
- from UTC in minutes;
- * Times before the implementation of UTC: the offset of the time
- zone from universal time;
- * Before the implementation of time zones: The longitude of the
- place of the event, in the range −180° to 180°, multiplied by
- 4 to convert to minutes.
-
- .. note:: **Comparison information:** When using the greater
- than or equal to operator, or the less than or equal to
- operator, two different time objects with the same UTC time
- after factoring in timezones are considered equal. However,
- the equality operator will return false if the timezone is
- different.
-
- :param year: The year as a signed integer of between 1 and 16 digits.
- :param month: Month of the timestamp, if it exists.
- :param day: Day of the timestamp, if it exists.
- :param hour: Hour of the timestamp, if it exists.
- :param minute: Minute of the timestamp, if it exists.
- :param second: Second of the timestamp, if it exists.
- :param precision: The unit of the precision of the time.
- :param before: Number of units after the given time it could be, if
- uncertain. The unit is given by the precision.
- :param after: Number of units before the given time it could be, if
- uncertain. The unit is given by the precision.
- :param timezone: Timezone information in minutes.
- :param calendarmodel: URI identifying the calendar model.
- :param site: The Wikibase site. If not provided, retrieves the data
- repository from the default site from user-config.py.
- Only used if calendarmodel is not given.
- """
- if year is None:
- raise ValueError('no year given')
- self.precision = self.PRECISION['year']
- if month is not None:
- self.precision = self.PRECISION['month']
- else:
- month = 1
- if day is not None:
- self.precision = self.PRECISION['day']
- else:
- day = 1
- if hour is not None:
- self.precision = self.PRECISION['hour']
- else:
- hour = 0
- if minute is not None:
- self.precision = self.PRECISION['minute']
- else:
- minute = 0
- if second is not None:
- self.precision = self.PRECISION['second']
- else:
- second = 0
- self.year = year
- self.month = month
- self.day = day
- self.hour = hour
- self.minute = minute
- self.second = second
- self.after = after
- self.before = before
- self.timezone = timezone
- if calendarmodel is None:
- if site is None:
- site = Site().data_repository()
- if site is None:
- raise ValueError(f'Site {Site()} has no data repository')
- calendarmodel = site.calendarmodel()
- self.calendarmodel = calendarmodel
- # if precision is given it overwrites the autodetection above
- if precision is not None:
- if (isinstance(precision, int)
- and precision in self.PRECISION.values()):
- self.precision = precision
- elif precision in self.PRECISION:
- assert isinstance(precision, str)
- self.precision = self.PRECISION[precision]
- else:
- raise ValueError(f'Invalid precision: "{precision}"')
-
- def _getSecondsAdjusted(self) -> int:
- """Return an internal representation of the time object as seconds.
-
- The value adjusts itself for timezones. It is not compatible
- with before/after.
-
- This value should *only* be used for comparisons, and its value
- may change without warning.
-
- .. versionadded:: 8.0
-
- :return: An integer roughly representing the number of seconds
- since January 1, 0000 AD, adjusted for leap years.
- """
- # This function ignores leap seconds. Since it is not required
- # to correlate to an actual UNIX timestamp, this is acceptable.
-
- # We are always required to have a year.
- elapsed_seconds = int(self.year * 365.25 * 24 * 60 * 60)
- if self.month > 1:
- elapsed_seconds += self._month_offset[self.month] * 24 * 60 * 60
- # The greogrian calendar
- if self.calendarmodel == 'http://www.wikidata.org/entity/Q1985727':
- if (self.year % 400 == 0
- or (self.year % 4 == 0 and self.year % 100 != 0)
- and self.month > 2):
- elapsed_seconds += 24 * 60 * 60 # Leap year
- # The julian calendar
- if self.calendarmodel == 'http://www.wikidata.org/entity/Q1985786':
- if self.year % 4 == 0 and self.month > 2:
- elapsed_seconds += 24 * 60 * 60
- if self.day > 1:
- # Days start at 1, not 0.
- elapsed_seconds += (self.day - 1) * 24 * 60 * 60
- elapsed_seconds += self.hour * 60 * 60
- elapsed_seconds += self.minute * 60
- elapsed_seconds += self.second
- if self.timezone is not None:
- # See T325866
- elapsed_seconds -= self.timezone * 60
- return elapsed_seconds
-
- def __lt__(self, other: object) -> bool:
- """Compare if self is less than other.
-
- .. versionadded:: 8.0
- """
- if isinstance(other, WbTime):
- return self._getSecondsAdjusted() < other._getSecondsAdjusted()
- return NotImplemented
-
- def __le__(self, other: object) -> bool:
- """Compare if self is less equals other.
-
- .. versionadded:: 8.0
- """
- if isinstance(other, WbTime):
- return self._getSecondsAdjusted() <= other._getSecondsAdjusted()
- return NotImplemented
-
- def __gt__(self, other: object) -> bool:
- """Compare if self is greater than other.
-
- .. versionadded:: 8.0
- """
- if isinstance(other, WbTime):
- return self._getSecondsAdjusted() > other._getSecondsAdjusted()
- return NotImplemented
-
- def __ge__(self, other: object) -> bool:
- """Compare if self is greater equals other.
-
- .. versionadded:: 8.0
- """
- if isinstance(other, WbTime):
- return self._getSecondsAdjusted() >= other._getSecondsAdjusted()
- return NotImplemented
-
- @classmethod
- def fromTimestr(cls: Type['WbTime'],
- datetimestr: str,
- precision: Union[int, str] = 14,
- before: int = 0,
- after: int = 0,
- timezone: int = 0,
- calendarmodel: Optional[str] = None,
- site: Optional[DataSite] = None) -> 'WbTime':
- """Create a new WbTime object from a UTC date/time string.
-
- The timestamp differs from ISO 8601 in that:
-
- * The year is always signed and having between 1 and 16 digits;
- * The month, day and time are zero if they are unknown;
- * The Z is discarded since time zone is determined from the timezone
- param.
-
- :param datetimestr: Timestamp in a format resembling ISO 8601,
- e.g. +2013-01-01T00:00:00Z
- :param precision: The unit of the precision of the time. Defaults to
- 14 (second).
- :param before: Number of units after the given time it could be, if
- uncertain. The unit is given by the precision.
- :param after: Number of units before the given time it could be, if
- uncertain. The unit is given by the precision.
- :param timezone: Timezone information in minutes.
- :param calendarmodel: URI identifying the calendar model.
- :param site: The Wikibase site. If not provided, retrieves the data
- repository from the default site from user-config.py.
- Only used if calendarmodel is not given.
- """
- match = re.match(r'([-+]?\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)Z',
- datetimestr)
- if not match:
- raise ValueError(f"Invalid format: '{datetimestr}'")
- t = match.groups()
- return cls(int(t[0]), int(t[1]), int(t[2]),
- int(t[3]), int(t[4]), int(t[5]),
- precision, before, after, timezone, calendarmodel, site)
-
- @classmethod
- def fromTimestamp(cls: Type['WbTime'],
- timestamp: 'Timestamp',
- precision: Union[int, str] = 14,
- before: int = 0,
- after: int = 0,
- timezone: int = 0,
- calendarmodel: Optional[str] = None,
- site: Optional[DataSite] = None,
- copy_timezone: bool = False) -> 'WbTime':
- """Create a new WbTime object from a pywikibot.Timestamp.
-
- .. versionchanged:: 8.0
- Added *copy_timezone* parameter.
-
- :param timestamp: Timestamp
- :param precision: The unit of the precision of the time.
- Defaults to 14 (second).
- :param before: Number of units after the given time it could be,
- if uncertain. The unit is given by the precision.
- :param after: Number of units before the given time it could be,
- if uncertain. The unit is given by the precision.
- :param timezone: Timezone information in minutes.
- :param calendarmodel: URI identifying the calendar model.
- :param site: The Wikibase site. If not provided, retrieves the
- data repository from the default site from user-config.py.
- Only used if calendarmodel is not given.
- :param copy_timezone: Whether to copy the timezone from the
- timestamp if it has timezone information. Defaults to False
- to maintain backwards compatibility. If a timezone is given,
- timezone information is discarded.
- """
- if not timezone and timestamp.tzinfo and copy_timezone:
- timezone = int(timestamp.utcoffset().total_seconds() / 60)
- return cls.fromTimestr(timestamp.isoformat(), precision=precision,
- before=before, after=after, timezone=timezone,
- calendarmodel=calendarmodel, site=site)
-
- def normalize(self) -> 'WbTime':
- """Normalizes the WbTime object to account for precision.
-
- Normalization is needed because WbTime objects can have hidden
- values that affect naive comparisons, such as an object set to
- a precision of YEAR but containing a month and day value.
-
- This function returns a new normalized object and does not do
- any modification in place.
-
- Normalization will delete timezone information if the precision
- is less than or equal to DAY.
-
- Note: Normalized WbTime objects can only be compared to other
- normalized WbTime objects of the same precision. Normalization
- might make a WbTime object that was less than another WbTime object
- before normalization, greater than it after normalization, or vice
- versa.
- """
- year = self.year
- # This is going to get messy.
- if self.PRECISION['1000000000'] <= self.precision <= self.PRECISION['10000']: # noqa: E501
- # 1000000000 == 10^9
- power_of_10 = 10 ** (9 - self.precision)
- # Wikidata rounds the number based on the first non-decimal digit.
- # Python's round function will round -15.5 to -16, and +15.5 to +16
- # so we don't need to do anything complicated like the other
- # examples.
- year = round(year / power_of_10) * power_of_10
- elif self.precision == self.PRECISION['millenia']:
- # Similar situation with centuries
- year_float = year / 1000
- if year_float < 0:
- year = math.floor(year_float)
- else:
- year = math.ceil(year_float)
- year *= 1000
- elif self.precision == self.PRECISION['century']:
- # For century, -1301 is the same century as -1400 but not -1401.
- # Similar for 1901 and 2000 vs 2001.
- year_float = year / 100
- if year_float < 0:
- year = math.floor(year_float)
- else:
- year = math.ceil(year_float)
- year *= 100
- elif self.precision == self.PRECISION['decade']:
- # For decade, -1340 is the same decade as -1349 but not -1350.
- # Similar for 2010 and 2019 vs 2020
- year_float = year / 10
- year = math.trunc(year_float)
- year *= 10
- kwargs = {
- 'precision': self.precision,
- 'before': self.before,
- 'after': self.after,
- 'calendarmodel': self.calendarmodel,
- 'year': year
- }
- if self.precision >= self.PRECISION['month']:
- kwargs['month'] = self.month
- if self.precision >= self.PRECISION['day']:
- kwargs['day'] = self.day
- if self.precision >= self.PRECISION['hour']:
- # See T326693
- kwargs['timezone'] = self.timezone
- kwargs['hour'] = self.hour
- if self.precision >= self.PRECISION['minute']:
- kwargs['minute'] = self.minute
- if self.precision >= self.PRECISION['second']:
- kwargs['second'] = self.second
- return type(self)(**kwargs)
-
- @remove_last_args(['normalize']) # since 8.2.0
- def toTimestr(self, force_iso: bool = False) -> str:
- """Convert the data to a UTC date/time string.
-
- .. seealso:: :meth:`fromTimestr` for differences between output
- with and without *force_iso* parameter.
-
- .. versionchanged:: 8.0
- *normalize* parameter was added.
- .. versionchanged:: 8.2
- *normalize* parameter was removed due to :phab:`T340495` and
- :phab:`57755`
-
- :param force_iso: whether the output should be forced to ISO 8601
- :return: Timestamp in a format resembling ISO 8601
- """
- if force_iso:
- return Timestamp._ISO8601Format_new.format(
- self.year, max(1, self.month), max(1, self.day),
- self.hour, self.minute, self.second)
- return self.FORMATSTR.format(self.year, self.month, self.day,
- self.hour, self.minute, self.second)
-
- def toTimestamp(self, timezone_aware: bool = False) -> Timestamp:
- """
- Convert the data to a pywikibot.Timestamp.
-
- .. versionchanged:: 8.0.1
- *timezone_aware* parameter was added.
-
- :param timezone_aware: Whether the timezone should be passed to
- the Timestamp object.
- :raises ValueError: instance value cannot be represented using
- Timestamp
- """
- if self.year <= 0:
- raise ValueError('You cannot turn BC dates into a Timestamp')
- ts = Timestamp.fromISOformat(
- self.toTimestr(force_iso=True).lstrip('+'))
- if timezone_aware:
- ts = ts.replace(tzinfo=datetime.timezone(
- datetime.timedelta(minutes=self.timezone)))
- return ts
-
- @remove_last_args(['normalize']) # since 8.2.0
- def toWikibase(self) -> Dict[str, Any]:
- """Convert the data to a JSON object for the Wikibase API.
-
- .. versionchanged:: 8.0
- *normalize* parameter was added.
- .. versionchanged:: 8.2
- *normalize* parameter was removed due to :phab:`T340495` and
- :phab:`57755`
-
- :return: Wikibase JSON
- """
- json = {'time': self.toTimestr(),
- 'precision': self.precision,
- 'after': self.after,
- 'before': self.before,
- 'timezone': self.timezone,
- 'calendarmodel': self.calendarmodel
- }
- return json
-
- @classmethod
- def fromWikibase(cls: Type['WbTime'], data: Dict[str, Any],
- site: Optional[DataSite] = None) -> 'WbTime':
- """
- 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
- repository from the default site from user-config.py.
- """
- return cls.fromTimestr(data['time'], data['precision'],
- data['before'], data['after'],
- data['timezone'], data['calendarmodel'], site)
-
-
-class WbQuantity(_WbRepresentation):
-
- """A Wikibase quantity representation."""
-
- _items = ('amount', 'upperBound', 'lowerBound', 'unit')
-
- @staticmethod
- def _require_errors(site: Optional[DataSite]) -> bool:
- """
- Check if Wikibase site is so old it requires error bounds to be given.
-
- If no site item is supplied it raises a warning and returns True.
-
- :param site: The Wikibase site
- """
- if not site:
- warning(
- "WbQuantity now expects a 'site' parameter. This is needed to "
- 'ensure correct handling of error bounds.')
- return False
- return site.mw_version < '1.29.0-wmf.2'
-
- @staticmethod
- def _todecimal(value: ToDecimalType) -> Optional[Decimal]:
- """
- Convert a string to a Decimal for use in WbQuantity.
-
- None value is returned as is.
-
- :param value: decimal number to convert
- """
- if isinstance(value, Decimal):
- return value
- if value is None:
- return None
- return Decimal(str(value))
-
- @staticmethod
- def _fromdecimal(value: Optional[Decimal]) -> Optional[str]:
- """
- Convert a Decimal to a string representation suitable for WikiBase.
-
- None value is returned as is.
-
- :param value: decimal number to convert
- """
- return format(value, '+g') if value is not None else None
-
- def __init__(self, amount: ToDecimalType,
- unit: ItemPageStrNoneType = None,
- error: Union[ToDecimalType,
- Tuple[ToDecimalType, ToDecimalType]] = None,
- site: Optional[DataSite] = None) -> None:
- """
- 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
- Wikibase item.
- :param error: the uncertainty of the amount (e.g. ±1)
- :param site: The Wikibase site
- """
- if amount is None:
- raise ValueError('no amount given')
-
- self.amount = self._todecimal(amount)
- self._unit = unit
- self.site = site or Site().data_repository()
-
- # also allow entity URIs to be provided via unit parameter
- if isinstance(unit, str) \
- and not unit.startswith(('http://', 'https://')):
- raise ValueError("'unit' must be an ItemPage or entity uri.")
-
- if error is None and not self._require_errors(site):
- self.upperBound = self.lowerBound = None
- else:
- if error is None:
- upperError: Optional[Decimal] = Decimal(0)
- lowerError: Optional[Decimal] = Decimal(0)
- elif isinstance(error, tuple):
- upperError = self._todecimal(error[0])
- lowerError = self._todecimal(error[1])
- else:
- upperError = lowerError = self._todecimal(error)
-
- assert upperError is not None and lowerError is not None
- assert self.amount is not None
-
- self.upperBound = self.amount + upperError
- self.lowerBound = self.amount - lowerError
-
- @property
- def unit(self) -> str:
- """Return _unit's entity uri or '1' if _unit is None."""
- if isinstance(self._unit, ItemPage):
- return self._unit.concept_uri()
- return self._unit or '1'
-
- def get_unit_item(self, repo: Optional[DataSite] = None,
- lazy_load: bool = False) -> 'ItemPage':
- """
- Return the ItemPage corresponding to the unit.
-
- Note that the unit need not be in the same data repository as the
- WbQuantity itself.
-
- A successful lookup is stored as an internal value to avoid the need
- for repeated lookups.
-
- :param repo: the Wikibase site for the unit, if different from that
- provided with the WbQuantity.
- :param lazy_load: Do not raise NoPage if ItemPage does not exist.
- :return: pywikibot.ItemPage
- """
- if not isinstance(self._unit, str):
- return self._unit
-
- repo = repo or self.site
- self._unit = ItemPage.from_entity_uri(repo, self._unit, lazy_load)
- return self._unit
-
- def toWikibase(self) -> Dict[str, Any]:
- """
- Convert the data to a JSON object for the Wikibase API.
-
- :return: Wikibase JSON
- """
- json = {'amount': self._fromdecimal(self.amount),
- 'upperBound': self._fromdecimal(self.upperBound),
- 'lowerBound': self._fromdecimal(self.lowerBound),
- 'unit': self.unit
- }
- return json
-
- @classmethod
- def fromWikibase(cls: Type['WbQuantity'], data: Dict[str, Any],
- site: Optional[DataSite] = None) -> 'WbQuantity':
- """
- Create a WbQuantity from the JSON data given by the Wikibase API.
-
- :param data: Wikibase JSON
- :param site: The Wikibase site
- """
- amount = cls._todecimal(data['amount'])
- upperBound = cls._todecimal(data.get('upperBound'))
- lowerBound = cls._todecimal(data.get('lowerBound'))
- bounds_provided = (upperBound is not None and lowerBound is not None)
- error = None
- if bounds_provided or cls._require_errors(site):
- error = (upperBound - amount, amount - lowerBound)
- if data['unit'] == '1':
- unit = None
- else:
- unit = data['unit']
- return cls(amount, unit, error, site)
-
-
-class WbMonolingualText(_WbRepresentation):
- """A Wikibase monolingual text representation."""
-
- _items = ('text', 'language')
-
- def __init__(self, text: str, language: str) -> None:
- """
- Create a new WbMonolingualText object.
-
- :param text: text string
- :param language: language code of the string
- """
- if not text or not language:
- raise ValueError('text and language cannot be empty')
- self.text = text
- self.language = language
-
- def toWikibase(self) -> Dict[str, Any]:
- """
- Convert the data to a JSON object for the Wikibase API.
-
- :return: Wikibase JSON
- """
- json = {'text': self.text,
- 'language': self.language
- }
- return json
-
- @classmethod
- def fromWikibase(cls: Type['WbMonolingualText'], data: Dict[str, Any],
- site: Optional[DataSite] = None) -> 'WbMonolingualText':
- """
- Create a WbMonolingualText from the JSON data given by Wikibase API.
-
- :param data: Wikibase JSON
- :param site: The Wikibase site
- """
- return cls(data['text'], data['language'])
-
-
-class _WbDataPage(_WbRepresentation):
- """
- A Wikibase representation for data pages.
-
- A temporary implementation until :phab:`T162336` has been resolved.
-
- Note that this class cannot be used directly
- """
-
- _items = ('page', )
-
- @classmethod
- def _get_data_site(cls: Type['_WbDataPage'], repo_site: DataSite
- ) -> APISite:
- """
- Return the site serving as a repository for a given data type.
-
- Must be implemented in the extended class.
-
- :param repo_site: The Wikibase site
- """
- raise NotImplementedError
-
- @classmethod
- def _get_type_specifics(cls: Type['_WbDataPage'], site: DataSite
- ) -> Dict[str, Any]:
- """
- Return the specifics for a given data type.
-
- Must be implemented in the extended class.
-
- The dict should have three keys:
-
- * ending: str, required filetype-like ending in page titles.
- * label: str, describing the data type for use in error messages.
- * data_site: APISite, site serving as a repository for
- the given data type.
-
- :param site: The Wikibase site
- """
- raise NotImplementedError
-
- @staticmethod
- def _validate(page: 'Page', data_site: 'BaseSite', ending: str,
- label: str) -> None:
- """
- 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
- data type.
- :param ending: Required filetype-like ending in page titles.
- E.g. '.map'
- :param label: Label describing the data type in error messages.
- """
- if not isinstance(page, Page):
- raise ValueError(f'Page {page} must be a pywikibot.Page object '
- f'not a {type(page)}.')
-
- # validate page exists
- if not page.exists():
- raise ValueError(f'Page {page} must exist.')
-
- # validate page is on the right site, and that site supports the type
- if not data_site:
- raise ValueError(
- f'The provided site does not support {label}.')
- if page.site != data_site:
- raise ValueError(
- f'Page must be on the {label} repository site.')
-
- # validate page title fulfills hard-coded Wikibase requirement
- # pcre regexp: '/^Data:[^\\[\\]#\\\:{|}]+\.map$/u' for geo-shape
- # pcre regexp: '/^Data:[^\\[\\]#\\\:{|}]+\.tab$/u' for tabular-data
- # As we have already checked for existence the following simplified
- # 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))
-
- def __init__(self, page: 'Page', site: Optional[DataSite] = None) -> None:
- """
- Create a new _WbDataPage object.
-
- :param page: page containing the data
- :param site: The Wikibase site
- """
- site = site or page.site.data_repository()
- specifics = type(self)._get_type_specifics(site)
- _WbDataPage._validate(page, specifics['data_site'],
- specifics['ending'], specifics['label'])
- self.page = page
-
- def __hash__(self) -> int:
- """Override super.hash() as toWikibase is a string for _WbDataPage."""
- return hash(self.toWikibase())
-
- def toWikibase(self) -> str:
- """
- Convert the data to the value required by the Wikibase API.
-
- :return: title of the data page incl. namespace
- """
- return self.page.title()
-
- @classmethod
- def fromWikibase(cls: Type['_WbDataPage'], page_name: str,
- site: Optional[DataSite]) -> '_WbDataPage':
- """
- Create a _WbDataPage from the JSON data given by the Wikibase API.
-
- :param page_name: page name from Wikibase value
- :param site: The Wikibase site
- """
- # TODO: This method signature does not match our parent class (which
- # takes a dictionary argument rather than a string). We should either
- # change this method's signature or rename this method.
-
- data_site = cls._get_data_site(site)
- page = Page(data_site, page_name)
- return cls(page, site)
-
-
-class WbGeoShape(_WbDataPage):
- """A Wikibase geo-shape representation."""
-
- @classmethod
- def _get_data_site(cls: Type['WbGeoShape'], site: DataSite) -> APISite:
- """
- Return the site serving as a geo-shape repository.
-
- :param site: The Wikibase site
- """
- return site.geo_shape_repository()
-
- @classmethod
- def _get_type_specifics(cls: Type['WbGeoShape'], site: DataSite
- ) -> Dict[str, Any]:
- """
- Return the specifics for WbGeoShape.
-
- :param site: The Wikibase site
- """
- specifics = {
- 'ending': '.map',
- 'label': 'geo-shape',
- 'data_site': cls._get_data_site(site)
- }
- return specifics
-
-
-class WbTabularData(_WbDataPage):
- """A Wikibase tabular-data representation."""
-
- @classmethod
- def _get_data_site(cls: Type['WbTabularData'], site: DataSite) -> APISite:
- """
- Return the site serving as a tabular-data repository.
-
- :param site: The Wikibase site
- """
- return site.tabular_data_repository()
-
- @classmethod
- def _get_type_specifics(cls: Type['WbTabularData'], site: DataSite
- ) -> Dict[str, Any]:
- """
- Return the specifics for WbTabularData.
-
- :param site: The Wikibase site
- """
- specifics = {
- 'ending': '.tab',
- 'label': 'tabular-data',
- 'data_site': cls._get_data_site(site)
- }
- return specifics
-
-
-class WbUnknown(_WbRepresentation):
- """
- A Wikibase representation for unknown data type.
-
- This will prevent the bot from breaking completely when a new type
- is introduced.
-
- This data type is just a json container
-
- .. versionadded:: 3.0
- """
-
- _items = ('json',)
-
- def __init__(self, json: Dict[str, Any]) -> None:
- """
- Create a new WbUnknown object.
-
- :param json: Wikibase JSON
- """
- self.json = json
-
- def toWikibase(self) -> Dict[str, Any]:
- """
- Return the JSON object for the Wikibase API.
-
- :return: Wikibase JSON
- """
- return self.json
-
- @classmethod
- def fromWikibase(cls: Type['WbUnknown'], data: Dict[str, Any],
- site: Optional[DataSite] = None) -> 'WbUnknown':
- """
- Create a WbUnknown from the JSON data given by the Wikibase API.
-
- :param data: Wikibase JSON
- :param site: The Wikibase site
- """
- return cls(data)
-
+link_regex = re.compile(r'\[\[(?P<title>[^\]|[<>{}]*)(\|.*?)?\]\]')
_sites: Dict[str, APISite] = {}
@@ -1213,7 +131,7 @@
return matched_sites[0]
-def Site(code: Optional[str] = None,
+def Site(code: Optional[str] = None, # noqa: 134
fam: Union[str, 'Family', None] = None,
user: Optional[str] = None, *,
interface: Union[str, 'BaseSite', None] = None,
@@ -1362,10 +280,9 @@
)
-link_regex = re.compile(r'\[\[(?P<title>[^\]|[<>{}]*)(\|.*?)?\]\]')
-
-
-def showDiff(oldtext: str, newtext: str, context: int = 0) -> None:
+def showDiff(oldtext: str, # noqa: 134
+ newtext: str,
+ context: int = 0) -> None:
"""
Output a string showing the differences between oldtext and newtext.
@@ -1417,14 +334,14 @@
debug('_flush() called')
def remaining() -> Tuple[int, datetime.timedelta]:
- remainingPages = page_put_queue.qsize()
+ remaining_pages = page_put_queue.qsize()
if stop:
# -1 because we added a None element to stop the queue
- remainingPages -= 1
+ remaining_pages -= 1
- remainingSeconds = datetime.timedelta(
- seconds=round(remainingPages * _config.put_throttle))
- return (remainingPages, remainingSeconds)
+ remaining_seconds = datetime.timedelta(
+ seconds=round(remaining_pages * _config.put_throttle))
+ return (remaining_pages, remaining_seconds)
if stop:
# None task element leaves async_manager
diff --git a/pywikibot/__wbtypes.py b/pywikibot/__wbtypes.py
deleted file mode 100644
index f9934a2..0000000
--- a/pywikibot/__wbtypes.py
+++ /dev/null
@@ -1,64 +0,0 @@
-"""Wikibase data type classes."""
-#
-# (C) Pywikibot team, 2013-2022
-#
-# Distributed under the terms of the MIT license.
-#
-import abc
-import json
-from typing import TYPE_CHECKING, Any, Optional
-
-from pywikibot.backports import Dict
-
-
-if TYPE_CHECKING:
- from pywikibot.site import DataSite
-
-
-class WbRepresentation(abc.ABC):
-
- """Abstract class for Wikibase representations."""
-
- @abc.abstractmethod
- def __init__(self) -> None:
- """Constructor."""
- raise NotImplementedError
-
- @abc.abstractmethod
- def toWikibase(self) -> Any:
- """Convert representation to JSON for the Wikibase API."""
- raise NotImplementedError
-
- @classmethod
- @abc.abstractmethod
- def fromWikibase(
- cls,
- data: Dict[str, Any],
- site: Optional['DataSite'] = None
- ) -> 'WbRepresentation':
- """Create a representation object based on JSON from Wikibase API."""
- raise NotImplementedError
-
- def __str__(self) -> str:
- return json.dumps(self.toWikibase(), indent=4, sort_keys=True,
- separators=(',', ': '))
-
- def __repr__(self) -> str:
- assert isinstance(self._items, tuple)
- assert all(isinstance(item, str) for item in self._items)
-
- values = ((attr, getattr(self, attr)) for attr in self._items)
- attrs = ', '.join(f'{attr}={value}'
- for attr, value in values)
- return f'{self.__class__.__name__}({attrs})'
-
- def __eq__(self, other: object) -> bool:
- if isinstance(other, self.__class__):
- return self.toWikibase() == other.toWikibase()
- return NotImplemented
-
- def __hash__(self) -> int:
- return hash(frozenset(self.toWikibase().items()))
-
- def __ne__(self, other: object) -> bool:
- return not self.__eq__(other)
diff --git a/pywikibot/_wbtypes.py b/pywikibot/_wbtypes.py
index f8e3564..97826cd 100644
--- a/pywikibot/_wbtypes.py
+++ b/pywikibot/_wbtypes.py
@@ -4,98 +4,88 @@
#
# Distributed under the terms of the MIT license.
#
-import atexit
+import abc
import datetime
+import json
import math
import re
-import sys
-import threading
-from contextlib import suppress
from decimal import Decimal
-from queue import Queue
-from time import sleep as time_sleep
-from typing import Any, Optional, Type, Union
-from urllib.parse import urlparse
-from warnings import warn
+from typing import TYPE_CHECKING, Any, Optional, Type, Union
-from pywikibot import config as _config
+import pywikibot
from pywikibot import exceptions
-from pywikibot.__metadata__ import (
- __copyright__,
- __description__,
- __download_url__,
- __license__,
- __maintainer__,
- __maintainer_email__,
- __name__,
- __url__,
- __version__,
-)
-from pywikibot._wbtypes import WbRepresentation as _WbRepresentation
-from pywikibot.backports import ( # skipcq: PY-W2000
- Callable,
- Dict,
- List,
- Tuple,
- cache,
- removesuffix,
-)
-from pywikibot.bot import (
- Bot,
- CurrentPageBot,
- WikidataBot,
- calledModuleName,
- handle_args,
- input,
- input_choice,
- input_yn,
- show_help,
- ui,
-)
-from pywikibot.diff import PatchManager
-from pywikibot.family import AutoFamily, Family
-from pywikibot.i18n import translate
-from pywikibot.logging import (
- critical,
- debug,
- error,
- exception,
- info,
- log,
- output,
- stdout,
- warning,
-)
-from pywikibot.site import APISite, BaseSite, DataSite
+from pywikibot.backports import Dict, Tuple
+from pywikibot.logging import warning
from pywikibot.time import Timestamp
-from pywikibot.tools import normalize_username, remove_last_args
+from pywikibot.tools import remove_last_args
+if TYPE_CHECKING:
+ from pywikibot.site import APISite, BaseSite, DataSite
-ItemPageStrNoneType = Union[str, 'ItemPage', None]
+ItemPageStrNoneType = Union[str, 'pywikibot.ItemPage', None]
ToDecimalType = Union[int, float, str, 'Decimal', None]
__all__ = (
- '__copyright__', '__description__', '__download_url__', '__license__',
- '__maintainer__', '__maintainer_email__', '__name__', '__url__',
- '__version__',
- 'async_manager', 'async_request', 'Bot', 'calledModuleName', 'Category',
- 'Claim', 'Coordinate', 'critical', 'CurrentPageBot', 'debug', 'error',
- 'exception', 'FilePage', 'handle_args', 'html2unicode', 'info', 'input',
- 'input_choice', 'input_yn', 'ItemPage', 'LexemeForm', 'LexemePage',
- 'LexemeSense', 'Link', 'log', 'MediaInfo', 'output', 'Page',
- 'page_put_queue', 'PropertyPage', 'showDiff', 'show_help', 'Site',
- 'SiteLink', 'sleep', 'stdout', 'stopme', 'Timestamp', 'translate', 'ui',
- 'url2unicode', 'User', 'warning', 'WbGeoShape', 'WbMonolingualText',
- 'WbQuantity', 'WbTabularData', 'WbTime', 'WbUnknown', 'WikidataBot',
+ 'Coordinate',
+ 'WbGeoShape',
+ 'WbMonolingualText',
+ 'WbQuantity',
+ 'WbTabularData',
+ 'WbTime',
+ 'WbUnknown',
)
-# argvu is set by pywikibot.bot when it's imported
-if not hasattr(sys.modules[__name__], 'argvu'):
- argvu: List[str] = []
+class WbRepresentation(abc.ABC):
+
+ """Abstract class for Wikibase representations."""
+
+ @abc.abstractmethod
+ def __init__(self) -> None:
+ """Constructor."""
+ raise NotImplementedError
+
+ @abc.abstractmethod
+ def toWikibase(self) -> Any:
+ """Convert representation to JSON for the Wikibase API."""
+ raise NotImplementedError
+
+ @classmethod
+ @abc.abstractmethod
+ def fromWikibase(
+ cls,
+ data: Dict[str, Any],
+ site: Optional['DataSite'] = None
+ ) -> 'WbRepresentation':
+ """Create a representation object based on JSON from Wikibase API."""
+ raise NotImplementedError
+
+ def __str__(self) -> str:
+ return json.dumps(self.toWikibase(), indent=4, sort_keys=True,
+ separators=(',', ': '))
+
+ def __repr__(self) -> str:
+ assert isinstance(self._items, tuple)
+ assert all(isinstance(item, str) for item in self._items)
+
+ values = ((attr, getattr(self, attr)) for attr in self._items)
+ attrs = ', '.join(f'{attr}={value}'
+ for attr, value in values)
+ return f'{self.__class__.__name__}({attrs})'
+
+ def __eq__(self, other: object) -> bool:
+ if isinstance(other, self.__class__):
+ return self.toWikibase() == other.toWikibase()
+ return NotImplemented
+
+ def __hash__(self) -> int:
+ return hash(frozenset(self.toWikibase().items()))
+
+ def __ne__(self, other: object) -> bool:
+ return not self.__eq__(other)
-class Coordinate(_WbRepresentation):
+class Coordinate(WbRepresentation):
"""Class for handling and storing Coordinates."""
@@ -105,7 +95,7 @@
precision: Optional[float] = None,
globe: Optional[str] = None, typ: str = '',
name: str = '', dim: Optional[int] = None,
- site: Optional[DataSite] = None,
+ site: Optional['DataSite'] = None,
globe_item: ItemPageStrNoneType = None,
primary: bool = False) -> None:
"""
@@ -133,7 +123,7 @@
self.type = typ
self.name = name
self._dim = dim
- self.site = site or Site().data_repository()
+ self.site = site or pywikibot.Site().data_repository()
self.primary = primary
if globe:
@@ -151,7 +141,7 @@
f'{self.globe} is not supported in Wikibase yet.')
return self.site.globes()[self.globe]
- if isinstance(self._entity, ItemPage):
+ if isinstance(self._entity, pywikibot.ItemPage):
return self._entity.concept_uri()
return self._entity
@@ -173,7 +163,7 @@
@classmethod
def fromWikibase(cls: Type['Coordinate'], data: Dict[str, Any],
- site: Optional[DataSite] = None) -> 'Coordinate':
+ site: Optional['DataSite'] = None) -> 'Coordinate':
"""
Constructor to create an object from Wikibase's JSON output.
@@ -181,7 +171,7 @@
:param site: The Wikibase site
"""
if site is None:
- site = Site().data_repository()
+ site = pywikibot.Site().data_repository()
globe = None
@@ -269,8 +259,8 @@
)
return self._dim
- def get_globe_item(self, repo: Optional[DataSite] = None,
- lazy_load: bool = False) -> 'ItemPage':
+ def get_globe_item(self, repo: Optional['DataSite'] = None,
+ lazy_load: bool = False) -> 'pywikibot.ItemPage':
"""
Return the ItemPage corresponding to the globe.
@@ -285,14 +275,14 @@
:param lazy_load: Do not raise NoPage if ItemPage does not exist.
:return: pywikibot.ItemPage
"""
- if isinstance(self._entity, ItemPage):
+ if isinstance(self._entity, pywikibot.ItemPage):
return self._entity
repo = repo or self.site
- return ItemPage.from_entity_uri(repo, self.entity, lazy_load)
+ return pywikibot.ItemPage.from_entity_uri(repo, self.entity, lazy_load)
-class WbTime(_WbRepresentation):
+class WbTime(WbRepresentation):
"""A Wikibase time representation.
@@ -356,7 +346,7 @@
after: int = 0,
timezone: int = 0,
calendarmodel: Optional[str] = None,
- site: Optional[DataSite] = None) -> None:
+ site: Optional['DataSite'] = None) -> None:
"""Create a new WbTime object.
The precision can be set by the Wikibase int value (0-14) or by
@@ -432,9 +422,10 @@
self.timezone = timezone
if calendarmodel is None:
if site is None:
- site = Site().data_repository()
+ site = pywikibot.Site().data_repository()
if site is None:
- raise ValueError(f'Site {Site()} has no data repository')
+ raise ValueError(
+ f'Site {pywikibot.Site()} has no data repository')
calendarmodel = site.calendarmodel()
self.calendarmodel = calendarmodel
# if precision is given it overwrites the autodetection above
@@ -534,7 +525,7 @@
after: int = 0,
timezone: int = 0,
calendarmodel: Optional[str] = None,
- site: Optional[DataSite] = None) -> 'WbTime':
+ site: Optional['DataSite'] = None) -> 'WbTime':
"""Create a new WbTime object from a UTC date/time string.
The timestamp differs from ISO 8601 in that:
@@ -575,7 +566,7 @@
after: int = 0,
timezone: int = 0,
calendarmodel: Optional[str] = None,
- site: Optional[DataSite] = None,
+ site: Optional['DataSite'] = None,
copy_timezone: bool = False) -> 'WbTime':
"""Create a new WbTime object from a pywikibot.Timestamp.
@@ -745,7 +736,7 @@
@classmethod
def fromWikibase(cls: Type['WbTime'], data: Dict[str, Any],
- site: Optional[DataSite] = None) -> 'WbTime':
+ site: Optional['DataSite'] = None) -> 'WbTime':
"""
Create a WbTime from the JSON data given by the Wikibase API.
@@ -758,14 +749,14 @@
data['timezone'], data['calendarmodel'], site)
-class WbQuantity(_WbRepresentation):
+class WbQuantity(WbRepresentation):
"""A Wikibase quantity representation."""
_items = ('amount', 'upperBound', 'lowerBound', 'unit')
@staticmethod
- def _require_errors(site: Optional[DataSite]) -> bool:
+ def _require_errors(site: Optional['DataSite']) -> bool:
"""
Check if Wikibase site is so old it requires error bounds to be given.
@@ -810,7 +801,7 @@
unit: ItemPageStrNoneType = None,
error: Union[ToDecimalType,
Tuple[ToDecimalType, ToDecimalType]] = None,
- site: Optional[DataSite] = None) -> None:
+ site: Optional['DataSite'] = None) -> None:
"""
Create a new WbQuantity object.
@@ -825,7 +816,7 @@
self.amount = self._todecimal(amount)
self._unit = unit
- self.site = site or Site().data_repository()
+ self.site = site or pywikibot.Site().data_repository()
# also allow entity URIs to be provided via unit parameter
if isinstance(unit, str) \
@@ -836,29 +827,29 @@
self.upperBound = self.lowerBound = None
else:
if error is None:
- upperError: Optional[Decimal] = Decimal(0)
- lowerError: Optional[Decimal] = Decimal(0)
+ upper_error: Optional[Decimal] = Decimal(0)
+ lower_error: Optional[Decimal] = Decimal(0)
elif isinstance(error, tuple):
- upperError = self._todecimal(error[0])
- lowerError = self._todecimal(error[1])
+ upper_error = self._todecimal(error[0])
+ lower_error = self._todecimal(error[1])
else:
- upperError = lowerError = self._todecimal(error)
+ upper_error = lower_error = self._todecimal(error)
- assert upperError is not None and lowerError is not None
+ assert upper_error is not None and lower_error is not None
assert self.amount is not None
- self.upperBound = self.amount + upperError
- self.lowerBound = self.amount - lowerError
+ self.upperBound = self.amount + upper_error
+ self.lowerBound = self.amount - lower_error
@property
def unit(self) -> str:
"""Return _unit's entity uri or '1' if _unit is None."""
- if isinstance(self._unit, ItemPage):
+ if isinstance(self._unit, pywikibot.ItemPage):
return self._unit.concept_uri()
return self._unit or '1'
- def get_unit_item(self, repo: Optional[DataSite] = None,
- lazy_load: bool = False) -> 'ItemPage':
+ def get_unit_item(self, repo: Optional['DataSite'] = None,
+ lazy_load: bool = False) -> 'pywikibot.ItemPage':
"""
Return the ItemPage corresponding to the unit.
@@ -877,7 +868,8 @@
return self._unit
repo = repo or self.site
- self._unit = ItemPage.from_entity_uri(repo, self._unit, lazy_load)
+ self._unit = pywikibot.ItemPage.from_entity_uri(
+ repo, self._unit, lazy_load)
return self._unit
def toWikibase(self) -> Dict[str, Any]:
@@ -895,7 +887,7 @@
@classmethod
def fromWikibase(cls: Type['WbQuantity'], data: Dict[str, Any],
- site: Optional[DataSite] = None) -> 'WbQuantity':
+ site: Optional['DataSite'] = None) -> 'WbQuantity':
"""
Create a WbQuantity from the JSON data given by the Wikibase API.
@@ -903,12 +895,12 @@
:param site: The Wikibase site
"""
amount = cls._todecimal(data['amount'])
- upperBound = cls._todecimal(data.get('upperBound'))
- lowerBound = cls._todecimal(data.get('lowerBound'))
- bounds_provided = (upperBound is not None and lowerBound is not None)
+ upper_bound = cls._todecimal(data.get('upperBound'))
+ lower_bound = cls._todecimal(data.get('lowerBound'))
+ bounds_provided = (upper_bound is not None and lower_bound is not None)
error = None
if bounds_provided or cls._require_errors(site):
- error = (upperBound - amount, amount - lowerBound)
+ error = (upper_bound - amount, amount - lower_bound)
if data['unit'] == '1':
unit = None
else:
@@ -916,7 +908,7 @@
return cls(amount, unit, error, site)
-class WbMonolingualText(_WbRepresentation):
+class WbMonolingualText(WbRepresentation):
"""A Wikibase monolingual text representation."""
_items = ('text', 'language')
@@ -946,7 +938,7 @@
@classmethod
def fromWikibase(cls: Type['WbMonolingualText'], data: Dict[str, Any],
- site: Optional[DataSite] = None) -> 'WbMonolingualText':
+ site: Optional['DataSite'] = None) -> 'WbMonolingualText':
"""
Create a WbMonolingualText from the JSON data given by Wikibase API.
@@ -956,50 +948,51 @@
return cls(data['text'], data['language'])
-class _WbDataPage(_WbRepresentation):
- """
- A Wikibase representation for data pages.
+class WbDataPage(WbRepresentation):
+ """An abstract Wikibase representation for data pages.
- A temporary implementation until :phab:`T162336` has been resolved.
-
- Note that this class cannot be used directly
+ .. warning:: Perhaps a temporary implementation until :phab:`T162336`
+ has been resolved.
+ .. note:: that this class cannot be used directly.
"""
_items = ('page', )
@classmethod
- def _get_data_site(cls: Type['_WbDataPage'], repo_site: DataSite
- ) -> APISite:
+ @abc.abstractmethod
+ def _get_data_site(cls: Type['WbDataPage'],
+ repo_site: 'DataSite') -> 'APISite':
"""
Return the site serving as a repository for a given data type.
- Must be implemented in the extended class.
+ .. note:: implemented in the extended class.
:param repo_site: The Wikibase site
"""
raise NotImplementedError
@classmethod
- def _get_type_specifics(cls: Type['_WbDataPage'], site: DataSite
- ) -> Dict[str, Any]:
+ @abc.abstractmethod
+ def _get_type_specifics(cls: Type['WbDataPage'],
+ site: 'DataSite') -> Dict[str, Any]:
"""
Return the specifics for a given data type.
- Must be implemented in the extended class.
+ .. note:: Must be implemented in the extended class.
The dict should have three keys:
* ending: str, required filetype-like ending in page titles.
* label: str, describing the data type for use in error messages.
* data_site: APISite, site serving as a repository for
- the given data type.
+ the given data type.
:param site: The Wikibase site
"""
raise NotImplementedError
@staticmethod
- def _validate(page: 'Page', data_site: 'BaseSite', ending: str,
+ def _validate(page: 'pywikibot.Page', data_site: 'BaseSite', ending: str,
label: str) -> None:
"""
Validate the provided page against general and type specific rules.
@@ -1011,7 +1004,7 @@
E.g. '.map'
:param label: Label describing the data type in error messages.
"""
- if not isinstance(page, Page):
+ if not isinstance(page, pywikibot.Page):
raise ValueError(f'Page {page} must be a pywikibot.Page object '
f'not a {type(page)}.')
@@ -1038,21 +1031,22 @@
"Page must be in 'Data:' namespace and end in '{}' "
'for {}.'.format(ending, label))
- def __init__(self, page: 'Page', site: Optional[DataSite] = None) -> None:
- """
- Create a new _WbDataPage object.
+ def __init__(self,
+ page: 'pywikibot.Page',
+ site: Optional['DataSite'] = None) -> None:
+ """Create a new WbDataPage object.
:param page: page containing the data
:param site: The Wikibase site
"""
site = site or page.site.data_repository()
specifics = type(self)._get_type_specifics(site)
- _WbDataPage._validate(page, specifics['data_site'],
- specifics['ending'], specifics['label'])
+ WbDataPage._validate(page, specifics['data_site'], specifics['ending'],
+ specifics['label'])
self.page = page
def __hash__(self) -> int:
- """Override super.hash() as toWikibase is a string for _WbDataPage."""
+ """Override super.hash() as toWikibase is a string for WbDataPage."""
return hash(self.toWikibase())
def toWikibase(self) -> str:
@@ -1064,10 +1058,9 @@
return self.page.title()
@classmethod
- def fromWikibase(cls: Type['_WbDataPage'], page_name: str,
- site: Optional[DataSite]) -> '_WbDataPage':
- """
- Create a _WbDataPage from the JSON data given by the Wikibase API.
+ def fromWikibase(cls: Type['WbDataPage'], page_name: str,
+ site: Optional['DataSite']) -> 'WbDataPage':
+ """Create a WbDataPage from the JSON data given by the Wikibase API.
:param page_name: page name from Wikibase value
:param site: The Wikibase site
@@ -1077,15 +1070,15 @@
# change this method's signature or rename this method.
data_site = cls._get_data_site(site)
- page = Page(data_site, page_name)
+ page = pywikibot.Page(data_site, page_name)
return cls(page, site)
-class WbGeoShape(_WbDataPage):
+class WbGeoShape(WbDataPage):
"""A Wikibase geo-shape representation."""
@classmethod
- def _get_data_site(cls: Type['WbGeoShape'], site: DataSite) -> APISite:
+ def _get_data_site(cls: Type['WbGeoShape'], site: 'DataSite') -> 'APISite':
"""
Return the site serving as a geo-shape repository.
@@ -1094,7 +1087,7 @@
return site.geo_shape_repository()
@classmethod
- def _get_type_specifics(cls: Type['WbGeoShape'], site: DataSite
+ def _get_type_specifics(cls: Type['WbGeoShape'], site: 'DataSite'
) -> Dict[str, Any]:
"""
Return the specifics for WbGeoShape.
@@ -1109,11 +1102,12 @@
return specifics
-class WbTabularData(_WbDataPage):
+class WbTabularData(WbDataPage):
"""A Wikibase tabular-data representation."""
@classmethod
- def _get_data_site(cls: Type['WbTabularData'], site: DataSite) -> APISite:
+ def _get_data_site(cls: Type['WbTabularData'],
+ site: 'DataSite') -> 'APISite':
"""
Return the site serving as a tabular-data repository.
@@ -1122,7 +1116,7 @@
return site.tabular_data_repository()
@classmethod
- def _get_type_specifics(cls: Type['WbTabularData'], site: DataSite
+ def _get_type_specifics(cls: Type['WbTabularData'], site: 'DataSite'
) -> Dict[str, Any]:
"""
Return the specifics for WbTabularData.
@@ -1137,7 +1131,7 @@
return specifics
-class WbUnknown(_WbRepresentation):
+class WbUnknown(WbRepresentation):
"""
A Wikibase representation for unknown data type.
@@ -1169,7 +1163,7 @@
@classmethod
def fromWikibase(cls: Type['WbUnknown'], data: Dict[str, Any],
- site: Optional[DataSite] = None) -> 'WbUnknown':
+ site: Optional['DataSite'] = None) -> 'WbUnknown':
"""
Create a WbUnknown from the JSON data given by the Wikibase API.
@@ -1177,331 +1171,3 @@
:param site: The Wikibase site
"""
return cls(data)
-
-
-_sites: Dict[str, APISite] = {}
-
-
-@cache
-def _code_fam_from_url(url: str, name: Optional[str] = None
- ) -> Tuple[str, str]:
- """Set url to cache and get code and family from cache.
-
- Site helper method.
- :param url: The site URL to get code and family
- :param name: A family name used by AutoFamily
- """
- matched_sites = []
- # Iterate through all families and look, which does apply to
- # the given URL
- for fam in _config.family_files:
- family = Family.load(fam)
- code = family.from_url(url)
- if code is not None:
- matched_sites.append((code, family))
-
- if not matched_sites:
- if not name: # create a name from url
- name = urlparse(url).netloc.split('.')[-2]
- name = removesuffix(name, 'wiki')
- family = AutoFamily(name, url)
- matched_sites.append((family.code, family))
-
- if len(matched_sites) > 1:
- warning('Found multiple matches for URL "{}": {} (use first)'
- .format(url, ', '.join(str(s) for s in matched_sites)))
- return matched_sites[0]
-
-
-def Site(code: Optional[str] = None,
- fam: Union[str, 'Family', None] = None,
- user: Optional[str] = None, *,
- interface: Union[str, 'BaseSite', None] = None,
- url: Optional[str] = None) -> BaseSite:
- """A factory method to obtain a Site object.
-
- Site objects are cached and reused by this method.
-
- By default rely on config settings. These defaults may all be overridden
- using the method parameters.
-
- Creating the default site using config.mylang and config.family::
-
- site = pywikibot.Site()
-
- Override default site code::
-
- site = pywikibot.Site('fr')
-
- Override default family::
-
- site = pywikibot.Site(fam='wikisource')
-
- Setting a specific site::
-
- site = pywikibot.Site('fr', 'wikisource')
-
- which is equal to::
-
- site = pywikibot.Site('wikisource:fr')
-
- .. note:: An already created site is cached an a new variable points
- to the same object if interface, family, code and user are equal:
-
- >>> import pywikibot
- >>> site_1 = pywikibot.Site('wikisource:fr')
- >>> site_2 = pywikibot.Site('fr', 'wikisource')
- >>> site_1 is site_2
- True
- >>> site_1
- APISite("fr", "wikisource")
-
- :class:`APISite<pywikibot.site._apisite.APISite>` is the default
- interface. Refer :py:obj:`pywikibot.site` for other interface types.
-
- .. warning:: Never create a site object via interface class directly.
- Always use this factory method.
-
- .. versionchanged:: 7.3
- Short creation if site code is equal to family name like
- `Site('commons')`, `Site('meta')` or `Site('wikidata')`.
-
- :param code: language code (override config.mylang)
- code may also be a sitename like 'wikipedia:test'
- :param fam: family name or object (override config.family)
- :param user: bot user name to use on this site (override config.usernames)
- :param interface: site class or name of class in :py:obj:`pywikibot.site`
- (override config.site_interface)
- :param url: Instead of code and fam, does try to get a Site based on the
- URL. Still requires that the family supporting that URL exists.
- :raises ValueError: URL and pair of code and family given
- :raises ValueError: Invalid interface name
- :raises ValueError: Missing Site code
- :raises ValueError: Missing Site family
- """
- if url:
- # Either code and fam or url with optional fam for AutoFamily name
- if code:
- raise ValueError(
- 'URL to the wiki OR a pair of code and family name '
- 'should be provided')
- code, fam = _code_fam_from_url(url, fam)
- elif code and ':' in code:
- if fam:
- raise ValueError(
- 'sitename OR a pair of code and family name '
- 'should be provided')
- fam, _, code = code.partition(':')
- else:
- if not fam: # try code as family
- with suppress(exceptions.UnknownFamilyError):
- fam = Family.load(code)
- # Fallback to config defaults
- code = code or _config.mylang
- fam = fam or _config.family
-
- if not (code and fam):
- raise ValueError(f"Missing Site {'code' if not code else 'family'}")
-
- if not isinstance(fam, Family):
- fam = Family.load(fam)
-
- interface = interface or fam.interface(code)
-
- # config.usernames is initialised with a defaultdict for each family name
- family_name = str(fam)
-
- code_to_user = {}
- if '*' in _config.usernames: # T253127: usernames is a defaultdict
- code_to_user = _config.usernames['*'].copy()
- code_to_user.update(_config.usernames[family_name])
- user = user or code_to_user.get(code) or code_to_user.get('*')
-
- if not isinstance(interface, type):
- # If it isn't a class, assume it is a string
- try:
- tmp = __import__('pywikibot.site', fromlist=[interface])
- except ImportError:
- raise ValueError(f'Invalid interface name: {interface}')
- else:
- interface = getattr(tmp, interface)
-
- if not issubclass(interface, BaseSite):
- warning(f'Site called with interface={interface.__name__}')
-
- user = normalize_username(user)
- key = f'{interface.__name__}:{fam}:{code}:{user}'
- if key not in _sites or not isinstance(_sites[key], interface):
- _sites[key] = interface(code=code, fam=fam, user=user)
- 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)
-
- return _sites[key]
-
-
-# These imports depend on Wb* classes above.
-from pywikibot.page import ( # noqa: E402
- Category,
- Claim,
- FilePage,
- ItemPage,
- LexemeForm,
- LexemePage,
- LexemeSense,
- Link,
- MediaInfo,
- Page,
- PropertyPage,
- SiteLink,
- User,
- html2unicode,
- url2unicode,
-)
-
-
-link_regex = re.compile(r'\[\[(?P<title>[^\]|[<>{}]*)(\|.*?)?\]\]')
-
-
-def showDiff(oldtext: str, newtext: str, context: int = 0) -> None:
- """
- Output a string showing the differences between oldtext and newtext.
-
- The differences are highlighted (only on compatible systems) to show which
- changes were made.
- """
- PatchManager(oldtext, newtext, context=context).print_hunks()
-
-
-# Throttle and thread handling
-
-
-def sleep(secs: int) -> None:
- """Suspend execution of the current thread for the given number of seconds.
-
- Drop this process from the throttle log if wait time is greater than
- 30 seconds by calling :func:`stopme`.
- """
- if secs >= 30:
- stopme()
- time_sleep(secs)
-
-
-def stopme() -> None:
- """Drop this process from the throttle log, after pending threads finish.
-
- Can be called manually if desired but usually it is not necessary.
- Does not clean :func:`async_manager`. This should be run when a bot
- does not interact with the Wiki, or when it has stopped doing so.
- After a bot has run ``stopme()`` it will not slow down other bots
- instances any more.
-
- ``stopme()`` is called with :func:`sleep` function during long
- delays and with :meth:`bot.BaseBot.exit` to wait for pending write
- threads.
- """
- _flush(False)
-
-
-def _flush(stop: bool = True) -> None:
- """Drop this process from the throttle log, after pending threads finish.
-
- Wait for the page-putter to flush its queue. Also drop this process
- from the throttle log. Called automatically at Python exit.
-
- :param stop: Also clear :func:`async_manager`s put queue. This is
- only done at exit time.
- """
- debug('_flush() called')
-
- def remaining() -> Tuple[int, datetime.timedelta]:
- remainingPages = page_put_queue.qsize()
- if stop:
- # -1 because we added a None element to stop the queue
- remainingPages -= 1
-
- remainingSeconds = datetime.timedelta(
- seconds=round(remainingPages * _config.put_throttle))
- return (remainingPages, remainingSeconds)
-
- if stop:
- # None task element leaves async_manager
- page_put_queue.put((None, [], {}))
-
- num, sec = remaining()
- if num > 0 and sec.total_seconds() > _config.noisysleep:
- output('<<lightblue>>Waiting for {num} pages to be put. '
- 'Estimated time remaining: {sec}<<default>>'
- .format(num=num, sec=sec))
-
- exit_queue = None
- if _putthread is not threading.current_thread():
- while _putthread.is_alive() and not (page_put_queue.empty()
- and page_put_queue_busy.empty()):
- try:
- _putthread.join(1)
- except KeyboardInterrupt:
- exit_queue = input_yn(
- 'There are {} pages remaining in the queue. Estimated '
- 'time remaining: {}\nReally exit?'.format(*remaining()),
- default=False, automatic_quit=False)
- break
-
- if exit_queue is False:
- # handle the queue when _putthread is stopped after KeyboardInterrupt
- with suppress(KeyboardInterrupt):
- async_manager(block=False)
-
- if not stop:
- # delete the put queue
- with page_put_queue.mutex:
- page_put_queue.all_tasks_done.notify_all()
- page_put_queue.queue.clear()
- page_put_queue.not_full.notify_all()
-
- # only need one drop() call because all throttles use the same global pid
- with suppress(KeyError):
- _sites.popitem()[1].throttle.drop()
- log('Dropped throttle(s).')
-
-
-# Create a separate thread for asynchronous page saves (and other requests)
-def async_manager(block=True) -> None:
- """Daemon to take requests from the queue and execute them in background.
-
- :param block: If true, block :attr:`page_put_queue` if necessary
- until a request is available to process. Otherwise process a
- request if one is immediately available, else leave the function.
- """
- while True:
- if not block and page_put_queue.empty():
- break
- (request, args, kwargs) = page_put_queue.get(block)
- page_put_queue_busy.put(None)
- if request is None:
- break
- request(*args, **kwargs)
- page_put_queue.task_done()
- page_put_queue_busy.get()
-
-
-def async_request(request: Callable, *args: Any, **kwargs: Any) -> None:
- """Put a request on the queue, and start the daemon if necessary."""
- if not _putthread.is_alive():
- with page_put_queue.mutex, suppress(AssertionError, RuntimeError):
- _putthread.start()
- page_put_queue.put((request, args, kwargs))
-
-
-#: Queue to hold pending requests
-page_put_queue: Queue = Queue(_config.max_queue_size)
-
-# queue to signal that async_manager is working on a request. See T147178.
-page_put_queue_busy: Queue = Queue(_config.max_queue_size)
-# set up the background thread
-_putthread = threading.Thread(target=async_manager,
- name='Put-Thread', # for debugging purposes
- daemon=True)
-atexit.register(_flush)
diff --git a/tox.ini b/tox.ini
index 59e59ea..21cbd8f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -147,7 +147,6 @@
# N816: mixedCase variable in global scope
per-file-ignores =
- pywikibot/__init__.py: N802, N806
pywikibot/_wbtypes.py: N802
pywikibot/backports.py: F401
pywikibot/bot.py: N802, N816
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/938434
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: If932fc115b6699de01acd56ed015349ee98c6fbe
Gerrit-Change-Number: 938434
Gerrit-PatchSet: 11
Gerrit-Owner: Xqt <info(a)gno.de>
Gerrit-Reviewer: Xqt <info(a)gno.de>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged