From c52be307c758bc8a22f52b75785664cca8df1e33 Mon Sep 17 00:00:00 2001
From: "i.nueske" <i.nueske@indiscale.com>
Date: Sat, 11 Jan 2025 14:57:44 +0100
Subject: [PATCH 1/4] ENH: Updated add_property to support special attributes:

- if given name is a special attributes, update corresponding attribute instead of creating a new property
- Add test for https://gitlab.com/linkahead/linkahead-pylib/-/issues/134
- Update changelog
---
 CHANGELOG.md                   |  4 +++-
 src/linkahead/common/models.py |  5 +++++
 unittests/test_apiutils.py     | 12 ++++++------
 unittests/test_issues.py       | 15 +++++++++++++++
 4 files changed, 29 insertions(+), 7 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a32e16d..e64b496 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -37,9 +37,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 * [#87](https://gitlab.com/linkahead/linkahead-pylib/-/issues/87)
   `XMLSyntaxError` messages when parsing (incomplete) responses in
   case of certain connection timeouts.
-  The diff returned by compare_entities now uses id instead of name as key if either property does not have a name
 * [#127](https://gitlab.com/linkahead/linkahead-pylib/-/issues/127)
   pylinkahead.ini now supports None and tuples as values for the `timeout` keyword
+* [#134](https://gitlab.com/linkahead/linkahead-pylib/-/issues/134)
+  Setting special attributes using add_property now updates the attribute 
+  instead of adding a new property with the same name
 
 ### Security ###
 
diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py
index 8bd8eac..38a485f 100644
--- a/src/linkahead/common/models.py
+++ b/src/linkahead/common/models.py
@@ -749,6 +749,11 @@ class Entity:
             raise UserWarning(
                 "This method expects you to pass at least an entity, a name or an id.")
 
+        # If the name is a special attribute, set the attribute instead
+        if name in SPECIAL_ATTRIBUTES and name in dir(self):
+            setattr(self, name, value)
+            return self
+
         new_property = Property(name=name, id=id, description=description, datatype=datatype,
                                 value=value, unit=unit)
 
diff --git a/unittests/test_apiutils.py b/unittests/test_apiutils.py
index 6667089..4b1bbf2 100644
--- a/unittests/test_apiutils.py
+++ b/unittests/test_apiutils.py
@@ -115,8 +115,8 @@ def test_compare_entities():
     r2.add_property("tester", )
     r1.add_property("tests_234234", value=45)
     r2.add_property("tests_TT", value=45)
-    r1.add_property("datatype", value=45, datatype=db.INTEGER)
-    r2.add_property("datatype", value=45)
+    r1.add_property("datatype_prop", value=45, datatype=db.INTEGER)
+    r2.add_property("datatype_prop", value=45)
     r1.add_property("entity_id", value=2)
     r2.add_property("entity_id", value=24)
     r1.add_property("entity_mix_e", value=2)
@@ -152,10 +152,10 @@ def test_compare_entities():
     assert "tests_234234" in diff_r1["properties"]
     assert "tests_TT" in diff_r2["properties"]
 
-    assert "datatype" in diff_r1["properties"]
-    assert "datatype" in diff_r1["properties"]["datatype"]
-    assert "datatype" in diff_r2["properties"]
-    assert "datatype" in diff_r2["properties"]["datatype"]
+    assert "datatype_prop" in diff_r1["properties"]
+    assert "datatype" in diff_r1["properties"]["datatype_prop"]
+    assert "datatype_prop" in diff_r2["properties"]
+    assert "datatype" in diff_r2["properties"]["datatype_prop"]
 
     assert "entity_id" in diff_r1["properties"]
     assert "entity_id" in diff_r2["properties"]
diff --git a/unittests/test_issues.py b/unittests/test_issues.py
index 3b0117b..5f44a90 100644
--- a/unittests/test_issues.py
+++ b/unittests/test_issues.py
@@ -128,3 +128,18 @@ def test_issue_73():
     assert "datatype=" in xml_str
     assert "Recursive reference" in xml_str
     assert len(xml_str) < 500
+
+
+def test_issue_134():
+    """
+    Test setting special attributes using add_property.
+    https://gitlab.com/linkahead/linkahead-pylib/-/issues/134
+    """
+    for attr, val in [("name", "TestRecord"), ("datatype", db.TEXT),
+                      ("description", "desc"), ("id", 1000),
+                      ("value", "Val"), ("unit", "°C")]:
+        rec = db.Record()
+        assert rec.__getattribute__(attr) is None
+        rec.add_property(name=attr, value=val)
+        assert rec.__getattribute__(attr) == val
+        assert rec.get_property(attr) is None
-- 
GitLab


From bbbc0f3ef39ef8cf2540ff148ebcfbf0b6e166e4 Mon Sep 17 00:00:00 2001
From: Florian Spreckelsen <f.spreckelsen@indiscale.com>
Date: Tue, 14 Jan 2025 10:21:19 +0100
Subject: [PATCH 2/4] DOC: Update changelog

---
 CHANGELOG.md | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a6b8774..19b6a1b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,12 +10,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added ###
 
 * New setup extra `test` which installs the dependencies for testing.
-* The Container class has a new member function `filter` which is based o `_filter_entity_list`.
+* The Container class has a new member function `filter_by_identity`
+  which is based on `_filter_entity_list`.
 * The `Entity` properties `_cuid` and `_flags` are now available for read-only access
   as `cuid` and `flags`, respectively.
 
 ### Changed ###
-* Renamed the `filter` function of Container, ParentList and PropertyList to `filter_by_identity`.
+
+* Renamed the `filter` function of Container, ParentList and
+  PropertyList to `filter_by_identity`.
+* [#134](https://gitlab.com/linkahead/linkahead-pylib/-/issues/134)
+  Setting special attributes using add_property now updates the attribute 
+  instead of adding a new property with the same name
 
 ### Deprecated ###
 
@@ -40,9 +46,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   case of certain connection timeouts.
 * [#127](https://gitlab.com/linkahead/linkahead-pylib/-/issues/127)
   pylinkahead.ini now supports None and tuples as values for the `timeout` keyword
-* [#134](https://gitlab.com/linkahead/linkahead-pylib/-/issues/134)
-  Setting special attributes using add_property now updates the attribute 
-  instead of adding a new property with the same name
 
 ### Security ###
 
-- 
GitLab


From 092b1863d5a033d4bd490c3d1fd33c1cbb7379e7 Mon Sep 17 00:00:00 2001
From: "i.nueske" <i.nueske@indiscale.com>
Date: Tue, 14 Jan 2025 10:59:13 +0100
Subject: [PATCH 3/4] ENH: Update add_property to prevent overwriting special
 attributes

---
 src/linkahead/common/models.py | 13 +++++++++++--
 unittests/test_issues.py       |  5 +++++
 2 files changed, 16 insertions(+), 2 deletions(-)

diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py
index 0b66acd..35db9f6 100644
--- a/src/linkahead/common/models.py
+++ b/src/linkahead/common/models.py
@@ -751,8 +751,17 @@ class Entity:
 
         # If the name is a special attribute, set the attribute instead
         if name in SPECIAL_ATTRIBUTES and name in dir(self):
-            setattr(self, name, value)
-            return self
+            if getattr(self, name) is None:
+                setattr(self, name, value)
+                return self
+            else:
+                raise ValueError(f"'{name}' is a special attribute and does not "
+                                 f"support multi-property. It is already set "
+                                 f"and cannot be overwritten using this method. "
+                                 f"Please use direct assignment for setting this "
+                                 f"property after the first time.")
+        # ToDo: Implement the same behaviour for special attribute ids,
+        #       https://gitlab.indiscale.com/caosdb/src/caosdb-pylib/-/issues/219
 
         new_property = Property(name=name, id=id, description=description, datatype=datatype,
                                 value=value, unit=unit)
diff --git a/unittests/test_issues.py b/unittests/test_issues.py
index 5f44a90..33fdc9e 100644
--- a/unittests/test_issues.py
+++ b/unittests/test_issues.py
@@ -143,3 +143,8 @@ def test_issue_134():
         rec.add_property(name=attr, value=val)
         assert rec.__getattribute__(attr) == val
         assert rec.get_property(attr) is None
+
+        exp_str = f"'{attr}' is a special attribute and does not support"
+        with raises(ValueError) as e:
+            rec.add_property(name=attr, value=val)
+        assert exp_str in str(e.value)
-- 
GitLab


From 2e69dca0c1d0f2844e27945d6a92b0193557b875 Mon Sep 17 00:00:00 2001
From: "i.nueske" <i.nueske@indiscale.com>
Date: Tue, 14 Jan 2025 11:50:04 +0100
Subject: [PATCH 4/4] ENH: Update add_property docstring, make special
 attribute case-insensitive

---
 src/linkahead/common/models.py | 10 +++++++++-
 unittests/test_issues.py       | 11 ++++++-----
 2 files changed, 15 insertions(+), 6 deletions(-)

diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py
index 35db9f6..5f6f7ec 100644
--- a/src/linkahead/common/models.py
+++ b/src/linkahead/common/models.py
@@ -622,6 +622,10 @@ class Entity:
         If you want to add a property to an already existing entity, the
         property ``id`` of that property needs to be specified before you send
         the updated entity to the server.
+        If the specified name matches the name of a special attribute in
+        lowercase, such as 'name' or 'description', the attribute is set
+        instead. If the attribute already has a value, this fails with a
+        ValueError.
 
         Parameters
         ----------
@@ -671,6 +675,9 @@ class Entity:
             If you try to add an ``Entity`` object with File or Record role (or,
             equivalently, a ``File`` or ``Record`` object) as a property, a
             ``ValueError`` is raised.
+        ValueError:
+            If the property to be added is a special attribute and already has
+            a value.
 
         Examples
         --------
@@ -750,7 +757,8 @@ class Entity:
                 "This method expects you to pass at least an entity, a name or an id.")
 
         # If the name is a special attribute, set the attribute instead
-        if name in SPECIAL_ATTRIBUTES and name in dir(self):
+        if name and name.lower() in SPECIAL_ATTRIBUTES and name.lower() in dir(self):
+            name = name.lower()
             if getattr(self, name) is None:
                 setattr(self, name, value)
                 return self
diff --git a/unittests/test_issues.py b/unittests/test_issues.py
index 33fdc9e..31c09ed 100644
--- a/unittests/test_issues.py
+++ b/unittests/test_issues.py
@@ -135,16 +135,17 @@ def test_issue_134():
     Test setting special attributes using add_property.
     https://gitlab.com/linkahead/linkahead-pylib/-/issues/134
     """
-    for attr, val in [("name", "TestRecord"), ("datatype", db.TEXT),
-                      ("description", "desc"), ("id", 1000),
+    for attr, val in [("nAme", "TestRecord"), ("datatype", db.TEXT),
+                      ("descriptIon", "desc"), ("id", 1000),
                       ("value", "Val"), ("unit", "°C")]:
         rec = db.Record()
-        assert rec.__getattribute__(attr) is None
+        assert rec.__getattribute__(attr.lower()) is None
         rec.add_property(name=attr, value=val)
-        assert rec.__getattribute__(attr) == val
+        assert rec.__getattribute__(attr.lower()) == val
         assert rec.get_property(attr) is None
+        assert rec.get_property(attr.lower()) is None
 
-        exp_str = f"'{attr}' is a special attribute and does not support"
+        exp_str = f"'{attr.lower()}' is a special attribute and does not"
         with raises(ValueError) as e:
             rec.add_property(name=attr, value=val)
         assert exp_str in str(e.value)
-- 
GitLab