polyxml 0.3.0__tar.gz → 0.4.0__tar.gz

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 (23) hide show
  1. {polyxml-0.3.0 → polyxml-0.4.0}/Cargo.lock +87 -58
  2. {polyxml-0.3.0 → polyxml-0.4.0}/Cargo.toml +1 -1
  3. {polyxml-0.3.0 → polyxml-0.4.0}/PKG-INFO +1 -1
  4. {polyxml-0.3.0 → polyxml-0.4.0}/crates/polyxml-core/Cargo.toml +2 -2
  5. {polyxml-0.3.0 → polyxml-0.4.0}/crates/polyxml-core/src/error.rs +4 -1
  6. {polyxml-0.3.0 → polyxml-0.4.0}/crates/polyxml-core/src/parser.rs +58 -16
  7. {polyxml-0.3.0 → polyxml-0.4.0}/crates/polyxml-python/Cargo.toml +1 -1
  8. {polyxml-0.3.0 → polyxml-0.4.0}/crates/polyxml-python/src/lib.rs +65 -35
  9. {polyxml-0.3.0 → polyxml-0.4.0}/crates/polyxml-python/tests/test_polyxml.py +17 -0
  10. {polyxml-0.3.0 → polyxml-0.4.0}/pyproject.toml +1 -1
  11. {polyxml-0.3.0 → polyxml-0.4.0}/README.md +0 -0
  12. {polyxml-0.3.0 → polyxml-0.4.0}/crates/polyxml-core/README.md +0 -0
  13. {polyxml-0.3.0 → polyxml-0.4.0}/crates/polyxml-core/benches/core_benchmarks.rs +0 -0
  14. {polyxml-0.3.0 → polyxml-0.4.0}/crates/polyxml-core/src/converters.rs +0 -0
  15. {polyxml-0.3.0 → polyxml-0.4.0}/crates/polyxml-core/src/lib.rs +0 -0
  16. {polyxml-0.3.0 → polyxml-0.4.0}/crates/polyxml-core/src/schema.rs +0 -0
  17. {polyxml-0.3.0 → polyxml-0.4.0}/crates/polyxml-core/src/serializer.rs +0 -0
  18. {polyxml-0.3.0 → polyxml-0.4.0}/crates/polyxml-core/src/value.rs +0 -0
  19. {polyxml-0.3.0 → polyxml-0.4.0}/crates/polyxml-core/tests/test_core.rs +0 -0
  20. {polyxml-0.3.0 → polyxml-0.4.0}/crates/polyxml-core/tests/test_security.rs +0 -0
  21. {polyxml-0.3.0 → polyxml-0.4.0}/crates/polyxml-python/README.md +0 -0
  22. {polyxml-0.3.0 → polyxml-0.4.0}/python/polyxml/__init__.py +0 -0
  23. {polyxml-0.3.0 → polyxml-0.4.0}/python/polyxml/py.typed +0 -0
@@ -11,6 +11,15 @@ dependencies = [
11
11
  "memchr",
12
12
  ]
13
13
 
14
+ [[package]]
15
+ name = "alloca"
16
+ version = "0.4.0"
17
+ source = "registry+https://github.com/rust-lang/crates.io-index"
18
+ checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"
19
+ dependencies = [
20
+ "cc",
21
+ ]
22
+
14
23
  [[package]]
15
24
  name = "anes"
16
25
  version = "0.1.6"
@@ -41,6 +50,16 @@ version = "0.3.0"
41
50
  source = "registry+https://github.com/rust-lang/crates.io-index"
42
51
  checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
43
52
 
53
+ [[package]]
54
+ name = "cc"
55
+ version = "1.4.5"
56
+ source = "registry+https://github.com/rust-lang/crates.io-index"
57
+ checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80"
58
+ dependencies = [
59
+ "find-msvc-tools",
60
+ "shlex",
61
+ ]
62
+
44
63
  [[package]]
45
64
  name = "cfg-if"
46
65
  version = "1.0.4"
