Passed
Pull Request — master (#3)
by
unknown
04:16
created

barentsz._discover._is_abstract()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
import glob
2
import inspect
3
import re
4
import sys
5
from abc import ABC
6
from importlib import import_module
7
from pathlib import Path
8
from typing import (
9
    Union,
10
    Dict,
11
    List,
12
    Any,
13
    Callable,
14
    Type,
15
    Iterable,
16
    Optional,
17
    Tuple,
18
    Set,
19
    TypeVar,
20
)
21
22
from typish import Module, subclass_of, instance_of
23
24
from barentsz._here import here
25
from barentsz._attribute import Attribute
26
27
28 View Code Duplication
def discover(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
29
        source: Any = None,
30
        *,
31
        what: Any = List[type],
32
        **kwargs: dict,
33
) -> list:
34
    """
35
    Convenience function for discovering types in some source. If not source
36
    is given, the directory is used in which the calling module is located.
37
38
    Args:
39
        source: the source in which is searched or the directory of the
40
        caller if None.
41
        what: the type that is to be discovered.
42
        **kwargs: any keyword argument that is passed on.
43
44
    Returns: a list of discoveries.
45
46
    """
47
    source = source or here(1)
48
49
    delegates = [
50
        (List[type], _discover_list),
51
        (list, _discover_list),
52
        (List, _discover_list),
53
    ]
54
55
    for tuple_ in delegates:
56
        type_, delegate = tuple_
57
        if subclass_of(what, type_):
58
            return delegate(what, source, **kwargs)
59
60
    accepted_types = ', '.join(['`{}`'.format(delegate)
61
                                for delegate, _ in delegates])
62
    raise ValueError('Type `{}` is not supported. This function accepts: '
63
                     '{}'.format(what, accepted_types))
64
65
66 View Code Duplication
def discover_paths(directory: Union[Path, str], pattern: str) -> List[Path]:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
67
    """
68
    Return a list of Paths within the given directory that match the given
69
    pattern.
70
71
    Args:
72
        directory: the directory in which is searched for paths.
73
        pattern: a pattern (example: '**/*.py').
74
75
    Returns: a list of Path objects.
76
77
    """
78
    directory_path = _path(directory)
79
    abspath = str(directory_path.absolute())
80
    sys.path.insert(0, abspath)
81
    path_to_discover = directory_path.joinpath(pattern)
82
    result = [Path(filename) for filename in
83
              glob.iglob(str(path_to_discover), recursive=True)]
84
    result.sort()
85
    return result
86
87
88
def discover_packages(directory: Union[Path, str]) -> List[str]:
89
    """
90
    Return a list of packages within the given directory. The directory must be
91
    a package.
92
    Args:
93
        directory: the directory in which is searched for packages.
94
95
    Returns: a list of packages.
96
97
    """
98
    result = list(_discover_packages_per_path(directory).values())
99
    result.sort()
100
    return result
101
102
103 View Code Duplication
def discover_module_names(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
104
        directory: Union[Path, str],
105
        include_privates: bool = False) -> List[str]:
106
    """
107
    Return a list of module names within the given directory. The directory
108
    must be a package and only names are returned of modules that are in
109
    packages.
110
    Args:
111
        directory: the directory in which is searched for modules.
112
        include_privates: if True, privates (unders and dunders) are also
113
        included.
114
115
    Returns: a list of module names (strings).
116
117
    """
118
    result = []
119
    packages_per_path = _discover_packages_per_path(directory)
120
    for path, package_name in packages_per_path.items():
121
        result.extend(['{}.{}'.format(package_name, p.stem)
122
                       for p in discover_paths(path, '*.py')
123
                       if p.stem != '__init__'
124
                       and (include_privates or not p.stem.startswith('_'))])
125
    result.sort()
126
    return result
127
128
129 View Code Duplication
def discover_modules(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
130
        directory: Union[Path, str],
131
        include_privates: bool = False,
132
        raise_on_fail: bool = False) -> List[Module]:
133
    """
134
    Return a list of modules within the given directory. The directory must be
135
    a package and only modules are returned that are in packages.
136
    Args:
137
        directory: the directory in which is searched for modules.
138
        include_privates: if True, privates (unders and dunders) are also
139
        included.
140
        raise_on_fail: if True, an ImportError is raised upon failing to
141
        import any module.
142
143
    Returns: a list of module objects.
144
145
    """
146
    modules = discover_module_names(directory, include_privates)
147
    result = []
148
    for module in modules:
149
        try:
150
            imported_module = import_module(module)
151
            result.append(imported_module)
152
        except Exception as err:
153
            if raise_on_fail:
154
                raise ImportError(err)
155
    result.sort(key=lambda module: module.__name__)
156
    return result
157
158
159 View Code Duplication
def discover_classes(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
160
        source: Union[Path, str, Module, Iterable[Module]],
161
        signature: type = Any,  # type: ignore
162
        include_privates: bool = False,
163
        in_private_modules: bool = False,
164
        raise_on_fail: bool = False,
165
        exclude: Union[Iterable[type], type] = None,
166
        exclude_abstract: bool = False
167
) -> List[type]:
168
    """
169
    Discover any classes within the given source and according to the given
170
    constraints.
171
172
    Args:
173
        source: the source in which is searched for any classes.
174
        signature: only classes that inherit from signature are returned.
175
        include_privates: if True, private classes are included as well.
176
        in_private_modules: if True, private modules are explored as well.
177
        raise_on_fail: if True, raises an ImportError upon the first import
178
        failure.
179
        exclude: a type or multiple types that are to be excluded from the
180
        result.
181
        exclude_abstract: if True, abstract classes are filtered out.
182
183
    Returns: a list of all discovered classes (types).
184
185
    """
186
    exclude_ = _ensure_set(exclude)
187
    elements = _discover_elements(source, inspect.isclass, include_privates,
188
                                  in_private_modules, raise_on_fail)
189
    result = list({cls for cls in elements
190
                   if (signature is Any or subclass_of(cls, signature))
191
                   and cls not in exclude_
192
                   and not (exclude_abstract and _is_abstract(cls))})
193
    result.sort(key=lambda cls: cls.__name__)
194
    return result
195
196
197 View Code Duplication
def discover_functions(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
198
        source: Union[Path, str, Module, Iterable[Module], type],
199
        signature: Type[Callable] = Callable,  # type: ignore
200
        include_privates: bool = False,
201
        in_private_modules: bool = False,
202
        raise_on_fail: bool = False) -> List[type]:
203
    """
204
    Discover any functions within the given source and according to the given
205
    constraints.
206
207
    Args:
208
        source: the source in which is searched for any functions.
209
        signature: only functions that have this signature (parameters and
210
        return type) are included.
211
        include_privates: if True, private functions are included as well.
212
        in_private_modules: if True, private modules are explored as well.
213
        raise_on_fail: if True, raises an ImportError upon the first import
214
        failure.
215
216
    Returns: a list of all discovered functions.
217
218
    """
219
220
    def filter_(*args_: Iterable[Any]) -> bool:
221
        return (inspect.isfunction(*args_)
222
                or inspect.ismethod(*args_))
223
224
    if not isinstance(source, type):
225
        filter_ = inspect.isfunction  # type: ignore
226
227
    elements = _discover_elements(source, filter_, include_privates,
228
                                  in_private_modules, raise_on_fail)
229
    result = [elem for elem in elements
230
              if (signature is Callable or instance_of(elem, signature))]
231
    result.sort(key=lambda func: func.__name__)
232
    return result
233
234
235 View Code Duplication
def discover_attributes(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
236
        source: Union[Path, str, Module, Iterable[Module]],
237
        signature: type = Any,  # type: ignore
238
        include_privates: bool = False,
239
        in_private_modules: bool = False,
240
        raise_on_fail: bool = False) -> List[Attribute]:
241
    """
242
    Discover any attributes within the given source and according to the given
243
    constraints.
244
245
    Args:
246
        source: the source in which is searched for any attributes.
247
        signature: only attributes that are subtypes of this signature are
248
        included.
249
        include_privates: if True, private attributes are included as well.
250
        in_private_modules: if True, private modules are explored as well.
251
        raise_on_fail: if True, raises an ImportError upon the first import
252
        failure.
253
254
    Returns: a list of all discovered attributes.
255
256
    """
257
    modules = _get_modules_from_source(source, in_private_modules,
258
                                       raise_on_fail)
259
    attributes: List[Attribute] = []
260
    for module in modules:
261
        with open(module.__file__) as module_file:
262
            lines = list(module_file)
263
        attributes += _discover_attributes_in_lines(
264
            lines, module, signature, include_privates)
265
    attributes.sort(key=lambda attr: attr.name)
266
    return attributes
267
268
269 View Code Duplication
def _discover_attributes_in_lines(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
270
        lines: List[str],
271
        module: Module,
272
        signature: type,
273
        include_privates: bool) -> List[Attribute]:
274
    """
275
    Discover any attributes within the lines of codee and according to the
276
    given constraints.
277
278
    Args:
279
        lines: the lines of code in which is searched for any attributes.
280
        module: the module from which these lines originate.
281
        signature: only attributes that are subtypes of this signature are
282
        included.
283
        include_privates: if True, private attributes are included as well.
284
285
    Returns: a list of all discovered attributes.
286
287
    """
288
    attributes = []
289
    for index, line in enumerate(lines):
290
        match = _match_attribute(line)
291
        if match:
292
            name, hint, value, comment = match
293
            docstring = _find_attribute_docstring(lines[0:index])
294
            attribute = _create_attribute(name, hint, value, docstring,
295
                                          comment, module, line, index + 1)
296
            if (instance_of(attribute.value, signature)
297
                    and (attribute.is_public or include_privates)):
298
                attributes.append(attribute)
299
    return attributes
300
301
302 View Code Duplication
def _discover_elements(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
303
        source: Union[Path, str, Module, Iterable[Module], type],
304
        filter_: Callable[[Any], bool],
305
        include_privates: bool = False,
306
        in_private_modules: bool = False,
307
        raise_on_fail: bool = False) -> List[Any]:
308
    """
309
    Discover elements (such as attributes or functions) in the given source.
310
    Args:
311
        source: the source that is explored.
312
        filter_: the filter that determines the type of element.
313
        include_privates: if True, private elements are returned as well.
314
        in_private_modules: if True, private modules are examined as well.
315
        raise_on_fail: if True, an ImportError will be raised upon import
316
        failure.
317
318
    Returns: a list of elements.
319
320
    """
321
    if isinstance(source, type):
322
        sources = [source]  # type: Iterable
323
    else:
324
        sources = _get_modules_from_source(source, in_private_modules,
325
                                           raise_on_fail)
326
327
    elements = [elem for src in sources
328
                for _, elem in inspect.getmembers(src, filter_)
329
                if (in_private_modules or not src.__name__.startswith('_'))
330
                and (include_privates or not elem.__name__.startswith('_'))]
331
    return elements
332
333
334 View Code Duplication
def _discover_packages_per_path(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
335
        directory: Union[Path, str]) -> Dict[Path, str]:
336
    """
337
    Discover packages and their original Paths within the given directory.
338
    Args:
339
        directory: the directory in which is searched for modules.
340
341
    Returns: a dict with Paths as keys and strings (the package names) as
342
    values.
343
344
    """
345
    directory_path = _path(directory)
346
    if not directory_path.exists():
347
        raise ValueError('The given directory does not exist. '
348
                         'Given: {}'.format(directory))
349
    if not _is_package(directory_path):
350
        raise ValueError('The given directory must itself be a package. '
351
                         'Given: {}'.format(directory))
352
353
    paths_to_inits = discover_paths(directory_path, '**/__init__.py')
354
    paths = [p.parent for p in paths_to_inits]
355
    packages_per_path = {p: _to_package_name(p) for p in paths}
356
357
    # All packages must have a straight line of packages from the base package.
358
    base_package = _to_package_name(directory_path)
359
    result = {path: package for path, package in packages_per_path.items()
360
              if package.startswith(base_package)}
361
362
    return result
363
364
365 View Code Duplication
def _path(directory: Union[Path, str]) -> Path:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
366
    """
367
    Return a path if directory is a string or return directory if it is a Path
368
    already. Raise a ValueError if it is neither a Path nor a string.
369
370
    Args:
371
        directory: the directory that is a string or Path.
372
373
    Returns: a Path instance.
374
375
    """
376
    if isinstance(directory, Path):
377
        result = directory
378
    elif isinstance(directory, str):
379
        result = Path(directory)
380
    else:
381
        raise ValueError('Invalid type ({}) for directory, provide a Path or '
382
                         'a string.'.format(type(directory)))
383
    return result
384
385
386 View Code Duplication
def _get_modules_from_source(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
387
        source: Union[Path, str, Module, Iterable[Module]],
388
        in_private_modules: bool = False,
389
        raise_on_fail: bool = False
390
) -> Iterable[Module]:
391
    """
392
    Get an iterable of Modules from the given source.
393
    Args:
394
        source: anything that can be turned into an iterable of Modules.
395
        in_private_modules: if True, private modules are explored as well.
396
        raise_on_fail: if True, raises an ImportError upon the first import
397
        failure.
398
399
    Returns: an iterable of Module instances.
400
401
    """
402
    if isinstance(source, Path):
403
        modules = discover_modules(source, in_private_modules, raise_on_fail)
404
    elif isinstance(source, str):
405
        modules = discover_modules(Path(source), in_private_modules,
406
                                   raise_on_fail)
407
    elif isinstance(source, Module):
408
        modules = [source]
409
    elif instance_of(source, Iterable[Module]):
410
        modules = source  # type: ignore
411
    else:
412
        raise ValueError('The given source must be a Path, string or module. '
413
                         'Given: {}'.format(source))
414
    return modules
415
416
417 View Code Duplication
def _match_attribute(line: str) -> Optional[Tuple[str, str, str, str]]:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
418
    """
419
    Try to match the given line with an attribute and return the name,
420
    type hint, value and inline comment (respectively) if a match was
421
    found.
422
423
    Args:
424
        line: the line of code that (may) contain an attribute declaration.
425
426
    Returns: a tuple with strings (name, hint, value, comment) or None.
427
428
    """
429
    attr_pattern = re.compile(
430
        r'^'
431
        r'\s*'
432
        r'([a-zA-Z_]+[a-zA-Z_0-9]*)'  # 1: Name.
433
        r'(\s*:\s*(\w+)\s*)?'  # 3: Type hint.
434
        r'\s*=\s*'
435
        r'(.+?)'  # 4: Value.
436
        r'\s*'
437
        r'(#\s*(.*?)\s*)?'  # 6: Inline comment.
438
        r'$'
439
    )
440
    match = attr_pattern.match(line)
441
    result = None
442
    if match:
443
        attr_name = match.group(1)
444
        hint = match.group(3)
445
        attr_value = match.group(4)
446
        inline_comments = match.group(6)
447
        result = attr_name, hint, attr_value, inline_comments
448
    return result
449
450
451 View Code Duplication
def _create_attribute(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
452
        name: str,
453
        hint: Optional[str],
454
        assigned_value: str,
455
        docstring: Optional[str],
456
        comment: Optional[str],
457
        module: Module,
458
        line: str,
459
        line_nr: int) -> Attribute:
460
    """
461
    Create and return an Attribute instance from the given parameters.
462
    Args:
463
        name: the name of the attribute.
464
        hint: the type hint of the attribute (if any).
465
        assigned_value: the string that was literally assigned.
466
        docstring: the docstring above this attribute.
467
        comment: an inline comment (if any).
468
        module: the module that contains the attribute.
469
        line: the line that defines the attribute.
470
        line_nr: the line number of the attribute.
471
472
    Returns: an Attribute instance.
473
474
    """
475
    value = getattr(module, name)
476
    type_ = type(value)
477
    return Attribute(
478
        name=name,
479
        type_=type_,
480
        value=value,
481
        doc=docstring,
482
        comment=comment,
483
        hint=hint,
484
        module=module,
485
        assigned_value=assigned_value,
486
        line=line,
487
        line_nr=line_nr
488
    )
489
490
491
def _is_package(directory: Path) -> bool:
492
    """
493
    Return True if the given directory is a package and False otherwise.
494
    Args:
495
        directory: the directory to check.
496
497
    Returns: True if directory is a package.
498
499
    """
500
    paths = discover_paths(directory, '__init__.py')
501
    return len(paths) > 0
502
503
504
def _to_package_name(directory: Path) -> str:
505
    """
506
    Translate the given directory to a package (str). Check every parent
507
    directory in the tree to find the complete fully qualified package name.
508
    Args:
509
        directory: the directory that is to become a package name.
510
511
    Returns: a package name as string.
512
513
    """
514
    parts: List[str] = []
515
    current_dir = directory
516
    while _is_package(current_dir):
517
        # See how far up the tree we can go while still in a package.
518
        parts.insert(0, current_dir.stem)
519
        current_dir = current_dir.parent
520
    return '.'.join(parts)
521
522
523 View Code Duplication
def _find_attribute_docstring(lines: List[str]) -> Optional[str]:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
524
    """
525
    Find any docstring that is right above an attribute.
526
    Args:
527
        lines: the lines of code that may contain a docstring.
528
529
    Returns: a docstring (str) or None.
530
531
    """
532
    result = None
533
    if lines:
534
        joined_lines = ''.join(lines).strip()
535
        docstring_pattern = re.compile(
536
            r'("{3}\s*([\s\S]+)\s*"{3}|'  # 2: docstring content.
537
            r'\'{3}\s*([\s\S]+)\s*\'{3})'  # 3: docstring content.
538
            r'$'
539
        )
540
        match = docstring_pattern.match(joined_lines)
541
        if match:
542
            result = (match.group(2) or match.group(3)).strip()
543
    return result
544
545
546
def _ensure_set(arg: Union[object, Iterable[object]]) -> Set[object]:
547
    # Make sure that arg is a set.
548
    result = arg or set()
549
    if not isinstance(result, Iterable):
550
        result = {result}
551
    else:
552
        result = set(result)
553
    return result
554
555
556 View Code Duplication
def _discover_list(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
557
        what_: List[type],
558
        source: Union[Path, str, Module, Iterable[Module]],
559
        **kwargs: dict) -> List[type]:
560
    args = getattr(what_, '__args__', None) or [Any]
561
    signature = args[0]
562
    if signature in (type, Type) or isinstance(signature, TypeVar):  # type: ignore[arg-type] # noqa
563
        signature = Any
564
    kwargs['signature'] = signature
565
    return discover_classes(source, **kwargs)  # type: ignore[arg-type]
566
567
568
def _is_abstract(cls: type) -> bool:
569
    return ABC in cls.mro() and hasattr(cls, '__abstractmethods__')
570