From bf3a96f0fb5026871263b8c5063f07b54233282f Mon Sep 17 00:00:00 2001
From: Amanda Lewis <amandalewis@lbl.gov>
Date: Tue, 15 Oct 2024 10:53:38 -0400
Subject: [PATCH 1/4] adding test for overwriting enums

---
 tests/containers_test.py | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/tests/containers_test.py b/tests/containers_test.py
index c857215..6f2920a 100644
--- a/tests/containers_test.py
+++ b/tests/containers_test.py
@@ -207,3 +207,25 @@ def test_institute_enum(institute_schema, text_array):
     out = obj.write_enum_table()
     assert "EXFOR institute codes" in out
     assert "1CANALA" not in out
+
+def test_overwrite(facility_schema, institute_schema, text_array):
+
+    with pytest.raises(AttributeError):
+        text_array.properties['body'].enum
+
+    facility = create_container(facility_schema, text_array)
+    assert "ACCEL" in facility.properties['body'].enum.keys()
+    assert facility.properties['body'].description == "list of the facility or facilities where the measurement was performed."
+    with pytest.raises(AttributeError):
+        text_array.properties['body'].enum
+
+
+    institute = create_container(institute_schema, text_array)
+    assert '1CANCAN' in institute.properties['body'].enum.keys()
+
+
+    # facility enum has now been over-written by the institute
+    assert facility.__name__ == 'facility'
+    assert facility.properties['body'].description == "list of the facility or facilities where the measurement was performed."
+    print(facility.properties['body'])
+    assert "ACCEL" in facility.properties['body'].enum.keys()
\ No newline at end of file
-- 
GitLab


From c19a38d0e4a90343c7536bc2fe4ac6f260d7e319 Mon Sep 17 00:00:00 2001
From: Amanda Lewis <amandalewis@lbl.gov>
Date: Mon, 21 Oct 2024 11:47:42 -0400
Subject: [PATCH 2/4] adding more tests that narrow problem down to
 modifications in the general container, not in the abstract containers

---
 tests/containers_test.py | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/tests/containers_test.py b/tests/containers_test.py
index 6f2920a..bc1ad50 100644
--- a/tests/containers_test.py
+++ b/tests/containers_test.py
@@ -208,7 +208,7 @@ def test_institute_enum(institute_schema, text_array):
     assert "EXFOR institute codes" in out
     assert "1CANALA" not in out
 
-def test_overwrite(facility_schema, institute_schema, text_array):
+def test_overwrite(facility_schema, institute_schema, text_array, abstract_containers):
 
     with pytest.raises(AttributeError):
         text_array.properties['body'].enum
@@ -216,6 +216,17 @@ def test_overwrite(facility_schema, institute_schema, text_array):
     facility = create_container(facility_schema, text_array)
     assert "ACCEL" in facility.properties['body'].enum.keys()
     assert facility.properties['body'].description == "list of the facility or facilities where the measurement was performed."
+    
+    # check if the underlying abstract class has been modified - there should NOT be 
+    # an enum on the text_array object
+    with pytest.raises(AttributeError):
+        abstract_containers['array'].enum
+        abstract_containers['object'].enum
+
+
+    # check if the underlying general class has been modified - there should NOT be 
+    # an enum on the text_array object
+    print(text_array.properties['body'])
     with pytest.raises(AttributeError):
         text_array.properties['body'].enum
 
-- 
GitLab


From 89761a61ce0ba03d8b57876920d61025cd2fd332 Mon Sep 17 00:00:00 2001
From: Amanda Lewis <amandalewis@lbl.gov>
Date: Mon, 21 Oct 2024 16:15:46 -0600
Subject: [PATCH 3/4] fixed properties class method setting, cleaned up
 imports, fixed formatting

---
 src/schema_api/abstract.py  |  4 +++
 src/schema_api/container.py | 57 ++++++++++++++++++++++++++++---------
 tests/containers_test.py    |  7 +++--
 3 files changed, 53 insertions(+), 15 deletions(-)

diff --git a/src/schema_api/abstract.py b/src/schema_api/abstract.py
index b848737..0967296 100644
--- a/src/schema_api/abstract.py
+++ b/src/schema_api/abstract.py
@@ -160,6 +160,10 @@ def create_abstract_container(**kwargs):
                     err_msg += f"API class attributes and methods currently allowed are:\n{_allowed_attributes_and_methods}"
 
                     raise AttributeError(err_msg)
+                
+            # because this is a class factory, any variable defined becomes a
+            # class attribute. Get rid of the attribute variable used for checking
+            del attributes
 
             # if all of the attributes are good, return the new class
             return x
diff --git a/src/schema_api/container.py b/src/schema_api/container.py
index 1196fc4..aef663f 100644
--- a/src/schema_api/container.py
+++ b/src/schema_api/container.py
@@ -1,5 +1,4 @@
-from textwrap import fill, wrap
-from schema_api import create_general_container
+from textwrap import fill
 
 
 def create_container(schema, GeneralContainer):
@@ -43,10 +42,49 @@ def create_container(schema, GeneralContainer):
         datatype = schema["type"]
         category = schema["category"]
 
