polyxml 0.3.0__tar.gz → 0.5.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 (25) hide show
  1. {polyxml-0.3.0 → polyxml-0.5.0}/Cargo.lock +87 -58
  2. {polyxml-0.3.0 → polyxml-0.5.0}/Cargo.toml +1 -1
  3. {polyxml-0.3.0 → polyxml-0.5.0}/PKG-INFO +4 -1
  4. {polyxml-0.3.0 → polyxml-0.5.0}/crates/polyxml-core/Cargo.toml +2 -2
  5. {polyxml-0.3.0 → polyxml-0.5.0}/crates/polyxml-core/src/error.rs +4 -1
  6. {polyxml-0.3.0 → polyxml-0.5.0}/crates/polyxml-core/src/parser.rs +58 -16
  7. {polyxml-0.3.0 → polyxml-0.5.0}/crates/polyxml-python/Cargo.toml +1 -1
  8. {polyxml-0.3.0 → polyxml-0.5.0}/crates/polyxml-python/src/lib.rs +65 -35
  9. {polyxml-0.3.0 → polyxml-0.5.0}/crates/polyxml-python/tests/test_polyxml.py +77 -4
  10. {polyxml-0.3.0 → polyxml-0.5.0}/pyproject.toml +5 -1
  11. polyxml-0.5.0/python/polyxml/AGENT_GUIDE.md +89 -0
  12. polyxml-0.5.0/python/polyxml/__init__.py +194 -0
  13. polyxml-0.3.0/python/polyxml/__init__.py +0 -87
  14. {polyxml-0.3.0 → polyxml-0.5.0}/README.md +0 -0
  15. {polyxml-0.3.0 → polyxml-0.5.0}/crates/polyxml-core/README.md +0 -0
  16. {polyxml-0.3.0 → polyxml-0.5.0}/crates/polyxml-core/benches/core_benchmarks.rs +0 -0
  17. {polyxml-0.3.0 → polyxml-0.5.0}/crates/polyxml-core/src/converters.rs +0 -0
  18. {polyxml-0.3.0 → polyxml-0.5.0}/crates/polyxml-core/src/lib.rs +0 -0
  19. {polyxml-0.3.0 → polyxml-0.5.0}/crates/polyxml-core/src/schema.rs +0 -0
  20. {polyxml-0.3.0 → polyxml-0.5.0}/crates/polyxml-core/src/serializer.rs +0 -0
  21. {polyxml-0.3.0 → polyxml-0.5.0}/crates/polyxml-core/src/value.rs +0 -0
  22. {polyxml-0.3.0 → polyxml-0.5.0}/crates/polyxml-core/tests/test_core.rs +0 -0
  23. {polyxml-0.3.0 → polyxml-0.5.0}/crates/polyxml-core/tests/test_security.rs +0 -0
  24. {polyxml-0.3.0 → polyxml-0.5.0}/crates/polyxml-python/README.md +0 -0
  25. {polyxml-0.3.0 → polyxml-0.5.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.5.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.5.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.5.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.5.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.5.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.5.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -16,7 +16,10 @@ Requires-Dist: pytest>=8.0.0 ; extra == 'dev'
16
16
  Requires-Dist: pytest-cov>=5.0.0 ; extra == 'dev'
17
17
  Requires-Dist: pydantic>=2.7.0 ; extra == 'dev'
18
18
  Requires-Dist: ruff>=0.4.0 ; extra == 'dev'
19
+ Requires-Dist: msgspec>=0.18.0 ; extra == 'dev'
20
+ Requires-Dist: msgspec>=0.18.0 ; extra == 'msgpack'
19
21
  Provides-Extra: dev
22
+ Provides-Extra: msgpack
20
23
  Summary: High-performance, polyglot XML data-binding engine built in Rust
21
24
  Author-email: Bailey Nguyen <bailey.tan.nguyen@gmail.com>
22
25
  License: MIT
@@ -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)
@@ -12,7 +12,6 @@ try:
12
12
  except ImportError:
13
13
 
14
14
  class XmlDate:
