Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lat.md/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ Dependabot checks the root and documentation Python dependency manifests weekly,

GitHub Actions workflows run with read-only tokens by default and use full SHA pins so third-party action updates are explicit.

Test fixtures keep one import form per module and avoid custom hashes without matching equality so CodeQL findings stay actionable.
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
Resolved

The `.github/workflows/` files declare the minimum `permissions:` scopes needed by each workflow, with CodeQL retaining `security-events: write` for result upload and TestPyPI retaining `id-token: write` for explicit trusted-publishing runs. The Python test job also scopes `id-token: write` and `FLAKINESS_PROJECT` to its pytest matrix so the flakiness reporter can authenticate through GitHub OIDC without exposing that permission to lint. Release-branch pushes build distributions and runs Twine checks; TestPyPI upload is a manual opt-in because that external registry requires separate publisher configuration. Action references are pinned to immutable commits with the upstream tag retained in comments for reviewability, and all CodeQL phases use the same release commit because mixed action versions reject each other's configuration. `.github/dependabot.yml` checks the `github-actions` ecosystem weekly so those pins do not silently age. The Python test matrix pins its PyPy 3.11 job to an explicit PyPy release so CI keeps exercising the intended CPython 3.11.15-compatible runtime instead of silently drifting with runner cache updates. The Python, Rust extension, and Rust wheel test workflows exercise stable CPython 3.15.0; Python 3.15 free-threaded builds remain outside the CI matrix.

