Passed
Push — main ( 50e5ea...b7d236 )
by Douglas
01:49
created

pocketutils.tools.common_tools   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 299
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 144
dl 0
loc 299
rs 8.96
c 0
b 0
f 0
wmc 43

17 Methods

Rating   Name   Duplication   Size   Complexity  
A CommonTools.limit() 0 4 2
A CommonTools.or_raise() 0 17 5
A CommonTools.or_null() 0 6 3
A CommonTools.succeeds() 0 4 1
A CommonTools.try_none() 0 20 2
A CommonTools.iterator_has_elements() 0 11 2
A CommonTools.iter_rowcol() 0 11 2
A CommonTools.unique() 0 13 1
A CommonTools.devnull() 0 10 1
A CommonTools.is_probable_null() 0 19 1
A CommonTools.first() 0 23 3
A CommonTools.is_null() 0 13 3
A CommonTools.is_empty() 0 26 4
A CommonTools.parse_bool_flex() 0 20 2
A CommonTools.parse_bool() 0 15 4
A CommonTools.mem_size() 0 12 1
B CommonTools.multidict() 0 23 6

How to fix   Complexity   

Complexity

Complex classes like pocketutils.tools.common_tools 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
import sys
0 ignored issues
show
introduced by
Missing module docstring
Loading history...
2
from collections import defaultdict
3
from typing import (
4
    Any,
5
    Callable,
6
    Generator,
7
    Iterable,
8
    Iterator,
9
    Mapping,
10
    Optional,
11
    Sequence,
12
    Tuple,
13
    Type,
14
    TypeVar,
15
    Union,
16
)
17
18
import numpy as np
0 ignored issues
show
introduced by
Unable to import 'numpy'
Loading history...
19
20
from pocketutils.core.exceptions import RefusingRequestError
21
from pocketutils.core.input_output import DevNull
22
from pocketutils.core.internal import nicesize
23
from pocketutils.tools.base_tools import BaseTools
24
25
Y = TypeVar("Y")
0 ignored issues
show
Coding Style Naming introduced by
Class name "Y" 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...
26
T = 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...
27
Z = TypeVar("Z")
0 ignored issues
show
Coding Style Naming introduced by
Class name "Z" 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...
28
Q = TypeVar("Q")
0 ignored issues
show
Coding Style Naming introduced by
Class name "Q" 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...
29
30
31
class CommonTools(BaseTools):
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
32
    @classmethod
33
    def limit(cls, items: Iterable[Q], n: int) -> Generator[Q, None, None]:
0 ignored issues
show
Coding Style Naming introduced by
Argument name "n" 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...
34
        for i, x in zip(range(n), items):
0 ignored issues
show
Coding Style Naming introduced by
Variable name "x" 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...
Unused Code introduced by
The variable i seems to be unused.
Loading history...
35
            yield x
36
37
    @classmethod
38
    def try_none(
39
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
40
        function: Callable[[], T],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
41
        fail_val: Optional[T] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
42
        exception=Exception,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
43
    ) -> Optional[T]:
44
        """
45
        Returns the value of a function or None if it raised an exception.
46
47
        Args:
48
            function: Try calling this function
49
            fail_val: Return this value
50
            exception: Restrict caught exceptions to subclasses of this type
51
        """
52
        # noinspection PyBroadException
53
        try:
54
            return function()
55
        except exception:
0 ignored issues
show
Best Practice introduced by
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
56
            return fail_val
57
58
    @classmethod
59
    def succeeds(cls, function: Callable[[], Any], exception=Exception) -> bool:
60
        """Returns True iff `function` does not raise an error."""
61
        return cls.try_none(function, exception=exception) is not None
62
63
    @classmethod
64
    def or_null(cls, x: Any, dtype=lambda s: s, or_else: Any = None) -> Optional[Any]:
0 ignored issues
show
Coding Style Naming introduced by
Argument name "x" 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...
Comprehensibility Best Practice introduced by
The variable s does not seem to be defined.
Loading history...
65
        """
66
        Return ``None`` if the operation ``dtype`` on ``x`` failed; returns the result otherwise.
67
        """
68
        return or_else if cls.is_null(x) else dtype(x)
69
70
    @classmethod
