linkml 1.9.5rc1__py3-none-any.whl → 1.9.5rc2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. linkml/cli/main.py +1 -1
  2. linkml/converter/__init__.py +0 -0
  3. linkml/generators/owlgen.py +9 -1
  4. linkml/generators/rustgen/build.py +3 -0
  5. linkml/generators/rustgen/cli.py +19 -1
  6. linkml/generators/rustgen/rustgen.py +169 -21
  7. linkml/generators/rustgen/template.py +51 -6
  8. linkml/generators/rustgen/templates/Cargo.toml.jinja +2 -2
  9. linkml/generators/rustgen/templates/anything.rs.jinja +8 -1
  10. linkml/generators/rustgen/templates/as_key_value.rs.jinja +33 -3
  11. linkml/generators/rustgen/templates/enum.rs.jinja +16 -0
  12. linkml/generators/rustgen/templates/file.rs.jinja +13 -0
  13. linkml/generators/rustgen/templates/lib_shim.rs.jinja +52 -0
  14. linkml/generators/rustgen/templates/poly_trait_property_impl.rs.jinja +3 -1
  15. linkml/generators/rustgen/templates/property.rs.jinja +12 -3
  16. linkml/generators/rustgen/templates/serde_utils.rs.jinja +186 -6
  17. linkml/generators/rustgen/templates/slot_range_as_union.rs.jinja +3 -0
  18. linkml/generators/rustgen/templates/struct.rs.jinja +6 -0
  19. linkml/generators/rustgen/templates/struct_or_subtype_enum.rs.jinja +4 -1
  20. linkml/generators/rustgen/templates/stub_gen.rs.jinja +71 -0
  21. linkml/generators/rustgen/templates/stub_utils.rs.jinja +76 -0
  22. linkml/generators/yarrrmlgen.py +20 -4
  23. linkml/utils/schema_builder.py +2 -0
  24. {linkml-1.9.5rc1.dist-info → linkml-1.9.5rc2.dist-info}/METADATA +1 -1
  25. {linkml-1.9.5rc1.dist-info → linkml-1.9.5rc2.dist-info}/RECORD +29 -25
  26. {linkml-1.9.5rc1.dist-info → linkml-1.9.5rc2.dist-info}/entry_points.txt +1 -1
  27. /linkml/{utils/converter.py → converter/cli.py} +0 -0
  28. {linkml-1.9.5rc1.dist-info → linkml-1.9.5rc2.dist-info}/WHEEL +0 -0
  29. {linkml-1.9.5rc1.dist-info → linkml-1.9.5rc2.dist-info}/licenses/LICENSE +0 -0
@@ -17,7 +17,9 @@ impl serde_utils::InlinedPair for {{ name }} {
17
17
  Value::Map(m) => m,
18
18
  _ => return Err("ClassDefinition must be a mapping".into()),
19
19
  };
20
- map.insert(Value::String("{{key_property_name}}".into()), Value::String(k));
20
+ let key_value = serde_value::to_value(k.clone())
21
+ .map_err(|e| format!("unable to serialize key: {}", e))?;
22
+ map.insert(Value::String("{{key_property_name}}".into()), key_value);
21
23
  let de = Value::Map(map).into_deserializer();
