Merge pull request #6365 from tk0miya/refactor_py_domain

Add :abstractmethod: option to py:method directive and support abstractmethod by autodoc
This commit is contained in:
Takeshi KOMIYA 2019-05-19 23:46:34 +09:00 committed by GitHub
commit 557d8d140e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 118 additions and 9 deletions

View File

@ -82,6 +82,7 @@ Features added
* #6289: autodoc: :confval:`autodoc_default_options` now supports * #6289: autodoc: :confval:`autodoc_default_options` now supports
``imported-members`` option ``imported-members`` option
* #4777: autodoc: Support coroutine * #4777: autodoc: Support coroutine
* #744: autodoc: Support abstractmethod
* #6212 autosummary: Add :confval:`autosummary_imported_members` to display * #6212 autosummary: Add :confval:`autosummary_imported_members` to display
imported members on autosummary imported members on autosummary
* #6271: ``make clean`` is catastrophically broken if building into '.' * #6271: ``make clean`` is catastrophically broken if building into '.'
@ -89,6 +90,7 @@ Features added
* #4777: py domain: Add ``:async:`` option to :rst:dir:`py:function` directive * #4777: py domain: Add ``:async:`` option to :rst:dir:`py:function` directive
* py domain: Add new options to :rst:dir:`py:method` directive * py domain: Add new options to :rst:dir:`py:method` directive
- ``:abstractmethod:``
- ``:async:`` - ``:async:``
- ``:classmethod:`` - ``:classmethod:``
- ``:property:`` - ``:property:``

View File

@ -226,16 +226,17 @@ The following directives are provided for module and class contents:
The ``async`` option can be given (with no value) to indicate the method is The ``async`` option can be given (with no value) to indicate the method is
an async method. an async method.
The ``classmethod`` option and ``staticmethod`` option can be given (with The ``abstractmethod``, ``classmethod`` option and ``staticmethod`` option
no value) to indicate the method is a class method (or a static method). can be given (with no value) to indicate the method is an abstract method,
a class method or a static method.
The ``property`` option can be given (with no value) to indicate the method The ``property`` option can be given (with no value) to indicate the method
is a property. is a property.
.. versionchanged:: 2.1 .. versionchanged:: 2.1
``:async:``, ``:classmethod:``, ``:property:`` and ``:staticmethod:`` ``:abstractmethod:``, ``:async:``, ``:classmethod:``, ``:property:`` and
options added. ``:staticmethod:`` options added.
.. rst:directive:: .. py:staticmethod:: name(parameters) .. rst:directive:: .. py:staticmethod:: name(parameters)

View File

