jenkins-bot merged this change.

View Change

Approvals: Xqt: Looks good to me, approved jenkins-bot: Verified
Sphinx autodoc workaround for Family classproperty-s

Sphinx uses getattr() on the class in order to get the attribute.
This works for normal instance properties, since it gets the property
instance itself, but for classproperties, it gets whatever the
classproperty code returns. This not only loses all documentation
the classproperty code have, and can potentially cause side effects.

Other methods to workaround the issue are described in: This includes subclassing the
return value (unknown consequences to user), metaclass property
(not extensible), proxy object (error-prone and ugly),and indirect
get method (breaks backwards-compatibility entirely). This workaround
makes Sphinx uses a custom getattr we defined in docs/ for
getting attributes of Family subclass, where it iterates through the
MRO looking for a definition. If it's defined as a classproperty,
it returns the classproperty instance instead; else it falls back
to sphinx's safe_getattr().

Other changes in this patch include:
* make all families defined in abstract and
* make classproperty fetch its docstring from its wrapped function
* make docs/ have its `from script.` import after insert to
sys.path so that it is easier to run sphinx locally.
* make no longer treat empty languages_by_size
as valid, because empty languages_by_size is always defined via
MRO and `hasattr(cls, 'languages_by_size')` is never False. This
also prevents being accessed directly.

Change-Id: I790580b8d5d9e6349a26ab52c8859a7148441bff
M docs/
M pywikibot/
M pywikibot/tools/
3 files changed, 36 insertions(+), 10 deletions(-)

diff --git a/docs/ b/docs/
index 14b44e8..dadc26c 100644
--- a/docs/
+++ b/docs/
@@ -22,9 +22,6 @@
import re
import sys

-from scripts.cosmetic_changes import warning
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
@@ -262,9 +259,10 @@
os.environ['PYWIKIBOT2_NO_USER_CONFIG'] = '1'

-def pywikibot_script_docstring_fixups(
- app, what, name, obj, options, lines):
+def pywikibot_script_docstring_fixups(app, what, name, obj, options, lines):
"""Pywikibot specific conversions."""
+ from scripts.cosmetic_changes import warning
if what != "module":

@@ -326,10 +324,35 @@
return skip or name in exclusions

+def pywikibot_family_classproperty_getattr(obj, name, *defargs):
+ """Custom getattr() to get classproperty instances."""
+ from sphinx.util.inspect import safe_getattr
+ from import Family
+ from import classproperty
+ if not isinstance(obj, type) or not issubclass(obj, Family):
+ return safe_getattr(obj, name, *defargs)
+ for base_class in obj.__mro__:
+ try:
+ prop = base_class.__dict__[name]
+ except KeyError:
+ continue
+ if not isinstance(prop, classproperty):
+ return safe_getattr(obj, name, *defargs)
+ return prop
+ else:
+ return safe_getattr(obj, name, *defargs)
def setup(app):
"""Implicit Sphinx extension hook."""
app.connect('autodoc-process-docstring', pywikibot_script_docstring_fixups)
app.connect('autodoc-skip-member', pywikibot_skip_members)
+ app.add_autodoc_attrgetter(type, pywikibot_family_classproperty_getattr)

diff --git a/pywikibot/ b/pywikibot/
index e5513c0..ae52a7b 100644
--- a/pywikibot/
+++ b/pywikibot/
@@ -46,9 +46,11 @@

def __new__(cls):
- if cls is Family:
- raise TypeError('Base Family class cannot be instantiated; '
- 'subclass it instead')
+ # any Family class defined in this file are abstract
+ if cls in globals().values():
+ raise TypeError(
+ 'Abstract Family class {0} cannot be instantiated; '
+ 'subclass it instead'.format(cls.__name__))

# Override classproperty
cls.instance = super(Family, cls).__new__(cls)
@@ -1544,7 +1546,7 @@
def codes(cls):
"""Property listing family codes."""
- if hasattr(cls, 'languages_by_size'):
+ if cls.languages_by_size:
return cls.languages_by_size
raise NotImplementedError(
'Family %s needs property "languages_by_size" or "codes"'
diff --git a/pywikibot/tools/ b/pywikibot/tools/
index 9a7f1ff..b0303d8 100644
--- a/pywikibot/tools/
+++ b/pywikibot/tools/
@@ -129,7 +129,7 @@
class classproperty(object): # noqa: N801

- Metaclass to accesss a class method as a property.
+ Descriptor class to accesss a class method as a property.

This class may be used as a decorator::

@@ -147,6 +147,7 @@
def __init__(self, cls_method):
"""Hold the class method."""
self.method = cls_method
+ self.__doc__ = self.method.__doc__

def __get__(self, instance, owner):
"""Get the attribute of the owner class by its method."""

To view, visit change 435676. To unsubscribe, visit settings.

Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-MessageType: merged
Gerrit-Change-Id: I790580b8d5d9e6349a26ab52c8859a7148441bff
Gerrit-Change-Number: 435676
Gerrit-PatchSet: 5
Gerrit-Owner: Zhuyifei1999 <>
Gerrit-Reviewer: Dvorapa <>
Gerrit-Reviewer: John Vandenberg <>
Gerrit-Reviewer: Xqt <>
Gerrit-Reviewer: Zhuyifei1999 <>
Gerrit-Reviewer: Zoranzoki21 <>
Gerrit-Reviewer: jenkins-bot <>