22
24
  match serde_path_to_error::deserialize(de) {
23
25
  Ok(ok) => Ok(ok),
@@ -29,7 +31,9 @@ impl serde_utils::InlinedPair for {{ name }} {
29
31
  {% if can_convert_from_primitive %}
30
32
  fn from_pair_simple(k: Self::Key, v: Value) -> Result<Self,Self::Error> {
31
33
  let mut map: BTreeMap<Value, Value> = BTreeMap::new();
32
- map.insert(Value::String("{{ key_property_name }}".into()), Value::String(k));
34
+ let key_value = serde_value::to_value(k.clone())
35
+ .map_err(|e| format!("unable to serialize key: {}", e))?;
36
+ map.insert(Value::String("{{ key_property_name }}".into()), key_value);
33
37
  map.insert(Value::String("{{ value_property_name }}".into()), v);
34
38
  let de = Value::Map(map).into_deserializer();
35
39
  match serde_path_to_error::deserialize(de) {
@@ -40,7 +44,9 @@ impl serde_utils::InlinedPair for {{ name }} {
40
44
  {% elif can_convert_from_empty %}
41
45
  fn from_pair_simple(k: Self::Key, _v: Value) -> Result<Self,Self::Error> {
42
46
  let mut map: BTreeMap<Value, Value> = BTreeMap::new();
43
- map.insert(Value::String("{{ key_property_name }}".into()), Value::String(k));
47
+ let key_value = serde_value::to_value(k.clone())
48
+ .map_err(|e| format!("unable to serialize key: {}", e))?;
49
+ map.insert(Value::String("{{ key_property_name }}".into()), key_value);
44
50
  let de = Value::Map(map).into_deserializer();
45
51
  match serde_path_to_error::deserialize(de) {
46
52
  Ok(ok) => Ok(ok),
@@ -53,4 +59,28 @@ impl serde_utils::InlinedPair for {{ name }} {
53
59
  Err("Cannot create a {{name}} from a primitive value!".into())
54
60
  {% endif %}
55
61
  }
62
+
63
+ {% if can_convert_from_primitive %}
64
+ fn simple_value(&self) -> Option<&Self::Value> {
65
+ {% if value_property_optional %}
66
+ self.{{ value_property_name }}.as_ref()
67
+ {% else %}
68
+ Some(&self.{{ value_property_name }})
69
+ {% endif %}
70
+ }
71
+ {% endif %}
72
+
73
+ fn compact_value(&self) -> Option<Value> {
74
+ let value = match serde_value::to_value(self) {
75
+ Ok(v) => v,
76
+ Err(_) => return None,
77
+ };
78
+ match value {
79
+ Value::Map(mut map) => {
80
+ map.remove(&Value::String("{{ key_property_name }}".into()));
81
+ Some(Value::Map(map))
82
+ }
83
+ _ => None,
84
+ }
85
+ }
56
86
  }
@@ -52,3 +52,19 @@ impl<'py> FromPyObject<'py> for {{ name }} {
52
52
  }
53
53
  }
54
54
  }
55
+
56
+ #[cfg(feature = "stubgen")]
57
+ impl ::pyo3_stub_gen::PyStubType for {{ name }} {
58
+ fn type_output() -> ::pyo3_stub_gen::TypeInfo {
59
+ {% set literal_values = items | map(attribute='text_literal') | list %}
60
+ {% set escaped_values = literal_values | map('replace', "'", "\\'" ) | list %}
61
+ {% if escaped_values %}
62
+ ::pyo3_stub_gen::TypeInfo::with_module(
63
+ "typing.Literal['{{ escaped_values | join("', '") }}']",
64
+ "typing".into(),
65
+ )
66
+ {% else %}
67
+ ::pyo3_stub_gen::TypeInfo::ident("typing.Any")
68
+ {% endif %}
69
+ }
70
+ }
@@ -11,6 +11,10 @@ mod serde_utils;
11
11
  pub mod poly;
12
12
  pub mod poly_containers;
13
13
  {% endif %}
14
+ {% if stubgen %}
15
+ #[cfg(feature = "stubgen")]
16
+ pub mod stub_utils;
17
+ {% endif %}
14
18
 
15
19
  {{ imports }}
16
20
 
@@ -48,12 +52,21 @@ fn overwrite_except_none<T>(left: &mut Option<T>, right: Option<T>) {
48
52
  }
49
53
  {% endif %}
50
54
 
55
+ {% if stubgen %}
56
+ #[cfg(feature = "stubgen")]
57
+ define_stub_info_gatherer!(stub_info);
58
+ {% endif %}
59
+
51
60
 
52
61
  {% if pyo3 %}
53
62
  #[cfg(feature = "pyo3")]
63
+ {% if handwritten_lib %}
64
+ pub fn register_pymodule(m: &Bound<'_, PyModule>) -> PyResult<()> {
65
+ {% else %}
54
66
  #[pymodule]
55
67
  #[pyo3(name="{{ name }}")]
56
68
  fn {{ name }}(m: &Bound<'_, PyModule>) -> PyResult<()> {
69
+ {% endif %}
57
70
  {% for s in pyclass_struct_names %}
58
71
  m.add_class::<{{ s }}>()?;
59
72
  {% endfor %}
@@ -0,0 +1,52 @@
1
+ //! This file will only be autogenerated once, so it is safe to manually edit (unlike the other generated files)
2
+ //! If you want to re-generate it, just delete it and rerun rust generator.
3
+
4
+ pub mod generated;
5
+
6
+ pub use generated::*;
7
+ pub use chrono::NaiveDate;
8
+ pub use chrono::NaiveDateTime;
9
+
10
+ {% if root_struct_name %}
11
+ #[cfg(feature = "serde")]
12
+ /// Example helper that loads a YAML document into the root class. Edit or extend as needed.
13
+ pub fn load_yaml_{{ root_struct_fn_snake }}<P>(
14
+ path: P,
15
+ ) -> Result<generated::{{ root_struct_name }}, Box<dyn std::error::Error + Send + Sync>>
16
+ where
17
+ P: AsRef<std::path::Path>,
18
+ {
19
+ let file = std::fs::File::open(path)?;
20
+ let reader = std::io::BufReader::new(file);
21
+ let parsed = serde_yml::from_reader(reader)?;
22
+ Ok(parsed)
23
+ }
24
+
25
+ #[cfg(all(feature = "pyo3", feature = "serde"))]
26
+ #[pyfunction(name = "load_yaml_{{ root_struct_fn_snake }}")]
27
+ fn load_yaml_{{ root_struct_fn_snake }}_py(path: &str) -> PyResult<generated::{{ root_struct_name }}> {
28
+ load_yaml_{{ root_struct_fn_snake }}(path)
29
+ .map_err(|err| PyErr::new::<pyo3::exceptions::PyIOError, _>(err.to_string()))
30
+ }
31
+ {% endif %}
32
+
33
+ {% if pyo3 %}
34
+ #[cfg(feature = "pyo3")]
35
+ use pyo3::prelude::*;
36
+ #[cfg(all(feature = "pyo3", feature = "serde"))]
37
+ use pyo3::wrap_pyfunction;
38
+
39
+ #[cfg(feature = "pyo3")]
40
+ #[pymodule]
41
+ #[pyo3(name="{{ module_name }}")]
42
+ fn {{ module_name }}(m: &Bound<'_, PyModule>) -> PyResult<()> {
43
+ generated::register_pymodule(m)?;
44
+ {% if root_struct_name %}
45
+ #[cfg(all(feature = "serde", feature = "pyo3"))]
46
+ {
47
+ m.add_function(wrap_pyfunction!(load_yaml_{{ root_struct_fn_snake }}_py, m)?)?;
48
+ }
49
+ {% endif %}
50
+ Ok(())
51
+ }
52
+ {% endif %}
@@ -118,7 +118,9 @@
118
118
  return Some(&self.{{ name }});
119
119
  {% endif %}
120
120
  {% else %}
121
- {% if is_copy %}
121
+ {% if ct == 'list' or ct == 'mapping' %}
122
+ return &self.{{ name }};
123
+ {% elif is_copy %}
122
124
  return self.{{ name }};
123
125
  {% elif type_getter == "&'a str" or type_getter == "&str" %}
124
126
  return &self.{{ name }}[..];
@@ -3,12 +3,21 @@
3
3
  {% endif %}
4
4
  {%- if is_key_value -%}
5
5
  {% if container_mode == "list" and inline_mode == "inline" %}
6
- #[cfg_attr(feature = "serde", serde(deserialize_with = "serde_utils::deserialize_inlined_dict_list{% if optional %}_optional{% endif %}{% if recursive %}_box{% endif %}"))]
6
+ #[cfg_attr(feature = "serde", serde(
7
+ deserialize_with = "serde_utils::deserialize_inlined_dict_list{% if optional %}_optional{% endif %}{% if recursive %}_box{% endif %}",
8
+ serialize_with = "serde_utils::serialize_inlined_dict_list{% if optional %}_optional{% endif %}"
9
+ ))]
7
10
  {% elif container_mode == "mapping" and inline_mode == "inline" %}
8
- #[cfg_attr(feature = "serde", serde(deserialize_with = "serde_utils::deserialize_inlined_dict_map{% if optional %}_optional{% endif %}{% if recursive %}_box{% endif %}"))]
11
+ #[cfg_attr(feature = "serde", serde(
12
+ deserialize_with = "serde_utils::deserialize_inlined_dict_map{% if optional %}_optional{% endif %}{% if recursive %}_box{% endif %}",
13
+ serialize_with = "serde_utils::serialize_inlined_dict_map{% if optional %}_optional{% endif %}"
14
+ ))]
9
15
  {% endif %}
