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