XZise has submitted this change and it was merged.
Change subject: Determine entity namespaces for site using API ......................................................................
Determine entity namespaces for site using API
Remove hardcoded namespace numbers for 0 and 120 replacing them with values obtained via existing siprop=namespaces query by new Namespace class.
Move namespace=0 title reparsing into ItemPage.
Description of class PropertyPage should not recommend including namespace in the title as namespace is and was already provided.
Change-Id: If7ca06adbb4b9932bba0abffc7588afcb320e934 --- M pywikibot/exceptions.py M pywikibot/page.py M pywikibot/site.py M tests/wikibase_tests.py 4 files changed, 340 insertions(+), 24 deletions(-)
Approvals: XZise: Looks good to me, approved
diff --git a/pywikibot/exceptions.py b/pywikibot/exceptions.py index 661fcf5..7feba4a 100644 --- a/pywikibot/exceptions.py +++ b/pywikibot/exceptions.py @@ -37,6 +37,7 @@
WikiBaseError: any issue specific to Wikibase. - CoordinateGlobeUnknownException: globe is not implemented yet. + - EntityTypeUnknownException: entity type is not available on the site.
""" # @@ -297,6 +298,15 @@
""" This globe is not implemented yet in either WikiBase or pywikibot """
+ pass + + +class EntityTypeUnknownException(WikiBaseError): + + """The requested entity type is not recognised on this site""" + + pass + # TODO: Warn about the deprecated usage import pywikibot.data.api UploadWarning = pywikibot.data.api.UploadWarning diff --git a/pywikibot/page.py b/pywikibot/page.py index 18c6c9c..1fc54eb 100644 --- a/pywikibot/page.py +++ b/pywikibot/page.py @@ -20,7 +20,7 @@ import sys import pywikibot from pywikibot import config -import pywikibot.site +from pywikibot.site import Namespace from pywikibot.exceptions import AutoblockUser, UserActionRefuse from pywikibot.tools import ComparableMixin, deprecated, deprecate_arg from pywikibot import textlib @@ -2612,24 +2612,96 @@ """
def __init__(self, site, title=u"", **kwargs): - """ Constructor. """ + """ Constructor. + + If title is provided, either ns or entity_type must also be provided, + and will be checked against the title parsed using the Page + initialisation logic. + + @param site: Wikibase data site + @type site: DataSite + @param title: normalized title of the page + @type title: unicode + @param ns: namespace + @type ns: Namespace instance, or int + @param entity_type: Wikibase entity type + @type entity_type: str ('item' or 'property') + + @raise TypeError: incorrect use of parameters + @raise ValueError: incorrect namespace + @raise pywikibot.Error: title parsing problems + @raise NotImplementedError: the entity type is not supported + """ if not isinstance(site, pywikibot.site.DataSite): raise TypeError("site must be a pywikibot.site.DataSite object") + if title and ('ns' not in kwargs and 'entity_type' not in kwargs): + pywikibot.debug("%s.__init__: %s title %r specified without " + "ns or entity_type" + % (self.__class__.__name__, site, title), + layer='wikibase') + + self._namespace = None + + if 'ns' in kwargs: + if isinstance(kwargs['ns'], Namespace): + self._namespace = kwargs.pop('ns') + kwargs['ns'] = self._namespace.id + else: + # numerical namespace given + ns = int(kwargs['ns']) + if site.item_namespace.id == ns: + self._namespace = site.item_namespace + elif site.property_namespace.id == ns: + self._namespace = site.property_namespace + else: + raise ValueError('%r: Namespace "%d" is not valid' + % self.site) + + if 'entity_type' in kwargs: + entity_type = kwargs.pop('entity_type') + if entity_type == 'item': + entity_type_ns = site.item_namespace + elif entity_type == 'property': + entity_type_ns = site.property_namespace + else: + raise ValueError('Wikibase entity type "%s" unknown' + % entity_type) + + if self._namespace: + if self._namespace != entity_type_ns: + raise ValueError('Namespace "%d" is not valid for Wikibase' + ' entity type "%s"' + % (kwargs['ns'], entity_type)) + else: + self._namespace = entity_type_ns + kwargs['ns'] = self._namespace.id + Page.__init__(self, site, title, **kwargs) + + # If a title was not provided, + # avoid checks which may cause an exception. + if not title: + self.repo = site + return + + if self._namespace: + if self._link.namespace != self._namespace.id: + raise ValueError(u"'%s' is not in the namespace %d" + % (title, self._namespace.id)) + else: + # Neither ns or entity_type was provided. + # Use the _link to determine entity type. + ns = self._link.namespace + if self.site.item_namespace.id == ns: + self._namespace = self.site.item_namespace + elif self.site.property_namespace.id == ns: + self._namespace = self.site.property_namespace + else: + raise ValueError('%r: Namespace "%d" is not valid' + % (self.site, ns)) + + # .site forces a parse of the Link title to determine site self.repo = self.site - - def title(self, **kwargs): - """ Page title. - - If the item was instantiated without an ID, - fetch the ID and reparse the title. - """ - if self.namespace() == 0: - self.getID() - if self._link._text != self.id: - self._link._text = self.id - del self._link._title - return Page(self).title(**kwargs)
def _defined_by(self, singular=False): """ @@ -2665,6 +2737,14 @@ params[id] = self.getID()
return params + + def namespace(self): + """Return the number of the namespace of the entity. + + @return: Namespace id + @rtype: int + """ + return self._namespace.id
def exists(self): """ @@ -2872,7 +2952,7 @@
class ItemPage(WikibasePage):
- """ A Wikibase item. + """ Wikibase entity of type 'item'.
A Wikibase item may be defined by either a 'Q' id (qid), or by a site & title. @@ -2888,9 +2968,26 @@ @param site: data repository @type site: pywikibot.site.DataSite @param title: id number of item, "Q###" + @type title: str """ - super(ItemPage, self).__init__(site, title, ns=0) - self.id = title.upper() # This might cause issues if not ns0? + super(ItemPage, self).__init__(site, title, + ns=site.item_namespace) + self.id = self._link.title.upper() + + def title(self, **kwargs): + """ + Get the title of the page. + + All optional keyword parameters are passed to the superclass. + """ + # If the item was instantiated without an ID, + # remove the existing Link title, force the Link text to be reparsed. + self.getID() + if self._link._text != self.id: + self._link._text = self.id + del self._link._title + + return super(ItemPage, self).title(**kwargs)
@classmethod def fromPage(cls, page): @@ -3107,6 +3204,8 @@ """ Constructor.
+ @param site: data repository + @type site: pywikibot.site.DataSite @param datatype: datatype of the property; if not given, it will be queried via the API @type datatype: basestring @@ -3160,7 +3259,7 @@ A Wikibase entity in the property namespace.
Should be created as: - PropertyPage(DataSite, 'Property:P21') + PropertyPage(DataSite, 'P21') """
def __init__(self, source, title=u""): @@ -3169,9 +3268,11 @@
@param source: data repository property is on @type source: pywikibot.site.DataSite - @param title: page name of property, like "Property:P##" + @param title: page name of property, like "P##" + @type title: str """ - WikibasePage.__init__(self, source, title, ns=120) + WikibasePage.__init__(self, source, title, + ns=source.property_namespace) Property.__init__(self, source, title) self.id = self.title(withNamespace=False).upper() if not self.id.startswith(u'P'): diff --git a/pywikibot/site.py b/pywikibot/site.py index 2389346..39d8e5f 100644 --- a/pywikibot/site.py +++ b/pywikibot/site.py @@ -4334,6 +4334,62 @@
class DataSite(APISite):
+ """Wikibase data capable site.""" + + def __init__(self, code, fam, user, sysop): + """Constructor.""" + APISite.__init__(self, code, fam, user, sysop) + self._item_namespace = None + self._property_namespace = None + + def _cache_entity_namespaces(self): + """Find namespaces for each known wikibase entity type.""" + self._item_namespace = False + self._property_namespace = False + + for namespace in self.namespaces().values(): + content_model = namespace.info.get('defaultcontentmodel') + if content_model == 'wikibase-item': + self._item_namespace = namespace + elif content_model == 'wikibase-property': + self._property_namespace = namespace + + @property + def item_namespace(self): + """ + Return namespace for items. + + @return: item namespace + @rtype: Namespace + """ + if self._item_namespace is None: + self._cache_entity_namespaces() + + if isinstance(self._item_namespace, Namespace): + return self._item_namespace + else: + raise pywikibot.exceptions.EntityTypeUnknownException( + '%r does not support entity type "item"' + % self) + + @property + def property_namespace(self): + """ + Return namespace for properties. + + @return: property namespace + @rtype: Namespace + """ + if self._property_namespace is None: + self._cache_entity_namespaces() + + if isinstance(self._property_namespace, Namespace): + return self._property_namespace + else: + raise pywikibot.exceptions.EntityTypeUnknownException( + '%r does not support entity type "property"' + % self) + def __getattr__(self, attr): """Provide data access methods.
diff --git a/tests/wikibase_tests.py b/tests/wikibase_tests.py index 9e1bca9..8d3698b 100644 --- a/tests/wikibase_tests.py +++ b/tests/wikibase_tests.py @@ -12,6 +12,7 @@ import pywikibot from pywikibot import pagegenerators from pywikibot.page import WikibasePage +from pywikibot.site import Namespace from pywikibot.data.api import APIError import json import copy @@ -238,11 +239,10 @@ check(title, APIError)
def test_item_untrimmed_title(self): - # spaces in the title cause an error item = pywikibot.ItemPage(wikidata, ' Q60 ') self.assertEqual(item._link._title, 'Q60') - self.assertEqual(item.title(), ' Q60 ') - self.assertRaises(APIError, item.get) + self.assertEqual(item.title(), 'Q60') + item.get()
def test_item_missing(self): # this item is deleted @@ -390,9 +390,11 @@ class TestPropertyPage(PywikibotTestCase):
def test_property_empty_property(self): + """Test creating a PropertyPage without a title.""" self.assertRaises(pywikibot.Error, pywikibot.PropertyPage, wikidata)
def test_globe_coordinate(self): + """Test a coordinate PropertyPage has the correct type.""" property_page = pywikibot.PropertyPage(wikidata, 'P625') self.assertEqual(property_page.type, 'globe-coordinate') self.assertEqual(property_page.getType(), 'globecoordinate') @@ -525,6 +527,153 @@ self.assertEqual(response, self.data_out)
+class TestNamespaces(PywikibotTestCase): + """Test cases to test namespaces of Wikibase entities""" + + def test_empty_wikibase_page(self): + # As a base class it should be able to instantiate + # it with minimal arguments + page = pywikibot.page.WikibasePage(wikidata) + self.assertRaises(AttributeError, page.namespace) + page = pywikibot.page.WikibasePage(wikidata, title='') + self.assertRaises(AttributeError, page.namespace) + + page = pywikibot.page.WikibasePage(wikidata, ns=0) + self.assertEqual(page.namespace(), 0) + page = pywikibot.page.WikibasePage(wikidata, entity_type='item') + self.assertEqual(page.namespace(), 0) + + page = pywikibot.page.WikibasePage(wikidata, ns=120) + self.assertEqual(page.namespace(), 120) + page = pywikibot.page.WikibasePage(wikidata, title='', ns=120) + self.assertEqual(page.namespace(), 120) + page = pywikibot.page.WikibasePage(wikidata, entity_type='property') + self.assertEqual(page.namespace(), 120) + + # mismatch in namespaces + self.assertRaises(ValueError, pywikibot.page.WikibasePage, wikidata, + ns=0, entity_type='property') + self.assertRaises(ValueError, pywikibot.page.WikibasePage, wikidata, + ns=120, entity_type='item') + + def test_wikibase_link_namespace(self): + """Test the title resolved to a namespace correctly.""" + # title without any namespace clues (ns or entity_type) + # should verify the Link namespace is appropriate + page = pywikibot.page.WikibasePage(wikidata, title='Q6') + self.assertEqual(page.namespace(), 0) + page = pywikibot.page.WikibasePage(wikidata, title='Property:P60') + self.assertEqual(page.namespace(), 120) + + def test_wikibase_namespace_selection(self): + """Test various ways to correctly specify the namespace.""" + page = pywikibot.page.ItemPage(wikidata, 'Q6') + self.assertEqual(page.namespace(), 0) + page = pywikibot.page.ItemPage(wikidata, title='Q6') + self.assertEqual(page.namespace(), 0) + + page = pywikibot.page.WikibasePage(wikidata, title='Q6', ns=0) + self.assertEqual(page.namespace(), 0) + page = pywikibot.page.WikibasePage(wikidata, title='Q6', + entity_type='item') + self.assertEqual(page.namespace(), 0) + + page = pywikibot.page.PropertyPage(wikidata, 'Property:P60') + self.assertEqual(page.namespace(), 120) + page = pywikibot.page.PropertyPage(wikidata, 'P60') + self.assertEqual(page.namespace(), 120) + + page = pywikibot.page.WikibasePage(wikidata, title='P60', ns=120) + self.assertEqual(page.namespace(), 120) + page = pywikibot.page.WikibasePage(wikidata, title='P60', + entity_type='property') + self.assertEqual(page.namespace(), 120) + + def test_wrong_namespaces(self): + """Test incorrect namespaces for Wikibase entities.""" + # All subclasses of WikibasePage raise a ValueError + # if the namespace for the page title is not correct + self.assertRaises(ValueError, pywikibot.page.WikibasePage, wikidata, + title='Wikidata:Main Page') + self.assertRaises(ValueError, pywikibot.ItemPage, wikidata, 'File:Q1') + self.assertRaises(ValueError, pywikibot.PropertyPage, wikidata, 'File:P60') + + def test_item_unknown_namespace(self): + """Test unknown namespaces for Wikibase entities.""" + # The 'Invalid:' is not a known namespace, so is parsed to be + # part of the title in namespace 0 + # TODO: These items have inappropriate titles, which should + # raise an error. + item = pywikibot.ItemPage(wikidata, 'Invalid:Q1') + self.assertEqual(item.namespace(), 0) + self.assertEqual(item.id, 'INVALID:Q1') + self.assertEqual(item.title(), 'INVALID:Q1') + self.assertEqual(hasattr(item, '_content'), False) + self.assertRaises(APIError, item.get) + self.assertEqual(hasattr(item, '_content'), False) + self.assertEqual(item.title(), 'INVALID:Q1') + + +class TestAlternateNamespaces(PywikibotTestCase): + """Test cases to test namespaces of Wikibase entities""" + + def setUp(self): + super(TestAlternateNamespaces, self).setUp() + + class DrySite(pywikibot.site.DataSite): + _namespaces = { + 90: Namespace(id=90, + canonical_name='Item', + defaultcontentmodel='wikibase-item'), + 92: Namespace(id=92, + canonical_name='Prop', + defaultcontentmodel='wikibase-property') + } + + __init__ = lambda *args: None + code = 'test' + family = lambda: None + family.name = 'test' + _logged_in_as = None + _siteinfo = {'case': 'first-letter'} + _item_namespace = None + _property_namespace = None + + def encoding(self): + return 'utf-8' + + def encodings(self): + return [] + + self.site = DrySite('test', 'mock', None, None) + + def test_alternate_item_namespace(self): + item = pywikibot.ItemPage(self.site, 'Q60') + self.assertEqual(item.namespace(), 90) + self.assertEqual(item.id, 'Q60') + self.assertEqual(item.title(), 'Item:Q60') + self.assertEqual(item._defined_by(), {'ids': 'Q60'}) + + item = pywikibot.ItemPage(self.site, 'Item:Q60') + self.assertEqual(item.namespace(), 90) + self.assertEqual(item.id, 'Q60') + self.assertEqual(item.title(), 'Item:Q60') + self.assertEqual(item._defined_by(), {'ids': 'Q60'}) + + def test_alternate_property_namespace(self): + prop = pywikibot.PropertyPage(self.site, 'P21') + self.assertEqual(prop.namespace(), 92) + self.assertEqual(prop.id, 'P21') + self.assertEqual(prop.title(), 'Prop:P21') + self.assertEqual(prop._defined_by(), {'ids': 'P21'}) + + prop = pywikibot.PropertyPage(self.site, 'Prop:P21') + self.assertEqual(prop.namespace(), 92) + self.assertEqual(prop.id, 'P21') + self.assertEqual(prop.title(), 'Prop:P21') + self.assertEqual(prop._defined_by(), {'ids': 'P21'}) + + if __name__ == '__main__': try: unittest.main()
pywikibot-commits@lists.wikimedia.org