10
16
  {% elif container_mode == "list" and inline_mode == "primitive" %}
11
- #[cfg_attr(feature = "serde", serde(deserialize_with = "serde_utils::deserialize_primitive_list_or_single_value{% if optional %}_optional{% endif %}"))]
17
+ #[cfg_attr(feature = "serde", serde(
18
+ deserialize_with = "serde_utils::deserialize_primitive_list_or_single_value{% if optional %}_optional{% endif %}",
19
+ serialize_with = "serde_utils::serialize_primitive_list_or_single_value{% if optional %}_optional{% endif %}"
20
+ ))]
12
21
  {% endif -%}
13
22
  {% if hasdefault %}
14
23
  #[cfg_attr(feature = "serde", serde(default))]
@@ -1,9 +1,15 @@
1
1
  #[cfg(feature = "serde")]
2
- use serde::{de::{DeserializeOwned, IntoDeserializer}, Deserialize, Deserializer};
2
+ use serde::{
3
+ de::{DeserializeOwned, IntoDeserializer},
4
+ Deserialize,
5
+ Deserializer,
6
+ Serialize,
7
+ Serializer,
8
+ };
3
9
  #[cfg(feature = "serde")]
4
10
  use serde::de::Error;
5
11
  #[cfg(feature = "serde")]
