From 8993014832a9b9e2cd2a5e9641b395f5663996b6 Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Thu, 19 Sep 2024 22:43:29 -0700 Subject: [PATCH] make the tests pass again - add validator to do the opposite of coerce_value, try to pass the input as the ``value`` field of the model. fix test assumptions and model creation. model update to follow --- nwb_linkml/src/nwb_linkml/adapters/dataset.py | 2 -- .../src/nwb_linkml/adapters/namespaces.py | 6 ++--- .../src/nwb_linkml/generators/pydantic.py | 2 ++ nwb_linkml/src/nwb_linkml/includes/base.py | 26 +++++++++++++++++-- .../tests/test_adapters/test_adapter.py | 2 +- .../test_adapters/test_adapter_namespaces.py | 8 +++--- nwb_linkml/tests/test_includes/conftest.py | 4 +-- 7 files changed, 37 insertions(+), 13 deletions(-) diff --git a/nwb_linkml/src/nwb_linkml/adapters/dataset.py b/nwb_linkml/src/nwb_linkml/adapters/dataset.py index 7b391de..0558862 100644 --- a/nwb_linkml/src/nwb_linkml/adapters/dataset.py +++ b/nwb_linkml/src/nwb_linkml/adapters/dataset.py @@ -59,9 +59,7 @@ class MapScalar(DatasetMap): slots: - name: MyScalar description: A scalar - multivalued: false range: int32 - required: false """ diff --git a/nwb_linkml/src/nwb_linkml/adapters/namespaces.py b/nwb_linkml/src/nwb_linkml/adapters/namespaces.py index 1db8bbb..6aa68ad 100644 --- a/nwb_linkml/src/nwb_linkml/adapters/namespaces.py +++ b/nwb_linkml/src/nwb_linkml/adapters/namespaces.py @@ -178,7 +178,7 @@ class NamespacesAdapter(Adapter): nwb-schema-language inheritance doesn't work like normal python inheritance - instead of inheriting everything at the 'top level' of a class, it also recursively merges all properties from the parent objects. - + While this operation does not take care to modify classes in a way that respect their order (i.e. roll down ancestor classes first, in order, before the leaf classes), it doesn't matter - this method should be both idempotent and order insensitive @@ -196,8 +196,8 @@ class NamespacesAdapter(Adapter): # merge and cast new_cls: dict = {} for i, parent in enumerate(parents): - # we want a full roll-down of all the ancestor classes, - # but we make an abbreviated leaf class + # we want a full roll-down of all the ancestor classes, + # but we make an abbreviated leaf class complete = False if i == len(parents) - 1 else True new_cls = roll_down_nwb_class(new_cls, parent, complete=complete) new_cls: Group | Dataset = type(cls)(**new_cls) diff --git a/nwb_linkml/src/nwb_linkml/generators/pydantic.py b/nwb_linkml/src/nwb_linkml/generators/pydantic.py index 927e9c2..f4c1c9e 100644 --- a/nwb_linkml/src/nwb_linkml/generators/pydantic.py +++ b/nwb_linkml/src/nwb_linkml/generators/pydantic.py @@ -26,6 +26,7 @@ from linkml_runtime.utils.formatutils import remove_empty_items from linkml_runtime.utils.schemaview import SchemaView from nwb_linkml.includes.base import ( + BASEMODEL_CAST_WITH_VALUE, BASEMODEL_COERCE_CHILD, BASEMODEL_COERCE_VALUE, BASEMODEL_GETITEM, @@ -55,6 +56,7 @@ class NWBPydanticGenerator(PydanticGenerator): 'object_id: Optional[str] = Field(None, description="Unique UUID for each object")', BASEMODEL_GETITEM, BASEMODEL_COERCE_VALUE, + BASEMODEL_CAST_WITH_VALUE, BASEMODEL_COERCE_CHILD, ) split: bool = True diff --git a/nwb_linkml/src/nwb_linkml/includes/base.py b/nwb_linkml/src/nwb_linkml/includes/base.py index 3ecae8c..75b5ca6 100644 --- a/nwb_linkml/src/nwb_linkml/includes/base.py +++ b/nwb_linkml/src/nwb_linkml/includes/base.py @@ -16,7 +16,7 @@ BASEMODEL_GETITEM = """ BASEMODEL_COERCE_VALUE = """ @field_validator("*", mode="wrap") @classmethod - def coerce_value(cls, v: Any, handler) -> Any: + def coerce_value(cls, v: Any, handler, info) -> Any: \"\"\"Try to rescue instantiation by using the value field\"\"\" try: return handler(v) @@ -27,7 +27,29 @@ BASEMODEL_COERCE_VALUE = """ try: return handler(v["value"]) except (IndexError, KeyError, TypeError): - raise e1 + raise ValueError( + f"coerce_value: Could not use the value field of {type(v)} " + f"to construct {cls.__name__}.{info.field_name}, " + f"expected type: {cls.model_fields[info.field_name].annotation}" + ) from e1 +""" + +BASEMODEL_CAST_WITH_VALUE = """ + @field_validator("*", mode="wrap") + @classmethod + def cast_with_value(cls, v: Any, handler, info) -> Any: + \"\"\"Try to rescue instantiation by casting into the model's value fiel\"\"\" + try: + return handler(v) + except Exception as e1: + try: + return handler({"value": v}) + except Exception: + raise ValueError( + f"cast_with_value: Could not cast {type(v)} as value field for " + f"{cls.__name__}.{info.field_name}," + f" expected_type: {cls.model_fields[info.field_name].annotation}" + ) from e1 """ BASEMODEL_COERCE_CHILD = """ diff --git a/nwb_linkml/tests/test_adapters/test_adapter.py b/nwb_linkml/tests/test_adapters/test_adapter.py index 4514f5d..b3fdb27 100644 --- a/nwb_linkml/tests/test_adapters/test_adapter.py +++ b/nwb_linkml/tests/test_adapters/test_adapter.py @@ -54,7 +54,7 @@ def test_walk_field_values(nwb_core_fixture): text_models = list(nwb_core_fixture.walk_field_values(nwb_core_fixture, "dtype", value="text")) assert all([d.dtype == "text" for d in text_models]) # 135 known value from regex search - assert len(text_models) == len([d for d in dtype_models if d.dtype == "text"]) == 135 + assert len(text_models) == len([d for d in dtype_models if d.dtype == "text"]) == 155 def test_build_result(linkml_schema_bare): diff --git a/nwb_linkml/tests/test_adapters/test_adapter_namespaces.py b/nwb_linkml/tests/test_adapters/test_adapter_namespaces.py index 2052778..4c8de11 100644 --- a/nwb_linkml/tests/test_adapters/test_adapter_namespaces.py +++ b/nwb_linkml/tests/test_adapters/test_adapter_namespaces.py @@ -135,8 +135,9 @@ def test_roll_down_inheritance(): child = child_ns_adapter.get("Child") # overrides simple attrs assert child.doc == "child" - # gets unassigned parent attrs - assert "b" in [attr.name for attr in child.attributes] + # we don't receive attrs that aren't overridden in the child, + # instead we let python/linkml inheritance handle that for us + assert "b" not in [attr.name for attr in child.attributes] # overrides values while preserving remaining values when set attr_a = [attr for attr in child.attributes if attr.name == "a"][0] assert attr_a.value == "z" @@ -146,7 +147,8 @@ def test_roll_down_inheritance(): # preserve unset values in child datasets assert child.datasets[0].dtype == parent_cls.datasets[0].dtype assert child.datasets[0].dims == parent_cls.datasets[0].dims - # gets undeclared attrs in child datasets + # we *do* get undeclared attrs in child datasets, + # since those are not handled by python/linkml inheritance assert "d" in [attr.name for attr in child.datasets[0].attributes] # overrides set values in child datasets while preserving unset c_attr = [attr for attr in child.datasets[0].attributes if attr.name == "c"][0] diff --git a/nwb_linkml/tests/test_includes/conftest.py b/nwb_linkml/tests/test_includes/conftest.py index 53e3a39..1a801ae 100644 --- a/nwb_linkml/tests/test_includes/conftest.py +++ b/nwb_linkml/tests/test_includes/conftest.py @@ -114,14 +114,14 @@ def _icephys_stimulus_and_response( n_samples = generator.integers(20, 50) stimulus = VoltageClampStimulusSeries( name=f"vcss_{i}", - data=VoltageClampStimulusSeriesData(value=[i] * n_samples), + data=VoltageClampStimulusSeriesData(value=np.array([i] * n_samples, dtype=float)), stimulus_description=f"{i}", sweep_number=i, electrode=electrode, ) response = VoltageClampSeries( name=f"vcs_{i}", - data=VoltageClampSeriesData(value=[i] * n_samples), + data=VoltageClampSeriesData(value=np.array([i] * n_samples, dtype=float)), stimulus_description=f"{i}", electrode=electrode, )