Repository: sphinx-doc/sphinx

Issue:
Extraneous class attribute
### Problem

With current master version of the `autodoc` extension, a *undocumented* module level attribute preceding a class leaks in the class documentation.

#### Procedure to reproduce the problem
The following snippet:
```python
import logging 

logger = getLogger(__name__)

class Foo(object):
    "Bar"
```

will result in a `Foo` class having a `logger` data member with a `None` value.

##### Test case
I added the following test in `tests/test_pycode.py`:
```python
def test_ModuleAnalyzer_spurious_attr():
    code = ('import logging\n\n'
            'logger = logging.getLogger(__name__)\n\n'
            'class Foo(object):\n'
            '    """Bar"""\n')
    analyzer = ModuleAnalyzer.for_string(code, 'module')
    docs = analyzer.find_attr_docs()
    assert set(docs) == {}
```

##### Error logs / results
When I execute `python run.py test_pycode.py` I get the following:
```
test_pycode.py .....F

========================== FAILURES ===========================
______________ test_ModuleAnalyzer_spurious_attr ______________

    def test_ModuleAnalyzer_spurious_attr():
        code = ('import logging\n\n'
                'logger = logging.getLogger(__name__)\n\n'
                'class Foo(object):\n'
                '    """Bar"""\n')
        analyzer = ModuleAnalyzer.for_string(code, 'module')
        docs = analyzer.find_attr_docs()
>       assert set(docs) == {}
E       AssertionError: assert {('Foo', 'logger')} == {}
E         Use -v to get the full diff

test_pycode.py:165: AssertionError
============= 1 failed, 5 passed in 0.06 seconds ==============
```

### Environment info
- OS: Debian GNU/Linux sid
- Python version: Python 3.6.3
- Sphinx version: current master, commit c39459ea7a894128ce72644ad6dc06ebb78988e7

Discussion:

Patch:
diff --git a/.appveyor.yml b/.appveyor.yml
index a3f83394f..deaf503b7 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -18,8 +18,8 @@ environment:
 
 install:
   - C:\Python%PYTHON%\python.exe -m pip install -U pip setuptools
-  - C:\Python%PYTHON%\python.exe -m pip install docutils==%DOCUTILS% mock
-  - C:\Python%PYTHON%\python.exe -m pip install .[test,websupport]
+  - C:\Python%PYTHON%\python.exe -m pip install docutils==%DOCUTILS%
+  - C:\Python%PYTHON%\python.exe -m pip install -r test-reqs.txt
 
 # No automatic build, just run python tests
 build: off
diff --git a/.travis.yml b/.travis.yml
index 1d065b178..6df4f7cfb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,9 @@
 language: python
 sudo: false
 dist: trusty
-cache: pip
+cache:
+  directories:
+    - $HOME/.cache/pip
 python:
   - "pypy-5.4.1"
   - "3.6"
@@ -38,8 +40,7 @@ addons:
 install:
   - pip install -U pip setuptools
   - pip install docutils==$DOCUTILS
-  - pip install .[test,websupport]
-  - pip install flake8
+  - pip install -r test-reqs.txt
   - if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then python3.6 -m pip install mypy typed-ast; fi
 script:
   - flake8
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 7c8a90c6b..eca6a01ac 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -116,7 +116,7 @@ These are the basic steps needed to start developing on Sphinx.
 
    * Run the unit tests::
 
-       pip install .[test,websupport]
+       pip install -r test-reqs.txt
        make test
 
    * Again, it's useful to turn on deprecation warnings on so they're shown in
diff --git a/MANIFEST.in b/MANIFEST.in
index a5699c23c..1530c28cd 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -12,6 +12,7 @@ include sphinx-autogen.py
 include sphinx-build.py
 include sphinx-quickstart.py
 include sphinx-apidoc.py
+include test-reqs.txt
 include tox.ini
 include sphinx/locale/.tx/config
 
diff --git a/setup.py b/setup.py
index a4dd6b078..cec86eed9 100644
--- a/setup.py
+++ b/setup.py
@@ -69,19 +69,19 @@
     ],
     'test': [
         'pytest',
-        'pytest-cov',
+        'mock',  # it would be better for 'test:python_version in 2.7'
+        'simplejson',  # better: 'test:platform_python_implementation=="PyPy"'
         'html5lib',
     ],
-    'test:python_version<"3"': [
-        'enum34',
-        'mock',
-    ],
-    'test:python_version>="3"': [
-        'mypy',
-        'typed_ast',
-    ],
 }
 
+# for sdist installation with pip-1.5.6
+if sys.platform == 'win32':
+    requires.append('colorama>=0.3.5')
+
+if sys.version_info < (3, 5):
+    requires.append('typing')
+
 # Provide a "compile_catalog" command that also creates the translated
 # JavaScript files if Babel is available.
 
@@ -90,7 +90,10 @@
 try:
     from babel.messages.pofile import read_po
     from babel.messages.frontend import compile_catalog
-    from json import dump
+    try:
+        from simplejson import dump
+    except ImportError:
+        from json import dump
 except ImportError:
     pass
 else:
diff --git a/sphinx/__init__.py b/sphinx/__init__.py
index 68844b8a7..9f0a295c6 100644
--- a/sphinx/__init__.py
+++ b/sphinx/__init__.py
@@ -66,7 +66,7 @@ def main(*args, **kwargs):
         RemovedInSphinx20Warning,
         stacklevel=2,
     )
-    return build.main(*args, **kwargs)
+    build.main(*args, **kwargs)
 
 
 if __name__ == '__main__':
diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py
index 28da09c78..9b5cb13c9 100644
--- a/sphinx/pycode/parser.py
+++ b/sphinx/pycode/parser.py
@@ -52,13 +52,8 @@ def get_lvar_names(node, self=None):
         else:
             raise TypeError('The assignment %r is not instance variable' % node)
     elif node_name in ('Tuple', 'List'):
-        members = []
-        for elt in node.elts:  # type: ignore
-            try:
-                members.extend(get_lvar_names(elt, self))
-            except TypeError:
-                pass
-        return members
+        members = [get_lvar_names(elt) for elt in node.elts]  # type: ignore
+        return sum(members, [])
     elif node_name == 'Attribute':
         if node.value.__class__.__name__ == 'Name' and self and node.value.id == self_id:  # type: ignore  # NOQA
             # instance variable
@@ -67,8 +62,6 @@ def get_lvar_names(node, self=None):
             raise TypeError('The assignment %r is not instance variable' % node)
     elif node_name == 'str':
         return [node]  # type: ignore
-    elif node_name == 'Starred':
-        return get_lvar_names(node.value, self)  # type: ignore
     else:
         raise NotImplementedError('Unexpected node name %r' % node_name)
 
@@ -343,6 +336,7 @@ def visit_ClassDef(self, node):
         self.current_classes.append(node.name)
         self.add_entry(node.name)
         self.context.append(node.name)
+        self.previous = node
         for child in node.body:
             self.visit(child)
         self.context.pop()
diff --git a/sphinx/util/requests.py b/sphinx/util/requests.py
index fb8761481..3dc1a30b2 100644
--- a/sphinx/util/requests.py
+++ b/sphinx/util/requests.py
@@ -109,7 +109,7 @@ def ignore_insecure_warning(**kwargs):
 
 def _get_tls_cacert(url, config):
     # type: (unicode, Config) -> Union[str, bool]
-    """Get additional CA cert for a specific URL.
+    """Get addiotinal CA cert for a specific URL.
 
     This also returns ``False`` if verification is disabled.
     And returns ``True`` if additional CA cert not found.
diff --git a/tox.ini b/tox.ini
index 00b3c99e2..519020f98 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,15 +1,16 @@
 [tox]
-minversion=2.0
-envlist=flake8,mypy,py{27,34,35,36},pypy,du{11,12,13,14}
+envlist=flake8,py27,py34,py35,py36,pypy,du14,du13,du12,du11
 
 [testenv]
 passenv = https_proxy http_proxy no_proxy