6
- use serde_value::{Value, ValueDeserializer};
12
+ use serde_value::{to_value, Value, ValueDeserializer};
7
13
  #[cfg(feature = "serde")]
8
14
  use std::collections::{BTreeMap, HashMap};
9
15
  #[cfg(all(feature = "serde", feature = "pyo3"))]
@@ -20,6 +26,14 @@ pub trait InlinedPair: Sized {
20
26
  fn from_pair_mapping(k: Self::Key, v: Value) -> Result<Self,Self::Error>;
21
27
  fn from_pair_simple(k: Self::Key, v: Value) -> Result<Self,Self::Error>;
22
28
  fn extract_key(&self) -> &Self::Key;
29
+
30
+ fn simple_value(&self) -> Option<&Self::Value> {
31
+ None
32
+ }
33
+
34
+ fn compact_value(&self) -> Option<Value> {
35
+ None
36
+ }
23
37
  }
24
38
 
25
39
  #[cfg(feature = "serde")]
@@ -46,6 +60,14 @@ where
46
60
  T::extract_key(self)
47
61
  }
48
62
 
63
+ fn simple_value(&self) -> Option<&Self::Value> {
64
+ self.as_ref().simple_value()
65
+ }
66
+
67
+ fn compact_value(&self) -> Option<Value> {
68
+ self.as_ref().compact_value()
69
+ }
70
+
49
71
  }
50
72
 
51
73
  #[cfg(feature = "serde")]
@@ -176,8 +198,12 @@ where
176
198
  }
177
199
 
178
200
  pub fn deserialize_primitive_list_or_single_value<'de, D, T>(
