linkml 1.8.1__py3-none-any.whl → 1.8.2__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/__init__.py +0 -0
- linkml/cli/__main__.py +4 -0
- linkml/cli/main.py +126 -0
- linkml/generators/common/build.py +105 -0
- linkml/generators/common/lifecycle.py +124 -0
- linkml/generators/common/template.py +89 -0
- linkml/generators/csvgen.py +1 -1
- linkml/generators/docgen/slot.md.jinja2 +4 -0
- linkml/generators/docgen.py +1 -1
- linkml/generators/dotgen.py +1 -1
- linkml/generators/erdiagramgen.py +1 -1
- linkml/generators/excelgen.py +1 -1
- linkml/generators/golanggen.py +1 -1
- linkml/generators/golrgen.py +1 -1
- linkml/generators/graphqlgen.py +1 -1
- linkml/generators/javagen.py +1 -1
- linkml/generators/jsonldcontextgen.py +4 -4
- linkml/generators/jsonldgen.py +1 -1
- linkml/generators/jsonschemagen.py +69 -22
- linkml/generators/linkmlgen.py +1 -1
- linkml/generators/markdowngen.py +1 -1
- linkml/generators/namespacegen.py +1 -1
- linkml/generators/oocodegen.py +2 -1
- linkml/generators/owlgen.py +1 -1
- linkml/generators/plantumlgen.py +1 -1
- linkml/generators/prefixmapgen.py +1 -1
- linkml/generators/projectgen.py +1 -1
- linkml/generators/protogen.py +1 -1
- linkml/generators/pydanticgen/__init__.py +8 -3
- linkml/generators/pydanticgen/array.py +114 -194
- linkml/generators/pydanticgen/build.py +64 -25
- linkml/generators/pydanticgen/includes.py +1 -31
- linkml/generators/pydanticgen/pydanticgen.py +616 -274
- linkml/generators/pydanticgen/template.py +152 -184
- linkml/generators/pydanticgen/templates/attribute.py.jinja +9 -7
- linkml/generators/pydanticgen/templates/base_model.py.jinja +0 -13
- linkml/generators/pydanticgen/templates/class.py.jinja +2 -2
- linkml/generators/pydanticgen/templates/footer.py.jinja +2 -10
- linkml/generators/pydanticgen/templates/module.py.jinja +2 -2
- linkml/generators/pydanticgen/templates/validator.py.jinja +0 -4
- linkml/generators/pythongen.py +12 -2
- linkml/generators/rdfgen.py +1 -1
- linkml/generators/shaclgen.py +6 -2
- linkml/generators/shexgen.py +1 -1
- linkml/generators/sparqlgen.py +1 -1
- linkml/generators/sqlalchemygen.py +1 -1
- linkml/generators/sqltablegen.py +1 -1
- linkml/generators/sssomgen.py +1 -1
- linkml/generators/summarygen.py +1 -1
- linkml/generators/terminusdbgen.py +7 -4
- linkml/generators/typescriptgen.py +1 -1
- linkml/generators/yamlgen.py +1 -1
- linkml/generators/yumlgen.py +1 -1
- linkml/linter/cli.py +1 -1
- linkml/transformers/logical_model_transformer.py +117 -18
- linkml/utils/converter.py +1 -1
- linkml/utils/execute_tutorial.py +2 -0
- linkml/utils/logictools.py +142 -29
- linkml/utils/schema_builder.py +7 -6
- linkml/utils/schema_fixer.py +1 -1
- linkml/utils/sqlutils.py +1 -1
- linkml/validator/cli.py +4 -1
- linkml/validators/jsonschemavalidator.py +1 -1
- linkml/validators/sparqlvalidator.py +1 -1
- linkml/workspaces/example_runner.py +1 -1
- {linkml-1.8.1.dist-info → linkml-1.8.2.dist-info}/METADATA +2 -2
- {linkml-1.8.1.dist-info → linkml-1.8.2.dist-info}/RECORD +70 -64
- {linkml-1.8.1.dist-info → linkml-1.8.2.dist-info}/entry_points.txt +1 -1
- {linkml-1.8.1.dist-info → linkml-1.8.2.dist-info}/LICENSE +0 -0
- {linkml-1.8.1.dist-info → linkml-1.8.2.dist-info}/WHEEL +0 -0
@@ -1,11 +1,13 @@
|
|
1
|
-
from copy import copy
|
2
1
|
from importlib.util import find_spec
|
3
|
-
from typing import Any, ClassVar, Dict, Generator, List, Literal, Optional, Union
|
2
|
+
from typing import Any, ClassVar, Dict, Generator, List, Literal, Optional, Union
|
4
3
|
|
5
4
|
from jinja2 import Environment, PackageLoader
|
6
|
-
from pydantic import BaseModel, Field
|
5
|
+
from pydantic import BaseModel, Field, field_validator
|
7
6
|
from pydantic.version import VERSION as PYDANTIC_VERSION
|
8
7
|
|
8
|
+
from linkml.generators.common.template import TemplateModel
|
9
|
+
from linkml.utils.deprecation import deprecation_warning
|
10
|
+
|
9
11
|
try:
|
10
12
|
if find_spec("black") is not None:
|
11
13
|
from linkml.generators.pydanticgen.black import format_black
|
@@ -19,10 +21,14 @@ except ImportError:
|
|
19
21
|
if int(PYDANTIC_VERSION[0]) >= 2:
|
20
22
|
from pydantic import computed_field
|
21
23
|
else:
|
22
|
-
|
24
|
+
deprecation_warning("pydantic-v1")
|
25
|
+
|
26
|
+
def computed_field(f):
|
27
|
+
"""No-op decorator to allow this module to not break imports until 1.9.0"""
|
28
|
+
return f
|
23
29
|
|
24
30
|
|
25
|
-
class TemplateModel
|
31
|
+
class PydanticTemplateModel(TemplateModel):
|
26
32
|
"""
|
27
33
|
Metaclass to render pydantic models with jinja templates.
|
28
34
|
|
@@ -59,15 +65,14 @@ class TemplateModel(BaseModel):
|
|
59
65
|
loader=PackageLoader("linkml.generators.pydanticgen", "templates"), trim_blocks=True, lstrip_blocks=True
|
60
66
|
)
|
61
67
|
|
62
|
-
pydantic_ver: int = int(PYDANTIC_VERSION[0])
|
63
68
|
meta_exclude: ClassVar[List[str]] = None
|
64
69
|
|
65
70
|
def render(self, environment: Optional[Environment] = None, black: bool = False) -> str:
|
66
71
|
"""
|
67
72
|
Recursively render a template model to a string.
|
68
73
|
|
69
|
-
For each field in the model, recurse through, rendering each :class:`.
|
70
|
-
using the template set in :attr:`.
|
74
|
+
For each field in the model, recurse through, rendering each :class:`.PydanticTemplateModel`
|
75
|
+
using the template set in :attr:`.PydanticTemplateModel.template` , but preserving the structure
|
71
76
|
of lists and dictionaries. Regular :class:`.BaseModel` s are rendered to dictionaries.
|
72
77
|
Any other value is passed through unchanged.
|
73
78
|
|
@@ -76,16 +81,10 @@ class TemplateModel(BaseModel):
|
|
76
81
|
black (bool): if ``True`` , format template with black. (default False)
|
77
82
|
"""
|
78
83
|
if environment is None:
|
79
|
-
environment =
|
84
|
+
environment = self.environment()
|
80
85
|
|
81
|
-
|
82
|
-
fields = {**self.model_fields, **self.model_computed_fields}
|
83
|
-
else:
|
84
|
-
fields = self.model_fields
|
86
|
+
rendered = super().render(environment=environment)
|
85
87
|
|
86
|
-
data = {k: _render(getattr(self, k, None), environment) for k in fields}
|
87
|
-
template = environment.get_template(self.template)
|
88
|
-
rendered = template.render(**data)
|
89
88
|
if format_black is not None and black:
|
90
89
|
try:
|
91
90
|
return format_black(rendered)
|
@@ -97,72 +96,6 @@ class TemplateModel(BaseModel):
|
|
97
96
|
else:
|
98
97
|
return rendered
|
99
98
|
|
100
|
-
@classmethod
|
101
|
-
def environment(cls) -> Environment:
|
102
|
-
"""
|
103
|
-
Default environment for Template models.
|
104
|
-
uses a :class:`jinja2.PackageLoader` for the templates directory within this module
|
105
|
-
with the ``trim_blocks`` and ``lstrip_blocks`` parameters set to ``True`` so that the
|
106
|
-
default templates could be written in a more readable way.
|
107
|
-
"""
|
108
|
-
return copy(cls._environment)
|
109
|
-
|
110
|
-
if int(PYDANTIC_VERSION[0]) < 2:
|
111
|
-
# simulate pydantic 2's model_fields behavior
|
112
|
-
# without using classmethod + property decorators
|
113
|
-
# see:
|
114
|
-
# - https://docs.python.org/3/whatsnew/3.11.html#language-builtins
|
115
|
-
# - https://github.com/python/cpython/issues/89519
|
116
|
-
# and:
|
117
|
-
# - https://docs.python.org/3/reference/datamodel.html#customizing-class-creation
|
118
|
-
# for this version.
|
119
|
-
model_fields: ClassVar[Dict[str, "ModelField"]]
|
120
|
-
|
121
|
-
def __init_subclass__(cls, **kwargs):
|
122
|
-
super().__init_subclass__(**kwargs)
|
123
|
-
cls.model_fields = cls.__fields__
|
124
|
-
|
125
|
-
@overload
|
126
|
-
def model_dump(self, mode: Literal["python"] = "python") -> dict: ...
|
127
|
-
|
128
|
-
@overload
|
129
|
-
def model_dump(self, mode: Literal["json"] = "json") -> str: ...
|
130
|
-
|
131
|
-
def model_dump(self, mode: Literal["python", "json"] = "python", **kwargs) -> Union[dict, str]:
|
132
|
-
if mode == "json":
|
133
|
-
return self.json(**kwargs)
|
134
|
-
return self.dict(**kwargs)
|
135
|
-
|
136
|
-
@classmethod
|
137
|
-
def exclude_from_meta(cls: "TemplateModel") -> List[str]:
|
138
|
-
"""
|
139
|
-
Attributes in the source definition to exclude from linkml_meta
|
140
|
-
"""
|
141
|
-
ret = [*cls.model_fields.keys()]
|
142
|
-
if cls.meta_exclude is not None:
|
143
|
-
ret = ret + cls.meta_exclude
|
144
|
-
return ret
|
145
|
-
|
146
|
-
|
147
|
-
def _render(
|
148
|
-
item: Union[TemplateModel, Any, List[Union[Any, TemplateModel]], Dict[str, Union[Any, TemplateModel]]],
|
149
|
-
environment: Environment,
|
150
|
-
) -> Union[str, List[str], Dict[str, str]]:
|
151
|
-
if isinstance(item, TemplateModel):
|
152
|
-
return item.render(environment)
|
153
|
-
elif isinstance(item, list):
|
154
|
-
return [_render(i, environment) for i in item]
|
155
|
-
elif isinstance(item, dict):
|
156
|
-
return {k: _render(v, environment) for k, v in item.items()}
|
157
|
-
elif isinstance(item, BaseModel):
|
158
|
-
if int(PYDANTIC_VERSION[0]) >= 2:
|
159
|
-
fields = item.model_fields
|
160
|
-
else:
|
161
|
-
fields = item.__fields__
|
162
|
-
return {k: _render(getattr(item, k, None), environment) for k in fields.keys()}
|
163
|
-
else:
|
164
|
-
return item
|
165
|
-
|
166
99
|
|
167
100
|
class EnumValue(BaseModel):
|
168
101
|
"""
|
@@ -174,7 +107,7 @@ class EnumValue(BaseModel):
|
|
174
107
|
description: Optional[str] = None
|
175
108
|
|
176
109
|
|
177
|
-
class PydanticEnum(
|
110
|
+
class PydanticEnum(PydanticTemplateModel):
|
178
111
|
"""
|
179
112
|
Model used to render a :class:`enum.Enum`
|
180
113
|
"""
|
@@ -186,7 +119,7 @@ class PydanticEnum(TemplateModel):
|
|
186
119
|
values: Dict[str, EnumValue] = Field(default_factory=dict)
|
187
120
|
|
188
121
|
|
189
|
-
class PydanticBaseModel(
|
122
|
+
class PydanticBaseModel(PydanticTemplateModel):
|
190
123
|
"""
|
191
124
|
Parameterization of the base model that generated pydantic classes inherit from
|
192
125
|
"""
|
@@ -207,24 +140,24 @@ class PydanticBaseModel(TemplateModel):
|
|
207
140
|
strict: bool = False
|
208
141
|
"""
|
209
142
|
Enable strict mode in the base model.
|
210
|
-
|
143
|
+
|
211
144
|
.. note::
|
212
|
-
|
145
|
+
|
213
146
|
Pydantic 2 only! Pydantic 1 only has strict types, not strict mode. See: https://github.com/linkml/linkml/issues/1955
|
214
|
-
|
147
|
+
|
215
148
|
References:
|
216
149
|
https://docs.pydantic.dev/latest/concepts/strict_mode
|
217
150
|
"""
|
218
151
|
|
219
152
|
|
220
|
-
class PydanticAttribute(
|
153
|
+
class PydanticAttribute(PydanticTemplateModel):
|
221
154
|
"""
|
222
155
|
Reduced version of SlotDefinition that carries all and only the information
|
223
156
|
needed by the template
|
224
157
|
"""
|
225
158
|
|
226
159
|
template: ClassVar[str] = "attribute.py.jinja"
|
227
|
-
meta_exclude: ClassVar[List[str]] = ["from_schema", "owner", "range", "
|
160
|
+
meta_exclude: ClassVar[List[str]] = ["from_schema", "owner", "range", "inlined", "inlined_as_list"]
|
228
161
|
|
229
162
|
name: str
|
230
163
|
required: bool = False
|
@@ -232,51 +165,32 @@ class PydanticAttribute(TemplateModel):
|
|
232
165
|
key: bool = False
|
233
166
|
predefined: Optional[str] = None
|
234
167
|
"""Fixed string to use in body of field"""
|
235
|
-
|
236
|
-
"""
|
237
|
-
Of the form::
|
238
|
-
|
239
|
-
annotations = {'python_range': {'value': 'int'}}
|
240
|
-
|
241
|
-
.. todo::
|
242
|
-
|
243
|
-
simplify when refactoring pydanticgen, should just be a string or a model
|
244
|
-
|
245
|
-
"""
|
168
|
+
range: Optional[str] = None
|
169
|
+
"""Type annotation used for model field"""
|
246
170
|
title: Optional[str] = None
|
247
171
|
description: Optional[str] = None
|
248
172
|
equals_number: Optional[Union[int, float]] = None
|
249
173
|
minimum_value: Optional[Union[int, float]] = None
|
250
174
|
maximum_value: Optional[Union[int, float]] = None
|
175
|
+
exact_cardinality: Optional[int] = None
|
176
|
+
minimum_cardinality: Optional[int] = None
|
177
|
+
maximum_cardinality: Optional[int] = None
|
178
|
+
multivalued: Optional[bool] = None
|
251
179
|
pattern: Optional[str] = None
|
252
180
|
meta: Optional[Dict[str, Any]] = None
|
253
181
|
"""
|
254
182
|
Metadata for the slot to be included in a Field annotation
|
255
183
|
"""
|
256
184
|
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
else:
|
267
|
-
return "None"
|
268
|
-
|
269
|
-
else:
|
270
|
-
field: Optional[str] = None
|
271
|
-
|
272
|
-
def __init__(self, **kwargs):
|
273
|
-
super(PydanticAttribute, self).__init__(**kwargs)
|
274
|
-
if self.predefined:
|
275
|
-
self.field = self.predefined
|
276
|
-
elif self.required or self.identifier or self.key:
|
277
|
-
self.field = "..."
|
278
|
-
else:
|
279
|
-
self.field = "None"
|
185
|
+
@computed_field
|
186
|
+
def field(self) -> str:
|
187
|
+
"""Computed value to use inside of the generated Field"""
|
188
|
+
if self.predefined:
|
189
|
+
return self.predefined
|
190
|
+
elif self.required or self.identifier or self.key:
|
191
|
+
return "..."
|
192
|
+
else:
|
193
|
+
return "None"
|
280
194
|
|
281
195
|
|
282
196
|
class PydanticValidator(PydanticAttribute):
|
@@ -287,7 +201,7 @@ class PydanticValidator(PydanticAttribute):
|
|
287
201
|
template: ClassVar[str] = "validator.py.jinja"
|
288
202
|
|
289
203
|
|
290
|
-
class PydanticClass(
|
204
|
+
class PydanticClass(PydanticTemplateModel):
|
291
205
|
"""
|
292
206
|
Reduced version of ClassDefinition that carries all and only the information
|
293
207
|
needed by the template.
|
@@ -316,29 +230,14 @@ class PydanticClass(TemplateModel):
|
|
316
230
|
|
317
231
|
return {k: PydanticValidator(**v.model_dump()) for k, v in self.attributes.items() if v.pattern is not None}
|
318
232
|
|
319
|
-
|
233
|
+
@computed_field
|
234
|
+
def validators(self) -> Optional[Dict[str, PydanticValidator]]:
|
235
|
+
return self._validators()
|
320
236
|
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
@computed_field
|
326
|
-
def slots(self) -> Optional[Dict[str, PydanticAttribute]]:
|
327
|
-
"""alias of attributes"""
|
328
|
-
return self.attributes
|
329
|
-
|
330
|
-
else:
|
331
|
-
validators: Optional[Dict[str, PydanticValidator]]
|
332
|
-
|
333
|
-
def __init__(self, **kwargs):
|
334
|
-
super(PydanticClass, self).__init__(**kwargs)
|
335
|
-
self.validators = self._validators()
|
336
|
-
|
337
|
-
def render(self, environment: Optional[Environment] = None, black: bool = False) -> str:
|
338
|
-
"""Overridden in pydantic 1 to ensure that validators are regenerated at rendering time"""
|
339
|
-
# refresh in case attributes have changed since init
|
340
|
-
self.validators = self._validators()
|
341
|
-
return super(PydanticClass, self).render(environment, black)
|
237
|
+
@computed_field
|
238
|
+
def slots(self) -> Optional[Dict[str, PydanticAttribute]]:
|
239
|
+
"""alias of attributes"""
|
240
|
+
return self.attributes
|
342
241
|
|
343
242
|
|
344
243
|
class ObjectImport(BaseModel):
|
@@ -352,7 +251,7 @@ class ObjectImport(BaseModel):
|
|
352
251
|
alias: Optional[str] = None
|
353
252
|
|
354
253
|
|
355
|
-
class Import(
|
254
|
+
class Import(PydanticTemplateModel):
|
356
255
|
"""
|
357
256
|
A python module, or module and classes to be imported.
|
358
257
|
|
@@ -386,6 +285,12 @@ class Import(TemplateModel):
|
|
386
285
|
module: str
|
387
286
|
alias: Optional[str] = None
|
388
287
|
objects: Optional[List[ObjectImport]] = None
|
288
|
+
is_schema: bool = False
|
289
|
+
"""
|
290
|
+
Whether or not this ``Import`` is importing another schema imported by the main schema --
|
291
|
+
ie. that it is not expected to be provided by the environment, but imported locally from within the package.
|
292
|
+
Used primarily in split schema generation, see :func:`.pydanticgen.generate_split` for example usage.
|
293
|
+
"""
|
389
294
|
|
390
295
|
def merge(self, other: "Import") -> List["Import"]:
|
391
296
|
"""
|
@@ -429,7 +334,14 @@ class Import(TemplateModel):
|
|
429
334
|
}
|
430
335
|
self_objs.update(other_objs)
|
431
336
|
|
432
|
-
return [
|
337
|
+
return [
|
338
|
+
Import(
|
339
|
+
module=self.module,
|
340
|
+
alias=alias,
|
341
|
+
objects=list(self_objs.values()),
|
342
|
+
is_schema=self.is_schema or other.is_schema,
|
343
|
+
)
|
344
|
+
]
|
433
345
|
else:
|
434
346
|
# one is a module, the other imports objects, keep both
|
435
347
|
return [self, other]
|
@@ -480,7 +392,7 @@ class ConditionalImport(Import):
|
|
480
392
|
alternative: Import
|
481
393
|
|
482
394
|
|
483
|
-
class Imports(
|
395
|
+
class Imports(PydanticTemplateModel):
|
484
396
|
"""
|
485
397
|
Container class for imports that can handle merging!
|
486
398
|
|
@@ -516,19 +428,38 @@ class Imports(TemplateModel):
|
|
516
428
|
|
517
429
|
imports: List[Union[Import, ConditionalImport]] = Field(default_factory=list)
|
518
430
|
|
519
|
-
|
431
|
+
@classmethod
|
432
|
+
def _merge(
|
433
|
+
cls, imports: List[Union[Import, ConditionalImport]], other: Union[Import, "Imports", List[Import]]
|
434
|
+
) -> List[Union[Import, ConditionalImport]]:
|
435
|
+
"""
|
436
|
+
Add a new import to an existing imports list, handling deduplication and flattening.
|
437
|
+
|
438
|
+
Mutates and returns ``imports``
|
439
|
+
|
440
|
+
Generally will prefer the imports in ``other`` , updating those in ``imports``.
|
441
|
+
If ``other`` ...
|
442
|
+
- doesn't match any ``module`` in ``imports``, add it!
|
443
|
+
- matches a single ``module`` in imports, :meth:`.Import.merge` the object imports
|
444
|
+
- matches multiple ``module``s in imports, then there must have been another
|
445
|
+
:class:`.ConditionalImport` already present, so we :meth:`.Import.merge` the existing
|
446
|
+
:class:`.Import` if it is one, and if it's a :class:`.ConditionalImport` just YOLO
|
447
|
+
and append it since there isn't a principled way to merge them from strings.
|
448
|
+
- is :class:`.Imports` or a list of :class:`.Import` s, call this recursively for each
|
449
|
+
item.
|
450
|
+
|
451
|
+
Since imports can be merged in an undefined order depending on the generator configuration,
|
452
|
+
default behavior for imports with matching ``module`` is to remove them and append to the
|
453
|
+
end of the imports list (rather than keeping it in the position of the existing
|
454
|
+
:class:`.Import` ). :class:`.ConditionalImports` make it possible to have namespace
|
455
|
+
conflicts, so in imperative import style we assume the most recently added :class:`.Import`
|
456
|
+
is the one that should prevail.
|
457
|
+
"""
|
458
|
+
#
|
520
459
|
if isinstance(other, Imports) or (isinstance(other, list) and all([isinstance(i, Import) for i in other])):
|
521
|
-
if hasattr(self, "model_copy"):
|
522
|
-
self_copy = self.model_copy(deep=True)
|
523
|
-
else:
|
524
|
-
self_copy = self.copy()
|
525
|
-
|
526
460
|
for i in other:
|
527
|
-
|
528
|
-
return
|
529
|
-
|
530
|
-
# check if we have one of these already
|
531
|
-
imports = self.imports.copy()
|
461
|
+
imports = cls._merge(imports, i)
|
462
|
+
return imports
|
532
463
|
|
533
464
|
existing = [i for i in imports if i.module == other.module]
|
534
465
|
|
@@ -554,8 +485,12 @@ class Imports(TemplateModel):
|
|
554
485
|
|
555
486
|
# SPECIAL CASE - __future__ annotations must happen at the top of a file
|
556
487
|
imports = sorted(imports, key=lambda i: i.module == "__future__", reverse=True)
|
488
|
+
return imports
|
557
489
|
|
558
|
-
|
490
|
+
def __add__(self, other: Union[Import, "Imports", List[Import]]) -> "Imports":
|
491
|
+
imports = self.imports.copy()
|
492
|
+
imports = self._merge(imports, other)
|
493
|
+
return Imports.model_construct(imports=imports)
|
559
494
|
|
560
495
|
def __len__(self) -> int:
|
561
496
|
return len(self.imports)
|
@@ -564,11 +499,61 @@ class Imports(TemplateModel):
|
|
564
499
|
for i in self.imports:
|
565
500
|
yield i
|
566
501
|
|
567
|
-
def __getitem__(self, item: int) -> Import:
|
568
|
-
|
502
|
+
def __getitem__(self, item: Union[int, str]) -> Import:
|
503
|
+
if isinstance(item, int):
|
504
|
+
return self.imports[item]
|
505
|
+
elif isinstance(item, str):
|
506
|
+
# the name of the module
|
507
|
+
an_import = [i for i in self.imports if i.module == item]
|
508
|
+
if len(an_import) == 0:
|
509
|
+
raise KeyError(f"No import with module {item} was found.\nWe have: {self.imports}")
|
510
|
+
return an_import[0]
|
511
|
+
else:
|
512
|
+
raise TypeError(f"Can only index with an int or a string as the name of the module,\nGot: {type(item)}")
|
513
|
+
|
514
|
+
def __contains__(self, item: Union[Import, "Imports", List[Import]]) -> bool:
|
515
|
+
"""
|
516
|
+
Check if all the objects are imported from the given module(s)
|
517
|
+
|
518
|
+
If the import is a bare module import (ie its :attr:`~.Import.objects` is ``None`` )
|
519
|
+
then we must also have a bare module import in this Imports (because even if
|
520
|
+
we import from the module, unless we import it specifically its name won't be
|
521
|
+
available in the namespace.
|
569
522
|
|
523
|
+
:attr:`.Import.alias` must always match for the same reason.
|
524
|
+
"""
|
525
|
+
if isinstance(item, Imports):
|
526
|
+
return all([i in self for i in item.imports])
|
527
|
+
elif isinstance(item, list):
|
528
|
+
return all([i in self for i in item])
|
529
|
+
elif isinstance(item, Import):
|
530
|
+
try:
|
531
|
+
an_import = self[item.module]
|
532
|
+
except KeyError:
|
533
|
+
return False
|
534
|
+
if item.objects is None:
|
535
|
+
return an_import == item
|
536
|
+
else:
|
537
|
+
return all([obj in an_import.objects for obj in item.objects])
|
538
|
+
else:
|
539
|
+
raise TypeError("Imports only contains single Import objects or other Imports\n" f"Got: {type(item)}")
|
570
540
|
|
571
|
-
|
541
|
+
@field_validator("imports", mode="after")
|
542
|
+
@classmethod
|
543
|
+
def imports_are_merged(
|
544
|
+
cls, imports: List[Union[Import, ConditionalImport]]
|
545
|
+
) -> List[Union[Import, ConditionalImport]]:
|
546
|
+
"""
|
547
|
+
When creating from a list of imports, construct model as if we have done so by iteratively
|
548
|
+
constructing with __add__ calls
|
549
|
+
"""
|
550
|
+
merged_imports = []
|
551
|
+
for i in imports:
|
552
|
+
merged_imports = cls._merge(merged_imports, i)
|
553
|
+
return merged_imports
|
554
|
+
|
555
|
+
|
556
|
+
class PydanticModule(PydanticTemplateModel):
|
572
557
|
"""
|
573
558
|
Top-level container model for generating a pydantic module :)
|
574
559
|
"""
|
@@ -588,23 +573,6 @@ class PydanticModule(TemplateModel):
|
|
588
573
|
Metadata for the schema to be included in a linkml_meta module-level instance of LinkMLMeta
|
589
574
|
"""
|
590
575
|
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
def class_names(self) -> List[str]:
|
595
|
-
return [c.name for c in self.classes.values()]
|
596
|
-
|
597
|
-
else:
|
598
|
-
class_names: List[str] = Field(default_factory=list)
|
599
|
-
|
600
|
-
def __init__(self, **kwargs):
|
601
|
-
super(PydanticModule, self).__init__(**kwargs)
|
602
|
-
self.class_names = [c.name for c in self.classes.values()]
|
603
|
-
|
604
|
-
def render(self, environment: Optional[Environment] = None, black: bool = False) -> str:
|
605
|
-
"""
|
606
|
-
Trivial override of parent method for pydantic 1 to ensure that
|
607
|
-
:attr:`.class_names` are correct at render time
|
608
|
-
"""
|
609
|
-
self.class_names = [c.name for c in self.classes.values()]
|
610
|
-
return super(PydanticModule, self).render(environment, black)
|
576
|
+
@computed_field
|
577
|
+
def class_names(self) -> List[str]:
|
578
|
+
return [c.name for c in self.classes.values()]
|
@@ -1,4 +1,4 @@
|
|
1
|
-
{{name}}: {{
|
1
|
+
{{name}}: {{ range }} = Field({{ field }}
|
2
2
|
{%- if title != None %}, title="{{title}}"{% endif -%}
|
3
3
|
{%- if description %}, description="""{{description}}"""{% endif -%}
|
4
4
|
{%- if equals_number != None %}
|
@@ -7,10 +7,12 @@
|
|
7
7
|
{%- if minimum_value != None %}, ge={{minimum_value}}{% endif -%}
|
8
8
|
{%- if maximum_value != None %}, le={{maximum_value}}{% endif -%}
|
9
9
|
{%- endif -%}
|
10
|
-
{%- if
|
11
|
-
{
|
12
|
-
|
13
|
-
{%
|
10
|
+
{%- if multivalued and exact_cardinality != None -%}
|
11
|
+
, min_length={{exact_cardinality}}, max_length={{exact_cardinality}}
|
12
|
+
{%- elif multivalued -%}
|
13
|
+
{%- if minimum_cardinality != None %}, min_length={{minimum_cardinality}}{% endif -%}
|
14
|
+
{%- if maximum_cardinality != None %}, max_length={{maximum_cardinality}}{% endif -%}
|
15
|
+
{%- endif -%}
|
16
|
+
{%- if meta -%}
|
14
17
|
, json_schema_extra = { "linkml_meta": {{ meta | pprint | indent(width=8) }} }
|
15
|
-
|
16
|
-
{%- endif -%})
|
18
|
+
{%- endif -%})
|
@@ -1,15 +1,3 @@
|
|
1
|
-
{% if pydantic_ver == 1 %}
|
2
|
-
class WeakRefShimBaseModel(BaseModel):
|
3
|
-
__slots__ = '__weakref__'
|
4
|
-
|
5
|
-
class {{ name }}(WeakRefShimBaseModel,
|
6
|
-
validate_assignment = True,
|
7
|
-
validate_all = True,
|
8
|
-
underscore_attrs_are_private = True,
|
9
|
-
extra = "{{ extra_fields }}",
|
10
|
-
arbitrary_types_allowed = True,
|
11
|
-
use_enum_values = True):
|
12
|
-
{% else %}
|
13
1
|
class {{ name }}(BaseModel):
|
14
2
|
model_config = ConfigDict(
|
15
3
|
validate_assignment = True,
|
@@ -19,7 +7,6 @@ class {{ name }}(BaseModel):
|
|
19
7
|
use_enum_values = True,
|
20
8
|
strict = {{ strict }},
|
21
9
|
)
|
22
|
-
{% endif %}
|
23
10
|
{% if fields is not none %}
|
24
11
|
{% for field in fields %}
|
25
12
|
{{ field }}
|
@@ -4,8 +4,8 @@ class {{ name }}({% if bases is string %}{{ bases }}{% else %}{{ bases | join(',
|
|
4
4
|
{{ description | indent(width=4) }}
|
5
5
|
"""
|
6
6
|
{% endif -%}
|
7
|
-
{% if meta
|
8
|
-
linkml_meta: ClassVar[LinkMLMeta] = LinkMLMeta({
|
7
|
+
{% if meta %}
|
8
|
+
linkml_meta: ClassVar[LinkMLMeta] = LinkMLMeta({{ meta | pprint | indent(width=8) }})
|
9
9
|
|
10
10
|
{% endif %}
|
11
11
|
{% if attributes or validators %}
|
@@ -1,13 +1,5 @@
|
|
1
|
-
{% if pydantic_ver == 1 %}
|
2
|
-
# Update forward refs
|
3
|
-
# see https://pydantic-docs.helpmanual.io/usage/postponed_annotations/
|
4
|
-
{% for c in class_names -%}
|
5
|
-
{{ c }}.update_forward_refs()
|
6
|
-
{% endfor %}
|
7
|
-
{% else %}
|
8
1
|
# Model rebuild
|
9
2
|
# see https://pydantic-docs.helpmanual.io/usage/models/#rebuilding-a-model
|
10
|
-
|
3
|
+
{% for c in class_names -%}
|
11
4
|
{{ c }}.model_rebuild()
|
12
|
-
|
13
|
-
{% endif %}
|
5
|
+
{% endfor %}
|
@@ -14,8 +14,8 @@ version = "{{version if version else None}}"
|
|
14
14
|
{{ c }}
|
15
15
|
{% endfor %}
|
16
16
|
{% endif %}
|
17
|
-
{% if meta
|
18
|
-
linkml_meta = LinkMLMeta({
|
17
|
+
{% if meta %}
|
18
|
+
linkml_meta = LinkMLMeta({{ meta | pprint | indent(width=4) }} )
|
19
19
|
{% else %}
|
20
20
|
linkml_meta = None
|
21
21
|
{% endif %}
|
linkml/generators/pythongen.py
CHANGED
@@ -54,6 +54,16 @@ class PythonGenerator(Generator):
|
|
54
54
|
gen_slots: bool = True
|
55
55
|
genmeta: bool = False
|
56
56
|
emit_metadata: bool = True
|
57
|
+
dataclass_repr: bool = False
|
58
|
+
"""
|
59
|
+
Whether generated dataclasses should also generate a default __repr__ method.
|
60
|
+
|
61
|
+
Default ``False`` so that the parent :class:`linkml_runtime.utils.yamlutils.YAMLRoot` 's
|
62
|
+
``__repr__`` method is inherited for model pretty printing.
|
63
|
+
|
64
|
+
References:
|
65
|
+
- https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass
|
66
|
+
"""
|
57
67
|
|
58
68
|
def __post_init__(self) -> None:
|
59
69
|
if isinstance(self.schema, Path):
|
@@ -388,7 +398,7 @@ dataclasses._init_fn = dataclasses_init_fn_with_kwargs
|
|
388
398
|
return f"\n{self.class_or_type_name(cls.name)} = Any"
|
389
399
|
|
390
400
|
cd_str = (
|
391
|
-
("\n@dataclass" if slotdefs else "")
|
401
|
+
(f"\n@dataclass(repr={self.dataclass_repr})" if slotdefs else "")
|
392
402
|
+ f"\nclass {self.class_or_type_name(cls.name)}{parentref}:{wrapped_description}"
|
393
403
|
+ f"{self.gen_inherited_slots(cls)}"
|
394
404
|
+ f"{self.gen_class_meta(cls)}"
|
@@ -1171,7 +1181,7 @@ class {enum_name}(EnumDefinitionImpl):
|
|
1171
1181
|
|
1172
1182
|
|
1173
1183
|
@shared_arguments(PythonGenerator)
|
1174
|
-
@click.command()
|
1184
|
+
@click.command(name="python")
|
1175
1185
|
@click.option("--head/--no-head", default=True, show_default=True, help="Emit metadata heading")
|
1176
1186
|
@click.option(
|
1177
1187
|
"--genmeta/--no-genmeta",
|
linkml/generators/rdfgen.py
CHANGED
linkml/generators/shaclgen.py
CHANGED
@@ -11,7 +11,7 @@ from linkml_runtime.utils.schemaview import SchemaView
|
|
11
11
|
from linkml_runtime.utils.yamlutils import TypedNode, extended_float, extended_int, extended_str
|
12
12
|
from rdflib import BNode, Graph, Literal, URIRef
|
13
13
|
from rdflib.collection import Collection
|
14
|
-
from rdflib.namespace import RDF, SH, XSD
|
14
|
+
from rdflib.namespace import RDF, RDFS, SH, XSD
|
15
15
|
|
16
16
|
from linkml._version import __version__
|
17
17
|
from linkml.generators.shacl.ifabsent_processor import IfAbsentProcessor
|
@@ -73,6 +73,10 @@ class ShaclGenerator(Generator):
|
|
73
73
|
class_uri_with_suffix += self.suffix
|
74
74
|
shape_pv(RDF.type, SH.NodeShape)
|
75
75
|
shape_pv(SH.targetClass, class_uri) # TODO
|
76
|
+
|
77
|
+
if c.is_a:
|
78
|
+
shape_pv(RDFS.subClassOf, URIRef(sv.get_uri(c.is_a, expand=True)))
|
79
|
+
|
76
80
|
if self.closed:
|
77
81
|
if c.mixin or c.abstract:
|
78
82
|
shape_pv(SH.closed, Literal(False))
|
@@ -303,7 +307,7 @@ def add_simple_data_type(func: Callable, r: ElementName) -> None:
|
|
303
307
|
|
304
308
|
|
305
309
|
@shared_arguments(ShaclGenerator)
|
306
|
-
@click.command()
|
310
|
+
@click.command(name="shacl")
|
307
311
|
@click.option(
|
308
312
|
"--closed/--non-closed",
|
309
313
|
default=True,
|