Passed
Push — main ( af1065...15d22f )
by Douglas
04:30
created

pocketutils.tools.common_tools.CommonTools.only()   B

Complexity

Conditions 7

Size

Total Lines 49
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 25
nop 5
dl 0
loc 49
rs 7.8799
c 0
b 0
f 0
1
import logging
0 ignored issues
show
introduced by
Missing module docstring
Loading history...
2
import sys
3
from collections import defaultdict
4
from collections.abc import ByteString, Callable, Generator, Iterable, Iterator, Mapping, Sequence
5
from contextlib import contextmanager
6
from typing import Any, Type, TypeVar
0 ignored issues
show
Unused Code introduced by
Unused Type imported from typing
Loading history...
7
8
from pocketutils.core._internal import is_lambda, look, parse_bool, parse_bool_flex
0 ignored issues
show
Bug introduced by
The name core does not seem to exist in module pocketutils.
Loading history...
9
from pocketutils.core.exceptions import (
0 ignored issues
show
Bug introduced by
The name core does not seem to exist in module pocketutils.
Loading history...
Unused Code introduced by
Unused XValueError imported from pocketutils.core.exceptions
Loading history...
10
    MultipleMatchesError,
11
    RefusingRequestError,
12
    XKeyError,
13
    XTypeError,
14
    XValueError,
15
)
16
from pocketutils.core.input_output import DevNull, Writeable
0 ignored issues
show
Bug introduced by
The name core does not seem to exist in module pocketutils.
Loading history...
17
18
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...
19
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...
20
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...
21
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...
22
logger = logging.getLogger("pocketutils")
23
24
25
class CommonTools:
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
best-practice introduced by
Too many public methods (28/20)
Loading history...
26
    def nice_size(n_bytes: int, *, space: str = "") -> str:
0 ignored issues
show
Coding Style Best Practice introduced by
Methods should have self as first argument.

It is a widespread convention and generally a good practice to name the first argument of methods self.

class SomeClass:
    def some_method(self):
        # ... do something
Loading history...
27
        """
28
        Uses IEC 1998 units, such as KiB (1024).
29
            n_bytes: Number of bytes
30
            space: Separator between digits and units
31
32
            Returns:
33
                Formatted string
34
        """
35
        data = {
36
            "PiB": 1024**5,
37
            "TiB": 1024**4,
38
            "GiB": 1024**3,
39
            "MiB": 1024**2,
40
            "KiB": 1024**1,
41
        }
42
        for suffix, scale in data.items():
43
            if n_bytes >= scale:
44
                break
45
        else:
46
            scale, suffix = 1, "B"
47
        return str(n_bytes // scale) + space + suffix
48
49
    @classmethod
50
    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...
introduced by
Value 'Iterable' is unsubscriptable
Loading history...
introduced by
Value 'Generator' is unsubscriptable
Loading history...
51
        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...
52
            yield x
53
54
    @classmethod
55
    def is_float(cls, s: Any) -> 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...
56
        """
57
        Returns whether ``float(s)`` succeeds.
58
        """
59
        try:
60
            float(s)
61
            return True
62
        except ValueError:
63
            return False
64
65
    @classmethod
66
    def try_none(
67
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
68
        function: Callable[[], T],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
introduced by
Value 'Callable' is unsubscriptable
Loading history...
69
        fail_val: T | None = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
70
        exception=Exception,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
71
    ) -> T | None:
72
        """
73
        Returns the value of a function or None if it raised an exception.
74
75
        Args:
76
            function: Try calling this function
77
            fail_val: Return this value
78
            exception: Restrict caught exceptions to subclasses of this type
79
        """
80
        # noinspection PyBroadException
81
        try:
82
            return function()
83
        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...
84
            return fail_val
85
86
    @classmethod
87
    def succeeds(cls, function: Callable[[], Any], exception=Exception) -> bool:
0 ignored issues
show
introduced by
Value 'Callable' is unsubscriptable
Loading history...
88
        """Returns True iff ``function`` does not raise an error."""
89
        return cls.try_none(function, exception=exception) is not None
90
91
    @classmethod
92
    def or_null(cls, x: Any, dtype=lambda s: s, or_else: Any = None) -> Any | None:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable s does not seem to be defined.
Loading history...
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...
93
        """
94
        Return ``None`` if the operation ``dtype`` on ``x`` failed; returns the result otherwise.
95
        """
96
        return or_else if cls.is_null(x) else dtype(x)
97
98
    @classmethod
99
    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...
100
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
101
        x: Any,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
102
        dtype=lambda s: s,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable s does not seem to be defined.
Loading history...
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
103
        or_else: BaseException | type[BaseException] | None = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
introduced by
Value 'type' is unsubscriptable
Loading history...
104
    ) -> Any:
