mirror of
https://github.com/p2p-ld/nwb-linkml.git
synced 2025-01-09 21:54:27 +00:00
correctly generating rolled down classes. model update to follow
This commit is contained in:
parent
cad57554fd
commit
1d27c6a259
6 changed files with 99 additions and 58 deletions
|
@ -20,6 +20,7 @@ from linkml_runtime.linkml_model import (
|
||||||
from pydantic import BaseModel, PrivateAttr
|
from pydantic import BaseModel, PrivateAttr
|
||||||
|
|
||||||
from nwb_linkml.logging import init_logger
|
from nwb_linkml.logging import init_logger
|
||||||
|
from nwb_linkml.maps.dtype import float_types, integer_types, string_types
|
||||||
from nwb_schema_language import Attribute, CompoundDtype, Dataset, Group, Schema
|
from nwb_schema_language import Attribute, CompoundDtype, Dataset, Group, Schema
|
||||||
|
|
||||||
if sys.version_info.minor >= 11:
|
if sys.version_info.minor >= 11:
|
||||||
|
@ -308,5 +309,48 @@ def has_attrs(cls: Dataset) -> bool:
|
||||||
return (
|
return (
|
||||||
cls.attributes is not None
|
cls.attributes is not None
|
||||||
and len(cls.attributes) > 0
|
and len(cls.attributes) > 0
|
||||||
and all([not a.value for a in cls.attributes])
|
and any([not a.value for a in cls.attributes])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def defaults(cls: Dataset | Attribute) -> dict:
|
||||||
|
"""
|
||||||
|
Handle default values -
|
||||||
|
|
||||||
|
* If ``value`` is present, yield `equals_string` or `equals_number` depending on dtype
|
||||||
|
**as well as** an ``ifabsent`` value - we both constrain the possible values to 1
|
||||||
|
and also supply it as the default
|
||||||
|
* else, if ``default_value`` is present, yield an appropriate ``ifabsent`` value
|
||||||
|
* If neither, yield an empty dict
|
||||||
|
|
||||||
|
Unlike nwb_schema_language, when ``value`` is set, we yield both a ``equals_*`` constraint
|
||||||
|
and an ``ifabsent`` constraint, because an ``equals_*`` can be declared without a default
|
||||||
|
in order to validate that a value is correctly set as the constrained value, and fail
|
||||||
|
if a value isn't provided.
|
||||||
|
"""
|
||||||
|
ret = {}
|
||||||
|
if cls.value:
|
||||||
|
if cls.dtype in integer_types:
|
||||||
|
ret["equals_number"] = cls.value
|
||||||
|
ret["ifabsent"] = f"integer({cls.value})"
|
||||||
|
elif cls.dtype in float_types:
|
||||||
|
ret["equals_number"] = cls.value
|
||||||
|
ret["ifabsent"] = f"float({cls.value})"
|
||||||
|
elif cls.dtype in string_types:
|
||||||
|
ret["equals_string"] = cls.value
|
||||||
|
ret["ifabsent"] = f"string({cls.value})"
|
||||||
|
else:
|
||||||
|
ret["equals_string"] = cls.value
|
||||||
|
ret["ifabsent"] = cls.value
|
||||||
|
|
||||||
|
elif cls.default_value:
|
||||||
|
if cls.dtype in string_types:
|
||||||
|
ret["ifabsent"] = f"string({cls.default_value})"
|
||||||
|
elif cls.dtype in integer_types:
|
||||||
|
ret["ifabsent"] = f"int({cls.default_value})"
|
||||||
|
elif cls.dtype in float_types:
|
||||||
|
ret["ifabsent"] = f"float({cls.default_value})"
|
||||||
|
else:
|
||||||
|
ret["ifabsent"] = cls.default_value
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
|
@ -7,26 +7,13 @@ from typing import ClassVar, Optional, Type, TypedDict
|
||||||
|
|
||||||
from linkml_runtime.linkml_model.meta import SlotDefinition
|
from linkml_runtime.linkml_model.meta import SlotDefinition
|
||||||
|
|
||||||
from nwb_linkml.adapters.adapter import Adapter, BuildResult, is_1d
|
from nwb_linkml.adapters.adapter import Adapter, BuildResult, defaults, is_1d
|
||||||
from nwb_linkml.adapters.array import ArrayAdapter
|
from nwb_linkml.adapters.array import ArrayAdapter
|
||||||
from nwb_linkml.maps import Map
|
from nwb_linkml.maps import Map
|
||||||
from nwb_linkml.maps.dtype import handle_dtype, inlined
|
from nwb_linkml.maps.dtype import handle_dtype, inlined
|
||||||
from nwb_schema_language import Attribute
|
from nwb_schema_language import Attribute
|
||||||
|
|
||||||
|
|
||||||
def _make_ifabsent(val: str | int | float | None) -> str | None:
|
|
||||||
if val is None:
|
|
||||||
return None
|
|
||||||
elif isinstance(val, str):
|
|
||||||
return f"string({val})"
|
|
||||||
elif isinstance(val, int):
|
|
||||||
return f"integer({val})"
|
|
||||||
elif isinstance(val, float):
|
|
||||||
return f"float({val})"
|
|
||||||
else:
|
|
||||||
return str(val)
|
|
||||||
|
|
||||||
|
|
||||||
class AttrDefaults(TypedDict):
|
class AttrDefaults(TypedDict):
|
||||||
"""Default fields for an attribute"""
|
"""Default fields for an attribute"""
|
||||||
|
|
||||||
|
@ -38,31 +25,6 @@ class AttrDefaults(TypedDict):
|
||||||
class AttributeMap(Map):
|
class AttributeMap(Map):
|
||||||
"""Base class for attribute mapping transformations :)"""
|
"""Base class for attribute mapping transformations :)"""
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def handle_defaults(cls, attr: Attribute) -> AttrDefaults:
|
|
||||||
"""
|
|
||||||
Construct arguments for linkml slot default metaslots from nwb schema lang attribute props
|
|
||||||
"""
|
|
||||||
equals_string = None
|
|
||||||
equals_number = None
|
|
||||||
default_value = None
|
|
||||||
if attr.value:
|
|
||||||
if isinstance(attr.value, (int, float)):
|
|
||||||
equals_number = attr.value
|
|
||||||
elif attr.value:
|
|
||||||
equals_string = str(attr.value)
|
|
||||||
|
|
||||||
if equals_number:
|
|
||||||
default_value = _make_ifabsent(equals_number)
|
|
||||||
elif equals_string:
|
|
||||||
default_value = _make_ifabsent(equals_string)
|
|
||||||
elif attr.default_value:
|
|
||||||
default_value = _make_ifabsent(attr.default_value)
|
|
||||||
|
|
||||||
return AttrDefaults(
|
|
||||||
equals_string=equals_string, equals_number=equals_number, ifabsent=default_value
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def check(cls, attr: Attribute) -> bool:
|
def check(cls, attr: Attribute) -> bool:
|
||||||
|
@ -105,7 +67,7 @@ class MapScalar(AttributeMap):
|
||||||
description=attr.doc,
|
description=attr.doc,
|
||||||
required=attr.required,
|
required=attr.required,
|
||||||
inlined=inlined(attr.dtype),
|
inlined=inlined(attr.dtype),
|
||||||
**cls.handle_defaults(attr),
|
**defaults(attr),
|
||||||
)
|
)
|
||||||
return BuildResult(slots=[slot])
|
return BuildResult(slots=[slot])
|
||||||
|
|
||||||
|
@ -154,7 +116,7 @@ class MapArray(AttributeMap):
|
||||||
required=attr.required,
|
required=attr.required,
|
||||||
inlined=inlined(attr.dtype),
|
inlined=inlined(attr.dtype),
|
||||||
**expressions,
|
**expressions,
|
||||||
**cls.handle_defaults(attr),
|
**defaults(attr),
|
||||||
)
|
)
|
||||||
return BuildResult(slots=[slot])
|
return BuildResult(slots=[slot])
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ from typing import ClassVar, Optional, Type
|
||||||
|
|
||||||
from linkml_runtime.linkml_model.meta import ArrayExpression, SlotDefinition
|
from linkml_runtime.linkml_model.meta import ArrayExpression, SlotDefinition
|
||||||
|
|
||||||
from nwb_linkml.adapters.adapter import BuildResult, has_attrs, is_1d, is_compound
|
from nwb_linkml.adapters.adapter import BuildResult, defaults, has_attrs, is_1d, is_compound
|
||||||
from nwb_linkml.adapters.array import ArrayAdapter
|
from nwb_linkml.adapters.array import ArrayAdapter
|
||||||
from nwb_linkml.adapters.classes import ClassAdapter
|
from nwb_linkml.adapters.classes import ClassAdapter
|
||||||
from nwb_linkml.maps import QUANTITY_MAP, Map
|
from nwb_linkml.maps import QUANTITY_MAP, Map
|
||||||
|
@ -108,6 +108,7 @@ class MapScalar(DatasetMap):
|
||||||
description=cls.doc,
|
description=cls.doc,
|
||||||
range=handle_dtype(cls.dtype),
|
range=handle_dtype(cls.dtype),
|
||||||
**QUANTITY_MAP[cls.quantity],
|
**QUANTITY_MAP[cls.quantity],
|
||||||
|
**defaults(cls),
|
||||||
)
|
)
|
||||||
res = BuildResult(slots=[this_slot])
|
res = BuildResult(slots=[this_slot])
|
||||||
return res
|
return res
|
||||||
|
@ -208,7 +209,19 @@ class MapScalarAttributes(DatasetMap):
|
||||||
"""
|
"""
|
||||||
Map to a scalar attribute with an adjoining "value" slot
|
Map to a scalar attribute with an adjoining "value" slot
|
||||||
"""
|
"""
|
||||||
value_slot = SlotDefinition(name="value", range=handle_dtype(cls.dtype), required=True)
|
# the *value slot* within the generated class is always required,
|
||||||
|
# but the slot in the parent class referring to this one will indicate whether the whole
|
||||||
|
# thing is optional or not. You can't provide the attributes of the optional dataset
|
||||||
|
# without providing its value
|
||||||
|
quantity = QUANTITY_MAP[cls.quantity].copy()
|
||||||
|
quantity["required"] = True
|
||||||
|
|
||||||
|
value_slot = SlotDefinition(
|
||||||
|
name="value",
|
||||||
|
range=handle_dtype(cls.dtype),
|
||||||
|
**quantity,
|
||||||
|
**defaults(cls),
|
||||||
|
)
|
||||||
res.classes[0].attributes["value"] = value_slot
|
res.classes[0].attributes["value"] = value_slot
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
|
@ -266,11 +266,7 @@ class NamespacesAdapter(Adapter):
|
||||||
in_schema = False
|
in_schema = False
|
||||||
if isinstance(cls, str) and cls in [
|
if isinstance(cls, str) and cls in [
|
||||||
c.neurodata_type_def for c in schema.created_classes
|
c.neurodata_type_def for c in schema.created_classes
|
||||||
]:
|
] or isinstance(cls, Dataset) and cls in schema.datasets or isinstance(cls, Group) and cls in schema.groups:
|
||||||
in_schema = True
|
|
||||||
elif isinstance(cls, Dataset) and cls in schema.datasets:
|
|
||||||
in_schema = True
|
|
||||||
elif isinstance(cls, Group) and cls in schema.groups:
|
|
||||||
in_schema = True
|
in_schema = True
|
||||||
|
|
||||||
if in_schema:
|
if in_schema:
|
||||||
|
@ -397,16 +393,16 @@ def roll_down_nwb_class(
|
||||||
Merge an ancestor (via ``neurodata_type_inc`` ) source class with a
|
Merge an ancestor (via ``neurodata_type_inc`` ) source class with a
|
||||||
child ``target`` class.
|
child ``target`` class.
|
||||||
|
|
||||||
On the first recurive pass, only those values that are set on the target are copied from the
|
On the first recursive pass, only those values that are set on the target are copied from the
|
||||||
source class - this isn't a true merging, what we are after is to recursively merge all the
|
source class - this isn't a true merging, what we are after is to recursively merge all the
|
||||||
values that are modified in the child class with those of the parent class below the top level,
|
values that are modified in the child class with those of the parent class below the top level,
|
||||||
the top-level attributes will be carried through via normal inheritance.
|
the top-level attributes will be carried through via normal inheritance.
|
||||||
|
|
||||||
Rather than re-instantiating the child class, we return the dictionary so that this
|
Rather than re-instantiating the child class, we return the dictionary so that this
|
||||||
function can be used in series to merge a whole ancestry chain within
|
function can be used in series to merge a whole ancestry chain within
|
||||||
:class:`.NamespacesAdapter` , but this isn't exposed in the function since
|
:class:`.NamespacesAdapter` , but merging isn't exposed in the function since
|
||||||
class definitions can be spread out over many schemas, and we need the orchestration
|
ancestor class definitions can be spread out over many schemas,
|
||||||
of the adapter to have them in all cases we'd be using this.
|
and we need the orchestration of the adapter to have them in all cases we'd be using this.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
source (dict): source dictionary
|
source (dict): source dictionary
|
||||||
|
@ -420,9 +416,9 @@ def roll_down_nwb_class(
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if isinstance(source, (Group, Dataset)):
|
if isinstance(source, (Group, Dataset)):
|
||||||
source = source.model_dump(exclude_unset=True, exclude_none=True)
|
source = source.model_dump(exclude_none=True)
|
||||||
if isinstance(target, (Group, Dataset)):
|
if isinstance(target, (Group, Dataset)):
|
||||||
target = target.model_dump(exclude_unset=True, exclude_none=True)
|
target = target.model_dump(exclude_none=True)
|
||||||
|
|
||||||
exclude = ("neurodata_type_def",)
|
exclude = ("neurodata_type_def",)
|
||||||
|
|
||||||
|
|
|
@ -66,6 +66,26 @@ flat_to_np = {
|
||||||
"isodatetime": np.datetime64,
|
"isodatetime": np.datetime64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
integer_types = {
|
||||||
|
"long",
|
||||||
|
"int64",
|
||||||
|
"int",
|
||||||
|
"int32",
|
||||||
|
"int16",
|
||||||
|
"short",
|
||||||
|
"int8",
|
||||||
|
"uint",
|
||||||
|
"uint32",
|
||||||
|
"uint16",
|
||||||
|
"uint8",
|
||||||
|
"uint64",
|
||||||
|
}
|
||||||
|
|
||||||
|
float_types = {"float", "float32", "double", "float64", "numeric"}
|
||||||
|
|
||||||
|
string_types = {"text", "utf", "utf8", "utf_8", "ascii"}
|
||||||
|
|
||||||
|
|
||||||
np_to_python = {
|
np_to_python = {
|
||||||
Any: Any,
|
Any: Any,
|
||||||
np.number: float,
|
np.number: float,
|
||||||
|
|
|
@ -9,10 +9,16 @@ We will handle cardinality of array dimensions elsewhere
|
||||||
"""
|
"""
|
||||||
|
|
||||||
QUANTITY_MAP = {
|
QUANTITY_MAP = {
|
||||||
"*": {"required": False, "multivalued": True},
|
"*": {"required": None, "multivalued": True},
|
||||||
"+": {"required": True, "multivalued": True},
|
"+": {"required": True, "multivalued": True},
|
||||||
"?": {"required": False, "multivalued": False},
|
"?": {"required": None, "multivalued": None},
|
||||||
1: {"required": True, "multivalued": False},
|
1: {"required": True, "multivalued": None},
|
||||||
# include the NoneType for indexing
|
# include the NoneType for indexing
|
||||||
None: {"required": None, "multivalued": None},
|
None: {"required": None, "multivalued": None},
|
||||||
}
|
}
|
||||||
|
"""
|
||||||
|
Map between NWB quantity values and linkml quantity metaslot values.
|
||||||
|
|
||||||
|
Use ``None`` for defaults (required: False, multivalued: False) rather than ``False``
|
||||||
|
to avoid adding unnecessary attributes
|
||||||
|
"""
|
||||||
|
|
Loading…
Reference in a new issue