Skip to content

Fix JSON conversion edge cases and URL reader robustness#284

Merged
vinitkumar merged 9 commits into
masterfrom
fix/json-edge-cases-283
Apr 24, 2026
Merged

Fix JSON conversion edge cases and URL reader robustness#284
vinitkumar merged 9 commits into
masterfrom
fix/json-edge-cases-283

Conversation

@vinitkumar
Copy link
Copy Markdown
Owner

@vinitkumar vinitkumar commented Apr 24, 2026

Summary

Fixes #283.

  • handle falsy JSON values through the public Json2xml API
  • add shared JSONValue typing for readers, CLI, and converter inputs
  • harden URL reads with a timeout and consistent URLReadError wrapping
  • stop mutating @attrs/@val caller data during conversion
  • normalize invalid XML names without double escaping and keep @flat out of scalar/dict tags
  • replace mocked URL reader tests with a local HTTP server
  • update lat.md behavior and test specs
  • remove the invalid pytest xvs config option

Validation

  • make lint
  • make typecheck
  • pytest -q tests
  • make test-rust
  • lat_check

Summary by Sourcery

Harden JSON input handling and XML conversion semantics while aligning tests and documentation with the new behavior.

New Features:

  • Introduce a shared JSONValue type used across readers, the CLI, and the Json2xml entry point for broader JSON input coverage.

Bug Fixes:

  • Ensure falsy JSON values like empty containers, zero, false, and empty strings are still converted through the public Json2xml API instead of being treated as missing.
  • Normalize invalid XML element names without double-escaping the original key and keep @Flat suffixes out of scalar and dict tag names so output stays well-formed.
  • Prevent dicttoxml from mutating caller data when using special @attrs and @Val attributes during conversion.
  • Make URL JSON loading wrap non-200 statuses, network failures, and invalid or undecodable JSON consistently in URLReadError.
  • Ensure file, string, and URL JSON readers wrap underlying IO and decoding errors in the appropriate custom exceptions without leaking raw errors.

Enhancements:

  • Update escape and XML naming helpers to handle None and invalid keys more robustly while preserving original names as metadata.
  • Refine Json2xml.to_xml to distinguish None from other falsy values so legitimate empty inputs are serialized instead of skipped.
  • Clarify behavior documentation around input readers and conversion semantics, including falsy value handling and non-mutating special attributes.

Tests:

  • Replace mocked URL reader tests with a local HTTP server to exercise real HTTP behavior and error wrapping.
  • Add regression tests covering falsy JSON conversions, non-mutating @attrs/@Val handling, invalid XML name normalization, and @Flat suffix behavior.
  • Extend test docs to describe the new input reader expectations and conversion invariants.

Chores:

  • Remove an invalid pytest xvs configuration option from the project settings.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Apr 24, 2026

Reviewer's Guide

Refactors JSON input handling and URL reading to use a shared JSONValue type, harden error handling and timeouts, and fixes XML conversion edge cases around falsy values, invalid names, @attrs/@Val mutation, and @Flat behavior, with tests and docs updated accordingly.

Sequence diagram for hardened readfromurl JSON loading

sequenceDiagram
    actor Caller
    participant utils_readfromurl as utils.readfromurl
    participant http_pool as urllib3.PoolManager

    Caller->>utils_readfromurl: readfromurl(url, params)
    utils_readfromurl->>http_pool: request(GET, url, fields=params, timeout=DEFAULT_URL_TIMEOUT, retries=false)

    alt network_error
        http_pool-->>utils_readfromurl: HTTPError
        utils_readfromurl-->>Caller: raise URLReadError
    else http_status_200
        utils_readfromurl->>utils_readfromurl: decode response.data as utf8
        alt valid_json
            utils_readfromurl-->>Caller: JSONValue
        else invalid_json_or_decode
            utils_readfromurl-->>Caller: raise URLReadError
        end
    else http_status_not_200
        http_pool-->>utils_readfromurl: response(status!=200)
        utils_readfromurl-->>Caller: raise URLReadError
    end
Loading

Class diagram for JSONValue usage and XML conversion helpers

