Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions sphinx/deprecation.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
from typing import Any, Dict, Type


class RemovedInSphinx40Warning(DeprecationWarning):
pass


class RemovedInSphinx50Warning(DeprecationWarning):
pass

Expand Down
87 changes: 85 additions & 2 deletions sphinx/ext/autodoc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2658,10 +2658,89 @@ class PropertyDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): #
# before AttributeDocumenter
priority = AttributeDocumenter.priority + 1

@staticmethod
def _find_declared_attribute(owner: Any, name: str) -> Any:
if not isinstance(owner, type):
return None

for base in inspect.getmro(owner):
if name in base.__dict__:
return base.__dict__[name]

return None

@classmethod
def _get_property_descriptor(cls, owner: Any, name: str) -> Optional[property]:
if owner is None:
return None

raw = cls._find_declared_attribute(owner, name)
descriptor = cls._unwrap_property(raw)
if descriptor:
return descriptor

metaclass = getattr(owner, '__class__', None)
if isinstance(owner, type) and isinstance(metaclass, type):
raw = cls._find_declared_attribute(metaclass, name)
descriptor = cls._unwrap_property(raw)
if descriptor:
return descriptor

return None

@staticmethod
def _unwrap_property(candidate: Any) -> Optional[property]:
if inspect.isproperty(candidate):
return candidate

if inspect.isclassmethod(candidate):
unwrapped = getattr(candidate, '__wrapped__', None)
if unwrapped is None:
unwrapped = getattr(candidate, '__func__', None)
if inspect.isproperty(unwrapped):
return unwrapped

return None

@staticmethod
def _is_abstract_property(descriptor: Any) -> bool:
if descriptor is None:
return False

if inspect.isabstractmethod(descriptor):
return True

fget = safe_getattr(descriptor, 'fget', None)
return bool(getattr(fget, '__isabstractmethod__', False))

@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool:
return inspect.isproperty(member) and isinstance(parent, ClassDocumenter)
if not isinstance(parent, ClassDocumenter):
return False

if inspect.isproperty(member):
return True

owner = getattr(parent, 'object', None)
descriptor = cls._get_property_descriptor(owner, membername)
return descriptor is not None

def import_object(self, raiseerror: bool = False) -> bool:
ret = super().import_object(raiseerror)
if not ret:
return ret

name = self.object_name or (self.objpath[-1] if self.objpath else None)
descriptor = None
if name:
descriptor = self._get_property_descriptor(self.parent, name)

if descriptor:
self.object = descriptor

self._property_is_abstract = self._is_abstract_property(self.object)
return ret

def document_members(self, all_members: bool = False) -> None:
pass
Expand All @@ -2673,7 +2752,11 @@ def get_real_modname(self) -> str:
def add_directive_header(self, sig: str) -> None:
super().add_directive_header(sig)
sourcename = self.get_sourcename()
if inspect.isabstractmethod(self.object):
is_abstract = getattr(self, '_property_is_abstract', None)
if is_abstract is None:
is_abstract = inspect.isabstractmethod(self.object)

if is_abstract:
self.add_line(' :abstractmethod:', sourcename)

if safe_getattr(self.object, 'fget', None) and self.config.autodoc_typehints != 'none':
Expand Down
38 changes: 37 additions & 1 deletion sphinx/ext/autosummary/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
from sphinx.util import logging, rst
from sphinx.util.docutils import (NullReporter, SphinxDirective, SphinxRole, new_document,
switch_source_input)
from sphinx.util.inspect import safe_getattr
from sphinx.util.matching import Matcher
from sphinx.util.typing import OptionSpec
from sphinx.writers.html import HTMLTranslator
Expand Down Expand Up @@ -221,10 +222,45 @@ def get_documenter(app: Sphinx, obj: Any, parent: Any) -> Type[Documenter]:
parent_doc = parent_doc_cls(FakeDirective(), parent.__name__)
else:
parent_doc = parent_doc_cls(FakeDirective(), "")
parent_doc.object = parent

membername = ''
if parent is not None:
candidate_names = []
try:
candidate_names.extend(dir(parent))
except Exception:
candidate_names = []

if isinstance(parent, type):
for meta in inspect.getmro(parent.__class__):
candidate_names.extend(meta.__dict__.keys())

checked = set()
for attr in candidate_names:
if attr in checked:
continue
checked.add(attr)

try:
value = safe_getattr(parent, attr)
except AttributeError:
continue

if value is obj:
membername = attr
break

try:
if value == obj:
membername = attr
break
except Exception:
continue

