Skip to content
Closed
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
142 changes: 104 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,63 +1,129 @@
# pathable

<a href="https://pypi.python.org/pypi/pathable" target="_blank">
<img src="https://img.shields.io/pypi/v/pathable.svg" alt="Package version">
</a>
<a href="https://travis-ci.org/p1c2u/pathable" target="_blank">
<img src="https://travis-ci.org/p1c2u/pathable.svg?branch=master" alt="Continuous Integration">
</a>
<a href="https://codecov.io/github/p1c2u/pathable?branch=master" target="_blank">
<img src="https://img.shields.io/codecov/c/github/p1c2u/pathable/master.svg?style=flat" alt="Tests coverage">
</a>
<a href="https://pypi.python.org/pypi/pathable" target="_blank">
<img src="https://img.shields.io/pypi/pyversions/pathable.svg" alt="Python versions">
</a>
<a href="https://pypi.python.org/pypi/pathable" target="_blank">
<img src="https://img.shields.io/pypi/format/pathable.svg" alt="Package format">
</a>
<a href="https://pypi.python.org/pypi/pathable" target="_blank">
<img src="https://img.shields.io/pypi/status/pathable.svg" alt="Development status">
</a>
[![Package version](https://img.shields.io/pypi/v/pathable.svg)](https://pypi.org/project/pathable/)
[![Python versions](https://img.shields.io/pypi/pyversions/pathable.svg)](https://pypi.org/project/pathable/)
[![License](https://img.shields.io/pypi/l/pathable.svg)](https://pypi.org/project/pathable/)

## About

Pathable provides a flexible, object-oriented interface for traversing and manipulating hierarchical data structures (such as lists or dictionaries) using path-like syntax. It enables intuitive navigation, access, and modification of nested resources in Python.
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 (`//`)

## Key features

* Intuitive path-based navigation for nested data (e.g., lists, dicts)
* Pluggable accessor layer for custom data sources or backends
* 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
* Cached lookup accessor for repeated reads of the same tree

## Quickstart

```python
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"
```

## Usage

```python
from pathable import DictPath
from pathable import LookupPath

d = {
data = {
"parts": {
"part1": {
"name": "Part One",
},
"part2": {
"name": "Part Two",
},
},
"part1": {"name": "Part One"},
"part2": {"name": "Part Two"},
}
}

dp = DictPath(d)
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))

# Concatenate paths with /
parts = dp / "parts"
# Strict access (raises KeyError if missing)
must_exist = parts // "part2"

# Stat path keys
"part2" in parts
# "Open" yields the current value as a context manager
with parts.open() as parts_value:
assert isinstance(parts_value, dict)

# Open path dict
with parts.open() as parts_dict:
print(parts_dict)
# Optional metadata
print(parts.stat())
```

## Filesystem example

Pathable can also traverse the filesystem via an accessor.

```python
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])
```

## Core concepts

* `BasePath` is a pure path (segments + separator) with `/` joining.
* `AccessorPath` is a `BasePath` bound to a `NodeAccessor`, enabling `read_value()`, `exists()`, `keys()`, iteration, etc.
* `FilesystemPath` is an `AccessorPath` specialized for filesystem objects.
* `LookupPath` is an `AccessorPath` specialized for mapping/list lookups.

Notes on parsing:

* A segment like `"a/b"` is split into parts using the separator.
* `None` segments are ignored.
* `"."` segments are ignored (relative no-op).

## Typing & compatibility

* Python: `>=3.9,<4.0`
* Lookup keys are `str | int` (use `int` for list indexes).

## Performance notes

`LookupPath` uses a cached accessor for reads. If you repeatedly read the same path on the same tree, it will not re-traverse the structure.

## Installation

Recommended way (via pip):
Expand Down
6 changes: 6 additions & 0 deletions pathable/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""Pathable module"""
from pathable.accessors import NodeAccessor
from pathable.accessors import PathAccessor
from pathable.paths import AccessorPath
from pathable.paths import BasePath
from pathable.paths import FilesystemPath
from pathable.paths import LookupPath
from pathable.paths import LookupPath as DictPath
from pathable.paths import LookupPath as ListPath
Expand All @@ -14,7 +17,10 @@
__all__ = [
"BasePath",
"AccessorPath",
"FilesystemPath",
"LookupPath",
"DictPath",
"ListPath",
"NodeAccessor",
"PathAccessor",
]
Loading
Loading