@@ -110,10 +129,11 @@ dependencies = [
110
129
 
111
130
  [[package]]
112
131
  name = "criterion"
113
- version = "0.7.0"
132
+ version = "0.8.2"
114
133
  source = "registry+https://github.com/rust-lang/crates.io-index"
115
- checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928"
134
+ checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3"
116
135
  dependencies = [
136
+ "alloca",
117
137
  "anes",
118
138
  "cast",
119
139
  "ciborium",
@@ -122,6 +142,7 @@ dependencies = [
122
142
  "itertools",
123
143
  "num-traits",
124
144
  "oorandom",
145
+ "page_size",
125
146
  "regex",
126
147
  "serde",
127
148
  "serde_json",
@@ -131,9 +152,9 @@ dependencies = [
131
152
 
132
153
  [[package]]
133
154
  name = "criterion-plot"
134
- version = "0.6.0"
155
+ version = "0.8.2"
135
156
  source = "registry+https://github.com/rust-lang/crates.io-index"
136
- checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338"
157
+ checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea"
137
158
  dependencies = [
138
159
  "cast",
139
160
  "itertools",
@@ -167,6 +188,12 @@ version = "1.18.0"
167
188
  source = "registry+https://github.com/rust-lang/crates.io-index"
168
189
  checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
169
190
 
191
+ [[package]]
192
+ name = "find-msvc-tools"
193
+ version = "0.1.12"
194
+ source = "registry+https://github.com/rust-lang/crates.io-index"
195
+ checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
196
+
170
197
  [[package]]
171
198
  name = "half"
172
199
  version = "2.7.1"
@@ -184,15 +211,6 @@ version = "0.5.0"
184
211
  source = "registry+https://github.com/rust-lang/crates.io-index"
185
212
  checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
186
213
 
187
- [[package]]
188
- name = "indoc"
189
- version = "2.0.7"
190
- source = "registry+https://github.com/rust-lang/crates.io-index"
191
- checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
192
- dependencies = [
193
- "rustversion",
194
- ]
195
-
196
214
  [[package]]
197
215
  name = "itertools"
198
216
  version = "0.13.0"
@@ -287,15 +305,6 @@ version = "2.8.3"
287
305
  source = "registry+https://github.com/rust-lang/crates.io-index"
288
306
  checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
289
307
 
290
- [[package]]
291
- name = "memoffset"
292
- version = "0.9.1"
293
- source = "registry+https://github.com/rust-lang/crates.io-index"
294
- checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
295
- dependencies = [
296
- "autocfg",
297
- ]
298
-
299
308
  [[package]]
300
309
  name = "napi"
301
310
  version = "2.16.17"
@@ -374,9 +383,19 @@ version = "11.1.5"
374
383
  source = "registry+https://github.com/rust-lang/crates.io-index"
375
384
  checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
376
385
 
386
+ [[package]]
387
+ name = "page_size"
388
+ version = "0.6.0"
389
+ source = "registry+https://github.com/rust-lang/crates.io-index"
390
+ checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
391
+ dependencies = [
392
+ "libc",
393
+ "winapi",
394
+ ]
395
+
377
396
  [[package]]
378
397
  name = "polyxml"
379
- version = "0.3.0"
398
+ version = "0.4.0"
380
399
  dependencies = [
381
400
  "criterion",
382
401
  "lexical-core",
@@ -389,14 +408,14 @@ dependencies = [
389
408
 
390
409
  [[package]]
391
410
  name = "polyxml-c"
392
- version = "0.3.0"
411
+ version = "0.4.0"
393
412
  dependencies = [
394
413
  "polyxml",
395
414
  ]
396
415
 
397
416
  [[package]]
398
417
  name = "polyxml-js"
399
- version = "0.3.0"
418
+ version = "0.4.0"
400
419
  dependencies = [
401
420
  "napi",
402
421
  "napi-build",
@@ -406,7 +425,7 @@ dependencies = [
406
425
 
407
426
  [[package]]
408
427
  name = "polyxml-python"
409
- version = "0.3.0"
428
+ version = "0.4.0"
410
429
  dependencies = [
411
430
  "lexical-core",
412
431
  "polyxml",
@@ -441,37 +460,32 @@ dependencies = [
441
460
 
442
461
  [[package]]
443
462
  name = "pyo3"
444
- version = "0.23.5"
463
+ version = "0.29.2"
445
464
  source = "registry+https://github.com/rust-lang/crates.io-index"
446
- checksum = "7778bffd85cf38175ac1f545509665d0b9b92a198ca7941f131f85f7a4f9a872"
465
+ checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b"
447
466
  dependencies = [
448
- "cfg-if",
449
- "indoc",
450
467
  "libc",
451
- "memoffset",
452
468
  "once_cell",
453
469
  "portable-atomic",
454
470
  "pyo3-build-config",
455
471
  "pyo3-ffi",
456
472
  "pyo3-macros",
457
- "unindent",
458
473
  ]
459
474
 
460
475
  [[package]]
461
476
  name = "pyo3-build-config"
462
- version = "0.23.5"
477
+ version = "0.29.2"
463
478
  source = "registry+https://github.com/rust-lang/crates.io-index"
464
- checksum = "94f6cbe86ef3bf18998d9df6e0f3fc1050a8c5efa409bf712e661a4366e010fb"
479
+ checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9"
465
480
  dependencies = [
466
- "once_cell",
467
481
  "target-lexicon",
468
482
  ]
469
483
 
470
484
  [[package]]
471
485
  name = "pyo3-ffi"
472
- version = "0.23.5"
486
+ version = "0.29.2"
473
487
  source = "registry+https://github.com/rust-lang/crates.io-index"
474
- checksum = "e9f1b4c431c0bb1c8fb0a338709859eed0d030ff6daa34368d3b152a63dfdd8d"
488
+ checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327"
475
489
  dependencies = [
476
490
  "libc",
477
491
  "pyo3-build-config",
@@ -479,9 +493,9 @@ dependencies = [
479
493
 
480
494
  [[package]]
481
495
  name = "pyo3-macros"
482
- version = "0.23.5"
496
+ version = "0.29.2"
483
497
  source = "registry+https://github.com/rust-lang/crates.io-index"
484
- checksum = "fbc2201328f63c4710f68abdf653c89d8dbc2858b88c5d88b0ff38a75288a9da"
498
+ checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c"
485
499
  dependencies = [
486
500
  "proc-macro2",
487
501
  "pyo3-macros-backend",
@@ -491,22 +505,21 @@ dependencies = [
491
505
 
492
506
  [[package]]
493
507
  name = "pyo3-macros-backend"
494
- version = "0.23.5"
508
+ version = "0.29.2"
495
509
  source = "registry+https://github.com/rust-lang/crates.io-index"
496
- checksum = "fca6726ad0f3da9c9de093d6f116a93c1a38e417ed73bf138472cf4064f72028"
510
+ checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952"
497
511
  dependencies = [
498
512
  "heck",
499
513
  "proc-macro2",
500
- "pyo3-build-config",
501
514
  "quote",
502
515
  "syn 2.0.119",
503
516
  ]
504
517
 
505
518
  [[package]]
506
519
  name = "quick-xml"
507
- version = "0.37.5"
520
+ version = "0.42.0"
508
521
  source = "registry+https://github.com/rust-lang/crates.io-index"
509
- checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
522
+ checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b"
510
523
  dependencies = [
511
524
  "memchr",
512
525
  "serde",
@@ -550,12 +563,6 @@ version = "0.8.11"
550
563
  source = "registry+https://github.com/rust-lang/crates.io-index"
551
564
  checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
552
565
 
553
- [[package]]
554
- name = "rustversion"
555
- version = "1.0.23"
556
- source = "registry+https://github.com/rust-lang/crates.io-index"
557
- checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
558
-
559
566
  [[package]]
560
567
  name = "same-file"
561
568
  version = "1.0.6"
@@ -614,6 +621,12 @@ dependencies = [
614
621
  "zmij",
615
622
  ]
616
623
 
624
+ [[package]]
625
+ name = "shlex"
626
+ version = "2.0.1"
627
+ source = "registry+https://github.com/rust-lang/crates.io-index"
628
+ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
629
+
617
630
  [[package]]
618
631
  name = "smallvec"
619
632
  version = "1.16.0"
@@ -644,9 +657,9 @@ dependencies = [
644
657
 
645
658
  [[package]]
646
659
  name = "target-lexicon"
647
- version = "0.12.16"
660
+ version = "0.13.5"
648
661
  source = "registry+https://github.com/rust-lang/crates.io-index"
649
- checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
662
+ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
650
663
 
651
664
  [[package]]
652
665
  name = "thiserror"
@@ -690,12 +703,6 @@ version = "1.13.3"
690
703
  source = "registry+https://github.com/rust-lang/crates.io-index"
691
704
  checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
692
705
 
693
- [[package]]
694
- name = "unindent"
695
- version = "0.2.4"
696
- source = "registry+https://github.com/rust-lang/crates.io-index"
697
- checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
698
-
699
706
  [[package]]
700
707
  name = "walkdir"
701
708
  version = "2.5.0"
@@ -706,6 +713,22 @@ dependencies = [
706
713
  "winapi-util",
707
714
  ]
708
715
 
716
+ [[package]]
717
+ name = "winapi"
718
+ version = "0.3.9"
719
+ source = "registry+https://github.com/rust-lang/crates.io-index"
720
+ checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
721
+ dependencies = [
722
+ "winapi-i686-pc-windows-gnu",
723
+ "winapi-x86_64-pc-windows-gnu",
724
+ ]
725
+
726
+ [[package]]
727
+ name = "winapi-i686-pc-windows-gnu"
728
+ version = "0.4.0"
729
+ source = "registry+https://github.com/rust-lang/crates.io-index"
730
+ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
731
+
709
732
  [[package]]
710
733
  name = "winapi-util"
711
734
  version = "0.1.11"
@@ -715,6 +738,12 @@ dependencies = [
715
738
  "windows-sys",
716
739
  ]
717
740
 
741
+ [[package]]
742
+ name = "winapi-x86_64-pc-windows-gnu"
743
+ version = "0.4.0"
744
+ source = "registry+https://github.com/rust-lang/crates.io-index"
745
+ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
746
+
718
747
  [[package]]
719
748
  name = "windows-link"
720
749
  version = "0.2.1"
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/polyxml-core", "crates/polyxml-python"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.3.0"
6
+ version = "0.4.0"
7
7
  edition = "2021"
8
8
  rust-version = "1.80"
9
9
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyxml
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -11,14 +11,14 @@ keywords = ["xml", "deserializer", "serializer", "data-binding", "quick-xml"]
11
11
  categories = ["parsing", "encoding"]
12
12
 
13
13
  [dependencies]
14
- quick-xml = { version = "0.37", features = ["serialize"] }
14
+ quick-xml = { version = "0.42", features = ["serialize"] }
15
15
  lexical-core = "1.0"
16
16
  memchr = "2.8"
17
17
  thiserror = "2.0"
18
18
  smallvec = "1.16"
19
19
 
20
20
  [dev-dependencies]
21
- criterion = { version = "0.7", default-features = false, features = ["cargo_bench_support"] }
21
+ criterion = { version = "0.8", default-features = false, features = ["cargo_bench_support"] }
22
22
  pretty_assertions = "1.4"
23
23
 
24
24
  [[bench]]
@@ -31,8 +31,11 @@ pub enum PolyXmlError {
31
31
  #[error("Maximum XML recursion depth exceeded: current {current} >= limit {max_depth}")]
32
32
  MaxDepthExceeded { max_depth: usize, current: usize },
33
33
 
34
- #[error("XML parsing or unescape error: {0}")]
34
+ #[error("XML parsing error: {0}")]
35
35
  XmlError(#[from] quick_xml::Error),
36
+
37
+ #[error("XML unescape error: {0}")]
38
+ EscapeError(#[from] quick_xml::escape::EscapeError),
36
39
  }
37
40
 
38
41
  pub type Result<T> = std::result::Result<T, PolyXmlError>;
@@ -86,8 +86,8 @@ pub struct XmlDeserializer;
86
86
  fn is_nil_element(e: &BytesStart) -> bool {
87
87
  for attr in e.attributes().flatten() {
88
88
  let key = attr.key.local_name();
89
- if (key.as_ref() == b"nil" || key.as_ref() == b"xsi:nil")
90
- && (attr.value.as_ref() == b"true" || attr.value.as_ref() == b"1")
89
+ if (key.as_ref() == "nil" || key.as_ref() == "xsi:nil")
90
+ && (attr.value.as_ref() == "true" || attr.value.as_ref() == "1")
91
91
  {
92
92
  return true;
93
93
  }
@@ -95,6 +95,33 @@ fn is_nil_element(e: &BytesStart) -> bool {
95
95
  false
96
96
  }
97
97
 
98
+ fn append_general_ref(
99
+ e: &quick_xml::events::BytesRef,
100
+ active_scalar: bool,
101
+ text_buf: &mut Vec<u8>,
102
+ frame_text_buf: Option<&mut Vec<u8>>,
103
+ ) -> Result<()> {
104
+ let mut char_buf = [0u8; 4];
105
+ let resolved_bytes: &[u8] = if e.is_char_ref() {
106
+ if let Some(ch) = e.resolve_char_ref().map_err(PolyXmlError::XmlError)? {
107
+ ch.encode_utf8(&mut char_buf).as_bytes()
108
+ } else {
109
+ b""
110
+ }
111
+ } else if let Some(s) = quick_xml::escape::resolve_xml_entity(e.as_ref()) {
112
+ s.as_bytes()
113
+ } else {
114
+ e.as_ref().as_bytes()
115
+ };
116
+
117
+ if active_scalar {
118
+ text_buf.extend_from_slice(resolved_bytes);
119
+ } else if let Some(tb) = frame_text_buf {
120
+ tb.extend_from_slice(resolved_bytes);
121
+ }
122
+ Ok(())
123
+ }
124
+
98
125
  pub const DEFAULT_MAX_DEPTH: usize = 256;
99
126
 
100
127
  impl XmlDeserializer {
@@ -108,7 +135,6 @@ impl XmlDeserializer {
108
135
  max_depth: usize,
109
136
  ) -> Result<PolyValue> {
110
137
  let mut reader = Reader::from_reader(xml_bytes);
111
- reader.config_mut().trim_text(true);
112
138
  let mut buf = Vec::new();
113
139
 
114
140
  loop {
@@ -166,7 +192,10 @@ impl XmlDeserializer {
166
192
  let is_nil = is_nil_element(e);
167
193
 
168
194
  let current_schema = Arc::clone(&stack.last().unwrap().schema);
169
- if let Some(&field_idx) = current_schema.element_map.get(local_name.as_ref()) {
195
+ if let Some(&field_idx) = current_schema
196
+ .element_map
197
+ .get(local_name.as_ref().as_bytes())
198
+ {
170
199
  let field = &current_schema.fields[field_idx];
171
200
  match &field.val_type {
172
201
  ValueType::Scalar(st) => {
@@ -210,7 +239,10 @@ impl XmlDeserializer {
210
239
  let is_nil = is_nil_element(e);
211
240
 
212
241
  let current_schema = Arc::clone(&stack.last().unwrap().schema);
213
- if let Some(&field_idx) = current_schema.element_map.get(local_name.as_ref()) {
242
+ if let Some(&field_idx) = current_schema
243
+ .element_map
244
+ .get(local_name.as_ref().as_bytes())
245
+ {
214
246
  let field = &current_schema.fields[field_idx];
215
247
  match &field.val_type {
216
248
  ValueType::Scalar(_) => {
@@ -251,7 +283,7 @@ impl XmlDeserializer {
251
283
  }
252
284
  }
253
285
  Ok(Event::Text(ref e)) => {
254
- let unescaped = e.unescape().map_err(PolyXmlError::XmlError)?;
286
+ let unescaped = quick_xml::escape::unescape(e.as_ref())?;
255
287
  if active_scalar_field.is_some() {
256
288
  text_buf.extend_from_slice(unescaped.as_bytes());
257
289
  } else if let Some(frame) = stack.last_mut() {
@@ -262,13 +294,17 @@ impl XmlDeserializer {
262
294
  }
263
295
  Ok(Event::CData(ref e)) => {
264
296
  if active_scalar_field.is_some() {
265
- text_buf.extend_from_slice(e.as_ref());
297
+ text_buf.extend_from_slice(e.as_ref().as_bytes());
266
298
  } else if let Some(frame) = stack.last_mut() {
267
299
  if let Some(ref mut tb) = frame.frame_text_buf {
268
- tb.extend_from_slice(e.as_ref());
300
+ tb.extend_from_slice(e.as_ref().as_bytes());
269
301
  }
270
302
  }
271
303
  }
304
+ Ok(Event::GeneralRef(ref e)) => {
305
+ let frame_tb = stack.last_mut().and_then(|f| f.frame_text_buf.as_mut());
306
+ append_general_ref(e, active_scalar_field.is_some(), &mut text_buf, frame_tb)?;
307
+ }
272
308
  Ok(Event::End(ref e)) => {
273
309
  if let Some((field_idx, ref scalar_type, is_list)) = active_scalar_field.take()
274
310
  {
@@ -287,7 +323,10 @@ impl XmlDeserializer {
287
323
  let instance = finished_frame.finish()?;
288
324
  let parent = stack.last_mut().unwrap();
289
325
 
290
- if let Some(&field_idx) = parent.schema.element_map.get(local_name.as_ref())
326
+ if let Some(&field_idx) = parent
327
+ .schema
328
+ .element_map
329
+ .get(local_name.as_ref().as_bytes())
291
330
  {
292
331
  let field = &parent.schema.fields[field_idx];
293
332
  if matches!(field.val_type, ValueType::List(_)) {
@@ -321,11 +360,15 @@ impl XmlDeserializer {
321
360
  pub(crate) fn parse_attributes(e: &BytesStart, frame: &mut StackFrame) -> Result<()> {
322
361
  for attr in e.attributes().flatten() {
323
362
  let key = attr.key.local_name();
324
- if let Some(&field_idx) = frame.schema.attribute_map.get(key.as_ref()) {
363
+ if let Some(&field_idx) = frame.schema.attribute_map.get(key.as_ref().as_bytes()) {
325
364
  let field = &frame.schema.fields[field_idx];
326
365
  if let ValueType::Scalar(ref st) = field.val_type {
327
- let unescaped = attr.unescape_value().map_err(PolyXmlError::XmlError)?;
328
- let val = ValueConverter::parse_scalar(st, unescaped.as_bytes(), &field.name)?;
366
+ let unescaped = quick_xml::escape::unescape(attr.value.as_ref())?;
367
+ let val = ValueConverter::parse_scalar(
368
+ st,
369
+ unescaped.as_ref().as_bytes(),
370
+ &field.name,
371
+ )?;
329
372
  frame.values[field_idx] = Some(val);
330
373
  }
331
374
  }
@@ -344,8 +387,7 @@ pub struct XmlItemStream<R: std::io::BufRead> {
344
387
 
345
388
  impl<R: std::io::BufRead> XmlItemStream<R> {
346
389
  pub fn new(reader: R, schema: Arc<ModelSchema>, target_tag: &[u8]) -> Self {
347
- let mut xml_reader = Reader::from_reader(reader);
348
- xml_reader.config_mut().trim_text(true);
390
+ let xml_reader = Reader::from_reader(reader);
349
391
  Self {
350
392
  reader: xml_reader,
351
393
  schema,
@@ -361,7 +403,7 @@ impl<R: std::io::BufRead> XmlItemStream<R> {
361
403
  match self.reader.read_event_into(&mut self.buf) {
362
404
  Ok(Event::Start(ref e)) => {
363
405
  let local = e.local_name();
364
- if local.as_ref() == self.target_tag.as_slice() {
406
+ if local.as_ref().as_bytes() == self.target_tag.as_slice() {
365
407
  let item = XmlDeserializer::parse_sub_tree(
366
408
  &mut self.reader,
367
409
  Arc::clone(&self.schema),
@@ -373,7 +415,7 @@ impl<R: std::io::BufRead> XmlItemStream<R> {
373
415
  }
374
416
  Ok(Event::Empty(ref e)) => {
375
417
  let local = e.local_name();
376
- if local.as_ref() == self.target_tag.as_slice() {
418
+ if local.as_ref().as_bytes() == self.target_tag.as_slice() {
377
419
  let mut frame = StackFrame::new(Arc::clone(&self.schema));
378
420
  XmlDeserializer::parse_attributes(e, &mut frame)?;
379
421
  return Ok(Some(frame.finish()?));
@@ -14,6 +14,6 @@ crate-type = ["cdylib"]
14
14
 
15
15
  [dependencies]
16
16
  polyxml = { path = "../polyxml-core" }
17
- pyo3 = { version = "0.23", features = ["abi3-py312", "extension-module"] }
17
+ pyo3 = { version = "0.29", features = ["abi3-py312", "extension-module"] }
18
18
  lexical-core = "1.0"
19
19
  smallvec = "1.16"
@@ -11,6 +11,8 @@ use std::sync::{Arc, RwLock};
11
11
  use polyxml::schema::{FieldKind, FieldSchema, ModelSchema, ScalarType, ValueType};
12
12
  use polyxml::value::PolyValue;
13
13
 
14
+ type PyObject = Py<PyAny>;
15
+
14
16
  struct CachedFieldMeta {
15
17
  py_string: Py<PyString>,
16
18
  is_init: bool,
@@ -45,21 +47,31 @@ fn lookup_py_class<'py>(py: Python<'py>, schema_name: &str) -> Option<Bound<'py,
45
47
  }
46
48
 
47
49
  fn unwrap_optional_type<'py>(type_obj: &Bound<'py, PyAny>) -> Bound<'py, PyAny> {
48
- if let Ok(origin) = type_obj.getattr("__origin__") {
50
+ let is_union = if let Ok(origin) = type_obj.getattr("__origin__") {
49
51
  if let Ok(origin_name) = origin.getattr("__name__") {
50
52
  let origin_str: String = origin_name.extract().unwrap_or_default();
51
- if origin_str == "Union" || origin_str == "UnionType" {
52
- if let Ok(args) = type_obj.getattr("__args__") {
53
- if let Ok(tuple) = args.downcast::<PyTuple>() {
54
- for arg in tuple.iter() {
55
- let arg_name: String = arg
56
- .getattr("__name__")
57
- .and_then(|n| n.extract())
58
- .unwrap_or_default();
59
- if arg_name != "NoneType" {
60
- return unwrap_optional_type(&arg);
61
- }
62
- }
53
+ origin_str == "Union" || origin_str == "UnionType"
54
+ } else {
55
+ false
56
+ }
57
+ } else {
58
+ type_obj
59
+ .get_type()
60
+ .name()
61
+ .map(|n| n == "UnionType")
62
+ .unwrap_or(false)
63
+ };
64
+
65
+ if is_union {
66
+ if let Ok(args) = type_obj.getattr("__args__") {
67
+ if let Ok(tuple) = args.cast::<PyTuple>() {
68
+ for arg in tuple.iter() {
69
+ let arg_name: String = arg
70
+ .getattr("__name__")
71
+ .and_then(|n| n.extract())
72
+ .unwrap_or_default();
73
+ if arg_name != "NoneType" {
74
+ return unwrap_optional_type(&arg);
63
75
  }
64
76
  }
65
77
  }
@@ -69,10 +81,10 @@ fn unwrap_optional_type<'py>(type_obj: &Bound<'py, PyAny>) -> Bound<'py, PyAny>
69
81
  }
70
82
 
71
83
  fn is_enum_class<'py>(py: Python<'py>, cls: &Bound<'py, PyAny>) -> bool {
72
- if let Ok(py_type) = cls.downcast::<PyType>() {
84
+ if let Ok(py_type) = cls.cast::<PyType>() {
73
85
  if let Ok(enum_module) = py.import("enum") {
74
86
  if let Ok(enum_cls) = enum_module.getattr("Enum") {
75
- if let Ok(py_enum) = enum_cls.downcast::<PyType>() {
87
+ if let Ok(py_enum) = enum_cls.cast::<PyType>() {
76
88
  return py_type.is_subclass(py_enum).unwrap_or(false);
77
89
  }
78
90
  }
@@ -108,26 +120,44 @@ fn resolve_scalar_type(py: Python<'_>, type_obj: &Bound<'_, PyAny>) -> PyResult<
108
120
  }
109
121
 
110
122
  fn resolve_value_type(py: Python<'_>, type_obj: &Bound<'_, PyAny>) -> PyResult<ValueType> {
111
- // Handle typing.Optional / Union
112
- if let Ok(origin) = type_obj.getattr("__origin__") {
123
+ // Handle typing.Optional / Union and PEP 604 UnionType
124
+ let is_union = if let Ok(origin) = type_obj.getattr("__origin__") {
113
125
  if let Ok(origin_name) = origin.getattr("__name__") {
114
126
  let origin_str: String = origin_name.extract().unwrap_or_default();
115
- if origin_str == "Union" || origin_str == "UnionType" {
116
- if let Ok(args) = type_obj.getattr("__args__") {
117
- let tuple: Bound<'_, pyo3::types::PyTuple> = args.downcast_into()?;
118
- for arg in tuple.iter() {
119
- let arg_name: String = arg
120
- .getattr("__name__")
121
- .and_then(|n| n.extract())
122
- .unwrap_or_default();
123
- if arg_name != "NoneType" {
124
- return resolve_value_type(py, &arg);
125
- }
127
+ origin_str == "Union" || origin_str == "UnionType"
128
+ } else {
129
+ false
130
+ }
131
+ } else {
132
+ type_obj
133
+ .get_type()
134
+ .name()
135
+ .map(|n| n == "UnionType")
136
+ .unwrap_or(false)
137
+ };
138
+
139
+ if is_union {
140
+ if let Ok(args) = type_obj.getattr("__args__") {
141
+ if let Ok(tuple) = args.cast_into::<PyTuple>() {
142
+ for arg in tuple.iter() {
143
+ let arg_name: String = arg
144
+ .getattr("__name__")
145
+ .and_then(|n| n.extract())
146
+ .unwrap_or_default();
147
+ if arg_name != "NoneType" {
148
+ return resolve_value_type(py, &arg);
126
149
  }
127
150
  }
128
- } else if origin_str == "list" || origin_str == "List" {
151
+ }
152
+ }
153
+ }
154
+
155
+ if let Ok(origin) = type_obj.getattr("__origin__") {
156
+ if let Ok(origin_name) = origin.getattr("__name__") {
157
+ let origin_str: String = origin_name.extract().unwrap_or_default();
158
+ if origin_str == "list" || origin_str == "List" {
129
159
  if let Ok(args) = type_obj.getattr("__args__") {
130
- let tuple: Bound<'_, pyo3::types::PyTuple> = args.downcast_into()?;
160
+ let tuple: Bound<'_, pyo3::types::PyTuple> = args.cast_into()?;
131
161
  if let Some(first) = tuple.iter().next() {
132
162
  let inner = resolve_value_type(py, &first)?;
133
163
  return Ok(ValueType::List(Box::new(inner)));
@@ -142,7 +172,7 @@ fn resolve_value_type(py: Python<'_>, type_obj: &Bound<'_, PyAny>) -> PyResult<V
142
172
 
143
173
  // Check if target is a nested dataclass or Pydantic model
144
174
  if type_obj.hasattr("__dataclass_fields__")? || type_obj.hasattr("model_fields")? {
145
- if let Ok(py_type) = type_obj.downcast::<PyType>() {
175
+ if let Ok(py_type) = type_obj.cast::<PyType>() {
146
176
  let (nested_schema, _) = get_or_create_schema(py_type)?;
147
177
  return Ok(ValueType::Nested(nested_schema));
148
178
  }
@@ -165,10 +195,10 @@ fn extract_schema_from_class<'py>(
165
195
  .ok()
166
196
  .and_then(|m| m.getattr("get_type_hints").ok())
167
197
  .and_then(|f| f.call1((cls,)).ok())
168
- .and_then(|h| h.downcast_into::<PyDict>().ok());
198
+ .and_then(|h| h.cast_into::<PyDict>().ok());
169
199
 
170
200
  if cls.hasattr("model_fields")? {
171
- let model_fields: Bound<'py, PyDict> = cls.getattr("model_fields")?.downcast_into()?;
201
+ let model_fields: Bound<'py, PyDict> = cls.getattr("model_fields")?.cast_into()?;
172
202
  for (name_obj, field_obj) in model_fields.iter() {
173
203
  let py_name: String = name_obj.extract()?;
174
204
  let py_string = PyString::new(py, &py_name).unbind();
@@ -221,7 +251,7 @@ fn extract_schema_from_class<'py>(
221
251
  ));
222
252
  }
223
253
  } else if cls.hasattr("__dataclass_fields__")? {
224
- let fields: Bound<'py, PyDict> = cls.getattr("__dataclass_fields__")?.downcast_into()?;
254
+ let fields: Bound<'py, PyDict> = cls.getattr("__dataclass_fields__")?.cast_into()?;
225
255
  for (name_obj, field_obj) in fields.iter() {
226
256
  let py_name: String = name_obj.extract()?;
227
257
  let py_string = PyString::new(py, &py_name).unbind();
@@ -631,7 +661,7 @@ fn py_to_poly_value<'py>(
631
661
  }
632
662
  },
633
663
  ValueType::List(inner) => {
634
- if let Ok(list) = val.downcast::<PyList>() {
664
+ if let Ok(list) = val.cast::<PyList>() {
635
665
  let mut poly_items = Vec::with_capacity(list.len());
636
666
  let child_meta = if let ValueType::Nested(sub_schema) = inner.as_ref() {
637
667
  lookup_cached_meta(&sub_schema.name)
@@ -329,3 +329,20 @@ def test_to_bytes_streams():
329
329
  def test_to_bytes_invalid_type():
330
330
  with pytest.raises(TypeError, match="Unsupported XML source type"):
331
331
  polyxml.deserialize(12345, SimpleItem) # type: ignore[arg-type]
332
+
333
+
334
+ @dataclass
335
+ class UnionWrapper:
336
+ child: None | ChildNode = field(
337
+ default=None, metadata={"type": "Element", "name": "child"}
338
+ )
339
+ count: int | None = field(default=None, metadata={"type": "Element"})
340
+
341
+
342
+ def test_pep604_union_nested_dataclass():
343
+ xml = "<UnionWrapper><child><tag>Payload</tag></child><count>42</count></UnionWrapper>"
344
+ res = polyxml.deserialize(xml, UnionWrapper)
345
+ assert res.child is not None
346
+ assert res.child.tag == "Payload"
347
+ assert res.count == 42
348
+
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "polyxml"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "High-performance, polyglot XML data-binding engine built in Rust"
9
9
  authors = [
10
10
  { name = "Bailey Nguyen", email = "bailey.tan.nguyen@gmail.com" }
File without changes
File without changes