179
- deserializer: D
180
- ) -> Result<Vec<T>, D::Error> where D: Deserializer<'de>, T: Deserialize<'de> {
201
+ deserializer: D
202
+ ) -> Result<Vec<T>, D::Error>
203
+ where
204
+ D: Deserializer<'de>,
205
+ T: Deserialize<'de>,
206
+ {
181
207
  let ast: Value = Value::deserialize(deserializer)?;
182
208
  match ast {
183
209
  Value::Seq(seq) => {
@@ -197,8 +223,12 @@ pub fn deserialize_primitive_list_or_single_value<'de, D, T>(
197
223
 
198
224
 
199
225
  pub fn deserialize_primitive_list_or_single_value_optional<'de, D, T>(
200
- deserializer: D
201
- ) -> Result<Option<Vec<T>>, D::Error> where D: Deserializer<'de>, T: Deserialize<'de> {
226
+ deserializer: D
227
+ ) -> Result<Option<Vec<T>>, D::Error>
228
+ where
229
+ D: Deserializer<'de>,
230
+ T: Deserialize<'de>,
231
+ {
202
232
  let ast: Value = Value::deserialize(deserializer)?;
203
233
  match ast {
204
234
  Value::Unit => Ok(None),
@@ -274,6 +304,146 @@ fn py_any_to_value(bound: &Bound<'_, PyAny>) -> PyResult<Value> {
274
304
  ))
275
305
  }
276
306
 
307
+ #[cfg(feature = "serde")]
308
+ #[allow(dead_code)]
309
+ pub fn serialize_inlined_dict_map<S, T>(
310
+ value: &HashMap<T::Key, T>,
311
+ serializer: S,
312
+ ) -> Result<S::Ok, S::Error>
313
+ where
314
+ S: Serializer,
315
+ T: InlinedPair + Serialize,
316
+ T::Key: Serialize + Clone + Ord,
317
+ T::Value: Serialize,
318
+ {
319
+ let mut ordered: BTreeMap<T::Key, &T> = BTreeMap::new();
320
+ for (k, v) in value.iter() {
321
+ ordered.insert(k.clone(), v);
322
+ }
323
+
324
+ let mut as_values: BTreeMap<T::Key, Value> = BTreeMap::new();
325
+ for (k, v) in ordered.iter() {
326
+ if let Some(simple) = v.simple_value() {
327
+ let val = to_value(simple)
328
+ .map_err(|e| <S::Error as serde::ser::Error>::custom(e))?;
329
+ as_values.insert((*k).clone(), val);
330
+ continue;
331
+ }
332
+ if let Some(compact) = v.compact_value() {
333
+ as_values.insert((*k).clone(), compact);
334
+ continue;
335
+ }
336
+ let val = to_value(*v)
337
+ .map_err(|e| <S::Error as serde::ser::Error>::custom(e))?;
338
+ as_values.insert((*k).clone(), val);
339
+ }
340
+ as_values.serialize(serializer)
341
+ }
342
+
343
+ #[cfg(feature = "serde")]
344
+ #[allow(dead_code)]
345
+ pub fn serialize_inlined_dict_map_optional<S, T>(
346
+ value: &Option<HashMap<T::Key, T>>,
347
+ serializer: S,
348
+ ) -> Result<S::Ok, S::Error>
349
+ where
350
+ S: Serializer,
351
+ T: InlinedPair + Serialize,
352
+ T::Key: Serialize + Clone + Ord,
353
+ T::Value: Serialize,
354
+ {
355
+ match value {
356
+ Some(map) => serialize_inlined_dict_map(map, serializer),
357
+ None => serializer.serialize_none(),
358
+ }
359
+ }
360
+
361
+ #[cfg(feature = "serde")]
362
+ #[allow(dead_code)]
363
+ pub fn serialize_inlined_dict_list<S, T>(
364
+ value: &Vec<T>,
365
+ serializer: S,
366
+ ) -> Result<S::Ok, S::Error>
367
+ where
368
+ S: Serializer,
369
+ T: InlinedPair + Serialize,
370
+ T::Key: Serialize + Clone + Ord,
371
+ T::Value: Serialize,
372
+ {
373
+ let mut ordered: BTreeMap<T::Key, &T> = BTreeMap::new();
374
+ for item in value.iter() {
375
+ ordered.insert(item.extract_key().clone(), item);
376
+ }
377
+
378
+ let mut as_values: BTreeMap<T::Key, Value> = BTreeMap::new();
379
+ for (k, v) in ordered.iter() {
380
+ if let Some(simple) = v.simple_value() {
381
+ let val = to_value(simple)
382
+ .map_err(|e| <S::Error as serde::ser::Error>::custom(e))?;
383
+ as_values.insert((*k).clone(), val);
384
+ continue;
385
+ }
386
+ if let Some(compact) = v.compact_value() {
387
+ as_values.insert((*k).clone(), compact);
388
+ continue;
389
+ }
390
+ let val = to_value(*v)
391
+ .map_err(|e| <S::Error as serde::ser::Error>::custom(e))?;
392
+ as_values.insert((*k).clone(), val);
393
+ }
394
+ as_values.serialize(serializer)
395
+ }
396
+
397
+ #[cfg(feature = "serde")]
398
+ #[allow(dead_code)]
399
+ pub fn serialize_inlined_dict_list_optional<S, T>(
400
+ value: &Option<Vec<T>>,
401
+ serializer: S,
402
+ ) -> Result<S::Ok, S::Error>
403
+ where
404
+ S: Serializer,
405
+ T: InlinedPair + Serialize,
406
+ T::Key: Serialize + Clone + Ord,
407
+ T::Value: Serialize,
408
+ {
409
+ match value {
410
+ Some(items) => serialize_inlined_dict_list(items, serializer),
411
+ None => serializer.serialize_none(),
412
+ }
413
+ }
414
+
415
+ #[cfg(feature = "serde")]
416
+ #[allow(dead_code)]
417
+ pub fn serialize_primitive_list_or_single_value<S, T>(
418
+ value: &Vec<T>,
419
+ serializer: S,
420
+ ) -> Result<S::Ok, S::Error>
421
+ where
422
+ S: Serializer,
423
+ T: Serialize,
424
+ {
425
+ match value.as_slice() {
426
+ [single] => single.serialize(serializer),
427
+ _ => value.serialize(serializer),
428
+ }
429
+ }
430
+
431
+ #[cfg(feature = "serde")]
432
+ #[allow(dead_code)]
433
+ pub fn serialize_primitive_list_or_single_value_optional<S, T>(
434
+ value: &Option<Vec<T>>,
435
+ serializer: S,
436
+ ) -> Result<S::Ok, S::Error>
437
+ where
438
+ S: Serializer,
439
+ T: Serialize,
440
+ {
441
+ match value {
442
+ Some(v) => serialize_primitive_list_or_single_value(v, serializer),
443
+ None => serializer.serialize_none(),
444
+ }
445
+ }
446
+
277
447
  #[cfg(all(feature = "serde", feature = "pyo3"))]
278
448
  pub fn deserialize_py_any<'py, T>(bound: &Bound<'py, PyAny>) -> PyResult<T>
279
449
  where
@@ -299,6 +469,16 @@ impl<T> PyValue<T> {
299
469
  }
300
470
  }
