diff --git a/docs/meta/todo.md b/docs/meta/todo.md index 8e1f62b..bf8b8e4 100644 --- a/docs/meta/todo.md +++ b/docs/meta/todo.md @@ -11,6 +11,8 @@ NWB schema translation Cleanup - [ ] Update pydantic generator - [ ] Restore regressions from stripping the generator +- [x] Make any_of with array ranges work +- [ ] PR upstream `equals_string` and `ifabsent` (if existing PR doesnt fix) - [ ] Use the class rather than a string in _get_class_slot_range_origin: ``` or inlined_as_list @@ -43,6 +45,9 @@ Important things that are not implemented yet! Remove monkeypatches/overrides once PRs are closed - [ ] https://github.com/linkml/linkml-runtime/pull/330 +Tests +- [ ] Ensure schemas and pydantic modules in repos are up to date + ## Docs TODOs ```{todolist} diff --git a/nwb_linkml/src/nwb_linkml/adapters/classes.py b/nwb_linkml/src/nwb_linkml/adapters/classes.py index 3cd79b5..1f36643 100644 --- a/nwb_linkml/src/nwb_linkml/adapters/classes.py +++ b/nwb_linkml/src/nwb_linkml/adapters/classes.py @@ -246,7 +246,6 @@ class ClassAdapter(Adapter): ifabsent=f"string({name})", equals_string=equals_string, range="string", - identifier=True, ) else: name_slot = SlotDefinition(name="name", required=True, range="string", identifier=True) diff --git a/nwb_linkml/src/nwb_linkml/generators/pydantic.py b/nwb_linkml/src/nwb_linkml/generators/pydantic.py index a0decad..6525d75 100644 --- a/nwb_linkml/src/nwb_linkml/generators/pydantic.py +++ b/nwb_linkml/src/nwb_linkml/generators/pydantic.py @@ -30,6 +30,7 @@ The `serialize` method: import inspect import pdb +import re import sys import warnings from copy import copy @@ -40,7 +41,8 @@ from typing import ClassVar, Dict, List, Optional, Tuple, Type, Union from linkml.generators import PydanticGenerator from linkml.generators.pydanticgen.build import SlotResult -from linkml.generators.pydanticgen.array import ArrayRepresentation +from linkml.generators.pydanticgen.array import ArrayRepresentation, NumpydanticArray +from linkml.generators.pydanticgen.template import PydanticModule from linkml_runtime.linkml_model.meta import ( Annotation, AnonymousSlotExpression, @@ -61,6 +63,8 @@ from pydantic import BaseModel from nwb_linkml.maps import flat_to_nptyping from nwb_linkml.maps.naming import module_case, version_module_case +OPTIONAL_PATTERN = re.compile(r'Optional\[([\w\.]*)\]') + @dataclass class NWBPydanticGenerator(PydanticGenerator): @@ -86,7 +90,7 @@ class NWBPydanticGenerator(PydanticGenerator): gen_slots: bool = True - skip_meta: ClassVar[Tuple[str]] = ('domain_of',) + skip_meta: ClassVar[Tuple[str]] = ('domain_of','alias') def _check_anyof( self, s: SlotDefinition, sn: SlotDefinitionName, sv: SchemaView @@ -111,18 +115,42 @@ class NWBPydanticGenerator(PydanticGenerator): if not base_range_subsumes_any_of: raise ValueError("Slot cannot have both range and any_of defined") - def before_generate_schema(self): - pass - def after_generate_slot(self, slot: SlotResult, sv: SchemaView) -> SlotResult: """ - strip unwanted metadata + - generate range with any_of """ for key in self.skip_meta: if key in slot.attribute.meta: del slot.attribute.meta[key] + + # make array ranges in any_of + if 'any_of' in slot.attribute.meta: + any_ofs = slot.attribute.meta['any_of'] + if all(['array' in expr for expr in any_ofs]): + ranges = [] + is_optional = False + for expr in any_ofs: + # remove optional from inner type + pyrange = slot.attribute.range + is_optional = OPTIONAL_PATTERN.match(pyrange) + if is_optional: + pyrange = is_optional.groups()[0] + range_generator = NumpydanticArray(ArrayExpression(**expr['array']), pyrange) + ranges.append(range_generator.make().range) + + slot.attribute.range = 'Union[' + ', '.join(ranges) + ']' + if is_optional: + slot.attribute.range = 'Optional[' + slot.attribute.range + ']' + del slot.attribute.meta['any_of'] + return slot + def before_render_template(self, template: PydanticModule, sv: SchemaView) -> PydanticModule: + if 'source_file' in template.meta: + del template.meta['source_file'] + + def compile_module( self, module_path: Path = None, module_name: str = "test", **kwargs ) -> ModuleType: # pragma: no cover - replaced with provider diff --git a/nwb_linkml/src/nwb_linkml/providers/pydantic.py b/nwb_linkml/src/nwb_linkml/providers/pydantic.py index 48e8322..a38a4c6 100644 --- a/nwb_linkml/src/nwb_linkml/providers/pydantic.py +++ b/nwb_linkml/src/nwb_linkml/providers/pydantic.py @@ -12,7 +12,7 @@ from types import ModuleType from typing import List, Optional, Type from linkml_runtime.linkml_model.meta import SchemaDefinition -from linkml.generators.pydanticgen.pydanticgen import SplitMode, _import_to_path +from linkml.generators.pydanticgen.pydanticgen import SplitMode, _import_to_path, _ensure_inits from pydantic import BaseModel from nwb_linkml.generators.pydantic import NWBPydanticGenerator @@ -148,7 +148,9 @@ class PydanticProvider(Provider): force: bool, **kwargs ) -> List[str]: + # FIXME: This is messy as all fuck, we're just getting it to work again so we can start iterating on the models themselves res = [] + module_paths = [] # first make the namespace file we were given @@ -170,6 +172,7 @@ class PydanticProvider(Provider): if dump: with open(ns_file, 'w') as ofile: ofile.write(serialized) + module_paths.append(ns_file) else: with open(ns_file, 'r') as ofile: serialized = ofile.read() @@ -203,6 +206,7 @@ class PydanticProvider(Provider): if dump: with open(import_file, 'w') as ofile: ofile.write(serialized) + module_paths.append(import_file) else: with open(import_file, 'r') as ofile: @@ -210,6 +214,10 @@ class PydanticProvider(Provider): res.append(serialized) + # make __init__.py files if we generated any files + if len(module_paths) > 0: + _ensure_inits(module_paths) + return res def _make_inits(self, out_file: Path) -> None: