wend 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wend-0.1.0/PKG-INFO +122 -0
- wend-0.1.0/README.md +101 -0
- wend-0.1.0/pyproject.toml +61 -0
- wend-0.1.0/src/wend/__init__.py +291 -0
wend-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: wend
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Templated paths using t-strings
|
|
5
|
+
Keywords: paths,t-strings
|
|
6
|
+
Author: Louis Maddox
|
|
7
|
+
Author-email: Louis Maddox <louismmx@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Natural Language :: English
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
17
|
+
Requires-Python: >=3.14
|
|
18
|
+
Project-URL: Homepage, https://github.com/lmmx/wend
|
|
19
|
+
Project-URL: Repository, https://github.com/lmmx/wend.git
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# wend
|
|
23
|
+
|
|
24
|
+
[](https://pypi.org/project/wend/)
|
|
25
|
+
[](https://pypi.org/project/wend/)
|
|
26
|
+
[](https://github.com/lmmx/wend/blob/master/LICENSE)
|
|
27
|
+
[](https://results.pre-commit.ci/latest/github/lmmx/wend/master)
|
|
28
|
+
|
|
29
|
+
A library for templated paths using Python t-strings (3.14+)
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
uv pip install wend
|
|
35
|
+
````
|
|
36
|
+
|
|
37
|
+
## Requirements
|
|
38
|
+
|
|
39
|
+
- Python 3.14+
|
|
40
|
+
- Git (with sparse-checkout support)
|
|
41
|
+
|
|
42
|
+
## About
|
|
43
|
+
|
|
44
|
+
For backstory read my blog about this package [cog.spin.systems/future-paths][blog]!
|
|
45
|
+
|
|
46
|
+
[blog]: https://cog.spin.systems/future-paths
|
|
47
|
+
|
|
48
|
+
The general idea is to use t-strings to house eagerly instantiated parameter objects which offer a
|
|
49
|
+
form of deferred execution.
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
Following the examples in the [blog post][blog]:
|
|
54
|
+
|
|
55
|
+
```py
|
|
56
|
+
from pathlib import Path
|
|
57
|
+
from wend import Param, P, T, RelativePath
|
|
58
|
+
|
|
59
|
+
# Define parameters (late-bound)
|
|
60
|
+
root = Param("root")
|
|
61
|
+
dataset = Param("dataset")
|
|
62
|
+
idx = Param("idx")
|
|
63
|
+
total = Param("total")
|
|
64
|
+
|
|
65
|
+
# Build expressions - these don't touch the filesystem
|
|
66
|
+
data_dir = root / "data" / dataset
|
|
67
|
+
chunk_file = data_dir / T(t"chunk_{idx:04d}-of-{total:04d}.parquet")
|
|
68
|
+
|
|
69
|
+
# Inspect what's needed
|
|
70
|
+
print("Required params:", chunk_file.required_params())
|
|
71
|
+
# -> {'root', 'dataset', 'idx', 'total'}
|
|
72
|
+
|
|
73
|
+
# Resolve with bindings
|
|
74
|
+
path = chunk_file.resolve({
|
|
75
|
+
"root": "/mnt/storage",
|
|
76
|
+
"dataset": "train",
|
|
77
|
+
"idx": 7,
|
|
78
|
+
"total": 100,
|
|
79
|
+
})
|
|
80
|
+
print("Resolved:", path)
|
|
81
|
+
# -> /mnt/storage/data/train/chunk_0007-of-0100.parquet
|
|
82
|
+
|
|
83
|
+
# Constant folding happens at construction
|
|
84
|
+
folded = P("/home") / "user" / "data"
|
|
85
|
+
print("Folded type:", type(folded).__name__)
|
|
86
|
+
# -> LiteralExpr (not nested JoinExprs)
|
|
87
|
+
|
|
88
|
+
# Parent simplification
|
|
89
|
+
expr = root / "a" / "b"
|
|
90
|
+
print("Parent of join:", expr.parent)
|
|
91
|
+
# -> JoinExpr(ParamExpr, LiteralExpr("a")) -- the "b" is gone structurally
|
|
92
|
+
|
|
93
|
+
# Suffix chain collapse
|
|
94
|
+
double_suffix = (root / "file.txt").with_suffix(".tmp").with_suffix(".json")
|
|
95
|
+
print("Suffix chain:", double_suffix)
|
|
96
|
+
# -> WithSuffixExpr(base=JoinExpr(...), suffix='.json') -- .tmp is gone
|
|
97
|
+
|
|
98
|
+
# Connectivity: rebasing
|
|
99
|
+
config = RelativePath(
|
|
100
|
+
base=root,
|
|
101
|
+
relative=P("config") / "settings.yaml"
|
|
102
|
+
)
|
|
103
|
+
print("Config path:", config.resolve({"root": "/project"}))
|
|
104
|
+
# -> /project/config/settings.yaml
|
|
105
|
+
|
|
106
|
+
# Rebase to test environment
|
|
107
|
+
test_config = config.rebase(P("/tmp/test"))
|
|
108
|
+
print("Test config:", test_config.resolve({}))
|
|
109
|
+
# -> /tmp/test/config/settings.yaml
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Contributing
|
|
113
|
+
|
|
114
|
+
Contributions are welcome! Please:
|
|
115
|
+
|
|
116
|
+
1. Open an issue to discuss bugs or feature requests
|
|
117
|
+
2. Fork the repo and submit a PR for changes
|
|
118
|
+
3. Install dev dependencies with `uv sync` and run `pre-commit install`
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
wend-0.1.0/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# wend
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/wend/)
|
|
4
|
+
[](https://pypi.org/project/wend/)
|
|
5
|
+
[](https://github.com/lmmx/wend/blob/master/LICENSE)
|
|
6
|
+
[](https://results.pre-commit.ci/latest/github/lmmx/wend/master)
|
|
7
|
+
|
|
8
|
+
A library for templated paths using Python t-strings (3.14+)
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
uv pip install wend
|
|
14
|
+
````
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- Python 3.14+
|
|
19
|
+
- Git (with sparse-checkout support)
|
|
20
|
+
|
|
21
|
+
## About
|
|
22
|
+
|
|
23
|
+
For backstory read my blog about this package [cog.spin.systems/future-paths][blog]!
|
|
24
|
+
|
|
25
|
+
[blog]: https://cog.spin.systems/future-paths
|
|
26
|
+
|
|
27
|
+
The general idea is to use t-strings to house eagerly instantiated parameter objects which offer a
|
|
28
|
+
form of deferred execution.
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
Following the examples in the [blog post][blog]:
|
|
33
|
+
|
|
34
|
+
```py
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from wend import Param, P, T, RelativePath
|
|
37
|
+
|
|
38
|
+
# Define parameters (late-bound)
|
|
39
|
+
root = Param("root")
|
|
40
|
+
dataset = Param("dataset")
|
|
41
|
+
idx = Param("idx")
|
|
42
|
+
total = Param("total")
|
|
43
|
+
|
|
44
|
+
# Build expressions - these don't touch the filesystem
|
|
45
|
+
data_dir = root / "data" / dataset
|
|
46
|
+
chunk_file = data_dir / T(t"chunk_{idx:04d}-of-{total:04d}.parquet")
|
|
47
|
+
|
|
48
|
+
# Inspect what's needed
|
|
49
|
+
print("Required params:", chunk_file.required_params())
|
|
50
|
+
# -> {'root', 'dataset', 'idx', 'total'}
|
|
51
|
+
|
|
52
|
+
# Resolve with bindings
|
|
53
|
+
path = chunk_file.resolve({
|
|
54
|
+
"root": "/mnt/storage",
|
|
55
|
+
"dataset": "train",
|
|
56
|
+
"idx": 7,
|
|
57
|
+
"total": 100,
|
|
58
|
+
})
|
|
59
|
+
print("Resolved:", path)
|
|
60
|
+
# -> /mnt/storage/data/train/chunk_0007-of-0100.parquet
|
|
61
|
+
|
|
62
|
+
# Constant folding happens at construction
|
|
63
|
+
folded = P("/home") / "user" / "data"
|
|
64
|
+
print("Folded type:", type(folded).__name__)
|
|
65
|
+
# -> LiteralExpr (not nested JoinExprs)
|
|
66
|
+
|
|
67
|
+
# Parent simplification
|
|
68
|
+
expr = root / "a" / "b"
|
|
69
|
+
print("Parent of join:", expr.parent)
|
|
70
|
+
# -> JoinExpr(ParamExpr, LiteralExpr("a")) -- the "b" is gone structurally
|
|
71
|
+
|
|
72
|
+
# Suffix chain collapse
|
|
73
|
+
double_suffix = (root / "file.txt").with_suffix(".tmp").with_suffix(".json")
|
|
74
|
+
print("Suffix chain:", double_suffix)
|
|
75
|
+
# -> WithSuffixExpr(base=JoinExpr(...), suffix='.json') -- .tmp is gone
|
|
76
|
+
|
|
77
|
+
# Connectivity: rebasing
|
|
78
|
+
config = RelativePath(
|
|
79
|
+
base=root,
|
|
80
|
+
relative=P("config") / "settings.yaml"
|
|
81
|
+
)
|
|
82
|
+
print("Config path:", config.resolve({"root": "/project"}))
|
|
83
|
+
# -> /project/config/settings.yaml
|
|
84
|
+
|
|
85
|
+
# Rebase to test environment
|
|
86
|
+
test_config = config.rebase(P("/tmp/test"))
|
|
87
|
+
print("Test config:", test_config.resolve({}))
|
|
88
|
+
# -> /tmp/test/config/settings.yaml
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Contributing
|
|
92
|
+
|
|
93
|
+
Contributions are welcome! Please:
|
|
94
|
+
|
|
95
|
+
1. Open an issue to discuss bugs or feature requests
|
|
96
|
+
2. Fork the repo and submit a PR for changes
|
|
97
|
+
3. Install dev dependencies with `uv sync` and run `pre-commit install`
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "uv_build"
|
|
3
|
+
requires = ["uv_build>=0.9.26,<0.10.0"]
|
|
4
|
+
|
|
5
|
+
[dependency-groups]
|
|
6
|
+
test = [
|
|
7
|
+
"pytest>=8.4.0",
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
[project]
|
|
11
|
+
name = "wend"
|
|
12
|
+
version = "0.1.0"
|
|
13
|
+
description = "Templated paths using t-strings"
|
|
14
|
+
readme = "README.md"
|
|
15
|
+
authors = [
|
|
16
|
+
{email = "louismmx@gmail.com", name = "Louis Maddox"},
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Natural Language :: English",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
27
|
+
]
|
|
28
|
+
keywords = [
|
|
29
|
+
"paths",
|
|
30
|
+
"t-strings",
|
|
31
|
+
]
|
|
32
|
+
requires-python = ">=3.14"
|
|
33
|
+
dependencies = []
|
|
34
|
+
|
|
35
|
+
[project.license]
|
|
36
|
+
text = "MIT"
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/lmmx/wend"
|
|
40
|
+
Repository = "https://github.com/lmmx/wend.git"
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
testpaths = "tests"
|
|
44
|
+
addopts = "-v"
|
|
45
|
+
|
|
46
|
+
[tool.ruff]
|
|
47
|
+
target-version = "py314"
|
|
48
|
+
|
|
49
|
+
[tool.ruff.lint]
|
|
50
|
+
ignore = ["C408", "C901", "E501", "E722", "E741"]
|
|
51
|
+
select = ["C", "E", "F", "I", "UP", "W"]
|
|
52
|
+
|
|
53
|
+
[tool.ruff.lint.isort]
|
|
54
|
+
known-first-party = ["wend"]
|
|
55
|
+
lines-after-imports = 2
|
|
56
|
+
|
|
57
|
+
[tool.ruff.lint.per-file-ignores]
|
|
58
|
+
"__init__.py" = ["E401", "F401"]
|
|
59
|
+
|
|
60
|
+
[tool.uv]
|
|
61
|
+
default-groups = ["test"]
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Deferred path expressions with late binding.
|
|
3
|
+
|
|
4
|
+
Build path expressions symbolically, resolve them later with concrete bindings.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from string.templatelib import Template
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Parameters (late binding slots)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class Param:
|
|
20
|
+
"""A named slot to be filled at resolution time."""
|
|
21
|
+
|
|
22
|
+
name: str
|
|
23
|
+
|
|
24
|
+
def __truediv__(self, other: PathExpr | str | Param) -> PathExpr:
|
|
25
|
+
if isinstance(other, Param):
|
|
26
|
+
return ParamExpr(self) / ParamExpr(other)
|
|
27
|
+
return ParamExpr(self) / other
|
|
28
|
+
|
|
29
|
+
def __rtruediv__(self, other: str) -> PathExpr:
|
|
30
|
+
return LiteralExpr(other) / ParamExpr(self)
|
|
31
|
+
|
|
32
|
+
def __repr__(self) -> str:
|
|
33
|
+
return f"Param({self.name!r})"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Path Expressions (composable, deferred)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PathExpr:
|
|
40
|
+
"""Base class for path expressions. Everything is deferred."""
|
|
41
|
+
|
|
42
|
+
def __truediv__(self, other: PathExpr | str | Param) -> PathExpr:
|
|
43
|
+
if isinstance(other, Param):
|
|
44
|
+
right = ParamExpr(other)
|
|
45
|
+
elif isinstance(other, PathExpr):
|
|
46
|
+
right = other
|
|
47
|
+
else:
|
|
48
|
+
right = LiteralExpr(other)
|
|
49
|
+
return JoinExpr.create(self, right)
|
|
50
|
+
|
|
51
|
+
def __rtruediv__(self, other: str) -> PathExpr:
|
|
52
|
+
return JoinExpr.create(LiteralExpr(other), self)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def parent(self) -> PathExpr:
|
|
56
|
+
return ParentExpr.create(self)
|
|
57
|
+
|
|
58
|
+
def with_name(self, name: str) -> PathExpr:
|
|
59
|
+
return WithNameExpr(self, name)
|
|
60
|
+
|
|
61
|
+
def with_suffix(self, suffix: str) -> PathExpr:
|
|
62
|
+
return WithSuffixExpr.create(self, suffix)
|
|
63
|
+
|
|
64
|
+
def required_params(self) -> set[str]:
|
|
65
|
+
"""Return all parameter names needed to resolve this expression."""
|
|
66
|
+
raise NotImplementedError
|
|
67
|
+
|
|
68
|
+
def resolve(self, bindings: dict[str, Any] | None = None) -> Path:
|
|
69
|
+
"""Resolve to a concrete Path with the given bindings."""
|
|
70
|
+
bindings = bindings or {}
|
|
71
|
+
missing = self.required_params() - set(bindings.keys())
|
|
72
|
+
if missing:
|
|
73
|
+
raise ValueError(f"Missing bindings: {missing}")
|
|
74
|
+
cache: dict[int, Path] = {}
|
|
75
|
+
return self._resolve(bindings, cache)
|
|
76
|
+
|
|
77
|
+
def _resolve(self, bindings: dict[str, Any], cache: dict[int, Path]) -> Path:
|
|
78
|
+
"""Internal resolve with memoization cache."""
|
|
79
|
+
raise NotImplementedError
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class LiteralExpr(PathExpr):
|
|
84
|
+
"""A concrete string/path segment."""
|
|
85
|
+
|
|
86
|
+
value: str | Path
|
|
87
|
+
|
|
88
|
+
def required_params(self) -> set[str]:
|
|
89
|
+
return set()
|
|
90
|
+
|
|
91
|
+
def _resolve(self, bindings: dict[str, Any], cache: dict[int, Path]) -> Path:
|
|
92
|
+
if id(self) in cache:
|
|
93
|
+
return cache[id(self)]
|
|
94
|
+
result = Path(self.value)
|
|
95
|
+
cache[id(self)] = result
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class ParamExpr(PathExpr):
|
|
101
|
+
"""A parameter reference—resolved from bindings."""
|
|
102
|
+
|
|
103
|
+
param: Param
|
|
104
|
+
|
|
105
|
+
def required_params(self) -> set[str]:
|
|
106
|
+
return {self.param.name}
|
|
107
|
+
|
|
108
|
+
def _resolve(self, bindings: dict[str, Any], cache: dict[int, Path]) -> Path:
|
|
109
|
+
if id(self) in cache:
|
|
110
|
+
return cache[id(self)]
|
|
111
|
+
result = Path(bindings[self.param.name])
|
|
112
|
+
cache[id(self)] = result
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class TemplateExpr(PathExpr):
|
|
118
|
+
"""A t-string template with interpolations."""
|
|
119
|
+
|
|
120
|
+
template: Template
|
|
121
|
+
_params: frozenset[str] = field(init=False, repr=False)
|
|
122
|
+
|
|
123
|
+
def __post_init__(self) -> None:
|
|
124
|
+
params = set()
|
|
125
|
+
for interp in self.template.interpolations:
|
|
126
|
+
if isinstance(interp.value, Param):
|
|
127
|
+
params.add(interp.value.name)
|
|
128
|
+
object.__setattr__(self, "_params", frozenset(params))
|
|
129
|
+
|
|
130
|
+
def required_params(self) -> set[str]:
|
|
131
|
+
return set(self._params)
|
|
132
|
+
|
|
133
|
+
def _resolve(self, bindings: dict[str, Any], cache: dict[int, Path]) -> Path:
|
|
134
|
+
if id(self) in cache:
|
|
135
|
+
return cache[id(self)]
|
|
136
|
+
parts = []
|
|
137
|
+
for item in self.template:
|
|
138
|
+
if isinstance(item, str):
|
|
139
|
+
parts.append(item)
|
|
140
|
+
else:
|
|
141
|
+
value = item.value
|
|
142
|
+
if isinstance(value, Param):
|
|
143
|
+
value = bindings[value.name]
|
|
144
|
+
parts.append(format(value, item.format_spec))
|
|
145
|
+
result = Path("".join(parts))
|
|
146
|
+
cache[id(self)] = result
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass(frozen=True)
|
|
151
|
+
class JoinExpr(PathExpr):
|
|
152
|
+
"""Path join: left / right"""
|
|
153
|
+
|
|
154
|
+
left: PathExpr
|
|
155
|
+
right: PathExpr
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def create(left: PathExpr, right: PathExpr) -> PathExpr:
|
|
159
|
+
"""Construct with constant folding."""
|
|
160
|
+
# Fold Literal / Literal -> Literal
|
|
161
|
+
if isinstance(left, LiteralExpr) and isinstance(right, LiteralExpr):
|
|
162
|
+
return LiteralExpr(Path(left.value) / right.value)
|
|
163
|
+
return JoinExpr(left, right)
|
|
164
|
+
|
|
165
|
+
def required_params(self) -> set[str]:
|
|
166
|
+
return self.left.required_params() | self.right.required_params()
|
|
167
|
+
|
|
168
|
+
def _resolve(self, bindings: dict[str, Any], cache: dict[int, Path]) -> Path:
|
|
169
|
+
if id(self) in cache:
|
|
170
|
+
return cache[id(self)]
|
|
171
|
+
result = self.left._resolve(bindings, cache) / self.right._resolve(
|
|
172
|
+
bindings, cache
|
|
173
|
+
)
|
|
174
|
+
cache[id(self)] = result
|
|
175
|
+
return result
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass(frozen=True)
|
|
179
|
+
class ParentExpr(PathExpr):
|
|
180
|
+
"""Parent of a path expression."""
|
|
181
|
+
|
|
182
|
+
child: PathExpr
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def create(child: PathExpr) -> PathExpr:
|
|
186
|
+
"""Construct with algebraic simplification."""
|
|
187
|
+
# parent(a / b) = a (when b is single component)
|
|
188
|
+
if isinstance(child, JoinExpr):
|
|
189
|
+
# Check if right side is a single component (no further joins)
|
|
190
|
+
if isinstance(child.right, (LiteralExpr, ParamExpr, TemplateExpr)):
|
|
191
|
+
return child.left
|
|
192
|
+
# parent(Literal) can be folded
|
|
193
|
+
if isinstance(child, LiteralExpr):
|
|
194
|
+
return LiteralExpr(Path(child.value).parent)
|
|
195
|
+
return ParentExpr(child)
|
|
196
|
+
|
|
197
|
+
def required_params(self) -> set[str]:
|
|
198
|
+
return self.child.required_params()
|
|
199
|
+
|
|
200
|
+
def _resolve(self, bindings: dict[str, Any], cache: dict[int, Path]) -> Path:
|
|
201
|
+
if id(self) in cache:
|
|
202
|
+
return cache[id(self)]
|
|
203
|
+
result = self.child._resolve(bindings, cache).parent
|
|
204
|
+
cache[id(self)] = result
|
|
205
|
+
return result
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@dataclass(frozen=True)
|
|
209
|
+
class WithNameExpr(PathExpr):
|
|
210
|
+
"""Replace the final component name."""
|
|
211
|
+
|
|
212
|
+
base: PathExpr
|
|
213
|
+
name: str
|
|
214
|
+
|
|
215
|
+
def required_params(self) -> set[str]:
|
|
216
|
+
return self.base.required_params()
|
|
217
|
+
|
|
218
|
+
def _resolve(self, bindings: dict[str, Any], cache: dict[int, Path]) -> Path:
|
|
219
|
+
if id(self) in cache:
|
|
220
|
+
return cache[id(self)]
|
|
221
|
+
result = self.base._resolve(bindings, cache).with_name(self.name)
|
|
222
|
+
cache[id(self)] = result
|
|
223
|
+
return result
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@dataclass(frozen=True)
|
|
227
|
+
class WithSuffixExpr(PathExpr):
|
|
228
|
+
"""Change suffix of a path."""
|
|
229
|
+
|
|
230
|
+
base: PathExpr
|
|
231
|
+
suffix: str
|
|
232
|
+
|
|
233
|
+
@staticmethod
|
|
234
|
+
def create(base: PathExpr, suffix: str) -> PathExpr:
|
|
235
|
+
"""Construct with suffix chain collapse."""
|
|
236
|
+
# path.with_suffix(".a").with_suffix(".b") -> path.with_suffix(".b")
|
|
237
|
+
if isinstance(base, WithSuffixExpr):
|
|
238
|
+
return WithSuffixExpr(base.base, suffix)
|
|
239
|
+
# Literal can be folded
|
|
240
|
+
if isinstance(base, LiteralExpr):
|
|
241
|
+
return LiteralExpr(Path(base.value).with_suffix(suffix))
|
|
242
|
+
return WithSuffixExpr(base, suffix)
|
|
243
|
+
|
|
244
|
+
def required_params(self) -> set[str]:
|
|
245
|
+
return self.base.required_params()
|
|
246
|
+
|
|
247
|
+
def _resolve(self, bindings: dict[str, Any], cache: dict[int, Path]) -> Path:
|
|
248
|
+
if id(self) in cache:
|
|
249
|
+
return cache[id(self)]
|
|
250
|
+
result = self.base._resolve(bindings, cache).with_suffix(self.suffix)
|
|
251
|
+
cache[id(self)] = result
|
|
252
|
+
return result
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# Convenience constructors
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def P(path: str | Path) -> LiteralExpr:
|
|
259
|
+
"""Literal path expression."""
|
|
260
|
+
return LiteralExpr(path)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def T(template: Template) -> TemplateExpr:
|
|
264
|
+
"""Template path expression from a t-string."""
|
|
265
|
+
return TemplateExpr(template)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# Connectivity (rebasing)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@dataclass(frozen=True)
|
|
272
|
+
class RelativePath:
|
|
273
|
+
"""A path defined relative to a base, preserving the relation."""
|
|
274
|
+
|
|
275
|
+
base: PathExpr
|
|
276
|
+
relative: PathExpr
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def expr(self) -> PathExpr:
|
|
280
|
+
"""The full path expression."""
|
|
281
|
+
return self.base / self.relative
|
|
282
|
+
|
|
283
|
+
def rebase(self, new_base: PathExpr) -> RelativePath:
|
|
284
|
+
"""Create equivalent path under a different base."""
|
|
285
|
+
return RelativePath(new_base, self.relative)
|
|
286
|
+
|
|
287
|
+
def resolve(self, bindings: dict[str, Any] | None = None) -> Path:
|
|
288
|
+
return self.expr.resolve(bindings)
|
|
289
|
+
|
|
290
|
+
def required_params(self) -> set[str]:
|
|
291
|
+
return self.expr.required_params()
|