71
    def or_raise(
0 ignored issues
show
Coding Style Naming introduced by
Argument name "x" 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...
72
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
73
        x: Any,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
74
        dtype=lambda s: s,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
Comprehensibility Best Practice introduced by
The variable s does not seem to be defined.
Loading history...
75
        or_else: Union[None, BaseException, Type[BaseException]] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
76
    ) -> Any:
77
        """
78
        Returns ``dtype(x)`` if ``x`` is not None, or raises ``or_else``.
79
        """
80
        if or_else is None:
81
            or_else = LookupError(f"Value is {x}")
82
        elif isinstance(or_else, type):
83
            or_else = or_else(f"Value is {x}")
84
        if cls.is_null(x):
85
            raise or_else
86
        return dtype(x)
87
88
    @classmethod
89
    def iterator_has_elements(cls, x: Iterator[Any]) -> bool:
0 ignored issues
show
Coding Style Naming introduced by
Argument name "x" 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...
90
        """
91
        Returns False iff ``next(x)`` raises a ``StopIteration``.
92
        WARNING: Tries to call ``next(x)``, progressing iterators. Don't use ``x`` after calling this.
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (102/100).

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

Loading history...
93
        Note that calling ``iterator_has_elements([5])`` will raise a `TypeError`
94
95
        Args:
96
            x: Must be an Iterator
97
        """
98
        return cls.succeeds(lambda: next(x), StopIteration)
99
100
    @classmethod
101
    def is_null(cls, x: Any) -> bool:
0 ignored issues
show
Coding Style Naming introduced by
Argument name "x" 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...
102
        """
103
        Returns True if x is either:
104
            - None
105
            - NaN
106
        """
107
        try:
108
            if np.isnan(x):
109
                return True
110
        except (ValueError, TypeError):
111
            pass
112
        return x is None or x == float("nan")
113
114
    @classmethod
115
    def is_empty(cls, x: Any) -> bool:
0 ignored issues
show
Coding Style Naming introduced by
Argument name "x" 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...
116
        """
117
        Returns True iff either:
118
            - x is None
119
            - np.is_nan(x)
120
            - x is something with 0 length
121
            - x is iterable and has 0 elements (will call ``__iter__``)
122
123
        Raises:
124
            RefusingRequestError If ``x`` is an Iterator. Calling this would empty the iterator, which is dangerous.
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (116/100).

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

Loading history...
125
        """
126
        if isinstance(x, Iterator):
0 ignored issues
show
introduced by
Second argument of isinstance is not a type
Loading history...
127
            raise RefusingRequestError("Do not call is_empty on an iterator.")
128
        try:
129
            if np.isnan(x):
130
                return True
131
        except (ValueError, TypeError):
132
            pass
133
        return (
134
            x is None
135
            or x == float("nan")
136
            or hasattr(x, "__len__")
137
            and len(x) == 0
138
            or hasattr(x, "__iter__")
139
            and len(list(iter(x))) == 0
140
        )
141
142
    @classmethod
143
    def is_probable_null(cls, x: Any) -> bool:
0 ignored issues
show
Coding Style Naming introduced by
Argument name "x" 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...
144
        """
145
        Returns True iff either:
146
            - x is None
147
            - np.is_nan(x)
148
            - x is something with 0 length
149
            - x is iterable and has 0 elements (will call ``__iter__``)
150
            - a str(x) is 'nan', 'n/a', 'null', or 'none'; case-insensitive
151
152
        Things that are **NOT** probable nulls:
153
            - "na"
154
            - 0
155
            - [None]
156
157
        Raises:
158
            TypeError If ``x`` is an Iterator. Calling this would empty the iterator, which is dangerous.
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...
159
        """
160
        return cls.is_empty(x) or str(x).lower() in ["nan", "n/a", "null", "none"]
161
162
    @classmethod
163
    def unique(cls, sequence: Iterable[T]) -> Sequence[T]:
164
        """
165
        Returns the unique items in `sequence`, in the order they appear in the iteration.
166
167
        Args:
168
            sequence: Any once-iterable sequence
169
170
        Returns:
171
            An ordered List of unique elements
172
        """
173
        seen = set()
174
        return [x for x in sequence if not (x in seen or seen.add(x))]
175
176
    @classmethod
177
    def first(cls, collection: Iterable[Any], attr: Optional[str] = None) -> Optional[Any]:
