Skip to content

Commit 9086144

Browse files
authored
feat: Add pathlib.Path normalization to string representation (#29)
* feat: add pathlib.Path normalization to string representation * refactor: remove redundant imports in path normalization tests * refactor: remove bytes/bytearray normalization from documentation
1 parent 559dada commit 9086144

2 files changed

Lines changed: 77 additions & 0 deletions

File tree

src/toon_format/normalize.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- datetime/date → ISO 8601 strings
77
- Decimal → float
88
- tuple/set/frozenset → sorted lists
9+
- pathlib.Path → string representation
910
- Infinity/NaN → null
1011
- Functions/callables → null
1112
- Negative zero → zero
@@ -16,6 +17,7 @@
1617
from collections.abc import Mapping
1718
from datetime import date, datetime
1819
from decimal import Decimal
20+
from pathlib import PurePath
1921
from typing import Any
2022

2123
# TypeGuard was added in Python 3.10, use typing_extensions for older versions
@@ -39,6 +41,7 @@ def normalize_value(value: Any) -> JsonValue:
3941
Converts Python-specific types to JSON-compatible equivalents:
4042
- datetime objects → ISO 8601 strings
4143
- sets → sorted lists
44+
- pathlib.Path → string representation
4245
- Large integers (>2^53-1) → strings (for JS compatibility)
4346
- Non-finite floats (inf, -inf, NaN) → null
4447
- Negative zero → positive zero
@@ -64,10 +67,15 @@ def normalize_value(value: Any) -> JsonValue:
6467
>>> normalize_value(2**60) # Large integer
6568
'1152921504606846976'
6669
70+
>>> from pathlib import Path
71+
>>> normalize_value(Path('/tmp/file.txt'))
72+
'/tmp/file.txt'
73+
6774
Note:
6875
- Recursive: normalizes nested structures
6976
- Sets are sorted for deterministic output
7077
- Heterogeneous sets sorted by repr() if natural sorting fails
78+
- Path objects are converted to their string representation
7179
"""
7280
if value is None:
7381
return None
@@ -100,6 +108,11 @@ def normalize_value(value: Any) -> JsonValue:
100108
return None
101109
return float(value)
102110

111+
# Handle pathlib.Path objects -> string representation
112+
if isinstance(value, PurePath):
113+
logger.debug(f"Converting {type(value).__name__} to string: {value}")
114+
return str(value)
115+
103116
if isinstance(value, datetime):
104117
try:
105118
result = value.isoformat()

tests/test_normalization.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"""
1818

1919
from decimal import Decimal
20+
from pathlib import Path, PurePosixPath, PureWindowsPath
2021

2122
from toon_format import decode, encode
2223

@@ -416,3 +417,66 @@ def test_roundtrip_numeric_precision(self):
416417
# All numbers should round-trip with fidelity
417418
for key, value in original.items():
418419
assert decoded[key] == value, f"Mismatch for {key}: {decoded[key]} != {value}"
420+
421+
422+
class TestPathNormalization:
423+
"""Test pathlib.Path normalization to strings."""
424+
425+
def test_path_to_string(self):
426+
"""pathlib.Path should be converted to string."""
427+
data = {"file": Path("/tmp/test.txt")}
428+
result = encode(data)
429+
decoded = decode(result)
430+
431+
assert decoded["file"] == "/tmp/test.txt"
432+
433+
def test_relative_path(self):
434+
"""Relative paths should be preserved."""
435+
data = {"rel": Path("./relative/path.txt")}
436+
result = encode(data)
437+
decoded = decode(result)
438+
439+
# Path normalization may vary, but should be a string
440+
assert isinstance(decoded["rel"], str)
441+
assert "relative" in decoded["rel"]
442+
assert "path.txt" in decoded["rel"]
443+
444+
def test_pure_path(self):
445+
"""PurePath objects should also be normalized."""
446+
data = {
447+
"posix": PurePosixPath("/usr/bin/python"),
448+
"windows": PureWindowsPath("C:\\Windows\\System32"),
449+
}
450+
result = encode(data)
451+
decoded = decode(result)
452+
453+
assert decoded["posix"] == "/usr/bin/python"
454+
assert decoded["windows"] == "C:\\Windows\\System32"
455+
456+
def test_path_in_array(self):
457+
"""Path objects in arrays should be normalized."""
458+
data = {"paths": [Path("/tmp/a"), Path("/tmp/b"), Path("/tmp/c")]}
459+
result = encode(data)
460+
decoded = decode(result)
461+
462+
assert decoded["paths"] == ["/tmp/a", "/tmp/b", "/tmp/c"]
463+
464+
def test_path_in_nested_structure(self):
465+
"""Path objects in nested structures should be normalized."""
466+
data = {
467+
"project": {
468+
"root": Path("/home/user/project"),
469+
"src": Path("/home/user/project/src"),
470+
"files": [
471+
{"name": "main.py", "path": Path("/home/user/project/src/main.py")},
472+
{"name": "test.py", "path": Path("/home/user/project/src/test.py")},
473+
],
474+
}
475+
}
476+
result = encode(data)
477+
decoded = decode(result)
478+
479+
assert decoded["project"]["root"] == "/home/user/project"
480+
assert decoded["project"]["src"] == "/home/user/project/src"
481+
assert decoded["project"]["files"][0]["path"] == "/home/user/project/src/main.py"
482+
assert decoded["project"]["files"][1]["path"] == "/home/user/project/src/test.py"

0 commit comments

Comments
 (0)