summaryrefslogtreecommitdiff
path: root/sphinx/ext/autodoc/importer.py
blob: 101cb930f4e63a78e5ccf2a68b4e539514accfd0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# -*- coding: utf-8 -*-
"""
    sphinx.ext.autodoc.importer
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~

    Importer utilities for autodoc

    :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS.
    :license: BSD, see LICENSE for details.
"""

import sys
import warnings
import traceback
import contextlib
from collections import namedtuple
from types import FunctionType, MethodType, ModuleType

from six import PY2

from sphinx.util import logging
from sphinx.util.inspect import isenumclass, safe_getattr

if False:
    # For type annotation
    from typing import Any, Callable, Dict, Generator, List, Optional, Set  # NOQA

logger = logging.getLogger(__name__)


class _MockObject(object):
    """Used by autodoc_mock_imports."""

    def __init__(self, *args, **kwargs):
        # type: (Any, Any) -> None
        pass

    def __len__(self):
        # type: () -> int
        return 0

    def __contains__(self, key):
        # type: (str) -> bool
        return False

    def __iter__(self):
        # type: () -> None
        pass

    def __getitem__(self, key):
        # type: (str) -> _MockObject
        return self

    def __getattr__(self, key):
        # type: (str) -> _MockObject
        return self

    def __call__(self, *args, **kw):
        # type: (Any, Any) -> Any
        if args and type(args[0]) in [FunctionType, MethodType]:
            # Appears to be a decorator, pass through unchanged
            return args[0]
        return self


class _MockModule(ModuleType):
    """Used by autodoc_mock_imports."""
    __file__ = '/dev/null'

    def __init__(self, name, loader):
        # type: (str, _MockImporter) -> None
        self.__name__ = self.__package__ = name
        self.__loader__ = loader
        self.__all__ = []  # type: List[str]
        self.__path__ = []  # type: List[str]

    def __getattr__(self, name):
        # type: (str) -> _MockObject
        o = _MockObject()
        o.__module__ = self.__name__
        return o


class _MockImporter(object):
    def __init__(self, names):
        # type: (List[str]) -> None
        self.base_packages = set()  # type: Set[str]
        for n in names:
            # Convert module names:
            #     ['a.b.c', 'd.e']
            # to a set of base packages:
            #     set(['a', 'd'])
            self.base_packages.add(n.split('.')[0])
        self.mocked_modules = []  # type: List[str]
        # enable hook by adding itself to meta_path
        sys.meta_path = sys.meta_path + [self]

    def disable(self):
        # remove `self` from `sys.meta_path` to disable import hook
        sys.meta_path = [i for i in sys.meta_path if i is not self]
        # remove mocked modules from sys.modules to avoid side effects after
        # running auto-documenter
        for m in self.mocked_modules:
            if m in sys.modules:
                del sys.modules[m]

    def find_module(self, name, path=None):
        # type: (str, str) -> Any
        base_package = name.split('.')[0]
        if base_package in self.base_packages:
            return self
        return None

    def load_module(self, name):
        # type: (str) -> ModuleType
        if name in sys.modules:
            # module has already been imported, return it
            return sys.modules[name]
        else:
            logger.debug('[autodoc] adding a mock module %s!', name)
            module = _MockModule(name, self)
            sys.modules[name] = module
            self.mocked_modules.append(name)
            return module


@contextlib.contextmanager
def mock(names):
    # type: (List[str]) -> Generator
    try:
        importer = _MockImporter(names)
        yield
    finally:
        importer.disable()


def import_module(modname, warningiserror=False):
    """
    Call __import__(modname), convert exceptions to ImportError
    """
    try:
        with warnings.catch_warnings():
            warnings.filterwarnings("ignore", category=ImportWarning)
            with logging.skip_warningiserror(not warningiserror):
                __import__(modname)
                return sys.modules[modname]
    except BaseException as exc:
        # Importing modules may cause any side effects, including
        # SystemExit, so we need to catch all errors.
        raise ImportError(exc, traceback.format_exc())


def import_object(modname, objpath, objtype='', attrgetter=safe_getattr, warningiserror=False):
    # type: (str, List[unicode], str, Callable[[Any, unicode], Any], bool) -> Any
    if objpath:
        logger.debug('[autodoc] from %s import %s', modname, '.'.join(objpath))
    else:
        logger.debug('[autodoc] import %s', modname)

    try:
        module = import_module(modname, warningiserror=warningiserror)
        logger.debug('[autodoc] => %r', module)
        obj = module
        parent = None
        object_name = None
        for attrname in objpath:
            parent = obj
            logger.debug('[autodoc] getattr(_, %r)', attrname)
            obj = attrgetter(obj, attrname)
            logger.debug('[autodoc] => %r', obj)
            object_name = attrname
        return [module, parent, object_name, obj]
    except (AttributeError, ImportError) as exc:
        if objpath:
            errmsg = ('autodoc: failed to import %s %r from module %r' %
                      (objtype, '.'.join(objpath), modname))
        else:
            errmsg = 'autodoc: failed to import %s %r' % (objtype, modname)

        if isinstance(exc, ImportError):
            # import_module() raises ImportError having real exception obj and
            # traceback
            real_exc, traceback_msg = exc.args
            if isinstance(real_exc, SystemExit):
                errmsg += ('; the module executes module level statement '
                           'and it might call sys.exit().')
            elif isinstance(real_exc, ImportError):
                errmsg += '; the following exception was raised:\n%s' % real_exc.args[0]
            else:
                errmsg += '; the following exception was raised:\n%s' % traceback_msg
        else:
            errmsg += '; the following exception was raised:\n%s' % traceback.format_exc()

        if PY2:
            errmsg = errmsg.decode('utf-8')  # type: ignore
        logger.debug(errmsg)
        raise ImportError(errmsg)


Attribute = namedtuple('Attribute', ['name', 'directly_defined', 'value'])


def get_object_members(subject, objpath, attrgetter, analyzer=None):
    # type: (Any, List[unicode], Callable, Any) -> Dict[str, Attribute]  # NOQA
    """Get members and attributes of target object."""
    # the members directly defined in the class
    obj_dict = attrgetter(subject, '__dict__', {})

    # Py34 doesn't have enum members in __dict__.
    if sys.version_info[:2] == (3, 4) and isenumclass(subject):
        obj_dict = dict(obj_dict)
        for name, value in subject.__members__.items():
            obj_dict[name] = value

    members = {}
    for name in dir(subject):
        try:
            value = attrgetter(subject, name)
            directly_defined = name in obj_dict
            members[name] = Attribute(name, directly_defined, value)
        except AttributeError:
            continue

    if analyzer:
        # append instance attributes (cf. self.attr1) if analyzer knows
        from sphinx.ext.autodoc import INSTANCEATTR

        namespace = '.'.join(objpath)
        for (ns, name) in analyzer.find_attr_docs():
            if namespace == ns and name not in members:
                members[name] = Attribute(name, True, INSTANCEATTR)

    return members