linkml 1.8.3__py3-none-any.whl → 1.8.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- linkml/generators/__init__.py +2 -0
- linkml/generators/docgen/index.md.jinja2 +6 -6
- linkml/generators/docgen.py +64 -14
- linkml/generators/golanggen.py +3 -1
- linkml/generators/jsonschemagen.py +4 -2
- linkml/generators/owlgen.py +36 -17
- linkml/generators/projectgen.py +13 -11
- linkml/generators/pydanticgen/array.py +340 -56
- linkml/generators/pydanticgen/build.py +4 -2
- linkml/generators/pydanticgen/pydanticgen.py +35 -16
- linkml/generators/pydanticgen/template.py +108 -3
- linkml/generators/pydanticgen/templates/imports.py.jinja +11 -3
- linkml/generators/pydanticgen/templates/module.py.jinja +1 -3
- linkml/generators/pydanticgen/templates/validator.py.jinja +2 -2
- linkml/generators/pythongen.py +12 -10
- linkml/generators/shaclgen.py +34 -10
- linkml/generators/sparqlgen.py +3 -1
- linkml/generators/sqlalchemygen.py +5 -3
- linkml/generators/sqltablegen.py +4 -2
- linkml/generators/typescriptgen.py +13 -6
- linkml/linter/linter.py +2 -1
- linkml/transformers/logical_model_transformer.py +3 -3
- linkml/transformers/relmodel_transformer.py +18 -4
- linkml/utils/converter.py +3 -1
- linkml/utils/exceptions.py +11 -0
- linkml/utils/execute_tutorial.py +22 -20
- linkml/utils/generator.py +6 -4
- linkml/utils/mergeutils.py +4 -2
- linkml/utils/schema_fixer.py +5 -5
- linkml/utils/schemaloader.py +5 -3
- linkml/utils/sqlutils.py +3 -1
- linkml/validator/plugins/pydantic_validation_plugin.py +1 -1
- linkml/validators/jsonschemavalidator.py +3 -1
- linkml/validators/sparqlvalidator.py +5 -3
- linkml/workspaces/example_runner.py +3 -1
- {linkml-1.8.3.dist-info → linkml-1.8.4.dist-info}/METADATA +3 -1
- {linkml-1.8.3.dist-info → linkml-1.8.4.dist-info}/RECORD +40 -39
- {linkml-1.8.3.dist-info → linkml-1.8.4.dist-info}/LICENSE +0 -0
- {linkml-1.8.3.dist-info → linkml-1.8.4.dist-info}/WHEEL +0 -0
- {linkml-1.8.3.dist-info → linkml-1.8.4.dist-info}/entry_points.txt +0 -0
@@ -45,6 +45,9 @@ from linkml.generators.python.python_ifabsent_processor import PythonIfAbsentPro
|
|
45
45
|
from linkml.utils import deprecation_warning
|
46
46
|
from linkml.utils.generator import shared_arguments
|
47
47
|
|
48
|
+
logger = logging.getLogger(__name__)
|
49
|
+
|
50
|
+
|
48
51
|
if int(PYDANTIC_VERSION[0]) == 1:
|
49
52
|
deprecation_warning("pydantic-v1")
|
50
53
|
|
@@ -189,7 +192,7 @@ class PydanticGenerator(OOCodeGenerator, LifecycleMixin):
|
|
189
192
|
template_dir: Optional[Union[str, Path]] = None
|
190
193
|
"""
|
191
194
|
Override templates for each PydanticTemplateModel.
|
192
|
-
|
195
|
+
|
193
196
|
Directory with templates that override the default :attr:`.PydanticTemplateModel.template`
|
194
197
|
for each class. If a matching template is not found in the override directory,
|
195
198
|
the default templates will be used.
|
@@ -218,7 +221,7 @@ class PydanticGenerator(OOCodeGenerator, LifecycleMixin):
|
|
218
221
|
)
|
219
222
|
|
220
223
|
"""
|
221
|
-
imports: Optional[List[Import]] = None
|
224
|
+
imports: Optional[Union[List[Import], Imports]] = None
|
222
225
|
"""
|
223
226
|
Additional imports to inject into generated module.
|
224
227
|
|
@@ -266,6 +269,13 @@ class PydanticGenerator(OOCodeGenerator, LifecycleMixin):
|
|
266
269
|
else:
|
267
270
|
from typing_extensions import Literal
|
268
271
|
|
272
|
+
"""
|
273
|
+
sort_imports: bool = True
|
274
|
+
"""
|
275
|
+
Before returning from :meth:`.PydanticGenerator.render`, sort imports with :meth:`.Imports.sort`
|
276
|
+
|
277
|
+
Default ``True``, but optional in case import order must be explicitly given,
|
278
|
+
eg. to avoid circular import errors in complex generator subclasses.
|
269
279
|
"""
|
270
280
|
metadata_mode: Union[MetadataMode, str, None] = MetadataMode.AUTO
|
271
281
|
"""
|
@@ -358,8 +368,8 @@ class PydanticGenerator(OOCodeGenerator, LifecycleMixin):
|
|
358
368
|
try:
|
359
369
|
return compile_python(pycode)
|
360
370
|
except NameError as e:
|
361
|
-
|
362
|
-
|
371
|
+
logger.error(f"Code:\n{pycode}")
|
372
|
+
logger.error(f"Error compiling generated python code: {e}")
|
363
373
|
raise e
|
364
374
|
|
365
375
|
def _get_classes(self, sv: SchemaView) -> Tuple[List[ClassDefinition], Optional[List[ClassDefinition]]]:
|
@@ -448,11 +458,12 @@ class PydanticGenerator(OOCodeGenerator, LifecycleMixin):
|
|
448
458
|
|
449
459
|
def generate_slot(self, slot: SlotDefinition, cls: ClassDefinition) -> SlotResult:
|
450
460
|
slot_args = {
|
451
|
-
k: slot
|
461
|
+
k: getattr(slot, k, None)
|
452
462
|
for k in PydanticAttribute.model_fields.keys()
|
453
|
-
if slot
|
463
|
+
if getattr(slot, k, None) is not None
|
454
464
|
}
|
455
|
-
|
465
|
+
slot_alias = slot.alias if slot.alias else slot.name
|
466
|
+
slot_args["name"] = underscore(slot_alias)
|
456
467
|
slot_args["description"] = slot.description.replace('"', '\\"') if slot.description is not None else None
|
457
468
|
predef = self.predefined_slot_values.get(camelcase(cls.name), {}).get(slot.name, None)
|
458
469
|
if predef is not None:
|
@@ -665,7 +676,7 @@ class PydanticGenerator(OOCodeGenerator, LifecycleMixin):
|
|
665
676
|
else:
|
666
677
|
# TODO: default ranges in schemagen
|
667
678
|
# pyrange = 'str'
|
668
|
-
#
|
679
|
+
# logger.error(f'range: {s.range} is unknown')
|
669
680
|
raise Exception(f"range: {slot_range}")
|
670
681
|
return pyrange
|
671
682
|
|
@@ -921,14 +932,20 @@ class PydanticGenerator(OOCodeGenerator, LifecycleMixin):
|
|
921
932
|
return Import(module=module, objects=[ObjectImport(name=camelcase(class_name))], is_schema=True)
|
922
933
|
|
923
934
|
def render(self) -> PydanticModule:
|
935
|
+
"""
|
936
|
+
Render the schema to a :class:`PydanticModule` model
|
937
|
+
"""
|
924
938
|
sv: SchemaView
|
925
939
|
sv = self.schemaview
|
926
940
|
|
927
941
|
# imports
|
928
942
|
imports = DEFAULT_IMPORTS
|
929
943
|
if self.imports is not None:
|
930
|
-
|
931
|
-
imports +=
|
944
|
+
if isinstance(self.imports, Imports):
|
945
|
+
imports += self.imports
|
946
|
+
else:
|
947
|
+
for i in self.imports:
|
948
|
+
imports += i
|
932
949
|
if self.split_mode == SplitMode.FULL:
|
933
950
|
imports += self._get_imports()
|
934
951
|
|
@@ -965,13 +982,14 @@ class PydanticGenerator(OOCodeGenerator, LifecycleMixin):
|
|
965
982
|
class_results = self.after_generate_classes(class_results, sv)
|
966
983
|
|
967
984
|
classes = {r.cls.name: r.cls for r in class_results}
|
968
|
-
|
969
985
|
injected_classes = self._clean_injected_classes(injected_classes)
|
970
986
|
|
987
|
+
imports.render_sorted = self.sort_imports
|
988
|
+
|
971
989
|
module = PydanticModule(
|
972
990
|
metamodel_version=self.schema.metamodel_version,
|
973
991
|
version=self.schema.version,
|
974
|
-
python_imports=imports
|
992
|
+
python_imports=imports,
|
975
993
|
base_model=base_model,
|
976
994
|
injected_classes=injected_classes,
|
977
995
|
enums=enums,
|
@@ -987,7 +1005,8 @@ class PydanticGenerator(OOCodeGenerator, LifecycleMixin):
|
|
987
1005
|
|
988
1006
|
Args:
|
989
1007
|
rendered_module ( :class:`.PydanticModule` ): Optional, if schema was previously
|
990
|
-
rendered with :meth
|
1008
|
+
rendered with :meth:`~.PydanticGenerator.render` , use that,
|
1009
|
+
otherwise :meth:`~.PydanticGenerator.render` fresh.
|
991
1010
|
"""
|
992
1011
|
if rendered_module is not None:
|
993
1012
|
module = rendered_module
|
@@ -1147,9 +1166,9 @@ def _ensure_inits(paths: List[Path]):
|
|
1147
1166
|
help="""
|
1148
1167
|
Optional jinja2 template directory to use for class generation.
|
1149
1168
|
|
1150
|
-
Pass a directory containing templates with the same name as any of the default
|
1151
|
-
:class:`.PydanticTemplateModel` templates to override them. The given directory will be
|
1152
|
-
searched for matching templates, and use the default templates as a fallback
|
1169
|
+
Pass a directory containing templates with the same name as any of the default
|
1170
|
+
:class:`.PydanticTemplateModel` templates to override them. The given directory will be
|
1171
|
+
searched for matching templates, and use the default templates as a fallback
|
1153
1172
|
if an override is not found
|
1154
1173
|
|
1155
1174
|
Available templates to override:
|
@@ -1,5 +1,6 @@
|
|
1
|
+
import sys
|
1
2
|
from importlib.util import find_spec
|
2
|
-
from typing import Any, ClassVar, Dict, Generator, List, Literal, Optional, Union
|
3
|
+
from typing import Any, ClassVar, Dict, Generator, List, Literal, Optional, Tuple, Union, get_args
|
3
4
|
|
4
5
|
from jinja2 import Environment, PackageLoader
|
5
6
|
from pydantic import BaseModel, Field, field_validator
|
@@ -28,6 +29,14 @@ else:
|
|
28
29
|
return f
|
29
30
|
|
30
31
|
|
32
|
+
IMPORT_GROUPS = Literal["future", "stdlib", "thirdparty", "local", "conditional"]
|
33
|
+
"""
|
34
|
+
See :attr:`.Import.group` and :attr:`.Imports.sort`
|
35
|
+
|
36
|
+
Order of this literal is used in sort and therefore not arbitrary.
|
37
|
+
"""
|
38
|
+
|
39
|
+
|
31
40
|
class PydanticTemplateModel(TemplateModel):
|
32
41
|
"""
|
33
42
|
Metaclass to render pydantic models with jinja templates.
|
@@ -292,6 +301,28 @@ class Import(PydanticTemplateModel):
|
|
292
301
|
Used primarily in split schema generation, see :func:`.pydanticgen.generate_split` for example usage.
|
293
302
|
"""
|
294
303
|
|
304
|
+
@computed_field
|
305
|
+
def group(self) -> IMPORT_GROUPS:
|
306
|
+
"""
|
307
|
+
Import group used when sorting
|
308
|
+
|
309
|
+
* ``future`` - from `__future__` import...
|
310
|
+
* ``stdlib`` - ... the standard library
|
311
|
+
* ``thirdparty`` - other dependencies not in the standard library
|
312
|
+
* ``local`` - relative imports (eg. from split generation)
|
313
|
+
* ``conditional`` - a :class:`.ConditionalImport`
|
314
|
+
"""
|
315
|
+
if self.module == "__future__":
|
316
|
+
return "future"
|
317
|
+
elif sys.version_info.minor >= 10 and self.module in sys.stdlib_module_names:
|
318
|
+
return "stdlib"
|
319
|
+
elif sys.version_info.minor < 10 and self.module in _some_stdlib_module_names:
|
320
|
+
return "stdlib"
|
321
|
+
elif self.module.startswith("."):
|
322
|
+
return "local"
|
323
|
+
else:
|
324
|
+
return "thirdparty"
|
325
|
+
|
295
326
|
def merge(self, other: "Import") -> List["Import"]:
|
296
327
|
"""
|
297
328
|
Merge one import with another, see :meth:`.Imports` for an example.
|
@@ -346,6 +377,16 @@ class Import(PydanticTemplateModel):
|
|
346
377
|
# one is a module, the other imports objects, keep both
|
347
378
|
return [self, other]
|
348
379
|
|
380
|
+
def sort(self) -> None:
|
381
|
+
"""
|
382
|
+
Sort imported objects
|
383
|
+
|
384
|
+
* First by whether the first letter is capitalized or not,
|
385
|
+
* Then alphabetically (by object name rather than alias)
|
386
|
+
"""
|
387
|
+
if self.objects:
|
388
|
+
self.objects = sorted(self.objects, key=lambda obj: (obj.name[0].islower(), obj.name))
|
389
|
+
|
349
390
|
|
350
391
|
class ConditionalImport(Import):
|
351
392
|
"""
|
@@ -391,6 +432,17 @@ class ConditionalImport(Import):
|
|
391
432
|
condition: str
|
392
433
|
alternative: Import
|
393
434
|
|
435
|
+
@computed_field
|
436
|
+
def group(self) -> Literal["conditional"]:
|
437
|
+
return "conditional"
|
438
|
+
|
439
|
+
def sort(self) -> None:
|
440
|
+
"""
|
441
|
+
:meth:`.Import.sort` called for self and :attr:`.alternative`
|
442
|
+
"""
|
443
|
+
super(ConditionalImport, self).sort()
|
444
|
+
self.alternative.sort()
|
445
|
+
|
394
446
|
|
395
447
|
class Imports(PydanticTemplateModel):
|
396
448
|
"""
|
@@ -427,6 +479,10 @@ class Imports(PydanticTemplateModel):
|
|
427
479
|
template: ClassVar[str] = "imports.py.jinja"
|
428
480
|
|
429
481
|
imports: List[Union[Import, ConditionalImport]] = Field(default_factory=list)
|
482
|
+
group_order: Tuple[str, ...] = get_args(IMPORT_GROUPS)
|
483
|
+
"""Order in which to sort imports by their :attr:`.Import.group`"""
|
484
|
+
render_sorted: bool = True
|
485
|
+
"""When rendering, render in sorted groups"""
|
430
486
|
|
431
487
|
@classmethod
|
432
488
|
def _merge(
|
@@ -484,13 +540,17 @@ class Imports(PydanticTemplateModel):
|
|
484
540
|
break
|
485
541
|
|
486
542
|
# SPECIAL CASE - __future__ annotations must happen at the top of a file
|
543
|
+
# sort here outside of sort method because our imports are invalid without it,
|
544
|
+
# where calling ``sort`` should be optional.
|
487
545
|
imports = sorted(imports, key=lambda i: i.module == "__future__", reverse=True)
|
488
546
|
return imports
|
489
547
|
|
490
548
|
def __add__(self, other: Union[Import, "Imports", List[Import]]) -> "Imports":
|
491
549
|
imports = self.imports.copy()
|
492
550
|
imports = self._merge(imports, other)
|
493
|
-
return Imports.model_construct(
|
551
|
+
return Imports.model_construct(
|
552
|
+
imports=imports, **{k: getattr(self, k, None) for k in self.model_fields if k != "imports"}
|
553
|
+
)
|
494
554
|
|
495
555
|
def __len__(self) -> int:
|
496
556
|
return len(self.imports)
|
@@ -552,6 +612,36 @@ class Imports(PydanticTemplateModel):
|
|
552
612
|
merged_imports = cls._merge(merged_imports, i)
|
553
613
|
return merged_imports
|
554
614
|
|
615
|
+
@computed_field
|
616
|
+
def import_groups(self) -> List[IMPORT_GROUPS]:
|
617
|
+
"""
|
618
|
+
List of what group each import belongs to
|
619
|
+
"""
|
620
|
+
return [i.group for i in self.imports]
|
621
|
+
|
622
|
+
def sort(self) -> None:
|
623
|
+
"""
|
624
|
+
Sort imports recursively, mimicking isort:
|
625
|
+
|
626
|
+
* First by :attr:`.Import.group` according to :attr:`.Imports.group_order`
|
627
|
+
* Then by whether the :class:`.Import` has any objects
|
628
|
+
(``import module`` comes before ``from module import name``)
|
629
|
+
* Then alphabetically by module name
|
630
|
+
"""
|
631
|
+
|
632
|
+
def _sort_key(i: Import) -> Tuple[int, int, str]:
|
633
|
+
return (self.group_order.index(i.group), int(i.objects is not None), i.module)
|
634
|
+
|
635
|
+
imports = sorted(self.imports, key=_sort_key)
|
636
|
+
for i in imports:
|
637
|
+
i.sort()
|
638
|
+
self.imports = imports
|
639
|
+
|
640
|
+
def render(self, environment: Optional[Environment] = None, black: bool = False) -> str:
|
641
|
+
if self.render_sorted:
|
642
|
+
self.sort()
|
643
|
+
return super(Imports, self).render(environment=environment, black=black)
|
644
|
+
|
555
645
|
|
556
646
|
class PydanticModule(PydanticTemplateModel):
|
557
647
|
"""
|
@@ -565,7 +655,7 @@ class PydanticModule(PydanticTemplateModel):
|
|
565
655
|
version: Optional[str] = None
|
566
656
|
base_model: PydanticBaseModel = PydanticBaseModel()
|
567
657
|
injected_classes: Optional[List[str]] = None
|
568
|
-
python_imports: List[Union[Import, ConditionalImport]] =
|
658
|
+
python_imports: Union[Imports, List[Union[Import, ConditionalImport]]] = Imports()
|
569
659
|
enums: Dict[str, PydanticEnum] = Field(default_factory=dict)
|
570
660
|
classes: Dict[str, PydanticClass] = Field(default_factory=dict)
|
571
661
|
meta: Optional[Dict[str, Any]] = None
|
@@ -573,6 +663,21 @@ class PydanticModule(PydanticTemplateModel):
|
|
573
663
|
Metadata for the schema to be included in a linkml_meta module-level instance of LinkMLMeta
|
574
664
|
"""
|
575
665
|
|
666
|
+
@field_validator("python_imports", mode="after")
|
667
|
+
@classmethod
|
668
|
+
def cast_imports(cls, imports: Union[Imports, List[Union[Import, ConditionalImport]]]) -> Imports:
|
669
|
+
if isinstance(imports, list):
|
670
|
+
imports = Imports(imports=imports)
|
671
|
+
return imports
|
672
|
+
|
576
673
|
@computed_field
|
577
674
|
def class_names(self) -> List[str]:
|
578
675
|
return [c.name for c in self.classes.values()]
|
676
|
+
|
677
|
+
|
678
|
+
_some_stdlib_module_names = {"copy", "datetime", "decimal", "enum", "inspect", "os", "re", "sys", "typing"}
|
679
|
+
"""
|
680
|
+
sys.stdlib_module_names is only present in 3.10 and later
|
681
|
+
so we make a cheap copy of the stdlib modules that we commonly use here,
|
682
|
+
but this should be removed whenever support for 3.9 is dropped.
|
683
|
+
"""
|
@@ -20,12 +20,20 @@ from {{ module }} import (
|
|
20
20
|
{%- endif %}
|
21
21
|
{%- endif %}
|
22
22
|
{% endmacro %}
|
23
|
+
{#- For when used with Import model -#}
|
23
24
|
{%- if module %}
|
24
25
|
{{ import_(module, alias, objects) }}
|
25
26
|
{% endif -%}
|
26
27
|
{#- For when used with Imports container -#}
|
27
28
|
{%- if imports -%}
|
28
|
-
{%-
|
29
|
-
{
|
30
|
-
{
|
29
|
+
{%- if render_sorted -%}
|
30
|
+
{% for i in range(imports | length) %}
|
31
|
+
{{ imports[i] }}
|
32
|
+
{%- if not loop.last and import_groups[i] != import_groups[i+1] %}{{ '\n' }}{% endif -%}
|
33
|
+
{% endfor %}
|
34
|
+
{%- else -%}
|
35
|
+
{%- for import in imports -%}
|
36
|
+
{{ import }}
|
37
|
+
{%- endfor -%}
|
38
|
+
{%- endif -%}
|
31
39
|
{% endif -%}
|
@@ -3,9 +3,9 @@
|
|
3
3
|
pattern=re.compile(r"{{pattern}}")
|
4
4
|
if isinstance(v,list):
|
5
5
|
for element in v:
|
6
|
-
if not pattern.match(element):
|
6
|
+
if isinstance(v, str) and not pattern.match(element):
|
7
7
|
raise ValueError(f"Invalid {{name}} format: {element}")
|
8
8
|
elif isinstance(v,str):
|
9
9
|
if not pattern.match(v):
|
10
10
|
raise ValueError(f"Invalid {{name}} format: {v}")
|
11
|
-
return v
|
11
|
+
return v
|
linkml/generators/pythongen.py
CHANGED
@@ -32,6 +32,8 @@ from linkml._version import __version__
|
|
32
32
|
from linkml.generators.python.python_ifabsent_processor import PythonIfAbsentProcessor
|
33
33
|
from linkml.utils.generator import Generator, shared_arguments
|
34
34
|
|
35
|
+
logger = logging.getLogger(__name__)
|
36
|
+
|
35
37
|
|
36
38
|
@dataclass
|
37
39
|
class PythonGenerator(Generator):
|
@@ -57,10 +59,10 @@ class PythonGenerator(Generator):
|
|
57
59
|
dataclass_repr: bool = False
|
58
60
|
"""
|
59
61
|
Whether generated dataclasses should also generate a default __repr__ method.
|
60
|
-
|
62
|
+
|
61
63
|
Default ``False`` so that the parent :class:`linkml_runtime.utils.yamlutils.YAMLRoot` 's
|
62
64
|
``__repr__`` method is inherited for model pretty printing.
|
63
|
-
|
65
|
+
|
64
66
|
References:
|
65
67
|
- https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass
|
66
68
|
"""
|
@@ -75,7 +77,7 @@ class PythonGenerator(Generator):
|
|
75
77
|
if self.format is None:
|
76
78
|
self.format = self.valid_formats[0]
|
77
79
|
if self.schema.default_prefix == "linkml" and not self.genmeta:
|
78
|
-
|
80
|
+
logger.error("Generating metamodel without --genmeta is highly inadvisable!")
|
79
81
|
if not self.schema.source_file and isinstance(self.sourcefile, str) and "\n" not in self.sourcefile:
|
80
82
|
self.schema.source_file = os.path.basename(self.sourcefile)
|
81
83
|
|
@@ -88,8 +90,8 @@ class PythonGenerator(Generator):
|
|
88
90
|
try:
|
89
91
|
return compile_python(pycode)
|
90
92
|
except NameError as e:
|
91
|
-
|
92
|
-
|
93
|
+
logger.error(f"Code:\n{pycode}")
|
94
|
+
logger.error(f"Error compiling generated python code: {e}")
|
93
95
|
raise e
|
94
96
|
|
95
97
|
def visit_schema(self, **kwargs) -> None:
|
@@ -944,11 +946,11 @@ dataclasses._init_fn = dataclasses_init_fn_with_kwargs
|
|
944
946
|
|
945
947
|
def forward_reference(self, slot_range: str, owning_class: str) -> bool:
|
946
948
|
"""Determine whether slot_range is a forward reference"""
|
947
|
-
#
|
949
|
+
# logger.info(f"CHECKING: {slot_range} {owning_class}")
|
948
950
|
if (slot_range in self.schema.classes and self.schema.classes[slot_range].imported_from) or (
|
949
951
|
slot_range in self.schema.enums and self.schema.enums[slot_range].imported_from
|
950
952
|
):
|
951
|
-
|
953
|
+
logger.info(
|
952
954
|
f"FALSE: FORWARD: {slot_range} {owning_class} // IMP={self.schema.classes[slot_range].imported_from}"
|
953
955
|
)
|
954
956
|
return False
|
@@ -957,10 +959,10 @@ dataclasses._init_fn = dataclasses_init_fn_with_kwargs
|
|
957
959
|
clist = [x.name for x in self._sort_classes(self.schema.classes.values())]
|
958
960
|
for cname in clist:
|
959
961
|
if cname == owning_class:
|
960
|
-
|
962
|
+
logger.info(f"TRUE: OCCURS SAME: {cname} == {slot_range} owning: {owning_class}")
|
961
963
|
return True # Occurs on or after
|
962
964
|
elif cname == slot_range:
|
963
|
-
|
965
|
+
logger.info(f"FALSE: OCCURS BEFORE: {cname} == {slot_range} owning: {owning_class}")
|
964
966
|
return False # Occurs before
|
965
967
|
return True
|
966
968
|
|
@@ -1214,7 +1216,7 @@ def cli(
|
|
1214
1216
|
)
|
1215
1217
|
if validate:
|
1216
1218
|
mod = gen.compile_module()
|
1217
|
-
|
1219
|
+
logger.info(f"Module {mod} compiled successfully")
|
1218
1220
|
print(gen.serialize(emit_metadata=head, **args))
|
1219
1221
|
|
1220
1222
|
|
linkml/generators/shaclgen.py
CHANGED
@@ -5,19 +5,21 @@ from typing import Callable, List
|
|
5
5
|
|
6
6
|
import click
|
7
7
|
from jsonasobj2 import JsonObj, as_dict
|
8
|
-
from linkml_runtime.linkml_model.meta import ElementName
|
8
|
+
from linkml_runtime.linkml_model.meta import ClassDefinition, ElementName
|
9
9
|
from linkml_runtime.utils.formatutils import underscore
|
10
10
|
from linkml_runtime.utils.schemaview import SchemaView
|
11
11
|
from linkml_runtime.utils.yamlutils import TypedNode, extended_float, extended_int, extended_str
|
12
12
|
from rdflib import BNode, Graph, Literal, URIRef
|
13
13
|
from rdflib.collection import Collection
|
14
|
-
from rdflib.namespace import RDF,
|
14
|
+
from rdflib.namespace import RDF, SH, XSD
|
15
15
|
|
16
16
|
from linkml._version import __version__
|
17
17
|
from linkml.generators.shacl.shacl_data_type import ShaclDataType
|
18
18
|
from linkml.generators.shacl.shacl_ifabsent_processor import ShaclIfAbsentProcessor
|
19
19
|
from linkml.utils.generator import Generator, shared_arguments
|
20
20
|
|
21
|
+
logger = logging.getLogger(__name__)
|
22
|
+
|
21
23
|
|
22
24
|
@dataclass
|
23
25
|
class ShaclGenerator(Generator):
|
@@ -74,9 +76,6 @@ class ShaclGenerator(Generator):
|
|
74
76
|
shape_pv(RDF.type, SH.NodeShape)
|
75
77
|
shape_pv(SH.targetClass, class_uri) # TODO
|
76
78
|
|
77
|
-
if c.is_a:
|
78
|
-
shape_pv(RDFS.subClassOf, URIRef(sv.get_uri(c.is_a, expand=True)))
|
79
|
-
|
80
79
|
if self.closed:
|
81
80
|
if c.mixin or c.abstract:
|
82
81
|
shape_pv(SH.closed, Literal(False))
|
@@ -88,9 +87,9 @@ class ShaclGenerator(Generator):
|
|
88
87
|
shape_pv(SH.name, Literal(c.title))
|
89
88
|
if c.description is not None:
|
90
89
|
shape_pv(SH.description, Literal(c.description))
|
91
|
-
|
92
|
-
|
93
|
-
|
90
|
+
|
91
|
+
shape_pv(SH.ignoredProperties, self._build_ignored_properties(g, c))
|
92
|
+
|
94
93
|
if c.annotations and self.include_annotations:
|
95
94
|
self._add_annotations(shape_pv, c)
|
96
95
|
order = 0
|
@@ -130,7 +129,7 @@ class ShaclGenerator(Generator):
|
|
130
129
|
# slot definition, as both are mapped to sh:in in SHACL
|
131
130
|
if s.equals_string or s.equals_string_in:
|
132
131
|
error = "'equals_string'/'equals_string_in' and 'any_of' are mutually exclusive"
|
133
|
-
raise ValueError(f'{TypedNode.yaml_loc(s, suffix="")} {error}')
|
132
|
+
raise ValueError(f'{TypedNode.yaml_loc(str(s), suffix="")} {error}')
|
134
133
|
|
135
134
|
or_node = BNode()
|
136
135
|
prop_pv(SH["or"], or_node)
|
@@ -244,7 +243,7 @@ class ShaclGenerator(Generator):
|
|
244
243
|
if rt.annotations and self.include_annotations:
|
245
244
|
self._add_annotations(func, rt)
|
246
245
|
else:
|
247
|
-
|
246
|
+
logger.error(f"No URI for type {rt.name}")
|
248
247
|
|
249
248
|
def _and_equals_string(self, g: Graph, func: Callable, values: List) -> None:
|
250
249
|
pv_node = BNode()
|
@@ -301,6 +300,31 @@ class ShaclGenerator(Generator):
|
|
301
300
|
)
|
302
301
|
func(SH["in"], pv_node)
|
303
302
|
|
303
|
+
def _build_ignored_properties(self, g: Graph, c: ClassDefinition) -> BNode:
|
304
|
+
def collect_child_properties(class_name: str, output: set) -> None:
|
305
|
+
for childName in self.schemaview.class_children(class_name, imports=True, mixins=False, is_a=True):
|
306
|
+
output.update(
|
307
|
+
{
|
308
|
+
URIRef(self.schemaview.get_uri(prop, expand=True))
|
309
|
+
for prop in self.schemaview.class_slots(childName)
|
310
|
+
}
|
311
|
+
)
|
312
|
+
collect_child_properties(childName, output)
|
313
|
+
|
314
|
+
child_properties = set()
|
315
|
+
collect_child_properties(c.name, child_properties)
|
316
|
+
|
317
|
+
class_slot_uris = {
|
318
|
+
URIRef(self.schemaview.get_uri(prop, expand=True)) for prop in self.schemaview.class_slots(c.name)
|
319
|
+
}
|
320
|
+
ignored_properties = child_properties.difference(class_slot_uris)
|
321
|
+
|
322
|
+
list_node = BNode()
|
323
|
+
ignored_properties.add(RDF.type)
|
324
|
+
Collection(g, list_node, list(ignored_properties))
|
325
|
+
|
326
|
+
return list_node
|
327
|
+
|
304
328
|
|
305
329
|
def add_simple_data_type(func: Callable, r: ElementName) -> None:
|
306
330
|
for datatype in list(ShaclDataType):
|
linkml/generators/sparqlgen.py
CHANGED
@@ -14,6 +14,8 @@ from linkml_runtime.utils.schemaview import SchemaView
|
|
14
14
|
from linkml._version import __version__
|
15
15
|
from linkml.utils.generator import Generator, shared_arguments
|
16
16
|
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
17
19
|
template = """
|
18
20
|
{% for pfxn, pfx in schema.prefixes.items() -%}
|
19
21
|
PREFIX {{pfxn}}: <{{pfx.prefix_reference}}>
|
@@ -150,7 +152,7 @@ class SparqlGenerator(Generator):
|
|
150
152
|
extra = ""
|
151
153
|
if named_graphs is not None:
|
152
154
|
extra += f'FILTER( ?graph in ( {",".join(named_graphs)} ))'
|
153
|
-
|
155
|
+
logger.info(f"Named Graphs = {named_graphs} // extra={extra}")
|
154
156
|
if limit is not None and isinstance(limit, int):
|
155
157
|
limit = f"LIMIT {limit}"
|
156
158
|
else:
|
@@ -21,6 +21,8 @@ from linkml.generators.sqltablegen import SQLTableGenerator
|
|
21
21
|
from linkml.transformers.relmodel_transformer import ForeignKeyPolicy, RelationalModelTransformer
|
22
22
|
from linkml.utils.generator import Generator, shared_arguments
|
23
23
|
|
24
|
+
logger = logging.getLogger(__name__)
|
25
|
+
|
24
26
|
|
25
27
|
class TemplateEnum(Enum):
|
26
28
|
DECLARATIVE = "declarative"
|
@@ -84,7 +86,7 @@ class SQLAlchemyGenerator(Generator):
|
|
84
86
|
template_obj = Template(template_str)
|
85
87
|
if model_path is None:
|
86
88
|
model_path = self.schema.name
|
87
|
-
|
89
|
+
logger.info(f"Package for dataclasses == {model_path}")
|
88
90
|
backrefs = defaultdict(list)
|
89
91
|
for m in tr_result.mappings:
|
90
92
|
backrefs[m.source_class].append(m)
|
@@ -113,7 +115,7 @@ class SQLAlchemyGenerator(Generator):
|
|
113
115
|
is_join_table=lambda c: any(tag for tag in c.annotations.keys() if tag == "linkml:derived_from"),
|
114
116
|
classes=rel_schema_classes_ordered,
|
115
117
|
)
|
116
|
-
|
118
|
+
logger.debug(f"# Generated code:\n{code}")
|
117
119
|
return code
|
118
120
|
|
119
121
|
def serialize(self, **kwargs) -> str:
|
@@ -173,7 +175,7 @@ class SQLAlchemyGenerator(Generator):
|
|
173
175
|
def skip(cls: ClassDefinition) -> bool:
|
174
176
|
is_skip = len(cls.attributes) == 0
|
175
177
|
if is_skip:
|
176
|
-
|
178
|
+
logger.error(f"SKIPPING: {cls.name}")
|
177
179
|
return is_skip
|
178
180
|
|
179
181
|
# TODO: move this
|
linkml/generators/sqltablegen.py
CHANGED
@@ -16,6 +16,8 @@ from linkml.transformers.relmodel_transformer import ForeignKeyPolicy, Relationa
|
|
16
16
|
from linkml.utils.generator import Generator, shared_arguments
|
17
17
|
from linkml.utils.schemaloader import SchemaLoader
|
18
18
|
|
19
|
+
logger = logging.getLogger(__name__)
|
20
|
+
|
19
21
|
|
20
22
|
class SqlNamingPolicy(Enum):
|
21
23
|
preserve = "preserve"
|
@@ -262,12 +264,12 @@ class SQLTableGenerator(Generator):
|
|
262
264
|
elif range is None:
|
263
265
|
return Text()
|
264
266
|
else:
|
265
|
-
|
267
|
+
logger.error(f"Unknown range: {range} for {slot.name} = {slot.range}")
|
266
268
|
return Text()
|
267
269
|
if range_base in RANGEMAP:
|
268
270
|
return RANGEMAP[range_base]
|
269
271
|
else:
|
270
|
-
|
272
|
+
logger.error(f"UNKNOWN range base: {range_base} for {slot.name} = {slot.range}")
|
271
273
|
return Text()
|
272
274
|
|
273
275
|
@staticmethod
|