jenkins-bot has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/core/+/630375 )
Change subject: [Py2] Remove Python 2 code in thread_tests.py
......................................................................
[Py2] Remove Python 2 code in thread_tests.py
Change-Id: I3e467ff07bde42bad43361a4deef4748dace4ad6
---
M tests/thread_tests.py
1 file changed, 4 insertions(+), 10 deletions(-)
Approvals:
Xqt: Looks good to me, approved
jenkins-bot: Verified
diff --git a/tests/thread_tests.py b/tests/thread_tests.py
index 2aef920..81d5ff7 100644
--- a/tests/thread_tests.py
+++ b/tests/thread_tests.py
@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
"""Tests for threading tools."""
#
-# (C) Pywikibot team, 2014-2018
+# (C) Pywikibot team, 2014-2020
#
# Distributed under the terms of the MIT license.
#
-from __future__ import absolute_import, division, unicode_literals
+from contextlib import suppress
from tests.aspects import unittest, TestCase
@@ -28,8 +28,7 @@
def gen_func(self):
"""Helper method for generator test."""
iterable = 'abcd'
- for i in iterable:
- yield i
+ yield from iterable
def test_run_from_gen_function(self):
"""Test thread running with generator as target."""
@@ -48,13 +47,10 @@
# If they are a generator, we need to convert to a list
# first otherwise the generator is empty the second time.
datasets = [list(gen) for gen in gens]
-
set_result = set(datasets[0]).intersection(*datasets[1:])
-
result = list(intersect_generators(datasets))
self.assertCountEqual(set(result), result)
-
self.assertCountEqual(result, set_result)
@@ -74,7 +70,5 @@
if __name__ == '__main__': # pragma: no cover
- try:
+ with suppress(SystemExit):
unittest.main()
- except SystemExit:
- pass
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/630375
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: I3e467ff07bde42bad43361a4deef4748dace4ad6
Gerrit-Change-Number: 630375
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/+/587635 )
Change subject: [IMPR] Shorten the loop in add_text.py
......................................................................
[IMPR] Shorten the loop in add_text.py
- if putText is not True add_text function may return earlier
- in result the next if statement needs only to check whether
text != newtext
- the while loop is only needed for 'b'rowser choice, so shorten
the loop here too.
Change-Id: Ie5002dec8e23b15a78c8f6fd7b9351e3cfe0c09d
---
M scripts/add_text.py
1 file changed, 13 insertions(+), 12 deletions(-)
Approvals:
Matěj Suchánek: Looks good to me, but someone else must approve
Xqt: Looks good to me, approved
jenkins-bot: Verified
diff --git a/scripts/add_text.py b/scripts/add_text.py
index 43e80c3..7481198 100755
--- a/scripts/add_text.py
+++ b/scripts/add_text.py
@@ -248,7 +248,12 @@
else:
newtext = addText + '\n' + text
- if putText and text != newtext:
+ if not putText:
+ # If someone load it as module, maybe it's not so useful to put the
+ # text in the page
+ return (text, newtext, always)
+
+ if text != newtext:
pywikibot.output(color_format(
'\n\n>>> {lightpurple}{0}{default} <<<', page.title()))
pywikibot.showDiff(text, newtext)
@@ -256,11 +261,6 @@
# Let's put the changes.
error_count = 0
while True:
- # If someone load it as module, maybe it's not so useful to put the
- # text in the page
- if not putText:
- return (text, newtext, always)
-
if not always:
try:
choice = pywikibot.input_choice(
@@ -276,13 +276,14 @@
return (False, False, always)
elif choice == 'b':
pywikibot.bot.open_webbrowser(page)
+ continue
- if always or choice == 'y':
- result = put_text(page, newtext, summary, error_count,
- asynchronous=not always)
- if result is not None:
- return (result, result, always)
- error_count += 1
+ # either always or choice == 'y' is selected
+ result = put_text(page, newtext, summary, error_count,
+ asynchronous=not always)
+ if result is not None:
+ return (result, result, always)
+ error_count += 1
def main(*args):
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/587635
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: Ie5002dec8e23b15a78c8f6fd7b9351e3cfe0c09d
Gerrit-Change-Number: 587635
Gerrit-PatchSet: 1
Gerrit-Owner: Xqt <info(a)gno.de>
Gerrit-Reviewer: D3r1ck01 <xsavitar.wiki(a)aol.com>
Gerrit-Reviewer: Matěj Suchánek <matejsuchanek97(a)gmail.com>
Gerrit-Reviewer: Xqt <info(a)gno.de>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged
jenkins-bot has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/core/+/616050 )
Change subject: [IMPR] derive abstract WbRepresentation from abstract base class abc.ABC
......................................................................
[IMPR] derive abstract WbRepresentation from abstract base class abc.ABC
Change-Id: I78144e78b4059fe4adc8d873bbf255d138f72a2b
---
M pywikibot/_wbtypes.py
1 file changed, 5 insertions(+), 1 deletion(-)
Approvals:
D3r1ck01: Looks good to me, but someone else must approve
Xqt: Looks good to me, approved
jenkins-bot: Verified
diff --git a/pywikibot/_wbtypes.py b/pywikibot/_wbtypes.py
index e074829..46d57a7 100644
--- a/pywikibot/_wbtypes.py
+++ b/pywikibot/_wbtypes.py
@@ -5,22 +5,26 @@
#
# Distributed under the terms of the MIT license.
#
+import abc
import json
-class WbRepresentation:
+class WbRepresentation(abc.ABC):
"""Abstract class for Wikibase representations."""
+ @abc.abstractmethod
def __init__(self):
"""Constructor."""
raise NotImplementedError
+ @abc.abstractmethod
def toWikibase(self):
"""Convert representation to JSON for the Wikibase API."""
raise NotImplementedError
@classmethod
+ @abc.abstractmethod
def fromWikibase(cls, json):
"""Create a representation object based on JSON from Wikibase API."""
raise NotImplementedError
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/616050
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: I78144e78b4059fe4adc8d873bbf255d138f72a2b
Gerrit-Change-Number: 616050
Gerrit-PatchSet: 2
Gerrit-Owner: Xqt <info(a)gno.de>
Gerrit-Reviewer: D3r1ck01 <xsavitar.wiki(a)aol.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/+/629062 )
Change subject: [IMPR] Add some classes and functions to __all__ variable in pywikibot
......................................................................
[IMPR] Add some classes and functions to __all__ variable in pywikibot
- ignore deprecated setAction function
- ignore stopme function which is always used at exit time
and it is not needed to call it
- remove inputChoice which has been dropped
- update documentation and fix reST markup
Bug: T122879
Change-Id: I08ad939bdb53c565adaf922a0547354be7676998
---
M pywikibot/__init__.py
1 file changed, 49 insertions(+), 50 deletions(-)
Approvals:
Xqt: Looks good to me, approved
jenkins-bot: Verified
diff --git a/pywikibot/__init__.py b/pywikibot/__init__.py
index 97dadda..ca7e428 100644
--- a/pywikibot/__init__.py
+++ b/pywikibot/__init__.py
@@ -15,6 +15,7 @@
from contextlib import suppress
from decimal import Decimal
from queue import Queue
+from typing import Optional, Union
from warnings import warn
from pywikibot.__metadata__ import (
@@ -84,23 +85,23 @@
'__maintainer__', '__maintainer_email__', '__name__', '__release__',
'__url__', '__version__',
'BadTitle', 'Bot', 'calledModuleName', 'CaptchaError', 'CascadeLockedPage',
- 'Category', 'CircularRedirect', 'Claim', 'config',
+ 'Category', 'CircularRedirect', 'Claim', 'config', 'Coordinate',
'CoordinateGlobeUnknownException', 'critical', 'CurrentPageBot', 'debug',
'EditConflict', 'error', 'Error', 'exception', 'FatalServerError',
'FilePage', 'handle_args', 'handleArgs', 'html2unicode', 'input',
- 'input_choice', 'input_yn', 'inputChoice', 'InterwikiRedirectPage',
- 'InvalidTitle', 'IsNotRedirectPage', 'IsRedirectPage', 'ItemPage', 'Link',
- 'LockedNoPage', 'LockedPage', 'log', 'NoCreateError', 'NoMoveTarget',
- 'NoPage', 'NoSuchSite', 'NoUsername', 'NoWikibaseEntity',
- 'OtherPageSaveError', 'output', 'Page', 'PageCreatedConflict',
- 'PageDeletedConflict', 'PageNotSaved', 'PageRelatedError',
- 'PageSaveRelatedError', 'PropertyPage', 'QuitKeyboardInterrupt',
- 'SectionError', 'Server504Error', 'ServerError', 'showHelp', 'Site',
- 'SiteDefinitionError', 'SiteLink', 'SpamblacklistError', 'stdout',
- 'TitleblacklistError', 'translate', 'ui', 'unicode2html',
- 'UnknownExtension', 'UnknownFamily', 'UnknownSite', 'UnsupportedPage',
- 'UploadWarning', 'url2unicode', 'User', 'UserBlocked', 'warning',
- 'WikiBaseError', 'WikidataBot',
+ 'input_choice', 'input_yn', 'InterwikiRedirectPage', 'InvalidTitle',
+ 'IsNotRedirectPage', 'IsRedirectPage', 'ItemPage', 'Link', 'LockedNoPage',
+ 'LockedPage', 'log', 'NoCreateError', 'NoMoveTarget', 'NoPage',
+ 'NoSuchSite', 'NoUsername', 'NoWikibaseEntity', 'OtherPageSaveError',
+ 'output', 'Page', 'PageCreatedConflict', 'PageDeletedConflict',
+ 'PageNotSaved', 'PageRelatedError', 'PageSaveRelatedError', 'PropertyPage',
+ 'QuitKeyboardInterrupt', 'SectionError', 'Server504Error', 'ServerError',
+ 'showDiff', 'showHelp', 'Site', 'SiteDefinitionError', 'SiteLink',
+ 'SpamblacklistError', 'stdout', 'Timestamp', 'TitleblacklistError',
+ 'translate', 'ui', 'unicode2html', 'UnknownExtension', 'UnknownFamily',
+ 'UnknownSite', 'UnsupportedPage', 'UploadWarning', 'url2unicode', 'User',
+ 'UserBlocked', 'warning', 'WbGeoShape', 'WbMonolingualText', 'WbQuantity',
+ 'WbTabularData', 'WbTime', 'WbUnknown', 'WikiBaseError', 'WikidataBot',
)
__all__ += textlib_methods
@@ -336,7 +337,7 @@
globe, site=site, globe_item=data['globe'])
@property
- def precision(self):
+ def precision(self) -> Optional[float]:
"""
Return the precision of the geo coordinate.
@@ -360,10 +361,9 @@
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))))
-
- @rtype: float or None
"""
if self._dim is None and self._precision is None:
return None
@@ -377,7 +377,7 @@
def precision(self, value):
self._precision = value
- def precisionToDim(self):
+ def precisionToDim(self) -> Optional[int]:
"""
Convert precision from Wikibase to GeoData's dim and return the latter.
@@ -387,17 +387,18 @@
Carrying on from the earlier derivation of precision, since
precision = math.degrees(dim/(radius*math.cos(math.radians(self.lat))))
- we get:
+ 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))))
-
- @rtype: int or None
"""
if self._dim is None and self._precision is None:
raise ValueError('No values set for dim or precision')
@@ -461,12 +462,20 @@
_items = ('year', 'month', 'day', 'hour', 'minute', 'second',
'precision', 'before', 'after', 'timezone', 'calendarmodel')
- def __init__(self, year=None, month=None, day=None,
- hour=None, minute=None, second=None,
- precision=None, before=0, after=0,
- timezone=0, calendarmodel=None, site=None):
- """
- Create a new WbTime object.
+ 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=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
@@ -474,6 +483,7 @@
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
@@ -483,29 +493,18 @@
to minutes.
@param year: The year as a signed integer of between 1 and 16 digits.
- @type year: int
@param month: Month
- @type month: int
@param day: Day
- @type day: int
@param hour: Hour
- @type hour: int
@param minute: Minute
- @type minute: int
@param second: Second
- @type second: int
@param precision: The unit of the precision of the time.
- @type precision: int or str
@param before: Number of units after the given time it could be, if
uncertain. The unit is given by the precision.
- @type before: int
@param after: Number of units before the given time it could be, if
uncertain. The unit is given by the precision.
- @type after: int
@param timezone: Timezone information in minutes.
- @type timezone: int
@param calendarmodel: URI identifying the calendar model
- @type calendarmodel: str
@param site: The Wikibase site
@type site: pywikibot.site.DataSite
"""
@@ -555,12 +554,18 @@
raise ValueError('Invalid precision: "%s"' % precision)
@classmethod
- def fromTimestr(cls, datetimestr, precision=14, before=0, after=0,
- timezone=0, calendarmodel=None, site=None):
- """
- Create a new WbTime object from a UTC date/time string.
+ def fromTimestr(cls,
+ datetimestr: str,
+ precision: Union[int, str] = 14,
+ before: int = 0,
+ after: int = 0,
+ timezone: int = 0,
+ calendarmodel: Optional[str] = None,
+ site=None):
+ """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
@@ -568,19 +573,13 @@
@param datetimestr: Timestamp in a format resembling ISO 8601,
e.g. +2013-01-01T00:00:00Z
- @type datetimestr: str
@param precision: The unit of the precision of the time.
- @type precision: int or str
@param before: Number of units after the given time it could be, if
uncertain. The unit is given by the precision.
- @type before: int
@param after: Number of units before the given time it could be, if
uncertain. The unit is given by the precision.
- @type after: int
@param timezone: Timezone information in minutes.
- @type timezone: int
@param calendarmodel: URI identifying the calendar model
- @type calendarmodel: str
@param site: The Wikibase site
@type site: pywikibot.site.DataSite
@rtype: pywikibot.WbTime
@@ -925,13 +924,14 @@
raise NotImplementedError
@classmethod
- def _get_type_specifics(cls, site):
+ def _get_type_specifics(cls, site) -> dict:
"""
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: pywikibot.site.APISite, site serving as a repository for
@@ -939,7 +939,6 @@
@param site: The Wikibase site
@type site: pywikibot.site.APISite
- @rtype: dict
"""
raise NotImplementedError
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/629062
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: I08ad939bdb53c565adaf922a0547354be7676998
Gerrit-Change-Number: 629062
Gerrit-PatchSet: 8
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/+/275169 )
Change subject: [IMPR] Use api.APIGenerator through site._generator
......................................................................
[IMPR] Use api.APIGenerator through site._generator
- Create APIGenerator through site_generator method which is common for
all other api generators.
Bug: T129013
Change-Id: I9adf0b53dcd9dbcac5ae76aef9d4ddd86804d1b2
---
M pywikibot/site/__init__.py
1 file changed, 26 insertions(+), 24 deletions(-)
Approvals:
Xqt: Looks good to me, approved
jenkins-bot: Verified
diff --git a/pywikibot/site/__init__.py b/pywikibot/site/__init__.py
index 98ce79a..ef6becb 100644
--- a/pywikibot/site/__init__.py
+++ b/pywikibot/site/__init__.py
@@ -29,6 +29,7 @@
from enum import IntEnum
from itertools import zip_longest
from textwrap import fill
+from typing import Optional
from warnings import warn
import pywikibot
@@ -1368,37 +1369,38 @@
raise ValueError('Cannot parse a site out of %s.' % dbname)
@deprecated_args(step=None)
- def _generator(self, gen_class, type_arg=None, namespaces=None,
- total=None, **args):
+ def _generator(self, gen_class, type_arg: Optional[str] = None,
+ namespaces=None, total: Optional[int] = None, **args):
"""Convenience method that returns an API generator.
- All generic keyword arguments are passed as MW API parameter except for
- 'g_content' which is passed as a normal parameter to the generator's
- Initializer.
+ All generic keyword arguments are passed as MW API parameter
+ except for 'g_content' which is passed as a normal parameter to
+ the generator's Initializer.
@param gen_class: the type of generator to construct (must be
- a subclass of pywikibot.data.api.QueryGenerator)
+ a subclass of pywikibot.data.api._RequestWrapper)
@param type_arg: query type argument to be passed to generator's
constructor unchanged (not all types require this)
- @type type_arg: str
- @param namespaces: if not None, limit the query to namespaces in this
- list
+ @param namespaces: if not None, limit the query to namespaces in
+ this list
@type namespaces: iterable of basestring or Namespace key,
or a single instance of those types. May be a '|' separated
list of namespace identifiers.
- @param total: if not None, limit the generator to yielding this many
- items in total
- @type total: int
+ @param total: if not None, limit the generator to yielding this
+ many items in total
@return: iterable with parameters set
- @rtype: api.QueryGenerator
+ @rtype: _RequestWrapper
@raises KeyError: a namespace identifier was not resolved
@raises TypeError: a namespace identifier has an inappropriate
type such as NoneType or bool
"""
- # TODO: Support parameters/simple modes?
- req_args = {'site': self, 'parameters': args}
+ req_args = {'site': self}
if 'g_content' in args:
req_args['g_content'] = args.pop('g_content')
+ if 'parameters' in args:
+ req_args.update(args)
+ else:
+ req_args['parameters'] = args
if type_arg is not None:
gen = gen_class(type_arg, **req_args)
else:
@@ -7630,18 +7632,17 @@
return pywikibot.ItemPage(self, result['entity']['id'])
@deprecated_args(limit='total')
- def search_entities(self, search: str, language: str, total=None,
- **kwargs):
+ def search_entities(self, search: str, language: str,
+ total: Optional[int] = None, **kwargs):
"""
Search for pages or properties that contain the given text.
@param search: Text to find.
@param language: Language to search in.
- @param total: Maximum number of pages to retrieve in total, or None in
- case of no limit.
- @type total: int or None
+ @param total: Maximum number of pages to retrieve in total, or
+ None in case of no limit.
@return: 'search' list from API output.
- @rtype: api.APIGenerator
+ @rtype: Generator
"""
lang_codes = self._paraminfo.parameter('wbsearchentities',
'language')['type']
@@ -7658,7 +7659,8 @@
del kwargs['site']
parameters = dict(search=search, language=language, **kwargs)
- gen = api.APIGenerator('wbsearchentities', data_name='search',
- site=self, parameters=parameters)
- gen.set_maximum_items(total)
+ gen = self._generator(api.APIGenerator,
+ type_arg='wbsearchentities',
+ data_name='search',
+ total=total, parameters=parameters)
return gen
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/275169
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: I9adf0b53dcd9dbcac5ae76aef9d4ddd86804d1b2
Gerrit-Change-Number: 275169
Gerrit-PatchSet: 18
Gerrit-Owner: Xqt <info(a)gno.de>
Gerrit-Reviewer: Dalba <dalba.wiki(a)gmail.com>
Gerrit-Reviewer: John Vandenberg <jayvdb(a)gmail.com>
Gerrit-Reviewer: Magul <tomasz.magulski(a)gmail.com>
Gerrit-Reviewer: Merlijn van Deen <valhallasw(a)arctus.nl>
Gerrit-Reviewer: Mpaa <mpaa.wiki(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/+/627829 )
Change subject: [doc] Update ROADMAP.rst
......................................................................
[doc] Update ROADMAP.rst
Change-Id: Ia12ed68a91a1457638bc534cf28b1c7f955b0796
---
M ROADMAP.rst
1 file changed, 13 insertions(+), 3 deletions(-)
Approvals:
Xqt: Looks good to me, approved
jenkins-bot: Verified
diff --git a/ROADMAP.rst b/ROADMAP.rst
index b0ce70e..bd90185 100644
--- a/ROADMAP.rst
+++ b/ROADMAP.rst
@@ -1,11 +1,18 @@
Current release changes
~~~~~~~~~~~~~~~~~~~~~~~
+* Support of MediaWiki releases below 1.19 has been dropped (T245350)
+* Page.get_best_claim () retrieves preferred Claim of a property referring to the given page (T175207)
+* Check whether _putthead is current_thread() to join() (T263331)
+* Add BasePage.has_deleted_revisions() method
+* Allow querying deleted revs without the deletedhistory right
+* Use ignore_discard for login cookie container (T261066)
+* Siteinfo.get() loads data via API instead from cache if expiry parameter is True (T260490)
* Move latest revision id handling to WikibaseEntity (T233406)
* Load wikibase entities when necessary (T245809)
* Fix path for stable release in version.getversion() (T262558)
* "since" parameter in EventStreams given as Timestamp or MediaWiki timestamp string has been fixed
-* Some methods deprecated for 6 years or longer were removed
+* Methods deprecated for 6 years or longer were removed
* Page.getVersionHistory and Page.fullVersionHistory() methods were removed (T136513, T151110)
* Allow multiple types of contributors parameter given for Page.revision_count()
* Deprecated tools.UnicodeMixin and tools.IteratorNextMixin has been removed
@@ -15,6 +22,11 @@
Future release notes
~~~~~~~~~~~~~~~~~~~~
+* 4.4.0: Property.getTspe() method will be removed
+* 4.4.0: Request.http_params() method will be removed
+* 4.4.0: DataSite.get_item() method will be removed
+* 4.4.0: date.MakeParameter() function will be removed
+* 4.4.0: pagegenerators.ReferringPageGenerator is desupported and will be removed
* 4.3.0: Unsused UserBlocked exception will be removed
* 4.3.0: Deprecated Page.contributingUsers() will be removed
* 4.2.0: tools.StringTypes will be removed
@@ -22,5 +34,3 @@
* 4.1.0: tools.open_compressed, tools.UnicodeType and tools.signature will be removed
* 4.1.0: comms.PywikibotCookieJar and comms.mode_check_decorator will be removed
* 4.0.0: Unused parameters of page methods like forceReload, insite, throttle, step will be removed
-* 4.0.0: Methods deprecated for 6 years or longer will be removed
-* 3.0.20200306: Support of MediaWiki releases below 1.19 will be dropped (T245350)
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/627829
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: Ia12ed68a91a1457638bc534cf28b1c7f955b0796
Gerrit-Change-Number: 627829
Gerrit-PatchSet: 11
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/+/616320 )
Change subject: Move Siteinfo to its own _siteinfo.py file
......................................................................
Move Siteinfo to its own _siteinfo.py file
Also move siteninfo tests from site_tests.py to new siteinfo_tests.py
No source changes where made.
site/__init__.py is the biggest framework file with 335 KB disk space
and 8280 lines of code. Currently it contains 13 classes. This should be
splitted a bit more into smaller parts for readability and maintainability.
Change-Id: I8a87dc2bf0b178aa862448153dbffa3fcfe81f79
---
M pywikibot/CONTENT.rst
M pywikibot/site/__init__.py
A pywikibot/site/_siteinfo.py
M tests/__init__.py
M tests/site_tests.py
A tests/siteinfo_tests.py
6 files changed, 492 insertions(+), 445 deletions(-)
Approvals:
Xqt: Looks good to me, approved
jenkins-bot: Verified
diff --git a/pywikibot/CONTENT.rst b/pywikibot/CONTENT.rst
index 30f92ee..de541b7 100644
--- a/pywikibot/CONTENT.rst
+++ b/pywikibot/CONTENT.rst
@@ -113,6 +113,8 @@
+----------------------------+------------------------------------------------------+
| _decorators.py | Decorators used by site models. |
+----------------------------+------------------------------------------------------+
+ | _siteinfo.py | Objects representing site info data contents. |
+ +----------------------------+------------------------------------------------------+
+----------------------------+------------------------------------------------------+
diff --git a/pywikibot/site/__init__.py b/pywikibot/site/__init__.py
index edfbf11..d090257 100644
--- a/pywikibot/site/__init__.py
+++ b/pywikibot/site/__init__.py
@@ -10,7 +10,6 @@
#
# Distributed under the terms of the MIT license.
#
-import copy
import datetime
import functools
import heapq
@@ -25,12 +24,11 @@
import uuid
from collections import defaultdict, namedtuple
-from collections.abc import Iterable, Container, Mapping
+from collections.abc import Iterable, Mapping
from contextlib import suppress
from enum import IntEnum
from itertools import zip_longest
from textwrap import fill
-from typing import Optional
from warnings import warn
import pywikibot
@@ -68,6 +66,7 @@
UnknownSite,
)
from pywikibot.site._decorators import need_extension, need_right, need_version
+from pywikibot.site._siteinfo import Siteinfo
from pywikibot.throttle import Throttle
from pywikibot.tools import (
ComparableMixin,
@@ -1208,345 +1207,6 @@
return api.encode_url(query)
-class Siteinfo(Container):
-
- """
- A 'dictionary' like container for siteinfo.
-
- This class queries the server to get the requested siteinfo property.
- Optionally it can cache this directly in the instance so that later
- requests don't need to query the server.
-
- All values of the siteinfo property 'general' are directly available.
- """
-
- WARNING_REGEX = re.compile(r'Unrecognized values? for parameter '
- r'["\']siprop["\']: (.+?)\.?$')
-
- # Until we get formatversion=2, we have to convert empty-string properties
- # into booleans so they are easier to use.
- BOOLEAN_PROPS = {
- 'general': [
- 'imagewhitelistenabled',
- 'langconversion',
- 'titleconversion',
- 'rtl',
- 'readonly',
- 'writeapi',
- 'variantarticlepath',
- 'misermode',
- 'uploadsenabled',
- ],
- 'namespaces': [ # for each namespace
- 'subpages',
- 'content',
- 'nonincludable',
- ],
- 'magicwords': [ # for each magicword
- 'case-sensitive',
- ],
- }
-
- def __init__(self, site):
- """Initialise it with an empty cache."""
- self._site = site
- self._cache = {}
-
- @staticmethod
- def _get_default(key: str):
- """
- Return the default value for different properties.
-
- If the property is 'restrictions' it returns a dictionary with:
- - 'cascadinglevels': 'sysop'
- - 'semiprotectedlevels': 'autoconfirmed'
- - 'levels': '' (everybody), 'autoconfirmed', 'sysop'
- - 'types': 'create', 'edit', 'move', 'upload'
- Otherwise it returns L{pywikibot.tools.EMPTY_DEFAULT}.
-
- @param key: The property name
- @return: The default value
- @rtype: dict or L{pywikibot.tools.EmptyDefault}
- """
- if key == 'restrictions':
- # implemented in b73b5883d486db0e9278ef16733551f28d9e096d
- return {
- 'cascadinglevels': ['sysop'],
- 'semiprotectedlevels': ['autoconfirmed'],
- 'levels': ['', 'autoconfirmed', 'sysop'],
- 'types': ['create', 'edit', 'move', 'upload']
- }
-
- if key == 'fileextensions':
- # the default file extensions in MediaWiki
- return [{'ext': ext} for ext in ['png', 'gif', 'jpg', 'jpeg']]
-
- return pywikibot.tools.EMPTY_DEFAULT
-
- @staticmethod
- def _post_process(prop, data):
- """Do some default handling of data. Directly modifies data."""
- # Be careful with version tests inside this here as it might need to
- # query this method to actually get the version number
-
- # Convert boolean props from empty strings to actual boolean values
- if prop in Siteinfo.BOOLEAN_PROPS.keys():
- # siprop=namespaces and
- # magicwords has properties per item in result
- if prop in ('namespaces', 'magicwords'):
- for index, value in enumerate(data):
- # namespaces uses a dict, while magicwords uses a list
- key = index if type(data) is list else value
- for p in Siteinfo.BOOLEAN_PROPS[prop]:
- data[key][p] = p in data[key]
- else:
- for p in Siteinfo.BOOLEAN_PROPS[prop]:
- data[p] = p in data
-
- def _get_siteinfo(self, prop, expiry) -> dict:
- """
- Retrieve a siteinfo property.
-
- All properties which the site doesn't
- support contain the default value. Because pre-1.12 no data was
- returned when a property doesn't exists, it queries each property
- independetly if a property is invalid.
-
- @param prop: The property names of the siteinfo.
- @type prop: str or iterable
- @param expiry: The expiry date of the cached request.
- @type expiry: int (days), L{datetime.timedelta}, False (config)
- @return: A dictionary with the properties of the site. Each entry in
- the dictionary is a tuple of the value and a boolean to save if it
- is the default value.
- @see: U{https://www.mediawiki.org/wiki/API:Meta#siteinfo_.2F_si}
- """
- def warn_handler(mod, message):
- """Return True if the warning is handled."""
- matched = Siteinfo.WARNING_REGEX.match(message)
- if mod == 'siteinfo' and matched:
- invalid_properties.extend(
- prop.strip() for prop in matched.group(1).split(','))
- return True
- else:
- return False
-
- props = [prop] if isinstance(prop, str) else prop
- if not props:
- raise ValueError('At least one property name must be provided.')
-
- invalid_properties = []
- request = self._site._request(
- expiry=pywikibot.config.API_config_expiry
- if expiry is False else expiry,
- parameters={
- 'action': 'query', 'meta': 'siteinfo', 'siprop': props,
- }
- )
- # With 1.25wmf5 it'll require continue or rawcontinue. As we don't
- # continue anyway we just always use continue.
- request['continue'] = True
- # warnings are handled later
- request._warning_handler = warn_handler
- try:
- data = request.submit()
- except api.APIError as e:
- if e.code == 'siunknown_siprop':
- if len(props) == 1:
- pywikibot.log(
- "Unable to get siprop '{0}'".format(props[0]))
- return {props[0]: (Siteinfo._get_default(props[0]), False)}
- else:
- pywikibot.log('Unable to get siteinfo, because at least '
- "one property is unknown: '{0}'".format(
- "', '".join(props)))
- results = {}
- for prop in props:
- results.update(self._get_siteinfo(prop, expiry))
- return results
- raise
- else:
- result = {}
- if invalid_properties:
- for prop in invalid_properties:
- result[prop] = (Siteinfo._get_default(prop), False)
- pywikibot.log("Unable to get siprop(s) '{0}'".format(
- "', '".join(invalid_properties)))
- if 'query' in data:
- # If the request is a CachedRequest, use the _cachetime attr.
- cache_time = getattr(
- request, '_cachetime', None) or datetime.datetime.utcnow()
- for prop in props:
- if prop in data['query']:
- self._post_process(prop, data['query'][prop])
- result[prop] = (data['query'][prop], cache_time)
- return result
-
- @staticmethod
- def _is_expired(cache_date, expire):
- """Return true if the cache date is expired."""
- if isinstance(expire, bool):
- return expire
-
- if not cache_date: # default values are always expired
- return True
-
- # cached date + expiry are in the past if it's expired
- return cache_date + expire < datetime.datetime.utcnow()
-
- def _get_general(self, key: str, expiry):
- """
- Return a siteinfo property which is loaded by default.
-
- The property 'general' will be queried if it wasn't yet or it's forced.
- Additionally all uncached default properties are queried. This way
- multiple default properties are queried with one request. It'll cache
- always all results.
-
- @param key: The key to search for.
- @param expiry: If the cache is older than the expiry it ignores the
- cache and queries the server to get the newest value.
- @type expiry: int (days), L{datetime.timedelta}, False (never)
- @return: If that property was retrieved via this method. Returns None
- if the key was not in the retrieved values.
- @rtype: various (the value), bool (if the default value is used)
- """
- if 'general' not in self._cache:
- pywikibot.debug('general siteinfo not loaded yet.', _logger)
- force = True
- props = ['namespaces', 'namespacealiases']
- else:
- force = Siteinfo._is_expired(self._cache['general'][1], expiry)
- props = []
- if force:
- props = [prop for prop in props if prop not in self._cache]
- if props:
- pywikibot.debug(
- "Load siteinfo properties '{0}' along with 'general'"
- .format("', '".join(props)), _logger)
- props += ['general']
- default_info = self._get_siteinfo(props, expiry)
- for prop in props:
- self._cache[prop] = default_info[prop]
- if key in default_info:
- return default_info[key]
- if key in self._cache['general'][0]:
- return self._cache['general'][0][key], self._cache['general']
- else:
- return None
-
- def __getitem__(self, key: str):
- """Return a siteinfo property, caching and not forcing it."""
- return self.get(key, False) # caches and doesn't force it
-
- def get(self, key: str, get_default: bool = True, cache: bool = True,
- expiry=False):
- """
- Return a siteinfo property.
-
- It will never throw an APIError if it only stated, that the siteinfo
- property doesn't exist. Instead it will use the default value.
-
- @param key: The name of the siteinfo property.
- @param get_default: Whether to throw an KeyError if the key is invalid.
- @param cache: Caches the result internally so that future accesses via
- this method won't query the server.
- @param expiry: If the cache is older than the expiry it ignores the
- cache and queries the server to get the newest value.
- @type expiry: int/float (days), L{datetime.timedelta},
- False (never expired), True (always expired)
- @return: The gathered property
- @rtype: various
- @raises KeyError: If the key is not a valid siteinfo property and the
- get_default option is set to False.
- @see: L{_get_siteinfo}
- """
- # If expiry is a float or int convert to timedelta
- # Note: bool is an instance of int
- if isinstance(expiry, float) or type(expiry) == int:
- expiry = datetime.timedelta(expiry)
-
- # expire = 0 (or timedelta(0)) are always expired and their bool is
- # False, so skip them EXCEPT if it's literally False, then they expire
- # never.
- if expiry and expiry is not True or expiry is False:
- try:
- cached = self._get_cached(key)
- except KeyError:
- pass
- else: # cached value available
- # is a default value, but isn't accepted
- if not cached[1] and not get_default:
- raise KeyError(key)
- if not Siteinfo._is_expired(cached[1], expiry):
- return copy.deepcopy(cached[0])
-
- preloaded = self._get_general(key, expiry)
- if not preloaded:
- preloaded = self._get_siteinfo(key, expiry)[key]
- else:
- cache = False
-
- if not preloaded[1] and not get_default:
- raise KeyError(key)
-
- if cache:
- self._cache[key] = preloaded
-
- return copy.deepcopy(preloaded[0])
-
- def _get_cached(self, key: str):
- """Return the cached value or a KeyError exception if not cached."""
- if 'general' in self._cache:
- if key in self._cache['general'][0]:
- return (self._cache['general'][0][key],
- self._cache['general'][1])
- else:
- return self._cache[key]
- raise KeyError(key)
-
- def __contains__(self, key: str) -> bool:
- """Return whether the value is cached."""
- try:
- self._get_cached(key)
- except KeyError:
- return False
- else:
- return True
-
- def is_recognised(self, key: str) -> Optional[bool]:
- """Return if 'key' is a valid property name. 'None' if not cached."""
- time = self.get_requested_time(key)
- return None if time is None else bool(time)
-
- def get_requested_time(self, key: str):
- """
- Return when 'key' was successfully requested from the server.
-
- If the property is actually in the siprop 'general' it returns the
- last request from the 'general' siprop.
-
- @param key: The siprop value or a property of 'general'.
- @return: The last time the siprop of 'key' was requested.
- @rtype: None (never), False (default), L{datetime.datetime} (cached)
- """
- with suppress(KeyError):
- return self._get_cached(key)[1]
-
- return None
-
- def __call__(self, key='general', force=False, dump=False):
- """DEPRECATED: Return the entry for key or dump the complete cache."""
- issue_deprecation_warning(
- 'Calling siteinfo', 'itself as a dictionary', since='20161221'
- )
- result = self.get(key, expiry=force)
- if not dump:
- return result
- else:
- return self._cache
-
-
class TokenWallet:
"""Container for tokens."""
diff --git a/pywikibot/site/_siteinfo.py b/pywikibot/site/_siteinfo.py
new file mode 100644
index 0000000..28f13cc
--- /dev/null
+++ b/pywikibot/site/_siteinfo.py
@@ -0,0 +1,361 @@
+# -*- coding: utf-8 -*-
+"""Objects representing site info data contents."""
+#
+# (C) Pywikibot team, 2008-2020
+#
+# Distributed under the terms of the MIT license.
+#
+import copy
+import datetime
+import re
+
+from collections.abc import Container
+from contextlib import suppress
+from typing import Optional
+
+import pywikibot
+
+from pywikibot.data import api
+from pywikibot.tools import EMPTY_DEFAULT, issue_deprecation_warning
+
+
+_logger = 'wiki.siteinfo'
+
+
+class Siteinfo(Container):
+
+ """
+ A 'dictionary' like container for siteinfo.
+
+ This class queries the server to get the requested siteinfo property.
+ Optionally it can cache this directly in the instance so that later
+ requests don't need to query the server.
+
+ All values of the siteinfo property 'general' are directly available.
+ """
+
+ WARNING_REGEX = re.compile(r'Unrecognized values? for parameter '
+ r'["\']siprop["\']: (.+?)\.?$')
+
+ # Until we get formatversion=2, we have to convert empty-string properties
+ # into booleans so they are easier to use.
+ BOOLEAN_PROPS = {
+ 'general': [
+ 'imagewhitelistenabled',
+ 'langconversion',
+ 'titleconversion',
+ 'rtl',
+ 'readonly',
+ 'writeapi',
+ 'variantarticlepath',
+ 'misermode',
+ 'uploadsenabled',
+ ],
+ 'namespaces': [ # for each namespace
+ 'subpages',
+ 'content',
+ 'nonincludable',
+ ],
+ 'magicwords': [ # for each magicword
+ 'case-sensitive',
+ ],
+ }
+
+ def __init__(self, site):
+ """Initialise it with an empty cache."""
+ self._site = site
+ self._cache = {}
+
+ @staticmethod
+ def _get_default(key: str):
+ """
+ Return the default value for different properties.
+
+ If the property is 'restrictions' it returns a dictionary with:
+ - 'cascadinglevels': 'sysop'
+ - 'semiprotectedlevels': 'autoconfirmed'
+ - 'levels': '' (everybody), 'autoconfirmed', 'sysop'
+ - 'types': 'create', 'edit', 'move', 'upload'
+ Otherwise it returns L{pywikibot.tools.EMPTY_DEFAULT}.
+
+ @param key: The property name
+ @return: The default value
+ @rtype: dict or L{pywikibot.tools.EmptyDefault}
+ """
+ if key == 'restrictions':
+ # implemented in b73b5883d486db0e9278ef16733551f28d9e096d
+ return {
+ 'cascadinglevels': ['sysop'],
+ 'semiprotectedlevels': ['autoconfirmed'],
+ 'levels': ['', 'autoconfirmed', 'sysop'],
+ 'types': ['create', 'edit', 'move', 'upload']
+ }
+
+ if key == 'fileextensions':
+ # the default file extensions in MediaWiki
+ return [{'ext': ext} for ext in ['png', 'gif', 'jpg', 'jpeg']]
+
+ return EMPTY_DEFAULT
+
+ @staticmethod
+ def _post_process(prop, data):
+ """Do some default handling of data. Directly modifies data."""
+ # Be careful with version tests inside this here as it might need to
+ # query this method to actually get the version number
+
+ # Convert boolean props from empty strings to actual boolean values
+ if prop in Siteinfo.BOOLEAN_PROPS.keys():
+ # siprop=namespaces and
+ # magicwords has properties per item in result
+ if prop in ('namespaces', 'magicwords'):
+ for index, value in enumerate(data):
+ # namespaces uses a dict, while magicwords uses a list
+ key = index if type(data) is list else value
+ for p in Siteinfo.BOOLEAN_PROPS[prop]:
+ data[key][p] = p in data[key]
+ else:
+ for p in Siteinfo.BOOLEAN_PROPS[prop]:
+ data[p] = p in data
+
+ def _get_siteinfo(self, prop, expiry) -> dict:
+ """
+ Retrieve a siteinfo property.
+
+ All properties which the site doesn't
+ support contain the default value. Because pre-1.12 no data was
+ returned when a property doesn't exists, it queries each property
+ independetly if a property is invalid.
+
+ @param prop: The property names of the siteinfo.
+ @type prop: str or iterable
+ @param expiry: The expiry date of the cached request.
+ @type expiry: int (days), L{datetime.timedelta}, False (config)
+ @return: A dictionary with the properties of the site. Each entry in
+ the dictionary is a tuple of the value and a boolean to save if it
+ is the default value.
+ @see: U{https://www.mediawiki.org/wiki/API:Meta#siteinfo_.2F_si}
+ """
+ def warn_handler(mod, message):
+ """Return True if the warning is handled."""
+ matched = Siteinfo.WARNING_REGEX.match(message)
+ if mod == 'siteinfo' and matched:
+ invalid_properties.extend(
+ prop.strip() for prop in matched.group(1).split(','))
+ return True
+ else:
+ return False
+
+ props = [prop] if isinstance(prop, str) else prop
+ if not props:
+ raise ValueError('At least one property name must be provided.')
+
+ invalid_properties = []
+ request = self._site._request(
+ expiry=pywikibot.config.API_config_expiry
+ if expiry is False else expiry,
+ parameters={
+ 'action': 'query', 'meta': 'siteinfo', 'siprop': props,
+ }
+ )
+ # With 1.25wmf5 it'll require continue or rawcontinue. As we don't
+ # continue anyway we just always use continue.
+ request['continue'] = True
+ # warnings are handled later
+ request._warning_handler = warn_handler
+ try:
+ data = request.submit()
+ except api.APIError as e:
+ if e.code == 'siunknown_siprop':
+ if len(props) == 1:
+ pywikibot.log(
+ "Unable to get siprop '{0}'".format(props[0]))
+ return {props[0]: (Siteinfo._get_default(props[0]), False)}
+ else:
+ pywikibot.log('Unable to get siteinfo, because at least '
+ "one property is unknown: '{0}'".format(
+ "', '".join(props)))
+ results = {}
+ for prop in props:
+ results.update(self._get_siteinfo(prop, expiry))
+ return results
+ raise
+ else:
+ result = {}
+ if invalid_properties:
+ for prop in invalid_properties:
+ result[prop] = (Siteinfo._get_default(prop), False)
+ pywikibot.log("Unable to get siprop(s) '{0}'".format(
+ "', '".join(invalid_properties)))
+ if 'query' in data:
+ # If the request is a CachedRequest, use the _cachetime attr.
+ cache_time = getattr(
+ request, '_cachetime', None) or datetime.datetime.utcnow()
+ for prop in props:
+ if prop in data['query']:
+ self._post_process(prop, data['query'][prop])
+ result[prop] = (data['query'][prop], cache_time)
+ return result
+
+ @staticmethod
+ def _is_expired(cache_date, expire):
+ """Return true if the cache date is expired."""
+ if isinstance(expire, bool):
+ return expire
+
+ if not cache_date: # default values are always expired
+ return True
+
+ # cached date + expiry are in the past if it's expired
+ return cache_date + expire < datetime.datetime.utcnow()
+
+ def _get_general(self, key: str, expiry):
+ """
+ Return a siteinfo property which is loaded by default.
+
+ The property 'general' will be queried if it wasn't yet or it's forced.
+ Additionally all uncached default properties are queried. This way
+ multiple default properties are queried with one request. It'll cache
+ always all results.
+
+ @param key: The key to search for.
+ @param expiry: If the cache is older than the expiry it ignores the
+ cache and queries the server to get the newest value.
+ @type expiry: int (days), L{datetime.timedelta}, False (never)
+ @return: If that property was retrieved via this method. Returns None
+ if the key was not in the retrieved values.
+ @rtype: various (the value), bool (if the default value is used)
+ """
+ if 'general' not in self._cache:
+ pywikibot.debug('general siteinfo not loaded yet.', _logger)
+ force = True
+ props = ['namespaces', 'namespacealiases']
+ else:
+ force = Siteinfo._is_expired(self._cache['general'][1], expiry)
+ props = []
+ if force:
+ props = [prop for prop in props if prop not in self._cache]
+ if props:
+ pywikibot.debug(
+ "Load siteinfo properties '{0}' along with 'general'"
+ .format("', '".join(props)), _logger)
+ props += ['general']
+ default_info = self._get_siteinfo(props, expiry)
+ for prop in props:
+ self._cache[prop] = default_info[prop]
+ if key in default_info:
+ return default_info[key]
+ if key in self._cache['general'][0]:
+ return self._cache['general'][0][key], self._cache['general']
+ else:
+ return None
+
+ def __getitem__(self, key: str):
+ """Return a siteinfo property, caching and not forcing it."""
+ return self.get(key, False) # caches and doesn't force it
+
+ def get(self, key: str, get_default: bool = True, cache: bool = True,
+ expiry=False):
+ """
+ Return a siteinfo property.
+
+ It will never throw an APIError if it only stated, that the siteinfo
+ property doesn't exist. Instead it will use the default value.
+
+ @param key: The name of the siteinfo property.
+ @param get_default: Whether to throw an KeyError if the key is invalid.
+ @param cache: Caches the result internally so that future accesses via
+ this method won't query the server.
+ @param expiry: If the cache is older than the expiry it ignores the
+ cache and queries the server to get the newest value.
+ @type expiry: int/float (days), L{datetime.timedelta},
+ False (never expired), True (always expired)
+ @return: The gathered property
+ @rtype: various
+ @raises KeyError: If the key is not a valid siteinfo property and the
+ get_default option is set to False.
+ @see: L{_get_siteinfo}
+ """
+ # If expiry is a float or int convert to timedelta
+ # Note: bool is an instance of int
+ if isinstance(expiry, float) or type(expiry) == int:
+ expiry = datetime.timedelta(expiry)
+
+ # expire = 0 (or timedelta(0)) are always expired and their bool is
+ # False, so skip them EXCEPT if it's literally False, then they expire
+ # never.
+ if expiry and expiry is not True or expiry is False:
+ try:
+ cached = self._get_cached(key)
+ except KeyError:
+ pass
+ else: # cached value available
+ # is a default value, but isn't accepted
+ if not cached[1] and not get_default:
+ raise KeyError(key)
+ if not Siteinfo._is_expired(cached[1], expiry):
+ return copy.deepcopy(cached[0])
+
+ preloaded = self._get_general(key, expiry)
+ if not preloaded:
+ preloaded = self._get_siteinfo(key, expiry)[key]
+ else:
+ cache = False
+
+ if not preloaded[1] and not get_default:
+ raise KeyError(key)
+
+ if cache:
+ self._cache[key] = preloaded
+
+ return copy.deepcopy(preloaded[0])
+
+ def _get_cached(self, key: str):
+ """Return the cached value or a KeyError exception if not cached."""
+ if 'general' in self._cache:
+ if key in self._cache['general'][0]:
+ return (self._cache['general'][0][key],
+ self._cache['general'][1])
+ else:
+ return self._cache[key]
+ raise KeyError(key)
+
+ def __contains__(self, key: str) -> bool:
+ """Return whether the value is cached."""
+ try:
+ self._get_cached(key)
+ except KeyError:
+ return False
+ else:
+ return True
+
+ def is_recognised(self, key: str) -> Optional[bool]:
+ """Return if 'key' is a valid property name. 'None' if not cached."""
+ time = self.get_requested_time(key)
+ return None if time is None else bool(time)
+
+ def get_requested_time(self, key: str):
+ """
+ Return when 'key' was successfully requested from the server.
+
+ If the property is actually in the siprop 'general' it returns the
+ last request from the 'general' siprop.
+
+ @param key: The siprop value or a property of 'general'.
+ @return: The last time the siprop of 'key' was requested.
+ @rtype: None (never), False (default), L{datetime.datetime} (cached)
+ """
+ with suppress(KeyError):
+ return self._get_cached(key)[1]
+
+ return None
+
+ def __call__(self, key='general', force=False, dump=False):
+ """DEPRECATED: Return the entry for key or dump the complete cache."""
+ issue_deprecation_warning(
+ 'Calling siteinfo', 'itself as a dictionary', since='20161221'
+ )
+ result = self.get(key, expiry=force)
+ if not dump:
+ return result
+ else:
+ return self._cache
diff --git a/tests/__init__.py b/tests/__init__.py
index fe69b79..b762659 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -105,6 +105,7 @@
'site',
'site_decorators',
'site_detect',
+ 'siteinfo',
'sparql',
'tests',
'textlib',
diff --git a/tests/site_tests.py b/tests/site_tests.py
index 6286be3..20b94b7 100644
--- a/tests/site_tests.py
+++ b/tests/site_tests.py
@@ -7,23 +7,21 @@
#
import pickle
import random
-import re
import threading
import time
from collections.abc import Iterable, Mapping
from contextlib import suppress
-from datetime import datetime
import pywikibot
-from pywikibot import async_request, config, page_put_queue
from pywikibot.comms import http
+from pywikibot import config
from pywikibot.data import api
from pywikibot.exceptions import HiddenKeyError
from pywikibot.tools import suppress_warnings
-from tests import patch, unittest_print, MagicMock
+from tests import patch, unittest_print
from tests.aspects import (
AlteredDefaultSiteTestCase,
DefaultDrySiteTestCase,
@@ -37,7 +35,6 @@
WikidataTestCase,
)
from tests.basepage import BasePageLoadRevisionsCachingTestBase
-from tests.utils import entered_loop
class TokenTestBase(TestCaseBase):
@@ -2381,104 +2378,6 @@
self.assertLength(mypage._revisions, 12)
-class TestSiteInfo(DefaultSiteTestCase):
-
- """Test cases for Site metadata and capabilities."""
-
- cached = True
-
- def test_siteinfo(self):
- """Test the siteinfo property."""
- # general enteries
- mysite = self.get_site()
- self.assertIsInstance(mysite.siteinfo['timeoffset'], (int, float))
- self.assertTrue(-12 * 60 <= mysite.siteinfo['timeoffset'] <= +14 * 60)
- self.assertEqual(mysite.siteinfo['timeoffset'] % 15, 0)
- self.assertRegex(mysite.siteinfo['timezone'],
- '([A-Z]{3,4}|[A-Z][a-z]+/[A-Z][a-z]+)')
- self.assertIn(mysite.siteinfo['case'], ['first-letter',
- 'case-sensitive'])
- self.assertIsInstance(
- datetime.strptime(mysite.siteinfo['time'], '%Y-%m-%dT%H:%M:%SZ'),
- datetime)
- self.assertEqual(re.findall(r'\$1', mysite.siteinfo['articlepath']),
- ['$1'])
-
- def test_siteinfo_boolean(self):
- """Test conversion of boolean properties from empty strings."""
- mysite = self.get_site()
- self.assertIsInstance(mysite.siteinfo['titleconversion'], bool)
-
- self.assertIsInstance(mysite.namespaces[0].subpages, bool)
- self.assertIsInstance(mysite.namespaces[0].content, bool)
-
- def test_properties_with_defaults(self):
- """Test the siteinfo properties with defaults."""
- # This does not test that the defaults work correct,
- # unless the default site is a version needing these defaults
- # 'fileextensions' introduced in v1.15:
- self.assertIsInstance(self.site.siteinfo.get('fileextensions'), list)
- self.assertIn('fileextensions', self.site.siteinfo)
- fileextensions = self.site.siteinfo.get('fileextensions')
- self.assertIn({'ext': 'png'}, fileextensions)
- # 'restrictions' introduced in v1.23:
- mysite = self.site
- self.assertIsInstance(mysite.siteinfo.get('restrictions'), dict)
- self.assertIn('restrictions', mysite.siteinfo)
- restrictions = self.site.siteinfo.get('restrictions')
- self.assertIn('cascadinglevels', restrictions)
-
- def test_no_cache(self):
- """Test siteinfo caching can be disabled."""
- if 'fileextensions' in self.site.siteinfo._cache:
- del self.site.siteinfo._cache['fileextensions']
- self.site.siteinfo.get('fileextensions', cache=False)
- self.assertNotIn('fileextensions', self.site.siteinfo)
-
- def test_not_exists(self):
- """Test accessing a property not in siteinfo."""
- not_exists = 'this-property-does-not-exist'
- mysite = self.site
- self.assertRaises(KeyError, mysite.siteinfo.__getitem__, not_exists)
- self.assertNotIn(not_exists, mysite.siteinfo)
- self.assertIsEmpty(mysite.siteinfo.get(not_exists))
- self.assertFalse(entered_loop(mysite.siteinfo.get(not_exists)))
- self.assertFalse(
- entered_loop(mysite.siteinfo.get(not_exists).items()))
- self.assertFalse(
- entered_loop(mysite.siteinfo.get(not_exists).values()))
- self.assertFalse(entered_loop(mysite.siteinfo.get(not_exists).keys()))
-
-
-class TestSiteinfoDry(DefaultDrySiteTestCase):
-
- """Test Siteinfo in dry mode."""
-
- def test_siteinfo_timestamps(self):
- """Test that cache has the timestamp of CachedRequest."""
- site = self.get_site()
- request_mock = MagicMock()
- request_mock.submit = lambda: {'query': {'_prop': '_value'}}
- request_mock._cachetime = '_cache_time'
- with patch.object(site, '_request', return_value=request_mock):
- siteinfo = pywikibot.site.Siteinfo(site)
- result = siteinfo._get_siteinfo('_prop', False)
- self.assertEqual(result, {'_prop': ('_value', '_cache_time')})
-
-
-class TestSiteinfoAsync(DefaultSiteTestCase):
-
- """Test asynchronous siteinfo fetch."""
-
- def test_async_request(self):
- """Test async request."""
- self.assertTrue(page_put_queue.empty())
- self.assertNotIn('statistics', self.site.siteinfo)
- async_request(self.site.siteinfo.get, 'statistics')
- page_put_queue.join()
- self.assertIn('statistics', self.site.siteinfo)
-
-
class TestSiteLoadRevisionsCaching(BasePageLoadRevisionsCachingTestBase,
DefaultSiteTestCase):
diff --git a/tests/siteinfo_tests.py b/tests/siteinfo_tests.py
new file mode 100644
index 0000000..6716f0a
--- /dev/null
+++ b/tests/siteinfo_tests.py
@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+"""Tests for the site module."""
+#
+# (C) Pywikibot team, 2008-2020
+#
+# Distributed under the terms of the MIT license.
+#
+import re
+
+from contextlib import suppress
+from datetime import datetime
+
+import pywikibot
+
+from pywikibot import async_request, page_put_queue
+
+from tests import patch, MagicMock
+from tests.aspects import (
+ unittest, DefaultSiteTestCase, DefaultDrySiteTestCase,
+)
+from tests.utils import entered_loop
+
+
+class TestSiteInfo(DefaultSiteTestCase):
+
+ """Test cases for Site metadata and capabilities."""
+
+ cached = True
+
+ def test_siteinfo(self):
+ """Test the siteinfo property."""
+ # general enteries
+ mysite = self.get_site()
+ self.assertIsInstance(mysite.siteinfo['timeoffset'], (int, float))
+ self.assertTrue(-12 * 60 <= mysite.siteinfo['timeoffset'] <= +14 * 60)
+ self.assertEqual(mysite.siteinfo['timeoffset'] % 15, 0)
+ self.assertRegex(mysite.siteinfo['timezone'],
+ '([A-Z]{3,4}|[A-Z][a-z]+/[A-Z][a-z]+)')
+ self.assertIn(mysite.siteinfo['case'], ['first-letter',
+ 'case-sensitive'])
+ self.assertIsInstance(
+ datetime.strptime(mysite.siteinfo['time'], '%Y-%m-%dT%H:%M:%SZ'),
+ datetime)
+ self.assertEqual(re.findall(r'\$1', mysite.siteinfo['articlepath']),
+ ['$1'])
+
+ def test_siteinfo_boolean(self):
+ """Test conversion of boolean properties from empty strings."""
+ mysite = self.get_site()
+ self.assertIsInstance(mysite.siteinfo['titleconversion'], bool)
+
+ self.assertIsInstance(mysite.namespaces[0].subpages, bool)
+ self.assertIsInstance(mysite.namespaces[0].content, bool)
+
+ def test_properties_with_defaults(self):
+ """Test the siteinfo properties with defaults."""
+ # This does not test that the defaults work correct,
+ # unless the default site is a version needing these defaults
+ # 'fileextensions' introduced in v1.15:
+ self.assertIsInstance(self.site.siteinfo.get('fileextensions'), list)
+ self.assertIn('fileextensions', self.site.siteinfo)
+ fileextensions = self.site.siteinfo.get('fileextensions')
+ self.assertIn({'ext': 'png'}, fileextensions)
+ # 'restrictions' introduced in v1.23:
+ mysite = self.site
+ self.assertIsInstance(mysite.siteinfo.get('restrictions'), dict)
+ self.assertIn('restrictions', mysite.siteinfo)
+ restrictions = self.site.siteinfo.get('restrictions')
+ self.assertIn('cascadinglevels', restrictions)
+
+ def test_no_cache(self):
+ """Test siteinfo caching can be disabled."""
+ if 'fileextensions' in self.site.siteinfo._cache:
+ del self.site.siteinfo._cache['fileextensions']
+ self.site.siteinfo.get('fileextensions', cache=False)
+ self.assertNotIn('fileextensions', self.site.siteinfo)
+
+ def test_not_exists(self):
+ """Test accessing a property not in siteinfo."""
+ not_exists = 'this-property-does-not-exist'
+ mysite = self.site
+ self.assertRaises(KeyError, mysite.siteinfo.__getitem__, not_exists)
+ self.assertNotIn(not_exists, mysite.siteinfo)
+ self.assertIsEmpty(mysite.siteinfo.get(not_exists))
+ self.assertFalse(entered_loop(mysite.siteinfo.get(not_exists)))
+ self.assertFalse(
+ entered_loop(mysite.siteinfo.get(not_exists).items()))
+ self.assertFalse(
+ entered_loop(mysite.siteinfo.get(not_exists).values()))
+ self.assertFalse(entered_loop(mysite.siteinfo.get(not_exists).keys()))
+
+
+class TestSiteinfoDry(DefaultDrySiteTestCase):
+
+ """Test Siteinfo in dry mode."""
+
+ def test_siteinfo_timestamps(self):
+ """Test that cache has the timestamp of CachedRequest."""
+ site = self.get_site()
+ request_mock = MagicMock()
+ request_mock.submit = lambda: {'query': {'_prop': '_value'}}
+ request_mock._cachetime = '_cache_time'
+ with patch.object(site, '_request', return_value=request_mock):
+ siteinfo = pywikibot.site.Siteinfo(site)
+ result = siteinfo._get_siteinfo('_prop', False)
+ self.assertEqual(result, {'_prop': ('_value', '_cache_time')})
+
+
+class TestSiteinfoAsync(DefaultSiteTestCase):
+
+ """Test asynchronous siteinfo fetch."""
+
+ def test_async_request(self):
+ """Test async request."""
+ self.assertTrue(page_put_queue.empty())
+ self.assertNotIn('statistics', self.site.siteinfo)
+ async_request(self.site.siteinfo.get, 'statistics')
+ page_put_queue.join()
+ self.assertIn('statistics', self.site.siteinfo)
+
+
+if __name__ == '__main__': # pragma: no cover
+ with suppress(SystemExit):
+ unittest.main()
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/616320
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: I8a87dc2bf0b178aa862448153dbffa3fcfe81f79
Gerrit-Change-Number: 616320
Gerrit-PatchSet: 16
Gerrit-Owner: Xqt <info(a)gno.de>
Gerrit-Reviewer: Dvorapa <dvorapa(a)seznam.cz>
Gerrit-Reviewer: Matěj Suchánek <matejsuchanek97(a)gmail.com>
Gerrit-Reviewer: Xqt <info(a)gno.de>
Gerrit-Reviewer: jenkins-bot
Gerrit-CC: Huji <huji.huji(a)gmail.com>
Gerrit-MessageType: merged