Passed
Push — main ( 82dd22...9813db )
by Douglas
01:58
created

mandos.model   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 292
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 167
dl 0
loc 292
rs 8.96
c 0
b 0
f 0
wmc 43

20 Methods

Rating   Name   Duplication   Size   Complexity  
A ReflectionUtils.optional_args() 0 13 1
A ReflectionUtils.subclass_dict() 0 3 1
A ReflectionUtils.injection() 0 23 2
A ReflectionUtils.subclasses() 0 10 5
A ReflectionUtils.default_arg_values() 0 3 1
A ReflectionUtils.required_args() 0 13 1
A CleverEnum._unmatched_type() 0 3 1
A ReflectionUtils._args() 0 10 1
A ReflectionUtils.get_generic_arg() 0 24 3
A MandosResources.path() 0 5 2
A MandosResources.json() 0 4 1
A MandosResources.a_path() 0 9 1
A MiscUtils.serialize_list() 0 3 1
A MiscUtils.empty_df() 0 10 3
A MiscUtils.utc() 0 3 1
B CleverEnum.of() 0 24 7
A MiscUtils.deserialize_list() 0 3 1
A MandosResources.contains() 0 4 1
A MiscUtils.ntp_utc() 0 6 1
B MiscUtils.adjust_filename() 0 16 8

How to fix   Complexity   

Complexity

Complex classes like mandos.model often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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