tomlstack 0.1.2__tar.gz → 0.2.0rc1__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.
- {tomlstack-0.1.2 → tomlstack-0.2.0rc1}/.gitignore +4 -1
- {tomlstack-0.1.2 → tomlstack-0.2.0rc1}/CHANGELOG.md +25 -0
- {tomlstack-0.1.2 → tomlstack-0.2.0rc1}/PKG-INFO +49 -32
- tomlstack-0.2.0rc1/README.md +178 -0
- tomlstack-0.2.0rc1/src/tomlstack/__init__.py +13 -0
- tomlstack-0.2.0rc1/src/tomlstack/_version.py +24 -0
- tomlstack-0.2.0rc1/src/tomlstack/api.py +95 -0
- tomlstack-0.2.0rc1/src/tomlstack/errors.py +34 -0
- tomlstack-0.2.0rc1/src/tomlstack/include.py +66 -0
- tomlstack-0.2.0rc1/src/tomlstack/interpolate.py +234 -0
- tomlstack-0.2.0rc1/src/tomlstack/loader.py +156 -0
- tomlstack-0.2.0rc1/src/tomlstack/nodes.py +109 -0
- tomlstack-0.2.0rc1/src/tomlstack/path_expr.py +160 -0
- tomlstack-0.2.0rc1/src/tomlstack/trace.py +62 -0
- tomlstack-0.2.0rc1/src/tomlstack/tree.py +54 -0
- tomlstack-0.2.0rc1/src/tomlstack/types.py +71 -0
- tomlstack-0.2.0rc1/tests/test_include_merge.py +480 -0
- tomlstack-0.2.0rc1/tests/test_interpolation.py +314 -0
- tomlstack-0.2.0rc1/tests/test_loader_model.py +32 -0
- {tomlstack-0.1.2 → tomlstack-0.2.0rc1}/tests/test_nodes.py +48 -13
- tomlstack-0.2.0rc1/tests/test_path_expr.py +55 -0
- {tomlstack-0.1.2 → tomlstack-0.2.0rc1}/tests/test_smoke.py +8 -1
- {tomlstack-0.1.2 → tomlstack-0.2.0rc1}/uv.lock +204 -188
- tomlstack-0.1.2/README.md +0 -161
- tomlstack-0.1.2/src/tomlstack/__init__.py +0 -3
- tomlstack-0.1.2/src/tomlstack/_version.py +0 -34
- tomlstack-0.1.2/src/tomlstack/api.py +0 -81
- tomlstack-0.1.2/src/tomlstack/base.py +0 -29
- tomlstack-0.1.2/src/tomlstack/errors.py +0 -46
- tomlstack-0.1.2/src/tomlstack/include.py +0 -121
- tomlstack-0.1.2/src/tomlstack/interpolate.py +0 -124
- tomlstack-0.1.2/src/tomlstack/loader.py +0 -202
- tomlstack-0.1.2/src/tomlstack/nodes.py +0 -89
- tomlstack-0.1.2/src/tomlstack/path_expr.py +0 -78
- tomlstack-0.1.2/tests/test_include_merge.py +0 -161
- tomlstack-0.1.2/tests/test_interpolation.py +0 -94
- {tomlstack-0.1.2 → tomlstack-0.2.0rc1}/.github/workflows/ci.yml +0 -0
- {tomlstack-0.1.2 → tomlstack-0.2.0rc1}/.github/workflows/publish-pypi.yml +0 -0
- {tomlstack-0.1.2 → tomlstack-0.2.0rc1}/.github/workflows/publish-testpypi.yml +0 -0
- {tomlstack-0.1.2 → tomlstack-0.2.0rc1}/CONTRIBUTING.md +0 -0
- {tomlstack-0.1.2 → tomlstack-0.2.0rc1}/LICENSE +0 -0
- {tomlstack-0.1.2 → tomlstack-0.2.0rc1}/RELEASE.md +0 -0
- {tomlstack-0.1.2 → tomlstack-0.2.0rc1}/pyproject.toml +0 -0
|
@@ -135,6 +135,9 @@ venv.bak/
|
|
|
135
135
|
.dmypy.json
|
|
136
136
|
dmypy.json
|
|
137
137
|
|
|
138
|
+
# ruff
|
|
139
|
+
.ruff_cache/
|
|
140
|
+
|
|
138
141
|
# Pyre type checker
|
|
139
142
|
.pyre/
|
|
140
143
|
|
|
@@ -153,7 +156,7 @@ cython_debug/
|
|
|
153
156
|
|
|
154
157
|
|
|
155
158
|
# custom
|
|
156
|
-
|
|
159
|
+
.codex
|
|
157
160
|
.vscode/*
|
|
158
161
|
# hatch-vcs generated version file
|
|
159
162
|
src/tomlstack/_version.py
|
|
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on Keep a Changelog,
|
|
6
6
|
and this project follows Semantic Versioning.
|
|
7
7
|
|
|
8
|
+
## [0.2.0] - 2026-06-24
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Include-tree inspection through `cfg.include_tree`.
|
|
13
|
+
- Node provenance and interpolation dependency inspection through `TomlNode`.
|
|
14
|
+
- Specific errors for invalid TOML, undefined interpolation references, and
|
|
15
|
+
interpolation cycles.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Configuration loading now binds each value to its source history internally,
|
|
20
|
+
making merge and interpolation provenance consistent.
|
|
21
|
+
- `TomlNode` is the public node-query type; nodes and `TomlStack` instances are
|
|
22
|
+
created through configuration loading and navigation only.
|
|
23
|
+
|
|
24
|
+
### Removed
|
|
25
|
+
|
|
26
|
+
- `TomlStack.view`; use `cfg.raw` for unexpanded data or `cfg.to_dict()` for a
|
|
27
|
+
resolved snapshot.
|
|
28
|
+
- `TomlStack.to_dict(resolve=False)`; use `cfg.raw`.
|
|
29
|
+
- `TomlHist` and `TomlFile.str_`; history now contains `TomlFile` values with
|
|
30
|
+
`reference` and resolved `path` fields.
|
|
31
|
+
- Legacy metadata/version directives and their associated error classes.
|
|
32
|
+
|
|
8
33
|
## [0.1.2] - 2026-03-01
|
|
9
34
|
|
|
10
35
|
- first publish version
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tomlstack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0rc1
|
|
4
4
|
Summary: TOML configuration loader with include/merge/interpolation
|
|
5
5
|
Project-URL: Homepage, https://github.com/wxzhao7/tomlstack
|
|
6
6
|
Project-URL: Repository, https://github.com/wxzhao7/tomlstack
|
|
@@ -39,12 +39,14 @@ Description-Content-Type: text/markdown
|
|
|
39
39
|
`tomlstack` is a lightweight TOML config loader for Python 3.11+ with:
|
|
40
40
|
|
|
41
41
|
- top-level `include` loading
|
|
42
|
+
- include-tree inspection with raw and resolved paths
|
|
42
43
|
- deterministic merge by include order
|
|
43
44
|
- `${path}` interpolation with cycle/undefined checks
|
|
44
|
-
- node-level provenance (`origin`, `
|
|
45
|
+
- node-level provenance (`origin`, `history`, `dependencies`)
|
|
45
46
|
|
|
46
47
|
tomlstack does not try to be a configuration framework.
|
|
47
|
-
It
|
|
48
|
+
It addresses two missing pieces in TOML: file composition and safe interpolation,
|
|
49
|
+
while keeping files self-contained and explainable.
|
|
48
50
|
|
|
49
51
|
## Install
|
|
50
52
|
|
|
@@ -79,15 +81,17 @@ port = 5432
|
|
|
79
81
|
Python:
|
|
80
82
|
|
|
81
83
|
```python
|
|
82
|
-
from tomlstack import load
|
|
84
|
+
from tomlstack import TomlNode, load
|
|
83
85
|
|
|
84
86
|
cfg = load("main.toml")
|
|
85
87
|
print(cfg["db"]["url"].raw) # raw interpolation string
|
|
86
|
-
cfg.resolve()
|
|
87
88
|
print(cfg["db"]["url"].value) # resolved value
|
|
88
89
|
print(cfg["db"]["url"].origin)
|
|
89
90
|
print(cfg["db"]["url"].history)
|
|
90
|
-
print(cfg.
|
|
91
|
+
print(cfg["db"]["url"].dependencies)
|
|
92
|
+
print(cfg["db"]["url"].explain())
|
|
93
|
+
print(cfg.raw) # raw configuration snapshot
|
|
94
|
+
print(cfg.to_dict()) # resolved configuration snapshot
|
|
91
95
|
```
|
|
92
96
|
|
|
93
97
|
## Include Semantics
|
|
@@ -95,28 +99,20 @@ print(cfg.to_dict())
|
|
|
95
99
|
- top-level `include` only; nested `include` is treated as normal data
|
|
96
100
|
- syntax: string or list of strings
|
|
97
101
|
- valid include path forms:
|
|
98
|
-
- `./...` or `../...`
|
|
99
|
-
- `@label/...` (label from `
|
|
100
|
-
- absolute path
|
|
101
|
-
- any other form raises error with
|
|
102
|
+
- `./...` or `../...`
|
|
103
|
+
- `@label/...` (label from `tomlstack.anchors`)
|
|
104
|
+
- absolute path
|
|
105
|
+
- any other form raises an error with a path-format hint
|
|
102
106
|
|
|
103
107
|
### Meta Include Directives
|
|
104
108
|
|
|
105
109
|
```toml
|
|
106
|
-
[
|
|
107
|
-
version = 1
|
|
108
|
-
|
|
109
|
-
[__meta__.include]
|
|
110
|
-
root = "../.."
|
|
111
|
-
|
|
112
|
-
[__meta__.include.anchors]
|
|
110
|
+
[tomlstack.anchors]
|
|
113
111
|
proj = "./shared"
|
|
114
112
|
```
|
|
115
113
|
|
|
116
|
-
-
|
|
117
|
-
-
|
|
118
|
-
- anchor/root path values must be absolute or start with `./` or `../`
|
|
119
|
-
- if any file explicitly sets `__meta__.version`, all files in include chain must share one supported version (`1`)
|
|
114
|
+
- anchors are local to the file that declares them
|
|
115
|
+
- anchor path values must be absolute or start with `./` or `../`
|
|
120
116
|
|
|
121
117
|
## Merge Rules
|
|
122
118
|
|
|
@@ -135,41 +131,62 @@ Conflict behavior:
|
|
|
135
131
|
|
|
136
132
|
## Interpolation Semantics
|
|
137
133
|
|
|
138
|
-
- interpolation
|
|
134
|
+
- interpolation is resolved lazily by `cfg.resolve()`, `cfg.to_dict()`, or
|
|
135
|
+
`node.value`
|
|
139
136
|
- path syntax supports dot and list index: `${db.apps[0]}`
|
|
140
137
|
- full-string interpolation (`"${db.port}"`) keeps source type
|
|
141
138
|
- embedded interpolation (`"postgres://${db.host}:${db.port}"`) allows only:
|
|
142
|
-
- `str`, `int`, `float`, `date`, `time`, `datetime`
|
|
139
|
+
- `str`, `int`, `float`, `bool`, `date`, `time`, `datetime`
|
|
143
140
|
- formatting syntax: `${path:spec}`
|
|
144
141
|
- for `date/time/datetime`, formatting uses `strftime`
|
|
145
142
|
- otherwise uses Python `format(value, spec)`
|
|
146
|
-
-
|
|
147
|
-
-
|
|
143
|
+
- invalid interpolation syntax and formatting failures raise `InterpolationError`
|
|
144
|
+
- undefined references raise `InterpolationUndefinedError`
|
|
145
|
+
- interpolation cycles raise `InterpolationCycleError`
|
|
146
|
+
|
|
147
|
+
Invalid TOML raises `TomlFormatError`; invalid include paths and anchors raise
|
|
148
|
+
`IncludeError`.
|
|
148
149
|
|
|
149
150
|
## Public API
|
|
150
151
|
|
|
151
152
|
- `cfg = load("f.toml")`
|
|
153
|
+
- `cfg.raw` — raw configuration snapshot
|
|
152
154
|
- `cfg.resolve()`
|
|
153
|
-
- `cfg.to_dict()`
|
|
154
|
-
- `
|
|
155
|
+
- `cfg.to_dict()` — resolved configuration snapshot
|
|
156
|
+
- `cfg.include_tree` — `IncludeNode` load-occurrence tree
|
|
157
|
+
- `cfg.include_tree.render()` — render raw include references
|
|
158
|
+
- `cfg.include_tree.render(absolute=True)` — include references with resolved paths
|
|
159
|
+
- `node: TomlNode = cfg["proj"][0]["path"]["foo"]`
|
|
155
160
|
- `node.raw`
|
|
156
161
|
- `node.value`
|
|
157
162
|
- `node.origin`
|
|
158
163
|
- `node.history`
|
|
164
|
+
- `node.dependencies` — direct interpolation dependencies
|
|
165
|
+
- `node.explain()` — transitive interpolation trace
|
|
159
166
|
- `node.preview()`
|
|
160
167
|
- `cfg.to_toml()` -> `NotImplementedError`
|
|
161
168
|
|
|
169
|
+
`cfg.raw`, `cfg.to_dict()`, `node.raw`, and `node.value` return independent data
|
|
170
|
+
snapshots; mutating their dictionaries or lists does not modify the loaded
|
|
171
|
+
configuration. `TomlNode` instances are created by configuration navigation and are
|
|
172
|
+
not constructed directly.
|
|
173
|
+
|
|
174
|
+
History records definitions of the same data path from lowest to highest priority.
|
|
175
|
+
When a list or value type is replaced, its old child paths are discarded. Resolving an
|
|
176
|
+
interpolation does not change the history of the node containing the expression.
|
|
177
|
+
Each history entry is a `TomlFile` with its raw `reference` and resolved absolute
|
|
178
|
+
`path`.
|
|
179
|
+
|
|
180
|
+
Dependencies describe interpolation separately from merge history. A full replacement
|
|
181
|
+
such as `target = "${source}"` leaves `target.history` at the file that defined
|
|
182
|
+
`target`, while `target.dependencies` records the source path and its history.
|
|
183
|
+
|
|
162
184
|
## Current Limitations
|
|
163
185
|
|
|
164
186
|
- interpolation path parser supports unquoted dot keys and numeric list indices
|
|
165
187
|
- no nested interpolation expressions
|
|
166
188
|
- `to_toml()` is not implemented yet
|
|
167
189
|
|
|
168
|
-
## TODO
|
|
169
|
-
|
|
170
|
-
- [ ] review the details of interpolation
|
|
171
|
-
- [ ] explain history with interpolation
|
|
172
|
-
|
|
173
190
|
## Release To PyPI
|
|
174
191
|
|
|
175
192
|
Build package:
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# tomlstack
|
|
2
|
+
|
|
3
|
+
`tomlstack` is a lightweight TOML config loader for Python 3.11+ with:
|
|
4
|
+
|
|
5
|
+
- top-level `include` loading
|
|
6
|
+
- include-tree inspection with raw and resolved paths
|
|
7
|
+
- deterministic merge by include order
|
|
8
|
+
- `${path}` interpolation with cycle/undefined checks
|
|
9
|
+
- node-level provenance (`origin`, `history`, `dependencies`)
|
|
10
|
+
|
|
11
|
+
tomlstack does not try to be a configuration framework.
|
|
12
|
+
It addresses two missing pieces in TOML: file composition and safe interpolation,
|
|
13
|
+
while keeping files self-contained and explainable.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install tomlstack
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
`main.toml`:
|
|
24
|
+
|
|
25
|
+
```toml
|
|
26
|
+
include = [
|
|
27
|
+
"./base.toml",
|
|
28
|
+
"./prod.toml",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[db]
|
|
32
|
+
url = "postgres://${db.user}:${db.pass}@${db.host}:${db.port}"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`base.toml`:
|
|
36
|
+
|
|
37
|
+
```toml
|
|
38
|
+
[db]
|
|
39
|
+
user = "alice"
|
|
40
|
+
pass = "secret"
|
|
41
|
+
host = "localhost"
|
|
42
|
+
port = 5432
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Python:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from tomlstack import TomlNode, load
|
|
49
|
+
|
|
50
|
+
cfg = load("main.toml")
|
|
51
|
+
print(cfg["db"]["url"].raw) # raw interpolation string
|
|
52
|
+
print(cfg["db"]["url"].value) # resolved value
|
|
53
|
+
print(cfg["db"]["url"].origin)
|
|
54
|
+
print(cfg["db"]["url"].history)
|
|
55
|
+
print(cfg["db"]["url"].dependencies)
|
|
56
|
+
print(cfg["db"]["url"].explain())
|
|
57
|
+
print(cfg.raw) # raw configuration snapshot
|
|
58
|
+
print(cfg.to_dict()) # resolved configuration snapshot
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Include Semantics
|
|
62
|
+
|
|
63
|
+
- top-level `include` only; nested `include` is treated as normal data
|
|
64
|
+
- syntax: string or list of strings
|
|
65
|
+
- valid include path forms:
|
|
66
|
+
- `./...` or `../...`
|
|
67
|
+
- `@label/...` (label from `tomlstack.anchors`)
|
|
68
|
+
- absolute path
|
|
69
|
+
- any other form raises an error with a path-format hint
|
|
70
|
+
|
|
71
|
+
### Meta Include Directives
|
|
72
|
+
|
|
73
|
+
```toml
|
|
74
|
+
[tomlstack.anchors]
|
|
75
|
+
proj = "./shared"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- anchors are local to the file that declares them
|
|
79
|
+
- anchor path values must be absolute or start with `./` or `../`
|
|
80
|
+
|
|
81
|
+
## Merge Rules
|
|
82
|
+
|
|
83
|
+
Load order for current file:
|
|
84
|
+
|
|
85
|
+
1. merge first include
|
|
86
|
+
2. merge second include
|
|
87
|
+
3. ...
|
|
88
|
+
4. merge current file (highest priority)
|
|
89
|
+
|
|
90
|
+
Conflict behavior:
|
|
91
|
+
|
|
92
|
+
- dict: recursive merge, later wins on key conflict
|
|
93
|
+
- list: later value replaces whole list
|
|
94
|
+
- scalar: later value replaces earlier
|
|
95
|
+
|
|
96
|
+
## Interpolation Semantics
|
|
97
|
+
|
|
98
|
+
- interpolation is resolved lazily by `cfg.resolve()`, `cfg.to_dict()`, or
|
|
99
|
+
`node.value`
|
|
100
|
+
- path syntax supports dot and list index: `${db.apps[0]}`
|
|
101
|
+
- full-string interpolation (`"${db.port}"`) keeps source type
|
|
102
|
+
- embedded interpolation (`"postgres://${db.host}:${db.port}"`) allows only:
|
|
103
|
+
- `str`, `int`, `float`, `bool`, `date`, `time`, `datetime`
|
|
104
|
+
- formatting syntax: `${path:spec}`
|
|
105
|
+
- for `date/time/datetime`, formatting uses `strftime`
|
|
106
|
+
- otherwise uses Python `format(value, spec)`
|
|
107
|
+
- invalid interpolation syntax and formatting failures raise `InterpolationError`
|
|
108
|
+
- undefined references raise `InterpolationUndefinedError`
|
|
109
|
+
- interpolation cycles raise `InterpolationCycleError`
|
|
110
|
+
|
|
111
|
+
Invalid TOML raises `TomlFormatError`; invalid include paths and anchors raise
|
|
112
|
+
`IncludeError`.
|
|
113
|
+
|
|
114
|
+
## Public API
|
|
115
|
+
|
|
116
|
+
- `cfg = load("f.toml")`
|
|
117
|
+
- `cfg.raw` — raw configuration snapshot
|
|
118
|
+
- `cfg.resolve()`
|
|
119
|
+
- `cfg.to_dict()` — resolved configuration snapshot
|
|
120
|
+
- `cfg.include_tree` — `IncludeNode` load-occurrence tree
|
|
121
|
+
- `cfg.include_tree.render()` — render raw include references
|
|
122
|
+
- `cfg.include_tree.render(absolute=True)` — include references with resolved paths
|
|
123
|
+
- `node: TomlNode = cfg["proj"][0]["path"]["foo"]`
|
|
124
|
+
- `node.raw`
|
|
125
|
+
- `node.value`
|
|
126
|
+
- `node.origin`
|
|
127
|
+
- `node.history`
|
|
128
|
+
- `node.dependencies` — direct interpolation dependencies
|
|
129
|
+
- `node.explain()` — transitive interpolation trace
|
|
130
|
+
- `node.preview()`
|
|
131
|
+
- `cfg.to_toml()` -> `NotImplementedError`
|
|
132
|
+
|
|
133
|
+
`cfg.raw`, `cfg.to_dict()`, `node.raw`, and `node.value` return independent data
|
|
134
|
+
snapshots; mutating their dictionaries or lists does not modify the loaded
|
|
135
|
+
configuration. `TomlNode` instances are created by configuration navigation and are
|
|
136
|
+
not constructed directly.
|
|
137
|
+
|
|
138
|
+
History records definitions of the same data path from lowest to highest priority.
|
|
139
|
+
When a list or value type is replaced, its old child paths are discarded. Resolving an
|
|
140
|
+
interpolation does not change the history of the node containing the expression.
|
|
141
|
+
Each history entry is a `TomlFile` with its raw `reference` and resolved absolute
|
|
142
|
+
`path`.
|
|
143
|
+
|
|
144
|
+
Dependencies describe interpolation separately from merge history. A full replacement
|
|
145
|
+
such as `target = "${source}"` leaves `target.history` at the file that defined
|
|
146
|
+
`target`, while `target.dependencies` records the source path and its history.
|
|
147
|
+
|
|
148
|
+
## Current Limitations
|
|
149
|
+
|
|
150
|
+
- interpolation path parser supports unquoted dot keys and numeric list indices
|
|
151
|
+
- no nested interpolation expressions
|
|
152
|
+
- `to_toml()` is not implemented yet
|
|
153
|
+
|
|
154
|
+
## Release To PyPI
|
|
155
|
+
|
|
156
|
+
Build package:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
uv run --with build python -m build
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Upload to TestPyPI first:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
uv run --with twine python -m twine upload --repository testpypi dist/*
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Verify install from TestPyPI:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
python -m pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple tomlstack
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Upload to PyPI:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
uv run --with twine python -m twine upload dist/*
|
|
178
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .api import TomlStack, load
|
|
2
|
+
from .nodes import TomlNode
|
|
3
|
+
from .types import IncludeNode, InterpolationDependency, ResolutionTrace, TraceNode
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"IncludeNode",
|
|
7
|
+
"InterpolationDependency",
|
|
8
|
+
"ResolutionTrace",
|
|
9
|
+
"TomlStack",
|
|
10
|
+
"TomlNode",
|
|
11
|
+
"TraceNode",
|
|
12
|
+
"load",
|
|
13
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.2.0rc1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0, 'rc1')
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from os import PathLike
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .interpolate import _ResolutionResult, _resolve_interpolations
|
|
9
|
+
from .loader import _load_toml_with_includes, _LoadedToml
|
|
10
|
+
from .nodes import TomlNode
|
|
11
|
+
from .path_expr import get_by_path
|
|
12
|
+
from .trace import _build_resolution_trace
|
|
13
|
+
from .tree import _DataNode
|
|
14
|
+
from .types import (
|
|
15
|
+
DataPath,
|
|
16
|
+
IncludeNode,
|
|
17
|
+
InterpolationDependency,
|
|
18
|
+
ResolutionTrace,
|
|
19
|
+
TomlFile,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(init=False)
|
|
24
|
+
class TomlStack:
|
|
25
|
+
_root: _DataNode
|
|
26
|
+
_include_tree: IncludeNode
|
|
27
|
+
_resolution: _ResolutionResult | None = field(
|
|
28
|
+
init=False, default=None, repr=False, compare=False, hash=False
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def __init__(self, *_: object, **__: object) -> None:
|
|
32
|
+
raise TypeError("TomlStack instances are created by load()")
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def _from_loaded(cls, loaded: _LoadedToml) -> TomlStack:
|
|
36
|
+
stack = object.__new__(cls)
|
|
37
|
+
stack._root = loaded.root
|
|
38
|
+
stack._include_tree = loaded.include_tree
|
|
39
|
+
stack._resolution = None
|
|
40
|
+
return stack
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def raw(self) -> Any:
|
|
44
|
+
return deepcopy(self._root.materialized)
|
|
45
|
+
|
|
46
|
+
def resolve(self) -> TomlStack:
|
|
47
|
+
if self._resolution is None:
|
|
48
|
+
self._resolution = _resolve_interpolations(self._root)
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
def to_dict(self) -> dict[str, Any]:
|
|
52
|
+
self.resolve()
|
|
53
|
+
assert self._resolution is not None
|
|
54
|
+
return deepcopy(self._resolution.data)
|
|
55
|
+
|
|
56
|
+
def to_toml(self) -> str:
|
|
57
|
+
raise NotImplementedError("to_toml is reserved for future implementation")
|
|
58
|
+
|
|
59
|
+
def __getitem__(self, key: str) -> TomlNode:
|
|
60
|
+
if not isinstance(self._root.value, dict) or key not in self._root.value:
|
|
61
|
+
raise KeyError(key)
|
|
62
|
+
return TomlNode._from_path(self, (key,))
|
|
63
|
+
|
|
64
|
+
def _get_raw(self, path: DataPath) -> Any:
|
|
65
|
+
return deepcopy(self._root._get_subnode(path).materialized)
|
|
66
|
+
|
|
67
|
+
def _get_history(self, path: DataPath) -> tuple[TomlFile, ...]:
|
|
68
|
+
return self._root._get_subnode(path).history
|
|
69
|
+
|
|
70
|
+
def _get_value(self, path: DataPath) -> Any:
|
|
71
|
+
self.resolve()
|
|
72
|
+
assert self._resolution is not None
|
|
73
|
+
return deepcopy(get_by_path(self._resolution.data, path))
|
|
74
|
+
|
|
75
|
+
def _get_dependencies(self, path: DataPath) -> tuple[InterpolationDependency, ...]:
|
|
76
|
+
self.resolve()
|
|
77
|
+
assert self._resolution is not None
|
|
78
|
+
return self._resolution.direct_dependencies.get(path, ())
|
|
79
|
+
|
|
80
|
+
def _get_trace(self, path: DataPath) -> ResolutionTrace:
|
|
81
|
+
self.resolve()
|
|
82
|
+
assert self._resolution is not None
|
|
83
|
+
return _build_resolution_trace(
|
|
84
|
+
self._root,
|
|
85
|
+
path,
|
|
86
|
+
self._resolution.direct_dependencies,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def include_tree(self) -> IncludeNode:
|
|
91
|
+
return self._include_tree
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def load(path: str | PathLike[str]) -> TomlStack:
|
|
95
|
+
return TomlStack._from_loaded(_load_toml_with_includes(path))
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
class TomlStackError(Exception):
|
|
2
|
+
"""Base error for tomlstack."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DataPathError(TomlStackError):
|
|
6
|
+
"""Raised when path parsing or resolution fails."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ContentError(TomlStackError):
|
|
10
|
+
"""Raised when content validation fails."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class IncludeError(TomlStackError):
|
|
14
|
+
"""Raised when include resolution fails."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TomlFormatError(TomlStackError):
|
|
18
|
+
"""Raised when TOML parsing fails."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class IncludeCycleError(IncludeError):
|
|
22
|
+
"""Raised when include cycle is detected."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class InterpolationError(TomlStackError):
|
|
26
|
+
"""Raised when interpolation fails."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class InterpolationUndefinedError(InterpolationError):
|
|
30
|
+
"""Raised when interpolation path is undefined."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class InterpolationCycleError(InterpolationError):
|
|
34
|
+
"""Raised when interpolation cycle is detected."""
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Self
|
|
4
|
+
|
|
5
|
+
from .errors import IncludeError
|
|
6
|
+
from .types import TomlFile
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class _IncludeResolver:
|
|
11
|
+
including_file: TomlFile
|
|
12
|
+
anchor_roots: dict[str, Path]
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def _is_relative(path: str) -> bool:
|
|
16
|
+
return path.startswith("./") or path.startswith("../")
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def _is_absolute(path: str) -> bool:
|
|
20
|
+
return Path(path).is_absolute()
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def _is_anchor_path(path: str) -> bool:
|
|
24
|
+
if not path.startswith("@"):
|
|
25
|
+
return False
|
|
26
|
+
label, sep, value = path[1:].partition("/")
|
|
27
|
+
return bool(label and sep and value)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def resolve_anchor_path(cls, base_dir: Path, anchor_path: str) -> Path:
|
|
31
|
+
if cls._is_absolute(anchor_path):
|
|
32
|
+
return Path(anchor_path).resolve()
|
|
33
|
+
if cls._is_relative(anchor_path):
|
|
34
|
+
return (base_dir / anchor_path).resolve()
|
|
35
|
+
raise IncludeError(
|
|
36
|
+
f"Invalid anchor path: {anchor_path}. "
|
|
37
|
+
"Anchor values must be absolute or start with ./ or ../"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def resolve_include_path(self, include_path: str) -> Path:
|
|
41
|
+
if self._is_absolute(include_path):
|
|
42
|
+
return Path(include_path).resolve()
|
|
43
|
+
|
|
44
|
+
if self._is_relative(include_path):
|
|
45
|
+
return (self.including_file.path.parent / include_path).resolve()
|
|
46
|
+
|
|
47
|
+
if self._is_anchor_path(include_path):
|
|
48
|
+
label, _, rest = include_path[1:].partition("/")
|
|
49
|
+
if label not in self.anchor_roots:
|
|
50
|
+
raise IncludeError(f"Undefined include anchor: {label}")
|
|
51
|
+
return (self.anchor_roots[label] / rest).resolve()
|
|
52
|
+
|
|
53
|
+
raise IncludeError(
|
|
54
|
+
f"Invalid include path format: {include_path}. "
|
|
55
|
+
"Use ./ or ../ or @label/ or absolute path."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_toml(cls, file: TomlFile, raw_anchors: dict[str, str]) -> Self:
|
|
60
|
+
anchor_roots: dict[str, Path] = {}
|
|
61
|
+
base_dir = file.path.parent
|
|
62
|
+
|
|
63
|
+
for label, raw_value in raw_anchors.items():
|
|
64
|
+
anchor_roots[label] = cls.resolve_anchor_path(base_dir, raw_value)
|
|
65
|
+
|
|
66
|
+
return cls(including_file=file, anchor_roots=anchor_roots)
|