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.
Files changed (44) hide show
  1. linkml/generators/__init__.py +2 -0
  2. linkml/generators/common/ifabsent_processor.py +98 -21
  3. linkml/generators/common/naming.py +106 -0
  4. linkml/generators/docgen/index.md.jinja2 +6 -6
  5. linkml/generators/docgen.py +80 -21
  6. linkml/generators/golanggen.py +3 -1
  7. linkml/generators/graphqlgen.py +34 -2
  8. linkml/generators/jsonschemagen.py +4 -2
  9. linkml/generators/owlgen.py +36 -17
  10. linkml/generators/projectgen.py +13 -11
  11. linkml/generators/pydanticgen/array.py +340 -56
  12. linkml/generators/pydanticgen/build.py +4 -2
  13. linkml/generators/pydanticgen/pydanticgen.py +35 -16
  14. linkml/generators/pydanticgen/template.py +119 -3
  15. linkml/generators/pydanticgen/templates/imports.py.jinja +11 -3
  16. linkml/generators/pydanticgen/templates/module.py.jinja +1 -3
  17. linkml/generators/pydanticgen/templates/validator.py.jinja +2 -2
  18. linkml/generators/python/python_ifabsent_processor.py +1 -1
  19. linkml/generators/pythongen.py +135 -31
  20. linkml/generators/shaclgen.py +34 -10
  21. linkml/generators/sparqlgen.py +3 -1
  22. linkml/generators/sqlalchemygen.py +5 -3
  23. linkml/generators/sqltablegen.py +4 -2
  24. linkml/generators/typescriptgen.py +13 -6
  25. linkml/linter/linter.py +2 -1
  26. linkml/transformers/logical_model_transformer.py +3 -3
  27. linkml/transformers/relmodel_transformer.py +18 -4
  28. linkml/utils/converter.py +3 -1
  29. linkml/utils/exceptions.py +11 -0
  30. linkml/utils/execute_tutorial.py +22 -20
  31. linkml/utils/generator.py +6 -4
  32. linkml/utils/mergeutils.py +4 -2
  33. linkml/utils/schema_fixer.py +5 -5
  34. linkml/utils/schemaloader.py +5 -3
  35. linkml/utils/sqlutils.py +3 -1
  36. linkml/validator/plugins/pydantic_validation_plugin.py +1 -1
  37. linkml/validators/jsonschemavalidator.py +3 -1
  38. linkml/validators/sparqlvalidator.py +5 -3
  39. linkml/workspaces/example_runner.py +3 -1
  40. {linkml-1.8.3.dist-info → linkml-1.8.5.dist-info}/METADATA +3 -1
  41. {linkml-1.8.3.dist-info → linkml-1.8.5.dist-info}/RECORD +44 -42
  42. {linkml-1.8.3.dist-info → linkml-1.8.5.dist-info}/LICENSE +0 -0
  43. {linkml-1.8.3.dist-info → linkml-1.8.5.dist-info}/WHEEL +0 -0
  44. {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
- logging.error(f"Code:\n{pycode}")
362
- logging.error(f"Error compiling generated python code: {e}")
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._as_dict.get(k, None)
461
+ k: getattr(slot, k, None)
452
462
  for k in PydanticAttribute.model_fields.keys()
453
- if slot._as_dict.get(k, None) is not None
463
+ if getattr(slot, k, None) is not None
454
464
  }
455
- slot_args["name"] = underscore(slot.name)
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
- # logging.error(f'range: {s.range} is unknown')
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
- for i in self.imports:
931
- imports += i
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.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:`.render` , use that, otherwise :meth:`.render` fresh.
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(imports=imports)
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]] = Field(default_factory=list)
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
- {%- for i in imports -%}
29
- {{ i }}
30
- {%- endfor -%}
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 -%}
@@ -1,6 +1,4 @@
1
- {% for import in python_imports %}
2
- {{ import }}
3
- {%- endfor -%}
1
+ {{ python_imports }}
4
2
 
5
3
  metamodel_version = "{{metamodel_version}}"
6
4
  version = "{{version if version else None}}"
@@ -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"{enum_name}.{permissible_value_name}"
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()
@@ -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
- logging.error("Generating metamodel without --genmeta is highly inadvisable!")
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
- logging.error(f"Code:\n{pycode}")
92
- logging.error(f"Error compiling generated python code: {e}")
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
- enumimports = (
130
- ""
131
- if self.genmeta
132
- else "from linkml_runtime.linkml_model.meta import EnumDefinition, PermissibleValue, PvFormulaOptions\n"
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
- handlerimport = "from linkml_runtime.utils.enumerations import EnumDefinitionImpl"
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
- import dataclasses
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
- # logging.info(f"CHECKING: {slot_range} {owning_class}")
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
- logging.info(
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
- logging.info(f"TRUE: OCCURS SAME: {cname} == {slot_range} owning: {owning_class}")
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
- logging.info(f"FALSE: OCCURS BEFORE: {cname} == {slot_range} owning: {owning_class}")
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
- logging.info(f"Module {mod} compiled successfully")
1321
+ logger.info(f"Module {mod} compiled successfully")
1218
1322
  print(gen.serialize(emit_metadata=head, **args))
1219
1323
 
1220
1324