linkml 1.9.5rc1__py3-none-any.whl → 1.9.5rc2__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 +1 -1
- linkml/converter/__init__.py +0 -0
- linkml/generators/owlgen.py +9 -1
- linkml/generators/rustgen/build.py +3 -0
- linkml/generators/rustgen/cli.py +19 -1
- linkml/generators/rustgen/rustgen.py +169 -21
- linkml/generators/rustgen/template.py +51 -6
- linkml/generators/rustgen/templates/Cargo.toml.jinja +2 -2
- linkml/generators/rustgen/templates/anything.rs.jinja +8 -1
- linkml/generators/rustgen/templates/as_key_value.rs.jinja +33 -3
- linkml/generators/rustgen/templates/enum.rs.jinja +16 -0
- linkml/generators/rustgen/templates/file.rs.jinja +13 -0
- linkml/generators/rustgen/templates/lib_shim.rs.jinja +52 -0
- linkml/generators/rustgen/templates/poly_trait_property_impl.rs.jinja +3 -1
- linkml/generators/rustgen/templates/property.rs.jinja +12 -3
- linkml/generators/rustgen/templates/serde_utils.rs.jinja +186 -6
- linkml/generators/rustgen/templates/slot_range_as_union.rs.jinja +3 -0
- linkml/generators/rustgen/templates/struct.rs.jinja +6 -0
- linkml/generators/rustgen/templates/struct_or_subtype_enum.rs.jinja +4 -1
- linkml/generators/rustgen/templates/stub_gen.rs.jinja +71 -0
- linkml/generators/rustgen/templates/stub_utils.rs.jinja +76 -0
- linkml/generators/yarrrmlgen.py +20 -4
- linkml/utils/schema_builder.py +2 -0
- {linkml-1.9.5rc1.dist-info → linkml-1.9.5rc2.dist-info}/METADATA +1 -1
- {linkml-1.9.5rc1.dist-info → linkml-1.9.5rc2.dist-info}/RECORD +29 -25
- {linkml-1.9.5rc1.dist-info → linkml-1.9.5rc2.dist-info}/entry_points.txt +1 -1
- /linkml/{utils/converter.py → converter/cli.py} +0 -0
- {linkml-1.9.5rc1.dist-info → linkml-1.9.5rc2.dist-info}/WHEEL +0 -0
- {linkml-1.9.5rc1.dist-info → linkml-1.9.5rc2.dist-info}/licenses/LICENSE +0 -0
linkml/cli/main.py
CHANGED
|
@@ -7,6 +7,7 @@ Gathers all the other linkml click entrypoints and puts them under ``linkml`` :)
|
|
|
7
7
|
import click
|
|
8
8
|
|
|
9
9
|
from linkml._version import __version__
|
|
10
|
+
from linkml.converter.cli import cli as linkml_convert
|
|
10
11
|
from linkml.generators.csvgen import cli as gen_csv
|
|
11
12
|
from linkml.generators.dbmlgen import cli as gen_dbml
|
|
12
13
|
from linkml.generators.docgen import cli as gen_doc
|
|
@@ -46,7 +47,6 @@ from linkml.generators.yamlgen import cli as gen_yaml
|
|
|
46
47
|
from linkml.generators.yarrrmlgen import cli as gen_yarrrml
|
|
47
48
|
from linkml.generators.yumlgen import cli as gen_yuml
|
|
48
49
|
from linkml.linter.cli import main as linkml_lint
|
|
49
|
-
from linkml.utils.converter import cli as linkml_convert
|
|
50
50
|
from linkml.utils.execute_tutorial import cli as run_tutorial
|
|
51
51
|
from linkml.utils.schema_fixer import main as linkml_schema_fixer
|
|
52
52
|
from linkml.utils.sqlutils import main as linkml_sqldb
|
|
File without changes
|
linkml/generators/owlgen.py
CHANGED
|
@@ -229,7 +229,7 @@ class OwlSchemaGenerator(Generator):
|
|
|
229
229
|
data = self.graph.serialize(format="turtle" if self.format in ["owl", "ttl"] else self.format)
|
|
230
230
|
return data
|
|
231
231
|
|
|
232
|
-
def add_metadata(self, e: Definition, uri: URIRef) -> None:
|
|
232
|
+
def add_metadata(self, e: Union[Definition, PermissibleValue], uri: URIRef) -> None:
|
|
233
233
|
"""
|
|
234
234
|
Add annotation properties.
|
|
235
235
|
|
|
@@ -866,6 +866,10 @@ class OwlSchemaGenerator(Generator):
|
|
|
866
866
|
if impl.startswith("rdfs:"):
|
|
867
867
|
return RDFS[impl.split(":")[1]]
|
|
868
868
|
if isinstance(default_value, str):
|
|
869
|
+
if default_value.startswith("owl:"):
|
|
870
|
+
return OWL[default_value.split(":")[1]]
|
|
871
|
+
if default_value.startswith("rdfs:"):
|
|
872
|
+
return RDFS[default_value.split(":")[1]]
|
|
869
873
|
return URIRef(default_value)
|
|
870
874
|
return default_value
|
|
871
875
|
|
|
@@ -873,6 +877,8 @@ class OwlSchemaGenerator(Generator):
|
|
|
873
877
|
g = self.graph
|
|
874
878
|
enum_uri = self._enum_uri(e.name)
|
|
875
879
|
g.add((enum_uri, RDF.type, OWL.Class))
|
|
880
|
+
|
|
881
|
+
self.add_metadata(e, enum_uri)
|
|
876
882
|
has_parent = False
|
|
877
883
|
if e.is_a:
|
|
878
884
|
self.graph.add((enum_uri, RDFS.subClassOf, self._enum_uri(e.is_a)))
|
|
@@ -916,6 +922,7 @@ class OwlSchemaGenerator(Generator):
|
|
|
916
922
|
)
|
|
917
923
|
)
|
|
918
924
|
if not isinstance(pv_node, Literal):
|
|
925
|
+
self.add_metadata(pv, pv_node)
|
|
919
926
|
g.add((pv_node, RDF.type, pv_owl_type))
|
|
920
927
|
g.add((pv_node, RDFS.label, Literal(pv.text)))
|
|
921
928
|
# TODO: make this configurable
|
|
@@ -936,6 +943,7 @@ class OwlSchemaGenerator(Generator):
|
|
|
936
943
|
if not has_parent and self.add_root_classes:
|
|
937
944
|
self.graph.add((pv_node, RDFS.subClassOf, URIRef(PermissibleValue.class_class_uri)))
|
|
938
945
|
if all([pv is not None for pv in pv_uris]):
|
|
946
|
+
# every single PV in the enum is not-null
|
|
939
947
|
all_is_class = all([owl_type == OWL.Class for owl_type in owl_types])
|
|
940
948
|
all_is_individual = all([owl_type == OWL.NamedIndividual for owl_type in owl_types])
|
|
941
949
|
all_is_literal = all([owl_type == RDFS.Literal for owl_type in owl_types])
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from pydantic import Field
|
|
2
|
+
|
|
1
3
|
from linkml.generators.common.build import (
|
|
2
4
|
BuildResult,
|
|
3
5
|
SchemaResult,
|
|
@@ -84,6 +86,7 @@ class CrateResult(RustBuildResult, SchemaResult):
|
|
|
84
86
|
file: RustFile
|
|
85
87
|
extra_files: dict[str, RustTemplateModel]
|
|
86
88
|
pyproject: RustPyProject
|
|
89
|
+
bin_files: dict[str, RustTemplateModel] = Field(default_factory=dict)
|
|
87
90
|
|
|
88
91
|
|
|
89
92
|
class FileResult(RustBuildResult, SchemaResult):
|
linkml/generators/rustgen/cli.py
CHANGED
|
@@ -40,6 +40,14 @@ from linkml.utils.generator import shared_arguments
|
|
|
40
40
|
"this flag only enables the crate feature by default."
|
|
41
41
|
),
|
|
42
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
|
+
)
|
|
43
51
|
@click.option("-n", "--crate-name", type=str, default=None, help="Name of the generated crate/module")
|
|
44
52
|
@click.option(
|
|
45
53
|
"-o",
|
|
@@ -56,10 +64,20 @@ def cli(
|
|
|
56
64
|
pyo3: bool = False,
|
|
57
65
|
serde: bool = False,
|
|
58
66
|
crate_name: Optional[str] = None,
|
|
67
|
+
handwritten_lib: bool = False,
|
|
59
68
|
output: Optional[Path] = None,
|
|
60
69
|
**kwargs,
|
|
61
70
|
):
|
|
62
|
-
gen = RustGenerator(
|
|
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
|
+
)
|
|
63
81
|
serialized = gen.serialize(force=force)
|
|
64
82
|
if output is None:
|
|
65
83
|
print(serialized)
|
|
@@ -44,6 +44,7 @@ from linkml.generators.rustgen.template import (
|
|
|
44
44
|
RustEnum,
|
|
45
45
|
RustEnumItem,
|
|
46
46
|
RustFile,
|
|
47
|
+
RustLibShim,
|
|
47
48
|
RustProperty,
|
|
48
49
|
RustPyProject,
|
|
49
50
|
RustRange,
|
|
@@ -53,6 +54,8 @@ from linkml.generators.rustgen.template import (
|
|
|
53
54
|
RustTypeAlias,
|
|
54
55
|
SerdeUtilsFile,
|
|
55
56
|
SlotRangeAsUnion,
|
|
57
|
+
StubGenBin,
|
|
58
|
+
StubUtilsFile,
|
|
56
59
|
)
|
|
57
60
|
from linkml.utils.generator import Generator
|
|
58
61
|
|
|
@@ -143,6 +146,22 @@ PYTHON_IMPORTS = Imports(
|
|
|
143
146
|
]
|
|
144
147
|
)
|
|
145
148
|
|
|
149
|
+
STUBGEN_IMPORTS = Imports(
|
|
150
|
+
imports=[
|
|
151
|
+
Import(
|
|
152
|
+
module="pyo3-stub-gen",
|
|
153
|
+
version="0.13.1",
|
|
154
|
+
objects=[
|
|
155
|
+
ObjectImport(name="define_stub_info_gatherer"),
|
|
156
|
+
ObjectImport(name="derive::gen_stub_pyclass"),
|
|
157
|
+
ObjectImport(name="derive::gen_stub_pymethods"),
|
|
158
|
+
],
|
|
159
|
+
feature_flag="stubgen",
|
|
160
|
+
feature_dependencies=["pyo3"],
|
|
161
|
+
),
|
|
162
|
+
]
|
|
163
|
+
)
|
|
164
|
+
|
|
146
165
|
|
|
147
166
|
class SlotContainerMode(Enum):
|
|
148
167
|
SINGLE_VALUE = "single_value"
|
|
@@ -369,6 +388,10 @@ class RustGenerator(Generator, LifecycleMixin):
|
|
|
369
388
|
pyo3_version: str = ">=0.21.1"
|
|
370
389
|
serde: bool = True
|
|
371
390
|
"""Generate serde derive serialization/deserialization attributes"""
|
|
391
|
+
stubgen: bool = True
|
|
392
|
+
"""Generate pyo3-stub-gen instrumentation alongside PyO3 bindings"""
|
|
393
|
+
handwritten_lib: bool = False
|
|
394
|
+
"""Place generated sources under src/generated and leave src/lib.rs for user code"""
|
|
372
395
|
mode: RUST_MODES = "crate"
|
|
373
396
|
"""Generate a cargo.toml file"""
|
|
374
397
|
output: Optional[Path] = None
|
|
@@ -386,12 +409,33 @@ class RustGenerator(Generator, LifecycleMixin):
|
|
|
386
409
|
self.schemaview: SchemaView = SchemaView(self.schema)
|
|
387
410
|
super().__post_init__()
|
|
388
411
|
|
|
412
|
+
def _select_root_class(self, class_defs: list[ClassDefinition]) -> Optional[ClassDefinition]:
|
|
413
|
+
"""Return the schema-local class marked ``tree_root`` if present."""
|
|
414
|
+
|
|
415
|
+
schema_id = getattr(self.schemaview.schema, "id", None)
|
|
416
|
+
|
|
417
|
+
def is_local(cls: ClassDefinition) -> bool:
|
|
418
|
+
if schema_id is None:
|
|
419
|
+
return cls.from_schema is None
|
|
420
|
+
return cls.from_schema == schema_id
|
|
421
|
+
|
|
422
|
+
local_classes = [cls for cls in class_defs if is_local(cls) and not getattr(cls, "mixin", False)]
|
|
423
|
+
|
|
424
|
+
for cls in local_classes:
|
|
425
|
+
if getattr(cls, "tree_root", False):
|
|
426
|
+
return cls
|
|
427
|
+
|
|
428
|
+
return None
|
|
429
|
+
|
|
389
430
|
def generate_type(self, type_: TypeDefinition) -> TypeResult:
|
|
390
431
|
type_ = self.before_generate_type(type_, self.schemaview)
|
|
391
432
|
res = TypeResult(
|
|
392
433
|
source=type_,
|
|
393
434
|
type_=RustTypeAlias(
|
|
394
|
-
name=get_name(type_),
|
|
435
|
+
name=get_name(type_),
|
|
436
|
+
type_=get_rust_type(type_.base, self.schemaview, self.pyo3),
|
|
437
|
+
pyo3=self.pyo3,
|
|
438
|
+
stubgen=self.stubgen,
|
|
395
439
|
),
|
|
396
440
|
imports=self.get_imports(type_),
|
|
397
441
|
)
|
|
@@ -414,6 +458,7 @@ class RustGenerator(Generator, LifecycleMixin):
|
|
|
414
458
|
items=items,
|
|
415
459
|
pyo3=self.pyo3,
|
|
416
460
|
serde=self.serde,
|
|
461
|
+
stubgen=self.stubgen,
|
|
417
462
|
),
|
|
418
463
|
)
|
|
419
464
|
res = self.after_generate_enum(res, self.schemaview)
|
|
@@ -435,6 +480,7 @@ class RustGenerator(Generator, LifecycleMixin):
|
|
|
435
480
|
multivalued=slot.multivalued,
|
|
436
481
|
pyo3=self.pyo3,
|
|
437
482
|
class_range=class_range,
|
|
483
|
+
stubgen=self.stubgen,
|
|
438
484
|
),
|
|
439
485
|
imports=self.get_imports(slot),
|
|
440
486
|
)
|
|
@@ -466,6 +512,7 @@ class RustGenerator(Generator, LifecycleMixin):
|
|
|
466
512
|
SlotRangeAsUnion(
|
|
467
513
|
slot_name=get_name(a),
|
|
468
514
|
ranges=[get_rust_type(r, self.schemaview, True) for r in ranges],
|
|
515
|
+
stubgen=self.stubgen,
|
|
469
516
|
)
|
|
470
517
|
)
|
|
471
518
|
|
|
@@ -473,6 +520,7 @@ class RustGenerator(Generator, LifecycleMixin):
|
|
|
473
520
|
class_name=get_name(cls),
|
|
474
521
|
class_name_snakecase=underscore(uncamelcase(cls.name)),
|
|
475
522
|
slot_ranges=slot_range_unions,
|
|
523
|
+
stubgen=self.stubgen,
|
|
476
524
|
)
|
|
477
525
|
|
|
478
526
|
attributes = [self.generate_attribute(attr, cls) for attr in induced_attrs]
|
|
@@ -489,6 +537,7 @@ class RustGenerator(Generator, LifecycleMixin):
|
|
|
489
537
|
unsendable=unsendable,
|
|
490
538
|
pyo3=self.pyo3,
|
|
491
539
|
serde=self.serde,
|
|
540
|
+
stubgen=self.stubgen,
|
|
492
541
|
as_key_value=self.generate_class_as_key_value(cls),
|
|
493
542
|
struct_or_subtype_enum=self.gen_struct_or_subtype_enum(cls),
|
|
494
543
|
class_module=cls_mod,
|
|
@@ -511,12 +560,17 @@ class RustGenerator(Generator, LifecycleMixin):
|
|
|
511
560
|
values = get_accepted_type_designator_values(self.schemaview, td, d_class)
|
|
512
561
|
td_mapping[d] = values
|
|
513
562
|
if len(descendants) > 0:
|
|
563
|
+
key_type = "String"
|
|
564
|
+
key_slot = get_key_or_identifier_slot(cls, self.schemaview)
|
|
565
|
+
if key_slot is not None:
|
|
566
|
+
key_type = get_rust_type(key_slot.range, self.schemaview, self.pyo3)
|
|
514
567
|
return RustStructOrSubtypeEnum(
|
|
515
568
|
enum_name=get_name(cls) + "OrSubtype",
|
|
516
569
|
struct_names=[get_name(self.schemaview.get_class(d)) for d in descendants],
|
|
517
570
|
type_designator_name=get_name(td) if td else None,
|
|
518
571
|
as_key_value=get_key_or_identifier_slot(cls, self.schemaview) is not None,
|
|
519
572
|
type_designators=td_mapping,
|
|
573
|
+
key_property_type=key_type,
|
|
520
574
|
)
|
|
521
575
|
return None
|
|
522
576
|
|
|
@@ -525,6 +579,7 @@ class RustGenerator(Generator, LifecycleMixin):
|
|
|
525
579
|
key_attr = None
|
|
526
580
|
value_attrs = []
|
|
527
581
|
value_args_no_default = []
|
|
582
|
+
non_key_attrs = []
|
|
528
583
|
|
|
529
584
|
for attr in induced_attrs:
|
|
530
585
|
if attr.identifier:
|
|
@@ -538,6 +593,7 @@ class RustGenerator(Generator, LifecycleMixin):
|
|
|
538
593
|
return None
|
|
539
594
|
key_attr = attr
|
|
540
595
|
else:
|
|
596
|
+
non_key_attrs.append(attr)
|
|
541
597
|
if not attr.multivalued:
|
|
542
598
|
value_attrs.append(attr)
|
|
543
599
|
if attr.required:
|
|
@@ -547,16 +603,27 @@ class RustGenerator(Generator, LifecycleMixin):
|
|
|
547
603
|
# attribute to serve as the value, do not treat this as a key/value class.
|
|
548
604
|
if len(value_attrs) == 0:
|
|
549
605
|
return None
|
|
606
|
+
value_attr = value_attrs[0]
|
|
607
|
+
simple_dict_possible = (
|
|
608
|
+
len(non_key_attrs) == 1
|
|
609
|
+
and not value_attr.multivalued
|
|
610
|
+
and (
|
|
611
|
+
value_attr.range not in self.schemaview.all_classes()
|
|
612
|
+
or not bool(getattr(value_attr, "inlined", False))
|
|
613
|
+
)
|
|
614
|
+
)
|
|
550
615
|
return AsKeyValue(
|
|
551
616
|
name=get_name(cls),
|
|
552
617
|
key_property_name=get_name(key_attr),
|
|
553
618
|
key_property_type=get_rust_type(key_attr.range, self.schemaview, self.pyo3),
|
|
554
|
-
value_property_name=get_name(
|
|
555
|
-
value_property_type=get_rust_type(
|
|
556
|
-
can_convert_from_primitive=
|
|
619
|
+
value_property_name=get_name(value_attr),
|
|
620
|
+
value_property_type=get_rust_type(value_attr.range, self.schemaview, self.pyo3),
|
|
621
|
+
can_convert_from_primitive=simple_dict_possible,
|
|
557
622
|
can_convert_from_empty=len(value_args_no_default) == 0,
|
|
623
|
+
value_property_optional=not bool(value_attr.required),
|
|
558
624
|
serde=self.serde,
|
|
559
625
|
pyo3=self.pyo3,
|
|
626
|
+
stubgen=self.stubgen,
|
|
560
627
|
)
|
|
561
628
|
return None
|
|
562
629
|
|
|
@@ -583,6 +650,7 @@ class RustGenerator(Generator, LifecycleMixin):
|
|
|
583
650
|
and self.generate_class_as_key_value(self.schemaview.get_class(attr.range)) is not None,
|
|
584
651
|
pyo3=self.pyo3,
|
|
585
652
|
serde=self.serde,
|
|
653
|
+
stubgen=self.stubgen,
|
|
586
654
|
),
|
|
587
655
|
imports=self.get_imports(attr),
|
|
588
656
|
)
|
|
@@ -602,6 +670,7 @@ class RustGenerator(Generator, LifecycleMixin):
|
|
|
602
670
|
pyo3_version=self.pyo3_version,
|
|
603
671
|
pyo3=self.pyo3,
|
|
604
672
|
serde=self.serde,
|
|
673
|
+
stubgen=self.stubgen,
|
|
605
674
|
)
|
|
606
675
|
|
|
607
676
|
def generate_pyproject(self) -> RustPyProject:
|
|
@@ -658,7 +727,10 @@ class RustGenerator(Generator, LifecycleMixin):
|
|
|
658
727
|
slots = self.after_generate_slots(slots, sv)
|
|
659
728
|
|
|
660
729
|
need_merge_crate = False
|
|
661
|
-
|
|
730
|
+
class_defs = [sv.induced_class(c) for c in sv.all_classes(ordered_by=OrderedBy.INHERITANCE)]
|
|
731
|
+
root_class_def = self._select_root_class(class_defs)
|
|
732
|
+
root_struct_name = get_name(root_class_def) if root_class_def is not None else None
|
|
733
|
+
classes = class_defs
|
|
662
734
|
for c in classes:
|
|
663
735
|
if MERGE_ANNOTATION in c.annotations:
|
|
664
736
|
need_merge_crate = True
|
|
@@ -673,6 +745,8 @@ class RustGenerator(Generator, LifecycleMixin):
|
|
|
673
745
|
imports = DEFAULT_IMPORTS.model_copy()
|
|
674
746
|
imports += PYTHON_IMPORTS
|
|
675
747
|
imports += SERDE_IMPORTS
|
|
748
|
+
if self.stubgen:
|
|
749
|
+
imports += STUBGEN_IMPORTS
|
|
676
750
|
if need_merge_crate:
|
|
677
751
|
imports += MERGE_IMPORTS
|
|
678
752
|
for result in [*enums, *slots, *classes]:
|
|
@@ -687,6 +761,9 @@ class RustGenerator(Generator, LifecycleMixin):
|
|
|
687
761
|
structs=[c.cls for c in classes],
|
|
688
762
|
pyo3=self.pyo3,
|
|
689
763
|
serde=self.serde,
|
|
764
|
+
stubgen=self.stubgen,
|
|
765
|
+
handwritten_lib=self.handwritten_lib,
|
|
766
|
+
root_struct_name=root_struct_name,
|
|
690
767
|
)
|
|
691
768
|
|
|
692
769
|
if mode == "crate":
|
|
@@ -694,9 +771,21 @@ class RustGenerator(Generator, LifecycleMixin):
|
|
|
694
771
|
extra_files["serde_utils"] = SerdeUtilsFile()
|
|
695
772
|
extra_files["poly"] = PolyFile(imports=imports, traits=poly_traits)
|
|
696
773
|
extra_files["poly_containers"] = PolyContainersFile()
|
|
774
|
+
if self.stubgen:
|
|
775
|
+
extra_files["stub_utils"] = StubUtilsFile()
|
|
697
776
|
cargo = self.generate_cargo(imports)
|
|
698
777
|
pyproject = self.generate_pyproject()
|
|
699
|
-
|
|
778
|
+
bin_files = {}
|
|
779
|
+
if self.stubgen:
|
|
780
|
+
bin_files["bin/stub_gen"] = StubGenBin(crate_name=cargo.name, stubgen=self.stubgen)
|
|
781
|
+
res = CrateResult(
|
|
782
|
+
cargo=cargo,
|
|
783
|
+
file=file,
|
|
784
|
+
pyproject=pyproject,
|
|
785
|
+
source=sv.schema,
|
|
786
|
+
extra_files=extra_files,
|
|
787
|
+
bin_files=bin_files,
|
|
788
|
+
)
|
|
700
789
|
return res
|
|
701
790
|
else:
|
|
702
791
|
# Single file: inline serde utils, and skip poly modules
|
|
@@ -973,33 +1062,54 @@ class RustGenerator(Generator, LifecycleMixin):
|
|
|
973
1062
|
rendered = self.render(mode="crate")
|
|
974
1063
|
|
|
975
1064
|
cargo = rendered.cargo.render(self.template_environment)
|
|
976
|
-
# Normalize EOF: exactly one trailing newline
|
|
977
|
-
cargo = cargo.rstrip("\n") + "\n"
|
|
978
1065
|
cargo_file = output / "Cargo.toml"
|
|
979
|
-
|
|
980
|
-
cfile.write(cargo)
|
|
1066
|
+
self._write_text_file(cargo_file, cargo, crate_root=output)
|
|
981
1067
|
|
|
982
1068
|
pyproject = rendered.pyproject.render(self.template_environment)
|
|
983
|
-
pyproject = pyproject.rstrip("\n") + "\n"
|
|
984
1069
|
pyproject_file = output / "pyproject.toml"
|
|
985
|
-
|
|
986
|
-
pyfile.write(pyproject)
|
|
1070
|
+
self._write_text_file(pyproject_file, pyproject, crate_root=output)
|
|
987
1071
|
|
|
988
1072
|
rust_file = rendered.file.render(self.template_environment)
|
|
989
|
-
rust_file = rust_file.rstrip("\n") + "\n"
|
|
990
1073
|
src_dir = output / "src"
|
|
991
1074
|
src_dir.mkdir(exist_ok=True)
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1075
|
+
if self.handwritten_lib:
|
|
1076
|
+
generated_dir = src_dir / "generated"
|
|
1077
|
+
generated_dir.mkdir(exist_ok=True)
|
|
1078
|
+
lib_file = generated_dir / "mod.rs"
|
|
1079
|
+
else:
|
|
1080
|
+
generated_dir = src_dir
|
|
1081
|
+
lib_file = src_dir / "lib.rs"
|
|
1082
|
+
self._write_text_file(lib_file, rust_file, crate_root=output)
|
|
995
1083
|
|
|
996
1084
|
for k, f in rendered.extra_files.items():
|
|
997
1085
|
extra_file = f.render(self.template_environment)
|
|
998
|
-
extra_file = extra_file.rstrip("\n") + "\n"
|
|
999
1086
|
extra_file_name = f"{k}.rs"
|
|
1000
|
-
extra_file_path =
|
|
1001
|
-
|
|
1002
|
-
|
|
1087
|
+
extra_file_path = self._safe_subpath(generated_dir, extra_file_name)
|
|
1088
|
+
self._write_text_file(extra_file_path, extra_file, crate_root=output)
|
|
1089
|
+
|
|
1090
|
+
if getattr(rendered, "bin_files", None):
|
|
1091
|
+
for rel_path, template in rendered.bin_files.items():
|
|
1092
|
+
rendered_bin = template.render(self.template_environment)
|
|
1093
|
+
safe_bin_base = self._safe_subpath(src_dir, rel_path)
|
|
1094
|
+
bin_path = safe_bin_base.with_suffix(".rs")
|
|
1095
|
+
self._write_text_file(bin_path, rendered_bin, crate_root=output)
|
|
1096
|
+
|
|
1097
|
+
if self.handwritten_lib:
|
|
1098
|
+
shim_path = src_dir / "lib.rs"
|
|
1099
|
+
if not shim_path.exists():
|
|
1100
|
+
root_struct_name = getattr(rendered.file, "root_struct_name", None)
|
|
1101
|
+
root_struct_fn_snake = underscore(uncamelcase(root_struct_name)) if root_struct_name else None
|
|
1102
|
+
shim_template = RustLibShim(
|
|
1103
|
+
module_name=rendered.file.name,
|
|
1104
|
+
pyo3=self.pyo3,
|
|
1105
|
+
serde=self.serde,
|
|
1106
|
+
stubgen=self.stubgen,
|
|
1107
|
+
handwritten_lib=self.handwritten_lib,
|
|
1108
|
+
root_struct_name=root_struct_name,
|
|
1109
|
+
root_struct_fn_snake=root_struct_fn_snake,
|
|
1110
|
+
)
|
|
1111
|
+
shim = shim_template.render(self.template_environment)
|
|
1112
|
+
self._write_text_file(shim_path, shim, crate_root=output)
|
|
1003
1113
|
|
|
1004
1114
|
return rust_file
|
|
1005
1115
|
|
|
@@ -1031,6 +1141,44 @@ class RustGenerator(Generator, LifecycleMixin):
|
|
|
1031
1141
|
|
|
1032
1142
|
return output
|
|
1033
1143
|
|
|
1144
|
+
def _safe_subpath(self, base: Path, relative: Union[str, Path]) -> Path:
|
|
1145
|
+
"""Return a path nested under base, validating it does not escape."""
|
|
1146
|
+
|
|
1147
|
+
rel_path = Path(relative)
|
|
1148
|
+
if rel_path.is_absolute():
|
|
1149
|
+
raise ValueError(f"Relative path expected, got absolute path: {relative}")
|
|
1150
|
+
|
|
1151
|
+
if not rel_path.parts:
|
|
1152
|
+
raise ValueError("Relative path must contain at least one segment")
|
|
1153
|
+
|
|
1154
|
+
for part in rel_path.parts:
|
|
1155
|
+
if part in (".", ".."):
|
|
1156
|
+
raise ValueError(f"Invalid path segment: {part}")
|
|
1157
|
+
if "/" in part or "\\" in part:
|
|
1158
|
+
raise ValueError(f"Path segment must not contain separators: {part}")
|
|
1159
|
+
|
|
1160
|
+
candidate = base / rel_path
|
|
1161
|
+
base_resolved = base.resolve()
|
|
1162
|
+
try:
|
|
1163
|
+
candidate.resolve().relative_to(base_resolved)
|
|
1164
|
+
except ValueError as exc: # pragma: no cover - defensive
|
|
1165
|
+
raise ValueError(f"Path {candidate} escapes base directory {base}") from exc
|
|
1166
|
+
|
|
1167
|
+
return candidate
|
|
1168
|
+
|
|
1169
|
+
def _write_text_file(self, path: Path, content: str, *, crate_root: Path) -> None:
|
|
1170
|
+
"""Normalize trailing newline, ensure parent dirs, and write text."""
|
|
1171
|
+
|
|
1172
|
+
base_resolved = crate_root.resolve()
|
|
1173
|
+
try:
|
|
1174
|
+
path.resolve().relative_to(base_resolved)
|
|
1175
|
+
except ValueError as exc:
|
|
1176
|
+
raise ValueError(f"Path {path} escapes crate root {crate_root}") from exc
|
|
1177
|
+
|
|
1178
|
+
normalized = content.rstrip("\n") + "\n"
|
|
1179
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1180
|
+
path.write_text(normalized)
|
|
1181
|
+
|
|
1034
1182
|
@property
|
|
1035
1183
|
def template_environment(self) -> Environment:
|
|
1036
1184
|
if self._environment is None:
|
|
@@ -221,6 +221,12 @@ class RustTemplateModel(TemplateModel):
|
|
|
221
221
|
"""
|
|
222
222
|
Whether serde serialization/deserialization annotations should be added.
|
|
223
223
|
"""
|
|
224
|
+
stubgen: bool = False
|
|
225
|
+
"""
|
|
226
|
+
Whether pyo3-stub-gen instrumentation should be emitted.
|
|
227
|
+
"""
|
|
228
|
+
handwritten_lib: bool = False
|
|
229
|
+
"""Place generated sources under src/generated and leave lib.rs untouched for user code."""
|
|
224
230
|
attributes: dict[str, str] = Field(default_factory=dict)
|
|
225
231
|
|
|
226
232
|
|
|
@@ -297,6 +303,8 @@ class Import(Import_, RustTemplateModel):
|
|
|
297
303
|
"""Features to require in Cargo.toml"""
|
|
298
304
|
## whether this import should be behind a feature flag
|
|
299
305
|
feature_flag: Optional[str] = None
|
|
306
|
+
feature_dependencies: list[str] = Field(default_factory=list)
|
|
307
|
+
"""Additional crate features to enable alongside the optional dependency."""
|
|
300
308
|
|
|
301
309
|
|
|
302
310
|
class Imports(Imports_, RustTemplateModel):
|
|
@@ -362,6 +370,7 @@ class AsKeyValue(RustTemplateModel):
|
|
|
362
370
|
value_property_type: str
|
|
363
371
|
can_convert_from_primitive: bool = False
|
|
364
372
|
can_convert_from_empty: bool = False
|
|
373
|
+
value_property_optional: bool = False
|
|
365
374
|
|
|
366
375
|
|
|
367
376
|
class RustStructOrSubtypeEnum(RustTemplateModel):
|
|
@@ -371,6 +380,7 @@ class RustStructOrSubtypeEnum(RustTemplateModel):
|
|
|
371
380
|
as_key_value: bool = False
|
|
372
381
|
type_designator_field: Optional[str] = None
|
|
373
382
|
type_designators: dict[str, str]
|
|
383
|
+
key_property_type: str = "String"
|
|
374
384
|
|
|
375
385
|
|
|
376
386
|
class SlotRangeAsUnion(RustTemplateModel):
|
|
@@ -519,6 +529,26 @@ class SerdeUtilsFile(RustTemplateModel):
|
|
|
519
529
|
template: ClassVar[str] = "serde_utils.rs.jinja"
|
|
520
530
|
|
|
521
531
|
|
|
532
|
+
class StubUtilsFile(RustTemplateModel):
|
|
533
|
+
"""Helper utilities shared by stub generation code."""
|
|
534
|
+
|
|
535
|
+
template: ClassVar[str] = "stub_utils.rs.jinja"
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
class StubGenBin(RustTemplateModel):
|
|
539
|
+
"""Binary entry point to orchestrate stub generation checks."""
|
|
540
|
+
|
|
541
|
+
template: ClassVar[str] = "stub_gen.rs.jinja"
|
|
542
|
+
crate_name: str
|
|
543
|
+
stubgen: bool
|
|
544
|
+
|
|
545
|
+
@computed_field
|
|
546
|
+
def crate_module(self) -> str:
|
|
547
|
+
"""Crate identifier usable in Rust source."""
|
|
548
|
+
|
|
549
|
+
return self.crate_name.replace("-", "_")
|
|
550
|
+
|
|
551
|
+
|
|
522
552
|
class PolyTraitProperty(RustTemplateModel):
|
|
523
553
|
template: ClassVar[str] = "poly_trait_property.rs.jinja"
|
|
524
554
|
name: str
|
|
@@ -763,6 +793,7 @@ class RustFile(RustTemplateModel):
|
|
|
763
793
|
inline_serde_utils: bool = False
|
|
764
794
|
emit_poly: bool = True
|
|
765
795
|
serde_utils: Optional[SerdeUtilsFile] = None
|
|
796
|
+
root_struct_name: Optional[str] = None
|
|
766
797
|
|
|
767
798
|
@computed_field
|
|
768
799
|
def struct_names(self) -> list[str]:
|
|
@@ -814,14 +845,19 @@ class RustCargo(RustTemplateModel):
|
|
|
814
845
|
|
|
815
846
|
@computed_field
|
|
816
847
|
def cratefeatures(self) -> dict[str, list[str]]:
|
|
817
|
-
feature_flags = {}
|
|
848
|
+
feature_flags: dict[str, list[str]] = {}
|
|
818
849
|
for i in self.imports.imports:
|
|
819
850
|
assert isinstance(i, Import)
|
|
820
|
-
if i.feature_flag is
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
851
|
+
if i.feature_flag is None:
|
|
852
|
+
continue
|
|
853
|
+
deps = feature_flags.setdefault(i.feature_flag, [])
|
|
854
|
+
module = i.module.split("::")[0]
|
|
855
|
+
dep_entry = f"dep:{module}"
|
|
856
|
+
if dep_entry not in deps:
|
|
857
|
+
deps.append(dep_entry)
|
|
858
|
+
for extra in i.feature_dependencies:
|
|
859
|
+
if extra not in deps:
|
|
860
|
+
deps.append(extra)
|
|
825
861
|
return feature_flags
|
|
826
862
|
|
|
827
863
|
@field_validator("name", mode="after")
|
|
@@ -863,3 +899,12 @@ class RustPyProject(RustTemplateModel):
|
|
|
863
899
|
@classmethod
|
|
864
900
|
def snake_case_name(cls, value: str) -> str:
|
|
865
901
|
return underscore(value)
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
class RustLibShim(RustTemplateModel):
|
|
905
|
+
"""Shim module that re-exports generated code and hosts the PyO3 entry point."""
|
|
906
|
+
|
|
907
|
+
template: ClassVar[str] = "lib_shim.rs.jinja"
|
|
908
|
+
module_name: str
|
|
909
|
+
root_struct_name: Optional[str] = None
|
|
910
|
+
root_struct_fn_snake: Optional[str] = None
|
|
@@ -6,7 +6,7 @@ edition = "{{ edition }}"
|
|
|
6
6
|
[lib]
|
|
7
7
|
name = "{{ name }}"
|
|
8
8
|
{% if pyo3 %}
|
|
9
|
-
crate-type = ["cdylib"]
|
|
9
|
+
crate-type = ["cdylib", "rlib"]
|
|
10
10
|
{% else %}
|
|
11
11
|
crate-type = ["lib"]
|
|
12
12
|
{% endif %}
|
|
@@ -38,5 +38,5 @@ default = ["serde"]
|
|
|
38
38
|
default = []
|
|
39
39
|
{% endif %}
|
|
40
40
|
{% for k,v in cratefeatures.items() %}
|
|
41
|
-
{{ k }} = [{% for i in v %}"
|
|
41
|
+
{{ k }} = [{% for i in v %}"{{ i }}"{% if not loop.last %}, {% endif %}{% endfor %}]
|
|
42
42
|
{% endfor %}
|
|
@@ -5,6 +5,14 @@ pub struct Anything(
|
|
|
5
5
|
);
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
#[cfg(feature = "stubgen")]
|
|
9
|
+
impl ::pyo3_stub_gen::PyStubType for Anything {
|
|
10
|
+
fn type_output() -> ::pyo3_stub_gen::TypeInfo {
|
|
11
|
+
::pyo3_stub_gen::TypeInfo::any()
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
8
16
|
#[cfg(feature = "serde")]
|
|
9
17
|
impl Serialize for Anything {
|
|
10
18
|
fn serialize<S>(&self, to_ser: S) -> Result<S::Ok, S::Error>
|
|
@@ -91,7 +99,6 @@ impl<'py> IntoPyObject<'py> for Anything {
|
|
|
91
99
|
|
|
92
100
|
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
|
|
93
101
|
use pyo3::types::{PyAny, PyDict, PyList, PyString};
|
|
94
|
-
use pyo3::PyObject;
|
|
95
102
|
use serde_value::Value;
|
|
96
103
|
|
|
97
104
|
fn value_to_py<'py>(py: Python<'py>, v: &Value) -> pyo3::PyResult<Bound<'py, PyAny>> {
|