Skip to content

Path model

RemotePath normalization, properties, validation, and the / operator.

"""Path model — `RemotePath` normalization, properties, validation, and the `/` operator.

Demonstrates:
- Path normalization rules (backslash, dots, slashes, trailing slashes)
- RemotePath properties: name, parent, parts, suffix
- The / operator for joining paths
- RemotePath.ROOT sentinel for root folder identity
- InvalidPath exceptions (double-dot, null byte, empty path)
- RemotePath immutability
- FileInfo.path is a RemotePath (working with listing results)
"""

from __future__ import annotations

from typing import Any

from remote_store import InvalidPath, RemotePath, Store
from remote_store.backends import MemoryBackend


def demo(store: Store) -> dict[str, Any]:
    """RemotePath construction, normalization, and properties. Returns results dict."""
    results: dict[str, Any] = {}

    # --- Normalization ---
    print("=== Normalization Rules ===\n")

    # Backslash to forward slash
    p1 = RemotePath("data\\reports\\q1.csv")
    results["backslash"] = str(p1)
    print(f"Backslash: 'data\\\\reports\\\\q1.csv' -> '{p1}'")

    # Trailing slash stripped
    p2 = RemotePath("data/reports/")
    results["trailing_slash"] = str(p2)
    print(f"Trailing slash: 'data/reports/' -> '{p2}'")

    # Consecutive slashes collapsed
    p3 = RemotePath("data///reports//q1.csv")
    results["double_slash"] = str(p3)
    print(f"Double slash: 'data///reports//q1.csv' -> '{p3}'")

    # Dot segments removed
    p4 = RemotePath("./data/./reports/./q1.csv")
    results["dot_segments"] = str(p4)
    print(f"Dot segments: './data/./reports/./q1.csv' -> '{p4}'")

    # All normalize to the same path
    all_equal = p1 == p2 / "q1.csv" == p3 == p4
    results["all_equal"] = all_equal
    print(f"All equivalent: {all_equal}")

    # --- Properties ---
    print("\n=== Properties ===\n")

    path = RemotePath("data/reports/quarterly/q1-2026.csv")

    results["name"] = path.name
    results["suffix"] = path.suffix
    results["parts"] = path.parts
    results["parent"] = str(path.parent)

    print(f"Path: '{path}'")
    print(f"  name: '{path.name}'")
    print(f"  suffix: '{path.suffix}'")
    print(f"  parts: {path.parts}")
    print(f"  parent: '{path.parent}'")

    # Parent chain
    current = path
    chain = [str(current)]
    while current.parent is not None:
        current = current.parent
        chain.append(str(current))
    results["parent_chain"] = chain
    print(f"  parent chain: {chain}")

    # No suffix
    no_ext = RemotePath("Makefile")
    results["no_suffix"] = no_ext.suffix
    print(f"\n  '{no_ext}' suffix: '{no_ext.suffix}' (empty)")

    # Single-component path has no parent
    single = RemotePath("file.txt")
    results["single_parent"] = single.parent
    print(f"  '{single}' parent: {single.parent}")

    # --- / operator ---
    print("\n=== / Operator ===\n")

    base = RemotePath("data")
    joined = base / "reports" / "q1.csv"
    results["joined"] = str(joined)
    print(f"RemotePath('data') / 'reports' / 'q1.csv' -> '{joined}'")

    # --- ROOT sentinel ---
    print("\n=== ROOT Sentinel ===\n")

    root = RemotePath.ROOT
    results["root_str"] = str(root)
    results["root_repr"] = repr(root)
    print(f"ROOT: str='{root}', repr={repr(root)}")

    # ROOT is used by get_folder_info for the store root
    store.write("a.txt", b"data")
    info = store.get_folder_info("")
    results["root_folder_path"] = str(info.path)
    is_root = info.path is RemotePath.ROOT
    results["is_root"] = is_root
    print(f"get_folder_info('').path == ROOT: {is_root}")

    # --- InvalidPath exceptions ---
    print("\n=== InvalidPath Exceptions ===\n")

    invalid_cases = {
        "double_dot": "../escape",
        "null_byte": "file\x00name.txt",
        "empty": "",
        "just_dots": "././.",
    }

    for label, raw in invalid_cases.items():
        try:
            RemotePath(raw)
            results[label] = None
        except InvalidPath as exc:
            results[label] = exc
            display = repr(raw) if "\x00" in raw else f"'{raw}'"
            print(f"  {display} -> InvalidPath: {exc.args[0]}")

    # --- Immutability ---
    print("\n=== Immutability ===\n")

    immutable_path = RemotePath("important/file.txt")
    try:
        immutable_path._path = "hacked"  # type: ignore[misc]
    except AttributeError as exc:
        results["immutable"] = True
        print(f"Cannot modify: {exc}")

    # --- FileInfo.path is a RemotePath ---
    print("\n=== FileInfo.path ===\n")

    store.write("docs/readme.md", b"# Readme")
    store.write("docs/guide.md", b"# Guide")
    files = store.list_files("docs")
    for f in sorted(files, key=lambda fi: str(fi.path)):
        print(f"  {repr(f.path)}  name='{f.path.name}'  suffix='{f.path.suffix}'")
    results["fileinfo_is_remotepath"] = all(isinstance(f.path, RemotePath) for f in files)
    print(f"All paths are RemotePath: {results['fileinfo_is_remotepath']}")

    return results


if __name__ == "__main__":
    store = Store(backend=MemoryBackend())
    demo(store)
    print("\nDone!")

See also