linkml 1.9.4rc1__py3-none-any.whl → 1.9.5__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 +5 -1
- linkml/converter/__init__.py +0 -0
- linkml/generators/__init__.py +2 -0
- linkml/generators/common/build.py +5 -20
- linkml/generators/common/template.py +289 -3
- linkml/generators/docgen.py +55 -10
- linkml/generators/erdiagramgen.py +9 -5
- linkml/generators/graphqlgen.py +32 -6
- linkml/generators/jsonldcontextgen.py +78 -12
- linkml/generators/jsonschemagen.py +29 -12
- linkml/generators/mermaidclassdiagramgen.py +21 -3
- linkml/generators/owlgen.py +13 -2
- linkml/generators/panderagen/dataframe_class.py +13 -0
- linkml/generators/panderagen/dataframe_field.py +50 -0
- linkml/generators/panderagen/linkml_pandera_validator.py +186 -0
- linkml/generators/panderagen/panderagen.py +22 -5
- linkml/generators/panderagen/panderagen_class_based/class.jinja2 +70 -13
- linkml/generators/panderagen/panderagen_class_based/custom_checks.jinja2 +27 -0
- linkml/generators/panderagen/panderagen_class_based/enums.jinja2 +3 -3
- linkml/generators/panderagen/panderagen_class_based/pandera.jinja2 +12 -2
- linkml/generators/panderagen/panderagen_class_based/slots.jinja2 +19 -17
- linkml/generators/panderagen/slot_generator_mixin.py +143 -16
- linkml/generators/panderagen/transforms/__init__.py +19 -0
- linkml/generators/panderagen/transforms/collection_dict_model_transform.py +62 -0
- linkml/generators/panderagen/transforms/list_dict_model_transform.py +66 -0
- linkml/generators/panderagen/transforms/model_transform.py +8 -0
- linkml/generators/panderagen/transforms/nested_struct_model_transform.py +27 -0
- linkml/generators/panderagen/transforms/simple_dict_model_transform.py +86 -0
- linkml/generators/plantumlgen.py +17 -11
- linkml/generators/pydanticgen/pydanticgen.py +53 -2
- linkml/generators/pydanticgen/template.py +45 -233
- linkml/generators/pydanticgen/templates/attribute.py.jinja +1 -0
- linkml/generators/pydanticgen/templates/base_model.py.jinja +16 -2
- linkml/generators/pydanticgen/templates/imports.py.jinja +1 -1
- linkml/generators/rdfgen.py +11 -2
- linkml/generators/rustgen/__init__.py +3 -0
- linkml/generators/rustgen/build.py +97 -0
- linkml/generators/rustgen/cli.py +83 -0
- linkml/generators/rustgen/rustgen.py +1186 -0
- linkml/generators/rustgen/template.py +910 -0
- linkml/generators/rustgen/templates/Cargo.toml.jinja +42 -0
- linkml/generators/rustgen/templates/anything.rs.jinja +149 -0
- linkml/generators/rustgen/templates/as_key_value.rs.jinja +86 -0
- linkml/generators/rustgen/templates/class_module.rs.jinja +8 -0
- linkml/generators/rustgen/templates/enum.rs.jinja +70 -0
- linkml/generators/rustgen/templates/file.rs.jinja +75 -0
- linkml/generators/rustgen/templates/import.rs.jinja +4 -0
- linkml/generators/rustgen/templates/imports.rs.jinja +8 -0
- linkml/generators/rustgen/templates/lib_shim.rs.jinja +52 -0
- linkml/generators/rustgen/templates/poly.rs.jinja +9 -0
- linkml/generators/rustgen/templates/poly_containers.rs.jinja +439 -0
- linkml/generators/rustgen/templates/poly_trait.rs.jinja +15 -0
- linkml/generators/rustgen/templates/poly_trait_impl.rs.jinja +5 -0
- linkml/generators/rustgen/templates/poly_trait_impl_orsubtype.rs.jinja +5 -0
- linkml/generators/rustgen/templates/poly_trait_property.rs.jinja +8 -0
- linkml/generators/rustgen/templates/poly_trait_property_impl.rs.jinja +134 -0
- linkml/generators/rustgen/templates/poly_trait_property_match.rs.jinja +10 -0
- linkml/generators/rustgen/templates/property.rs.jinja +28 -0
- linkml/generators/rustgen/templates/pyproject.toml.jinja +10 -0
- linkml/generators/rustgen/templates/serde_utils.rs.jinja +490 -0
- linkml/generators/rustgen/templates/slot_range_as_union.rs.jinja +64 -0
- linkml/generators/rustgen/templates/struct.rs.jinja +81 -0
- linkml/generators/rustgen/templates/struct_or_subtype_enum.rs.jinja +111 -0
- linkml/generators/rustgen/templates/stub_gen.rs.jinja +71 -0
- linkml/generators/rustgen/templates/stub_utils.rs.jinja +76 -0
- linkml/generators/rustgen/templates/typealias.rs.jinja +13 -0
- linkml/generators/sqltablegen.py +18 -16
- linkml/generators/yarrrmlgen.py +173 -0
- linkml/linter/config/datamodel/config.py +160 -293
- linkml/linter/config/datamodel/config.yaml +34 -26
- linkml/linter/config/default.yaml +4 -0
- linkml/linter/config/recommended.yaml +4 -0
- linkml/linter/linter.py +1 -2
- linkml/linter/rules.py +37 -0
- linkml/utils/schema_builder.py +2 -0
- linkml/utils/schemaloader.py +76 -3
- {linkml-1.9.4rc1.dist-info → linkml-1.9.5.dist-info}/METADATA +2 -2
- {linkml-1.9.4rc1.dist-info → linkml-1.9.5.dist-info}/RECORD +82 -40
- {linkml-1.9.4rc1.dist-info → linkml-1.9.5.dist-info}/entry_points.txt +2 -1
- linkml/generators/panderagen/panderagen_class_based/mixins.jinja2 +0 -26
- /linkml/{utils/converter.py → converter/cli.py} +0 -0
- {linkml-1.9.4rc1.dist-info → linkml-1.9.5.dist-info}/WHEEL +0 -0
- {linkml-1.9.4rc1.dist-info → linkml-1.9.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
import sys
|
|
2
|
-
from collections.abc import Generator
|
|
3
2
|
from importlib.util import find_spec
|
|
3
|
+
from keyword import iskeyword
|
|
4
4
|
from typing import Any, ClassVar, Literal, Optional, Union, get_args
|
|
5
5
|
|
|
6
|
+
try:
|
|
7
|
+
from typing import Self
|
|
8
|
+
except ImportError:
|
|
9
|
+
from typing_extensions import Self
|
|
10
|
+
|
|
6
11
|
from jinja2 import Environment, PackageLoader
|
|
7
|
-
from pydantic import BaseModel, Field, field_validator
|
|
12
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
8
13
|
from pydantic.version import VERSION as PYDANTIC_VERSION
|
|
9
14
|
|
|
10
|
-
from linkml.generators.common.template import
|
|
15
|
+
from linkml.generators.common.template import (
|
|
16
|
+
ConditionalImport as ConditionalImport_,
|
|
17
|
+
)
|
|
18
|
+
from linkml.generators.common.template import (
|
|
19
|
+
Import as Import_,
|
|
20
|
+
)
|
|
21
|
+
from linkml.generators.common.template import (
|
|
22
|
+
Imports as Imports_,
|
|
23
|
+
)
|
|
24
|
+
from linkml.generators.common.template import (
|
|
25
|
+
ObjectImport, # noqa: F401
|
|
26
|
+
TemplateModel,
|
|
27
|
+
)
|
|
11
28
|
from linkml.utils.deprecation import deprecation_warning
|
|
12
29
|
|
|
13
30
|
try:
|
|
@@ -113,9 +130,19 @@ class EnumValue(BaseModel):
|
|
|
113
130
|
"""
|
|
114
131
|
|
|
115
132
|
label: str
|
|
133
|
+
alias: Optional[str] = None
|
|
116
134
|
value: str
|
|
117
135
|
description: Optional[str] = None
|
|
118
136
|
|
|
137
|
+
@model_validator(mode="after")
|
|
138
|
+
def alias_python_keywords(self) -> Self:
|
|
139
|
+
"""Mask Python keywords used for `label` by appending `_`"""
|
|
140
|
+
if iskeyword(self.label):
|
|
141
|
+
if self.alias is None:
|
|
142
|
+
self.alias = self.label
|
|
143
|
+
self.label = self.label + "_"
|
|
144
|
+
return self
|
|
145
|
+
|
|
119
146
|
|
|
120
147
|
class PydanticEnum(PydanticTemplateModel):
|
|
121
148
|
"""
|
|
@@ -170,6 +197,7 @@ class PydanticAttribute(PydanticTemplateModel):
|
|
|
170
197
|
meta_exclude: ClassVar[list[str]] = ["from_schema", "owner", "range", "inlined", "inlined_as_list"]
|
|
171
198
|
|
|
172
199
|
name: str
|
|
200
|
+
alias: Optional[str] = None
|
|
173
201
|
required: bool = False
|
|
174
202
|
identifier: bool = False
|
|
175
203
|
key: bool = False
|
|
@@ -200,8 +228,19 @@ class PydanticAttribute(PydanticTemplateModel):
|
|
|
200
228
|
elif self.required or self.identifier or self.key:
|
|
201
229
|
return "..."
|
|
202
230
|
else:
|
|
231
|
+
if self.range and self.range.startswith("Optional[list"):
|
|
232
|
+
return "[]"
|
|
203
233
|
return "None"
|
|
204
234
|
|
|
235
|
+
@model_validator(mode="after")
|
|
236
|
+
def alias_python_keywords(self) -> Self:
|
|
237
|
+
"""Mask Python keywords used for `name` by appending `_`"""
|
|
238
|
+
if iskeyword(self.name):
|
|
239
|
+
if self.alias is None:
|
|
240
|
+
self.alias = self.name
|
|
241
|
+
self.name = self.name + "_"
|
|
242
|
+
return self
|
|
243
|
+
|
|
205
244
|
|
|
206
245
|
class PydanticValidator(PydanticAttribute):
|
|
207
246
|
"""
|
|
@@ -250,18 +289,7 @@ class PydanticClass(PydanticTemplateModel):
|
|
|
250
289
|
return self.attributes
|
|
251
290
|
|
|
252
291
|
|
|
253
|
-
class
|
|
254
|
-
"""
|
|
255
|
-
An object to be imported from within a module.
|
|
256
|
-
|
|
257
|
-
See :class:`.Import` for examples
|
|
258
|
-
"""
|
|
259
|
-
|
|
260
|
-
name: str
|
|
261
|
-
alias: Optional[str] = None
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
class Import(PydanticTemplateModel):
|
|
292
|
+
class Import(Import_, PydanticTemplateModel):
|
|
265
293
|
"""
|
|
266
294
|
A python module, or module and classes to be imported.
|
|
267
295
|
|
|
@@ -292,15 +320,6 @@ class Import(PydanticTemplateModel):
|
|
|
292
320
|
"""
|
|
293
321
|
|
|
294
322
|
template: ClassVar[str] = "imports.py.jinja"
|
|
295
|
-
module: str
|
|
296
|
-
alias: Optional[str] = None
|
|
297
|
-
objects: Optional[list[ObjectImport]] = None
|
|
298
|
-
is_schema: bool = False
|
|
299
|
-
"""
|
|
300
|
-
Whether or not this ``Import`` is importing another schema imported by the main schema --
|
|
301
|
-
ie. that it is not expected to be provided by the environment, but imported locally from within the package.
|
|
302
|
-
Used primarily in split schema generation, see :func:`.pydanticgen.generate_split` for example usage.
|
|
303
|
-
"""
|
|
304
323
|
|
|
305
324
|
@computed_field
|
|
306
325
|
def group(self) -> IMPORT_GROUPS:
|
|
@@ -324,72 +343,8 @@ class Import(PydanticTemplateModel):
|
|
|
324
343
|
else:
|
|
325
344
|
return "thirdparty"
|
|
326
345
|
|
|
327
|
-
def merge(self, other: "Import") -> list["Import"]:
|
|
328
|
-
"""
|
|
329
|
-
Merge one import with another, see :meth:`.Imports` for an example.
|
|
330
|
-
|
|
331
|
-
* If module don't match, return both
|
|
332
|
-
* If one or the other are a :class:`.ConditionalImport`, return both
|
|
333
|
-
* If modules match, neither contain objects, but the other has an alias, return the other
|
|
334
|
-
* If modules match, one contains objects but the other doesn't, return both
|
|
335
|
-
* If modules match, both contain objects, merge the object lists, preferring objects with aliases
|
|
336
|
-
"""
|
|
337
|
-
# return both if we are orthogonal
|
|
338
|
-
if self.module != other.module:
|
|
339
|
-
return [self, other]
|
|
340
|
-
|
|
341
|
-
# handle conditionals
|
|
342
|
-
if isinstance(self, ConditionalImport) and isinstance(other, ConditionalImport):
|
|
343
|
-
# If our condition is the same, return the newer version
|
|
344
|
-
if self.condition == other.condition:
|
|
345
|
-
return [other]
|
|
346
|
-
if isinstance(self, ConditionalImport) or isinstance(other, ConditionalImport):
|
|
347
|
-
# we don't have a good way of combining conditionals, just return both
|
|
348
|
-
return [self, other]
|
|
349
|
-
|
|
350
|
-
# handle module vs. object imports
|
|
351
|
-
elif other.objects is None and self.objects is None:
|
|
352
|
-
# both are modules, return the other only if it updates the alias
|
|
353
|
-
if other.alias:
|
|
354
|
-
return [other]
|
|
355
|
-
else:
|
|
356
|
-
return [self]
|
|
357
|
-
elif other.objects is not None and self.objects is not None:
|
|
358
|
-
# both are object imports, merge and return
|
|
359
|
-
alias = self.alias if other.alias is None else other.alias
|
|
360
|
-
# FIXME: super awkward implementation
|
|
361
|
-
# keep ours if it has an alias and the other doesn't,
|
|
362
|
-
# otherwise take the other's version
|
|
363
|
-
self_objs = {obj.name: obj for obj in self.objects}
|
|
364
|
-
other_objs = {
|
|
365
|
-
obj.name: obj for obj in other.objects if obj.name not in self_objs or self_objs[obj.name].alias is None
|
|
366
|
-
}
|
|
367
|
-
self_objs.update(other_objs)
|
|
368
|
-
|
|
369
|
-
return [
|
|
370
|
-
Import(
|
|
371
|
-
module=self.module,
|
|
372
|
-
alias=alias,
|
|
373
|
-
objects=list(self_objs.values()),
|
|
374
|
-
is_schema=self.is_schema or other.is_schema,
|
|
375
|
-
)
|
|
376
|
-
]
|
|
377
|
-
else:
|
|
378
|
-
# one is a module, the other imports objects, keep both
|
|
379
|
-
return [self, other]
|
|
380
|
-
|
|
381
|
-
def sort(self) -> None:
|
|
382
|
-
"""
|
|
383
|
-
Sort imported objects
|
|
384
|
-
|
|
385
|
-
* First by whether the first letter is capitalized or not,
|
|
386
|
-
* Then alphabetically (by object name rather than alias)
|
|
387
|
-
"""
|
|
388
|
-
if self.objects:
|
|
389
|
-
self.objects = sorted(self.objects, key=lambda obj: (obj.name[0].islower(), obj.name))
|
|
390
|
-
|
|
391
346
|
|
|
392
|
-
class ConditionalImport(
|
|
347
|
+
class ConditionalImport(ConditionalImport_, PydanticTemplateModel):
|
|
393
348
|
"""
|
|
394
349
|
Import that depends on some condition in the environment, common when
|
|
395
350
|
using backported features or straddling dependency versions.
|
|
@@ -430,22 +385,13 @@ class ConditionalImport(Import):
|
|
|
430
385
|
"""
|
|
431
386
|
|
|
432
387
|
template: ClassVar[str] = "conditional_import.py.jinja"
|
|
433
|
-
condition: str
|
|
434
|
-
alternative: Import
|
|
435
388
|
|
|
436
389
|
@computed_field
|
|
437
390
|
def group(self) -> Literal["conditional"]:
|
|
438
391
|
return "conditional"
|
|
439
392
|
|
|
440
|
-
def sort(self) -> None:
|
|
441
|
-
"""
|
|
442
|
-
:meth:`.Import.sort` called for self and :attr:`.alternative`
|
|
443
|
-
"""
|
|
444
|
-
super().sort()
|
|
445
|
-
self.alternative.sort()
|
|
446
|
-
|
|
447
393
|
|
|
448
|
-
class Imports(PydanticTemplateModel):
|
|
394
|
+
class Imports(Imports_, PydanticTemplateModel):
|
|
449
395
|
"""
|
|
450
396
|
Container class for imports that can handle merging!
|
|
451
397
|
|
|
@@ -482,135 +428,6 @@ class Imports(PydanticTemplateModel):
|
|
|
482
428
|
imports: list[Union[Import, ConditionalImport]] = Field(default_factory=list)
|
|
483
429
|
group_order: tuple[str, ...] = get_args(IMPORT_GROUPS)
|
|
484
430
|
"""Order in which to sort imports by their :attr:`.Import.group`"""
|
|
485
|
-
render_sorted: bool = True
|
|
486
|
-
"""When rendering, render in sorted groups"""
|
|
487
|
-
|
|
488
|
-
@classmethod
|
|
489
|
-
def _merge(
|
|
490
|
-
cls, imports: list[Union[Import, ConditionalImport]], other: Union[Import, "Imports", list[Import]]
|
|
491
|
-
) -> list[Union[Import, ConditionalImport]]:
|
|
492
|
-
"""
|
|
493
|
-
Add a new import to an existing imports list, handling deduplication and flattening.
|
|
494
|
-
|
|
495
|
-
Mutates and returns ``imports``
|
|
496
|
-
|
|
497
|
-
Generally will prefer the imports in ``other`` , updating those in ``imports``.
|
|
498
|
-
If ``other`` ...
|
|
499
|
-
- doesn't match any ``module`` in ``imports``, add it!
|
|
500
|
-
- matches a single ``module`` in imports, :meth:`.Import.merge` the object imports
|
|
501
|
-
- matches multiple ``module``s in imports, then there must have been another
|
|
502
|
-
:class:`.ConditionalImport` already present, so we :meth:`.Import.merge` the existing
|
|
503
|
-
:class:`.Import` if it is one, and if it's a :class:`.ConditionalImport` just YOLO
|
|
504
|
-
and append it since there isn't a principled way to merge them from strings.
|
|
505
|
-
- is :class:`.Imports` or a list of :class:`.Import` s, call this recursively for each
|
|
506
|
-
item.
|
|
507
|
-
|
|
508
|
-
Since imports can be merged in an undefined order depending on the generator configuration,
|
|
509
|
-
default behavior for imports with matching ``module`` is to remove them and append to the
|
|
510
|
-
end of the imports list (rather than keeping it in the position of the existing
|
|
511
|
-
:class:`.Import` ). :class:`.ConditionalImports` make it possible to have namespace
|
|
512
|
-
conflicts, so in imperative import style we assume the most recently added :class:`.Import`
|
|
513
|
-
is the one that should prevail.
|
|
514
|
-
"""
|
|
515
|
-
#
|
|
516
|
-
if isinstance(other, Imports) or (isinstance(other, list) and all([isinstance(i, Import) for i in other])):
|
|
517
|
-
for i in other:
|
|
518
|
-
imports = cls._merge(imports, i)
|
|
519
|
-
return imports
|
|
520
|
-
|
|
521
|
-
existing = [i for i in imports if i.module == other.module]
|
|
522
|
-
|
|
523
|
-
# if we have nothing importing from this module yet, add it!
|
|
524
|
-
if len(existing) == 0:
|
|
525
|
-
imports.append(other)
|
|
526
|
-
elif len(existing) == 1:
|
|
527
|
-
imports.remove(existing[0])
|
|
528
|
-
imports.extend(existing[0].merge(other))
|
|
529
|
-
else:
|
|
530
|
-
# we have both a conditional and at least one nonconditional already.
|
|
531
|
-
# If this is another conditional, we just add it, otherwise, we merge it
|
|
532
|
-
# with the single nonconditional
|
|
533
|
-
if isinstance(other, ConditionalImport):
|
|
534
|
-
imports.append(other)
|
|
535
|
-
else:
|
|
536
|
-
for e in existing:
|
|
537
|
-
if isinstance(e, Import):
|
|
538
|
-
imports.remove(e)
|
|
539
|
-
merged = e.merge(other)
|
|
540
|
-
imports.extend(merged)
|
|
541
|
-
break
|
|
542
|
-
|
|
543
|
-
# SPECIAL CASE - __future__ annotations must happen at the top of a file
|
|
544
|
-
# sort here outside of sort method because our imports are invalid without it,
|
|
545
|
-
# where calling ``sort`` should be optional.
|
|
546
|
-
imports = sorted(imports, key=lambda i: i.module == "__future__", reverse=True)
|
|
547
|
-
return imports
|
|
548
|
-
|
|
549
|
-
def __add__(self, other: Union[Import, "Imports", list[Import]]) -> "Imports":
|
|
550
|
-
imports = self.imports.copy()
|
|
551
|
-
imports = self._merge(imports, other)
|
|
552
|
-
return Imports.model_construct(
|
|
553
|
-
imports=imports, **{k: getattr(self, k, None) for k in self.model_fields if k != "imports"}
|
|
554
|
-
)
|
|
555
|
-
|
|
556
|
-
def __len__(self) -> int:
|
|
557
|
-
return len(self.imports)
|
|
558
|
-
|
|
559
|
-
def __iter__(self) -> Generator[Import, None, None]:
|
|
560
|
-
yield from self.imports
|
|
561
|
-
|
|
562
|
-
def __getitem__(self, item: Union[int, str]) -> Import:
|
|
563
|
-
if isinstance(item, int):
|
|
564
|
-
return self.imports[item]
|
|
565
|
-
elif isinstance(item, str):
|
|
566
|
-
# the name of the module
|
|
567
|
-
an_import = [i for i in self.imports if i.module == item]
|
|
568
|
-
if len(an_import) == 0:
|
|
569
|
-
raise KeyError(f"No import with module {item} was found.\nWe have: {self.imports}")
|
|
570
|
-
return an_import[0]
|
|
571
|
-
else:
|
|
572
|
-
raise TypeError(f"Can only index with an int or a string as the name of the module,\nGot: {type(item)}")
|
|
573
|
-
|
|
574
|
-
def __contains__(self, item: Union[Import, "Imports", list[Import]]) -> bool:
|
|
575
|
-
"""
|
|
576
|
-
Check if all the objects are imported from the given module(s)
|
|
577
|
-
|
|
578
|
-
If the import is a bare module import (ie its :attr:`~.Import.objects` is ``None`` )
|
|
579
|
-
then we must also have a bare module import in this Imports (because even if
|
|
580
|
-
we import from the module, unless we import it specifically its name won't be
|
|
581
|
-
available in the namespace.
|
|
582
|
-
|
|
583
|
-
:attr:`.Import.alias` must always match for the same reason.
|
|
584
|
-
"""
|
|
585
|
-
if isinstance(item, Imports):
|
|
586
|
-
return all([i in self for i in item.imports])
|
|
587
|
-
elif isinstance(item, list):
|
|
588
|
-
return all([i in self for i in item])
|
|
589
|
-
elif isinstance(item, Import):
|
|
590
|
-
try:
|
|
591
|
-
an_import = self[item.module]
|
|
592
|
-
except KeyError:
|
|
593
|
-
return False
|
|
594
|
-
if item.objects is None:
|
|
595
|
-
return an_import == item
|
|
596
|
-
else:
|
|
597
|
-
return all([obj in an_import.objects for obj in item.objects])
|
|
598
|
-
else:
|
|
599
|
-
raise TypeError(f"Imports only contains single Import objects or other Imports\nGot: {type(item)}")
|
|
600
|
-
|
|
601
|
-
@field_validator("imports", mode="after")
|
|
602
|
-
@classmethod
|
|
603
|
-
def imports_are_merged(
|
|
604
|
-
cls, imports: list[Union[Import, ConditionalImport]]
|
|
605
|
-
) -> list[Union[Import, ConditionalImport]]:
|
|
606
|
-
"""
|
|
607
|
-
When creating from a list of imports, construct model as if we have done so by iteratively
|
|
608
|
-
constructing with __add__ calls
|
|
609
|
-
"""
|
|
610
|
-
merged_imports = []
|
|
611
|
-
for i in imports:
|
|
612
|
-
merged_imports = cls._merge(merged_imports, i)
|
|
613
|
-
return merged_imports
|
|
614
431
|
|
|
615
432
|
@computed_field
|
|
616
433
|
def import_groups(self) -> list[IMPORT_GROUPS]:
|
|
@@ -637,11 +454,6 @@ class Imports(PydanticTemplateModel):
|
|
|
637
454
|
i.sort()
|
|
638
455
|
self.imports = imports
|
|
639
456
|
|
|
640
|
-
def render(self, environment: Optional[Environment] = None, black: bool = False) -> str:
|
|
641
|
-
if self.render_sorted:
|
|
642
|
-
self.sort()
|
|
643
|
-
return super().render(environment=environment, black=black)
|
|
644
|
-
|
|
645
457
|
|
|
646
458
|
class PydanticModule(PydanticTemplateModel):
|
|
647
459
|
"""
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
class {{ name }}(BaseModel):
|
|
2
2
|
model_config = ConfigDict(
|
|
3
|
+
serialize_by_alias = True,
|
|
4
|
+
validate_by_name = True,
|
|
3
5
|
validate_assignment = True,
|
|
4
6
|
validate_default = True,
|
|
5
7
|
extra = "{{ extra_fields }}",
|
|
@@ -11,6 +13,18 @@ class {{ name }}(BaseModel):
|
|
|
11
13
|
{% for field in fields %}
|
|
12
14
|
{{ field }}
|
|
13
15
|
{% endfor %}
|
|
14
|
-
{% else %}
|
|
15
|
-
{{ "pass" }}
|
|
16
16
|
{% endif %}
|
|
17
|
+
|
|
18
|
+
@model_serializer(mode='wrap', when_used='unless-none')
|
|
19
|
+
def treat_empty_lists_as_none(
|
|
20
|
+
self, handler: SerializerFunctionWrapHandler,
|
|
21
|
+
info: SerializationInfo) -> dict[str, Any]:
|
|
22
|
+
if info.exclude_none:
|
|
23
|
+
_instance = self.model_copy()
|
|
24
|
+
for field, field_info in type(_instance).model_fields.items():
|
|
25
|
+
if getattr(_instance, field) == [] and not(
|
|
26
|
+
field_info.is_required()):
|
|
27
|
+
setattr(_instance, field, None)
|
|
28
|
+
else:
|
|
29
|
+
_instance = self
|
|
30
|
+
return handler(_instance, info)
|
|
@@ -5,7 +5,7 @@ import {{ module }}
|
|
|
5
5
|
import {{ module }} as {{ alias }}
|
|
6
6
|
{%- else %}
|
|
7
7
|
{% if objects | length == 1 %}
|
|
8
|
-
from {{ module }} import {{ objects[0]['name'] }}
|
|
8
|
+
from {{ module }} import {{ objects[0]['name'] }}{% if objects[0]['alias'] is not none %} as {{ objects[0]['alias'] }}{% endif %}
|
|
9
9
|
{%- else %}
|
|
10
10
|
from {{ module }} import (
|
|
11
11
|
{% for object in objects %}
|
linkml/generators/rdfgen.py
CHANGED
|
@@ -67,12 +67,21 @@ class RDFGenerator(Generator):
|
|
|
67
67
|
base=str(self.namespaces._base),
|
|
68
68
|
prefix=True,
|
|
69
69
|
)
|
|
70
|
-
out = self._data(graph)
|
|
71
70
|
if output:
|
|
71
|
+
# Binary-safe when -o/--output is used:
|
|
72
|
+
# delegate to RDFLib (Graph.serialize(destination=..., format=...)).
|
|
73
|
+
# Serializers that produce bytes write directly to the file; stdout stays empty.
|
|
74
|
+
fmt = "turtle" if self.format == "ttl" else self.format
|
|
75
|
+
try:
|
|
76
|
+
out = graph.serialize(format=fmt)
|
|
77
|
+
except UnicodeDecodeError:
|
|
78
|
+
graph.serialize(destination=output, format=fmt)
|
|
79
|
+
return ""
|
|
72
80
|
with open(output, "w", encoding="UTF-8") as outf:
|
|
73
81
|
outf.write(out)
|
|
82
|
+
return out
|
|
74
83
|
|
|
75
|
-
return
|
|
84
|
+
return self._data(graph)
|
|
76
85
|
|
|
77
86
|
|
|
78
87
|
@shared_arguments(RDFGenerator)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from pydantic import Field
|
|
2
|
+
|
|
3
|
+
from linkml.generators.common.build import (
|
|
4
|
+
BuildResult,
|
|
5
|
+
SchemaResult,
|
|
6
|
+
)
|
|
7
|
+
from linkml.generators.common.build import (
|
|
8
|
+
ClassResult as ClassResult_,
|
|
9
|
+
)
|
|
10
|
+
from linkml.generators.common.build import EnumResult as EnumResult_
|
|
11
|
+
from linkml.generators.common.build import (
|
|
12
|
+
SlotResult as SlotResult_,
|
|
13
|
+
)
|
|
14
|
+
from linkml.generators.common.build import TypeResult as TypeResult_
|
|
15
|
+
from linkml.generators.rustgen.template import (
|
|
16
|
+
Imports,
|
|
17
|
+
RustCargo,
|
|
18
|
+
RustEnum,
|
|
19
|
+
RustFile,
|
|
20
|
+
RustProperty,
|
|
21
|
+
RustPyProject,
|
|
22
|
+
RustStruct,
|
|
23
|
+
RustTemplateModel,
|
|
24
|
+
RustTypeAlias,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RustBuildResult(BuildResult):
|
|
29
|
+
"""
|
|
30
|
+
BuildResult parent class for rustgen
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
imports: Imports = Imports()
|
|
34
|
+
|
|
35
|
+
def merge(self, other: "RustBuildResult") -> "RustBuildResult":
|
|
36
|
+
self.imports += other.imports
|
|
37
|
+
return self
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TypeResult(RustBuildResult, TypeResult_):
|
|
41
|
+
"""
|
|
42
|
+
A linkml type as a type alias
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
type_: RustTypeAlias
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ClassResult(RustBuildResult, ClassResult_):
|
|
49
|
+
"""
|
|
50
|
+
A single built rust struct
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
cls: RustStruct
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SlotResult(RustBuildResult, SlotResult_):
|
|
57
|
+
"""
|
|
58
|
+
A type alias
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
slot: RustTypeAlias
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AttributeResult(RustBuildResult, SlotResult_):
|
|
65
|
+
"""
|
|
66
|
+
A field within a rust struct
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
attribute: RustProperty
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class EnumResult(RustBuildResult, EnumResult_):
|
|
73
|
+
"""
|
|
74
|
+
A rust enum!
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
enum: RustEnum
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class CrateResult(RustBuildResult, SchemaResult):
|
|
81
|
+
"""
|
|
82
|
+
A schema built into a rust crate
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
cargo: RustCargo
|
|
86
|
+
file: RustFile
|
|
87
|
+
extra_files: dict[str, RustTemplateModel]
|
|
88
|
+
pyproject: RustPyProject
|
|
89
|
+
bin_files: dict[str, RustTemplateModel] = Field(default_factory=dict)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class FileResult(RustBuildResult, SchemaResult):
|
|
93
|
+
"""
|
|
94
|
+
A schema built into a single rust file
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
file: RustFile
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional, get_args
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from linkml._version import __version__
|
|
7
|
+
from linkml.generators.rustgen import RUST_MODES, RustGenerator
|
|
8
|
+
from linkml.utils.generator import shared_arguments
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@shared_arguments(RustGenerator)
|
|
12
|
+
@click.option(
|
|
13
|
+
"-m",
|
|
14
|
+
"--mode",
|
|
15
|
+
type=click.Choice([a for a in get_args(RUST_MODES)]),
|
|
16
|
+
default="crate",
|
|
17
|
+
help="Generation mode: 'crate' (Cargo package) or 'file' (single .rs)",
|
|
18
|
+
)
|
|
19
|
+
@click.option(
|
|
20
|
+
"-f",
|
|
21
|
+
"--force",
|
|
22
|
+
is_flag=True,
|
|
23
|
+
help="Overwrite output if it already exists",
|
|
24
|
+
)
|
|
25
|
+
@click.option(
|
|
26
|
+
"-p",
|
|
27
|
+
"--pyo3",
|
|
28
|
+
is_flag=True,
|
|
29
|
+
help=(
|
|
30
|
+
"Add 'pyo3' to Cargo.toml default features and emit Python module glue (cdylib + #[pymodule]). "
|
|
31
|
+
'Source always includes #[cfg(feature="pyo3")] gates; this flag only enables the crate feature by default.'
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
@click.option(
|
|
35
|
+
"-s",
|
|
36
|
+
"--serde",
|
|
37
|
+
is_flag=True,
|
|
38
|
+
help=(
|
|
39
|
+
"Add 'serde' to Cargo.toml default features. Source always includes #[cfg(feature=\"serde\")] derives/attrs; "
|
|
40
|
+
"this flag only enables the crate feature by default."
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
@click.option(
|
|
44
|
+
"--handwritten-lib/--no-handwritten-lib",
|
|
45
|
+
default=False,
|
|
46
|
+
help=(
|
|
47
|
+
"When enabled, place generated sources under src/generated and create a shim lib.rs for handwritten code. "
|
|
48
|
+
"The shim is only created on first run and left untouched on subsequent regenerations."
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
@click.option("-n", "--crate-name", type=str, default=None, help="Name of the generated crate/module")
|
|
52
|
+
@click.option(
|
|
53
|
+
"-o",
|
|
54
|
+
"--output",
|
|
55
|
+
type=click.Path(dir_okay=True),
|
|
56
|
+
help="Output directory (crate mode) or .rs file (file mode)",
|
|
57
|
+
)
|
|
58
|
+
@click.version_option(__version__, "-V", "--version")
|
|
59
|
+
@click.command(name="rust")
|
|
60
|
+
def cli(
|
|
61
|
+
yamlfile: Path,
|
|
62
|
+
mode: RUST_MODES = "crate",
|
|
63
|
+
force: bool = False,
|
|
64
|
+
pyo3: bool = False,
|
|
65
|
+
serde: bool = False,
|
|
66
|
+
crate_name: Optional[str] = None,
|
|
67
|
+
handwritten_lib: bool = False,
|
|
68
|
+
output: Optional[Path] = None,
|
|
69
|
+
**kwargs,
|
|
70
|
+
):
|
|
71
|
+
gen = RustGenerator(
|
|
72
|
+
yamlfile,
|
|
73
|
+
mode=mode,
|
|
74
|
+
pyo3=pyo3,
|
|
75
|
+
serde=serde,
|
|
76
|
+
output=output,
|
|
77
|
+
crate_name=crate_name,
|
|
78
|
+
handwritten_lib=handwritten_lib,
|
|
79
|
+
**kwargs,
|
|
80
|
+
)
|
|
81
|
+
serialized = gen.serialize(force=force)
|
|
82
|
+
if output is None:
|
|
83
|
+
print(serialized)
|