Passed
Push — main ( e0f181...a5d2a4 )
by Douglas
01:37
created

pocketutils.tools.reflection_tools   A

Complexity

Total Complexity 15

Size/Duplication

Total Lines 123
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 62
dl 0
loc 123
rs 10
c 0
b 0
f 0
wmc 15

8 Methods

Rating   Name   Duplication   Size   Complexity  
A ReflectionTools.injection() 0 24 2
A ReflectionTools.optional_args() 0 13 1
A ReflectionTools.subclasses() 0 10 5
A ReflectionTools.get_generic_arg() 0 24 3
A ReflectionTools.default_arg_values() 0 3 1
A ReflectionTools._args() 0 10 1
A ReflectionTools.required_args() 0 13 1
A ReflectionTools.subclass_dict() 0 3 1
1
import inspect
0 ignored issues
show
introduced by
Missing module docstring
Loading history...
2
import sys
3
import typing
4
from typing import Any, Mapping, Optional, Type
5
6
from pocketutils.core.exceptions import InjectionError
7
8
T = typing.TypeVar("T")
0 ignored issues
show
Coding Style Naming introduced by
Class name "T" doesn't conform to PascalCase naming style ('[^\\W\\da-z][^\\W_]+$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
9
10
11
class ReflectionTools:
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
12
    @classmethod
13
    def get_generic_arg(cls, clazz: Type[T], bound: Optional[Type[T]] = None) -> Type:
14
        """
15
        Finds the generic argument (specific TypeVar) of a :class:`~typing.Generic` class.
16
        **Assumes that ``clazz`` only has one type parameter. Always returns the first.**
17
18
        Args:
19
            clazz: The Generic class
20
            bound: If non-None, requires the returned type to be a subclass of ``bound`` (or equal to it)
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (105/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
21
22
        Returns:
23
            The class
24
25
        Raises:
26
            AssertionError: For most errors
27
        """
28
        bases = clazz.__orig_bases__
29
        try:
30
            param = typing.get_args(bases[0])[0]
0 ignored issues
show
Bug introduced by
The Module typing does not seem to have a member named get_args.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
31
        except KeyError:
32
            raise AssertionError(f"Failed to get generic type on {cls}")
33
        if not issubclass(param, bound):
34
            raise AssertionError(f"{param} is not a {bound}")
35
        return param
36
37
    @classmethod
38
    def subclass_dict(cls, clazz: Type[T], concrete: bool = False) -> Mapping[str, Type[T]]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
39
        return {c.__name__: c for c in cls.subclasses(clazz, concrete=concrete)}
40
41
    @classmethod
42
    def subclasses(cls, clazz, concrete: bool = False):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
43
        for subclass in clazz.__subclasses__():
44
            yield from cls.subclasses(subclass, concrete=concrete)
45
            if (
46
                not concrete
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
47
                or not inspect.isabstract(subclass)
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
48
                and not subclass.__name__.startswith("_")
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
49
            ):
50
                yield subclass
51
52
    @classmethod
53
    def default_arg_values(cls, func) -> Mapping[str, Optional[Any]]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
54
        return {k: v.default for k, v in cls.optional_args(func).items()}
55
56
    @classmethod
57
    def required_args(cls, func):
58
        """
59
        Finds parameters that lack default values.
60
61
        Args:
62
            func: A function or method
63
64
        Returns:
65
            A dict mapping parameter names to instances of ``MappingProxyType``,
66
            just as ``inspect.signature(func).parameters`` does.
67
        """
68
        return cls._args(func, True)
69
70
    @classmethod
71
    def optional_args(cls, func):
72
        """
73
        Finds parameters that have default values.
74
75
        Args:
76
            func: A function or method
77
78
        Returns:
79
            A dict mapping parameter names to instances of ``MappingProxyType``,
80
            just as ``inspect.signature(func).parameters`` does.
81
        """
82
        return cls._args(func, False)
83
84
    @classmethod
85
    def injection(cls, fully_qualified: str, clazz: Type[T]) -> Type[T]:
86
        """
87
        Gets a **class** by its fully-resolved class name.
88
89
        Args:
90
            fully_qualified:
91
            clazz:
92
93
        Returns:
94
            The Type
95
96
        Raises:
97
            InjectionError: If the class was not found
98
        """
99
        s = fully_qualified
0 ignored issues
show
Coding Style Naming introduced by
Variable name "s" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
100
        mod = s[: s.rfind(".")]
101
        clz = s[s.rfind(".") :]
102
        try:
103
            return getattr(sys.modules[mod], clz)
104
        except AttributeError:
105
            raise InjectionError(
106
                f"Did not find {clazz} by fully-qualified class name {fully_qualified}"
107
            ) from None
108
109
    @classmethod
110
    def _args(cls, func, req):
111
        signature = inspect.signature(func)
112
        return {
113
            k: v
114
            for k, v in signature.parameters.items()
115
            if req
116
            and v.default is inspect.Parameter.empty
117
            or not req
118
            and v.default is not inspect.Parameter.empty
119
        }
120
121
122
__all__ = ["ReflectionTools"]
123