Created
January 18, 2026 23:34
-
-
Save lmmx/f5d1b07d266f160f9a431c1f6bdc8a17 to your computer and use it in GitHub Desktop.
Deferred templated pathlib Path-like type with t-strings
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from string.templatelib import Interpolation, Template | |
| from typing import Any | |
| # ============================================================ | |
| # Layer 1: Parameters (late binding slots) | |
| # ============================================================ | |
| @dataclass(frozen=True) | |
| class Param: | |
| """A named slot to be filled at resolution time.""" | |
| name: str | |
| def __truediv__(self, other: PathExpr | str) -> PathExpr: | |
| return ParamExpr(self) / other | |
| def __rtruediv__(self, other: str) -> PathExpr: | |
| return LiteralExpr(other) / ParamExpr(self) | |
| def __repr__(self): | |
| return repr(f"${self.name}") | |
| # ============================================================ | |
| # Layer 2: Path Expressions (composable, deferred) | |
| # ============================================================ | |
| class PathExpr: | |
| """Base class for path expressions. Everything is deferred.""" | |
| def __truediv__(self, other: PathExpr | str) -> PathExpr: | |
| return JoinExpr(self, other) | |
| def __rtruediv__(self, other: str) -> PathExpr: | |
| return JoinExpr(LiteralExpr(other), self) | |
| @property | |
| def parent(self) -> PathExpr: | |
| return ParentExpr(self) | |
| def with_name(self, name: str) -> PathExpr: | |
| return SiblingExpr(self, name) | |
| def with_suffix(self, suffix: str) -> PathExpr: | |
| return WithSuffixExpr(self, suffix) | |
| def resolve(self, bindings: dict[str, Any] = {}) -> Path: | |
| raise NotImplementedError | |
| @dataclass(frozen=True) | |
| class LiteralExpr(PathExpr): | |
| """A concrete string/path segment.""" | |
| value: str | Path | |
| def resolve(self, bindings: dict[str, Any] = {}) -> Path: | |
| return Path(self.value) | |
| @dataclass(frozen=True) | |
| class ParamExpr(PathExpr): | |
| """A parameter reference—resolved from bindings.""" | |
| param: Param | |
| def resolve(self, bindings: dict[str, Any] = {}) -> Path: | |
| if self.param.name not in bindings: | |
| raise ValueError(f"Unbound parameter: {self.param.name}") | |
| return Path(bindings[self.param.name]) | |
| @dataclass(frozen=True) | |
| class TemplateExpr(PathExpr): | |
| template: Template | |
| def resolve(self, bindings: dict[str, Any] = {}) -> Path: | |
| parts = [] | |
| for item in self.template: | |
| if isinstance(item, str): | |
| parts.append(item) | |
| else: | |
| value = item.value | |
| if isinstance(value, Param): | |
| if value.name not in bindings: | |
| raise ValueError(f"Unbound parameter: {value.name}") | |
| value = bindings[value.name] | |
| parts.append(format(value, item.format_spec)) | |
| return Path("".join(parts)) | |
| @property | |
| def parent(self) -> PathExpr: | |
| # Find last '/' in the static structure | |
| strings = self.template.strings | |
| interps = self.template.interpolations | |
| # Walk backwards through strings to find last separator | |
| cumulative = [] | |
| for i, s in enumerate(strings): | |
| cumulative.append(("s", i, s)) | |
| if i < len(interps): | |
| cumulative.append(("i", i, interps[i])) | |
| # Find the last '/' and truncate there | |
| new_parts = [] | |
| last_sep_idx = None | |
| for j, (kind, i, val) in enumerate(cumulative): | |
| if kind == "s" and "/" in val: | |
| last_sep_idx = j | |
| last_sep_pos = val.rfind("/") | |
| if last_sep_idx is None: | |
| return ParentExpr(self) # No separator found, fall back | |
| # Rebuild template up to (but not including) last segment | |
| new_parts = [] | |
| for j, (kind, i, val) in enumerate(cumulative): | |
| if j < last_sep_idx: | |
| new_parts.append(val if kind == "s" else val) | |
| elif j == last_sep_idx: | |
| new_parts.append(val[:last_sep_pos]) # Truncate at last '/' | |
| break | |
| return TemplateExpr(Template(*new_parts)) | |
| @dataclass(frozen=True) | |
| class JoinExpr(PathExpr): | |
| """Path join: left / right""" | |
| left: PathExpr | |
| right: PathExpr | str | |
| def __post_init__(self): | |
| # Normalize string to LiteralExpr | |
| if isinstance(self.right, str): | |
| object.__setattr__(self, "right", LiteralExpr(self.right)) | |
| def resolve(self, bindings: dict[str, Any] = {}) -> Path: | |
| return self.left.resolve(bindings) / self.right.resolve(bindings) | |
| @dataclass(frozen=True) | |
| class ParentExpr(PathExpr): | |
| child: PathExpr | |
| def resolve(self, bindings: dict[str, Any] = {}) -> Path: | |
| # Structural: parent(a / b) = a | |
| if isinstance(self.child, JoinExpr): | |
| return self.child.left.resolve(bindings) | |
| return self.child.resolve(bindings).parent | |
| @dataclass(frozen=True) | |
| class SiblingExpr(PathExpr): | |
| base: PathExpr | |
| name: str | |
| def resolve(self, bindings: dict[str, Any] = {}) -> Path: | |
| # Expression-level parent, then resolve | |
| return self.base.parent.resolve(bindings) / self.name | |
| @dataclass(frozen=True) | |
| class WithSuffixExpr(PathExpr): | |
| """Change suffix of a path.""" | |
| base: PathExpr | |
| suffix: str | |
| def resolve(self, bindings: dict[str, Any] = {}) -> Path: | |
| return self.base.resolve(bindings).with_suffix(self.suffix) | |
| # ============================================================ | |
| # Layer 3: Convenience constructors | |
| # ============================================================ | |
| def P(name: str) -> PathExpr: | |
| """Shorthand for a parameter expression.""" | |
| return ParamExpr(Param(name)) | |
| def T(template: Template) -> PathExpr: | |
| """Wrap a t-string as a PathExpr.""" | |
| return TemplateExpr(template) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment