1"""
2    test_pycode
3    ~~~~~~~~~~~
4
5    Test pycode.
6
7    :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
8    :license: BSD, see LICENSE for details.
9"""
10
11import os
12import sys
13
14import pytest
15
16import sphinx
17from sphinx.errors import PycodeError
18from sphinx.pycode import ModuleAnalyzer
19
20SPHINX_MODULE_PATH = os.path.splitext(sphinx.__file__)[0] + '.py'
21
22
23def test_ModuleAnalyzer_get_module_source():
24    assert ModuleAnalyzer.get_module_source('sphinx') == (sphinx.__file__, sphinx.__loader__.get_source('sphinx'))
25
26    # failed to obtain source information from builtin modules
27    with pytest.raises(PycodeError):
28        ModuleAnalyzer.get_module_source('builtins')
29    with pytest.raises(PycodeError):
30        ModuleAnalyzer.get_module_source('itertools')
31
32
33def test_ModuleAnalyzer_for_string():
34    analyzer = ModuleAnalyzer.for_string('print("Hello world")', 'module_name')
35    assert analyzer.modname == 'module_name'
36    assert analyzer.srcname == '<string>'
37
38
39def test_ModuleAnalyzer_for_file():
40    analyzer = ModuleAnalyzer.for_string(SPHINX_MODULE_PATH, 'sphinx')
41    assert analyzer.modname == 'sphinx'
42    assert analyzer.srcname == '<string>'
43
44
45def test_ModuleAnalyzer_for_module(rootdir):
46    analyzer = ModuleAnalyzer.for_module('sphinx')
47    assert analyzer.modname == 'sphinx'
48    assert analyzer.srcname in (SPHINX_MODULE_PATH,
49                                os.path.abspath(SPHINX_MODULE_PATH))
50
51    path = rootdir / 'test-pycode'
52    sys.path.insert(0, path)
53    try:
54        analyzer = ModuleAnalyzer.for_module('cp_1251_coded')
55        docs = analyzer.find_attr_docs()
56        assert docs == {('', 'X'): ['It MUST look like X="\u0425"', '']}
57    finally:
58        sys.path.pop(0)
59
60
61def test_ModuleAnalyzer_for_file_in_egg(rootdir):
62    try:
63        path = rootdir / 'test-pycode-egg' / 'sample-0.0.0-py3.7.egg'
64        sys.path.insert(0, path)
65
66        import sample
67        analyzer = ModuleAnalyzer.for_file(sample.__file__, 'sample')
68        docs = analyzer.find_attr_docs()
69        assert docs == {('', 'CONSTANT'): ['constant on sample.py', '']}
70    finally:
71        sys.path.pop(0)
72
73
74def test_ModuleAnalyzer_for_module_in_egg(rootdir):
75    try:
76        path = rootdir / 'test-pycode-egg' / 'sample-0.0.0-py3.7.egg'
77        sys.path.insert(0, path)
78
79        analyzer = ModuleAnalyzer.for_module('sample')
80        docs = analyzer.find_attr_docs()
81        assert docs == {('', 'CONSTANT'): ['constant on sample.py', '']}
82    finally:
83        sys.path.pop(0)
84
85
86def test_ModuleAnalyzer_find_tags():
87    code = ('class Foo(object):\n'  # line: 1
88            '    """class Foo!"""\n'
89            '    def __init__(self):\n'
90            '       pass\n'
91            '\n'
92            '    def bar(self, arg1, arg2=True, *args, **kwargs):\n'
93            '       """method Foo.bar"""\n'
94            '       pass\n'
95            '\n'
96            '    class Baz(object):\n'
97            '       def __init__(self):\n'  # line: 11
98            '           pass\n'
99            '\n'
100            'def qux():\n'
101            '   """function baz"""\n'
102            '   pass\n'
103            '\n'
104            '@decorator1\n'
105            '@decorator2\n'
106            'def quux():\n'
107            '   pass\n'  # line: 21
108            '\n'
109            'class Corge(object):\n'
110            '    @decorator1\n'
111            '    @decorator2\n'
112            '    def grault(self):\n'
113            '        pass\n')
114    analyzer = ModuleAnalyzer.for_string(code, 'module')
115    tags = analyzer.find_tags()
116    assert set(tags.keys()) == {'Foo', 'Foo.__init__', 'Foo.bar',
117                                'Foo.Baz', 'Foo.Baz.__init__', 'qux', 'quux',
118                                'Corge', 'Corge.grault'}
119    assert tags['Foo'] == ('class', 1, 12)  # type, start, end
120    assert tags['Foo.__init__'] == ('def', 3, 4)
121    assert tags['Foo.bar'] == ('def', 6, 8)
122    assert tags['Foo.Baz'] == ('class', 10, 12)
123    assert tags['Foo.Baz.__init__'] == ('def', 11, 12)
124    assert tags['qux'] == ('def', 14, 16)
125    assert tags['quux'] == ('def', 18, 21)
126    assert tags['Corge'] == ('class', 23, 27)
127    assert tags['Corge.grault'] == ('def', 24, 27)
128
129
130def test_ModuleAnalyzer_find_attr_docs():
131    code = ('class Foo(object):\n'
132            '    """class Foo!"""\n'
133            '    #: comment before attr1\n'
134            '    attr1 = None\n'
135            '    attr2 = None  # attribute comment for attr2 (without colon)\n'
136            '    attr3 = None  #: attribute comment for attr3\n'
137            '    attr4 = None  #: long attribute comment\n'
138            '                  #: for attr4\n'
139            '    #: comment before attr5\n'
140            '    attr5 = None  #: attribute comment for attr5\n'
141            '    attr6, attr7 = 1, 2  #: this comment is ignored\n'
142            '\n'
143            '    def __init__(self):\n'
144            '       self.attr8 = None  #: first attribute comment (ignored)\n'
145            '       self.attr8 = None  #: attribute comment for attr8\n'
146            '       #: comment before attr9\n'
147            '       self.attr9 = None  #: comment after attr9\n'
148            '       "string after attr9"\n'
149            '\n'
150            '    def bar(self, arg1, arg2=True, *args, **kwargs):\n'
151            '       """method Foo.bar"""\n'
152            '       pass\n'
153            '\n'
154            'def baz():\n'
155            '   """function baz"""\n'
156            '   pass\n'
157            '\n'
158            'class Qux: attr1 = 1; attr2 = 2')
159    analyzer = ModuleAnalyzer.for_string(code, 'module')
160    docs = analyzer.find_attr_docs()
161    assert set(docs) == {('Foo', 'attr1'),
162                         ('Foo', 'attr3'),
163                         ('Foo', 'attr4'),
164                         ('Foo', 'attr5'),
165                         ('Foo', 'attr6'),
166                         ('Foo', 'attr7'),
167                         ('Foo', 'attr8'),
168                         ('Foo', 'attr9')}
169    assert docs[('Foo', 'attr1')] == ['comment before attr1', '']
170    assert docs[('Foo', 'attr3')] == ['attribute comment for attr3', '']
171    assert docs[('Foo', 'attr4')] == ['long attribute comment', '']
172    assert docs[('Foo', 'attr4')] == ['long attribute comment', '']
173    assert docs[('Foo', 'attr5')] == ['attribute comment for attr5', '']
174    assert docs[('Foo', 'attr6')] == ['this comment is ignored', '']
175    assert docs[('Foo', 'attr7')] == ['this comment is ignored', '']
176    assert docs[('Foo', 'attr8')] == ['attribute comment for attr8', '']
177    assert docs[('Foo', 'attr9')] == ['string after attr9', '']
178    assert analyzer.tagorder == {'Foo': 0,
179                                 'Foo.__init__': 8,
180                                 'Foo.attr1': 1,
181                                 'Foo.attr2': 2,
182                                 'Foo.attr3': 3,
183                                 'Foo.attr4': 4,
184                                 'Foo.attr5': 5,
185                                 'Foo.attr6': 6,
186                                 'Foo.attr7': 7,
187                                 'Foo.attr8': 10,
188                                 'Foo.attr9': 12,
189                                 'Foo.bar': 13,
190                                 'baz': 14,
191                                 'Qux': 15,
192                                 'Qux.attr1': 16,
193                                 'Qux.attr2': 17}
194