linkml 1.8.3__py3-none-any.whl → 1.8.5__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/common/ifabsent_processor.py +98 -21
- linkml/generators/common/naming.py +106 -0
- linkml/generators/docgen/index.md.jinja2 +6 -6
- linkml/generators/docgen.py +80 -21
- linkml/generators/golanggen.py +3 -1
- linkml/generators/graphqlgen.py +34 -2
- 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 +119 -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/python/python_ifabsent_processor.py +1 -1
- linkml/generators/pythongen.py +135 -31
- 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.5.dist-info}/METADATA +3 -1
- {linkml-1.8.3.dist-info → linkml-1.8.5.dist-info}/RECORD +44 -42
- {linkml-1.8.3.dist-info → linkml-1.8.5.dist-info}/LICENSE +0 -0
- {linkml-1.8.3.dist-info → linkml-1.8.5.dist-info}/WHEEL +0 -0
- {linkml-1.8.3.dist-info → linkml-1.8.5.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,32 @@ 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 = {
|
679
|
+
"copy",
|
680
|
+
"datetime",
|
681
|
+
"decimal",
|
682
|
+
"enum",
|
683
|
+
"inspect",
|
684
|
+
"os",
|
685
|
+
"re",
|
686
|
+
"sys",
|
687
|
+
"typing",
|
688
|
+
"dataclasses",
|
689
|
+
}
|
690
|
+
"""
|
691
|
+
sys.stdlib_module_names is only present in 3.10 and later
|
692
|
+
so we make a cheap copy of the stdlib modules that we commonly use here,
|
693
|
+
but this should be removed whenever support for 3.9 is dropped.
|
694
|
+
"""
|
@@ -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
|
@@ -71,7 +71,7 @@ class PythonIfAbsentProcessor(IfAbsentProcessor):
|
|
71
71
|
def map_enum_default_value(
|
72
72
|
self, enum_name: EnumDefinitionName, permissible_value_name: str, slot: SlotDefinition, cls: ClassDefinition
|
73
73
|
):
|
74
|
-
return f"{
|
74
|
+
return f"'{permissible_value_name}'"
|
75
75
|
|
76
76
|
def map_nc_name_default_value(self, default_value: str, slot: SlotDefinition, cls: ClassDefinition):
|
77
77
|
raise NotImplementedError()
|
linkml/generators/pythongen.py
CHANGED
@@ -29,9 +29,12 @@ from rdflib import URIRef
|
|
29
29
|
|
30
30
|
import linkml
|
31
31
|
from linkml._version import __version__
|
32
|
+
from linkml.generators.pydanticgen.template import Import, Imports, ObjectImport
|
32
33
|
from linkml.generators.python.python_ifabsent_processor import PythonIfAbsentProcessor
|
33
34
|
from linkml.utils.generator import Generator, shared_arguments
|
34
35
|
|
36
|
+
logger = logging.getLogger(__name__)
|
37
|
+
|
35
38
|
|
36
39
|
@dataclass
|
37
40
|
class PythonGenerator(Generator):
|
@@ -57,10 +60,10 @@ class PythonGenerator(Generator):
|
|
57
60
|
dataclass_repr: bool = False
|
58
61
|
"""
|
59
62
|
Whether generated dataclasses should also generate a default __repr__ method.
|
60
|
-
|
63
|
+
|
61
64
|
Default ``False`` so that the parent :class:`linkml_runtime.utils.yamlutils.YAMLRoot` 's
|
62
65
|
``__repr__`` method is inherited for model pretty printing.
|
63
|
-
|
66
|
+
|
64
67
|
References:
|
65
68
|
- https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass
|
66
69
|
"""
|
@@ -75,7 +78,7 @@ class PythonGenerator(Generator):
|
|
75
78
|
if self.format is None:
|
76
79
|
self.format = self.valid_formats[0]
|
77
80
|
if self.schema.default_prefix == "linkml" and not self.genmeta:
|
78
|
-
|
81
|
+
logger.error("Generating metamodel without --genmeta is highly inadvisable!")
|
79
82
|
if not self.schema.source_file and isinstance(self.sourcefile, str) and "\n" not in self.sourcefile:
|
80
83
|
self.schema.source_file = os.path.basename(self.sourcefile)
|
81
84
|
|
@@ -88,8 +91,8 @@ class PythonGenerator(Generator):
|
|
88
91
|
try:
|
89
92
|
return compile_python(pycode)
|
90
93
|
except NameError as e:
|
91
|
-
|
92
|
-
|
94
|
+
logger.error(f"Code:\n{pycode}")
|
95
|
+
logger.error(f"Error compiling generated python code: {e}")
|
93
96
|
raise e
|
94
97
|
|
95
98
|
def visit_schema(self, **kwargs) -> None:
|
@@ -125,13 +128,120 @@ class PythonGenerator(Generator):
|
|
125
128
|
self.emit_prefixes.add(type_prefix)
|
126
129
|
|
127
130
|
def gen_schema(self) -> str:
|
131
|
+
all_imports = Imports()
|
132
|
+
# generic imports
|
133
|
+
all_imports = (
|
134
|
+
all_imports
|
135
|
+
+ Import(module="dataclasses")
|
136
|
+
+ Import(module="re")
|
137
|
+
+ Import(
|
138
|
+
module="jsonasobj2",
|
139
|
+
objects=[
|
140
|
+
ObjectImport(name="JsonObj"),
|
141
|
+
ObjectImport(name="as_dict"),
|
142
|
+
],
|
143
|
+
)
|
144
|
+
+ Import(
|
145
|
+
module="typing",
|
146
|
+
objects=[
|
147
|
+
ObjectImport(name="Optional"),
|
148
|
+
ObjectImport(name="List"),
|
149
|
+
ObjectImport(name="Union"),
|
150
|
+
ObjectImport(name="Dict"),
|
151
|
+
ObjectImport(name="ClassVar"),
|
152
|
+
ObjectImport(name="Any"),
|
153
|
+
],
|
154
|
+
)
|
155
|
+
+ Import(
|
156
|
+
module="dataclasses",
|
157
|
+
objects=[
|
158
|
+
ObjectImport(name="dataclass"),
|
159
|
+
],
|
160
|
+
)
|
161
|
+
+ Import(
|
162
|
+
module="datetime",
|
163
|
+
objects=[
|
164
|
+
ObjectImport(name="date"),
|
165
|
+
ObjectImport(name="datetime"),
|
166
|
+
ObjectImport(name="time"),
|
167
|
+
],
|
168
|
+
)
|
169
|
+
)
|
170
|
+
|
128
171
|
# The metamodel uses Enumerations to define itself, so don't import if we are generating the metamodel
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
172
|
+
if not self.genmeta:
|
173
|
+
all_imports = all_imports + Import(
|
174
|
+
module="linkml_runtime.linkml_model.meta",
|
175
|
+
objects=[
|
176
|
+
ObjectImport(name="EnumDefinition"),
|
177
|
+
ObjectImport(name="PermissibleValue"),
|
178
|
+
ObjectImport(name="PvFormulaOptions"),
|
179
|
+
],
|
180
|
+
)
|
181
|
+
# linkml imports
|
182
|
+
all_imports = (
|
183
|
+
all_imports
|
184
|
+
+ Import(
|
185
|
+
module="linkml_runtime.utils.slot",
|
186
|
+
objects=[
|
187
|
+
ObjectImport(name="Slot"),
|
188
|
+
],
|
189
|
+
)
|
190
|
+
+ Import(
|
191
|
+
module="linkml_runtime.utils.metamodelcore",
|
192
|
+
objects=[
|
193
|
+
ObjectImport(name="empty_list"),
|
194
|
+
ObjectImport(name="empty_dict"),
|
195
|
+
ObjectImport(name="bnode"),
|
196
|
+
],
|
197
|
+
)
|
198
|
+
+ Import(
|
199
|
+
module="linkml_runtime.utils.yamlutils",
|
200
|
+
objects=[
|
201
|
+
ObjectImport(name="YAMLRoot"),
|
202
|
+
ObjectImport(name="extended_str"),
|
203
|
+
ObjectImport(name="extended_float"),
|
204
|
+
ObjectImport(name="extended_int"),
|
205
|
+
],
|
206
|
+
)
|
207
|
+
+ Import(
|
208
|
+
module="linkml_runtime.utils.dataclass_extensions_376",
|
209
|
+
objects=[
|
210
|
+
ObjectImport(name="dataclasses_init_fn_with_kwargs"),
|
211
|
+
],
|
212
|
+
)
|
213
|
+
+ Import(
|
214
|
+
module="linkml_runtime.utils.formatutils",
|
215
|
+
objects=[
|
216
|
+
ObjectImport(name="camelcase"),
|
217
|
+
ObjectImport(name="underscore"),
|
218
|
+
ObjectImport(name="sfx"),
|
219
|
+
],
|
220
|
+
)
|
133
221
|
)
|
134
|
-
|
222
|
+
|
223
|
+
# handler import
|
224
|
+
all_imports = all_imports + Import(
|
225
|
+
module="linkml_runtime.utils.enumerations", objects=[ObjectImport(name="EnumDefinitionImpl")]
|
226
|
+
)
|
227
|
+
# other imports
|
228
|
+
all_imports = (
|
229
|
+
all_imports
|
230
|
+
+ Import(
|
231
|
+
module="rdflib",
|
232
|
+
objects=[
|
233
|
+
ObjectImport(name="Namespace"),
|
234
|
+
ObjectImport(name="URIRef"),
|
235
|
+
],
|
236
|
+
)
|
237
|
+
+ Import(
|
238
|
+
module="linkml_runtime.utils.curienamespace",
|
239
|
+
objects=[
|
240
|
+
ObjectImport(name="CurieNamespace"),
|
241
|
+
],
|
242
|
+
)
|
243
|
+
)
|
244
|
+
|
135
245
|
split_description = ""
|
136
246
|
if self.schema.description:
|
137
247
|
split_description = "\n# ".join(d for d in self.schema.description.split("\n") if d is not None)
|
@@ -149,21 +259,7 @@ class PythonGenerator(Generator):
|
|
149
259
|
# description: {split_description}
|
150
260
|
# license: {be(self.schema.license)}
|
151
261
|
|
152
|
-
|
153
|
-
import re
|
154
|
-
from jsonasobj2 import JsonObj, as_dict
|
155
|
-
from typing import Optional, List, Union, Dict, ClassVar, Any
|
156
|
-
from dataclasses import dataclass
|
157
|
-
from datetime import date, datetime, time
|
158
|
-
{enumimports}
|
159
|
-
from linkml_runtime.utils.slot import Slot
|
160
|
-
from linkml_runtime.utils.metamodelcore import empty_list, empty_dict, bnode
|
161
|
-
from linkml_runtime.utils.yamlutils import YAMLRoot, extended_str, extended_float, extended_int
|
162
|
-
from linkml_runtime.utils.dataclass_extensions_376 import dataclasses_init_fn_with_kwargs
|
163
|
-
from linkml_runtime.utils.formatutils import camelcase, underscore, sfx
|
164
|
-
{handlerimport}
|
165
|
-
from rdflib import Namespace, URIRef
|
166
|
-
from linkml_runtime.utils.curienamespace import CurieNamespace
|
262
|
+
{all_imports.render()}
|
167
263
|
{self.gen_imports()}
|
168
264
|
|
169
265
|
metamodel_version = "{self.schema.metamodel_version}"
|
@@ -834,7 +930,15 @@ dataclasses._init_fn = dataclasses_init_fn_with_kwargs
|
|
834
930
|
):
|
835
931
|
rlines.append(f"\tself.{aliased_slot_name} = {base_type_name}()")
|
836
932
|
else:
|
837
|
-
if
|
933
|
+
if slot.range in self.schema.enums and slot.ifabsent:
|
934
|
+
# `ifabsent` for an enumeration cannot be assigned to
|
935
|
+
# the dataclass field default, because it would be a
|
936
|
+
# mutable. `python_ifabsent_processor.py` can specify
|
937
|
+
# the default as string and here that string gets
|
938
|
+
# converted into an object attribute invocation
|
939
|
+
# TODO: fix according https://github.com/linkml/linkml/pull/2329#discussion_r1797534588
|
940
|
+
rlines.append(f"\tself.{aliased_slot_name} = getattr({slot.range}, self.{aliased_slot_name})")
|
941
|
+
elif (
|
838
942
|
(self.class_identifier(slot.range) and not slot.inlined)
|
839
943
|
or slot.range in self.schema.types
|
840
944
|
or slot.range in self.schema.enums
|
@@ -944,11 +1048,11 @@ dataclasses._init_fn = dataclasses_init_fn_with_kwargs
|
|
944
1048
|
|
945
1049
|
def forward_reference(self, slot_range: str, owning_class: str) -> bool:
|
946
1050
|
"""Determine whether slot_range is a forward reference"""
|
947
|
-
#
|
1051
|
+
# logger.info(f"CHECKING: {slot_range} {owning_class}")
|
948
1052
|
if (slot_range in self.schema.classes and self.schema.classes[slot_range].imported_from) or (
|
949
1053
|
slot_range in self.schema.enums and self.schema.enums[slot_range].imported_from
|
950
1054
|
):
|
951
|
-
|
1055
|
+
logger.info(
|
952
1056
|
f"FALSE: FORWARD: {slot_range} {owning_class} // IMP={self.schema.classes[slot_range].imported_from}"
|
953
1057
|
)
|
954
1058
|
return False
|
@@ -957,10 +1061,10 @@ dataclasses._init_fn = dataclasses_init_fn_with_kwargs
|
|
957
1061
|
clist = [x.name for x in self._sort_classes(self.schema.classes.values())]
|
958
1062
|
for cname in clist:
|
959
1063
|
if cname == owning_class:
|
960
|
-
|
1064
|
+
logger.info(f"TRUE: OCCURS SAME: {cname} == {slot_range} owning: {owning_class}")
|
961
1065
|
return True # Occurs on or after
|
962
1066
|
elif cname == slot_range:
|
963
|
-
|
1067
|
+
logger.info(f"FALSE: OCCURS BEFORE: {cname} == {slot_range} owning: {owning_class}")
|
964
1068
|
return False # Occurs before
|
965
1069
|
return True
|
966
1070
|
|
@@ -1214,7 +1318,7 @@ def cli(
|
|
1214
1318
|
)
|
1215
1319
|
if validate:
|
1216
1320
|
mod = gen.compile_module()
|
1217
|
-
|
1321
|
+
logger.info(f"Module {mod} compiled successfully")
|
1218
1322
|
print(gen.serialize(emit_metadata=head, **args))
|
1219
1323
|
|
1220
1324
|
|