Skip to content

propose fix for issue 61 #62

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 5, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion hololinked/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.2.9"
__version__ = "0.2.10"
19 changes: 17 additions & 2 deletions hololinked/param/parameterized.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,9 @@ def __get__(self, obj : typing.Union['Parameterized', typing.Any],
class's value (default).
"""
if self.class_member:
if self.fget is not None:
# For class properties, bind the getter to the class
return self.fget.__get__(None, objtype)(objtype)
return objtype.__dict__.get(self._internal_name, self.default)
if obj is None:
return self
Expand Down Expand Up @@ -418,7 +421,11 @@ def __set__(self, obj : typing.Union['Parameterized', typing.Any], value : typin

# The following needs to be optimised, probably through lambda functions?
if self.fset is not None:
self.fset(obj, value)
if self.class_member:
# For class properties, bind the setter to the class
self.fset.__get__(None, obj)(obj, value)
else:
self.fset(obj, value)
else:
obj.__dict__[self._internal_name] = value

Expand Down Expand Up @@ -453,6 +460,9 @@ def __set__(self, obj : typing.Union['Parameterized', typing.Any], value : typin

def __delete__(self, obj : typing.Union['Parameterized', typing.Any]) -> None:
if self.fdel is not None:
if self.class_member:
# For class properties, bind the deletor to the class
return self.fdel.__get__(None, obj)(obj)
return self.fdel(obj)
raise NotImplementedError("Parameter deletion not implemented.")

Expand Down Expand Up @@ -1795,7 +1805,12 @@ def __setattr__(mcs, attribute_name : str, value : typing.Any) -> None:
# parameter = copy.copy(parameter)
# parameter.owner = mcs
# type.__setattr__(mcs, attribute_name, parameter)
mcs.__dict__[attribute_name].__set__(mcs, value)
if parameter.class_member:
# For class member properties, use the descriptor protocol
parameter.__set__(None, value)
else:
parameter.__set__(mcs, value)

return
# set with None should not supported as with mcs it supports
# class attributes which can be validated
Expand Down
Empty file added issue-61-fix.patch
Empty file.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

setuptools.setup(
name="hololinked",
version="0.2.9",
version="0.2.10",
author="Vignesh Vaidyanathan",
author_email="vignesh.vaidyanathan@hololinked.dev",
description="A ZMQ-based Object Oriented RPC tool-kit for instrument control/data acquisition or controlling generic python objects.",
Expand Down
131 changes: 131 additions & 0 deletions tests/test_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,137 @@ def test_6_pydantic_model_property(self):
self.thing_client.pydantic_simple_prop = '5str'
self.assertTrue("validation error for 'int'" in str(ex.exception))

class TestClassPropertyThing(Thing):
# Simple class property with default value
simple_class_prop = Number(class_member=True, default=42)

# Class property with custom getter/setter
managed_class_prop = Number(class_member=True)

@managed_class_prop.getter
def managed_class_prop(cls):
return getattr(cls, '_managed_value', 0)

@managed_class_prop.setter
def managed_class_prop(cls, value):
if value < 0:
raise ValueError("Value must be non-negative")
cls._managed_value = value

# Read-only class property
readonly_class_prop = String(class_member=True, readonly=True)

@readonly_class_prop.getter
def readonly_class_prop(cls):
return "read-only-value"

# Deletable class property
deletable_class_prop = Number(class_member=True, default=100)

@deletable_class_prop.getter
def deletable_class_prop(cls):
return getattr(cls, '_deletable_value', 100)

@deletable_class_prop.setter
def deletable_class_prop(cls, value):
cls._deletable_value = value

@deletable_class_prop.deleter
def deletable_class_prop(cls):
if hasattr(cls, '_deletable_value'):
del cls._deletable_value


class TestClassProperty(TestCase):

def setUp(self):
self.thing = TestClassPropertyThing(instance_name='test-class-property')

def test_1_simple_class_property(self):
"""Test basic class property functionality"""
# Test class-level access
self.assertEqual(TestClassPropertyThing.simple_class_prop, 42)
TestClassPropertyThing.simple_class_prop = 100
self.assertEqual(TestClassPropertyThing.simple_class_prop, 100)

# Test that instance-level access reflects class value
instance1 = TestClassPropertyThing(instance_name='test1')
instance2 = TestClassPropertyThing(instance_name='test2')
self.assertEqual(instance1.simple_class_prop, 100)
self.assertEqual(instance2.simple_class_prop, 100)

# Test that instance-level changes affect class value
instance1.simple_class_prop = 200
self.assertEqual(TestClassPropertyThing.simple_class_prop, 200)
self.assertEqual(instance2.simple_class_prop, 200)

def test_2_managed_class_property(self):
"""Test class property with custom getter/setter"""
# Test initial value
self.assertEqual(TestClassPropertyThing.managed_class_prop, 0)

# Test valid value assignment
TestClassPropertyThing.managed_class_prop = 50
self.assertEqual(TestClassPropertyThing.managed_class_prop, 50)

# Test validation in setter
with self.assertRaises(ValueError):
TestClassPropertyThing.managed_class_prop = -10

# Verify value wasn't changed after failed assignment
self.assertEqual(TestClassPropertyThing.managed_class_prop, 50)

# Test instance-level validation
instance = TestClassPropertyThing(instance_name='test3')
with self.assertRaises(ValueError):
instance.managed_class_prop = -20

# Test that instance-level access reflects class value
self.assertEqual(instance.managed_class_prop, 50)

# Test that instance-level changes affects class value
instance.managed_class_prop = 100
self.assertEqual(TestClassPropertyThing.managed_class_prop, 100)
self.assertEqual(instance.managed_class_prop, 100)

def test_3_readonly_class_property(self):
"""Test read-only class property behavior"""
# Test reading the value
self.assertEqual(TestClassPropertyThing.readonly_class_prop, "read-only-value")

# Test that setting raises an error at class level
with self.assertRaises(ValueError):
TestClassPropertyThing.readonly_class_prop = "new-value"

# Test that setting raises an error at instance level
instance = TestClassPropertyThing(instance_name='test4')
with self.assertRaises(ValueError):
instance.readonly_class_prop = "new-value"

# Verify value remains unchanged
self.assertEqual(TestClassPropertyThing.readonly_class_prop, "read-only-value")
self.assertEqual(instance.readonly_class_prop, "read-only-value")

def test_4_deletable_class_property(self):
"""Test class property deletion"""
# Test initial value
self.assertEqual(TestClassPropertyThing.deletable_class_prop, 100)

# Test setting new value
TestClassPropertyThing.deletable_class_prop = 150
self.assertEqual(TestClassPropertyThing.deletable_class_prop, 150)

# Test deletion
del TestClassPropertyThing.deletable_class_prop
self.assertEqual(TestClassPropertyThing.deletable_class_prop, 100) # Should return to default
self.assertEqual(instance.deletable_class_prop, 100)

# Test instance-level deletion
instance = TestClassPropertyThing(instance_name='test5')
instance.deletable_class_prop = 200
self.assertEqual(TestClassPropertyThing.deletable_class_prop, 200)
del instance.deletable_class_prop
self.assertEqual(TestClassPropertyThing.deletable_class_prop, 100) # Should return to default


if __name__ == '__main__':
Expand Down
Loading