The native backend is only selected for payloads it renders byte-identically. [[json2xml/backend_selector.py#rust_renders_identically]] is the reference definition of that subset -- dict, list, and exact `str`, `bool`, `int`, `float`, and `None` values, under keys both implementations name the same way -- and `payload_is_supported` in the crate is its native form, used at dispatch so the walk does not cost more than the conversion it guards. Subclasses and non-JSON-native values stay on Python because its isinstance fallbacks classify them in ways the native writer does not reproduce. Tuples also stay on Python: it applies its list-shape rules to them, while the native writer only recognizes lists.
Expand Down
4 changes: 4 additions & 0 deletions lat.md/tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,10 @@ These tests pin low-level XML helper contracts so performance refactors keep the

Common built-in numbers should avoid abstract-class dispatch while `Decimal`, `Fraction`, complex, and custom `Number` implementations remain supported.

### Unique ID generation delegates to make_id

The public unique-ID helper should return the production `make_id` result so its test exercises the real delegation instead of replacing the function under test.

### Date-like values serialize the same in every position

Objects exposing `isoformat` should serialize identically as a root value, a dict value, and a list item, because one classifier decides the kind for all three writers.
Expand Down
54 changes: 16 additions & 38 deletions tests/test_dict2xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,12 +346,10 @@ def test_list_parent_elements(self) -> None:

def test_dict2xml_str_list_header(self) -> None:
"""Test dict2xml_str with list headers."""
from json2xml.dicttoxml import dict2xml_str

item_func = dicttoxml.default_item_func
item = {"frame_color": "red"}
parent = "Bike"
xml_str = dict2xml_str(
xml_str = dicttoxml.dict2xml_str(
attr_type=False,
attr={},
item=item,
Expand Down Expand Up @@ -1144,37 +1142,21 @@ def test_dicttoxml_with_cdata(self) -> None:
result = dicttoxml.dicttoxml(data, cdata=True, attr_type=False, root=False)
assert b"<key><![CDATA[value]]></key>" == result

def test_get_unique_id_with_duplicates(self, monkeypatch: MonkeyPatch) -> None:
"""Test get_unique_id when duplicates are generated."""
import json2xml.dicttoxml as module

call_count = 0
# @lat: [[tests#Tests#XML helper behavior#Unique ID generation delegates to make_id]]
def test_get_unique_id_uses_make_id(self, monkeypatch: MonkeyPatch) -> None:
"""Test get_unique_id delegates to the production make_id helper."""
calls: list[str] = []

def mock_make_id(element: str, start: int = 100000, end: int = 999999) -> str:
nonlocal call_count
call_count += 1
if call_count == 1:
return "test_123456"
return "test_789012"

def patched_get_unique_id(element: str) -> str:
ids = ["test_123456"]
this_id = module.make_id(element)
dup = True
while dup:
if this_id not in ids:
dup = False
ids.append(this_id)
else:
this_id = module.make_id(element)
return ids[-1]

monkeypatch.setattr(module, "make_id", mock_make_id)
monkeypatch.setattr(module, "get_unique_id", patched_get_unique_id)
calls.append(element)
return "test_123456"

monkeypatch.setattr(dicttoxml, "make_id", mock_make_id)

result = dicttoxml.get_unique_id("test")
assert result == "test_789012"
assert call_count == 2

assert result == "test_123456"
assert calls == ["test"]

def test_convert_with_bool_direct(self) -> None:
"""Test convert function with boolean input directly."""
Expand Down Expand Up @@ -1416,16 +1398,14 @@ def test_dict2xml_str_with_primitive_dict_rawitem(
self, monkeypatch: MonkeyPatch
) -> None:
"""Test dict2xml_str with primitive dict as rawitem to trigger line 274."""
import json2xml.dicttoxml as module

original_is_primitive = module.is_primitive_type
original_is_primitive = dicttoxml.is_primitive_type

def mock_is_primitive(val: Any) -> bool:
if isinstance(val, dict) and val == {"test": "data"}:
return True
return original_is_primitive(val)

monkeypatch.setattr(module, "is_primitive_type", mock_is_primitive)
monkeypatch.setattr(dicttoxml, "is_primitive_type", mock_is_primitive)
item = {"@val": {"test": "data"}}
result = dicttoxml.dict2xml_str(
attr_type=False,
Expand Down Expand Up @@ -1522,21 +1502,19 @@ def test_attrs_empty_and_none_values(self) -> None:

def test_make_attrstring_function_directly(self) -> None:
"""Test the make_attrstring function directly."""
from json2xml.dicttoxml import make_attrstring

# Test basic escaping
attrs = {
"test": "value <here>",
"ampersand": "Tom & Jerry",
"quotes": 'Say "hello"',
}
result = make_attrstring(attrs)
result = dicttoxml.make_attrstring(attrs)

assert 'test="value &lt;here&gt;"' in result
assert 'ampersand="Tom &amp; Jerry"' in result
assert 'quotes="Say &quot;hello&quot;"' in result

# Test empty attributes
empty_attrs: dict[str, Any] = {}
result = make_attrstring(empty_attrs)
result = dicttoxml.make_attrstring(empty_attrs)
assert result == ""
7 changes: 3 additions & 4 deletions tests/test_dicttoxml_fast_fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import json2xml.dicttoxml_fast as fast_module
from json2xml import dicttoxml as py_dicttoxml
from json2xml.backend_selector import rust_renders_identically
from json2xml.dicttoxml_fast import _RustBindings


def _gate_with_limits(
Expand All @@ -23,7 +22,7 @@ def _gate_with_limits(
return rust_renders_identically(obj)


def _fake_bindings(**overrides: Any) -> _RustBindings:
def _fake_bindings(**overrides: Any) -> fast_module._RustBindings:
"""Build Rust bindings backed by the Python reference implementations."""
fields: dict[str, Any] = {
"dicttoxml": Mock(return_value=b"<rust/>"),
Expand All @@ -33,7 +32,7 @@ def _fake_bindings(**overrides: Any) -> _RustBindings:
"enforces_limits": True,
}
fields.update(overrides)
return _RustBindings(**fields)
return fast_module._RustBindings(**fields)


def _force_rust_backend(monkeypatch: pytest.MonkeyPatch) -> Mock:
Expand Down Expand Up @@ -98,7 +97,7 @@ def test_loader_binds_a_complete_extension(monkeypatch: pytest.MonkeyPatch) -> N

bindings = fast_module._load_rust_bindings()

assert bindings == _RustBindings(
assert bindings == fast_module._RustBindings(
dicttoxml=_COMPLETE_EXPORTS["dicttoxml"],
payload_is_supported=rust_renders_identically,
escape_xml=py_dicttoxml.escape_xml,
Expand Down
3 changes: 3 additions & 0 deletions tests/test_dicttoxml_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@


class CustomNumber(numbers.Number):
def __eq__(self, other: object) -> bool:
return isinstance(other, CustomNumber)

def __hash__(self) -> int:
return 7

Expand Down
5 changes: 5 additions & 0 deletions tests/test_missing_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ def __init__(self, value: float) -> None:
def __str__(self) -> str:
return str(self.value)

def __eq__(self, other: object) -> bool:
if not isinstance(other, CustomNumber):
return NotImplemented
return self.value == other.value

def __hash__(self) -> int:
return hash(self.value)

Expand Down
21 changes: 7 additions & 14 deletions tests/test_rust_dicttoxml.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,13 @@
from json2xml import dicttoxml as py_dicttoxml
from json2xml.backend_selector import rust_renders_identically
from json2xml.dicttoxml import SerializerConfig, default_item_func
from json2xml.dicttoxml_fast import (
_RustBackendAdapter,
get_backend,
is_rust_available,
)
from json2xml.dicttoxml_fast import (
dicttoxml as fast_dicttoxml,
)
from json2xml.dicttoxml_fast import (
escape_xml as fast_escape_xml,
)
from json2xml.dicttoxml_fast import (
wrap_cdata as fast_wrap_cdata,
)

_RustBackendAdapter = fast_module._RustBackendAdapter
get_backend = fast_module.get_backend
is_rust_available = fast_module.is_rust_available
fast_dicttoxml = fast_module.dicttoxml
fast_escape_xml = fast_module.escape_xml
fast_wrap_cdata = fast_module.wrap_cdata

# Skip all tests if Rust is not available
pytestmark = pytest.mark.skipif(
Expand Down
Loading