From 880352d9a4470bd67b45007d25fe2754388adda7 Mon Sep 17 00:00:00 2001 From: sneakers-the-rat Date: Thu, 12 Sep 2024 22:40:14 -0700 Subject: [PATCH] v0.2.0 of nwb_schema_language - parentization --- .github/workflows/tests.yml | 4 + nwb_linkml/pyproject.toml | 2 +- nwb_linkml/src/nwb_linkml/adapters/adapter.py | 4 + nwb_linkml/src/nwb_linkml/adapters/group.py | 20 +- .../src/nwb_linkml/adapters/namespaces.py | 65 +- .../test_adapters/test_adapter_namespaces.py | 80 ++- nwb_schema_language/Makefile | 4 +- nwb_schema_language/pyproject.toml | 3 +- .../src/nwb_schema_language/__init__.py | 4 +- .../datamodel/nwb_schema_pydantic.py | 675 ++++++++++++++++-- .../src/nwb_schema_language/generator.py | 52 ++ .../src/nwb_schema_language/patches.py | 25 +- .../schema/nwb_schema_language.yaml | 15 +- nwb_schema_language/tests/test_data.py | 23 - nwb_schema_language/tests/test_mixins.py | 31 + 15 files changed, 893 insertions(+), 114 deletions(-) create mode 100644 nwb_schema_language/src/nwb_schema_language/generator.py delete mode 100644 nwb_schema_language/tests/test_data.py create mode 100644 nwb_schema_language/tests/test_mixins.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b1283e1..e89654a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -46,6 +46,10 @@ jobs: run: pytest working-directory: nwb_linkml + - name: Run nwb_schema_language Tests + run: pytest + working-directory: nwb_schema_language + - name: Coveralls Parallel uses: coverallsapp/github-action@v2.3.0 if: runner.os != 'macOS' diff --git a/nwb_linkml/pyproject.toml b/nwb_linkml/pyproject.toml index c8ccd36..edf3579 100644 --- a/nwb_linkml/pyproject.toml +++ b/nwb_linkml/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "nwb-models>=0.2.0", "pyyaml>=6.0", "linkml-runtime>=1.7.7", - "nwb-schema-language>=0.1.3", + "nwb-schema-language>=0.2.0", "rich>=13.5.2", #"linkml>=1.7.10", "linkml @ git+https://github.com/sneakers-the-rat/linkml@nwb-linkml", diff --git a/nwb_linkml/src/nwb_linkml/adapters/adapter.py b/nwb_linkml/src/nwb_linkml/adapters/adapter.py index acbc896..f7b4f2f 100644 --- a/nwb_linkml/src/nwb_linkml/adapters/adapter.py +++ b/nwb_linkml/src/nwb_linkml/adapters/adapter.py @@ -170,6 +170,10 @@ class Adapter(BaseModel): # so skip to avoid combinatoric walking if key == "imports" and type(input).__name__ == "SchemaAdapter": continue + # nwb_schema_language objects have a reference to their parent, + # which causes cycles + if key == "parent": + continue val = getattr(input, key) yield (key, val) if isinstance(val, (BaseModel, dict, list)): diff --git a/nwb_linkml/src/nwb_linkml/adapters/group.py b/nwb_linkml/src/nwb_linkml/adapters/group.py index 0703aa0..f0e44ea 100644 --- a/nwb_linkml/src/nwb_linkml/adapters/group.py +++ b/nwb_linkml/src/nwb_linkml/adapters/group.py @@ -29,7 +29,7 @@ class GroupAdapter(ClassAdapter): """ # Handle container groups with only * quantity unnamed groups if ( - len(self.cls.groups) > 0 + self.cls.groups and not self.cls.links and all([self._check_if_container(g) for g in self.cls.groups]) ): # and \ @@ -38,8 +38,8 @@ class GroupAdapter(ClassAdapter): # handle if we are a terminal container group without making a new class if ( - len(self.cls.groups) == 0 - and len(self.cls.datasets) == 0 + not self.cls.groups + and not self.cls.datasets and self.cls.neurodata_type_inc is not None and self.parent is not None ): @@ -177,15 +177,17 @@ class GroupAdapter(ClassAdapter): # Datasets are simple, they are terminal classes, and all logic # for creating slots vs. classes is handled by the adapter class dataset_res = BuildResult() - for dset in self.cls.datasets: - dset_adapter = DatasetAdapter(cls=dset, parent=self) - dataset_res += dset_adapter.build() + if self.cls.datasets: + for dset in self.cls.datasets: + dset_adapter = DatasetAdapter(cls=dset, parent=self) + dataset_res += dset_adapter.build() group_res = BuildResult() - for group in self.cls.groups: - group_adapter = GroupAdapter(cls=group, parent=self) - group_res += group_adapter.build() + if self.cls.groups: + for group in self.cls.groups: + group_adapter = GroupAdapter(cls=group, parent=self) + group_res += group_adapter.build() res = dataset_res + group_res diff --git a/nwb_linkml/src/nwb_linkml/adapters/namespaces.py b/nwb_linkml/src/nwb_linkml/adapters/namespaces.py index c6abd70..96d653e 100644 --- a/nwb_linkml/src/nwb_linkml/adapters/namespaces.py +++ b/nwb_linkml/src/nwb_linkml/adapters/namespaces.py @@ -9,11 +9,12 @@ import contextlib from copy import copy from pathlib import Path from pprint import pformat -from typing import Dict, List, Optional +from typing import Dict, Generator, List, Optional from linkml_runtime.dumpers import yaml_dumper from linkml_runtime.linkml_model import Annotation, SchemaDefinition from pydantic import Field, model_validator +import networkx as nx from nwb_linkml.adapters.adapter import Adapter, BuildResult from nwb_linkml.adapters.schema import SchemaAdapter @@ -31,6 +32,9 @@ class NamespacesAdapter(Adapter): schemas: List[SchemaAdapter] imported: List["NamespacesAdapter"] = Field(default_factory=list) + _completed: bool = False + """whether we have run the :meth:`.complete_namespace` method""" + @classmethod def from_yaml(cls, path: Path) -> "NamespacesAdapter": """ @@ -65,7 +69,7 @@ class NamespacesAdapter(Adapter): needed_adapter = NamespacesAdapter.from_yaml(needed_source_ns) ns_adapter.imported.append(needed_adapter) - ns_adapter.populate_imports() + ns_adapter.complete_namespaces() return ns_adapter @@ -76,6 +80,9 @@ class NamespacesAdapter(Adapter): Build the NWB namespace to the LinkML Schema """ + if not self._completed: + self.complete_namespaces() + sch_result = BuildResult() for sch in self.schemas: if progress is not None: @@ -149,6 +156,50 @@ class NamespacesAdapter(Adapter): break return self + def complete_namespaces(self): + """ + After loading the namespace, and after any imports have been added afterwards, + this must be called to complete the definitions of the contained schema objects. + + This is not automatic because NWB doesn't have a formal dependency resolution system, + so it is often impossible to know which imports are needed until after the namespace + adapter has been instantiated. + + It **is** automatically called if it hasn't been already by the :meth:`.build` method. + """ + self.populate_imports() + self._roll_down_inheritance() + + for i in self.imported: + i.complete_namespaces() + + self._completed = True + + def _roll_down_inheritance(self): + """ + 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. + + References: + https://github.com/NeurodataWithoutBorders/pynwb/issues/1954 + """ + pass + + def inheritance_graph(self) -> nx.DiGraph: + """ + Make a graph of all ``neurodata_types`` in the namespace and imports such that + each node contains the group or dataset it describes, + and has directed edges pointing at all the classes that inherit from it. + + In the case that the inheriting class does not itself have a ``neurodata_type_def``, + it is + """ + g = nx.DiGraph() + for sch in self.all_schemas(): + for cls in sch.created_classes: + pass + def find_type_source(self, name: str) -> SchemaAdapter: """ Given some neurodata_type_inc, find the schema that it's defined in. @@ -279,3 +330,13 @@ class NamespacesAdapter(Adapter): if name in sources: return ns.name return None + + def all_schemas(self) -> Generator[SchemaAdapter, None, None]: + """ + Iterator over all schemas including imports + """ + for sch in self.schemas: + yield sch + for imported in self.imported: + for sch in imported: + yield sch diff --git a/nwb_linkml/tests/test_adapters/test_adapter_namespaces.py b/nwb_linkml/tests/test_adapters/test_adapter_namespaces.py index bbcb739..768669b 100644 --- a/nwb_linkml/tests/test_adapters/test_adapter_namespaces.py +++ b/nwb_linkml/tests/test_adapters/test_adapter_namespaces.py @@ -1,6 +1,7 @@ import pytest - -from nwb_linkml.adapters import SchemaAdapter +from pathlib import Path +from nwb_linkml.adapters import NamespacesAdapter, SchemaAdapter +from nwb_schema_language import Attribute, Group, Namespace, Dataset, Namespaces, Schema, FlatDtype @pytest.mark.parametrize( @@ -48,8 +49,7 @@ def test_skip_imports(nwb_core_fixture): assert all([ns == "core" for ns in namespaces]) -@pytest.mark.skip() -def test_populate_inheritance(nwb_core_fixture): +def test_roll_down_inheritance(): """ Classes should receive and override the properties of their parents when they have neurodata_type_inc @@ -59,4 +59,74 @@ def test_populate_inheritance(nwb_core_fixture): Returns: """ - pass + parent_cls = Group( + neurodata_type_def="Parent", + doc="parent", + attributes=[ + Attribute(name="a", dims=["a", "b"], shape=[1, 2], doc="a", value="a"), + Attribute(name="b", dims=["c", "d"], shape=[3, 4], doc="b", value="b"), + ], + datasets=[ + Dataset( + name="data", + dims=["a", "b"], + shape=[1, 2], + doc="data", + attributes=[ + Attribute(name="c", dtype=FlatDtype.int32, doc="c"), + Attribute(name="d", dtype=FlatDtype.int32, doc="d"), + ], + ) + ], + ) + parent_sch = Schema(source="parent.yaml") + parent_ns = Namespaces( + namespaces=[ + Namespace( + author="hey", + contact="sup", + name="parent", + doc="a parent", + version="1", + schema=[parent_sch], + ) + ] + ) + + child_cls = Group( + neurodata_type_def="Child", + neurodata_type_inc="Parent", + doc="child", + attributes=[Attribute(name="a", doc="a")], + datasets=[ + Dataset( + name="data", + doc="data again", + attributes=[Attribute(name="a", doc="c", value="z"), Attribute(name="c", doc="c")], + ) + ], + ) + child_sch = Schema(source="child.yaml") + child_ns = Namespaces( + namespaces=[ + Namespace( + author="hey", + contact="sup", + name="child", + doc="a child", + version="1", + schema=[child_sch, Schema(namespace="parent")], + ) + ] + ) + + parent_schema_adapter = SchemaAdapter(path=Path("parent.yaml"), groups=[parent_cls]) + parent_ns_adapter = NamespacesAdapter(namespaces=parent_ns, schemas=[parent_schema_adapter]) + child_schema_adapter = SchemaAdapter(path=Path("child.yaml"), groups=[child_cls]) + child_ns_adapter = NamespacesAdapter( + namespaces=child_ns, schemas=[child_schema_adapter], imported=[parent_ns_adapter] + ) + + child_ns_adapter.complete_namespaces() + + child = child_ns_adapter.get("Child") diff --git a/nwb_schema_language/Makefile b/nwb_schema_language/Makefile index 9d6f45f..2f8cd76 100644 --- a/nwb_schema_language/Makefile +++ b/nwb_schema_language/Makefile @@ -6,7 +6,7 @@ SHELL := bash .SUFFIXES: .SECONDARY: -RUN = poetry run +RUN = pdm run # get values from about.yaml file SCHEMA_NAME = $(shell ${SHELL} ./utils/get-value.sh name) SOURCE_SCHEMA_PATH = $(shell ${SHELL} ./utils/get-value.sh source_schema_path) @@ -107,7 +107,7 @@ gen-project: $(PYMODEL) $(RUN) gen-project ${GEN_PARGS} -d $(DEST) $(SOURCE_SCHEMA_PATH) && mv $(DEST)/*.py $(PYMODEL) gen-pydantic: $(PYMODEL) - $(RUN) gen-pydantic $(SOURCE_SCHEMA_PATH) --pydantic_version 2 > $(PYMODEL)/nwb_schema_pydantic.py + $(RUN) generate_pydantic $(RUN) run_patches --phase post_generation_pydantic test: test-schema test-python test-examples diff --git a/nwb_schema_language/pyproject.toml b/nwb_schema_language/pyproject.toml index 1a59159..b912c77 100644 --- a/nwb_schema_language/pyproject.toml +++ b/nwb_schema_language/pyproject.toml @@ -9,7 +9,7 @@ dependencies = [ "linkml-runtime>=1.7.7", "pydantic>=2.3.0", ] -version = "0.1.3" +version = "0.2.0" description = "Translation of the nwb-schema-language to LinkML" readme = "README.md" @@ -20,6 +20,7 @@ documentation = "https://nwb-linkml.readthedocs.io" [project.scripts] run_patches = "nwb_schema_language.patches:main" +generate_pydantic = "nwb_schema_language.generator:generate" [tool.pdm] [tool.pdm.dev-dependencies] diff --git a/nwb_schema_language/src/nwb_schema_language/__init__.py b/nwb_schema_language/src/nwb_schema_language/__init__.py index 653b6ff..d211475 100644 --- a/nwb_schema_language/src/nwb_schema_language/__init__.py +++ b/nwb_schema_language/src/nwb_schema_language/__init__.py @@ -22,10 +22,10 @@ try: DTypeType = Union[List[CompoundDtype], FlatDtype, ReferenceDtype] -except (NameError, RecursionError): +except (NameError, RecursionError) as e: warnings.warn( "Error importing pydantic classes, passing because we might be in the process of patching" - " them, but it is likely they are broken and you will be unable to use them!", + f" them, but it is likely they are broken and you will be unable to use them!\n{e}", stacklevel=1, ) diff --git a/nwb_schema_language/src/nwb_schema_language/datamodel/nwb_schema_pydantic.py b/nwb_schema_language/src/nwb_schema_language/datamodel/nwb_schema_pydantic.py index 84132d0..d1bbac3 100644 --- a/nwb_schema_language/src/nwb_schema_language/datamodel/nwb_schema_pydantic.py +++ b/nwb_schema_language/src/nwb_schema_language/datamodel/nwb_schema_pydantic.py @@ -1,14 +1,13 @@ from __future__ import annotations -from datetime import datetime, date -from enum import Enum -from typing import List, Dict, Optional, Any, Union -from pydantic import BaseModel as BaseModel, Field -import sys -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal +import re +import sys +from datetime import date, datetime, time +from decimal import Decimal +from enum import Enum +from typing import Any, ClassVar, Dict, List, Literal, Optional, Union + +from pydantic import BaseModel, ConfigDict, Field, RootModel, field_validator, model_validator metamodel_version = "None" @@ -16,11 +15,81 @@ version = "None" class ConfiguredBaseModel(BaseModel): + model_config = ConfigDict( + validate_assignment=False, + validate_default=True, + extra="forbid", + arbitrary_types_allowed=True, + use_enum_values=True, + strict=False, + ) pass -class ReftypeOptions(str, Enum): +class LinkMLMeta(RootModel): + root: Dict[str, Any] = {} + model_config = ConfigDict(frozen=True) + def __getattr__(self, key: str): + return getattr(self.root, key) + + def __getitem__(self, key: str): + return self.root[key] + + def __setitem__(self, key: str, value): + self.root[key] = value + + def __contains__(self, key: str) -> bool: + return key in self.root + + +class ParentizeMixin(BaseModel): + + @model_validator(mode="after") + def parentize(self): + """Set the parent attribute for all our fields they have one""" + for field_name in self.model_fields: + if field_name == "parent": + continue + field = getattr(self, field_name) + if not isinstance(field, list): + field = [field] + for item in field: + if hasattr(item, "parent"): + item.parent = self + + return self + + +linkml_meta = LinkMLMeta( + { + "default_prefix": "nwb_schema_language", + "default_range": "string", + "description": "Translation of the nwb-schema-language to LinkML", + "id": "https://w3id.org/p2p_ld/nwb-schema-language", + "imports": ["linkml:types"], + "license": "GNU GPL v3.0", + "name": "nwb-schema-language", + "prefixes": { + "linkml": {"prefix_prefix": "linkml", "prefix_reference": "https://w3id.org/linkml/"}, + "nwb_schema_language": { + "prefix_prefix": "nwb_schema_language", + "prefix_reference": "https://w3id.org/p2p_ld/nwb-schema-language/", + }, + "schema": {"prefix_prefix": "schema", "prefix_reference": "http://schema.org/"}, + }, + "see_also": ["https://p2p_ld.github.io/nwb-schema-language"], + "settings": { + "email": {"setting_key": "email", "setting_value": "\\S+@\\S+{\\.\\w}+"}, + "protected_string": {"setting_key": "protected_string", "setting_value": "^[A-Za-z_][A-Za-z0-9_]*$"}, + }, + "source_file": "/Users/jonny/git/p2p-ld/nwb-linkml/nwb_schema_language/src/nwb_schema_language/schema/nwb_schema_language.yaml", + "title": "nwb-schema-language", + } +) + + +class ReftypeOptions(str, Enum): # Reference to another group or dataset of the given target_type ref = "ref" # Reference to another group or dataset of the given target_type @@ -32,7 +101,6 @@ class ReftypeOptions(str, Enum): class QuantityEnum(str, Enum): - # Zero or more instances, equivalent to zero_or_many ASTERISK = "*" # Zero or one instances, equivalent to zero_or_one @@ -48,7 +116,6 @@ class QuantityEnum(str, Enum): class FlatDtype(str, Enum): - # single precision floating point (32 bit) float = "float" # single precision floating point (32 bit) @@ -100,164 +167,642 @@ class FlatDtype(str, Enum): class Namespace(ConfiguredBaseModel): - - doc: str = Field(..., description="""Description of corresponding object.""") - name: str = Field(...) - full_name: Optional[str] = Field( - None, description="""Optional string with extended full name for the namespace.""" + linkml_meta: ClassVar[LinkMLMeta] = LinkMLMeta( + { + "from_schema": "https://w3id.org/p2p_ld/nwb-schema-language", + "slot_usage": {"name": {"name": "name", "required": True}}, + } ) - version: str = Field(...) + + doc: str = Field( + ..., + description="""Description of corresponding object.""", + json_schema_extra={ + "linkml_meta": { + "alias": "doc", + "domain_of": ["Namespace", "Schema", "Group", "Attribute", "Link", "Dataset", "CompoundDtype"], + } + }, + ) + name: str = Field( + ..., + json_schema_extra={ + "linkml_meta": { + "alias": "name", + "domain_of": ["Namespace", "Group", "Attribute", "Link", "Dataset", "CompoundDtype"], + "structured_pattern": {"interpolated": True, "syntax": "{protected_string}"}, + } + }, + ) + full_name: Optional[str] = Field( + None, + description="""Optional string with extended full name for the namespace.""", + json_schema_extra={"linkml_meta": {"alias": "full_name", "domain_of": ["Namespace"]}}, + ) + version: str = Field(..., json_schema_extra={"linkml_meta": {"alias": "version", "domain_of": ["Namespace"]}}) date: Optional[datetime] = Field( - None, description="""Date that a namespace was last modified or released""" + None, + description="""Date that a namespace was last modified or released""", + json_schema_extra={ + "linkml_meta": { + "alias": "date", + "domain_of": ["Namespace"], + "examples": [{"value": "2017-04-25 17:14:13"}], + "slot_uri": "schema:dateModified", + } + }, ) author: List[str] | str = Field( - default_factory=list, + ..., description="""List of strings with the names of the authors of the namespace.""", + json_schema_extra={"linkml_meta": {"alias": "author", "domain_of": ["Namespace"], "slot_uri": "schema:author"}}, ) contact: List[str] | str = Field( - default_factory=list, + ..., description="""List of strings with the contact information for the authors. Ordering of the contacts should match the ordering of the authors.""", + json_schema_extra={ + "linkml_meta": { + "alias": "contact", + "domain_of": ["Namespace"], + "slot_uri": "schema:email", + "structured_pattern": {"interpolated": True, "syntax": "{email}"}, + } + }, ) schema_: Optional[List[Schema]] = Field( + None, alias="schema", - default_factory=list, description="""List of the schema to be included in this namespace.""", + json_schema_extra={"linkml_meta": {"alias": "schema_", "domain_of": ["Namespace"]}}, ) class Namespaces(ConfiguredBaseModel): + linkml_meta: ClassVar[LinkMLMeta] = LinkMLMeta({"from_schema": "https://w3id.org/p2p_ld/nwb-schema-language"}) - namespaces: Optional[List[Namespace]] = Field(default_factory=list) + namespaces: Optional[List[Namespace]] = Field( + None, json_schema_extra={"linkml_meta": {"alias": "namespaces", "domain_of": ["Namespaces"]}} + ) class Schema(ConfiguredBaseModel): + linkml_meta: ClassVar[LinkMLMeta] = LinkMLMeta( + { + "from_schema": "https://w3id.org/p2p_ld/nwb-schema-language", + "rules": [ + { + "description": "If namespace is absent, source is required", + "postconditions": {"slot_conditions": {"source": {"name": "source", "required": True}}}, + "preconditions": { + "slot_conditions": {"namespace": {"name": "namespace", "value_presence": "ABSENT"}} + }, + }, + { + "description": "If source is absent, namespace is required.", + "postconditions": {"slot_conditions": {"namespace": {"name": "namespace", "required": True}}}, + "preconditions": {"slot_conditions": {"source": {"name": "source", "value_presence": "ABSENT"}}}, + }, + { + "description": "If namespace is present, source is cannot be", + "postconditions": {"slot_conditions": {"source": {"name": "source", "value_presence": "ABSENT"}}}, + "preconditions": { + "slot_conditions": {"namespace": {"name": "namespace", "value_presence": "PRESENT"}} + }, + }, + { + "description": "If source is present, namespace cannot be.", + "postconditions": { + "slot_conditions": {"namespace": {"name": "namespace", "value_presence": "ABSENT"}} + }, + "preconditions": {"slot_conditions": {"source": {"name": "source", "value_presence": "PRESENT"}}}, + }, + ], + } + ) source: Optional[str] = Field( None, description="""describes the name of the YAML (or JSON) file with the schema specification. The schema files should be located in the same folder as the namespace file.""", + json_schema_extra={"linkml_meta": {"alias": "source", "domain_of": ["Schema"]}}, ) namespace: Optional[str] = Field( None, description="""describes a named reference to another namespace. In contrast to source, this is a reference by name to a known namespace (i.e., the namespace is resolved during the build and must point to an already existing namespace). This mechanism is used to allow, e.g., extension of a core namespace (here the NWB core namespace) without requiring hard paths to the files describing the core namespace. Either source or namespace must be specified, but not both.""", + json_schema_extra={"linkml_meta": {"alias": "namespace", "domain_of": ["Schema"]}}, ) title: Optional[str] = Field( - None, description="""a descriptive title for a file for documentation purposes.""" + None, + description="""a descriptive title for a file for documentation purposes.""", + json_schema_extra={"linkml_meta": {"alias": "title", "domain_of": ["Schema"]}}, ) neurodata_types: Optional[List[Union[Dataset, Group]]] = Field( - default_factory=list, + None, description="""an optional list of strings indicating which data types should be included from the given specification source or namespace. The default is null indicating that all data types should be included.""", + json_schema_extra={ + "linkml_meta": { + "alias": "neurodata_types", + "any_of": [{"range": "Dataset"}, {"range": "Group"}], + "domain_of": ["Schema"], + } + }, + ) + doc: Optional[str] = Field( + None, + json_schema_extra={ + "linkml_meta": { + "alias": "doc", + "domain_of": ["Namespace", "Schema", "Group", "Attribute", "Link", "Dataset", "CompoundDtype"], + } + }, ) - doc: Optional[str] = Field(None) -class Group(ConfiguredBaseModel): +class Group(ConfiguredBaseModel, ParentizeMixin): + linkml_meta: ClassVar[LinkMLMeta] = LinkMLMeta({"from_schema": "https://w3id.org/p2p_ld/nwb-schema-language"}) neurodata_type_def: Optional[str] = Field( None, description="""Used alongside neurodata_type_inc to indicate inheritance, naming, and mixins""", + json_schema_extra={ + "linkml_meta": { + "alias": "neurodata_type_def", + "domain_of": ["Group", "Dataset"], + "structured_pattern": {"interpolated": True, "syntax": "{protected_string}"}, + } + }, ) neurodata_type_inc: Optional[str] = Field( None, description="""Used alongside neurodata_type_def to indicate inheritance, naming, and mixins""", + json_schema_extra={ + "linkml_meta": { + "alias": "neurodata_type_inc", + "domain_of": ["Group", "Dataset"], + "structured_pattern": {"interpolated": True, "syntax": "{protected_string}"}, + } + }, + ) + name: Optional[str] = Field( + None, + json_schema_extra={ + "linkml_meta": { + "alias": "name", + "domain_of": ["Namespace", "Group", "Attribute", "Link", "Dataset", "CompoundDtype"], + "structured_pattern": {"interpolated": True, "syntax": "{protected_string}"}, + } + }, + ) + default_name: Optional[str] = Field( + None, + json_schema_extra={ + "linkml_meta": { + "alias": "default_name", + "domain_of": ["Group", "Dataset"], + "structured_pattern": {"interpolated": True, "syntax": "{protected_string}"}, + } + }, + ) + doc: str = Field( + ..., + description="""Description of corresponding object.""", + json_schema_extra={ + "linkml_meta": { + "alias": "doc", + "domain_of": ["Namespace", "Schema", "Group", "Attribute", "Link", "Dataset", "CompoundDtype"], + } + }, + ) + quantity: Optional[Union[QuantityEnum, int]] = Field( + "1", + json_schema_extra={ + "linkml_meta": { + "alias": "quantity", + "any_of": [{"minimum_value": 1, "range": "integer"}, {"range": "QuantityEnum"}], + "domain_of": ["Group", "Link", "Dataset"], + "ifabsent": "int(1)", + "todos": ["logic to check that the corresponding class can only be " "implemented quantity times."], + } + }, + ) + linkable: Optional[bool] = Field( + None, json_schema_extra={"linkml_meta": {"alias": "linkable", "domain_of": ["Group", "Dataset"]}} + ) + attributes: Optional[List[Attribute]] = Field( + None, json_schema_extra={"linkml_meta": {"alias": "attributes", "domain_of": ["Group", "Dataset"]}} + ) + datasets: Optional[List[Dataset]] = Field( + None, json_schema_extra={"linkml_meta": {"alias": "datasets", "domain_of": ["Group", "Datasets"]}} + ) + groups: Optional[List[Group]] = Field( + None, json_schema_extra={"linkml_meta": {"alias": "groups", "domain_of": ["Group", "Groups"]}} + ) + links: Optional[List[Link]] = Field( + None, json_schema_extra={"linkml_meta": {"alias": "links", "domain_of": ["Group"]}} + ) + parent: Optional[Group] = Field( + None, + exclude=True, + description="""The parent group that contains this dataset or group""", + json_schema_extra={"linkml_meta": {"alias": "parent", "domain_of": ["Group", "Attribute", "Dataset"]}}, ) - name: Optional[str] = Field(None) - default_name: Optional[str] = Field(None) - doc: str = Field(..., description="""Description of corresponding object.""") - quantity: Optional[Union[QuantityEnum, int]] = Field(1) - linkable: Optional[bool] = Field(None) - attributes: Optional[List[Attribute]] = Field(default_factory=list) - datasets: Optional[List[Dataset]] = Field(default_factory=list) - groups: Optional[List[Group]] = Field(default_factory=list) - links: Optional[List[Link]] = Field(default_factory=list) class Groups(ConfiguredBaseModel): + linkml_meta: ClassVar[LinkMLMeta] = LinkMLMeta({"from_schema": "https://w3id.org/p2p_ld/nwb-schema-language"}) - groups: Optional[List[Group]] = Field(default_factory=list) + groups: Optional[List[Group]] = Field( + None, json_schema_extra={"linkml_meta": {"alias": "groups", "domain_of": ["Group", "Groups"]}} + ) class Link(ConfiguredBaseModel): + linkml_meta: ClassVar[LinkMLMeta] = LinkMLMeta({"from_schema": "https://w3id.org/p2p_ld/nwb-schema-language"}) - name: Optional[str] = Field(None) - doc: str = Field(..., description="""Description of corresponding object.""") + name: Optional[str] = Field( + None, + json_schema_extra={ + "linkml_meta": { + "alias": "name", + "domain_of": ["Namespace", "Group", "Attribute", "Link", "Dataset", "CompoundDtype"], + "structured_pattern": {"interpolated": True, "syntax": "{protected_string}"}, + } + }, + ) + doc: str = Field( + ..., + description="""Description of corresponding object.""", + json_schema_extra={ + "linkml_meta": { + "alias": "doc", + "domain_of": ["Namespace", "Schema", "Group", "Attribute", "Link", "Dataset", "CompoundDtype"], + } + }, + ) target_type: str = Field( ..., description="""Describes the neurodata_type of the target that the reference points to""", + json_schema_extra={"linkml_meta": {"alias": "target_type", "domain_of": ["Link", "ReferenceDtype"]}}, + ) + quantity: Optional[Union[QuantityEnum, int]] = Field( + "1", + json_schema_extra={ + "linkml_meta": { + "alias": "quantity", + "any_of": [{"minimum_value": 1, "range": "integer"}, {"range": "QuantityEnum"}], + "domain_of": ["Group", "Link", "Dataset"], + "ifabsent": "int(1)", + "todos": ["logic to check that the corresponding class can only be " "implemented quantity times."], + } + }, ) - quantity: Optional[Union[QuantityEnum, int]] = Field(1) class Datasets(ConfiguredBaseModel): + linkml_meta: ClassVar[LinkMLMeta] = LinkMLMeta({"from_schema": "https://w3id.org/p2p_ld/nwb-schema-language"}) - datasets: Optional[List[Dataset]] = Field(default_factory=list) + datasets: Optional[List[Dataset]] = Field( + None, json_schema_extra={"linkml_meta": {"alias": "datasets", "domain_of": ["Group", "Datasets"]}} + ) class ReferenceDtype(ConfiguredBaseModel): + linkml_meta: ClassVar[LinkMLMeta] = LinkMLMeta({"from_schema": "https://w3id.org/p2p_ld/nwb-schema-language"}) target_type: str = Field( ..., description="""Describes the neurodata_type of the target that the reference points to""", + json_schema_extra={"linkml_meta": {"alias": "target_type", "domain_of": ["Link", "ReferenceDtype"]}}, ) reftype: Optional[ReftypeOptions] = Field( - None, description="""describes the kind of reference""" + None, + description="""describes the kind of reference""", + json_schema_extra={"linkml_meta": {"alias": "reftype", "domain_of": ["ReferenceDtype"]}}, ) class CompoundDtype(ConfiguredBaseModel): + linkml_meta: ClassVar[LinkMLMeta] = LinkMLMeta( + { + "from_schema": "https://w3id.org/p2p_ld/nwb-schema-language", + "slot_usage": { + "dtype": { + "any_of": [{"range": "ReferenceDtype"}, {"range": "FlatDtype"}], + "multivalued": False, + "name": "dtype", + "required": True, + }, + "name": {"name": "name", "required": True}, + }, + } + ) - name: str = Field(...) - doc: str = Field(..., description="""Description of corresponding object.""") - dtype: Union[FlatDtype, ReferenceDtype] = Field(...) + name: str = Field( + ..., + json_schema_extra={ + "linkml_meta": { + "alias": "name", + "domain_of": ["Namespace", "Group", "Attribute", "Link", "Dataset", "CompoundDtype"], + "structured_pattern": {"interpolated": True, "syntax": "{protected_string}"}, + } + }, + ) + doc: str = Field( + ..., + description="""Description of corresponding object.""", + json_schema_extra={ + "linkml_meta": { + "alias": "doc", + "domain_of": ["Namespace", "Schema", "Group", "Attribute", "Link", "Dataset", "CompoundDtype"], + } + }, + ) + dtype: Union[FlatDtype, ReferenceDtype] = Field( + ..., + json_schema_extra={ + "linkml_meta": { + "alias": "dtype", + "any_of": [{"range": "ReferenceDtype"}, {"range": "FlatDtype"}], + "domain_of": ["CompoundDtype", "DtypeMixin"], + } + }, + ) class DtypeMixin(ConfiguredBaseModel): + linkml_meta: ClassVar[LinkMLMeta] = LinkMLMeta( + { + "from_schema": "https://w3id.org/p2p_ld/nwb-schema-language", + "mixin": True, + "rules": [ + { + "postconditions": {"slot_conditions": {"dtype": {"multivalued": False, "name": "dtype"}}}, + "preconditions": {"slot_conditions": {"dtype": {"name": "dtype", "range": "FlatDtype"}}}, + } + ], + } + ) dtype: Optional[Union[List[CompoundDtype], FlatDtype, ReferenceDtype]] = Field( - default_factory=list + None, + json_schema_extra={ + "linkml_meta": { + "alias": "dtype", + "any_of": [{"range": "FlatDtype"}, {"range": "CompoundDtype"}, {"range": "ReferenceDtype"}], + "domain_of": ["CompoundDtype", "DtypeMixin"], + } + }, ) class Attribute(DtypeMixin): + linkml_meta: ClassVar[LinkMLMeta] = LinkMLMeta( + { + "from_schema": "https://w3id.org/p2p_ld/nwb-schema-language", + "mixins": ["DtypeMixin"], + "slot_usage": { + "name": {"name": "name", "required": True}, + "parent": {"any_of": [{"range": "Group"}, {"range": "Dataset"}], "name": "parent"}, + }, + } + ) - name: str = Field(...) - dims: Optional[List[Union[Any, str]]] = Field(None) - shape: Optional[List[Union[Any, int, str]]] = Field(None) + name: str = Field( + ..., + json_schema_extra={ + "linkml_meta": { + "alias": "name", + "domain_of": ["Namespace", "Group", "Attribute", "Link", "Dataset", "CompoundDtype"], + "structured_pattern": {"interpolated": True, "syntax": "{protected_string}"}, + } + }, + ) + dims: Optional[List[Union[Any, str]]] = Field( + None, + json_schema_extra={ + "linkml_meta": { + "alias": "dims", + "any_of": [{"range": "string"}, {"range": "AnyType"}], + "domain_of": ["Attribute", "Dataset"], + "todos": [ + "Can't quite figure out how to allow an array of arrays - see " + "https://github.com/linkml/linkml/issues/895" + ], + } + }, + ) + shape: Optional[List[Union[Any, int, str]]] = Field( + None, + json_schema_extra={ + "linkml_meta": { + "alias": "shape", + "any_of": [ + {"minimum_value": 1, "range": "integer"}, + {"equals_string": "null", "range": "string"}, + {"range": "AnyType"}, + ], + "domain_of": ["Attribute", "Dataset"], + "todos": [ + "Can't quite figure out how to allow an array of arrays - see " + "https://github.com/linkml/linkml/issues/895" + ], + } + }, + ) value: Optional[Any] = Field( - None, description="""Optional constant, fixed value for the attribute.""" + None, + description="""Optional constant, fixed value for the attribute.""", + json_schema_extra={"linkml_meta": {"alias": "value", "domain_of": ["Attribute", "Dataset"]}}, ) default_value: Optional[Any] = Field( - None, description="""Optional default value for variable-valued attributes.""" + None, + description="""Optional default value for variable-valued attributes.""", + json_schema_extra={"linkml_meta": {"alias": "default_value", "domain_of": ["Attribute", "Dataset"]}}, + ) + doc: str = Field( + ..., + description="""Description of corresponding object.""", + json_schema_extra={ + "linkml_meta": { + "alias": "doc", + "domain_of": ["Namespace", "Schema", "Group", "Attribute", "Link", "Dataset", "CompoundDtype"], + } + }, ) - doc: str = Field(..., description="""Description of corresponding object.""") required: Optional[bool] = Field( True, description="""Optional boolean key describing whether the attribute is required. Default value is True.""", + json_schema_extra={"linkml_meta": {"alias": "required", "domain_of": ["Attribute"], "ifabsent": "true"}}, + ) + parent: Optional[Union[Dataset, Group]] = Field( + None, + exclude=True, + description="""The parent group that contains this dataset or group""", + json_schema_extra={ + "linkml_meta": { + "alias": "parent", + "any_of": [{"range": "Group"}, {"range": "Dataset"}], + "domain_of": ["Group", "Attribute", "Dataset"], + } + }, + ) + dtype: Optional[Union[List[CompoundDtype], FlatDtype, ReferenceDtype]] = Field( + None, + json_schema_extra={ + "linkml_meta": { + "alias": "dtype", + "any_of": [{"range": "FlatDtype"}, {"range": "CompoundDtype"}, {"range": "ReferenceDtype"}], + "domain_of": ["CompoundDtype", "DtypeMixin"], + } + }, ) - dtype: Optional[Union[List[CompoundDtype], FlatDtype, ReferenceDtype]] = Field(None) -class Dataset(DtypeMixin): +class Dataset(ConfiguredBaseModel, ParentizeMixin): + linkml_meta: ClassVar[LinkMLMeta] = LinkMLMeta( + {"from_schema": "https://w3id.org/p2p_ld/nwb-schema-language", "mixins": ["DtypeMixin"]} + ) neurodata_type_def: Optional[str] = Field( None, description="""Used alongside neurodata_type_inc to indicate inheritance, naming, and mixins""", + json_schema_extra={ + "linkml_meta": { + "alias": "neurodata_type_def", + "domain_of": ["Group", "Dataset"], + "structured_pattern": {"interpolated": True, "syntax": "{protected_string}"}, + } + }, ) neurodata_type_inc: Optional[str] = Field( None, description="""Used alongside neurodata_type_def to indicate inheritance, naming, and mixins""", + json_schema_extra={ + "linkml_meta": { + "alias": "neurodata_type_inc", + "domain_of": ["Group", "Dataset"], + "structured_pattern": {"interpolated": True, "syntax": "{protected_string}"}, + } + }, + ) + name: Optional[str] = Field( + None, + json_schema_extra={ + "linkml_meta": { + "alias": "name", + "domain_of": ["Namespace", "Group", "Attribute", "Link", "Dataset", "CompoundDtype"], + "structured_pattern": {"interpolated": True, "syntax": "{protected_string}"}, + } + }, + ) + default_name: Optional[str] = Field( + None, + json_schema_extra={ + "linkml_meta": { + "alias": "default_name", + "domain_of": ["Group", "Dataset"], + "structured_pattern": {"interpolated": True, "syntax": "{protected_string}"}, + } + }, + ) + dims: Optional[List[Union[Any, str]]] = Field( + None, + json_schema_extra={ + "linkml_meta": { + "alias": "dims", + "any_of": [{"range": "string"}, {"range": "AnyType"}], + "domain_of": ["Attribute", "Dataset"], + "todos": [ + "Can't quite figure out how to allow an array of arrays - see " + "https://github.com/linkml/linkml/issues/895" + ], + } + }, + ) + shape: Optional[List[Union[Any, int, str]]] = Field( + None, + json_schema_extra={ + "linkml_meta": { + "alias": "shape", + "any_of": [ + {"minimum_value": 1, "range": "integer"}, + {"equals_string": "null", "range": "string"}, + {"range": "AnyType"}, + ], + "domain_of": ["Attribute", "Dataset"], + "todos": [ + "Can't quite figure out how to allow an array of arrays - see " + "https://github.com/linkml/linkml/issues/895" + ], + } + }, ) - name: Optional[str] = Field(None) - default_name: Optional[str] = Field(None) - dims: Optional[List[Union[Any, str]]] = Field(None) - shape: Optional[List[Union[Any, int, str]]] = Field(None) value: Optional[Any] = Field( - None, description="""Optional constant, fixed value for the attribute.""" + None, + description="""Optional constant, fixed value for the attribute.""", + json_schema_extra={"linkml_meta": {"alias": "value", "domain_of": ["Attribute", "Dataset"]}}, ) default_value: Optional[Any] = Field( - None, description="""Optional default value for variable-valued attributes.""" + None, + description="""Optional default value for variable-valued attributes.""", + json_schema_extra={"linkml_meta": {"alias": "default_value", "domain_of": ["Attribute", "Dataset"]}}, ) - doc: str = Field(..., description="""Description of corresponding object.""") - quantity: Optional[Union[QuantityEnum, int]] = Field(1) - linkable: Optional[bool] = Field(None) - attributes: Optional[List[Attribute]] = Field(None) - dtype: Optional[Union[List[CompoundDtype], FlatDtype, ReferenceDtype]] = Field(None) + doc: str = Field( + ..., + description="""Description of corresponding object.""", + json_schema_extra={ + "linkml_meta": { + "alias": "doc", + "domain_of": ["Namespace", "Schema", "Group", "Attribute", "Link", "Dataset", "CompoundDtype"], + } + }, + ) + quantity: Optional[Union[QuantityEnum, int]] = Field( + "1", + json_schema_extra={ + "linkml_meta": { + "alias": "quantity", + "any_of": [{"minimum_value": 1, "range": "integer"}, {"range": "QuantityEnum"}], + "domain_of": ["Group", "Link", "Dataset"], + "ifabsent": "int(1)", + "todos": ["logic to check that the corresponding class can only be " "implemented quantity times."], + } + }, + ) + linkable: Optional[bool] = Field( + None, json_schema_extra={"linkml_meta": {"alias": "linkable", "domain_of": ["Group", "Dataset"]}} + ) + attributes: Optional[List[Attribute]] = Field( + None, json_schema_extra={"linkml_meta": {"alias": "attributes", "domain_of": ["Group", "Dataset"]}} + ) + parent: Optional[Group] = Field( + None, + exclude=True, + description="""The parent group that contains this dataset or group""", + json_schema_extra={"linkml_meta": {"alias": "parent", "domain_of": ["Group", "Attribute", "Dataset"]}}, + ) + dtype: Optional[Union[List[CompoundDtype], FlatDtype, ReferenceDtype]] = Field( + None, + json_schema_extra={ + "linkml_meta": { + "alias": "dtype", + "any_of": [{"range": "FlatDtype"}, {"range": "CompoundDtype"}, {"range": "ReferenceDtype"}], + "domain_of": ["CompoundDtype", "DtypeMixin"], + } + }, + ) + + +# Model rebuild +# see https://pydantic-docs.helpmanual.io/usage/models/#rebuilding-a-model +Namespace.model_rebuild() +Namespaces.model_rebuild() +Schema.model_rebuild() +Group.model_rebuild() +Groups.model_rebuild() +Link.model_rebuild() +Datasets.model_rebuild() +ReferenceDtype.model_rebuild() +CompoundDtype.model_rebuild() +DtypeMixin.model_rebuild() +Attribute.model_rebuild() +Dataset.model_rebuild() diff --git a/nwb_schema_language/src/nwb_schema_language/generator.py b/nwb_schema_language/src/nwb_schema_language/generator.py new file mode 100644 index 0000000..eefad6b --- /dev/null +++ b/nwb_schema_language/src/nwb_schema_language/generator.py @@ -0,0 +1,52 @@ +from pathlib import Path +from dataclasses import dataclass + +from linkml.generators.pydanticgen import PydanticGenerator +from linkml.generators.pydanticgen.build import ClassResult +from linkml.generators.pydanticgen.template import Import, ObjectImport +from linkml_runtime import SchemaView +from pydantic import BaseModel, model_validator + + +class ParentizeMixin(BaseModel): + + @model_validator(mode="after") + def parentize(self): + """Set the parent attribute for all our fields they have one""" + for field_name in self.model_fields: + if field_name == "parent": + continue + field = getattr(self, field_name) + if not isinstance(field, list): + field = [field] + for item in field: + if hasattr(item, "parent"): + item.parent = self + + return self + + +@dataclass +class NWBSchemaLangGenerator(PydanticGenerator): + + def __init__(self, *args, **kwargs): + kwargs["injected_classes"] = [ParentizeMixin] + kwargs["imports"] = [ + Import(module="pydantic", objects=[ObjectImport(name="model_validator")]) + ] + kwargs["black"] = True + super().__init__(*args, **kwargs) + + def after_generate_class(self, cls: ClassResult, sv: SchemaView) -> ClassResult: + if cls.cls.name in ("Dataset", "Group"): + cls.cls.bases = ["ConfiguredBaseModel", "ParentizeMixin"] + return cls + + +def generate(): + schema = Path(__file__).parent / "schema" / "nwb_schema_language.yaml" + output = Path(__file__).parent / "datamodel" / "nwb_schema_pydantic.py" + generator = NWBSchemaLangGenerator(schema=schema) + generated = generator.serialize() + with open(output, "w") as ofile: + ofile.write(generated) diff --git a/nwb_schema_language/src/nwb_schema_language/patches.py b/nwb_schema_language/src/nwb_schema_language/patches.py index 1b2c9a5..f6fa5b1 100644 --- a/nwb_schema_language/src/nwb_schema_language/patches.py +++ b/nwb_schema_language/src/nwb_schema_language/patches.py @@ -49,8 +49,15 @@ class Patch: patch_schema_slot = Patch( phase=Phases.post_generation_pydantic, path=Path("src/nwb_schema_language/datamodel/nwb_schema_pydantic.py"), - match=r"\n\s*(schema:)(.*Field\()(.*)", - replacement=r'\n schema_:\2alias="schema", \3', + match=r"\n\s*(schema:)(.*Field\(\n\s*None,\n)(.*)", + replacement=r'\n schema_:\2 alias="schema",\n\3', +) + +patch_schema_slot_no_newline = Patch( + phase=Phases.post_generation_pydantic, + path=Path("src/nwb_schema_language/datamodel/nwb_schema_pydantic.py"), + match=r"\n\s*(schema:)(.*Field\(None,)(.*)", + replacement=r'\n schema_:\2 alias="schema", \3', ) patch_dtype_single_multiple = Patch( @@ -74,6 +81,20 @@ patch_contact_single_multiple = Patch( replacement="contact: List[str] | str", ) +patch_validate_assignment = Patch( + phase=Phases.post_generation_pydantic, + path=Path("src/nwb_schema_language/datamodel/nwb_schema_pydantic.py"), + match=r"validate_assignment=True", + replacement="validate_assignment=False", +) + +patch_exclude_parent = Patch( + phase=Phases.post_generation_pydantic, + path=Path("src/nwb_schema_language/datamodel/nwb_schema_pydantic.py"), + match=r"(parent:.*Field\(\n\s*None,\n)(.*)", + replacement=r"\1 exclude=True,\n\2", +) + def run_patches(phase: Phases, verbose: bool = False) -> None: """ diff --git a/nwb_schema_language/src/nwb_schema_language/schema/nwb_schema_language.yaml b/nwb_schema_language/src/nwb_schema_language/schema/nwb_schema_language.yaml index ff06a56..00c9aa9 100644 --- a/nwb_schema_language/src/nwb_schema_language/schema/nwb_schema_language.yaml +++ b/nwb_schema_language/src/nwb_schema_language/schema/nwb_schema_language.yaml @@ -78,6 +78,7 @@ classes: - datasets - groups - links + - parent Groups: slots: @@ -94,9 +95,14 @@ classes: - default_value - doc - required + - parent slot_usage: name: required: true + parent: + any_of: + - range: Group + - range: Dataset Link: slots: @@ -121,6 +127,7 @@ classes: - quantity - linkable - attributes + - parent Datasets: slots: @@ -177,7 +184,7 @@ slots: description: Optional string with extended full name for the namespace. version: required: true - pattern: "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" +# pattern: "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" date: range: datetime slot_uri: schema:dateModified @@ -207,7 +214,6 @@ slots: # schema source: description: describes the name of the YAML (or JSON) file with the schema specification. The schema files should be located in the same folder as the namespace file. - pattern: ".*\\.(yml|yaml|json)" namespace: description: describes a named reference to another namespace. In contrast to source, this is a reference by name to a known namespace (i.e., the namespace is resolved during the build and must point to an already existing namespace). This mechanism is used to allow, e.g., extension of a core namespace (here the NWB core namespace) without requiring hard paths to the files describing the core namespace. Either source or namespace must be specified, but not both. namespaces: @@ -312,6 +318,11 @@ slots: description: describes the kind of reference range: reftype_options + # extra - not defined in nwb-schema-language but useful when working with class objects + parent: + description: The parent group that contains this dataset or group + range: Group + required: false enums: diff --git a/nwb_schema_language/tests/test_data.py b/nwb_schema_language/tests/test_data.py deleted file mode 100644 index b2f7030..0000000 --- a/nwb_schema_language/tests/test_data.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Data test.""" - -import os -import glob -import unittest - -from linkml_runtime.loaders import yaml_loader -from nwb_schema_language.datamodel.nwb_schema_language import Namespaces - -ROOT = os.path.join(os.path.dirname(__file__), "..") -DATA_DIR = os.path.join(ROOT, "src", "data", "tests") - -EXAMPLE_FILES = glob.glob(os.path.join(DATA_DIR, "*.yaml")) - - -class TestData(unittest.TestCase): - """Test data and datamodel.""" - - def test_namespaces(self): - """Date test.""" - namespace_file = [f for f in EXAMPLE_FILES if "namespace.yaml" in f][0] - obj = yaml_loader.load(namespace_file, target_class=Namespaces) - assert obj diff --git a/nwb_schema_language/tests/test_mixins.py b/nwb_schema_language/tests/test_mixins.py new file mode 100644 index 0000000..ba98e6e --- /dev/null +++ b/nwb_schema_language/tests/test_mixins.py @@ -0,0 +1,31 @@ +from nwb_schema_language import Group, Dataset, Attribute + + +def test_parentize_mixin(): + """ + the parentize mixin should populate the "parent" attribute for applicable children + """ + dset_attr = Attribute(name="dset_attr", doc="") + dset = Dataset( + name="dataset", doc="", attributes=[dset_attr, {"name": "dict_based_attr", "doc": ""}] + ) + group_attr = Attribute(name="group_attr", doc="") + group = Group( + name="group", + doc="", + attributes=[group_attr, {"name": "dict_based_attr", "doc": ""}], + datasets=[dset, {"name": "dict_based_dset", "doc": ""}], + ) + + assert dset_attr.parent is dset + assert dset.attributes[1].name == "dict_based_attr" + assert dset.attributes[1].parent is dset + assert dset.parent is group + assert group_attr.parent is group + assert group.attributes[1].name == "dict_based_attr" + assert group.attributes[1].parent is group + assert group.datasets[1].name == "dict_based_dset" + assert group.datasets[1].parent is group + + dumped = group.model_dump() + assert "parent" not in dumped