Pathable provides a small set of "path" objects for traversing hierarchical data (mappings, lists, and other subscriptable trees) using a familiar path-like syntax.
It’s especially handy when you want to:
- express deep lookups as a single object (and pass it around)
- build paths incrementally (
p / "a" / 0 / "b") - safely probe (
exists(),get(...)) or strictly require segments (//)
- Intuitive path-based navigation for nested data (e.g., dicts/lists)
- Pluggable accessor layer for custom backends
- Pythonic, chainable API for concise and readable code
- Per-instance (bounded LRU) cached lookup accessor for repeated reads of the same tree
from pathable import LookupPath
data = {
"parts": {
"part1": {"name": "Part One"},
"part2": {"name": "Part Two"},
}
}
root = LookupPath.from_lookup(data)
name = (root / "parts" / "part2" / "name").read_value()
assert name == "Part Two"from pathable import LookupPath
data = {
"parts": {
"part1": {"name": "Part One"},
"part2": {"name": "Part Two"},
}
}
p = LookupPath.from_lookup(data)
# Concatenate path segments with /
parts = p / "parts"
# Check membership (mapping keys or list indexes)
assert "part2" in parts
# Read a value
assert (parts / "part2" / "name").read_value() == "Part Two"
# Iterate children as paths
for child in parts:
print(child, child.read_value())
# Work with keys/items
print(list(parts.keys()))
print({k: v.read_value() for k, v in parts.items()})
# Safe access
print(parts.get("missing", default=None))
# Strict access (raises KeyError if missing)
must_exist = parts // "part2"
# "Open" yields the current value as a context manager
with parts.open() as parts_value:
assert isinstance(parts_value, dict)
# Optional metadata
print(parts.stat())Pathable can also traverse the filesystem via an accessor.
from pathlib import Path
from pathable import FilesystemPath
root_dir = Path(".")
p = FilesystemPath.from_path(root_dir)
readme = p / "README.md"
if readme.exists():
content = readme.read_value() # bytes
print(content[:100])BasePathis a pure path (segments + separator) with/joining.AccessorPathis aBasePathbound to aNodeAccessor, enablingread_value(),exists(),keys(), iteration, etc.FilesystemPathis anAccessorPathspecialized for filesystem objects.LookupPathis anAccessorPathspecialized for mapping/list lookups.
Notes on parsing:
- A segment like
"a/b"is split into parts using the separator. Nonesegments are ignored."."segments are ignored (relative no-op).- Operations like
relative_to()andis_relative_to()also respect the instance separator.
Equality and ordering:
- Two
BasePathinstances are equal if theirpartsare equal. The separator is presentation only —BasePath("a", separator="/") == BasePath("a", separator="."). - Two
AccessorPathinstances are equal if they have equalpartsand their accessors compare equal under the accessor's own__eq__. A plainBasePathis never equal to anAccessorPath. - Path parts are type-sensitive (
0is not equal to"0"). - Ordering is address-based: separator is not part of the order, and it remains deterministic across mixed part types. For
AccessorPath, different bindings with the samepartsmay compare ordering-equivalent while remaining unequal.
Identity and lifecycle:
- Build one accessor per resource and reuse it for every path you derive.
LookupPath.from_lookup(data)constructs a fresh accessor on each call, which is convenient for one-off use but defeats the cache when called repeatedly over the same data:
from pathable import LookupPath
from pathable.accessors import LookupAccessor
# Construct the accessor once, reuse it.
accessor = LookupAccessor(data)
root = LookupPath(accessor)
# Every path derived from `accessor` shares its cache.
a = root / "parts" / "part1"
b = root / "parts" / "part1"
assert a == bpath.is_same_binding(other)is a stricter version of==that additionally requires both paths to share the same accessor instance (object identity), not just an==-equal one. Use it when you need to verify cache attribution or detect accidental accessor swaps.
Lookup caching:
LookupPathuses a per-instance LRU cache (default maxsize: 128) on its accessor.- You can control it via
path.accessor.clear_cache(),path.accessor.disable_cache(), andpath.accessor.enable_cache(maxsize=...). path.accessor.nodeis immutable; to point at a different tree, create a newLookupPath/accessor.
Recommended way (via pip):
pip install pathableAlternatively you can download the code and install from the repository:
pip install -e git+https://github.com/p1c2u/pathable.git#egg=pathableBenchmarks live in tests/benchmarks/ and produce JSON reports.
Local run (recommended as modules):
poetry run python -m tests.benchmarks.bench_parse --output reports/bench-parse.json
poetry run python -m tests.benchmarks.bench_lookup --output reports/bench-lookup.jsonQuick sanity run:
poetry run python -m tests.benchmarks.bench_parse --quick --output reports/bench-parse.quick.json
poetry run python -m tests.benchmarks.bench_lookup --quick --output reports/bench-lookup.quick.jsonCompare two results (fails if candidate is >20% slower in any scenario):
poetry run python -m tests.benchmarks.compare_results \
--baseline reports/bench-before.json \
--candidate reports/bench-after.json \
--tolerance 0.20CI (on-demand):
- GitHub Actions workflow
Benchmarksruns viaworkflow_dispatchand uploads the JSON artifacts.