From 198ed3bcea3a561320ccf6db35fa0f6ae5270299 Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Mon, 30 Sep 2024 23:14:50 -0700 Subject: [PATCH] test named slot, add skeletons for basemodel methods --- .../src/nwb_linkml/generators/pydantic.py | 4 +- nwb_linkml/src/nwb_linkml/includes/base.py | 2 +- nwb_linkml/tests/fixtures/schema.py | 9 ++++ .../test_generator_pydantic.py | 28 +++++++++++- nwb_linkml/tests/test_includes/test_base.py | 45 +++++++++++++++++++ 5 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 nwb_linkml/tests/test_includes/test_base.py diff --git a/nwb_linkml/src/nwb_linkml/generators/pydantic.py b/nwb_linkml/src/nwb_linkml/generators/pydantic.py index 49e51c5..7506ff8 100644 --- a/nwb_linkml/src/nwb_linkml/generators/pydantic.py +++ b/nwb_linkml/src/nwb_linkml/generators/pydantic.py @@ -25,7 +25,7 @@ from linkml_runtime.utils.schemaview import SchemaView from nwb_linkml.includes.base import ( BASEMODEL_CAST_WITH_VALUE, - BASEMODEL_COERCE_CHILD, + BASEMODEL_COERCE_SUBCLASS, BASEMODEL_COERCE_VALUE, BASEMODEL_EXTRA_TO_VALUE, BASEMODEL_GETITEM, @@ -56,7 +56,7 @@ class NWBPydanticGenerator(PydanticGenerator): BASEMODEL_GETITEM, BASEMODEL_COERCE_VALUE, BASEMODEL_CAST_WITH_VALUE, - BASEMODEL_COERCE_CHILD, + BASEMODEL_COERCE_SUBCLASS, BASEMODEL_EXTRA_TO_VALUE, ) split: bool = True diff --git a/nwb_linkml/src/nwb_linkml/includes/base.py b/nwb_linkml/src/nwb_linkml/includes/base.py index 4747f57..1b04041 100644 --- a/nwb_linkml/src/nwb_linkml/includes/base.py +++ b/nwb_linkml/src/nwb_linkml/includes/base.py @@ -44,7 +44,7 @@ BASEMODEL_CAST_WITH_VALUE = """ raise e1 """ -BASEMODEL_COERCE_CHILD = """ +BASEMODEL_COERCE_SUBCLASS = """ @field_validator("*", mode="before") @classmethod def coerce_subclass(cls, v: Any, info) -> Any: diff --git a/nwb_linkml/tests/fixtures/schema.py b/nwb_linkml/tests/fixtures/schema.py index 4f041bd..a6890cc 100644 --- a/nwb_linkml/tests/fixtures/schema.py +++ b/nwb_linkml/tests/fixtures/schema.py @@ -101,6 +101,15 @@ def linkml_schema_bare() -> TestSchemas: inlined_as_list=False, any_of=[{"range": "OtherClass"}, {"range": "StillAnotherClass"}], ), + SlotDefinition( + name="named_slot", + description=( + "A slot that should use the Named[] generic to set the name param" + ), + annotations=[{"named": True}], + range="OtherClass", + inlined=True, + ), SlotDefinition( name="value", description="Main class's array", diff --git a/nwb_linkml/tests/test_generators/test_generator_pydantic.py b/nwb_linkml/tests/test_generators/test_generator_pydantic.py index a11336d..5b44092 100644 --- a/nwb_linkml/tests/test_generators/test_generator_pydantic.py +++ b/nwb_linkml/tests/test_generators/test_generator_pydantic.py @@ -17,6 +17,7 @@ import pytest from linkml_runtime.utils.compile_python import compile_python from numpydantic.dtype import Float from numpydantic.ndarray import NDArrayMeta +from pydantic import ValidationError from nwb_linkml.generators.pydantic import NWBPydanticGenerator @@ -86,7 +87,9 @@ def imported_schema(linkml_schema, request) -> TestModules: def test_array(imported_schema): """ - Arraylike classes are converted to slots that specify nptyping arrays + Arraylike classes are converted to slots that specify nptyping arrays. + + Test that we can use any_of with the array slot (unlike the upstream generator, currently) array: Optional[Union[ NDArray[Shape["* x, * y"], Number], @@ -155,3 +158,26 @@ def test_get_item(imported_schema): """We can get without explicitly addressing array""" cls = imported_schema["core"].MainTopLevel(value=np.array([[1, 2, 3], [4, 5, 6]], dtype=float)) assert np.array_equal(cls[0], np.array([1, 2, 3], dtype=float)) + + +def test_named_slot(imported_schema): + """ + Slots that have a ``named`` annotation should get their ``name`` attribute set automatically + """ + OtherClass = imported_schema["core"].OtherClass + MainClass = imported_schema["core"].MainTopLevel + + # We did in fact get the outer annotation + # this is a wild ass way to get the function name but hey + annotation = MainClass.model_fields["named_slot"].annotation.__args__[0] + validation_fn_name = annotation.__metadata__[0].func.__name__ + assert validation_fn_name == "_get_name" + + # we can't instantiate OtherClass without the ``name`` + with pytest.raises(ValidationError, match=".*name.*"): + _ = OtherClass() + + # but when we instantiate MainClass the name gets set automatically + instance = MainClass(named_slot={}) + assert isinstance(instance.named_slot, OtherClass) + assert instance.named_slot.name == "named_slot" diff --git a/nwb_linkml/tests/test_includes/test_base.py b/nwb_linkml/tests/test_includes/test_base.py new file mode 100644 index 0000000..7a25af8 --- /dev/null +++ b/nwb_linkml/tests/test_includes/test_base.py @@ -0,0 +1,45 @@ +""" +Base includes +""" + +import pytest + + +@pytest.mark.skip +def test_basemodel_getitem(imported_schema): + """ + We can get a value from ``value`` if we have it + """ + pass + + +@pytest.mark.skip +def test_basemodel_coerce_value(imported_schema): + """ + We can instantiate something by trying to grab it's "value" item + """ + pass + + +@pytest.mark.skip +def test_basemodel_cast_with_value(imported_schema): + """ + Opposite of above, we try to cast **into** the ``value`` field + """ + pass + + +@pytest.mark.skip +def test_basemodel_coerce_subclass(imported_schema): + """ + We try to rescue by coercing to a child class if possible + """ + pass + + +@pytest.mark.skip +def test_basemodel_extra_to_value(imported_schema): + """ + We gather extra fields and put them into a value dict when it's present + """ + pass