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