From a418f56aab9dd1f1f3be2fa880be2d02d6a89f17 Mon Sep 17 00:00:00 2001 From: Zen Date: Sun, 11 Jan 2026 22:03:38 -0600 Subject: [PATCH 1/4] fix validated dataclass annotations for 3.14 also add more tests Signed-off-by: Zen --- src/zenlib/types/validated_dataclass.py | 67 +++++++++++++------------ tests/test_validated_dataclass.py | 20 ++++++++ 2 files changed, 54 insertions(+), 33 deletions(-) diff --git a/src/zenlib/types/validated_dataclass.py b/src/zenlib/types/validated_dataclass.py index 2d7bb4f..11e2358 100644 --- a/src/zenlib/types/validated_dataclass.py +++ b/src/zenlib/types/validated_dataclass.py @@ -2,41 +2,42 @@ from typing import ForwardRef, Union, get_args, get_origin, get_type_hints +class ValidatedDataclass: + def __setattr__(self, attribute, value): + value = self._validate_attribute(attribute, value) + super().__setattr__(attribute, value) + + def _validate_attribute(self, attribute, value): + """Ensures the attribute is the correct type""" + if attribute == "logger": + return value + if value is None: + return + + expected_type = self.__class__.__annotations__.get(attribute) + if not expected_type: + return value # No type hint, so we can't validate it + if get_origin(expected_type) is Union and isinstance(get_args(expected_type)[0], ForwardRef): + expected_type = get_type_hints(self.__class__)[attribute] + + if not isinstance(value, expected_type): + try: + value = expected_type(value) + except ValueError: + raise TypeError(f"[{attribute}] Type mismatch: '{expected_type}' != {type(value)}") + return value + + def validatedDataclass(cls): from zenlib.logging import loggify - from zenlib.util import merge_class - cls = loggify(dataclass(cls)) - base_annotations = {} + annotations = {} for base in cls.__mro__: - base_annotations.update(getattr(base, "__annotations__", {})) - - cls.__annotations__.update(base_annotations) - - class ValidatedDataclass(cls): - def __setattr__(self, attribute, value): - value = self._validate_attribute(attribute, value) - super().__setattr__(attribute, value) - - def _validate_attribute(self, attribute, value): - """Ensures the attribute is the correct type""" - if attribute == "logger": - return value - if value is None: - return - - expected_type = self.__class__.__annotations__.get(attribute) - if not expected_type: - return value # No type hint, so we can't validate it - if get_origin(expected_type) is Union and isinstance(get_args(expected_type)[0], ForwardRef): - expected_type = get_type_hints(self.__class__)[attribute] - - if not isinstance(value, expected_type): - try: - value = expected_type(value) - except ValueError: - raise TypeError(f"[{attribute}] Type mismatch: '{expected_type}' != {type(value)}") - return value + annotations.update(getattr(base, "__annotations__", {})) + + cls_dict = dict(cls.__dict__) + cls_dict["__annotations__"] = annotations + + vdc = loggify(dataclass(type(cls.__name__, (ValidatedDataclass, cls), cls_dict))) - merge_class(cls, ValidatedDataclass, ignored_attributes=["__setattr__"]) - return ValidatedDataclass + return vdc diff --git a/tests/test_validated_dataclass.py b/tests/test_validated_dataclass.py index 76d739f..a723046 100644 --- a/tests/test_validated_dataclass.py +++ b/tests/test_validated_dataclass.py @@ -9,6 +9,12 @@ class testDataClass: b: str = None +@validatedDataclass +class anotherDataClass: + x: float = 0.0 + y: str = "default" + + class TestValidatedDataclass(TestCase): def test_validated_dataclass(self): c = testDataClass() @@ -16,11 +22,25 @@ def test_validated_dataclass(self): c.b = "test" self.assertTrue(hasattr(c, "logger")) + def test_default_values(self): + d = anotherDataClass() + self.assertEqual(d.x, 0.0) + self.assertEqual(d.y, "default") + d.x = 3.14 + d.y = "hello" + self.assertEqual(d.x, 3.14) + self.assertEqual(d.y, "hello") + def test_bad_type(self): c = testDataClass() + d = anotherDataClass() + with self.assertRaises(TypeError): c.a = "test" + with self.assertRaises(TypeError): + d.x = "not a float" + if __name__ == "__main__": main() From ab9b4e41ccff8b565741656b2ae9179dfa8b453b Mon Sep 17 00:00:00 2001 From: Zen Date: Sun, 11 Jan 2026 22:04:20 -0600 Subject: [PATCH 2/4] add ci tests for 3.14 Signed-off-by: Zen --- .github/workflows/unit_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index ba766fc..02c1db2 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.11", "3.12", "3.13"] + python-version: ["3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 From 9539fa6b066eacede8ab8a2ba5c362b134ea0974 Mon Sep 17 00:00:00 2001 From: Zen Date: Sun, 11 Jan 2026 22:04:36 -0600 Subject: [PATCH 3/4] bup ver Signed-off-by: Zen --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cdd3622..cc5e92d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "zenlib" -version = "3.1.8" +version = "3.1.9" authors = [ { name="Desultory", email="dev@pyl.onl" }, ] From a2c2afb52f46b6fd72dc0677df9aa37db283753e Mon Sep 17 00:00:00 2001 From: Zen Date: Sun, 11 Jan 2026 22:28:23 -0600 Subject: [PATCH 4/4] use an event to track unshared process, avoid race Signed-off-by: Zen --- src/zenlib/namespace/namespace_process.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/zenlib/namespace/namespace_process.py b/src/zenlib/namespace/namespace_process.py index 0cd1f76..bc21322 100644 --- a/src/zenlib/namespace/namespace_process.py +++ b/src/zenlib/namespace/namespace_process.py @@ -1,6 +1,6 @@ -from multiprocessing import Event, Pipe, Process, Queue -from os import chroot, chdir, getgid, getuid, setgid, setuid from getpass import getuser +from multiprocessing import Event, Pipe, Process, Queue +from os import chdir, chroot, getgid, getuid, setgid, setuid from .namespace import get_id_map, new_id_map, unshare_namespace @@ -18,6 +18,7 @@ def __init__(self, target=None, args=None, kwargs=None, **ekwargs): self.orig_uid = getuid() self.orig_gid = getgid() self.uidmapped = Event() + self.unshared = Event() self.completed = Event() self.exception_recv, self.exception_send = Pipe() self.function_queue = Queue() @@ -29,11 +30,15 @@ def map_ids(self): def map_unshare_uids(self): self.start() + + self.unshared.wait() self.map_ids() + self.uidmapped.set() def run(self): unshare_namespace() + self.unshared.set() self.uidmapped.wait() setuid(0) setgid(0)