jenkins-bot has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/core/+/871226 )
Change subject: [FEAT] Support federated Wikibase ......................................................................
[FEAT] Support federated Wikibase
Entity sources are hardcoded in family files. Complete backward compatibility, no deprecation.
Bug: T173195 Change-Id: Idc9890d099a9d7cf973af94fb468fe2d0da84f85 --- M pywikibot/families/commons_family.py M pywikibot/site/_datasite.py M pywikibot/page/_wikibase.py M tests/file_tests.py M pywikibot/families/wikidata_family.py 5 files changed, 154 insertions(+), 20 deletions(-)
Approvals: Xqt: Looks good to me, approved jenkins-bot: Verified
diff --git a/pywikibot/families/commons_family.py b/pywikibot/families/commons_family.py index 718f3e8..41b3303 100644 --- a/pywikibot/families/commons_family.py +++ b/pywikibot/families/commons_family.py @@ -45,3 +45,66 @@ .. versionadded:: 6.5 """ return 'DataSite' + + def calendarmodel(self, code) -> str: + """Default calendar model for WbTime datatype.""" + return 'http://www.wikidata.org/entity/Q1985727' + + def default_globe(self, code) -> str: + """Default globe for Coordinate datatype.""" + return 'earth' + + def globes(self, code): + """Supported globes for Coordinate datatype.""" + return { + 'ariel': 'http://www.wikidata.org/entity/Q3343', + 'bennu': 'http://www.wikidata.org/entity/Q11558', + 'callisto': 'http://www.wikidata.org/entity/Q3134', + 'ceres': 'http://www.wikidata.org/entity/Q596', + 'deimos': 'http://www.wikidata.org/entity/Q7548', + 'dione': 'http://www.wikidata.org/entity/Q15040', + 'earth': 'http://www.wikidata.org/entity/Q2', + 'enceladus': 'http://www.wikidata.org/entity/Q3303', + 'eros': 'http://www.wikidata.org/entity/Q16711', + 'europa': 'http://www.wikidata.org/entity/Q3143', + 'ganymede': 'http://www.wikidata.org/entity/Q3169', + 'gaspra': 'http://www.wikidata.org/entity/Q158244', + 'hyperion': 'http://www.wikidata.org/entity/Q15037', + 'iapetus': 'http://www.wikidata.org/entity/Q17958', + 'io': 'http://www.wikidata.org/entity/Q3123', + 'jupiter': 'http://www.wikidata.org/entity/Q319', + 'lutetia': 'http://www.wikidata.org/entity/Q107556', + 'mars': 'http://www.wikidata.org/entity/Q111', + 'mercury': 'http://www.wikidata.org/entity/Q308', + 'mimas': 'http://www.wikidata.org/entity/Q15034', + 'miranda': 'http://www.wikidata.org/entity/Q3352', + 'moon': 'http://www.wikidata.org/entity/Q405', + 'oberon': 'http://www.wikidata.org/entity/Q3332', + 'phobos': 'http://www.wikidata.org/entity/Q7547', + 'phoebe': 'http://www.wikidata.org/entity/Q17975', + 'pluto': 'http://www.wikidata.org/entity/Q339', + 'rhea': 'http://www.wikidata.org/entity/Q15050', + 'ryugu': 'http://www.wikidata.org/entity/Q1385178', + 'steins': 'http://www.wikidata.org/entity/Q150249', + 'tethys': 'http://www.wikidata.org/entity/Q15047', + 'titan': 'http://www.wikidata.org/entity/Q2565', + 'titania': 'http://www.wikidata.org/entity/Q3322', + 'triton': 'http://www.wikidata.org/entity/Q3359', + 'umbriel': 'http://www.wikidata.org/entity/Q3338', + 'venus': 'http://www.wikidata.org/entity/Q313', + 'vesta': 'http://www.wikidata.org/entity/Q3030', + } + + def entity_sources(self, code): + if code == 'commons': + return { + 'item': ('wikidata', 'wikidata'), + 'property': ('wikidata', 'wikidata'), + } + if code in ('test', 'beta'): + return { + 'item': (code, 'wikidata'), + 'property': (code, 'wikidata'), + } + + return {} # default diff --git a/pywikibot/families/wikidata_family.py b/pywikibot/families/wikidata_family.py index 335ad86..9030a5c 100644 --- a/pywikibot/families/wikidata_family.py +++ b/pywikibot/families/wikidata_family.py @@ -93,3 +93,6 @@ 'venus': 'http://www.wikidata.org/entity/Q313', 'vesta': 'http://www.wikidata.org/entity/Q3030', } + + def entity_sources(self, code): + return {} diff --git a/pywikibot/page/_wikibase.py b/pywikibot/page/_wikibase.py index d2af3ac..080c265 100644 --- a/pywikibot/page/_wikibase.py +++ b/pywikibot/page/_wikibase.py @@ -276,6 +276,10 @@ value = cls.fromJSON(self._content.get(key, {}), self.repo) setattr(self, key, value) data[key] = value + # xxx: need better handling for this + if key in ['claims', 'statements']: + value.set_on_item(self) + return data
def editEntity( @@ -340,9 +344,17 @@ title_pattern = r'M[1-9]\d*' DATA_ATTRIBUTES = { 'labels': LanguageDict, - # TODO: 'statements': ClaimCollection, + 'statements': ClaimCollection, }
+ def __getattr__(self, name): + if name == 'claims': # T149410 + name = 'statements' + if hasattr(self, name): + return getattr(self, name) + + return super().__getattr__(name) + @property def file(self) -> FilePage: """Get the file associated with the mediainfo.""" @@ -577,10 +589,6 @@ if 'pageid' in self._content: self._pageid = self._content['pageid']
- # xxx: this is ugly - if 'claims' in data: - self.claims.set_on_item(self) - return data
@property @@ -1357,12 +1365,17 @@
TARGET_CONVERTER = { 'wikibase-item': lambda value, site: - ItemPage(site, 'Q' + str(value['numeric-id'])), + ItemPage(site.get_repo_for_entity_type('item'), + 'Q' + str(value['numeric-id'])), 'wikibase-property': lambda value, site: - PropertyPage(site, 'P' + str(value['numeric-id'])), - 'wikibase-lexeme': lambda value, site: LexemePage(site, value['id']), - 'wikibase-form': lambda value, site: LexemeForm(site, value['id']), - 'wikibase-sense': lambda value, site: LexemeSense(site, value['id']), + PropertyPage(site.get_repo_for_entity_type('property'), + 'P' + str(value['numeric-id'])), + 'wikibase-lexeme': lambda value, site: + LexemePage(site.get_repo_for_entity_type('lexeme'), value['id']), + 'wikibase-form': lambda value, site: + LexemeForm(site.get_repo_for_entity_type('lexeme'), value['id']), + 'wikibase-sense': lambda value, site: + LexemeSense(site.get_repo_for_entity_type('lexeme'), value['id']), 'commonsMedia': lambda value, site: FilePage(pywikibot.Site('commons'), value), # T90492 'globe-coordinate': pywikibot.Coordinate.fromWikibase, @@ -1392,7 +1405,9 @@
Defined by the "snak" value, supplemented by site + pid
- :param site: repository the claim is on + :param site: Repository where the property of the claim is defined. + Note that this does not have to correspond to the repository + where the claim has been stored. :type site: pywikibot.site.DataSite :param pid: property id, with "P" prefix :param snak: snak identifier for claim @@ -1528,7 +1543,8 @@
:rtype: pywikibot.page.Claim """ - claim = cls(site, data['mainsnak']['property'], + claim_repo = site.get_repo_for_entity_type('property') + claim = cls(claim_repo, data['mainsnak']['property'], datatype=data['mainsnak'].get('datatype', None)) if 'id' in data: claim.snak = data['id'] @@ -1538,7 +1554,7 @@ if claim.getSnakType() == 'value': value = data['mainsnak']['datavalue']['value'] # The default covers string, url types - if claim.type in cls.types or claim.type == 'wikibase-property': + if claim.type in cls.types: claim.target = cls.TARGET_CONVERTER.get( claim.type, lambda value, site: value)(value, site) else: @@ -1678,8 +1694,8 @@ if value: self.setTarget(value)
- data = self.repo.changeClaimTarget(self, snaktype=snaktype, - **kwargs) + data = self.on_item.repo.changeClaimTarget(self, snaktype=snaktype, + **kwargs) # TODO: Re-create the entire item from JSON, not just id self.snak = data['claim']['id'] self.on_item.latest_revision_id = data['pageinfo']['lastrevid'] @@ -1729,7 +1745,7 @@ self._assert_mainsnak('Cannot change rank on a {}') self._assert_attached() self.rank = rank - return self.repo.save_claim(self, **kwargs) + return self.on_item.repo.save_claim(self, **kwargs)
def changeSnakType(self, value=None, **kwargs) -> None: """ @@ -1767,7 +1783,8 @@ raise ValueError( 'The provided Claim instance is already used in an entity') if self.on_item is not None: - data = self.repo.editSource(self, claims, new=True, **kwargs) + data = self.on_item.repo.editSource(self, claims, new=True, + **kwargs) self.on_item.latest_revision_id = data['pageinfo']['lastrevid'] for claim in claims: claim.hash = data['reference']['hash'] @@ -1796,7 +1813,7 @@ """ self._assert_mainsnak('Cannot remove sources from a {}') self._assert_attached() - data = self.repo.removeSources(self, sources, **kwargs) + data = self.on_item.repo.removeSources(self, sources, **kwargs) self.on_item.latest_revision_id = data['pageinfo']['lastrevid'] for source in sources: source_dict = defaultdict(list) @@ -1814,7 +1831,7 @@ raise ValueError( 'The provided Claim instance is already used in an entity') if self.on_item is not None: - data = self.repo.editQualifier(self, qualifier, **kwargs) + data = self.on_item.repo.editQualifier(self, qualifier, **kwargs) self.on_item.latest_revision_id = data['pageinfo']['lastrevid'] qualifier.on_item = self.on_item qualifier.isQualifier = True @@ -1841,7 +1858,7 @@ """ self._assert_mainsnak('Cannot remove qualifiers from a {}') self._assert_attached() - data = self.repo.remove_qualifiers(self, qualifiers, **kwargs) + data = self.on_item.repo.remove_qualifiers(self, qualifiers, **kwargs) self.on_item.latest_revision_id = data['pageinfo']['lastrevid'] for qualifier in qualifiers: self.qualifiers[qualifier.getID()].remove(qualifier) diff --git a/pywikibot/site/_datasite.py b/pywikibot/site/_datasite.py index 054452e..ab5394e 100644 --- a/pywikibot/site/_datasite.py +++ b/pywikibot/site/_datasite.py @@ -47,6 +47,29 @@ 'sense': pywikibot.LexemeSense, }
+ def get_repo_for_entity_type(self, entity_type: str) -> 'DataSite': + """ + Get the data repository for the entity type. + + When no foreign repository is defined for the entity type, + the method returns this repository itself even if it does not + support that entity type either. + + .. seealso:: https://www.mediawiki.org/wiki/Wikibase/Federation + .. versionadded:: 8.0 + + :raises ValueError: when invalid entity type was provided + """ + if entity_type not in self._type_to_class: + raise ValueError(f'Invalid entity type "{entity_type}"') + entity_sources = self.entity_sources() + if entity_type in entity_sources: + return pywikibot.Site( + *entity_sources[entity_type], + interface='DataSite', + user=self.username()) + return self + def _cache_entity_namespaces(self) -> None: """Find namespaces for each known wikibase entity type.""" self._entity_namespaces = {} diff --git a/tests/file_tests.py b/tests/file_tests.py index 64294b1..197cf94 100755 --- a/tests/file_tests.py +++ b/tests/file_tests.py @@ -9,6 +9,7 @@ import re import unittest from contextlib import suppress +from itertools import chain
import pywikibot from pywikibot.exceptions import ( @@ -320,6 +321,20 @@ self.assertEqual('M14634781', item.getID()) self.assertIsInstance( item.labels, pywikibot.page._collections.LanguageDict) + self.assertIsInstance( + item.statements, pywikibot.page._collections.ClaimCollection) + self.assertTrue(item.claims is item.statements) + + all_claims = list(chain.from_iterable(item.statements.values())) + self.assertEqual({claim.on_item for claim in all_claims}, {item}) + + claims = [claim for claim in all_claims + if isinstance(claim.target, pywikibot.page.WikibaseEntity)] + self.assertEqual({str(claim.repo) for claim in claims}, + {'wikidata:wikidata'}) + self.assertEqual({str(claim.target.repo) for claim in claims}, + {'wikidata:wikidata'}) + del item._file self.assertEqual(page, item.file)