dycw-utilities 0.138.4__py3-none-any.whl → 0.138.6__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.138.4
3
+ Version: 0.138.6
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -1,4 +1,4 @@
1
- utilities/__init__.py,sha256=AJA02WwvABRcKbyBb6aLadckmkmDr4Ah5qHq77UCnD8,60
1
+ utilities/__init__.py,sha256=V7k67oxstsllNw_dt-tpMRsylqRrsJMOdAMNNakV9po,60
2
2
  utilities/aiolimiter.py,sha256=mD0wEiqMgwpty4XTbawFpnkkmJS6R4JRsVXFUaoitSU,628
3
3
  utilities/altair.py,sha256=HeZBVUocjkrTNwwKrClppsIqgNFF-ykv05HfZSoHYno,9104
4
4
  utilities/asyncio.py,sha256=dcGeKQzjLBXxKzZkVIk5oZsFXEcynVbRB9iNB5XEDZk,38526
@@ -42,7 +42,7 @@ utilities/optuna.py,sha256=C-fhWYiXHVPo1l8QctYkFJ4DyhbSrGorzP1dJb_qvd8,1933
42
42
  utilities/orjson.py,sha256=WWV2QukCIuwT8OAOtmKhLhxezXPVbeA_fQCucmGmbRA,37106
43
43
  utilities/os.py,sha256=yMNAKMyY8oFgQ1yN3TQYnwa5-A_FXz4tCDbhIctQHSs,3736
44
44
  utilities/parse.py,sha256=JcJn5yXKhIWXBCwgBdPsyu7Hvcuw6kyEdqvaebCaI9k,17951
45
- utilities/pathlib.py,sha256=jCFPZm4rBKylEva9wDVTwQlTTVKMepu92WrTpoGa438,3248
45
+ utilities/pathlib.py,sha256=1QTfoMze_RYX0wrbm9F9FSaZNfO8n1bSFD60-E1Vmc0,6912
46
46
  utilities/period.py,sha256=6jEff_qAiE7xdFaQ1DnKgNf10D2wHhzt7hQXCBoKlgc,6842
47
47
  utilities/pickle.py,sha256=MBT2xZCsv0pH868IXLGKnlcqNx2IRVKYNpRcqiQQqxw,653
48
48
  utilities/platform.py,sha256=5uCKRf_ij7ukJDcbnNfhY2ay9fbrpiNLRO1t2QvcwqQ,2825
@@ -87,7 +87,7 @@ utilities/warnings.py,sha256=un1LvHv70PU-LLv8RxPVmugTzDJkkGXRMZTE2-fTQHw,1771
87
87
  utilities/whenever.py,sha256=R5d9UCNCdAOyjwLUmfH2Vn8Ykee8OHQi2skRTFfbZMM,20492
88
88
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
89
89
  utilities/zoneinfo.py,sha256=oEH-nL3t4h9uawyZqWDtNtDAl6M-CLpLYGI_nI6DulM,1971
90
- dycw_utilities-0.138.4.dist-info/METADATA,sha256=Am7nGDdcdeHFgK71Hso5euQ7WwzvnqcQoRuqGb-uPBw,1638
91
- dycw_utilities-0.138.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
- dycw_utilities-0.138.4.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
93
- dycw_utilities-0.138.4.dist-info/RECORD,,
90
+ dycw_utilities-0.138.6.dist-info/METADATA,sha256=EMQ0EEZfe_ywhxuelhNvMoXkahxUom89cJL4Hh40abU,1638
91
+ dycw_utilities-0.138.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
+ dycw_utilities-0.138.6.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
93
+ dycw_utilities-0.138.6.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.138.4"
3
+ __version__ = "0.138.6"
utilities/pathlib.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Callable
4
- from contextlib import contextmanager, suppress
4
+ from contextlib import contextmanager
5
5
  from dataclasses import dataclass
6
6
  from itertools import chain
7
7
  from os import chdir
@@ -9,8 +9,9 @@ from os.path import expandvars
9
9
  from pathlib import Path
10
10
  from re import IGNORECASE, search
11
11
  from subprocess import PIPE, CalledProcessError, check_output
12
- from typing import TYPE_CHECKING, assert_never, overload, override
12
+ from typing import TYPE_CHECKING, Literal, assert_never, overload, override
13
13
 
14
+ from utilities.errors import ImpossibleCaseError
14
15
  from utilities.sentinel import Sentinel, sentinel
15
16
 
16
17
  if TYPE_CHECKING:
