dycw-utilities 0.174.12__py3-none-any.whl → 0.175.31__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.

@@ -1,6 +1,7 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dycw-utilities
3
- Version: 0.174.12
3
+ Version: 0.175.31
4
+ Summary: Miscellaneous Python utilities
4
5
  Author: Derek Wan
5
6
  Author-email: Derek Wan <d.wan@icloud.com>
6
7
  Requires-Dist: atomicwrites>=1.4.1,<1.5
@@ -28,14 +29,6 @@ Provides-Extra: logging
28
29
  Provides-Extra: test
29
30
  Description-Content-Type: text/markdown
30
31
 
31
- [![PyPI version](https://badge.fury.io/py/dycw-utilities.svg)](https://badge.fury.io/py/dycw-utilities)
32
+ # `python-utilities`
32
33
 
33
- # `dycw-utilities`
34
-
35
- [All the Python functions I don't want to write twice.](https://github.com/nvim-lua/plenary.nvim)
36
-
37
- ## Installation
38
-
39
- - `pip install dycw-utilities`
40
-
41
- or with [extras](https://github.com/dycw/python-utilities/blob/master/pyproject.toml).
34
+ Miscellaneous Python utilities
@@ -1,23 +1,22 @@
1
- utilities/__init__.py,sha256=VFU3kmpNxjyAnXuMdW-ygHCxl3CNmUA0rP-wy1y_uzg,61
2
- utilities/aeventkit.py,sha256=OmDBhYGgbsKrB7cdC5FFpJHUatX9O76eTeKVVTksp2Y,12673
3
- utilities/altair.py,sha256=rUK99g9x6CYDDfiZrf-aTx5fSRbL1Q8ctgKORowzXHg,9060
1
+ utilities/__init__.py,sha256=y-0IecU_rQ9B_0aVstJ6gMlcyWn2-Hw_v-RcSXW8pWY,61
2
+ utilities/altair.py,sha256=TLfRFbG9HwG7SLXoJ-v0r-t49ZaGgTQZD82cpjVi4vs,9085
4
3
  utilities/asyncio.py,sha256=aJySVxBY0gqsIYnoNmH7-1r8djKuf4vSsU69VCD08t8,16772
5
4
  utilities/atomicwrites.py,sha256=tPo6r-Rypd9u99u66B9z86YBPpnLrlHtwox_8Z7T34Y,5790
6
5
  utilities/atools.py,sha256=6neeCcgXxK2dlsc0xp15Za7nSucbCgFtAJepGI_-WXU,2549
7
6
  utilities/cachetools.py,sha256=2S9LMHIunDYMIu8JGI7OLN04sQ7-xZGdEdP1Li0vksA,2775
8
- utilities/click.py,sha256=-_B7V5arQopuVJA6emnwBlZGQvcnq7nq6IYc3cPX5FA,19204
7
+ utilities/click.py,sha256=ScLzBLoBp8Si5YjgB18A0IVMAR-r4sGUnVfJbAaru98,19191
9
8
  utilities/concurrent.py,sha256=fHeW2SZ_TEMfFY0C8pyQI6aPlnecvx9x6SuUwBWj_JY,2853
10
9
  utilities/contextlib.py,sha256=iP7R2tIm6ZsbfLD5ks6UKBYwj50e9gBI8AkpLN-chro,7476
11
10
  utilities/contextvars.py,sha256=J8OhC7jqozAGYOCe2KUWysbPXNGe5JYz3HfaY_mIs08,883
12
11
  utilities/cryptography.py,sha256=5PFrzsNUGHay91dFgYnDKwYprXxahrBqztmUqViRzBk,956
13
12
  utilities/cvxpy.py,sha256=Rv1-fD-XYerosCavRF8Pohop2DBkU3AlFaGTfD8AEAA,13776
14
13
  utilities/dataclasses.py,sha256=xbU3QN1GFy7RC6hIJRZIeUZm7YRlodrgEWmahWG6k2g,32465
15
- utilities/docker.py,sha256=DBgSz-UPBDHk_XJLPNaNMkNym1kKK8l3Os8IqMwEyW8,7866
14
+ utilities/docker.py,sha256=IOOdxAqfJfy1SXwy3e4VaAR9ag42W0HTcNcQz7DJPF8,10501
16
15
  utilities/enum.py,sha256=5l6pwZD1cjSlVW4ss-zBPspWvrbrYrdtJWcg6f5_J5w,5781
17
16
  utilities/errors.py,sha256=mFlDGSM0LI1jZ1pbqwLAH3ttLZ2JVIxyZLojw8tGVZU,1479
18
17
  utilities/fastapi.py,sha256=TqyKvBjiMS594sXPjrz-KRTLMb3l3D3rZ1zAYV7GfOk,1454
19
18
  utilities/fpdf2.py,sha256=dSiYz0FJTD2sQuxpxqFWwwIe2-p6Y7oTB9Tv0Jajit0,1866
20
- utilities/functions.py,sha256=82qCAaPIB0JmZ5wsQurA3MTYl7fh8LHcoBFkxPs7Zeg,21478
19
+ utilities/functions.py,sha256=18Zda7nTloARdcEudH8YJ4e13xAdWShAGhPNN4w2Gyc,21498
21
20
  utilities/functools.py,sha256=I00ru2gQPakZw2SHVeKIKXfTv741655s6HI0lUoE0D4,1552
22
21
  utilities/getpass.py,sha256=DfN5UgMAtFCqS3dSfFHUfqIMZX2shXvwphOz_6J6f6A,103
23
22
  utilities/git.py,sha256=U1RFvCTANGENgx9wVBDvllioqBQZM2ns12ivKhOsaO4,414
@@ -25,8 +24,8 @@ utilities/grp.py,sha256=1vV3gNR9dQsl1vtUtvC_2qgVdQzm7O8lLMSh56cTbeg,694
25
24
  utilities/gzip.py,sha256=fkGP3KdsBfXlstodT4wtlp-PwNyUsogpbDCVVVGdsm4,781
26
25
  utilities/hashlib.py,sha256=SVTgtguur0P4elppvzOBbLEjVM3Pea0eWB61yg2ilxo,309
27
26
  utilities/http.py,sha256=TsavEfHlRtlLaeV21Z6KZh0qbPw-kvD1zsQdZ7Kep5Q,977
28
- utilities/hypothesis.py,sha256=wk1HiNdBg7tGPEKLZ5uiNVbtlSZl58QJjlediYoSHkA,46753
29
- utilities/importlib.py,sha256=mV1xT_O_zt_GnZZ36tl3xOmMaN_3jErDWY54fX39F6Y,429
27
+ utilities/hypothesis.py,sha256=NUu30pl5kjL3tzo-m8SMRwTqLAmTWK-_Sau2NemJcQo,46773
28
+ utilities/importlib.py,sha256=SkVVtIjVC7bjJ36doXnmnmFiYe5tLbip4YAfYJj8Ycg,892
30
29
  utilities/inflect.py,sha256=v7YkOWSu8NAmVghPcf4F3YBZQoJCS47_DLf9jbfWIs0,581
31
30
  utilities/ipython.py,sha256=V2oMYHvEKvlNBzxDXdLvKi48oUq2SclRg5xasjaXStw,763
32
31
  utilities/iterables.py,sha256=t2TsW-K3rVlS6y4_tqcc1fk9RwJV-bi7G_VwduMABK0,42558
@@ -46,11 +45,11 @@ utilities/optuna.py,sha256=C-fhWYiXHVPo1l8QctYkFJ4DyhbSrGorzP1dJb_qvd8,1933
46
45
  utilities/orjson.py,sha256=T_0SlK811ysg46d3orvIPY3JpBa4FRMpP2wlPQo7-gU,41854
47
46
  utilities/os.py,sha256=kjKKSQfnRqFTTZ315iavaaGd3gGuYNoSWlxVLCJjyQs,4852
48
47
  utilities/parse.py,sha256=g7Qm9eBOIeDId2tGA021CIaeF6jp1TI8rx4srdvlyoo,17937
49
- utilities/pathlib.py,sha256=EKZn-wWxH7MEWFrQGqHIoB-GJzyXeiEj8iDIgvkr8Wk,9325
50
- utilities/permissions.py,sha256=u-1PMJGC8PDbZltR4SozBuKKX5yIubmXPuiKrBx3IKU,8943
48
+ utilities/pathlib.py,sha256=N4Ip8R9eCM-6GfvxUJ3T9oQIle2C2P52F-13BCFRdTg,9345
49
+ utilities/permissions.py,sha256=vLXlWztSVYffbrxptne7ksj6dU1HLekm4fEvS4ny_4Q,8944
51
50
  utilities/pickle.py,sha256=MBT2xZCsv0pH868IXLGKnlcqNx2IRVKYNpRcqiQQqxw,653
52
- utilities/platform.py,sha256=0pYO5v7L2sU5UN87zHhEEhTKsZ9NIEM8N6UCr0F7bLY,2778
53
- utilities/polars.py,sha256=cNFBLWgOMUAp_Sz4xtlto17uZswZRrcfQYC95QKyaY4,87483
51
+ utilities/platform.py,sha256=R3ldt2-DlI7la9ng6Rxt1CThd2lL0Ai2tC0TbabtCC0,2800
52
+ utilities/polars.py,sha256=JPzN4UqQDC7R4IXsIuXEIXRiwHSrkiSZcD8UOfwGPuE,87535
54
53
  utilities/polars_ols.py,sha256=LNTFNLPuYW7fcAHymlbnams_DhitToblYvib3mhKbwI,5615
55
54
  utilities/postgres.py,sha256=g3tEwTI8TdmiCbRME61ffQ0xaibdpXPu8mJOOHvjPKc,12532
56
55
  utilities/pottery.py,sha256=nA0SsF9irvfC0tk68YAr08tuL9lGRSlBKihSx7Ibk84,3963
@@ -66,7 +65,7 @@ utilities/pytest.py,sha256=9HHwYgZQe6CRF0ekHQEFH05gmoP4Ne0V54RrtUNDfi4,10524
66
65
  utilities/pytest_plugins/__init__.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
67
66
  utilities/pytest_plugins/pytest_randomly.py,sha256=B1qYVlExGOxTywq2r1SMi5o7btHLk2PNdY_b1p98dkE,409
68
67
  utilities/pytest_plugins/pytest_regressions.py,sha256=mnHYBfdprz50UGVkVzV1bZERZN5CFfoF8YbokGxdFwU,1639
69
- utilities/pytest_regressions.py,sha256=nKdVZAa88_aMo1a8V-EIHg_smZYdk1iDQBm50R3FlO4,4044
68
+ utilities/pytest_regressions.py,sha256=tJxW38u-zpoyjW1N4zogBx4V_07r-ibDInddcEUyXmc,4763
70
69
  utilities/random.py,sha256=hZlH4gnAtoaofWswuJYjcygejrY8db4CzP-z_adO2Mo,4165
71
70
  utilities/re.py,sha256=S4h-DLL6ScMPqjboZ_uQ1BVTJajrqV06r_81D--_HCE,4573
72
71
  utilities/redis.py,sha256=gybjqKea33Jy50n4dHTS14JdquqHaJqHF2dixQljYWQ,30172
@@ -81,8 +80,8 @@ utilities/sqlalchemy.py,sha256=HQYpd7LFxdTF5WYVWYtCJeEBI71EJm7ytvCGyAH9B-U,37163
81
80
  utilities/sqlalchemy_polars.py,sha256=JCGhB37raSR7fqeWV5dTsciRTMVzIdVT9YSqKT0piT0,13370
82
81
  utilities/statsmodels.py,sha256=koyiBHvpMcSiBfh99wFUfSggLNx7cuAw3rwyfAhoKpQ,3410
83
82
  utilities/string.py,sha256=shmBK87zZwzGyixuNuXCiUbqzfeZ9xlrFwz6JTaRvDk,582
84
- utilities/subprocess.py,sha256=dIBguvLB0WepJKqgBxlzF6r8z9HW5JiGKLg-eslpuec,22549
85
- utilities/tempfile.py,sha256=Lx6qa16lL1XVH6WdmD_G9vlN6gLI8nrIurxmsFkPKvg,3022
83
+ utilities/subprocess.py,sha256=ei0OD73S0WX7ZqHCQp9UB3hY9OWbAUVIrLoz9niIfQQ,53031
84
+ utilities/tempfile.py,sha256=a3_M1QyxGZql_VcGkBOQBeWbbkItjgkfIpVyzU1UAic,3843
86
85
  utilities/testbook.py,sha256=j1KmaVbrX9VrbeMgtPh5gk55myAsn3dyRUn7jGbPbRk,1294
87
86
  utilities/text.py,sha256=7SvwcSR2l_5cOrm1samGnR4C-ZI6qyFLHLzSpO1zeHQ,13958
88
87
  utilities/threading.py,sha256=GvBOp4CyhHfN90wGXZuA2VKe9fGzMaEa7oCl4f3nnPU,1009
@@ -98,7 +97,7 @@ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
98
97
  utilities/whenever.py,sha256=F4ek0-OBWxHYrZdmoZt76N2RnNyKY5KrEHt7rqO4AQE,60183
99
98
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
100
99
  utilities/zoneinfo.py,sha256=tdIScrTB2-B-LH0ukb1HUXKooLknOfJNwHk10MuMYvA,3619
101
- dycw_utilities-0.174.12.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
102
- dycw_utilities-0.174.12.dist-info/entry_points.txt,sha256=ykGI1ArwOPHqm2g5Cqh3ENdMxEej_a_FcOUov5EM5Oc,155
103
- dycw_utilities-0.174.12.dist-info/METADATA,sha256=Cf8kQqLOY8YDH8nG9Y7WcGf6xJFGxfDIMC1Amw13isc,1710
104
- dycw_utilities-0.174.12.dist-info/RECORD,,
100
+ dycw_utilities-0.175.31.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
101
+ dycw_utilities-0.175.31.dist-info/entry_points.txt,sha256=cOGtKeJI0KXLSV7MJ8Dhc2G8jPgDcBDm53MVNJU4ycI,136
102
+ dycw_utilities-0.175.31.dist-info/METADATA,sha256=WX68a6sQoOamTeIZsrXvDdI_VKItf-Jm3Cam7p1jR_U,1443
103
+ dycw_utilities-0.175.31.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.18
2
+ Generator: uv 0.9.21
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,5 +1,3 @@
1
- [console_scripts]
2
-
3
1
  [pytest11]
4
2
  pytest-randomly = utilities.pytest_plugins.pytest_randomly
5
3
  pytest-regressions = utilities.pytest_plugins.pytest_regressions
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.174.12"
3
+ __version__ = "0.175.31"
utilities/altair.py CHANGED
@@ -145,7 +145,9 @@ def plot_dataframes(
145
145
  ]
146
146
  zoom = selection_interval(bind="scales", encodings=["x"])
147
147
  chart = (
148
- vconcat(*layers).add_params(zoom).resolve_scale(color="independent", x="shared")
148
+ vconcat_charts(*layers)
149
+ .add_params(zoom)
150
+ .resolve_scale(color="independent", x="shared")
149
151
  )
150
152
  if title is not None:
151
153
  chart = chart.properties(title=title)
utilities/click.py CHANGED
@@ -210,8 +210,7 @@ class EnumPartial[E: enum.Enum](ParamType):
210
210
  self.fail(str(error), param, ctx)
211
211
  if enum in self._members:
212
212
  return enum
213
- self.fail(f"{enum.value!r} is not a selected member")
214
- return None
213
+ return self.fail(f"{enum.value!r} is not a selected member")
215
214
 
216
215
  @override
217
216
  def get_metavar(self, param: Parameter, ctx: Context) -> str | None:
utilities/docker.py CHANGED
@@ -5,6 +5,7 @@ from pathlib import Path
5
5
  from typing import TYPE_CHECKING, Literal, overload
6
6
 
7
7
  from utilities.errors import ImpossibleCaseError
8
+ from utilities.iterables import always_iterable
8
9
  from utilities.logging import to_logger
9
10
  from utilities.subprocess import (
10
11
  MKTEMP_DIR_CMD,
@@ -18,7 +19,87 @@ from utilities.subprocess import (
18
19
  if TYPE_CHECKING:
19
20
  from collections.abc import Iterator
20
21
 
21
- from utilities.types import LoggerLike, PathLike, Retry, StrStrMapping
22
+ from utilities.types import (
23
+ LoggerLike,
24
+ MaybeIterable,
25
+ PathLike,
26
+ Retry,
27
+ StrStrMapping,
28
+ )
29
+
30
+
31
+ def docker_compose_down(
32
+ *,
33
+ files: MaybeIterable[PathLike] | None = None,
34
+ print: bool = False, # noqa: A002
35
+ print_stdout: bool = False,
36
+ print_stderr: bool = False,
37
+ ) -> None:
38
+ """Stop and remove containers."""
39
+ args = docker_compose_down_cmd(files=files) # pragma: no cover
40
+ run( # pragma: no cover
41
+ *args, print=print, print_stdout=print_stdout, print_stderr=print_stderr
42
+ )
43
+
44
+
45
+ def docker_compose_down_cmd(
46
+ *, files: MaybeIterable[PathLike] | None = None
47
+ ) -> list[str]:
48
+ """Command to use 'docker compose down' to stop and remove containers."""
49
+ return _docker_compose_cmd("down", files=files)
50
+
51
+
52
+ def docker_compose_pull(
53
+ *,
54
+ files: MaybeIterable[PathLike] | None = None,
55
+ print: bool = False, # noqa: A002
56
+ print_stdout: bool = False,
57
+ print_stderr: bool = False,
58
+ ) -> None:
59
+ """Pull service images."""
60
+ args = docker_compose_pull_cmd(files=files) # pragma: no cover
61
+ run( # pragma: no cover
62
+ *args, print=print, print_stdout=print_stdout, print_stderr=print_stderr
63
+ )
64
+
65
+
66
+ def docker_compose_pull_cmd(
67
+ *, files: MaybeIterable[PathLike] | None = None
68
+ ) -> list[str]:
69
+ """Command to use 'docker compose pull' to pull service images."""
70
+ return _docker_compose_cmd("pull", files=files)
71
+
72
+
73
+ def docker_compose_up(
74
+ *,
75
+ files: MaybeIterable[PathLike] | None = None,
76
+ print: bool = False, # noqa: A002
77
+ print_stdout: bool = False,
78
+ print_stderr: bool = False,
79
+ ) -> None:
80
+ """Create and start containers."""
81
+ args = docker_compose_up_cmd(files=files) # pragma: no cover
82
+ run( # pragma: no cover
83
+ *args, print=print, print_stdout=print_stdout, print_stderr=print_stderr
84
+ )
85
+
86
+
87
+ def docker_compose_up_cmd(*, files: MaybeIterable[PathLike] | None = None) -> list[str]:
88
+ """Command to use 'docker compose up' to create and start containers."""
89
+ return _docker_compose_cmd("up", files=files)
90
+
91
+
92
+ def _docker_compose_cmd(
93
+ cmd: str, /, *, files: MaybeIterable[PathLike] | None = None
94
+ ) -> list[str]:
95
+ args: list[str] = ["docker", "compose"]
96
+ if files is not None:
97
+ for file in always_iterable(files):
98
+ args.extend(["--file", str(file)])
99
+ return [*args, cmd]
100
+
101
+
102
+ ##
22
103
 
23
104
 
24
105
  @overload
@@ -47,48 +128,39 @@ def docker_cp(
47
128
  sudo: bool = False,
48
129
  logger: LoggerLike | None = None,
49
130
  ) -> None:
50
- match src, dest:
131
+ """Copy between a container and the local file system."""
132
+ match src, dest: # skipif-ci
51
133
  case Path() | str(), (str() as cont, Path() | str() as dest_path):
52
134
  docker_exec(
53
135
  cont, *maybe_sudo_cmd(*mkdir_cmd(dest_path, parent=True), sudo=sudo)
54
136
  )
55
- run(*docker_cp_cmd(src, dest, sudo=sudo), logger=logger)
137
+ run(*maybe_sudo_cmd(*docker_cp_cmd(src, dest), sudo=sudo), logger=logger)
56
138
  case (str(), Path() | str()), Path() | str():
57
139
  mkdir(dest, parent=True, sudo=sudo)
58
- run(*docker_cp_cmd(src, dest, sudo=sudo), logger=logger)
140
+ run(*maybe_sudo_cmd(*docker_cp_cmd(src, dest), sudo=sudo), logger=logger)
59
141
  case _: # pragma: no cover
60
142
  raise ImpossibleCaseError(case=[f"{src}", f"{dest=}"])
61
143
 
62
144
 
63
145
  @overload
64
- def docker_cp_cmd(
65
- src: tuple[str, PathLike], dest: PathLike, /, *, sudo: bool = False
66
- ) -> list[str]: ...
146
+ def docker_cp_cmd(src: tuple[str, PathLike], dest: PathLike, /) -> list[str]: ...
67
147
  @overload
148
+ def docker_cp_cmd(src: PathLike, dest: tuple[str, PathLike], /) -> list[str]: ...
68
149
  def docker_cp_cmd(
69
- src: PathLike, dest: tuple[str, PathLike], /, *, sudo: bool = False
70
- ) -> list[str]: ...
71
- def docker_cp_cmd(
72
- src: PathLike | tuple[str, PathLike],
73
- dest: PathLike | tuple[str, PathLike],
74
- /,
75
- *,
76
- sudo: bool = False,
150
+ src: PathLike | tuple[str, PathLike], dest: PathLike | tuple[str, PathLike], /
77
151
  ) -> list[str]:
152
+ """Command to use 'docker cp' to copy between a container and the local file system."""
153
+ args: list[str] = ["docker", "cp"]
78
154
  match src, dest:
79
- case (Path() | str()) as src_use, (
80
- str() as dest_cont,
81
- Path() | str() as dest_path,
82
- ):
83
- dest_use = f"{dest_cont}:{dest_path}"
84
- case (str() as src_cont, (Path() | str()) as src_path), (
85
- Path() | str() as dest_use
86
- ):
87
- src_use = f"{src_cont}:{src_path}"
155
+ case ((Path() | str()), (str() as cont, Path() | str() as path)):
156
+ return [*args, str(src), f"{cont}:{path}"]
157
+ case (str() as cont, (Path() | str()) as path), (Path() | str() as dest):
158
+ return [*args, f"{cont}:{path}", str(dest)]
88
159
  case _: # pragma: no cover
89
160
  raise ImpossibleCaseError(case=[f"{src}", f"{dest=}"])
90
- parts: list[str] = ["docker", "cp", str(src_use), str(dest_use)]
91
- return maybe_sudo_cmd(*parts, sudo=sudo)
161
+
162
+
163
+ ##
92
164
 
93
165
 
94
166
  @overload
@@ -210,6 +282,7 @@ def docker_exec(
210
282
  logger: LoggerLike | None = None,
211
283
  **env_kwargs: str,
212
284
  ) -> str | None:
285
+ """Execute a command in a container."""
213
286
  cmd_and_args = docker_exec_cmd( # skipif-ci
214
287
  container,
215
288
  cmd,
@@ -245,7 +318,7 @@ def docker_exec_cmd(
245
318
  workdir: PathLike | None = None,
246
319
  **env_kwargs: str,
247
320
  ) -> list[str]:
248
- """Build a command for `docker exec`."""
321
+ """Command to use `docker exec` to execute a command in a container."""
249
322
  args: list[str] = ["docker", "exec"]
250
323
  mapping: dict[str, str] = ({} if env is None else dict(env)) | env_kwargs
251
324
  for key, value in mapping.items():
@@ -259,6 +332,9 @@ def docker_exec_cmd(
259
332
  return [*args, container, cmd, *cmds_or_args]
260
333
 
261
334
 
335
+ ##
336
+
337
+
262
338
  @contextmanager
263
339
  def yield_docker_temp_dir(
264
340
  container: str,
@@ -269,6 +345,7 @@ def yield_docker_temp_dir(
269
345
  logger: LoggerLike | None = None,
270
346
  keep: bool = False,
271
347
  ) -> Iterator[Path]:
348
+ """Yield a temporary directory in a Docker container."""
272
349
  path = Path( # skipif-ci
273
350
  docker_exec(
274
351
  container,
@@ -289,4 +366,16 @@ def yield_docker_temp_dir(
289
366
  docker_exec(container, *rm_cmd(path), user=user, retry=retry, logger=logger)
290
367
 
291
368
 
292
- __all__ = ["docker_cp_cmd", "docker_exec", "docker_exec_cmd", "yield_docker_temp_dir"]
369
+ __all__ = [
370
+ "docker_compose_down",
371
+ "docker_compose_down_cmd",
372
+ "docker_compose_pull",
373
+ "docker_compose_pull_cmd",
374
+ "docker_compose_up",
375
+ "docker_compose_up_cmd",
376
+ "docker_cp",
377
+ "docker_cp_cmd",
378
+ "docker_exec",
379
+ "docker_exec_cmd",
380
+ "yield_docker_temp_dir",
381
+ ]
utilities/functions.py CHANGED
@@ -693,7 +693,7 @@ def second[U](pair: tuple[Any, U], /) -> U:
693
693
 
694
694
  def skip_if_optimize[**P](func: Callable[P, None], /) -> Callable[P, None]:
695
695
  """Skip a function if we are in the optimized mode."""
696
- if __debug__:
696
+ if __debug__: # pragma: no cover
697
697
  return func
698
698
 
699
699
  @wraps(func)
utilities/hypothesis.py CHANGED
@@ -1040,7 +1040,7 @@ def setup_hypothesis_profiles(
1040
1040
  assert_never(never)
1041
1041
 
1042
1042
  phases = {Phase.explicit, Phase.reuse, Phase.generate, Phase.target}
1043
- if "HYPOTHESIS_NO_SHRINK" not in environ:
1043
+ if "HYPOTHESIS_NO_SHRINK" not in environ: # pragma: no cover
1044
1044
  phases.add(Phase.shrink)
1045
1045
  for profile in Profile:
1046
1046
  try:
utilities/importlib.py CHANGED
@@ -1,7 +1,23 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import importlib.resources
3
4
  from importlib import import_module
4
5
  from importlib.util import find_spec
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ from utilities.errors import ImpossibleCaseError
10
+
11
+ if TYPE_CHECKING:
12
+ from importlib.resources import Anchor
13
+
14
+
15
+ def files(*, anchor: Anchor | None = None) -> Path:
16
+ """Get the path for an anchor."""
17
+ path = importlib.resources.files(anchor)
18
+ if isinstance(path, Path):
19
+ return path
20
+ raise ImpossibleCaseError(case=[f"{path=}"]) # pragma: no cover
5
21
 
6
22
 
7
23
  def is_valid_import(module: str, /, *, name: str | None = None) -> bool:
@@ -15,4 +31,4 @@ def is_valid_import(module: str, /, *, name: str | None = None) -> bool:
15
31
  return hasattr(mod, name)
16
32
 
17
33
 
18
- __all__ = ["is_valid_import"]
34
+ __all__ = ["files", "is_valid_import"]
utilities/pathlib.py CHANGED
@@ -127,7 +127,7 @@ class GetRepoRootError(Exception): ...
127
127
  class _GetRepoRootGitNotFoundError(GetRepoRootError):
128
128
  @override
129
129
  def __str__(self) -> str:
130
- return "'git' not found"
130
+ return "'git' not found" # pragma: no cover
131
131
 
132
132
 
133
133
  @dataclass(kw_only=True, slots=True)
utilities/permissions.py CHANGED
@@ -25,6 +25,7 @@ from utilities.sentinel import Sentinel, sentinel
25
25
  if TYPE_CHECKING:
26
26
  from utilities.types import PathLike
27
27
 
28
+
28
29
  type PermissionsLike = Permissions | int | str
29
30
 
30
31
 
utilities/platform.py CHANGED
@@ -16,7 +16,7 @@ System = Literal["windows", "mac", "linux"]
16
16
  def get_system() -> System:
17
17
  """Get the system/OS name."""
18
18
  sys = system()
19
- if sys == "Windows":
19
+ if sys == "Windows": # skipif-not-windows
20
20
  return "windows"
21
21
  if sys == "Darwin": # skipif-not-macos
22
22
  return "mac"
utilities/polars.py CHANGED
@@ -2639,6 +2639,8 @@ def search_period(
2639
2639
  return None
2640
2640
  item: dt.datetime = series[index]["start"]
2641
2641
  return index if py_date_time > item else None
2642
+ case never:
2643
+ assert_never(never)
2642
2644
 
2643
2645
 
2644
2646
  ##
@@ -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, path1: Path, path2: Path, /) -> None:
74
- left = loads(path1.read_text())
75
- right = loads(path2.read_text())
76
- 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
+
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
  ##