15
-
16
15
  def __init__(self, val: str):
17
16
  self.val = val
18
17
 
@@ -27,7 +26,6 @@ except ImportError:
27
26
  return self.val
28
27
 
29
28
  class XmlDateTime:
30
-
31
29
  def __init__(self, val: str):
32
30
  self.val = val
33
31
 
@@ -42,7 +40,6 @@ except ImportError:
42
40
  return self.val
43
41
 
44
42
  class XmlTime:
45
-
46
43
  def __init__(self, val: str):
47
44
  self.val = val
48
45
 
@@ -57,7 +54,6 @@ except ImportError:
57
54
  return self.val
58
55
 
59
56
  class XmlDuration:
60
-
61
57
  def __init__(self, val: str):
62
58
  self.val = val
63
59
 
@@ -67,6 +63,7 @@ except ImportError:
67
63
  def __str__(self):
68
64
  return self.val
69
65
 
66
+
70
67
  import polyxml
71
68
 
72
69
 
@@ -329,3 +326,79 @@ def test_to_bytes_streams():
329
326
  def test_to_bytes_invalid_type():
330
327
  with pytest.raises(TypeError, match="Unsupported XML source type"):
331
328
  polyxml.deserialize(12345, SimpleItem) # type: ignore[arg-type]
329
+
330
+
331
+ @dataclass
332
+ class UnionWrapper:
333
+ child: None | ChildNode = field(default=None, metadata={"type": "Element", "name": "child"})
334
+ count: int | None = field(default=None, metadata={"type": "Element"})
335
+
336
+
337
+ def test_pep604_union_nested_dataclass():
338
+ xml = "<UnionWrapper><child><tag>Payload</tag></child><count>42</count></UnionWrapper>"
339
+ res = polyxml.deserialize(xml, UnionWrapper)
340
+ assert res.child is not None
341
+ assert res.child.tag == "Payload"
342
+ assert res.count == 42
343
+
344
+
345
+ def test_binary_serialization_dataclass():
346
+ item = SimpleItem(id=42, name="TestItem", price=19.99, active=True)
347
+ payload = polyxml.dumps_binary(item)
348
+ assert isinstance(payload, bytes)
349
+ assert len(payload) > 0
350
+
351
+ decoded = polyxml.loads_binary(payload, SimpleItem)
352
+ assert isinstance(decoded, SimpleItem)
353
+ assert decoded.id == 42
354
+ assert decoded.name == "TestItem"
355
+ assert decoded.price == 19.99
356
+ assert decoded.active is True
357
+
358
+
359
+ def test_binary_serialization_decimal():
360
+ @dataclass
361
+ class DecimalItem:
362
+ val: Decimal
363
+
364
+ item = DecimalItem(val=Decimal("19.99"))
365
+ payload = polyxml.dumps_binary(item)
366
+ decoded = polyxml.loads_binary(payload, DecimalItem)
367
+ assert decoded.val == Decimal("19.99")
368
+
369
+
370
+ def test_binary_serialization_untyped():
371
+ data = {"name": "Alice", "score": 100}
372
+ payload = polyxml.dumps_binary(data)
373
+ decoded = polyxml.loads_binary(payload)
374
+ assert decoded == data
375
+
376
+
377
+ def test_binary_serialization_xmldate_and_path():
378
+ @dataclass
379
+ class ExtraItem:
380
+ date: XmlDate
381
+ path: pathlib.Path
382
+
383
+ orig = ExtraItem(date=XmlDate("2026-09-12"), path=pathlib.Path("/tmp/test.xml"))
384
+ payload = polyxml.dumps_binary(orig)
385
+ decoded = polyxml.loads_binary(payload, ExtraItem)
386
+ assert decoded.date == XmlDate("2026-09-12")
387
+ assert decoded.path == pathlib.Path("/tmp/test.xml")
388
+
389
+
390
+ def test_binary_serialization_unsupported_type():
391
+ class CustomObject:
392
+ pass
393
+
394
+ with pytest.raises(NotImplementedError, match="PolyXML binary serializer cannot serialize"):
395
+ polyxml.dumps_binary(CustomObject())
396
+
397
+
398
+ def test_binary_deserialization_unsupported_type():
399
+ class UnhandledClass:
400
+ pass
401
+
402
+ payload = polyxml.dumps_binary({"a": 1})
403
+ with pytest.raises(NotImplementedError, match="PolyXML binary deserializer cannot deserialize"):
404
+ polyxml.loads_binary(payload, UnhandledClass)
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "polyxml"
7
- version = "0.3.0"
7
+ version = "0.5.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" }
@@ -28,11 +28,15 @@ classifiers = [
28
28
  dependencies = []
29
29
 
30
30
  [project.optional-dependencies]
31
+ msgpack = [
32
+ "msgspec>=0.18.0",
33
+ ]
31
34
  dev = [
32
35
  "pytest>=8.0.0",
33
36
  "pytest-cov>=5.0.0",
34
37
  "pydantic>=2.7.0",
35
38
  "ruff>=0.4.0",
39
+ "msgspec>=0.18.0",
36
40
  ]
37
41
 
38
42
  [project.urls]
@@ -0,0 +1,89 @@
1
+ # PolyXML AI Agent Guide
2
+
3
+ Quick-reference instructions for AI coding assistants (Claude, Cursor, Copilot, Antigravity, etc.) using `polyxml` in Python projects.
4
+
5
+ ---
6
+
7
+ ## 1. Overview & Capabilities
8
+
9
+ `polyxml` is an ultra-fast, native XML data-binding engine written in pure Rust (using `quick-xml` and `lexical-core`) with stable ABI (`abi3-py312`) Python bindings. It provides bidirectional deserialization and serialization for Python 3.12+ `dataclasses` and Pydantic v2 models.
10
+
11
+ ---
12
+
13
+ ## 2. Common Recipes
14
+
15
+ ### A. Deserializing XML (Strings, Bytes, or Files)
16
+ ```python
17
+ from dataclasses import dataclass, field
18
+ import pathlib
19
+ import polyxml
20
+
21
+
22
+ @dataclass
23
+ class Item:
24
+ id: int = field(metadata={"type": "Attribute"})
25
+ name: str = field(metadata={"type": "Element"})
26
+ price: float = field(metadata={"type": "Element"})
27
+
28
+
29
+ # 1. From raw XML string:
30
+ item = polyxml.deserialize('<Item id="1"><name>Bolt</name><price>0.99</price></Item>', Item)
31
+
32
+ # 2. From raw bytes:
33
+ item = polyxml.deserialize(b"<Item ...>...</Item>", Item)
34
+
35
+ # 3. From pathlib.Path:
36
+ item = polyxml.deserialize(pathlib.Path("item.xml"), Item)
37
+ ```
38
+
39
+ ### B. Streaming Massive XML Files with O(1) Constant Memory
40
+ For large datasets (hundreds of MBs or GBs such as transit timetables or feeds), do **NOT** load the entire XML tree at once. Use `iterparse`:
41
+
42
+ ```python
43
+ import polyxml
44
+ from my_models import VehicleJourney
45
+
46
+ # Streams elements one-by-one with zero intermediate DOM allocations
47
+ for journey in polyxml.iterparse("large_transit_dump.xml", VehicleJourney, tag="VehicleJourney"):
48
+ process_journey(journey)
49
+ ```
50
+
51
+ ### C. Pydantic v2 Models
52
+ PolyXML natively deserializes into Pydantic v2 `BaseModel` classes with the same API:
53
+
54
+ ```python
55
+ from pydantic import BaseModel, Field
56
+ import polyxml
57
+
58
+
59
+ class StopPoint(BaseModel):
60
+ id: str = Field(json_schema_extra={"type": "Attribute"})
61
+ name: str
62
+
63
+
64
+ stop = polyxml.deserialize(
65
+ '<StopPoint id="SP1"><name>Central Station</name></StopPoint>', StopPoint
66
+ )
67
+ ```
68
+
69
+ ### D. Serializing to XML
70
+ ```python
71
+ import polyxml
72
+
73
+ # Serialize model to UTF-8 XML bytes
74
+ xml_bytes = polyxml.serialize(item, indent=2)
75
+
76
+ # If string is required:
77
+ xml_string = xml_bytes.decode("utf-8")
78
+ ```
79
+
80
+ ---
81
+
82
+ ## 3. Common Pitfalls & Quick Fixes
83
+
84
+ - **Pitfall**: Attempting `polyxml.XmlParser()`
85
+ **Fix**: PolyXML uses module-level functional entrypoints: `polyxml.deserialize(...)` and `polyxml.iterparse(...)`.
86
+ - **Pitfall**: Passing file handles or paths without reading
87
+ **Fix**: `polyxml.deserialize(...)` accepts strings, byte buffers, `pathlib.Path`, or `IO` streams directly.
88
+ - **Pitfall**: High memory usage on massive XML files
89
+ **Fix**: Switch from `polyxml.deserialize` to `polyxml.iterparse(source, ModelClass, tag="TagName")`.
@@ -0,0 +1,194 @@
1
+ """PolyXML: High-performance, polyglot native XML data-binding engine.
2
+
3
+ QUICKSTART FOR AI AGENTS & DEVELOPERS:
4
+
5
+ 1. Deserialize XML (string, bytes, or file Path) into a typed Dataclass or Pydantic model:
6
+ >>> import polyxml
7
+ >>> from dataclasses import dataclass, field
8
+ >>>
9
+ >>> @dataclass
10
+ >>> class User:
11
+ ... name: str = field(metadata={"type": "Element"})
12
+ >>>
13
+ >>> user = polyxml.deserialize("<User><name>Alice</name></User>", User)
14
+ >>> user.name
15
+ 'Alice'
16
+
17
+ 2. Stream large XML files with O(1) constant memory (Streaming / Facet Extraction):
18
+ >>> for user in polyxml.iterparse("massive_users.xml", User, tag="User"):
19
+ ... process(user)
20
+
21
+ 3. Serialize a model instance back into XML bytes:
22
+ >>> xml_bytes = polyxml.serialize(user, indent=2)
23
+
24
+ See `AGENT_GUIDE.md` or https://nth-bailey.github.io/PolyXML/ for complete documentation.
25
+ """
26
+
27
+ import pathlib
28
+ from collections.abc import Iterator
29
+ from decimal import Decimal
30
+ from typing import IO
31
+
32
+ from polyxml._polyxml import ( # type: ignore[import-not-found]
33
+ deserialize as _deserialize,
34
+ )
35
+ from polyxml._polyxml import (
36
+ iterparse as _iterparse,
37
+ )
38
+ from polyxml._polyxml import (
39
+ serialize as _serialize,
40
+ )
41
+ from polyxml._polyxml import (
42
+ version as _version,
43
+ )
44
+
45
+ __version__: str = _version()
46
+
47
+
48
+ def _to_bytes(source: bytes | str | pathlib.Path | IO[bytes] | IO[str]) -> bytes:
49
+ if isinstance(source, bytes):
50
+ return source
51
+ if isinstance(source, str):
52
+ if source.lstrip().startswith("<"):
53
+ return source.encode("utf-8")
54
+ path = pathlib.Path(source)
55
+ if path.is_file():
56
+ return path.read_bytes()
57
+ return source.encode("utf-8")
58
+ if isinstance(source, pathlib.Path):
59
+ return source.read_bytes()
60
+ if hasattr(source, "read"):
61
+ data = source.read()
62
+ return data.encode("utf-8") if isinstance(data, str) else data
63
+ raise TypeError(
64
+ f"Unsupported XML source type: {type(source).__name__}. "
65
+ "Expected bytes, str (XML string or file path), pathlib.Path, or a binary/text IO stream."
66
+ )
67
+
68
+
69
+ def deserialize[T](
70
+ source: bytes | str | pathlib.Path | IO[bytes] | IO[str], target_type: type[T]
71
+ ) -> T:
72
+ """Deserialize XML bytes, string, file path, or stream into a typed model.
73
+
74
+ Args:
75
+ source: XML content as raw bytes, string, Path, or file stream.
76
+ target_type: The target dataclass or model class.
77
+
78
+ Returns:
79
+ The deserialized model instance.
80
+ """
81
+ return _deserialize(_to_bytes(source), target_type)
82
+
83
+
84
+ def iterparse[T](
85
+ source: bytes | str | pathlib.Path | IO[bytes] | IO[str],
86
+ target_type: type[T],
87
+ tag: str | None = None,
88
+ ) -> Iterator[T]:
89
+ """Stream and deserialize XML elements one-by-one with O(1) constant memory.
90
+
91
+ Args:
92
+ source: XML content as raw bytes, string, Path, or file stream.
93
+ target_type: The target dataclass or model class for each element.
94
+ tag: Optional XML element tag name to match. Defaults to the model schema name.
95
+
96
+ Yields:
97
+ Deserialized model instances as they are streamed.
98
+ """
99
+ return _iterparse(_to_bytes(source), target_type, tag)
100
+
101
+
102
+ def serialize(obj: object, *, indent: int | None = None) -> bytes:
103
+ """Serialize a strongly-typed model instance back into XML bytes.
104
+
105
+ Args:
106
+ obj: Python dataclass or Pydantic model instance.
107
+ indent: Optional indentation size in spaces for pretty-printing.
108
+
109
+ Returns:
110
+ UTF-8 encoded XML bytes representing the model instance.
111
+ """
112
+ return _serialize(obj, indent=indent)
113
+
114
+
115
+ try:
116
+ import msgspec
117
+
118
+ _HAS_MSGSPEC = True
119
+ except ImportError: # pragma: no cover
120
+ msgspec = None # type: ignore[assignment]
121
+ _HAS_MSGSPEC = False
122
+
123
+
124
+ def _enc_hook(obj: object) -> str:
125
+ if isinstance(obj, pathlib.Path | Decimal):
126
+ return str(obj)
127
+ if hasattr(obj, "from_string") and hasattr(obj, "val"):
128
+ return str(obj)
129
+ raise NotImplementedError(
130
+ f"PolyXML binary serializer cannot serialize object of type {type(obj).__name__}"
131
+ )
132
+
133
+
134
+ def _dec_hook(target_type: type[object], obj: object) -> object:
135
+ if target_type is pathlib.Path:
136
+ return pathlib.Path(str(obj))
137
+ if hasattr(target_type, "from_string"):
138
+ return target_type.from_string(str(obj))
139
+ raise NotImplementedError(
140
+ f"PolyXML binary deserializer cannot deserialize object into type {target_type.__name__}"
141
+ )
142
+
143
+
144
+ def dumps_binary(obj: object) -> bytes:
145
+ """Serialize a model or dataclass into a high-throughput binary MessagePack buffer.
146
+
147
+ Args:
148
+ obj: Python dataclass, Pydantic model, or object.
149
+
150
+ Returns:
151
+ Compact binary MessagePack bytes.
152
+
153
+ Raises:
154
+ RuntimeError: If msgspec is not installed.
155
+ """
156
+ if not _HAS_MSGSPEC: # pragma: no cover
157
+ raise RuntimeError(
158
+ "PolyXML binary serialization requires 'msgspec'. "
159
+ "Install it via: pip install 'polyxml[msgpack]' or pip install msgspec"
160
+ )
161
+ return msgspec.msgpack.encode(obj, enc_hook=_enc_hook)
162
+
163
+
164
+ def loads_binary[T](data: bytes, target_type: type[T] | None = None) -> T | object:
165
+ """Deserialize binary MessagePack bytes into a strongly-typed model or object.
166
+
167
+ Args:
168
+ data: Binary MessagePack bytes to decode.
169
+ target_type: Optional target dataclass or model class. If omitted, returns dynamic structure.
170
+
171
+ Returns:
172
+ The deserialized model instance or object.
173
+
174
+ Raises:
175
+ RuntimeError: If msgspec is not installed.
176
+ """
177
+ if not _HAS_MSGSPEC: # pragma: no cover
178
+ raise RuntimeError(
179
+ "PolyXML binary deserialization requires 'msgspec'. "
180
+ "Install it via: pip install 'polyxml[msgpack]' or pip install msgspec"
181
+ )
182
+ if target_type is not None:
183
+ return msgspec.msgpack.decode(data, type=target_type, dec_hook=_dec_hook)
184
+ return msgspec.msgpack.decode(data, dec_hook=_dec_hook)
185
+
186
+
187
+ __all__ = [
188
+ "__version__",
189
+ "deserialize",
190
+ "dumps_binary",
191
+ "iterparse",
192
+ "loads_binary",
193
+ "serialize",
194
+ ]
@@ -1,87 +0,0 @@
1
- """PolyXML: High-performance, polyglot XML data-binding engine."""
2
-
3
- import pathlib
4
- from collections.abc import Iterator
5
- from typing import IO
6
-
7
- from polyxml._polyxml import ( # type: ignore[import-not-found]
8
- deserialize as _deserialize,
9
- )
10
- from polyxml._polyxml import (
11
- iterparse as _iterparse,
12
- )
13
- from polyxml._polyxml import (
14
- serialize as _serialize,
15
- )
16
- from polyxml._polyxml import (
17
- version as _version,
18
- )
19
-
20
- __version__: str = _version()
21
-
22
-
23
- def _to_bytes(source: bytes | str | pathlib.Path | IO[bytes] | IO[str]) -> bytes:
24
- if isinstance(source, bytes):
25
- return source
26
- if isinstance(source, str):
27
- if source.lstrip().startswith("<"):
28
- return source.encode("utf-8")
29
- path = pathlib.Path(source)
30
- if path.is_file():
31
- return path.read_bytes()
32
- return source.encode("utf-8")
33
- if isinstance(source, pathlib.Path):
34
- return source.read_bytes()
35
- if hasattr(source, "read"):
36
- data = source.read()
37
- return data.encode("utf-8") if isinstance(data, str) else data
38
- raise TypeError(f"Unsupported XML source type: {type(source)}")
39
-
40
-
41
- def deserialize[T](
42
- source: bytes | str | pathlib.Path | IO[bytes] | IO[str], target_type: type[T]
43
- ) -> T:
44
- """Deserialize XML bytes, string, file path, or stream into a typed model.
45
-
46
- Args:
47
- source: XML content as raw bytes, string, Path, or file stream.
48
- target_type: The target dataclass or model class.
49
-
50
- Returns:
51
- The deserialized model instance.
52
- """
53
- return _deserialize(_to_bytes(source), target_type)
54
-
55
-
56
- def iterparse[T](
57
- source: bytes | str | pathlib.Path | IO[bytes] | IO[str],
58
- target_type: type[T],
59
- tag: str | None = None,
60
- ) -> Iterator[T]:
61
- """Stream and deserialize XML elements one-by-one with O(1) constant memory.
62
-
63
- Args:
64
- source: XML content as raw bytes, string, Path, or file stream.
65
- target_type: The target dataclass or model class for each element.
66
- tag: Optional XML element tag name to match. Defaults to the model schema name.
67
-
68
- Yields:
69
- Deserialized model instances as they are streamed.
70
- """
71
- return _iterparse(_to_bytes(source), target_type, tag)
72
-
73
-
74
- def serialize(obj: object, *, indent: int | None = None) -> bytes:
75
- """Serialize a strongly-typed model instance back into XML bytes.
76
-
77
- Args:
78
- obj: Python dataclass or Pydantic model instance.
79
- indent: Optional indentation size in spaces for pretty-printing.
80
-
81
- Returns:
82
- UTF-8 encoded XML bytes representing the model instance.
83
- """
84
- return _serialize(obj, indent=indent)
85
-
86
-
87
- __all__ = ["__version__", "deserialize", "iterparse", "serialize"]
File without changes
File without changes