178
        """
179
        Gets the first element.
180
181
        WARNING: Tries to call ``next(x)``, progressing iterators.
182
183
        Args:
184
            collection: Any iterable
185
            attr: The name of the attribute that might be defined on the elements,
186
                or None to indicate the elements themselves should be used
187
188
        Returns:
189
            - The attribute of the first element if ``attr`` is defined on an element
190
            - None if the the sequence is empty
191
            - None if the sequence has no attribute ``attr``
192
        """
193
        try:
194
            # note: calling iter on an iterator creates a view only
195
            x = next(iter(collection))
0 ignored issues
show
Coding Style Naming introduced by
Variable name "x" 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...
196
            return x if attr is None else cls.look(x, attr)
197
        except StopIteration:
198
            return None
199
200
    @classmethod
201
    def iter_rowcol(cls, n_rows: int, n_cols: int) -> Generator[Tuple[int, int], None, None]:
202
        """
203
        An iterator over (row column) pairs for a row-first traversal of a grid with ``n_cols`` columns.
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (104/100).

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

Loading history...
204
205
        Example:
206
            >>> it = CommonTools.iter_rowcol(5, 3)
207
            >>> [next(it) for _ in range(5)]  # [(0,0),(0,1),(0,2),(1,0),(1,1)]
208
        """
209
        for i in range(n_rows * n_cols):
210
            yield i // n_cols, i % n_cols
211
212
    @classmethod
213
    def multidict(
214
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
215
        sequence: Iterable[Z],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
216
        key_attr: Union[str, Iterable[str], Callable[[Y], Z]],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
217
        skip_none: bool = False,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
218
    ) -> Mapping[Y, Sequence[Z]]:
219
        """
220
        Builds a mapping of some attribute in `sequence` to the containing elements of ``sequence``.
221
222
        Args:
223
            sequence: Any iterable
224
            key_attr: Usually string like 'attr1.attr2'; see `look`
225
            skip_none: If None, raises a `KeyError` if the key is missing for any item; otherwise, skips it
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (107/100).

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

Loading history...
226
        """
227
        dct = defaultdict(lambda: [])
228
        for item in sequence:
229
            v = CommonTools.look(item, key_attr)
0 ignored issues
show
Coding Style Naming introduced by
Variable name "v" 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...
230
            if not skip_none and v is None:
231
                raise KeyError(f"No {key_attr} in {item}")
232
            if v is not None:
233
                dct[v].append(item)
234
        return dct
235
236
    @classmethod
237
    def mem_size(cls, obj) -> str:
238
        """
239
        Returns the size of the object in memory as a human-readable string.
240
241
        Args:
242
            obj: Any Python object
243
244
        Returns:
245
            A human-readable size with units
246
        """
247
        return nicesize(sys.getsizeof(obj))
248
249
    @classmethod
250
    def devnull(cls):
251
        """
252
        Yields a 'writer' that does nothing.
253
254
        Example:
255
            >>> with CommonTools.devnull() as devnull:
256
            >>>     devnull.write('hello')
257
        """
258
        yield DevNull()
259
260
    @classmethod
261
    def parse_bool(cls, s: str) -> bool:
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...
262
        """
263
        Parses a 'true'/'false' string to a bool, ignoring case.
264
265
        Raises:
266
            ValueError: If neither true nor false
267
        """
268
        if isinstance(s, bool):
269
            return s
270
        if s.lower() == "false":
271
            return False
272
        if s.lower() == "true":
273
            return True
274
        raise ValueError(f"{s} is not true/false")
275
276
    @classmethod
277
    def parse_bool_flex(cls, s: str) -> bool:
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...
278
        """
279
        Parses a 'true'/'false'/'yes'/'no'/... string to a bool, ignoring case.
280
281
        Allowed:
282
            - "true", "t", "yes", "y", "1", "+"
283
            - "false", "f", "no", "n", "0", "-"
284
285
        Raises:
286
            ValueError: If neither true nor false
287
        """
288
        mp = {
0 ignored issues
show
Coding Style Naming introduced by
Variable name "mp" 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...
289
            **{v: True for v in {"true", "t", "yes", "y", "1", "+"}},
290
            **{v: False for v in {"false", "f", "no", "n", "0", "-"}},
291
        }
292
        v = mp.get(s.lower())
0 ignored issues
show
Coding Style Naming introduced by
Variable name "v" 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...
293
        if v is None:
294
            raise ValueError(f"{s.lower()} is not in {','.join(mp.keys())}")
295
        return v
296
297
298
__all__ = ["CommonTools"]
299