105
        """
106
        Returns ``dtype(x)`` if ``x`` is not None, or raises ``or_else``.
107
        """
108
        if or_else is None:
109
            or_else = LookupError(f"Value is {x}")
110
        elif isinstance(or_else, type):
111
            or_else = or_else(f"Value is {x}")
112
        if cls.is_null(x):
113
            raise or_else
114
        return dtype(x)
115
116
    @classmethod
117
    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...
introduced by
Value 'Iterator' is unsubscriptable
Loading history...
118
        """
119
        Returns False iff ``next(x)`` raises a ``StopIteration``.
120
        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...
121
        Note that calling ``iterator_has_elements([5])`` will raise a `TypeError`
122
123
        Args:
124
            x: Must be an Iterator
125
        """
126
        return cls.succeeds(lambda: next(x), StopIteration)
127
128
    @classmethod
129
    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...
130
        """
131
        Returns True for None, NaN, and NaT (not a time) values from Numpy, Pandas, and Python.
132
        Not perfect; may return false positive True for types declared outside Numpy and Pandas.
133
        """
134
        if x is None:
135
            return True
136
        if isinstance(x, str):
137
            return False
138
        return str(x) in [
139
            "nan",  # float('NaN') and Numpy float NaN
140
            "NaN",  # Pandas NaN and decimal.Decimal NaN
141
            "<NA>",  # Pandas pd.NA
142
            "NaT",  # Numpy datetime and timedelta NaT
143
        ]
144
145
    @classmethod
146
    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...
147
        """
148
        Returns True if x is None, NaN according to Pandas, or contains 0 items.
149
150
        That is, if and only if:
151
            - :meth:`is_null`
152
            - x is something with 0 length
153
            - x is iterable and has 0 elements (will call ``__iter__``)
154
155
        Raises:
156
            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...
157
        """
158
        if isinstance(x, Iterator):
159
            raise RefusingRequestError("Do not call is_empty on an iterator.")
160
        try:
161
            if cls.is_null(x):
162
                return True
163
        except (ValueError, TypeError):
164
            pass
165
        return (
166
            hasattr(x, "__len__")
167
            and len(x) == 0
168
            or hasattr(x, "__iter__")
169
            and len(list(iter(x))) == 0
170
        )
171
172
    @classmethod
173
    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...
174
        """
175
        Returns True if ``x`` is None, NaN according to Pandas, 0 length, or a string representation.
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (101/100).

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

Loading history...
176
177
        Specifically, returns True if and only if:
178
            - :meth:`is_null`
179
            - x is something with 0 length
180
            - x is iterable and has 0 elements (will call ``__iter__``)
181
            - a str(x) is 'nan', 'na', 'n/a', 'null', or 'none'; case-insensitive
182
183
        Things that are **NOT** probable nulls:
184
            - ``0``
185
            - ``[None]``
186
187
        Raises:
188
            TypeError If ``x`` is an Iterator.
189
                      Calling this would empty the iterator, which is dangerous.
190
        """
191
        return cls.is_empty(x) or str(x).lower() in ["nan", "n/a", "na", "null", "none"]
192
193
    @classmethod
194
    def unique(cls, sequence: Iterable[T]) -> Sequence[T]:
0 ignored issues
show
introduced by
Value 'Iterable' is unsubscriptable
Loading history...
introduced by
Value 'Sequence' is unsubscriptable
Loading history...
195
        """
196
        Returns the unique items in `sequence`, in the order they appear in the iteration.
197
198
        Args:
199
            sequence: Any once-iterable sequence
200
201
        Returns:
202
            An ordered List of unique elements
203
        """
