Repository: sphinx-doc/sphinx

Issue:
:private-members: + :undoc-members: + private instance attributes = AttributeError
Sphinx version: 1.6.3  
Python version: 3.5.2 (CPython)  
Docutils version: 0.13.1 release  
Jinja2 version: 2.9.6

`foo.py`:
```
class Foo:
    """ This is a class. """

    def __init__(self, bar, baz):
        #: This is a public attribute.
        self.bar = bar
        #: This is a private attribute.
        self._baz = baz
```

`index.rst`:
```
===
Foo
===

.. automodule:: foo
    :members:
    :undoc-members:
    :private-members:
```

`conf.py`:
```
import os.path
import sys
sys.path.insert(0, os.path.abspath('.'))

master_doc = 'index'
extensions = ['sphinx.ext.autodoc']
exclude_patterns = ['_build', 'venv']
```

`sphinx-build -b html -W . _build/html`:
```
Running Sphinx v1.6.3
loading pickled environment... done
building [mo]: targets for 0 po files that are out of date
building [html]: targets for 1 source files that are out of date
updating environment: 0 added, 1 changed, 0 removed
reading sources... [100%] index

Warning, treated as error:
/home/jwodder/tmp/sphinx-bug/index.rst:5: (WARNING/2) autodoc: failed to import attribute 'Foo._baz' from module 'foo'; the following exception was raised:
Traceback (most recent call last):
  File "/home/jwodder/tmp/sphinx-bug/venv/lib/python3.5/site-packages/sphinx/util/inspect.py", line 169, in safe_getattr
    return getattr(obj, name, *defargs)
AttributeError: type object 'Foo' has no attribute '_baz'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/jwodder/tmp/sphinx-bug/venv/lib/python3.5/site-packages/sphinx/ext/autodoc.py", line 664, in import_object
    obj = self.get_attr(obj, part)
  File "/home/jwodder/tmp/sphinx-bug/venv/lib/python3.5/site-packages/sphinx/ext/autodoc.py", line 554, in get_attr
    return safe_getattr(obj, name, *defargs)
  File "/home/jwodder/tmp/sphinx-bug/venv/lib/python3.5/site-packages/sphinx/util/inspect.py", line 185, in safe_getattr
    raise AttributeError(name)
AttributeError: _baz
```

If `:private-members:` is removed, the documentation builds successfully.  It also builds successfully if only `:undoc-members:` is removed, though in that case `Foo._baz` doesn't show up in the output!


Discussion:

Patch:
diff --git a/CHANGES b/CHANGES
index e850b3910..c7c273390 100644
--- a/CHANGES
+++ b/CHANGES
@@ -27,6 +27,7 @@ Bugs fixed
 * #4026: nature: Fix macOS Safari scrollbar color
 * #3877: Fix for C++ multiline signatures.
 * #4006: Fix crash on parallel build
+* #3969: private instance attributes causes AttributeError
 
 Testing
 --------
diff --git a/sphinx/ext/autodoc.py b/sphinx/ext/autodoc.py
index 91feb6a2a..bb5f78332 100644
--- a/sphinx/ext/autodoc.py
+++ b/sphinx/ext/autodoc.py
@@ -962,14 +962,20 @@ def filter_members(self, members, want_all):
                     self.options.special_members is not ALL and \
                         membername in self.options.special_members:
                     keep = has_doc or self.options.undoc_members
+            elif (namespace, membername) in attr_docs:
+                if want_all and membername.startswith('_'):
+                    # ignore members whose name starts with _ by default
+                    keep = self.options.private_members and \
+                        (has_doc or self.options.undoc_members)
+                else:
+                    # keep documented attributes
+                    keep = True
+                isattr = True
+                print(membername, keep)
             elif want_all and membername.startswith('_'):
                 # ignore members whose name starts with _ by default
                 keep = self.options.private_members and \
                     (has_doc or self.options.undoc_members)
-            elif (namespace, membername) in attr_docs:
-                # keep documented attributes
-                keep = True
-                isattr = True
             else:
                 # ignore undocumented members if :undoc-members: is not given
                 keep = has_doc or self.options.undoc_members


Test patch:
diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py
index e04e38bb3..6eb087567 100644
--- a/tests/test_autodoc.py
+++ b/tests/test_autodoc.py
@@ -107,7 +107,7 @@ def process_signature(app, what, name, obj, options, args, retann):
 def skip_member(app, what, name, obj, skip, options):
     if name in ('__special1__', '__special2__'):
         return skip
-    if name.startswith('_'):
+    if name.startswith('__'):
         return True
     if name == 'skipmeth':
         return True
@@ -756,6 +756,7 @@ def assert_order(items, objtype, name, member_order, **kw):
 
     # test autodoc_member_order == 'source'
     directive.env.ref_context['py:module'] = 'test_autodoc'
+    options.private_members = True
     if PY3:
         roger_line = '   .. py:classmethod:: Class.roger(a, *, b=2, c=3, d=4, e=5, f=6)'
     else:
@@ -773,6 +774,7 @@ def assert_order(items, objtype, name, member_order, **kw):
                   '   .. py:classmethod:: Class.moore(a, e, f) -> happiness',
                   '   .. py:attribute:: Class.inst_attr_comment',
                   '   .. py:attribute:: Class.inst_attr_string',
+                  '   .. py:attribute:: Class._private_inst_attr',
                   '   .. py:method:: Class.inheritedmeth()',
                   ],
                  'class', 'Class', member_order='bysource', all_members=True)
@@ -989,6 +991,7 @@ def __init__(self, arg):
         self.inst_attr_comment = None
         self.inst_attr_string = None
         """a documented instance attribute"""
+        self._private_inst_attr = None  #: a private instance attribute
 
     def __special1__(self):
         """documented special method"""