dycw-utilities 0.127.0__py3-none-any.whl → 0.129.13__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.
utilities/fastapi.py CHANGED
@@ -1,15 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from asyncio import Task, create_task
3
4
  from dataclasses import InitVar, dataclass, field
4
- from typing import TYPE_CHECKING, Any, Literal, override
5
+ from typing import TYPE_CHECKING, Any, Literal, Self, override
5
6
 
6
7
  from fastapi import FastAPI
7
8
  from uvicorn import Config, Server
8
9
 
9
- from utilities.asyncio import InfiniteLooper
10
+ from utilities.asyncio import Looper
10
11
  from utilities.datetime import SECOND, datetime_duration_to_float
11
12
 
12
13
  if TYPE_CHECKING:
14
+ from types import TracebackType
15
+
13
16
  from utilities.types import Duration
14
17
 
15
18
 
@@ -36,7 +39,7 @@ class _PingerReceiverApp(FastAPI):
36
39
 
37
40
 
38
41
  @dataclass(kw_only=True)
39
- class PingReceiver(InfiniteLooper):
42
+ class PingReceiver(Looper[None]):
40
43
  """A ping receiver."""
41
44
 
42
45
  host: InitVar[str] = _LOCALHOST
@@ -44,12 +47,31 @@ class PingReceiver(InfiniteLooper):
44
47
  _app: _PingerReceiverApp = field(
45
48
  default_factory=_PingerReceiverApp, init=False, repr=False
46
49
  )
47
- _await_upon_aenter: bool = field(default=False, init=False, repr=False)
48
50
  _server: Server = field(init=False, repr=False)
51
+ _server_task: Task[None] | None = field(default=None, init=False, repr=False)
49
52
 
53
+ @override
50
54
  def __post_init__(self, host: str, port: int, /) -> None:
55
+ super().__post_init__() # skipif-ci
51
56
  self._server = Server(Config(self._app, host=host, port=port)) # skipif-ci
52
57
 
58
+ @override
59
+ async def __aenter__(self) -> Self:
60
+ _ = await super().__aenter__() # skipif-ci
61
+ async with self._lock: # skipif-ci
62
+ self._server_task = create_task(self._server.serve())
63
+ return self # skipif-ci
64
+
65
+ @override
66
+ async def __aexit__(
67
+ self,
68
+ exc_type: type[BaseException] | None = None,
69
+ exc_value: BaseException | None = None,
70
+ traceback: TracebackType | None = None,
71
+ ) -> None:
72
+ await super().__aexit__(exc_type, exc_value, traceback) # skipif-ci
73
+ await self._server.shutdown() # skipif-ci
74
+
53
75
  @classmethod
