linkml 1.8.4__py3-none-any.whl → 1.8.6__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 (38) hide show
  1. linkml/cli/main.py +2 -0
  2. linkml/generators/common/build.py +1 -2
  3. linkml/generators/common/ifabsent_processor.py +98 -21
  4. linkml/generators/common/lifecycle.py +18 -2
  5. linkml/generators/common/naming.py +106 -0
  6. linkml/generators/dbmlgen.py +173 -0
  7. linkml/generators/docgen.py +16 -7
  8. linkml/generators/erdiagramgen.py +1 -0
  9. linkml/generators/graphqlgen.py +34 -2
  10. linkml/generators/jsonldcontextgen.py +7 -1
  11. linkml/generators/jsonschemagen.py +73 -53
  12. linkml/generators/linkmlgen.py +13 -1
  13. linkml/generators/owlgen.py +11 -1
  14. linkml/generators/plantumlgen.py +17 -10
  15. linkml/generators/pydanticgen/array.py +21 -61
  16. linkml/generators/pydanticgen/template.py +12 -1
  17. linkml/generators/pydanticgen/templates/attribute.py.jinja +1 -1
  18. linkml/generators/python/python_ifabsent_processor.py +1 -1
  19. linkml/generators/pythongen.py +123 -21
  20. linkml/generators/shaclgen.py +16 -5
  21. linkml/generators/typescriptgen.py +3 -1
  22. linkml/linter/rules.py +3 -1
  23. linkml/utils/converter.py +17 -0
  24. linkml/utils/deprecation.py +10 -0
  25. linkml/utils/helpers.py +65 -0
  26. linkml/utils/validation.py +2 -1
  27. linkml/validator/__init__.py +2 -2
  28. linkml/validator/plugins/jsonschema_validation_plugin.py +1 -0
  29. linkml/validator/report.py +4 -1
  30. linkml/validator/validator.py +4 -4
  31. linkml/validators/jsonschemavalidator.py +10 -0
  32. linkml/validators/sparqlvalidator.py +7 -0
  33. linkml/workspaces/example_runner.py +20 -1
  34. {linkml-1.8.4.dist-info → linkml-1.8.6.dist-info}/METADATA +2 -2
  35. {linkml-1.8.4.dist-info → linkml-1.8.6.dist-info}/RECORD +38 -36
  36. {linkml-1.8.4.dist-info → linkml-1.8.6.dist-info}/entry_points.txt +1 -0
  37. {linkml-1.8.4.dist-info → linkml-1.8.6.dist-info}/LICENSE +0 -0
  38. {linkml-1.8.4.dist-info → linkml-1.8.6.dist-info}/WHEEL +0 -0
@@ -1,11 +1,14 @@
1
+ import logging
1
2
  import os
3
+ import re
2
4
  from dataclasses import dataclass
3
5
 
4
6
  import click
5
- from linkml_runtime.linkml_model.meta import ClassDefinition, SlotDefinition
7
+ from linkml_runtime.linkml_model.meta import ClassDefinition, EnumDefinition, SlotDefinition
6
8
  from linkml_runtime.utils.formatutils import camelcase, lcamelcase
7
9
 
8
10
  from linkml._version import __version__
11
+ from linkml.generators.common.naming import NameCompatibility, NamingProfiles
9
12
  from linkml.utils.generator import Generator, shared_arguments
10
13
 
11
14
 
@@ -19,6 +22,13 @@ class GraphqlGenerator(Generator):
19
22
  uses_schemaloader = True
20
23
  requires_metamodel = False
21
24
 
25
+ strict_naming: bool = False
26
+ _permissible_value_valid_characters = re.compile("^[_A-Za-z][_0-9A-Za-z]*?$")
27
+
28
+ def __post_init__(self):
29
+ self.name_compatiblity = NameCompatibility(profile=NamingProfiles.graphql, do_not_fix=self.strict_naming)
30
+ super().__post_init__()
31
+
22
32
  def visit_schema(self, **kwargs) -> str:
