Passed
Push — main ( a79885...50e5ea )
by Douglas
02:25
created

CommonTools.mem_size()   A

Complexity

Conditions 1

Size

Total Lines 12
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nop 2
dl 0
loc 12
rs 10
c 0
b 0
f 0
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
        return (
108
            x is None
109
            or isinstance(x, np.float)
110
            and np.isnan(x)
111
            or (isinstance(x, float) and x == float("nan"))
112
        )
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
        return (
129
            x is None
130
            or isinstance(x, np.float)
131
            and np.isnan(x)
132
            or (isinstance(x, float) and x == float("nan"))
133
            or hasattr(x, "__len__")
134
            and len(x) == 0
135
            or hasattr(x, "__iter__")
136
            and len(list(iter(x))) == 0
137
        )
138
139
    @classmethod
140
    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...
141
        """
142
        Returns True iff either:
143
            - x is None
144
            - np.is_nan(x)
145
            - x is something with 0 length
146
            - x is iterable and has 0 elements (will call ``__iter__``)
147
            - a str(x) is 'nan', 'n/a', 'null', or 'none'; case-insensitive
148
149
        Things that are **NOT** probable nulls:
150
            - "na"
151
            - 0
152
            - [None]
153
154
        Raises:
155
            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...
156
        """
157
        return cls.is_empty(x) or str(x).lower() in ["nan", "n/a", "null", "none"]
158
159
    @classmethod
160
    def unique(cls, sequence: Iterable[T]) -> Sequence[T]:
161
        """
162
        Returns the unique items in `sequence`, in the order they appear in the iteration.
163
164
        Args:
165
            sequence: Any once-iterable sequence
166
167
        Returns:
168
            An ordered List of unique elements
169
        """
170
        seen = set()
171
        return [x for x in sequence if not (x in seen or seen.add(x))]
172
173
    @classmethod
174
    def first(cls, collection: Iterable[Any], attr: Optional[str] = None) -> Optional[Any]:
175
        """
176
        Gets the first element.
177
178
        WARNING: Tries to call ``next(x)``, progressing iterators.
179
180
        Args:
181
            collection: Any iterable
182
            attr: The name of the attribute that might be defined on the elements,
183
                or None to indicate the elements themselves should be used
184
185
        Returns:
186
            - The attribute of the first element if ``attr`` is defined on an element
187
            - None if the the sequence is empty
188
            - None if the sequence has no attribute ``attr``
189
        """
190
        try:
191
            # note: calling iter on an iterator creates a view only
192
            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...
193
            return x if attr is None else cls.look(x, attr)
194
        except StopIteration:
195
            return None
196
197
    @classmethod
198
    def iter_rowcol(cls, n_rows: int, n_cols: int) -> Generator[Tuple[int, int], None, None]:
199
        """
200
        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...
201
202
        Example:
203
            >>> it = CommonTools.iter_rowcol(5, 3)
204
            >>> [next(it) for _ in range(5)]  # [(0,0),(0,1),(0,2),(1,0),(1,1)]
205
        """
206
        for i in range(n_rows * n_cols):
207
            yield i // n_cols, i % n_cols
208
209
    @classmethod
210
    def multidict(
211
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
212
        sequence: Iterable[Z],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
213
        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...
214
        skip_none: bool = False,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
215
    ) -> Mapping[Y, Sequence[Z]]:
216
        """
217
        Builds a mapping of some attribute in `sequence` to the containing elements of ``sequence``.
218
219
        Args:
220
            sequence: Any iterable
221
            key_attr: Usually string like 'attr1.attr2'; see `look`
222
            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...
223
        """
224
        dct = defaultdict(lambda: [])
225
        for item in sequence:
226
            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...
227
            if not skip_none and v is None:
228
                raise KeyError(f"No {key_attr} in {item}")
229
            if v is not None:
230
                dct[v].append(item)
231
        return dct
232
233
    @classmethod
234
    def mem_size(cls, obj) -> str:
235
        """
236
        Returns the size of the object in memory as a human-readable string.
237
238
        Args:
239
            obj: Any Python object
240
241
        Returns:
242
            A human-readable size with units
243
        """
244
        return nicesize(sys.getsizeof(obj))
245
246
    @classmethod
247
    def devnull(cls):
248
        """
249
        Yields a 'writer' that does nothing.
250
251
        Example:
252
            >>> with CommonTools.devnull() as devnull:
253
            >>>     devnull.write('hello')
254
        """
255
        yield DevNull()
256
257
    @classmethod
258
    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...
259
        """
260
        Parses a 'true'/'false' string to a bool, ignoring case.
261
262
        Raises:
263
            ValueError: If neither true nor false
264
        """
265
        if isinstance(s, bool):
266
            return s
267
        if s.lower() == "false":
268
            return False
269
        if s.lower() == "true":
270
            return True
271
        raise ValueError(f"{s} is not true/false")
272
273
    @classmethod
274
    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...
275
        """
276
        Parses a 'true'/'false'/'yes'/'no'/... string to a bool, ignoring case.
277
278
        Allowed:
279
            - "true", "t", "yes", "y", "1", "+"
280
            - "false", "f", "no", "n", "0", "-"
281
282
        Raises:
283
            ValueError: If neither true nor false
284
        """
285
        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...
286
            **{v: True for v in {"true", "t", "yes", "y", "1", "+"}},
287
            **{v: False for v in {"false", "f", "no", "n", "0", "-"}},
288
        }
289
        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...
290
        if v is None:
291
            raise ValueError(f"{s.lower()} is not in {','.join(mp.keys())}")
292
        return v
293
294
295
__all__ = ["CommonTools"]
296