-# TODO(stephenfin) Replace this with the 'extras' config option when tox 2.4 is
-# widely available, likely some time after the Ubuntu 18.04 release
-#
-# https://tox.readthedocs.io/en/latest/config.html#confval-extras=MULTI-LINE-LIST
-deps =
-    .[test,websupport]
+deps=
+    six
+    pytest
+    pytest-cov
+    html5lib
+    mock
+    enum34
+    typing
 setenv =
     SPHINX_TEST_TEMPDIR = {envdir}/testbuild
 commands=
@@ -17,6 +18,11 @@ commands=
         --durations 25 {posargs}
     sphinx-build -q -W -b html -d {envtmpdir}/doctrees doc {envtmpdir}/html
 
+[testenv:pypy]
+deps=
+    simplejson
+    {[testenv]deps}
+
 [testenv:du11]
 deps=
     docutils==0.11
@@ -53,6 +59,10 @@ deps=
     {[testenv]deps}
 
 [testenv:py35]
+deps=
+    mypy
+    typed_ast
+    {[testenv]deps}
 commands=
     {envpython} -Wall tests/run.py --cov=sphinx --durations 25 {posargs}
     sphinx-build -q -W -b html -d {envtmpdir}/doctrees doc {envtmpdir}/html


Test patch:
diff --git a/test-reqs.txt b/test-reqs.txt
new file mode 100644
index 000000000..3a7bde8ea
--- /dev/null
+++ b/test-reqs.txt
@@ -0,0 +1,20 @@
+flake8
+pytest>=3.0
+pytest-cov
+mock
+six>=1.4
+Jinja2>=2.3
+Pygments>=2.0
+docutils>=0.11
+snowballstemmer>=1.1
+babel
+sqlalchemy>=0.9
+whoosh>=2.0
+alabaster
+sphinx_rtd_theme
+sphinxcontrib-websupport
+imagesize
+requests
+html5lib
+enum34
+typing
diff --git a/tests/test_pycode_parser.py b/tests/test_pycode_parser.py
index d794376fa..8b7a945d4 100644
--- a/tests/test_pycode_parser.py
+++ b/tests/test_pycode_parser.py
@@ -9,9 +9,6 @@
     :license: BSD, see LICENSE for details.
 """
 
-import pytest
-from six import PY2
-
 from sphinx.pycode.parser import Parser
 
 
@@ -119,23 +116,6 @@ def test_complex_assignment():
     assert parser.definitions == {}
 
 
-@pytest.mark.skipif(PY2, reason='tests for py3 syntax')
-def test_complex_assignment_py3():
-    source = ('a, *b, c = (1, 2, 3, 4)  #: unpack assignment\n'
-              'd, *self.attr = (5, 6, 7)  #: unpack assignment2\n'
-              'e, *f[0] = (8, 9, 0)  #: unpack assignment3\n'
-              )
-    parser = Parser(source)
-    parser.parse()
-    assert parser.comments == {('', 'a'): 'unpack assignment',
-                               ('', 'b'): 'unpack assignment',
-                               ('', 'c'): 'unpack assignment',
-                               ('', 'd'): 'unpack assignment2',
-                               ('', 'e'): 'unpack assignment3',
-                               }
-    assert parser.definitions == {}
-
-
 def test_obj_assignment():
     source = ('obj = SomeObject()  #: some object\n'
               'obj.attr = 1  #: attr1\n'
@@ -246,6 +226,18 @@ def test_nested_class():
                                 'Foo.Bar.attr2': 3}
 
 
+def test_class_comment():
+    source = ('import logging\n'
+              'logger = logging.getLogger(__name__)\n'
+              '\n'
+              'class Foo(object):\n'
+              '    """Bar"""\n')
+    parser = Parser(source)
+    parser.parse()
+    assert parser.comments == {}
+    assert parser.definitions == {'Foo': ('class', 4, 5)}
+
+
 def test_comment_picker_multiline_string():
     source = ('class Foo(object):\n'
               '    a = None\n'