classDiagram
    class JSONValue {
        <<type_alias>>
        dict_str_any
        list_any
        str
        int
        float
        bool
        NoneType
    }

    class Json2xml {
        - data JSONValue
        - wrapper str
        - root bool
        - pretty bool
        - attr_type bool
        - item_wrap bool
        - cdata bool
        - list_headers bool
        + Json2xml(data JSONValue, wrapper str, root bool, pretty bool, attr_type bool, item_wrap bool, cdata bool, list_headers bool)
        + to_xml() Any
    }

    class utils_module {
        + readfromjson(filename str) JSONValue
        + readfromurl(url str, params dict_str_str) JSONValue
        + readfromstring(jsondata object) JSONValue
        + JSONReadError
        + URLReadError
        + StringReadError
    }

    class dicttoxml_module {
        + dicttoxml(data JSONValue, root bool, wrapper str, attr_type bool, item_wrap bool, cdata bool, list_headers bool) bytes
        + get_xml_type(val Any) str
        + escape_xml(s str_int_float_number_none) str
        + make_valid_xml_name(key str, attr dict_str_any) tuple_str_dict
        + dict2xml_str(parent str, item Any, attr dict_str_any, attr_type bool, item_name str, item_func function, cdata bool, list_headers bool) str
        + convert(obj Any, parent str, attr_type bool, item_name str, item_func function, cdata bool, item_wrap bool, list_headers bool) str
        + convert_dict(obj dict_str_any, parent str, attr_type bool, item_func function, cdata bool, item_wrap bool, list_headers bool) str
    }

    Json2xml ..> JSONValue : holds
    Json2xml ..> dicttoxml_module : calls dicttoxml
    utils_module ..> JSONValue : returns
    dicttoxml_module ..> JSONValue : consumes
    dicttoxml_module ..> JSONValue : serializes falsy_values
    dicttoxml_module ..> JSONValue : respects_attrs_and_val
    dicttoxml_module ..> JSONValue : handles_flat_keys
Loading

File-Level Changes

Change Details Files
Harden JSON reader utilities and URL fetching, including consistent error wrapping and timeouts, and broaden accepted JSON types.
  • Introduce a shared JSONValue type and use it as the return type for file, string, stdin, CLI, and URL readers.
  • Update readfromjson to wrap ValueError and OSError in JSONReadError with exception chaining.
  • Rewrite readfromurl to use a module-level urllib3.PoolManager with a bounded Timeout, disable retries, wrap network failures as URLReadError, and distinguish HTTP status errors from JSON/decoding errors.
  • Update readfromstring to enforce string input and wrap JSON decoding errors in StringReadError with chaining.
  • Adjust CLI read_input/read_from_stdin and Json2xml.data typing to depend on JSONValue and treat only None as missing input.
json2xml/types.py
json2xml/utils.py
json2xml/cli.py
json2xml/json2xml.py
lat.md/behavior.md
Fix dict-to-XML conversion semantics for invalid XML names, special attributes (@attrs/@Val), falsy and None values, and @Flat suffix handling without mutating caller data.
  • Extend escape_xml to accept None and normalize handling of primitive and falsy values in dict2xml_str, including None->empty text and bool->lowercase string.
  • Change make_valid_xml_name to avoid pre-escaping keys, handle numeric and space-containing keys via normalization, preserve namespace-style keys where valid, and move the original key into a name attribute without double-escaping.
  • Rework @attrs/@Val handling to copy attribute dicts, avoid in-place mutation of caller data, and construct rawitem without popping from the input mapping.
  • Update convert_dict to detect keys ending with @Flat, strip the suffix for XML element names while preserving flattening behavior for lists via item_name, and ensure scalar/dict keys never include @Flat in tag names.
  • Align tests with the new behavior for invalid names, special attribute immutability, @Flat semantics, and falsy JSON values converting through Json2xml.
json2xml/dicttoxml.py
tests/test_dict2xml.py
tests/test_json2xml.py
lat.md/tests.md
lat.md/architecture.md
Replace mocked URL reader tests with a real local HTTP server and expand coverage of URL error cases.
  • Introduce a JsonTestHandler and json_server pytest fixture that starts a ThreadingHTTPServer serving fixed JSON and error responses.
  • Rewrite TestReadFromUrl tests to use the real HTTP server instead of mocking urllib3.PoolManager, covering success, query params, HTTP error, server error, and invalid JSON responses.
  • Add a dedicated network error test that binds an unused local port and asserts URLReadError wrapping for connection failures.
  • Update URL-to-XML integration test to read from the local json_server instead of a mocked response.