301
471
 
472
+ #[cfg(all(feature = "pyo3", feature = "stubgen"))]
473
+ impl<T> ::pyo3_stub_gen::PyStubType for PyValue<T>
474
+ where
475
+ T: ::pyo3_stub_gen::PyStubType,
476
+ {
477
+ fn type_output() -> ::pyo3_stub_gen::TypeInfo {
478
+ T::type_output()
479
+ }
480
+ }
481
+
302
482
  #[cfg(all(feature = "pyo3", feature = "serde"))]
303
483
  impl<'py, T> FromPyObject<'py> for PyValue<T>
304
484
  where
@@ -59,3 +59,6 @@
59
59
  ))
60
60
  }
61
61
  }
62
+
63
+ #[cfg(feature = "stubgen")]
64
+ ::pyo3_stub_gen::impl_stub_type!({{ slot_name }}_range = {{ ranges | join(' | ') }});
@@ -8,6 +8,9 @@ pub type AnyValue = Anything;
8
8
  {% endif %}
9
9
  #[derive(Debug, Clone, PartialEq)]
10
10
  #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
11
+ {% if stubgen %}
12
+ #[cfg_attr(feature = "stubgen", gen_stub_pyclass)]
13
+ {% endif %}
11
14
  #[cfg_attr(feature = "pyo3", pyclass(subclass, get_all, set_all))]
12
15
  {% if generate_merge %}
13
16
  #[derive(Merge)]
@@ -22,6 +25,9 @@ pub struct {{ name }} {
22
25
  }
23
26
  {% if properties | length > 0 %}
24
27
  #[cfg(feature = "pyo3")]
28
+ {% if stubgen %}
29
+ #[cfg_attr(feature = "stubgen", gen_stub_pymethods)]
30
+ {% endif %}
25
31
  #[pymethods]
