linkml 1.9.4rc1__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.
Files changed (77) hide show
  1. linkml/cli/main.py +4 -0
  2. linkml/generators/__init__.py +2 -0
  3. linkml/generators/common/build.py +5 -20
  4. linkml/generators/common/template.py +289 -3
  5. linkml/generators/docgen.py +55 -10
  6. linkml/generators/erdiagramgen.py +9 -5
  7. linkml/generators/graphqlgen.py +32 -6
  8. linkml/generators/jsonldcontextgen.py +78 -12
  9. linkml/generators/jsonschemagen.py +29 -12
  10. linkml/generators/mermaidclassdiagramgen.py +21 -3
  11. linkml/generators/owlgen.py +4 -1
  12. linkml/generators/panderagen/dataframe_class.py +13 -0
  13. linkml/generators/panderagen/dataframe_field.py +50 -0
  14. linkml/generators/panderagen/linkml_pandera_validator.py +186 -0
  15. linkml/generators/panderagen/panderagen.py +22 -5
  16. linkml/generators/panderagen/panderagen_class_based/class.jinja2 +70 -13
  17. linkml/generators/panderagen/panderagen_class_based/custom_checks.jinja2 +27 -0
  18. linkml/generators/panderagen/panderagen_class_based/enums.jinja2 +3 -3
  19. linkml/generators/panderagen/panderagen_class_based/pandera.jinja2 +12 -2
  20. linkml/generators/panderagen/panderagen_class_based/slots.jinja2 +19 -17
  21. linkml/generators/panderagen/slot_generator_mixin.py +143 -16
  22. linkml/generators/panderagen/transforms/__init__.py +19 -0
  23. linkml/generators/panderagen/transforms/collection_dict_model_transform.py +62 -0
  24. linkml/generators/panderagen/transforms/list_dict_model_transform.py +66 -0
  25. linkml/generators/panderagen/transforms/model_transform.py +8 -0
  26. linkml/generators/panderagen/transforms/nested_struct_model_transform.py +27 -0
  27. linkml/generators/panderagen/transforms/simple_dict_model_transform.py +86 -0
  28. linkml/generators/plantumlgen.py +17 -11
  29. linkml/generators/pydanticgen/pydanticgen.py +53 -2
  30. linkml/generators/pydanticgen/template.py +45 -233
  31. linkml/generators/pydanticgen/templates/attribute.py.jinja +1 -0
  32. linkml/generators/pydanticgen/templates/base_model.py.jinja +16 -2
  33. linkml/generators/pydanticgen/templates/imports.py.jinja +1 -1
  34. linkml/generators/rdfgen.py +11 -2
  35. linkml/generators/rustgen/__init__.py +3 -0
  36. linkml/generators/rustgen/build.py +94 -0
  37. linkml/generators/rustgen/cli.py +65 -0
  38. linkml/generators/rustgen/rustgen.py +1038 -0
  39. linkml/generators/rustgen/template.py +865 -0
  40. linkml/generators/rustgen/templates/Cargo.toml.jinja +42 -0
  41. linkml/generators/rustgen/templates/anything.rs.jinja +142 -0
  42. linkml/generators/rustgen/templates/as_key_value.rs.jinja +56 -0
  43. linkml/generators/rustgen/templates/class_module.rs.jinja +8 -0
  44. linkml/generators/rustgen/templates/enum.rs.jinja +54 -0
  45. linkml/generators/rustgen/templates/file.rs.jinja +62 -0
  46. linkml/generators/rustgen/templates/import.rs.jinja +4 -0
  47. linkml/generators/rustgen/templates/imports.rs.jinja +8 -0
  48. linkml/generators/rustgen/templates/poly.rs.jinja +9 -0
  49. linkml/generators/rustgen/templates/poly_containers.rs.jinja +439 -0
  50. linkml/generators/rustgen/templates/poly_trait.rs.jinja +15 -0
  51. linkml/generators/rustgen/templates/poly_trait_impl.rs.jinja +5 -0
  52. linkml/generators/rustgen/templates/poly_trait_impl_orsubtype.rs.jinja +5 -0
  53. linkml/generators/rustgen/templates/poly_trait_property.rs.jinja +8 -0
  54. linkml/generators/rustgen/templates/poly_trait_property_impl.rs.jinja +132 -0
  55. linkml/generators/rustgen/templates/poly_trait_property_match.rs.jinja +10 -0
  56. linkml/generators/rustgen/templates/property.rs.jinja +19 -0
  57. linkml/generators/rustgen/templates/pyproject.toml.jinja +10 -0
  58. linkml/generators/rustgen/templates/serde_utils.rs.jinja +310 -0
  59. linkml/generators/rustgen/templates/slot_range_as_union.rs.jinja +61 -0
  60. linkml/generators/rustgen/templates/struct.rs.jinja +75 -0
  61. linkml/generators/rustgen/templates/struct_or_subtype_enum.rs.jinja +108 -0
  62. linkml/generators/rustgen/templates/typealias.rs.jinja +13 -0
  63. linkml/generators/sqltablegen.py +18 -16
  64. linkml/generators/yarrrmlgen.py +157 -0
  65. linkml/linter/config/datamodel/config.py +160 -293
  66. linkml/linter/config/datamodel/config.yaml +34 -26
  67. linkml/linter/config/default.yaml +4 -0
  68. linkml/linter/config/recommended.yaml +4 -0
  69. linkml/linter/linter.py +1 -2
  70. linkml/linter/rules.py +37 -0
  71. linkml/utils/schemaloader.py +55 -3
  72. {linkml-1.9.4rc1.dist-info → linkml-1.9.5rc1.dist-info}/METADATA +2 -2
  73. {linkml-1.9.4rc1.dist-info → linkml-1.9.5rc1.dist-info}/RECORD +76 -38
  74. {linkml-1.9.4rc1.dist-info → linkml-1.9.5rc1.dist-info}/entry_points.txt +1 -0
  75. linkml/generators/panderagen/panderagen_class_based/mixins.jinja2 +0 -26
  76. {linkml-1.9.4rc1.dist-info → linkml-1.9.5rc1.dist-info}/WHEEL +0 -0
  77. {linkml-1.9.4rc1.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)