classes adapter

This commit is contained in:
sneakers-the-rat 2023-10-09 20:13:42 -07:00
parent 79397ec398
commit adaf939497
4 changed files with 212 additions and 25 deletions

View file

@ -5,8 +5,10 @@
:maxdepth: 3 :maxdepth: 3
api/index api/index
todo
changelog changelog
``` ```

8
docs/todo.md Normal file
View 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.

View file

@ -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

View file

@ -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