linkml 1.9.4rc2__py3-none-any.whl → 1.9.5rc1__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 +4 -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 +4 -1
- 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 +94 -0
- linkml/generators/rustgen/cli.py +65 -0
- linkml/generators/rustgen/rustgen.py +1038 -0
- linkml/generators/rustgen/template.py +865 -0
- linkml/generators/rustgen/templates/Cargo.toml.jinja +42 -0
- linkml/generators/rustgen/templates/anything.rs.jinja +142 -0
- linkml/generators/rustgen/templates/as_key_value.rs.jinja +56 -0
- linkml/generators/rustgen/templates/class_module.rs.jinja +8 -0
- linkml/generators/rustgen/templates/enum.rs.jinja +54 -0
- linkml/generators/rustgen/templates/file.rs.jinja +62 -0
- linkml/generators/rustgen/templates/import.rs.jinja +4 -0
- linkml/generators/rustgen/templates/imports.rs.jinja +8 -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 +132 -0
- linkml/generators/rustgen/templates/poly_trait_property_match.rs.jinja +10 -0
- linkml/generators/rustgen/templates/property.rs.jinja +19 -0
- linkml/generators/rustgen/templates/pyproject.toml.jinja +10 -0
- linkml/generators/rustgen/templates/serde_utils.rs.jinja +310 -0
- linkml/generators/rustgen/templates/slot_range_as_union.rs.jinja +61 -0
- linkml/generators/rustgen/templates/struct.rs.jinja +75 -0
- linkml/generators/rustgen/templates/struct_or_subtype_enum.rs.jinja +108 -0
- linkml/generators/rustgen/templates/typealias.rs.jinja +13 -0
- linkml/generators/sqltablegen.py +18 -16
- linkml/generators/yarrrmlgen.py +157 -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/schemaloader.py +55 -3
- {linkml-1.9.4rc2.dist-info → linkml-1.9.5rc1.dist-info}/METADATA +1 -1
- {linkml-1.9.4rc2.dist-info → linkml-1.9.5rc1.dist-info}/RECORD +76 -38
- {linkml-1.9.4rc2.dist-info → linkml-1.9.5rc1.dist-info}/entry_points.txt +1 -0
- linkml/generators/panderagen/panderagen_class_based/mixins.jinja2 +0 -26
- {linkml-1.9.4rc2.dist-info → linkml-1.9.5rc1.dist-info}/WHEEL +0 -0
- {linkml-1.9.4rc2.dist-info → linkml-1.9.5rc1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import ClassVar, Optional
|
|
3
|
+
|
|
4
|
+
from jinja2 import Environment, PackageLoader
|
|
5
|
+
from linkml_runtime.utils.formatutils import uncamelcase, underscore
|
|
6
|
+
from pydantic import BaseModel, Field, computed_field, field_validator
|
|
7
|
+
|
|
8
|
+
from linkml.generators.common.template import Import as Import_
|
|
9
|
+
from linkml.generators.common.template import Imports as Imports_
|
|
10
|
+
from linkml.generators.common.template import TemplateModel, _render
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ContainerType(Enum):
|
|
14
|
+
LIST = "list"
|
|
15
|
+
MAPPING = "mapping"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RustRange(BaseModel):
|
|
19
|
+
optional: bool = False
|
|
20
|
+
containerType: Optional[ContainerType] = None
|
|
21
|
+
has_default: bool = False
|
|
22
|
+
is_class_range: bool = False
|
|
23
|
+
is_reference: bool = False
|
|
24
|
+
box_needed: bool = False
|
|
25
|
+
has_class_subtypes: bool = False
|
|
26
|
+
child_ranges: Optional[list["RustRange"]] = None
|
|
27
|
+
type_: str
|
|
28
|
+
|
|
29
|
+
def type_name(self) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Canonical Rust type name (no module path).
|
|
32
|
+
|
|
33
|
+
Centralizes how we derive a display/type identifier for variants,
|
|
34
|
+
avoiding ad-hoc string splitting in templates or other models.
|
|
35
|
+
"""
|
|
36
|
+
# For references, range.type_ is already "String" in current generator,
|
|
37
|
+
# but keep behavior robust even if callers pass a namespaced path.
|
|
38
|
+
t = self.type_
|
|
39
|
+
# Trim any module path qualifiers like crate::mod::Type
|
|
40
|
+
if "::" in t:
|
|
41
|
+
t = t.split("::")[-1]
|
|
42
|
+
return t
|
|
43
|
+
|
|
44
|
+
def is_copy(self) -> bool:
|
|
45
|
+
"""Return True when values of this range implement Rust's Copy.
|
|
46
|
+
|
|
47
|
+
Used to decide whether scalar getters can return by value or must
|
|
48
|
+
borrow. The set is intentionally conservative and limited to common
|
|
49
|
+
numeric and boolean primitives.
|
|
50
|
+
"""
|
|
51
|
+
return self.type_ in ["i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64", "bool", "isize"]
|
|
52
|
+
|
|
53
|
+
def type_for_field(self):
|
|
54
|
+
"""Concrete Rust type to store in a struct field.
|
|
55
|
+
|
|
56
|
+
Applies structural adjustments in this order:
|
|
57
|
+
- Promote class types with subtypes to `OrSubtype` (unless references).
|
|
58
|
+
- Box inlined class values when needed to break recursive ownership.
|
|
59
|
+
- Wrap in `Vec<>` or `HashMap<String, _>` for list/map slots.
|
|
60
|
+
- Wrap in `Option<>` when the slot is optional.
|
|
61
|
+
"""
|
|
62
|
+
tp = self.type_
|
|
63
|
+
if self.has_class_subtypes:
|
|
64
|
+
if not self.is_reference:
|
|
65
|
+
tp = f"{tp}OrSubtype"
|
|
66
|
+
if self.box_needed:
|
|
67
|
+
tp = f"Box<{tp}>"
|
|
68
|
+
if self.containerType == ContainerType.LIST:
|
|
69
|
+
tp = f"Vec<{tp}>"
|
|
70
|
+
elif self.containerType == ContainerType.MAPPING:
|
|
71
|
+
tp = f"HashMap<String, {tp}>"
|
|
72
|
+
if self.optional:
|
|
73
|
+
tp = f"Option<{tp}>"
|
|
74
|
+
return tp
|
|
75
|
+
|
|
76
|
+
def type_without_option(self) -> str:
|
|
77
|
+
"""Field type without the outer Option wrapper."""
|
|
78
|
+
|
|
79
|
+
tp = self.type_for_field()
|
|
80
|
+
if self.optional and tp.startswith("Option<") and tp.endswith(">"):
|
|
81
|
+
return tp[7:-1]
|
|
82
|
+
return tp
|
|
83
|
+
|
|
84
|
+
def type_for_constructor(self) -> str:
|
|
85
|
+
"""Parameter type for struct constructors (PyO3 new).
|
|
86
|
+
|
|
87
|
+
Class ranges accept a serde-backed adapter that can deserialize
|
|
88
|
+
dictionaries or reuse existing pyclass instances. Other ranges
|
|
89
|
+
keep their field types unchanged.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
if self.is_class_range and not self.is_reference:
|
|
93
|
+
inner = self.type_without_option()
|
|
94
|
+
wrapped = f"serde_utils::PyValue<{inner}>"
|
|
95
|
+
if self.optional:
|
|
96
|
+
return f"Option<{wrapped}>"
|
|
97
|
+
return wrapped
|
|
98
|
+
return self.type_for_field()
|
|
99
|
+
|
|
100
|
+
def needs_constructor_conversion(self) -> bool:
|
|
101
|
+
"""True when constructor arguments need post-processing."""
|
|
102
|
+
|
|
103
|
+
return self.is_class_range and not self.is_reference
|
|
104
|
+
|
|
105
|
+
def convert_constructor_value(self, var_name: str) -> str:
|
|
106
|
+
"""Expression to convert constructor argument into field value."""
|
|
107
|
+
|
|
108
|
+
if not self.needs_constructor_conversion():
|
|
109
|
+
return var_name
|
|
110
|
+
if self.optional:
|
|
111
|
+
return f"{var_name}.map(|v| v.into_inner())"
|
|
112
|
+
return f"{var_name}.into_inner()"
|
|
113
|
+
|
|
114
|
+
def type_for_trait(self, crateref: Optional[str], setter: bool = False):
|
|
115
|
+
"""Signature type for trait getters/setters over this range.
|
|
116
|
+
|
|
117
|
+
- For getters (setter=False):
|
|
118
|
+
- Lists/Maps return borrowed views (`SeqRef`/`MapRef`) with explicit lifetimes.
|
|
119
|
+
- Scalars borrow `&T` for non-Copy values; Copy values return by value.
|
|
120
|
+
- Class types without subtypes are prefixed with `crate::` for stability;
|
|
121
|
+
with subtypes, use `OrSubtype`.
|
|
122
|
+
- Optional scalars collapse to `Option<&str>` for strings and borrow for others.
|
|
123
|
+
- For setters (setter=True):
|
|
124
|
+
- Lists/Maps accept `&Vec<_>`/`&HashMap<_, _>`.
|
|
125
|
+
- Class scalars accept a generic `E` with `Into<T>` bound (computed separately).
|
|
126
|
+
"""
|
|
127
|
+
tp = self.type_
|
|
128
|
+
if self.is_class_range and not self.has_class_subtypes and not setter:
|
|
129
|
+
if crateref and not self.is_reference:
|
|
130
|
+
tp = f"{crateref}::{tp}"
|
|
131
|
+
if self.has_class_subtypes and not self.is_reference:
|
|
132
|
+
tp = f"{tp}OrSubtype"
|
|
133
|
+
if self.is_class_range and setter and not self.is_reference:
|
|
134
|
+
tp = "E"
|
|
135
|
+
convert_ref = False
|
|
136
|
+
if self.containerType == ContainerType.LIST:
|
|
137
|
+
if not setter:
|
|
138
|
+
# tp = f"poly_containers::ListView<{tp}>"
|
|
139
|
+
tp = f"impl poly_containers::SeqRef<'a, {tp}>"
|
|
140
|
+
else:
|
|
141
|
+
tp = f"&Vec<{tp}>"
|
|
142
|
+
elif self.containerType == ContainerType.MAPPING:
|
|
143
|
+
if not setter:
|
|
144
|
+
tp = f"impl poly_containers::MapRef<'a, String,{tp}>"
|
|
145
|
+
else:
|
|
146
|
+
tp = f"&HashMap<String, {tp}>"
|
|
147
|
+
else:
|
|
148
|
+
if not self.is_copy():
|
|
149
|
+
convert_ref = True
|
|
150
|
+
if not setter and convert_ref and (not self.optional or (self.is_class_range and not self.is_reference)):
|
|
151
|
+
tp = f"&'a {tp}"
|
|
152
|
+
if self.optional:
|
|
153
|
+
if self.containerType or (self.is_class_range and not self.is_reference):
|
|
154
|
+
tp = f"Option<{tp}>"
|
|
155
|
+
else:
|
|
156
|
+
if tp == "String":
|
|
157
|
+
tp = "Option<&'a str>"
|
|
158
|
+
else:
|
|
159
|
+
if self.is_copy():
|
|
160
|
+
tp = f"Option<{tp}>"
|
|
161
|
+
else:
|
|
162
|
+
tp = f"Option<&'a {tp}>"
|
|
163
|
+
if tp == "&'a String":
|
|
164
|
+
tp = "&'a str"
|
|
165
|
+
return tp
|
|
166
|
+
|
|
167
|
+
def type_for_trait_value(self, crateref: Optional[str]) -> str:
|
|
168
|
+
"""By-value getter type for trait methods.
|
|
169
|
+
|
|
170
|
+
Used when the trait returns a canonical union or otherwise needs owned
|
|
171
|
+
values. Containers become `Vec<_>`/`HashMap<_, _>`. For class scalars,
|
|
172
|
+
add `crate::` unless the range admits subtypes (then use `OrSubtype`).
|
|
173
|
+
"""
|
|
174
|
+
# Union type: already canonical
|
|
175
|
+
if self.child_ranges is not None and len(self.child_ranges) > 1:
|
|
176
|
+
tp = self.type_
|
|
177
|
+
else:
|
|
178
|
+
tp = self.type_
|
|
179
|
+
if self.is_class_range and not self.is_reference:
|
|
180
|
+
if self.has_class_subtypes:
|
|
181
|
+
tp = f"{tp}OrSubtype"
|
|
182
|
+
elif crateref:
|
|
183
|
+
tp = f"{crateref}::{tp}"
|
|
184
|
+
if self.containerType == ContainerType.LIST:
|
|
185
|
+
tp = f"Vec<{tp}>"
|
|
186
|
+
elif self.containerType == ContainerType.MAPPING:
|
|
187
|
+
tp = f"HashMap<String, {tp}>"
|
|
188
|
+
if self.optional:
|
|
189
|
+
tp = f"Option<{tp}>"
|
|
190
|
+
return tp
|
|
191
|
+
|
|
192
|
+
def type_bound_for_setter(self, crateref: Optional[str]) -> Optional[str]:
|
|
193
|
+
"""Generic bound to accept convertible values in setter signatures.
|
|
194
|
+
|
|
195
|
+
For class ranges, setters use a type parameter `E` constrained as
|
|
196
|
+
`Into<T>` to allow ergonomic conversions. Non-class ranges do not
|
|
197
|
+
require a bound and return None.
|
|
198
|
+
"""
|
|
199
|
+
if self.is_class_range:
|
|
200
|
+
tp = self.type_
|
|
201
|
+
return f"Into<{tp}>"
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class RustTemplateModel(TemplateModel):
|
|
206
|
+
"""
|
|
207
|
+
Parent class for rust template models :)
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
template: ClassVar[str]
|
|
211
|
+
_environment: ClassVar[Environment] = Environment(
|
|
212
|
+
loader=PackageLoader("linkml.generators.rustgen", "templates"), trim_blocks=True, lstrip_blocks=True
|
|
213
|
+
)
|
|
214
|
+
meta_exclude: ClassVar[list[str]] = None
|
|
215
|
+
|
|
216
|
+
pyo3: bool = True
|
|
217
|
+
"""
|
|
218
|
+
Whether pyO3 annotations should be added to generated items :)
|
|
219
|
+
"""
|
|
220
|
+
serde: bool = False
|
|
221
|
+
"""
|
|
222
|
+
Whether serde serialization/deserialization annotations should be added.
|
|
223
|
+
"""
|
|
224
|
+
attributes: dict[str, str] = Field(default_factory=dict)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class RustTypeViews(BaseModel):
|
|
228
|
+
"""Precomputed trait signature fragments for a slot's range.
|
|
229
|
+
|
|
230
|
+
Encapsulates the decisions around by-value vs borrowed returns, container
|
|
231
|
+
view types, optional wrapping, and whether explicit lifetimes are required
|
|
232
|
+
in the generated method signature. Templates consume these strings directly
|
|
233
|
+
to avoid branching on typing details.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
type_getter: str
|
|
237
|
+
needs_lifetime: bool
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _needs_lifetime(sig: str) -> bool:
|
|
241
|
+
"""Heuristic to detect if a signature string requires an explicit lifetime.
|
|
242
|
+
|
|
243
|
+
We consider borrowed types (`&T`), container views (`SeqRef`/`MapRef`), and
|
|
244
|
+
explicit generics (`<'a>`) as requiring a lifetime parameter on the method.
|
|
245
|
+
"""
|
|
246
|
+
return ("&" in sig) or ("SeqRef" in sig) or ("MapRef" in sig) or ("<'a>" in sig)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def build_trait_views_for_promoted(promoted: "RustRange") -> RustTypeViews:
|
|
250
|
+
"""Return signature strings for a trait using the promoted (max) range.
|
|
251
|
+
|
|
252
|
+
- Raw getter: by-value for unions; otherwise borrowed/container view types.
|
|
253
|
+
- Typed getter: by-value for unions; otherwise borrowed/container view types
|
|
254
|
+
with `SeqRef`/`MapRef` and optional wrapping.
|
|
255
|
+
The goal is consistency across a class hierarchy regardless of concrete fields.
|
|
256
|
+
"""
|
|
257
|
+
# Raw getter: by-value union if union; else borrowed/containers
|
|
258
|
+
if promoted.child_ranges is not None and len(promoted.child_ranges) > 1:
|
|
259
|
+
raw = promoted.type_for_trait_value(crateref="crate")
|
|
260
|
+
else:
|
|
261
|
+
raw = promoted.type_for_trait(setter=False, crateref="crate")
|
|
262
|
+
|
|
263
|
+
return RustTypeViews(
|
|
264
|
+
type_getter=raw,
|
|
265
|
+
needs_lifetime=_needs_lifetime(raw),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def build_trait_views_for_range(rng: "RustRange") -> RustTypeViews:
|
|
270
|
+
"""Return signature strings for a concrete (non-promoted) range.
|
|
271
|
+
|
|
272
|
+
Mirrors the promoted logic but based on the actual field's range, used in
|
|
273
|
+
impls so that borrow vs value and container view decisions match the trait.
|
|
274
|
+
"""
|
|
275
|
+
# Raw getter
|
|
276
|
+
if rng.child_ranges is not None and len(rng.child_ranges) > 1:
|
|
277
|
+
raw = rng.type_for_trait_value(crateref="crate")
|
|
278
|
+
else:
|
|
279
|
+
raw = rng.type_for_trait(setter=False, crateref="crate")
|
|
280
|
+
|
|
281
|
+
return RustTypeViews(
|
|
282
|
+
type_getter=raw,
|
|
283
|
+
needs_lifetime=_needs_lifetime(raw),
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class PolyContainersFile(RustTemplateModel):
|
|
288
|
+
template: ClassVar[str] = "poly_containers.rs.jinja"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class Import(Import_, RustTemplateModel):
|
|
292
|
+
template: ClassVar[str] = "import.rs.jinja"
|
|
293
|
+
|
|
294
|
+
version: Optional[str] = None
|
|
295
|
+
"""Version specifier to use in Cargo.toml"""
|
|
296
|
+
features: Optional[list[str]] = None
|
|
297
|
+
"""Features to require in Cargo.toml"""
|
|
298
|
+
## whether this import should be behind a feature flag
|
|
299
|
+
feature_flag: Optional[str] = None
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class Imports(Imports_, RustTemplateModel):
|
|
303
|
+
template: ClassVar[str] = "imports.rs.jinja"
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class RustProperty(RustTemplateModel):
|
|
307
|
+
"""
|
|
308
|
+
A property within a rust struct
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
template: ClassVar[str] = "property.rs.jinja"
|
|
312
|
+
inline_mode: str
|
|
313
|
+
alias: Optional[str] = None
|
|
314
|
+
generate_merge: bool = False
|
|
315
|
+
container_mode: str
|
|
316
|
+
name: str
|
|
317
|
+
type_: RustRange # might be a union type, so list length > 1
|
|
318
|
+
required: bool
|
|
319
|
+
multivalued: bool = False
|
|
320
|
+
is_key_value: bool = False
|
|
321
|
+
|
|
322
|
+
@computed_field
|
|
323
|
+
def optional(self) -> bool:
|
|
324
|
+
"""
|
|
325
|
+
Whether this property is optional
|
|
326
|
+
"""
|
|
327
|
+
return self.type_.optional
|
|
328
|
+
|
|
329
|
+
@computed_field
|
|
330
|
+
def merge_strategy(self) -> str:
|
|
331
|
+
if self.type_.optional:
|
|
332
|
+
return "strategy = overwrite_except_none"
|
|
333
|
+
elif self.type_.containerType == ContainerType.LIST:
|
|
334
|
+
return "skip"
|
|
335
|
+
elif self.type_.containerType == ContainerType.MAPPING:
|
|
336
|
+
return "strategy = merge::hashmap::overwrite"
|
|
337
|
+
else:
|
|
338
|
+
return "skip"
|
|
339
|
+
|
|
340
|
+
@computed_field
|
|
341
|
+
def type_for_field(self) -> str:
|
|
342
|
+
"""
|
|
343
|
+
The type of this field, as it would be used in a struct definition
|
|
344
|
+
"""
|
|
345
|
+
return self.type_.type_for_field()
|
|
346
|
+
|
|
347
|
+
@computed_field
|
|
348
|
+
def hasdefault(self) -> bool:
|
|
349
|
+
return self.multivalued or not self.required
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class AsKeyValue(RustTemplateModel):
|
|
353
|
+
"""
|
|
354
|
+
A key-value representation for this struct
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
template: ClassVar[str] = "as_key_value.rs.jinja"
|
|
358
|
+
name: str
|
|
359
|
+
key_property_name: str
|
|
360
|
+
key_property_type: str
|
|
361
|
+
value_property_name: str
|
|
362
|
+
value_property_type: str
|
|
363
|
+
can_convert_from_primitive: bool = False
|
|
364
|
+
can_convert_from_empty: bool = False
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class RustStructOrSubtypeEnum(RustTemplateModel):
|
|
368
|
+
template: ClassVar[str] = "struct_or_subtype_enum.rs.jinja"
|
|
369
|
+
enum_name: str
|
|
370
|
+
struct_names: list[str]
|
|
371
|
+
as_key_value: bool = False
|
|
372
|
+
type_designator_field: Optional[str] = None
|
|
373
|
+
type_designators: dict[str, str]
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class SlotRangeAsUnion(RustTemplateModel):
|
|
377
|
+
"""
|
|
378
|
+
A union of ranges!
|
|
379
|
+
"""
|
|
380
|
+
|
|
381
|
+
template: ClassVar[str] = "slot_range_as_union.rs.jinja"
|
|
382
|
+
slot_name: str
|
|
383
|
+
ranges: list[str]
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class RustClassModule(RustTemplateModel):
|
|
387
|
+
class_name: str
|
|
388
|
+
class_name_snakecase: str
|
|
389
|
+
template: ClassVar[str] = "class_module.rs.jinja"
|
|
390
|
+
slot_ranges: list[SlotRangeAsUnion]
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class RustStruct(RustTemplateModel):
|
|
394
|
+
"""
|
|
395
|
+
A struct!
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
template: ClassVar[str] = "struct.rs.jinja"
|
|
399
|
+
special_case_enabled: bool = False
|
|
400
|
+
class_module: Optional[RustClassModule] = None
|
|
401
|
+
|
|
402
|
+
name: str
|
|
403
|
+
bases: Optional[list[str]] = None
|
|
404
|
+
"""
|
|
405
|
+
Base classes to inherit from - must have entire MRO, just just immediate ancestor
|
|
406
|
+
"""
|
|
407
|
+
properties: list[RustProperty] = Field(default_factory=list)
|
|
408
|
+
unsendable: bool = False
|
|
409
|
+
generate_merge: bool = False
|
|
410
|
+
as_key_value: Optional[AsKeyValue] = None
|
|
411
|
+
struct_or_subtype_enum: Optional[RustStructOrSubtypeEnum] = None
|
|
412
|
+
|
|
413
|
+
@computed_field()
|
|
414
|
+
def property_names_and_types(self) -> dict[str, str]:
|
|
415
|
+
return [(p.name, p.type_.type_for_constructor()) for p in self.constructor_params]
|
|
416
|
+
|
|
417
|
+
@computed_field()
|
|
418
|
+
def property_names(self) -> list[str]:
|
|
419
|
+
return [p.name for p in self.properties]
|
|
420
|
+
|
|
421
|
+
@computed_field()
|
|
422
|
+
def constructor_params(self) -> list[RustProperty]:
|
|
423
|
+
required = [p for p in self.properties if not p.type_.optional]
|
|
424
|
+
optional = [p for p in self.properties if p.type_.optional]
|
|
425
|
+
return required + optional
|
|
426
|
+
|
|
427
|
+
@computed_field()
|
|
428
|
+
def constructor_signature(self) -> str:
|
|
429
|
+
parts: list[str] = []
|
|
430
|
+
for prop in self.constructor_params:
|
|
431
|
+
if prop.type_.optional:
|
|
432
|
+
parts.append(f"{prop.name}=None")
|
|
433
|
+
else:
|
|
434
|
+
parts.append(prop.name)
|
|
435
|
+
return ", ".join(parts)
|
|
436
|
+
|
|
437
|
+
@computed_field()
|
|
438
|
+
def constructor_conversions(self) -> list[tuple[str, str]]:
|
|
439
|
+
return [
|
|
440
|
+
(p.name, p.type_.convert_constructor_value(p.name))
|
|
441
|
+
for p in self.constructor_params
|
|
442
|
+
if p.type_.needs_constructor_conversion()
|
|
443
|
+
]
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class RustEnum(RustTemplateModel):
|
|
447
|
+
"""
|
|
448
|
+
A rust enum!
|
|
449
|
+
"""
|
|
450
|
+
|
|
451
|
+
template: ClassVar[str] = "enum.rs.jinja"
|
|
452
|
+
|
|
453
|
+
name: str
|
|
454
|
+
items: list["RustEnumItem"]
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
class RustEnumItem(BaseModel):
|
|
458
|
+
"""Single enum variant with its original permissible value text."""
|
|
459
|
+
|
|
460
|
+
variant: str
|
|
461
|
+
text: str
|
|
462
|
+
|
|
463
|
+
@computed_field
|
|
464
|
+
def python_literals(self) -> list[str]:
|
|
465
|
+
"""Return acceptable string literals when converting from Python."""
|
|
466
|
+
|
|
467
|
+
literals = [self.text]
|
|
468
|
+
if self.variant != self.text:
|
|
469
|
+
literals.append(self.variant)
|
|
470
|
+
return literals
|
|
471
|
+
|
|
472
|
+
@staticmethod
|
|
473
|
+
def _escape(value: str) -> str:
|
|
474
|
+
return value.replace("\\", "\\\\").replace('"', '\\"')
|
|
475
|
+
|
|
476
|
+
@computed_field
|
|
477
|
+
def text_literal(self) -> str:
|
|
478
|
+
"""Escaped literal suitable for embedding in Rust source."""
|
|
479
|
+
|
|
480
|
+
return self._escape(self.text)
|
|
481
|
+
|
|
482
|
+
@computed_field
|
|
483
|
+
def python_match_pattern(self) -> str:
|
|
484
|
+
"""Match arm pattern accepting any permitted Python literal."""
|
|
485
|
+
|
|
486
|
+
literals = []
|
|
487
|
+
for literal in self.python_literals:
|
|
488
|
+
if literal not in literals:
|
|
489
|
+
literals.append(literal)
|
|
490
|
+
escaped = [f'"{self._escape(lit)}"' for lit in literals]
|
|
491
|
+
return " | ".join(escaped)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
class RustTypeAlias(RustTemplateModel):
|
|
495
|
+
"""
|
|
496
|
+
A type alias used to represent slots
|
|
497
|
+
"""
|
|
498
|
+
|
|
499
|
+
template: ClassVar[str] = "typealias.rs.jinja"
|
|
500
|
+
|
|
501
|
+
name: str
|
|
502
|
+
type_: str
|
|
503
|
+
description: Optional[str] = None
|
|
504
|
+
multivalued: Optional[bool] = False
|
|
505
|
+
class_range: bool = False
|
|
506
|
+
slot_range_as_union: Optional[SlotRangeAsUnion] = None
|
|
507
|
+
|
|
508
|
+
@field_validator("attributes", mode="before")
|
|
509
|
+
@classmethod
|
|
510
|
+
def attr_values_as_strings(cls, value: dict[str, any]) -> dict[str, str]:
|
|
511
|
+
return {k: str(v) for k, v in value.items()}
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
class SerdeUtilsFile(RustTemplateModel):
|
|
515
|
+
"""
|
|
516
|
+
A file containing utility functions for serde serialization/deserialization
|
|
517
|
+
"""
|
|
518
|
+
|
|
519
|
+
template: ClassVar[str] = "serde_utils.rs.jinja"
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
class PolyTraitProperty(RustTemplateModel):
|
|
523
|
+
template: ClassVar[str] = "poly_trait_property.rs.jinja"
|
|
524
|
+
name: str
|
|
525
|
+
range: RustRange
|
|
526
|
+
promoted_range: RustRange
|
|
527
|
+
promoted_range: RustRange
|
|
528
|
+
|
|
529
|
+
@computed_field
|
|
530
|
+
def class_range(self) -> bool:
|
|
531
|
+
"""
|
|
532
|
+
Whether this range is a class range
|
|
533
|
+
"""
|
|
534
|
+
return self.range.is_class_range
|
|
535
|
+
|
|
536
|
+
@computed_field
|
|
537
|
+
def type_getter(self) -> str:
|
|
538
|
+
# Centralized via RustTypeViews builder
|
|
539
|
+
views = build_trait_views_for_promoted(self.promoted_range)
|
|
540
|
+
return views.type_getter
|
|
541
|
+
|
|
542
|
+
@computed_field
|
|
543
|
+
def needs_lifetime(self) -> bool:
|
|
544
|
+
"""Whether the trait getter requires an explicit lifetime."""
|
|
545
|
+
views = build_trait_views_for_promoted(self.promoted_range)
|
|
546
|
+
return views.needs_lifetime
|
|
547
|
+
|
|
548
|
+
@computed_field
|
|
549
|
+
def type_setter(self) -> str:
|
|
550
|
+
return self.range.type_for_trait(setter=True, crateref="crate")
|
|
551
|
+
|
|
552
|
+
@computed_field
|
|
553
|
+
def type_bound(self) -> Optional[str]:
|
|
554
|
+
"""
|
|
555
|
+
The type bound for the setter method
|
|
556
|
+
"""
|
|
557
|
+
return self.range.type_bound_for_setter(crateref="crate")
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
class PolyTraitPropertyImpl(RustTemplateModel):
|
|
561
|
+
template: ClassVar[str] = "poly_trait_property_impl.rs.jinja"
|
|
562
|
+
name: str
|
|
563
|
+
range: RustRange
|
|
564
|
+
struct_name: str
|
|
565
|
+
definition_range: RustRange
|
|
566
|
+
trait_range: RustRange
|
|
567
|
+
|
|
568
|
+
@computed_field
|
|
569
|
+
def is_copy(self) -> bool:
|
|
570
|
+
"""
|
|
571
|
+
Whether this range is a copy type
|
|
572
|
+
"""
|
|
573
|
+
return self.range.is_copy()
|
|
574
|
+
|
|
575
|
+
@computed_field
|
|
576
|
+
def need_option_wrap(self) -> bool:
|
|
577
|
+
return self.definition_range.optional and not self.range.optional
|
|
578
|
+
|
|
579
|
+
@computed_field
|
|
580
|
+
def class_range(self) -> bool:
|
|
581
|
+
"""
|
|
582
|
+
Whether this range is a class range
|
|
583
|
+
"""
|
|
584
|
+
return self.range.is_class_range
|
|
585
|
+
|
|
586
|
+
@computed_field
|
|
587
|
+
def ct(self) -> str:
|
|
588
|
+
"""
|
|
589
|
+
The container type for this range, if any
|
|
590
|
+
"""
|
|
591
|
+
return self.range.containerType.value if self.range.containerType else "None"
|
|
592
|
+
|
|
593
|
+
@computed_field
|
|
594
|
+
def type_getter(self) -> str:
|
|
595
|
+
# Mirror trait signature: promoted union by value; else concrete borrowed
|
|
596
|
+
if self.definition_range.child_ranges is not None and len(self.definition_range.child_ranges) > 1:
|
|
597
|
+
return build_trait_views_for_promoted(self.definition_range).type_getter
|
|
598
|
+
return build_trait_views_for_range(self.trait_range).type_getter
|
|
599
|
+
|
|
600
|
+
@computed_field
|
|
601
|
+
def needs_lifetime(self) -> bool:
|
|
602
|
+
sig = self.type_getter
|
|
603
|
+
return ("&" in sig) or ("SeqRef" in sig) or ("MapRef" in sig)
|
|
604
|
+
|
|
605
|
+
# Note: typed getters removed
|
|
606
|
+
|
|
607
|
+
@computed_field
|
|
608
|
+
def union_type(self) -> str:
|
|
609
|
+
return self.definition_range.type_
|
|
610
|
+
|
|
611
|
+
@computed_field
|
|
612
|
+
def range_variant(self) -> str:
|
|
613
|
+
# Use centralized type-name logic for a variant identifier
|
|
614
|
+
return self.range.type_name()
|
|
615
|
+
|
|
616
|
+
@computed_field
|
|
617
|
+
def current_union_types(self) -> dict[str, str]:
|
|
618
|
+
m: dict[str, str] = {}
|
|
619
|
+
for c in self.cases:
|
|
620
|
+
sc = underscore(uncamelcase(c))
|
|
621
|
+
m[c] = f"{sc}_utl::{self.name}_range"
|
|
622
|
+
return m
|
|
623
|
+
|
|
624
|
+
@computed_field
|
|
625
|
+
def base_union_variants(self) -> list[str]:
|
|
626
|
+
vs: list[str] = []
|
|
627
|
+
if self.definition_range.child_ranges is not None and len(self.definition_range.child_ranges) > 1:
|
|
628
|
+
for cr in self.definition_range.child_ranges:
|
|
629
|
+
vs.append(cr.type_name())
|
|
630
|
+
return vs
|
|
631
|
+
|
|
632
|
+
@computed_field
|
|
633
|
+
def current_union_type(self) -> str:
|
|
634
|
+
return self.range.type_
|
|
635
|
+
|
|
636
|
+
@computed_field
|
|
637
|
+
def type_setter(self) -> str:
|
|
638
|
+
return self.definition_range.type_for_trait(setter=True, crateref="crate")
|
|
639
|
+
|
|
640
|
+
@computed_field
|
|
641
|
+
def type_bound(self) -> Optional[str]:
|
|
642
|
+
"""
|
|
643
|
+
The type bound for the setter method
|
|
644
|
+
"""
|
|
645
|
+
return self.definition_range.type_bound_for_setter(crateref="crate")
|
|
646
|
+
|
|
647
|
+
@computed_field
|
|
648
|
+
def union_conversion_arms(self) -> list[str]:
|
|
649
|
+
"""Pre-rendered match arms to convert current union -> base union.
|
|
650
|
+
|
|
651
|
+
Example: Current::A(x) => Base::A(x.clone()),
|
|
652
|
+
"""
|
|
653
|
+
arms: list[str] = []
|
|
654
|
+
if self.definition_range.child_ranges is not None and len(self.definition_range.child_ranges) > 1:
|
|
655
|
+
base = self.union_type
|
|
656
|
+
cur = self.current_union_type
|
|
657
|
+
for vn in self.base_union_variants:
|
|
658
|
+
arms.append(f"{cur}::{vn}(x) => {base}::{vn}(x.clone()),")
|
|
659
|
+
return arms
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
class PolyTraitImpl(RustTemplateModel):
|
|
663
|
+
"""Implementation of a :class:`PolyTrait` for a particular struct."""
|
|
664
|
+
|
|
665
|
+
template: ClassVar[str] = "poly_trait_impl.rs.jinja"
|
|
666
|
+
name: str
|
|
667
|
+
struct_name: str
|
|
668
|
+
attrs: list[PolyTraitPropertyImpl]
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
class PolyTraitPropertyMatch(RustTemplateModel):
|
|
672
|
+
template: ClassVar[str] = "poly_trait_property_match.rs.jinja"
|
|
673
|
+
name: str
|
|
674
|
+
range: RustRange
|
|
675
|
+
struct_name: str
|
|
676
|
+
cases: list[str]
|
|
677
|
+
|
|
678
|
+
@computed_field
|
|
679
|
+
def is_optional(self) -> bool:
|
|
680
|
+
"""
|
|
681
|
+
Whether this property is optional
|
|
682
|
+
"""
|
|
683
|
+
return self.range.optional
|
|
684
|
+
|
|
685
|
+
@computed_field
|
|
686
|
+
def is_container(self) -> bool:
|
|
687
|
+
"""
|
|
688
|
+
Whether this property is a container type
|
|
689
|
+
"""
|
|
690
|
+
return self.range.containerType is not None
|
|
691
|
+
|
|
692
|
+
@computed_field
|
|
693
|
+
def type_getter(self) -> str:
|
|
694
|
+
if self.range.child_ranges is not None and len(self.range.child_ranges) > 1:
|
|
695
|
+
return self.range.type_for_trait_value(crateref="crate")
|
|
696
|
+
return self.range.type_for_trait(setter=False, crateref="crate")
|
|
697
|
+
|
|
698
|
+
@computed_field
|
|
699
|
+
def needs_lifetime(self) -> bool:
|
|
700
|
+
t = self.type_getter
|
|
701
|
+
return ("&" in t) or ("SeqRef" in t) or ("MapRef" in t)
|
|
702
|
+
|
|
703
|
+
@computed_field
|
|
704
|
+
def base_union_type(self) -> str:
|
|
705
|
+
return self.range.type_
|
|
706
|
+
|
|
707
|
+
@computed_field
|
|
708
|
+
def range_variant(self) -> str:
|
|
709
|
+
# Always use centralized type name logic
|
|
710
|
+
return self.range.type_name()
|
|
711
|
+
|
|
712
|
+
@computed_field
|
|
713
|
+
def current_union_types(self) -> dict[str, str]:
|
|
714
|
+
m: dict[str, str] = {}
|
|
715
|
+
for c in self.cases:
|
|
716
|
+
sc = underscore(uncamelcase(c))
|
|
717
|
+
m[c] = f"{sc}_utl::{self.name}_range"
|
|
718
|
+
return m
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
class PolyTraitImplForSubtypeEnum(RustTemplateModel):
|
|
722
|
+
"""Trait implementation that dispatches based on subtype enums."""
|
|
723
|
+
|
|
724
|
+
template: ClassVar[str] = "poly_trait_impl_orsubtype.rs.jinja"
|
|
725
|
+
enum_name: str
|
|
726
|
+
name: str
|
|
727
|
+
attrs: list[PolyTraitPropertyMatch]
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
class PolyTrait(RustTemplateModel):
|
|
731
|
+
"""Definition of a polymorphic trait generated from a class hierarchy."""
|
|
732
|
+
|
|
733
|
+
template: ClassVar[str] = "poly_trait.rs.jinja"
|
|
734
|
+
name: str
|
|
735
|
+
attrs: list[PolyTraitProperty]
|
|
736
|
+
superclass_names: list[str]
|
|
737
|
+
impls: list[PolyTraitImpl]
|
|
738
|
+
subtypes: list[PolyTraitImplForSubtypeEnum]
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
class PolyFile(RustTemplateModel):
|
|
742
|
+
"""Rust file aggregating polymorphic traits."""
|
|
743
|
+
|
|
744
|
+
template: ClassVar[str] = "poly.rs.jinja"
|
|
745
|
+
imports: Imports = Imports()
|
|
746
|
+
traits: list[PolyTrait]
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
class RustFile(RustTemplateModel):
|
|
750
|
+
"""
|
|
751
|
+
A whole rust file!
|
|
752
|
+
"""
|
|
753
|
+
|
|
754
|
+
template: ClassVar[str] = "file.rs.jinja"
|
|
755
|
+
|
|
756
|
+
name: str
|
|
757
|
+
imports: Imports = Imports()
|
|
758
|
+
types: list[RustTypeAlias] = Field(default_factory=list)
|
|
759
|
+
structs: list[RustStruct] = Field(default_factory=list)
|
|
760
|
+
enums: list[RustEnum] = Field(default_factory=list)
|
|
761
|
+
slots: list[RustTypeAlias] = Field(default_factory=list)
|
|
762
|
+
# Single-file generation knobs
|
|
763
|
+
inline_serde_utils: bool = False
|
|
764
|
+
emit_poly: bool = True
|
|
765
|
+
serde_utils: Optional[SerdeUtilsFile] = None
|
|
766
|
+
|
|
767
|
+
@computed_field
|
|
768
|
+
def struct_names(self) -> list[str]:
|
|
769
|
+
"""Names of all the structs we have!"""
|
|
770
|
+
return [c.name for c in self.structs]
|
|
771
|
+
|
|
772
|
+
@computed_field
|
|
773
|
+
def pyclass_struct_names(self) -> list[str]:
|
|
774
|
+
"""Names of structs that implement PyClass and should be registered.
|
|
775
|
+
|
|
776
|
+
Excludes special cases like `Anything` (and its alias `AnyValue`) which
|
|
777
|
+
are not exposed as #[pyclass] in the special-case template.
|
|
778
|
+
"""
|
|
779
|
+
out: list[str] = []
|
|
780
|
+
for c in self.structs:
|
|
781
|
+
if c.name in ("Anything", "AnyValue"):
|
|
782
|
+
continue
|
|
783
|
+
out.append(c.name)
|
|
784
|
+
return out
|
|
785
|
+
|
|
786
|
+
@computed_field
|
|
787
|
+
def needs_overwrite_except_none(self) -> bool:
|
|
788
|
+
"""Whether any struct uses the custom merge helper."""
|
|
789
|
+
return any(s.generate_merge for s in self.structs)
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
class RangeEnum(RustTemplateModel):
|
|
793
|
+
"""
|
|
794
|
+
A range enum!
|
|
795
|
+
"""
|
|
796
|
+
|
|
797
|
+
template: ClassVar[str] = "range_enum.rs.jinja"
|
|
798
|
+
name: str
|
|
799
|
+
type_: list[str]
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
class RustCargo(RustTemplateModel):
|
|
803
|
+
"""
|
|
804
|
+
A Cargo.toml file
|
|
805
|
+
"""
|
|
806
|
+
|
|
807
|
+
template: ClassVar[str] = "Cargo.toml.jinja"
|
|
808
|
+
|
|
809
|
+
name: str
|
|
810
|
+
version: str = "0.0.0"
|
|
811
|
+
edition: str = "2021"
|
|
812
|
+
pyo3_version: str = "0.21.1"
|
|
813
|
+
imports: Imports = Imports()
|
|
814
|
+
|
|
815
|
+
@computed_field
|
|
816
|
+
def cratefeatures(self) -> dict[str, list[str]]:
|
|
817
|
+
feature_flags = {}
|
|
818
|
+
for i in self.imports.imports:
|
|
819
|
+
assert isinstance(i, Import)
|
|
820
|
+
if i.feature_flag is not None:
|
|
821
|
+
if i.feature_flag not in feature_flags:
|
|
822
|
+
feature_flags[i.feature_flag] = [i.module]
|
|
823
|
+
else:
|
|
824
|
+
feature_flags[i.feature_flag].append(i.module)
|
|
825
|
+
return feature_flags
|
|
826
|
+
|
|
827
|
+
@field_validator("name", mode="after")
|
|
828
|
+
@classmethod
|
|
829
|
+
def snake_case_name(cls, value: str) -> str:
|
|
830
|
+
return underscore(value)
|
|
831
|
+
|
|
832
|
+
def render(self, environment: Optional[Environment] = None, **kwargs) -> str:
|
|
833
|
+
if environment is None:
|
|
834
|
+
environment = RustTemplateModel.environment()
|
|
835
|
+
|
|
836
|
+
fields = {**self.model_fields, **self.model_computed_fields}
|
|
837
|
+
data = {k: getattr(self, k, None) for k in fields}
|
|
838
|
+
# don't render the template
|
|
839
|
+
imports = []
|
|
840
|
+
for i in data.get("imports", []):
|
|
841
|
+
imp = i.model_dump()
|
|
842
|
+
imp["module"] = imp["module"].split("::")[0]
|
|
843
|
+
imports.append(imp)
|
|
844
|
+
data["imports"] = imports
|
|
845
|
+
|
|
846
|
+
data = {k: _render(v, environment) for k, v in data.items()}
|
|
847
|
+
template = environment.get_template(self.template)
|
|
848
|
+
rendered = template.render(**data)
|
|
849
|
+
return rendered
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
class RustPyProject(RustTemplateModel):
|
|
853
|
+
"""
|
|
854
|
+
A pyproject.toml file to go with a maturin/pyo3 package
|
|
855
|
+
"""
|
|
856
|
+
|
|
857
|
+
template: ClassVar[str] = "pyproject.toml.jinja"
|
|
858
|
+
|
|
859
|
+
name: str
|
|
860
|
+
version: str = "0.0.0"
|
|
861
|
+
|
|
862
|
+
@field_validator("name", mode="after")
|
|
863
|
+
@classmethod
|
|
864
|
+
def snake_case_name(cls, value: str) -> str:
|
|
865
|
+
return underscore(value)
|