@ -585,6 +585,7 @@ class PyMethod(PyObject):
option_spec = PyObject.option_spec.copy() option_spec = PyObject.option_spec.copy()
option_spec.update({ option_spec.update({
'abstractmethod': directives.flag,
'async': directives.flag, 'async': directives.flag,
'classmethod': directives.flag, 'classmethod': directives.flag,
'property': directives.flag, 'property': directives.flag,
@ -601,6 +602,8 @@ class PyMethod(PyObject):
def get_signature_prefix(self, sig): def get_signature_prefix(self, sig):
# type: (str) -> str # type: (str) -> str
prefix = [] prefix = []
if 'abstractmethod' in self.options:
prefix.append('abstract')
if 'async' in self.options: if 'async' in self.options:
prefix.append('async') prefix.append('async')
if 'classmethod' in self.options: if 'classmethod' in self.options:

View File

@ -1336,6 +1336,8 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
sourcename = self.get_sourcename() sourcename = self.get_sourcename()
obj = self.parent.__dict__.get(self.object_name, self.object) obj = self.parent.__dict__.get(self.object_name, self.object)
if inspect.isabstractmethod(obj):
self.add_line(' :abstractmethod:', sourcename)
if inspect.iscoroutinefunction(obj): if inspect.iscoroutinefunction(obj):
self.add_line(' :async:', sourcename) self.add_line(' :async:', sourcename)
if inspect.isclassmethod(obj): if inspect.isclassmethod(obj):
@ -1453,7 +1455,10 @@ class PropertyDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): #
def add_directive_header(self, sig): def add_directive_header(self, sig):
# type: (str) -> None # type: (str) -> None
super().add_directive_header(sig) super().add_directive_header(sig)
self.add_line(' :property:', self.get_sourcename()) sourcename = self.get_sourcename()
if inspect.isabstractmethod(self.object):
self.add_line(' :abstractmethod:', sourcename)
self.add_line(' :property:', sourcename)
class InstanceAttributeDocumenter(AttributeDocumenter): class InstanceAttributeDocumenter(AttributeDocumenter):

View File

@ -172,6 +172,12 @@ def isdescriptor(x):
return False return False
def isabstractmethod(obj):
# type: (Any) -> bool
"""Check if the object is an abstractmethod."""
return safe_getattr(obj, '__isabstractmethod__', False) is True
def isattributedescriptor(obj): def isattributedescriptor(obj):
# type: (Any) -> bool # type: (Any) -> bool
"""Check if the object is an attribute like descriptor.""" """Check if the object is an attribute like descriptor."""
@ -231,7 +237,7 @@ def isproperty(obj):
def safe_getattr(obj, name, *defargs): def safe_getattr(obj, name, *defargs):
# type: (Any, str, str) -> object # type: (Any, str, Any) -> Any
"""A getattr() that turns all exceptions into AttributeErrors.""" """A getattr() that turns all exceptions into AttributeErrors."""
try: try:
return getattr(obj, name, *defargs) return getattr(obj, name, *defargs)
@ -319,9 +325,9 @@ def is_builtin_class_method(obj, attr_name):
classes = [c for c in inspect.getmro(obj) if attr_name in c.__dict__] classes = [c for c in inspect.getmro(obj) if attr_name in c.__dict__]
cls = classes[0] if classes else object cls = classes[0] if classes else object
if not hasattr(builtins, safe_getattr(cls, '__name__', '')): # type: ignore if not hasattr(builtins, safe_getattr(cls, '__name__', '')):
return False return False
return getattr(builtins, safe_getattr(cls, '__name__', '')) is cls # type: ignore return getattr(builtins, safe_getattr(cls, '__name__', '')) is cls
class Parameter: class Parameter:

View File

@ -0,0 +1,29 @@
from abc import abstractmethod
class Base():
def meth(self):
pass
@abstractmethod
def abstractmeth(self):
pass
@staticmethod
@abstractmethod
def staticmeth():
pass
@classmethod
@abstractmethod
def classmeth(cls):
pass
@property
@abstractmethod
def prop(self):
pass
@abstractmethod
async def coroutinemeth(self):
pass

View File

@ -1485,6 +1485,55 @@ def test_mocked_module_imports(app, warning):
assert warning.getvalue() == '' assert warning.getvalue() == ''
@pytest.mark.usefixtures('setup_test')
def test_abstractmethods():
options = {"members": None,
"undoc-members": None}
actual = do_autodoc(app, 'module', 'target.abstractmethods', options)
assert list(actual) == [
'',
'.. py:module:: target.abstractmethods',
'',
'',
'.. py:class:: Base',
' :module: target.abstractmethods',
'',
' ',
' .. py:method:: Base.abstractmeth()',
' :module: target.abstractmethods',
' :abstractmethod:',
' ',
' ',
' .. py:method:: Base.classmeth()',
' :module: target.abstractmethods',
' :abstractmethod:',
' :classmethod:',
' ',
' ',
' .. py:method:: Base.coroutinemeth()',
' :module: target.abstractmethods',
' :abstractmethod:',
' :async:',
' ',
' ',
' .. py:method:: Base.meth()',
' :module: target.abstractmethods',
' ',
' ',
' .. py:method:: Base.prop',
' :module: target.abstractmethods',
' :abstractmethod:',
' :property:',
' ',
' ',
' .. py:method:: Base.staticmeth()',
' :module: target.abstractmethods',
' :abstractmethod:',
' :staticmethod:',
' '
]
@pytest.mark.usefixtures('setup_test') @pytest.mark.usefixtures('setup_test')
def test_partialfunction(): def test_partialfunction():
options = {"members": None} options = {"members": None}

View File

@ -338,7 +338,9 @@ def test_pymethod_options(app):
" .. py:method:: meth4\n" " .. py:method:: meth4\n"
" :async:\n" " :async:\n"
" .. py:method:: meth5\n" " .. py:method:: meth5\n"
" :property:\n") " :property:\n"
" .. py:method:: meth6\n"
" :abstractmethod:\n")
domain = app.env.get_domain('py') domain = app.env.get_domain('py')
doctree = restructuredtext.parse(app, text) doctree = restructuredtext.parse(app, text)
assert_node(doctree, (addnodes.index, assert_node(doctree, (addnodes.index,
@ -353,6 +355,8 @@ def test_pymethod_options(app):
addnodes.index, addnodes.index,
desc, desc,
addnodes.index, addnodes.index,
desc,
addnodes.index,
desc)])])) desc)])]))
# method # method
@ -403,6 +407,16 @@ def test_pymethod_options(app):
assert 'Class.meth5' in domain.objects assert 'Class.meth5' in domain.objects
assert domain.objects['Class.meth5'] == ('index', 'method') assert domain.objects['Class.meth5'] == ('index', 'method')
# :abstractmethod:
assert_node(doctree[1][1][10], addnodes.index,
entries=[('single', 'meth6() (Class method)', 'Class.meth6', '', None)])
assert_node(doctree[1][1][11], ([desc_signature, ([desc_annotation, "abstract "],
[desc_name, "meth6"],
[desc_parameterlist, ()])],
[desc_content, ()]))
assert 'Class.meth6' in domain.objects
assert domain.objects['Class.meth6'] == ('index', 'method')
def test_pyclassmethod(app): def test_pyclassmethod(app):
text = (".. py:class:: Class\n" text = (".. py:class:: Class\n"