dycw-utilities 0.146.2__py3-none-any.whl → 0.178.1__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.
Potentially problematic release.
This version of dycw-utilities might be problematic. Click here for more details.
- dycw_utilities-0.178.1.dist-info/METADATA +34 -0
- dycw_utilities-0.178.1.dist-info/RECORD +105 -0
- dycw_utilities-0.178.1.dist-info/WHEEL +4 -0
- {dycw_utilities-0.146.2.dist-info → dycw_utilities-0.178.1.dist-info}/entry_points.txt +1 -0
- utilities/__init__.py +1 -1
- utilities/altair.py +10 -7
- utilities/asyncio.py +129 -50
- utilities/atomicwrites.py +1 -1
- utilities/atools.py +64 -4
- utilities/cachetools.py +9 -6
- utilities/click.py +144 -49
- utilities/concurrent.py +1 -1
- utilities/contextlib.py +4 -2
- utilities/contextvars.py +20 -1
- utilities/cryptography.py +3 -3
- utilities/dataclasses.py +15 -28
- utilities/docker.py +387 -0
- utilities/enum.py +2 -2
- utilities/errors.py +17 -3
- utilities/fastapi.py +8 -3
- utilities/fpdf2.py +2 -2
- utilities/functions.py +20 -297
- utilities/git.py +19 -0
- utilities/grp.py +28 -0
- utilities/hypothesis.py +361 -79
- utilities/importlib.py +17 -1
- utilities/inflect.py +1 -1
- utilities/iterables.py +33 -58
- utilities/jinja2.py +148 -0
- utilities/json.py +1 -1
- utilities/libcst.py +7 -7
- utilities/logging.py +131 -93
- utilities/math.py +8 -4
- utilities/more_itertools.py +4 -6
- utilities/operator.py +1 -1
- utilities/orjson.py +86 -34
- utilities/os.py +49 -2
- utilities/packaging.py +115 -0
- utilities/parse.py +2 -2
- utilities/pathlib.py +66 -34
- utilities/permissions.py +298 -0
- utilities/platform.py +5 -4
- utilities/polars.py +934 -420
- utilities/polars_ols.py +1 -1
- utilities/postgres.py +317 -153
- utilities/pottery.py +10 -86
- utilities/pqdm.py +3 -3
- utilities/pwd.py +28 -0
- utilities/pydantic.py +4 -51
- utilities/pydantic_settings.py +240 -0
- utilities/pydantic_settings_sops.py +76 -0
- utilities/pyinstrument.py +5 -5
- utilities/pytest.py +100 -126
- utilities/pytest_plugins/pytest_randomly.py +1 -1
- utilities/pytest_plugins/pytest_regressions.py +7 -3
- utilities/pytest_regressions.py +27 -8
- utilities/random.py +11 -6
- utilities/re.py +1 -1
- utilities/redis.py +101 -64
- utilities/sentinel.py +10 -0
- utilities/shelve.py +4 -1
- utilities/shutil.py +25 -0
- utilities/slack_sdk.py +9 -4
- utilities/sqlalchemy.py +422 -352
- utilities/sqlalchemy_polars.py +28 -52
- utilities/string.py +1 -1
- utilities/subprocess.py +1977 -0
- utilities/tempfile.py +112 -4
- utilities/testbook.py +50 -0
- utilities/text.py +174 -42
- utilities/throttle.py +158 -0
- utilities/timer.py +2 -2
- utilities/traceback.py +59 -38
- utilities/types.py +68 -22
- utilities/typing.py +479 -19
- utilities/uuid.py +42 -5
- utilities/version.py +27 -26
- utilities/whenever.py +663 -178
- utilities/zoneinfo.py +80 -22
- dycw_utilities-0.146.2.dist-info/METADATA +0 -41
- dycw_utilities-0.146.2.dist-info/RECORD +0 -99
- dycw_utilities-0.146.2.dist-info/WHEEL +0 -4
- dycw_utilities-0.146.2.dist-info/licenses/LICENSE +0 -21
- utilities/aiolimiter.py +0 -25
- utilities/eventkit.py +0 -388
- utilities/period.py +0 -237
- utilities/python_dotenv.py +0 -101
- utilities/streamlit.py +0 -105
- utilities/typed_settings.py +0 -144
utilities/pytest.py
CHANGED
|
@@ -1,58 +1,76 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from collections.abc import Callable
|
|
3
4
|
from dataclasses import dataclass
|
|
4
|
-
from functools import partial
|
|
5
|
-
from inspect import iscoroutinefunction
|
|
5
|
+
from functools import partial
|
|
6
6
|
from os import environ
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from
|
|
8
|
+
from re import sub
|
|
9
|
+
from types import FunctionType
|
|
10
|
+
from typing import TYPE_CHECKING, Any, NoReturn, override
|
|
9
11
|
|
|
10
|
-
from whenever import ZonedDateTime
|
|
11
|
-
|
|
12
|
-
from utilities.atomicwrites import writer
|
|
13
12
|
from utilities.functools import cache
|
|
14
13
|
from utilities.hashlib import md5_hash
|
|
15
|
-
from utilities.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
IS_NOT_MAC,
|
|
22
|
-
IS_NOT_WINDOWS,
|
|
23
|
-
IS_WINDOWS,
|
|
14
|
+
from utilities.pathlib import (
|
|
15
|
+
_GetTailEmptyError,
|
|
16
|
+
ensure_suffix,
|
|
17
|
+
get_root,
|
|
18
|
+
get_tail,
|
|
19
|
+
module_path,
|
|
24
20
|
)
|
|
21
|
+
from utilities.platform import IS_LINUX, IS_MAC, IS_NOT_LINUX, IS_NOT_MAC
|
|
22
|
+
from utilities.throttle import throttle
|
|
25
23
|
from utilities.types import MaybeCoro
|
|
26
|
-
from utilities.whenever import SECOND
|
|
24
|
+
from utilities.whenever import SECOND
|
|
27
25
|
|
|
28
26
|
if TYPE_CHECKING:
|
|
29
|
-
from collections.abc import
|
|
30
|
-
|
|
31
|
-
from utilities.types import Coro, Delta, PathLike
|
|
27
|
+
from collections.abc import Iterable
|
|
32
28
|
|
|
33
|
-
try: # WARNING: this package cannot use unguarded `pytest` imports
|
|
34
29
|
from _pytest.config import Config
|
|
35
30
|
from _pytest.config.argparsing import Parser
|
|
36
31
|
from _pytest.python import Function
|
|
37
|
-
|
|
32
|
+
|
|
33
|
+
from utilities.types import Delta, PathLike
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
IS_CI = "CI" in environ
|
|
37
|
+
IS_CI_AND_NOT_LINUX = IS_CI and IS_NOT_LINUX
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
try: # WARNING: this package cannot use unguarded `pytest` imports
|
|
41
|
+
from pytest import mark
|
|
38
42
|
except ModuleNotFoundError: # pragma: no cover
|
|
39
|
-
from typing import Any as Config
|
|
40
|
-
from typing import Any as Function
|
|
41
|
-
from typing import Any as Parser
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
def skipif_ci[F: Callable](func: F) -> F:
|
|
45
|
+
return func
|
|
46
|
+
|
|
47
|
+
def skipif_mac[F: Callable](func: F) -> F:
|
|
48
|
+
return func
|
|
49
|
+
|
|
50
|
+
def skipif_linux[F: Callable](func: F) -> F:
|
|
51
|
+
return func
|
|
52
|
+
|
|
53
|
+
def skipif_not_mac[F: Callable](func: F) -> F:
|
|
54
|
+
return func
|
|
55
|
+
|
|
56
|
+
def skipif_not_linux[F: Callable](func: F) -> F:
|
|
57
|
+
return func
|
|
58
|
+
|
|
59
|
+
def skipif_ci_and_not_linux[F: Callable](func: F) -> F:
|
|
60
|
+
return func
|
|
61
|
+
|
|
46
62
|
else:
|
|
47
|
-
|
|
63
|
+
skipif_ci = mark.skipif(IS_CI, reason="Skipped for CI")
|
|
48
64
|
skipif_mac = mark.skipif(IS_MAC, reason="Skipped for Mac")
|
|
49
65
|
skipif_linux = mark.skipif(IS_LINUX, reason="Skipped for Linux")
|
|
50
|
-
skipif_not_windows = mark.skipif(IS_NOT_WINDOWS, reason="Skipped for non-Windows")
|
|
51
66
|
skipif_not_mac = mark.skipif(IS_NOT_MAC, reason="Skipped for non-Mac")
|
|
52
67
|
skipif_not_linux = mark.skipif(IS_NOT_LINUX, reason="Skipped for non-Linux")
|
|
68
|
+
skipif_ci_and_not_linux = mark.skipif(
|
|
69
|
+
IS_CI_AND_NOT_LINUX, reason="Skipped for CI/non-Linux"
|
|
70
|
+
)
|
|
53
71
|
|
|
54
72
|
|
|
55
|
-
def add_pytest_addoption(parser: Parser, options:
|
|
73
|
+
def add_pytest_addoption(parser: Parser, options: list[str], /) -> None:
|
|
56
74
|
"""Add the `--slow`, etc options to pytest.
|
|
57
75
|
|
|
58
76
|
Usage:
|
|
@@ -73,7 +91,7 @@ def add_pytest_addoption(parser: Parser, options: Sequence[str], /) -> None:
|
|
|
73
91
|
|
|
74
92
|
|
|
75
93
|
def add_pytest_collection_modifyitems(
|
|
76
|
-
config: Config, items: Iterable[Function], options:
|
|
94
|
+
config: Config, items: Iterable[Function], options: list[str], /
|
|
77
95
|
) -> None:
|
|
78
96
|
"""Add the @mark.skips as necessary.
|
|
79
97
|
|
|
@@ -82,6 +100,8 @@ def add_pytest_collection_modifyitems(
|
|
|
82
100
|
def pytest_collection_modifyitems(config, items):
|
|
83
101
|
add_pytest_collection_modifyitems(config, items, ["slow"])
|
|
84
102
|
"""
|
|
103
|
+
from pytest import mark
|
|
104
|
+
|
|
85
105
|
options = list(options)
|
|
86
106
|
missing = {opt for opt in options if not config.getoption(f"--{opt}")}
|
|
87
107
|
for item in items:
|
|
@@ -111,9 +131,10 @@ def add_pytest_configure(config: Config, options: Iterable[tuple[str, str]], /)
|
|
|
111
131
|
##
|
|
112
132
|
|
|
113
133
|
|
|
114
|
-
def
|
|
115
|
-
|
|
116
|
-
|
|
134
|
+
def make_ids(obj: Any, /) -> str:
|
|
135
|
+
if isinstance(obj, FunctionType): # pragma: no cover
|
|
136
|
+
return sub(r"\s+at +0x[0-9a-fA-F]+", "", repr(obj))
|
|
137
|
+
return repr(obj) # pragma: no cover
|
|
117
138
|
|
|
118
139
|
|
|
119
140
|
##
|
|
@@ -126,10 +147,15 @@ def node_id_path(
|
|
|
126
147
|
path_file, *parts = node_id.split("::")
|
|
127
148
|
path_file = Path(path_file)
|
|
128
149
|
if path_file.suffix != ".py":
|
|
129
|
-
raise
|
|
150
|
+
raise _NodeIdToPathNotPythonFileError(node_id=node_id)
|
|
130
151
|
path = path_file.with_suffix("")
|
|
131
152
|
if root is not None:
|
|
132
|
-
|
|
153
|
+
try:
|
|
154
|
+
path = get_tail(path, root)
|
|
155
|
+
except _GetTailEmptyError as error:
|
|
156
|
+
raise _NodeIdToPathNotGetTailError(
|
|
157
|
+
node_id=node_id, path=error.path, root=error.root
|
|
158
|
+
) from None
|
|
133
159
|
path = Path(module_path(path), "__".join(parts))
|
|
134
160
|
if suffix is not None:
|
|
135
161
|
path = ensure_suffix(path, suffix)
|
|
@@ -140,96 +166,57 @@ def node_id_path(
|
|
|
140
166
|
class NodeIdToPathError(Exception):
|
|
141
167
|
node_id: str
|
|
142
168
|
|
|
169
|
+
|
|
170
|
+
@dataclass(kw_only=True, slots=True)
|
|
171
|
+
class _NodeIdToPathNotPythonFileError(NodeIdToPathError):
|
|
143
172
|
@override
|
|
144
173
|
def __str__(self) -> str:
|
|
145
174
|
return f"Node ID must be a Python file; got {self.node_id!r}"
|
|
146
175
|
|
|
147
176
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
*, root: PathLike | None = None, delta: Delta = SECOND, on_try: bool = False
|
|
153
|
-
) -> Callable[[F], F]:
|
|
154
|
-
"""Throttle a test. On success by default, on try otherwise."""
|
|
155
|
-
return cast("Any", partial(_throttle_inner, root=root, delta=delta, on_try=on_try))
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
def _throttle_inner[F: Callable[..., MaybeCoro[None]]](
|
|
159
|
-
func: F,
|
|
160
|
-
/,
|
|
161
|
-
*,
|
|
162
|
-
root: PathLike | None = None,
|
|
163
|
-
delta: Delta = SECOND,
|
|
164
|
-
on_try: bool = False,
|
|
165
|
-
) -> F:
|
|
166
|
-
"""Throttle a test function/method."""
|
|
167
|
-
if get_env_var("THROTTLE", nullable=True) is not None:
|
|
168
|
-
return func
|
|
169
|
-
match bool(iscoroutinefunction(func)), on_try:
|
|
170
|
-
case False, False:
|
|
171
|
-
|
|
172
|
-
@wraps(func)
|
|
173
|
-
def throttle_sync_on_pass(*args: Any, **kwargs: Any) -> None:
|
|
174
|
-
_skipif_recent(root=root, delta=delta)
|
|
175
|
-
cast("Callable[..., None]", func)(*args, **kwargs)
|
|
176
|
-
_write(root=root)
|
|
177
|
-
|
|
178
|
-
return cast("Any", throttle_sync_on_pass)
|
|
177
|
+
@dataclass(kw_only=True, slots=True)
|
|
178
|
+
class _NodeIdToPathNotGetTailError(NodeIdToPathError):
|
|
179
|
+
path: PathLike
|
|
180
|
+
root: PathLike
|
|
179
181
|
|
|
180
|
-
|
|
182
|
+
@override
|
|
183
|
+
def __str__(self) -> str:
|
|
184
|
+
return (
|
|
185
|
+
f"Unable to get the tail of {str(self.path)!r} with root {str(self.root)!r}"
|
|
186
|
+
)
|
|
181
187
|
|
|
182
|
-
@wraps(func)
|
|
183
|
-
def throttle_sync_on_try(*args: Any, **kwargs: Any) -> None:
|
|
184
|
-
_skipif_recent(root=root, delta=delta)
|
|
185
|
-
_write(root=root)
|
|
186
|
-
cast("Callable[..., None]", func)(*args, **kwargs)
|
|
187
188
|
|
|
188
|
-
|
|
189
|
+
##
|
|
189
190
|
|
|
190
|
-
case True, False:
|
|
191
191
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
192
|
+
def throttle_test[F: Callable[..., MaybeCoro[None]]](
|
|
193
|
+
*, on_try: bool = False, root: PathLike | None = None, delta: Delta = SECOND
|
|
194
|
+
) -> Callable[[F], F]:
|
|
195
|
+
"""Throttle a test. On success by default, on try otherwise."""
|
|
196
|
+
return throttle(
|
|
197
|
+
on_try=on_try,
|
|
198
|
+
delta=delta,
|
|
199
|
+
path=partial(_get_test_path, root=root),
|
|
200
|
+
raiser=_run_skip,
|
|
201
|
+
)
|
|
197
202
|
|
|
198
|
-
return cast("Any", throttle_async_on_pass)
|
|
199
203
|
|
|
200
|
-
|
|
204
|
+
def _run_skip() -> NoReturn:
|
|
205
|
+
from pytest import skip
|
|
201
206
|
|
|
202
|
-
|
|
203
|
-
async def throttle_async_on_try(*args: Any, **kwargs: Any) -> None:
|
|
204
|
-
_skipif_recent(root=root, delta=delta)
|
|
205
|
-
_write(root=root)
|
|
206
|
-
await cast("Callable[..., Coro[None]]", func)(*args, **kwargs)
|
|
207
|
+
skip(reason=f"{_get_name()} throttled")
|
|
207
208
|
|
|
208
|
-
return cast("Any", throttle_async_on_try)
|
|
209
209
|
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
def _get_name() -> str:
|
|
211
|
+
return environ["PYTEST_CURRENT_TEST"]
|
|
212
212
|
|
|
213
213
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
path = _get_path(root=root)
|
|
218
|
-
try:
|
|
219
|
-
contents = path.read_text()
|
|
220
|
-
except FileNotFoundError:
|
|
221
|
-
return
|
|
222
|
-
try:
|
|
223
|
-
last = ZonedDateTime.parse_common_iso(contents)
|
|
224
|
-
except ValueError:
|
|
225
|
-
return
|
|
226
|
-
now = get_now_local()
|
|
227
|
-
if (now - delta) < last:
|
|
228
|
-
age = now - last
|
|
229
|
-
_ = skip(reason=f"{_get_name()} throttled (age {age})")
|
|
214
|
+
@cache
|
|
215
|
+
def _md5_hash_cached(text: str, /) -> str:
|
|
216
|
+
return md5_hash(text)
|
|
230
217
|
|
|
231
218
|
|
|
232
|
-
def
|
|
219
|
+
def _get_test_path(*, root: PathLike | None = None) -> Path:
|
|
233
220
|
if root is None:
|
|
234
221
|
root_use = get_root().joinpath(".pytest_cache", "throttle") # pragma: no cover
|
|
235
222
|
else:
|
|
@@ -237,33 +224,20 @@ def _get_path(*, root: PathLike | None = None) -> Path:
|
|
|
237
224
|
return Path(root_use, _md5_hash_cached(_get_name()))
|
|
238
225
|
|
|
239
226
|
|
|
240
|
-
@cache
|
|
241
|
-
def _md5_hash_cached(text: str, /) -> str:
|
|
242
|
-
return md5_hash(text)
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
def _get_name() -> str:
|
|
246
|
-
return environ["PYTEST_CURRENT_TEST"]
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
def _write(*, root: PathLike | None = None) -> None:
|
|
250
|
-
path = _get_path(root=root)
|
|
251
|
-
with writer(path, overwrite=True) as temp:
|
|
252
|
-
_ = temp.write_text(get_now_local().format_common_iso())
|
|
253
|
-
|
|
254
|
-
|
|
255
227
|
__all__ = [
|
|
228
|
+
"IS_CI",
|
|
229
|
+
"IS_CI_AND_NOT_LINUX",
|
|
256
230
|
"NodeIdToPathError",
|
|
257
231
|
"add_pytest_addoption",
|
|
258
232
|
"add_pytest_collection_modifyitems",
|
|
259
233
|
"add_pytest_configure",
|
|
260
|
-
"
|
|
234
|
+
"make_ids",
|
|
261
235
|
"node_id_path",
|
|
236
|
+
"skipif_ci",
|
|
237
|
+
"skipif_ci_and_not_linux",
|
|
262
238
|
"skipif_linux",
|
|
263
239
|
"skipif_mac",
|
|
264
240
|
"skipif_not_linux",
|
|
265
241
|
"skipif_not_mac",
|
|
266
|
-
"
|
|
267
|
-
"skipif_windows",
|
|
268
|
-
"throttle",
|
|
242
|
+
"throttle_test",
|
|
269
243
|
]
|
|
@@ -41,12 +41,16 @@ else:
|
|
|
41
41
|
|
|
42
42
|
def _get_path(request: FixtureRequest, /) -> Path:
|
|
43
43
|
from utilities.pathlib import get_root
|
|
44
|
-
from utilities.pytest import node_id_path
|
|
44
|
+
from utilities.pytest import _NodeIdToPathNotGetTailError, node_id_path
|
|
45
45
|
|
|
46
46
|
path = Path(cast("Any", request).fspath)
|
|
47
47
|
root = Path("src", "tests")
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
try:
|
|
49
|
+
tail = node_id_path(request.node.nodeid, root=root)
|
|
50
|
+
except _NodeIdToPathNotGetTailError:
|
|
51
|
+
root = Path("tests")
|
|
52
|
+
tail = node_id_path(request.node.nodeid, root=root)
|
|
53
|
+
return get_root(path).joinpath(root, "regressions", tail)
|
|
50
54
|
|
|
51
55
|
|
|
52
56
|
__all__ = ["orjson_regression", "polars_regression"]
|
utilities/pytest_regressions.py
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from contextlib import suppress
|
|
4
|
+
from dataclasses import dataclass
|
|
4
5
|
from json import loads
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from shutil import copytree
|
|
7
|
-
from typing import TYPE_CHECKING, Any, assert_never
|
|
8
|
+
from typing import TYPE_CHECKING, Any, assert_never, override
|
|
8
9
|
|
|
9
10
|
from pytest_regressions.file_regression import FileRegressionFixture
|
|
10
11
|
|
|
11
12
|
from utilities.functions import ensure_str
|
|
12
13
|
from utilities.operator import is_equal
|
|
14
|
+
from utilities.reprlib import get_repr
|
|
13
15
|
|
|
14
16
|
if TYPE_CHECKING:
|
|
15
17
|
from polars import DataFrame, Series
|
|
@@ -70,10 +72,28 @@ class OrjsonRegressionFixture:
|
|
|
70
72
|
check_fn=self._check_fn,
|
|
71
73
|
)
|
|
72
74
|
|
|
73
|
-
def _check_fn(self,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
def _check_fn(self, path_obtained: Path, path_existing: Path, /) -> None:
|
|
76
|
+
obtained = loads(path_obtained.read_text())
|
|
77
|
+
existing = loads(path_existing.read_text())
|
|
78
|
+
if not is_equal(obtained, existing):
|
|
79
|
+
raise OrjsonRegressionError(
|
|
80
|
+
path_obtained=path_obtained,
|
|
81
|
+
path_existing=path_existing,
|
|
82
|
+
obtained=obtained,
|
|
83
|
+
existing=existing,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(kw_only=True, slots=True)
|
|
88
|
+
class OrjsonRegressionError(Exception):
|
|
89
|
+
path_obtained: Path
|
|
90
|
+
path_existing: Path
|
|
91
|
+
obtained: Any
|
|
92
|
+
existing: Any
|
|
93
|
+
|
|
94
|
+
@override
|
|
95
|
+
def __str__(self) -> str:
|
|
96
|
+
return f"Obtained object (at {str(self.path_obtained)!r}) and existing object (at {str(self.path_existing)!r}) differ; got {get_repr(self.obtained)} and {get_repr(self.existing)}"
|
|
77
97
|
|
|
78
98
|
|
|
79
99
|
##
|
|
@@ -97,7 +117,6 @@ class PolarsRegressionFixture:
|
|
|
97
117
|
"describe": obj.describe(percentiles=[i / 10 for i in range(1, 10)]).rows(
|
|
98
118
|
named=True
|
|
99
119
|
),
|
|
100
|
-
"estimated_size": obj.estimated_size(),
|
|
101
120
|
"is_empty": obj.is_empty(),
|
|
102
121
|
"n_unique": obj.n_unique(),
|
|
103
122
|
}
|
|
@@ -115,9 +134,9 @@ class PolarsRegressionFixture:
|
|
|
115
134
|
col(column).approx_n_unique()
|
|
116
135
|
).item()
|
|
117
136
|
data["approx_n_unique"] = approx_n_unique
|
|
118
|
-
data["glimpse"] = df.glimpse(
|
|
137
|
+
data["glimpse"] = df.glimpse(return_type="string")
|
|
119
138
|
data["null_count"] = df.null_count().row(0, named=True)
|
|
120
|
-
case
|
|
139
|
+
case never:
|
|
121
140
|
assert_never(never)
|
|
122
141
|
self._fixture.check(data, suffix=suffix)
|
|
123
142
|
|
utilities/random.py
CHANGED
|
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
from random import Random, SystemRandom
|
|
4
4
|
from typing import TYPE_CHECKING
|
|
5
5
|
|
|
6
|
+
from utilities.functools import cache
|
|
7
|
+
|
|
6
8
|
if TYPE_CHECKING:
|
|
7
9
|
from collections.abc import Iterable
|
|
8
10
|
|
|
@@ -17,16 +19,16 @@ SYSTEM_RANDOM = SystemRandom()
|
|
|
17
19
|
|
|
18
20
|
def bernoulli(*, true: float = 0.5, seed: Seed | None = None) -> bool:
|
|
19
21
|
"""Return a Bernoulli random variate."""
|
|
20
|
-
|
|
21
|
-
return bool(
|
|
22
|
+
state = get_state(seed)
|
|
23
|
+
return bool(state.binomialvariate(p=true))
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
##
|
|
25
27
|
|
|
26
28
|
|
|
27
|
-
def get_docker_name(
|
|
29
|
+
def get_docker_name(seed: Seed | None = None, /) -> str:
|
|
28
30
|
"""Get a docker name."""
|
|
29
|
-
state = get_state(seed
|
|
31
|
+
state = get_state(seed)
|
|
30
32
|
prefix = state.choice(_DOCKER_PREFIXES)
|
|
31
33
|
suffix = state.choice(_DOCKER_SUFFIXES)
|
|
32
34
|
digit = state.randint(0, 9)
|
|
@@ -47,16 +49,19 @@ _DOCKER_SUFFIXES = [
|
|
|
47
49
|
##
|
|
48
50
|
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
@cache
|
|
53
|
+
def get_state(seed: Seed | None = None, /) -> Random:
|
|
51
54
|
"""Get a random state."""
|
|
52
55
|
return seed if isinstance(seed, Random) else Random(x=seed)
|
|
53
56
|
|
|
54
57
|
|
|
55
58
|
##
|
|
59
|
+
|
|
60
|
+
|
|
56
61
|
def shuffle[T](iterable: Iterable[T], /, *, seed: Seed | None = None) -> list[T]:
|
|
57
62
|
"""Shuffle an iterable."""
|
|
58
63
|
copy = list(iterable).copy()
|
|
59
|
-
state = get_state(seed
|
|
64
|
+
state = get_state(seed)
|
|
60
65
|
state.shuffle(copy)
|
|
61
66
|
return copy
|
|
62
67
|
|