204
        seen = set()
205
        return [x for x in sequence if not (x in seen or seen.add(x))]
206
207
    @classmethod
208
    def first(cls, collection: Iterable[Any], attr: str | None = None) -> Any:
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
introduced by
Value 'Iterable' is unsubscriptable
Loading history...
209
        """
210
        Gets the first element.
211
212
        .. warning::
213
            Tries to call ``next(x)``, progressing iterators.
214
215
        Args:
216
            collection: Any iterable
217
            attr: The name of the attribute that might be defined on the elements,
218
                or None to indicate the elements themselves should be used
219
220
        Returns:
221
            Either ``None`` or the value, according to the rules:
222
                - The attribute of the first element if ``attr`` is defined on an element
223
                - None if the sequence is empty
224
                - None if the sequence has no attribute ``attr``
225
        """
226
        try:
227
            # note: calling iter on an iterator creates a view only
228
            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...
229
            return x if attr is None else look(x, attr)
230
        except StopIteration:
231
            return None
232
233
    @classmethod
234
    def iter_rowcol(cls, n_rows: int, n_cols: int) -> Generator[tuple[int, int], None, None]:
0 ignored issues
show
introduced by
Value 'Generator' is unsubscriptable
Loading history...
introduced by
Value 'tuple' is unsubscriptable
Loading history...
235
        """
236
        An iterator over (row column) pairs for a row-first grid traversal.
237
238
        Example:
239
            .. code-block::
240
                it = CommonTools.iter_rowcol(5, 3)
241
                [next(it) for _ in range(5)]  # [(0,0),(0,1),(0,2),(1,0),(1,1)]
242
        """
243
        for i in range(n_rows * n_cols):
244
            yield i // n_cols, i % n_cols
245
246
    @classmethod
247
    def multidict(
248
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
249
        sequence: Iterable[Z],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
introduced by
Value 'Iterable' is unsubscriptable
Loading history...
250
        key_attr: str | Iterable[str] | Callable[[Y], Z],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
introduced by
Value 'Iterable' is unsubscriptable
Loading history...
introduced by
Value 'Callable' is unsubscriptable
Loading history...
251
        skip_none: bool = False,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
252
    ) -> Mapping[Y, Sequence[Z]]:
0 ignored issues
show
introduced by
Value 'Mapping' is unsubscriptable
Loading history...
introduced by
Value 'Sequence' is unsubscriptable
Loading history...
253
        """
254
        Builds a mapping from keys to multiple values.
255
        Builds a mapping of some attribute in ``sequence`` to
256
        the containing elements of ``sequence``.
257
258
        Args:
259
            sequence: Any iterable
260
            key_attr: Usually string like 'attr1.attr2'; see `look`
261
            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...
262
        """
263
        dct = defaultdict(lambda: [])
264
        for item in sequence:
265
            v = 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...
266
            if not skip_none and v is None:
267
                raise XKeyError(f"No {key_attr} in {item}", key=key_attr)
268
            if v is not None:
269
                dct[v].append(item)
270
        return dct
271
272
    @classmethod
273
    def devnull(cls):
274
        """
275
        Yields a 'writer' that does nothing.
276
277
        Example:
278
            .. code-block::
279
280
                with CommonTools.devnull() as devnull:
281
                    devnull.write('hello')
282
        """
283
        yield DevNull()
284
285
    @classmethod
286
    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...
287
        """
288
        Parses a 'true'/'false' string to a bool, ignoring case.
289
290
        Raises:
291
            ValueError: If neither true nor false
292
        """
293
        return parse_bool(s)
294
295
    @classmethod
296
    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...
297
        """
298
        Parses a 'true'/'false'/'yes'/'no'/... string to a bool, ignoring case.
299
300
        Allowed:
301
            - "true", "t", "yes", "y", "1"
302
            - "false", "f", "no", "n", "0"
303
304
        Raises:
305
            XValueError: If neither true nor false
306
        """