# Get the corrent documenter class for *obj*
classes = [cls for cls in app.registry.documenters.values()
if cls.can_document_member(obj, '', False, parent_doc)]
if cls.can_document_member(obj, membername, False, parent_doc)]
if classes:
classes.sort(key=lambda cls: cls.priority)
return classes[-1]
Expand Down
46 changes: 46 additions & 0 deletions tests/roots/test-ext-autodoc/target/classmethod_properties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from __future__ import annotations

from abc import ABCMeta, abstractmethod


class Basic:
"""Container for basic class level descriptor."""

@classmethod
@property
def value(cls) -> int:
"""Class-level counter."""
return 1


class Inherited(Basic):
"""Subclass used to verify inherited descriptors."""


class AbstractBase(metaclass=ABCMeta):
@classmethod
@property
@abstractmethod
def token(cls) -> str:
"""Abstract identifier."""
return NotImplemented


class AbstractImpl(AbstractBase):
@classmethod
@property
def token(cls) -> str:
"""Real identifier."""
return "impl"


class PropertyMeta(type):
@classmethod
@property
def label(mcls) -> str:
"""Metaclass provided label."""
return "meta"


class WithMeta(metaclass=PropertyMeta):
"""Target class exposing metaclass property."""
17 changes: 17 additions & 0 deletions tests/roots/test-ext-autosummary-classmethod/conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import os
import sys


sys.path.insert(0, os.path.abspath('.'))


extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.autosummary',
]

autosummary_generate = True
autosummary_generate_overwrite = True

master_doc = 'index'
source_suffix = '.rst'
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
target.classmethod\_properties.AbstractBase.token
=================================================

.. currentmodule:: target.classmethod_properties

.. autoproperty:: AbstractBase.token
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
target.classmethod\_properties.Basic.value
==========================================

.. currentmodule:: target.classmethod_properties

.. autoproperty:: Basic.value
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
target.classmethod\_properties.WithMeta.label
=============================================

.. currentmodule:: target.classmethod_properties

.. autoproperty:: WithMeta.label
9 changes: 9 additions & 0 deletions tests/roots/test-ext-autosummary-classmethod/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Classmethod property autosummary
================================

.. autosummary::
:toctree: generated

target.classmethod_properties.Basic.value
target.classmethod_properties.AbstractBase.token
target.classmethod_properties.WithMeta.label
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from __future__ import annotations

from abc import ABCMeta, abstractmethod


class Basic:
@classmethod
@property
def value(cls) -> int:
"""Class-level counter."""
return 1


class Inherited(Basic):
pass


class AbstractBase(metaclass=ABCMeta):
@classmethod
@property
@abstractmethod
def token(cls) -> str:
"""Abstract identifier."""
return NotImplemented


class AbstractImpl(AbstractBase):
@classmethod
@property
def token(cls) -> str:
"""Real identifier."""
return "impl"


class PropertyMeta(type):
@classmethod
@property
def label(mcls) -> str:
"""Metaclass provided label."""
return "meta"


class WithMeta(metaclass=PropertyMeta):
pass
71 changes: 71 additions & 0 deletions tests/test_ext_autodoc_autoproperty.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,74 @@ def test_properties(app):
' docstring',
'',
]


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_classmethod_property(app):
actual = do_autodoc(app, 'property', 'target.classmethod_properties.Basic.value')
assert list(actual) == [
'',
'.. py:property:: Basic.value',
' :module: target.classmethod_properties',
' :type: int',
'',
' Class-level counter.',
'',
]


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_classmethod_property_inherited(app):
actual = do_autodoc(app, 'property', 'target.classmethod_properties.Inherited.value')
assert list(actual) == [
'',
'.. py:property:: Inherited.value',
' :module: target.classmethod_properties',
' :type: int',
'',
' Class-level counter.',
'',
]


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_classmethod_property_abstract(app):
actual = do_autodoc(app, 'property', 'target.classmethod_properties.AbstractBase.token')
assert list(actual) == [
'',
'.. py:property:: AbstractBase.token',
' :module: target.classmethod_properties',
' :abstractmethod:',
' :type: str',
'',
' Abstract identifier.',
'',
]


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_classmethod_property_concrete(app):
actual = do_autodoc(app, 'property', 'target.classmethod_properties.AbstractImpl.token')
assert list(actual) == [
'',
'.. py:property:: AbstractImpl.token',
' :module: target.classmethod_properties',
' :type: str',
'',
' Real identifier.',
'',
]


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_classmethod_property_metaclass(app):
actual = do_autodoc(app, 'property', 'target.classmethod_properties.WithMeta.label')
assert list(actual) == [
'',
'.. py:property:: WithMeta.label',
' :module: target.classmethod_properties',
' :type: str',
'',
' Metaclass provided label.',
'',
]
Loading