Skip to content

Commit ae37db3

Browse files
Merge pull request #14 from p2p-ld/roll-down
roll down parent inheritance recursively
2 parents f94a144 + 2ce1367 commit ae37db3

File tree

315 files changed

+21727
-5817
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

315 files changed

+21727
-5817
lines changed

.github/workflows/tests.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ jobs:
4646
run: pytest
4747
working-directory: nwb_linkml
4848

49+
- name: Run nwb_schema_language Tests
50+
run: pytest
51+
working-directory: nwb_schema_language
52+
4953
- name: Coveralls Parallel
5054
uses: coverallsapp/[email protected]
5155
if: runner.os != 'macOS'

docs/meta/todo.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ Loading
5353
- [ ] Top-level containers are still a little janky, eg. how `ProcessingModule` just accepts
5454
extra args rather than properly abstracting `value` as a `__getitem__(self, key) -> T:`
5555

56+
Changes to linkml
57+
- [ ] Allow parameterizing "extra" fields, so we don't have to stuff things into `value` dicts
58+
5659
## Docs TODOs
5760

5861
```{todolist}

nwb_linkml/pdm.lock

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nwb_linkml/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ dependencies = [
1212
"nwb-models>=0.2.0",
1313
"pyyaml>=6.0",
1414
"linkml-runtime>=1.7.7",
15-
"nwb-schema-language>=0.1.3",
15+
"nwb-schema-language>=0.2.0",
1616
"rich>=13.5.2",
1717
#"linkml>=1.7.10",
1818
"linkml @ git+https://github.com/sneakers-the-rat/linkml@nwb-linkml",
@@ -22,7 +22,7 @@ dependencies = [
2222
"pydantic-settings>=2.0.3",
2323
"tqdm>=4.66.1",
2424
'typing-extensions>=4.12.2;python_version<"3.11"',
25-
"numpydantic>=1.5.0",
25+
"numpydantic>=1.6.0",
2626
"black>=24.4.2",
2727
"pandas>=2.2.2",
2828
"networkx>=3.3",

nwb_linkml/src/nwb_linkml/adapters/adapter.py

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
SlotDefinition,
1818
TypeDefinition,
1919
)
20-
from pydantic import BaseModel
20+
from pydantic import BaseModel, PrivateAttr
2121

2222
from nwb_linkml.logging import init_logger
23+
from nwb_linkml.maps.dtype import float_types, integer_types, string_types
2324
from nwb_schema_language import Attribute, CompoundDtype, Dataset, Group, Schema
2425

2526
if sys.version_info.minor >= 11:
@@ -103,6 +104,7 @@ class Adapter(BaseModel):
103104

104105
_logger: Optional[Logger] = None
105106
_debug: Optional[bool] = None
107+
_nwb_classes: dict[str, Dataset | Group] = PrivateAttr(default_factory=dict)
106108

107109
@property
108110
def debug(self) -> bool:
@@ -135,7 +137,10 @@ def get(self, name: str) -> Union[Group, Dataset]:
135137
136138
Convenience wrapper around :meth:`.walk_field_values`
137139
"""
138-
return next(self.walk_field_values(self, "neurodata_type_def", name))
140+
if name not in self._nwb_classes:
141+
cls = next(self.walk_field_values(self, "neurodata_type_def", name))
142+
self._nwb_classes[name] = cls
143+
return self._nwb_classes[name]
139144

140145
def get_model_with_field(self, field: str) -> Generator[Union[Group, Dataset], None, None]:
141146
"""
@@ -170,6 +175,10 @@ def walk(
170175
# so skip to avoid combinatoric walking
171176
if key == "imports" and type(input).__name__ == "SchemaAdapter":
172177
continue
178+
# nwb_schema_language objects have a reference to their parent,
179+
# which causes cycles
180+
if key == "parent":
181+
continue
173182
val = getattr(input, key)
174183
yield (key, val)
175184
if isinstance(val, (BaseModel, dict, list)):
@@ -300,5 +309,85 @@ def has_attrs(cls: Dataset) -> bool:
300309
return (
301310
cls.attributes is not None
302311
and len(cls.attributes) > 0
303-
and all([not a.value for a in cls.attributes])
312+
and any([not a.value for a in cls.attributes])
313+
)
314+
315+
316+
def defaults(cls: Dataset | Attribute) -> dict:
317+
"""
318+
Handle default values -
319+
320+
* If ``value`` is present, yield `equals_string` or `equals_number` depending on dtype
321+
**as well as** an ``ifabsent`` value - we both constrain the possible values to 1
322+
and also supply it as the default
323+
* else, if ``default_value`` is present, yield an appropriate ``ifabsent`` value
324+
* If neither, yield an empty dict
325+
326+
Unlike nwb_schema_language, when ``value`` is set, we yield both a ``equals_*`` constraint
327+
and an ``ifabsent`` constraint, because an ``equals_*`` can be declared without a default
328+
in order to validate that a value is correctly set as the constrained value, and fail
329+
if a value isn't provided.
330+
"""
331+
ret = {}
332+
if cls.value:
333+
if cls.dtype in integer_types:
334+
ret["equals_number"] = cls.value
335+
ret["ifabsent"] = f"integer({cls.value})"
336+
elif cls.dtype in float_types:
337+
ret["equals_number"] = cls.value
338+
ret["ifabsent"] = f"float({cls.value})"
339+
elif cls.dtype in string_types:
340+
ret["equals_string"] = cls.value
341+
ret["ifabsent"] = f"string({cls.value})"
342+
else:
343+
ret["equals_string"] = cls.value
344+
ret["ifabsent"] = cls.value
345+
346+
elif cls.default_value:
347+
if cls.dtype in string_types:
348+
ret["ifabsent"] = f"string({cls.default_value})"
349+
elif cls.dtype in integer_types:
350+
ret["ifabsent"] = f"int({cls.default_value})"
351+
elif cls.dtype in float_types:
352+
ret["ifabsent"] = f"float({cls.default_value})"
353+
else:
354+
ret["ifabsent"] = cls.default_value
355+
356+
return ret
357+
358+
359+
def is_container(group: Group) -> bool:
360+
"""
361+
Check if a group is a container group.
362+
363+
i.e. a group that...
364+
* has no name
365+
* multivalued quantity
366+
* has a ``neurodata_type_inc``
367+
* has no ``neurodata_type_def``
368+
* has no sub-groups
369+
* has no datasets
370+
* has no attributes
371+
372+
Examples:
373+
374+
.. code-block:: yaml
375+
376+
- name: templates
377+
groups:
378+
- neurodata_type_inc: TimeSeries
379+
doc: TimeSeries objects containing template data of presented stimuli.
380+
quantity: '*'
381+
- neurodata_type_inc: Images
382+
doc: Images objects containing images of presented stimuli.
383+
quantity: '*'
384+
"""
385+
return (
386+
not group.name
387+
and group.quantity == "*"
388+
and group.neurodata_type_inc
389+
and not group.neurodata_type_def
390+
and not group.datasets
391+
and not group.groups
392+
and not group.attributes
304393
)

nwb_linkml/src/nwb_linkml/adapters/attribute.py

Lines changed: 3 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,13 @@
77

88
from linkml_runtime.linkml_model.meta import SlotDefinition
99

10-
from nwb_linkml.adapters.adapter import Adapter, BuildResult, is_1d
10+
from nwb_linkml.adapters.adapter import Adapter, BuildResult, defaults, is_1d
1111
from nwb_linkml.adapters.array import ArrayAdapter
1212
from nwb_linkml.maps import Map
1313
from nwb_linkml.maps.dtype import handle_dtype, inlined
1414
from nwb_schema_language import Attribute
1515

1616

17-
def _make_ifabsent(val: str | int | float | None) -> str | None:
18-
if val is None:
19-
return None
20-
elif isinstance(val, str):
21-
return f"string({val})"
22-
elif isinstance(val, int):
23-
return f"integer({val})"
24-
elif isinstance(val, float):
25-
return f"float({val})"
26-
else:
27-
return str(val)
28-
29-
3017
class AttrDefaults(TypedDict):
3118
"""Default fields for an attribute"""
3219

@@ -38,31 +25,6 @@ class AttrDefaults(TypedDict):
3825
class AttributeMap(Map):
3926
"""Base class for attribute mapping transformations :)"""
4027

41-
@classmethod
42-
def handle_defaults(cls, attr: Attribute) -> AttrDefaults:
43-
"""
44-
Construct arguments for linkml slot default metaslots from nwb schema lang attribute props
45-
"""
46-
equals_string = None
47-
equals_number = None
48-
default_value = None
49-
if attr.value:
50-
if isinstance(attr.value, (int, float)):
51-
equals_number = attr.value
52-
elif attr.value:
53-
equals_string = str(attr.value)
54-
55-
if equals_number:
56-
default_value = _make_ifabsent(equals_number)
57-
elif equals_string:
58-
default_value = _make_ifabsent(equals_string)
59-
elif attr.default_value:
60-
default_value = _make_ifabsent(attr.default_value)
61-
62-
return AttrDefaults(
63-
equals_string=equals_string, equals_number=equals_number, ifabsent=default_value
64-
)
65-
6628
@classmethod
6729
@abstractmethod
6830
def check(cls, attr: Attribute) -> bool:
@@ -105,7 +67,7 @@ def apply(cls, attr: Attribute, res: Optional[BuildResult] = None) -> BuildResul
10567
description=attr.doc,
10668
required=attr.required,
10769
inlined=inlined(attr.dtype),
108-
**cls.handle_defaults(attr),
70+
**defaults(attr),
10971
)
11072
return BuildResult(slots=[slot])
11173

@@ -154,7 +116,7 @@ def apply(cls, attr: Attribute, res: Optional[BuildResult] = None) -> BuildResul
154116
required=attr.required,
155117
inlined=inlined(attr.dtype),
156118
**expressions,
157-
**cls.handle_defaults(attr),
119+
**defaults(attr),
158120
)
159121
return BuildResult(slots=[slot])
160122

0 commit comments

Comments
 (0)