tomlstack 0.1.2__tar.gz → 0.2.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.
Files changed (43) hide show
  1. {tomlstack-0.1.2 → tomlstack-0.2.0}/.github/workflows/publish-pypi.yml +9 -2
  2. {tomlstack-0.1.2 → tomlstack-0.2.0}/.gitignore +4 -1
  3. {tomlstack-0.1.2 → tomlstack-0.2.0}/CHANGELOG.md +25 -0
  4. {tomlstack-0.1.2 → tomlstack-0.2.0}/PKG-INFO +49 -32
  5. tomlstack-0.2.0/README.md +178 -0
  6. tomlstack-0.2.0/src/tomlstack/__init__.py +13 -0
  7. tomlstack-0.2.0/src/tomlstack/_version.py +24 -0
  8. tomlstack-0.2.0/src/tomlstack/api.py +95 -0
  9. tomlstack-0.2.0/src/tomlstack/errors.py +34 -0
  10. tomlstack-0.2.0/src/tomlstack/include.py +66 -0
  11. tomlstack-0.2.0/src/tomlstack/interpolate.py +234 -0
  12. tomlstack-0.2.0/src/tomlstack/loader.py +156 -0
  13. tomlstack-0.2.0/src/tomlstack/nodes.py +109 -0
  14. tomlstack-0.2.0/src/tomlstack/path_expr.py +160 -0
  15. tomlstack-0.2.0/src/tomlstack/trace.py +62 -0
  16. tomlstack-0.2.0/src/tomlstack/tree.py +54 -0
  17. tomlstack-0.2.0/src/tomlstack/types.py +71 -0
  18. tomlstack-0.2.0/tests/test_include_merge.py +480 -0
  19. tomlstack-0.2.0/tests/test_interpolation.py +314 -0
  20. tomlstack-0.2.0/tests/test_loader_model.py +32 -0
  21. {tomlstack-0.1.2 → tomlstack-0.2.0}/tests/test_nodes.py +48 -13
  22. tomlstack-0.2.0/tests/test_path_expr.py +55 -0
  23. {tomlstack-0.1.2 → tomlstack-0.2.0}/tests/test_smoke.py +8 -1
  24. {tomlstack-0.1.2 → tomlstack-0.2.0}/uv.lock +204 -188
  25. tomlstack-0.1.2/README.md +0 -161
  26. tomlstack-0.1.2/src/tomlstack/__init__.py +0 -3
  27. tomlstack-0.1.2/src/tomlstack/_version.py +0 -34
  28. tomlstack-0.1.2/src/tomlstack/api.py +0 -81
  29. tomlstack-0.1.2/src/tomlstack/base.py +0 -29
  30. tomlstack-0.1.2/src/tomlstack/errors.py +0 -46
  31. tomlstack-0.1.2/src/tomlstack/include.py +0 -121
  32. tomlstack-0.1.2/src/tomlstack/interpolate.py +0 -124
  33. tomlstack-0.1.2/src/tomlstack/loader.py +0 -202
  34. tomlstack-0.1.2/src/tomlstack/nodes.py +0 -89
  35. tomlstack-0.1.2/src/tomlstack/path_expr.py +0 -78
  36. tomlstack-0.1.2/tests/test_include_merge.py +0 -161
  37. tomlstack-0.1.2/tests/test_interpolation.py +0 -94
  38. {tomlstack-0.1.2 → tomlstack-0.2.0}/.github/workflows/ci.yml +0 -0
  39. {tomlstack-0.1.2 → tomlstack-0.2.0}/.github/workflows/publish-testpypi.yml +0 -0
  40. {tomlstack-0.1.2 → tomlstack-0.2.0}/CONTRIBUTING.md +0 -0
  41. {tomlstack-0.1.2 → tomlstack-0.2.0}/LICENSE +0 -0
  42. {tomlstack-0.1.2 → tomlstack-0.2.0}/RELEASE.md +0 -0
  43. {tomlstack-0.1.2 → tomlstack-0.2.0}/pyproject.toml +0 -0
@@ -3,7 +3,12 @@ name: Publish PyPI
3
3
  on:
4
4
  push:
5
5
  tags:
6
- - "v*"
6
+ # Final releases only. Release candidates are routed to TestPyPI below.
7
+ - "v*"
8
+ - "!v*rc*"
9
+ - "!v*alpha*"
10
+ - "!v*beta*"
11
+ - "!v*dev*"
7
12
 
8
13
  jobs:
9
14
  publish:
@@ -11,7 +16,9 @@ jobs:
11
16
  runs-on: ubuntu-latest
12
17
  environment: PyPI
13
18
  permissions:
14
- contents: read
19
+ # Creating the GitHub Release and uploading dist/* both write repository
20
+ # contents. id-token is used by PyPI trusted publishing.
21
+ contents: write
15
22
  id-token: write
16
23
 
17
24
  steps:
@@ -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.1.2
3
+ Version: 0.2.0
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`, `explain`, `history`)
45
+ - node-level provenance (`origin`, `history`, `dependencies`)
45
46
 
46
47
  tomlstack does not try to be a configuration framework.
47
- It address two missing pieces to TOML: file composition and safe interpolation — while keeping files self-contained and explainable.
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.to_dict())
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 `__meta__.include.anchors`)
100
- - absolute path
101
- - any other form raises error with hint: `Use ./ or ../ or @label/`
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
- [__meta__]
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
- - `__meta__.include.root` is sugar for `anchors.root`
117
- - if both `root` and `anchors.root` exist and resolve differently, error
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 happens on `cfg.resolve()`
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
- - undefined reference raises `InterpolationUndefinedError`
147
- - interpolation cycle raises `InterpolationCycleError`
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
- - `node = cfg["proj"][0]["path"]["foo"]`
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.0'
22
+ __version_tuple__ = version_tuple = (0, 2, 0)
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)