Repository: Chilipp/autodocsumm

Issue:
Add support for autoexception directive
The Sphinx autodoc extension supports a directive `autoexception` (https://documentation.help/Sphinx/autodoc.html#directive-autoexception).

Specifying `:autosummary:` on the `autoexception` directive fails:
```
.../docs/client/exceptions.rst:49: WARNING: An option to autoexception is either unknown or has an invalid value: 'autosummary'
```

For consistency, it would be great to enable its use. I believe the `autoexception` directive behaves pretty much the same as the `autoclass` directive, but I did not dig deep into this. The autodoc documentation is not very specific about it, unfortunately.

Discussion:

Patch:
diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml
index 9f40900..304131a 100644
--- a/.github/workflows/python-app.yml
+++ b/.github/workflows/python-app.yml
@@ -75,31 +75,3 @@ jobs:
         "sphinxcontrib-jsmath<1.0.1"
         "sphinxcontrib-qthelp<1.0.7"
         "sphinxcontrib-serializinghtml<1.1.10"'
-
-
-  build-legacy-sphinx-30plus:
-    name: Build
-
-    strategy:
-      fail-fast: false
-      matrix:
-        python-version: [ "3.7", "3.8", "3.9" ]
-        sphinx-version: [
-          "3.0.*",  # possible range: 3.0.0 - 3.5.4
-        ]
-        include:
-          - python-version: "3.7"
-            sphinx-version: "3.5.*"  # latest version that supports py3.7
-    uses: ./.github/workflows/build.yml
-    with:
-      python-version: ${{ matrix.python-version }}
-      extra-requirements: '\
-        "sphinx==${{ matrix.sphinx-version }}"
-        "jinja2<3.1"
-        "alabaster<0.7.14"
-        "sphinxcontrib-applehelp<1.0.8"
-        "sphinxcontrib-devhelp<1.0.6"
-        "sphinxcontrib-htmlhelp<2.0.5"
-        "sphinxcontrib-jsmath<1.0.1"
-        "sphinxcontrib-qthelp<1.0.7"
-        "sphinxcontrib-serializinghtml<1.1.10"'
diff --git a/autodocsumm/__init__.py b/autodocsumm/__init__.py
index 6219741..b97a611 100755
--- a/autodocsumm/__init__.py
+++ b/autodocsumm/__init__.py
@@ -53,7 +53,7 @@ from sphinx.util.docutils import SphinxDirective
 
 from sphinx.ext.autodoc import (
     ClassDocumenter, ModuleDocumenter, ALL, PycodeError,
-    ModuleAnalyzer, AttributeDocumenter, DataDocumenter, Options,
+    ModuleAnalyzer, AttributeDocumenter, DataDocumenter, Options, ExceptionDocumenter,
     Documenter, prepare_docstring)
 import sphinx.ext.autodoc as ad
 
@@ -349,7 +349,7 @@ class AutoSummModuleDocumenter(ModuleDocumenter, AutosummaryDocumenter):
 
     #: slightly higher priority than
     #: :class:`sphinx.ext.autodoc.ModuleDocumenter`
-    priority = ModuleDocumenter.priority + 0.1
+    priority = ModuleDocumenter.priority + 0.1  # type: ignore[assignment]
 
     #: original option_spec from :class:`sphinx.ext.autodoc.ModuleDocumenter`
     #: but with additional autosummary boolean option
@@ -399,7 +399,7 @@ class AutoSummClassDocumenter(ClassDocumenter, AutosummaryDocumenter):
 
     #: slightly higher priority than
     #: :class:`sphinx.ext.autodoc.ClassDocumenter`
-    priority = ClassDocumenter.priority + 0.1
+    priority = ClassDocumenter.priority + 0.1  # type: ignore[assignment]
 
     #: original option_spec from :class:`sphinx.ext.autodoc.ClassDocumenter`
     #: but with additional autosummary boolean option
@@ -437,11 +437,64 @@ class AutoSummClassDocumenter(ClassDocumenter, AutosummaryDocumenter):
             self.add_autosummary(relative_ref_paths=True)
 
 
+class AutoSummExceptionDocumenter(ExceptionDocumenter, AutosummaryDocumenter):
+    """Exception Documenter with autosummary tables for its members.
+
+    This class has the same functionality as the base
+    :class:`sphinx.ext.autodoc.ExceptionDocumenter` class but with an 
+    additional `autosummary` option to provide the ability to provide a summary 
+    of all methods and attributes.
+    It's priority is slightly higher than the one of the ExceptionDocumenter
+    """
+
+    #: slightly higher priority than
+    #: :class:`sphinx.ext.autodoc.ExceptionDocumenter`
+    priority = ExceptionDocumenter.priority + 0.1  # type: ignore[assignment]
+
+    #: original option_spec from 
+    #: :class:`sphinx.ext.autodoc.ExceptionDocumenter` but with additional 
+    #: autosummary boolean option
+    option_spec = ExceptionDocumenter.option_spec.copy()
+    option_spec['autosummary'] = bool_option
+    option_spec['autosummary-no-nesting'] = bool_option
+    option_spec['autosummary-sections'] = list_option
+    option_spec['autosummary-no-titles'] = bool_option
+    option_spec['autosummary-force-inline'] = bool_option
+    option_spec['autosummary-nosignatures'] = bool_option
+
+    #: Add options for members for the autosummary
+    for _option in member_options.intersection(option_spec):
+        option_spec['autosummary-' + _option] = option_spec[_option]
+    del _option
+
+    member_sections = {
+        ad.ExceptionDocumenter.member_order: 'Classes',
+        ad.MethodDocumenter.member_order: 'Methods',
+        ad.AttributeDocumenter.member_order: 'Attributes',
+    }
+    """:class:`dict` that includes the autosummary sections
+
+    This dictionary defines the sections for the autosummmary option. The
+    values correspond to the :attr:`sphinx.ext.autodoc.Documenter.member_order`
+    attribute that shall be used for each section."""
+
+    def add_content(self, *args, **kwargs):
+        super().add_content(*args, **kwargs)
+
+        # If the class is already documented under another name, Sphinx
+        # documents it as data/attribute. In this case, we do not want to
+        # generate an autosummary of the class for the attribute (see #69).
+        if not self.doc_as_attr:
+            self.add_autosummary(relative_ref_paths=True)
+
+
 class CallableDataDocumenter(DataDocumenter):
     """:class:`sphinx.ext.autodoc.DataDocumenter` that uses the __call__ attr
     """
 
-    priority = DataDocumenter.priority + 0.1
+    #: slightly higher priority than
+    #: :class:`sphinx.ext.autodoc.DataDocumenter`
+    priority = DataDocumenter.priority + 0.1  # type: ignore[assignment]
 
     def format_args(self):
         # for classes, the relevant signature is the __init__ method's
@@ -474,6 +527,8 @@ class CallableDataDocumenter(DataDocumenter):
 
         doc = []
         for docstring in docstrings:
+            encoding = _get_arg("encoding", 0, None, *args, **kwargs)
+            ignore = _get_arg("ignore", 1, 1, *args, **kwargs)
             if not isinstance(docstring, str):
                 docstring = force_decode(docstring, encoding)
             doc.append(prepare_docstring(docstring, ignore))
@@ -486,7 +541,9 @@ class CallableAttributeDocumenter(AttributeDocumenter):
     attr
     """
 
-    priority = AttributeDocumenter.priority + 0.1
+    #: slightly higher priority than
+    #: :class:`sphinx.ext.autodoc.AttributeDocumenter`
+    priority = AttributeDocumenter.priority + 0.1  # type: ignore[assignment]
 
     def format_args(self):
         # for classes, the relevant signature is the __init__ method's
@@ -565,7 +622,7 @@ class NoDataDataDocumenter(CallableDataDocumenter):
     """DataDocumenter that prevents the displaying of large data"""
 
     #: slightly higher priority as the one of the CallableDataDocumenter
-    priority = CallableDataDocumenter.priority + 0.1
+    priority = CallableDataDocumenter.priority + 0.1  # type: ignore[assignment]
 
     def __init__(self, *args, **kwargs):
         super(NoDataDataDocumenter, self).__init__(*args, **kwargs)
@@ -580,7 +637,7 @@ class NoDataAttributeDocumenter(CallableAttributeDocumenter):
     """AttributeDocumenter that prevents the displaying of large data"""
 
     #: slightly higher priority as the one of the CallableAttributeDocumenter
-    priority = CallableAttributeDocumenter.priority + 0.1
+    priority = CallableAttributeDocumenter.priority + 0.1  # type: ignore[assignment]
 
     def __init__(self, *args, **kwargs):
         super(NoDataAttributeDocumenter, self).__init__(*args, **kwargs)
@@ -596,13 +653,15 @@ class AutoDocSummDirective(SphinxDirective):
 
     Usage::
 
-        .. autoclasssum:: <Class>
+        .. autoclasssumm:: <Class>
+
+        .. automodsumm:: <module>
 
-        .. automodsum:: <module>
+        .. autoexceptionsumm:: <ExceptionClass>
 
-    The directive additionally supports all options of the ``autoclass`` or
-    ``automod`` directive respectively. Sections can be a list of section titles
-    to be included. If ommitted, all sections are used.
+    The directive additionally supports all options of the ``autoclass``,
+    ``automod``, or ``autoexception`` directive respectively. Sections can be a 
+    list of section titles to be included. If ommitted, all sections are used.
     """
 
     has_content = False
@@ -616,9 +675,9 @@ class AutoDocSummDirective(SphinxDirective):
         reporter = self.state.document.reporter
 
         try:
-            source, lineno = reporter.get_source_and_line(self.lineno)
+            _, lineno = reporter.get_source_and_line(self.lineno)
         except AttributeError:
-            source, lineno = (None, None)
+            _, lineno = (None, None)
 
         # look up target Documenter
         objtype = self.name[4:-4]  # strip prefix (auto-) and suffix (-summ).
@@ -659,6 +718,7 @@ def setup(app):
     app.setup_extension('sphinx.ext.autosummary')
     app.setup_extension('sphinx.ext.autodoc')
     app.add_directive('autoclasssumm', AutoDocSummDirective)
+    app.add_directive('autoexceptionsumm', AutoDocSummDirective)
     app.add_directive('automodulesumm', AutoDocSummDirective)
 
     AUTODOC_DEFAULT_OPTIONS.extend(
@@ -673,7 +733,7 @@ def setup(app):
     registry = app.registry.documenters
     for cls in [AutoSummClassDocumenter, AutoSummModuleDocumenter,
                 CallableAttributeDocumenter, NoDataDataDocumenter,
-                NoDataAttributeDocumenter]:
+                NoDataAttributeDocumenter, AutoSummExceptionDocumenter]:
         if not issubclass(registry.get(cls.objtype), cls):
             app.add_autodocumenter(cls, override=True)
 
diff --git a/docs/conf_settings.rst b/docs/conf_settings.rst
index 7f3e923..51b1a77 100644
--- a/docs/conf_settings.rst
+++ b/docs/conf_settings.rst
@@ -78,6 +78,11 @@ Directives
     By default, this directives also sets the `:members:` option unless you
     specify `:no-members`.
 
+.. rst:directive:: autoexceptionsumm
+
+    The same as the ``autoclasssumm`` directive, just for an ``Exception`` 
+    subclass.
+
 .. rst:directive:: automodulesumm
 
     The same as the ``autoclasssumm`` directive, just for a module.
diff --git a/docs/demo_exception.rst b/docs/demo_exception.rst
new file mode 100644
index 0000000..473a1de
--- /dev/null
+++ b/docs/demo_exception.rst
@@ -0,0 +1,8 @@
+.. _demo_exception:
+
+Demo Exception
+==============
+
+.. autoexception:: dummy.MyException
+    :members:
+    :noindex:
\ No newline at end of file
diff --git a/docs/dummy.py b/docs/dummy.py
index 3383ced..d169088 100644
--- a/docs/dummy.py
+++ b/docs/dummy.py
@@ -22,5 +22,18 @@ class MyClass(object):
     some_other_attr = None
 
 
+class MyException(object):
+    """Some Exception
+
+    With some description"""
+
+    def do_something_exceptional(self):
+        """Do something exceptional"""
+        pass
+
+    #: Any instance attribute
+    some_exception_attr = None
+
+
 #: Some module data
 large_data = 'Whatever'
diff --git a/docs/examples.rst b/docs/examples.rst
index 14d274b..5d730d9 100644
--- a/docs/examples.rst
+++ b/docs/examples.rst
@@ -8,6 +8,7 @@ Examples
 
     Demo Module <demo_module>
     Demo Class <demo_class>
+    Demo Exception <demo_exception>
     Demo Grouper <demo_grouper>
 
 Including a table of contents
@@ -24,11 +25,16 @@ The *autosummary* flag introduces a small table of contents. So::
 
 produces :ref:`this <demo_module>`. And::
 
-    .. autoclass:: dummy.SomeClass
+    .. autoclass:: dummy.MyClass
         :members:
         :autosummary:
 
-produces :ref:`this <demo_class>`.
+produces :ref:`this <demo_class>`, and for exceptions::
+
+    .. autoexception:: dummy.MyException
+        :members:
+        :autosummary:
+produces :ref:`this <demo_exception>`.
 
 By default, module members are (mainly) grouped according into *Functions*,
 *Classes* and *Data*, class members are grouped into *Methods* and
@@ -178,8 +184,8 @@ section of a class, you can specify::
 Multiple sections might be separated by `;;`, e.g.
 ``:autosummary-sections: Methods ;; Attributes``.
 
-This also works for the ``autoclasssumm`` and ``automodulesumm`` directives,
-e.g.::
+This also works for the ``autoclasssumm``, ``autoexceptionsumm`` and 
+``automodulesumm`` directives, e.g.::
 
     .. autoclasssumm:: dummy.SomeClass
         :autosummary-sections: Methods
diff --git a/pyproject.toml b/pyproject.toml
index f10aeae..387fea4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,7 +33,7 @@ classifiers = [
 
 requires-python = '>= 3.7'
 dependencies = [
-    'Sphinx >= 2.2, < 9.0',
+    'Sphinx >= 4.0, < 9.0',
 ]
 
 [project.urls]


Test patch:
diff --git a/tests/test-root/dummy.py b/tests/test-root/dummy.py
index f980498..d816860 100644
--- a/tests/test-root/dummy.py
+++ b/tests/test-root/dummy.py
@@ -67,6 +67,18 @@ class TestClass(object):
     small_data = 'Should be skipped'
 
 
+class TestException(Exception):
+    """Exception test for autosummary"""
+
+    def __init__(self):
+        #: This is an exception attribute
+        self.exception_instance_attribute = 1
+
+    def test_exception_method(self):
+        """Test if the method is included"""
+        pass
+
+
 class InheritedTestClass(TestClass):
     """Class test for inherited attributes"""
 
diff --git a/tests/test-root/test_autoexceptionsumm.rst b/tests/test-root/test_autoexceptionsumm.rst
new file mode 100644
index 0000000..9daa169
--- /dev/null
+++ b/tests/test-root/test_autoexceptionsumm.rst
@@ -0,0 +1,4 @@
+Autoexceptionsumm of Dummy Exception
+====================================
+
+.. autoexceptionsumm:: dummy.TestException
diff --git a/tests/test-root/test_exception.rst b/tests/test-root/test_exception.rst
new file mode 100644
index 0000000..2ce6c71
--- /dev/null
+++ b/tests/test-root/test_exception.rst
@@ -0,0 +1,4 @@
+Dummy Exception Doc
+===================
+
+.. autoexception:: dummy.TestException
\ No newline at end of file
diff --git a/tests/test_autodocsumm.py b/tests/test_autodocsumm.py
index 2b01981..d34157c 100644
--- a/tests/test_autodocsumm.py
+++ b/tests/test_autodocsumm.py
@@ -252,6 +252,17 @@ class TestAutosummaryDocumenter:
             'DummySection'
         )
 
+    def test_exception(self, app):
+        app.build()
+        html = get_html(app, 'test_exception.html')
+
+        if sphinx_version[:2] > [3, 1]:
+            assert in_autosummary("exception_instance_attribute", html)
+        elif sphinx_version[:2] < [3, 1]:
+            assert in_autosummary("TestException.exception_instance_attribute", html)
+
+        assert in_autosummary("test_exception_method", html)
+
     @pytest.mark.skipif(
         sphinx_version[:2] < [3, 1], reason="Only available for sphinx>=3"
     )
@@ -412,6 +423,19 @@ class TestAutoDocSummDirective:
         assert in_autosummary("test_method", html)
         assert in_autosummary("test_attr", html)
 
+    def test_autoexceptionsumm(self, app):
+        """Test building the autosummary of a class."""
+        app.build()
+
+        html = get_html(app, 'test_autoexceptionsumm.html')
+
+        # the class docstring must not be in the html
+        assert "Class exception for autosummary" not in html
+
+        # test if the methods and attributes are there in a table
+        assert in_autosummary("test_exception_method", html)
+        assert in_autosummary("exception_instance_attribute", html)
+
     def test_autoclasssumm_no_titles(self, app):
         """Test building the autosummary of a class."""
         app.build()