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.
- linkml/generators/__init__.py +2 -0
- linkml/generators/docgen/class.md.jinja2 +15 -2
- linkml/generators/docgen/slot.md.jinja2 +18 -4
- linkml/generators/docgen.py +17 -3
- linkml/generators/jsonldcontextgen.py +40 -17
- linkml/generators/jsonldgen.py +3 -1
- linkml/generators/owlgen.py +16 -0
- linkml/generators/prefixmapgen.py +5 -4
- linkml/generators/projectgen.py +14 -2
- linkml/generators/pydanticgen/__init__.py +29 -0
- linkml/generators/pydanticgen/array.py +457 -0
- linkml/generators/pydanticgen/black.py +29 -0
- linkml/generators/pydanticgen/build.py +79 -0
- linkml/generators/{pydanticgen.py → pydanticgen/pydanticgen.py} +252 -304
- linkml/generators/pydanticgen/template.py +577 -0
- linkml/generators/pydanticgen/templates/attribute.py.jinja +10 -0
- linkml/generators/pydanticgen/templates/base_model.py.jinja +29 -0
- linkml/generators/pydanticgen/templates/class.py.jinja +21 -0
- linkml/generators/pydanticgen/templates/conditional_import.py.jinja +9 -0
- linkml/generators/pydanticgen/templates/enum.py.jinja +16 -0
- linkml/generators/pydanticgen/templates/footer.py.jinja +13 -0
- linkml/generators/pydanticgen/templates/imports.py.jinja +31 -0
- linkml/generators/pydanticgen/templates/module.py.jinja +27 -0
- linkml/generators/pydanticgen/templates/validator.py.jinja +15 -0
- linkml/generators/pythongen.py +13 -7
- linkml/generators/shacl/__init__.py +3 -0
- linkml/generators/shacl/ifabsent_processor.py +59 -0
- linkml/generators/shacl/shacl_data_type.py +40 -0
- linkml/generators/shaclgen.py +105 -82
- linkml/generators/shexgen.py +1 -1
- linkml/generators/sqlalchemygen.py +1 -1
- linkml/generators/sqltablegen.py +32 -22
- linkml/generators/terminusdbgen.py +7 -1
- linkml/linter/config/datamodel/config.py +8 -0
- linkml/linter/rules.py +11 -2
- linkml/utils/generator.py +7 -6
- linkml/utils/ifabsent_functions.py +7 -9
- linkml/utils/schemaloader.py +1 -9
- linkml/utils/sqlutils.py +39 -25
- {linkml-1.7.5.dist-info → linkml-1.7.7.dist-info}/METADATA +9 -4
- {linkml-1.7.5.dist-info → linkml-1.7.7.dist-info}/RECORD +44 -27
- {linkml-1.7.5.dist-info → linkml-1.7.7.dist-info}/LICENSE +0 -0
- {linkml-1.7.5.dist-info → linkml-1.7.7.dist-info}/WHEEL +0 -0
- {linkml-1.7.5.dist-info → linkml-1.7.7.dist-info}/entry_points.txt +0 -0
@@ -1,16 +1,24 @@
|
|
1
1
|
import inspect
|
2
2
|
import logging
|
3
3
|
import os
|
4
|
+
import textwrap
|
4
5
|
from collections import defaultdict
|
5
6
|
from copy import deepcopy
|
6
7
|
from dataclasses import dataclass, field
|
8
|
+
from pathlib import Path
|
7
9
|
from types import ModuleType
|
8
|
-
from typing import
|
10
|
+
from typing import (
|
11
|
+
Dict,
|
12
|
+
List,
|
13
|
+
Literal,
|
14
|
+
Optional,
|
15
|
+
Set,
|
16
|
+
Type,
|
17
|
+
Union,
|
18
|
+
)
|
9
19
|
|
10
20
|
import click
|
11
|
-
from jinja2 import
|
12
|
-
|
13
|
-
# from linkml.generators import pydantic_GEN_VERSION
|
21
|
+
from jinja2 import ChoiceLoader, Environment, FileSystemLoader
|
14
22
|
from linkml_runtime.linkml_model.meta import (
|
15
23
|
Annotation,
|
16
24
|
ClassDefinition,
|
@@ -29,255 +37,23 @@ from linkml.generators.common.type_designators import (
|
|
29
37
|
get_type_designator_value,
|
30
38
|
)
|
31
39
|
from linkml.generators.oocodegen import OOCodeGenerator
|
40
|
+
from linkml.generators.pydanticgen.array import ArrayRangeGenerator, ArrayRepresentation
|
41
|
+
from linkml.generators.pydanticgen.build import SlotResult
|
42
|
+
from linkml.generators.pydanticgen.template import (
|
43
|
+
ConditionalImport,
|
44
|
+
Import,
|
45
|
+
Imports,
|
46
|
+
ObjectImport,
|
47
|
+
PydanticAttribute,
|
48
|
+
PydanticBaseModel,
|
49
|
+
PydanticClass,
|
50
|
+
PydanticModule,
|
51
|
+
TemplateModel,
|
52
|
+
)
|
32
53
|
from linkml.utils.generator import shared_arguments
|
33
54
|
from linkml.utils.ifabsent_functions import ifabsent_value_declaration
|
34
55
|
|
35
56
|
|
36
|
-
def default_template(
|
37
|
-
pydantic_ver: str = "1", extra_fields: str = "forbid", injected_classes: Optional[List[Union[Type, str]]] = None
|
38
|
-
) -> str:
|
39
|
-
"""Constructs a default template for pydantic classes based on the version of pydantic"""
|
40
|
-
### HEADER ###
|
41
|
-
template = """
|
42
|
-
{#-
|
43
|
-
|
44
|
-
Jinja2 Template for a pydantic classes
|
45
|
-
-#}
|
46
|
-
from __future__ import annotations
|
47
|
-
from datetime import datetime, date
|
48
|
-
from enum import Enum
|
49
|
-
{% if uses_numpy -%}
|
50
|
-
import numpy as np
|
51
|
-
{%- endif %}
|
52
|
-
from decimal import Decimal
|
53
|
-
from typing import List, Dict, Optional, Any, Union"""
|
54
|
-
if pydantic_ver == "1":
|
55
|
-
template += """
|
56
|
-
from pydantic import BaseModel as BaseModel, Field, validator"""
|
57
|
-
elif pydantic_ver == "2":
|
58
|
-
template += """
|
59
|
-
from pydantic import BaseModel as BaseModel, ConfigDict, Field, field_validator"""
|
60
|
-
template += """
|
61
|
-
import re
|
62
|
-
import sys
|
63
|
-
if sys.version_info >= (3, 8):
|
64
|
-
from typing import Literal
|
65
|
-
else:
|
66
|
-
from typing_extensions import Literal
|
67
|
-
|
68
|
-
{% if imports is not none %}
|
69
|
-
{%- for import_module, import_classes in imports.items() -%}
|
70
|
-
{% if import_classes is none -%}
|
71
|
-
import {{ import_module }}
|
72
|
-
{% elif import_classes is mapping -%}
|
73
|
-
import {{ import_module }} as {{ import_classes['as'] }}
|
74
|
-
{% else -%}
|
75
|
-
from {{ import_module }} import (
|
76
|
-
{% for imported_class in import_classes %}
|
77
|
-
{%- if imported_class is string -%}
|
78
|
-
{{ imported_class }}
|
79
|
-
{%- else -%}
|
80
|
-
{{ imported_class['name'] }} as {{ imported_class['as'] }}
|
81
|
-
{%- endif -%}
|
82
|
-
{%- if not loop.last %},{{ '\n ' }}{% else %}{{ '\n' }}{%- endif -%}
|
83
|
-
{% endfor -%}
|
84
|
-
)
|
85
|
-
{% endif -%}
|
86
|
-
{% endfor -%}
|
87
|
-
{% endif %}
|
88
|
-
metamodel_version = "{{metamodel_version}}"
|
89
|
-
version = "{{version if version else None}}"
|
90
|
-
"""
|
91
|
-
### BASE MODEL ###
|
92
|
-
if pydantic_ver == "1":
|
93
|
-
template += f"""
|
94
|
-
class WeakRefShimBaseModel(BaseModel):
|
95
|
-
__slots__ = '__weakref__'
|
96
|
-
|
97
|
-
class ConfiguredBaseModel(WeakRefShimBaseModel,
|
98
|
-
validate_assignment = True,
|
99
|
-
validate_all = True,
|
100
|
-
underscore_attrs_are_private = True,
|
101
|
-
extra = '{extra_fields}',
|
102
|
-
arbitrary_types_allowed = True,
|
103
|
-
use_enum_values = True):
|
104
|
-
"""
|
105
|
-
else:
|
106
|
-
template += f"""
|
107
|
-
class ConfiguredBaseModel(BaseModel):
|
108
|
-
model_config = ConfigDict(
|
109
|
-
validate_assignment=True,
|
110
|
-
validate_default=True,
|
111
|
-
extra = '{extra_fields}',
|
112
|
-
arbitrary_types_allowed=True,
|
113
|
-
use_enum_values = True)
|
114
|
-
"""
|
115
|
-
|
116
|
-
### Fields injected into base model
|
117
|
-
template += """{% if injected_fields is not none %}
|
118
|
-
{% for field in injected_fields -%}
|
119
|
-
{{ field }}
|
120
|
-
{% endfor %}
|
121
|
-
{% else %}
|
122
|
-
pass
|
123
|
-
{% endif %}
|
124
|
-
"""
|
125
|
-
|
126
|
-
### Extra classes
|
127
|
-
if injected_classes is not None and len(injected_classes) > 0:
|
128
|
-
template += """{{ '\n\n' }}"""
|
129
|
-
for cls in injected_classes:
|
130
|
-
if isinstance(cls, str):
|
131
|
-
template += cls + "\n\n"
|
132
|
-
else:
|
133
|
-
template += inspect.getsource(cls) + "\n\n"
|
134
|
-
|
135
|
-
### ENUMS ###
|
136
|
-
template += """
|
137
|
-
{% for e in enums.values() %}
|
138
|
-
class {{ e.name }}(str{% if e['values'] %}, Enum{% endif %}):
|
139
|
-
{% if e.description -%}
|
140
|
-
\"\"\"
|
141
|
-
{{ e.description }}
|
142
|
-
\"\"\"
|
143
|
-
{%- endif %}
|
144
|
-
{% for _, pv in e['values'].items() -%}
|
145
|
-
{% if pv.description -%}
|
146
|
-
# {{pv.description}}
|
147
|
-
{%- endif %}
|
148
|
-
{{pv.label}} = "{{pv.value}}"
|
149
|
-
{% endfor %}
|
150
|
-
{% if not e['values'] -%}
|
151
|
-
dummy = "dummy"
|
152
|
-
{% endif %}
|
153
|
-
{% endfor %}
|
154
|
-
"""
|
155
|
-
### CLASSES ###
|
156
|
-
if pydantic_ver == "1":
|
157
|
-
template += """
|
158
|
-
{%- for c in schema.classes.values() %}
|
159
|
-
class {{ c.name }}
|
160
|
-
{%- if class_isa_plus_mixins[c.name] -%}
|
161
|
-
({{class_isa_plus_mixins[c.name]|join(', ')}})
|
162
|
-
{%- else -%}
|
163
|
-
(ConfiguredBaseModel)
|
164
|
-
{%- endif -%}
|
165
|
-
:
|
166
|
-
{% if c.description -%}
|
167
|
-
\"\"\"
|
168
|
-
{{ c.description }}
|
169
|
-
\"\"\"
|
170
|
-
{%- endif %}
|
171
|
-
{% for attr in c.attributes.values() if c.attributes -%}
|
172
|
-
{{attr.name}}: {{ attr.annotations['python_range'].value }} = Field(
|
173
|
-
{%- if predefined_slot_values[c.name][attr.name] is not callable -%}
|
174
|
-
{{ predefined_slot_values[c.name][attr.name] }}
|
175
|
-
{%- elif (attr.required or attr.identifier or attr.key) -%}
|
176
|
-
...
|
177
|
-
{%- else -%}
|
178
|
-
None
|
179
|
-
{%- endif -%}
|
180
|
-
{%- if attr.title != None %}, title="{{attr.title}}"{% endif -%}
|
181
|
-
{%- if attr.description %}, description=\"\"\"{{attr.description}}\"\"\"{% endif -%}
|
182
|
-
{%- if attr.equals_number != None %}, le={{attr.equals_number}}, ge={{attr.equals_number}}
|
183
|
-
{%- else -%}
|
184
|
-
{%- if attr.minimum_value != None %}, ge={{attr.minimum_value}}{% endif -%}
|
185
|
-
{%- if attr.maximum_value != None %}, le={{attr.maximum_value}}{% endif -%}
|
186
|
-
{%- endif -%}
|
187
|
-
)
|
188
|
-
{% else -%}
|
189
|
-
None
|
190
|
-
{% endfor %}
|
191
|
-
{% for attr in c.attributes.values() if c.attributes -%}
|
192
|
-
{%- if attr.pattern %}
|
193
|
-
@validator('{{attr.name}}', allow_reuse=True)
|
194
|
-
def pattern_{{attr.name}}(cls, v):
|
195
|
-
pattern=re.compile(r"{{attr.pattern}}")
|
196
|
-
if isinstance(v,list):
|
197
|
-
for element in v:
|
198
|
-
if not pattern.match(element):
|
199
|
-
raise ValueError(f"Invalid {{attr.name}} format: {element}")
|
200
|
-
elif isinstance(v,str):
|
201
|
-
if not pattern.match(v):
|
202
|
-
raise ValueError(f"Invalid {{attr.name}} format: {v}")
|
203
|
-
return v
|
204
|
-
{% endif -%}
|
205
|
-
{% endfor %}
|
206
|
-
{% endfor %}
|
207
|
-
"""
|
208
|
-
elif pydantic_ver == "2":
|
209
|
-
template += """
|
210
|
-
{%- for c in schema.classes.values() %}
|
211
|
-
class {{ c.name }}
|
212
|
-
{%- if class_isa_plus_mixins[c.name] -%}
|
213
|
-
({{class_isa_plus_mixins[c.name]|join(', ')}})
|
214
|
-
{%- else -%}
|
215
|
-
(ConfiguredBaseModel)
|
216
|
-
{%- endif -%}
|
217
|
-
:
|
218
|
-
{% if c.description -%}
|
219
|
-
\"\"\"
|
220
|
-
{{ c.description }}
|
221
|
-
\"\"\"
|
222
|
-
{%- endif %}
|
223
|
-
{% for attr in c.attributes.values() if c.attributes -%}
|
224
|
-
{{attr.name}}: {{ attr.annotations['python_range'].value }} = Field(
|
225
|
-
{%- if predefined_slot_values[c.name][attr.name] is not callable -%}
|
226
|
-
{{ predefined_slot_values[c.name][attr.name] }}
|
227
|
-
{%- elif (attr.required or attr.identifier or attr.key) -%}
|
228
|
-
...
|
229
|
-
{%- else -%}
|
230
|
-
None
|
231
|
-
{%- endif -%}
|
232
|
-
{%- if attr.title != None %}, title="{{attr.title}}"{% endif -%}
|
233
|
-
{%- if attr.description %}, description=\"\"\"{{attr.description}}\"\"\"{% endif -%}
|
234
|
-
{%- if attr.equals_number != None %}, le={{attr.equals_number}}, ge={{attr.equals_number}}
|
235
|
-
{%- else -%}
|
236
|
-
{%- if attr.minimum_value != None %}, ge={{attr.minimum_value}}{% endif -%}
|
237
|
-
{%- if attr.maximum_value != None %}, le={{attr.maximum_value}}{% endif -%}
|
238
|
-
{%- endif -%}
|
239
|
-
)
|
240
|
-
{% else -%}
|
241
|
-
None
|
242
|
-
{% endfor %}
|
243
|
-
{% for attr in c.attributes.values() if c.attributes -%}
|
244
|
-
{%- if attr.pattern %}
|
245
|
-
@field_validator('{{attr.name}}')
|
246
|
-
def pattern_{{attr.name}}(cls, v):
|
247
|
-
pattern=re.compile(r"{{attr.pattern}}")
|
248
|
-
if isinstance(v,list):
|
249
|
-
for element in v:
|
250
|
-
if not pattern.match(element):
|
251
|
-
raise ValueError(f"Invalid {{attr.name}} format: {element}")
|
252
|
-
elif isinstance(v,str):
|
253
|
-
if not pattern.match(v):
|
254
|
-
raise ValueError(f"Invalid {{attr.name}} format: {v}")
|
255
|
-
return v
|
256
|
-
{% endif -%}
|
257
|
-
{% endfor %}
|
258
|
-
{% endfor %}
|
259
|
-
"""
|
260
|
-
|
261
|
-
### FWD REFS / REBUILD MODEL ###
|
262
|
-
if pydantic_ver == "1":
|
263
|
-
template += """
|
264
|
-
# Update forward refs
|
265
|
-
# see https://pydantic-docs.helpmanual.io/usage/postponed_annotations/
|
266
|
-
{% for c in schema.classes.values() -%}
|
267
|
-
{{ c.name }}.update_forward_refs()
|
268
|
-
{% endfor %}
|
269
|
-
"""
|
270
|
-
else:
|
271
|
-
template += """
|
272
|
-
# Model rebuild
|
273
|
-
# see https://pydantic-docs.helpmanual.io/usage/models/#rebuilding-a-model
|
274
|
-
{% for c in schema.classes.values() -%}
|
275
|
-
{{ c.name }}.model_rebuild()
|
276
|
-
{% endfor %}
|
277
|
-
"""
|
278
|
-
return template
|
279
|
-
|
280
|
-
|
281
57
|
def _get_pyrange(t: TypeDefinition, sv: SchemaView) -> str:
|
282
58
|
pyrange = t.repr if t is not None else None
|
283
59
|
if pyrange is None:
|
@@ -293,6 +69,42 @@ def _get_pyrange(t: TypeDefinition, sv: SchemaView) -> str:
|
|
293
69
|
return pyrange
|
294
70
|
|
295
71
|
|
72
|
+
DEFAULT_IMPORTS = (
|
73
|
+
Imports()
|
74
|
+
+ Import(module="__future__", objects=[ObjectImport(name="annotations")])
|
75
|
+
+ Import(module="datetime", objects=[ObjectImport(name="datetime"), ObjectImport(name="date")])
|
76
|
+
+ Import(module="decimal", objects=[ObjectImport(name="Decimal")])
|
77
|
+
+ Import(module="enum", objects=[ObjectImport(name="Enum")])
|
78
|
+
+ Import(module="re")
|
79
|
+
+ Import(
|
80
|
+
module="typing",
|
81
|
+
objects=[
|
82
|
+
ObjectImport(name="Any"),
|
83
|
+
ObjectImport(name="List"),
|
84
|
+
ObjectImport(name="Literal"),
|
85
|
+
ObjectImport(name="Dict"),
|
86
|
+
ObjectImport(name="Optional"),
|
87
|
+
ObjectImport(name="Union"),
|
88
|
+
],
|
89
|
+
)
|
90
|
+
+ Import(module="pydantic.version", objects=[ObjectImport(name="VERSION", alias="PYDANTIC_VERSION")])
|
91
|
+
+ ConditionalImport(
|
92
|
+
condition="int(PYDANTIC_VERSION[0])>=2",
|
93
|
+
module="pydantic",
|
94
|
+
objects=[
|
95
|
+
ObjectImport(name="BaseModel"),
|
96
|
+
ObjectImport(name="ConfigDict"),
|
97
|
+
ObjectImport(name="Field"),
|
98
|
+
ObjectImport(name="field_validator"),
|
99
|
+
],
|
100
|
+
alternative=Import(
|
101
|
+
module="pydantic",
|
102
|
+
objects=[ObjectImport(name="BaseModel"), ObjectImport(name="Field"), ObjectImport(name="validator")],
|
103
|
+
),
|
104
|
+
)
|
105
|
+
)
|
106
|
+
|
107
|
+
|
296
108
|
@dataclass
|
297
109
|
class PydanticGenerator(OOCodeGenerator):
|
298
110
|
"""
|
@@ -308,9 +120,21 @@ class PydanticGenerator(OOCodeGenerator):
|
|
308
120
|
file_extension = "py"
|
309
121
|
|
310
122
|
# ObjectVars
|
311
|
-
|
312
|
-
|
313
|
-
|
123
|
+
array_representations: List[ArrayRepresentation] = field(default_factory=lambda: [ArrayRepresentation.LIST])
|
124
|
+
black: bool = False
|
125
|
+
"""
|
126
|
+
If black is present in the environment, format the serialized code with it
|
127
|
+
"""
|
128
|
+
pydantic_version: int = int(PYDANTIC_VERSION[0])
|
129
|
+
template_dir: Optional[Union[str, Path]] = None
|
130
|
+
"""
|
131
|
+
Override templates for each TemplateModel.
|
132
|
+
|
133
|
+
Directory with templates that override the default :attr:`.TemplateModel.template`
|
134
|
+
for each class. If a matching template is not found in the override directory,
|
135
|
+
the default templates will be used.
|
136
|
+
"""
|
137
|
+
extra_fields: Literal["allow", "forbid", "ignore"] = "forbid"
|
314
138
|
gen_mixin_inheritance: bool = True
|
315
139
|
injected_classes: Optional[List[Union[Type, str]]] = None
|
316
140
|
"""
|
@@ -334,44 +158,54 @@ class PydanticGenerator(OOCodeGenerator):
|
|
334
158
|
)
|
335
159
|
|
336
160
|
"""
|
337
|
-
imports: Optional[
|
161
|
+
imports: Optional[List[Import]] = None
|
338
162
|
"""
|
339
163
|
Additional imports to inject into generated module.
|
340
164
|
|
341
|
-
A dictionary mapping a module to a list of objects to import.
|
342
|
-
If the value of an entry is ``None`` , import the whole module.
|
343
|
-
|
344
|
-
Import Aliases:
|
345
|
-
If the value of a module import is a dictionary with a key "as",
|
346
|
-
or a class is specified as a dictionary with a "name" and "as",
|
347
|
-
then the import is renamed like "from module import class as renamedClass".
|
348
|
-
|
349
165
|
Examples:
|
350
166
|
|
351
167
|
.. code-block:: python
|
352
168
|
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
169
|
+
from linkml.generators.pydanticgen.template import (
|
170
|
+
ConditionalImport,
|
171
|
+
ObjectImport,
|
172
|
+
Import,
|
173
|
+
Imports
|
174
|
+
)
|
175
|
+
|
176
|
+
imports = (Imports() +
|
177
|
+
Import(module='sys') +
|
178
|
+
Import(module='numpy', alias='np') +
|
179
|
+
Import(module='pathlib', objects=[
|
180
|
+
ObjectImport(name="Path"),
|
181
|
+
ObjectImport(name="PurePath", alias="RenamedPurePath")
|
182
|
+
]) +
|
183
|
+
ConditionalImport(
|
184
|
+
module="typing",
|
185
|
+
objects=[ObjectImport(name="Literal")],
|
186
|
+
condition="sys.version_info >= (3, 8)",
|
187
|
+
alternative=Import(
|
188
|
+
module="typing_extensions",
|
189
|
+
objects=[ObjectImport(name="Literal")]
|
190
|
+
),
|
191
|
+
).imports
|
192
|
+
)
|
359
193
|
|
360
194
|
becomes:
|
361
195
|
|
362
196
|
.. code-block:: python
|
363
197
|
|
364
|
-
|
365
|
-
Dict,
|
366
|
-
List,
|
367
|
-
Union
|
368
|
-
)
|
369
|
-
import types
|
198
|
+
import sys
|
370
199
|
import numpy as np
|
371
|
-
from
|
372
|
-
|
200
|
+
from pathlib import (
|
201
|
+
Path,
|
202
|
+
PurePath as RenamedPurePath
|
373
203
|
)
|
374
|
-
|
204
|
+
if sys.version_info >= (3, 8):
|
205
|
+
from typing import Literal
|
206
|
+
else:
|
207
|
+
from typing_extensions import Literal
|
208
|
+
|
375
209
|
"""
|
376
210
|
|
377
211
|
# ObjectVars (identical to pythongen)
|
@@ -664,13 +498,29 @@ class PydanticGenerator(OOCodeGenerator):
|
|
664
498
|
return _get_pyrange(value_slot_range_type, sv)
|
665
499
|
return None
|
666
500
|
|
667
|
-
def
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
501
|
+
def _template_environment(self) -> Environment:
|
502
|
+
env = TemplateModel.environment()
|
503
|
+
if self.template_dir is not None:
|
504
|
+
loader = ChoiceLoader([FileSystemLoader(self.template_dir), env.loader])
|
505
|
+
env.loader = loader
|
506
|
+
return env
|
507
|
+
|
508
|
+
def get_array_representations_range(self, slot: SlotDefinition, range: str) -> List[SlotResult]:
|
509
|
+
"""
|
510
|
+
Generate the python range for array representations
|
511
|
+
"""
|
512
|
+
array_reps = []
|
513
|
+
for repr in self.array_representations:
|
514
|
+
generator = ArrayRangeGenerator.get_generator(repr)
|
515
|
+
result = generator(slot.array, range, self.pydantic_version).make()
|
516
|
+
array_reps.append(result)
|
517
|
+
|
518
|
+
if len(array_reps) == 0:
|
519
|
+
raise ValueError("No array representation generated, but one was requested!")
|
673
520
|
|
521
|
+
return array_reps
|
522
|
+
|
523
|
+
def render(self) -> PydanticModule:
|
674
524
|
sv: SchemaView
|
675
525
|
sv = self.schemaview
|
676
526
|
schema = sv.schema
|
@@ -680,8 +530,14 @@ class PydanticGenerator(OOCodeGenerator):
|
|
680
530
|
description=schema.description.replace('"', '\\"') if schema.description else None,
|
681
531
|
)
|
682
532
|
enums = self.generate_enums(sv.all_enums())
|
533
|
+
injected_classes = []
|
534
|
+
if self.injected_classes is not None:
|
535
|
+
injected_classes += self.injected_classes
|
683
536
|
|
684
|
-
|
537
|
+
imports = DEFAULT_IMPORTS
|
538
|
+
if self.imports is not None:
|
539
|
+
for i in self.imports:
|
540
|
+
imports += i
|
685
541
|
|
686
542
|
sorted_classes = self.sort_classes(list(sv.all_classes().values()))
|
687
543
|
self.sorted_class_names = [camelcase(c.name) for c in sorted_classes]
|
@@ -735,12 +591,22 @@ class PydanticGenerator(OOCodeGenerator):
|
|
735
591
|
else:
|
736
592
|
raise Exception(f"Could not generate python range for {class_name}.{s.name}")
|
737
593
|
|
738
|
-
if
|
594
|
+
if s.array is not None:
|
739
595
|
# TODO add support for xarray
|
740
|
-
|
596
|
+
results = self.get_array_representations_range(s, pyrange)
|
597
|
+
# TODO: Move results unpacking to own function that is used after each slot build stage :)
|
598
|
+
for res in results:
|
599
|
+
if res.injected_classes:
|
600
|
+
injected_classes += res.injected_classes
|
601
|
+
if res.imports:
|
602
|
+
imports += res.imports
|
603
|
+
if len(results) == 1:
|
604
|
+
pyrange = results[0].annotation
|
605
|
+
else:
|
606
|
+
pyrange = f"Union[{', '.join([res.annotation for res in results])}]"
|
607
|
+
|
741
608
|
if "linkml:ColumnOrderedArray" in class_def.implements:
|
742
609
|
raise NotImplementedError("Cannot generate Pydantic code for ColumnOrderedArrays.")
|
743
|
-
uses_numpy = True
|
744
610
|
elif s.multivalued:
|
745
611
|
if s.inlined or s.inlined_as_list:
|
746
612
|
collection_key = self.generate_collection_key(slot_ranges, s, class_def)
|
@@ -761,33 +627,101 @@ class PydanticGenerator(OOCodeGenerator):
|
|
761
627
|
pyrange = f"Optional[{pyrange}]"
|
762
628
|
ann = Annotation("python_range", pyrange)
|
763
629
|
s.annotations[ann.tag] = ann
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
630
|
+
|
631
|
+
# TODO: Make cleaning injected classes its own method
|
632
|
+
injected_classes = list(
|
633
|
+
dict.fromkeys([c if isinstance(c, str) else inspect.getsource(c) for c in injected_classes])
|
634
|
+
)
|
635
|
+
injected_classes = [textwrap.dedent(c) for c in injected_classes]
|
636
|
+
|
637
|
+
base_model = PydanticBaseModel(
|
638
|
+
pydantic_ver=self.pydantic_version, extra_fields=self.extra_fields, fields=self.injected_fields
|
639
|
+
)
|
640
|
+
|
641
|
+
classes = {}
|
642
|
+
predefined = self.get_predefined_slot_values()
|
643
|
+
bases = self.get_class_isa_plus_mixins()
|
644
|
+
for k, c in pyschema.classes.items():
|
645
|
+
attrs = {}
|
646
|
+
for attr_name, src_attr in c.attributes.items():
|
647
|
+
new_fields = {
|
648
|
+
k: src_attr._as_dict.get(k, None)
|
649
|
+
for k in PydanticAttribute.model_fields.keys()
|
650
|
+
if src_attr._as_dict.get(k, None) is not None
|
651
|
+
}
|
652
|
+
predef_slot = predefined.get(k, {}).get(attr_name, None)
|
653
|
+
if predef_slot is not None:
|
654
|
+
predef_slot = str(predef_slot)
|
655
|
+
new_fields["predefined"] = predef_slot
|
656
|
+
new_fields["name"] = attr_name
|
657
|
+
attrs[attr_name] = PydanticAttribute(**new_fields, pydantic_ver=self.pydantic_version)
|
658
|
+
|
659
|
+
new_class = PydanticClass(
|
660
|
+
name=k, attributes=attrs, description=c.description, pydantic_ver=self.pydantic_version
|
661
|
+
)
|
662
|
+
if k in bases:
|
663
|
+
new_class.bases = bases[k]
|
664
|
+
classes[k] = new_class
|
665
|
+
|
666
|
+
module = PydanticModule(
|
667
|
+
pydantic_ver=self.pydantic_version,
|
770
668
|
metamodel_version=self.schema.metamodel_version,
|
771
669
|
version=self.schema.version,
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
670
|
+
imports=imports.imports,
|
671
|
+
base_model=base_model,
|
672
|
+
injected_classes=injected_classes,
|
673
|
+
enums=enums,
|
674
|
+
classes=classes,
|
776
675
|
)
|
777
|
-
return
|
676
|
+
return module
|
677
|
+
|
678
|
+
def serialize(self) -> str:
|
679
|
+
module = self.render()
|
680
|
+
return module.render(self._template_environment(), self.black)
|
778
681
|
|
779
682
|
def default_value_for_type(self, typ: str) -> str:
|
780
683
|
return "None"
|
781
684
|
|
782
685
|
|
686
|
+
def _subclasses(cls: Type):
|
687
|
+
return set(cls.__subclasses__()).union([s for c in cls.__subclasses__() for s in _subclasses(c)])
|
688
|
+
|
689
|
+
|
690
|
+
_TEMPLATE_NAMES = sorted(list(set([c.template for c in _subclasses(TemplateModel)])))
|
691
|
+
|
692
|
+
|
783
693
|
@shared_arguments(PydanticGenerator)
|
784
|
-
@click.option("--template-file",
|
694
|
+
@click.option("--template-file", hidden=True)
|
695
|
+
@click.option(
|
696
|
+
"--template-dir",
|
697
|
+
type=click.Path(),
|
698
|
+
help="""
|
699
|
+
Optional jinja2 template directory to use for class generation.
|
700
|
+
|
701
|
+
Pass a directory containing templates with the same name as any of the default
|
702
|
+
:class:`.TemplateModel` templates to override them. The given directory will be
|
703
|
+
searched for matching templates, and use the default templates as a fallback
|
704
|
+
if an override is not found
|
705
|
+
|
706
|
+
Available templates to override:
|
707
|
+
|
708
|
+
\b
|
709
|
+
"""
|
710
|
+
+ "\n".join(["- " + name for name in _TEMPLATE_NAMES]),
|
711
|
+
)
|
785
712
|
@click.option(
|
786
713
|
"--pydantic-version",
|
787
|
-
type=click.
|
788
|
-
default=
|
714
|
+
type=click.IntRange(1, 2),
|
715
|
+
default=1,
|
789
716
|
help="Pydantic version to use (1 or 2)",
|
790
717
|
)
|
718
|
+
@click.option(
|
719
|
+
"--array-representations",
|
720
|
+
type=click.Choice([k.value for k in ArrayRepresentation]),
|
721
|
+
multiple=True,
|
722
|
+
default=["list"],
|
723
|
+
help="List of array representations to accept for array slots. Default is list of lists.",
|
724
|
+
)
|
791
725
|
@click.option(
|
792
726
|
"--extra-fields",
|
793
727
|
type=click.Choice(["allow", "ignore", "forbid"], case_sensitive=False),
|
@@ -799,25 +733,39 @@ class PydanticGenerator(OOCodeGenerator):
|
|
799
733
|
def cli(
|
800
734
|
yamlfile,
|
801
735
|
template_file=None,
|
736
|
+
template_dir: Optional[str] = None,
|
802
737
|
head=True,
|
803
|
-
emit_metadata=False,
|
804
738
|
genmeta=False,
|
805
739
|
classvars=True,
|
806
740
|
slots=True,
|
807
|
-
|
808
|
-
|
741
|
+
array_representations=list("list"),
|
742
|
+
pydantic_version=1,
|
743
|
+
extra_fields: Literal["allow", "forbid", "ignore"] = "forbid",
|
809
744
|
**args,
|
810
745
|
):
|
811
746
|
"""Generate pydantic classes to represent a LinkML model"""
|
747
|
+
if template_file is not None:
|
748
|
+
raise DeprecationWarning(
|
749
|
+
(
|
750
|
+
"Passing a single template_file is deprecated. Pass a directory of template files instead. "
|
751
|
+
"See help string for --template-dir"
|
752
|
+
)
|
753
|
+
)
|
754
|
+
|
755
|
+
if template_dir is not None:
|
756
|
+
if not Path(template_dir).exists():
|
757
|
+
raise FileNotFoundError(f"The template directory {template_dir} does not exist!")
|
758
|
+
|
812
759
|
gen = PydanticGenerator(
|
813
760
|
yamlfile,
|
814
|
-
template_file=template_file,
|
815
761
|
pydantic_version=pydantic_version,
|
762
|
+
array_representations=[ArrayRepresentation(x) for x in array_representations],
|
816
763
|
extra_fields=extra_fields,
|
817
764
|
emit_metadata=head,
|
818
765
|
genmeta=genmeta,
|
819
766
|
gen_classvars=classvars,
|
820
767
|
gen_slots=slots,
|
768
|
+
template_dir=template_dir,
|
821
769
|
**args,
|
822
770
|
)
|
823
771
|
print(gen.serialize())
|