dycw-utilities 0.166.30__py3-none-any.whl → 0.185.8__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.185.8.dist-info/METADATA +33 -0
- dycw_utilities-0.185.8.dist-info/RECORD +90 -0
- {dycw_utilities-0.166.30.dist-info → dycw_utilities-0.185.8.dist-info}/WHEEL +1 -1
- {dycw_utilities-0.166.30.dist-info → dycw_utilities-0.185.8.dist-info}/entry_points.txt +1 -0
- utilities/__init__.py +1 -1
- utilities/altair.py +17 -10
- utilities/asyncio.py +50 -72
- utilities/atools.py +9 -11
- utilities/cachetools.py +16 -11
- utilities/click.py +76 -19
- utilities/concurrent.py +1 -1
- utilities/constants.py +492 -0
- utilities/contextlib.py +23 -30
- utilities/contextvars.py +1 -23
- utilities/core.py +2581 -0
- utilities/dataclasses.py +16 -119
- utilities/docker.py +387 -0
- utilities/enum.py +1 -1
- utilities/errors.py +2 -16
- utilities/fastapi.py +5 -5
- utilities/fpdf2.py +2 -1
- utilities/functions.py +34 -265
- utilities/http.py +2 -3
- utilities/hypothesis.py +84 -29
- utilities/importlib.py +17 -1
- utilities/iterables.py +39 -575
- utilities/jinja2.py +145 -0
- utilities/jupyter.py +5 -3
- utilities/libcst.py +1 -1
- utilities/lightweight_charts.py +4 -6
- utilities/logging.py +24 -24
- utilities/math.py +1 -36
- utilities/more_itertools.py +4 -6
- utilities/numpy.py +2 -1
- utilities/operator.py +2 -2
- utilities/orjson.py +42 -43
- utilities/os.py +4 -147
- utilities/packaging.py +129 -0
- utilities/parse.py +35 -15
- utilities/pathlib.py +3 -120
- utilities/platform.py +8 -90
- utilities/polars.py +38 -32
- utilities/postgres.py +37 -33
- utilities/pottery.py +20 -18
- utilities/pqdm.py +3 -4
- utilities/psutil.py +2 -3
- utilities/pydantic.py +25 -0
- utilities/pydantic_settings.py +87 -16
- utilities/pydantic_settings_sops.py +16 -3
- utilities/pyinstrument.py +4 -4
- utilities/pytest.py +96 -125
- utilities/pytest_plugins/pytest_regressions.py +2 -2
- utilities/pytest_regressions.py +32 -11
- utilities/random.py +2 -8
- utilities/redis.py +98 -94
- utilities/reprlib.py +11 -118
- utilities/shellingham.py +66 -0
- utilities/shutil.py +25 -0
- utilities/slack_sdk.py +13 -12
- utilities/sqlalchemy.py +57 -30
- utilities/sqlalchemy_polars.py +16 -25
- utilities/subprocess.py +2590 -0
- utilities/tabulate.py +32 -0
- utilities/testbook.py +8 -8
- utilities/text.py +24 -99
- utilities/throttle.py +159 -0
- utilities/time.py +18 -0
- utilities/timer.py +31 -14
- utilities/traceback.py +16 -23
- utilities/types.py +42 -2
- utilities/typing.py +26 -14
- utilities/uuid.py +1 -1
- utilities/version.py +202 -45
- utilities/whenever.py +53 -150
- dycw_utilities-0.166.30.dist-info/METADATA +0 -41
- dycw_utilities-0.166.30.dist-info/RECORD +0 -98
- dycw_utilities-0.166.30.dist-info/licenses/LICENSE +0 -21
- utilities/aeventkit.py +0 -388
- utilities/atomicwrites.py +0 -182
- utilities/cryptography.py +0 -41
- utilities/getpass.py +0 -8
- utilities/git.py +0 -19
- utilities/gzip.py +0 -31
- utilities/json.py +0 -70
- utilities/pickle.py +0 -25
- utilities/re.py +0 -156
- utilities/sentinel.py +0 -73
- utilities/socket.py +0 -8
- utilities/string.py +0 -20
- utilities/tempfile.py +0 -77
- utilities/typed_settings.py +0 -152
- utilities/tzdata.py +0 -11
- utilities/tzlocal.py +0 -28
- utilities/warnings.py +0 -65
- utilities/zipfile.py +0 -25
- utilities/zoneinfo.py +0 -133
utilities/os.py
CHANGED
|
@@ -1,42 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from contextlib import suppress
|
|
4
3
|
from dataclasses import dataclass
|
|
5
|
-
from
|
|
6
|
-
from typing import TYPE_CHECKING, Literal, assert_never, overload, override
|
|
4
|
+
from typing import TYPE_CHECKING, assert_never, override
|
|
7
5
|
|
|
8
|
-
from utilities.
|
|
9
|
-
from utilities.iterables import OneStrEmptyError, one_str
|
|
6
|
+
from utilities.constants import CPU_COUNT
|
|
10
7
|
|
|
11
8
|
if TYPE_CHECKING:
|
|
12
|
-
from
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
type IntOrAll = int | Literal["all"]
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
##
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def get_cpu_count() -> int:
|
|
22
|
-
"""Get the CPU count."""
|
|
23
|
-
count = cpu_count()
|
|
24
|
-
if count is None: # pragma: no cover
|
|
25
|
-
raise GetCPUCountError
|
|
26
|
-
return count
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@dataclass(kw_only=True, slots=True)
|
|
30
|
-
class GetCPUCountError(Exception):
|
|
31
|
-
@override
|
|
32
|
-
def __str__(self) -> str:
|
|
33
|
-
return "CPU count must not be None" # pragma: no cover
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
CPU_COUNT = get_cpu_count()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
##
|
|
9
|
+
from utilities.types import IntOrAll
|
|
40
10
|
|
|
41
11
|
|
|
42
12
|
def get_cpu_use(*, n: IntOrAll = "all") -> int:
|
|
@@ -61,117 +31,4 @@ class GetCPUUseError(Exception):
|
|
|
61
31
|
return f"Invalid number of CPUs to use: {self.n}"
|
|
62
32
|
|
|
63
33
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
@overload
|
|
68
|
-
def get_env_var(
|
|
69
|
-
key: str, /, *, case_sensitive: bool = False, default: str, nullable: bool = False
|
|
70
|
-
) -> str: ...
|
|
71
|
-
@overload
|
|
72
|
-
def get_env_var(
|
|
73
|
-
key: str,
|
|
74
|
-
/,
|
|
75
|
-
*,
|
|
76
|
-
case_sensitive: bool = False,
|
|
77
|
-
default: None = None,
|
|
78
|
-
nullable: Literal[False] = False,
|
|
79
|
-
) -> str: ...
|
|
80
|
-
@overload
|
|
81
|
-
def get_env_var(
|
|
82
|
-
key: str,
|
|
83
|
-
/,
|
|
84
|
-
*,
|
|
85
|
-
case_sensitive: bool = False,
|
|
86
|
-
default: str | None = None,
|
|
87
|
-
nullable: bool = False,
|
|
88
|
-
) -> str | None: ...
|
|
89
|
-
def get_env_var(
|
|
90
|
-
key: str,
|
|
91
|
-
/,
|
|
92
|
-
*,
|
|
93
|
-
case_sensitive: bool = False,
|
|
94
|
-
default: str | None = None,
|
|
95
|
-
nullable: bool = False,
|
|
96
|
-
) -> str | None:
|
|
97
|
-
"""Get an environment variable."""
|
|
98
|
-
try:
|
|
99
|
-
key_use = one_str(environ, key, case_sensitive=case_sensitive)
|
|
100
|
-
except OneStrEmptyError:
|
|
101
|
-
match default, nullable:
|
|
102
|
-
case None, False:
|
|
103
|
-
raise GetEnvVarError(key=key, case_sensitive=case_sensitive) from None
|
|
104
|
-
case None, True:
|
|
105
|
-
return None
|
|
106
|
-
case str(), _:
|
|
107
|
-
return default
|
|
108
|
-
case never:
|
|
109
|
-
assert_never(never)
|
|
110
|
-
return environ[key_use]
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
@dataclass(kw_only=True, slots=True)
|
|
114
|
-
class GetEnvVarError(Exception):
|
|
115
|
-
key: str
|
|
116
|
-
case_sensitive: bool = False
|
|
117
|
-
|
|
118
|
-
@override
|
|
119
|
-
def __str__(self) -> str:
|
|
120
|
-
desc = f"No environment variable {self.key!r}"
|
|
121
|
-
return desc if self.case_sensitive else f"{desc} (modulo case)"
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
##
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def is_debug() -> bool:
|
|
128
|
-
"""Check if we are in `DEBUG` mode."""
|
|
129
|
-
return get_env_var("DEBUG", nullable=True) is not None
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
##
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def is_pytest() -> bool:
|
|
136
|
-
"""Check if `pytest` is running."""
|
|
137
|
-
return get_env_var("PYTEST_VERSION", nullable=True) is not None
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
##
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
@enhanced_context_manager
|
|
144
|
-
def temp_environ(
|
|
145
|
-
env: Mapping[str, str | None] | None = None, **env_kwargs: str | None
|
|
146
|
-
) -> Iterator[None]:
|
|
147
|
-
"""Context manager with temporary environment variable set."""
|
|
148
|
-
mapping: dict[str, str | None] = ({} if env is None else dict(env)) | env_kwargs
|
|
149
|
-
prev = {key: getenv(key) for key in mapping}
|
|
150
|
-
|
|
151
|
-
def apply(mapping: Mapping[str, str | None], /) -> None:
|
|
152
|
-
for key, value in mapping.items():
|
|
153
|
-
if value is None:
|
|
154
|
-
with suppress(KeyError):
|
|
155
|
-
del environ[key]
|
|
156
|
-
else:
|
|
157
|
-
environ[key] = value
|
|
158
|
-
|
|
159
|
-
apply(mapping)
|
|
160
|
-
try:
|
|
161
|
-
yield
|
|
162
|
-
finally:
|
|
163
|
-
apply(prev)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
__all__ = [
|
|
167
|
-
"CPU_COUNT",
|
|
168
|
-
"GetCPUCountError",
|
|
169
|
-
"GetCPUUseError",
|
|
170
|
-
"IntOrAll",
|
|
171
|
-
"get_cpu_count",
|
|
172
|
-
"get_cpu_use",
|
|
173
|
-
"get_env_var",
|
|
174
|
-
"is_debug",
|
|
175
|
-
"is_pytest",
|
|
176
|
-
"temp_environ",
|
|
177
|
-
]
|
|
34
|
+
__all__ = ["GetCPUUseError", "get_cpu_use"]
|
utilities/packaging.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import TYPE_CHECKING, Self, overload, override
|
|
5
|
+
|
|
6
|
+
import packaging._parser
|
|
7
|
+
import packaging.requirements
|
|
8
|
+
from packaging.requirements import _parse_requirement
|
|
9
|
+
from packaging.specifiers import Specifier, SpecifierSet
|
|
10
|
+
|
|
11
|
+
from utilities.core import OneEmptyError, one
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from packaging._parser import MarkerList
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(order=True, unsafe_hash=True, slots=True)
|
|
18
|
+
class Requirement:
|
|
19
|
+
requirement: str
|
|
20
|
+
_parsed_req: packaging._parser.ParsedRequirement = field(init=False, repr=False)
|
|
21
|
+
_custom_req: _CustomRequirement = field(init=False, repr=False)
|
|
22
|
+
|
|
23
|
+
def __getitem__(self, operator: str, /) -> str:
|
|
24
|
+
return self.specifier_set[operator]
|
|
25
|
+
|
|
26
|
+
def __post_init__(self) -> None:
|
|
27
|
+
self._parsed_req = _parse_requirement(self.requirement)
|
|
28
|
+
self._custom_req = _CustomRequirement(self.requirement)
|
|
29
|
+
|
|
30
|
+
@override
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
return str(self._custom_req)
|
|
33
|
+
|
|
34
|
+
def drop(self, operator: str, /) -> Self:
|
|
35
|
+
return type(self)(str(self._custom_req.drop(operator)))
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def extras(self) -> list[str]:
|
|
39
|
+
return self._parsed_req.extras
|
|
40
|
+
|
|
41
|
+
@overload
|
|
42
|
+
def get(self, operator: str, default: str, /) -> str: ...
|
|
43
|
+
@overload
|
|
44
|
+
def get(self, operator: str, default: None = None, /) -> str | None: ...
|
|
45
|
+
def get(self, operator: str, default: str | None = None, /) -> str | None:
|
|
46
|
+
return self.specifier_set.get(operator, default)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def marker(self) -> MarkerList | None:
|
|
50
|
+
return self._parsed_req.marker
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def name(self) -> str:
|
|
54
|
+
return self._parsed_req.name
|
|
55
|
+
|
|
56
|
+
def replace(self, operator: str, version: str | None, /) -> Self:
|
|
57
|
+
return type(self)(str(self._custom_req.replace(operator, version)))
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def specifier(self) -> str:
|
|
61
|
+
return self._parsed_req.specifier
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def specifier_set(self) -> _CustomSpecifierSet:
|
|
65
|
+
return _CustomSpecifierSet(_parse_requirement(self.requirement).specifier)
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def url(self) -> str:
|
|
69
|
+
return self._parsed_req.url
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class _CustomRequirement(packaging.requirements.Requirement):
|
|
73
|
+
specifier: _CustomSpecifierSet
|
|
74
|
+
|
|
75
|
+
@override
|
|
76
|
+
def __init__(self, requirement_string: str) -> None:
|
|
77
|
+
super().__init__(requirement_string)
|
|
78
|
+
parsed = _parse_requirement(requirement_string)
|
|
79
|
+
self.specifier = _CustomSpecifierSet(parsed.specifier) # pyright: ignore[reportIncompatibleVariableOverride]
|
|
80
|
+
|
|
81
|
+
def drop(self, operator: str, /) -> Self:
|
|
82
|
+
new = type(self)(super().__str__())
|
|
83
|
+
new.specifier = self.specifier.drop(operator)
|
|
84
|
+
return new
|
|
85
|
+
|
|
86
|
+
def replace(self, operator: str, version: str | None, /) -> Self:
|
|
87
|
+
new = type(self)(super().__str__())
|
|
88
|
+
new.specifier = self.specifier.replace(operator, version)
|
|
89
|
+
return new
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class _CustomSpecifierSet(SpecifierSet):
|
|
93
|
+
def __getitem__(self, operator: str, /) -> str:
|
|
94
|
+
try:
|
|
95
|
+
return one(s.version for s in self if s.operator == operator)
|
|
96
|
+
except OneEmptyError:
|
|
97
|
+
raise KeyError(operator) from None
|
|
98
|
+
|
|
99
|
+
@override
|
|
100
|
+
def __str__(self) -> str:
|
|
101
|
+
specs = sorted(self._specs, key=self._sort_key)
|
|
102
|
+
return ", ".join(map(str, specs))
|
|
103
|
+
|
|
104
|
+
def drop(self, operator: str, /) -> Self:
|
|
105
|
+
if any(s.operator == operator for s in self):
|
|
106
|
+
return type(self)(s for s in self if s.operator != operator)
|
|
107
|
+
raise KeyError(operator)
|
|
108
|
+
|
|
109
|
+
@overload
|
|
110
|
+
def get(self, operator: str, default: str, /) -> str: ...
|
|
111
|
+
@overload
|
|
112
|
+
def get(self, operator: str, default: None = None, /) -> str | None: ...
|
|
113
|
+
def get(self, operator: str, default: str | None = None, /) -> str | None:
|
|
114
|
+
try:
|
|
115
|
+
return self[operator]
|
|
116
|
+
except KeyError:
|
|
117
|
+
return default
|
|
118
|
+
|
|
119
|
+
def replace(self, operator: str, version: str | None, /) -> Self:
|
|
120
|
+
specifiers = [s for s in self if s.operator != operator]
|
|
121
|
+
if version is not None:
|
|
122
|
+
specifiers.append(Specifier(spec=f"{operator}{version}"))
|
|
123
|
+
return type(self)(specifiers)
|
|
124
|
+
|
|
125
|
+
def _sort_key(self, spec: Specifier, /) -> int:
|
|
126
|
+
return ["==", "!=", "~=", ">", ">=", "<", "<="].index(spec.operator)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
__all__ = ["Requirement"]
|
utilities/parse.py
CHANGED
|
@@ -21,15 +21,24 @@ from whenever import (
|
|
|
21
21
|
ZonedDateTime,
|
|
22
22
|
)
|
|
23
23
|
|
|
24
|
-
from utilities.
|
|
25
|
-
from utilities.iterables import OneEmptyError, OneNonUniqueError, one, one_str
|
|
26
|
-
from utilities.math import ParseNumberError, parse_number
|
|
27
|
-
from utilities.re import ExtractGroupError, extract_group
|
|
28
|
-
from utilities.sentinel import ParseSentinelError, Sentinel, parse_sentinel
|
|
29
|
-
from utilities.text import (
|
|
24
|
+
from utilities.constants import (
|
|
30
25
|
BRACKETS,
|
|
31
26
|
LIST_SEPARATOR,
|
|
32
27
|
PAIR_SEPARATOR,
|
|
28
|
+
Sentinel,
|
|
29
|
+
SentinelParseError,
|
|
30
|
+
)
|
|
31
|
+
from utilities.core import (
|
|
32
|
+
ExtractGroupError,
|
|
33
|
+
OneEmptyError,
|
|
34
|
+
OneNonUniqueError,
|
|
35
|
+
extract_group,
|
|
36
|
+
one,
|
|
37
|
+
one_str,
|
|
38
|
+
)
|
|
39
|
+
from utilities.enum import ParseEnumError, parse_enum
|
|
40
|
+
from utilities.math import ParseNumberError, parse_number
|
|
41
|
+
from utilities.text import (
|
|
33
42
|
ParseBoolError,
|
|
34
43
|
ParseNoneError,
|
|
35
44
|
join_strs,
|
|
@@ -52,7 +61,12 @@ from utilities.typing import (
|
|
|
52
61
|
is_tuple_type,
|
|
53
62
|
is_union_type,
|
|
54
63
|
)
|
|
55
|
-
from utilities.version import
|
|
64
|
+
from utilities.version import (
|
|
65
|
+
Version2,
|
|
66
|
+
Version3,
|
|
67
|
+
_Version2ParseError,
|
|
68
|
+
_Version3ParseError,
|
|
69
|
+
)
|
|
56
70
|
|
|
57
71
|
if TYPE_CHECKING:
|
|
58
72
|
from collections.abc import Iterable, Mapping, Sequence
|
|
@@ -204,20 +218,25 @@ def _parse_object_type(
|
|
|
204
218
|
),
|
|
205
219
|
):
|
|
206
220
|
try:
|
|
207
|
-
return cls.
|
|
221
|
+
return cls.parse_iso(text)
|
|
208
222
|
except ValueError:
|
|
209
223
|
raise _ParseObjectParseError(type_=cls, text=text) from None
|
|
210
224
|
if issubclass(cls, Path):
|
|
211
225
|
return Path(text).expanduser()
|
|
212
226
|
if issubclass(cls, Sentinel):
|
|
213
227
|
try:
|
|
214
|
-
return
|
|
215
|
-
except
|
|
228
|
+
return Sentinel.parse(text)
|
|
229
|
+
except SentinelParseError:
|
|
230
|
+
raise _ParseObjectParseError(type_=cls, text=text) from None
|
|
231
|
+
if issubclass(cls, Version2):
|
|
232
|
+
try:
|
|
233
|
+
return Version2.parse(text)
|
|
234
|
+
except _Version2ParseError:
|
|
216
235
|
raise _ParseObjectParseError(type_=cls, text=text) from None
|
|
217
|
-
if issubclass(cls,
|
|
236
|
+
if issubclass(cls, Version3):
|
|
218
237
|
try:
|
|
219
|
-
return
|
|
220
|
-
except
|
|
238
|
+
return Version3.parse(text)
|
|
239
|
+
except _Version3ParseError:
|
|
221
240
|
raise _ParseObjectParseError(type_=cls, text=text) from None
|
|
222
241
|
raise _ParseObjectParseError(type_=cls, text=text)
|
|
223
242
|
|
|
@@ -460,7 +479,8 @@ def serialize_object(
|
|
|
460
479
|
| IPv6Address
|
|
461
480
|
| Path
|
|
462
481
|
| Sentinel
|
|
463
|
-
|
|
|
482
|
+
| Version2
|
|
483
|
+
| Version3,
|
|
464
484
|
):
|
|
465
485
|
return str(obj)
|
|
466
486
|
if isinstance(
|
|
@@ -477,7 +497,7 @@ def serialize_object(
|
|
|
477
497
|
ZonedDateTime,
|
|
478
498
|
),
|
|
479
499
|
):
|
|
480
|
-
return obj.
|
|
500
|
+
return obj.format_iso()
|
|
481
501
|
if isinstance(obj, Enum):
|
|
482
502
|
return obj.name
|
|
483
503
|
if isinstance(obj, dict):
|
utilities/pathlib.py
CHANGED
|
@@ -3,26 +3,19 @@ from __future__ import annotations
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from itertools import chain
|
|
6
|
-
from os import chdir
|
|
7
|
-
from os.path import expandvars
|
|
8
6
|
from pathlib import Path
|
|
9
7
|
from re import IGNORECASE, search
|
|
10
8
|
from subprocess import PIPE, CalledProcessError, check_output
|
|
11
9
|
from typing import TYPE_CHECKING, Literal, assert_never, overload, override
|
|
12
10
|
|
|
13
|
-
from utilities.
|
|
14
|
-
from utilities.errors import ImpossibleCaseError
|
|
15
|
-
from utilities.sentinel import Sentinel
|
|
11
|
+
from utilities.constants import Sentinel
|
|
16
12
|
|
|
17
13
|
if TYPE_CHECKING:
|
|
18
|
-
from collections.abc import
|
|
14
|
+
from collections.abc import Sequence
|
|
19
15
|
|
|
20
16
|
from utilities.types import MaybeCallablePathLike, PathLike
|
|
21
17
|
|
|
22
18
|
|
|
23
|
-
PWD = Path.cwd()
|
|
24
|
-
|
|
25
|
-
|
|
26
19
|
def ensure_suffix(path: PathLike, suffix: str, /) -> Path:
|
|
27
20
|
"""Ensure a path has a given suffix."""
|
|
28
21
|
path = Path(path)
|
|
@@ -42,43 +35,6 @@ def ensure_suffix(path: PathLike, suffix: str, /) -> Path:
|
|
|
42
35
|
##
|
|
43
36
|
|
|
44
37
|
|
|
45
|
-
def expand_path(path: PathLike, /) -> Path:
|
|
46
|
-
"""Expand a path."""
|
|
47
|
-
path = str(path)
|
|
48
|
-
path = expandvars(path)
|
|
49
|
-
return Path(path).expanduser()
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
##
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def get_package_root(path: MaybeCallablePathLike = Path.cwd, /) -> Path:
|
|
56
|
-
"""Get the package root."""
|
|
57
|
-
path = to_path(path)
|
|
58
|
-
path_dir = path.parent if path.is_file() else path
|
|
59
|
-
all_paths = list(chain([path_dir], path_dir.parents))
|
|
60
|
-
try:
|
|
61
|
-
return next(
|
|
62
|
-
p.resolve()
|
|
63
|
-
for p in all_paths
|
|
64
|
-
if any(p_i.name == "pyproject.toml" for p_i in p.iterdir())
|
|
65
|
-
)
|
|
66
|
-
except StopIteration:
|
|
67
|
-
raise GetPackageRootError(path=path) from None
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@dataclass(kw_only=True, slots=True)
|
|
71
|
-
class GetPackageRootError(Exception):
|
|
72
|
-
path: PathLike
|
|
73
|
-
|
|
74
|
-
@override
|
|
75
|
-
def __str__(self) -> str:
|
|
76
|
-
return f"Path is not part of a package: {self.path}"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
##
|
|
80
|
-
|
|
81
|
-
|
|
82
38
|
def get_repo_root(path: MaybeCallablePathLike = Path.cwd, /) -> Path:
|
|
83
39
|
"""Get the repo root."""
|
|
84
40
|
path = to_path(path)
|
|
@@ -112,7 +68,7 @@ class GetRepoRootError(Exception): ...
|
|
|
112
68
|
class _GetRepoRootGitNotFoundError(GetRepoRootError):
|
|
113
69
|
@override
|
|
114
70
|
def __str__(self) -> str:
|
|
115
|
-
return "'git' not found"
|
|
71
|
+
return "'git' not found" # pragma: no cover
|
|
116
72
|
|
|
117
73
|
|
|
118
74
|
@dataclass(kw_only=True, slots=True)
|
|
@@ -127,50 +83,6 @@ class _GetRepoRootNotARepoError(GetRepoRootError):
|
|
|
127
83
|
##
|
|
128
84
|
|
|
129
85
|
|
|
130
|
-
def get_root(path: MaybeCallablePathLike = Path.cwd, /) -> Path:
|
|
131
|
-
"""Get the root of a path."""
|
|
132
|
-
path = to_path(path)
|
|
133
|
-
try:
|
|
134
|
-
repo = get_repo_root(path)
|
|
135
|
-
except GetRepoRootError:
|
|
136
|
-
repo = None
|
|
137
|
-
try:
|
|
138
|
-
package = get_package_root(path)
|
|
139
|
-
except GetPackageRootError:
|
|
140
|
-
package = None
|
|
141
|
-
match repo, package:
|
|
142
|
-
case None, None:
|
|
143
|
-
raise GetRootError(path=path)
|
|
144
|
-
case Path(), None:
|
|
145
|
-
return repo
|
|
146
|
-
case None, Path():
|
|
147
|
-
return package
|
|
148
|
-
case Path(), Path():
|
|
149
|
-
if repo == package:
|
|
150
|
-
return repo
|
|
151
|
-
if is_sub_path(repo, package, strict=True):
|
|
152
|
-
return repo
|
|
153
|
-
if is_sub_path(package, repo, strict=True):
|
|
154
|
-
return package
|
|
155
|
-
raise ImpossibleCaseError( # pragma: no cover
|
|
156
|
-
case=[f"{repo=}", f"{package=}"]
|
|
157
|
-
)
|
|
158
|
-
case never:
|
|
159
|
-
assert_never(never)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
@dataclass(kw_only=True, slots=True)
|
|
163
|
-
class GetRootError(Exception):
|
|
164
|
-
path: PathLike
|
|
165
|
-
|
|
166
|
-
@override
|
|
167
|
-
def __str__(self) -> str:
|
|
168
|
-
return f"Unable to determine root from {str(self.path)!r}"
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
##
|
|
172
|
-
|
|
173
|
-
|
|
174
86
|
type _GetTailDisambiguate = Literal["raise", "earlier", "later"]
|
|
175
87
|
|
|
176
88
|
|
|
@@ -268,15 +180,6 @@ def module_path(
|
|
|
268
180
|
##
|
|
269
181
|
|
|
270
182
|
|
|
271
|
-
def is_sub_path(x: PathLike, y: PathLike, /, *, strict: bool = False) -> bool:
|
|
272
|
-
"""Check if a path is a sub path of another."""
|
|
273
|
-
x, y = [Path(i).resolve() for i in [x, y]]
|
|
274
|
-
return x.is_relative_to(y) and not (strict and y.is_relative_to(x))
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
##
|
|
278
|
-
|
|
279
|
-
|
|
280
183
|
def list_dir(path: PathLike, /) -> Sequence[Path]:
|
|
281
184
|
"""List the contents of a directory."""
|
|
282
185
|
return sorted(Path(path).iterdir())
|
|
@@ -285,20 +188,6 @@ def list_dir(path: PathLike, /) -> Sequence[Path]:
|
|
|
285
188
|
##
|
|
286
189
|
|
|
287
190
|
|
|
288
|
-
@enhanced_context_manager
|
|
289
|
-
def temp_cwd(path: PathLike, /) -> Iterator[None]:
|
|
290
|
-
"""Context manager with temporary current working directory set."""
|
|
291
|
-
prev = Path.cwd()
|
|
292
|
-
chdir(path)
|
|
293
|
-
try:
|
|
294
|
-
yield
|
|
295
|
-
finally:
|
|
296
|
-
chdir(prev)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
##
|
|
300
|
-
|
|
301
|
-
|
|
302
191
|
@overload
|
|
303
192
|
def to_path(path: Sentinel, /) -> Sentinel: ...
|
|
304
193
|
@overload
|
|
@@ -321,18 +210,12 @@ def to_path(
|
|
|
321
210
|
|
|
322
211
|
|
|
323
212
|
__all__ = [
|
|
324
|
-
"PWD",
|
|
325
|
-
"GetPackageRootError",
|
|
326
213
|
"GetRepoRootError",
|
|
327
214
|
"GetTailError",
|
|
328
215
|
"ensure_suffix",
|
|
329
|
-
"expand_path",
|
|
330
|
-
"get_package_root",
|
|
331
216
|
"get_repo_root",
|
|
332
217
|
"get_tail",
|
|
333
|
-
"is_sub_path",
|
|
334
218
|
"list_dir",
|
|
335
219
|
"module_path",
|
|
336
|
-
"temp_cwd",
|
|
337
220
|
"to_path",
|
|
338
221
|
]
|
utilities/platform.py
CHANGED
|
@@ -1,74 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from platform import system
|
|
6
3
|
from re import sub
|
|
7
|
-
from typing import
|
|
4
|
+
from typing import assert_never
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
from collections.abc import Iterable, Iterator
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
System = Literal["windows", "mac", "linux"]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def get_system() -> System:
|
|
17
|
-
"""Get the system/OS name."""
|
|
18
|
-
sys = system()
|
|
19
|
-
if sys == "Windows": # skipif-not-windows
|
|
20
|
-
return "windows"
|
|
21
|
-
if sys == "Darwin": # skipif-not-macos
|
|
22
|
-
return "mac"
|
|
23
|
-
if sys == "Linux": # skipif-not-linux
|
|
24
|
-
return "linux"
|
|
25
|
-
raise GetSystemError(sys=sys) # pragma: no cover
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@dataclass(kw_only=True, slots=True)
|
|
29
|
-
class GetSystemError(Exception):
|
|
30
|
-
sys: str
|
|
31
|
-
|
|
32
|
-
@override
|
|
33
|
-
def __str__(self) -> str:
|
|
34
|
-
return ( # pragma: no cover
|
|
35
|
-
f"System must be one of Windows, Darwin, Linux; got {self.sys!r} instead"
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
SYSTEM = get_system()
|
|
40
|
-
IS_WINDOWS = SYSTEM == "windows"
|
|
41
|
-
IS_MAC = SYSTEM == "mac"
|
|
42
|
-
IS_LINUX = SYSTEM == "linux"
|
|
43
|
-
IS_NOT_WINDOWS = not IS_WINDOWS
|
|
44
|
-
IS_NOT_MAC = not IS_MAC
|
|
45
|
-
IS_NOT_LINUX = not IS_LINUX
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
##
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def get_max_pid() -> int | None:
|
|
52
|
-
"""Get the maximum process ID."""
|
|
53
|
-
match SYSTEM:
|
|
54
|
-
case "windows": # pragma: no cover
|
|
55
|
-
return None
|
|
56
|
-
case "mac": # skipif-not-macos
|
|
57
|
-
return 99999
|
|
58
|
-
case "linux": # skipif-not-linux
|
|
59
|
-
path = Path("/proc/sys/kernel/pid_max")
|
|
60
|
-
try:
|
|
61
|
-
return int(path.read_text())
|
|
62
|
-
except FileNotFoundError: # pragma: no cover
|
|
63
|
-
return None
|
|
64
|
-
case never:
|
|
65
|
-
assert_never(never)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
MAX_PID = get_max_pid()
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
##
|
|
6
|
+
from utilities.constants import SYSTEM
|
|
72
7
|
|
|
73
8
|
|
|
74
9
|
def get_strftime(text: str, /) -> str:
|
|
@@ -87,32 +22,15 @@ def get_strftime(text: str, /) -> str:
|
|
|
87
22
|
##
|
|
88
23
|
|
|
89
24
|
|
|
90
|
-
def
|
|
91
|
-
"""
|
|
25
|
+
def maybe_lower_case(text: str, /) -> str:
|
|
26
|
+
"""Lower-case text if the platform is case-insensitive w.r.t. filenames."""
|
|
92
27
|
match SYSTEM:
|
|
93
|
-
case "windows": # skipif-
|
|
94
|
-
|
|
95
|
-
case "mac": # skipif-not-macos
|
|
96
|
-
yield from (t.lower() for t in text)
|
|
28
|
+
case "windows" | "mac": # skipif-linux
|
|
29
|
+
return text.lower()
|
|
97
30
|
case "linux": # skipif-not-linux
|
|
98
|
-
|
|
31
|
+
return text
|
|
99
32
|
case never:
|
|
100
33
|
assert_never(never)
|
|
101
34
|
|
|
102
35
|
|
|
103
|
-
__all__ = [
|
|
104
|
-
"IS_LINUX",
|
|
105
|
-
"IS_MAC",
|
|
106
|
-
"IS_NOT_LINUX",
|
|
107
|
-
"IS_NOT_MAC",
|
|
108
|
-
"IS_NOT_WINDOWS",
|
|
109
|
-
"IS_WINDOWS",
|
|
110
|
-
"MAX_PID",
|
|
111
|
-
"SYSTEM",
|
|
112
|
-
"GetSystemError",
|
|
113
|
-
"System",
|
|
114
|
-
"get_max_pid",
|
|
115
|
-
"get_strftime",
|
|
116
|
-
"get_system",
|
|
117
|
-
"maybe_yield_lower_case",
|
|
118
|
-
]
|
|
36
|
+
__all__ = ["get_strftime", "maybe_lower_case"]
|