Passed
Push — main ( 520e83...b06663 )
by Douglas
01:43
created

pocketutils.tools.base_tools   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 309
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 157
dl 0
loc 309
rs 8.96
c 0
b 0
f 0
wmc 43

13 Methods

Rating   Name   Duplication   Size   Complexity  
B BaseTools.zip_strict() 0 38 8
A BaseTools.zip_list() 0 23 2
A BaseTools.__repr__() 0 2 1
A BaseTools.to_true_iterable() 0 13 2
A BaseTools.is_lambda() 0 13 3
A BaseTools.look() 0 26 1
A BaseTools.is_true_iterable() 0 11 1
C BaseTools.get_log_function() 0 37 11
B BaseTools.only() 0 48 7
A BaseTools.forever() 0 10 2
A BaseTools.__str__() 0 2 1
A BaseTools.null_context() 0 17 1
A BaseTools.make_writer() 0 18 3

How to fix   Complexity   

Complexity

Complex classes like pocketutils.tools.base_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 logging
0 ignored issues
show
introduced by
Missing module docstring
Loading history...
2
import sys
3
from contextlib import contextmanager
4
from typing import (
5
    Any,
6
    Callable,
7
    Generator,
8
    Iterable,
9
    Iterator,
10
    List,
11
    Optional,
12
    Tuple,
13
    TypeVar,
14
    Union,
15
)
16
17
from pocketutils.core.input_output import Writeable
0 ignored issues
show
Bug introduced by
The name core does not seem to exist in module pocketutils.
Loading history...
18
19
from pocketutils.core._internal import look as _look
0 ignored issues
show
Bug introduced by
The name core does not seem to exist in module pocketutils.
Loading history...
20
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...
21
    LengthError,
22
    LengthMismatchError,
23
    MultipleMatchesError,
24
    XTypeError,
25
)
26
27
logger = logging.getLogger("pocketutils")
28
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...
29
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...
30
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...
31
32
33
class BaseTools:
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
34
    @classmethod
35
    def is_lambda(cls, function: Any) -> bool:
36
        """
37
        Returns whether this is a lambda function. Will return False for non-callables.
38
        """
39
        LAMBDA = lambda: 0
0 ignored issues
show
Coding Style Naming introduced by
Variable name "LAMBDA" 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...
40
        if not hasattr(function, "__name__"):
41
            return False  # not a function
42
        return (
43
            isinstance(function, type(LAMBDA))
44
            and function.__name__ == LAMBDA.__name__
45
            or str(function).startswith("<function <lambda> at ")
46
            and str(function).endswith(">")
47
        )
48
49
    @classmethod
50
    def only(
51
        cls,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
52
        sequence: Iterable[Any],
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
53
        condition: Union[str, Callable[[Any], bool]] = None,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
54
        name: str = "collection",
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
55
    ) -> Any:
56
        """
57
        Returns either the SINGLE (ONLY) UNIQUE ITEM in the sequence or raises an exception.
58
        Each item must have __hash__ defined on it.
59
60
        Args:
61
            sequence: A list of any items (untyped)
62
            condition: If nonnull, consider only those matching this condition
63
            name: Just a name for the collection to use in an error message
64
65
        Returns:
66
            The first item the sequence.
67
68
        Raises:
69
            LookupError If the sequence is empty
70
            MultipleMatchesError If there is more than one unique item.
71
        """
72
73
        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...
74
            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...
75
            if len(st) > 1:
76
                raise MultipleMatchesError("More then 1 item in " + str(name))
77
            if len(st) == 0:
78
                raise LookupError("Empty " + str(name))
79
            return next(iter(st))
80
81
        if condition and isinstance(condition, str):
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
82
            return _only(
83
                [
84
                    s
85
                    for s in sequence
86
                    if (
87
                        not getattr(s, condition[1:])
88
                        if condition.startswith("!")
89
                        else getattr(s, condition)
90
                    )
91
                ]
92
            )
93
        elif condition:
94
            return _only([s for s in sequence if condition(s)])
95
        else:
96
            return _only(sequence)
97
98
    @classmethod
99
    def zip_strict(cls, *args: Iterable[Any]) -> Generator[Tuple[Any], None, None]:
100
        """
101
        Same as zip(), but raises an IndexError if the lengths don't match.
102
103
        Args:
104
            args: Same as with ``zip``
105
106
        Yields:
107
            Tuples corresponding to the input items
108
109
        Raises:
110
            LengthMismatchError: If the lengths of the input iterators don't match
111
        """
112
        # we need to catch these cases before or they'll fail
113
        # in particular, 1 element would fail with a LengthMismatchError
114
        # and 0 elements would loop forever
115
        if len(args) < 2:
116
            yield from zip(*args)
117
            return
118
        iters = [iter(axis) for axis in args]
119
        n_elements = 0
120
        failures = []
121
        while len(failures) == 0:
122
            values = []
123
            failures = []
124
            for axis, iterator in enumerate(iters):
125
                try:
126
                    values.append(next(iterator))
127
                except StopIteration:
128
                    failures.append(axis)
129
            if len(failures) == 0:
130
                yield tuple(values)
131
            elif len(failures) == 1:
132
                raise LengthError(f"Too few elements ({n_elements}) along axis {failures[0]}")
133
            elif len(failures) < len(iters):
134
                raise LengthError(f"Too few elements ({n_elements}) along axes {failures}")
135
            n_elements += 1
136
137
    @classmethod
138
    def zip_list(cls, *args) -> List[Tuple[Any]]:
