linkml 1.7.5__py3-none-any.whl → 1.7.7__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/docgen/class.md.jinja2 +15 -2
  3. linkml/generators/docgen/slot.md.jinja2 +18 -4
  4. linkml/generators/docgen.py +17 -3
  5. linkml/generators/jsonldcontextgen.py +40 -17
  6. linkml/generators/jsonldgen.py +3 -1
  7. linkml/generators/owlgen.py +16 -0
  8. linkml/generators/prefixmapgen.py +5 -4
  9. linkml/generators/projectgen.py +14 -2
  10. linkml/generators/pydanticgen/__init__.py +29 -0
  11. linkml/generators/pydanticgen/array.py +457 -0
  12. linkml/generators/pydanticgen/black.py +29 -0
  13. linkml/generators/pydanticgen/build.py +79 -0
  14. linkml/generators/{pydanticgen.py → pydanticgen/pydanticgen.py} +252 -304
  15. linkml/generators/pydanticgen/template.py +577 -0
  16. linkml/generators/pydanticgen/templates/attribute.py.jinja +10 -0
  17. linkml/generators/pydanticgen/templates/base_model.py.jinja +29 -0
  18. linkml/generators/pydanticgen/templates/class.py.jinja +21 -0
  19. linkml/generators/pydanticgen/templates/conditional_import.py.jinja +9 -0
  20. linkml/generators/pydanticgen/templates/enum.py.jinja +16 -0
  21. linkml/generators/pydanticgen/templates/footer.py.jinja +13 -0
  22. linkml/generators/pydanticgen/templates/imports.py.jinja +31 -0
  23. linkml/generators/pydanticgen/templates/module.py.jinja +27 -0
  24. linkml/generators/pydanticgen/templates/validator.py.jinja +15 -0
  25. linkml/generators/pythongen.py +13 -7
  26. linkml/generators/shacl/__init__.py +3 -0
  27. linkml/generators/shacl/ifabsent_processor.py +59 -0
  28. linkml/generators/shacl/shacl_data_type.py +40 -0
  29. linkml/generators/shaclgen.py +105 -82
  30. linkml/generators/shexgen.py +1 -1
  31. linkml/generators/sqlalchemygen.py +1 -1
  32. linkml/generators/sqltablegen.py +32 -22
  33. linkml/generators/terminusdbgen.py +7 -1
  34. linkml/linter/config/datamodel/config.py +8 -0
  35. linkml/linter/rules.py +11 -2
  36. linkml/utils/generator.py +7 -6
  37. linkml/utils/ifabsent_functions.py +7 -9
  38. linkml/utils/schemaloader.py +1 -9
  39. linkml/utils/sqlutils.py +39 -25
  40. {linkml-1.7.5.dist-info → linkml-1.7.7.dist-info}/METADATA +9 -4
  41. {linkml-1.7.5.dist-info → linkml-1.7.7.dist-info}/RECORD +44 -27
  42. {linkml-1.7.5.dist-info → linkml-1.7.7.dist-info}/LICENSE +0 -0
  43. {linkml-1.7.5.dist-info → linkml-1.7.7.dist-info}/WHEEL +0 -0
  44. {linkml-1.7.5.dist-info → linkml-1.7.7.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,457 @@
1
+ import sys
2
+ from abc import ABC, abstractmethod
3
+ from enum import Enum
4
+ from typing import (
5
+ Any,
6
+ ClassVar,
7
+ Generic,
8
+ Iterable,
9
+ List,
10
+ Optional,
11
+ Type,
12
+ TypeVar,
13
+ Union,
14
+ get_args,
15
+ )
16
+
17
+ from linkml_runtime.linkml_model import Element
18
+ from linkml_runtime.linkml_model.meta import ArrayExpression, DimensionExpression
19
+ from pydantic import VERSION as PYDANTIC_VERSION
20
+
21
+ if int(PYDANTIC_VERSION[0]) < 2:
22
+ pass
23
+ else:
24
+ from pydantic import GetCoreSchemaHandler
25
+ from pydantic_core import CoreSchema, core_schema
26
+
27
+ if sys.version_info.minor <= 8:
28
+ from typing_extensions import Annotated
29
+ else:
30
+ from typing import Annotated
31
+
32
+ from linkml.generators.pydanticgen.build import SlotResult
33
+ from linkml.generators.pydanticgen.template import Import, Imports, ObjectImport
34
+
35
+
36
+ class ArrayRepresentation(Enum):
37
+ LIST = "list"
38
+ NPARRAY = "nparray" # numpy and nptyping must be installed to use this
39
+
40
+
41
+ _BOUNDED_ARRAY_FIELDS = ("exact_number_dimensions", "minimum_number_dimensions", "maximum_number_dimensions")
42
+
43
+ _T = TypeVar("_T")
44
+ _RecursiveListType = Iterable[Union[_T, Iterable["_RecursiveListType"]]]
45
+ if int(PYDANTIC_VERSION[0]) >= 2:
46
+
47
+ class AnyShapeArrayType(Generic[_T]):
48
+ @classmethod
49
+ def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema:
50
+ # double-nested parameterized types here
51
+ # source_type: List[Union[T,List[...]]]
52
+ item_type = Any if get_args(get_args(source_type)[0])[0] is _T else get_args(get_args(source_type)[0])[0]
53
+
54
+ item_schema = handler.generate_schema(item_type)
55
+ if item_schema.get("type", "any") != "any":
56
+ item_schema["strict"] = True
57
+ array_ref = f"any-shape-array-{item_type.__name__}"
58
+
59
+ schema = core_schema.definitions_schema(
60
+ core_schema.list_schema(core_schema.definition_reference_schema(array_ref)),
61
+ [
62
+ core_schema.union_schema(
63
+ [
64
+ core_schema.list_schema(core_schema.definition_reference_schema(array_ref)),
65
+ item_schema,
66
+ ],
67
+ ref=array_ref,
68
+ )
69
+ ],
70
+ )
71
+
72
+ return schema
73
+
74
+ AnyShapeArray = Annotated[_RecursiveListType, AnyShapeArrayType]
75
+
76
+ _AnyShapeArrayImports = (
77
+ Imports()
78
+ + Import(
79
+ module="typing",
80
+ objects=[
81
+ ObjectImport(name="Annotated"),
82
+ ObjectImport(name="Generic"),
83
+ ObjectImport(name="Iterable"),
84
+ ObjectImport(name="TypeVar"),
85
+ ObjectImport(name="Union"),
86
+ ObjectImport(name="get_args"),
87
+ ],
88
+ )
89
+ + Import(module="pydantic", objects=[ObjectImport(name="GetCoreSchemaHandler")])
90
+ + Import(module="pydantic_core", objects=[ObjectImport(name="CoreSchema"), ObjectImport(name="core_schema")])
91
+ )
92
+
93
+ # annotated types are special and inspect.getsource() can't stringify them
94
+ _AnyShapeArrayInjects = [
95
+ '_T = TypeVar("_T")',
96
+ '_RecursiveListType = Iterable[Union[_T, Iterable["_RecursiveListType"]]]',
97
+ AnyShapeArrayType,
98
+ "AnyShapeArray = Annotated[_RecursiveListType, AnyShapeArrayType]",
99
+ ]
100
+
101
+ else:
102
+
103
+ class AnyShapeArray(Generic[_T]):
104
+ type_: Type[Any] = Any
105
+
106
+ def __class_getitem__(cls, item):
107
+ alias = type(f"AnyShape_{str(item.__name__)}", (AnyShapeArray,), {"type_": item})
108
+ alias.type_ = item
109
+ return alias
110
+
111
+ @classmethod
112
+ def __get_validators__(cls):
113
+ yield cls.validate
114
+
115
+ @classmethod
116
+ def __modify_schema__(cls, field_schema):
117
+ try:
118
+ item_type = field_schema["allOf"][0]["type"]
119
+ type_schema = {"type": item_type}
120
+ del field_schema["allOf"]
121
+ except KeyError as e:
122
+ if "allOf" in str(e):
123
+ item_type = "Any"
124
+ type_schema = {}
125
+ else:
126
+ raise e
127
+
128
+ array_id = f"#any-shape-array-{item_type}"
129
+ field_schema["anyOf"] = [
130
+ type_schema,
131
+ {"type": "array", "items": {"$ref": array_id}},
132
+ ]
133
+ field_schema["$id"] = array_id
134
+
135
+ @classmethod
136
+ def validate(cls, v: Union[List[_T], list]):
137
+ if str(type(v)) == "<class 'numpy.ndarray'>":
138
+ v = v.tolist()
139
+
140
+ if not isinstance(v, list):
141
+ raise TypeError(f"Must be a list of lists! got {v}")
142
+
143
+ def _validate(_v: Union[List[_T], list]):
144
+ for item in _v:
145
+ if isinstance(item, list):
146
+ _validate(item)
147
+ else:
148
+ try:
149
+ anytype = cls.type_.__name__ in ("AnyType", "Any")
150
+ except AttributeError:
151
+ # in python 3.8 and 3.9, `typing.Any` has no __name__
152
+ anytype = str(cls.type_).split(".")[-1] in ("AnyType", "Any")
153
+
154
+ if not anytype and not isinstance(item, cls.type_):
155
+ raise TypeError(
156
+ (
157
+ f"List items must be list of lists, or the type used in "
158
+ f"the subscript ({cls.type_}. Got item {item} and outer value {v}"
159
+ )
160
+ )
161
+ return _v
162
+
163
+ return _validate(v)
164
+
165
+ _AnyShapeArrayImports = Imports() + Import(
166
+ module="typing",
167
+ objects=[ObjectImport(name="Generic"), ObjectImport(name="TypeVar"), ObjectImport(name="_GenericAlias")],
168
+ )
169
+ _AnyShapeArrayInjects = [
170
+ '_T = TypeVar("_T")',
171
+ AnyShapeArray,
172
+ ]
173
+
174
+ _ConListImports = Imports() + Import(module="pydantic", objects=[ObjectImport(name="conlist")])
175
+
176
+
177
+ class ArrayRangeGenerator(ABC):
178
+ """
179
+ Metaclass for generating a given format of array annotation.
180
+
181
+ See :ref:`array-forms` for more details on array range forms.
182
+
183
+ These classes do only enough validation of the array specification to decide
184
+ which kind of representation to generate. Proper value validation should
185
+ happen elsewhere (ie. in the metamodel and generated :class:`.ArrayExpression` class.)
186
+
187
+ Each of the array representation generation methods should be able to handle
188
+ the supported pydantic versions (currently still 1 and 2).
189
+
190
+ Notes:
191
+
192
+ When checking for array specification, recall that there is a semantic difference between
193
+ ``None`` and ``False`` , particularly for :attr:`.ArrayExpression.max_number_dimensions` -
194
+ check for absence of specification with ``is None`` rather than checking for truthiness/falsiness
195
+ (unless that's what you intend to do ofc ;)
196
+
197
+ Attributes:
198
+ array (:class:`.ArrayExpression` ): Array to create an annotation for
199
+ dtype (Union[str, :class:`.Element` ): dtype of the entire array as a string
200
+ pydantic_ver (str): Pydantic version to generate array form for -
201
+ currently only pydantic 1 and 2 are differentiated, and pydantic 1 will be deprecated soon.
202
+
203
+ """
204
+
205
+ REPR: ClassVar[ArrayRepresentation]
206
+
207
+ def __init__(
208
+ self, array: Optional[ArrayExpression], dtype: Union[str, Element], pydantic_ver: str = PYDANTIC_VERSION
209
+ ):
210
+ self.array = array
211
+ self.dtype = dtype
212
+ self.pydantic_ver = pydantic_ver
213
+
214
+ def make(self) -> SlotResult:
215
+ """Create the string form of the array representation"""
216
+ if not self.array.dimensions and not self.has_bounded_dimensions:
217
+ # any-shaped array
218
+ return self.any_shape(self.array)
219
+ elif not self.array.dimensions and self.has_bounded_dimensions:
220
+ return self.bounded_dimensions(self.array)
221
+ elif self.array.dimensions and not self.has_bounded_dimensions:
222
+ return self.parameterized_dimensions(self.array)
223
+ else:
224
+ return self.complex_dimensions(self.array)
225
+
226
+ @property
227
+ def has_bounded_dimensions(self) -> bool:
228
+ """Whether the :class:`.ArrayExpression` has some shape specification aside from ``dimensions``"""
229
+ return any([getattr(self.array, arr_field, None) is not None for arr_field in _BOUNDED_ARRAY_FIELDS])
230
+
231
+ @classmethod
232
+ def get_generator(cls, repr: ArrayRepresentation) -> Type["ArrayRangeGenerator"]:
233
+ """Get the generator class for a given array representation"""
234
+ for subclass in cls.__subclasses__():
235
+ if repr in (subclass.REPR, subclass.REPR.value):
236
+ return subclass
237
+ raise ValueError(f"Generator for array representation {repr} not found!")
238
+
239
+ @abstractmethod
240
+ def any_shape(self, array: Optional[ArrayRepresentation] = None) -> SlotResult:
241
+ """Any shaped array!"""
242
+ pass
243
+
244
+ @abstractmethod
245
+ def bounded_dimensions(self, array: ArrayExpression) -> SlotResult:
246
+ """Array shape specified numerically, without axis parameterization"""
247
+ pass
248
+
249
+ @abstractmethod
250
+ def parameterized_dimensions(self, array: ArrayExpression) -> SlotResult:
251
+ """Array shape specified with ``dimensions`` without additional parameterized dimensions"""
252
+ pass
253
+
254
+ @abstractmethod
255
+ def complex_dimensions(self, array: ArrayExpression) -> SlotResult:
256
+ """Array shape with both ``parameterized`` and ``bounded`` dimensions"""
257
+ pass
258
+
259
+
260
+ class ListOfListsArray(ArrayRangeGenerator):
261
+ """
262
+ Represent arrays as lists of lists!
263
+
264
+ TODO: Move all validation of values (eg. anywhere we raise a ValueError) to the ArrayExpression
265
+ dataclass and out of the generator class
266
+ """
267
+
268
+ REPR = ArrayRepresentation.LIST
269
+
270
+ @staticmethod
271
+ def _list_of_lists(dimensions: int, dtype: str) -> str:
272
+ return ("List[" * dimensions) + dtype + ("]" * dimensions)
273
+
274
+ @staticmethod
275
+ def _parameterized_dimension(dimension: DimensionExpression, dtype: str) -> SlotResult:
276
+ # TODO: Preserve label representation in some readable way! doing the MVP now of using conlist
277
+ if dimension.exact_cardinality and (dimension.minimum_cardinality or dimension.maximum_cardinality):
278
+ raise ValueError("Can only specify EITHER exact_cardinality OR minimum/maximum cardinality")
279
+ elif dimension.exact_cardinality:
280
+ dmin = dimension.exact_cardinality
281
+ dmax = dimension.exact_cardinality
282
+ elif dimension.minimum_cardinality or dimension.maximum_cardinality:
283
+ dmin = dimension.minimum_cardinality
284
+ dmax = dimension.maximum_cardinality
285
+ else:
286
+ # TODO: handle labels for labeled but unshaped arrays
287
+ return SlotResult(annotation="List[" + dtype + "]")
288
+
289
+ items = []
290
+ if int(PYDANTIC_VERSION[0]) >= 2:
291
+ if dmin is not None:
292
+ items.append(f"min_length={dmin}")
293
+ if dmax is not None:
294
+ items.append(f"max_length={dmax}")
295
+ else:
296
+ if dmin is not None:
297
+ items.append(f"min_items={dmin}")
298
+ if dmax is not None:
299
+ items.append(f"max_items={dmax}")
300
+ items.append(f"item_type={dtype}")
301
+ items = ", ".join(items)
302
+ annotation = f"conlist({items})"
303
+
304
+ return SlotResult(annotation=annotation, imports=_ConListImports)
305
+
306
+ def any_shape(self, array: Optional[ArrayExpression] = None, with_inner_union: bool = False) -> SlotResult:
307
+ """
308
+ An AnyShaped array (using :class:`.AnyShapeArray` )
309
+
310
+ Args:
311
+ array (:class:`.ArrayExpression`): The array expression (not used)
312
+ with_inner_union (bool): If ``True`` , the innermost type is a ``Union`` of the ``AnyShapeArray`` class
313
+ and ``dtype`` (default: ``False`` )
314
+
315
+ """
316
+ if self.dtype in ("Any", "AnyType"):
317
+ annotation = "AnyShapeArray"
318
+ else:
319
+ annotation = f"AnyShapeArray[{self.dtype}]"
320
+
321
+ if with_inner_union:
322
+ annotation = f"Union[{annotation}, {self.dtype}]"
323
+ return SlotResult(annotation=annotation, injected_classes=_AnyShapeArrayInjects, imports=_AnyShapeArrayImports)
324
+
325
+ def bounded_dimensions(self, array: ArrayExpression) -> SlotResult:
326
+ """
327
+ A nested series of ``List[]`` annotations with :attr:`.dtype` at the center.
328
+
329
+ When an array expression allows for a range of dimensions, each set of ``List`` s is joined by a ``Union`` .
330
+ """
331
+ if array.exact_number_dimensions or (
332
+ array.minimum_number_dimensions
333
+ and array.maximum_number_dimensions
334
+ and array.minimum_number_dimensions == array.maximum_number_dimensions
335
+ ):
336
+ exact_dims = array.exact_number_dimensions or array.minimum_number_dimensions
337
+ return SlotResult(annotation=self._list_of_lists(exact_dims, self.dtype))
338
+ elif not array.maximum_number_dimensions and (
339
+ array.minimum_number_dimensions is None or array.minimum_number_dimensions == 1
340
+ ):
341
+ return self.any_shape()
342
+ elif array.maximum_number_dimensions:
343
+ # e.g., if min = 2, max = 3, annotation = Union[List[List[dtype]], List[List[List[dtype]]]]
344
+ min_dims = array.minimum_number_dimensions if array.minimum_number_dimensions is not None else 1
345
+ annotations = [
346
+ self._list_of_lists(i, self.dtype) for i in range(min_dims, array.maximum_number_dimensions + 1)
347
+ ]
348
+ # TODO: Format this nicely!
349
+ return SlotResult(annotation="Union[" + ", ".join(annotations) + "]")
350
+ else:
351
+ # min specified with no max
352
+ # e.g., if min = 3, annotation = List[List[AnyShapeArray[dtype]]]
353
+ return SlotResult(
354
+ annotation=self._list_of_lists(array.minimum_number_dimensions - 1, self.any_shape().annotation),
355
+ injected_classes=_AnyShapeArrayInjects,
356
+ imports=_AnyShapeArrayImports,
357
+ )
358
+
359
+ def parameterized_dimensions(self, array: ArrayExpression) -> SlotResult:
360
+ """
361
+ Constrained shapes using :func:`pydantic.conlist`
362
+
363
+ TODO:
364
+ - preservation of aliases
365
+ - (what other metadata is allowable on labeled dimensions?)
366
+ """
367
+ # generate dimensions from inside out and then format
368
+ # e.g., if dimensions = [{min_card: 3}, {min_card: 2}],
369
+ # annotation = conlist(min_length=3, item_type=conlist(min_length=2, item_type=dtype))
370
+ range = self.dtype
371
+ for dimension in reversed(array.dimensions):
372
+ range = self._parameterized_dimension(dimension, range).annotation
373
+
374
+ return SlotResult(annotation=range, imports=_ConListImports)
375
+
376
+ def complex_dimensions(self, array: ArrayExpression) -> SlotResult:
377
+ """
378
+ Mixture of parameterized dimensions with a max or min (or both) shape for anonymous dimensions.
379
+
380
+ A mixture of ``List`` , :class:`.conlist` , and :class:`.AnyShapeArray` .
381
+ """
382
+ # first process any unlabeled dimensions which must be the innermost level of the annotation,
383
+ # then wrap that with labeled dimensions
384
+ if array.exact_number_dimensions or (
385
+ array.minimum_number_dimensions
386
+ and array.maximum_number_dimensions
387
+ and array.minimum_number_dimensions == array.maximum_number_dimensions
388
+ ):
389
+ exact_dims = array.exact_number_dimensions or array.minimum_number_dimensions
390
+ if exact_dims > len(array.dimensions):
391
+ res = SlotResult(annotation=self._list_of_lists(exact_dims - len(array.dimensions), self.dtype))
392
+ elif exact_dims == len(array.dimensions):
393
+ # equivalent to labeled shape
394
+ return self.parameterized_dimensions(array)
395
+ else:
396
+ raise ValueError(
397
+ "if exact_number_dimensions is provided, it must be greater than the parameterized dimensions"
398
+ )
399
+
400
+ elif array.maximum_number_dimensions is not None and not array.maximum_number_dimensions:
401
+ # unlimited n dimensions, so innermost is AnyShape with dtype
402
+ res = self.any_shape(with_inner_union=True)
403
+
404
+ if array.minimum_number_dimensions and array.minimum_number_dimensions > len(array.dimensions):
405
+ # some minimum anonymous dimensions but unlimited max dimensions
406
+ # e.g., if min = 3, len(dim) = 2, then res.annotation = List[Union[AnyShapeArray[dtype], dtype]]
407
+ # res.annotation will be wrapped with the 2 labeled dimensions later
408
+ res.annotation = self._list_of_lists(
409
+ array.minimum_number_dimensions - len(array.dimensions), res.annotation
410
+ )
411
+
412
+ elif array.minimum_number_dimensions and array.maximum_number_dimensions is None:
413
+ raise ValueError(
414
+ (
415
+ "Cannot specify a minimum_number_dimensions while maximum is None while using labeled dimensions - "
416
+ "either use exact_number_dimensions > len(dimensions) for extra parameterized dimensions or set "
417
+ "maximum_number_dimensions explicitly to False for unbounded dimensions"
418
+ )
419
+ )
420
+ elif array.maximum_number_dimensions:
421
+ initial_min = array.minimum_number_dimensions if array.minimum_number_dimensions is not None else 0
422
+ dmin = max(len(array.dimensions), initial_min) - len(array.dimensions)
423
+ dmax = array.maximum_number_dimensions - len(array.dimensions)
424
+
425
+ res = self.bounded_dimensions(
426
+ ArrayExpression(minimum_number_dimensions=dmin, maximum_number_dimensions=dmax)
427
+ )
428
+ else:
429
+ raise ValueError("Unsupported array specification! this is almost certainly a bug!") # pragma: no cover
430
+
431
+ # Wrap inner dimension with labeled dimension
432
+ # e.g., if dimensions = [{min_card: 3}, {min_card: 2}]
433
+ # and res.annotation = List[Union[AnyShapeArray[dtype], dtype]]
434
+ # (min 3 dims, no max dims)
435
+ # then the final annotation = conlist(
436
+ # min_length=3,
437
+ # item_type=conlist(
438
+ # min_length=2,
439
+ # item_type=List[Union[AnyShapeArray[dtype], dtype]]
440
+ # )
441
+ # )
442
+ for dim in reversed(array.dimensions):
443
+ res = res.merge(self._parameterized_dimension(dim, dtype=res.annotation))
444
+
445
+ return res
446
+
447
+
448
+ class NPTypingArray(ArrayRangeGenerator):
449
+ """
450
+ Represent array range with nptyping, and serialization/loading with an ArrayProxy
451
+ """
452
+
453
+ REPR = ArrayRepresentation.NPARRAY
454
+
455
+ def __init__(self, **kwargs):
456
+ super(self).__init__(**kwargs)
457
+ raise NotImplementedError("NPTyping array ranges are not implemented yet :(")
@@ -0,0 +1,29 @@
1
+ """
2
+ Utilities for formatting pydanticgen outputs
3
+
4
+ Kept separate in case we decide we don't want it/we want it to be moved to a place
5
+ that's better for other generators to use.
6
+ """
7
+
8
+ from typing import Optional
9
+
10
+ try:
11
+ from black import Mode, TargetVersion, format_str
12
+ except ImportError:
13
+ import warnings
14
+
15
+ warnings.warn("Black is an optional dependency, install it with the extra 'black' like `pip install linkml[black]`")
16
+
17
+
18
+ def _default_mode() -> "Mode":
19
+ return Mode(
20
+ target_versions={TargetVersion.PY311},
21
+ )
22
+
23
+
24
+ def format_black(code: str, mode: Optional["Mode"] = None) -> str:
25
+ if mode is None:
26
+ mode = _default_mode()
27
+
28
+ formatted = format_str(code, mode=mode)
29
+ return formatted
@@ -0,0 +1,79 @@
1
+ from typing import List, Optional, Type, TypeVar, Union
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from linkml.generators.pydanticgen.template import Import, Imports
6
+
7
+ T = TypeVar("T", bound="BuildResult", covariant=True)
8
+
9
+
10
+ class BuildResult(BaseModel):
11
+ """
12
+ The result of any build phase for any linkML object
13
+
14
+ BuildResults are merged in the serialization process, and are used
15
+ to keep track of not only the particular representation
16
+ of the thing in question, but any "side effects" that need to happen
17
+ elsewhere in the generation process (like adding imports, injecting classes, etc.)
18
+ """
19
+
20
+ imports: Optional[Union[List[Import], Imports]] = None
21
+ injected_classes: Optional[List[Union[str, Type]]] = None
22
+
23
+ def merge(self, other: T) -> T:
24
+ """
25
+ Merge a build result with another.
26
+
27
+ - Merges imports with :meth:`.Imports.__add__`
28
+ - Extends (with simple deduplication) injected classes
29
+
30
+ .. note::
31
+
32
+ This returns a (shallow) copy of ``self``, so subclasses don't need to make additional copies.
33
+
34
+ Args:
35
+ other (:class:`.BuildResult`): A subclass of BuildResult, generic over whatever we have passed.
36
+
37
+ Returns:
38
+ :class:`.BuildResult`
39
+ """
40
+ self_copy = self.copy()
41
+ if other.imports:
42
+ if self.imports is not None:
43
+ self_copy.imports += other.imports
44
+ else:
45
+ self_copy.imports = other.imports
46
+ if other.injected_classes:
47
+ self_copy.injected_classes.extend(other.injected_classes)
48
+ self_copy.injected_classes = list(dict.fromkeys(self_copy.injected_classes))
49
+ return self_copy
50
+
51
+
52
+ class SlotResult(BuildResult):
53
+ annotation: str
54
+ """The type annotation used in the generated model"""
55
+ field_extras: Optional[dict] = None
56
+ """Additional metadata for this slot to be held in the Field object"""
57
+
58
+ def merge(self, other: "SlotResult") -> "SlotResult":
59
+ """
60
+ Merge two SlotResults...
61
+
62
+ - calling :meth:`.BuildResult.merge`
63
+ - replacing the existing annotation with that given by ``other`` .
64
+ - updating any ``field_extras`` with the other
65
+
66
+ Args:
67
+ other (:class:`.SlotResult`): The other slot result to merge!
68
+
69
+ Returns:
70
+ :class:`.SlotResult`
71
+ """
72
+ res = super(SlotResult, self).merge(other)
73
+ # Replace with other's annotation
74
+ res.annotation = other.annotation
75
+ if other.field_extras is not None:
76
+ if res.field_extras is None:
77
+ res.field_extras = {}
78
+ res.field_extras.update(other.field_extras)
79
+ return res