23
33
  return self.generate_header()
24
34
 
@@ -50,13 +60,35 @@ class GraphqlGenerator(Generator):
50
60
  slotrange = slotrange + "!"
51
61
  return f"\n {lcamelcase(aliased_slot_name)}: {slotrange}"
52
62
 
63
+ def visit_enum(self, enum: EnumDefinition):
64
+ if enum.permissible_values:
65
+ permissible_values = []
66
+ for value in enum.permissible_values:
67
+ permissible_values.append(self.name_compatiblity.compatible(value))
68
+ values = "\n ".join(permissible_values)
69
+ return f"enum {camelcase(enum.name).replace(' ','')}\n {{\n {values}\n }}\n\n"
70
+ else:
71
+ logging.warning(
72
+ f"Enumeration {enum.name} using `reachable_from` instead of `permissible_values` "
73
+ + "to specify permissible values is not supported yet."
74
+ + "Enumeration {enum.name} will be silently ignored!!"
75
+ )
76
+ return ""
77
+
53
78
 
54
79
  @shared_arguments(GraphqlGenerator)
55
80
  @click.command(name="graphql")
81
+ @click.option(
82
+ "--strict-naming",
83
+ is_flag=True,
84
+ show_default=True,
85
+ help="Treat warnings about invalid names or schema elements as errors.",
86
+ )
56
87
  @click.version_option(__version__, "-V", "--version")
57
88
  def cli(yamlfile, **args):
58
89
  """Generate graphql representation of a LinkML model"""
59
- print(GraphqlGenerator(yamlfile, **args).serialize(**args))
90
+ generator = GraphqlGenerator(yamlfile, **args)
91
+ print(generator.serialize(**args))
60
92
 
61
93
 
62
94
  if __name__ == "__main__":
@@ -146,7 +146,13 @@ class ContextGenerator(Generator):
146
146
  slot_def = {}
147
147
  if not slot.usage_slot_name:
148
148
  any_of_ranges = [any_of_el.range for any_of_el in slot.any_of]
149
- if slot.range in self.schema.classes or any(rng in self.schema.classes for rng in any_of_ranges):
149
+ if slot.range in self.schema.classes:
150
+ range_class_uri = self.schema.classes[slot.range].class_uri
151
+ if range_class_uri and slot.inlined:
152
+ slot_def["@type"] = range_class_uri
153
+ else:
154
+ slot_def["@type"] = "@id"
155
+ elif any(rng in self.schema.classes for rng in any_of_ranges):
150
156
  slot_def["@type"] = "@id"
151
157
  elif slot.range in self.schema.enums:
152
158
  slot_def["@context"] = ENUM_CONTEXT
