Completed
Pull Request — master (#2345)
by Mischa
02:09
created

_windows_get_case_sensitive_filename()   B

Complexity

Conditions 5

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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