linkml 1.9.4rc2__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.
Files changed (83) hide show
  1. linkml/cli/main.py +5 -1
  2. linkml/converter/__init__.py +0 -0
  3. linkml/generators/__init__.py +2 -0
  4. linkml/generators/common/build.py +5 -20
  5. linkml/generators/common/template.py +289 -3
  6. linkml/generators/docgen.py +55 -10
  7. linkml/generators/erdiagramgen.py +9 -5
  8. linkml/generators/graphqlgen.py +32 -6
  9. linkml/generators/jsonldcontextgen.py +78 -12
  10. linkml/generators/jsonschemagen.py +29 -12
  11. linkml/generators/mermaidclassdiagramgen.py +21 -3
  12. linkml/generators/owlgen.py +13 -2
  13. linkml/generators/panderagen/dataframe_class.py +13 -0
  14. linkml/generators/panderagen/dataframe_field.py +50 -0
  15. linkml/generators/panderagen/linkml_pandera_validator.py +186 -0
  16. linkml/generators/panderagen/panderagen.py +22 -5
  17. linkml/generators/panderagen/panderagen_class_based/class.jinja2 +70 -13
  18. linkml/generators/panderagen/panderagen_class_based/custom_checks.jinja2 +27 -0
  19. linkml/generators/panderagen/panderagen_class_based/enums.jinja2 +3 -3
  20. linkml/generators/panderagen/panderagen_class_based/pandera.jinja2 +12 -2
  21. linkml/generators/panderagen/panderagen_class_based/slots.jinja2 +19 -17
  22. linkml/generators/panderagen/slot_generator_mixin.py +143 -16
  23. linkml/generators/panderagen/transforms/__init__.py +19 -0
  24. linkml/generators/panderagen/transforms/collection_dict_model_transform.py +62 -0
  25. linkml/generators/panderagen/transforms/list_dict_model_transform.py +66 -0
  26. linkml/generators/panderagen/transforms/model_transform.py +8 -0
  27. linkml/generators/panderagen/transforms/nested_struct_model_transform.py +27 -0
  28. linkml/generators/panderagen/transforms/simple_dict_model_transform.py +86 -0
  29. linkml/generators/plantumlgen.py +17 -11
  30. linkml/generators/pydanticgen/pydanticgen.py +53 -2
  31. linkml/generators/pydanticgen/template.py +45 -233
  32. linkml/generators/pydanticgen/templates/attribute.py.jinja +1 -0
  33. linkml/generators/pydanticgen/templates/base_model.py.jinja +16 -2
  34. linkml/generators/pydanticgen/templates/imports.py.jinja +1 -1
  35. linkml/generators/rdfgen.py +11 -2
  36. linkml/generators/rustgen/__init__.py +3 -0
  37. linkml/generators/rustgen/build.py +97 -0
  38. linkml/generators/rustgen/cli.py +83 -0
  39. linkml/generators/rustgen/rustgen.py +1186 -0
  40. linkml/generators/rustgen/template.py +910 -0
  41. linkml/generators/rustgen/templates/Cargo.toml.jinja +42 -0
  42. linkml/generators/rustgen/templates/anything.rs.jinja +149 -0
  43. linkml/generators/rustgen/templates/as_key_value.rs.jinja +86 -0
  44. linkml/generators/rustgen/templates/class_module.rs.jinja +8 -0
  45. linkml/generators/rustgen/templates/enum.rs.jinja +70 -0
  46. linkml/generators/rustgen/templates/file.rs.jinja +75 -0
  47. linkml/generators/rustgen/templates/import.rs.jinja +4 -0
  48. linkml/generators/rustgen/templates/imports.rs.jinja +8 -0
  49. linkml/generators/rustgen/templates/lib_shim.rs.jinja +52 -0
  50. linkml/generators/rustgen/templates/poly.rs.jinja +9 -0
  51. linkml/generators/rustgen/templates/poly_containers.rs.jinja +439 -0
  52. linkml/generators/rustgen/templates/poly_trait.rs.jinja +15 -0
  53. linkml/generators/rustgen/templates/poly_trait_impl.rs.jinja +5 -0
  54. linkml/generators/rustgen/templates/poly_trait_impl_orsubtype.rs.jinja +5 -0
  55. linkml/generators/rustgen/templates/poly_trait_property.rs.jinja +8 -0
  56. linkml/generators/rustgen/templates/poly_trait_property_impl.rs.jinja +134 -0
  57. linkml/generators/rustgen/templates/poly_trait_property_match.rs.jinja +10 -0
  58. linkml/generators/rustgen/templates/property.rs.jinja +28 -0
  59. linkml/generators/rustgen/templates/pyproject.toml.jinja +10 -0
  60. linkml/generators/rustgen/templates/serde_utils.rs.jinja +490 -0
  61. linkml/generators/rustgen/templates/slot_range_as_union.rs.jinja +64 -0
  62. linkml/generators/rustgen/templates/struct.rs.jinja +81 -0
  63. linkml/generators/rustgen/templates/struct_or_subtype_enum.rs.jinja +111 -0
  64. linkml/generators/rustgen/templates/stub_gen.rs.jinja +71 -0
  65. linkml/generators/rustgen/templates/stub_utils.rs.jinja +76 -0
  66. linkml/generators/rustgen/templates/typealias.rs.jinja +13 -0
  67. linkml/generators/sqltablegen.py +18 -16
  68. linkml/generators/yarrrmlgen.py +173 -0
  69. linkml/linter/config/datamodel/config.py +160 -293
  70. linkml/linter/config/datamodel/config.yaml +34 -26
  71. linkml/linter/config/default.yaml +4 -0
  72. linkml/linter/config/recommended.yaml +4 -0
  73. linkml/linter/linter.py +1 -2
  74. linkml/linter/rules.py +37 -0
  75. linkml/utils/schema_builder.py +2 -0
  76. linkml/utils/schemaloader.py +76 -3
  77. {linkml-1.9.4rc2.dist-info → linkml-1.9.5.dist-info}/METADATA +1 -1
  78. {linkml-1.9.4rc2.dist-info → linkml-1.9.5.dist-info}/RECORD +82 -40
  79. {linkml-1.9.4rc2.dist-info → linkml-1.9.5.dist-info}/entry_points.txt +2 -1
  80. linkml/generators/panderagen/panderagen_class_based/mixins.jinja2 +0 -26
  81. /linkml/{utils/converter.py → converter/cli.py} +0 -0
  82. {linkml-1.9.4rc2.dist-info → linkml-1.9.5.dist-info}/WHEEL +0 -0
  83. {linkml-1.9.4rc2.dist-info → linkml-1.9.5.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,910 @@
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
+ 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."""
230
+ attributes: dict[str, str] = Field(default_factory=dict)
231
+
232
+
233
+ class RustTypeViews(BaseModel):
234
+ """Precomputed trait signature fragments for a slot's range.
235
+
236
+ Encapsulates the decisions around by-value vs borrowed returns, container
237
+ view types, optional wrapping, and whether explicit lifetimes are required
238
+ in the generated method signature. Templates consume these strings directly
239
+ to avoid branching on typing details.
240
+ """
241
+
242
+ type_getter: str
243
+ needs_lifetime: bool
244
+
245
+
246
+ def _needs_lifetime(sig: str) -> bool:
247
+ """Heuristic to detect if a signature string requires an explicit lifetime.
248
+
249
+ We consider borrowed types (`&T`), container views (`SeqRef`/`MapRef`), and
250
+ explicit generics (`<'a>`) as requiring a lifetime parameter on the method.
251
+ """
252
+ return ("&" in sig) or ("SeqRef" in sig) or ("MapRef" in sig) or ("<'a>" in sig)
253
+
254
+
255
+ def build_trait_views_for_promoted(promoted: "RustRange") -> RustTypeViews:
256
+ """Return signature strings for a trait using the promoted (max) range.
257
+
258
+ - Raw getter: by-value for unions; otherwise borrowed/container view types.
259
+ - Typed getter: by-value for unions; otherwise borrowed/container view types
260
+ with `SeqRef`/`MapRef` and optional wrapping.
261
+ The goal is consistency across a class hierarchy regardless of concrete fields.
262
+ """
263
+ # Raw getter: by-value union if union; else borrowed/containers
264
+ if promoted.child_ranges is not None and len(promoted.child_ranges) > 1:
265
+ raw = promoted.type_for_trait_value(crateref="crate")
266
+ else:
267
+ raw = promoted.type_for_trait(setter=False, crateref="crate")
268
+
269
+ return RustTypeViews(
270
+ type_getter=raw,
271
+ needs_lifetime=_needs_lifetime(raw),
272
+ )
273
+
274
+
275
+ def build_trait_views_for_range(rng: "RustRange") -> RustTypeViews:
276
+ """Return signature strings for a concrete (non-promoted) range.
277
+
278
+ Mirrors the promoted logic but based on the actual field's range, used in
279
+ impls so that borrow vs value and container view decisions match the trait.
280
+ """
281
+ # Raw getter
282
+ if rng.child_ranges is not None and len(rng.child_ranges) > 1:
283
+ raw = rng.type_for_trait_value(crateref="crate")
284
+ else:
285
+ raw = rng.type_for_trait(setter=False, crateref="crate")
286
+
287
+ return RustTypeViews(
288
+ type_getter=raw,
289
+ needs_lifetime=_needs_lifetime(raw),
290
+ )
291
+
292
+
293
+ class PolyContainersFile(RustTemplateModel):
294
+ template: ClassVar[str] = "poly_containers.rs.jinja"
295
+
296
+
297
+ class Import(Import_, RustTemplateModel):
298
+ template: ClassVar[str] = "import.rs.jinja"
299
+
300
+ version: Optional[str] = None
301
+ """Version specifier to use in Cargo.toml"""
302
+ features: Optional[list[str]] = None
303
+ """Features to require in Cargo.toml"""
304
+ ## whether this import should be behind a feature flag
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."""
308
+
309
+
310
+ class Imports(Imports_, RustTemplateModel):
311
+ template: ClassVar[str] = "imports.rs.jinja"
312
+
313
+
314
+ class RustProperty(RustTemplateModel):
315
+ """
316
+ A property within a rust struct
317
+ """
318
+
319
+ template: ClassVar[str] = "property.rs.jinja"
320
+ inline_mode: str
321
+ alias: Optional[str] = None
322
+ generate_merge: bool = False
323
+ container_mode: str
324
+ name: str
325
+ type_: RustRange # might be a union type, so list length > 1
326
+ required: bool
327
+ multivalued: bool = False
328
+ is_key_value: bool = False
329
+
330
+ @computed_field
331
+ def optional(self) -> bool:
332
+ """
333
+ Whether this property is optional
334
+ """
335
+ return self.type_.optional
336
+
337
+ @computed_field
338
+ def merge_strategy(self) -> str:
339
+ if self.type_.optional:
340
+ return "strategy = overwrite_except_none"
341
+ elif self.type_.containerType == ContainerType.LIST:
342
+ return "skip"
343
+ elif self.type_.containerType == ContainerType.MAPPING:
344
+ return "strategy = merge::hashmap::overwrite"
345
+ else:
346
+ return "skip"
347
+
348
+ @computed_field
349
+ def type_for_field(self) -> str:
350
+ """
351
+ The type of this field, as it would be used in a struct definition
352
+ """
353
+ return self.type_.type_for_field()
354
+
355
+ @computed_field
356
+ def hasdefault(self) -> bool:
357
+ return self.multivalued or not self.required
358
+
359
+
360
+ class AsKeyValue(RustTemplateModel):
361
+ """
362
+ A key-value representation for this struct
363
+ """
364
+
365
+ template: ClassVar[str] = "as_key_value.rs.jinja"
366
+ name: str
367
+ key_property_name: str
368
+ key_property_type: str
369
+ value_property_name: str
370
+ value_property_type: str
371
+ can_convert_from_primitive: bool = False
372
+ can_convert_from_empty: bool = False
373
+ value_property_optional: bool = False
374
+
375
+
376
+ class RustStructOrSubtypeEnum(RustTemplateModel):
377
+ template: ClassVar[str] = "struct_or_subtype_enum.rs.jinja"
378
+ enum_name: str
379
+ struct_names: list[str]
380
+ as_key_value: bool = False
381
+ type_designator_field: Optional[str] = None
382
+ type_designators: dict[str, str]
383
+ key_property_type: str = "String"
384
+
385
+
386
+ class SlotRangeAsUnion(RustTemplateModel):
387
+ """
388
+ A union of ranges!
389
+ """
390
+
391
+ template: ClassVar[str] = "slot_range_as_union.rs.jinja"
392
+ slot_name: str
393
+ ranges: list[str]
394
+
395
+
396
+ class RustClassModule(RustTemplateModel):
397
+ class_name: str
398
+ class_name_snakecase: str
399
+ template: ClassVar[str] = "class_module.rs.jinja"
400
+ slot_ranges: list[SlotRangeAsUnion]
401
+
402
+
403
+ class RustStruct(RustTemplateModel):
404
+ """
405
+ A struct!
406
+ """
407
+
408
+ template: ClassVar[str] = "struct.rs.jinja"
409
+ special_case_enabled: bool = False
410
+ class_module: Optional[RustClassModule] = None
411
+
412
+ name: str
413
+ bases: Optional[list[str]] = None
414
+ """
415
+ Base classes to inherit from - must have entire MRO, just just immediate ancestor
416
+ """
417
+ properties: list[RustProperty] = Field(default_factory=list)
418
+ unsendable: bool = False
419
+ generate_merge: bool = False
420
+ as_key_value: Optional[AsKeyValue] = None
421
+ struct_or_subtype_enum: Optional[RustStructOrSubtypeEnum] = None
422
+
423
+ @computed_field()
424
+ def property_names_and_types(self) -> dict[str, str]:
425
+ return [(p.name, p.type_.type_for_constructor()) for p in self.constructor_params]
426
+
427
+ @computed_field()
428
+ def property_names(self) -> list[str]:
429
+ return [p.name for p in self.properties]
430
+
431
+ @computed_field()
432
+ def constructor_params(self) -> list[RustProperty]:
433
+ required = [p for p in self.properties if not p.type_.optional]
434
+ optional = [p for p in self.properties if p.type_.optional]
435
+ return required + optional
436
+
437
+ @computed_field()
438
+ def constructor_signature(self) -> str:
439
+ parts: list[str] = []
440
+ for prop in self.constructor_params:
441
+ if prop.type_.optional:
442
+ parts.append(f"{prop.name}=None")
443
+ else:
444
+ parts.append(prop.name)
445
+ return ", ".join(parts)
446
+
447
+ @computed_field()
448
+ def constructor_conversions(self) -> list[tuple[str, str]]:
449
+ return [
450
+ (p.name, p.type_.convert_constructor_value(p.name))
451
+ for p in self.constructor_params
452
+ if p.type_.needs_constructor_conversion()
453
+ ]
454
+
455
+
456
+ class RustEnum(RustTemplateModel):
457
+ """
458
+ A rust enum!
459
+ """
460
+
461
+ template: ClassVar[str] = "enum.rs.jinja"
462
+
463
+ name: str
464
+ items: list["RustEnumItem"]
465
+
466
+
467
+ class RustEnumItem(BaseModel):
468
+ """Single enum variant with its original permissible value text."""
469
+
470
+ variant: str
471
+ text: str
472
+
473
+ @computed_field
474
+ def python_literals(self) -> list[str]:
475
+ """Return acceptable string literals when converting from Python."""
476
+
477
+ literals = [self.text]
478
+ if self.variant != self.text:
479
+ literals.append(self.variant)
480
+ return literals
481
+
482
+ @staticmethod
483
+ def _escape(value: str) -> str:
484
+ return value.replace("\\", "\\\\").replace('"', '\\"')
485
+
486
+ @computed_field
487
+ def text_literal(self) -> str:
488
+ """Escaped literal suitable for embedding in Rust source."""
489
+
490
+ return self._escape(self.text)
491
+
492
+ @computed_field
493
+ def python_match_pattern(self) -> str:
494
+ """Match arm pattern accepting any permitted Python literal."""
495
+
496
+ literals = []
497
+ for literal in self.python_literals:
498
+ if literal not in literals:
499
+ literals.append(literal)
500
+ escaped = [f'"{self._escape(lit)}"' for lit in literals]
501
+ return " | ".join(escaped)
502
+
503
+
504
+ class RustTypeAlias(RustTemplateModel):
505
+ """
506
+ A type alias used to represent slots
507
+ """
508
+
509
+ template: ClassVar[str] = "typealias.rs.jinja"
510
+
511
+ name: str
512
+ type_: str
513
+ description: Optional[str] = None
514
+ multivalued: Optional[bool] = False
515
+ class_range: bool = False
516
+ slot_range_as_union: Optional[SlotRangeAsUnion] = None
517
+
518
+ @field_validator("attributes", mode="before")
519
+ @classmethod
520
+ def attr_values_as_strings(cls, value: dict[str, any]) -> dict[str, str]:
521
+ return {k: str(v) for k, v in value.items()}
522
+
523
+
524
+ class SerdeUtilsFile(RustTemplateModel):
525
+ """
526
+ A file containing utility functions for serde serialization/deserialization
527
+ """
528
+
529
+ template: ClassVar[str] = "serde_utils.rs.jinja"
530
+
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
+
552
+ class PolyTraitProperty(RustTemplateModel):
553
+ template: ClassVar[str] = "poly_trait_property.rs.jinja"
554
+ name: str
555
+ range: RustRange
556
+ promoted_range: RustRange
557
+ promoted_range: RustRange
558
+
559
+ @computed_field
560
+ def class_range(self) -> bool:
561
+ """
562
+ Whether this range is a class range
563
+ """
564
+ return self.range.is_class_range
565
+
566
+ @computed_field
567
+ def type_getter(self) -> str:
568
+ # Centralized via RustTypeViews builder
569
+ views = build_trait_views_for_promoted(self.promoted_range)
570
+ return views.type_getter
571
+
572
+ @computed_field
573
+ def needs_lifetime(self) -> bool:
574
+ """Whether the trait getter requires an explicit lifetime."""
575
+ views = build_trait_views_for_promoted(self.promoted_range)
576
+ return views.needs_lifetime
577
+
578
+ @computed_field
579
+ def type_setter(self) -> str:
580
+ return self.range.type_for_trait(setter=True, crateref="crate")
581
+
582
+ @computed_field
583
+ def type_bound(self) -> Optional[str]:
584
+ """
585
+ The type bound for the setter method
586
+ """
587
+ return self.range.type_bound_for_setter(crateref="crate")
588
+
589
+
590
+ class PolyTraitPropertyImpl(RustTemplateModel):
591
+ template: ClassVar[str] = "poly_trait_property_impl.rs.jinja"
592
+ name: str
593
+ range: RustRange
594
+ struct_name: str
595
+ definition_range: RustRange
596
+ trait_range: RustRange
597
+
598
+ @computed_field
599
+ def is_copy(self) -> bool:
600
+ """
601
+ Whether this range is a copy type
602
+ """
603
+ return self.range.is_copy()
604
+
605
+ @computed_field
606
+ def need_option_wrap(self) -> bool:
607
+ return self.definition_range.optional and not self.range.optional
608
+
609
+ @computed_field
610
+ def class_range(self) -> bool:
611
+ """
612
+ Whether this range is a class range
613
+ """
614
+ return self.range.is_class_range
615
+
616
+ @computed_field
617
+ def ct(self) -> str:
618
+ """
619
+ The container type for this range, if any
620
+ """
621
+ return self.range.containerType.value if self.range.containerType else "None"
622
+
623
+ @computed_field
624
+ def type_getter(self) -> str:
625
+ # Mirror trait signature: promoted union by value; else concrete borrowed
626
+ if self.definition_range.child_ranges is not None and len(self.definition_range.child_ranges) > 1:
627
+ return build_trait_views_for_promoted(self.definition_range).type_getter
628
+ return build_trait_views_for_range(self.trait_range).type_getter
629
+
630
+ @computed_field
631
+ def needs_lifetime(self) -> bool:
632
+ sig = self.type_getter
633
+ return ("&" in sig) or ("SeqRef" in sig) or ("MapRef" in sig)
634
+
635
+ # Note: typed getters removed
636
+
637
+ @computed_field
638
+ def union_type(self) -> str:
639
+ return self.definition_range.type_
640
+
641
+ @computed_field
642
+ def range_variant(self) -> str:
643
+ # Use centralized type-name logic for a variant identifier
644
+ return self.range.type_name()
645
+
646
+ @computed_field
647
+ def current_union_types(self) -> dict[str, str]:
648
+ m: dict[str, str] = {}
649
+ for c in self.cases:
650
+ sc = underscore(uncamelcase(c))
651
+ m[c] = f"{sc}_utl::{self.name}_range"
652
+ return m
653
+
654
+ @computed_field
655
+ def base_union_variants(self) -> list[str]:
656
+ vs: list[str] = []
657
+ if self.definition_range.child_ranges is not None and len(self.definition_range.child_ranges) > 1:
658
+ for cr in self.definition_range.child_ranges:
659
+ vs.append(cr.type_name())
660
+ return vs
661
+
662
+ @computed_field
663
+ def current_union_type(self) -> str:
664
+ return self.range.type_
665
+
666
+ @computed_field
667
+ def type_setter(self) -> str:
668
+ return self.definition_range.type_for_trait(setter=True, crateref="crate")
669
+
670
+ @computed_field
671
+ def type_bound(self) -> Optional[str]:
672
+ """
673
+ The type bound for the setter method
674
+ """
675
+ return self.definition_range.type_bound_for_setter(crateref="crate")
676
+
677
+ @computed_field
678
+ def union_conversion_arms(self) -> list[str]:
679
+ """Pre-rendered match arms to convert current union -> base union.
680
+
681
+ Example: Current::A(x) => Base::A(x.clone()),
682
+ """
683
+ arms: list[str] = []
684
+ if self.definition_range.child_ranges is not None and len(self.definition_range.child_ranges) > 1:
685
+ base = self.union_type
686
+ cur = self.current_union_type
687
+ for vn in self.base_union_variants:
688
+ arms.append(f"{cur}::{vn}(x) => {base}::{vn}(x.clone()),")
689
+ return arms
690
+
691
+
692
+ class PolyTraitImpl(RustTemplateModel):
693
+ """Implementation of a :class:`PolyTrait` for a particular struct."""
694
+
695
+ template: ClassVar[str] = "poly_trait_impl.rs.jinja"
696
+ name: str
697
+ struct_name: str
698
+ attrs: list[PolyTraitPropertyImpl]
699
+
700
+
701
+ class PolyTraitPropertyMatch(RustTemplateModel):
702
+ template: ClassVar[str] = "poly_trait_property_match.rs.jinja"
703
+ name: str
704
+ range: RustRange
705
+ struct_name: str
706
+ cases: list[str]
707
+
708
+ @computed_field
709
+ def is_optional(self) -> bool:
710
+ """
711
+ Whether this property is optional
712
+ """
713
+ return self.range.optional
714
+
715
+ @computed_field
716
+ def is_container(self) -> bool:
717
+ """
718
+ Whether this property is a container type
719
+ """
720
+ return self.range.containerType is not None
721
+
722
+ @computed_field
723
+ def type_getter(self) -> str:
724
+ if self.range.child_ranges is not None and len(self.range.child_ranges) > 1:
725
+ return self.range.type_for_trait_value(crateref="crate")
726
+ return self.range.type_for_trait(setter=False, crateref="crate")
727
+
728
+ @computed_field
729
+ def needs_lifetime(self) -> bool:
730
+ t = self.type_getter
731
+ return ("&" in t) or ("SeqRef" in t) or ("MapRef" in t)
732
+
733
+ @computed_field
734
+ def base_union_type(self) -> str:
735
+ return self.range.type_
736
+
737
+ @computed_field
738
+ def range_variant(self) -> str:
739
+ # Always use centralized type name logic
740
+ return self.range.type_name()
741
+
742
+ @computed_field
743
+ def current_union_types(self) -> dict[str, str]:
744
+ m: dict[str, str] = {}
745
+ for c in self.cases:
746
+ sc = underscore(uncamelcase(c))
747
+ m[c] = f"{sc}_utl::{self.name}_range"
748
+ return m
749
+
750
+
751
+ class PolyTraitImplForSubtypeEnum(RustTemplateModel):
752
+ """Trait implementation that dispatches based on subtype enums."""
753
+
754
+ template: ClassVar[str] = "poly_trait_impl_orsubtype.rs.jinja"
755
+ enum_name: str
756
+ name: str
757
+ attrs: list[PolyTraitPropertyMatch]
758
+
759
+
760
+ class PolyTrait(RustTemplateModel):
761
+ """Definition of a polymorphic trait generated from a class hierarchy."""
762
+
763
+ template: ClassVar[str] = "poly_trait.rs.jinja"
764
+ name: str
765
+ attrs: list[PolyTraitProperty]
766
+ superclass_names: list[str]
767
+ impls: list[PolyTraitImpl]
768
+ subtypes: list[PolyTraitImplForSubtypeEnum]
769
+
770
+
771
+ class PolyFile(RustTemplateModel):
772
+ """Rust file aggregating polymorphic traits."""
773
+
774
+ template: ClassVar[str] = "poly.rs.jinja"
775
+ imports: Imports = Imports()
776
+ traits: list[PolyTrait]
777
+
778
+
779
+ class RustFile(RustTemplateModel):
780
+ """
781
+ A whole rust file!
782
+ """
783
+
784
+ template: ClassVar[str] = "file.rs.jinja"
785
+
786
+ name: str
787
+ imports: Imports = Imports()
788
+ types: list[RustTypeAlias] = Field(default_factory=list)
789
+ structs: list[RustStruct] = Field(default_factory=list)
790
+ enums: list[RustEnum] = Field(default_factory=list)
791
+ slots: list[RustTypeAlias] = Field(default_factory=list)
792
+ # Single-file generation knobs
793
+ inline_serde_utils: bool = False
794
+ emit_poly: bool = True
795
+ serde_utils: Optional[SerdeUtilsFile] = None
796
+ root_struct_name: Optional[str] = None
797
+
798
+ @computed_field
799
+ def struct_names(self) -> list[str]:
800
+ """Names of all the structs we have!"""
801
+ return [c.name for c in self.structs]
802
+
803
+ @computed_field
804
+ def pyclass_struct_names(self) -> list[str]:
805
+ """Names of structs that implement PyClass and should be registered.
806
+
807
+ Excludes special cases like `Anything` (and its alias `AnyValue`) which
808
+ are not exposed as #[pyclass] in the special-case template.
809
+ """
810
+ out: list[str] = []
811
+ for c in self.structs:
812
+ if c.name in ("Anything", "AnyValue"):
813
+ continue
814
+ out.append(c.name)
815
+ return out
816
+
817
+ @computed_field
818
+ def needs_overwrite_except_none(self) -> bool:
819
+ """Whether any struct uses the custom merge helper."""
820
+ return any(s.generate_merge for s in self.structs)
821
+
822
+
823
+ class RangeEnum(RustTemplateModel):
824
+ """
825
+ A range enum!
826
+ """
827
+
828
+ template: ClassVar[str] = "range_enum.rs.jinja"
829
+ name: str
830
+ type_: list[str]
831
+
832
+
833
+ class RustCargo(RustTemplateModel):
834
+ """
835
+ A Cargo.toml file
836
+ """
837
+
838
+ template: ClassVar[str] = "Cargo.toml.jinja"
839
+
840
+ name: str
841
+ version: str = "0.0.0"
842
+ edition: str = "2021"
843
+ pyo3_version: str = "0.21.1"
844
+ imports: Imports = Imports()
845
+
846
+ @computed_field
847
+ def cratefeatures(self) -> dict[str, list[str]]:
848
+ feature_flags: dict[str, list[str]] = {}
849
+ for i in self.imports.imports:
850
+ assert isinstance(i, Import)
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)
861
+ return feature_flags
862
+
863
+ @field_validator("name", mode="after")
864
+ @classmethod
865
+ def snake_case_name(cls, value: str) -> str:
866
+ return underscore(value)
867
+
868
+ def render(self, environment: Optional[Environment] = None, **kwargs) -> str:
869
+ if environment is None:
870
+ environment = RustTemplateModel.environment()
871
+
872
+ fields = {**self.model_fields, **self.model_computed_fields}
873
+ data = {k: getattr(self, k, None) for k in fields}
874
+ # don't render the template
875
+ imports = []
876
+ for i in data.get("imports", []):
877
+ imp = i.model_dump()
878
+ imp["module"] = imp["module"].split("::")[0]
879
+ imports.append(imp)
880
+ data["imports"] = imports
881
+
882
+ data = {k: _render(v, environment) for k, v in data.items()}
883
+ template = environment.get_template(self.template)
884
+ rendered = template.render(**data)
885
+ return rendered
886
+
887
+
888
+ class RustPyProject(RustTemplateModel):
889
+ """
890
+ A pyproject.toml file to go with a maturin/pyo3 package
891
+ """
892
+
893
+ template: ClassVar[str] = "pyproject.toml.jinja"
894
+
895
+ name: str
896
+ version: str = "0.0.0"
897
+
898
+ @field_validator("name", mode="after")
899
+ @classmethod
900
+ def snake_case_name(cls, value: str) -> str:
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