139
        """
140
        Same as ``zip_strict``, but more informative.
141
        Converts to a list and can provide a more detailed error message.
142
        Zips two sequences into a list of tuples and raises an
143
        IndexError if the lengths don't match.
144
145
        Args:
146
            args: Same as with ``zip``
147
148
        Yields:
149
            Tuples corresponding to the input items
150
151
        Raises:
152
            LengthMismatchError: If the lengths of the input iterators don't match
153
        """
154
        try:
155
            return list(cls.zip_strict(*args))
156
        except LengthMismatchError:
157
            raise LengthMismatchError(
158
                "Length mismatch in zip_strict: Sizes are {}".format([len(x) for x in args])
159
            ) from None
160
161
    @classmethod
162
    def forever(cls) -> Iterator[int]:
163
        """
164
        Yields i for i in range(0, infinity).
165
        Useful for simplifying a i = 0; while True: i += 1 block.
166
        """
167
        i = 0
168
        while True:
169
            yield i
170
            i += 1
171
172
    @classmethod
173
    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...
174
        """
175
        See :meth:`is_true_iterable`.
176
177
        Examples:
178
            - ``to_true_iterable('abc')         # ['abc']``
179
            - ``to_true_iterable(['ab', 'cd')]  # ['ab', 'cd']``
180
        """
181
        if BaseTools.is_true_iterable(s):
0 ignored issues
show
unused-code introduced by
Unnecessary "else" after "return"
Loading history...
182
            return s
183
        else:
184
            return [s]
185
186
    @classmethod
187
    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...
188
        """
189
        Returns whether ``s`` is a probably "proper" iterable.
190
        In other words, iterable but not a string or bytes
191
        """
192
        return (
193
            s is not None
194
            and isinstance(s, Iterable)
0 ignored issues
show
introduced by
Second argument of isinstance is not a type
Loading history...
195
            and not isinstance(s, str)
196
            and not isinstance(s, bytes)
197
        )
198
199
    @classmethod
200
    @contextmanager
201
    def null_context(cls) -> Generator[None, None, None]:
202
        """
203
        Returns an empty context (literally just yields).
204
        Useful to simplify when a generator needs to be used depending on a switch.
205
        Ex:
206
            if verbose_flag:
207
                do_something()
208
            else:
209
                with Tools.silenced():
210
                    do_something()
211
        Can become:
212
            with (Tools.null_context() if verbose else Tools.silenced()):
213
                do_something()
214
        """
215
        yield
216
217
    @classmethod
218
    def look(cls, obj: Y, attrs: Union[str, Iterable[str], Callable[[Y], Z]]) -> Optional[Z]:
219
        """
220
        Returns the value of a chain of attributes on object ``obj``,
221
        or None any object in that chain is None or lacks the next attribute.
222
223
        Example:
224
            Get a kitten's breed::
225
226
            BaseTools.look(kitten), 'breed.name')  # either None or a string
227
228
        Args:
229
            obj: Any object
230
            attrs: One of:
231
                - A string in the form attr1.attr2, translating to ``obj.attr1``
232
                - An iterable of strings of the attributes
233
                - A function that maps ``obj`` to its output;
234
                   equivalent to calling `attrs(obj)` but returning None on ``AttributeError``.
235
236
        Returns:
237
            Either None or the type of the attribute
238
239
        Raises:
240
            TypeError:
241
        """
242
        return _look(obj, attrs)
243
244
    @classmethod
245
    def make_writer(cls, writer: Union[Writeable, Callable[[str], Any]]):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
246
        if Writeable.isinstance(writer):
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
247
            return writer
248
        elif callable(writer):
249
250
            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...
251
                def write(self, msg):
252
                    writer(msg)
253
254
                def flush(self):
255
                    pass
256
257
                def close(self):
258
                    pass
259
260
            return W_()
261
        raise XTypeError(f"{type(writer)} cannot be wrapped into a Writeable")
262
263
    @classmethod
264
    def get_log_function(
0 ignored issues
show
best-practice introduced by
Too many return statements (8/6)
Loading history...
265
        cls, log: Union[None, str, Callable[[str], None], Any]
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
266
    ) -> Callable[[str], None]:
267
        """
268
        Gets a logging function from user input.
269
        The rules are:
270
            - If None, uses logger.info
271
            - If 'print' or 'stdout',  use sys.stdout.write
272
            - If 'stderr', use sys.stderr.write
273
            - If another str or int, try using that logger level (raises an error if invalid)
274
            - If callable, returns it
275
            - If it has a callable method called 'write', uses that
276
277
        Returns:
278
            A function of the log message that returns None
279
        """
280
        if log is None:
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
281
            return logger.info
282
        elif isinstance(log, str) and log.lower() in ["print", "stdout"]:
283
            # noinspection PyTypeChecker
284
            return sys.stdout.write
285
        elif log == "stderr":
286
            # noinspection PyTypeChecker
287
            return sys.stderr.write
288
        elif isinstance(log, int):
289
            return getattr(logger, logging.getLevelName(log).lower())
290
        elif isinstance(log, str):
291
            return getattr(logger, log.lower())
292
        elif callable(log):
293
            return log
294
        elif hasattr(log, "write") and getattr(log, "write"):
295
            return getattr(log, "write")
296
        elif hasattr(log, "write"):
297
            return log.write
298
        else:
299
            raise XTypeError(f"Log type {type(log)} not known", actual=str(type(log)))
300
301
    def __repr__(self):
302
        return self.__class__.__name__
303
304
    def __str__(self):
305
        return self.__class__.__name__
306
307
308
__all__ = ["BaseTools"]
309