Skip to content

Commit eee477b

Browse files
committed
fix: address PR review feedback from Sourcery AI
- Handle very large integers (beyond i64) by falling back to string representation instead of raising OverflowError - Add compatibility tests for item_wrap=False and list_headers=True (marked xfail for known implementation differences) - Tighten test_numeric_string_key assertion to match actual behavior - Add test for very large integers beyond i64 range - Gate benchmark job to only run on push to main/master or manual trigger, not on every PR (reduces CI time) - Add workflow_dispatch trigger for manual benchmark runs
1 parent ec80bef commit eee477b

File tree

3 files changed

+65
-12
lines changed

3 files changed

+65
-12
lines changed

.github/workflows/rust-ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ on:
1313
- 'rust/**'
1414
- 'tests/test_rust_dicttoxml.py'
1515
- '.github/workflows/rust-ci.yml'
16+
workflow_dispatch: # Allow manual trigger
1617

1718
env:
1819
CARGO_TERM_COLOR: always
@@ -87,6 +88,8 @@ jobs:
8788
benchmark:
8889
name: Performance Benchmark
8990
runs-on: ubuntu-latest
91+
# Only run benchmarks on push to main/master or manual trigger, not on PRs
92+
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
9093
steps:
9194
- uses: actions/checkout@v4
9295

rust/src/lib.rs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,13 @@ fn convert_value(
121121
return convert_bool(item_name, val, config);
122122
}
123123

124-
// Handle int
124+
// Handle int - try i64 first, fall back to string for large integers
125125
if obj.is_instance_of::<PyInt>() {
126-
let val: i64 = obj.extract()?;
127-
return convert_number(item_name, &val.to_string(), "int", config);
126+
let val_str = match obj.extract::<i64>() {
127+
Ok(val) => val.to_string(),
128+
Err(_) => obj.str()?.extract::<String>()?, // Fall back for big ints
129+
};
130+
return convert_number(item_name, &val_str, "int", config);
128131
}
129132

130133
// Handle float
@@ -277,9 +280,12 @@ fn convert_dict(
277280
)
278281
.unwrap();
279282
}
280-
// Handle int
283+
// Handle int - try i64 first, fall back to string for large integers
281284
else if val.is_instance_of::<PyInt>() {
282-
let int_val: i64 = val.extract()?;
285+
let int_str = match val.extract::<i64>() {
286+
Ok(v) => v.to_string(),
287+
Err(_) => val.str()?.extract::<String>()?,
288+
};
283289
let mut attrs = Vec::new();
284290
if let Some((k, v)) = name_attr {
285291
attrs.push((k, v));
@@ -291,7 +297,7 @@ fn convert_dict(
291297
write!(
292298
output,
293299
"<{}{}>{}</{}>",
294-
xml_key, attr_string, int_val, xml_key
300+
xml_key, attr_string, int_str, xml_key
295301
)
296302
.unwrap();
297303
}
@@ -456,9 +462,12 @@ fn convert_list(
456462
)
457463
.unwrap();
458464
}
459-
// Handle int
465+
// Handle int - try i64 first, fall back to string for large integers
460466
else if item.is_instance_of::<PyInt>() {
461-
let int_val: i64 = item.extract()?;
467+
let int_str = match item.extract::<i64>() {
468+
Ok(v) => v.to_string(),
469+
Err(_) => item.str()?.extract::<String>()?,
470+
};
462471
let mut attrs = Vec::new();
463472
if config.attr_type {
464473
attrs.push(("type".to_string(), "int".to_string()));
@@ -467,7 +476,7 @@ fn convert_list(
467476
write!(
468477
output,
469478
"<{}{}>{}</{}>",
470-
tag_name, attr_string, int_val, tag_name
479+
tag_name, attr_string, int_str, tag_name
471480
)
472481
.unwrap();
473482
}

tests/test_rust_dicttoxml.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,40 @@ def test_complex_nested_matches(self):
325325
rust, python = self.compare_outputs(data)
326326
assert rust == python
327327

328+
def test_item_wrap_false_matches(self):
329+
"""Test that item_wrap=False produces matching output."""
330+
data = {"colors": ["red", "green", "blue"]}
331+
rust, python = self.compare_outputs(data, item_wrap=False)
332+
assert rust == python
333+
334+
@pytest.mark.xfail(reason="Rust list_headers implementation differs from Python - uses different wrapping semantics")
335+
def test_list_headers_true_matches(self):
336+
"""Test that list_headers=True produces matching output."""
337+
data = {"items": ["one", "two", "three"]}
338+
rust, python = self.compare_outputs(data, list_headers=True)
339+
assert rust == python
340+
341+
@pytest.mark.xfail(reason="Rust item_wrap=False with nested dicts differs from Python - known limitation")
342+
def test_item_wrap_false_with_nested_dict_matches(self):
343+
"""Test item_wrap=False with nested dicts in list."""
344+
data = {"users": [{"name": "Alice"}, {"name": "Bob"}]}
345+
rust, python = self.compare_outputs(data, item_wrap=False)
346+
assert rust == python
347+
348+
@pytest.mark.xfail(reason="Rust list_headers with nested structures differs from Python - known limitation")
349+
def test_list_headers_with_nested_matches(self):
350+
"""Test list_headers=True with nested structures."""
351+
data = {"products": [{"id": 1, "name": "Widget"}, {"id": 2, "name": "Gadget"}]}
352+
rust, python = self.compare_outputs(data, list_headers=True)
353+
assert rust == python
354+
355+
def test_very_large_integer_matches(self):
356+
"""Test that very large integers (beyond i64) produce matching output."""
357+
big_int = 10**30 # Way beyond i64 range
358+
data = {"huge": big_int}
359+
rust, python = self.compare_outputs(data)
360+
assert rust == python
361+
328362

329363
class TestFastDicttoxmlWrapper:
330364
"""Test the dicttoxml_fast wrapper module."""
@@ -378,11 +412,11 @@ def test_unicode_keys(self):
378412
assert "México" in result_str
379413

380414
def test_numeric_string_key(self):
381-
# Keys that are numbers should be prefixed with 'n'
415+
# Keys that are purely numeric should be prefixed with 'n'
382416
data = {"123": "value"}
383417
result = rust_dicttoxml(data)
384-
# Either the key is modified or wrapped in a name attribute
385-
assert b"<n123" in result or b'name="123"' in result
418+
# Numeric keys are always prefixed with 'n' per make_valid_xml_name
419+
assert b"<n123" in result
386420

387421
def test_key_with_spaces(self):
388422
data = {"my key": "value"}
@@ -395,6 +429,13 @@ def test_large_integer(self):
395429
result = rust_dicttoxml(data)
396430
assert b"9999999999999999" in result
397431

432+
def test_very_large_integer_beyond_i64(self):
433+
"""Test integers larger than i64 max (2^63-1) are handled gracefully."""
434+
big_int = 10**30 # Way beyond i64 range
435+
data = {"huge": big_int}
436+
result = rust_dicttoxml(data)
437+
assert str(big_int).encode() in result
438+
398439
def test_negative_number(self):
399440
data = {"negative": -42}
400441
result = rust_dicttoxml(data)

0 commit comments

Comments
 (0)