54
76
  async def ping(
55
77
  cls, port: int, /, *, host: str = _LOCALHOST, timeout: Duration = _TIMEOUT
@@ -66,13 +88,5 @@ class PingReceiver(InfiniteLooper):
66
88
  return False
67
89
  return response.text if response.status_code == 200 else False # skipif-ci
68
90
 
69
- @override
70
- async def _initialize(self) -> None:
71
- await self._server.serve() # skipif-ci
72
-
73
- @override
74
- async def _teardown(self) -> None:
75
- await self._server.shutdown() # skipif-ci
76
-
77
91
 
78
92
  __all__ = ["PingReceiver"]
utilities/git.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from pathlib import Path
5
5
  from re import IGNORECASE, search
6
- from subprocess import PIPE, CalledProcessError, check_call, check_output
6
+ from subprocess import PIPE, CalledProcessError, check_output
7
7
  from typing import TYPE_CHECKING, override
8
8
 
9
9
  from utilities.pathlib import PWD
@@ -12,63 +12,17 @@ if TYPE_CHECKING:
12
12
  from utilities.types import PathLike
13
13
 
14
14
 
15
- def fetch_all_tags(*, cwd: PathLike = PWD) -> None:
16
- """Fetch the tags."""
17
- _ = check_call(["git", "fetch", "--all", "--tags"], cwd=cwd)
18
-
19
-
20
- ##
21
-
22
-
23
- def get_branch_name(*, cwd: PathLike = PWD) -> str:
24
- """Get the current branch name."""
25
- output = check_output(
26
- _GIT_REV_PARSE_ABBREV_REV_HEAD, stderr=PIPE, cwd=cwd, text=True
27
- )
28
- return output.strip("\n")
29
-
30
-
31
- _GIT_REV_PARSE_ABBREV_REV_HEAD = ["git", "rev-parse", "--abbrev-ref", "HEAD"]
32
-
33
-
34
- ##
35
-
36
-
37
- def get_ref_tags(ref: str, /, *, cwd: PathLike = PWD) -> list[str]:
38
- """Get the tags of a reference."""
39
- output = check_output([*_GIT_TAG_POINTS_AT, ref], stderr=PIPE, cwd=cwd, text=True)
40
- return output.strip("\n").splitlines()
41
-
42
-
43
- _GIT_TAG_POINTS_AT = ["git", "tag", "--points-at"]
44
-
45
-
46
- ##
47
-
48
-
49
- def get_repo_name(*, cwd: PathLike = PWD) -> str:
50
- """Get the repo name."""
51
- output = check_output(_GIT_REMOTE_GET_URL_ORIGIN, stderr=PIPE, cwd=cwd, text=True)
52
- return Path(output.strip("\n")).stem # not valid_path
53
-
54
-
55
- _GIT_REMOTE_GET_URL_ORIGIN = ["git", "remote", "get-url", "origin"]
56
-
57
-
58
- ##
59
-
60
-
61
- def get_repo_root(*, cwd: PathLike = PWD) -> Path:
15
+ def get_repo_root(*, path: PathLike = PWD) -> Path:
62
16
  """Get the repo root."""
63
17
  try:
64
18
  output = check_output(
65
- ["git", "rev-parse", "--show-toplevel"], stderr=PIPE, cwd=cwd, text=True
19
+ ["git", "rev-parse", "--show-toplevel"], stderr=PIPE, cwd=path, text=True
66
20
  )
67
21
  except CalledProcessError as error:
68
22
  # newer versions of git report "Not a git repository", whilst older
69
23
  # versions report "not a git repository"
70
24
  if search("fatal: not a git repository", error.stderr, flags=IGNORECASE):
71
- raise GetRepoRootError(cwd=cwd) from error
25
+ raise GetRepoRootError(cwd=path) from error
72
26
  raise # pragma: no cover
73
27
  else:
74
28
  return Path(output.strip("\n"))
@@ -83,11 +37,4 @@ class GetRepoRootError(Exception):
83
37
  return f"Path is not part of a `git` repository: {self.cwd}"
84
38
 
85
39
 
86
- __all__ = [
87
- "GetRepoRootError",
88
- "fetch_all_tags",
89
- "get_branch_name",
90
- "get_ref_tags",
91
- "get_repo_name",
92
- "get_repo_root",
93
- ]
40
+ __all__ = ["GetRepoRootError", "get_repo_root"]
utilities/hypothesis.py CHANGED
@@ -506,13 +506,7 @@ def floats_extra(
506
506
 
507
507
 
508
508
  @composite
509
- def git_repos(
510
- draw: DrawFn,
511
- /,
512
- *,
513
- branch: MaybeSearchStrategy[str | None] = None,
514
- remote: MaybeSearchStrategy[str | None] = None,
515
- ) -> Path:
509
+ def git_repos(draw: DrawFn, /) -> Path:
516
510
  path = draw(temp_paths())
517
511
  with temp_cwd(path):
518
512
  _ = check_call(["git", "init", "-b", "master"])
@@ -525,10 +519,6 @@ def git_repos(
525
519
  _ = check_call(["git", "commit", "-m", "add"])
526
520
  _ = check_call(["git", "rm", file_str])
527
521
  _ = check_call(["git", "commit", "-m", "rm"])
528
- if (branch_ := draw2(draw, branch)) is not None:
529
- _ = check_call(["git", "checkout", "-b", branch_])
530
- if (remote_ := draw2(draw, remote)) is not None:
531
- _ = check_call(["git", "remote", "add", "origin", remote_])
532
522
  return path
533
523
 
534
524
 
utilities/logging.py CHANGED
@@ -23,7 +23,7 @@ from logging import (
23
23
  )
24
24
  from logging.handlers import BaseRotatingHandler, TimedRotatingFileHandler
25
25
  from pathlib import Path
26
- from re import Pattern, search
26
+ from re import Pattern
27
27
  from sys import stdout
28
28
  from time import time
29
29
  from typing import (
@@ -46,9 +46,8 @@ from utilities.datetime import (
46
46
  serialize_compact,
47
47
  )
48
48
  from utilities.errors import ImpossibleCaseError
49
- from utilities.git import get_repo_root
50
49
  from utilities.iterables import OneEmptyError, always_iterable, one
51
- from utilities.pathlib import ensure_suffix, resolve_path
50
+ from utilities.pathlib import ensure_suffix, get_path, get_root
52
51
  from utilities.reprlib import (
53
52
  RICH_EXPAND_ALL,
54
53
  RICH_INDENT_SIZE,
@@ -68,9 +67,9 @@ if TYPE_CHECKING:
68
67
  from utilities.types import (
69
68
  LoggerOrName,
70
69
  LogLevel,
70
+ MaybeCallablePathLike,
71
71
  MaybeIterable,
72
72
  PathLike,
73
- PathLikeOrCallable,
74
73
  )
75
74
  from utilities.version import MaybeCallableVersionLike
76
75
 
@@ -86,6 +85,9 @@ except ModuleNotFoundError: # pragma: no cover
86
85
  type _When = Literal[
87
86
  "S", "M", "H", "D", "midnight", "W0", "W1", "W2", "W3", "W4", "W5", "W6"
88
87
  ]
88
+ _BACKUP_COUNT: int = 100
89
+ _MAX_BYTES: int = 10 * 1024**2
90
+ _WHEN: _When = "D"
89
91
 
90
92
 
91
93
  class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
@@ -101,15 +103,16 @@ class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
101
103
  encoding: str | None = None,
102
104
  delay: bool = False,
103
105
  errors: Literal["strict", "ignore", "replace"] | None = None,
104
- maxBytes: int = 0,
105
- when: _When = "midnight",
106
+ maxBytes: int = _MAX_BYTES,
107
+ when: _When = _WHEN,
106
108
  interval: int = 1,
107
- backupCount: int = 0,
109
+ backupCount: int = _BACKUP_COUNT,
108
110
  utc: bool = False,
109
111
  atTime: dt.time | None = None,
110
112
  ) -> None:
111
- filename = str(Path(filename))
112
- super().__init__(filename, mode, encoding=encoding, delay=delay, errors=errors)
113
+ path = Path(filename)
114
+ path.parent.mkdir(parents=True, exist_ok=True)
115
+ super().__init__(path, mode, encoding=encoding, delay=delay, errors=errors)
113
116
  self._max_bytes = maxBytes if maxBytes >= 1 else None
114
117
  self._backup_count = backupCount if backupCount >= 1 else None
115
118
  self._filename = Path(self.baseFilename)
@@ -118,7 +121,7 @@ class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
118
121
  self._suffix = self._filename.suffix
119
122
  self._patterns = _compute_rollover_patterns(self._stem, self._suffix)
120
123
  self._time_handler = TimedRotatingFileHandler(
121
- filename,
124
+ path,
122
125
  when=when,
123
126
  interval=interval,
124
127
  backupCount=backupCount,
@@ -161,13 +164,11 @@ class SizeAndTimeRotatingFileHandler(BaseRotatingHandler):
161
164
  def _should_rollover(self, record: LogRecord, /) -> bool:
162
165
  if self._max_bytes is not None: # skipif-ci-and-windows
163
166
  try:
164
- current = self._filename.stat().st_size
167
+ size = self._filename.stat().st_size
165
168
  except FileNotFoundError:
166
169
  pass
167
170
  else:
168
- delta = len(f"{self.format(record)}\n")
169
- new = current + delta
170
- if new >= self._max_bytes:
171
+ if size >= self._max_bytes:
171
172
  return True
172
173
  return bool(self._time_handler.shouldRollover(record)) # skipif-ci-and-windows
173
174
 
@@ -383,10 +384,10 @@ class StandaloneFileHandler(Handler):
383
384
 
384
385
  @override
385
386
  def __init__(
386
- self, *, level: int = NOTSET, path: PathLikeOrCallable | None = None
387
+ self, *, level: int = NOTSET, path: MaybeCallablePathLike | None = None
387
388
  ) -> None:
388
389
  super().__init__(level=level)
389
- self._path = path
390
+ self._path = get_path(path=path)
390
391
 
391
392
  @override
392
393
  def emit(self, record: LogRecord) -> None:
@@ -394,10 +395,8 @@ class StandaloneFileHandler(Handler):
394
395
  from utilities.tzlocal import get_now_local
395
396
 
396
397
  try:
397
- path = (
398
- resolve_path(path=self._path)
399
- .joinpath(serialize_compact(get_now_local()))
400
- .with_suffix(".txt")
398
+ path = self._path.joinpath(serialize_compact(get_now_local())).with_suffix(
399
+ ".txt"
401
400
  )
402
401
  formatted = self.format(record)
403
402
  with writer(path, overwrite=True) as temp, temp.open(mode="w") as fh:
@@ -420,16 +419,53 @@ def add_filters(handler: Handler, /, *filters: _FilterType) -> None:
420
419
 
421
420
  def basic_config(
422
421
  *,
423
- format: str = "{asctime} | {name} | {levelname:8} | {message}", # noqa: A002
422
+ obj: LoggerOrName | Handler | None = None,
423
+ format_: str = "{asctime} | {name} | {levelname:8} | {message}",
424
+ whenever: bool = False,
424
425
  level: LogLevel = "INFO",
426
+ plain: bool = False,
425
427
  ) -> None:
426
428
  """Do the basic config."""
427
- basicConfig(
428
- format=format,
429
- datefmt=maybe_sub_pct_y("%Y-%m-%d %H:%M:%S"),
430
- style="{",
431
- level=level,
432
- )
429
+ if whenever:
430
+ format_ = format_.replace("{asctime}", "{zoned_datetime}")
431
+ datefmt = maybe_sub_pct_y("%Y-%m-%d %H:%M:%S")
432
+ match obj:
433
+ case None:
434
+ basicConfig(format=format_, datefmt=datefmt, style="{", level=level)
435
+ case Logger() as logger:
436
+ logger.setLevel(level)
437
+ logger.addHandler(handler := StreamHandler())
438
+ basic_config(
439
+ obj=handler,
440
+ format_=format_,
441
+ whenever=whenever,
442
+ level=level,
443
+ plain=plain,
444
+ )
445
+ case str() as name:
446
+ basic_config(
447
+ obj=get_logger(logger=name),
448
+ format_=format_,
449
+ whenever=whenever,
450
+ level=level,
451
+ plain=plain,
452
+ )
453
+ case Handler() as handler:
454
+ handler.setLevel(level)
455
+ if plain:
456
+ formatter = Formatter(fmt=format_, datefmt=datefmt, style="{")
457
+ else:
458
+ try:
459
+ from coloredlogs import ColoredFormatter
460
+ except ModuleNotFoundError: # pragma: no cover
461
+ formatter = Formatter(fmt=format_, datefmt=datefmt, style="{")
462
+ else:
463
+ formatter = ColoredFormatter(
464
+ fmt=format_, datefmt=datefmt, style="{"
465
+ )
466
+ handler.setFormatter(formatter)
467
+ case _ as never:
468
+ assert_never(never)
433
469
 
434
470
 
435
471
  ##
@@ -473,7 +509,7 @@ class FilterForKeyError(Exception):
473
509
 
474
510
  def get_default_logging_path() -> Path:
475
511
  """Get the logging default path."""
476
- return get_repo_root().joinpath(".logs")
512
+ return get_root().joinpath(".logs")
477
513
 
478
514
 
479
515
  ##
@@ -520,11 +556,11 @@ def setup_logging(
520
556
  console_level: LogLevel | None = "INFO",
521
557
  console_filters: Iterable[_FilterType] | None = None,
522
558
  console_fmt: str = "❯ {_zoned_datetime_str} | {name}:{funcName}:{lineno} | {message}", # noqa: RUF001
523
- files_dir: PathLikeOrCallable | None = get_default_logging_path,
524
- files_when: _When = "D",
559
+ files_dir: MaybeCallablePathLike | None = get_default_logging_path,
560
+ files_when: _When = _WHEN,
525
561
  files_interval: int = 1,
526
- files_backup_count: int = 10,
527
- files_max_bytes: int = 10 * 1024**2,
562
+ files_backup_count: int = _BACKUP_COUNT,
563
+ files_max_bytes: int = _MAX_BYTES,
528
564
  files_filters: Iterable[_FilterType] | None = None,
529
565
  files_fmt: str = "{_zoned_datetime_str} | {name}:{funcName}:{lineno} | {levelname:8} | {message}",
530
566
  filters: MaybeIterable[_FilterType] | None = None,
@@ -616,7 +652,7 @@ def setup_logging(
616
652
  logger_use.addHandler(console_high_and_exc_handler)
617
653
 
618
654
  # debug & info
619
- directory = resolve_path(path=files_dir) # skipif-ci-and-windows
655
+ directory = get_path(path=files_dir) # skipif-ci-and-windows
620
656
  levels: list[LogLevel] = ["DEBUG", "INFO"] # skipif-ci-and-windows
621
657
  for level, (subpath, files_or_plain_formatter) in product( # skipif-ci-and-windows
622
658
  levels, [(Path(), files_formatter), (Path("plain"), plain_formatter)]
@@ -754,29 +790,6 @@ class _AdvancedLogRecord(LogRecord):
754
790
  cls.time_zone = time_zone.key # skipif-ci-and-windows
755
791
  super().__init_subclass__(**kwargs) # skipif-ci-and-windows
756
792
 
757
- @override
758
- def getMessage(self) -> str:
759
- """Return the message for this LogRecord."""
760
- msg = str(self.msg) # pragma: no cover
761
- if self.args: # pragma: no cover
762
- try:
763
- return msg % self.args # compability for 3rd party code
764
- except ValueError as error:
765
- if len(error.args) == 0:
766
- raise
767
- first = error.args[0]
768
- if search("unsupported format character", first):
769
- return msg.format(*self.args)
770
- raise
771
- except TypeError as error:
772
- if len(error.args) == 0:
773
- raise
774
- first = error.args[0]
775
- if search("not all arguments converted", first):
776
- return msg.format(*self.args)
777
- raise
778
- return msg # pragma: no cover
779
-
780
793
  @classmethod
781
794
  def get_now(cls) -> Any:
782
795
  """Get the current zoned datetime."""
utilities/pathlib.py CHANGED
@@ -1,15 +1,22 @@
1
1
  from __future__ import annotations
2
2
 
3
- from contextlib import contextmanager
3
+ from collections.abc import Callable
4
+ from contextlib import contextmanager, suppress
5
+ from dataclasses import dataclass
4
6
  from itertools import chain
5
7
  from os import chdir
8
+ from os.path import expandvars
6
9
  from pathlib import Path
7
- from typing import TYPE_CHECKING
10
+ from re import IGNORECASE, search
11
+ from subprocess import PIPE, CalledProcessError, check_output
12
+ from typing import TYPE_CHECKING, assert_never, overload, override
13
+
14
+ from utilities.sentinel import Sentinel, sentinel
8
15
 
9
16
  if TYPE_CHECKING:
10
17
  from collections.abc import Iterator, Sequence
11
18
 
12
- from utilities.types import PathLike, PathLikeOrCallable
19
+ from utilities.types import MaybeCallablePathLike, PathLike
13
20
 
14
21
  PWD = Path.cwd()
15
22
 
@@ -25,20 +32,83 @@ def ensure_suffix(path: PathLike, suffix: str, /) -> Path:
25
32
  return path.with_name(name)
26
33
 
27
34
 
28
- def list_dir(path: PathLike, /) -> Sequence[Path]:
29
- """List the contents of a directory."""
30
- return sorted(Path(path).iterdir())
35
+ ##
36
+
37
+
38
+ def expand_path(path: PathLike, /) -> Path:
39
+ """Expand a path."""
40
+ path = str(path)
41
+ path = expandvars(path)
42
+ return Path(path).expanduser()
43
+
31
44
 
45
+ ##
32
46
 
33
- def resolve_path(*, path: PathLikeOrCallable | None = None) -> Path:
34
- """Resolve for a path."""
47
+
48
+ @overload
49
+ def get_path(*, path: MaybeCallablePathLike | None) -> Path: ...
50
+ @overload
51
+ def get_path(*, path: Sentinel) -> Sentinel: ...
52
+ def get_path(
53
+ *, path: MaybeCallablePathLike | None | Sentinel = sentinel
54
+ ) -> Path | None | Sentinel:
55
+ """Get the path."""
35
56
  match path:
57
+ case Path() | Sentinel():
58
+ return path
59
+ case str():
60
+ return Path(path)
36
61
  case None:
37
62
  return Path.cwd()
38
- case Path() | str():
39
- return Path(path)
40
- case _:
41
- return Path(path())
63
+ case Callable() as func:
64
+ return get_path(path=func())
65
+ case _ as never:
66
+ assert_never(never)
67
+
68
+
69
+ ##
70
+
71
+
72
+ def get_root(*, path: MaybeCallablePathLike | None = None) -> Path:
73
+ """Get the root of a path."""
74
+ path = get_path(path=path)
75
+ try:
76
+ output = check_output(
77
+ ["git", "rev-parse", "--show-toplevel"], stderr=PIPE, cwd=path, text=True
78
+ )
79
+ except CalledProcessError as error:
80
+ # newer versions of git report "Not a git repository", whilst older
81
+ # versions report "not a git repository"
82
+ if not search("fatal: not a git repository", error.stderr, flags=IGNORECASE):
83
+ raise # pragma: no cover
84
+ else:
85
+ return Path(output.strip("\n"))
86
+ all_paths = list(chain([path], path.parents))
87
+ with suppress(StopIteration):
88
+ return next(
89
+ p for p in all_paths if any(p_i.name == ".envrc" for p_i in p.iterdir())
90
+ )
91
+ raise GetRootError(path=path)
92
+
93
+
94
+ @dataclass(kw_only=True, slots=True)
95
+ class GetRootError(Exception):
96
+ path: PathLike
97
+
98
+ @override
99
+ def __str__(self) -> str:
100
+ return f"Unable to determine root from {str(self.path)!r}"
101
+
102
+
103
+ ##
104
+
105
+
106
+ def list_dir(path: PathLike, /) -> Sequence[Path]:
107
+ """List the contents of a directory."""
108
+ return sorted(Path(path).iterdir())
109
+
110
+
111
+ ##
42
112
 
43
113
 
44
114
  @contextmanager
@@ -52,4 +122,4 @@ def temp_cwd(path: PathLike, /) -> Iterator[None]:
52
122
  chdir(prev)
53
123
 
54
124
 
55
- __all__ = ["ensure_suffix", "list_dir", "resolve_path", "temp_cwd"]
125
+ __all__ = ["PWD", "ensure_suffix", "expand_path", "get_path", "list_dir", "temp_cwd"]
utilities/pyinstrument.py CHANGED
@@ -7,23 +7,25 @@ from typing import TYPE_CHECKING
7
7
  from pyinstrument.profiler import Profiler
8
8
 
9
9
  from utilities.datetime import serialize_compact
10
- from utilities.pathlib import PWD
10
+ from utilities.pathlib import get_path
11
11
  from utilities.tzlocal import get_now_local
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from collections.abc import Iterator
15
15
 
16
- from utilities.types import PathLike
16
+ from utilities.types import MaybeCallablePathLike
17
17
 
18
18
 
19
19
  @contextmanager
20
- def profile(*, path: PathLike = PWD) -> Iterator[None]:
20
+ def profile(*, path: MaybeCallablePathLike | None = Path.cwd) -> Iterator[None]:
21
21
  """Profile the contents of a block."""
22
22
  from utilities.atomicwrites import writer
23
23
 
24
24
  with Profiler() as profiler:
25
25
  yield
26
- filename = Path(path, f"profile__{serialize_compact(get_now_local())}.html")
26
+ filename = get_path(path=path).joinpath(
27
+ f"profile__{serialize_compact(get_now_local())}.html"
28
+ )
27
29
  with writer(filename) as temp, temp.open(mode="w") as fh:
28
30
  _ = fh.write(profiler.output_html())
29
31
 
@@ -10,8 +10,8 @@ from pytest import fixture
10
10
  from pytest_regressions.file_regression import FileRegressionFixture
11
11
 
12
12
  from utilities.functions import ensure_str
13
- from utilities.git import get_repo_root
14
13
  from utilities.operator import is_equal
14
+ from utilities.pathlib import get_root
15
15
  from utilities.pytest import node_id_to_path
16
16
 
17
17
  if TYPE_CHECKING:
@@ -153,7 +153,7 @@ def polars_regression(
153
153
 
154
154
  def _get_path(request: FixtureRequest, /) -> Path:
155
155
  tail = node_id_to_path(request.node.nodeid, head=_PATH_TESTS)
156
- return get_repo_root().joinpath(_PATH_TESTS, "regressions", tail)
156
+ return get_root().joinpath(_PATH_TESTS, "regressions", tail)
157
157
 
158
158
 
159
159
  __all__ = [
@@ -2,29 +2,33 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
4
  from os import environ
5
+ from pathlib import Path
5
6
  from typing import TYPE_CHECKING, override
6
7
 
7
8
  from dotenv import dotenv_values
8
9
 
9
10
  from utilities.dataclasses import _ParseDataClassMissingValuesError, parse_dataclass
10
- from utilities.git import get_repo_root
11
11
  from utilities.iterables import MergeStrMappingsError, merge_str_mappings
12
- from utilities.pathlib import PWD
12
+ from utilities.pathlib import get_root
13
13
  from utilities.reprlib import get_repr
14
14
 
15
15
  if TYPE_CHECKING:
16
16
  from collections.abc import Mapping
17
17
  from collections.abc import Set as AbstractSet
18
- from pathlib import Path
19
18
 
20
- from utilities.types import ParseObjectExtra, PathLike, StrMapping, TDataclass
19
+ from utilities.types import (
20
+ MaybeCallablePathLike,
21
+ ParseObjectExtra,
22
+ StrMapping,
23
+ TDataclass,
24
+ )
21
25
 
22
26
 
23
27
  def load_settings(
24
28
  cls: type[TDataclass],
25
29
  /,
26
30
  *,
27
- cwd: PathLike = PWD,
31
+ path: MaybeCallablePathLike | None = Path.cwd,
28
32
  globalns: StrMapping | None = None,
29
33
  localns: StrMapping | None = None,
30
34
  warn_name_errors: bool = False,
@@ -33,7 +37,7 @@ def load_settings(
33
37
  extra_parsers: ParseObjectExtra | None = None,
34
38
  ) -> TDataclass:
35
39
  """Load a set of settings from the `.env` file."""
36
- path = get_repo_root(cwd=cwd).joinpath(".env")
40
+ path = get_root(path=path).joinpath(".env")
37
41
  if not path.exists():
38
42
  raise _LoadSettingsFileNotFoundError(path=path) from None
39
43
  maybe_values_dotenv = dotenv_values(path)