Completed
Pull Request — master (#2345)
by Mischa
02:30 queued 30s
created

iimport_objects()   B

Complexity

Conditions 5

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
dl 0
loc 25
rs 8.0894
c 0
b 0
f 0
1
from contextlib import ExitStack
2
from importlib import import_module
3
import inspect
4
import os
5
import platform
6
import sys
7
8
from coalib.misc.ContextManagers import suppress_stdout
9
from coala_decorators.decorators import _to_list, yield_once
10
11
12
def _windows_get_case_sensitive_filename(path):
13
    basepath, lastname = os.path.split(os.path.realpath(path))
14
15
    if basepath[1] == ":" and lastname == "":
16
        # Found a drive letter only, end of path reached.
17
        return basepath[0].upper() + basepath[1:]
18
19
    lastname_lowered = lastname.lower()
20
21
    for element in os.listdir(basepath):
22
        if element.lower() == lastname_lowered:
23
            return os.path.join(_windows_get_case_sensitive_filename(basepath),
24
                                element)
25
26
    raise FileNotFoundError
27
28
29
def _import_module(file_path, base_path):
30
    if not os.path.exists(file_path):
31
        raise ImportError
32
33
    if base_path not in sys.path:
34
        sys.path.append(base_path)
35
36
    # Ugly inconsistency: Python will insist on correctly cased module names
37
    # independent of whether the OS is case-sensitive or not.
38
    # We want all cases to match though.
39
    if platform.system() == 'Windows':  # pragma: nocover
40
        file_path = _windows_get_case_sensitive_filename(file_path)
41
42
    module_path_without_extension = os.path.splitext(file_path)[0]
43
44
    import_fullname = os.path.relpath(
45
        module_path_without_extension, base_path).replace(os.path.sep, ".")
46
    return import_module(import_fullname)
47
48
49
def _is_subclass(test_class, superclasses):
50
    for superclass in superclasses:
51
        try:
52
            if issubclass(test_class, superclass):
53
                return True
54
        except TypeError:
55
            pass
56
    return False
57
58
59
def _has_all(obj, attribute_names):
60
    for attribute_name in attribute_names:
61
        if not hasattr(obj, attribute_name):
62
            return False
63
    return True
64
65
66
def object_defined_in(obj, file_path):
67
    """
68
    Check if the object is defined in the given file.
69
70
    >>> object_defined_in(object_defined_in, __file__)
71
    True
72
    >>> object_defined_in(object_defined_in, "somewhere else")
73
    False
74
75
    Builtins are always defined outside any given file:
76
77
    >>> object_defined_in(False, __file__)
78
    False
79
80
    :param obj:       The object to check.
81
    :param file_path: The path it might be defined in.
82
    :return:          True if the object is defined in the file.
83
    """
84
    try:
85
        source = inspect.getfile(obj)
86
        if (platform.system() == 'Windows' and
87
                source.lower() == file_path.lower() or
88
                source == file_path):
89
            return True
90
    except TypeError:  # Builtin values don't have a source location
91
        pass
92
93
    return False
94
95
96
def _is_defined_in(obj, file_path):
97
    """
98
    Check if a class is defined in the given file.
99
100
    Any class is considered to be defined in the given file if any of it's
101
    parent classes or the class itself is defined in it.
102
    """
103
    if not inspect.isclass(obj):
104
        return object_defined_in(obj, file_path)
105
106
    for base in inspect.getmro(obj):
107
        if object_defined_in(base, file_path):
108
            return True
109
110
    return False
111
112
113
@yield_once
114
def _iimport_objects(file_paths, names, types, supers, attributes, local):
115
    """
116
    Import all objects from the given modules that fulfill the requirements
117
118
    :param file_paths: dict of file paths and their base import paths from
119
                       which objects will be imported.
120
    :param names:      Name(s) an objects need to have one of
121
    :param types:      Type(s) an objects need to be out of
122
    :param supers:     Class(es) objects need to be a subclass of
123
    :param attributes: Attribute(s) an object needs to (all) have
124
    :param local:      if True: Objects need to be defined in the file they
125
                       appear in to be collected
126
    :return:           iterator that yields all matching python objects
127
    :raises Exception: Any exception that is thrown in module code or an
128
                       ImportError if paths are erroneous.
129
    """
130
    names = _to_list(names)
131
    types = _to_list(types)
132
    supers = _to_list(supers)
133
    attributes = _to_list(attributes)
134
    local = _to_list(local)
135
136
    if not (file_paths and (names or types or supers or attributes)):
137
        return
138
139
    for file_path, base_path in file_paths.items():
140
        module = _import_module(file_path, base_path)
141
        for obj_name, obj in inspect.getmembers(module):
142
            if ((not names or obj_name in names) and
143
                    (not types or isinstance(obj, tuple)) and
144
                    (not supers or _is_subclass(obj, supers)) and
145
                    (not attributes or _has_all(obj, attributes)) and
146
                    (local[0] is False or _is_defined_in(obj, file_path))):
147
                yield obj
148
149
150
def iimport_objects(file_paths, names=None, types=None, supers=None,
151
                    attributes=None, local=False, verbose=False):
152
    """
153
    Import all objects from the given modules that fulfill the requirements
154
155
    :param file_paths: dict of file paths and their base import paths from
156
                       which objects will be imported.
157
    :param names:      Name(s) an objects need to have one of
158
    :param types:      Type(s) an objects need to be out of
159
    :param supers:     Class(es) objects need to be a subclass of
160
    :param attributes: Attribute(s) an object needs to (all) have
161
    :param local:      if True: Objects need to be defined in the file they
162
                       appear in to be collected
163
    :param verbose:    Whether verbose logs shall be displayed on console or
164
                       not.
165
    :return:           iterator that yields all matching python objects
166
    :raises Exception: Any exception that is thrown in module code or an
167
                       ImportError if paths are erroneous.
168
    """
169
    with ExitStack() as stack:
170
        if not verbose:
171
            stack.enter_context(suppress_stdout())
172
173
        yield from _iimport_objects(file_paths, names, types, supers,
174
                                    attributes, local)
175
176
177
def import_objects(file_paths, names=None, types=None, supers=None,
178
                   attributes=None, local=False, verbose=False):
179
    """
180
    Import all objects from the given modules that fulfill the requirements
181
182
    :param file_paths: File path(s) from which objects will be imported
183
    :param names:      Name(s) an objects need to have one of
184
    :param types:      Type(s) an objects need to be out of
185
    :param supers:     Class(es) objects need to be a subclass of
186
    :param attributes: Attribute(s) an object needs to (all) have
187
    :param local:      if True: Objects need to be defined in the file they
188
                       appear in to be collected
189
    :return:           list of all matching python objects
190
    :raises Exception: Any exception that is thrown in module code or an
191
                       ImportError if paths are erroneous.
192
    """
193
    return list(iimport_objects(file_paths, names, types, supers, attributes,
194
                                local, verbose))
195