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.

Files changed (97) hide show
  1. dycw_utilities-0.178.1.dist-info/METADATA +34 -0
  2. dycw_utilities-0.178.1.dist-info/RECORD +105 -0
  3. dycw_utilities-0.178.1.dist-info/WHEEL +4 -0
  4. dycw_utilities-0.178.1.dist-info/entry_points.txt +4 -0
  5. utilities/__init__.py +1 -1
  6. utilities/altair.py +13 -10
  7. utilities/asyncio.py +312 -787
  8. utilities/atomicwrites.py +18 -6
  9. utilities/atools.py +64 -4
  10. utilities/cachetools.py +9 -6
  11. utilities/click.py +195 -77
  12. utilities/concurrent.py +1 -1
  13. utilities/contextlib.py +216 -17
  14. utilities/contextvars.py +20 -1
  15. utilities/cryptography.py +3 -3
  16. utilities/dataclasses.py +15 -28
  17. utilities/docker.py +387 -0
  18. utilities/enum.py +2 -2
  19. utilities/errors.py +17 -3
  20. utilities/fastapi.py +28 -59
  21. utilities/fpdf2.py +2 -2
  22. utilities/functions.py +24 -269
  23. utilities/git.py +9 -30
  24. utilities/grp.py +28 -0
  25. utilities/gzip.py +31 -0
  26. utilities/http.py +3 -2
  27. utilities/hypothesis.py +513 -159
  28. utilities/importlib.py +17 -1
  29. utilities/inflect.py +12 -4
  30. utilities/iterables.py +33 -58
  31. utilities/jinja2.py +148 -0
  32. utilities/json.py +70 -0
  33. utilities/libcst.py +38 -17
  34. utilities/lightweight_charts.py +4 -7
  35. utilities/logging.py +136 -93
  36. utilities/math.py +8 -4
  37. utilities/more_itertools.py +43 -45
  38. utilities/operator.py +27 -27
  39. utilities/orjson.py +189 -36
  40. utilities/os.py +61 -4
  41. utilities/packaging.py +115 -0
  42. utilities/parse.py +8 -5
  43. utilities/pathlib.py +269 -40
  44. utilities/permissions.py +298 -0
  45. utilities/platform.py +7 -6
  46. utilities/polars.py +1205 -413
  47. utilities/polars_ols.py +1 -1
  48. utilities/postgres.py +408 -0
  49. utilities/pottery.py +43 -19
  50. utilities/pqdm.py +3 -3
  51. utilities/psutil.py +5 -57
  52. utilities/pwd.py +28 -0
  53. utilities/pydantic.py +4 -52
  54. utilities/pydantic_settings.py +240 -0
  55. utilities/pydantic_settings_sops.py +76 -0
  56. utilities/pyinstrument.py +7 -7
  57. utilities/pytest.py +104 -143
  58. utilities/pytest_plugins/__init__.py +1 -0
  59. utilities/pytest_plugins/pytest_randomly.py +23 -0
  60. utilities/pytest_plugins/pytest_regressions.py +56 -0
  61. utilities/pytest_regressions.py +26 -46
  62. utilities/random.py +11 -6
  63. utilities/re.py +1 -1
  64. utilities/redis.py +220 -343
  65. utilities/sentinel.py +10 -0
  66. utilities/shelve.py +4 -1
  67. utilities/shutil.py +25 -0
  68. utilities/slack_sdk.py +35 -104
  69. utilities/sqlalchemy.py +496 -471
  70. utilities/sqlalchemy_polars.py +29 -54
  71. utilities/string.py +2 -3
  72. utilities/subprocess.py +1977 -0
  73. utilities/tempfile.py +112 -4
  74. utilities/testbook.py +50 -0
  75. utilities/text.py +174 -42
  76. utilities/throttle.py +158 -0
  77. utilities/timer.py +2 -2
  78. utilities/traceback.py +70 -35
  79. utilities/types.py +102 -30
  80. utilities/typing.py +479 -19
  81. utilities/uuid.py +42 -5
  82. utilities/version.py +27 -26
  83. utilities/whenever.py +1559 -361
  84. utilities/zoneinfo.py +80 -22
  85. dycw_utilities-0.135.0.dist-info/METADATA +0 -39
  86. dycw_utilities-0.135.0.dist-info/RECORD +0 -96
  87. dycw_utilities-0.135.0.dist-info/WHEEL +0 -4
  88. dycw_utilities-0.135.0.dist-info/licenses/LICENSE +0 -21
  89. utilities/aiolimiter.py +0 -25
  90. utilities/arq.py +0 -216
  91. utilities/eventkit.py +0 -388
  92. utilities/luigi.py +0 -183
  93. utilities/period.py +0 -152
  94. utilities/pudb.py +0 -62
  95. utilities/python_dotenv.py +0 -101
  96. utilities/streamlit.py +0 -105
  97. 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, wraps
