Passed
Push — main ( ec3fe3...82dd22 )
by Douglas
02:00
created

mandos.model.MiscUtils.adjust_filename()   B

Complexity

Conditions 7

Size

Total Lines 11
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 11
nop 4
dl 0
loc 11
rs 8
c 0
b 0
f 0
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__:
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...
168
        """
169
        Turns a string or int into this type.
170
        Case-insensitive. Replaces `` `` and ``-`` with ``_``.
171
        """
172
        key = s.replace(" ", "_").replace("-", "_").lower()
173
        try:
174
            if isinstance(s, str):
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
175
                return cls[key]
176
            elif isinstance(key, int):
177
                return cls(key)
178
            else:
179
                raise TypeError(f"Lookup type {type(s)} for value {s} not a str or int")
180
        except KeyError:
181
            unk = cls._unmatched_type()
182
            if unk is None:
183
                raise
184
            logger.error(f"Value {key} not found. Using TargetType.unknown.")
185
            if not isinstance(unk, cls):
186
                raise AssertionError(f"Wrong type {type(unk)} (lookup: {s})")
187
            return unk
188
189
190
class MandosResources:
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
191
    @classmethod
192
    def contains(cls, *nodes: Union[Path, str], suffix: Optional[str] = None) -> bool:
193
        """Returns whether a resource file (or dir) exists."""
194
        return cls.path(*nodes, suffix=suffix).exists()
195
196
    @classmethod
197
    def path(cls, *nodes: Union[Path, str], suffix: Optional[str] = None) -> Path:
198
        """Gets a path of a test resource file under ``resources/``."""
199
        path = Path(Path(__file__).parent.parent, "resources", *nodes)
200
        return path.with_suffix(path.suffix if suffix is None else suffix)
201
202
    @classmethod
203
    def a_path(cls, *nodes: Union[Path, str], suffixes: Optional[typing.Set[str]] = None) -> Path:
204
        """Gets a path of a test resource file under ``resources/``, ignoring suffix."""
205
        path = Path(Path(__file__).parent.parent, "resources", *nodes)
206
        return CommonTools.only(
207
            [
208
                p
209
                for p in path.parent.glob(path.stem + "*")
210
                if p.is_file() and (suffixes is None or p.suffix in suffixes)
211
            ]
212
        )
213
214
    @classmethod
215
    def json(cls, *nodes: Union[Path, str], suffix: Optional[str] = None) -> NestedDotDict:
216
        """Reads a JSON file under ``resources/``."""
217
        return NestedDotDict.read_json(cls.path(*nodes, suffix=suffix))
218
219
220
class MiscUtils:
221
    """
222
    These are here to make sure I always use the same NTP server, etc.
223
    """
224
225
    @classmethod
226
    def empty_df(cls, clazz: Type[T], reserved: bool = False) -> T:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
227
        if issubclass(clazz, TypedDf):
0 ignored issues
show
unused-code introduced by
Unnecessary "else" after "return"
Loading history...
228
            if reserved:
229
                req = clazz.known_names()
230
            else:
231
                req = [*clazz.required_index_names(), *clazz.required_columns]
232
            return clazz({r: [] for r in req})
233
        else:
234
            return clazz({})
235
236
    @classmethod
237
    def adjust_filename(cls, to: Optional[Path], default: Union[str, Path], replace: bool) -> Path:
0 ignored issues
show
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...
introduced by
Missing function or method docstring
Loading history...
238
        if to is None:
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
239
            return Path(default)
240
        elif str(to).startswith("."):
241
            return Path(default).with_suffix(str(to))
242
        elif to.is_dir() or to.suffix == "":
243
            return to / default
244
        if replace or not to.exists():
245
            return to
246
        raise FileExistsError(f"File {to} already exists")
247
248
    @classmethod
249
    def ntp_utc(cls) -> datetime:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
250
        ntp = Suretime.tagged.now_utc_ntp(
251
            ntp_server=MANDOS_SETTINGS.ntp_continent, ntp_clock="client-sent"
252
        )
253
        return ntp.use_clock_as_dt.dt
254
255
    @classmethod
256
    def utc(cls) -> datetime:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
257
        return Suretime.tagged.now_utc_sys().dt
258
259
    @classmethod
260
    def serialize_list(cls, lst: Sequence[str]) -> str:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
261
        return " || ".join([str(x) for x in lst])
262
263
    @classmethod
264
    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...
265
        return s.split(" || ")
266
267
268
START_TIME = MiscUtils.utc()
269
START_TIMESTAMP = START_TIME.isoformat(timespec="milliseconds")
270
271
272
__all__ = [
273
    "Api",
274
    "CompoundNotFoundError",
275
    "MandosResources",
276
    "CleverEnum",
277
    "ReflectionUtils",
278
    "InjectionError",
279
    "MiscUtils",
280
    "START_TIME",
281
    "START_TIMESTAMP",
282
]
283