Passed
Push — main ( 9813db...5006f2 )
by Douglas
01:43
created

mandos.model.utils.ReflectionUtils.injection()   A

Complexity

Conditions 2

Size

Total Lines 24
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 11
nop 3
dl 0
loc 24
rs 9.85
c 0
b 0
f 0
1
import enum
0 ignored issues
show
introduced by
Missing module docstring
Loading history...
2
import inspect
3
import sys
4
import typing
5
from datetime import datetime
6
from pathlib import Path
7
from typing import Type, TypeVar, Optional, Mapping, Any, Union, Sequence
8
9
from mandos import logger
10
from suretime import Suretime
0 ignored issues
show
introduced by
Unable to import 'suretime'
Loading history...
11
from typeddfs import TypedDf
0 ignored issues
show
introduced by
Unable to import 'typeddfs'
Loading history...
12
13
from mandos.model.settings import MANDOS_SETTINGS
0 ignored issues
show
introduced by
Imports from package mandos are not grouped
Loading history...
14
15
T = TypeVar("T", covariant=True)
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...
16
17
18
class InjectionError(LookupError):
0 ignored issues
show
Documentation introduced by
Empty class docstring
Loading history...
19
    """ """
20
21
22
class ReflectionUtils:
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
23
    @classmethod
24
    def get_generic_arg(cls, clazz: Type[T], bound: Optional[Type[T]] = None) -> Type:
25
        """
26
        Finds the generic argument (specific TypeVar) of a :py:class:`~typing.Generic` class.
27
        **Assumes that ``clazz`` only has one type parameter. Always returns the first.**
28
29
        Args:
30
            clazz: The Generic class
31
            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...
32
33
        Returns:
34
            The class
35
36
        Raises:
37
            AssertionError: For most errors
38
        """
39
        bases = clazz.__orig_bases__
40
        try:
41
            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...
42
        except KeyError:
43
            raise AssertionError(f"Failed to get generic type on {cls}")
44
        if not issubclass(param, bound):
45
            raise AssertionError(f"{param} is not a {bound}")
46
        return param
47
48
    @classmethod
49
    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...
50
        return {c.__name__: c for c in cls.subclasses(clazz, concrete=concrete)}
51
52
    @classmethod
53
    def subclasses(cls, clazz, concrete: bool = False):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
54
        for subclass in clazz.__subclasses__():
55
            yield from cls.subclasses(subclass, concrete=concrete)
56
            if (
57
                not concrete
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
58
                or not inspect.isabstract(subclass)
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
59
                and not subclass.__name__.startswith("_")
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
60
            ):
61
                yield subclass
62
63
    @classmethod
64
    def default_arg_values(cls, func) -> Mapping[str, Optional[Any]]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
65
        return {k: v.default for k, v in cls.optional_args(func).items()}
66
67
    @classmethod
68
    def required_args(cls, func):
69
        """
70
        Finds parameters that lack default values.
71
72
        Args:
73
            func: A function or method
74
75
        Returns:
76
            A dict mapping parameter names to instances of ``MappingProxyType``,
77
            just as ``inspect.signature(func).parameters`` does.
78
        """
79
        return cls._args(func, True)
80
81
    @classmethod
82
    def optional_args(cls, func):
83
        """
84
        Finds parameters that have default values.
85
86
        Args:
87
            func: A function or method
88
89
        Returns:
90
            A dict mapping parameter names to instances of ``MappingProxyType``,
91
            just as ``inspect.signature(func).parameters`` does.
92
        """
93
        return cls._args(func, False)
94
95
    @classmethod
96
    def _args(cls, func, req):
97
        signature = inspect.signature(func)
98
        return {
99
            k: v
100
            for k, v in signature.parameters.items()
101
            if req
102
            and v.default is inspect.Parameter.empty
103
            or not req
104
            and v.default is not inspect.Parameter.empty
105
        }
106
107
    @classmethod
108
    def injection(cls, fully_qualified: str, clazz: Type[T]) -> Type[T]:
109
        """
110
        Gets a **class** by its fully-resolved class name.
111
112
        Args:
113
            fully_qualified:
114
            clazz:
115
116
        Returns:
117
            The Type
118
119
        Raises:
120
            InjectionError: If the class was not found
121
        """
122
        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...
123
        mod = s[: s.rfind(".")]
124
        clz = s[s.rfind(".") :]
125
        try:
126
            return getattr(sys.modules[mod], clz)
127
        except AttributeError:
128
            raise InjectionError(
129
                f"Did not find {clazz} by fully-qualified class name {fully_qualified}"
130
            ) from None
131
132
133
class MiscUtils:
134
    """
135
    These are here to make sure I always use the same NTP server, etc.
136
    """
