dycw-utilities 0.129.10__py3-none-any.whl → 0.175.17__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.
- dycw_utilities-0.175.17.dist-info/METADATA +34 -0
- dycw_utilities-0.175.17.dist-info/RECORD +103 -0
- dycw_utilities-0.175.17.dist-info/WHEEL +4 -0
- dycw_utilities-0.175.17.dist-info/entry_points.txt +4 -0
- utilities/__init__.py +1 -1
- utilities/altair.py +14 -14
- utilities/asyncio.py +350 -819
- utilities/atomicwrites.py +18 -6
- utilities/atools.py +77 -22
- utilities/cachetools.py +24 -29
- utilities/click.py +393 -237
- utilities/concurrent.py +8 -11
- utilities/contextlib.py +216 -17
- utilities/contextvars.py +20 -1
- utilities/cryptography.py +3 -3
- utilities/dataclasses.py +83 -118
- utilities/docker.py +293 -0
- utilities/enum.py +26 -23
- utilities/errors.py +17 -3
- utilities/fastapi.py +29 -65
- utilities/fpdf2.py +3 -3
- utilities/functions.py +169 -416
- utilities/functools.py +18 -19
- utilities/git.py +9 -30
- utilities/grp.py +28 -0
- utilities/gzip.py +31 -0
- utilities/http.py +3 -2
- utilities/hypothesis.py +738 -589
- utilities/importlib.py +17 -1
- utilities/inflect.py +25 -0
- utilities/iterables.py +194 -262
- utilities/jinja2.py +148 -0
- utilities/json.py +70 -0
- utilities/libcst.py +38 -17
- utilities/lightweight_charts.py +5 -9
- utilities/logging.py +345 -543
- utilities/math.py +18 -13
- utilities/memory_profiler.py +11 -15
- utilities/more_itertools.py +200 -131
- utilities/operator.py +33 -29
- utilities/optuna.py +6 -6
- utilities/orjson.py +272 -137
- utilities/os.py +61 -4
- utilities/parse.py +59 -61
- utilities/pathlib.py +281 -40
- utilities/permissions.py +298 -0
- utilities/pickle.py +2 -2
- utilities/platform.py +24 -5
- utilities/polars.py +1214 -430
- utilities/polars_ols.py +1 -1
- utilities/postgres.py +408 -0
- utilities/pottery.py +113 -26
- utilities/pqdm.py +10 -11
- utilities/psutil.py +6 -57
- utilities/pwd.py +28 -0
- utilities/pydantic.py +4 -54
- utilities/pydantic_settings.py +240 -0
- utilities/pydantic_settings_sops.py +76 -0
- utilities/pyinstrument.py +8 -10
- utilities/pytest.py +227 -121
- 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 +13 -9
- utilities/re.py +58 -28
- utilities/redis.py +401 -550
- utilities/scipy.py +1 -1
- utilities/sentinel.py +10 -0
- utilities/shelve.py +4 -1
- utilities/shutil.py +25 -0
- utilities/slack_sdk.py +36 -106
- utilities/sqlalchemy.py +502 -473
- utilities/sqlalchemy_polars.py +38 -94
- utilities/string.py +2 -3
- utilities/subprocess.py +1572 -0
- utilities/tempfile.py +86 -4
- utilities/testbook.py +50 -0
- utilities/text.py +165 -42
- utilities/timer.py +37 -65
- utilities/traceback.py +158 -929
- utilities/types.py +146 -116
- utilities/typing.py +531 -71
- utilities/tzdata.py +1 -53
- utilities/tzlocal.py +6 -23
- utilities/uuid.py +43 -5
- utilities/version.py +27 -26
- utilities/whenever.py +1776 -386
- utilities/zoneinfo.py +84 -22
- dycw_utilities-0.129.10.dist-info/METADATA +0 -241
- dycw_utilities-0.129.10.dist-info/RECORD +0 -96
- dycw_utilities-0.129.10.dist-info/WHEEL +0 -4
- dycw_utilities-0.129.10.dist-info/licenses/LICENSE +0 -21
- utilities/datetime.py +0 -1409
- utilities/eventkit.py +0 -402
- utilities/loguru.py +0 -144
- utilities/luigi.py +0 -228
- utilities/period.py +0 -324
- utilities/pyrsistent.py +0 -89
- utilities/python_dotenv.py +0 -105
- utilities/streamlit.py +0 -105
- utilities/sys.py +0 -87
- utilities/tenacity.py +0 -145
utilities/pytest.py
CHANGED
|
@@ -1,68 +1,82 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from collections.abc import Callable
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from functools import partial, wraps
|
|
5
6
|
from inspect import iscoroutinefunction
|
|
6
7
|
from os import environ
|
|
7
8
|
from pathlib import Path
|
|
8
|
-
from
|
|
9
|
+
from re import sub
|
|
10
|
+
from types import FunctionType
|
|
11
|
+
from typing import TYPE_CHECKING, Any, assert_never, cast, override
|
|
9
12
|
|
|
10
|
-
from
|
|
13
|
+
from whenever import ZonedDateTime
|
|
11
14
|
|
|
12
|
-
from utilities.
|
|
15
|
+
from utilities.atomicwrites import writer
|
|
13
16
|
from utilities.functools import cache
|
|
14
17
|
from utilities.hashlib import md5_hash
|
|
15
|
-
from utilities.
|
|
16
|
-
from utilities.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
IS_WINDOWS,
|
|
18
|
+
from utilities.os import get_env_var
|
|
19
|
+
from utilities.pathlib import (
|
|
20
|
+
_GetTailEmptyError,
|
|
21
|
+
ensure_suffix,
|
|
22
|
+
get_root,
|
|
23
|
+
get_tail,
|
|
24
|
+
module_path,
|
|
23
25
|
)
|
|
24
|
-
from utilities.
|
|
26
|
+
from utilities.platform import IS_LINUX, IS_MAC, IS_NOT_LINUX, IS_NOT_MAC
|
|
27
|
+
from utilities.random import bernoulli
|
|
28
|
+
from utilities.text import to_bool
|
|
29
|
+
from utilities.types import MaybeCallableBoolLike, MaybeCoro, Seed
|
|
30
|
+
from utilities.whenever import SECOND, get_now_local
|
|
25
31
|
|
|
26
32
|
if TYPE_CHECKING:
|
|
27
|
-
from collections.abc import
|
|
28
|
-
from random import Random
|
|
29
|
-
|
|
30
|
-
from utilities.types import (
|
|
31
|
-
Coroutine1,
|
|
32
|
-
Duration,
|
|
33
|
-
PathLike,
|
|
34
|
-
TCallableMaybeCoroutine1None,
|
|
35
|
-
)
|
|
33
|
+
from collections.abc import Iterable
|
|
36
34
|
|
|
37
|
-
try: # WARNING: this package cannot use unguarded `pytest` imports
|
|
38
35
|
from _pytest.config import Config
|
|
39
36
|
from _pytest.config.argparsing import Parser
|
|
40
37
|
from _pytest.python import Function
|
|
41
|
-
|
|
38
|
+
|
|
39
|
+
from utilities.types import Coro, Delta, PathLike
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
IS_CI = "CI" in environ
|
|
43
|
+
IS_CI_AND_NOT_LINUX = IS_CI and IS_NOT_LINUX
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
try: # WARNING: this package cannot use unguarded `pytest` imports
|
|
47
|
+
from pytest import mark
|
|
42
48
|
except ModuleNotFoundError: # pragma: no cover
|
|
43
|
-
from typing import Any as Config
|
|
44
|
-
from typing import Any as Function
|
|
45
|
-
from typing import Any as Parser
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
def skipif_ci[F: Callable](func: F) -> F:
|
|
51
|
+
return func
|
|
52
|
+
|
|
53
|
+
def skipif_mac[F: Callable](func: F) -> F:
|
|
54
|
+
return func
|
|
55
|
+
|
|
56
|
+
def skipif_linux[F: Callable](func: F) -> F:
|
|
57
|
+
return func
|
|
58
|
+
|
|
59
|
+
def skipif_not_mac[F: Callable](func: F) -> F:
|
|
60
|
+
return func
|
|
61
|
+
|
|
62
|
+
def skipif_not_linux[F: Callable](func: F) -> F:
|
|
63
|
+
return func
|
|
64
|
+
|
|
65
|
+
def skipif_ci_and_not_linux[F: Callable](func: F) -> F:
|
|
66
|
+
return func
|
|
67
|
+
|
|
50
68
|
else:
|
|
51
|
-
|
|
69
|
+
skipif_ci = mark.skipif(IS_CI, reason="Skipped for CI")
|
|
52
70
|
skipif_mac = mark.skipif(IS_MAC, reason="Skipped for Mac")
|
|
53
71
|
skipif_linux = mark.skipif(IS_LINUX, reason="Skipped for Linux")
|
|
54
|
-
skipif_not_windows = mark.skipif(IS_NOT_WINDOWS, reason="Skipped for non-Windows")
|
|
55
72
|
skipif_not_mac = mark.skipif(IS_NOT_MAC, reason="Skipped for non-Mac")
|
|
56
73
|
skipif_not_linux = mark.skipif(IS_NOT_LINUX, reason="Skipped for non-Linux")
|
|
74
|
+
skipif_ci_and_not_linux = mark.skipif(
|
|
75
|
+
IS_CI_AND_NOT_LINUX, reason="Skipped for CI/non-Linux"
|
|
76
|
+
)
|
|
57
77
|
|
|
58
78
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
##
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def add_pytest_addoption(parser: Parser, options: Sequence[str], /) -> None:
|
|
79
|
+
def add_pytest_addoption(parser: Parser, options: list[str], /) -> None:
|
|
66
80
|
"""Add the `--slow`, etc options to pytest.
|
|
67
81
|
|
|
68
82
|
Usage:
|
|
@@ -83,7 +97,7 @@ def add_pytest_addoption(parser: Parser, options: Sequence[str], /) -> None:
|
|
|
83
97
|
|
|
84
98
|
|
|
85
99
|
def add_pytest_collection_modifyitems(
|
|
86
|
-
config: Config, items: Iterable[Function], options:
|
|
100
|
+
config: Config, items: Iterable[Function], options: list[str], /
|
|
87
101
|
) -> None:
|
|
88
102
|
"""Add the @mark.skips as necessary.
|
|
89
103
|
|
|
@@ -92,6 +106,8 @@ def add_pytest_collection_modifyitems(
|
|
|
92
106
|
def pytest_collection_modifyitems(config, items):
|
|
93
107
|
add_pytest_collection_modifyitems(config, items, ["slow"])
|
|
94
108
|
"""
|
|
109
|
+
from pytest import mark
|
|
110
|
+
|
|
95
111
|
options = list(options)
|
|
96
112
|
missing = {opt for opt in options if not config.getoption(f"--{opt}")}
|
|
97
113
|
for item in items:
|
|
@@ -121,26 +137,32 @@ def add_pytest_configure(config: Config, options: Iterable[tuple[str, str]], /)
|
|
|
121
137
|
##
|
|
122
138
|
|
|
123
139
|
|
|
124
|
-
def
|
|
125
|
-
|
|
126
|
-
|
|
140
|
+
def make_ids(obj: Any, /) -> str:
|
|
141
|
+
if isinstance(obj, FunctionType):
|
|
142
|
+
return sub(r"\s+at +0x[0-9a-fA-F]+", "", repr(obj))
|
|
143
|
+
return repr(obj)
|
|
127
144
|
|
|
128
145
|
|
|
129
146
|
##
|
|
130
147
|
|
|
131
148
|
|
|
132
|
-
def
|
|
133
|
-
node_id: str, /, *,
|
|
149
|
+
def node_id_path(
|
|
150
|
+
node_id: str, /, *, root: PathLike | None = None, suffix: str | None = None
|
|
134
151
|
) -> Path:
|
|
135
|
-
"""
|
|
152
|
+
"""Get the path of a node ID."""
|
|
136
153
|
path_file, *parts = node_id.split("::")
|
|
137
154
|
path_file = Path(path_file)
|
|
138
155
|
if path_file.suffix != ".py":
|
|
139
|
-
raise
|
|
156
|
+
raise _NodeIdToPathNotPythonFileError(node_id=node_id)
|
|
140
157
|
path = path_file.with_suffix("")
|
|
141
|
-
if
|
|
142
|
-
|
|
143
|
-
|
|
158
|
+
if root is not None:
|
|
159
|
+
try:
|
|
160
|
+
path = get_tail(path, root)
|
|
161
|
+
except _GetTailEmptyError as error:
|
|
162
|
+
raise _NodeIdToPathNotGetTailError(
|
|
163
|
+
node_id=node_id, path=error.path, root=error.root
|
|
164
|
+
) from None
|
|
165
|
+
path = Path(module_path(path), "__".join(parts))
|
|
144
166
|
if suffix is not None:
|
|
145
167
|
path = ensure_suffix(path, suffix)
|
|
146
168
|
return path
|
|
@@ -150,123 +172,207 @@ def node_id_to_path(
|
|
|
150
172
|
class NodeIdToPathError(Exception):
|
|
151
173
|
node_id: str
|
|
152
174
|
|
|
175
|
+
|
|
176
|
+
@dataclass(kw_only=True, slots=True)
|
|
177
|
+
class _NodeIdToPathNotPythonFileError(NodeIdToPathError):
|
|
153
178
|
@override
|
|
154
179
|
def __str__(self) -> str:
|
|
155
180
|
return f"Node ID must be a Python file; got {self.node_id!r}"
|
|
156
181
|
|
|
157
182
|
|
|
158
|
-
|
|
159
|
-
|
|
183
|
+
@dataclass(kw_only=True, slots=True)
|
|
184
|
+
class _NodeIdToPathNotGetTailError(NodeIdToPathError):
|
|
185
|
+
path: PathLike
|
|
186
|
+
root: PathLike
|
|
160
187
|
|
|
161
|
-
@
|
|
162
|
-
def
|
|
163
|
-
|
|
164
|
-
|
|
188
|
+
@override
|
|
189
|
+
def __str__(self) -> str:
|
|
190
|
+
return (
|
|
191
|
+
f"Unable to get the tail of {str(self.path)!r} with root {str(self.root)!r}"
|
|
192
|
+
)
|
|
165
193
|
|
|
166
194
|
|
|
167
195
|
##
|
|
168
196
|
|
|
169
197
|
|
|
170
|
-
def
|
|
171
|
-
*,
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
198
|
+
def run_frac[F: Callable[..., MaybeCoro[None]]](
|
|
199
|
+
*,
|
|
200
|
+
predicate: MaybeCallableBoolLike | None = None,
|
|
201
|
+
frac: float = 0.5,
|
|
202
|
+
seed: Seed | None = None,
|
|
203
|
+
) -> Callable[[F], F]:
|
|
204
|
+
"""Run a test only a fraction of the time.."""
|
|
175
205
|
return cast(
|
|
176
|
-
"Any", partial(
|
|
206
|
+
"Any", partial(_run_frac_inner, predicate=predicate, frac=frac, seed=seed)
|
|
177
207
|
)
|
|
178
208
|
|
|
179
209
|
|
|
180
|
-
def
|
|
181
|
-
func:
|
|
210
|
+
def _run_frac_inner[F: Callable[..., MaybeCoro[None]]](
|
|
211
|
+
func: F,
|
|
182
212
|
/,
|
|
183
213
|
*,
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
) ->
|
|
188
|
-
"""Throttle a test function/method."""
|
|
214
|
+
predicate: MaybeCallableBoolLike | None = None,
|
|
215
|
+
frac: float = 0.5,
|
|
216
|
+
seed: Seed | None = None,
|
|
217
|
+
) -> F:
|
|
189
218
|
match bool(iscoroutinefunction(func)):
|
|
190
219
|
case False:
|
|
191
|
-
func_typed = cast("Callable[..., None]", func)
|
|
192
220
|
|
|
193
221
|
@wraps(func)
|
|
194
|
-
def
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
func_typed(*args, **kwargs)
|
|
201
|
-
_throttle_write(path, now)
|
|
202
|
-
return None
|
|
203
|
-
|
|
204
|
-
return cast("TCallableMaybeCoroutine1None", throttle_sync)
|
|
222
|
+
def run_frac_sync(*args: Any, **kwargs: Any) -> None:
|
|
223
|
+
_skipif_frac(predicate=predicate, frac=frac, seed=seed)
|
|
224
|
+
cast("Callable[..., None]", func)(*args, **kwargs)
|
|
225
|
+
|
|
226
|
+
return cast("Any", run_frac_sync)
|
|
227
|
+
|
|
205
228
|
case True:
|
|
206
|
-
func_typed = cast("Callable[..., Coroutine1[None]]", func)
|
|
207
229
|
|
|
208
230
|
@wraps(func)
|
|
209
|
-
async def
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
_throttle_write(path, now)
|
|
217
|
-
return None
|
|
218
|
-
|
|
219
|
-
return cast("TCallableMaybeCoroutine1None", throttle_async)
|
|
220
|
-
case _ as never:
|
|
231
|
+
async def run_frac_async(*args: Any, **kwargs: Any) -> None:
|
|
232
|
+
_skipif_frac(predicate=predicate, frac=frac, seed=seed)
|
|
233
|
+
await cast("Callable[..., Coro[None]]", func)(*args, **kwargs)
|
|
234
|
+
|
|
235
|
+
return cast("Any", run_frac_async)
|
|
236
|
+
|
|
237
|
+
case never:
|
|
221
238
|
assert_never(never)
|
|
222
239
|
|
|
223
240
|
|
|
224
|
-
def
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
prev = None
|
|
235
|
-
now = get_now().timestamp()
|
|
236
|
-
if (
|
|
237
|
-
(skip is not None)
|
|
238
|
-
and (prev is not None)
|
|
239
|
-
and ((now - prev) < datetime_duration_to_float(duration))
|
|
241
|
+
def _skipif_frac(
|
|
242
|
+
*,
|
|
243
|
+
predicate: MaybeCallableBoolLike | None = None,
|
|
244
|
+
frac: float = 0.5,
|
|
245
|
+
seed: Seed | None = None,
|
|
246
|
+
) -> None:
|
|
247
|
+
from pytest import skip
|
|
248
|
+
|
|
249
|
+
if ((predicate is None) or to_bool(predicate)) and bernoulli(
|
|
250
|
+
true=1 - frac, seed=seed
|
|
240
251
|
):
|
|
241
|
-
|
|
242
|
-
|
|
252
|
+
skip(reason=f"{_get_name()} skipped (run {frac:.0%})")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
##
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def throttle[F: Callable[..., MaybeCoro[None]]](
|
|
259
|
+
*, root: PathLike | None = None, delta: Delta = SECOND, on_try: bool = False
|
|
260
|
+
) -> Callable[[F], F]:
|
|
261
|
+
"""Throttle a test. On success by default, on try otherwise."""
|
|
262
|
+
return cast("Any", partial(_throttle_inner, root=root, delta=delta, on_try=on_try))
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _throttle_inner[F: Callable[..., MaybeCoro[None]]](
|
|
266
|
+
func: F,
|
|
267
|
+
/,
|
|
268
|
+
*,
|
|
269
|
+
root: PathLike | None = None,
|
|
270
|
+
delta: Delta = SECOND,
|
|
271
|
+
on_try: bool = False,
|
|
272
|
+
) -> F:
|
|
273
|
+
if get_env_var("THROTTLE", nullable=True) is not None:
|
|
274
|
+
return func
|
|
275
|
+
match bool(iscoroutinefunction(func)), on_try:
|
|
276
|
+
case False, False:
|
|
277
|
+
|
|
278
|
+
@wraps(func)
|
|
279
|
+
def throttle_sync_on_pass(*args: Any, **kwargs: Any) -> None:
|
|
280
|
+
_skipif_recent(root=root, delta=delta)
|
|
281
|
+
cast("Callable[..., None]", func)(*args, **kwargs)
|
|
282
|
+
_write(root)
|
|
283
|
+
|
|
284
|
+
return cast("Any", throttle_sync_on_pass)
|
|
285
|
+
|
|
286
|
+
case False, True:
|
|
287
|
+
|
|
288
|
+
@wraps(func)
|
|
289
|
+
def throttle_sync_on_try(*args: Any, **kwargs: Any) -> None:
|
|
290
|
+
_skipif_recent(root=root, delta=delta)
|
|
291
|
+
_write(root)
|
|
292
|
+
cast("Callable[..., None]", func)(*args, **kwargs)
|
|
293
|
+
|
|
294
|
+
return cast("Any", throttle_sync_on_try)
|
|
295
|
+
|
|
296
|
+
case True, False:
|
|
297
|
+
|
|
298
|
+
@wraps(func)
|
|
299
|
+
async def throttle_async_on_pass(*args: Any, **kwargs: Any) -> None:
|
|
300
|
+
_skipif_recent(root=root, delta=delta)
|
|
301
|
+
await cast("Callable[..., Coro[None]]", func)(*args, **kwargs)
|
|
302
|
+
_write(root)
|
|
303
|
+
|
|
304
|
+
return cast("Any", throttle_async_on_pass)
|
|
305
|
+
|
|
306
|
+
case True, True:
|
|
307
|
+
|
|
308
|
+
@wraps(func)
|
|
309
|
+
async def throttle_async_on_try(*args: Any, **kwargs: Any) -> None:
|
|
310
|
+
_skipif_recent(root=root, delta=delta)
|
|
311
|
+
_write(root)
|
|
312
|
+
await cast("Callable[..., Coro[None]]", func)(*args, **kwargs)
|
|
313
|
+
|
|
314
|
+
return cast("Any", throttle_async_on_try)
|
|
315
|
+
|
|
316
|
+
case never:
|
|
317
|
+
assert_never(never)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _skipif_recent(*, root: PathLike | None = None, delta: Delta = SECOND) -> None:
|
|
321
|
+
from pytest import skip
|
|
322
|
+
|
|
323
|
+
path = _get_path(root)
|
|
324
|
+
try:
|
|
325
|
+
contents = path.read_text()
|
|
326
|
+
except FileNotFoundError:
|
|
327
|
+
return
|
|
328
|
+
try:
|
|
329
|
+
last = ZonedDateTime.parse_iso(contents)
|
|
330
|
+
except ValueError:
|
|
331
|
+
return
|
|
332
|
+
now = get_now_local()
|
|
333
|
+
if (now - delta) < last:
|
|
334
|
+
age = now - last
|
|
335
|
+
_ = skip(reason=f"{_get_name()} throttled (age {age})")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _get_path(root: PathLike | None = None, /) -> Path:
|
|
339
|
+
if root is None:
|
|
340
|
+
root_use = get_root().joinpath(".pytest_cache", "throttle") # pragma: no cover
|
|
341
|
+
else:
|
|
342
|
+
root_use = root
|
|
343
|
+
return Path(root_use, _md5_hash_cached(_get_name()))
|
|
243
344
|
|
|
244
345
|
|
|
245
346
|
@cache
|
|
246
|
-
def
|
|
347
|
+
def _md5_hash_cached(text: str, /) -> str:
|
|
247
348
|
return md5_hash(text)
|
|
248
349
|
|
|
249
350
|
|
|
250
|
-
def
|
|
251
|
-
|
|
351
|
+
def _get_name() -> str:
|
|
352
|
+
return environ["PYTEST_CURRENT_TEST"]
|
|
353
|
+
|
|
252
354
|
|
|
253
|
-
|
|
254
|
-
|
|
355
|
+
def _write(root: PathLike | None = None, /) -> None:
|
|
356
|
+
path = _get_path(root)
|
|
357
|
+
with writer(path, overwrite=True) as temp:
|
|
358
|
+
_ = temp.write_text(get_now_local().format_iso())
|
|
255
359
|
|
|
256
360
|
|
|
257
361
|
__all__ = [
|
|
362
|
+
"IS_CI",
|
|
363
|
+
"IS_CI_AND_NOT_LINUX",
|
|
258
364
|
"NodeIdToPathError",
|
|
259
365
|
"add_pytest_addoption",
|
|
260
366
|
"add_pytest_collection_modifyitems",
|
|
261
367
|
"add_pytest_configure",
|
|
262
|
-
"
|
|
263
|
-
"
|
|
264
|
-
"
|
|
368
|
+
"make_ids",
|
|
369
|
+
"node_id_path",
|
|
370
|
+
"run_frac",
|
|
371
|
+
"skipif_ci",
|
|
372
|
+
"skipif_ci_and_not_linux",
|
|
265
373
|
"skipif_linux",
|
|
266
374
|
"skipif_mac",
|
|
267
375
|
"skipif_not_linux",
|
|
268
376
|
"skipif_not_mac",
|
|
269
|
-
"skipif_not_windows",
|
|
270
|
-
"skipif_windows",
|
|
271
377
|
"throttle",
|
|
272
378
|
]
|
|
@@ -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"]
|