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()
--
To view, visit
https://gerrit.wikimedia.org/r/135385
To unsubscribe, visit
https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: If7ca06adbb4b9932bba0abffc7588afcb320e934
Gerrit-PatchSet: 22
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Owner: John Vandenberg <jayvdb(a)gmail.com>
Gerrit-Reviewer: JAn DudÃk <jan.dudik(a)gmail.com>
Gerrit-Reviewer: John Vandenberg <jayvdb(a)gmail.com>
Gerrit-Reviewer: Ladsgroup <ladsgroup(a)gmail.com>
Gerrit-Reviewer: Legoktm <legoktm.wikipedia(a)gmail.com>
Gerrit-Reviewer: Merlijn van Deen <valhallasw(a)arctus.nl>
Gerrit-Reviewer: Mpaa <mpaa.wiki(a)gmail.com>
Gerrit-Reviewer: Ricordisamoa <ricordisamoa(a)openmailbox.org>
Gerrit-Reviewer: XZise <CommodoreFabianus(a)gmx.de>
Gerrit-Reviewer: jenkins-bot <>