From 581ae050d13113fd2453a1ac3c76fbbb3798fb9e Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 10:59:52 +0900 Subject: [PATCH 01/35] 2026-01-17 10:59:52 (Sat) > DW-Mac > derekwan --- src/utilities/atomicwrites.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/utilities/atomicwrites.py b/src/utilities/atomicwrites.py index 7b6fcf7a2..1159f349d 100644 --- a/src/utilities/atomicwrites.py +++ b/src/utilities/atomicwrites.py @@ -26,13 +26,15 @@ def copy(src: PathLike, dest: PathLike, /, *, overwrite: bool = False) -> None: src, dest = map(Path, [src, dest]) match file_or_dir(src): case "file": - with TemporaryFile(data=src.read_bytes()) as temp: + with TemporaryFile( + suffix=".tmp", prefix=name, dir=parent, data=src.read_bytes() + ) as temp: try: move(temp, dest, overwrite=overwrite) except _MoveFileExistsError as error: raise _CopyFileExistsError(src=error.src, dest=error.dest) from None case "dir": - with TemporaryDirectory() as temp: + with TemporaryDirectory(suffix=".tmp", prefix=name, dir=parent) as temp: temp_sub_dir = temp / "sub_dir" _ = copytree(src, temp_sub_dir) try: From 0d56ca0133fdc94d213495a8686fe690029425a1 Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 11:20:03 +0900 Subject: [PATCH 02/35] 2026-01-17 11:20:03 (Sat) > DW-Mac > derekwan --- .bumpversion.toml | 2 +- pyproject.toml | 2 +- src/utilities/__init__.py | 2 +- src/utilities/atomicwrites.py | 7 ++++--- uv.lock | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.bumpversion.toml b/.bumpversion.toml index 9a53bc766..a75c0aa69 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,6 +1,6 @@ [tool.bumpversion] allow_dirty = true - current_version = "0.183.5" + current_version = "0.182.8" [[tool.bumpversion.files]] filename = "pyproject.toml" diff --git a/pyproject.toml b/pyproject.toml index ecf6c3693..6b32b84a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,7 +116,7 @@ name = "dycw-utilities" readme = "README.md" requires-python = ">= 3.12" - version = "0.183.5" + version = "0.182.8" [project.entry-points.pytest11] pytest-randomly = "utilities.pytest_plugins.pytest_randomly" diff --git a/src/utilities/__init__.py b/src/utilities/__init__.py index b74045cbb..e2bca4148 100644 --- a/src/utilities/__init__.py +++ b/src/utilities/__init__.py @@ -1,3 +1,3 @@ from __future__ import annotations -__version__ = "0.183.5" +__version__ = "0.182.8" diff --git a/src/utilities/atomicwrites.py b/src/utilities/atomicwrites.py index 1159f349d..bd51f4a68 100644 --- a/src/utilities/atomicwrites.py +++ b/src/utilities/atomicwrites.py @@ -24,10 +24,12 @@ def copy(src: PathLike, dest: PathLike, /, *, overwrite: bool = False) -> None: """Copy a file/directory atomically.""" src, dest = map(Path, [src, dest]) + name, parent = dest.name, dest.parent + parent.mkdir(parents=True, exist_ok=True) match file_or_dir(src): case "file": with TemporaryFile( - suffix=".tmp", prefix=name, dir=parent, data=src.read_bytes() + dir=parent, suffix=".tmp", prefix=name, data=src.read_bytes() ) as temp: try: move(temp, dest, overwrite=overwrite) @@ -164,9 +166,8 @@ def writer( ) -> Iterator[Path]: """Yield a path for atomically writing files to disk.""" path = Path(path) - parent = path.parent + name, parent = path.name, path.parent parent.mkdir(parents=True, exist_ok=True) - name = path.name with TemporaryDirectory(suffix=".tmp", prefix=name, dir=parent) as temp_dir: temp_path1 = Path(temp_dir, name) try: diff --git a/uv.lock b/uv.lock index aff2becfd..0c9cc685e 100644 --- a/uv.lock +++ b/uv.lock @@ -625,7 +625,7 @@ wheels = [ [[package]] name = "dycw-utilities" -version = "0.183.5" +version = "0.182.8" source = { editable = "." } dependencies = [ { name = "atomicwrites" }, From d24414ce3cc9ab99b153cdeb4617eb8d2d872ac2 Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 12:22:30 +0900 Subject: [PATCH 03/35] 2026-01-17 12:22:30 (Sat) > DW-Mac > derekwan --- src/tests/test_tempfile.py | 11 +---- src/utilities/atomicwrites.py | 90 +++++++++++++++++++++++++---------- src/utilities/os.py | 62 +++++++++++++++++++++++- src/utilities/tempfile.py | 23 ++++++++- 4 files changed, 148 insertions(+), 38 deletions(-) diff --git a/src/tests/test_tempfile.py b/src/tests/test_tempfile.py index 8796ea39c..238e97453 100644 --- a/src/tests/test_tempfile.py +++ b/src/tests/test_tempfile.py @@ -91,14 +91,5 @@ def test_text(self) -> None: class TestYieldTempAt: - def test_dir(self, *, temp_path_not_exist: Path) -> None: - with yield_temp_dir_at(temp_path_not_exist) as temp: - assert temp.is_dir() - assert temp.parent == temp_path_not_exist.parent - assert temp.name.startswith(temp_path_not_exist.name) - def test_file(self, *, temp_path_not_exist: Path) -> None: - with yield_temp_file_at(temp_path_not_exist) as temp: - assert temp.is_file() - assert temp.parent == temp_path_not_exist.parent - assert temp.name.startswith(temp_path_not_exist.name) + a diff --git a/src/utilities/atomicwrites.py b/src/utilities/atomicwrites.py index bd51f4a68..14337a701 100644 --- a/src/utilities/atomicwrites.py +++ b/src/utilities/atomicwrites.py @@ -4,9 +4,10 @@ import shutil from contextlib import ExitStack from dataclasses import dataclass +from os import rename from pathlib import Path -from shutil import copyfileobj, copytree, rmtree -from typing import TYPE_CHECKING, assert_never, override +from shutil import copyfile, copyfileobj, copytree, rmtree +from typing import TYPE_CHECKING, Literal, assert_never, override from atomicwrites import replace_atomic @@ -24,19 +25,15 @@ def copy(src: PathLike, dest: PathLike, /, *, overwrite: bool = False) -> None: """Copy a file/directory atomically.""" src, dest = map(Path, [src, dest]) - name, parent = dest.name, dest.parent - parent.mkdir(parents=True, exist_ok=True) match file_or_dir(src): case "file": - with TemporaryFile( - dir=parent, suffix=".tmp", prefix=name, data=src.read_bytes() - ) as temp: + with TemporaryFile(data=src.read_bytes()) as temp: try: move(temp, dest, overwrite=overwrite) except _MoveFileExistsError as error: raise _CopyFileExistsError(src=error.src, dest=error.dest) from None case "dir": - with TemporaryDirectory(suffix=".tmp", prefix=name, dir=parent) as temp: + with TemporaryDirectory() as temp: temp_sub_dir = temp / "sub_dir" _ = copytree(src, temp_sub_dir) try: @@ -112,38 +109,80 @@ def move(src: PathLike, dest: PathLike, /, *, overwrite: bool = False) -> None: assert_never(never) +def _move_or_copy( + src: PathLike, + dest: PathLike, + /, + *, + overwrite: bool = False, + delete_src: bool = False, +) -> None: + src, dest = map(Path, [src, dest]) + match file_or_dir(src): + case None: + raise _MoveOrCopySourceNotFoundError(src=src) + case "file": + _move_or_copy_file(src, dest, overwrite=overwrite, delete_src=delete_src) + case "dir": + _move_or_copy_dir(src, dest, overwrite=overwrite, delete_src=delete_src) + case never: + assert_never(never) + + +def _move_or_copy_file( + src: PathLike, + dest: PathLike, + /, + *, + overwrite: bool = False, + delete_src: bool = False, +) -> None: + src, dest = map(Path, [src, dest]) + dir_, name = dest.parent, dest.name + match file_or_dir(dest), overwrite: + case None, _: + dir_ = dest.parent + dir_.mkdir(parents=True, exist_ok=True) + name = dest.name + with TemporaryFile(dir=dir_, suffix=".tmp", prefix=name) as temp: + _ = shutil.copyfile(name, temp) + case "file" | "dir", False: + raise _MoveOrCopyFileExistsError(src=src, dest=dest) + case "file", True: + dir_ = dest.parent + dir_.mkdir(parents=True, exist_ok=True) + name = dest.name + with TemporaryFile(dir=dir_, suffix=".tmp", prefix=name) as temp: + _ = shutil.copyfile(name, temp) + case "dir", True: + rmtree(dest) + with TemporaryFile(dir=dir_, suffix=".tmp", prefix=name) as temp: + _ = shutil.copyfile(name, temp) + _ = temp.replace(dest) + case never: + assert_never(never) + + @dataclass(kw_only=True, slots=True) -class MoveError(Exception): ... +class _MoveOrCopyError(Exception): ... @dataclass(kw_only=True, slots=True) -class _MoveSourceNotFoundError(MoveError): +class _MoveOrCopySourceNotFoundError(_MoveOrCopyError): src: Path - @override - def __str__(self) -> str: - return f"Source {str(self.src)!r} does not exist" - @dataclass(kw_only=True, slots=True) -class _MoveFileExistsError(MoveError): +class _MoveOrCopyFileExistsError(_MoveOrCopyError): src: Path dest: Path - @override - def __str__(self) -> str: - return f"Cannot move file {str(self.src)!r} as destination {str(self.dest)!r} already exists" - @dataclass(kw_only=True, slots=True) -class _MoveDirectoryExistsError(MoveError): +class _MoveOrCopyDirectoryExistsError(_MoveOrCopyError): src: Path dest: Path - @override - def __str__(self) -> str: - return f"Cannot move directory {str(self.src)!r} as destination {str(self.dest)!r} already exists" - ## @@ -166,8 +205,9 @@ def writer( ) -> Iterator[Path]: """Yield a path for atomically writing files to disk.""" path = Path(path) - name, parent = path.name, path.parent + parent = path.parent parent.mkdir(parents=True, exist_ok=True) + name = path.name with TemporaryDirectory(suffix=".tmp", prefix=name, dir=parent) as temp_dir: temp_path1 = Path(temp_dir, name) try: diff --git a/src/utilities/os.py b/src/utilities/os.py index 3a5d05978..7023f6e49 100644 --- a/src/utilities/os.py +++ b/src/utilities/os.py @@ -2,17 +2,75 @@ from contextlib import suppress from dataclasses import dataclass -from os import environ, getenv +from os import cpu_count, environ, getenv, replace +from pathlib import Path from typing import TYPE_CHECKING, Literal, assert_never, overload, override from utilities.constants import CPU_COUNT from utilities.contextlib import enhanced_context_manager from utilities.iterables import OneStrEmptyError, one_str +from utilities.pathlib import file_or_dir +from utilities.platform import SYSTEM if TYPE_CHECKING: from collections.abc import Iterator, Mapping - from utilities.types import IntOrAll + from utilities.types import PathLike + + +type IntOrAll = int | Literal["all"] + + +## + + +def move(src: PathLike, dest: PathLike, /, *, overwrite: bool = False) -> None: + """Move/replace a file/directory atomically.""" + src, dest = map(Path, [src, dest]) + match file_or_dir(src), file_or_dir(dest), overwrite: + case None, _, _: + raise _MoveSourceNotFoundError(src=src) + case "file", "file" | "dir", False: + raise _MoveFileExistsError(src=src, dest=dest) from None + case "file", "dir", _: + rmtree(dest, ignore_errors=True) + replace_atomic(str(src), str(dest)) # must be `str`s + case "file", _, _: + replace_atomic(str(src), str(dest)) # must be `str`s + case "dir", "file" | "dir", False: + raise _MoveDirectoryExistsError(src=src, dest=dest) + case "dir", "dir", _: + rmtree(dest, ignore_errors=True) + _ = shutil.move(src, dest) + case "dir", _, _: + dest.unlink(missing_ok=True) + _ = shutil.move(src, dest) + case never: + assert_never(never) + + +## + + +def get_cpu_count() -> int: + """Get the CPU count.""" + count = cpu_count() + if count is None: # pragma: no cover + raise GetCPUCountError + return count + + +@dataclass(kw_only=True, slots=True) +class GetCPUCountError(Exception): + @override + def __str__(self) -> str: + return "CPU count must not be None" # pragma: no cover + + +CPU_COUNT = get_cpu_count() + + +## def get_cpu_use(*, n: IntOrAll = "all") -> int: diff --git a/src/utilities/tempfile.py b/src/utilities/tempfile.py index 4333f864b..e5f5e14d8 100644 --- a/src/utilities/tempfile.py +++ b/src/utilities/tempfile.py @@ -3,7 +3,7 @@ import tempfile from contextlib import contextmanager from pathlib import Path -from shutil import move +from shutil import copyfile, move from tempfile import NamedTemporaryFile as _NamedTemporaryFile from typing import TYPE_CHECKING, override from warnings import catch_warnings, filterwarnings @@ -179,9 +179,30 @@ def yield_temp_file_at(path: PathLike, /) -> Iterator[Path]: yield temp +## + + +def yield_temp_dir_at(path: PathLike, /) -> Iterator[Path]: + """Yield a temporary dir for a target path.""" + + path = Path(path) + with TemporaryDirectory(suffix=".tmp", prefix=path.name, dir=path.parent) as temp: + yield temp + + +def yield_temp_file_at(path: PathLike, /) -> Iterator[Path]: + """Yield a temporary file for a target path.""" + + path = Path(path) + with TemporaryFile(dir=path.parent, suffix=".tmp", prefix=path.name) as temp: + yield temp + + __all__ = [ + "TEMP_DIR", "TemporaryDirectory", "TemporaryFile", + "gettempdir", "yield_temp_dir_at", "yield_temp_file_at", ] From 168e3b815212e51ae14070fe0ae124c4376452de Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 12:44:50 +0900 Subject: [PATCH 04/35] 2026-01-17 12:44:50 (Sat) > DW-Mac > derekwan --- src/tests/test_tempfile.py | 16 ++- src/utilities/atomicwrites.py | 111 +-------------------- src/utilities/os.py | 179 ++++++++++++++++++++++++++++++---- src/utilities/tempfile.py | 2 +- 4 files changed, 175 insertions(+), 133 deletions(-) diff --git a/src/tests/test_tempfile.py b/src/tests/test_tempfile.py index 238e97453..4a25c24ea 100644 --- a/src/tests/test_tempfile.py +++ b/src/tests/test_tempfile.py @@ -3,13 +3,24 @@ from pathlib import Path from utilities.tempfile import ( + TEMP_DIR, TemporaryDirectory, TemporaryFile, - yield_temp_dir_at, + gettempdir, yield_temp_file_at, ) +class TestGetTempDir: + def test_main(self) -> None: + assert isinstance(gettempdir(), Path) + + +class TestTempDir: + def test_main(self) -> None: + assert isinstance(TEMP_DIR, Path) + + class TestTemporaryDirectory: def test_main(self) -> None: temp_dir = TemporaryDirectory() @@ -92,4 +103,5 @@ def test_text(self) -> None: class TestYieldTempAt: def test_file(self, *, temp_path_not_exist: Path) -> None: - a + with yield_temp_file_at(temp_path_not_exist): + assert 0, 1 diff --git a/src/utilities/atomicwrites.py b/src/utilities/atomicwrites.py index 14337a701..21e861f2e 100644 --- a/src/utilities/atomicwrites.py +++ b/src/utilities/atomicwrites.py @@ -1,15 +1,11 @@ from __future__ import annotations import gzip -import shutil from contextlib import ExitStack from dataclasses import dataclass -from os import rename from pathlib import Path -from shutil import copyfile, copyfileobj, copytree, rmtree -from typing import TYPE_CHECKING, Literal, assert_never, override - -from atomicwrites import replace_atomic +from shutil import copyfileobj, copytree, rmtree +from typing import TYPE_CHECKING, assert_never, override from utilities.contextlib import enhanced_context_manager from utilities.iterables import transpose @@ -84,109 +80,6 @@ def __str__(self) -> str: ## -def move(src: PathLike, dest: PathLike, /, *, overwrite: bool = False) -> None: - """Move/replace a file/directory atomically.""" - src, dest = map(Path, [src, dest]) - match file_or_dir(src), file_or_dir(dest), overwrite: - case None, _, _: - raise _MoveSourceNotFoundError(src=src) - case "file", "file" | "dir", False: - raise _MoveFileExistsError(src=src, dest=dest) from None - case "file", "dir", _: - rmtree(dest, ignore_errors=True) - replace_atomic(str(src), str(dest)) # must be `str`s - case "file", _, _: - replace_atomic(str(src), str(dest)) # must be `str`s - case "dir", "file" | "dir", False: - raise _MoveDirectoryExistsError(src=src, dest=dest) - case "dir", "dir", _: - rmtree(dest, ignore_errors=True) - _ = shutil.move(src, dest) - case "dir", _, _: - dest.unlink(missing_ok=True) - _ = shutil.move(src, dest) - case never: - assert_never(never) - - -def _move_or_copy( - src: PathLike, - dest: PathLike, - /, - *, - overwrite: bool = False, - delete_src: bool = False, -) -> None: - src, dest = map(Path, [src, dest]) - match file_or_dir(src): - case None: - raise _MoveOrCopySourceNotFoundError(src=src) - case "file": - _move_or_copy_file(src, dest, overwrite=overwrite, delete_src=delete_src) - case "dir": - _move_or_copy_dir(src, dest, overwrite=overwrite, delete_src=delete_src) - case never: - assert_never(never) - - -def _move_or_copy_file( - src: PathLike, - dest: PathLike, - /, - *, - overwrite: bool = False, - delete_src: bool = False, -) -> None: - src, dest = map(Path, [src, dest]) - dir_, name = dest.parent, dest.name - match file_or_dir(dest), overwrite: - case None, _: - dir_ = dest.parent - dir_.mkdir(parents=True, exist_ok=True) - name = dest.name - with TemporaryFile(dir=dir_, suffix=".tmp", prefix=name) as temp: - _ = shutil.copyfile(name, temp) - case "file" | "dir", False: - raise _MoveOrCopyFileExistsError(src=src, dest=dest) - case "file", True: - dir_ = dest.parent - dir_.mkdir(parents=True, exist_ok=True) - name = dest.name - with TemporaryFile(dir=dir_, suffix=".tmp", prefix=name) as temp: - _ = shutil.copyfile(name, temp) - case "dir", True: - rmtree(dest) - with TemporaryFile(dir=dir_, suffix=".tmp", prefix=name) as temp: - _ = shutil.copyfile(name, temp) - _ = temp.replace(dest) - case never: - assert_never(never) - - -@dataclass(kw_only=True, slots=True) -class _MoveOrCopyError(Exception): ... - - -@dataclass(kw_only=True, slots=True) -class _MoveOrCopySourceNotFoundError(_MoveOrCopyError): - src: Path - - -@dataclass(kw_only=True, slots=True) -class _MoveOrCopyFileExistsError(_MoveOrCopyError): - src: Path - dest: Path - - -@dataclass(kw_only=True, slots=True) -class _MoveOrCopyDirectoryExistsError(_MoveOrCopyError): - src: Path - dest: Path - - -## - - def move_many(*paths: tuple[PathLike, PathLike], overwrite: bool = False) -> None: """Move a set of files concurrently.""" srcs, dests = transpose(paths) diff --git a/src/utilities/os.py b/src/utilities/os.py index 7023f6e49..cf726132e 100644 --- a/src/utilities/os.py +++ b/src/utilities/os.py @@ -1,15 +1,17 @@ from __future__ import annotations +import shutil from contextlib import suppress from dataclasses import dataclass from os import cpu_count, environ, getenv, replace from pathlib import Path +from shutil import rmtree +from tempfile import TemporaryDirectory, TemporaryFile from typing import TYPE_CHECKING, Literal, assert_never, overload, override from utilities.constants import CPU_COUNT from utilities.contextlib import enhanced_context_manager from utilities.iterables import OneStrEmptyError, one_str -from utilities.pathlib import file_or_dir from utilities.platform import SYSTEM if TYPE_CHECKING: @@ -24,29 +26,154 @@ ## +def copy(src: PathLike, dest: PathLike, /, *, overwrite: bool = False) -> None: + """Copy/replace a file/directory atomically.""" + try: + _move_or_copy(src, dest, overwrite=overwrite, delete_src=False) + except _MoveOrCopySourceNotFoundError as error: + raise _CopySourceNotFoundError(src=error.src) from None + except _MoveOrCopyDestinationExistsError as error: + raise _CopyDestinationExistsError(src=error.src, dest=error.dest) from None + + +@dataclass(kw_only=True, slots=True) +class CopyError(Exception): ... + + +@dataclass(kw_only=True, slots=True) +class _CopySourceNotFoundError(CopyError): + src: Path + + @override + def __str__(self) -> str: + return f"Source {str(self.src)!r} does not exist" + + +@dataclass(kw_only=True, slots=True) +class _CopyDestinationExistsError(CopyError): + src: Path + dest: Path + + @override + def __str__(self) -> str: + return f"Cannot copy {str(self.src)!r} as destination {str(self.dest)!r} already exists" + + def move(src: PathLike, dest: PathLike, /, *, overwrite: bool = False) -> None: """Move/replace a file/directory atomically.""" + try: + _move_or_copy(src, dest, overwrite=overwrite, delete_src=True) + except _MoveOrCopySourceNotFoundError as error: + raise _MoveSourceNotFoundError(src=error.src) from None + except _MoveOrCopyDestinationExistsError as error: + raise _MoveDestinationExistsError(src=error.src, dest=error.dest) from None + + +@dataclass(kw_only=True, slots=True) +class MoveError(Exception): ... + + +@dataclass(kw_only=True, slots=True) +class _MoveSourceNotFoundError(MoveError): + src: Path + + @override + def __str__(self) -> str: + return f"Source {str(self.src)!r} does not exist" + + +@dataclass(kw_only=True, slots=True) +class _MoveDestinationExistsError(MoveError): + src: Path + dest: Path + + @override + def __str__(self) -> str: + return f"Cannot move {str(self.src)!r} as destination {str(self.dest)!r} already exists" + + +def _move_or_copy( + src: PathLike, + dest: PathLike, + /, + *, + overwrite: bool = False, + delete_src: bool = False, +) -> None: src, dest = map(Path, [src, dest]) - match file_or_dir(src), file_or_dir(dest), overwrite: - case None, _, _: - raise _MoveSourceNotFoundError(src=src) - case "file", "file" | "dir", False: - raise _MoveFileExistsError(src=src, dest=dest) from None - case "file", "dir", _: - rmtree(dest, ignore_errors=True) - replace_atomic(str(src), str(dest)) # must be `str`s - case "file", _, _: - replace_atomic(str(src), str(dest)) # must be `str`s - case "dir", "file" | "dir", False: - raise _MoveDirectoryExistsError(src=src, dest=dest) - case "dir", "dir", _: - rmtree(dest, ignore_errors=True) - _ = shutil.move(src, dest) - case "dir", _, _: - dest.unlink(missing_ok=True) - _ = shutil.move(src, dest) - case never: - assert_never(never) + if not src.exists(): + raise _MoveOrCopySourceNotFoundError(src=src) + if dest.exists() and not overwrite: + raise _MoveOrCopyDestinationExistsError(src=src, dest=dest) + if src.is_file(): + _move_or_copy_file(src, dest, overwrite=overwrite, delete_src=delete_src) + elif src.is_dir(): + _move_or_copy_dir(src, dest, overwrite=overwrite, delete_src=delete_src) + else: # pragma: no cover + raise TypeError(src) + + +def _move_or_copy_file( + src: PathLike, + dest: PathLike, + /, + *, + overwrite: bool = False, + delete_src: bool = False, +) -> None: + src, dest = map(Path, [src, dest]) + name, dir_ = dest.name, dest.parent + if (not dest.exists()) or (dest.is_file() and overwrite): + ... + elif dest.is_dir() and overwrite: + rmtree(dest, ignore_errors=True) + else: # pragma: no cover + raise RuntimeError(dest, overwrite) + with TemporaryDirectory(suffix=".tmp", prefix=name, dir=dir_) as temp_dir: + temp_file = Path(temp_dir, src.name) + _ = shutil.copyfile(src, temp_file) + _ = temp_file.replace(dest) + if delete_src: + src.unlink(missing_ok=True) + + +def _move_or_copy_dir( + src: PathLike, + dest: PathLike, + /, + *, + overwrite: bool = False, + delete_src: bool = False, +) -> None: + src, dest = map(Path, [src, dest]) + name, dir_ = dest.name, dest.parent + if (not dest.exists()) or (dest.is_dir() and overwrite): + ... + elif dest.is_file() and overwrite: + dest.unlink(missing_ok=True) + else: # pragma: no cover + raise RuntimeError(dest, overwrite) + with TemporaryDirectory(suffix=".tmp", prefix=name, dir=dir_) as temp_dir: + temp_file = Path(temp_dir, src.name) + _ = shutil.copyfile(src, temp_file) + _ = temp_file.replace(dest) + if delete_src: + rmtree(src, ignore_errors=True) + + +@dataclass(kw_only=True, slots=True) +class _MoveOrCopyError(Exception): ... + + +@dataclass(kw_only=True, slots=True) +class _MoveOrCopySourceNotFoundError(_MoveOrCopyError): + src: Path + + +@dataclass(kw_only=True, slots=True) +class _MoveOrCopyDestinationExistsError(_MoveOrCopyError): + src: Path + dest: Path ## @@ -198,10 +325,20 @@ def apply(mapping: Mapping[str, str | None], /) -> None: __all__ = [ + "CPU_COUNT", + "EFFECTIVE_GROUP_ID", + "EFFECTIVE_USER_ID", + "CopyError", + "GetCPUCountError", "GetCPUUseError", + "IntOrAll", + "MoveError", + "copy", + "get_cpu_count", "get_cpu_use", "get_env_var", "is_debug", "is_pytest", + "move", "temp_environ", ] diff --git a/src/utilities/tempfile.py b/src/utilities/tempfile.py index e5f5e14d8..dddc32f88 100644 --- a/src/utilities/tempfile.py +++ b/src/utilities/tempfile.py @@ -3,7 +3,7 @@ import tempfile from contextlib import contextmanager from pathlib import Path -from shutil import copyfile, move +from shutil import move from tempfile import NamedTemporaryFile as _NamedTemporaryFile from typing import TYPE_CHECKING, override from warnings import catch_warnings, filterwarnings From 3487f30d1321053a8267a4440ad1badedfd931a5 Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 12:56:38 +0900 Subject: [PATCH 05/35] 2026-01-17 12:56:38 (Sat) > DW-Mac > derekwan --- src/utilities/constants.py | 358 +++---------------------------------- 1 file changed, 26 insertions(+), 332 deletions(-) diff --git a/src/utilities/constants.py b/src/utilities/constants.py index 0bb8ac8f5..aa5a9b05f 100644 --- a/src/utilities/constants.py +++ b/src/utilities/constants.py @@ -1,68 +1,19 @@ from __future__ import annotations -from dataclasses import dataclass -from getpass import getuser -from logging import getLogger -from os import cpu_count, environ from pathlib import Path from platform import system -from random import SystemRandom -from re import IGNORECASE, search -from socket import gethostname -from tempfile import gettempdir -from typing import TYPE_CHECKING, Any, assert_never, cast, override -from zoneinfo import ZoneInfo +from typing import TYPE_CHECKING, assert_never -from tzlocal import get_localzone -from whenever import DateDelta, DateTimeDelta, PlainDateTime, TimeDelta, ZonedDateTime +from whenever import DateDelta, TimeDelta if TYPE_CHECKING: - from utilities.types import System, TimeZone - - -# getpass - - -USER: str = getuser() - - -# math - - -MIN_FLOAT32, MAX_FLOAT32 = -3.4028234663852886e38, 3.4028234663852886e38 -MIN_FLOAT64, MAX_FLOAT64 = -1.7976931348623157e308, 1.7976931348623157e308 -MIN_INT8, MAX_INT8 = -(2 ** (8 - 1)), 2 ** (8 - 1) - 1 -MIN_INT16, MAX_INT16 = -(2 ** (16 - 1)), 2 ** (16 - 1) - 1 -MIN_INT32, MAX_INT32 = -(2 ** (32 - 1)), 2 ** (32 - 1) - 1 -MIN_INT64, MAX_INT64 = -(2 ** (64 - 1)), 2 ** (64 - 1) - 1 -MIN_UINT8, MAX_UINT8 = 0, 2**8 - 1 -MIN_UINT16, MAX_UINT16 = 0, 2**16 - 1 -MIN_UINT32, MAX_UINT32 = 0, 2**32 - 1 -MIN_UINT64, MAX_UINT64 = 0, 2**64 - 1 - - -# os - - -IS_CI: bool = "CI" in environ - - -def _get_cpu_count() -> int: - """Get the CPU count.""" - count = cpu_count() - if count is None: # pragma: no cover - raise ValueError(count) - return count - - -CPU_COUNT: int = _get_cpu_count() + from utilities.types import System # platform def _get_system() -> System: - """Get the system/OS name.""" sys = system() if sys == "Windows": # skipif-not-windows return "windows" @@ -73,23 +24,16 @@ def _get_system() -> System: raise ValueError(sys) # pragma: no cover -SYSTEM: System = _get_system() -IS_WINDOWS: bool = SYSTEM == "windows" -IS_MAC: bool = SYSTEM == "mac" -IS_LINUX: bool = SYSTEM == "linux" -IS_NOT_WINDOWS: bool = not IS_WINDOWS -IS_NOT_MAC: bool = not IS_MAC -IS_NOT_LINUX: bool = not IS_LINUX -IS_CI_AND_WINDOWS: bool = IS_CI and IS_WINDOWS -IS_CI_AND_MAC: bool = IS_CI and IS_MAC -IS_CI_AND_LINUX: bool = IS_CI and IS_LINUX -IS_CI_AND_NOT_WINDOWS: bool = IS_CI and IS_NOT_WINDOWS -IS_CI_AND_NOT_MAC: bool = IS_CI and IS_NOT_MAC -IS_CI_AND_NOT_LINUX: bool = IS_CI and IS_NOT_LINUX +SYSTEM = _get_system() +IS_WINDOWS = SYSTEM == "windows" +IS_MAC = SYSTEM == "mac" +IS_LINUX = SYSTEM == "linux" +IS_NOT_WINDOWS = not IS_WINDOWS +IS_NOT_MAC = not IS_MAC +IS_NOT_LINUX = not IS_LINUX def _get_max_pid() -> int | None: - """Get the system max process ID.""" match SYSTEM: case "windows": # skipif-not-windows return None @@ -105,14 +49,7 @@ def _get_max_pid() -> int | None: assert_never(never) -MAX_PID: int | None = _get_max_pid() - - -# pathlib - - -HOME: Path = Path.home() -PWD: Path = Path.cwd() +MAX_PID = _get_max_pid() # platform -> os @@ -131,11 +68,7 @@ def _get_effective_group_id() -> int | None: assert_never(never) -EFFECTIVE_GROUP_ID: int | None = _get_effective_group_id() - - def _get_effective_user_id() -> int | None: - """Get the effective user ID.""" match SYSTEM: case "windows": # skipif-not-windows return None @@ -147,14 +80,14 @@ def _get_effective_user_id() -> int | None: assert_never(never) -EFFECTIVE_USER_ID: int | None = _get_effective_user_id() +EFFECTIVE_USER_ID = _get_effective_user_id() +EFFECTIVE_GROUP_ID = _get_effective_group_id() # platform -> os -> grp def _get_gid_name(gid: int, /) -> str | None: - """Get the name of a group ID.""" match SYSTEM: case "windows": # skipif-not-windows return None @@ -166,221 +99,26 @@ def _get_gid_name(gid: int, /) -> str | None: assert_never(never) -ROOT_GROUP_NAME: str | None = _get_gid_name(0) -EFFECTIVE_GROUP_NAME: str | None = ( +ROOT_GROUP_NAME = _get_gid_name(0) +EFFECTIVE_GROUP_NAME = ( None if EFFECTIVE_GROUP_ID is None else _get_gid_name(EFFECTIVE_GROUP_ID) ) -# platform -> os -> pwd - - -def _get_uid_name(uid: int, /) -> str | None: - """Get the name of a user ID.""" - match SYSTEM: - case "windows": # skipif-not-windows - return None - case "mac" | "linux": # skipif-windows - from pwd import getpwuid - - return getpwuid(uid).pw_name - case never: - assert_never(never) - - -ROOT_USER_NAME: str | None = _get_uid_name(0) -EFFECTIVE_USER_NAME: str | None = ( - None if EFFECTIVE_USER_ID is None else _get_uid_name(EFFECTIVE_USER_ID) -) - - -# random - - -SYSTEM_RANDOM: SystemRandom = SystemRandom() - - -# reprlib - - -RICH_MAX_WIDTH: int = 80 -RICH_INDENT_SIZE: int = 4 -RICH_MAX_LENGTH: int | None = 20 -RICH_MAX_STRING: int | None = None -RICH_MAX_DEPTH: int | None = None -RICH_EXPAND_ALL: bool = False - - -# sentinel - - -class _Meta(type): - """Metaclass for the sentinel.""" - - instance: Any = None - - @override - def __call__(cls, *args: Any, **kwargs: Any) -> Any: - if cls.instance is None: - cls.instance = super().__call__(*args, **kwargs) - return cls.instance - - -_SENTINEL_REPR = "" - - -class Sentinel(metaclass=_Meta): - """Base class for the sentinel object.""" - - @override - def __repr__(self) -> str: - return _SENTINEL_REPR - - @override - def __str__(self) -> str: - return repr(self) - - @classmethod - def parse(cls, text: str, /) -> Sentinel: - """Parse text into the Sentinel value.""" - if search("^(|sentinel|)$", text, flags=IGNORECASE): - return sentinel - raise SentinelParseError(text=text) - - -@dataclass(kw_only=True, slots=True) -class SentinelParseError(Exception): - text: str - - @override - def __str__(self) -> str: - return f"Unable to parse sentinel; got {self.text!r}" - - -sentinel = Sentinel() - - -# socket - - -HOSTNAME = gethostname() - - -# tempfile - - -TEMP_DIR: Path = Path(gettempdir()) - - -# text - - -LIST_SEPARATOR: str = "," -PAIR_SEPARATOR: str = "=" -BRACKETS: set[tuple[str, str]] = {("(", ")"), ("[", "]"), ("{", "}")} - - -# tzlocal - - -def _get_local_time_zone() -> ZoneInfo: - """Get the local time zone, with the logging disabled.""" - logger = getLogger("tzlocal") # avoid import cycle - init_disabled = logger.disabled - logger.disabled = True - time_zone = get_localzone() - logger.disabled = init_disabled - return time_zone - - -LOCAL_TIME_ZONE: ZoneInfo = _get_local_time_zone() -LOCAL_TIME_ZONE_NAME: TimeZone = cast("TimeZone", LOCAL_TIME_ZONE.key) - - -# tzlocal -> whenever - - -def _get_now_local() -> ZonedDateTime: - """Get the current zoned date-time in the local time-zone.""" - return ZonedDateTime.now(LOCAL_TIME_ZONE_NAME) - - -NOW_LOCAL = _get_now_local() -TODAY_LOCAL = NOW_LOCAL.date() -TIME_LOCAL = NOW_LOCAL.time() -NOW_LOCAL_PLAIN = NOW_LOCAL.to_plain() - - # whenever -ZERO_DAYS: DateDelta = DateDelta() -ZERO_TIME: TimeDelta = TimeDelta() -NANOSECOND: TimeDelta = TimeDelta(nanoseconds=1) -MICROSECOND: TimeDelta = TimeDelta(microseconds=1) -MILLISECOND: TimeDelta = TimeDelta(milliseconds=1) -SECOND: TimeDelta = TimeDelta(seconds=1) -MINUTE: TimeDelta = TimeDelta(minutes=1) -HOUR: TimeDelta = TimeDelta(hours=1) -DAY: DateDelta = DateDelta(days=1) -WEEK: DateDelta = DateDelta(weeks=1) -MONTH: DateDelta = DateDelta(months=1) -YEAR: DateDelta = DateDelta(years=1) - - -DATE_DELTA_MIN: DateDelta = DateDelta(weeks=-521722, days=-5) -DATE_DELTA_MAX: DateDelta = DateDelta(weeks=521722, days=5) -TIME_DELTA_MIN: TimeDelta = TimeDelta(hours=-87831216) -TIME_DELTA_MAX: TimeDelta = TimeDelta(hours=87831216) -DATE_TIME_DELTA_MIN: DateTimeDelta = DateTimeDelta( - weeks=-521722, - days=-5, - hours=-23, - minutes=-59, - seconds=-59, - milliseconds=-999, - microseconds=-999, - nanoseconds=-999, -) -DATE_TIME_DELTA_MAX: DateTimeDelta = DateTimeDelta( - weeks=521722, - days=5, - hours=23, - minutes=59, - seconds=59, - milliseconds=999, - microseconds=999, - nanoseconds=999, -) - - -SECONDS_PER_DAY = 24 * 60 * 60 -NANOSECONDS_PER_SECOND = 1_000_000_000 -NANOSECONDS_PER_DAY = SECONDS_PER_DAY * NANOSECONDS_PER_SECOND - - -# zoneinfo - - -UTC: ZoneInfo = ZoneInfo("UTC") - - -# zoneinfo -> whenever - - -ZONED_DATE_TIME_MIN: ZonedDateTime = PlainDateTime.MIN.assume_tz(UTC.key) -ZONED_DATE_TIME_MAX: ZonedDateTime = PlainDateTime.MAX.assume_tz(UTC.key) - - -def _get_now(time_zone: str = UTC.key, /) -> ZonedDateTime: - """Get the current zoned date-time.""" - return ZonedDateTime.now(time_zone) - - -NOW_UTC = _get_now() -TODAY_UTC = NOW_UTC.date() -TIME_UTC = NOW_UTC.time() -NOW_UTC_PLAIN = NOW_UTC.to_plain() +ZERO_DAYS = DateDelta() +ZERO_TIME = TimeDelta() +MICROSECOND = TimeDelta(microseconds=1) +MILLISECOND = TimeDelta(milliseconds=1) +SECOND = TimeDelta(seconds=1) +MINUTE = TimeDelta(minutes=1) +HOUR = TimeDelta(hours=1) +DAY = DateDelta(days=1) +WEEK = DateDelta(weeks=1) +MONTH = DateDelta(months=1) +YEAR = DateDelta(years=1) __all__ = [ @@ -394,37 +132,14 @@ def _get_now(time_zone: str = UTC.key, /) -> ZonedDateTime: "EFFECTIVE_GROUP_ID", "EFFECTIVE_GROUP_NAME", "EFFECTIVE_USER_ID", - "EFFECTIVE_USER_NAME", - "HOME", - "HOSTNAME", "HOUR", - "IS_CI", - "IS_CI_AND_LINUX", - "IS_CI_AND_MAC", - "IS_CI_AND_NOT_LINUX", - "IS_CI_AND_NOT_MAC", - "IS_CI_AND_NOT_WINDOWS", - "IS_CI_AND_WINDOWS", "IS_LINUX", "IS_MAC", "IS_NOT_LINUX", "IS_NOT_MAC", "IS_NOT_WINDOWS", "IS_WINDOWS", - "LIST_SEPARATOR", - "LOCAL_TIME_ZONE", - "LOCAL_TIME_ZONE_NAME", - "MAX_FLOAT32", - "MAX_FLOAT64", - "MAX_INT8", - "MAX_INT16", - "MAX_INT32", - "MAX_INT64", "MAX_PID", - "MAX_UINT8", - "MAX_UINT16", - "MAX_UINT32", - "MAX_UINT64", "MICROSECOND", "MILLISECOND", "MINUTE", @@ -439,30 +154,9 @@ def _get_now(time_zone: str = UTC.key, /) -> ZonedDateTime: "MIN_UINT32", "MIN_UINT64", "MONTH", - "NANOSECOND", - "NANOSECONDS_PER_DAY", - "NANOSECONDS_PER_SECOND", - "NOW_LOCAL", - "NOW_LOCAL_PLAIN", - "NOW_UTC", - "NOW_UTC_PLAIN", - "PAIR_SEPARATOR", - "PWD", "ROOT_GROUP_NAME", - "ROOT_USER_NAME", "SECOND", - "SECONDS_PER_DAY", "SYSTEM", - "SYSTEM_RANDOM", - "TEMP_DIR", - "TIME_DELTA_MAX", - "TIME_DELTA_MIN", - "TIME_LOCAL", - "TIME_UTC", - "TODAY_LOCAL", - "TODAY_UTC", - "USER", - "UTC", "WEEK", "YEAR", "ZERO_DAYS", From 6e715d681230d05e2f1a6e0397e0d757b66c43eb Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 12:59:05 +0900 Subject: [PATCH 06/35] 2026-01-17 12:59:05 (Sat) > DW-Mac > derekwan --- src/tests/test_constants.py | 260 +---------------------------------- src/tests/test_subprocess.py | 9 +- src/tests/test_types.py | 2 +- src/utilities/constants.py | 23 ++++ src/utilities/os.py | 5 +- 5 files changed, 32 insertions(+), 267 deletions(-) diff --git a/src/tests/test_constants.py b/src/tests/test_constants.py index 5fce7389d..5444e880e 100644 --- a/src/tests/test_constants.py +++ b/src/tests/test_constants.py @@ -1,111 +1,16 @@ from __future__ import annotations -from pathlib import Path -from random import SystemRandom -from typing import TYPE_CHECKING, assert_never -from zoneinfo import ZoneInfo +from typing import assert_never -from pytest import mark, param, raises -from whenever import ( - Date, - DateDelta, - DateTimeDelta, - PlainDateTime, - Time, - TimeDelta, - ZonedDateTime, -) +from pytest import mark, param from utilities.constants import ( - _SENTINEL_REPR, - CPU_COUNT, - DATE_DELTA_MAX, - DATE_DELTA_MIN, - EFFECTIVE_GROUP_ID, EFFECTIVE_GROUP_NAME, - EFFECTIVE_USER_ID, EFFECTIVE_USER_NAME, - HOME, - HOSTNAME, - IS_LINUX, - IS_MAC, - IS_NOT_LINUX, - IS_NOT_MAC, - IS_NOT_WINDOWS, - IS_WINDOWS, - LOCAL_TIME_ZONE, - LOCAL_TIME_ZONE_NAME, - MAX_PID, - NANOSECOND, - NOW_LOCAL, - NOW_LOCAL_PLAIN, - NOW_UTC, - NOW_UTC_PLAIN, - PWD, ROOT_GROUP_NAME, ROOT_USER_NAME, - SYSTEM_RANDOM, - TEMP_DIR, - TIME_DELTA_MAX, - TIME_DELTA_MIN, - TIME_LOCAL, - TIME_UTC, - TODAY_LOCAL, - TODAY_UTC, - USER, - ZONED_DATE_TIME_MAX, - ZONED_DATE_TIME_MIN, - Sentinel, - SentinelParseError, - sentinel, ) from utilities.platform import SYSTEM -from utilities.types import System, TimeZone -from utilities.typing import get_literal_elements - -if TYPE_CHECKING: - from collections.abc import Callable - - -class TestCPUCount: - def test_main(self) -> None: - assert isinstance(CPU_COUNT, int) - assert CPU_COUNT >= 1 - - -class TestDateDeltaMinMax: - def test_min(self) -> None: - with raises(ValueError, match=r"days out of range"): - _ = DateDelta(weeks=-521722, days=-6) - with raises(ValueError, match=r"Addition result out of bounds"): - _ = DATE_DELTA_MIN - DateDelta(days=1) - - def test_date_delta_max(self) -> None: - with raises(ValueError, match=r"days out of range"): - _ = DateDelta(weeks=521722, days=6) - with raises(ValueError, match=r"Addition result out of bounds"): - _ = DATE_DELTA_MAX + DateDelta(days=1) - - -class TestDateTimeDeltaMinMax: - def test_min(self) -> None: - with raises(ValueError, match=r"Out of range"): - _ = DateTimeDelta(weeks=-521722, days=-6) - - def test_max(self) -> None: - with raises(ValueError, match=r"Out of range"): - _ = DateTimeDelta(weeks=521722, days=6) - - -class TestGroupId: - def test_main(self) -> None: - match SYSTEM: - case "windows": # skipif-not-windows - assert EFFECTIVE_GROUP_ID is None - case "mac" | "linux": # skipif-windows - assert isinstance(EFFECTIVE_GROUP_ID, int) - case never: - assert_never(never) class TestGroupName: @@ -116,7 +21,7 @@ class TestGroupName: param(EFFECTIVE_GROUP_NAME, id="effective"), ], ) - def test_main(self, *, group: str | None) -> None: + def test_constant(self, *, group: str | None) -> None: match SYSTEM: case "windows": # skipif-not-windows assert group is None @@ -126,159 +31,12 @@ def test_main(self, *, group: str | None) -> None: assert_never(never) -class TestHostname: - def test_main(self) -> None: - assert isinstance(HOSTNAME, str) - - -class TestLocalTimeZone: - def test_main(self) -> None: - assert isinstance(LOCAL_TIME_ZONE, ZoneInfo) - - -class TestLocalTimeZoneName: - def test_main(self) -> None: - assert isinstance(LOCAL_TIME_ZONE_NAME, str) - assert LOCAL_TIME_ZONE_NAME in get_literal_elements(TimeZone) - - -class TestMaxPID: - def test_main(self) -> None: - match SYSTEM: - case "windows": # skipif-not-windows - assert MAX_PID is None - case "mac": # skipif-not-macos - assert isinstance(MAX_PID, int) - case "linux": # skipif-not-linux - assert isinstance(MAX_PID, int) - case never: - assert_never(never) - - -class TestNow: - @mark.parametrize("date_time", [param(NOW_LOCAL), param(NOW_UTC)]) - def test_now(self, *, date_time: ZonedDateTime) -> None: - assert isinstance(date_time, ZonedDateTime) - - @mark.parametrize("date", [param(TODAY_LOCAL), param(TODAY_UTC)]) - def test_today(self, *, date: Date) -> None: - assert isinstance(date, Date) - - @mark.parametrize("time", [param(TIME_LOCAL), param(TIME_UTC)]) - def test_time(self, *, time: Time) -> None: - assert isinstance(time, Time) - - @mark.parametrize("date_time", [param(NOW_LOCAL_PLAIN), param(NOW_UTC_PLAIN)]) - def test_plain(self, *, date_time: PlainDateTime) -> None: - assert isinstance(date_time, PlainDateTime) - - -class TestPaths: - @mark.parametrize("path", [param(HOME), param(PWD)]) - def test_main(self, *, path: Path) -> None: - assert isinstance(path, Path) - assert path.is_dir() - - -class TestSentinel: - def test_isinstance(self) -> None: - assert isinstance(sentinel, Sentinel) - - @mark.parametrize( - "text", - [ - param("", id="blank"), - param(_SENTINEL_REPR, id="default"), - param(_SENTINEL_REPR.lower(), id="lower"), - param(_SENTINEL_REPR.upper(), id="upper"), - ], - ) - def test_parse(self, *, text: str) -> None: - result = Sentinel.parse(text) - assert result is sentinel - - @mark.parametrize("method", [param(repr), param(str)]) - def test_repr_and_str(self, method: Callable[..., str]) -> None: - assert method(sentinel) == _SENTINEL_REPR - - def test_singleton(self) -> None: - assert Sentinel() is sentinel - - @mark.parametrize("text", [param("invalid"), param("ssentinell")]) - def test_error_parse(self, *, text: str) -> None: - with raises(SentinelParseError, match=r"Unable to parse sentinel; got '.*'"): - _ = Sentinel.parse(text) - - -class TestSystemRandom: - def test_main(self) -> None: - assert isinstance(SYSTEM_RANDOM, SystemRandom) - - -class TestSystem: - def test_main(self) -> None: - assert isinstance(SYSTEM, str) - assert SYSTEM in get_literal_elements(System) - - @mark.parametrize( - "predicate", - [ - param(IS_WINDOWS, id="IS_WINDOWS"), - param(IS_MAC, id="IS_MAC"), - param(IS_LINUX, id="IS_LINUX"), - param(IS_NOT_WINDOWS, id="IS_NOT_WINDOWS"), - param(IS_NOT_MAC, id="IS_NOT_MAC"), - param(IS_NOT_LINUX, id="IS_NOT_LINUX"), - ], - ) - def test_predicates(self, *, predicate: bool) -> None: - assert isinstance(predicate, bool) - - -class TestTempDir: - def test_main(self) -> None: - assert isinstance(TEMP_DIR, Path) - - -class TestTimeDeltaMinMax: - def test_min(self) -> None: - with raises(ValueError, match=r"hours out of range"): - _ = TimeDelta(hours=-87831217) - with raises(ValueError, match=r"TimeDelta out of range"): - _ = TimeDelta(nanoseconds=TIME_DELTA_MIN.in_nanoseconds() - 1) - with raises(ValueError, match=r"Addition result out of range"): - _ = TIME_DELTA_MIN - NANOSECOND - - def test_max(self) -> None: - with raises(ValueError, match=r"hours out of range"): - _ = TimeDelta(hours=87831217) - with raises(ValueError, match=r"TimeDelta out of range"): - _ = TimeDelta(nanoseconds=TIME_DELTA_MAX.in_nanoseconds() + 1) - _ = TIME_DELTA_MAX + NANOSECOND - - -class TestUser: - def test_main(self) -> None: - assert isinstance(USER, str) - - -class TestUserId: - def test_main(self) -> None: - match SYSTEM: - case "windows": # skipif-not-windows - assert EFFECTIVE_USER_ID is None - case "mac" | "linux": # skipif-windows - assert isinstance(EFFECTIVE_USER_ID, int) - case never: - assert_never(never) - - class TestUserName: @mark.parametrize( "user", [param(ROOT_USER_NAME, id="root"), param(EFFECTIVE_USER_NAME, id="effective")], ) - def test_main(self, *, user: str | None) -> None: + def test_constant(self, *, user: str | None) -> None: match SYSTEM: case "windows": # skipif-not-windows assert user is None @@ -286,13 +44,3 @@ def test_main(self, *, user: str | None) -> None: assert isinstance(user, str) case never: assert_never(never) - - -class TestZonedDateTimeMinMax: - def test_min(self) -> None: - with raises(ValueError, match=r"Instant is out of range"): - _ = ZONED_DATE_TIME_MAX.add(microseconds=1) - - def test_max(self) -> None: - with raises(ValueError, match=r"Instant is out of range"): - _ = ZONED_DATE_TIME_MIN.subtract(nanoseconds=1) diff --git a/src/tests/test_subprocess.py b/src/tests/test_subprocess.py index 57d8ebc86..8eec2edd9 100644 --- a/src/tests/test_subprocess.py +++ b/src/tests/test_subprocess.py @@ -9,14 +9,9 @@ from pytest import LogCaptureFixture, mark, param, raises from pytest_lazy_fixtures import lf +from utilities.grp import EFFECTIVE_GROUP_NAME -from utilities.constants import ( - EFFECTIVE_GROUP_NAME, - EFFECTIVE_USER_NAME, - HOME, - MINUTE, - SECOND, -) +from utilities.constants import MINUTE, SECOND from utilities.iterables import one from utilities.pathlib import get_file_group, get_file_owner from utilities.permissions import Permissions diff --git a/src/tests/test_types.py b/src/tests/test_types.py index 73fb1db1a..a248a5674 100644 --- a/src/tests/test_types.py +++ b/src/tests/test_types.py @@ -8,7 +8,7 @@ from hypothesis.strategies import sampled_from from pytest import mark, param -from utilities.constants import HOME, SYSTEM +from utilities.constants import SYSTEM from utilities.types import TIME_ZONES, Dataclass, Number, PathLike diff --git a/src/utilities/constants.py b/src/utilities/constants.py index aa5a9b05f..b2462e87f 100644 --- a/src/utilities/constants.py +++ b/src/utilities/constants.py @@ -105,6 +105,27 @@ def _get_gid_name(gid: int, /) -> str | None: ) +# platform -> os -> pwd + + +def _get_uid_name(uid: int, /) -> str | None: + match SYSTEM: + case "windows": # skipif-not-windows + return None + case "mac" | "linux": # skipif-windows + from pwd import getpwuid + + return getpwuid(uid).pw_name + case never: + assert_never(never) + + +ROOT_USER_NAME = _get_uid_name(0) +EFFECTIVE_USER_NAME = ( + None if EFFECTIVE_USER_ID is None else _get_uid_name(EFFECTIVE_USER_ID) +) + + # whenever @@ -132,6 +153,7 @@ def _get_gid_name(gid: int, /) -> str | None: "EFFECTIVE_GROUP_ID", "EFFECTIVE_GROUP_NAME", "EFFECTIVE_USER_ID", + "EFFECTIVE_USER_NAME", "HOUR", "IS_LINUX", "IS_MAC", @@ -155,6 +177,7 @@ def _get_gid_name(gid: int, /) -> str | None: "MIN_UINT64", "MONTH", "ROOT_GROUP_NAME", + "ROOT_USER_NAME", "SECOND", "SYSTEM", "WEEK", diff --git a/src/utilities/os.py b/src/utilities/os.py index cf726132e..e78c228fb 100644 --- a/src/utilities/os.py +++ b/src/utilities/os.py @@ -3,16 +3,15 @@ import shutil from contextlib import suppress from dataclasses import dataclass -from os import cpu_count, environ, getenv, replace +from os import cpu_count, environ, getenv from pathlib import Path from shutil import rmtree -from tempfile import TemporaryDirectory, TemporaryFile +from tempfile import TemporaryDirectory from typing import TYPE_CHECKING, Literal, assert_never, overload, override from utilities.constants import CPU_COUNT from utilities.contextlib import enhanced_context_manager from utilities.iterables import OneStrEmptyError, one_str -from utilities.platform import SYSTEM if TYPE_CHECKING: from collections.abc import Iterator, Mapping From 0739ec5fa4d26f273381fa8e05741b0b31da9521 Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 13:00:29 +0900 Subject: [PATCH 07/35] 2026-01-17 13:00:29 (Sat) > DW-Mac > derekwan --- src/tests/test_constants.py | 45 +++++++++++++++++++++++++++++++++++-- src/tests/test_platform.py | 9 +++++++- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/tests/test_constants.py b/src/tests/test_constants.py index 5444e880e..50eb13e50 100644 --- a/src/tests/test_constants.py +++ b/src/tests/test_constants.py @@ -7,10 +7,19 @@ from utilities.constants import ( EFFECTIVE_GROUP_NAME, EFFECTIVE_USER_NAME, + IS_LINUX, + IS_MAC, + IS_NOT_LINUX, + IS_NOT_MAC, + IS_NOT_WINDOWS, + IS_WINDOWS, + MAX_PID, ROOT_GROUP_NAME, ROOT_USER_NAME, ) from utilities.platform import SYSTEM +from utilities.types import System +from utilities.typing import get_args class TestGroupName: @@ -21,7 +30,7 @@ class TestGroupName: param(EFFECTIVE_GROUP_NAME, id="effective"), ], ) - def test_constant(self, *, group: str | None) -> None: + def test_main(self, *, group: str | None) -> None: match SYSTEM: case "windows": # skipif-not-windows assert group is None @@ -31,12 +40,44 @@ def test_constant(self, *, group: str | None) -> None: assert_never(never) +class TestMaxPID: + def test_main(self) -> None: + match SYSTEM: + case "windows": # skipif-not-windows + assert MAX_PID is None + case "mac": # skipif-not-macos + assert isinstance(MAX_PID, int) + case "linux": # skipif-not-linux + assert isinstance(MAX_PID, int) + case never: + assert_never(never) + + +class TestSystem: + def test_main(self) -> None: + assert SYSTEM in get_args(System) + + @mark.parametrize( + "predicate", + [ + param(IS_WINDOWS, id="IS_WINDOWS"), + param(IS_MAC, id="IS_MAC"), + param(IS_LINUX, id="IS_LINUX"), + param(IS_NOT_WINDOWS, id="IS_NOT_WINDOWS"), + param(IS_NOT_MAC, id="IS_NOT_MAC"), + param(IS_NOT_LINUX, id="IS_NOT_LINUX"), + ], + ) + def test_predicates(self, *, predicate: bool) -> None: + assert isinstance(predicate, bool) + + class TestUserName: @mark.parametrize( "user", [param(ROOT_USER_NAME, id="root"), param(EFFECTIVE_USER_NAME, id="effective")], ) - def test_constant(self, *, user: str | None) -> None: + def test_main(self, *, user: str | None) -> None: match SYSTEM: case "windows": # skipif-not-windows assert user is None diff --git a/src/tests/test_platform.py b/src/tests/test_platform.py index 84acfae23..89b43f650 100644 --- a/src/tests/test_platform.py +++ b/src/tests/test_platform.py @@ -6,7 +6,14 @@ from hypothesis import assume, given from utilities.hypothesis import text_clean -from utilities.platform import get_strftime, maybe_lower_case +from utilities.platform import ( + SYSTEM, + System, + get_max_pid, + get_strftime, + get_system, + maybe_lower_case, +) from utilities.text import unique_str if TYPE_CHECKING: From 3c58d04bdf9273b606b1162b5bf14f7980515413 Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 13:02:11 +0900 Subject: [PATCH 08/35] 2026-01-17 13:02:10 (Sat) > DW-Mac > derekwan --- src/utilities/constants.py | 22 ++++++++++++++++++---- src/utilities/os.py | 28 +--------------------------- 2 files changed, 19 insertions(+), 31 deletions(-) diff --git a/src/utilities/constants.py b/src/utilities/constants.py index b2462e87f..3b8b3f9fd 100644 --- a/src/utilities/constants.py +++ b/src/utilities/constants.py @@ -1,5 +1,6 @@ from __future__ import annotations +from os import cpu_count from pathlib import Path from platform import system from typing import TYPE_CHECKING, assert_never @@ -10,6 +11,23 @@ from utilities.types import System +# os + + +## + + +def _get_cpu_count() -> int: + """Get the CPU count.""" + count = cpu_count() + if count is None: # pragma: no cover + raise ValueError(count) + return count + + +CPU_COUNT = _get_cpu_count() + + # platform @@ -145,10 +163,6 @@ def _get_uid_name(uid: int, /) -> str | None: __all__ = [ "BRACKETS", "CPU_COUNT", - "DATE_DELTA_MAX", - "DATE_DELTA_MIN", - "DATE_TIME_DELTA_MAX", - "DATE_TIME_DELTA_MIN", "DAY", "EFFECTIVE_GROUP_ID", "EFFECTIVE_GROUP_NAME", diff --git a/src/utilities/os.py b/src/utilities/os.py index e78c228fb..21547f38a 100644 --- a/src/utilities/os.py +++ b/src/utilities/os.py @@ -19,9 +19,6 @@ from utilities.types import PathLike -type IntOrAll = int | Literal["all"] - - ## @@ -178,25 +175,7 @@ class _MoveOrCopyDestinationExistsError(_MoveOrCopyError): ## -def get_cpu_count() -> int: - """Get the CPU count.""" - count = cpu_count() - if count is None: # pragma: no cover - raise GetCPUCountError - return count - - -@dataclass(kw_only=True, slots=True) -class GetCPUCountError(Exception): - @override - def __str__(self) -> str: - return "CPU count must not be None" # pragma: no cover - - -CPU_COUNT = get_cpu_count() - - -## +type IntOrAll = int | Literal["all"] def get_cpu_use(*, n: IntOrAll = "all") -> int: @@ -324,16 +303,11 @@ def apply(mapping: Mapping[str, str | None], /) -> None: __all__ = [ - "CPU_COUNT", - "EFFECTIVE_GROUP_ID", - "EFFECTIVE_USER_ID", "CopyError", - "GetCPUCountError", "GetCPUUseError", "IntOrAll", "MoveError", "copy", - "get_cpu_count", "get_cpu_use", "get_env_var", "is_debug", From 107cb56e02fbfd11c0f56681c58aa65210463c84 Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 13:03:26 +0900 Subject: [PATCH 09/35] 2026-01-17 13:03:26 (Sat) > DW-Mac > derekwan --- src/tests/test_constants.py | 7 +++++++ src/tests/test_platform.py | 9 +-------- src/tests/test_subprocess.py | 1 + src/utilities/constants.py | 11 ++++++++--- src/utilities/os.py | 2 +- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/tests/test_constants.py b/src/tests/test_constants.py index 50eb13e50..47ce31717 100644 --- a/src/tests/test_constants.py +++ b/src/tests/test_constants.py @@ -1,5 +1,6 @@ from __future__ import annotations +from random import SystemRandom from typing import assert_never from pytest import mark, param @@ -16,6 +17,7 @@ MAX_PID, ROOT_GROUP_NAME, ROOT_USER_NAME, + SYSTEM_RANDOM, ) from utilities.platform import SYSTEM from utilities.types import System @@ -53,6 +55,11 @@ def test_main(self) -> None: assert_never(never) +class TestSystemRandom: + def test_main(self) -> None: + assert isinstance(SYSTEM_RANDOM, SystemRandom) + + class TestSystem: def test_main(self) -> None: assert SYSTEM in get_args(System) diff --git a/src/tests/test_platform.py b/src/tests/test_platform.py index 89b43f650..84acfae23 100644 --- a/src/tests/test_platform.py +++ b/src/tests/test_platform.py @@ -6,14 +6,7 @@ from hypothesis import assume, given from utilities.hypothesis import text_clean -from utilities.platform import ( - SYSTEM, - System, - get_max_pid, - get_strftime, - get_system, - maybe_lower_case, -) +from utilities.platform import get_strftime, maybe_lower_case from utilities.text import unique_str if TYPE_CHECKING: diff --git a/src/tests/test_subprocess.py b/src/tests/test_subprocess.py index 8eec2edd9..504bf52d5 100644 --- a/src/tests/test_subprocess.py +++ b/src/tests/test_subprocess.py @@ -10,6 +10,7 @@ from pytest import LogCaptureFixture, mark, param, raises from pytest_lazy_fixtures import lf from utilities.grp import EFFECTIVE_GROUP_NAME +from utilities.pwd import EFFECTIVE_USER_NAME from utilities.constants import MINUTE, SECOND from utilities.iterables import one diff --git a/src/utilities/constants.py b/src/utilities/constants.py index 3b8b3f9fd..0bc160af5 100644 --- a/src/utilities/constants.py +++ b/src/utilities/constants.py @@ -3,6 +3,7 @@ from os import cpu_count from pathlib import Path from platform import system +from random import SystemRandom from typing import TYPE_CHECKING, assert_never from whenever import DateDelta, TimeDelta @@ -14,9 +15,6 @@ # os -## - - def _get_cpu_count() -> int: """Get the CPU count.""" count = cpu_count() @@ -144,6 +142,12 @@ def _get_uid_name(uid: int, /) -> str | None: ) +# random + + +SYSTEM_RANDOM = SystemRandom() + + # whenever @@ -194,6 +198,7 @@ def _get_uid_name(uid: int, /) -> str | None: "ROOT_USER_NAME", "SECOND", "SYSTEM", + "SYSTEM_RANDOM", "WEEK", "YEAR", "ZERO_DAYS", diff --git a/src/utilities/os.py b/src/utilities/os.py index 21547f38a..f07d4109b 100644 --- a/src/utilities/os.py +++ b/src/utilities/os.py @@ -3,7 +3,7 @@ import shutil from contextlib import suppress from dataclasses import dataclass -from os import cpu_count, environ, getenv +from os import environ, getenv from pathlib import Path from shutil import rmtree from tempfile import TemporaryDirectory From f79f03bad4495e8df5f5c54018f37fac737deb2c Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 13:03:34 +0900 Subject: [PATCH 10/35] 2026-01-17 13:03:34 (Sat) > DW-Mac > derekwan --- src/utilities/atomicwrites.py | 64 +++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/utilities/atomicwrites.py b/src/utilities/atomicwrites.py index 21e861f2e..7b6fcf7a2 100644 --- a/src/utilities/atomicwrites.py +++ b/src/utilities/atomicwrites.py @@ -1,12 +1,15 @@ from __future__ import annotations import gzip +import shutil from contextlib import ExitStack from dataclasses import dataclass from pathlib import Path from shutil import copyfileobj, copytree, rmtree from typing import TYPE_CHECKING, assert_never, override +from atomicwrites import replace_atomic + from utilities.contextlib import enhanced_context_manager from utilities.iterables import transpose from utilities.pathlib import file_or_dir @@ -80,6 +83,67 @@ def __str__(self) -> str: ## +def move(src: PathLike, dest: PathLike, /, *, overwrite: bool = False) -> None: + """Move/replace a file/directory atomically.""" + src, dest = map(Path, [src, dest]) + match file_or_dir(src), file_or_dir(dest), overwrite: + case None, _, _: + raise _MoveSourceNotFoundError(src=src) + case "file", "file" | "dir", False: + raise _MoveFileExistsError(src=src, dest=dest) from None + case "file", "dir", _: + rmtree(dest, ignore_errors=True) + replace_atomic(str(src), str(dest)) # must be `str`s + case "file", _, _: + replace_atomic(str(src), str(dest)) # must be `str`s + case "dir", "file" | "dir", False: + raise _MoveDirectoryExistsError(src=src, dest=dest) + case "dir", "dir", _: + rmtree(dest, ignore_errors=True) + _ = shutil.move(src, dest) + case "dir", _, _: + dest.unlink(missing_ok=True) + _ = shutil.move(src, dest) + case never: + assert_never(never) + + +@dataclass(kw_only=True, slots=True) +class MoveError(Exception): ... + + +@dataclass(kw_only=True, slots=True) +class _MoveSourceNotFoundError(MoveError): + src: Path + + @override + def __str__(self) -> str: + return f"Source {str(self.src)!r} does not exist" + + +@dataclass(kw_only=True, slots=True) +class _MoveFileExistsError(MoveError): + src: Path + dest: Path + + @override + def __str__(self) -> str: + return f"Cannot move file {str(self.src)!r} as destination {str(self.dest)!r} already exists" + + +@dataclass(kw_only=True, slots=True) +class _MoveDirectoryExistsError(MoveError): + src: Path + dest: Path + + @override + def __str__(self) -> str: + return f"Cannot move directory {str(self.src)!r} as destination {str(self.dest)!r} already exists" + + +## + + def move_many(*paths: tuple[PathLike, PathLike], overwrite: bool = False) -> None: """Move a set of files concurrently.""" srcs, dests = transpose(paths) From b95f0ea05159539950e37a71ae20a48782ff323a Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 13:05:40 +0900 Subject: [PATCH 11/35] 2026-01-17 13:05:40 (Sat) > DW-Mac > derekwan --- src/tests/test_constants.py | 14 ++++++++++++++ src/utilities/constants.py | 8 ++++++++ 2 files changed, 22 insertions(+) diff --git a/src/tests/test_constants.py b/src/tests/test_constants.py index 47ce31717..2aec60226 100644 --- a/src/tests/test_constants.py +++ b/src/tests/test_constants.py @@ -6,6 +6,7 @@ from pytest import mark, param from utilities.constants import ( + CPU_COUNT, EFFECTIVE_GROUP_NAME, EFFECTIVE_USER_NAME, IS_LINUX, @@ -18,12 +19,19 @@ ROOT_GROUP_NAME, ROOT_USER_NAME, SYSTEM_RANDOM, + USER, ) from utilities.platform import SYSTEM from utilities.types import System from utilities.typing import get_args +class TestCPUCount: + def test_main(self) -> None: + assert isinstance(CPU_COUNT, int) + assert CPU_COUNT >= 1 + + class TestGroupName: @mark.parametrize( "group", @@ -62,6 +70,7 @@ def test_main(self) -> None: class TestSystem: def test_main(self) -> None: + assert isinstance(SYSTEM, str) assert SYSTEM in get_args(System) @mark.parametrize( @@ -79,6 +88,11 @@ def test_predicates(self, *, predicate: bool) -> None: assert isinstance(predicate, bool) +class TestUser: + def test_main(self) -> None: + assert isinstance(USER, str) + + class TestUserName: @mark.parametrize( "user", diff --git a/src/utilities/constants.py b/src/utilities/constants.py index 0bc160af5..db8ef9a8c 100644 --- a/src/utilities/constants.py +++ b/src/utilities/constants.py @@ -1,5 +1,6 @@ from __future__ import annotations +from getpass import getuser from os import cpu_count from pathlib import Path from platform import system @@ -12,6 +13,12 @@ from utilities.types import System +# getpass + + +USER = getuser() + + # os @@ -199,6 +206,7 @@ def _get_uid_name(uid: int, /) -> str | None: "SECOND", "SYSTEM", "SYSTEM_RANDOM", + "USER", "WEEK", "YEAR", "ZERO_DAYS", From b736f7f2fcd73d3e60279067d5902b51aa9587f5 Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 13:09:46 +0900 Subject: [PATCH 12/35] 2026-01-17 13:09:46 (Sat) > DW-Mac > derekwan --- src/utilities/constants.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/utilities/constants.py b/src/utilities/constants.py index db8ef9a8c..f177a0b15 100644 --- a/src/utilities/constants.py +++ b/src/utilities/constants.py @@ -1,7 +1,7 @@ from __future__ import annotations from getpass import getuser -from os import cpu_count +from os import cpu_count, environ from pathlib import Path from platform import system from random import SystemRandom @@ -22,6 +22,9 @@ # os +IS_CI = "CI" in environ + + def _get_cpu_count() -> int: """Get the CPU count.""" count = cpu_count() @@ -54,6 +57,12 @@ def _get_system() -> System: IS_NOT_WINDOWS = not IS_WINDOWS IS_NOT_MAC = not IS_MAC IS_NOT_LINUX = not IS_LINUX +IS_CI_AND_WINDOWS = IS_CI and IS_WINDOWS +IS_CI_AND_MAC = IS_CI and IS_MAC +IS_CI_AND_LINUX = IS_CI and IS_LINUX +IS_CI_AND_NOT_WINDOWS = IS_CI and IS_NOT_WINDOWS +IS_CI_AND_NOT_MAC = IS_CI and IS_NOT_MAC +IS_CI_AND_NOT_LINUX = IS_CI and IS_NOT_LINUX def _get_max_pid() -> int | None: @@ -180,6 +189,13 @@ def _get_uid_name(uid: int, /) -> str | None: "EFFECTIVE_USER_ID", "EFFECTIVE_USER_NAME", "HOUR", + "IS_CI", + "IS_CI_AND_LINUX", + "IS_CI_AND_MAC", + "IS_CI_AND_NOT_LINUX", + "IS_CI_AND_NOT_MAC", + "IS_CI_AND_NOT_WINDOWS", + "IS_CI_AND_WINDOWS", "IS_LINUX", "IS_MAC", "IS_NOT_LINUX", From f0a7d5734a2075fe58ec163dcc8b438aa0db72e6 Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 13:13:05 +0900 Subject: [PATCH 13/35] 2026-01-17 13:13:05 (Sat) > DW-Mac > derekwan --- src/tests/test_constants.py | 14 +++++++++++--- src/utilities/constants.py | 32 ++++++++++++++++++++++++++++++-- src/utilities/zoneinfo.py | 2 +- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/tests/test_constants.py b/src/tests/test_constants.py index 2aec60226..522ee812b 100644 --- a/src/tests/test_constants.py +++ b/src/tests/test_constants.py @@ -15,6 +15,8 @@ IS_NOT_MAC, IS_NOT_WINDOWS, IS_WINDOWS, + LOCAL_TIME_ZONE, + LOCAL_TIME_ZONE_NAME, MAX_PID, ROOT_GROUP_NAME, ROOT_USER_NAME, @@ -22,8 +24,8 @@ USER, ) from utilities.platform import SYSTEM -from utilities.types import System -from utilities.typing import get_args +from utilities.types import System, TimeZone +from utilities.typing import get_literal_elements class TestCPUCount: @@ -50,6 +52,12 @@ def test_main(self, *, group: str | None) -> None: assert_never(never) +class TestLocalTimeZone: + def test_main(self) -> None: + assert isinstance(LOCAL_TIME_ZONE, ZoneInfo) + assert LOCAL_TIME_ZONE_NAME in get_literal_elements(TimeZone) + + class TestMaxPID: def test_main(self) -> None: match SYSTEM: @@ -71,7 +79,7 @@ def test_main(self) -> None: class TestSystem: def test_main(self) -> None: assert isinstance(SYSTEM, str) - assert SYSTEM in get_args(System) + assert SYSTEM in get_literal_elements(System) @mark.parametrize( "predicate", diff --git a/src/utilities/constants.py b/src/utilities/constants.py index f177a0b15..9e8f712b6 100644 --- a/src/utilities/constants.py +++ b/src/utilities/constants.py @@ -1,16 +1,19 @@ from __future__ import annotations from getpass import getuser +from logging import getLogger from os import cpu_count, environ from pathlib import Path from platform import system from random import SystemRandom -from typing import TYPE_CHECKING, assert_never +from typing import TYPE_CHECKING, assert_never, cast +from zoneinfo import ZoneInfo +from tzlocal import get_localzone from whenever import DateDelta, TimeDelta if TYPE_CHECKING: - from utilities.types import System + from utilities.types import System, TimeZone # getpass @@ -164,6 +167,22 @@ def _get_uid_name(uid: int, /) -> str | None: SYSTEM_RANDOM = SystemRandom() +# tzlocal + + +def _get_local_time_zone() -> ZoneInfo: + logger = getLogger("tzlocal") # avoid import cycle + init_disabled = logger.disabled + logger.disabled = True + time_zone = get_localzone() + logger.disabled = init_disabled + return time_zone + + +LOCAL_TIME_ZONE = _get_local_time_zone() +LOCAL_TIME_ZONE_NAME = cast("TimeZone", LOCAL_TIME_ZONE.key) + + # whenever @@ -180,6 +199,12 @@ def _get_uid_name(uid: int, /) -> str | None: YEAR = DateDelta(years=1) +# zoneinfo + + +UTC = ZoneInfo("UTC") + + __all__ = [ "BRACKETS", "CPU_COUNT", @@ -202,6 +227,8 @@ def _get_uid_name(uid: int, /) -> str | None: "IS_NOT_MAC", "IS_NOT_WINDOWS", "IS_WINDOWS", + "LOCAL_TIME_ZONE", + "LOCAL_TIME_ZONE_NAME", "MAX_PID", "MICROSECOND", "MILLISECOND", @@ -223,6 +250,7 @@ def _get_uid_name(uid: int, /) -> str | None: "SYSTEM", "SYSTEM_RANDOM", "USER", + "UTC", "WEEK", "YEAR", "ZERO_DAYS", diff --git a/src/utilities/zoneinfo.py b/src/utilities/zoneinfo.py index 97aea9fc2..681bbc575 100644 --- a/src/utilities/zoneinfo.py +++ b/src/utilities/zoneinfo.py @@ -7,7 +7,7 @@ from whenever import ZonedDateTime -from utilities.constants import LOCAL_TIME_ZONE, LOCAL_TIME_ZONE_NAME, UTC +from utilities.constants import UTC from utilities.types import TIME_ZONES if TYPE_CHECKING: From fd63a2309781a6b568afab40468c88f6192c5e1f Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 13:13:08 +0900 Subject: [PATCH 14/35] 2026-01-17 13:13:08 (Sat) > DW-Mac > derekwan --- src/tests/test_constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/test_constants.py b/src/tests/test_constants.py index 522ee812b..267fb223e 100644 --- a/src/tests/test_constants.py +++ b/src/tests/test_constants.py @@ -2,6 +2,7 @@ from random import SystemRandom from typing import assert_never +from zoneinfo import ZoneInfo from pytest import mark, param From 55f4edad4b45980b4274bba691f35f7a86436e8e Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 13:14:40 +0900 Subject: [PATCH 15/35] 2026-01-17 13:14:40 (Sat) > DW-Mac > derekwan --- src/tests/test_constants.py | 5 +++++ src/utilities/constants.py | 9 ++++++++- src/utilities/whenever.py | 19 ++++++++++--------- src/utilities/zoneinfo.py | 2 +- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/tests/test_constants.py b/src/tests/test_constants.py index 267fb223e..bbc374af8 100644 --- a/src/tests/test_constants.py +++ b/src/tests/test_constants.py @@ -56,6 +56,11 @@ def test_main(self, *, group: str | None) -> None: class TestLocalTimeZone: def test_main(self) -> None: assert isinstance(LOCAL_TIME_ZONE, ZoneInfo) + + +class TestLocalTimeZoneName: + def test_main(self) -> None: + assert isinstance(LOCAL_TIME_ZONE_NAME, str) assert LOCAL_TIME_ZONE_NAME in get_literal_elements(TimeZone) diff --git a/src/utilities/constants.py b/src/utilities/constants.py index 9e8f712b6..3a005bd14 100644 --- a/src/utilities/constants.py +++ b/src/utilities/constants.py @@ -10,7 +10,7 @@ from zoneinfo import ZoneInfo from tzlocal import get_localzone -from whenever import DateDelta, TimeDelta +from whenever import DateDelta, PlainDateTime, TimeDelta if TYPE_CHECKING: from utilities.types import System, TimeZone @@ -205,6 +205,13 @@ def _get_local_time_zone() -> ZoneInfo: UTC = ZoneInfo("UTC") +# zoneinfo -> whenever + + +ZONED_DATE_TIME_MIN = PlainDateTime.MIN.assume_tz(UTC.key) +ZONED_DATE_TIME_MAX = PlainDateTime.MAX.assume_tz(UTC.key) + + __all__ = [ "BRACKETS", "CPU_COUNT", diff --git a/src/utilities/whenever.py b/src/utilities/whenever.py index 26e8ba01b..757667322 100644 --- a/src/utilities/whenever.py +++ b/src/utilities/whenever.py @@ -32,15 +32,7 @@ ZonedDateTime, ) -from utilities.constants import ( - LOCAL_TIME_ZONE, - LOCAL_TIME_ZONE_NAME, - UTC, - Sentinel, - _get_now, - sentinel, -) -from utilities.constants import _get_now_local as get_now_local +from utilities.constants import LOCAL_TIME_ZONE, LOCAL_TIME_ZONE_NAME, UTC from utilities.dataclasses import replace_non_sentinel from utilities.functions import get_class_name from utilities.math import sign @@ -1954,6 +1946,15 @@ def __str__(self) -> str: "DATE_TIME_DELTA_PARSABLE_MIN", "DATE_TWO_DIGIT_YEAR_MAX", "DATE_TWO_DIGIT_YEAR_MIN", + "NOW_LOCAL", + "NOW_LOCAL_PLAIN", + "NOW_PLAIN", + "TIME_DELTA_MAX", + "TIME_DELTA_MIN", + "TIME_LOCAL", + "TIME_UTC", + "TODAY_LOCAL", + "TODAY_UTC", "DatePeriod", "DatePeriodError", "MeanDateTimeError", diff --git a/src/utilities/zoneinfo.py b/src/utilities/zoneinfo.py index 681bbc575..97aea9fc2 100644 --- a/src/utilities/zoneinfo.py +++ b/src/utilities/zoneinfo.py @@ -7,7 +7,7 @@ from whenever import ZonedDateTime -from utilities.constants import UTC +from utilities.constants import LOCAL_TIME_ZONE, LOCAL_TIME_ZONE_NAME, UTC from utilities.types import TIME_ZONES if TYPE_CHECKING: From c7efe914e0daf305fb614e7a8aa4bda40c2263aa Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 13:20:23 +0900 Subject: [PATCH 16/35] 2026-01-17 13:20:23 (Sat) > DW-Mac > derekwan --- src/tests/test_constants.py | 7 +++++++ src/tests/test_tempfile.py | 25 +++++++++++-------------- src/utilities/constants.py | 33 +++++++++++++++++++++++++++++++++ src/utilities/hypothesis.py | 6 ------ src/utilities/tempfile.py | 21 --------------------- 5 files changed, 51 insertions(+), 41 deletions(-) diff --git a/src/tests/test_constants.py b/src/tests/test_constants.py index bbc374af8..2418d7fb2 100644 --- a/src/tests/test_constants.py +++ b/src/tests/test_constants.py @@ -1,5 +1,6 @@ from __future__ import annotations +from pathlib import Path from random import SystemRandom from typing import assert_never from zoneinfo import ZoneInfo @@ -22,6 +23,7 @@ ROOT_GROUP_NAME, ROOT_USER_NAME, SYSTEM_RANDOM, + TEMP_DIR, USER, ) from utilities.platform import SYSTEM @@ -102,6 +104,11 @@ def test_predicates(self, *, predicate: bool) -> None: assert isinstance(predicate, bool) +class TestTempDir: + def test_main(self) -> None: + assert isinstance(TEMP_DIR, Path) + + class TestUser: def test_main(self) -> None: assert isinstance(USER, str) diff --git a/src/tests/test_tempfile.py b/src/tests/test_tempfile.py index 4a25c24ea..8796ea39c 100644 --- a/src/tests/test_tempfile.py +++ b/src/tests/test_tempfile.py @@ -3,24 +3,13 @@ from pathlib import Path from utilities.tempfile import ( - TEMP_DIR, TemporaryDirectory, TemporaryFile, - gettempdir, + yield_temp_dir_at, yield_temp_file_at, ) -class TestGetTempDir: - def test_main(self) -> None: - assert isinstance(gettempdir(), Path) - - -class TestTempDir: - def test_main(self) -> None: - assert isinstance(TEMP_DIR, Path) - - class TestTemporaryDirectory: def test_main(self) -> None: temp_dir = TemporaryDirectory() @@ -102,6 +91,14 @@ def test_text(self) -> None: class TestYieldTempAt: + def test_dir(self, *, temp_path_not_exist: Path) -> None: + with yield_temp_dir_at(temp_path_not_exist) as temp: + assert temp.is_dir() + assert temp.parent == temp_path_not_exist.parent + assert temp.name.startswith(temp_path_not_exist.name) + def test_file(self, *, temp_path_not_exist: Path) -> None: - with yield_temp_file_at(temp_path_not_exist): - assert 0, 1 + with yield_temp_file_at(temp_path_not_exist) as temp: + assert temp.is_file() + assert temp.parent == temp_path_not_exist.parent + assert temp.name.startswith(temp_path_not_exist.name) diff --git a/src/utilities/constants.py b/src/utilities/constants.py index 3a005bd14..5ee4ee2c7 100644 --- a/src/utilities/constants.py +++ b/src/utilities/constants.py @@ -6,6 +6,7 @@ from pathlib import Path from platform import system from random import SystemRandom +from tempfile import gettempdir from typing import TYPE_CHECKING, assert_never, cast from zoneinfo import ZoneInfo @@ -22,6 +23,21 @@ USER = getuser() +# math + + +MIN_FLOAT32, MAX_FLOAT32 = -3.4028234663852886e38, 3.4028234663852886e38 +MIN_FLOAT64, MAX_FLOAT64 = -1.7976931348623157e308, 1.7976931348623157e308 +MIN_INT8, MAX_INT8 = -(2 ** (8 - 1)), 2 ** (8 - 1) - 1 +MIN_INT16, MAX_INT16 = -(2 ** (16 - 1)), 2 ** (16 - 1) - 1 +MIN_INT32, MAX_INT32 = -(2 ** (32 - 1)), 2 ** (32 - 1) - 1 +MIN_INT64, MAX_INT64 = -(2 ** (64 - 1)), 2 ** (64 - 1) - 1 +MIN_UINT8, MAX_UINT8 = 0, 2**8 - 1 +MIN_UINT16, MAX_UINT16 = 0, 2**16 - 1 +MIN_UINT32, MAX_UINT32 = 0, 2**32 - 1 +MIN_UINT64, MAX_UINT64 = 0, 2**64 - 1 + + # os @@ -167,6 +183,12 @@ def _get_uid_name(uid: int, /) -> str | None: SYSTEM_RANDOM = SystemRandom() +# tempfile + + +TEMP_DIR = Path(gettempdir()) + + # tzlocal @@ -236,7 +258,17 @@ def _get_local_time_zone() -> ZoneInfo: "IS_WINDOWS", "LOCAL_TIME_ZONE", "LOCAL_TIME_ZONE_NAME", + "MAX_FLOAT32", + "MAX_FLOAT64", + "MAX_INT8", + "MAX_INT16", + "MAX_INT32", + "MAX_INT64", "MAX_PID", + "MAX_UINT8", + "MAX_UINT16", + "MAX_UINT32", + "MAX_UINT64", "MICROSECOND", "MILLISECOND", "MINUTE", @@ -256,6 +288,7 @@ def _get_local_time_zone() -> ZoneInfo: "SECOND", "SYSTEM", "SYSTEM_RANDOM", + "TEMP_DIR", "USER", "UTC", "WEEK", diff --git a/src/utilities/hypothesis.py b/src/utilities/hypothesis.py index b53373111..2cd71bf43 100644 --- a/src/utilities/hypothesis.py +++ b/src/utilities/hypothesis.py @@ -51,10 +51,6 @@ ) from utilities.constants import ( - DATE_DELTA_MAX, - DATE_DELTA_MIN, - DATE_TIME_DELTA_MAX, - DATE_TIME_DELTA_MIN, DAY, IS_LINUX, MAX_FLOAT32, @@ -78,8 +74,6 @@ MIN_UINT32, MIN_UINT64, TEMP_DIR, - TIME_DELTA_MAX, - TIME_DELTA_MIN, UTC, Sentinel, sentinel, diff --git a/src/utilities/tempfile.py b/src/utilities/tempfile.py index dddc32f88..4333f864b 100644 --- a/src/utilities/tempfile.py +++ b/src/utilities/tempfile.py @@ -179,30 +179,9 @@ def yield_temp_file_at(path: PathLike, /) -> Iterator[Path]: yield temp -## - - -def yield_temp_dir_at(path: PathLike, /) -> Iterator[Path]: - """Yield a temporary dir for a target path.""" - - path = Path(path) - with TemporaryDirectory(suffix=".tmp", prefix=path.name, dir=path.parent) as temp: - yield temp - - -def yield_temp_file_at(path: PathLike, /) -> Iterator[Path]: - """Yield a temporary file for a target path.""" - - path = Path(path) - with TemporaryFile(dir=path.parent, suffix=".tmp", prefix=path.name) as temp: - yield temp - - __all__ = [ - "TEMP_DIR", "TemporaryDirectory", "TemporaryFile", - "gettempdir", "yield_temp_dir_at", "yield_temp_file_at", ] From dc7d205755d9a688192e4fc2b9147667c927e9fe Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 13:24:38 +0900 Subject: [PATCH 17/35] 2026-01-17 13:24:38 (Sat) > DW-Mac > derekwan --- src/tests/test_constants.py | 14 +++++++++++++- src/tests/test_orjson.py | 12 +----------- src/tests/test_polars.py | 2 +- src/tests/test_whenever.py | 18 +++++++++++------- 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/tests/test_constants.py b/src/tests/test_constants.py index 2418d7fb2..e60945052 100644 --- a/src/tests/test_constants.py +++ b/src/tests/test_constants.py @@ -5,7 +5,7 @@ from typing import assert_never from zoneinfo import ZoneInfo -from pytest import mark, param +from pytest import mark, param, raises from utilities.constants import ( CPU_COUNT, @@ -25,6 +25,8 @@ SYSTEM_RANDOM, TEMP_DIR, USER, + ZONED_DATE_TIME_MAX, + ZONED_DATE_TIME_MIN, ) from utilities.platform import SYSTEM from utilities.types import System, TimeZone @@ -127,3 +129,13 @@ def test_main(self, *, user: str | None) -> None: assert isinstance(user, str) case never: assert_never(never) + + +class TestZonedDateTimeMinMax: + def test_min(self) -> None: + with raises(ValueError, match=r"Instant is out of range"): + _ = ZONED_DATE_TIME_MAX.add(microseconds=1) + + def test_max(self) -> None: + with raises(ValueError, match=r"Instant is out of range"): + _ = ZONED_DATE_TIME_MIN.subtract(nanoseconds=1) diff --git a/src/tests/test_orjson.py b/src/tests/test_orjson.py index dc17f9ffb..2da974366 100644 --- a/src/tests/test_orjson.py +++ b/src/tests/test_orjson.py @@ -49,17 +49,7 @@ DataClassFutureTypeLiteral, DataClassFutureTypeLiteralNullable, ) -from utilities.constants import ( - HOUR, - LOCAL_TIME_ZONE, - MAX_INT64, - MIN_INT64, - MINUTE, - SECOND, - UTC, - Sentinel, - sentinel, -) +from utilities.constants import HOUR, LOCAL_TIME_ZONE, MINUTE, SECOND, UTC from utilities.hypothesis import ( date_periods, dates, diff --git a/src/tests/test_polars.py b/src/tests/test_polars.py index 3eecf37cf..1322a7084 100644 --- a/src/tests/test_polars.py +++ b/src/tests/test_polars.py @@ -70,7 +70,7 @@ import tests.test_math import utilities.polars -from utilities.constants import NOW_UTC, PWD, TODAY_UTC, UTC, Sentinel, sentinel +from utilities.constants import UTC from utilities.hypothesis import ( assume_does_not_raise, date_deltas, diff --git a/src/tests/test_whenever.py b/src/tests/test_whenever.py index c2fcff1c5..51594394d 100644 --- a/src/tests/test_whenever.py +++ b/src/tests/test_whenever.py @@ -24,18 +24,12 @@ ) from utilities.constants import ( - DATE_TIME_DELTA_MAX, - DATE_TIME_DELTA_MIN, DAY, + LOCAL_TIME_ZONE_NAME, MICROSECOND, MINUTE, MONTH, - NOW_UTC, SECOND, - TIME_DELTA_MAX, - TIME_DELTA_MIN, - TODAY_LOCAL, - TODAY_UTC, UTC, ZERO_DAYS, Sentinel, @@ -64,6 +58,16 @@ DATE_DELTA_PARSABLE_MIN, DATE_TIME_DELTA_PARSABLE_MAX, DATE_TIME_DELTA_PARSABLE_MIN, + NOW_LOCAL, + NOW_LOCAL_PLAIN, + NOW_PLAIN, + NOW_UTC, + TIME_DELTA_MAX, + TIME_DELTA_MIN, + TIME_LOCAL, + TIME_UTC, + TODAY_LOCAL, + TODAY_UTC, DatePeriod, DatePeriodError, MeanDateTimeError, From fbf0eb5ad44a1b31ed6a16cb524b5a45b8c6bcbf Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 13:25:40 +0900 Subject: [PATCH 18/35] 2026-01-17 13:25:40 (Sat) > DW-Mac > derekwan --- src/tests/test_hypothesis.py | 4 ++-- src/tests/test_orjson.py | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/tests/test_hypothesis.py b/src/tests/test_hypothesis.py index 162289f51..58c982488 100644 --- a/src/tests/test_hypothesis.py +++ b/src/tests/test_hypothesis.py @@ -39,7 +39,6 @@ ) from utilities.constants import ( - IS_LINUX, MAX_FLOAT32, MAX_FLOAT64, MAX_INT8, @@ -126,7 +125,8 @@ ) from utilities.iterables import one from utilities.libcst import parse_import -from utilities.platform import maybe_lower_case +from utilities.platform import IS_LINUX, maybe_lower_case +from utilities.sentinel import is_sentinel from utilities.version import Version from utilities.whenever import ( DATE_TWO_DIGIT_YEAR_MAX, diff --git a/src/tests/test_orjson.py b/src/tests/test_orjson.py index 2da974366..458b354b6 100644 --- a/src/tests/test_orjson.py +++ b/src/tests/test_orjson.py @@ -49,7 +49,15 @@ DataClassFutureTypeLiteral, DataClassFutureTypeLiteralNullable, ) -from utilities.constants import HOUR, LOCAL_TIME_ZONE, MINUTE, SECOND, UTC +from utilities.constants import ( + HOUR, + LOCAL_TIME_ZONE, + MAX_INT64, + MIN_INT64, + MINUTE, + SECOND, + UTC, +) from utilities.hypothesis import ( date_periods, dates, From ee1e31e7b2967f1690f72b36c00aa23a0f2b728e Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 13:25:54 +0900 Subject: [PATCH 19/35] 2026-01-17 13:25:54 (Sat) > DW-Mac > derekwan --- src/tests/test_hypothesis.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tests/test_hypothesis.py b/src/tests/test_hypothesis.py index 58c982488..9162eca9a 100644 --- a/src/tests/test_hypothesis.py +++ b/src/tests/test_hypothesis.py @@ -39,6 +39,7 @@ ) from utilities.constants import ( + IS_LINUX, MAX_FLOAT32, MAX_FLOAT64, MAX_INT8, @@ -125,7 +126,7 @@ ) from utilities.iterables import one from utilities.libcst import parse_import -from utilities.platform import IS_LINUX, maybe_lower_case +from utilities.platform import maybe_lower_case from utilities.sentinel import is_sentinel from utilities.version import Version from utilities.whenever import ( From 7c5a268a82e9100efbb9458c728957ce657339e4 Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 13:28:10 +0900 Subject: [PATCH 20/35] 2026-01-17 13:28:10 (Sat) > DW-Mac > derekwan --- src/tests/test_constants.py | 24 ++++++++++++++++++++++++ src/utilities/constants.py | 4 +++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/tests/test_constants.py b/src/tests/test_constants.py index e60945052..d0ea779ad 100644 --- a/src/tests/test_constants.py +++ b/src/tests/test_constants.py @@ -9,7 +9,9 @@ from utilities.constants import ( CPU_COUNT, + EFFECTIVE_GROUP_ID, EFFECTIVE_GROUP_NAME, + EFFECTIVE_USER_ID, EFFECTIVE_USER_NAME, IS_LINUX, IS_MAC, @@ -39,6 +41,17 @@ def test_main(self) -> None: assert CPU_COUNT >= 1 +class TestGroupId: + def test_main(self) -> None: + match SYSTEM: + case "windows": # skipif-not-windows + assert EFFECTIVE_GROUP_ID is None + case "mac" | "linux": # skipif-windows + assert isinstance(EFFECTIVE_GROUP_ID, int) + case never: + assert_never(never) + + class TestGroupName: @mark.parametrize( "group", @@ -116,6 +129,17 @@ def test_main(self) -> None: assert isinstance(USER, str) +class TestUserId: + def test_main(self) -> None: + match SYSTEM: + case "windows": # skipif-not-windows + assert EFFECTIVE_USER_ID is None + case "mac" | "linux": # skipif-windows + assert isinstance(EFFECTIVE_USER_ID, int) + case never: + assert_never(never) + + class TestUserName: @mark.parametrize( "user", diff --git a/src/utilities/constants.py b/src/utilities/constants.py index 5ee4ee2c7..a82d54956 100644 --- a/src/utilities/constants.py +++ b/src/utilities/constants.py @@ -119,6 +119,9 @@ def _get_effective_group_id() -> int | None: assert_never(never) +EFFECTIVE_GROUP_ID = _get_effective_group_id() + + def _get_effective_user_id() -> int | None: match SYSTEM: case "windows": # skipif-not-windows @@ -132,7 +135,6 @@ def _get_effective_user_id() -> int | None: EFFECTIVE_USER_ID = _get_effective_user_id() -EFFECTIVE_GROUP_ID = _get_effective_group_id() # platform -> os -> grp From e6bc3e6ce931124bd033677d4a04f7b8ce22f1a0 Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 13:31:44 +0900 Subject: [PATCH 21/35] 2026-01-17 13:31:44 (Sat) > DW-Mac > derekwan --- src/tests/test_constants.py | 6 ++++++ src/tests/test_pathlib.py | 2 +- src/tests/test_polars.py | 2 +- src/utilities/constants.py | 7 +++++++ src/utilities/subprocess.py | 2 +- 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/tests/test_constants.py b/src/tests/test_constants.py index d0ea779ad..0e9f35241 100644 --- a/src/tests/test_constants.py +++ b/src/tests/test_constants.py @@ -22,6 +22,7 @@ LOCAL_TIME_ZONE, LOCAL_TIME_ZONE_NAME, MAX_PID, + PWD, ROOT_GROUP_NAME, ROOT_USER_NAME, SYSTEM_RANDOM, @@ -94,6 +95,11 @@ def test_main(self) -> None: assert_never(never) +class TestPwd: + def test_main(self) -> None: + assert isinstance(PWD, Path) + + class TestSystemRandom: def test_main(self) -> None: assert isinstance(SYSTEM_RANDOM, SystemRandom) diff --git a/src/tests/test_pathlib.py b/src/tests/test_pathlib.py index 0f6c534a7..734b4fcfb 100644 --- a/src/tests/test_pathlib.py +++ b/src/tests/test_pathlib.py @@ -10,7 +10,7 @@ from pytest import mark, param, raises from utilities.atomicwrites import copy -from utilities.constants import HOME, SYSTEM, Sentinel, sentinel +from utilities.constants import SYSTEM from utilities.dataclasses import replace_non_sentinel from utilities.hypothesis import git_repos, pairs, paths, temp_paths from utilities.pathlib import ( diff --git a/src/tests/test_polars.py b/src/tests/test_polars.py index 1322a7084..8d721bc46 100644 --- a/src/tests/test_polars.py +++ b/src/tests/test_polars.py @@ -70,7 +70,7 @@ import tests.test_math import utilities.polars -from utilities.constants import UTC +from utilities.constants import PWD, UTC from utilities.hypothesis import ( assume_does_not_raise, date_deltas, diff --git a/src/utilities/constants.py b/src/utilities/constants.py index a82d54956..1953ec462 100644 --- a/src/utilities/constants.py +++ b/src/utilities/constants.py @@ -103,6 +103,12 @@ def _get_max_pid() -> int | None: MAX_PID = _get_max_pid() +# pathlib + + +PWD = Path.cwd() + + # platform -> os @@ -285,6 +291,7 @@ def _get_local_time_zone() -> ZoneInfo: "MIN_UINT32", "MIN_UINT64", "MONTH", + "PWD", "ROOT_GROUP_NAME", "ROOT_USER_NAME", "SECOND", diff --git a/src/utilities/subprocess.py b/src/utilities/subprocess.py index 8a24b7a13..f06bfbc2d 100644 --- a/src/utilities/subprocess.py +++ b/src/utilities/subprocess.py @@ -20,7 +20,7 @@ copy, move, ) -from utilities.constants import HOME, PWD, SECOND +from utilities.constants import PWD, SECOND from utilities.contextlib import enhanced_context_manager from utilities.errors import ImpossibleCaseError from utilities.functions import in_timedelta From ea3e63022c67e1e05f71212cb9aa800c97267d95 Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 13:34:19 +0900 Subject: [PATCH 22/35] 2026-01-17 13:34:19 (Sat) > DW-Mac > derekwan --- src/tests/test_constants.py | 9 ++++++--- src/tests/test_functions.py | 2 +- src/tests/test_pathlib.py | 2 +- src/tests/test_subprocess.py | 10 +++++++--- src/tests/test_types.py | 2 +- src/utilities/constants.py | 2 ++ src/utilities/reprlib.py | 8 ++++++++ src/utilities/subprocess.py | 2 +- src/utilities/traceback.py | 7 +++++-- 9 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/tests/test_constants.py b/src/tests/test_constants.py index 0e9f35241..af4aeac93 100644 --- a/src/tests/test_constants.py +++ b/src/tests/test_constants.py @@ -13,6 +13,7 @@ EFFECTIVE_GROUP_NAME, EFFECTIVE_USER_ID, EFFECTIVE_USER_NAME, + HOME, IS_LINUX, IS_MAC, IS_NOT_LINUX, @@ -95,9 +96,11 @@ def test_main(self) -> None: assert_never(never) -class TestPwd: - def test_main(self) -> None: - assert isinstance(PWD, Path) +class TestPaths: + @mark.parametrize("path", [param(HOME), param(PWD)]) + def test_main(self, *, path: Path) -> None: + assert isinstance(path, Path) + assert path.is_dir() class TestSystemRandom: diff --git a/src/tests/test_functions.py b/src/tests/test_functions.py index bcf1b7a15..36cdcfd15 100644 --- a/src/tests/test_functions.py +++ b/src/tests/test_functions.py @@ -25,7 +25,7 @@ ) from pytest import approx, mark, param, raises -from utilities.constants import HOME, MILLISECOND, NOW_UTC, SECOND, ZERO_TIME, sentinel +from utilities.constants import HOME, MILLISECOND, SECOND, ZERO_TIME from utilities.errors import ImpossibleCaseError from utilities.functions import ( EnsureBoolError, diff --git a/src/tests/test_pathlib.py b/src/tests/test_pathlib.py index 734b4fcfb..c43bc51b3 100644 --- a/src/tests/test_pathlib.py +++ b/src/tests/test_pathlib.py @@ -10,7 +10,7 @@ from pytest import mark, param, raises from utilities.atomicwrites import copy -from utilities.constants import SYSTEM +from utilities.constants import HOME, SYSTEM from utilities.dataclasses import replace_non_sentinel from utilities.hypothesis import git_repos, pairs, paths, temp_paths from utilities.pathlib import ( diff --git a/src/tests/test_subprocess.py b/src/tests/test_subprocess.py index 504bf52d5..57d8ebc86 100644 --- a/src/tests/test_subprocess.py +++ b/src/tests/test_subprocess.py @@ -9,10 +9,14 @@ from pytest import LogCaptureFixture, mark, param, raises from pytest_lazy_fixtures import lf -from utilities.grp import EFFECTIVE_GROUP_NAME -from utilities.pwd import EFFECTIVE_USER_NAME -from utilities.constants import MINUTE, SECOND +from utilities.constants import ( + EFFECTIVE_GROUP_NAME, + EFFECTIVE_USER_NAME, + HOME, + MINUTE, + SECOND, +) from utilities.iterables import one from utilities.pathlib import get_file_group, get_file_owner from utilities.permissions import Permissions diff --git a/src/tests/test_types.py b/src/tests/test_types.py index a248a5674..73fb1db1a 100644 --- a/src/tests/test_types.py +++ b/src/tests/test_types.py @@ -8,7 +8,7 @@ from hypothesis.strategies import sampled_from from pytest import mark, param -from utilities.constants import SYSTEM +from utilities.constants import HOME, SYSTEM from utilities.types import TIME_ZONES, Dataclass, Number, PathLike diff --git a/src/utilities/constants.py b/src/utilities/constants.py index 1953ec462..d2f023d2a 100644 --- a/src/utilities/constants.py +++ b/src/utilities/constants.py @@ -106,6 +106,7 @@ def _get_max_pid() -> int | None: # pathlib +HOME = Path.home() PWD = Path.cwd() @@ -250,6 +251,7 @@ def _get_local_time_zone() -> ZoneInfo: "EFFECTIVE_GROUP_NAME", "EFFECTIVE_USER_ID", "EFFECTIVE_USER_NAME", + "HOME", "HOUR", "IS_CI", "IS_CI_AND_LINUX", diff --git a/src/utilities/reprlib.py b/src/utilities/reprlib.py index 7a93ac794..a6a954ce2 100644 --- a/src/utilities/reprlib.py +++ b/src/utilities/reprlib.py @@ -19,6 +19,14 @@ from utilities.types import StrMapping +RICH_MAX_WIDTH: int = 80 +RICH_INDENT_SIZE: int = 4 +RICH_MAX_LENGTH: int | None = 20 +RICH_MAX_STRING: int | None = None +RICH_MAX_DEPTH: int | None = None +RICH_EXPAND_ALL: bool = False + + ## diff --git a/src/utilities/subprocess.py b/src/utilities/subprocess.py index f06bfbc2d..8a24b7a13 100644 --- a/src/utilities/subprocess.py +++ b/src/utilities/subprocess.py @@ -20,7 +20,7 @@ copy, move, ) -from utilities.constants import PWD, SECOND +from utilities.constants import HOME, PWD, SECOND from utilities.contextlib import enhanced_context_manager from utilities.errors import ImpossibleCaseError from utilities.functions import in_timedelta diff --git a/src/utilities/traceback.py b/src/utilities/traceback.py index c3b7af1e8..29b0cf77b 100644 --- a/src/utilities/traceback.py +++ b/src/utilities/traceback.py @@ -14,8 +14,11 @@ from typing import TYPE_CHECKING, override from utilities.atomicwrites import writer -from utilities.constants import ( - LOCAL_TIME_ZONE_NAME, +from utilities.constants import LOCAL_TIME_ZONE_NAME +from utilities.errors import repr_error +from utilities.iterables import OneEmptyError, one +from utilities.pathlib import module_path, to_path +from utilities.reprlib import ( RICH_EXPAND_ALL, RICH_INDENT_SIZE, RICH_MAX_DEPTH, From 869eefc586659addbc280df48238ac4945d89001 Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 13:37:28 +0900 Subject: [PATCH 23/35] 2026-01-17 13:37:28 (Sat) > DW-Mac > derekwan --- src/utilities/constants.py | 99 +++++++++++++++++++++----------------- src/utilities/reprlib.py | 8 --- src/utilities/traceback.py | 7 +-- 3 files changed, 57 insertions(+), 57 deletions(-) diff --git a/src/utilities/constants.py b/src/utilities/constants.py index d2f023d2a..9477aabc9 100644 --- a/src/utilities/constants.py +++ b/src/utilities/constants.py @@ -11,7 +11,7 @@ from zoneinfo import ZoneInfo from tzlocal import get_localzone -from whenever import DateDelta, PlainDateTime, TimeDelta +from whenever import DateDelta, PlainDateTime, TimeDelta, ZonedDateTime if TYPE_CHECKING: from utilities.types import System, TimeZone @@ -20,7 +20,7 @@ # getpass -USER = getuser() +USER: str = getuser() # math @@ -41,7 +41,7 @@ # os -IS_CI = "CI" in environ +IS_CI: bool = "CI" in environ def _get_cpu_count() -> int: @@ -52,7 +52,7 @@ def _get_cpu_count() -> int: return count -CPU_COUNT = _get_cpu_count() +CPU_COUNT: int = _get_cpu_count() # platform @@ -69,19 +69,19 @@ def _get_system() -> System: raise ValueError(sys) # pragma: no cover -SYSTEM = _get_system() -IS_WINDOWS = SYSTEM == "windows" -IS_MAC = SYSTEM == "mac" -IS_LINUX = SYSTEM == "linux" -IS_NOT_WINDOWS = not IS_WINDOWS -IS_NOT_MAC = not IS_MAC -IS_NOT_LINUX = not IS_LINUX -IS_CI_AND_WINDOWS = IS_CI and IS_WINDOWS -IS_CI_AND_MAC = IS_CI and IS_MAC -IS_CI_AND_LINUX = IS_CI and IS_LINUX -IS_CI_AND_NOT_WINDOWS = IS_CI and IS_NOT_WINDOWS -IS_CI_AND_NOT_MAC = IS_CI and IS_NOT_MAC -IS_CI_AND_NOT_LINUX = IS_CI and IS_NOT_LINUX +SYSTEM: System = _get_system() +IS_WINDOWS: bool = SYSTEM == "windows" +IS_MAC: bool = SYSTEM == "mac" +IS_LINUX: bool = SYSTEM == "linux" +IS_NOT_WINDOWS: bool = not IS_WINDOWS +IS_NOT_MAC: bool = not IS_MAC +IS_NOT_LINUX: bool = not IS_LINUX +IS_CI_AND_WINDOWS: bool = IS_CI and IS_WINDOWS +IS_CI_AND_MAC: bool = IS_CI and IS_MAC +IS_CI_AND_LINUX: bool = IS_CI and IS_LINUX +IS_CI_AND_NOT_WINDOWS: bool = IS_CI and IS_NOT_WINDOWS +IS_CI_AND_NOT_MAC: bool = IS_CI and IS_NOT_MAC +IS_CI_AND_NOT_LINUX: bool = IS_CI and IS_NOT_LINUX def _get_max_pid() -> int | None: @@ -100,14 +100,14 @@ def _get_max_pid() -> int | None: assert_never(never) -MAX_PID = _get_max_pid() +MAX_PID: int | None = _get_max_pid() # pathlib -HOME = Path.home() -PWD = Path.cwd() +HOME: Path = Path.home() +PWD: Path = Path.cwd() # platform -> os @@ -126,7 +126,7 @@ def _get_effective_group_id() -> int | None: assert_never(never) -EFFECTIVE_GROUP_ID = _get_effective_group_id() +EFFECTIVE_GROUP_ID: int | None = _get_effective_group_id() def _get_effective_user_id() -> int | None: @@ -141,7 +141,7 @@ def _get_effective_user_id() -> int | None: assert_never(never) -EFFECTIVE_USER_ID = _get_effective_user_id() +EFFECTIVE_USER_ID: int | None = _get_effective_user_id() # platform -> os -> grp @@ -159,8 +159,8 @@ def _get_gid_name(gid: int, /) -> str | None: assert_never(never) -ROOT_GROUP_NAME = _get_gid_name(0) -EFFECTIVE_GROUP_NAME = ( +ROOT_GROUP_NAME: str | None = _get_gid_name(0) +EFFECTIVE_GROUP_NAME: str | None = ( None if EFFECTIVE_GROUP_ID is None else _get_gid_name(EFFECTIVE_GROUP_ID) ) @@ -180,8 +180,8 @@ def _get_uid_name(uid: int, /) -> str | None: assert_never(never) -ROOT_USER_NAME = _get_uid_name(0) -EFFECTIVE_USER_NAME = ( +ROOT_USER_NAME: str | None = _get_uid_name(0) +EFFECTIVE_USER_NAME: str | None = ( None if EFFECTIVE_USER_ID is None else _get_uid_name(EFFECTIVE_USER_ID) ) @@ -189,13 +189,24 @@ def _get_uid_name(uid: int, /) -> str | None: # random -SYSTEM_RANDOM = SystemRandom() +SYSTEM_RANDOM: SystemRandom = SystemRandom() + + +# reprlib + + +RICH_MAX_WIDTH: int = 80 +RICH_INDENT_SIZE: int = 4 +RICH_MAX_LENGTH: int | None = 20 +RICH_MAX_STRING: int | None = None +RICH_MAX_DEPTH: int | None = None +RICH_EXPAND_ALL: bool = False # tempfile -TEMP_DIR = Path(gettempdir()) +TEMP_DIR: Path = Path(gettempdir()) # tzlocal @@ -210,37 +221,37 @@ def _get_local_time_zone() -> ZoneInfo: return time_zone -LOCAL_TIME_ZONE = _get_local_time_zone() -LOCAL_TIME_ZONE_NAME = cast("TimeZone", LOCAL_TIME_ZONE.key) +LOCAL_TIME_ZONE: ZoneInfo = _get_local_time_zone() +LOCAL_TIME_ZONE_NAME: TimeZone = cast("TimeZone", LOCAL_TIME_ZONE.key) # whenever -ZERO_DAYS = DateDelta() -ZERO_TIME = TimeDelta() -MICROSECOND = TimeDelta(microseconds=1) -MILLISECOND = TimeDelta(milliseconds=1) -SECOND = TimeDelta(seconds=1) -MINUTE = TimeDelta(minutes=1) -HOUR = TimeDelta(hours=1) -DAY = DateDelta(days=1) -WEEK = DateDelta(weeks=1) -MONTH = DateDelta(months=1) -YEAR = DateDelta(years=1) +ZERO_DAYS: DateDelta = DateDelta() +ZERO_TIME: TimeDelta = TimeDelta() +MICROSECOND: TimeDelta = TimeDelta(microseconds=1) +MILLISECOND: TimeDelta = TimeDelta(milliseconds=1) +SECOND: TimeDelta = TimeDelta(seconds=1) +MINUTE: TimeDelta = TimeDelta(minutes=1) +HOUR: TimeDelta = TimeDelta(hours=1) +DAY: DateDelta = DateDelta(days=1) +WEEK: DateDelta = DateDelta(weeks=1) +MONTH: DateDelta = DateDelta(months=1) +YEAR: DateDelta = DateDelta(years=1) # zoneinfo -UTC = ZoneInfo("UTC") +UTC: ZoneInfo = ZoneInfo("UTC") # zoneinfo -> whenever -ZONED_DATE_TIME_MIN = PlainDateTime.MIN.assume_tz(UTC.key) -ZONED_DATE_TIME_MAX = PlainDateTime.MAX.assume_tz(UTC.key) +ZONED_DATE_TIME_MIN: ZonedDateTime = PlainDateTime.MIN.assume_tz(UTC.key) +ZONED_DATE_TIME_MAX: ZonedDateTime = PlainDateTime.MAX.assume_tz(UTC.key) __all__ = [ diff --git a/src/utilities/reprlib.py b/src/utilities/reprlib.py index a6a954ce2..7a93ac794 100644 --- a/src/utilities/reprlib.py +++ b/src/utilities/reprlib.py @@ -19,14 +19,6 @@ from utilities.types import StrMapping -RICH_MAX_WIDTH: int = 80 -RICH_INDENT_SIZE: int = 4 -RICH_MAX_LENGTH: int | None = 20 -RICH_MAX_STRING: int | None = None -RICH_MAX_DEPTH: int | None = None -RICH_EXPAND_ALL: bool = False - - ## diff --git a/src/utilities/traceback.py b/src/utilities/traceback.py index 29b0cf77b..c3b7af1e8 100644 --- a/src/utilities/traceback.py +++ b/src/utilities/traceback.py @@ -14,11 +14,8 @@ from typing import TYPE_CHECKING, override from utilities.atomicwrites import writer -from utilities.constants import LOCAL_TIME_ZONE_NAME -from utilities.errors import repr_error -from utilities.iterables import OneEmptyError, one -from utilities.pathlib import module_path, to_path -from utilities.reprlib import ( +from utilities.constants import ( + LOCAL_TIME_ZONE_NAME, RICH_EXPAND_ALL, RICH_INDENT_SIZE, RICH_MAX_DEPTH, From c2e8bb40706cf4e2bbe7e9989f1ae9f22c7ae964 Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 13:45:20 +0900 Subject: [PATCH 24/35] 2026-01-17 13:45:20 (Sat) > DW-Mac > derekwan --- src/tests/test_constants.py | 26 ++++++++++++++++++++++++++ src/utilities/constants.py | 12 ++++++++++++ src/utilities/hypothesis.py | 2 ++ src/utilities/whenever.py | 2 ++ 4 files changed, 42 insertions(+) diff --git a/src/tests/test_constants.py b/src/tests/test_constants.py index af4aeac93..39f688a91 100644 --- a/src/tests/test_constants.py +++ b/src/tests/test_constants.py @@ -6,9 +6,12 @@ from zoneinfo import ZoneInfo from pytest import mark, param, raises +from whenever import DateDelta from utilities.constants import ( CPU_COUNT, + DATE_DELTA_MAX, + DATE_DELTA_MIN, EFFECTIVE_GROUP_ID, EFFECTIVE_GROUP_NAME, EFFECTIVE_USER_ID, @@ -23,11 +26,14 @@ LOCAL_TIME_ZONE, LOCAL_TIME_ZONE_NAME, MAX_PID, + NANOSECOND, PWD, ROOT_GROUP_NAME, ROOT_USER_NAME, SYSTEM_RANDOM, TEMP_DIR, + TIME_DELTA_MAX, + TIME_DELTA_MIN, USER, ZONED_DATE_TIME_MAX, ZONED_DATE_TIME_MIN, @@ -43,6 +49,16 @@ def test_main(self) -> None: assert CPU_COUNT >= 1 +class TestDateDeltaMinMax: + def test_min(self) -> None: + with raises(ValueError, match=r"Addition result out of bounds"): + _ = DATE_DELTA_MIN - DateDelta(days=1) + + def test_date_delta_max(self) -> None: + with raises(ValueError, match=r"Addition result out of bounds"): + _ = DATE_DELTA_MAX + DateDelta(days=1) + + class TestGroupId: def test_main(self) -> None: match SYSTEM: @@ -133,6 +149,16 @@ def test_main(self) -> None: assert isinstance(TEMP_DIR, Path) +class TestTimeDeltaMinMax: + def test_min(self) -> None: + with raises(ValueError, match=r"Addition result out of range"): + _ = TIME_DELTA_MIN - NANOSECOND + + def test_max(self) -> None: + with raises(ValueError, match=r"Addition result out of range"): + _ = TIME_DELTA_MAX + NANOSECOND + + class TestUser: def test_main(self) -> None: assert isinstance(USER, str) diff --git a/src/utilities/constants.py b/src/utilities/constants.py index 9477aabc9..21ddbf944 100644 --- a/src/utilities/constants.py +++ b/src/utilities/constants.py @@ -230,6 +230,7 @@ def _get_local_time_zone() -> ZoneInfo: ZERO_DAYS: DateDelta = DateDelta() ZERO_TIME: TimeDelta = TimeDelta() +NANOSECOND: TimeDelta = TimeDelta(nanoseconds=1) MICROSECOND: TimeDelta = TimeDelta(microseconds=1) MILLISECOND: TimeDelta = TimeDelta(milliseconds=1) SECOND: TimeDelta = TimeDelta(seconds=1) @@ -241,6 +242,12 @@ def _get_local_time_zone() -> ZoneInfo: YEAR: DateDelta = DateDelta(years=1) +DATE_DELTA_MIN: DateDelta = DateDelta(weeks=-521722, days=-5) +DATE_DELTA_MAX: DateDelta = DateDelta(weeks=521722, days=5) +TIME_DELTA_MIN: TimeDelta = TimeDelta(hours=-87831216) +TIME_DELTA_MAX: TimeDelta = TimeDelta(hours=87831216) + + # zoneinfo @@ -257,6 +264,8 @@ def _get_local_time_zone() -> ZoneInfo: __all__ = [ "BRACKETS", "CPU_COUNT", + "DATE_DELTA_MAX", + "DATE_DELTA_MIN", "DAY", "EFFECTIVE_GROUP_ID", "EFFECTIVE_GROUP_NAME", @@ -304,6 +313,7 @@ def _get_local_time_zone() -> ZoneInfo: "MIN_UINT32", "MIN_UINT64", "MONTH", + "NANOSECOND", "PWD", "ROOT_GROUP_NAME", "ROOT_USER_NAME", @@ -311,6 +321,8 @@ def _get_local_time_zone() -> ZoneInfo: "SYSTEM", "SYSTEM_RANDOM", "TEMP_DIR", + "TIME_DELTA_MAX", + "TIME_DELTA_MIN", "USER", "UTC", "WEEK", diff --git a/src/utilities/hypothesis.py b/src/utilities/hypothesis.py index 2cd71bf43..8693422bd 100644 --- a/src/utilities/hypothesis.py +++ b/src/utilities/hypothesis.py @@ -51,6 +51,8 @@ ) from utilities.constants import ( + DATE_DELTA_MAX, + DATE_DELTA_MIN, DAY, IS_LINUX, MAX_FLOAT32, diff --git a/src/utilities/whenever.py b/src/utilities/whenever.py index 757667322..9630c6ac8 100644 --- a/src/utilities/whenever.py +++ b/src/utilities/whenever.py @@ -72,6 +72,8 @@ microseconds=999, nanoseconds=999, ) +TIME_DELTA_MIN = TimeDelta(hours=-87831216) +TIME_DELTA_MAX = TimeDelta(hours=87831216) DATE_TIME_DELTA_PARSABLE_MIN = DateTimeDelta( From c794d103f39a6bfdc671b76c45a8c7a83229c702 Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 14:07:43 +0900 Subject: [PATCH 25/35] 2026-01-17 14:07:43 (Sat) > DW-Mac > derekwan --- src/tests/test_constants.py | 27 ++++++++++++++++++++++++--- src/tests/test_whenever.py | 6 ++++-- src/utilities/constants.py | 32 +++++++++++++++++++++++++++++++- src/utilities/hypothesis.py | 4 ++++ src/utilities/whenever.py | 4 ---- 5 files changed, 63 insertions(+), 10 deletions(-) diff --git a/src/tests/test_constants.py b/src/tests/test_constants.py index 39f688a91..a4b571b7e 100644 --- a/src/tests/test_constants.py +++ b/src/tests/test_constants.py @@ -6,7 +6,7 @@ from zoneinfo import ZoneInfo from pytest import mark, param, raises -from whenever import DateDelta +from whenever import DateDelta, DateTimeDelta, TimeDelta from utilities.constants import ( CPU_COUNT, @@ -51,14 +51,28 @@ def test_main(self) -> None: class TestDateDeltaMinMax: def test_min(self) -> None: + with raises(ValueError, match=r"days out of range"): + _ = DateDelta(weeks=-521722, days=-6) with raises(ValueError, match=r"Addition result out of bounds"): _ = DATE_DELTA_MIN - DateDelta(days=1) def test_date_delta_max(self) -> None: + with raises(ValueError, match=r"days out of range"): + _ = DateDelta(weeks=521722, days=6) with raises(ValueError, match=r"Addition result out of bounds"): _ = DATE_DELTA_MAX + DateDelta(days=1) +class TestDateTimeDeltaMinMax: + def test_min(self) -> None: + with raises(ValueError, match=r"Out of range"): + _ = DateTimeDelta(weeks=-521722, days=-6) + + def test_max(self) -> None: + with raises(ValueError, match=r"Out of range"): + _ = DateTimeDelta(weeks=521722, days=6) + + class TestGroupId: def test_main(self) -> None: match SYSTEM: @@ -151,12 +165,19 @@ def test_main(self) -> None: class TestTimeDeltaMinMax: def test_min(self) -> None: + with raises(ValueError, match=r"hours out of range"): + _ = TimeDelta(hours=-87831217) + with raises(ValueError, match=r"TimeDelta out of range"): + _ = TimeDelta(nanoseconds=TIME_DELTA_MIN.in_nanoseconds() - 1) with raises(ValueError, match=r"Addition result out of range"): _ = TIME_DELTA_MIN - NANOSECOND def test_max(self) -> None: - with raises(ValueError, match=r"Addition result out of range"): - _ = TIME_DELTA_MAX + NANOSECOND + with raises(ValueError, match=r"hours out of range"): + _ = TimeDelta(hours=87831217) + with raises(ValueError, match=r"TimeDelta out of range"): + _ = TimeDelta(nanoseconds=TIME_DELTA_MAX.in_nanoseconds() + 1) + _ = TIME_DELTA_MAX + NANOSECOND class TestUser: diff --git a/src/tests/test_whenever.py b/src/tests/test_whenever.py index 51594394d..13d93ba0c 100644 --- a/src/tests/test_whenever.py +++ b/src/tests/test_whenever.py @@ -24,12 +24,16 @@ ) from utilities.constants import ( + DATE_TIME_DELTA_MAX, + DATE_TIME_DELTA_MIN, DAY, LOCAL_TIME_ZONE_NAME, MICROSECOND, MINUTE, MONTH, SECOND, + TIME_DELTA_MAX, + TIME_DELTA_MIN, UTC, ZERO_DAYS, Sentinel, @@ -62,8 +66,6 @@ NOW_LOCAL_PLAIN, NOW_PLAIN, NOW_UTC, - TIME_DELTA_MAX, - TIME_DELTA_MIN, TIME_LOCAL, TIME_UTC, TODAY_LOCAL, diff --git a/src/utilities/constants.py b/src/utilities/constants.py index 21ddbf944..1e937ec33 100644 --- a/src/utilities/constants.py +++ b/src/utilities/constants.py @@ -11,7 +11,7 @@ from zoneinfo import ZoneInfo from tzlocal import get_localzone -from whenever import DateDelta, PlainDateTime, TimeDelta, ZonedDateTime +from whenever import DateDelta, DateTimeDelta, PlainDateTime, TimeDelta, ZonedDateTime if TYPE_CHECKING: from utilities.types import System, TimeZone @@ -246,6 +246,31 @@ def _get_local_time_zone() -> ZoneInfo: DATE_DELTA_MAX: DateDelta = DateDelta(weeks=521722, days=5) TIME_DELTA_MIN: TimeDelta = TimeDelta(hours=-87831216) TIME_DELTA_MAX: TimeDelta = TimeDelta(hours=87831216) +DATE_TIME_DELTA_MIN: DateTimeDelta = DateTimeDelta( + weeks=-521722, + days=-5, + hours=-23, + minutes=-59, + seconds=-59, + milliseconds=-999, + microseconds=-999, + nanoseconds=-999, +) +DATE_TIME_DELTA_MAX: DateTimeDelta = DateTimeDelta( + weeks=521722, + days=5, + hours=23, + minutes=59, + seconds=59, + milliseconds=999, + microseconds=999, + nanoseconds=999, +) + + +SECONDS_PER_DAY = 24 * 60 * 60 +NANOSECONDS_PER_SECOND = 1_000_000_000 +NANOSECONDS_PER_DAY = SECONDS_PER_DAY * NANOSECONDS_PER_SECOND # zoneinfo @@ -266,6 +291,8 @@ def _get_local_time_zone() -> ZoneInfo: "CPU_COUNT", "DATE_DELTA_MAX", "DATE_DELTA_MIN", + "DATE_TIME_DELTA_MAX", + "DATE_TIME_DELTA_MIN", "DAY", "EFFECTIVE_GROUP_ID", "EFFECTIVE_GROUP_NAME", @@ -314,10 +341,13 @@ def _get_local_time_zone() -> ZoneInfo: "MIN_UINT64", "MONTH", "NANOSECOND", + "NANOSECONDS_PER_DAY", + "NANOSECONDS_PER_SECOND", "PWD", "ROOT_GROUP_NAME", "ROOT_USER_NAME", "SECOND", + "SECONDS_PER_DAY", "SYSTEM", "SYSTEM_RANDOM", "TEMP_DIR", diff --git a/src/utilities/hypothesis.py b/src/utilities/hypothesis.py index 8693422bd..b53373111 100644 --- a/src/utilities/hypothesis.py +++ b/src/utilities/hypothesis.py @@ -53,6 +53,8 @@ from utilities.constants import ( DATE_DELTA_MAX, DATE_DELTA_MIN, + DATE_TIME_DELTA_MAX, + DATE_TIME_DELTA_MIN, DAY, IS_LINUX, MAX_FLOAT32, @@ -76,6 +78,8 @@ MIN_UINT32, MIN_UINT64, TEMP_DIR, + TIME_DELTA_MAX, + TIME_DELTA_MIN, UTC, Sentinel, sentinel, diff --git a/src/utilities/whenever.py b/src/utilities/whenever.py index 9630c6ac8..802a0fe60 100644 --- a/src/utilities/whenever.py +++ b/src/utilities/whenever.py @@ -72,8 +72,6 @@ microseconds=999, nanoseconds=999, ) -TIME_DELTA_MIN = TimeDelta(hours=-87831216) -TIME_DELTA_MAX = TimeDelta(hours=87831216) DATE_TIME_DELTA_PARSABLE_MIN = DateTimeDelta( @@ -1951,8 +1949,6 @@ def __str__(self) -> str: "NOW_LOCAL", "NOW_LOCAL_PLAIN", "NOW_PLAIN", - "TIME_DELTA_MAX", - "TIME_DELTA_MIN", "TIME_LOCAL", "TIME_UTC", "TODAY_LOCAL", From 49d97d646e7e88331a27ac1dc4ec856e8cbe57f2 Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 14:09:47 +0900 Subject: [PATCH 26/35] 2026-01-17 14:09:47 (Sat) > DW-Mac > derekwan --- src/utilities/constants.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/utilities/constants.py b/src/utilities/constants.py index 1e937ec33..86c4611de 100644 --- a/src/utilities/constants.py +++ b/src/utilities/constants.py @@ -59,6 +59,7 @@ def _get_cpu_count() -> int: def _get_system() -> System: + """Get the system/OS name.""" sys = system() if sys == "Windows": # skipif-not-windows return "windows" @@ -85,6 +86,7 @@ def _get_system() -> System: def _get_max_pid() -> int | None: + """Get the system max process ID.""" match SYSTEM: case "windows": # skipif-not-windows return None @@ -130,6 +132,7 @@ def _get_effective_group_id() -> int | None: def _get_effective_user_id() -> int | None: + """Get the effective user ID.""" match SYSTEM: case "windows": # skipif-not-windows return None @@ -148,6 +151,7 @@ def _get_effective_user_id() -> int | None: def _get_gid_name(gid: int, /) -> str | None: + """Get the name of a group ID.""" match SYSTEM: case "windows": # skipif-not-windows return None @@ -169,6 +173,7 @@ def _get_gid_name(gid: int, /) -> str | None: def _get_uid_name(uid: int, /) -> str | None: + """Get the name of a user ID.""" match SYSTEM: case "windows": # skipif-not-windows return None @@ -213,6 +218,7 @@ def _get_uid_name(uid: int, /) -> str | None: def _get_local_time_zone() -> ZoneInfo: + """Get the local time zone, with the logging disabled.""" logger = getLogger("tzlocal") # avoid import cycle init_disabled = logger.disabled logger.disabled = True From 2f15425c0571230b35d3a0d368f1eb9ff1f43fbd Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 14:32:27 +0900 Subject: [PATCH 27/35] 2026-01-17 14:32:27 (Sat) > DW-Mac > derekwan --- src/tests/test_constants.py | 36 +++++++++++++++++++++++++++++++++++- src/tests/test_whenever.py | 12 +++--------- src/utilities/constants.py | 33 +++++++++++++++++++++++++++++++++ src/utilities/whenever.py | 10 ++-------- 4 files changed, 73 insertions(+), 18 deletions(-) diff --git a/src/tests/test_constants.py b/src/tests/test_constants.py index a4b571b7e..b68317f9d 100644 --- a/src/tests/test_constants.py +++ b/src/tests/test_constants.py @@ -6,7 +6,15 @@ from zoneinfo import ZoneInfo from pytest import mark, param, raises -from whenever import DateDelta, DateTimeDelta, TimeDelta +from whenever import ( + Date, + DateDelta, + DateTimeDelta, + PlainDateTime, + Time, + TimeDelta, + ZonedDateTime, +) from utilities.constants import ( CPU_COUNT, @@ -27,6 +35,10 @@ LOCAL_TIME_ZONE_NAME, MAX_PID, NANOSECOND, + NOW_LOCAL, + NOW_LOCAL_PLAIN, + NOW_UTC, + NOW_UTC_PLAIN, PWD, ROOT_GROUP_NAME, ROOT_USER_NAME, @@ -34,6 +46,10 @@ TEMP_DIR, TIME_DELTA_MAX, TIME_DELTA_MIN, + TIME_LOCAL, + TIME_UTC, + TODAY_LOCAL, + TODAY_UTC, USER, ZONED_DATE_TIME_MAX, ZONED_DATE_TIME_MIN, @@ -126,6 +142,24 @@ def test_main(self) -> None: assert_never(never) +class TestNow: + @mark.parametrize("date_time", [param(NOW_LOCAL), param(NOW_UTC)]) + def test_now(self, *, date_time: ZonedDateTime) -> None: + assert isinstance(date_time, ZonedDateTime) + + @mark.parametrize("date", [param(TODAY_LOCAL), param(TODAY_UTC)]) + def test_today(self, *, date: Date) -> None: + assert isinstance(date, Date) + + @mark.parametrize("time", [param(TIME_LOCAL), param(TIME_UTC)]) + def test_time(self, *, time: Time) -> None: + assert isinstance(time, Time) + + @mark.parametrize("date_time", [param(NOW_LOCAL_PLAIN), param(NOW_UTC_PLAIN)]) + def test_plain(self, *, date_time: PlainDateTime) -> None: + assert isinstance(date_time, PlainDateTime) + + class TestPaths: @mark.parametrize("path", [param(HOME), param(PWD)]) def test_main(self, *, path: Path) -> None: diff --git a/src/tests/test_whenever.py b/src/tests/test_whenever.py index 13d93ba0c..c2fcff1c5 100644 --- a/src/tests/test_whenever.py +++ b/src/tests/test_whenever.py @@ -27,13 +27,15 @@ DATE_TIME_DELTA_MAX, DATE_TIME_DELTA_MIN, DAY, - LOCAL_TIME_ZONE_NAME, MICROSECOND, MINUTE, MONTH, + NOW_UTC, SECOND, TIME_DELTA_MAX, TIME_DELTA_MIN, + TODAY_LOCAL, + TODAY_UTC, UTC, ZERO_DAYS, Sentinel, @@ -62,14 +64,6 @@ DATE_DELTA_PARSABLE_MIN, DATE_TIME_DELTA_PARSABLE_MAX, DATE_TIME_DELTA_PARSABLE_MIN, - NOW_LOCAL, - NOW_LOCAL_PLAIN, - NOW_PLAIN, - NOW_UTC, - TIME_LOCAL, - TIME_UTC, - TODAY_LOCAL, - TODAY_UTC, DatePeriod, DatePeriodError, MeanDateTimeError, diff --git a/src/utilities/constants.py b/src/utilities/constants.py index 86c4611de..d574a5d05 100644 --- a/src/utilities/constants.py +++ b/src/utilities/constants.py @@ -231,6 +231,20 @@ def _get_local_time_zone() -> ZoneInfo: LOCAL_TIME_ZONE_NAME: TimeZone = cast("TimeZone", LOCAL_TIME_ZONE.key) +# tzlocal -> whenever + + +def _get_now_local() -> ZonedDateTime: + """Get the current zoned date-time in the local time-zone.""" + return ZonedDateTime.now(LOCAL_TIME_ZONE_NAME) + + +NOW_LOCAL = _get_now_local() +TODAY_LOCAL = NOW_LOCAL.date() +TIME_LOCAL = NOW_LOCAL.time() +NOW_LOCAL_PLAIN = NOW_LOCAL.to_plain() + + # whenever @@ -292,6 +306,17 @@ def _get_local_time_zone() -> ZoneInfo: ZONED_DATE_TIME_MAX: ZonedDateTime = PlainDateTime.MAX.assume_tz(UTC.key) +def _get_now(time_zone: str = UTC.key, /) -> ZonedDateTime: + """Get the current zoned date-time.""" + return ZonedDateTime.now(time_zone) + + +NOW_UTC = _get_now() +TODAY_UTC = NOW_UTC.date() +TIME_UTC = NOW_UTC.time() +NOW_UTC_PLAIN = NOW_UTC.to_plain() + + __all__ = [ "BRACKETS", "CPU_COUNT", @@ -349,6 +374,10 @@ def _get_local_time_zone() -> ZoneInfo: "NANOSECOND", "NANOSECONDS_PER_DAY", "NANOSECONDS_PER_SECOND", + "NOW_LOCAL", + "NOW_LOCAL_PLAIN", + "NOW_UTC", + "NOW_UTC_PLAIN", "PWD", "ROOT_GROUP_NAME", "ROOT_USER_NAME", @@ -359,6 +388,10 @@ def _get_local_time_zone() -> ZoneInfo: "TEMP_DIR", "TIME_DELTA_MAX", "TIME_DELTA_MIN", + "TIME_LOCAL", + "TIME_UTC", + "TODAY_LOCAL", + "TODAY_UTC", "USER", "UTC", "WEEK", diff --git a/src/utilities/whenever.py b/src/utilities/whenever.py index 802a0fe60..7868bde34 100644 --- a/src/utilities/whenever.py +++ b/src/utilities/whenever.py @@ -32,7 +32,8 @@ ZonedDateTime, ) -from utilities.constants import LOCAL_TIME_ZONE, LOCAL_TIME_ZONE_NAME, UTC +from utilities.constants import LOCAL_TIME_ZONE, LOCAL_TIME_ZONE_NAME, UTC, _get_now +from utilities.constants import _get_now_local as get_now_local from utilities.dataclasses import replace_non_sentinel from utilities.functions import get_class_name from utilities.math import sign @@ -1946,13 +1947,6 @@ def __str__(self) -> str: "DATE_TIME_DELTA_PARSABLE_MIN", "DATE_TWO_DIGIT_YEAR_MAX", "DATE_TWO_DIGIT_YEAR_MIN", - "NOW_LOCAL", - "NOW_LOCAL_PLAIN", - "NOW_PLAIN", - "TIME_LOCAL", - "TIME_UTC", - "TODAY_LOCAL", - "TODAY_UTC", "DatePeriod", "DatePeriodError", "MeanDateTimeError", From c30dec5ae4f07038221953547ea597bc755bbe94 Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 14:35:41 +0900 Subject: [PATCH 28/35] 2026-01-17 14:35:41 (Sat) > DW-Mac > derekwan --- .bumpversion.toml | 2 +- pyproject.toml | 2 +- src/tests/test_functions.py | 2 +- src/tests/test_polars.py | 2 +- src/utilities/__init__.py | 2 +- uv.lock | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.bumpversion.toml b/.bumpversion.toml index a75c0aa69..f1f152b83 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,6 +1,6 @@ [tool.bumpversion] allow_dirty = true - current_version = "0.182.8" + current_version = "0.183.0" [[tool.bumpversion.files]] filename = "pyproject.toml" diff --git a/pyproject.toml b/pyproject.toml index 6b32b84a1..ab7658f1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,7 +116,7 @@ name = "dycw-utilities" readme = "README.md" requires-python = ">= 3.12" - version = "0.182.8" + version = "0.183.0" [project.entry-points.pytest11] pytest-randomly = "utilities.pytest_plugins.pytest_randomly" diff --git a/src/tests/test_functions.py b/src/tests/test_functions.py index 36cdcfd15..4604fddf5 100644 --- a/src/tests/test_functions.py +++ b/src/tests/test_functions.py @@ -25,7 +25,7 @@ ) from pytest import approx, mark, param, raises -from utilities.constants import HOME, MILLISECOND, SECOND, ZERO_TIME +from utilities.constants import HOME, MILLISECOND, NOW_UTC, SECOND, ZERO_TIME from utilities.errors import ImpossibleCaseError from utilities.functions import ( EnsureBoolError, diff --git a/src/tests/test_polars.py b/src/tests/test_polars.py index 8d721bc46..6398fd175 100644 --- a/src/tests/test_polars.py +++ b/src/tests/test_polars.py @@ -70,7 +70,7 @@ import tests.test_math import utilities.polars -from utilities.constants import PWD, UTC +from utilities.constants import NOW_UTC, PWD, TODAY_UTC, UTC from utilities.hypothesis import ( assume_does_not_raise, date_deltas, diff --git a/src/utilities/__init__.py b/src/utilities/__init__.py index e2bca4148..a81c9e4a2 100644 --- a/src/utilities/__init__.py +++ b/src/utilities/__init__.py @@ -1,3 +1,3 @@ from __future__ import annotations -__version__ = "0.182.8" +__version__ = "0.183.0" diff --git a/uv.lock b/uv.lock index 0c9cc685e..50f1ad1dd 100644 --- a/uv.lock +++ b/uv.lock @@ -625,7 +625,7 @@ wheels = [ [[package]] name = "dycw-utilities" -version = "0.182.8" +version = "0.183.0" source = { editable = "." } dependencies = [ { name = "atomicwrites" }, From 0ae96625ae8e43f9ec15dd5365b9c343f4fa5442 Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 14:40:36 +0900 Subject: [PATCH 29/35] 2026-01-17 14:40:36 (Sat) > DW-Mac > derekwan --- src/utilities/os.py | 170 +------------------------------------------- 1 file changed, 1 insertion(+), 169 deletions(-) diff --git a/src/utilities/os.py b/src/utilities/os.py index f07d4109b..6e2496a99 100644 --- a/src/utilities/os.py +++ b/src/utilities/os.py @@ -1,182 +1,18 @@ from __future__ import annotations -import shutil from contextlib import suppress from dataclasses import dataclass from os import environ, getenv -from pathlib import Path -from shutil import rmtree -from tempfile import TemporaryDirectory from typing import TYPE_CHECKING, Literal, assert_never, overload, override from utilities.constants import CPU_COUNT from utilities.contextlib import enhanced_context_manager from utilities.iterables import OneStrEmptyError, one_str +from utilities.types import IntOrAll if TYPE_CHECKING: from collections.abc import Iterator, Mapping - from utilities.types import PathLike - - -## - - -def copy(src: PathLike, dest: PathLike, /, *, overwrite: bool = False) -> None: - """Copy/replace a file/directory atomically.""" - try: - _move_or_copy(src, dest, overwrite=overwrite, delete_src=False) - except _MoveOrCopySourceNotFoundError as error: - raise _CopySourceNotFoundError(src=error.src) from None - except _MoveOrCopyDestinationExistsError as error: - raise _CopyDestinationExistsError(src=error.src, dest=error.dest) from None - - -@dataclass(kw_only=True, slots=True) -class CopyError(Exception): ... - - -@dataclass(kw_only=True, slots=True) -class _CopySourceNotFoundError(CopyError): - src: Path - - @override - def __str__(self) -> str: - return f"Source {str(self.src)!r} does not exist" - - -@dataclass(kw_only=True, slots=True) -class _CopyDestinationExistsError(CopyError): - src: Path - dest: Path - - @override - def __str__(self) -> str: - return f"Cannot copy {str(self.src)!r} as destination {str(self.dest)!r} already exists" - - -def move(src: PathLike, dest: PathLike, /, *, overwrite: bool = False) -> None: - """Move/replace a file/directory atomically.""" - try: - _move_or_copy(src, dest, overwrite=overwrite, delete_src=True) - except _MoveOrCopySourceNotFoundError as error: - raise _MoveSourceNotFoundError(src=error.src) from None - except _MoveOrCopyDestinationExistsError as error: - raise _MoveDestinationExistsError(src=error.src, dest=error.dest) from None - - -@dataclass(kw_only=True, slots=True) -class MoveError(Exception): ... - - -@dataclass(kw_only=True, slots=True) -class _MoveSourceNotFoundError(MoveError): - src: Path - - @override - def __str__(self) -> str: - return f"Source {str(self.src)!r} does not exist" - - -@dataclass(kw_only=True, slots=True) -class _MoveDestinationExistsError(MoveError): - src: Path - dest: Path - - @override - def __str__(self) -> str: - return f"Cannot move {str(self.src)!r} as destination {str(self.dest)!r} already exists" - - -def _move_or_copy( - src: PathLike, - dest: PathLike, - /, - *, - overwrite: bool = False, - delete_src: bool = False, -) -> None: - src, dest = map(Path, [src, dest]) - if not src.exists(): - raise _MoveOrCopySourceNotFoundError(src=src) - if dest.exists() and not overwrite: - raise _MoveOrCopyDestinationExistsError(src=src, dest=dest) - if src.is_file(): - _move_or_copy_file(src, dest, overwrite=overwrite, delete_src=delete_src) - elif src.is_dir(): - _move_or_copy_dir(src, dest, overwrite=overwrite, delete_src=delete_src) - else: # pragma: no cover - raise TypeError(src) - - -def _move_or_copy_file( - src: PathLike, - dest: PathLike, - /, - *, - overwrite: bool = False, - delete_src: bool = False, -) -> None: - src, dest = map(Path, [src, dest]) - name, dir_ = dest.name, dest.parent - if (not dest.exists()) or (dest.is_file() and overwrite): - ... - elif dest.is_dir() and overwrite: - rmtree(dest, ignore_errors=True) - else: # pragma: no cover - raise RuntimeError(dest, overwrite) - with TemporaryDirectory(suffix=".tmp", prefix=name, dir=dir_) as temp_dir: - temp_file = Path(temp_dir, src.name) - _ = shutil.copyfile(src, temp_file) - _ = temp_file.replace(dest) - if delete_src: - src.unlink(missing_ok=True) - - -def _move_or_copy_dir( - src: PathLike, - dest: PathLike, - /, - *, - overwrite: bool = False, - delete_src: bool = False, -) -> None: - src, dest = map(Path, [src, dest]) - name, dir_ = dest.name, dest.parent - if (not dest.exists()) or (dest.is_dir() and overwrite): - ... - elif dest.is_file() and overwrite: - dest.unlink(missing_ok=True) - else: # pragma: no cover - raise RuntimeError(dest, overwrite) - with TemporaryDirectory(suffix=".tmp", prefix=name, dir=dir_) as temp_dir: - temp_file = Path(temp_dir, src.name) - _ = shutil.copyfile(src, temp_file) - _ = temp_file.replace(dest) - if delete_src: - rmtree(src, ignore_errors=True) - - -@dataclass(kw_only=True, slots=True) -class _MoveOrCopyError(Exception): ... - - -@dataclass(kw_only=True, slots=True) -class _MoveOrCopySourceNotFoundError(_MoveOrCopyError): - src: Path - - -@dataclass(kw_only=True, slots=True) -class _MoveOrCopyDestinationExistsError(_MoveOrCopyError): - src: Path - dest: Path - - -## - - -type IntOrAll = int | Literal["all"] - def get_cpu_use(*, n: IntOrAll = "all") -> int: """Resolve for the number of CPUs to use.""" @@ -303,15 +139,11 @@ def apply(mapping: Mapping[str, str | None], /) -> None: __all__ = [ - "CopyError", "GetCPUUseError", "IntOrAll", - "MoveError", - "copy", "get_cpu_use", "get_env_var", "is_debug", "is_pytest", - "move", "temp_environ", ] From b5448e1ef9a637afdc05f4446c63bb701504b8c0 Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 14:55:48 +0900 Subject: [PATCH 30/35] 2026-01-17 14:55:48 (Sat) > DW-Mac > derekwan --- src/tests/test_constants.py | 41 +++++++++++++++++++++++++++- src/tests/test_sentinel.py | 13 +++++++++ src/utilities/constants.py | 53 ++++++++++++++++++++++++++++++++++++- src/utilities/os.py | 4 +-- src/utilities/parse.py | 8 +----- src/utilities/sentinel.py | 15 +++++++++++ 6 files changed, 123 insertions(+), 11 deletions(-) create mode 100644 src/tests/test_sentinel.py create mode 100644 src/utilities/sentinel.py diff --git a/src/tests/test_constants.py b/src/tests/test_constants.py index b68317f9d..c6f7c9d84 100644 --- a/src/tests/test_constants.py +++ b/src/tests/test_constants.py @@ -2,9 +2,11 @@ from pathlib import Path from random import SystemRandom -from typing import assert_never +from typing import TYPE_CHECKING, assert_never from zoneinfo import ZoneInfo +from hypothesis import given +from hypothesis.strategies import sampled_from from pytest import mark, param, raises from whenever import ( Date, @@ -17,6 +19,7 @@ ) from utilities.constants import ( + _SENTINEL_REPR, CPU_COUNT, DATE_DELTA_MAX, DATE_DELTA_MIN, @@ -53,11 +56,17 @@ USER, ZONED_DATE_TIME_MAX, ZONED_DATE_TIME_MIN, + Sentinel, + SentinelParseError, + sentinel, ) from utilities.platform import SYSTEM from utilities.types import System, TimeZone from utilities.typing import get_literal_elements +if TYPE_CHECKING: + from collections.abc import Callable + class TestCPUCount: def test_main(self) -> None: @@ -167,6 +176,36 @@ def test_main(self, *, path: Path) -> None: assert path.is_dir() +class TestSentinel: + def test_isinstance(self) -> None: + assert isinstance(sentinel, Sentinel) + + @mark.parametrize( + "text", + [ + param("", id="blank"), + param(_SENTINEL_REPR, id="default"), + param(_SENTINEL_REPR.lower(), id="lower"), + param(_SENTINEL_REPR.upper(), id="upper"), + ], + ) + def test_parse(self, *, text: str) -> None: + result = Sentinel.parse(text) + assert result is sentinel + + @mark.parametrize("method", [param(repr), param(str)]) + def test_repr_and_str(self, method: Callable[..., str]) -> None: + assert method(sentinel) == _SENTINEL_REPR + + def test_singleton(self) -> None: + assert Sentinel() is sentinel + + @given(text=sampled_from(["invalid", "ssentinell"])) + def test_error_parse(self, *, text: str) -> None: + with raises(SentinelParseError, match=r"Unable to parse sentinel; got '.*'"): + _ = Sentinel.parse(text) + + class TestSystemRandom: def test_main(self) -> None: assert isinstance(SYSTEM_RANDOM, SystemRandom) diff --git a/src/tests/test_sentinel.py b/src/tests/test_sentinel.py new file mode 100644 index 000000000..085b0555d --- /dev/null +++ b/src/tests/test_sentinel.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from typing import Any + +from pytest import mark, param + +from utilities.sentinel import is_sentinel, sentinel + + +class TestIsSentinel: + @mark.parametrize(("obj", "expected"), [param(None, False), param(sentinel, True)]) + def test_main(self, *, obj: Any, expected: bool) -> None: + assert is_sentinel(obj) is expected diff --git a/src/utilities/constants.py b/src/utilities/constants.py index d574a5d05..0f2660caa 100644 --- a/src/utilities/constants.py +++ b/src/utilities/constants.py @@ -1,13 +1,15 @@ from __future__ import annotations +from dataclasses import dataclass from getpass import getuser from logging import getLogger from os import cpu_count, environ from pathlib import Path from platform import system from random import SystemRandom +from re import IGNORECASE, search from tempfile import gettempdir -from typing import TYPE_CHECKING, assert_never, cast +from typing import TYPE_CHECKING, Any, assert_never, cast, override from zoneinfo import ZoneInfo from tzlocal import get_localzone @@ -208,6 +210,55 @@ def _get_uid_name(uid: int, /) -> str | None: RICH_EXPAND_ALL: bool = False +# sentinel + + +class _Meta(type): + """Metaclass for the sentinel.""" + + instance: Any = None + + @override + def __call__(cls, *args: Any, **kwargs: Any) -> Any: + if cls.instance is None: + cls.instance = super().__call__(*args, **kwargs) + return cls.instance + + +_SENTINEL_REPR = "" + + +class Sentinel(metaclass=_Meta): + """Base class for the sentinel object.""" + + @override + def __repr__(self) -> str: + return _SENTINEL_REPR + + @override + def __str__(self) -> str: + return repr(self) + + @classmethod + def parse(cls, text: str, /) -> Sentinel: + """Parse text into the Sentinel value.""" + if search("^(|sentinel|)$", text, flags=IGNORECASE): + return sentinel + raise SentinelParseError(text=text) + + +@dataclass(kw_only=True, slots=True) +class SentinelParseError(Exception): + text: str + + @override + def __str__(self) -> str: + return f"Unable to parse sentinel; got {self.text!r}" + + +sentinel = Sentinel() + + # tempfile diff --git a/src/utilities/os.py b/src/utilities/os.py index 6e2496a99..3a5d05978 100644 --- a/src/utilities/os.py +++ b/src/utilities/os.py @@ -8,11 +8,12 @@ from utilities.constants import CPU_COUNT from utilities.contextlib import enhanced_context_manager from utilities.iterables import OneStrEmptyError, one_str -from utilities.types import IntOrAll if TYPE_CHECKING: from collections.abc import Iterator, Mapping + from utilities.types import IntOrAll + def get_cpu_use(*, n: IntOrAll = "all") -> int: """Resolve for the number of CPUs to use.""" @@ -140,7 +141,6 @@ def apply(mapping: Mapping[str, str | None], /) -> None: __all__ = [ "GetCPUUseError", - "IntOrAll", "get_cpu_use", "get_env_var", "is_debug", diff --git a/src/utilities/parse.py b/src/utilities/parse.py index 120342ca1..90793db72 100644 --- a/src/utilities/parse.py +++ b/src/utilities/parse.py @@ -21,13 +21,7 @@ ZonedDateTime, ) -from utilities.constants import ( - BRACKETS, - LIST_SEPARATOR, - PAIR_SEPARATOR, - Sentinel, - SentinelParseError, -) +from utilities.constants import Sentinel, SentinelParseError from utilities.enum import ParseEnumError, parse_enum from utilities.iterables import OneEmptyError, OneNonUniqueError, one, one_str from utilities.math import ParseNumberError, parse_number diff --git a/src/utilities/sentinel.py b/src/utilities/sentinel.py new file mode 100644 index 000000000..2251f3954 --- /dev/null +++ b/src/utilities/sentinel.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from typing import Any + +from typing_extensions import TypeIs + +from utilities.constants import Sentinel, sentinel + + +def is_sentinel(obj: Any, /) -> TypeIs[Sentinel]: + """Check if an object is the sentinel.""" + return obj is sentinel + + +__all__ = ["is_sentinel"] From a1fcaf427c7830dec57b5126eb76bab374f60dc8 Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 18:28:49 +0900 Subject: [PATCH 31/35] 2026-01-17 18:28:49 (Sat) > DW-Mac > derekwan --- src/tests/test_constants.py | 4 +--- src/tests/test_functions.py | 6 +++--- src/tests/test_hypothesis.py | 1 - src/tests/test_orjson.py | 2 ++ src/tests/test_pathlib.py | 2 +- src/tests/test_polars.py | 2 +- src/tests/test_sentinel.py | 13 ------------- src/utilities/dataclasses.py | 8 +------- src/utilities/hypothesis.py | 1 + src/utilities/sentinel.py | 15 --------------- src/utilities/text.py | 2 +- src/utilities/whenever.py | 9 ++++++++- 12 files changed, 19 insertions(+), 46 deletions(-) delete mode 100644 src/tests/test_sentinel.py delete mode 100644 src/utilities/sentinel.py diff --git a/src/tests/test_constants.py b/src/tests/test_constants.py index c6f7c9d84..ba770b2f6 100644 --- a/src/tests/test_constants.py +++ b/src/tests/test_constants.py @@ -5,8 +5,6 @@ from typing import TYPE_CHECKING, assert_never from zoneinfo import ZoneInfo -from hypothesis import given -from hypothesis.strategies import sampled_from from pytest import mark, param, raises from whenever import ( Date, @@ -200,7 +198,7 @@ def test_repr_and_str(self, method: Callable[..., str]) -> None: def test_singleton(self) -> None: assert Sentinel() is sentinel - @given(text=sampled_from(["invalid", "ssentinell"])) + @mark.parametrize("text", [param("invalid"), param("ssentinell")]) def test_error_parse(self, *, text: str) -> None: with raises(SentinelParseError, match=r"Unable to parse sentinel; got '.*'"): _ = Sentinel.parse(text) diff --git a/src/tests/test_functions.py b/src/tests/test_functions.py index 4604fddf5..898826de0 100644 --- a/src/tests/test_functions.py +++ b/src/tests/test_functions.py @@ -25,7 +25,7 @@ ) from pytest import approx, mark, param, raises -from utilities.constants import HOME, MILLISECOND, NOW_UTC, SECOND, ZERO_TIME +from utilities.constants import HOME, MILLISECOND, NOW_UTC, SECOND, ZERO_TIME, sentinel from utilities.errors import ImpossibleCaseError from utilities.functions import ( EnsureBoolError, @@ -586,8 +586,8 @@ def test_main(self, *, duration: Duration) -> None: class TestIsNoneAndIsNotNone: - @mark.parametrize( - ("func", "obj", "expected"), + @mark.parameter( + "func, obj, expected", [ param(is_none, None, True), param(is_none, 0, False), diff --git a/src/tests/test_hypothesis.py b/src/tests/test_hypothesis.py index 9162eca9a..162289f51 100644 --- a/src/tests/test_hypothesis.py +++ b/src/tests/test_hypothesis.py @@ -127,7 +127,6 @@ from utilities.iterables import one from utilities.libcst import parse_import from utilities.platform import maybe_lower_case -from utilities.sentinel import is_sentinel from utilities.version import Version from utilities.whenever import ( DATE_TWO_DIGIT_YEAR_MAX, diff --git a/src/tests/test_orjson.py b/src/tests/test_orjson.py index 458b354b6..dc17f9ffb 100644 --- a/src/tests/test_orjson.py +++ b/src/tests/test_orjson.py @@ -57,6 +57,8 @@ MINUTE, SECOND, UTC, + Sentinel, + sentinel, ) from utilities.hypothesis import ( date_periods, diff --git a/src/tests/test_pathlib.py b/src/tests/test_pathlib.py index c43bc51b3..0f6c534a7 100644 --- a/src/tests/test_pathlib.py +++ b/src/tests/test_pathlib.py @@ -10,7 +10,7 @@ from pytest import mark, param, raises from utilities.atomicwrites import copy -from utilities.constants import HOME, SYSTEM +from utilities.constants import HOME, SYSTEM, Sentinel, sentinel from utilities.dataclasses import replace_non_sentinel from utilities.hypothesis import git_repos, pairs, paths, temp_paths from utilities.pathlib import ( diff --git a/src/tests/test_polars.py b/src/tests/test_polars.py index 6398fd175..3eecf37cf 100644 --- a/src/tests/test_polars.py +++ b/src/tests/test_polars.py @@ -70,7 +70,7 @@ import tests.test_math import utilities.polars -from utilities.constants import NOW_UTC, PWD, TODAY_UTC, UTC +from utilities.constants import NOW_UTC, PWD, TODAY_UTC, UTC, Sentinel, sentinel from utilities.hypothesis import ( assume_does_not_raise, date_deltas, diff --git a/src/tests/test_sentinel.py b/src/tests/test_sentinel.py deleted file mode 100644 index 085b0555d..000000000 --- a/src/tests/test_sentinel.py +++ /dev/null @@ -1,13 +0,0 @@ -from __future__ import annotations - -from typing import Any - -from pytest import mark, param - -from utilities.sentinel import is_sentinel, sentinel - - -class TestIsSentinel: - @mark.parametrize(("obj", "expected"), [param(None, False), param(sentinel, True)]) - def test_main(self, *, obj: Any, expected: bool) -> None: - assert is_sentinel(obj) is expected diff --git a/src/utilities/dataclasses.py b/src/utilities/dataclasses.py index a4e163454..29c6710c8 100644 --- a/src/utilities/dataclasses.py +++ b/src/utilities/dataclasses.py @@ -5,13 +5,7 @@ from dataclasses import MISSING, dataclass, field, fields, replace from typing import TYPE_CHECKING, Any, Literal, assert_never, overload, override -from utilities.constants import ( - BRACKETS, - LIST_SEPARATOR, - PAIR_SEPARATOR, - Sentinel, - sentinel, -) +from utilities.constants import Sentinel, sentinel from utilities.errors import ImpossibleCaseError from utilities.functions import get_class_name, is_sentinel from utilities.iterables import ( diff --git a/src/utilities/hypothesis.py b/src/utilities/hypothesis.py index b53373111..df3e1b33f 100644 --- a/src/utilities/hypothesis.py +++ b/src/utilities/hypothesis.py @@ -96,6 +96,7 @@ from utilities.os import get_env_var from utilities.pathlib import module_path, temp_cwd from utilities.permissions import Permissions +from utilities.sentinel import is_sentinel from utilities.tempfile import TemporaryDirectory from utilities.version import Version from utilities.whenever import ( diff --git a/src/utilities/sentinel.py b/src/utilities/sentinel.py deleted file mode 100644 index 2251f3954..000000000 --- a/src/utilities/sentinel.py +++ /dev/null @@ -1,15 +0,0 @@ -from __future__ import annotations - -from typing import Any - -from typing_extensions import TypeIs - -from utilities.constants import Sentinel, sentinel - - -def is_sentinel(obj: Any, /) -> TypeIs[Sentinel]: - """Check if an object is the sentinel.""" - return obj is sentinel - - -__all__ = ["is_sentinel"] diff --git a/src/utilities/text.py b/src/utilities/text.py index 92606c854..ed901fc9a 100644 --- a/src/utilities/text.py +++ b/src/utilities/text.py @@ -21,7 +21,7 @@ ) from uuid import uuid4 -from utilities.constants import BRACKETS, LIST_SEPARATOR, PAIR_SEPARATOR, Sentinel +from utilities.constants import Sentinel from utilities.iterables import CheckDuplicatesError, check_duplicates, transpose from utilities.reprlib import get_repr diff --git a/src/utilities/whenever.py b/src/utilities/whenever.py index 7868bde34..26e8ba01b 100644 --- a/src/utilities/whenever.py +++ b/src/utilities/whenever.py @@ -32,7 +32,14 @@ ZonedDateTime, ) -from utilities.constants import LOCAL_TIME_ZONE, LOCAL_TIME_ZONE_NAME, UTC, _get_now +from utilities.constants import ( + LOCAL_TIME_ZONE, + LOCAL_TIME_ZONE_NAME, + UTC, + Sentinel, + _get_now, + sentinel, +) from utilities.constants import _get_now_local as get_now_local from utilities.dataclasses import replace_non_sentinel from utilities.functions import get_class_name From 5e65e7d2fb729728165ff3dd88f1889a45565aed Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sat, 17 Jan 2026 20:45:38 +0900 Subject: [PATCH 32/35] 2026-01-17 20:45:38 (Sat) > DW-Mac > derekwan --- .bumpversion.toml | 2 +- pyproject.toml | 2 +- src/tests/test_constants.py | 6 ++++++ src/utilities/__init__.py | 2 +- src/utilities/constants.py | 18 ++++++++++++++++++ src/utilities/dataclasses.py | 8 +++++++- src/utilities/hypothesis.py | 1 - src/utilities/parse.py | 8 +++++++- src/utilities/text.py | 2 +- uv.lock | 2 +- 10 files changed, 43 insertions(+), 8 deletions(-) diff --git a/.bumpversion.toml b/.bumpversion.toml index f1f152b83..afe4db406 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,6 +1,6 @@ [tool.bumpversion] allow_dirty = true - current_version = "0.183.0" + current_version = "0.183.1" [[tool.bumpversion.files]] filename = "pyproject.toml" diff --git a/pyproject.toml b/pyproject.toml index ab7658f1b..c5c8af2bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,7 +116,7 @@ name = "dycw-utilities" readme = "README.md" requires-python = ">= 3.12" - version = "0.183.0" + version = "0.183.1" [project.entry-points.pytest11] pytest-randomly = "utilities.pytest_plugins.pytest_randomly" diff --git a/src/tests/test_constants.py b/src/tests/test_constants.py index ba770b2f6..5fce7389d 100644 --- a/src/tests/test_constants.py +++ b/src/tests/test_constants.py @@ -26,6 +26,7 @@ EFFECTIVE_USER_ID, EFFECTIVE_USER_NAME, HOME, + HOSTNAME, IS_LINUX, IS_MAC, IS_NOT_LINUX, @@ -125,6 +126,11 @@ def test_main(self, *, group: str | None) -> None: assert_never(never) +class TestHostname: + def test_main(self) -> None: + assert isinstance(HOSTNAME, str) + + class TestLocalTimeZone: def test_main(self) -> None: assert isinstance(LOCAL_TIME_ZONE, ZoneInfo) diff --git a/src/utilities/__init__.py b/src/utilities/__init__.py index a81c9e4a2..77d0057e8 100644 --- a/src/utilities/__init__.py +++ b/src/utilities/__init__.py @@ -1,3 +1,3 @@ from __future__ import annotations -__version__ = "0.183.0" +__version__ = "0.183.1" diff --git a/src/utilities/constants.py b/src/utilities/constants.py index 0f2660caa..0bb8ac8f5 100644 --- a/src/utilities/constants.py +++ b/src/utilities/constants.py @@ -8,6 +8,7 @@ from platform import system from random import SystemRandom from re import IGNORECASE, search +from socket import gethostname from tempfile import gettempdir from typing import TYPE_CHECKING, Any, assert_never, cast, override from zoneinfo import ZoneInfo @@ -259,12 +260,26 @@ def __str__(self) -> str: sentinel = Sentinel() +# socket + + +HOSTNAME = gethostname() + + # tempfile TEMP_DIR: Path = Path(gettempdir()) +# text + + +LIST_SEPARATOR: str = "," +PAIR_SEPARATOR: str = "=" +BRACKETS: set[tuple[str, str]] = {("(", ")"), ("[", "]"), ("{", "}")} + + # tzlocal @@ -381,6 +396,7 @@ def _get_now(time_zone: str = UTC.key, /) -> ZonedDateTime: "EFFECTIVE_USER_ID", "EFFECTIVE_USER_NAME", "HOME", + "HOSTNAME", "HOUR", "IS_CI", "IS_CI_AND_LINUX", @@ -395,6 +411,7 @@ def _get_now(time_zone: str = UTC.key, /) -> ZonedDateTime: "IS_NOT_MAC", "IS_NOT_WINDOWS", "IS_WINDOWS", + "LIST_SEPARATOR", "LOCAL_TIME_ZONE", "LOCAL_TIME_ZONE_NAME", "MAX_FLOAT32", @@ -429,6 +446,7 @@ def _get_now(time_zone: str = UTC.key, /) -> ZonedDateTime: "NOW_LOCAL_PLAIN", "NOW_UTC", "NOW_UTC_PLAIN", + "PAIR_SEPARATOR", "PWD", "ROOT_GROUP_NAME", "ROOT_USER_NAME", diff --git a/src/utilities/dataclasses.py b/src/utilities/dataclasses.py index 29c6710c8..a4e163454 100644 --- a/src/utilities/dataclasses.py +++ b/src/utilities/dataclasses.py @@ -5,7 +5,13 @@ from dataclasses import MISSING, dataclass, field, fields, replace from typing import TYPE_CHECKING, Any, Literal, assert_never, overload, override -from utilities.constants import Sentinel, sentinel +from utilities.constants import ( + BRACKETS, + LIST_SEPARATOR, + PAIR_SEPARATOR, + Sentinel, + sentinel, +) from utilities.errors import ImpossibleCaseError from utilities.functions import get_class_name, is_sentinel from utilities.iterables import ( diff --git a/src/utilities/hypothesis.py b/src/utilities/hypothesis.py index df3e1b33f..b53373111 100644 --- a/src/utilities/hypothesis.py +++ b/src/utilities/hypothesis.py @@ -96,7 +96,6 @@ from utilities.os import get_env_var from utilities.pathlib import module_path, temp_cwd from utilities.permissions import Permissions -from utilities.sentinel import is_sentinel from utilities.tempfile import TemporaryDirectory from utilities.version import Version from utilities.whenever import ( diff --git a/src/utilities/parse.py b/src/utilities/parse.py index 90793db72..120342ca1 100644 --- a/src/utilities/parse.py +++ b/src/utilities/parse.py @@ -21,7 +21,13 @@ ZonedDateTime, ) -from utilities.constants import Sentinel, SentinelParseError +from utilities.constants import ( + BRACKETS, + LIST_SEPARATOR, + PAIR_SEPARATOR, + Sentinel, + SentinelParseError, +) from utilities.enum import ParseEnumError, parse_enum from utilities.iterables import OneEmptyError, OneNonUniqueError, one, one_str from utilities.math import ParseNumberError, parse_number diff --git a/src/utilities/text.py b/src/utilities/text.py index ed901fc9a..92606c854 100644 --- a/src/utilities/text.py +++ b/src/utilities/text.py @@ -21,7 +21,7 @@ ) from uuid import uuid4 -from utilities.constants import Sentinel +from utilities.constants import BRACKETS, LIST_SEPARATOR, PAIR_SEPARATOR, Sentinel from utilities.iterables import CheckDuplicatesError, check_duplicates, transpose from utilities.reprlib import get_repr diff --git a/uv.lock b/uv.lock index 50f1ad1dd..8430dd912 100644 --- a/uv.lock +++ b/uv.lock @@ -625,7 +625,7 @@ wheels = [ [[package]] name = "dycw-utilities" -version = "0.183.0" +version = "0.183.1" source = { editable = "." } dependencies = [ { name = "atomicwrites" }, From e821d117bbe4942677fc68478e32a2d06c850df8 Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sun, 18 Jan 2026 08:13:20 +0900 Subject: [PATCH 33/35] 2026-01-18 08:13:20 (Sun) > DW-Mac > derekwan --- .bumpversion.toml | 2 +- pyproject.toml | 2 +- src/utilities/__init__.py | 2 +- uv.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.toml b/.bumpversion.toml index afe4db406..9a53bc766 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,6 +1,6 @@ [tool.bumpversion] allow_dirty = true - current_version = "0.183.1" + current_version = "0.183.5" [[tool.bumpversion.files]] filename = "pyproject.toml" diff --git a/pyproject.toml b/pyproject.toml index c5c8af2bf..ecf6c3693 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,7 +116,7 @@ name = "dycw-utilities" readme = "README.md" requires-python = ">= 3.12" - version = "0.183.1" + version = "0.183.5" [project.entry-points.pytest11] pytest-randomly = "utilities.pytest_plugins.pytest_randomly" diff --git a/src/utilities/__init__.py b/src/utilities/__init__.py index 77d0057e8..b74045cbb 100644 --- a/src/utilities/__init__.py +++ b/src/utilities/__init__.py @@ -1,3 +1,3 @@ from __future__ import annotations -__version__ = "0.183.1" +__version__ = "0.183.5" diff --git a/uv.lock b/uv.lock index 8430dd912..aff2becfd 100644 --- a/uv.lock +++ b/uv.lock @@ -625,7 +625,7 @@ wheels = [ [[package]] name = "dycw-utilities" -version = "0.183.1" +version = "0.183.5" source = { editable = "." } dependencies = [ { name = "atomicwrites" }, From c9a34d4ad25f9d960bac2b53e191561e0325bb94 Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sun, 18 Jan 2026 08:14:23 +0900 Subject: [PATCH 34/35] 2026-01-18 08:14:23 (Sun) > DW-Mac > derekwan --- src/tests/test_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/test_functions.py b/src/tests/test_functions.py index 898826de0..bcf1b7a15 100644 --- a/src/tests/test_functions.py +++ b/src/tests/test_functions.py @@ -586,8 +586,8 @@ def test_main(self, *, duration: Duration) -> None: class TestIsNoneAndIsNotNone: - @mark.parameter( - "func, obj, expected", + @mark.parametrize( + ("func", "obj", "expected"), [ param(is_none, None, True), param(is_none, 0, False), From 2d8f368a5664b7e4f0c6dc1f77e58595062eb77b Mon Sep 17 00:00:00 2001 From: github-actions-bot Date: Sun, 18 Jan 2026 08:58:33 +0900 Subject: [PATCH 35/35] 2026-01-18 08:58:33 (Sun) > DW-Mac > derekwan --- .bumpversion.toml | 2 +- pyproject.toml | 2 +- src/utilities/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.toml b/.bumpversion.toml index 9a53bc766..17cb78525 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,6 +1,6 @@ [tool.bumpversion] allow_dirty = true - current_version = "0.183.5" + current_version = "0.183.6" [[tool.bumpversion.files]] filename = "pyproject.toml" diff --git a/pyproject.toml b/pyproject.toml index ecf6c3693..886ac12a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,7 +116,7 @@ name = "dycw-utilities" readme = "README.md" requires-python = ">= 3.12" - version = "0.183.5" + version = "0.183.6" [project.entry-points.pytest11] pytest-randomly = "utilities.pytest_plugins.pytest_randomly" diff --git a/src/utilities/__init__.py b/src/utilities/__init__.py index b74045cbb..0a8eac948 100644 --- a/src/utilities/__init__.py +++ b/src/utilities/__init__.py @@ -1,3 +1,3 @@ from __future__ import annotations -__version__ = "0.183.5" +__version__ = "0.183.6"