linkml 1.8.2__py3-none-any.whl → 1.8.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- linkml/cli/main.py +4 -0
- linkml/generators/__init__.py +2 -0
- linkml/generators/common/ifabsent_processor.py +286 -0
- linkml/generators/docgen/index.md.jinja2 +6 -6
- linkml/generators/docgen.py +64 -14
- linkml/generators/golanggen.py +3 -1
- linkml/generators/jsonldcontextgen.py +0 -1
- linkml/generators/jsonschemagen.py +4 -2
- linkml/generators/owlgen.py +36 -17
- linkml/generators/projectgen.py +13 -11
- linkml/generators/pydanticgen/array.py +340 -56
- linkml/generators/pydanticgen/build.py +4 -2
- linkml/generators/pydanticgen/pydanticgen.py +46 -24
- linkml/generators/pydanticgen/template.py +108 -3
- linkml/generators/pydanticgen/templates/imports.py.jinja +11 -3
- linkml/generators/pydanticgen/templates/module.py.jinja +1 -3
- linkml/generators/pydanticgen/templates/validator.py.jinja +2 -2
- linkml/generators/python/__init__.py +1 -0
- linkml/generators/python/python_ifabsent_processor.py +92 -0
- linkml/generators/pythongen.py +19 -31
- linkml/generators/shacl/__init__.py +1 -3
- linkml/generators/shacl/shacl_data_type.py +1 -1
- linkml/generators/shacl/shacl_ifabsent_processor.py +89 -0
- linkml/generators/shaclgen.py +39 -13
- 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.2.dist-info → linkml-1.8.4.dist-info}/METADATA +3 -1
- {linkml-1.8.2.dist-info → linkml-1.8.4.dist-info}/RECORD +48 -45
- linkml/generators/shacl/ifabsent_processor.py +0 -59
- linkml/utils/ifabsent_functions.py +0 -138
- {linkml-1.8.2.dist-info → linkml-1.8.4.dist-info}/LICENSE +0 -0
- {linkml-1.8.2.dist-info → linkml-1.8.4.dist-info}/WHEEL +0 -0
- {linkml-1.8.2.dist-info → linkml-1.8.4.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,7 @@
|
|
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, Optional, Type, TypeVar, Union, get_args
|
4
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Generic, Iterable, List, Optional, Type, TypeVar, Union, get_args
|
5
5
|
|
6
6
|
from linkml_runtime.linkml_model import Element
|
7
7
|
from linkml_runtime.linkml_model.meta import ArrayExpression, DimensionExpression
|
@@ -26,11 +26,12 @@ else:
|
|
26
26
|
|
27
27
|
from linkml.generators.pydanticgen.build import RangeResult
|
28
28
|
from linkml.generators.pydanticgen.template import ConditionalImport, Import, Imports, ObjectImport
|
29
|
+
from linkml.utils.exceptions import ValidationError
|
29
30
|
|
30
31
|
|
31
32
|
class ArrayRepresentation(Enum):
|
32
33
|
LIST = "list"
|
33
|
-
|
34
|
+
NUMPYDANTIC = "numpydantic" # numpydantic must be installed to use this
|
34
35
|
|
35
36
|
|
36
37
|
_BOUNDED_ARRAY_FIELDS = ("exact_number_dimensions", "minimum_number_dimensions", "maximum_number_dimensions")
|
@@ -44,17 +45,18 @@ class AnyShapeArrayType(Generic[_T]):
|
|
44
45
|
def __get_pydantic_core_schema__(cls, source_type: Any, handler: "GetCoreSchemaHandler") -> "CoreSchema":
|
45
46
|
# double-nested parameterized types here
|
46
47
|
# source_type: List[Union[T,List[...]]]
|
47
|
-
item_type = Any if get_args(get_args(source_type)[0])[0] is _T else get_args(get_args(source_type)[0])[
|
48
|
+
item_type = (Any,) if get_args(get_args(source_type)[0])[0] is _T else get_args(get_args(source_type)[0])[:-1]
|
48
49
|
|
49
|
-
|
50
|
-
|
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]):
|
51
56
|
item_schema["strict"] = True
|
52
57
|
|
53
|
-
|
54
|
-
|
55
|
-
item_name = "Any"
|
56
|
-
else:
|
57
|
-
item_name = item_type.__name__
|
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])
|
58
60
|
|
59
61
|
array_ref = f"any-shape-array-{item_name}"
|
60
62
|
|
@@ -109,6 +111,124 @@ _AnyShapeArrayInjects = [
|
|
109
111
|
_ConListImports = Imports() + Import(module="pydantic", objects=[ObjectImport(name="conlist")])
|
110
112
|
|
111
113
|
|
114
|
+
class ArrayValidator:
|
115
|
+
"""
|
116
|
+
Validate the specification of a LinkML Array
|
117
|
+
|
118
|
+
.. todo::
|
119
|
+
|
120
|
+
It looks like :mod:`linkml.validator` is for validating instances against schema, rather
|
121
|
+
than validating the schema itself, so am not subclassing/writing as a plugin.
|
122
|
+
Unsure if there is a more general means of validating schema, but for now this is
|
123
|
+
an independent class
|
124
|
+
"""
|
125
|
+
|
126
|
+
@classmethod
|
127
|
+
def validate(cls, array: ArrayExpression):
|
128
|
+
"""
|
129
|
+
Validate an array expression.
|
130
|
+
|
131
|
+
Raises:
|
132
|
+
:class:`.ValidationError` if invalid
|
133
|
+
"""
|
134
|
+
cls.array_exact_dimensions(array)
|
135
|
+
cls.array_consistent_n_dimensions(array)
|
136
|
+
cls.array_explicitly_unbounded(array)
|
137
|
+
cls.array_dimensions_ordinal(array)
|
138
|
+
|
139
|
+
if array.dimensions:
|
140
|
+
for dimension in array.dimensions:
|
141
|
+
cls.validate_dimension(dimension)
|
142
|
+
|
143
|
+
@classmethod
|
144
|
+
def validate_dimension(cls, dimension: DimensionExpression):
|
145
|
+
"""
|
146
|
+
Validate a single array dimension
|
147
|
+
|
148
|
+
Raises:
|
149
|
+
:class:`.ValidationError` if invalid
|
150
|
+
"""
|
151
|
+
cls.dimension_exact_cardinality(dimension)
|
152
|
+
cls.dimension_ordinal(dimension)
|
153
|
+
|
154
|
+
@staticmethod
|
155
|
+
def array_exact_dimensions(array: ArrayExpression):
|
156
|
+
"""Arrays can have exact_number_dimensions OR min/max_number_dimensions, but not both"""
|
157
|
+
if array.exact_number_dimensions is not None and (
|
158
|
+
array.minimum_number_dimensions is not None or array.maximum_number_dimensions is not None
|
159
|
+
):
|
160
|
+
raise ValidationError(
|
161
|
+
f"Can only specify EITHER exact_number_dimensions OR minimum/maximum dimensions, got: {array}"
|
162
|
+
)
|
163
|
+
|
164
|
+
@staticmethod
|
165
|
+
def array_consistent_n_dimensions(array: ArrayExpression):
|
166
|
+
"""
|
167
|
+
Complex arrays with both exact/min/max_number_dimensions and parameterized dimensions
|
168
|
+
need to have the exact/min/max_number_dimensions greater than the number of parameterized dimensions!
|
169
|
+
"""
|
170
|
+
if not array.dimensions:
|
171
|
+
return
|
172
|
+
|
173
|
+
for field_name in _BOUNDED_ARRAY_FIELDS:
|
174
|
+
field = getattr(array, field_name, None)
|
175
|
+
if field and field < len(array.dimensions):
|
176
|
+
raise ValidationError(
|
177
|
+
"if exact/minimum/maximum_number_dimensions is provided, "
|
178
|
+
"it must be greater than the parameterized dimensions. "
|
179
|
+
f"got\n- {field_name}: {field}\n- dimensions: {array.dimensions}"
|
180
|
+
)
|
181
|
+
|
182
|
+
@staticmethod
|
183
|
+
def array_dimensions_ordinal(array: ArrayExpression):
|
184
|
+
"""
|
185
|
+
minimum_number_dimensions needs to be less than maximum_number_dimensions when both are set
|
186
|
+
"""
|
187
|
+
if array.minimum_number_dimensions is not None and array.maximum_number_dimensions:
|
188
|
+
if array.minimum_number_dimensions > array.maximum_number_dimensions:
|
189
|
+
raise ValidationError(
|
190
|
+
"minimum_number_dimensions must be lesser than maximum_number_dimensions when both are set. "
|
191
|
+
f"got minimum: {array.minimum_number_dimensions}, maximum: {array.maximum_number_dimensions}"
|
192
|
+
)
|
193
|
+
|
194
|
+
@staticmethod
|
195
|
+
def array_explicitly_unbounded(array: ArrayExpression):
|
196
|
+
"""
|
197
|
+
Complex arrays with a minimum_number_dimensions and parameterized dimensions
|
198
|
+
need to either use exact_number_dimensions to specify extra anonymous dimensions
|
199
|
+
or set maximum_number_dimensions to ``False`` to specify unbounded extra anonymous
|
200
|
+
dimensions to avoid ambiguity.
|
201
|
+
"""
|
202
|
+
if array.minimum_number_dimensions is not None and array.maximum_number_dimensions is None and array.dimensions:
|
203
|
+
raise ValidationError(
|
204
|
+
(
|
205
|
+
"Cannot specify a minimum_number_dimensions while maximum is None while using labeled dimensions - "
|
206
|
+
"either use exact_number_dimensions > len(dimensions) for extra parameterized dimensions or set "
|
207
|
+
"maximum_number_dimensions explicitly to False for unbounded dimensions"
|
208
|
+
)
|
209
|
+
)
|
210
|
+
|
211
|
+
@staticmethod
|
212
|
+
def dimension_exact_cardinality(dimension: DimensionExpression):
|
213
|
+
"""Dimensions can only have exact_cardinality OR min/max_cardinality, but not both"""
|
214
|
+
if dimension.exact_cardinality is not None and (
|
215
|
+
dimension.minimum_cardinality is not None or dimension.maximum_cardinality is not None
|
216
|
+
):
|
217
|
+
raise ValidationError(
|
218
|
+
f"Can only specify EITHER exact_cardinality OR minimum/maximum cardinality, got: {dimension}"
|
219
|
+
)
|
220
|
+
|
221
|
+
@staticmethod
|
222
|
+
def dimension_ordinal(dimension: DimensionExpression):
|
223
|
+
"""minimum_cardinality must be less than maximum_cardinality when both are set"""
|
224
|
+
if dimension.minimum_cardinality is not None and dimension.maximum_cardinality is not None:
|
225
|
+
if dimension.minimum_cardinality > dimension.maximum_cardinality:
|
226
|
+
raise ValidationError(
|
227
|
+
"minimum_cardinality must be lesser than maximum_cardinality when both are set. "
|
228
|
+
f"got minimum: {dimension.minimum_cardinality}, maximum: {dimension.maximum_cardinality}"
|
229
|
+
)
|
230
|
+
|
231
|
+
|
112
232
|
class ArrayRangeGenerator(ABC):
|
113
233
|
"""
|
114
234
|
Metaclass for generating a given format of array range.
|
@@ -142,16 +262,33 @@ class ArrayRangeGenerator(ABC):
|
|
142
262
|
self.dtype = dtype
|
143
263
|
|
144
264
|
def make(self) -> RangeResult:
|
145
|
-
"""
|
265
|
+
"""
|
266
|
+
Create the string form of the array representation, validating first
|
267
|
+
"""
|
268
|
+
self.validate()
|
269
|
+
|
146
270
|
if not self.array.dimensions and not self.has_bounded_dimensions:
|
147
|
-
|
148
|
-
return self.any_shape(self.array)
|
271
|
+
return self._any_shape(self.array)
|
149
272
|
elif not self.array.dimensions and self.has_bounded_dimensions:
|
150
|
-
return self.
|
273
|
+
return self._bounded_dimensions(self.array)
|
151
274
|
elif self.array.dimensions and not self.has_bounded_dimensions:
|
152
|
-
return self.
|
275
|
+
return self._parameterized_dimensions(self.array)
|
153
276
|
else:
|
154
|
-
return self.
|
277
|
+
return self._complex_dimensions(self.array)
|
278
|
+
|
279
|
+
def validate(self):
|
280
|
+
"""
|
281
|
+
Ensure that the given ArrayExpression is valid using :class:`.ArrayValidator`
|
282
|
+
|
283
|
+
.. todo::
|
284
|
+
|
285
|
+
Integrate with more general schema validation that happens when a schema is loaded,
|
286
|
+
rather than when an array is generated
|
287
|
+
|
288
|
+
Raises:
|
289
|
+
:class:`.ValidationError` if the schema is invalid
|
290
|
+
"""
|
291
|
+
ArrayValidator.validate(self.array)
|
155
292
|
|
156
293
|
@property
|
157
294
|
def has_bounded_dimensions(self) -> bool:
|
@@ -167,22 +304,22 @@ class ArrayRangeGenerator(ABC):
|
|
167
304
|
raise ValueError(f"Generator for array representation {repr} not found!")
|
168
305
|
|
169
306
|
@abstractmethod
|
170
|
-
def
|
307
|
+
def _any_shape(self, array: Optional[ArrayRepresentation] = None) -> RangeResult:
|
171
308
|
"""Any shaped array!"""
|
172
309
|
pass
|
173
310
|
|
174
311
|
@abstractmethod
|
175
|
-
def
|
312
|
+
def _bounded_dimensions(self, array: ArrayExpression) -> RangeResult:
|
176
313
|
"""Array shape specified numerically, without axis parameterization"""
|
177
314
|
pass
|
178
315
|
|
179
316
|
@abstractmethod
|
180
|
-
def
|
317
|
+
def _parameterized_dimensions(self, array: ArrayExpression) -> RangeResult:
|
181
318
|
"""Array shape specified with ``dimensions`` without additional parameterized dimensions"""
|
182
319
|
pass
|
183
320
|
|
184
321
|
@abstractmethod
|
185
|
-
def
|
322
|
+
def _complex_dimensions(self, array: ArrayExpression) -> RangeResult:
|
186
323
|
"""Array shape with both ``parameterized`` and ``bounded`` dimensions"""
|
187
324
|
pass
|
188
325
|
|
@@ -190,9 +327,6 @@ class ArrayRangeGenerator(ABC):
|
|
190
327
|
class ListOfListsArray(ArrayRangeGenerator):
|
191
328
|
"""
|
192
329
|
Represent arrays as lists of lists!
|
193
|
-
|
194
|
-
TODO: Move all validation of values (eg. anywhere we raise a ValueError) to the ArrayExpression
|
195
|
-
dataclass and out of the generator class
|
196
330
|
"""
|
197
331
|
|
198
332
|
REPR = ArrayRepresentation.LIST
|
@@ -204,9 +338,7 @@ class ListOfListsArray(ArrayRangeGenerator):
|
|
204
338
|
@staticmethod
|
205
339
|
def _parameterized_dimension(dimension: DimensionExpression, dtype: str) -> RangeResult:
|
206
340
|
# TODO: Preserve label representation in some readable way! doing the MVP now of using conlist
|
207
|
-
if dimension.exact_cardinality
|
208
|
-
raise ValueError("Can only specify EITHER exact_cardinality OR minimum/maximum cardinality")
|
209
|
-
elif dimension.exact_cardinality:
|
341
|
+
if dimension.exact_cardinality:
|
210
342
|
dmin = dimension.exact_cardinality
|
211
343
|
dmax = dimension.exact_cardinality
|
212
344
|
elif dimension.minimum_cardinality or dimension.maximum_cardinality:
|
@@ -228,7 +360,7 @@ class ListOfListsArray(ArrayRangeGenerator):
|
|
228
360
|
|
229
361
|
return RangeResult(range=range, imports=_ConListImports)
|
230
362
|
|
231
|
-
def
|
363
|
+
def _any_shape(self, array: Optional[ArrayExpression] = None, with_inner_union: bool = False) -> RangeResult:
|
232
364
|
"""
|
233
365
|
An AnyShaped array (using :class:`.AnyShapeArray` )
|
234
366
|
|
@@ -247,7 +379,7 @@ class ListOfListsArray(ArrayRangeGenerator):
|
|
247
379
|
range = f"Union[{range}, {self.dtype}]"
|
248
380
|
return RangeResult(range=range, injected_classes=_AnyShapeArrayInjects, imports=_AnyShapeArrayImports)
|
249
381
|
|
250
|
-
def
|
382
|
+
def _bounded_dimensions(self, array: ArrayExpression) -> RangeResult:
|
251
383
|
"""
|
252
384
|
A nested series of ``List[]`` ranges with :attr:`.dtype` at the center.
|
253
385
|
|
@@ -263,23 +395,22 @@ class ListOfListsArray(ArrayRangeGenerator):
|
|
263
395
|
elif not array.maximum_number_dimensions and (
|
264
396
|
array.minimum_number_dimensions is None or array.minimum_number_dimensions == 1
|
265
397
|
):
|
266
|
-
return self.
|
398
|
+
return self._any_shape()
|
267
399
|
elif array.maximum_number_dimensions:
|
268
400
|
# e.g., if min = 2, max = 3, range = Union[List[List[dtype]], List[List[List[dtype]]]]
|
269
401
|
min_dims = array.minimum_number_dimensions if array.minimum_number_dimensions is not None else 1
|
270
402
|
ranges = [self._list_of_lists(i, self.dtype) for i in range(min_dims, array.maximum_number_dimensions + 1)]
|
271
|
-
# TODO: Format this nicely!
|
272
403
|
return RangeResult(range="Union[" + ", ".join(ranges) + "]")
|
273
404
|
else:
|
274
405
|
# min specified with no max
|
275
406
|
# e.g., if min = 3, range = List[List[AnyShapeArray[dtype]]]
|
276
407
|
return RangeResult(
|
277
|
-
range=self._list_of_lists(array.minimum_number_dimensions - 1, self.
|
408
|
+
range=self._list_of_lists(array.minimum_number_dimensions - 1, self._any_shape().range),
|
278
409
|
injected_classes=_AnyShapeArrayInjects,
|
279
410
|
imports=_AnyShapeArrayImports,
|
280
411
|
)
|
281
412
|
|
282
|
-
def
|
413
|
+
def _parameterized_dimensions(self, array: ArrayExpression) -> RangeResult:
|
283
414
|
"""
|
284
415
|
Constrained shapes using :func:`pydantic.conlist`
|
285
416
|
|
@@ -296,12 +427,13 @@ class ListOfListsArray(ArrayRangeGenerator):
|
|
296
427
|
|
297
428
|
return RangeResult(range=range, imports=_ConListImports)
|
298
429
|
|
299
|
-
def
|
430
|
+
def _complex_dimensions(self, array: ArrayExpression) -> RangeResult:
|
300
431
|
"""
|
301
432
|
Mixture of parameterized dimensions with a max or min (or both) shape for anonymous dimensions.
|
302
433
|
|
303
434
|
A mixture of ``List`` , :class:`.conlist` , and :class:`.AnyShapeArray` .
|
304
435
|
"""
|
436
|
+
res = None
|
305
437
|
# first process any unlabeled dimensions which must be the innermost level of the range,
|
306
438
|
# then wrap that with labeled dimensions
|
307
439
|
if array.exact_number_dimensions or (
|
@@ -314,39 +446,29 @@ class ListOfListsArray(ArrayRangeGenerator):
|
|
314
446
|
res = RangeResult(range=self._list_of_lists(exact_dims - len(array.dimensions), self.dtype))
|
315
447
|
elif exact_dims == len(array.dimensions):
|
316
448
|
# equivalent to labeled shape
|
317
|
-
return self.
|
318
|
-
else:
|
319
|
-
raise ValueError(
|
320
|
-
"if exact_number_dimensions is provided, it must be greater than the parameterized dimensions"
|
321
|
-
)
|
449
|
+
return self._parameterized_dimensions(array)
|
450
|
+
# else is invalid, see: ArrayValidator.array_consistent_n_dimensions
|
322
451
|
|
323
452
|
elif array.maximum_number_dimensions is not None and not array.maximum_number_dimensions:
|
324
453
|
# unlimited n dimensions, so innermost is AnyShape with dtype
|
325
|
-
res = self.
|
454
|
+
res = self._any_shape(with_inner_union=True)
|
326
455
|
|
327
|
-
if array.minimum_number_dimensions
|
456
|
+
if array.minimum_number_dimensions:
|
328
457
|
# some minimum anonymous dimensions but unlimited max dimensions
|
329
458
|
# e.g., if min = 3, len(dim) = 2, then res.range = List[Union[AnyShapeArray[dtype], dtype]]
|
330
459
|
# res.range will be wrapped with the 2 labeled dimensions later
|
331
460
|
res.range = self._list_of_lists(array.minimum_number_dimensions - len(array.dimensions), res.range)
|
332
461
|
|
333
|
-
elif array.minimum_number_dimensions and array.maximum_number_dimensions is None:
|
334
|
-
raise ValueError(
|
335
|
-
(
|
336
|
-
"Cannot specify a minimum_number_dimensions while maximum is None while using labeled dimensions - "
|
337
|
-
"either use exact_number_dimensions > len(dimensions) for extra parameterized dimensions or set "
|
338
|
-
"maximum_number_dimensions explicitly to False for unbounded dimensions"
|
339
|
-
)
|
340
|
-
)
|
341
462
|
elif array.maximum_number_dimensions:
|
342
463
|
initial_min = array.minimum_number_dimensions if array.minimum_number_dimensions is not None else 0
|
343
464
|
dmin = max(len(array.dimensions), initial_min) - len(array.dimensions)
|
344
465
|
dmax = array.maximum_number_dimensions - len(array.dimensions)
|
345
466
|
|
346
|
-
res = self.
|
467
|
+
res = self._bounded_dimensions(
|
347
468
|
ArrayExpression(minimum_number_dimensions=dmin, maximum_number_dimensions=dmax)
|
348
469
|
)
|
349
|
-
|
470
|
+
|
471
|
+
if res is None:
|
350
472
|
raise ValueError("Unsupported array specification! this is almost certainly a bug!") # pragma: no cover
|
351
473
|
|
352
474
|
# Wrap inner dimension with labeled dimension
|
@@ -366,13 +488,175 @@ class ListOfListsArray(ArrayRangeGenerator):
|
|
366
488
|
return res
|
367
489
|
|
368
490
|
|
369
|
-
class
|
491
|
+
class NumpydanticArray(ArrayRangeGenerator):
|
492
|
+
"""
|
493
|
+
Represent array range with :class:`numpydantic.NDArray` annotations,
|
494
|
+
allowing an abstract array specification to be used with many different array
|
495
|
+
libraries.
|
496
|
+
"""
|
497
|
+
|
498
|
+
REPR = ArrayRepresentation.NUMPYDANTIC
|
499
|
+
MIN_NUMPYDANTIC_VERSION = "1.6.1"
|
370
500
|
"""
|
371
|
-
|
501
|
+
Minimum numpydantic version needed to be installed in the environment using
|
502
|
+
the generated models
|
372
503
|
"""
|
504
|
+
IMPORTS = Imports() + Import(
|
505
|
+
module="numpydantic", objects=[ObjectImport(name="NDArray"), ObjectImport(name="Shape")]
|
506
|
+
)
|
507
|
+
INJECTS = [f'MIN_NUMPYDANTIC_VERSION = "{MIN_NUMPYDANTIC_VERSION}"']
|
508
|
+
|
509
|
+
def make(self) -> RangeResult:
|
510
|
+
result = super().make()
|
511
|
+
result.imports = self.IMPORTS.model_copy()
|
512
|
+
result.injected_classes = self.INJECTS.copy()
|
513
|
+
return result
|
514
|
+
|
515
|
+
@staticmethod
|
516
|
+
def ndarray_annotation(shape: Optional[List[Union[int, str]]] = None, dtype: Optional[str] = None) -> str:
|
517
|
+
"""
|
518
|
+
Make a stringified :class:`numpydantic.NDArray` annotation for a given shape
|
519
|
+
and dtype.
|
520
|
+
|
521
|
+
If either ``shape`` or ``dtype`` is ``None`` , use ``Any``
|
522
|
+
"""
|
523
|
+
if shape is None:
|
524
|
+
shape = "Any"
|
525
|
+
else:
|
526
|
+
shape_expression = ", ".join([str(i) for i in shape])
|
527
|
+
shape = f'Shape["{shape_expression}"]'
|
528
|
+
|
529
|
+
if dtype is None or dtype in ("Any", "AnyType"):
|
530
|
+
dtype = "Any"
|
531
|
+
|
532
|
+
if shape == "Any" and dtype == "Any":
|
533
|
+
return "NDArray"
|
534
|
+
else:
|
535
|
+
return f"NDArray[{shape}, {dtype}]"
|
536
|
+
|
537
|
+
@staticmethod
|
538
|
+
def _dimension_shape(dimension: DimensionExpression) -> str:
|
539
|
+
if dimension.exact_cardinality:
|
540
|
+
shape = str(dimension.exact_cardinality)
|
541
|
+
elif dimension.minimum_cardinality and not dimension.maximum_cardinality:
|
542
|
+
shape = f"{dimension.minimum_cardinality}-*"
|
543
|
+
elif dimension.maximum_cardinality and not dimension.minimum_cardinality:
|
544
|
+
shape = f"*-{dimension.maximum_cardinality}"
|
545
|
+
elif dimension.minimum_cardinality and dimension.maximum_cardinality:
|
546
|
+
shape = f"{dimension.minimum_cardinality}-{dimension.maximum_cardinality}"
|
547
|
+
else:
|
548
|
+
shape = "*"
|
549
|
+
|
550
|
+
return shape
|
551
|
+
|
552
|
+
@classmethod
|
553
|
+
def _parameterized_dimension(cls, dimension: DimensionExpression) -> str:
|
554
|
+
shape = cls._dimension_shape(dimension)
|
555
|
+
if dimension.alias is not None:
|
556
|
+
return f"{shape} {dimension.alias}"
|
557
|
+
else:
|
558
|
+
return shape
|
559
|
+
|
560
|
+
def _any_shape(self, array: Optional[ArrayRepresentation] = None) -> RangeResult:
|
561
|
+
"""
|
562
|
+
Any shaped array, either an unparameterized :class:`numpydantic.NDArray`
|
563
|
+
if dtype is :class:`typing.Any` , or like ``NDArray[Any, {self.dtype}]``
|
564
|
+
otherwise.
|
565
|
+
"""
|
566
|
+
if self.dtype in ("Any", "AnyType"):
|
567
|
+
range = "NDArray"
|
568
|
+
else:
|
569
|
+
range = f"NDArray[Any, {self.dtype}]"
|
570
|
+
|
571
|
+
return RangeResult(range=range)
|
572
|
+
|
573
|
+
def _bounded_dimensions(self, array: ArrayExpression) -> RangeResult:
|
574
|
+
"""
|
575
|
+
Number of dimensions specified without shape
|
576
|
+
"""
|
577
|
+
if array.exact_number_dimensions or (
|
578
|
+
array.minimum_number_dimensions
|
579
|
+
and array.maximum_number_dimensions
|
580
|
+
and array.minimum_number_dimensions == array.maximum_number_dimensions
|
581
|
+
):
|
582
|
+
exact_dims = array.exact_number_dimensions or array.minimum_number_dimensions
|
583
|
+
|
584
|
+
return RangeResult(range=self.ndarray_annotation(["*"] * exact_dims, self.dtype))
|
585
|
+
elif not array.maximum_number_dimensions and (
|
586
|
+
array.minimum_number_dimensions is None or array.minimum_number_dimensions == 1
|
587
|
+
):
|
588
|
+
return self._any_shape()
|
589
|
+
elif array.maximum_number_dimensions:
|
590
|
+
# e.g., if min = 2, max = 3, range = Union[NDArray[Shape["*, *"], dtype], NDArray[Shape["*, *, *"], dtype]]
|
591
|
+
min_dims = array.minimum_number_dimensions if array.minimum_number_dimensions is not None else 1
|
592
|
+
ranges = [
|
593
|
+
self.ndarray_annotation(["*"] * i, self.dtype)
|
594
|
+
for i in range(min_dims, array.maximum_number_dimensions + 1)
|
595
|
+
]
|
596
|
+
return RangeResult(range="Union[" + ", ".join(ranges) + "]")
|
597
|
+
else:
|
598
|
+
# min specified with no max
|
599
|
+
# e.g., if min = 3, range = NDArray[Shape[*, *, *, ...], dtype]
|
600
|
+
shape_inner = ["*"] * array.minimum_number_dimensions
|
601
|
+
shape_inner.append("...")
|
602
|
+
return RangeResult(range=self.ndarray_annotation(shape_inner, self.dtype))
|
603
|
+
|
604
|
+
def _parameterized_dimensions(self, array: ArrayExpression) -> RangeResult:
|
605
|
+
"""
|
606
|
+
Arrays with constrained shapes or labels
|
607
|
+
"""
|
608
|
+
dims = [self._parameterized_dimension(d) for d in array.dimensions]
|
609
|
+
range = self.ndarray_annotation(dims, self.dtype)
|
610
|
+
return RangeResult(range=range)
|
611
|
+
|
612
|
+
def _complex_dimensions(self, array: ArrayExpression) -> RangeResult:
|
613
|
+
"""
|
614
|
+
Mixture of parameterized dimensions with a max or min (or both) shape for anonymous dimensions.
|
615
|
+
"""
|
616
|
+
dims = [self._parameterized_dimension(d) for d in array.dimensions]
|
617
|
+
res = None
|
618
|
+
|
619
|
+
if array.exact_number_dimensions or (
|
620
|
+
array.minimum_number_dimensions
|
621
|
+
and array.maximum_number_dimensions
|
622
|
+
and array.minimum_number_dimensions == array.maximum_number_dimensions
|
623
|
+
):
|
624
|
+
exact_dims = array.exact_number_dimensions or array.minimum_number_dimensions
|
625
|
+
if exact_dims > len(array.dimensions):
|
626
|
+
dims.extend(["*"] * (exact_dims - len(dims)))
|
627
|
+
res = self.ndarray_annotation(dims, self.dtype)
|
628
|
+
elif exact_dims == len(array.dimensions):
|
629
|
+
# equivalent to labeled shape
|
630
|
+
return self._parameterized_dimensions(array)
|
631
|
+
# else is invalid, see: ArrayValidator.array_consistent_n_dimensions(array)
|
632
|
+
|
633
|
+
elif array.maximum_number_dimensions is not None and not array.maximum_number_dimensions:
|
634
|
+
# unlimited n dimensions
|
635
|
+
|
636
|
+
if array.minimum_number_dimensions:
|
637
|
+
# some minimum anonymous dimensions but unlimited max dimensions
|
638
|
+
dims.extend(["*"] * (array.minimum_number_dimensions - len(dims)))
|
639
|
+
|
640
|
+
dims.append("...")
|
641
|
+
res = self.ndarray_annotation(dims, self.dtype)
|
642
|
+
|
643
|
+
elif array.maximum_number_dimensions:
|
644
|
+
# some res of anonymous dimensions
|
645
|
+
|
646
|
+
if array.minimum_number_dimensions:
|
647
|
+
min_dim = array.minimum_number_dimensions
|
648
|
+
else:
|
649
|
+
min_dim = len(dims)
|
650
|
+
|
651
|
+
dim_union = []
|
652
|
+
for i in range(min_dim, array.maximum_number_dimensions + 1):
|
653
|
+
this_dims = dims.copy()
|
654
|
+
this_dims.extend(["*"] * (i - len(dims)))
|
655
|
+
dim_union.append(self.ndarray_annotation(this_dims, self.dtype))
|
656
|
+
dim_union = ", ".join(dim_union)
|
657
|
+
res = f"Union[{dim_union}]"
|
373
658
|
|
374
|
-
|
659
|
+
if res is None:
|
660
|
+
raise ValueError(f"Unhandled range case! {array}")
|
375
661
|
|
376
|
-
|
377
|
-
super(self).__init__(**kwargs)
|
378
|
-
raise NotImplementedError("NPTyping array ranges are not implemented yet :(")
|
662
|
+
return RangeResult(range=res)
|
@@ -59,8 +59,10 @@ class PydanticBuildResult(BuildResult):
|
|
59
59
|
self_copy.imports = other.imports
|
60
60
|
if other.injected_classes:
|
61
61
|
if self_copy.injected_classes is not None:
|
62
|
-
|
63
|
-
self_copy.injected_classes
|
62
|
+
# only combine and dedupe when injected_classes don't match
|
63
|
+
if self_copy.injected_classes != other.injected_classes:
|
64
|
+
self_copy.injected_classes.extend(other.injected_classes)
|
65
|
+
self_copy.injected_classes = list(dict.fromkeys(self_copy.injected_classes))
|
64
66
|
else:
|
65
67
|
self_copy.injected_classes = other.injected_classes
|
66
68
|
return self_copy
|