polyxml 0.2.1__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.2.1 → polyxml-0.4.0}/Cargo.lock +89 -81
  2. {polyxml-0.2.1 → polyxml-0.4.0}/Cargo.toml +1 -1
  3. {polyxml-0.2.1 → polyxml-0.4.0}/PKG-INFO +1 -1
  4. {polyxml-0.2.1 → polyxml-0.4.0}/crates/polyxml-core/Cargo.toml +2 -2
  5. {polyxml-0.2.1 → polyxml-0.4.0}/crates/polyxml-core/benches/core_benchmarks.rs +3 -1
  6. {polyxml-0.2.1 → polyxml-0.4.0}/crates/polyxml-core/src/error.rs +4 -1
  7. {polyxml-0.2.1 → polyxml-0.4.0}/crates/polyxml-core/src/parser.rs +58 -16
  8. {polyxml-0.2.1 → polyxml-0.4.0}/crates/polyxml-python/Cargo.toml +1 -2
  9. {polyxml-0.2.1 → polyxml-0.4.0}/crates/polyxml-python/src/lib.rs +87 -56
  10. {polyxml-0.2.1 → polyxml-0.4.0}/crates/polyxml-python/tests/test_polyxml.py +17 -0
  11. {polyxml-0.2.1 → polyxml-0.4.0}/pyproject.toml +1 -1
  12. {polyxml-0.2.1 → polyxml-0.4.0}/README.md +0 -0
  13. {polyxml-0.2.1 → polyxml-0.4.0}/crates/polyxml-core/README.md +0 -0
  14. {polyxml-0.2.1 → polyxml-0.4.0}/crates/polyxml-core/src/converters.rs +0 -0
  15. {polyxml-0.2.1 → polyxml-0.4.0}/crates/polyxml-core/src/lib.rs +0 -0
  16. {polyxml-0.2.1 → polyxml-0.4.0}/crates/polyxml-core/src/schema.rs +0 -0
  17. {polyxml-0.2.1 → polyxml-0.4.0}/crates/polyxml-core/src/serializer.rs +0 -0
  18. {polyxml-0.2.1 → polyxml-0.4.0}/crates/polyxml-core/src/value.rs +0 -0
  19. {polyxml-0.2.1 → polyxml-0.4.0}/crates/polyxml-core/tests/test_core.rs +0 -0
  20. {polyxml-0.2.1 → polyxml-0.4.0}/crates/polyxml-core/tests/test_security.rs +0 -0
  21. {polyxml-0.2.1 → polyxml-0.4.0}/crates/polyxml-python/README.md +0 -0
  22. {polyxml-0.2.1 → polyxml-0.4.0}/python/polyxml/__init__.py +0 -0
  23. {polyxml-0.2.1 → 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,23 +129,22 @@ dependencies = [
110
129
 
111
130
  [[package]]
112
131
  name = "criterion"
113
- version = "0.5.1"
132
+ version = "0.8.2"
114
133
  source = "registry+https://github.com/rust-lang/crates.io-index"
115
- checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
134
+ checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3"
116
135
  dependencies = [
136
+ "alloca",
117
137
  "anes",
118
138
  "cast",
119
139
  "ciborium",
120
140
  "clap",
121
141
  "criterion-plot",
122
- "is-terminal",
123
142
  "itertools",
124
143
  "num-traits",
125
- "once_cell",
126
144
  "oorandom",
145
+ "page_size",
127
146
  "regex",
128
147
  "serde",
129
- "serde_derive",
130
148
  "serde_json",
131
149
  "tinytemplate",
132
150
  "walkdir",
@@ -134,9 +152,9 @@ dependencies = [
134
152
 
135
153
  [[package]]
136
154
  name = "criterion-plot"
137
- version = "0.5.0"
155
+ version = "0.8.2"
138
156
  source = "registry+https://github.com/rust-lang/crates.io-index"
139
- checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
157
+ checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea"
140
158
  dependencies = [
141
159
  "cast",
142
160
  "itertools",
@@ -170,6 +188,12 @@ version = "1.18.0"
170
188
  source = "registry+https://github.com/rust-lang/crates.io-index"
171
189
  checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
172
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
+
173
197
  [[package]]
174
198
  name = "half"
175
199
  version = "2.7.1"
@@ -187,37 +211,11 @@ version = "0.5.0"
187
211
  source = "registry+https://github.com/rust-lang/crates.io-index"
188
212
  checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
189
213
 
190
- [[package]]
191
- name = "hermit-abi"
192
- version = "0.5.3"
193
- source = "registry+https://github.com/rust-lang/crates.io-index"
194
- checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
195
-
196
- [[package]]
197
- name = "indoc"
198
- version = "2.0.7"
199
- source = "registry+https://github.com/rust-lang/crates.io-index"
200
- checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
201
- dependencies = [
202
- "rustversion",
203
- ]
204
-
205
- [[package]]
206
- name = "is-terminal"
207
- version = "0.4.17"
208
- source = "registry+https://github.com/rust-lang/crates.io-index"
209
- checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
210
- dependencies = [
211
- "hermit-abi",
212
- "libc",
213
- "windows-sys",
214
- ]
215
-
216
214
  [[package]]
217
215
  name = "itertools"
218
- version = "0.10.5"
216
+ version = "0.13.0"
219
217
  source = "registry+https://github.com/rust-lang/crates.io-index"
220
- checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
218
+ checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
221
219
  dependencies = [
222
220
  "either",
223
221
  ]
@@ -307,15 +305,6 @@ version = "2.8.3"
307
305
  source = "registry+https://github.com/rust-lang/crates.io-index"
308
306
  checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
309
307
 
310
- [[package]]
311
- name = "memoffset"
312
- version = "0.9.1"
313
- source = "registry+https://github.com/rust-lang/crates.io-index"
314
- checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
315
- dependencies = [
316
- "autocfg",
317
- ]
318
-
319
308
  [[package]]
320
309
  name = "napi"
321
310
  version = "2.16.17"
@@ -394,9 +383,19 @@ version = "11.1.5"
394
383
  source = "registry+https://github.com/rust-lang/crates.io-index"
395
384
  checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
396
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
+
397
396
  [[package]]
398
397
  name = "polyxml"
399
- version = "0.2.1"
398
+ version = "0.4.0"
400
399
  dependencies = [
401
400
  "criterion",
402
401
  "lexical-core",
@@ -409,14 +408,14 @@ dependencies = [
409
408
 
410
409
  [[package]]
411
410
  name = "polyxml-c"
412
- version = "0.2.1"
411
+ version = "0.4.0"
413
412
  dependencies = [
414
413
  "polyxml",
415
414
  ]
416
415
 
417
416
  [[package]]
418
417
  name = "polyxml-js"
419
- version = "0.2.1"
418
+ version = "0.4.0"
420
419
  dependencies = [
421
420
  "napi",
422
421
  "napi-build",
@@ -426,12 +425,11 @@ dependencies = [
426
425
 
427
426
  [[package]]
428
427
  name = "polyxml-python"
429
- version = "0.2.1"
428
+ version = "0.4.0"
430
429
  dependencies = [
431
430
  "lexical-core",
432
431
  "polyxml",
433
432
  "pyo3",
434
- "quick-xml",
435
433
  "smallvec",
436
434
  ]
437
435
 
@@ -462,37 +460,32 @@ dependencies = [
462
460
 
463
461
  [[package]]
464
462
  name = "pyo3"
465
- version = "0.22.6"
463
+ version = "0.29.2"
466
464
  source = "registry+https://github.com/rust-lang/crates.io-index"
467
- checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884"
465
+ checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b"
468
466
  dependencies = [
469
- "cfg-if",
470
- "indoc",
471
467
  "libc",
472
- "memoffset",
473
468
  "once_cell",
474
469
  "portable-atomic",
475
470
  "pyo3-build-config",
476
471
  "pyo3-ffi",
477
472
  "pyo3-macros",
478
- "unindent",
479
473
  ]
480
474
 
481
475
  [[package]]
482
476
  name = "pyo3-build-config"
483
- version = "0.22.6"
477
+ version = "0.29.2"
484
478
  source = "registry+https://github.com/rust-lang/crates.io-index"
485
- checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38"
479
+ checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9"
486
480
  dependencies = [
487
- "once_cell",
488
481
  "target-lexicon",
489
482
  ]
490
483
 
491
484
  [[package]]
492
485
  name = "pyo3-ffi"
493
- version = "0.22.6"
486
+ version = "0.29.2"
494
487
  source = "registry+https://github.com/rust-lang/crates.io-index"
495
- checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636"
488
+ checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327"
496
489
  dependencies = [
497
490
  "libc",
498
491
  "pyo3-build-config",
@@ -500,9 +493,9 @@ dependencies = [
500
493
 
501
494
  [[package]]
502
495
  name = "pyo3-macros"
503
- version = "0.22.6"
496
+ version = "0.29.2"
504
497
  source = "registry+https://github.com/rust-lang/crates.io-index"
505
- checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453"
498
+ checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c"
506
499
  dependencies = [
507
500
  "proc-macro2",
508
501
  "pyo3-macros-backend",
@@ -512,22 +505,21 @@ dependencies = [
512
505
 
513
506
  [[package]]
514
507
  name = "pyo3-macros-backend"
515
- version = "0.22.6"
508
+ version = "0.29.2"
516
509
  source = "registry+https://github.com/rust-lang/crates.io-index"
517
- checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe"
510
+ checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952"
518
511
  dependencies = [
519
512
  "heck",
520
513
  "proc-macro2",
521
- "pyo3-build-config",
522
514
  "quote",
523
515
  "syn 2.0.119",
524
516
  ]
525
517
 
526
518
  [[package]]
527
519
  name = "quick-xml"
528
- version = "0.36.2"
520
+ version = "0.42.0"
529
521
  source = "registry+https://github.com/rust-lang/crates.io-index"
530
- checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
522
+ checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b"
531
523
  dependencies = [
532
524
  "memchr",
533
525
  "serde",
@@ -571,12 +563,6 @@ version = "0.8.11"
571
563
  source = "registry+https://github.com/rust-lang/crates.io-index"
572
564
  checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
573
565
 
574
- [[package]]
575
- name = "rustversion"
576
- version = "1.0.23"
577
- source = "registry+https://github.com/rust-lang/crates.io-index"
578
- checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
579
-
580
566
  [[package]]
581
567
  name = "same-file"
582
568
  version = "1.0.6"
@@ -635,6 +621,12 @@ dependencies = [
635
621
  "zmij",
636
622
  ]
637
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
+
638
630
  [[package]]
639
631
  name = "smallvec"
640
632
  version = "1.16.0"
@@ -665,9 +657,9 @@ dependencies = [
665
657
 
666
658
  [[package]]
667
659
  name = "target-lexicon"
668
- version = "0.12.16"
660
+ version = "0.13.5"
669
661
  source = "registry+https://github.com/rust-lang/crates.io-index"
670
- checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
662
+ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
671
663
 
672
664
  [[package]]
673
665
  name = "thiserror"
@@ -711,12 +703,6 @@ version = "1.13.3"
711
703
  source = "registry+https://github.com/rust-lang/crates.io-index"
712
704
  checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
713
705
 
714
- [[package]]
715
- name = "unindent"
716
- version = "0.2.4"
717
- source = "registry+https://github.com/rust-lang/crates.io-index"
718
- checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
719
-
720
706
  [[package]]
721
707
  name = "walkdir"
722
708
  version = "2.5.0"
@@ -727,6 +713,22 @@ dependencies = [
727
713
  "winapi-util",
728
714
  ]
729
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
+
730
732
  [[package]]
731
733
  name = "winapi-util"
732
734
  version = "0.1.11"
@@ -736,6 +738,12 @@ dependencies = [
736
738
  "windows-sys",
737
739
  ]
738
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
+
739
747
  [[package]]
740
748
  name = "windows-link"
741
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.2.1"
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.2.1
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.36", 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.5", 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]]
@@ -1,4 +1,6 @@
1
- use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
1
+ use std::hint::black_box;
2
+
3
+ use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
2
4
  use polyxml::schema::{FieldKind, FieldSchema, ModelSchema, ScalarType, ValueType};
3
5
  use polyxml::{deserialize, serialize};
4
6
  use std::sync::Arc;
@@ -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,7 +14,6 @@ crate-type = ["cdylib"]
14
14
 
15
15
  [dependencies]
16
16
  polyxml = { path = "../polyxml-core" }
17
- pyo3 = { version = "0.22", features = ["abi3-py312", "extension-module"] }
18
- quick-xml = { version = "0.36" }
17
+ pyo3 = { version = "0.29", features = ["abi3-py312", "extension-module"] }
19
18
  lexical-core = "1.0"
20
19
  smallvec = "1.16"
@@ -4,12 +4,15 @@
4
4
 
5
5
  use pyo3::prelude::*;
6
6
  use pyo3::types::{PyBytes, PyDict, PyList, PyString, PyTuple, PyType};
7
+ use pyo3::IntoPyObjectExt;
7
8
  use std::collections::HashMap;
8
9
  use std::sync::{Arc, RwLock};
9
10
 
10
11
  use polyxml::schema::{FieldKind, FieldSchema, ModelSchema, ScalarType, ValueType};
11
12
  use polyxml::value::PolyValue;
12
13
 
14
+ type PyObject = Py<PyAny>;
15
+
13
16
  struct CachedFieldMeta {
14
17
  py_string: Py<PyString>,
15
18
  is_init: bool,
@@ -44,21 +47,31 @@ fn lookup_py_class<'py>(py: Python<'py>, schema_name: &str) -> Option<Bound<'py,
44
47
  }
45
48
 
46
49
  fn unwrap_optional_type<'py>(type_obj: &Bound<'py, PyAny>) -> Bound<'py, PyAny> {
47
- if let Ok(origin) = type_obj.getattr("__origin__") {
50
+ let is_union = if let Ok(origin) = type_obj.getattr("__origin__") {
48
51
  if let Ok(origin_name) = origin.getattr("__name__") {
49
52
  let origin_str: String = origin_name.extract().unwrap_or_default();
50
- if origin_str == "Union" || origin_str == "UnionType" {
51
- if let Ok(args) = type_obj.getattr("__args__") {
52
- if let Ok(tuple) = args.downcast::<PyTuple>() {
53
- for arg in tuple.iter() {
54
- let arg_name: String = arg
55
- .getattr("__name__")
56
- .and_then(|n| n.extract())
57
- .unwrap_or_default();
58
- if arg_name != "NoneType" {
59
- return unwrap_optional_type(&arg);
60
- }
61
- }
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);
62
75
  }
63
76
  }
64
77
  }
@@ -68,10 +81,10 @@ fn unwrap_optional_type<'py>(type_obj: &Bound<'py, PyAny>) -> Bound<'py, PyAny>
68
81
  }
69
82
 
70
83
  fn is_enum_class<'py>(py: Python<'py>, cls: &Bound<'py, PyAny>) -> bool {
71
- if let Ok(py_type) = cls.downcast::<PyType>() {
72
- if let Ok(enum_module) = py.import_bound("enum") {
84
+ if let Ok(py_type) = cls.cast::<PyType>() {
85
+ if let Ok(enum_module) = py.import("enum") {
73
86
  if let Ok(enum_cls) = enum_module.getattr("Enum") {
74
- if let Ok(py_enum) = enum_cls.downcast::<PyType>() {
87
+ if let Ok(py_enum) = enum_cls.cast::<PyType>() {
75
88
  return py_type.is_subclass(py_enum).unwrap_or(false);
76
89
  }
77
90
  }
@@ -107,26 +120,44 @@ fn resolve_scalar_type(py: Python<'_>, type_obj: &Bound<'_, PyAny>) -> PyResult<
107
120
  }
108
121
 
109
122
  fn resolve_value_type(py: Python<'_>, type_obj: &Bound<'_, PyAny>) -> PyResult<ValueType> {
110
- // Handle typing.Optional / Union
111
- 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__") {
112
125
  if let Ok(origin_name) = origin.getattr("__name__") {
113
126
  let origin_str: String = origin_name.extract().unwrap_or_default();
114
- if origin_str == "Union" || origin_str == "UnionType" {
115
- if let Ok(args) = type_obj.getattr("__args__") {
116
- let tuple: Bound<'_, pyo3::types::PyTuple> = args.downcast_into()?;
117
- for arg in tuple.iter() {
118
- let arg_name: String = arg
119
- .getattr("__name__")
120
- .and_then(|n| n.extract())
121
- .unwrap_or_default();
122
- if arg_name != "NoneType" {
123
- return resolve_value_type(py, &arg);
124
- }
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);
125
149
  }
126
150
  }
127
- } 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" {
128
159
  if let Ok(args) = type_obj.getattr("__args__") {
129
- let tuple: Bound<'_, pyo3::types::PyTuple> = args.downcast_into()?;
160
+ let tuple: Bound<'_, pyo3::types::PyTuple> = args.cast_into()?;
130
161
  if let Some(first) = tuple.iter().next() {
131
162
  let inner = resolve_value_type(py, &first)?;
132
163
  return Ok(ValueType::List(Box::new(inner)));
@@ -141,7 +172,7 @@ fn resolve_value_type(py: Python<'_>, type_obj: &Bound<'_, PyAny>) -> PyResult<V
141
172
 
142
173
  // Check if target is a nested dataclass or Pydantic model
143
174
  if type_obj.hasattr("__dataclass_fields__")? || type_obj.hasattr("model_fields")? {
144
- if let Ok(py_type) = type_obj.downcast::<PyType>() {
175
+ if let Ok(py_type) = type_obj.cast::<PyType>() {
145
176
  let (nested_schema, _) = get_or_create_schema(py_type)?;
146
177
  return Ok(ValueType::Nested(nested_schema));
147
178
  }
@@ -160,17 +191,17 @@ fn extract_schema_from_class<'py>(
160
191
  let mut cached_fields = Vec::new();
161
192
 
162
193
  let type_hints: Option<Bound<'py, PyDict>> = py
163
- .import_bound("typing")
194
+ .import("typing")
164
195
  .ok()
165
196
  .and_then(|m| m.getattr("get_type_hints").ok())
166
197
  .and_then(|f| f.call1((cls,)).ok())
167
- .and_then(|h| h.downcast_into::<PyDict>().ok());
198
+ .and_then(|h| h.cast_into::<PyDict>().ok());
168
199
 
169
200
  if cls.hasattr("model_fields")? {
170
- 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()?;
171
202
  for (name_obj, field_obj) in model_fields.iter() {
172
203
  let py_name: String = name_obj.extract()?;
173
- let py_string = PyString::new_bound(py, &py_name).unbind();
204
+ let py_string = PyString::new(py, &py_name).unbind();
174
205
  let mut xml_name = py_name.clone();
175
206
  let mut kind = FieldKind::Element;
176
207
 
@@ -220,10 +251,10 @@ fn extract_schema_from_class<'py>(
220
251
  ));
221
252
  }
222
253
  } else if cls.hasattr("__dataclass_fields__")? {
223
- let fields: Bound<'py, PyDict> = cls.getattr("__dataclass_fields__")?.downcast_into()?;
254
+ let fields: Bound<'py, PyDict> = cls.getattr("__dataclass_fields__")?.cast_into()?;
224
255
  for (name_obj, field_obj) in fields.iter() {
225
256
  let py_name: String = name_obj.extract()?;
226
- let py_string = PyString::new_bound(py, &py_name).unbind();
257
+ let py_string = PyString::new(py, &py_name).unbind();
227
258
  let mut xml_name = py_name.clone();
228
259
  let mut kind = FieldKind::Element;
229
260
 
@@ -333,7 +364,7 @@ fn convert_scalar_to_py<'py>(
333
364
  ) -> PyResult<PyObject> {
334
365
  match val {
335
366
  PolyValue::Null => Ok(py.None()),
336
- PolyValue::Bool(b) => Ok(b.to_object(py)),
367
+ PolyValue::Bool(b) => b.into_py_any(py),
337
368
  PolyValue::Int(i) => {
338
369
  if let Some(target_type) = py_type_opt {
339
370
  if is_enum_class(py, target_type) {
@@ -342,9 +373,9 @@ fn convert_scalar_to_py<'py>(
342
373
  }
343
374
  }
344
375
  }
345
- Ok(i.to_object(py))
376
+ i.into_py_any(py)
346
377
  }
347
- PolyValue::Float(f) => Ok(f.to_object(py)),
378
+ PolyValue::Float(f) => f.into_py_any(py),
348
379
  PolyValue::String(s) => {
349
380
  if let Some(target_type) = py_type_opt {
350
381
  if is_enum_class(py, target_type) {
@@ -368,7 +399,7 @@ fn convert_scalar_to_py<'py>(
368
399
 
369
400
  match type_name.as_str() {
370
401
  "Decimal" => {
371
- let decimal_cls = py.import_bound("decimal")?.getattr("Decimal")?;
402
+ let decimal_cls = py.import("decimal")?.getattr("Decimal")?;
372
403
  let dec = decimal_cls.call1((s.as_str(),))?;
373
404
  return Ok(dec.unbind());
374
405
  }
@@ -376,7 +407,7 @@ fn convert_scalar_to_py<'py>(
376
407
  if let Ok(val) = target_type.call_method1("from_string", (s.as_str(),)) {
377
408
  return Ok(val.unbind());
378
409
  }
379
- if let Ok(datatype_mod) = py.import_bound("pyxsdata.models.datatype") {
410
+ if let Ok(datatype_mod) = py.import("pyxsdata.models.datatype") {
380
411
  if let Ok(cls) = datatype_mod.getattr(type_name.as_str()) {
381
412
  if let Ok(val) = cls.call_method1("from_string", (s.as_str(),)) {
382
413
  return Ok(val.unbind());
@@ -392,7 +423,7 @@ fn convert_scalar_to_py<'py>(
392
423
  "XmlTime" => "time",
393
424
  _ => "",
394
425
  };
395
- if let Ok(datetime_mod) = py.import_bound("datetime") {
426
+ if let Ok(datetime_mod) = py.import("datetime") {
396
427
  if let Ok(cls) = datetime_mod.getattr(mod_name) {
397
428
  if let Ok(val) = cls.call_method1("fromisoformat", (s.as_str(),)) {
398
429
  return Ok(val.unbind());
@@ -404,7 +435,7 @@ fn convert_scalar_to_py<'py>(
404
435
  if let Ok(val) = target_type.call1((s.as_str(),)) {
405
436
  return Ok(val.unbind());
406
437
  }
407
- if let Ok(datatype_mod) = py.import_bound("pyxsdata.models.datatype") {
438
+ if let Ok(datatype_mod) = py.import("pyxsdata.models.datatype") {
408
439
  if let Ok(cls) = datatype_mod.getattr("XmlDuration") {
409
440
  if let Ok(val) = cls.call1((s.as_str(),)) {
410
441
  return Ok(val.unbind());
@@ -415,7 +446,7 @@ fn convert_scalar_to_py<'py>(
415
446
  _ => {}
416
447
  }
417
448
  }
418
- Ok(PyString::new_bound(py, s).into_any().unbind())
449
+ Ok(PyString::new(py, s).into_any().unbind())
419
450
  }
420
451
  _ => Ok(py.None()),
421
452
  }
@@ -430,7 +461,7 @@ fn poly_value_to_py<'py>(
430
461
  ) -> PyResult<PyObject> {
431
462
  match val {
432
463
  PolyValue::Null => Ok(py.None()),
433
- PolyValue::Bool(b) => Ok(b.to_object(py)),
464
+ PolyValue::Bool(b) => b.into_py_any(py),
434
465
  PolyValue::Int(_) | PolyValue::Float(_) | PolyValue::String(_) => {
435
466
  let st = match val_type {
436
467
  ValueType::Scalar(ref s) => s,
@@ -456,10 +487,10 @@ fn poly_value_to_py<'py>(
456
487
  poly_value_to_py(py, item, inner_type, inner_cls.as_ref(), field_meta)?;
457
488
  py_items.push(py_item);
458
489
  }
459
- let py_list = PyList::new_bound(py, &py_items);
490
+ let py_list = PyList::new(py, &py_items)?;
460
491
  Ok(py_list.into_any().unbind())
461
492
  } else {
462
- let py_list = PyList::empty_bound(py);
493
+ let py_list = PyList::empty(py);
463
494
  Ok(py_list.into_any().unbind())
464
495
  }
465
496
  }
@@ -508,7 +539,7 @@ fn poly_value_to_py<'py>(
508
539
  }
509
540
  }
510
541
  if all_found {
511
- let tuple = pyo3::types::PyTuple::new_bound(py, &args_vec);
542
+ let tuple = pyo3::types::PyTuple::new(py, &args_vec)?;
512
543
  if let Ok(instance) = target_cls.call1(tuple) {
513
544
  return Ok(instance.unbind());
514
545
  }
@@ -517,7 +548,7 @@ fn poly_value_to_py<'py>(
517
548
  }
518
549
 
519
550
  // Method 2: Keyword arguments with init=False post-init support
520
- let kwargs = PyDict::new_bound(py);
551
+ let kwargs = PyDict::new(py);
521
552
  let mut post_init_fields: Vec<(Py<PyString>, PyObject)> = Vec::new();
522
553
 
523
554
  for (i, field) in schema.fields.iter().enumerate() {
@@ -546,7 +577,7 @@ fn poly_value_to_py<'py>(
546
577
  kwargs.set_item(&field.name, py_val)?;
547
578
  } else {
548
579
  post_init_fields
549
- .push((PyString::new_bound(py, &field.name).unbind(), py_val));
580
+ .push((PyString::new(py, &field.name).unbind(), py_val));
550
581
  }
551
582
  }
552
583
  }
@@ -556,7 +587,7 @@ fn poly_value_to_py<'py>(
556
587
  }
557
588
  return Ok(instance.unbind());
558
589
  }
559
- let dict = PyDict::new_bound(py);
590
+ let dict = PyDict::new(py);
560
591
  for (k, v) in map {
561
592
  let py_v =
562
593
  poly_value_to_py(py, v, &ValueType::Scalar(ScalarType::Any), None, None)?;
@@ -565,7 +596,7 @@ fn poly_value_to_py<'py>(
565
596
  let instance = target_cls.call((), Some(&dict))?;
566
597
  Ok(instance.unbind())
567
598
  } else {
568
- let dict = PyDict::new_bound(py);
599
+ let dict = PyDict::new(py);
569
600
  for (k, v) in map {
570
601
  let py_v =
571
602
  poly_value_to_py(py, v, &ValueType::Scalar(ScalarType::Any), None, None)?;
@@ -630,7 +661,7 @@ fn py_to_poly_value<'py>(
630
661
  }
631
662
  },
632
663
  ValueType::List(inner) => {
633
- if let Ok(list) = val.downcast::<PyList>() {
664
+ if let Ok(list) = val.cast::<PyList>() {
634
665
  let mut poly_items = Vec::with_capacity(list.len());
635
666
  let child_meta = if let ValueType::Nested(sub_schema) = inner.as_ref() {
636
667
  lookup_cached_meta(&sub_schema.name)
@@ -763,7 +794,7 @@ fn serialize<'py>(
763
794
  let bytes = polyxml::serialize(root_name, &poly_val, &meta.schema, indent)
764
795
  .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
765
796
 
766
- Ok(PyBytes::new_bound(py, &bytes))
797
+ Ok(PyBytes::new(py, &bytes))
767
798
  }
768
799
 
769
800
  #[pyfunction]
@@ -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.2.1"
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