307
        return parse_bool_flex(s)
308
309
    @classmethod
310
    def is_lambda(cls, function: Any) -> bool:
311
        """
312
        Returns whether this is a lambda function. Will return False for non-callables.
313
        """
314
        return is_lambda(function)
315
316
    @classmethod
317
    def only(
318
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
319
        sequence: Iterable[Any],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
introduced by
Value 'Iterable' is unsubscriptable
Loading history...
320
        condition: str | Callable[[Any], bool] = None,
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
introduced by
Value 'Callable' is unsubscriptable
Loading history...
321
        *,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
322
        name: str = "collection",
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
323
    ) -> Any:
324
        """
325
        Returns either the SINGLE (ONLY) UNIQUE ITEM in the sequence or raises an exception.
326
        Each item must have __hash__ defined on it.
327
328
        Args:
329
            sequence: A list of any items (untyped)
330
            condition: If nonnull, consider only those matching this condition
331
            name: Just a name for the collection to use in an error message
332
333
        Returns:
334
            The first item the sequence.
335
336
        Raises:
337
            LookupError If the sequence is empty
338
            MultipleMatchesError If there is more than one unique item.
339
        """
340
341
        def _only(sq):
0 ignored issues
show
Coding Style Naming introduced by
Argument name "sq" 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...
342
            st = set(sq)
0 ignored issues
show
Coding Style Naming introduced by
Variable name "st" 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...
343
            if len(st) > 1:
344
                raise MultipleMatchesError("More then 1 item in " + str(name))
345
            if len(st) == 0:
346
                raise LookupError("Empty " + str(name))
347
            return next(iter(st))
348
349
        if condition and isinstance(condition, str):
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
350
            return _only(
351
                [
352
                    s
353
                    for s in sequence
354
                    if (
355
                        not getattr(s, condition[1:])
356
                        if condition.startswith("!")
357
                        else getattr(s, condition)
358
                    )
359
                ]
360
            )
361
        elif condition:
362
            return _only([s for s in sequence if condition(s)])
363
        else:
364
            return _only(sequence)
365
366
    @classmethod
367
    def forever(cls) -> Iterator[int]:
0 ignored issues
show
introduced by
Value 'Iterator' is unsubscriptable
Loading history...
368
        """
369
        Yields i for i in range(0, infinity).
370
        Useful for simplifying a i = 0; while True: i += 1 block.
371
        """
372
        i = 0
373
        while True:
374
            yield i
375
            i += 1
376
377
    @classmethod
378
    def to_true_iterable(cls, s: Any) -> Iterable[Any]:
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
Value 'Iterable' is unsubscriptable
Loading history...
379
        """
380
        See :meth:`is_true_iterable`.
381
382
        Examples:
383
            - ``to_true_iterable('abc')         # ['abc']``
384
            - ``to_true_iterable(['ab', 'cd')]  # ['ab', 'cd']``
385
        """
386
        if cls.is_true_iterable(s):
0 ignored issues
show
unused-code introduced by
Unnecessary "else" after "return"
Loading history...
387
            return s
388
        else:
389
            return [s]
390
391
    @classmethod
392
    def is_true_iterable(cls, s: Any) -> 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...
393
        """
394
        Returns whether ``s`` is a probably "proper" iterable.
395
        In other words, iterable but not a string or bytes.
396
397
        .. caution::
398
            This is not fully reliable.
399
            Types that do not define ``__iter__`` but are iterable
400
            via ``__getitem__`` will not be included.
401
        """
402
        return (
403
            s is not None
404
            and isinstance(s, Iterable)
405
            and not isinstance(s, str)
406
            and not isinstance(s, ByteString)
407
        )
408
409
    @classmethod
410
    @contextmanager
411
    def null_context(cls) -> Generator[None, None, None]:
0 ignored issues
show
introduced by
Value 'Generator' is unsubscriptable
Loading history...
412
        """
413
        Returns an empty context (literally just yields).
414
        Useful to simplify when a generator needs to be used depending on a switch.
415
        Ex::
416
            if verbose_flag:
417
                do_something()
418
            else:
419
                with Tools.silenced():
420
                    do_something()
421
        Can become::
422
            with (Tools.null_context() if verbose else Tools.silenced()):
423
                do_something()
424
        """
