From 358e5824900a5fe14cace232aac5728e615862c0 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Tue, 14 May 2019 23:45:46 +0900 Subject: [PATCH 1/2] Add :abstractmethod: option to py:method directive (refs: #6138) --- CHANGES | 1 + doc/usage/restructuredtext/domains.rst | 9 +++++---- sphinx/domains/python.py | 3 +++ tests/test_domain_py.py | 16 +++++++++++++++- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index a90602d9d..066cc6120 100644 --- a/CHANGES +++ b/CHANGES @@ -88,6 +88,7 @@ Features added * #4777: py domain: Add ``:async:`` option to :rst:dir:`py:function` directive * py domain: Add new options to :rst:dir:`py:method` directive + - ``:abstractmethod:`` - ``:async:`` - ``:classmethod:`` - ``:property:`` diff --git a/doc/usage/restructuredtext/domains.rst b/doc/usage/restructuredtext/domains.rst index 8b06bf0e0..2c2d38ee0 100644 --- a/doc/usage/restructuredtext/domains.rst +++ b/doc/usage/restructuredtext/domains.rst @@ -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 an async method. - The ``classmethod`` option and ``staticmethod`` option can be given (with - no value) to indicate the method is a class method (or a static method). + The ``abstractmethod``, ``classmethod`` option and ``staticmethod`` option + 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 is a property. .. versionchanged:: 2.1 - ``:async:``, ``:classmethod:``, ``:property:`` and ``:staticmethod:`` - options added. + ``:abstractmethod:``, ``:async:``, ``:classmethod:``, ``:property:`` and + ``:staticmethod:`` options added. .. rst:directive:: .. py:staticmethod:: name(parameters) diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index c4971ba60..85ab1c003 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -585,6 +585,7 @@ class PyMethod(PyObject): option_spec = PyObject.option_spec.copy() option_spec.update({ + 'abstractmethod': directives.flag, 'async': directives.flag, 'classmethod': directives.flag, 'property': directives.flag, @@ -601,6 +602,8 @@ class PyMethod(PyObject): def get_signature_prefix(self, sig): # type: (str) -> str prefix = [] + if 'abstractmethod' in self.options: + prefix.append('abstract') if 'async' in self.options: prefix.append('async') if 'classmethod' in self.options: diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index fac8a838f..12f05cf85 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -335,7 +335,9 @@ def test_pymethod_options(app): " .. py:method:: meth4\n" " :async:\n" " .. py:method:: meth5\n" - " :property:\n") + " :property:\n" + " .. py:method:: meth6\n" + " :abstractmethod:\n") domain = app.env.get_domain('py') doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, @@ -350,6 +352,8 @@ def test_pymethod_options(app): addnodes.index, desc, addnodes.index, + desc, + addnodes.index, desc)])])) # method @@ -400,6 +404,16 @@ def test_pymethod_options(app): assert 'Class.meth5' in domain.objects 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): text = (".. py:class:: Class\n" From e2889999339f47731ebc050736ea2b6bc0fd9fcb Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Tue, 14 May 2019 23:45:49 +0900 Subject: [PATCH 2/2] Close #744: autodoc: Support abstractmethod --- CHANGES | 1 + sphinx/ext/autodoc/__init__.py | 7 ++- sphinx/util/inspect.py | 12 +++-- .../target/abstractmethods.py | 29 +++++++++++ tests/test_autodoc.py | 49 +++++++++++++++++++ 5 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 tests/roots/test-ext-autodoc/target/abstractmethods.py diff --git a/CHANGES b/CHANGES index 066cc6120..00230b9b4 100644 --- a/CHANGES +++ b/CHANGES @@ -81,6 +81,7 @@ Features added * #6289: autodoc: :confval:`autodoc_default_options` now supports ``imported-members`` option * #4777: autodoc: Support coroutine +* #744: autodoc: Support abstractmethod * #6212 autosummary: Add :confval:`autosummary_imported_members` to display imported members on autosummary * #6271: ``make clean`` is catastrophically broken if building into '.' diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 26936956e..d99475876 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -1338,6 +1338,8 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: sourcename = self.get_sourcename() obj = self.parent.__dict__.get(self.object_name, self.object) + if inspect.isabstractmethod(obj): + self.add_line(' :abstractmethod:', sourcename) if inspect.iscoroutinefunction(obj): self.add_line(' :async:', sourcename) if inspect.isclassmethod(obj): @@ -1455,7 +1457,10 @@ class PropertyDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): # def add_directive_header(self, sig): # type: (str) -> None 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): diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 58eca7ffb..ba0056879 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -172,6 +172,12 @@ def isdescriptor(x): 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): # type: (Any) -> bool """Check if the object is an attribute like descriptor.""" @@ -231,7 +237,7 @@ def isproperty(obj): def safe_getattr(obj, name, *defargs): - # type: (Any, str, str) -> object + # type: (Any, str, Any) -> Any """A getattr() that turns all exceptions into AttributeErrors.""" try: 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__] 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 getattr(builtins, safe_getattr(cls, '__name__', '')) is cls # type: ignore + return getattr(builtins, safe_getattr(cls, '__name__', '')) is cls class Parameter: diff --git a/tests/roots/test-ext-autodoc/target/abstractmethods.py b/tests/roots/test-ext-autodoc/target/abstractmethods.py new file mode 100644 index 000000000..a4396d5c9 --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/abstractmethods.py @@ -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 diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index 7561d808b..3d3a5943c 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -1485,6 +1485,55 @@ def test_mocked_module_imports(app, warning): 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') def test_partialfunction(): options = {"members": None}