linkml 1.7.5__py3-none-any.whl → 1.7.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.
- linkml/generators/__init__.py +2 -0
- linkml/generators/docgen/class.md.jinja2 +15 -2
- linkml/generators/docgen/slot.md.jinja2 +14 -1
- linkml/generators/docgen.py +17 -3
- linkml/generators/jsonldcontextgen.py +9 -6
- linkml/generators/jsonldgen.py +3 -1
- linkml/generators/owlgen.py +14 -0
- linkml/generators/prefixmapgen.py +5 -4
- linkml/generators/projectgen.py +14 -2
- linkml/generators/pydanticgen/__init__.py +29 -0
- linkml/generators/{pydanticgen.py → pydanticgen/pydanticgen.py} +192 -299
- linkml/generators/pydanticgen/template.py +504 -0
- linkml/generators/pydanticgen/templates/attribute.py.jinja +10 -0
- linkml/generators/pydanticgen/templates/base_model.py.jinja +27 -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 +41 -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.6.dist-info}/METADATA +4 -1
- {linkml-1.7.5.dist-info → linkml-1.7.6.dist-info}/RECORD +41 -27
- {linkml-1.7.5.dist-info → linkml-1.7.6.dist-info}/LICENSE +0 -0
- {linkml-1.7.5.dist-info → linkml-1.7.6.dist-info}/WHEEL +0 -0
- {linkml-1.7.5.dist-info → linkml-1.7.6.dist-info}/entry_points.txt +0 -0
@@ -3,12 +3,13 @@ import logging
|
|
3
3
|
import os
|
4
4
|
from collections import defaultdict
|
5
5
|
from copy import deepcopy
|
6
|
-
from dataclasses import dataclass
|
6
|
+
from dataclasses import dataclass
|
7
|
+
from pathlib import Path
|
7
8
|
from types import ModuleType
|
8
|
-
from typing import Dict, List, Optional, Set, Type, Union
|
9
|
+
from typing import Dict, List, Literal, Optional, Set, Type, Union
|
9
10
|
|
10
11
|
import click
|
11
|
-
from jinja2 import
|
12
|
+
from jinja2 import ChoiceLoader, Environment, FileSystemLoader
|
12
13
|
|
13
14
|
# from linkml.generators import pydantic_GEN_VERSION
|
14
15
|
from linkml_runtime.linkml_model.meta import (
|
@@ -29,255 +30,21 @@ from linkml.generators.common.type_designators import (
|
|
29
30
|
get_type_designator_value,
|
30
31
|
)
|
31
32
|
from linkml.generators.oocodegen import OOCodeGenerator
|
33
|
+
from linkml.generators.pydanticgen.template import (
|
34
|
+
ConditionalImport,
|
35
|
+
Import,
|
36
|
+
Imports,
|
37
|
+
ObjectImport,
|
38
|
+
PydanticAttribute,
|
39
|
+
PydanticBaseModel,
|
40
|
+
PydanticClass,
|
41
|
+
PydanticModule,
|
42
|
+
TemplateModel,
|
43
|
+
)
|
32
44
|
from linkml.utils.generator import shared_arguments
|
33
45
|
from linkml.utils.ifabsent_functions import ifabsent_value_declaration
|
34
46
|
|
35
47
|
|
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
48
|
def _get_pyrange(t: TypeDefinition, sv: SchemaView) -> str:
|
282
49
|
pyrange = t.repr if t is not None else None
|
283
50
|
if pyrange is None:
|
@@ -293,6 +60,42 @@ def _get_pyrange(t: TypeDefinition, sv: SchemaView) -> str:
|
|
293
60
|
return pyrange
|
294
61
|
|
295
62
|
|
63
|
+
DEFAULT_IMPORTS = (
|
64
|
+
Imports()
|
65
|
+
+ Import(module="__future__", objects=[ObjectImport(name="annotations")])
|
66
|
+
+ Import(module="datetime", objects=[ObjectImport(name="datetime"), ObjectImport(name="date")])
|
67
|
+
+ Import(module="decimal", objects=[ObjectImport(name="Decimal")])
|
68
|
+
+ Import(module="enum", objects=[ObjectImport(name="Enum")])
|
69
|
+
+ Import(module="re")
|
70
|
+
+ Import(
|
71
|
+
module="typing",
|
72
|
+
objects=[
|
73
|
+
ObjectImport(name="Any"),
|
74
|
+
ObjectImport(name="List"),
|
75
|
+
ObjectImport(name="Literal"),
|
76
|
+
ObjectImport(name="Dict"),
|
77
|
+
ObjectImport(name="Optional"),
|
78
|
+
ObjectImport(name="Union"),
|
79
|
+
],
|
80
|
+
)
|
81
|
+
+ Import(module="pydantic.version", objects=[ObjectImport(name="VERSION", alias="PYDANTIC_VERSION")])
|
82
|
+
+ ConditionalImport(
|
83
|
+
condition="int(PYDANTIC_VERSION[0])>=2",
|
84
|
+
module="pydantic",
|
85
|
+
objects=[
|
86
|
+
ObjectImport(name="BaseModel"),
|
87
|
+
ObjectImport(name="ConfigDict"),
|
88
|
+
ObjectImport(name="Field"),
|
89
|
+
ObjectImport(name="field_validator"),
|
90
|
+
],
|
91
|
+
alternative=Import(
|
92
|
+
module="pydantic",
|
93
|
+
objects=[ObjectImport(name="BaseModel"), ObjectImport(name="Field"), ObjectImport(name="validator")],
|
94
|
+
),
|
95
|
+
)
|
96
|
+
)
|
97
|
+
|
98
|
+
|
296
99
|
@dataclass
|
297
100
|
class PydanticGenerator(OOCodeGenerator):
|
298
101
|
"""
|
@@ -308,9 +111,16 @@ class PydanticGenerator(OOCodeGenerator):
|
|
308
111
|
file_extension = "py"
|
309
112
|
|
310
113
|
# ObjectVars
|
311
|
-
pydantic_version:
|
312
|
-
|
313
|
-
|
114
|
+
pydantic_version: int = int(PYDANTIC_VERSION[0])
|
115
|
+
template_dir: Optional[Union[str, Path]] = None
|
116
|
+
"""
|
117
|
+
Override templates for each TemplateModel.
|
118
|
+
|
119
|
+
Directory with templates that override the default :attr:`.TemplateModel.template`
|
120
|
+
for each class. If a matching template is not found in the override directory,
|
121
|
+
the default templates will be used.
|
122
|
+
"""
|
123
|
+
extra_fields: Literal["allow", "forbid", "ignore"] = "forbid"
|
314
124
|
gen_mixin_inheritance: bool = True
|
315
125
|
injected_classes: Optional[List[Union[Type, str]]] = None
|
316
126
|
"""
|
@@ -334,44 +144,54 @@ class PydanticGenerator(OOCodeGenerator):
|
|
334
144
|
)
|
335
145
|
|
336
146
|
"""
|
337
|
-
imports: Optional[
|
147
|
+
imports: Optional[List[Import]] = None
|
338
148
|
"""
|
339
149
|
Additional imports to inject into generated module.
|
340
150
|
|
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
151
|
Examples:
|
350
152
|
|
351
153
|
.. code-block:: python
|
352
154
|
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
155
|
+
from linkml.generators.pydanticgen.template import (
|
156
|
+
ConditionalImport,
|
157
|
+
ObjectImport,
|
158
|
+
Import,
|
159
|
+
Imports
|
160
|
+
)
|
161
|
+
|
162
|
+
imports = (Imports() +
|
163
|
+
Import(module='sys') +
|
164
|
+
Import(module='numpy', alias='np') +
|
165
|
+
Import(module='pathlib', objects=[
|
166
|
+
ObjectImport(name="Path"),
|
167
|
+
ObjectImport(name="PurePath", alias="RenamedPurePath")
|
168
|
+
]) +
|
169
|
+
ConditionalImport(
|
170
|
+
module="typing",
|
171
|
+
objects=[ObjectImport(name="Literal")],
|
172
|
+
condition="sys.version_info >= (3, 8)",
|
173
|
+
alternative=Import(
|
174
|
+
module="typing_extensions",
|
175
|
+
objects=[ObjectImport(name="Literal")]
|
176
|
+
),
|
177
|
+
).imports
|
178
|
+
)
|
359
179
|
|
360
180
|
becomes:
|
361
181
|
|
362
182
|
.. code-block:: python
|
363
183
|
|
364
|
-
|
365
|
-
Dict,
|
366
|
-
List,
|
367
|
-
Union
|
368
|
-
)
|
369
|
-
import types
|
184
|
+
import sys
|
370
185
|
import numpy as np
|
371
|
-
from
|
372
|
-
|
186
|
+
from pathlib import (
|
187
|
+
Path,
|
188
|
+
PurePath as RenamedPurePath
|
373
189
|
)
|
374
|
-
|
190
|
+
if sys.version_info >= (3, 8):
|
191
|
+
from typing import Literal
|
192
|
+
else:
|
193
|
+
from typing_extensions import Literal
|
194
|
+
|
375
195
|
"""
|
376
196
|
|
377
197
|
# ObjectVars (identical to pythongen)
|
@@ -664,13 +484,14 @@ class PydanticGenerator(OOCodeGenerator):
|
|
664
484
|
return _get_pyrange(value_slot_range_type, sv)
|
665
485
|
return None
|
666
486
|
|
667
|
-
def
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
487
|
+
def _template_environment(self) -> Environment:
|
488
|
+
env = TemplateModel.environment()
|
489
|
+
if self.template_dir is not None:
|
490
|
+
loader = ChoiceLoader([FileSystemLoader(self.template_dir), env.loader])
|
491
|
+
env.loader = loader
|
492
|
+
return env
|
673
493
|
|
494
|
+
def serialize(self) -> str:
|
674
495
|
sv: SchemaView
|
675
496
|
sv = self.schemaview
|
676
497
|
schema = sv.schema
|
@@ -681,7 +502,10 @@ class PydanticGenerator(OOCodeGenerator):
|
|
681
502
|
)
|
682
503
|
enums = self.generate_enums(sv.all_enums())
|
683
504
|
|
684
|
-
|
505
|
+
imports = DEFAULT_IMPORTS
|
506
|
+
if self.imports is not None:
|
507
|
+
for i in self.imports:
|
508
|
+
imports += i
|
685
509
|
|
686
510
|
sorted_classes = self.sort_classes(list(sv.all_classes().values()))
|
687
511
|
self.sorted_class_names = [camelcase(c.name) for c in sorted_classes]
|
@@ -738,9 +562,9 @@ class PydanticGenerator(OOCodeGenerator):
|
|
738
562
|
if "linkml:elements" in s.implements:
|
739
563
|
# TODO add support for xarray
|
740
564
|
pyrange = "np.ndarray"
|
565
|
+
imports += Import(module="numpy", alias="np")
|
741
566
|
if "linkml:ColumnOrderedArray" in class_def.implements:
|
742
567
|
raise NotImplementedError("Cannot generate Pydantic code for ColumnOrderedArrays.")
|
743
|
-
uses_numpy = True
|
744
568
|
elif s.multivalued:
|
745
569
|
if s.inlined or s.inlined_as_list:
|
746
570
|
collection_key = self.generate_collection_key(slot_ranges, s, class_def)
|
@@ -761,31 +585,88 @@ class PydanticGenerator(OOCodeGenerator):
|
|
761
585
|
pyrange = f"Optional[{pyrange}]"
|
762
586
|
ann = Annotation("python_range", pyrange)
|
763
587
|
s.annotations[ann.tag] = ann
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
588
|
+
|
589
|
+
if self.injected_classes is not None:
|
590
|
+
injected_classes = [c if isinstance(c, str) else inspect.getsource(c) for c in self.injected_classes]
|
591
|
+
else:
|
592
|
+
injected_classes = None
|
593
|
+
|
594
|
+
base_model = PydanticBaseModel(
|
595
|
+
pydantic_ver=self.pydantic_version, extra_fields=self.extra_fields, fields=self.injected_fields
|
596
|
+
)
|
597
|
+
|
598
|
+
classes = {}
|
599
|
+
predefined = self.get_predefined_slot_values()
|
600
|
+
bases = self.get_class_isa_plus_mixins()
|
601
|
+
for k, c in pyschema.classes.items():
|
602
|
+
attrs = {}
|
603
|
+
for attr_name, src_attr in c.attributes.items():
|
604
|
+
new_fields = {
|
605
|
+
k: src_attr._as_dict.get(k, None)
|
606
|
+
for k in PydanticAttribute.model_fields.keys()
|
607
|
+
if src_attr._as_dict.get(k, None) is not None
|
608
|
+
}
|
609
|
+
predef_slot = predefined.get(k, {}).get(attr_name, None)
|
610
|
+
if predef_slot is not None:
|
611
|
+
predef_slot = str(predef_slot)
|
612
|
+
new_fields["predefined"] = predef_slot
|
613
|
+
new_fields["name"] = attr_name
|
614
|
+
attrs[attr_name] = PydanticAttribute(**new_fields, pydantic_ver=self.pydantic_version)
|
615
|
+
|
616
|
+
new_class = PydanticClass(
|
617
|
+
name=k, attributes=attrs, description=c.description, pydantic_ver=self.pydantic_version
|
618
|
+
)
|
619
|
+
if k in bases:
|
620
|
+
new_class.bases = bases[k]
|
621
|
+
classes[k] = new_class
|
622
|
+
|
623
|
+
module = PydanticModule(
|
624
|
+
pydantic_ver=self.pydantic_version,
|
770
625
|
metamodel_version=self.schema.metamodel_version,
|
771
626
|
version=self.schema.version,
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
627
|
+
imports=imports.imports,
|
628
|
+
base_model=base_model,
|
629
|
+
injected_classes=injected_classes,
|
630
|
+
enums=enums,
|
631
|
+
classes=classes,
|
776
632
|
)
|
633
|
+
code = module.render(self._template_environment())
|
777
634
|
return code
|
778
635
|
|
779
636
|
def default_value_for_type(self, typ: str) -> str:
|
780
637
|
return "None"
|
781
638
|
|
782
639
|
|
640
|
+
def _subclasses(cls: Type):
|
641
|
+
return set(cls.__subclasses__()).union([s for c in cls.__subclasses__() for s in _subclasses(c)])
|
642
|
+
|
643
|
+
|
644
|
+
_TEMPLATE_NAMES = sorted(list(set([c.template for c in _subclasses(TemplateModel)])))
|
645
|
+
|
646
|
+
|
783
647
|
@shared_arguments(PydanticGenerator)
|
784
|
-
@click.option("--template-file",
|
648
|
+
@click.option("--template-file", hidden=True)
|
649
|
+
@click.option(
|
650
|
+
"--template-dir",
|
651
|
+
type=click.Path(),
|
652
|
+
help="""
|
653
|
+
Optional jinja2 template directory to use for class generation.
|
654
|
+
|
655
|
+
Pass a directory containing templates with the same name as any of the default
|
656
|
+
:class:`.TemplateModel` templates to override them. The given directory will be
|
657
|
+
searched for matching templates, and use the default templates as a fallback
|
658
|
+
if an override is not found
|
659
|
+
|
660
|
+
Available templates to override:
|
661
|
+
|
662
|
+
\b
|
663
|
+
"""
|
664
|
+
+ "\n".join(["- " + name for name in _TEMPLATE_NAMES]),
|
665
|
+
)
|
785
666
|
@click.option(
|
786
667
|
"--pydantic-version",
|
787
|
-
type=click.
|
788
|
-
default=
|
668
|
+
type=click.IntRange(1, 2),
|
669
|
+
default=1,
|
789
670
|
help="Pydantic version to use (1 or 2)",
|
790
671
|
)
|
791
672
|
@click.option(
|
@@ -799,25 +680,37 @@ class PydanticGenerator(OOCodeGenerator):
|
|
799
680
|
def cli(
|
800
681
|
yamlfile,
|
801
682
|
template_file=None,
|
683
|
+
template_dir: Optional[str] = None,
|
802
684
|
head=True,
|
803
|
-
emit_metadata=False,
|
804
685
|
genmeta=False,
|
805
686
|
classvars=True,
|
806
687
|
slots=True,
|
807
|
-
pydantic_version=
|
688
|
+
pydantic_version=1,
|
808
689
|
extra_fields="forbid",
|
809
690
|
**args,
|
810
691
|
):
|
811
692
|
"""Generate pydantic classes to represent a LinkML model"""
|
693
|
+
if template_file is not None:
|
694
|
+
raise DeprecationWarning(
|
695
|
+
(
|
696
|
+
"Passing a single template_file is deprecated. Pass a directory of template files instead. "
|
697
|
+
"See help string for --template-dir"
|
698
|
+
)
|
699
|
+
)
|
700
|
+
|
701
|
+
if template_dir is not None:
|
702
|
+
if not Path(template_dir).exists():
|
703
|
+
raise FileNotFoundError(f"The template directory {template_dir} does not exist!")
|
704
|
+
|
812
705
|
gen = PydanticGenerator(
|
813
706
|
yamlfile,
|
814
|
-
template_file=template_file,
|
815
707
|
pydantic_version=pydantic_version,
|
816
708
|
extra_fields=extra_fields,
|
817
709
|
emit_metadata=head,
|
818
710
|
genmeta=genmeta,
|
819
711
|
gen_classvars=classvars,
|
820
712
|
gen_slots=slots,
|
713
|
+
template_dir=template_dir,
|
821
714
|
**args,
|
822
715
|
)
|
823
716
|
print(gen.serialize())
|