@@ -21,8 +21,11 @@ from linkml_runtime.linkml_model.meta import (
21
21
  from linkml_runtime.utils.formatutils import be, camelcase, underscore
22
22
 
23
23
  from linkml._version import __version__
24
+ from linkml.generators.common import build
25
+ from linkml.generators.common.lifecycle import LifecycleMixin
24
26
  from linkml.generators.common.type_designators import get_type_designator_value
25
27
  from linkml.utils.generator import Generator, shared_arguments
28
+ from linkml.utils.helpers import get_range_associated_slots
26
29
 
27
30
  logger = logging.getLogger(__name__)
28
31
 
@@ -177,8 +180,32 @@ class JsonSchema(dict):
177
180
  return JsonSchema(schema)
178
181
 
179
182
 
183
+ class SchemaResult(build.SchemaResult):
184
+ """Top-level result of building a json schema"""
185
+
186
+ schema_: JsonSchema
187
+
188
+
189
+ class EnumResult(build.EnumResult):
190
+ """A single built enum"""
191
+
192
+ schema_: JsonSchema
193
+
194
+
195
+ class ClassResult(build.ClassResult):
196
+ """A single built class"""
197
+
198
+ schema_: JsonSchema
199
+
200
+
201
+ class SlotResult(build.SlotResult):
202
+ """A slot within the context of a class"""
203
+
204
+ schema_: JsonSchema
205
+
206
+
180
207
  @dataclass
181
- class JsonSchemaGenerator(Generator):
208
+ class JsonSchemaGenerator(Generator, LifecycleMixin):
182
209
  """
183
210
  Generates JSONSchema documents from a LinkML SchemaDefinition
184
211
 
@@ -187,6 +214,21 @@ class JsonSchemaGenerator(Generator):
187
214
  - Composition not yet implemented
188
215
  - Enumerations treated as strings
189
216
  - Foreign key references are treated as semantics-free strings
217
+
218
+ This generator implements the following :class:`.LifecycleMixin` methods:
219
+
220
+ * :meth:`.LifecycleMixin.before_generate_schema`
221
+ * :meth:`.LifecycleMixin.after_generate_schema`
222
+ * :meth:`.LifecycleMixin.before_generate_classes`
223
+ * :meth:`.LifecycleMixin.before_generate_enums`
224
+ * :meth:`.LifecycleMixin.before_generate_class_slots`
225
+ * :meth:`.LifecycleMixin.before_generate_class`
226
+ * :meth:`.LifecycleMixin.after_generate_class`
227
+ * :meth:`.LifecycleMixin.before_generate_class_slot`
228
+ * :meth:`.LifecycleMixin.after_generate_class_slot`
229
+ * :meth:`.LifecycleMixin.before_generate_enum`
230
+ * :meth:`.LifecycleMixin.after_generate_enum`
231
+
190
232
  """
191
233
 
192
234
  # ClassVars
@@ -233,7 +275,7 @@ class JsonSchemaGenerator(Generator):
233
275
  if self.schemaview.get_class(self.top_class) is None:
234
276
  logger.warning(f"No class in schema named {self.top_class}")
235
277
 
236
- def start_schema(self, inline: bool = False) -> JsonSchema:
278
+ def start_schema(self, inline: bool = False):
237
279
  self.inline = inline
238
280
 
239
281
  self.top_level_schema = JsonSchema(
@@ -249,6 +291,8 @@ class JsonSchemaGenerator(Generator):
249
291
  )
250
292
 
251
293
  def handle_class(self, cls: ClassDefinition) -> None:
294
+ cls = self.before_generate_class(cls, self.schemaview)
295
+
252
296
  if cls.mixin or cls.abstract:
253
297
  return
254
298
 
@@ -268,7 +312,10 @@ class JsonSchemaGenerator(Generator):
268
312
  if self.title_from == "title" and cls.title:
269
313
  class_subschema["title"] = cls.title
270
314
 
271
- for slot_definition in self.schemaview.class_induced_slots(cls.name):
315
+ class_slots = self.before_generate_class_slots(
316
+ self.schemaview.class_induced_slots(cls.name), cls, self.schemaview
317
+ )
318
+ for slot_definition in class_slots:
272
319
  self.handle_class_slot(subschema=class_subschema, cls=cls, slot=slot_definition)
273
320
 
274
321
  rule_subschemas = []
@@ -318,6 +365,10 @@ class JsonSchemaGenerator(Generator):
318
365
  class_subschema["allOf"] = []
319
366
  class_subschema["allOf"].extend(rule_subschemas)
320
367
 
368
+ class_subschema = self.after_generate_class(
369
+ ClassResult.model_construct(schema_=class_subschema, source=cls), self.schemaview
370
+ ).schema_
371
+
321
372
  self.top_level_schema.add_def(cls.name, class_subschema)
322
373
 
323
374
  if (self.top_class is not None and camelcase(self.top_class) == camelcase(cls.name)) or (
@@ -375,6 +426,7 @@ class JsonSchemaGenerator(Generator):
375
426
  def handle_enum(self, enum: EnumDefinition) -> None:
376
427
  # TODO: this only works with explicitly permitted values. It will need to be extended to
377
428
  # support other pv_formula
429
+ enum = self.before_generate_enum(enum, self.schemaview)
378
430
 
379
431
  def extract_permissible_text(pv):
380
432
  if isinstance(pv, str):
@@ -398,6 +450,10 @@ class JsonSchemaGenerator(Generator):
398
450
 
399
451
  if permissible_values_texts:
400
452
  enum_schema["enum"] = permissible_values_texts
453
+
454
+ enum_schema = self.after_generate_enum(
455
+ EnumResult.model_construct(schema_=enum_schema, source=enum), self.schemaview
456
+ ).schema_
401
457
  self.top_level_schema.add_def(enum.name, enum_schema)
402
458
 
403
459
  def get_type_info_for_slot_subschema(
@@ -490,7 +546,7 @@ class JsonSchemaGenerator(Generator):
490
546
  range_id_slot,
491
547
  range_simple_dict_value_slot,
492
548
  range_required_slots,
493
- ) = self._get_range_associated_slots(slot)
549
+ ) = get_range_associated_slots(self.schemaview, slot.range)
494
550
  # if the range class has an ID and the slot is not inlined as a list, then we need to consider
495
551
  # various inlined as dict formats
496
552
  if range_id_slot is not None and not slot.inlined_as_list:
@@ -596,6 +652,7 @@ class JsonSchemaGenerator(Generator):
596
652
  return prop
597
653
 
598
654
  def handle_class_slot(self, subschema: JsonSchema, cls: ClassDefinition, slot: SlotDefinition) -> None:
655
+ slot = self.before_generate_class_slot(slot, cls, self.schemaview)
599
656
  class_id_slot = self.schemaview.get_identifier_slot(cls.name, use_key=True)
600
657
  value_required = (
601
658
  slot.required or slot == class_id_slot or slot.value_presence == PresenceEnum(PresenceEnum.PRESENT)
@@ -604,6 +661,9 @@ class JsonSchemaGenerator(Generator):
604
661
 
605
662
  aliased_slot_name = self.aliased_slot_name(slot)
606
663
  prop = self.get_subschema_for_slot(slot, include_null=self.include_null)
664
+ prop = self.after_generate_class_slot(
665
+ SlotResult.model_construct(schema_=prop, source=slot), cls, self.schemaview
666
+ ).schema_
607
667
  subschema.add_property(
608
668
  aliased_slot_name, prop, value_required=value_required, value_disallowed=value_disallowed
609
669
  )
@@ -613,65 +673,25 @@ class JsonSchemaGenerator(Generator):
613
673
  prop["enum"] = [type_value]
614
674
 
615
675
  def generate(self) -> JsonSchema:
676
+ self.schema = self.before_generate_schema(self.schema, self.schemaview)
616
677
  self.start_schema()
617
- for enum_definition in self.schemaview.all_enums().values():
678
+
679
+ all_enums = self.before_generate_enums(self.schemaview.all_enums().values(), self.schemaview)
680
+ for enum_definition in all_enums:
618
681
  self.handle_enum(enum_definition)
619
682
 
620
- for class_definition in self.schemaview.all_classes().values():
683
+ all_classes = self.before_generate_classes(self.schemaview.all_classes().values(), self.schemaview)
684
+ for class_definition in all_classes:
621
685
  self.handle_class(class_definition)
622
686
 
687
+ self.top_level_schema = self.after_generate_schema(
688
+ SchemaResult.model_construct(schema_=self.top_level_schema, source=self.schema), self.schemaview
689
+ ).schema_
623
690
  return self.top_level_schema
624
691
 
625
692
  def serialize(self, **kwargs) -> str:
626
693
  return self.generate().to_json(sort_keys=True, indent=self.indent if self.indent > 0 else None)
627
694
 
628
- def _get_range_associated_slots(
629
- self, slot: SlotDefinition
630
- ) -> Tuple[Union[SlotDefinition, None], Union[SlotDefinition, None], Union[List[SlotDefinition], None]]:
631
- range_class = self.schemaview.get_class(slot.range)
632
- if range_class is None:
633
- return None, None, None
634
-
635
- range_class_id_slot = self.schemaview.get_identifier_slot(range_class.name, use_key=True)
636
- if range_class_id_slot is None:
637
- return None, None, None
638
-
639
- non_id_slots = [
640
- s for s in self.schemaview.class_induced_slots(range_class.name) if s.name != range_class_id_slot.name
641
- ]
642
- non_id_required_slots = [s for s in non_id_slots if s.required]
643
-
644
- # Some lists of objects can be serialized as SimpleDicts.
645
- # A SimpleDict is serialized as simple key-value pairs where the value is atomic.
646
- # The key must be declared as a key, and the value must satisfy one of the following conditions:
647
- # 1. The value slot is the only other slot in the object other than the key
648
- # 2. The value slot is explicitly annotated as a simple_dict_value
649
- # 3. The value slot is the only non-key that is required
650
- # See also: https://github.com/linkml/linkml/issues/1250
651
- range_simple_dict_value_slot = None
652
- if len(non_id_slots) == 1:
653
- range_simple_dict_value_slot = non_id_slots[0]
654
- elif len(non_id_slots) > 1:
655
- candidate_non_id_slots = []
656
- for non_id_slot in non_id_slots:
657
- if isinstance(non_id_slot.annotations, dict):
658
- is_simple_dict_value = non_id_slot.annotations.get("simple_dict_value", False)
659
- else:
660
- is_simple_dict_value = getattr(non_id_slot.annotations, "simple_dict_value", False)
661
- if is_simple_dict_value:
662
- candidate_non_id_slots.append(non_id_slot)
663
- if len(candidate_non_id_slots) == 1:
664
- range_simple_dict_value_slot = candidate_non_id_slots[0]
665
- else:
666
- candidate_non_id_slots = []
667
- for non_id_slot in non_id_slots:
668
- if non_id_slot.required:
669
- candidate_non_id_slots.append(non_id_slot)
670
- if len(candidate_non_id_slots) == 1:
671
- range_simple_dict_value_slot = candidate_non_id_slots[0]
672
-
673
- return range_class_id_slot, range_simple_dict_value_slot, non_id_required_slots
674
-
675
695
 
676
696
  @shared_arguments(JsonSchemaGenerator)
677
697
  @click.command(name="json-schema")
@@ -78,6 +78,12 @@ class LinkmlGenerator(Generator):
78
78
 
79
79
 
80
80
  @shared_arguments(LinkmlGenerator)
81
+ @click.option(
82
+ "--materialize/--no-materialize",
83
+ default=True,
84
+ show_default=True,
85
+ help="Materialize both, induced slots as attributes and structured patterns as patterns",
86
+ )
81
87
  @click.option(
82
88
  "--materialize-attributes/--no-materialize-attributes",
83
89
  default=True,
@@ -86,7 +92,7 @@ class LinkmlGenerator(Generator):
86
92
  )
87
93
  @click.option(
88
94
  "--materialize-patterns/--no-materialize-patterns",
89
- default=False,
95
+ default=True,
90
96
  show_default=True,
91
97
  help="Materialize structured patterns as patterns",
92
98
  )
@@ -100,11 +106,17 @@ class LinkmlGenerator(Generator):
100
106
  @click.command(name="linkml")
101
107
  def cli(
102
108
  yamlfile,
109
+ materialize: bool,
103
110
  materialize_attributes: bool,
104
111
  materialize_patterns: bool,
105
112
  output: FILE_TYPE = None,
106
113
  **kwargs,
107
114
  ):
115
+ # You can use the `--materialize` / `--no-materialize` for control
116
+ # over both attribute and pattern materialization.
117
+ materialize_attributes = bool(materialize)
118
+ materialize_patterns = bool(materialize)
119
+
108
120
  gen = LinkmlGenerator(
109
121
  yamlfile,
110
122
  materialize_attributes=materialize_attributes,
@@ -170,6 +170,9 @@ class OwlSchemaGenerator(Generator):
170
170
  default_factory=lambda: package_schemaview("linkml_runtime.linkml_model.meta")
171
171
  )
172
172
 
173
+ enum_iri_separator: str = "#"
174
+ """Separator for enum IRI. Can be overridden for example if your namespace IRI already contains a #"""
175
+
173
176
  def as_graph(self) -> Graph:
174
177
  """
175
178
  Generate an rdflib Graph from the LinkML schema.
@@ -1254,7 +1257,7 @@ class OwlSchemaGenerator(Generator):
1254
1257
  if pv.meaning:
1255
1258
  return URIRef(self.schemaview.expand_curie(pv.meaning))
1256
1259
  else:
1257
- return URIRef(enum_uri + "#" + pv.text.replace(" ", "+"))
1260
+ return URIRef(enum_uri + self.enum_iri_separator + pv.text.replace(" ", "+"))
1258
1261
 
1259
1262
  def slot_owl_type(self, slot: SlotDefinition) -> URIRef:
1260
1263
  sv = self.schemaview
@@ -1353,6 +1356,13 @@ class OwlSchemaGenerator(Generator):
1353
1356
  show_default=True,
1354
1357
  help="Default OWL type for permissible values",
1355
1358
  )
1359
+ @click.option(
1360
+ "--enum-iri-separator",
1361
+ default="#",
1362
+ is_flag=False,
1363
+ show_default=True,
1364
+ help="IRI separator for enums.",
1365
+ )
1356
1366
  @click.version_option(__version__, "-V", "--version")
1357
1367
  def cli(yamlfile, metadata_profile: str, **kwargs):
1358
1368
  """Generate an OWL representation of a LinkML model
@@ -49,14 +49,13 @@ class PlantumlGenerator(Generator):
49
49
  classes: Set[ClassDefinitionName] = None
50
50
  directory: Optional[str] = None
51
51
  kroki_server: Optional[str] = "https://kroki.io"
52
- load_image: bool = True
53
52
  tooltips_flag: bool = False
53
+ dry_run: bool = False
54
54
 
55
55
  def visit_schema(
56
56
  self,
57
57
  classes: Set[ClassDefinitionName] = None,
58
58
  directory: Optional[str] = None,
59
- load_image: bool = True,
60
59
  **_,
61
60
  ) -> Optional[str]:
62
61
  if directory:
@@ -96,20 +95,21 @@ class PlantumlGenerator(Generator):
96
95
  b64_diagram = base64.urlsafe_b64encode(zlib.compress(plantuml_code.encode(), 9))
97
96
 
98
97
  plantuml_url = self.kroki_server + "/plantuml/svg/" + b64_diagram.decode()
98
+ if self.dry_run:
99
+ return plantuml_url
99
100
  if directory:
100
101
  file_suffix = ".svg" if self.format == "puml" or self.format == "puml" else "." + self.format
101
102
  self.output_file_name = os.path.join(
102
103
  directory,
103
104
  camelcase(sorted(classes)[0] if classes else self.schema.name) + file_suffix,
104
105
  )
105
- if load_image:
106
- resp = requests.get(plantuml_url, stream=True, timeout=REQUESTS_TIMEOUT)
107
- if resp.ok:
108
- with open(self.output_file_name, "wb") as f:
109
- for chunk in resp.iter_content(chunk_size=2048):
110
- f.write(chunk)
111
- else:
112
- self.logger.error(f"{resp.reason} accessing {plantuml_url}")
106
+ resp = requests.get(plantuml_url, stream=True, timeout=REQUESTS_TIMEOUT)
107
+ if resp.ok:
108
+ with open(self.output_file_name, "wb") as f:
109
+ for chunk in resp.iter_content(chunk_size=2048):
110
+ f.write(chunk)
111
+ else:
112
+ self.logger.error(f"{resp.reason} accessing {plantuml_url}")
113
113
  else:
114
114
  out = (
115
115
  "@startuml\n"
@@ -352,6 +352,13 @@ class PlantumlGenerator(Generator):
352
352
  help="URL of the Kroki server to use for diagram drawing",
353
353
  default="https://kroki.io",
354
354
  )
355
+ @click.option(
356
+ "--dry-run",
357
+ is_flag=True,
358
+ default=False,
359
+ show_default=True,
360
+ help="Print out Kroki URL calls instead of sending the real requests",
361
+ )
355
362
  @click.version_option(__version__, "-V", "--version")
356
363
  def cli(yamlfile, **args):
357
364
  """Generate a UML representation of a LinkML model"""
@@ -1,7 +1,15 @@
1
1
  import sys
2
2
  from abc import ABC, abstractmethod
3
3
  from enum import Enum
4
- from typing import TYPE_CHECKING, Any, ClassVar, Generic, Iterable, List, Optional, Type, TypeVar, Union, get_args
4
+ from typing import (
5
+ ClassVar,
6
+ Iterable,
7
+ List,
8
+ Optional,
9
+ Type,
10
+ TypeVar,
11
+ Union,
12
+ )
5
13
 
6
14
  from linkml_runtime.linkml_model import Element
7
15
  from linkml_runtime.linkml_model.meta import ArrayExpression, DimensionExpression
@@ -9,20 +17,15 @@ from pydantic import VERSION as PYDANTIC_VERSION
9
17
 
10
18
  from linkml.utils.deprecation import deprecation_warning
11
19
 
12
- if int(PYDANTIC_VERSION[0]) >= 2:
13
- from pydantic_core import core_schema
14
- else:
20
+ if int(PYDANTIC_VERSION[0]) < 2:
15
21
  # Support for having pydantic 1 installed in the same environment will be dropped in 1.9.0
16
22
  deprecation_warning("pydantic-v1")
17
23
 
18
- if TYPE_CHECKING:
19
- from pydantic import GetCoreSchemaHandler
20
- from pydantic_core import CoreSchema
21
-
22
- if sys.version_info.minor <= 8:
23
- from typing_extensions import Annotated
24
+ if sys.version_info.minor < 12:
25
+ from typing_extensions import TypeAliasType
24
26
  else:
25
- from typing import Annotated
27
+ from typing import TypeAliasType
28
+
26
29
 
27
30
  from linkml.generators.pydanticgen.build import RangeResult
28
31
  from linkml.generators.pydanticgen.template import ConditionalImport, Import, Imports, ObjectImport
@@ -37,75 +40,32 @@ class ArrayRepresentation(Enum):
37
40
  _BOUNDED_ARRAY_FIELDS = ("exact_number_dimensions", "minimum_number_dimensions", "maximum_number_dimensions")
38
41
 
39
42
  _T = TypeVar("_T")
40
- _RecursiveListType = Iterable[Union[_T, Iterable["_RecursiveListType"]]]
41
-
42
-
43
- class AnyShapeArrayType(Generic[_T]):
44
- @classmethod
45
- def __get_pydantic_core_schema__(cls, source_type: Any, handler: "GetCoreSchemaHandler") -> "CoreSchema":
46
- # double-nested parameterized types here
47
- # source_type: List[Union[T,List[...]]]
48
- item_type = (Any,) if get_args(get_args(source_type)[0])[0] is _T else get_args(get_args(source_type)[0])[:-1]
49
-
50
- if len(item_type) == 1:
51
- item_schema = handler.generate_schema(item_type[0])
52
- else:
53
- item_schema = core_schema.union_schema([handler.generate_schema(i) for i in item_type])
54
-
55
- if all([getattr(i, "__module__", "") == "builtins" and i is not Any for i in item_type]):
56
- item_schema["strict"] = True
57
-
58
- # Before python 3.11, `Any` type was a special object without a __name__
59
- item_name = "_".join(["Any" if i is Any else i.__name__ for i in item_type])
60
-
61
- array_ref = f"any-shape-array-{item_name}"
62
-
63
- schema = core_schema.definitions_schema(
64
- core_schema.list_schema(core_schema.definition_reference_schema(array_ref)),
65
- [
66
- core_schema.union_schema(
67
- [
68
- core_schema.list_schema(core_schema.definition_reference_schema(array_ref)),
69
- item_schema,
70
- ],
71
- ref=array_ref,
72
- )
73
- ],
74
- )
75
-
76
- return schema
77
-
78
-
79
- AnyShapeArray = Annotated[_RecursiveListType, AnyShapeArrayType]
43
+ AnyShapeArray = TypeAliasType("AnyShapeArray", Iterable[Union[_T, Iterable["AnyShapeArray[_T]"]]], type_params=(_T,))
80
44
 
81
45
  _AnyShapeArrayImports = (
82
46
  Imports()
83
47
  + Import(
84
48
  module="typing",
85
49
  objects=[
86
- ObjectImport(name="Generic"),
87
50
  ObjectImport(name="Iterable"),
88
51
  ObjectImport(name="TypeVar"),
89
52
  ObjectImport(name="Union"),
90
- ObjectImport(name="get_args"),
91
53
  ],
92
54
  )
93
55
  + ConditionalImport(
94
- condition="sys.version_info.minor > 8",
56
+ condition="sys.version_info.minor >= 12",
95
57
  module="typing",
96
- objects=[ObjectImport(name="Annotated")],
97
- alternative=Import(module="typing_extensions", objects=[ObjectImport(name="Annotated")]),
58
+ objects=[ObjectImport(name="TypeAliasType")],
59
+ alternative=Import(module="typing_extensions", objects=[ObjectImport(name="TypeAliasType")]),
98
60
  )
99
- + Import(module="pydantic", objects=[ObjectImport(name="GetCoreSchemaHandler")])
100
- + Import(module="pydantic_core", objects=[ObjectImport(name="CoreSchema"), ObjectImport(name="core_schema")])
101
61
  )
102
62
 
103
63
  # annotated types are special and inspect.getsource() can't stringify them
104
64
  _AnyShapeArrayInjects = [
105
65
  '_T = TypeVar("_T")',
106
- '_RecursiveListType = Iterable[Union[_T, Iterable["_RecursiveListType"]]]',
107
- AnyShapeArrayType,
108
- "AnyShapeArray = Annotated[_RecursiveListType, AnyShapeArrayType]",
66
+ """AnyShapeArray = TypeAliasType(
67
+ "AnyShapeArray", Iterable[Union[_T, Iterable["AnyShapeArray[_T]"]]], type_params=(_T,)
68
+ )""",
109
69
  ]
110
70
 
111
71
  _ConListImports = Imports() + Import(module="pydantic", objects=[ObjectImport(name="conlist")])
@@ -675,7 +675,18 @@ class PydanticModule(PydanticTemplateModel):
675
675
  return [c.name for c in self.classes.values()]
676
676
 
677
677
 
678
- _some_stdlib_module_names = {"copy", "datetime", "decimal", "enum", "inspect", "os", "re", "sys", "typing"}
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
+ }
679
690
  """
680
691
  sys.stdlib_module_names is only present in 3.10 and later
681
692
  so we make a cheap copy of the stdlib modules that we commonly use here,
@@ -1,4 +1,4 @@
1
- {{name}}: {{ range }} = Field({{ field }}
1
+ {{name}}: {{ range }} = Field(default={{ field }}
2
2
  {%- if title != None %}, title="{{title}}"{% endif -%}
3
3
  {%- if description %}, description="""{{description}}"""{% endif -%}
4
4
  {%- if equals_number != None %}
@@ -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()