425
        yield
426
427
    @classmethod
428
    def look(cls, obj: Y, attrs: str | Iterable[str] | Callable[[Y], Z]) -> Z | None:
0 ignored issues
show
introduced by
Value 'Iterable' is unsubscriptable
Loading history...
introduced by
Value 'Callable' is unsubscriptable
Loading history...
429
        """
430
        Follows a dotted syntax for getting an item nested in class attributes.
431
        Returns the value of a chain of attributes on object ``obj``,
432
        or None any object in that chain is None or lacks the next attribute.
433
434
        Example:
435
            Get a kitten's breed::
436
437
                BaseTools.look(kitten), 'breed.name')  # either None or a string
438
439
        Args:
440
            obj: Any object
441
            attrs: One of:
442
                - A string in the form attr1.attr2, translating to ``obj.attr1``
443
                - An iterable of strings of the attributes
444
                - A function that maps ``obj`` to its output;
445
                   equivalent to calling `attrs(obj)` but returning None on ``AttributeError``.
446
447
        Returns:
448
            Either None or the type of the attribute
449
450
        Raises:
451
            TypeError:
452
        """
453
        return look(obj, attrs)
454
455
    @classmethod
456
    def make_writer(cls, writer: Writeable | Callable[[str], Any]):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
introduced by
Value 'Callable' is unsubscriptable
Loading history...
457
        if Writeable.isinstance(writer):
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
458
            return writer
459
        elif callable(writer):
460
461
            class W_(Writeable):
0 ignored issues
show
Coding Style Naming introduced by
Class name "W_" 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...
introduced by
Missing class docstring
Loading history...
462
                def write(self, msg):
463
                    writer(msg)
464
465
                def flush(self):
466
                    pass
467
468
                def close(self):
469
                    pass
470
471
            return W_()
472
        raise XTypeError(f"{type(writer)} cannot be wrapped into a Writeable")
473
474
    @classmethod
475
    def get_log_function(cls, log: str | Callable[[str], Any] | None) -> Callable[[str], None]:
0 ignored issues
show
introduced by
Value 'Callable' is unsubscriptable
Loading history...
best-practice introduced by
Too many return statements (7/6)
Loading history...
476
        """
477
        Gets a logging function from user input.
478
        The rules are:
479
            - If None, uses logger.info
480
            - If 'print' or 'stdout',  use sys.stdout.write
481
            - If 'stderr', use sys.stderr.write
482
            - If another str or int, try using that logger level (raises an error if invalid)
483
            - If callable, returns it
484
            - If it has a callable method called 'write', uses that
485
486
        Returns:
487
            A function of the log message that returns None
488
        """
489
        if log is None:
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
490
            return logger.info
491
        elif isinstance(log, str) and log.lower() in ["print", "stdout"]:
492
            # noinspection PyTypeChecker
493
            return sys.stdout.write
494
        elif log == "stderr":
495
            # noinspection PyTypeChecker
496
            return sys.stderr.write
497
        elif isinstance(log, int):
498
            return getattr(logger, logging.getLevelName(log).lower())
499
        elif isinstance(log, str):
500
            return getattr(logger, log.lower())
501
        elif callable(log):
502
            return log
503
        elif hasattr(log, "write"):
504
            return log.write
505
        else:
506
            raise XTypeError(f"Log type {type(log)} not known", actual=str(type(log)))
507
508
    @classmethod
509
    def sentinel(cls, name: str) -> Any:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
510
        class _Sentinel:
511
            def __eq__(self, other):
512
                return self is other
513
514
            def __reduce__(self):
515
                return name  # returning string is for singletons
516
517
            def __str__(self):
518
                return name
519
520
            def __repr__(self):
521
                return name
522
523
        return _Sentinel()
524
525
    def __repr__(self):
526
        return self.__class__.__name__
527
528
    def __str__(self):
529
        return self.__class__.__name__
530
531
532
__all__ = ["CommonTools", "Writeable"]
533