Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
Binary file modified .coverage
Binary file not shown.
44 changes: 36 additions & 8 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
name: Json2xml

on: [push, pull_request]
on:
push:
branches: [main, master]
paths-ignore:
- 'docs/**'
- '*.md'
- '*.rst'
pull_request:
paths-ignore:
- 'docs/**'
- '*.md'
- '*.rst'

permissions:
contents: read
Expand All @@ -25,12 +36,19 @@ jobs:
macos-latest,
]
include:
# Add exact version 3.14.0-alpha.0 for ubuntu-latest only
# Add exact version 3.14.0-beta.1 for ubuntu-latest only
- python-version: 3.14.0-beta.1
tox-python-version: py314-full
os: ubuntu-latest
# Add special linting and type-checking jobs
- python-version: '3.12'
tox-python-version: lint
os: ubuntu-latest
- python-version: '3.12'
tox-python-version: typecheck
os: ubuntu-latest
exclude:
# Exclude other OSes with Python 3.14.0-alpha.0
# Exclude other OSes with Python 3.14.0-beta.1
- python-version: 3.14.0-beta.1
tox-python-version: py314-full
os: windows-latest
Expand All @@ -48,25 +66,35 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
cache: 'pip'
cache-dependency-path: |
requirements*.txt
requirements-dev.in

- name: install uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: requirements-dev.txt
cache-dependency-glob: requirements*.txt

- name: Install dependencies
run: uv pip install --system tox tox-uv
run: |
uv pip install --system tox tox-uv
uv pip install --system pytest pytest-xdist pytest-cov

- name: Run tox targets for ${{ matrix.python-version }}
run: tox -e ${{matrix.tox-python-version}}
run: tox -e ${{matrix.tox-python-version}} --parallel auto
env:
PYTHONPATH: ${{ github.workspace }}

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
if: success() && matrix.tox-python-version != 'lint' && matrix.tox-python-version != 'typecheck'
with:
directory: ./coverage/reports/
env_vars: OS,PYTHON
fail_ci_if_error: true
files: ./coverage.xml,./coverage2.xml,!./cache
files: ./coverage.xml
flags: unittests
token: ${{ secrets.CODECOV_TOKEN }}
name: codecov-umbrella
Expand Down
28 changes: 19 additions & 9 deletions json2xml/dicttoxml.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def get_unique_id(element: str) -> str:
float,
bool,
numbers.Number,
Sequence[str],
Sequence[Any],
datetime.datetime,
datetime.date,
None,
Expand Down Expand Up @@ -97,7 +97,7 @@ def get_xml_type(val: ELEMENT) -> str:
return type(val).__name__


