jenkins-bot has submitted this change and it was merged. ( https://gerrit.wikimedia.org/r/435676 )
Change subject: Sphinx autodoc workaround for Family classproperty-s ......................................................................
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: https://stackoverflow.com/q/22357961. 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/conf.py 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 family.py abstract and un-instantiatable. * make classproperty fetch its docstring from its wrapped function * make docs/conf.py have its `from script.` import after insert to sys.path so that it is easier to run sphinx locally. * make SubdomainFamily.codes 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 SubdomainFamily.codes being accessed directly.
Change-Id: I790580b8d5d9e6349a26ab52c8859a7148441bff --- M docs/conf.py M pywikibot/family.py M pywikibot/tools/__init__.py 3 files changed, 36 insertions(+), 10 deletions(-)
Approvals: Xqt: Looks good to me, approved jenkins-bot: Verified
diff --git a/docs/conf.py b/docs/conf.py index 14b44e8..dadc26c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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": return
@@ -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 pywikibot.family import Family + from pywikibot.tools 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)
pywikibot_env() diff --git a/pywikibot/family.py b/pywikibot/family.py index e5513c0..ae52a7b 100644 --- a/pywikibot/family.py +++ b/pywikibot/family.py @@ -46,9 +46,11 @@
def __new__(cls): """Allocator.""" - 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 @@ @classproperty 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/__init__.py b/pywikibot/tools/__init__.py index 9a7f1ff..b0303d8 100644 --- a/pywikibot/tools/__init__.py +++ b/pywikibot/tools/__init__.py @@ -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."""
pywikibot-commits@lists.wikimedia.org