-        # set hidden attribute so metaclass can assign correctly
-        __attributes = {
-            k: v for k, v in schema.items() if k not in ["type", "properties"]
-        }
+        # set hidden attribute so metaclass can assign correctly. Type is not reset.
+        # Properties, as a mutable class attribute, is dealt with separately below.
+        __attributes = {k: v for k, v in schema.items() if k not in ["type", "properties"]}
+
+        # properties is a class attribute on the GeneralContainer class, and also a class
+        # attribute on the Container class, but they should not both point to the same
+        # object. This code creates a properties attribute on the Container class,
+        # which severs the connection (Container.properties no longer points to
+        # GeneralContainers.properties), and then fills in the Container.properties
+        # dictionary with starting values from the GeneralContainers object, then
+        # updates the values from the given schema
+        if "properties" in schema.keys():
+
+            # set a Container.properties class attribute that does not point back
+            # to GeneralContainers.properties
+            properties = {}
+
+            # fill in the properties attribute based on the GeneralContainer
+            for key, val in GeneralContainer.properties.items():
+
+                # create a copy of the Abstract object, which has all the same values
+                # as the GeneralContainer properties object
+                properties[key] = type(val.__name__, val.__bases__, dict(val.__dict__))
+
+                # change any values given in the schema
+                if key in schema["properties"].keys():
+                    for k, v in schema["properties"][key].items():
+                        setattr(properties[key], k, v)
+
+            # because this is a class factory, any variable defined becomes a
+            # class attribute. Get rid of the iter variables used here
+                    del k
+                    del v
+            del key
+            del val
+
+    # make sure that properties is local (a Container class attribute, separate from the
+    # GeneralContainer class attribute)
+    if "properties" in schema.keys():
+        if "properties" not in Container.__dict__.keys():
+            raise KeyError(
+                f"In the creation of {schema['title']} class, the property attribute was not created properly."
+            )
 
     Container.__name__ = schema["title"]
 
@@ -76,11 +114,4 @@ def create_container(schema, GeneralContainer):
 
     Container.__doc__ = doc_string
 
-    # if properties are listed, they are additional information to the
-    # datatype class, so add them here
-    if "properties" in schema.keys():
-        for prop in schema["properties"].keys():
-            for k, v in schema["properties"][prop].items():
-                setattr(Container.properties[prop], k, v)
-
     return Container
diff --git a/tests/containers_test.py b/tests/containers_test.py
index bc1ad50..dfbb2f4 100644
--- a/tests/containers_test.py
+++ b/tests/containers_test.py
@@ -218,15 +218,18 @@ def test_overwrite(facility_schema, institute_schema, text_array, abstract_conta
     assert facility.properties['body'].description == "list of the facility or facilities where the measurement was performed."
     
     # check if the underlying abstract class has been modified - there should NOT be 
-    # an enum on the text_array object
+    # an enum on any of the abstract classes
+    with pytest.raises(AttributeError):
+        abstract_containers['object'].properties
     with pytest.raises(AttributeError):
         abstract_containers['array'].enum
+    with pytest.raises(AttributeError):
         abstract_containers['object'].enum
 
 
     # check if the underlying general class has been modified - there should NOT be 
     # an enum on the text_array object
-    print(text_array.properties['body'])
+    print("\n\n",text_array.properties['body'].__dict__.keys())
     with pytest.raises(AttributeError):
         text_array.properties['body'].enum
 
-- 
GitLab


From 30b82d8730a3f891fa34a2711b590a7d6dcc936b Mon Sep 17 00:00:00 2001
From: Amanda Lewis <amandalewis@lbl.gov>
Date: Tue, 10 Dec 2024 09:58:49 -0700
Subject: [PATCH 4/4] adding failing test, which shows that mutability at lower
 levels is still a problem

---
 tests/containers_test.py | 27 ++++++++++++++++++++++++++-
 1 file changed, 26 insertions(+), 1 deletion(-)

diff --git a/tests/containers_test.py b/tests/containers_test.py
index dfbb2f4..8488f89 100644
--- a/tests/containers_test.py
+++ b/tests/containers_test.py
@@ -1,6 +1,7 @@
 import jsonref
 from pathlib import Path
 import pytest
+import copy
 
 from schema_api import create_abstract_container
 from schema_api import create_general_container
@@ -242,4 +243,28 @@ def test_overwrite(facility_schema, institute_schema, text_array, abstract_conta
     assert facility.__name__ == 'facility'
     assert facility.properties['body'].description == "list of the facility or facilities where the measurement was performed."
     print(facility.properties['body'])
-    assert "ACCEL" in facility.properties['body'].enum.keys()
\ No newline at end of file
+    assert "ACCEL" in facility.properties['body'].enum.keys()
+
+
+def test_overwrite_on_general(flight_path_schema, measured_quantity):
+
+    flightpath1 = create_container(flight_path_schema, measured_quantity)
+
+    print(measured_quantity.properties['attributes'].properties['value_type'].enum)
+    print(flightpath1.properties['attributes'].properties['value_type'].enum)
+
+    new_schema = copy.deepcopy(flight_path_schema)
+
+    assert new_schema == flight_path_schema
+
+    new_schema['title'] = "test"
+    assert new_schema != flight_path_schema
+
+    # create flight path 2
+    flightpath2 = create_container(new_schema, measured_quantity)
+    assert flightpath2.title == "test"
+    assert flightpath1.properties['attributes'].properties['value_type'].enum == flightpath2.properties['attributes'].properties['value_type'].enum
+
+    # change flightpath2, but should NOT change flightpath1
+    flightpath2.properties['attributes'].properties['value_type'].enum = {}
+    assert flightpath1.properties['attributes'].properties['value_type'].enum != flightpath2.properties['attributes'].properties['value_type'].enum
-- 
GitLab