Path Model Specification¶
Overview¶
RemotePath is an immutable value object representing a normalized, validated path within a remote store. It enforces safety invariants and provides path manipulation utilities.
PATH-001: Immutability¶
Invariant: A RemotePath instance cannot be modified after construction.
Postconditions: All attributes are read-only. Attempting to set attributes raises AttributeError.
PATH-002: Backslash Normalization¶
Invariant: Backslash characters (\) are converted to forward slashes (/) during normalization.
Example:
PATH-003: Double-Dot Rejection¶
Invariant: A path segment exactly equal to .. is rejected. A segment that merely contains .. as a substring (e.g. a filename like a..b) is allowed.
Raises: InvalidPath
Example:
PATH-004: Leading and Trailing Slash Stripping¶
Invariant: Leading and trailing / characters are stripped from the normalized path.
Example:
PATH-005: Consecutive Slash Collapsing¶
Invariant: Consecutive / characters are collapsed to a single /.
Example:
PATH-006: Dot Segment Removal¶
Invariant: Single-dot (.) segments are removed during normalization.
Example:
PATH-007: Null Byte Rejection¶
Invariant: Paths containing null bytes (\0) are rejected.
Raises: InvalidPath
PATH-008: Empty Path Rejection¶
Invariant: A path that normalizes to an empty string is rejected.
Raises: InvalidPath
Example:
with pytest.raises(InvalidPath):
RemotePath("")
with pytest.raises(InvalidPath):
RemotePath("/")
with pytest.raises(InvalidPath):
RemotePath(".")
PATH-009: Name Property¶
Invariant: name returns the final component of the path.
Example:
PATH-010: Parent Property¶
Invariant: parent returns the parent RemotePath, or None for a single-component path.
Example:
PATH-011: Parts Property¶
Invariant: parts returns a tuple of path components.
Example:
PATH-012: Join Operator¶
Invariant: The / operator joins a RemotePath with a string to produce a new RemotePath.
Example:
PATH-013: Equality and Hashing¶
Invariant: Equality and hashing are based on the normalized path string. Example:
assert RemotePath("a/b") == RemotePath("a//b")
assert hash(RemotePath("a/b")) == hash(RemotePath("a//b"))
PATH-014: Suffix Property¶
Invariant: suffix returns the file extension (including the dot), or empty string if none.
Example:
PATH-015: Root Sentinel¶
Invariant: RemotePath.ROOT is a class-level singleton sentinel representing the root folder.
It bypasses __init__ validation (PATH-008 still rejects RemotePath("") and RemotePath(".")).
Properties:
- str(ROOT) returns ".".
- ROOT.name returns ".".
- ROOT.parent returns None.
- ROOT.parts returns (".",).
- ROOT.suffix returns "".
- ROOT / "a" produces RemotePath("a") (join strips the dot prefix).
- ROOT is immutable: __setattr__ and __delattr__ raise AttributeError.
- ROOT is a singleton: RemotePath.ROOT is RemotePath.ROOT.
Round-trip: Store methods that accept a folder path (get_folder_info, list_files, etc.)
accept "." as a root alias, so str(folder_info.path) can be fed back into Store methods.
Example:
fi = store.get_folder_info("")
assert fi.path is RemotePath.ROOT
assert str(fi.path) == "."
# Round-trip: "." works as input
fi2 = store.get_folder_info(str(fi.path))
assert fi2.path is RemotePath.ROOT
PATH-016: as_posix Method¶
Invariant: as_posix() is a method (called with parentheses, matching
pathlib.PurePath.as_posix) that returns the forward-slash path string.
Because RemotePath always stores forward slashes, the result equals
str(self) on every platform.
Postconditions: RemotePath("a/b").as_posix() == "a/b";
RemotePath.ROOT.as_posix() == ".".
Example:
PATH-017: Not os.PathLike¶
Invariant: RemotePath deliberately does not implement __fspath__
and is therefore not an os.PathLike. RemotePath is a remote-store key,
not a local filesystem path; implementing __fspath__ would let it be passed
silently to open(), os.path.*, and similar APIs that operate on the local
filesystem, masking a category error. Callers that need a string use
as_posix() (PATH-016) or str().
Postconditions: isinstance(RemotePath("a"), os.PathLike) is False;
os.fspath(RemotePath("a")) raises TypeError.
Example:
pathlib parity scope (informative)¶
RemotePath is a deliberately minimal subset of pathlib.PurePath, not a
drop-in replacement. Beyond as_posix() (PATH-016), the following PurePath
members are intentionally absent and tracked as deferred parity candidates:
stem, suffixes, with_name, with_suffix, with_stem, joinpath,
parents, match, relative_to / is_relative_to, and is_absolute. The
as_uri and drive / root / anchor accessors are out of scope entirely
(meaningless for a rootless remote key). See the backlog for the follow-up
that may close the deferred candidates.