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: 1 addition & 1 deletion CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ A reminder for the maintainers on how to deploy.
Make sure all your changes are committed (including an entry in HISTORY.rst).
Then run::

$ bumpversion patch # possible: major / minor / patch
Update the version in pyproject.toml and json2xml/__init__.py
$ git push
$ git push --tags

Expand Down
34 changes: 2 additions & 32 deletions benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@
import json
import os
import random
import string
import subprocess
import sys
import tempfile
import time
from pathlib import Path

from benchmark_utils import Colors, colorize, format_time, random_string

# Base directory for repo-relative defaults
BASE_DIR = Path(__file__).resolve().parent

Expand All @@ -29,27 +30,6 @@
GO_CLI = Path(os.environ.get("JSON2XML_GO_CLI", "json2xml-go"))
EXAMPLES_DIR = Path(os.environ.get("JSON2XML_EXAMPLES_DIR", str(BASE_DIR / "examples")))

# Colors for terminal output
class Colors:
RED = "\033[0;31m"
GREEN = "\033[0;32m"
BLUE = "\033[0;34m"
YELLOW = "\033[1;33m"
CYAN = "\033[0;36m"
BOLD = "\033[1m"
NC = "\033[0m" # No Color


def colorize(text: str, color: str) -> str:
"""Wrap text in color codes."""
return f"{color}{text}{Colors.NC}"


def random_string(length: int = 10) -> str:
"""Generate a random string."""
return "".join(random.choices(string.ascii_letters, k=length))


def generate_large_json(num_records: int = 1000) -> str:
"""Generate a large JSON file for benchmarking."""
data = []
Expand Down Expand Up @@ -119,16 +99,6 @@ def run_benchmark(
}


def format_time(ms: float) -> str:
"""Format time in milliseconds."""
if ms < 1:
return f"{ms * 1000:.2f}µs"
elif ms < 1000:
return f"{ms:.2f}ms"
else:
return f"{ms / 1000:.2f}s"


def print_header(title: str) -> None:
"""Print a section header."""
print(colorize("=" * 50, Colors.BLUE))
Expand Down
30 changes: 1 addition & 29 deletions benchmark_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import os
import random
import shutil
import string
import subprocess
import sys
import tempfile
Expand All @@ -24,6 +23,7 @@
# Add the json2xml module to path
sys.path.insert(0, str(Path(__file__).parent))

from benchmark_utils import Colors, colorize, format_time, random_string
from json2xml import dicttoxml as py_dicttoxml

# Try to import Rust implementation
Expand All @@ -38,25 +38,6 @@
ZIG_AVAILABLE = shutil.which("json2xml-zig") is not None


class Colors:
RED = "\033[0;31m"
GREEN = "\033[0;32m"
BLUE = "\033[0;34m"
YELLOW = "\033[1;33m"
CYAN = "\033[0;36m"
MAGENTA = "\033[0;35m"
BOLD = "\033[1m"
NC = "\033[0m"


def colorize(text: str, color: str) -> str:
return f"{color}{text}{Colors.NC}"


def random_string(length: int = 10) -> str:
return "".join(random.choices(string.ascii_letters, k=length))


def generate_test_data(num_records: int) -> list[dict]:
"""Generate test data with various types."""
data = []
Expand Down Expand Up @@ -151,15 +132,6 @@ def benchmark_cli(cmd: str, json_file: str, iterations: int = 10, warmup: int =
}


def format_time(ms: float) -> str:
if ms < 1:
return f"{ms * 1000:.2f}µs"
elif ms < 1000:
return f"{ms:.2f}ms"
else:
return f"{ms / 1000:.2f}s"


def run_benchmark(name: str, data: dict | list, iterations: int = 10):
"""Run benchmark for all implementations."""
print(colorize(f"\n{'=' * 70}", Colors.BLUE))
Expand Down
35 changes: 2 additions & 33 deletions benchmark_multi_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@
import os
import random
import shutil
import string
import subprocess
import sys
import tempfile
import time
from dataclasses import dataclass
from pathlib import Path

from benchmark_utils import Colors, colorize, format_time, random_string

# Configuration
BASE_DIR = Path(__file__).resolve().parent
VENVS_DIR = BASE_DIR / ".benchmark_venvs"
Expand Down Expand Up @@ -67,28 +68,6 @@ def _uv_python_path(distribution: str, executable: str) -> str:
]


# Colors for terminal output
class Colors:
RED = "\033[0;31m"
GREEN = "\033[0;32m"
BLUE = "\033[0;34m"
YELLOW = "\033[1;33m"
CYAN = "\033[0;36m"
MAGENTA = "\033[0;35m"
BOLD = "\033[1m"
NC = "\033[0m"


def colorize(text: str, color: str) -> str:
"""Wrap text in color codes."""
return f"{color}{text}{Colors.NC}"


def random_string(length: int = 10) -> str:
"""Generate a random string."""
return "".join(random.choices(string.ascii_letters, k=length))


def generate_test_json(num_records: int = 1000) -> str:
"""Generate a JSON file for benchmarking."""
data = []
Expand Down Expand Up @@ -221,16 +200,6 @@ def run_benchmark(
)


def format_time(ms: float) -> str:
"""Format time in milliseconds."""
if ms < 1:
return f"{ms * 1000:.2f}µs"
elif ms < 1000:
return f"{ms:.2f}ms"
else:
return f"{ms / 1000:.2f}s"


def print_header(title: str) -> None:
"""Print a section header."""
print(colorize("=" * 70, Colors.BLUE))
Expand Down
30 changes: 2 additions & 28 deletions benchmark_rust.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@