tests/test_utils.py
lat.md/tests.md
Tidy project configuration and docs around behavior and testing.
  • Document new input reader behavior, including bounded URL requests and their error semantics, in behavior.md.
  • Clarify serializer behavior, including that Json2xml treats only None as absent input and that @attrs/@Val do not mutate caller data, in architecture.md.
  • Remove the invalid pytest ini option xvs from pyproject.toml.
lat.md/behavior.md
lat.md/architecture.md
pyproject.toml

Assessment against linked issues

Issue Objective Addressed Explanation
#283 Correct JSON-to-XML behavior and typing: falsy JSON values must serialize through the public Json2xml API; conversion must not mutate caller-provided data (including @attrs/@Val); invalid XML names must be normalized without double escaping; @Flat suffix must not produce malformed XML for scalar or dict keys; and type hints must accurately model supported JSON values.
#283 Improve URL reader robustness and tests: readfromurl must use a reasonable timeout, wrap HTTP status, network, and JSON/decoding failures in URLReadError, and be covered by regression tests using a small local HTTP server instead of mocks; remove the non-standard pytest xvs option.
#283 Update documentation/specs to reflect the new behavior: lat.md behavior and tests descriptions must be updated so that lat check passes and match the new input-reader and conversion behaviors.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 24, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.07%. Comparing base (c7f054e) to head (5076ca3).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #284      +/-   ##
==========================================
+ Coverage   95.93%   96.07%   +0.14%     
==========================================
  Files           5        6       +1     
  Lines         467      484      +17     
==========================================
+ Hits          448      465      +17     
  Misses         19       19              
Flag Coverage Δ
unittests 96.07% <100.00%> (+0.14%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Comment thread json2xml/cli.py
Comment thread json2xml/types.py Fixed
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="tests/test_dict2xml.py" line_range="680-681" />
<code_context>
+    # @lat: [[tests#Conversion behavior#Special attributes do not mutate input]]
</code_context>
<issue_to_address>
**suggestion (testing):** Add explicit tests for @val with None and boolean values to cover new dict2xml_str primitive handling

The new `dict2xml_str` logic adds special handling for `None` and booleans in `rawitem`, but the tests here only cover `@attrs/@val` mutation, invalid XML names, and `@flat`. To cover the new behavior, please also add tests like:

- `{"field": {"@val": None}}` to verify we emit an empty element and don’t raise.
- `@val` set to `True`/`False` to verify we serialize as lowercase `"true"`/`"false"` and that attributes/element names are still correct.

This will better protect against regressions in the new primitive-handling path.

```suggestion
    # @lat: [[tests#Conversion behavior#Special attributes do not mutate input]]

    def test_dict2xml_val_none_emits_empty_element(self) -> None:
        """@val=None should serialize as an empty element without raising."""
        xml = dicttoxml.dict2xml_str({"field": {"@val": None}})

        # We should get a <field> element, but never the string "None"
        assert "<field" in xml
        assert "None" not in xml
        # Accept either self-closing or explicit close-tag forms
        assert (
            "<field/>" in xml
            or "<field />" in xml
            or "<field></field>" in xml
        )

    def test_dict2xml_val_bool_serialization(self) -> None:
        """@val with boolean values should serialize to lowercase true/false."""
        xml_true = dicttoxml.dict2xml_str({"active": {"@val": True}})
        xml_false = dicttoxml.dict2xml_str({"active": {"@val": False}})

        # Booleans should be serialized as lowercase true/false
        assert "<active>true</active>" in xml_true
        assert "<active>false</active>" in xml_false

        # Uppercase Python reprs must not leak into the XML
        assert "True" not in xml_true
        assert "False" not in xml_false

        # Attributes must still be preserved on the element
        xml_with_attr = dicttoxml.dict2xml_str(
            {"active": {"@attrs": {"flag": "yes"}, "@val": True}}
        )
        assert '<active flag="yes">true</active>' in xml_with_attr

    def test_dicttoxml_does_not_mutate_special_attribute_input(self) -> None:
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread tests/test_dict2xml.py
@vinitkumar vinitkumar merged commit 2153b07 into master Apr 24, 2026
62 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix JSON conversion edge cases and URL reader robustness

2 participants