26
32
  impl {{ name }} {
27
33
  #[new]
@@ -75,7 +75,7 @@ impl<'py> FromPyObject<'py> for Box<{{ enum_name }}> {
75
75
  {% if as_key_value %}
76
76
  #[cfg(feature = "serde")]
77
77
  impl serde_utils::InlinedPair for {{enum_name}} {
78
- type Key = String;
78
+ type Key = {{ key_property_type }};
79
79
  type Value = serde_value::Value;
80
80
  type Error = String;
81
81
 
@@ -106,3 +106,6 @@ impl serde_utils::InlinedPair for {{enum_name}} {
106
106
  }
107
107
  }
108
108
  {% endif %}
109
+
110
+ #[cfg(feature = "stubgen")]
111
+ ::pyo3_stub_gen::impl_stub_type!({{ enum_name }} = {{ struct_names | join(' | ') }});
@@ -0,0 +1,71 @@
1
+ #[cfg(feature = "stubgen")]
2
+ use {{ crate_module }}::stub_utils;
3
+
4
+ #[cfg(feature = "stubgen")]
5
+ fn main() -> pyo3_stub_gen::Result<()> {
6
+ let check_only = std::env::args().skip(1).any(|arg| arg == "--check");
7
+ let stub = {{ crate_name }}::stub_info()?;
8
+
9
+ if check_only {
10
+ check_stubs(&stub)
11
+ } else {
12
+ stub.generate()
13
+ }
14
+ }
15
+
16
+ #[cfg(feature = "stubgen")]
17
+ fn check_stubs(stub: &pyo3_stub_gen::StubInfo) -> pyo3_stub_gen::Result<()> {
18
+ use std::fs;
19
+ use std::io::ErrorKind;
20
+
21
+ let mut issues = Vec::new();
22
+
23
+ for (name, module) in &stub.modules {
24
+ let module_path = stub_utils::normalize_stub_module(name)?;
25
+ let mut dest = stub.python_root.join(&module_path);
26
+ if module.submodules.is_empty() {
27
+ dest.set_extension("pyi");
28
+ } else {
29
+ dest.push("__init__.pyi");
30
+ }
31
+
32
+ let expected = module.to_string();
33
+ match fs::read_to_string(&dest) {
34
+ Ok(actual) => {
35
+ if actual != expected {
36
+ issues.push(format!(
37
+ "updated content differs for `{}` (module `{name}`)",
38
+ dest.display()
39
+ ));
40
+ }
41
+ }
42
+ Err(err) if err.kind() == ErrorKind::NotFound => {
43
+ issues.push(format!(
44
+ "missing stub file `{}` for module `{name}`",
45
+ dest.display()
46
+ ));
47
+ }
48
+ Err(err) => return Err(err.into()),
49
+ }
50
+ }
51
+
52
+ if issues.is_empty() {
53
+ Ok(())
54
+ } else {
55
+ let mut msg = String::from("Stub files are out of date:\n");
56
+ for issue in issues {
57
+ msg.push_str(" - ");
58
+ msg.push_str(&issue);
59
+ msg.push('\n');
60
+ }
61
+ msg.push_str("Run `cargo run --bin stub_gen --features stubgen` to regenerate.");
62
+ Err(std::io::Error::new(ErrorKind::Other, msg).into())
63
+ }
64
+ }
65
+
66
+ #[cfg(not(feature = "stubgen"))]
67
+ fn main() {
68
+ eprintln!(
69
+ "Enable the `stubgen` feature (alongside `pyo3`) to run this generator."
70
+ );
71
+ }
@@ -0,0 +1,76 @@
1
+ use std::io::{Error, ErrorKind};
2
+ use std::path::{Component, PathBuf};
3
+
4
+ /// Convert a stub module name into a relative filesystem path.
5
+ ///
6
+ /// The generator currently expects module names that mirror Python package
7
+ /// semantics (``foo.bar``) with optional hyphens. Hyphens are replaced with
8
+ /// underscores and dots map to directory separators. The helper rejects
9
+ /// absolute paths, traversal markers, or characters that cannot appear in
10
+ /// stub files so we never escape the designated stub root.
11
+ pub fn normalize_stub_module(name: &str) -> std::io::Result<PathBuf> {
12
+ if name.is_empty() {
13
+ return Err(Error::new(
14
+ ErrorKind::InvalidInput,
15
+ "module name may not be empty",
16
+ ));
17
+ }
18
+
19
+ let normalized = name.replace('-', "_");
20
+ let path_str = normalized.replace('.', "/");
21
+ let path = PathBuf::from(&path_str);
22
+
23
+ if path.is_absolute() {
24
+ return Err(Error::new(
25
+ ErrorKind::InvalidInput,
26
+ format!("absolute module path `{name}` not allowed"),
27
+ ));
28
+ }
29
+
30
+ for component in path.components() {
31
+ match component {
32
+ Component::Normal(part) => {
33
+ let segment = part.to_string_lossy();
34
+ if segment.is_empty() {
35
+ return Err(Error::new(
36
+ ErrorKind::InvalidInput,
37
+ format!("module name `{name}` contains an empty segment"),
38
+ ));
39
+ }
40
+ if segment == "." || segment == ".." {
41
+ return Err(Error::new(
42
+ ErrorKind::InvalidInput,
43
+ format!(
44
+ "module name `{name}` contains disallowed segment `{segment}`"
45
+ ),
46
+ ));
47
+ }
48
+ if segment
49
+ .chars()
50
+ .any(|ch| matches!(ch, '*' | '?' | '<' | '>' | '|' | ':' | '\\' | '/'))
51
+ {
52
+ return Err(Error::new(
53
+ ErrorKind::InvalidInput,
54
+ format!(
55
+ "module name `{name}` contains invalid characters in `{segment}`"
56
+ ),
57
+ ));
58
+ }
59
+ }
60
+ Component::CurDir | Component::ParentDir => {
61
+ return Err(Error::new(
62
+ ErrorKind::InvalidInput,
63
+ format!("module name `{name}` attempts directory traversal"),
64
+ ));
65
+ }
66
+ Component::RootDir | Component::Prefix(_) => {
67
+ return Err(Error::new(
68
+ ErrorKind::InvalidInput,
69
+ format!("module name `{name}` resolves outside stub root"),
70
+ ));
71
+ }
72
+ }
73
+ }
74
+
75
+ Ok(path)
76
+ }
@@ -32,7 +32,18 @@ class YarrrmlGenerator(Generator):
32
32
  visit_all_class_slots = False
33
33
 
34
34
  def __init__(self, schema: str | TextIO | SchemaDefinition, format: str = "yml", **kwargs):
35
- src = kwargs.pop("source", None)
35
+ def _infer_source_suffix(path: str) -> str:
36
+ p = (path or "").lower()
37
+ if "~" in p:
38
+ return path # already has ~jsonpath or ~csv
39
+ if p.endswith(".json"):
40
+ return f"{path}~jsonpath"
41
+ if p.endswith(".csv") or p.endswith(".tsv"):
42
+ return f"{path}~csv"
43
+ return path
44
+
45
+ # in __init__ right after you read src:
46
+ raw_src = kwargs.pop("source", None)
36
47
  it = kwargs.pop("iterator_template", None)
37
48
 
38
49
  super().__init__(schema, **kwargs)
@@ -42,7 +53,12 @@ class YarrrmlGenerator(Generator):
42
53
 
43
54
  self.format = format
44
55
 
45
- self.source: str = src or DEFAULT_SOURCE_JSON
56
+ # normalize source: if user passed file without "~csv/~jsonpath", infer it
57
+ if raw_src:
58
+ self.source = _infer_source_suffix(raw_src)
59
+ else:
60
+ self.source = DEFAULT_SOURCE_JSON
61
+
46
62
  self.iterator_template: str = it or DEFAULT_ITERATOR
47
63
 
48
64
  # public
@@ -70,7 +86,7 @@ class YarrrmlGenerator(Generator):
70
86
  if self._is_json_source():
71
87
  mapping["sources"] = [[self.source, self._iterator_for_class(cls)]]
72
88
  else:
73
- mapping["sources"] = [self.source]
89
+ mapping["sources"] = [[self.source]]
74
90
 
75
91
  mappings[str(cls.name)] = mapping
76
92
 
@@ -136,7 +152,7 @@ class YarrrmlGenerator(Generator):
136
152
  @click.command(name="yarrrml")
137
153
  @click.option(
138
154
  "--source",
139
- help="YARRRML source shorthand, e.g., data.json~jsonpath or data.csv~csv",
155
+ help="YARRRML source shorthand, e.g., data.json~jsonpath or data.csv~csv (TSV works too)",
140
156
  )
141
157
  @click.option(
142
158
  "--iterator-template",