import json
import random
import string
import sys
import time
from pathlib import Path

from benchmark_utils import Colors, colorize, format_time, random_string

# Add the json2xml module to path
sys.path.insert(0, str(Path(__file__).parent))

Expand All @@ -29,24 +30,6 @@
print("WARNING: Rust extension not built. Run 'cd rust && maturin develop --release'")


class Colors:
RED = "\033[0;31m"
GREEN = "\033[0;32m"
BLUE = "\033[0;34m"
YELLOW = "\033[1;33m"
CYAN = "\033[0;36m"
BOLD = "\033[1m"
NC = "\033[0m"


def colorize(text: str, color: str) -> str:
return f"{color}{text}{Colors.NC}"


def random_string(length: int = 10) -> str:
return "".join(random.choices(string.ascii_letters, k=length))


def generate_test_data(num_records: int) -> list[dict]:
"""Generate test data with various types."""
data = []
Expand Down Expand Up @@ -94,15 +77,6 @@ def benchmark(func, data, iterations: int = 10, warmup: int = 2) -> dict:
}


def format_time(ms: float) -> str:
if ms < 1:
return f"{ms * 1000:.2f}µs"
elif ms < 1000:
return f"{ms:.2f}ms"
else:
return f"{ms / 1000:.2f}s"


def run_benchmark(name: str, data: dict | list, iterations: int = 10):
"""Run and print benchmark for both implementations."""
print(colorize(f"\n--- {name} ---", Colors.BLUE))
Expand Down
38 changes: 38 additions & 0 deletions benchmark_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Shared formatting helpers for the standalone benchmark scripts."""

from __future__ import annotations

import random
import string


class Colors:
"""ANSI colors used by benchmark output."""

RED = "\033[0;31m"
GREEN = "\033[0;32m"
BLUE = "\033[0;34m"
YELLOW = "\033[1;33m"
CYAN = "\033[0;36m"
MAGENTA = "\033[0;35m"
BOLD = "\033[1m"
NC = "\033[0m"


def colorize(text: str, color: str) -> str:
"""Wrap text in ANSI color codes."""
return f"{color}{text}{Colors.NC}"


def random_string(length: int = 10) -> str:
"""Generate a random ASCII string for benchmark payloads."""
return "".join(random.choices(string.ascii_letters, k=length))


def format_time(milliseconds: float) -> str:
"""Format elapsed milliseconds for human-readable benchmark output."""
if milliseconds < 1:
return f"{milliseconds * 1000:.2f}µs"
if milliseconds < 1000:
return f"{milliseconds:.2f}ms"
return f"{milliseconds / 1000:.2f}s"
15 changes: 6 additions & 9 deletions json2xml/backend_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,13 @@ def render(self, request: ConversionRequest) -> bytes:
def has_special_keys(obj: Any) -> bool:
"""Return True when the payload uses Python-only special key semantics."""
if isinstance(obj, dict):
for key, value in obj.items():
if isinstance(key, str) and (key.startswith("@") or key.endswith("@flat")):
return True
if has_special_keys(value):
return True
return False
return any(
(isinstance(key, str) and (key.startswith("@") or key.endswith("@flat")))
or has_special_keys(value)
for key, value in obj.items()
)

if isinstance(obj, list):
for item in obj:
if has_special_keys(item):
return True
return any(has_special_keys(item) for item in obj)

return False
9 changes: 5 additions & 4 deletions json2xml/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,9 +369,10 @@ def create_parser() -> argparse.ArgumentParser:


# @lat: [[behavior#Input readers]]
def read_input(args: argparse.Namespace) -> JSONValue:
def read_input(args: argparse.Namespace | CLIConversionOptions) -> JSONValue:
"""Read JSON input from the specified source."""
return _APP.read_input(CLIConversionOptions.from_namespace(args))
options = args if isinstance(args, CLIConversionOptions) else CLIConversionOptions.from_namespace(args)
return _APP.read_input(options)


def read_from_stdin() -> JSONValue:
Expand All @@ -388,15 +389,15 @@ def main(argv: list[str] | None = None) -> int:
"""Main entry point for the CLI."""
parser = create_parser()
args = parser.parse_args(argv)
options = CLIConversionOptions.from_namespace(args)

try:
data = read_input(args)
data = read_input(options)
except Exception as error:
print(f"Error reading input: {error}", file=sys.stderr)
return 1

try:
options = CLIConversionOptions.from_namespace(args)
xml_output = _APP.convert(data, options)
write_output(xml_output, options.output)
except Exception as error:
Expand Down
18 changes: 3 additions & 15 deletions json2xml/dicttoxml.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

import datetime
import logging
import numbers
from collections.abc import Callable, Sequence
from dataclasses import dataclass
Expand All @@ -17,8 +16,6 @@
# Create a safe random number generator
_SAFE_RANDOM = SystemRandom()

# Set up logging
LOG = logging.getLogger("dicttoxml")
_XML_ESCAPE_CHARS = frozenset("&\"'<>")


Expand Down Expand Up @@ -54,24 +51,15 @@ def make_id(element: str, start: int = 100000, end: int = 999999) -> str:

def get_unique_id(element: str) -> str:
"""
Generate a unique ID for a given element.
Generate an ID for a given element.

Args:
element (str): The element to generate an ID for.

Returns:
str: The unique ID.
str: The generated ID.
"""
ids: list[str] = [] # initialize list of unique ids
this_id = make_id(element)
dup = True
while dup:
if this_id not in ids:
dup = False
ids.append(this_id)
else: # pragma: no cover
this_id = make_id(element)
return ids[-1]
return make_id(element)


ELEMENT = Union[
Expand Down
Loading
Loading