137
138
    @classmethod
139
    def empty_df(cls, clazz: Type[T], reserved: bool = False) -> T:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
140
        if issubclass(clazz, TypedDf):
0 ignored issues
show
unused-code introduced by
Unnecessary "else" after "return"
Loading history...
141
            if reserved:
142
                req = clazz.known_names()
143
            else:
144
                req = [*clazz.required_index_names(), *clazz.required_columns]
145
            return clazz({r: [] for r in req})
146
        else:
147
            return clazz({})
148
149
    @classmethod
150
    def adjust_filename(cls, to: Optional[Path], default: Union[str, Path], replace: bool) -> Path:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
Coding Style Naming introduced by
Argument name "to" 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...
151
        if to is None:
152
            path = Path(default)
153
        elif str(to).startswith("."):
154
            path = Path(default).with_suffix(str(to))
155
        elif to.is_dir() or to.suffix == "":
156
            path = to / default
157
        else:
158
            raise AssertionError(str(to))
159
        path = Path(path)
160
        if path.exists() and not replace:
0 ignored issues
show
Unused Code introduced by
Unnecessary "elif" after "raise"
Loading history...
161
            raise FileExistsError(f"File {path} already exists")
162
        elif replace:
163
            logger.info(f"Overwriting existing file {path}.")
164
        return path
165
166
    @classmethod
167
    def ntp_utc(cls) -> datetime:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
168
        ntp = Suretime.tagged.now_utc_ntp(
169
            ntp_server=MANDOS_SETTINGS.ntp_continent, ntp_clock="client-sent"
170
        )
171
        return ntp.use_clock_as_dt.dt
172
173
    @classmethod
174
    def utc(cls) -> datetime:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
175
        return Suretime.tagged.now_utc_sys().dt
176
177
    @classmethod
178
    def serialize_list(cls, lst: Sequence[str]) -> str:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
179
        return " || ".join([str(x) for x in lst])
180
181
    @classmethod
182
    def deserialize_list(cls, s: str) -> Sequence[str]:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
Coding Style Naming introduced by
Argument 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...
183
        return s.split(" || ")
184
185
186
class TrueFalseUnknown(enum.Enum):
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
187
    true = enum.auto()
188
    false = enum.auto()
189
    unknown = enum.auto()
190
191
    @classmethod
192
    def parse(cls, s: str):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
Coding Style Naming introduced by
Argument 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...
193
        tf_map = {
194
            "t": TrueFalseUnknown.true,
195
            "f": TrueFalseUnknown.false,
196
            "true": TrueFalseUnknown.true,
197
            "false": TrueFalseUnknown.false,
198
        }
199
        return tf_map.get(s.lower().strip(), TrueFalseUnknown.unknown)
200
201
202
class CleverEnum(enum.Enum):
203
    """
204
    An enum with a ``.of`` method that finds values
205
    with limited string/value fixing.
206
    May support an "unmatched" type -- a fallback value when there is no match.
207
    This is similar to pocketutils' simpler ``SmartEnum``.
208
    It is mainly useful for enums corresponding to concepts in ChEMBL and PubChem,
209
    where it's acceptable for the user to input spaces (like the database concepts use)
210
    rather than the underscores that Python requires.
211
    """
212
213
    @classmethod
214
    def _unmatched_type(cls) -> Optional[__qualname__]:
215
        return None
216
217
    @classmethod
218
    def of(cls, s: Union[int, str, __qualname__]) -> __qualname__:
0 ignored issues
show
Coding Style Naming introduced by
Argument 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...
Coding Style Naming introduced by
Method name "of" 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...
219
        """
220
        Turns a string or int into this type.
221
        Case-insensitive. Replaces `` ``, ``.``, and ``-`` with ``_``.
222
        """
223
        if isinstance(s, cls):
224
            return s
225
        key = s.strip().replace(" ", "_").replace(".", "_").replace("-", "_").lower()
226
        try:
227
            if isinstance(s, str):
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
228
                return cls[key]
229
            elif isinstance(key, int):
230
                return cls(key)
231
            else:
232
                raise TypeError(f"Lookup type {type(s)} for value {s} not a str or int")
233
        except KeyError:
234
            unk = cls._unmatched_type()
235
            if unk is None:
236
                raise
237
            logger.error(f"Value {key} not found. Using {unk}")
238
            if not isinstance(unk, cls):
239
                raise AssertionError(f"Wrong type {type(unk)} (lookup: {s})")
240
            return unk
241
242
243
__all__ = ["InjectionError", "ReflectionUtils", "MiscUtils", "TrueFalseUnknown", "CleverEnum"]
244