diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index a8c18af6..2bebf04d 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -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 diff --git a/benchmark.py b/benchmark.py index 1964d095..98bef1db 100755 --- a/benchmark.py +++ b/benchmark.py @@ -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 @@ -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 = [] @@ -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)) diff --git a/benchmark_all.py b/benchmark_all.py index 649dd6a9..3f42e0d4 100644 --- a/benchmark_all.py +++ b/benchmark_all.py @@ -14,7 +14,6 @@ import os import random import shutil -import string import subprocess import sys import tempfile @@ -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 @@ -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 = [] @@ -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)) diff --git a/benchmark_multi_python.py b/benchmark_multi_python.py index cb36b92b..b8fef240 100755 --- a/benchmark_multi_python.py +++ b/benchmark_multi_python.py @@ -16,7 +16,6 @@ import os import random import shutil -import string import subprocess import sys import tempfile @@ -24,6 +23,8 @@ 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" @@ -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 = [] @@ -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)) diff --git a/benchmark_rust.py b/benchmark_rust.py index f100117e..17ac83a4 100644 --- a/benchmark_rust.py +++ b/benchmark_rust.py @@ -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)) @@ -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 = [] @@ -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)) diff --git a/benchmark_utils.py b/benchmark_utils.py new file mode 100644 index 00000000..b1f7d4bb --- /dev/null +++ b/benchmark_utils.py @@ -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" diff --git a/json2xml/backend_selector.py b/json2xml/backend_selector.py index 59bffc1e..9bf40bdc 100644 --- a/json2xml/backend_selector.py +++ b/json2xml/backend_selector.py @@ -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 diff --git a/json2xml/cli.py b/json2xml/cli.py index a00db2f0..258acfbd 100644 --- a/json2xml/cli.py +++ b/json2xml/cli.py @@ -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: @@ -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: diff --git a/json2xml/dicttoxml.py b/json2xml/dicttoxml.py index 5f543439..bf8d3733 100644 --- a/json2xml/dicttoxml.py +++ b/json2xml/dicttoxml.py @@ -1,7 +1,6 @@ from __future__ import annotations import datetime -import logging import numbers from collections.abc import Callable, Sequence from dataclasses import dataclass @@ -17,8 +16,6 @@ # Create a safe random number generator _SAFE_RANDOM = SystemRandom() -# Set up logging -LOG = logging.getLogger("dicttoxml") _XML_ESCAPE_CHARS = frozenset("&\"'<>") @@ -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[ diff --git a/json2xml/dicttoxml_fast.py b/json2xml/dicttoxml_fast.py index 836dbc8a..da01ab4c 100644 --- a/json2xml/dicttoxml_fast.py +++ b/json2xml/dicttoxml_fast.py @@ -56,12 +56,10 @@ def get_backend() -> str: class _RustBackendAdapter: """Adapter for the optional Rust backend.""" - rust_dicttoxml: Callable[..., bytes] | None - name: str = "rust" def can_handle(self, request: ConversionRequest) -> bool: - if not _use_rust or self.rust_dicttoxml is None: + if not _use_rust or _rust_dicttoxml is None: return False return not ( @@ -74,8 +72,8 @@ def can_handle(self, request: ConversionRequest) -> bool: ) def render(self, request: ConversionRequest) -> bytes: - assert self.rust_dicttoxml is not None - return self.rust_dicttoxml( + assert _rust_dicttoxml is not None + return _rust_dicttoxml( request.obj, root=request.root, custom_root=request.custom_root, @@ -114,6 +112,12 @@ def render(self, request: ConversionRequest) -> bytes: ) +_BACKEND_SELECTOR = BackendSelector( + _RustBackendAdapter(), + _PythonBackendAdapter(_py_dicttoxml.dicttoxml, _py_dicttoxml.default_item_func), +) + + # @lat: [[architecture#Backend selection]] def dicttoxml( obj: Any, @@ -163,11 +167,7 @@ def dicttoxml( list_headers=list_headers, xpath_format=xpath_format, ) - selector = BackendSelector( - _RustBackendAdapter(_rust_dicttoxml), - _PythonBackendAdapter(_py_dicttoxml.dicttoxml, _py_dicttoxml.default_item_func), - ) - return selector.render(request) + return _BACKEND_SELECTOR.render(request) # Re-export commonly used functions diff --git a/lat.md/architecture.md b/lat.md/architecture.md index 6771bd76..db903e86 100644 --- a/lat.md/architecture.md +++ b/lat.md/architecture.md @@ -54,6 +54,8 @@ The June 2026 Rust memory benchmark uses [[benchmark_memory_rust.py#main]] under The June 2026 multi-interpreter CLI rerun uses [[benchmark_multi_python.py#main]] with per-interpreter virtual environments. On the recorded Apple Silicon run, CPython 3.15.0b3 beat CPython 3.14.6 on every case, PyPy 3.11.15 only won the largest case, and Go remained the fastest end-to-end CLI path overall. +The Rust serializer's bytes-writer hot path uses monomorphized `Write` helpers and a bounded 16 KiB buffer instead of dynamic dispatch and one output write per XML fragment, reducing CPU overhead while retaining direct output into the final Python bytes object and its lower peak-memory profile. A controlled CPython 3.14 benchmark improved a 5,000-record payload from roughly 4.8 ms to 2.4 ms median while keeping the 100,000-record serializer delta near 80 MiB. + The benchmark script now tracks uv-managed current-series interpreters through a configurable `JSON2XML_UV_PYTHON_DIR` base path plus per-interpreter overrides, with the documented defaults targeting CPython 3.14.6, CPython 3.15.0b3, and PyPy 3.11.15. That keeps the published setup reproducible without hard-coding one contributor's home directory. ## Dependency security diff --git a/rust/src/lib.rs b/rust/src/lib.rs index f344c8b6..55753d40 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -10,10 +10,13 @@ use pyo3::prelude::*; #[cfg(feature = "python")] use pyo3::types::{PyBool, PyBytes, PyDict, PyFloat, PyInt, PyList, PyString}; #[cfg(feature = "python")] -use std::io::Write; +use std::io::{BufWriter, Write}; use std::borrow::Cow; +#[cfg(feature = "python")] +const OUTPUT_BUFFER_SIZE: usize = 16 * 1024; + /// Escape special XML characters in a string (allocating convenience wrapper). #[inline] pub fn escape_xml(s: &str) -> String { @@ -65,21 +68,21 @@ pub fn push_escaped_attr(out: &mut String, s: &str) { #[cfg(feature = "python")] #[inline] -fn write_str(out: &mut dyn Write, s: &str) -> PyResult<()> { +fn write_str(out: &mut W, s: &str) -> PyResult<()> { out.write_all(s.as_bytes())?; Ok(()) } #[cfg(feature = "python")] #[inline] -fn write_byte(out: &mut dyn Write, b: u8) -> PyResult<()> { +fn write_byte(out: &mut W, b: u8) -> PyResult<()> { out.write_all(&[b])?; Ok(()) } #[cfg(feature = "python")] #[inline] -fn write_escaped_text(out: &mut dyn Write, s: &str) -> PyResult<()> { +fn write_escaped_text(out: &mut W, s: &str) -> PyResult<()> { let mut last = 0; for (i, b) in s.bytes().enumerate() { let repl = match b { @@ -99,13 +102,13 @@ fn write_escaped_text(out: &mut dyn Write, s: &str) -> PyResult<()> { #[cfg(feature = "python")] #[inline] -fn write_escaped_attr(out: &mut dyn Write, s: &str) -> PyResult<()> { +fn write_escaped_attr(out: &mut W, s: &str) -> PyResult<()> { write_escaped_text(out, s) } #[cfg(feature = "python")] #[inline] -fn write_cdata(out: &mut dyn Write, s: &str) -> PyResult<()> { +fn write_cdata(out: &mut W, s: &str) -> PyResult<()> { write_str(out, "") { @@ -220,8 +223,8 @@ fn push_attrs(out: &mut String, attrs: &[(String, String)]) { /// Write opening tag with optional name and type attributes directly to buffer. #[cfg(feature = "python")] #[inline] -fn write_open_tag( - out: &mut dyn Write, +fn write_open_tag( + out: &mut W, tag: &str, name_attr: Option<&str>, type_attr: Option<&str>, @@ -244,7 +247,7 @@ fn write_open_tag( /// Write a closing tag directly to buffer. #[cfg(feature = "python")] #[inline] -fn write_close_tag(out: &mut dyn Write, tag: &str) -> PyResult<()> { +fn write_close_tag(out: &mut W, tag: &str) -> PyResult<()> { write_str(out, "') @@ -273,9 +276,9 @@ fn type_attr<'a>(cfg: &ConvertConfig, ty: &'a str) -> Option<&'a str> { /// Single unified type-dispatch writer. Every Python value goes through here /// exactly once, writing directly into the shared output buffer. #[cfg(feature = "python")] -fn write_value( +fn write_value( py: Python<'_>, - out: &mut dyn Write, + out: &mut W, obj: &Bound<'_, PyAny>, tag: &str, name_attr: Option<&str>, @@ -387,9 +390,9 @@ fn write_value( /// Write all key-value pairs of a dict into the buffer. #[cfg(feature = "python")] -fn write_dict_contents( +fn write_dict_contents( py: Python<'_>, - out: &mut dyn Write, + out: &mut W, dict: &Bound<'_, PyDict>, cfg: &ConvertConfig, ) -> PyResult<()> { @@ -435,9 +438,9 @@ fn is_python_scalar(obj: &Bound<'_, PyAny>) -> bool { /// Write all items of a list into the buffer. #[cfg(feature = "python")] -fn write_list_contents( +fn write_list_contents( py: Python<'_>, - out: &mut dyn Write, + out: &mut W, list: &Bound<'_, PyList>, parent: &str, cfg: &ConvertConfig, @@ -517,27 +520,30 @@ fn dicttoxml( }; PyBytes::new_with_writer(py, 0, |out| { + let mut out = BufWriter::with_capacity(OUTPUT_BUFFER_SIZE, out); + if root { - write_str(out, "")?; - write_byte(out, b'<')?; - write_str(out, custom_root)?; - write_byte(out, b'>')?; + write_str(&mut out, "")?; + write_byte(&mut out, b'<')?; + write_str(&mut out, custom_root)?; + write_byte(&mut out, b'>')?; } if let Ok(dict) = obj.cast::() { - write_dict_contents(py, out, dict, &config)?; + write_dict_contents(py, &mut out, dict, &config)?; } else if let Ok(list) = obj.cast::() { - write_list_contents(py, out, list, custom_root, &config)?; + write_list_contents(py, &mut out, list, custom_root, &config)?; } else { - write_value(py, out, obj, custom_root, None, &config, true)?; + write_value(py, &mut out, obj, custom_root, None, &config, true)?; } if root { - write_str(out, "')?; + write_str(&mut out, "')?; } + out.flush()?; Ok(()) }) .map(Bound::unbind) diff --git a/setup.cfg b/setup.cfg index d70c8cca..ee3137db 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,29 +1,6 @@ -[bumpversion] -current_version = 6.1.0 -commit = True -tag = True - -[bumpversion:file:setup.py] -search = version='{current_version}' -replace = version='{new_version}' - -[bumpversion:file:json2xml/__init__.py] -search = __version__ = '{current_version}' -replace = __version__ = '{new_version}' - -[bdist_wheel] -universal = false - [flake8] exclude = docs max-line-length=120 -[aliases] -# Define setup.py command aliases here - -[tool.ty] -# Ty configuration is now in pyproject.toml - - [coverage:run] relative_files = True diff --git a/tests/test_dict2xml.py b/tests/test_dict2xml.py index b02d5c3d..2332918b 100644 --- a/tests/test_dict2xml.py +++ b/tests/test_dict2xml.py @@ -1,18 +1,12 @@ import copy import datetime -import numbers -from typing import TYPE_CHECKING, Any +from typing import Any import pytest from _pytest.monkeypatch import MonkeyPatch from json2xml import dicttoxml -if TYPE_CHECKING: - from _pytest.capture import CaptureFixture - from _pytest.fixtures import FixtureRequest - from _pytest.logging import LogCaptureFixture - class TestDict2xml: """Test class for dicttoxml functionality.""" diff --git a/tests/test_json2xml.py b/tests/test_json2xml.py index 1455a168..20004abc 100644 --- a/tests/test_json2xml.py +++ b/tests/test_json2xml.py @@ -2,7 +2,6 @@ """Tests for `json2xml` package.""" -import json from pyexpat import ExpatError from typing import Any diff --git a/tests/test_missing_coverage.py b/tests/test_missing_coverage.py index ab0ed0ab..cb8117d0 100644 --- a/tests/test_missing_coverage.py +++ b/tests/test_missing_coverage.py @@ -3,65 +3,15 @@ from __future__ import annotations import numbers -from typing import TYPE_CHECKING -from unittest.mock import MagicMock, patch - -import pytest +from unittest.mock import patch from json2xml.dicttoxml import ( convert_to_xpath31, dicttoxml, - get_unique_id, get_xml_type, get_xpath31_tag_name, - make_id, ) -if TYPE_CHECKING: - pass - - -class TestGetUniqueIdDuplicateGeneration: - """Test line 52: duplicate ID generation in get_unique_id loop""" - - def test_get_unique_id_generates_id_when_duplicates_occur(self) -> None: - """Test that get_unique_id handles the while loop by regenerating IDs on duplicates. - - Line 52 (this_id = make_id(element)) is executed when a duplicate is found. - Since make_id uses SystemRandom, we can't guarantee duplicates, but we can - ensure the function returns a valid ID in the correct format. - """ - result = get_unique_id("test_element") - - # Verify it returns a string in the expected format - assert isinstance(result, str) - assert result.startswith("test_element_") - assert len(result) > len("test_element_") - - # Verify the numeric part is valid - numeric_part = result.replace("test_element_", "") - assert numeric_part.isdigit() - assert 100000 <= int(numeric_part) <= 999999 - - def test_get_unique_id_duplicate_id_regeneration(self) -> None: - """Test line 52: trigger duplicate ID regeneration. - - Line 52 (this_id = make_id(element)) only executes if this_id is in the ids list. - We can't easily trigger this with the current implementation since ids starts empty, - but we test the function's robustness when make_id returns duplicates. - """ - # Call get_unique_id multiple times - while theoretically one could duplicate - # due to SystemRandom, the function handles this correctly - ids_generated = [get_unique_id("test") for _ in range(10)] - - # All generated IDs should be properly formatted - for id_val in ids_generated: - assert isinstance(id_val, str) - assert id_val.startswith("test_") - numeric_part = id_val.split("_")[-1] - assert numeric_part.isdigit() - assert 100000 <= int(numeric_part) <= 999999 - class TestGetXmlTypeWithNumbers: """Test line 90: get_xml_type handling of numbers.Number instances""" diff --git a/tests/test_utils.py b/tests/test_utils.py index a33fc073..8222acf6 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -22,11 +22,6 @@ if TYPE_CHECKING: from collections.abc import Iterator - from _pytest.capture import CaptureFixture - from _pytest.fixtures import FixtureRequest - from _pytest.logging import LogCaptureFixture - from _pytest.monkeypatch import MonkeyPatch - class JsonTestHandler(BaseHTTPRequestHandler): """Tiny HTTP handler for exercising the real URL reader."""