mirror of
https://github.com/p2p-ld/nwb-linkml.git
synced 2024-11-10 00:34:29 +00:00
classes adapter
This commit is contained in:
parent
79397ec398
commit
adaf939497
4 changed files with 212 additions and 25 deletions
|
@ -5,8 +5,10 @@
|
||||||
:maxdepth: 3
|
:maxdepth: 3
|
||||||
|
|
||||||
api/index
|
api/index
|
||||||
|
todo
|
||||||
changelog
|
changelog
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
8
docs/todo.md
Normal file
8
docs/todo.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# TODO
|
||||||
|
|
||||||
|
Important things that are not implemented yet!
|
||||||
|
|
||||||
|
- {meth}`nwb_linkml.adapters.classes.ClassAdapter.handle_dtype` does not yet handle compound dtypes,
|
||||||
|
leaving them as `AnyType` instead. This is fine for a first draft since they are used rarely within
|
||||||
|
NWB, but we will need to handle them by making slots for each of the dtypes since they typically
|
||||||
|
represent table-like data.
|
|
@ -10,8 +10,6 @@ from nwb_linkml.maps import QUANTITY_MAP
|
||||||
from nwb_linkml.maps.naming import camel_to_snake
|
from nwb_linkml.maps.naming import camel_to_snake
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ClassAdapter(Adapter):
|
class ClassAdapter(Adapter):
|
||||||
"""
|
"""
|
||||||
Abstract adapter to class-like things in linkml, holds methods common to
|
Abstract adapter to class-like things in linkml, holds methods common to
|
||||||
|
@ -33,10 +31,24 @@ class ClassAdapter(Adapter):
|
||||||
"""
|
"""
|
||||||
Build the basic class and attributes before adding any specific
|
Build the basic class and attributes before adding any specific
|
||||||
modifications for groups or datasets.
|
modifications for groups or datasets.
|
||||||
|
|
||||||
|
The main distinction in behavior for this method is whether this class has a parent class -
|
||||||
|
ie this is one of the anonymous nested child datasets or groups within another group.
|
||||||
|
|
||||||
|
If the class has no parent, then...
|
||||||
|
|
||||||
|
* Its name is inferred from its `neurodata_type_def`, fixed name, or `neurodata_type_inc` in that order
|
||||||
|
* It is just built as normal class!
|
||||||
|
* It will be indicated as a ``tree_root`` (which will primarily be used to invert the translation for write operations)
|
||||||
|
|
||||||
|
If the class has a parent, then...
|
||||||
|
|
||||||
|
* If it has a `neurodata_type_def` or `inc`, that will be used as its name, otherwise concatenate `parent__child`,
|
||||||
|
eg. ``TimeSeries__TimeSeriesData``
|
||||||
|
* A slot will also be made and returned with the BuildResult, which the parent will then have as one of its attributes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Build this class
|
# Build this class
|
||||||
#name = self._get_full_name()
|
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
if self.parent is not None:
|
if self.parent is not None:
|
||||||
kwargs['name'] = self._get_full_name()
|
kwargs['name'] = self._get_full_name()
|
||||||
|
@ -105,7 +117,6 @@ class ClassAdapter(Adapter):
|
||||||
else:
|
else:
|
||||||
raise ValueError('Not sure what our name is!')
|
raise ValueError('Not sure what our name is!')
|
||||||
|
|
||||||
|
|
||||||
return name
|
return name
|
||||||
|
|
||||||
def _get_attr_name(self) -> str:
|
def _get_attr_name(self) -> str:
|
||||||
|
@ -113,19 +124,13 @@ class ClassAdapter(Adapter):
|
||||||
Get the name to use as the attribute name,
|
Get the name to use as the attribute name,
|
||||||
again distinct from the actual name of the instantiated object
|
again distinct from the actual name of the instantiated object
|
||||||
"""
|
"""
|
||||||
# return self._get_full_name()
|
if self.cls.neurodata_type_def is not None:
|
||||||
name = None
|
|
||||||
if self.cls.neurodata_type_def:
|
|
||||||
# name = camel_to_snake(self.cls.neurodata_type_def)
|
|
||||||
name = self.cls.neurodata_type_def
|
name = self.cls.neurodata_type_def
|
||||||
elif self.cls.name is not None:
|
elif self.cls.name is not None:
|
||||||
# we do have a unique name
|
|
||||||
name = self.cls.name
|
name = self.cls.name
|
||||||
elif self.cls.neurodata_type_inc:
|
elif self.cls.neurodata_type_inc is not None:
|
||||||
# name = camel_to_snake(self.cls.neurodata_type_inc)
|
|
||||||
name = self.cls.neurodata_type_inc
|
name = self.cls.neurodata_type_inc
|
||||||
|
else:
|
||||||
if name is None:
|
|
||||||
raise ValueError(f'Class has no name!: {self.cls}')
|
raise ValueError(f'Class has no name!: {self.cls}')
|
||||||
|
|
||||||
return name
|
return name
|
||||||
|
@ -136,19 +141,13 @@ class ClassAdapter(Adapter):
|
||||||
used to dodge name overlaps by snake-casing!
|
used to dodge name overlaps by snake-casing!
|
||||||
again distinct from the actual name of the instantiated object
|
again distinct from the actual name of the instantiated object
|
||||||
"""
|
"""
|
||||||
# return self._get_full_name()
|
|
||||||
name = None
|
|
||||||
if self.cls.neurodata_type_def:
|
if self.cls.neurodata_type_def:
|
||||||
name = camel_to_snake(self.cls.neurodata_type_def)
|
name = camel_to_snake(self.cls.neurodata_type_def)
|
||||||
# name = self.cls.neurodata_type_def
|
|
||||||
elif self.cls.name is not None:
|
elif self.cls.name is not None:
|
||||||
# we do have a unique name
|
|
||||||
name = self.cls.name
|
name = self.cls.name
|
||||||
elif self.cls.neurodata_type_inc:
|
elif self.cls.neurodata_type_inc:
|
||||||
name = camel_to_snake(self.cls.neurodata_type_inc)
|
name = camel_to_snake(self.cls.neurodata_type_inc)
|
||||||
# name = self.cls.neurodata_type_inc
|
else:
|
||||||
|
|
||||||
if name is None:
|
|
||||||
raise ValueError(f'Class has no name!: {self.cls}')
|
raise ValueError(f'Class has no name!: {self.cls}')
|
||||||
|
|
||||||
return name
|
return name
|
||||||
|
@ -167,7 +166,6 @@ class ClassAdapter(Adapter):
|
||||||
# so we'll... uh... treat them as slots.
|
# so we'll... uh... treat them as slots.
|
||||||
# TODO
|
# TODO
|
||||||
return 'AnyType'
|
return 'AnyType'
|
||||||
#raise NotImplementedError('got distracted, need to implement')
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# flat dtype
|
# flat dtype
|
||||||
|
|
|
@ -4,11 +4,190 @@ import pytest
|
||||||
|
|
||||||
from ..fixtures import linkml_schema_bare, linkml_schema, nwb_schema
|
from ..fixtures import linkml_schema_bare, linkml_schema, nwb_schema
|
||||||
|
|
||||||
from nwb_linkml.adapters import DatasetAdapter, ClassAdapter
|
from linkml_runtime.linkml_model import SlotDefinition
|
||||||
|
from nwb_linkml.adapters import DatasetAdapter, ClassAdapter, GroupAdapter
|
||||||
|
from nwb_schema_language import Group, Dataset, ReferenceDtype, CompoundDtype
|
||||||
|
|
||||||
def test_build_base(nwb_schema):
|
def test_build_base(nwb_schema):
|
||||||
# simplest case, nothing special here
|
# simplest case, nothing special here. Should be same behavior between dataset and group
|
||||||
dset = DatasetAdapter(cls=nwb_schema.datasets['image'])
|
dset = DatasetAdapter(cls=nwb_schema.datasets['image'])
|
||||||
|
base = dset.build_base()
|
||||||
|
assert len(base.slots) == 0
|
||||||
|
assert len(base.classes) == 1
|
||||||
|
img = base.classes[0]
|
||||||
|
assert img.name == "Image"
|
||||||
|
# no parent class, tree_root shoudl be true
|
||||||
|
assert img.tree_root
|
||||||
|
assert len(img.attributes) == 3
|
||||||
|
|
||||||
|
# now with parent class
|
||||||
|
groups = GroupAdapter(cls=nwb_schema.groups['images'])
|
||||||
|
dset.parent = groups
|
||||||
|
base = dset.build_base()
|
||||||
|
# we made a self-slot (will be tested elsewhere)
|
||||||
|
assert len(base.slots) == 1
|
||||||
|
assert len(base.classes) == 1
|
||||||
|
img = base.classes[0]
|
||||||
|
assert not img.tree_root
|
||||||
|
assert len(img.attributes) == 3
|
||||||
|
|
||||||
|
# now try adding an extra attribute
|
||||||
|
slot = SlotDefinition(name="newslot", range="string")
|
||||||
|
# should coerce single slot to a list within the method
|
||||||
|
base = dset.build_base(extra_attrs=slot)
|
||||||
|
assert len(base.slots) == 1
|
||||||
|
assert len(base.classes) == 1
|
||||||
|
img = base.classes[0]
|
||||||
|
assert len(img.attributes) == 4
|
||||||
|
assert img.attributes['newslot'] is slot
|
||||||
|
|
||||||
|
def test_get_attr_name():
|
||||||
|
"""Name method used by parentless classes"""
|
||||||
|
cls = Dataset(neurodata_type_def='MyClass', doc='a class')
|
||||||
|
adapter = DatasetAdapter(cls=cls)
|
||||||
|
# type_defs get their original name
|
||||||
|
assert adapter._get_attr_name() == 'MyClass'
|
||||||
|
|
||||||
|
# explicit names get that name, but only if there is no type_def
|
||||||
|
adapter.cls.name = 'MyClassName'
|
||||||
|
assert adapter._get_attr_name() == 'MyClass'
|
||||||
|
adapter.cls.neurodata_type_def = None
|
||||||
|
assert adapter._get_attr_name() == 'MyClassName'
|
||||||
|
|
||||||
|
# if neither, use the type inc
|
||||||
|
adapter.cls.neurodata_type_inc = 'MyThirdName'
|
||||||
|
assert adapter._get_attr_name() == 'MyClassName'
|
||||||
|
adapter.cls.name = None
|
||||||
|
assert adapter._get_attr_name() == 'MyThirdName'
|
||||||
|
|
||||||
|
# if none are present, raise a value error
|
||||||
|
adapter.cls.neurodata_type_inc = None
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
adapter._get_attr_name()
|
||||||
|
|
||||||
|
def test_get_full_name():
|
||||||
|
"""Name used by child classes"""
|
||||||
|
cls = Dataset(neurodata_type_def='Child', doc='a class')
|
||||||
|
parent = GroupAdapter(cls=Group(neurodata_type_def='Parent', doc='a class'))
|
||||||
|
adapter = DatasetAdapter(cls=cls, parent=parent)
|
||||||
|
|
||||||
|
# if child has its own type_def, use that
|
||||||
|
assert adapter._get_full_name() == 'Child'
|
||||||
|
|
||||||
|
# same thing with type_inc
|
||||||
|
adapter.cls.neurodata_type_def = None
|
||||||
|
adapter.cls.neurodata_type_inc = 'ChildInc'
|
||||||
|
assert adapter._get_full_name() == 'ChildInc'
|
||||||
|
|
||||||
|
# if it just has a name, it gets concatenated with its parents
|
||||||
|
adapter.cls.neurodata_type_inc = None
|
||||||
|
adapter.cls.name = 'ChildName'
|
||||||
|
assert adapter._get_full_name() == 'Parent__ChildName'
|
||||||
|
|
||||||
|
# this should work at any depth of nesting if the parent is not an independently defined class
|
||||||
|
grandparent = GroupAdapter(cls=Group(neurodata_type_def='Grandparent', doc='a class'))
|
||||||
|
parent.cls.neurodata_type_def = None
|
||||||
|
parent.cls.name = 'ParentName'
|
||||||
|
parent.parent = grandparent
|
||||||
|
assert adapter._get_full_name() == 'Grandparent__ParentName__ChildName'
|
||||||
|
|
||||||
|
# if it has none, raise value error
|
||||||
|
adapter.cls.name = None
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
adapter._get_full_name()
|
||||||
|
|
||||||
|
def test_self_slot():
|
||||||
|
"""
|
||||||
|
Slot that represents ourselves to our parent
|
||||||
|
|
||||||
|
Quantity map is tested elsewhere
|
||||||
|
"""
|
||||||
|
cls = Dataset(neurodata_type_def='ChildClass', doc='a class', quantity='?')
|
||||||
|
parent = GroupAdapter(cls=Group(neurodata_type_def='Parent', doc='a class'))
|
||||||
|
adapter = DatasetAdapter(cls=cls, parent=parent)
|
||||||
|
|
||||||
|
# base case - snake case a type def
|
||||||
|
slot = adapter.build_self_slot()
|
||||||
|
assert slot.name == 'child_class'
|
||||||
|
assert slot.range == 'ChildClass' == adapter._get_full_name()
|
||||||
|
|
||||||
|
# this should be the slot that gets build with the build_base method
|
||||||
|
base = adapter.build_base()
|
||||||
|
assert len(base.slots) == 1
|
||||||
|
assert base.slots[0] == slot
|
||||||
|
|
||||||
|
# if class has a unique name, use that without changing, but only if no type_def
|
||||||
|
|
||||||
|
adapter.cls.name = "FixedName"
|
||||||
|
slot = adapter.build_self_slot()
|
||||||
|
assert slot.name == 'child_class'
|
||||||
|
adapter.cls.neurodata_type_def = None
|
||||||
|
slot = adapter.build_self_slot()
|
||||||
|
assert slot.name == 'FixedName'
|
||||||
|
assert slot.range == adapter._get_full_name()
|
||||||
|
|
||||||
|
# type_inc works the same as type_def, but only if name and type_def are None
|
||||||
|
adapter.cls.neurodata_type_inc = 'IncName'
|
||||||
|
slot = adapter.build_self_slot()
|
||||||
|
assert slot.name == 'FixedName'
|
||||||
|
adapter.cls.name = None
|
||||||
|
slot = adapter.build_self_slot()
|
||||||
|
assert slot.name == 'inc_name'
|
||||||
|
assert slot.range == adapter._get_full_name()
|
||||||
|
|
||||||
|
# if we have nothing, raise value error
|
||||||
|
adapter.cls.neurodata_type_inc = None
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
adapter.build_self_slot()
|
||||||
|
|
||||||
|
|
||||||
|
def test_name_slot():
|
||||||
|
"""Classes with a fixed name should name slot with a fixed value"""
|
||||||
|
# no name
|
||||||
|
cls = DatasetAdapter(cls=Dataset(neurodata_type_def='MyClass', doc='a class'))
|
||||||
|
slot = cls.build_name_slot()
|
||||||
|
assert slot.name == 'name'
|
||||||
|
assert slot.required
|
||||||
|
assert slot.range == 'string'
|
||||||
|
assert slot.identifier
|
||||||
|
assert slot.ifabsent is None
|
||||||
|
assert slot.equals_string is None
|
||||||
|
|
||||||
|
cls.cls.name = 'FixedName'
|
||||||
|
slot = cls.build_name_slot()
|
||||||
|
assert slot.name == 'name'
|
||||||
|
assert slot.required
|
||||||
|
assert slot.range == 'string'
|
||||||
|
assert slot.identifier
|
||||||
|
assert slot.ifabsent == 'string(FixedName)'
|
||||||
|
assert slot.equals_string == 'FixedName'
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_dtype(nwb_schema):
|
||||||
|
"""
|
||||||
|
Dtypes should be translated from nwb schema language to linkml
|
||||||
|
|
||||||
|
Dtypes are validated by the nwb_schema_language classes, so we don't do that here
|
||||||
|
"""
|
||||||
|
cls = DatasetAdapter(cls=Dataset(neurodata_type_def='MyClass', doc='a class'))
|
||||||
|
|
||||||
|
reftype = ReferenceDtype(target_type='TargetClass', reftype='reference')
|
||||||
|
compoundtype = [
|
||||||
|
CompoundDtype(name='field_a', doc='field a!', dtype='int32'),
|
||||||
|
CompoundDtype(name='field_b', doc='field b!', dtype='text'),
|
||||||
|
CompoundDtype(name="reference", doc="reference!", dtype=reftype)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
assert cls.handle_dtype(reftype) == 'TargetClass'
|
||||||
|
assert cls.handle_dtype(None) == 'AnyType'
|
||||||
|
assert cls.handle_dtype([]) == 'AnyType'
|
||||||
|
# handling compound types is currently TODO
|
||||||
|
assert cls.handle_dtype(compoundtype) == 'AnyType'
|
||||||
|
assert cls.handle_dtype('int32') == 'int32'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#pdb.set_trace()
|
|
||||||
pass
|
|
Loading…
Reference in a new issue