5
- from inspect import iscoroutinefunction
5
+ from functools import partial
6
6
  from os import environ
7
7
  from pathlib import Path
8
- from typing import TYPE_CHECKING, Any, assert_never, cast, override
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 ensure_suffix
18
- from utilities.platform import (
19
- IS_LINUX,
20
- IS_MAC,
21
- IS_NOT_LINUX,
22
- IS_NOT_MAC,
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.random import get_state
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, get_now_local
24
+ from utilities.whenever import SECOND
29
25
 
30
26
  if TYPE_CHECKING:
31
- from collections.abc import Callable, Iterable, Sequence
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
- from pytest import mark, skip
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
- mark = skip = skipif_windows = skipif_mac = skipif_linux = skipif_not_windows = (
49
- skipif_not_mac
50
- ) = skipif_not_linux = None
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
- skipif_windows = mark.skipif(IS_WINDOWS, reason="Skipped for Windows")
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: Sequence[str], /) -> None:
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: Sequence[str], /
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 is_pytest() -> bool:
120
- """Check if `pytest` is running."""
121
- return "PYTEST_VERSION" in environ
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 node_id_to_path(
128
- node_id: str, /, *, head: PathLike | None = None, suffix: str | None = None
143
+ def node_id_path(
144
+ node_id: str, /, *, root: PathLike | None = None, suffix: str | None = None
129
145
  ) -> Path:
130
- """Map a node ID to a path."""
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 NodeIdToPathError(node_id=node_id)
150
+ raise _NodeIdToPathNotPythonFileError(node_id=node_id)
135
151
  path = path_file.with_suffix("")
136
- if head is not None:
137
- path = path.relative_to(head)
138
- path = Path(".".join(path.parts), "__".join(parts))
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
- @fixture
157
- def random_state(*, seed: int) -> Random:
158
- """Fixture for a random state."""
159
- return get_state(seed=seed)
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 throttle[F: Callable[..., MaybeCoro[None]]](
166
- *, root: PathLike | None = None, delta: TimeDelta = SECOND, on_try: bool = False
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 cast("Any", partial(_throttle_inner, root=root, delta=delta, on_try=on_try))
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
- @wraps(func)
185
- def throttle_sync_on_pass(*args: Any, **kwargs: Any) -> None:
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
- return cast("Any", throttle_sync_on_pass)
207
+ skip(reason=f"{_get_name()} throttled")
191
208
 
192
- case False, True:
193
209
 
194
- @wraps(func)
195
- def throttle_sync_on_try(*args: Any, **kwargs: Any) -> None:
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
- def _skipif_recent(*, root: PathLike | None = None, delta: TimeDelta = SECOND) -> None:
227
- if skip is None:
228
- return # pragma: no cover
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 _get_path(*, root: PathLike | None = None) -> Path:
219
+ def _get_test_path(*, root: PathLike | None = None) -> Path:
243
220
  if root is None:
244
- root_use = get_repo_root().joinpath( # pragma: no cover
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
- "is_pytest",
273
- "node_id_to_path",
274
- "random_state",
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
- "skipif_not_windows",
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"]
@@ -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.pathlib import get_root
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, path1: Path, path2: Path, /) -> None:
80
- with path1.open(mode="r") as fh:
81
- left = loads(fh.read())
82
- with path2.open(mode="r") as fh:
83
- right = loads(fh.read())
84
- assert is_equal(left, right), f"{left=}, {right=}"
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
- @fixture
88
- def orjson_regression(
89
- *, request: FixtureRequest, tmp_path: Path
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(return_as_string=True)
137
+ data["glimpse"] = df.glimpse(return_type="string")
136
138
  data["null_count"] = df.null_count().row(0, named=True)
137
- case _ as never:
139
+ case never:
138
140
  assert_never(never)
139
141
  self._fixture.check(data, suffix=suffix)
140
142
 
141
143
 
142
- @fixture
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
- seed = get_state(seed=seed)
21
- return bool(seed.binomialvariate(p=true))
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(*, seed: Seed | None = None) -> str:
29
+ def get_docker_name(seed: Seed | None = None, /) -> str:
28
30
  """Get a docker name."""
29
- state = get_state(seed=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
- def get_state(*, seed: Seed | None = None) -> Random:
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=seed)
64
+ state = get_state(seed)
60
65
  state.shuffle(copy)
61
66
  return copy
62
67
 
utilities/re.py CHANGED
@@ -16,7 +16,7 @@ def ensure_pattern(pattern: PatternLike, /, *, flags: int = 0) -> Pattern[str]:
16
16
  return pattern
17
17
  case str():
18
18
  return re.compile(pattern, flags=flags)
19
- case _ as never:
19
+ case never:
20
20
  assert_never(never)
21
21
 
22
22