python-utils 4.1.4__py3-none-any.whl → 4.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_utils/aio.py +13 -0
- python_utils/containers.py +49 -1
- python_utils/converters.py +12 -1
- python_utils/generators.py +42 -2
- python_utils/import_.py +21 -7
- python_utils/types.py +8 -0
- {python_utils-4.1.4.dist-info → python_utils-4.2.0.dist-info}/METADATA +2 -1
- {python_utils-4.1.4.dist-info → python_utils-4.2.0.dist-info}/RECORD +10 -10
- {python_utils-4.1.4.dist-info → python_utils-4.2.0.dist-info}/WHEEL +1 -1
- {python_utils-4.1.4.dist-info → python_utils-4.2.0.dist-info}/licenses/LICENSE +0 -0
python_utils/aio.py
CHANGED
|
@@ -18,6 +18,8 @@ _T = typing.TypeVar('_T')
|
|
|
18
18
|
_K = typing.TypeVar('_K')
|
|
19
19
|
#: Value type when collecting an async iterable of pairs into a dict.
|
|
20
20
|
_V = typing.TypeVar('_V')
|
|
21
|
+
#: Concrete collection returned by a custom container factory.
|
|
22
|
+
_C = typing.TypeVar('_C', bound=collections.abc.Collection[typing.Any])
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
async def acount(
|
|
@@ -83,6 +85,15 @@ async def acontainer(
|
|
|
83
85
|
"""Overload: collect the async iterable into a ``set``."""
|
|
84
86
|
|
|
85
87
|
|
|
88
|
+
@typing.overload
|
|
89
|
+
async def acontainer(
|
|
90
|
+
iterable: collections.abc.AsyncIterable[_T]
|
|
91
|
+
| collections.abc.Callable[..., collections.abc.AsyncIterable[_T]],
|
|
92
|
+
container: collections.abc.Callable[[collections.abc.Iterable[_T]], _C],
|
|
93
|
+
) -> _C:
|
|
94
|
+
"""Overload: preserve the result type of a custom container factory."""
|
|
95
|
+
|
|
96
|
+
|
|
86
97
|
async def acontainer(
|
|
87
98
|
iterable: collections.abc.AsyncIterable[_T]
|
|
88
99
|
| collections.abc.Callable[..., collections.abc.AsyncIterable[_T]],
|
|
@@ -96,6 +107,8 @@ async def acontainer(
|
|
|
96
107
|
So instead of doing `[item async for item in iterable]` you can do
|
|
97
108
|
`await acontainer(iterable)`.
|
|
98
109
|
|
|
110
|
+
``container`` may be any factory that accepts the collected items, such
|
|
111
|
+
as ``frozenset`` or a custom function. Its concrete return type is kept.
|
|
99
112
|
"""
|
|
100
113
|
iterable_: collections.abc.AsyncIterable[_T]
|
|
101
114
|
if callable(iterable):
|
python_utils/containers.py
CHANGED
|
@@ -73,6 +73,9 @@ if typing.TYPE_CHECKING:
|
|
|
73
73
|
KT = typing.TypeVar('KT')
|
|
74
74
|
#: A type alias for a type that can be used as a value in a dictionary.
|
|
75
75
|
VT = typing.TypeVar('VT')
|
|
76
|
+
GD = typing.TypeVar('GD')
|
|
77
|
+
#: Marks a key that ``LazyCastedDict.get`` did not find.
|
|
78
|
+
_MISSING: typing.Final[object] = object()
|
|
76
79
|
#: A type alias for a dictionary with keys of type KT and values of type VT.
|
|
77
80
|
DT = dict[KT, VT]
|
|
78
81
|
#: A type alias for the casted type of a dictionary key.
|
|
@@ -453,6 +456,8 @@ class LazyCastedDict(CastedDictBase[KT, VT]):
|
|
|
453
456
|
[(1, 2), (3, 4), (5, 6), (7, 8)]
|
|
454
457
|
>>> d[3]
|
|
455
458
|
4
|
|
459
|
+
>>> d.get('3')
|
|
460
|
+
4
|
|
456
461
|
|
|
457
462
|
# Casts are optional and can be disabled by passing None as the cast
|
|
458
463
|
>>> d = LazyCastedDict()
|
|
@@ -507,6 +512,40 @@ class LazyCastedDict(CastedDictBase[KT, VT]):
|
|
|
507
512
|
|
|
508
513
|
return value
|
|
509
514
|
|
|
515
|
+
@typing.overload
|
|
516
|
+
def get(self, key: typing.Any, default: None = None, /) -> VT | None:
|
|
517
|
+
"""Return a cast value or ``None`` when the key is absent."""
|
|
518
|
+
|
|
519
|
+
@typing.overload
|
|
520
|
+
def get(self, key: typing.Any, default: VT, /) -> VT:
|
|
521
|
+
"""Return a cast value or a default of the same type."""
|
|
522
|
+
|
|
523
|
+
@typing.overload
|
|
524
|
+
def get(self, key: typing.Any, default: GD, /) -> VT | GD:
|
|
525
|
+
"""Return a cast value or an arbitrary default unchanged."""
|
|
526
|
+
|
|
527
|
+
def get(
|
|
528
|
+
self, key: typing.Any, default: typing.Any = None, /
|
|
529
|
+
) -> typing.Any:
|
|
530
|
+
"""Read a lazily cast value, returning ``default`` for missing keys.
|
|
531
|
+
|
|
532
|
+
Value conversion errors propagate just as they do for indexed reads.
|
|
533
|
+
A key that the key cast rejects cannot have been stored, so it is
|
|
534
|
+
missing. A missing default is not converted or stored, and
|
|
535
|
+
``__missing__`` is not called, as with ``dict.get``.
|
|
536
|
+
"""
|
|
537
|
+
if self._key_cast is not None:
|
|
538
|
+
try:
|
|
539
|
+
key = self._key_cast(key)
|
|
540
|
+
except (TypeError, ValueError, ArithmeticError):
|
|
541
|
+
return super().get(key, default)
|
|
542
|
+
value = super().get(key, _MISSING)
|
|
543
|
+
if value is _MISSING:
|
|
544
|
+
return default
|
|
545
|
+
if self._value_cast is not None:
|
|
546
|
+
value = self._value_cast(value)
|
|
547
|
+
return value
|
|
548
|
+
|
|
510
549
|
def items( # type: ignore[override]
|
|
511
550
|
self,
|
|
512
551
|
) -> collections.abc.Generator[tuple[KT, VT], None, None]:
|
|
@@ -554,6 +593,15 @@ class UniqueList(list[HT]):
|
|
|
554
593
|
>>> l
|
|
555
594
|
[5, 10, 2, 3, 4]
|
|
556
595
|
|
|
596
|
+
Assigning an item to itself keeps it, including a NaN that does not
|
|
597
|
+
compare equal to itself:
|
|
598
|
+
|
|
599
|
+
>>> missing = float('nan')
|
|
600
|
+
>>> samples = UniqueList(missing, on_duplicate='raise')
|
|
601
|
+
>>> samples[0] = missing
|
|
602
|
+
>>> len(samples)
|
|
603
|
+
1
|
|
604
|
+
|
|
557
605
|
A value that was removed or replaced can be added again:
|
|
558
606
|
|
|
559
607
|
>>> l = UniqueList(1, 2, 3)
|
|
@@ -952,7 +1000,7 @@ class UniqueList(list[HT]):
|
|
|
952
1000
|
"""
|
|
953
1001
|
old_value: HT = self[index]
|
|
954
1002
|
known: bool = value in self._set
|
|
955
|
-
if known and value != old_value:
|
|
1003
|
+
if known and value is not old_value and value != old_value:
|
|
956
1004
|
if self.on_duplicate == 'raise':
|
|
957
1005
|
raise ValueError(f'Duplicate value: {value}')
|
|
958
1006
|
else:
|
python_utils/converters.py
CHANGED
|
@@ -139,7 +139,7 @@ def to_int(
|
|
|
139
139
|
|
|
140
140
|
def to_float(
|
|
141
141
|
input_: str,
|
|
142
|
-
default:
|
|
142
|
+
default: _aliases.Number = 0,
|
|
143
143
|
exception: _aliases.ExceptionsType = (ValueError, TypeError),
|
|
144
144
|
regexp: _RegexpType = None,
|
|
145
145
|
) -> _aliases.Number:
|
|
@@ -308,6 +308,9 @@ def scale_1024(
|
|
|
308
308
|
|
|
309
309
|
Use to format numbers of bytes to KiB, MiB, etc.
|
|
310
310
|
|
|
311
|
+
A prefix is used only when the input reaches its power of 1024. For
|
|
312
|
+
integer inputs, the prefix is chosen before converting the value to float.
|
|
313
|
+
|
|
311
314
|
>>> scale_1024(310, 3)
|
|
312
315
|
(310.0, 0)
|
|
313
316
|
>>> scale_1024(2048, 3)
|
|
@@ -325,6 +328,14 @@ def scale_1024(
|
|
|
325
328
|
# Never below zero: a number under 1 has a negative logarithm, and a
|
|
326
329
|
# negative power would index the prefixes from the wrong end.
|
|
327
330
|
power = max(min(int(math.log(x, 2) / 10), n_prefixes - 1), 0)
|
|
331
|
+
# A logarithm can round a value just below a prefix boundary up to
|
|
332
|
+
# that boundary. Compare with the exact integer power before the
|
|
333
|
+
# input itself is rounded to float. Only an int needs the exact
|
|
334
|
+
# comparison: anything else goes through float(), as 4.1.4 did, so
|
|
335
|
+
# a numeric type without `__lt__` keeps working.
|
|
336
|
+
exact: int | float = x if isinstance(x, int) else float(x)
|
|
337
|
+
if power and exact < 2 ** (10 * power):
|
|
338
|
+
power -= 1
|
|
328
339
|
scaled = float(x) / (2 ** (10 * power))
|
|
329
340
|
return scaled, power
|
|
330
341
|
|
python_utils/generators.py
CHANGED
|
@@ -15,6 +15,7 @@ import asyncio
|
|
|
15
15
|
import collections.abc
|
|
16
16
|
import time
|
|
17
17
|
import typing
|
|
18
|
+
import warnings
|
|
18
19
|
|
|
19
20
|
import python_utils
|
|
20
21
|
from python_utils import _aliases
|
|
@@ -23,6 +24,29 @@ from python_utils import _aliases
|
|
|
23
24
|
_T = typing.TypeVar('_T')
|
|
24
25
|
|
|
25
26
|
|
|
27
|
+
def _warn_nonpositive_batch_size(
|
|
28
|
+
batch_size: int | None, interval: _aliases.delta_type | None = None
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Warn for a size below 1 that is used without an interval.
|
|
31
|
+
|
|
32
|
+
The size itself is left alone, so 4.x keeps its results: with an
|
|
33
|
+
interval, a size of 0 still yields an empty batch for an idle interval.
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
nonpositive: bool = batch_size is not None and batch_size < 1
|
|
37
|
+
except TypeError:
|
|
38
|
+
# A size that does not compare with 1 never matched a batch length
|
|
39
|
+
# either, so it keeps its 4.x result.
|
|
40
|
+
return
|
|
41
|
+
if nonpositive and not interval:
|
|
42
|
+
warnings.warn(
|
|
43
|
+
'batch_size must be greater than zero; sizes below 1 are '
|
|
44
|
+
'deprecated and will raise ValueError in python-utils 5.0',
|
|
45
|
+
DeprecationWarning,
|
|
46
|
+
stacklevel=3,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
26
50
|
async def _cancel(pending: set[asyncio.Future[_T]]) -> None:
|
|
27
51
|
"""Cancel the items that are still on their way and wait for them.
|
|
28
52
|
|
|
@@ -78,14 +102,22 @@ async def abatcher(
|
|
|
78
102
|
Args:
|
|
79
103
|
generator: The async generator or iterator to batch.
|
|
80
104
|
batch_size (typing.Optional[int], optional): The number of items per
|
|
81
|
-
batch. Defaults to None.
|
|
105
|
+
batch. Defaults to None. With an interval, None or a value below
|
|
106
|
+
1 means no size limit. Without an interval, sizes below 1 are
|
|
107
|
+
deprecated and will raise ValueError in version 5.0.
|
|
82
108
|
interval (typing.Optional[_aliases.delta_type], optional): The time
|
|
83
109
|
interval to wait before yielding a batch. Defaults to None.
|
|
84
110
|
|
|
85
111
|
Yields:
|
|
86
112
|
collections.abc.AsyncGenerator[list[_T], None]: A generator that yields
|
|
87
113
|
batches of items.
|
|
114
|
+
|
|
115
|
+
Warns:
|
|
116
|
+
DeprecationWarning: If batch_size is below 1 without an interval.
|
|
117
|
+
The existing behavior is preserved until version 5.0.
|
|
88
118
|
"""
|
|
119
|
+
_warn_nonpositive_batch_size(batch_size, interval)
|
|
120
|
+
|
|
89
121
|
batch: list[_T] = []
|
|
90
122
|
|
|
91
123
|
assert batch_size or interval, 'Must specify either batch_size or interval'
|
|
@@ -170,12 +202,20 @@ def batcher(
|
|
|
170
202
|
Args:
|
|
171
203
|
iterable (collections.abc.Iterable[_T]): The iterable to batch.
|
|
172
204
|
batch_size (int, optional): The number of items per batch. Defaults
|
|
173
|
-
to 10.
|
|
205
|
+
to 10. Sizes below 1 are deprecated and will raise ValueError in
|
|
206
|
+
version 5.0. Until then, they yield all items in a single batch,
|
|
207
|
+
or no batches for an empty iterable.
|
|
174
208
|
|
|
175
209
|
Yields:
|
|
176
210
|
collections.abc.Generator[list[_T], None, None]: A generator that
|
|
177
211
|
yields batches of items.
|
|
212
|
+
|
|
213
|
+
Warns:
|
|
214
|
+
DeprecationWarning: If batch_size is below 1. The warning is issued
|
|
215
|
+
when iteration begins, before consuming any source items.
|
|
178
216
|
"""
|
|
217
|
+
_warn_nonpositive_batch_size(batch_size)
|
|
218
|
+
|
|
179
219
|
batch: list[_T] = []
|
|
180
220
|
for item in iterable:
|
|
181
221
|
batch.append(item)
|
python_utils/import_.py
CHANGED
|
@@ -87,7 +87,8 @@ def import_global( # noqa: C901
|
|
|
87
87
|
for everything. An empty list also imports everything. A single
|
|
88
88
|
name needs a list as well, a bare string is read as a collection
|
|
89
89
|
of one-character names. Names that start with an underscore are
|
|
90
|
-
never imported.
|
|
90
|
+
never imported. Declared or explicitly requested names may be
|
|
91
|
+
provided lazily by the module, even if absent from ``dir()``.
|
|
91
92
|
exceptions (Exception): the exception to catch, e.g. ImportError
|
|
92
93
|
locals_: the `locals()` method (in case you need a different scope)
|
|
93
94
|
globals_: the `globals()` method (in case you need a different scope)
|
|
@@ -96,7 +97,7 @@ def import_global( # noqa: C901
|
|
|
96
97
|
"""
|
|
97
98
|
frame = None
|
|
98
99
|
name_parts: list[str] = name.split('.')
|
|
99
|
-
modules_set: set[
|
|
100
|
+
modules_set: set[object] = set()
|
|
100
101
|
try:
|
|
101
102
|
# If locals_ or globals_ are not given, autodetect them by inspecting
|
|
102
103
|
# the current stack
|
|
@@ -136,12 +137,25 @@ def import_global( # noqa: C901
|
|
|
136
137
|
if not modules:
|
|
137
138
|
modules_set = set(getattr(module, '__all__', dir(module)))
|
|
138
139
|
else:
|
|
139
|
-
modules_set = set(modules)
|
|
140
|
-
|
|
141
|
-
# Add all items in modules to the global scope
|
|
142
|
-
|
|
143
|
-
|
|
140
|
+
modules_set = set(modules)
|
|
141
|
+
|
|
142
|
+
# Add all items in modules to the global scope. A name that dir()
|
|
143
|
+
# does not list can still be a lazy export of the module's
|
|
144
|
+
# __getattr__. It is resolved only when __all__ declares it, and
|
|
145
|
+
# skipped when that fails, as it was before lazy exports.
|
|
146
|
+
listed = set(dir(module))
|
|
147
|
+
exported: set[object] = set(getattr(module, '__all__', ()))
|
|
148
|
+
for k in modules_set:
|
|
149
|
+
if not isinstance(k, str) or not k or k[0] == '_':
|
|
150
|
+
continue
|
|
151
|
+
if k in listed:
|
|
144
152
|
globals_[k] = getattr(module, k)
|
|
153
|
+
elif k in exported:
|
|
154
|
+
try:
|
|
155
|
+
globals_[k] = getattr(module, k)
|
|
156
|
+
except Exception:
|
|
157
|
+
# A failing lazy export was never imported before.
|
|
158
|
+
continue
|
|
145
159
|
except exceptions as e:
|
|
146
160
|
return e
|
|
147
161
|
finally:
|
python_utils/types.py
CHANGED
|
@@ -16,6 +16,7 @@ The module also configures Pyright to ignore wildcard import warnings.
|
|
|
16
16
|
# `from python_utils.types import datetime, decimal` worked in 3.x.
|
|
17
17
|
import datetime as datetime
|
|
18
18
|
import decimal as decimal
|
|
19
|
+
import sys
|
|
19
20
|
from re import Match, Pattern
|
|
20
21
|
from types import * # pragma: no cover # noqa: F403
|
|
21
22
|
from typing import * # pragma: no cover # noqa: F403
|
|
@@ -175,3 +176,10 @@ __all__ = [
|
|
|
175
176
|
'runtime_checkable',
|
|
176
177
|
'timestamp_type',
|
|
177
178
|
]
|
|
179
|
+
|
|
180
|
+
# Python 3.15 removed these from `typing`. Keeping them in `__all__` there
|
|
181
|
+
# would make `from python_utils.types import *` raise `AttributeError`. They
|
|
182
|
+
# stay at their 4.1.4 positions on older versions.
|
|
183
|
+
if sys.version_info >= (3, 15): # pragma: no cover
|
|
184
|
+
__all__.remove('ByteString')
|
|
185
|
+
__all__.remove('no_type_check_decorator')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-utils
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.2.0
|
|
4
4
|
Summary: Python Utils is a module with some convenient utilities not included with the standard Python install
|
|
5
5
|
Keywords: utils,utilities,helpers,typing,typed,async,lazy-imports
|
|
6
6
|
Author: Rick van Hattem
|
|
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
19
20
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
21
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
21
22
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
python_utils/__about__.py,sha256=qmqR375psvJc8IPYmLhruuaiouHzhdHu7DozRZG34DM,1343
|
|
2
2
|
python_utils/__init__.py,sha256=atYwTBHh2KNGdPSawwQKYCL9UolHho3L4WFxo901WDg,5958
|
|
3
3
|
python_utils/_aliases.py,sha256=xf_HrYaAa9ZKZAdYYJwIen-OB3nFGI5TTUETH5_ayGY,1641
|
|
4
|
-
python_utils/aio.py,sha256=
|
|
5
|
-
python_utils/containers.py,sha256=
|
|
6
|
-
python_utils/converters.py,sha256=
|
|
4
|
+
python_utils/aio.py,sha256=C4R3BLDINMMucuDc9-kGdGdP70ayn2Ctvjx6Q6BxJZA,4846
|
|
5
|
+
python_utils/containers.py,sha256=cC-htd9QBu4hSoR5meB_10845XKQDIsoS-2mzyhW0hg,38906
|
|
6
|
+
python_utils/converters.py,sha256=Klk5wOYdKzshMqaeaP6MkUKmYSjjHl8xqAcyfkEighE,17927
|
|
7
7
|
python_utils/decorators.py,sha256=UyVuV7Phf8kjAt38uA-F9MFH2o8zM4t-fmlgt3KL5cc,8225
|
|
8
8
|
python_utils/exceptions.py,sha256=B5wljs3Id6eQ9eRaur-ZMPGGmhHNwh9F08Sfw6fuLX4,1230
|
|
9
9
|
python_utils/formatters.py,sha256=rvbeP7H3h8Hll573PZkWsJPuHETjEHpWleb0tFck8MM,7031
|
|
10
|
-
python_utils/generators.py,sha256=
|
|
11
|
-
python_utils/import_.py,sha256=
|
|
10
|
+
python_utils/generators.py,sha256=dw-x72fitGgdeqLS4_xyspS81ayqExvT77THHoZBA5I,8307
|
|
11
|
+
python_utils/import_.py,sha256=xabWnas9OnFs7ks52D7yqpeKg8SvkBdHZIfTmuk5yJ8,6308
|
|
12
12
|
python_utils/logger.py,sha256=-QuF7Zfo7pxc4nTmAPc9Pm-oz5wsxXIX7NeCnYEPI6I,14043
|
|
13
13
|
python_utils/loguru.py,sha256=qsap-aXByGXsd4KrYmtneTSUARVIdeVu9xgmrFPtqSI,2696
|
|
14
14
|
python_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
15
|
python_utils/terminal.py,sha256=GL3n2_rT98LBNmxy-amY-hezGKStCo-9_d_pR-TMgv8,6823
|
|
16
16
|
python_utils/time.py,sha256=fZz00bpTeLXFFhbAo03LiMLWuzSJU4iPbyGSe7PYshU,16979
|
|
17
|
-
python_utils/types.py,sha256=
|
|
18
|
-
python_utils-4.
|
|
19
|
-
python_utils-4.
|
|
20
|
-
python_utils-4.
|
|
21
|
-
python_utils-4.
|
|
17
|
+
python_utils/types.py,sha256=VQLrKDRerOuhYxMhCaWWbUBnDXJhFx_G20gIfyolgug,4682
|
|
18
|
+
python_utils-4.2.0.dist-info/licenses/LICENSE,sha256=_Zrs9NZu-G6_aZT2g3Jv5_JREJQfhRNBC61EH6WDH-o,1501
|
|
19
|
+
python_utils-4.2.0.dist-info/WHEEL,sha256=MWx7psoF7KNST9G72pGtiBYuaXRXQXBk9pw5R6FzYko,80
|
|
20
|
+
python_utils-4.2.0.dist-info/METADATA,sha256=b5rgvrejxzOa3qwhB3eernFS84IR-8IsYM2VBliBDqo,16039
|
|
21
|
+
python_utils-4.2.0.dist-info/RECORD,,
|
|
File without changes
|