From 14685d04b6387a6f4e99a36f86b41d4b6dfdbd9b Mon Sep 17 00:00:00 2001 From: Fabian Zills <46721498+PythonFZ@users.noreply.github.com> Date: Wed, 14 Dec 2022 11:39:39 +0100 Subject: [PATCH] add frozen and metadata attributes + bump version (#12) --- pyproject.toml | 2 +- tests/test_descriptor.py | 33 +++++++++++++++++++++++++++++++++ tests/test_i_zninit.py | 29 +++++++++++++++++++++++++++++ tests/test_zninit.py | 2 +- zninit/descriptor/__init__.py | 27 ++++++++++++++++++++------- 5 files changed, 84 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3a41a13..ab784a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "zninit" -version = "0.1.6" +version = "0.1.7" description = "Descriptor based dataclass implementation" authors = ["zincwarecode "] license = "Apache-2.0" diff --git a/tests/test_descriptor.py b/tests/test_descriptor.py index 7a20e02..174a287 100644 --- a/tests/test_descriptor.py +++ b/tests/test_descriptor.py @@ -146,3 +146,36 @@ def test_get_desc_values(): assert desc1_1.__get__(self) == "Hello" assert desc1_2.__get__(self) == "World" + + +class FrozenExample: + """ZnInit Frozen Descriptor.""" + + value = Descriptor(frozen=True) + + +def test_frozen_descriptor(): + """Test a frozen descriptor.""" + example = FrozenExample() + example.value = 42 + with pytest.raises(TypeError): + example.value = 25 + assert example.value == 42 + + # Running twice is a test. + example = FrozenExample() + example.value = 42 + with pytest.raises(TypeError): + example.value = 25 + assert example.value == 42 + + +class WithMetadata: + """ZnInit Descriptor with metadata.""" + + value = Descriptor(metadata={"foo": "bar"}) + + +def test_descriptor_metadata(): + """Test a descriptor with metadata.""" + assert WithMetadata.value.metadata["foo"] == "bar" diff --git a/tests/test_i_zninit.py b/tests/test_i_zninit.py index 48a50ca..bbd577f 100644 --- a/tests/test_i_zninit.py +++ b/tests/test_i_zninit.py @@ -1,4 +1,6 @@ """General 'ZnInit' integration tests.""" +import pytest + import zninit @@ -14,6 +16,7 @@ class ExampleCls(zninit.ZnInit, metaclass=GetItemMeta): """Example 'ZnInit' with metaclass.""" parameter = zninit.Descriptor() + frozen_parameter = zninit.Descriptor(None, frozen=True) def test_ExampleCls(): @@ -21,3 +24,29 @@ def test_ExampleCls(): example = ExampleCls(parameter=25) assert example.parameter == 25 assert ExampleCls[42] == 42 + + +def test_frozen_parameter(): + """Test frozen parameter.""" + example = ExampleCls(parameter=18, frozen_parameter=42) + assert example.frozen_parameter == 42 + with pytest.raises(TypeError): + example.frozen_parameter = 43 + + example = ExampleCls(parameter=18, frozen_parameter=42) + assert example.frozen_parameter == 42 + with pytest.raises(TypeError): + example.frozen_parameter = 43 + + +def test_frozen_parameter_default(): + """Test frozen_parameter with default value.""" + example = ExampleCls(parameter=18) + assert example.frozen_parameter is None + with pytest.raises(TypeError): + example.frozen_parameter = 43 + + example = ExampleCls(parameter=18, frozen_parameter=42) + assert example.frozen_parameter == 42 + with pytest.raises(TypeError): + example.frozen_parameter = 43 diff --git a/tests/test_zninit.py b/tests/test_zninit.py index 1fcdcb7..dda70c3 100644 --- a/tests/test_zninit.py +++ b/tests/test_zninit.py @@ -5,4 +5,4 @@ def test_version(): """Test the installed version.""" - assert zninit.__version__ == "0.1.6" + assert zninit.__version__ == "0.1.7" diff --git a/zninit/descriptor/__init__.py b/zninit/descriptor/__init__.py index 187d05e..ab1b634 100644 --- a/zninit/descriptor/__init__.py +++ b/zninit/descriptor/__init__.py @@ -5,6 +5,7 @@ import functools import sys import typing +import weakref with contextlib.suppress(ImportError): import typeguard @@ -18,7 +19,7 @@ class Empty: # pylint: disable=too-few-public-methods """ -class Descriptor: +class Descriptor: # pylint: disable=too-many-instance-attributes """Simple Python Descriptor that allows adding. This class allows to add metadata to arbitrary class arguments: @@ -53,6 +54,8 @@ def __init__( name="", use_repr: bool = True, check_types: bool = False, + metadata: dict = None, + frozen: bool = False, ): # pylint: disable=too-many-arguments """Define a Descriptor object. @@ -71,6 +74,10 @@ def __init__( descriptor should be used in the __repr__ string. check_types: bool, default=False Check the type when using __set__ against the type annotation. + frozen: bool, default=False + Freeze the attribute after the first __set__ call. + metadata: dict, default=None + additional metadata for the descriptor. """ self._default = default self._owner = owner @@ -78,6 +85,9 @@ def __init__( self._name = name self.use_repr = use_repr self.check_types = check_types + self.metadata = metadata or {} + self.frozen = frozen + self._frozen = weakref.WeakKeyDictionary() if check_types and ("typeguard" not in sys.modules): raise ImportError( "Need to install 'pip install zninit[typeguard]' for type checking." @@ -117,12 +127,11 @@ def annotation(self): except AttributeError: annotations = {} - if self.check_types: - if self.name not in annotations: - raise KeyError( - f"Could not find 'annotation' for {self.name} in '{self.owner}' with" - " 'check_types=True'" - ) + if self.check_types and self.name not in annotations: + raise KeyError( + f"Could not find 'annotation' for {self.name} in '{self.owner}' with" + " 'check_types=True'" + ) return annotations.get(self.name) def __set_name__(self, owner, name): @@ -149,12 +158,16 @@ def __get__(self, instance, owner=None): def __set__(self, instance, value): """Save value to instance.__dict__.""" + if self._frozen.get(instance, False): + raise TypeError(f"Frozen attribute '{self.name}' can not be changed.") if self.check_types: typeguard.check_type( argname=self.name, value=value, expected_type=self.annotation ) self._instance = instance instance.__dict__[self.name] = value + if self.frozen: + self._frozen[instance] = True DescriptorTypeT = typing.TypeVar("DescriptorTypeT", bound=Descriptor)