Repository: sphinx-doc/sphinx

Issue:
autodoc: :members: list causes :special-members: not to be shown
In a `autoclass` statement, when its `:members:` option specifies a list of members to show, the special members are no longer shown.

This fragment in index.rst will show all public non-special members and the two listed special members in the output:

```
.. autoclass:: mypackage.MyClass
   :members:
   :special-members: __str__, __repr__
```

While this one produces the failure by showing the specified public members but no more special members in the output:

```
.. autoclass:: mypackage.MyClass
   :members: method1, method2
   :special-members: __str__, __repr__
```

I am using Sphinx 1.4 on CentOS 6.7.
Let me know in case you need more data.


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/sphinx-doc/sphinx/pull/5376?src=pr&el=h1) Report
> Merging [#5376](https://codecov.io/gh/sphinx-doc/sphinx/pull/5376?src=pr&el=desc) into [1.8](https://codecov.io/gh/sphinx-doc/sphinx/commit/c6e0d92d108c05126226e1f2cd84f9f02a4f4e42?src=pr&el=desc) will **decrease** coverage by `0.84%`.
> The diff coverage is `85%`.

[![Impacted file tree graph](https://codecov.io/gh/sphinx-doc/sphinx/pull/5376/graphs/tree.svg?width=650&token=VFlFSELbkw&height=150&src=pr)](https://codecov.io/gh/sphinx-doc/sphinx/pull/5376?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##              1.8    #5376      +/-   ##
==========================================
- Coverage   82.09%   81.24%   -0.85%     
==========================================
  Files         303      293      -10     
  Lines       40176    39457     -719     
  Branches     6209     5913     -296     
==========================================
- Hits        32982    32057     -925     
- Misses       5823     5996     +173     
- Partials     1371     1404      +33
```


| [Impacted Files](https://codecov.io/gh/sphinx-doc/sphinx/pull/5376?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [tests/test\_autodoc.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/5376/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9hdXRvZG9jLnB5) | `95.14% <100%> (-1.37%)` | :arrow_down: |
| [sphinx/ext/autodoc/\_\_init\_\_.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/5376/diff?src=pr&el=tree#diff-c3BoaW54L2V4dC9hdXRvZG9jL19faW5pdF9fLnB5) | `85.08% <80%> (-1.02%)` | :arrow_down: |
| [sphinx/util/pycompat.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/5376/diff?src=pr&el=tree#diff-c3BoaW54L3V0aWwvcHljb21wYXQucHk=) | `47.14% <0%> (-42.86%)` | :arrow_down: |
| [tests/test\_util\_inspect.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/5376/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF91dGlsX2luc3BlY3QucHk=) | `57.64% <0%> (-32.55%)` | :arrow_down: |
| [sphinx/util/inspect.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/5376/diff?src=pr&el=tree#diff-c3BoaW54L3V0aWwvaW5zcGVjdC5weQ==) | `49.57% <0%> (-26.19%)` | :arrow_down: |
| [sphinx/pycode/\_\_init\_\_.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/5376/diff?src=pr&el=tree#diff-c3BoaW54L3B5Y29kZS9fX2luaXRfXy5weQ==) | `78.66% <0%> (-9.34%)` | :arrow_down: |
| [tests/test\_pycode\_parser.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/5376/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9weWNvZGVfcGFyc2VyLnB5) | `91.73% <0%> (-8.27%)` | :arrow_down: |
| [tests/test\_smartquotes.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/5376/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9zbWFydHF1b3Rlcy5weQ==) | `92.5% <0%> (-7.5%)` | :arrow_down: |
| [tests/test\_pycode.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/5376/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9weWNvZGUucHk=) | `92.59% <0%> (-7.41%)` | :arrow_down: |
| [sphinx/ext/napoleon/\_\_init\_\_.py](https://codecov.io/gh/sphinx-doc/sphinx/pull/5376/diff?src=pr&el=tree#diff-c3BoaW54L2V4dC9uYXBvbGVvbi9fX2luaXRfXy5weQ==) | `68.67% <0%> (-7.23%)` | :arrow_down: |
| ... and [43 more](https://codecov.io/gh/sphinx-doc/sphinx/pull/5376/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/sphinx-doc/sphinx/pull/5376?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/sphinx-doc/sphinx/pull/5376?src=pr&el=footer). Last update [c6e0d92...1718ac3](https://codecov.io/gh/sphinx-doc/sphinx/pull/5376?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/CHANGES b/CHANGES
index dfcf2a6bd..524f18f08 100644
--- a/CHANGES
+++ b/CHANGES
@@ -31,6 +31,7 @@ Bugs fixed
 * #5282: html theme: ``pygments_style`` of theme was overrided by ``conf.py``
   by default
 * #4379: toctree shows confusible warning when document is excluded
+* #2401: autodoc: ``:members:`` causes ``:special-members:`` not to be shown
 
 Testing
 --------
diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py
index 41a6731d2..ead3a25fd 100644
--- a/sphinx/ext/autodoc/__init__.py
+++ b/sphinx/ext/autodoc/__init__.py
@@ -110,6 +110,20 @@ def bool_option(arg):
     return True
 
 
+def merge_special_members_option(options):
+    # type: (Dict) -> None
+    """Merge :special-members: option to :members: option."""
+    if 'special-members' in options and options['special-members'] is not ALL:
+        if options.get('members') is ALL:
+            pass
+        elif options.get('members'):
+            for member in options['special-members']:
+                if member not in options['members']:
+                    options['members'].append(member)
+        else:
+            options['members'] = options['special-members']
+
+
 class AutodocReporter(object):
     """
     A reporter replacement that assigns the correct source name
@@ -823,6 +837,11 @@ class ModuleDocumenter(Documenter):
         'imported-members': bool_option, 'ignore-module-all': bool_option
     }  # type: Dict[unicode, Callable]
 
+    def __init__(self, *args):
+        # type: (Any) -> None
+        super(ModuleDocumenter, self).__init__(*args)
+        merge_special_members_option(self.options)
+
     @classmethod
     def can_document_member(cls, member, membername, isattr, parent):
         # type: (Any, unicode, bool, Any) -> bool
@@ -1075,6 +1094,11 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter):  # type:
         'private-members': bool_option, 'special-members': members_option,
     }  # type: Dict[unicode, Callable]
 
+    def __init__(self, *args):
+        # type: (Any) -> None
+        super(ClassDocumenter, self).__init__(*args)
+        merge_special_members_option(self.options)
+
     @classmethod
     def can_document_member(cls, member, membername, isattr, parent):
         # type: (Any, unicode, bool, Any) -> bool
diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py
index c45576c3a..dbadfe8d5 100644
--- a/sphinx/ext/autosummary/generate.py
+++ b/sphinx/ext/autosummary/generate.py
@@ -43,11 +43,12 @@
 
 if False:
     # For type annotation
-    from typing import Any, Callable, Dict, Tuple, List  # NOQA
+    from typing import Any, Callable, Dict, List, Tuple, Type  # NOQA
     from jinja2 import BaseLoader  # NOQA
     from sphinx import addnodes  # NOQA
     from sphinx.builders import Builder  # NOQA
     from sphinx.environment import BuildEnvironment  # NOQA
+    from sphinx.ext.autodoc import Documenter  # NOQA
 
 
 class DummyApplication(object):
@@ -69,7 +70,7 @@ def setup_documenters(app):
         ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter,
         FunctionDocumenter, MethodDocumenter, AttributeDocumenter,
         InstanceAttributeDocumenter
-    ]
+    ]  # type: List[Type[Documenter]]
     for documenter in documenters:
         app.registry.add_documenter(documenter.objtype, documenter)
 


Test patch:
diff --git a/tests/py35/test_autodoc_py35.py b/tests/py35/test_autodoc_py35.py
index 51a9ef1ff..23b3eb83e 100644
--- a/tests/py35/test_autodoc_py35.py
+++ b/tests/py35/test_autodoc_py35.py
@@ -18,7 +18,7 @@
 from docutils.statemachine import ViewList
 from six import StringIO
 
-from sphinx.ext.autodoc import add_documenter, FunctionDocumenter, ALL  # NOQA
+from sphinx.ext.autodoc import add_documenter, FunctionDocumenter, ALL, Options  # NOQA
 from sphinx.testing.util import SphinxTestApp, Struct
 from sphinx.util import logging
 
@@ -49,7 +49,7 @@ def setup_test():
     global options, directive
     global processed_docstrings, processed_signatures
 
-    options = Struct(
+    options = Options(
         inherited_members = False,
         undoc_members = False,
         private_members = False,
diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py
index c9a342f94..cefceb833 100644
--- a/tests/test_autodoc.py
+++ b/tests/test_autodoc.py
@@ -21,7 +21,7 @@
 
 from sphinx.ext.autodoc import (
     AutoDirective, ModuleLevelDocumenter, cut_lines, between, ALL,
-    merge_autodoc_default_flags
+    merge_autodoc_default_flags, Options
 )
 from sphinx.ext.autodoc.directive import DocumenterBridge, process_documenter_options
 from sphinx.testing.util import SphinxTestApp, Struct  # NOQA
@@ -79,7 +79,7 @@ def setup_test():
     global options, directive
     global processed_docstrings, processed_signatures
 
-    options = Struct(
+    options = Options(
         inherited_members = False,
         undoc_members = False,
         private_members = False,
@@ -757,44 +757,40 @@ def test_autodoc_imported_members(app):
 
 @pytest.mark.sphinx('html', testroot='ext-autodoc')
 def test_autodoc_special_members(app):
-    # all special methods
-    options = {"members": None,
+    # specific special methods
+    options = {"undoc-members": None,
+               "special-members": "__init__,__special1__"}
+    actual = do_autodoc(app, 'class', 'target.Class', options)
+    assert list(filter(lambda l: '::' in l, actual)) == [
+        '.. py:class:: Class(arg)',
+        '   .. py:method:: Class.__init__(arg)',
+        '   .. py:method:: Class.__special1__()',
+    ]
+
+    # combination with specific members
+    options = {"members": "attr,docattr",
                "undoc-members": None,
-               "special-members": None}
+               "special-members": "__init__,__special1__"}
     actual = do_autodoc(app, 'class', 'target.Class', options)
     assert list(filter(lambda l: '::' in l, actual)) == [
         '.. py:class:: Class(arg)',
         '   .. py:method:: Class.__init__(arg)',
-        '   .. py:attribute:: Class.__module__',
         '   .. py:method:: Class.__special1__()',
-        '   .. py:method:: Class.__special2__()',
         '   .. py:attribute:: Class.attr',
-        '   .. py:attribute:: Class.descr',
         '   .. py:attribute:: Class.docattr',
-        '   .. py:method:: Class.excludemeth()',
-        '   .. py:attribute:: Class.inst_attr_comment',
-        '   .. py:attribute:: Class.inst_attr_inline',
-        '   .. py:attribute:: Class.inst_attr_string',
-        '   .. py:attribute:: Class.mdocattr',
-        '   .. py:method:: Class.meth()',
-        '   .. py:classmethod:: Class.moore(a, e, f) -> happiness',
-        '   .. py:attribute:: Class.prop',
-        ROGER_METHOD,
-        '   .. py:attribute:: Class.skipattr',
-        '   .. py:method:: Class.skipmeth()',
-        '   .. py:attribute:: Class.udocattr',
-        '   .. py:method:: Class.undocmeth()'
     ]
 
-    # specific special methods
+    # all special methods
     options = {"members": None,
                "undoc-members": None,
-               "special-members": "__init__,__special1__"}
+               "special-members": None}
     actual = do_autodoc(app, 'class', 'target.Class', options)
     assert list(filter(lambda l: '::' in l, actual)) == [
         '.. py:class:: Class(arg)',
         '   .. py:method:: Class.__init__(arg)',
+        '   .. py:attribute:: Class.__module__',
         '   .. py:method:: Class.__special1__()',
+        '   .. py:method:: Class.__special2__()',
         '   .. py:attribute:: Class.attr',
         '   .. py:attribute:: Class.descr',
         '   .. py:attribute:: Class.docattr',
@@ -1551,9 +1547,7 @@ def test_autodoc_default_options_with_values(app):
     assert '   .. py:attribute:: EnumCls.val4' not in actual
 
     # with :special-members:
-    # Note that :members: must be *on* for :special-members: to work.
     app.config.autodoc_default_options = {
-        'members': None,
         'special-members': '__init__,__iter__',
     }
     actual = do_autodoc(app, 'class', 'target.CustomIter')