@@ -73,23 +74,50 @@ def get_path(
73
74
  def get_root(*, path: MaybeCallablePathLike | None = None) -> Path:
74
75
  """Get the root of a path."""
75
76
  path = get_path(path=path)
77
+ path_dir = path.parent if path.is_file() else path
76
78
  try:
77
79
  output = check_output(
78
- ["git", "rev-parse", "--show-toplevel"], stderr=PIPE, cwd=path, text=True
80
+ ["git", "rev-parse", "--show-toplevel"],
81
+ stderr=PIPE,
82
+ cwd=path_dir,
83
+ text=True,
79
84
  )
80
85
  except CalledProcessError as error:
81
86
  # newer versions of git report "Not a git repository", whilst older
82
87
  # versions report "not a git repository"
83
88
  if not search("fatal: not a git repository", error.stderr, flags=IGNORECASE):
84
89
  raise # pragma: no cover
90
+ root_git = None
85
91
  else:
86
- return Path(output.strip("\n"))
87
- all_paths = list(chain([path], path.parents))
88
- with suppress(StopIteration):
89
- return next(
90
- p for p in all_paths if any(p_i.name == ".envrc" for p_i in p.iterdir())
92
+ root_git = Path(output.strip("\n")).resolve()
93
+ all_paths = list(chain([path_dir], path_dir.parents))
94
+ try:
95
+ root_envrc = next(
96
+ p.resolve()
97
+ for p in all_paths
98
+ if any(p_i.name == ".envrc" for p_i in p.iterdir())
91
99
  )
92
- raise GetRootError(path=path)
100
+ except StopIteration:
101
+ root_envrc = None
102
+ match root_git, root_envrc:
103
+ case None, None:
104
+ raise GetRootError(path=path)
105
+ case Path(), None:
106
+ return root_git
107
+ case None, Path():
108
+ return root_envrc
109
+ case Path(), Path():
110
+ if root_git == root_envrc:
111
+ return root_git
112
+ if is_sub_path(root_git, root_envrc, strict=True):
113
+ return root_git
114
+ if is_sub_path(root_envrc, root_git, strict=True):
115
+ return root_envrc
116
+ raise ImpossibleCaseError( # pragma: no cover
117
+ case=[f"{root_git=}", f"{root_envrc=}"]
118
+ )
119
+ case _ as never:
120
+ assert_never(never)
93
121
 
94
122
 
95
123
  @dataclass(kw_only=True, slots=True)
@@ -104,6 +132,94 @@ class GetRootError(Exception):
104
132
  ##
105
133
 
106
134
 
135
+ type _GetTailDisambiguate = Literal["raise", "earlier", "later"]
136
+
137
+
138
+ def get_tail(
139
+ path: PathLike, root: PathLike, /, *, disambiguate: _GetTailDisambiguate = "raise"
140
+ ) -> Path:
141
+ """Get the tail of a path following a root match."""
142
+ path_parts, root_parts = [Path(p).parts for p in [path, root]]
143
+ len_path, len_root = map(len, [path_parts, root_parts])
144
+ if len_root > len_path:
145
+ raise _GetTailLengthError(path=path, root=root, len_root=len_root)
146
+ candidates = {
147
+ i + len_root: path_parts[i : i + len_root]
148
+ for i in range(len_path + 1 - len_root)
149
+ }
150
+ matches = {k: v for k, v in candidates.items() if v == root_parts}
151
+ match len(matches), disambiguate:
152
+ case 0, _:
153
+ raise _GetTailEmptyError(path=path, root=root)
154
+ case 1, _:
155
+ return _get_tail_core(path, next(iter(matches)))
156
+ case _, "raise":
157
+ first, second, *_ = matches
158
+ raise _GetTailNonUniqueError(
159
+ path=path,
160
+ root=root,
161
+ first=_get_tail_core(path, first),
162
+ second=_get_tail_core(path, second),
163
+ )
164
+ case _, "earlier":
165
+ return _get_tail_core(path, next(iter(matches)))
166
+ case _, "later":
167
+ return _get_tail_core(path, next(iter(reversed(matches))))
168
+ case _ as never:
169
+ assert_never(never)
170
+
171
+
172
+ def _get_tail_core(path: PathLike, i: int, /) -> Path:
173
+ parts = Path(path).parts
174
+ return Path(*parts[i:])
175
+
176
+
177
+ @dataclass(kw_only=True, slots=True)
178
+ class GetTailError(Exception):
179
+ path: PathLike
180
+ root: PathLike
181
+
182
+
183
+ @dataclass(kw_only=True, slots=True)
184
+ class _GetTailLengthError(GetTailError):
185
+ len_root: int
186
+
187
+ @override
188
+ def __str__(self) -> str:
189
+ return f"Unable to get the tail of {str(self.path)!r} with root of length {self.len_root}"
190
+
191
+
192
+ @dataclass(kw_only=True, slots=True)
193
+ class _GetTailEmptyError(GetTailError):
194
+ @override
195
+ def __str__(self) -> str:
196
+ return (
197
+ f"Unable to get the tail of {str(self.path)!r} with root {str(self.root)!r}"
198
+ )
199
+
200
+
201
+ @dataclass(kw_only=True, slots=True)
202
+ class _GetTailNonUniqueError(GetTailError):
203
+ first: Path
204
+ second: Path
205
+
206
+ @override
207
+ def __str__(self) -> str:
208
+ return f"Path {str(self.path)!r} must contain exactly one tail with root {str(self.root)!r}; got {str(self.first)!r}, {str(self.second)!r} and perhaps more"
209
+
210
+
211
+ ##
212
+
213
+
214
+ def is_sub_path(x: PathLike, y: PathLike, /, *, strict: bool = False) -> bool:
215
+ """Check if a path is a sub path of another."""
216
+ x, y = [Path(i).resolve() for i in [x, y]]
217
+ return x.is_relative_to(y) and not (strict and y.is_relative_to(x))
218
+
219
+
220
+ ##
221
+
222
+
107
223
  def list_dir(path: PathLike, /) -> Sequence[Path]:
108
224
  """List the contents of a directory."""
109
225
  return sorted(Path(path).iterdir())
@@ -123,4 +239,14 @@ def temp_cwd(path: PathLike, /) -> Iterator[None]:
123
239
  chdir(prev)
124
240
 
125
241
 
126
- __all__ = ["PWD", "ensure_suffix", "expand_path", "get_path", "list_dir", "temp_cwd"]
242
+ __all__ = [
243
+ "PWD",
244
+ "GetTailError",
245
+ "ensure_suffix",
246
+ "expand_path",
247
+ "get_path",
248
+ "get_tail",
249
+ "is_sub_path",
250
+ "list_dir",
251
+ "temp_cwd",
252
+ ]