def escape_xml(s: str | numbers.Number) -> str:
def escape_xml(s: str | int | float | numbers.Number) -> str:
"""
Escape a string for use in XML.

Expand Down Expand Up @@ -178,7 +178,7 @@ def make_valid_xml_name(key: str, attr: dict[str, Any]) -> tuple[str, dict[str,
return key, attr


def wrap_cdata(s: str | numbers.Number) -> str:
def wrap_cdata(s: str | int | float | numbers.Number) -> str:
"""Wraps a string into CDATA sections"""
s = str(s).replace("]]>", "]]]]><![CDATA[>")
return "<![CDATA[" + s + "]]>"
Expand Down Expand Up @@ -505,24 +505,32 @@ def convert_list(

def convert_kv(
key: str,
val: str | numbers.Number,
val: str | int | float | numbers.Number | datetime.datetime | datetime.date,
Copy link

Copilot AI Jun 10, 2025

Choose a reason for hiding this comment

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

[nitpick] The union type for 'val' explicitly lists int and float alongside numbers.Number, which already covers these types. Consider removing the redundant types for clarity, unless the explicitness is needed.

Suggested change
val: str | int | float | numbers.Number | datetime.datetime | datetime.date,
val: str | numbers.Number | datetime.datetime | datetime.date,

Copilot uses AI. Check for mistakes.

attr_type: bool,
attr: dict[str, Any] = {},
attr: dict[str, Any] | None = None,
cdata: bool = False,
) -> str:
"""Converts a number or string into an XML element"""
"""Converts a number, string, or datetime into an XML element"""
if attr is None:
attr = {}
key, attr = make_valid_xml_name(key, attr)

# Convert datetime to isoformat string
if hasattr(val, "isoformat") and isinstance(val, (datetime.datetime, datetime.date)):
val = val.isoformat()

if attr_type:
attr["type"] = get_xml_type(val)
attr_string = make_attrstring(attr)
return f"<{key}{attr_string}>{wrap_cdata(val) if cdata else escape_xml(val)}</{key}>"


def convert_bool(
key: str, val: bool, attr_type: bool, attr: dict[str, Any] = {}, cdata: bool = False
key: str, val: bool, attr_type: bool, attr: dict[str, Any] | None = None, cdata: bool = False
) -> str:
"""Converts a boolean into an XML element"""
if attr is None:
attr = {}
key, attr = make_valid_xml_name(key, attr)

if attr_type:
Expand All @@ -532,9 +540,11 @@ def convert_bool(


def convert_none(
key: str, attr_type: bool, attr: dict[str, Any] = {}, cdata: bool = False
key: str, attr_type: bool, attr: dict[str, Any] | None = None, cdata: bool = False
) -> str:
"""Converts a null value into an XML element"""
if attr is None:
attr = {}
key, attr = make_valid_xml_name(key, attr)

if attr_type:
Expand All @@ -544,7 +554,7 @@ def convert_none(


def dicttoxml(
obj: dict[str, Any],
obj: ELEMENT,
root: bool = True,
custom_root: str = "root",
ids: list[int] | None = None,
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ test = [

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
xvs = true
addopts = "--cov=json2xml --cov-report=xml:coverage/reports/coverage.xml --cov-report=term"
[tool.ruff]
exclude = [
".env",
Expand Down
85 changes: 85 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Pytest configuration for json2xml tests."""
from __future__ import annotations

import json
import os
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union

import pytest

if TYPE_CHECKING:
from _pytest.capture import CaptureFixture
from _pytest.fixtures import FixtureRequest
from _pytest.logging import LogCaptureFixture
from _pytest.monkeypatch import MonkeyPatch
from pytest_mock.plugin import MockerFixture


@pytest.fixture
def sample_json_string() -> str:
"""Return a sample JSON string for testing.
Returns:
str: A sample JSON string
"""
return '{"login":"mojombo","id":1,"avatar_url":"https://avatars0.githubusercontent.com/u/1?v=4"}'


@pytest.fixture
def sample_json_dict() -> Dict[str, Any]:
"""Return a sample JSON dictionary for testing.
Returns:
Dict[str, Any]: A sample JSON dictionary
"""
return {
"login": "mojombo",
"id": 1,
"avatar_url": "https://avatars0.githubusercontent.com/u/1?v=4",
}


@pytest.fixture
def sample_json_list() -> List[Dict[str, Any]]:
"""Return a sample JSON list for testing.
Returns:
List[Dict[str, Any]]: A sample list of JSON dictionaries
"""
return [
{
"login": "mojombo",
"id": 1,
"avatar_url": "https://avatars0.githubusercontent.com/u/1?v=4",
},
{
"login": "defunkt",
"id": 2,
"avatar_url": "https://avatars0.githubusercontent.com/u/2?v=4",
},
]


@pytest.fixture
def sample_json_file(tmp_path: Path) -> Path:
"""Create a sample JSON file for testing.
Args:
tmp_path (Path): Pytest temporary path fixture
Returns:
Path: Path to the created JSON file
"""
file_path = tmp_path / "sample.json"

data = {
"login": "mojombo",
"id": 1,
"avatar_url": "https://avatars0.githubusercontent.com/u/1?v=4",
}

with open(file_path, "w") as f:
json.dump(data, f)

return file_path
Loading
Loading