yamlsmith 0.1.1__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.
- yamlsmith-0.1.1/.github/dependabot.yml +10 -0
- yamlsmith-0.1.1/.github/workflows/ci.yml +41 -0
- yamlsmith-0.1.1/.github/workflows/publish.yml +20 -0
- yamlsmith-0.1.1/.gitignore +14 -0
- yamlsmith-0.1.1/CHANGELOG.md +32 -0
- yamlsmith-0.1.1/Makefile +22 -0
- yamlsmith-0.1.1/PKG-INFO +377 -0
- yamlsmith-0.1.1/PLAN.md +97 -0
- yamlsmith-0.1.1/README.md +356 -0
- yamlsmith-0.1.1/pyproject.toml +49 -0
- yamlsmith-0.1.1/src/yamlsmith/__init__.py +32 -0
- yamlsmith-0.1.1/src/yamlsmith/api.py +115 -0
- yamlsmith-0.1.1/src/yamlsmith/composer.py +177 -0
- yamlsmith-0.1.1/src/yamlsmith/constructor.py +250 -0
- yamlsmith-0.1.1/src/yamlsmith/emitter.py +291 -0
- yamlsmith-0.1.1/src/yamlsmith/errors.py +27 -0
- yamlsmith-0.1.1/src/yamlsmith/nodes.py +48 -0
- yamlsmith-0.1.1/src/yamlsmith/parser.py +453 -0
- yamlsmith-0.1.1/src/yamlsmith/py.typed +0 -0
- yamlsmith-0.1.1/src/yamlsmith/representer.py +156 -0
- yamlsmith-0.1.1/src/yamlsmith/roundtrip.py +101 -0
- yamlsmith-0.1.1/src/yamlsmith/scanner.py +886 -0
- yamlsmith-0.1.1/tests/__init__.py +0 -0
- yamlsmith-0.1.1/tests/test_api.py +273 -0
- yamlsmith-0.1.1/tests/test_composer.py +168 -0
- yamlsmith-0.1.1/tests/test_constructor.py +174 -0
- yamlsmith-0.1.1/tests/test_emitter.py +311 -0
- yamlsmith-0.1.1/tests/test_parser.py +151 -0
- yamlsmith-0.1.1/tests/test_representer.py +144 -0
- yamlsmith-0.1.1/tests/test_scanner.py +302 -0
- yamlsmith-0.1.1/uv.lock +336 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
os: [ubuntu-latest, macos-latest]
|
|
15
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
16
|
+
fail-fast: false
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v6
|
|
19
|
+
- uses: actions/setup-python@v6
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
- name: Install uv
|
|
23
|
+
uses: astral-sh/setup-uv@v7
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: uv sync
|
|
26
|
+
- name: Run tests
|
|
27
|
+
run: uv run pytest tests/ -v
|
|
28
|
+
|
|
29
|
+
typecheck:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v6
|
|
33
|
+
- uses: actions/setup-python@v6
|
|
34
|
+
with:
|
|
35
|
+
python-version: "3.13"
|
|
36
|
+
- name: Install uv
|
|
37
|
+
uses: astral-sh/setup-uv@v7
|
|
38
|
+
- name: Install dependencies
|
|
39
|
+
run: uv sync
|
|
40
|
+
- name: Run mypy
|
|
41
|
+
run: uv run mypy src/ --strict
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v6
|
|
15
|
+
- name: Install uv
|
|
16
|
+
uses: astral-sh/setup-uv@v7
|
|
17
|
+
- name: Build distribution
|
|
18
|
+
run: uv build
|
|
19
|
+
- name: Publish to PyPI
|
|
20
|
+
run: uv publish
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to yamlsmith are documented here.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## v0.1.1 (2026-03-15)
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Pre-comment placement:** Pre-comments (standalone comment lines before a key) were incorrectly emitted as inline comments on the previous key line. They are now preserved on their own line above the key.
|
|
12
|
+
- **Integer/float/bool round-trip quoting:** Plain scalars such as `5432` were quoted after a load→dump cycle. The constructor now stores `None` as the tag for implicitly resolved non-string types so the representer derives the correct tag from the Python type rather than a stale string tag stored in `RoundTripScalar`.
|
|
13
|
+
- **Plain scalar quoting:** Unquoted scalars that resolve to non-string types no longer receive spurious quotes on re-emit.
|
|
14
|
+
|
|
15
|
+
## v0.1.0 (2026-03-14)
|
|
16
|
+
|
|
17
|
+
Initial release.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- YAML 1.2 scanner/tokenizer with comment metadata
|
|
22
|
+
- Event-stream parser
|
|
23
|
+
- Node graph composer with anchor/alias resolution
|
|
24
|
+
- Constructor: YAML nodes → Python objects (`str`, `int`, `float`, `bool`, `None`, `datetime`, `bytes`)
|
|
25
|
+
- Representer: Python objects → YAML nodes
|
|
26
|
+
- Emitter with comment preservation and block/flow style support
|
|
27
|
+
- Round-trip types: `RoundTripDict`, `RoundTripList`, `RoundTripScalar`
|
|
28
|
+
- Public API: `YAML` class and `load` / `dump` / `load_all` / `dump_all` convenience functions
|
|
29
|
+
- Error hierarchy: `YAMLError`, `ScannerError`, `ParserError`, `ComposerError`, `ConstructorError`, `EmitterError`
|
|
30
|
+
- PEP 561 typed package (`py.typed` marker)
|
|
31
|
+
- Python 3.10+ support
|
|
32
|
+
- Zero runtime dependencies
|
yamlsmith-0.1.1/Makefile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
.PHONY: install build test lint fmt clean ci
|
|
2
|
+
|
|
3
|
+
install:
|
|
4
|
+
uv sync
|
|
5
|
+
|
|
6
|
+
build:
|
|
7
|
+
uv build
|
|
8
|
+
|
|
9
|
+
test:
|
|
10
|
+
uv run pytest tests/ -v
|
|
11
|
+
|
|
12
|
+
lint:
|
|
13
|
+
uv run ruff check src/
|
|
14
|
+
uv run mypy src/
|
|
15
|
+
|
|
16
|
+
fmt:
|
|
17
|
+
uv run ruff format src/ tests/
|
|
18
|
+
|
|
19
|
+
clean:
|
|
20
|
+
rm -rf dist/ build/ *.egg-info/ .pytest_cache/ .mypy_cache/
|
|
21
|
+
|
|
22
|
+
ci: lint test
|
yamlsmith-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yamlsmith
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Round-trip YAML 1.2 library with comment preservation — a modern ruamel.yaml replacement
|
|
5
|
+
Project-URL: Homepage, https://github.com/agentine/yamlsmith
|
|
6
|
+
Project-URL: Repository, https://github.com/agentine/yamlsmith
|
|
7
|
+
Project-URL: Issue Tracker, https://github.com/agentine/yamlsmith/issues
|
|
8
|
+
Author: Agentine
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# yamlsmith
|
|
23
|
+
|
|
24
|
+
[](https://pypi.org/project/yamlsmith/)
|
|
25
|
+
[](https://pypi.org/project/yamlsmith/)
|
|
26
|
+
[](LICENSE)
|
|
27
|
+
[](https://mypy-lang.org/)
|
|
28
|
+
|
|
29
|
+
Round-trip YAML 1.2 library with comment preservation — a modern, drop-in replacement for ruamel.yaml.
|
|
30
|
+
|
|
31
|
+
Pure Python, zero dependencies, fully typed ([PEP 561](https://peps.python.org/pep-0561/)), Python 3.10+.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Why yamlsmith?
|
|
36
|
+
|
|
37
|
+
[ruamel.yaml](https://pypi.org/project/ruamel.yaml/) has 155M+ monthly downloads and is the only production-ready Python library for round-trip YAML with comment preservation. But it carries significant risk:
|
|
38
|
+
|
|
39
|
+
- **Bus factor = 1.** Single maintainer with no governance structure.
|
|
40
|
+
- **PEP 625 crisis.** The namespace package naming may prevent continued PyPI uploads.
|
|
41
|
+
- **Hostile contribution model.** SourceForge/Mercurial hosting makes community contribution nearly impossible.
|
|
42
|
+
- **Failed fork.** ruyaml (pycontribs) was created to address these risks and itself stalled.
|
|
43
|
+
|
|
44
|
+
yamlsmith solves this: same round-trip semantics, modern codebase, zero dependencies, YAML 1.2 strict mode (no legacy boolean quirks from 1.1).
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install yamlsmith
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
### Load and modify without losing comments
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from yamlsmith import load, dump
|
|
62
|
+
|
|
63
|
+
text = """\
|
|
64
|
+
# Database configuration
|
|
65
|
+
host: localhost
|
|
66
|
+
port: 5432 # default PostgreSQL port
|
|
67
|
+
enabled: true
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
data = load(text)
|
|
71
|
+
data["port"] = 5433 # modify a value
|
|
72
|
+
data["timeout"] = 30 # add a new key
|
|
73
|
+
|
|
74
|
+
print(dump(data))
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Output — comments preserved, structure intact:
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
# Database configuration
|
|
81
|
+
host: localhost
|
|
82
|
+
port: 5433 # default PostgreSQL port
|
|
83
|
+
enabled: true
|
|
84
|
+
timeout: 30
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Multi-document streams
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from yamlsmith import load_all, dump_all
|
|
91
|
+
|
|
92
|
+
text = """\
|
|
93
|
+
# Document 1
|
|
94
|
+
name: alice
|
|
95
|
+
---
|
|
96
|
+
# Document 2
|
|
97
|
+
name: bob
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
docs = load_all(text)
|
|
101
|
+
docs[0]["name"] = "ALICE"
|
|
102
|
+
print(dump_all(docs))
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### File I/O
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from yamlsmith import YAML
|
|
109
|
+
|
|
110
|
+
yaml = YAML()
|
|
111
|
+
|
|
112
|
+
with open("config.yaml") as f:
|
|
113
|
+
data = yaml.load(f)
|
|
114
|
+
|
|
115
|
+
data["version"] = "2.0"
|
|
116
|
+
|
|
117
|
+
with open("config.yaml", "w") as f:
|
|
118
|
+
yaml.dump(data, f)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## API Reference
|
|
124
|
+
|
|
125
|
+
### Convenience functions
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from yamlsmith import load, dump, load_all, dump_all
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
| Function | Signature | Description |
|
|
132
|
+
|---|---|---|
|
|
133
|
+
| `load` | `(text: str \| bytes \| IO) → Any` | Load a single YAML document |
|
|
134
|
+
| `dump` | `(data: Any, stream: IO \| None = None) → str` | Dump to YAML string (optionally also write to stream) |
|
|
135
|
+
| `load_all` | `(text: str \| bytes \| IO) → list[Any]` | Load all documents from a multi-document stream |
|
|
136
|
+
| `dump_all` | `(data: list[Any], stream: IO \| None = None) → str` | Dump a list of documents separated by `---` |
|
|
137
|
+
|
|
138
|
+
All functions use round-trip mode: mappings and sequences are loaded as `RoundTripDict` / `RoundTripList` with comment metadata attached.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
### `YAML` class
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from yamlsmith import YAML
|
|
146
|
+
|
|
147
|
+
yaml = YAML(indent=2, default_flow_style=False)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Constructor parameters:**
|
|
151
|
+
|
|
152
|
+
| Parameter | Type | Default | Description |
|
|
153
|
+
|---|---|---|---|
|
|
154
|
+
| `indent` | `int` | `2` | Indentation width for block mappings and sequences |
|
|
155
|
+
| `default_flow_style` | `bool` | `False` | Emit flow style (`{a: 1}`) instead of block style by default |
|
|
156
|
+
|
|
157
|
+
**Methods:**
|
|
158
|
+
|
|
159
|
+
| Method | Signature | Description |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `load` | `(stream: str \| bytes \| IO) → Any` | Load a single YAML document |
|
|
162
|
+
| `dump` | `(data: Any, stream: IO \| None = None) → str` | Dump to YAML string, optionally also write to stream |
|
|
163
|
+
| `load_all` | `(stream: str \| bytes \| IO) → list[Any]` | Load all documents from a stream |
|
|
164
|
+
| `dump_all` | `(data: list[Any], stream: IO \| None = None) → str` | Dump multiple documents with `---` separators |
|
|
165
|
+
|
|
166
|
+
Streams may be `str`, `bytes`, or any file-like object with a `.read()` / `.write()` method.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### Round-trip types
|
|
171
|
+
|
|
172
|
+
When you load YAML, mappings and sequences are returned as round-trip types that carry comment metadata:
|
|
173
|
+
|
|
174
|
+
#### `RoundTripDict`
|
|
175
|
+
|
|
176
|
+
A `dict` subclass preserving insertion order and YAML comments.
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from yamlsmith import RoundTripDict
|
|
180
|
+
|
|
181
|
+
d = RoundTripDict({"a": 1, "b": 2})
|
|
182
|
+
|
|
183
|
+
# Attach comments to a key
|
|
184
|
+
d.set_comment("a", pre="# section header", inline="# inline note")
|
|
185
|
+
|
|
186
|
+
# Read comments back
|
|
187
|
+
pre, inline = d.get_comment("a")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
| Method | Signature | Description |
|
|
191
|
+
|---|---|---|
|
|
192
|
+
| `set_comment` | `(key, *, pre=None, inline=None)` | Attach a pre-comment and/or inline comment to `key` |
|
|
193
|
+
| `get_comment` | `(key) → tuple[str \| None, str \| None]` | Return `(pre_comment, inline_comment)` for `key` |
|
|
194
|
+
|
|
195
|
+
#### `RoundTripList`
|
|
196
|
+
|
|
197
|
+
A `list` subclass preserving YAML comments per item.
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from yamlsmith import RoundTripList
|
|
201
|
+
|
|
202
|
+
lst = RoundTripList([1, 2, 3])
|
|
203
|
+
|
|
204
|
+
# Attach a comment to item at index 0
|
|
205
|
+
lst.set_item_comment(0, pre="# first item", inline="# note")
|
|
206
|
+
|
|
207
|
+
pre, inline = lst.get_item_comment(0)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
| Method | Signature | Description |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| `set_item_comment` | `(index, *, pre=None, inline=None)` | Attach a pre-comment and/or inline comment to item `index` |
|
|
213
|
+
| `get_item_comment` | `(index) → tuple[str \| None, str \| None]` | Return `(pre_comment, inline_comment)` for item `index` |
|
|
214
|
+
|
|
215
|
+
#### `RoundTripScalar`
|
|
216
|
+
|
|
217
|
+
A wrapper for a scalar value that carries comment metadata and style information. Returned by the loader when a scalar has an inline comment or non-default quoting style.
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
from yamlsmith import RoundTripScalar
|
|
221
|
+
|
|
222
|
+
s = RoundTripScalar(42, inline_comment="# answer", style="plain")
|
|
223
|
+
print(s.value) # 42
|
|
224
|
+
print(s == 42) # True — compares by .value
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
| Attribute | Type | Description |
|
|
228
|
+
|---|---|---|
|
|
229
|
+
| `value` | `Any` | The underlying Python value |
|
|
230
|
+
| `pre_comment` | `str \| None` | Comment line(s) before the scalar |
|
|
231
|
+
| `inline_comment` | `str \| None` | Comment after the value on the same line |
|
|
232
|
+
| `style` | `str \| None` | Scalar style: `plain`, `single`, `double`, `literal`, `folded` |
|
|
233
|
+
| `tag` | `str \| None` | Explicit YAML tag, or `None` for implicit resolution |
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
### Error types
|
|
238
|
+
|
|
239
|
+
All errors inherit from `YAMLError`.
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
from yamlsmith import YAMLError, ScannerError, ParserError, ComposerError, ConstructorError, EmitterError
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
data = load("key: :")
|
|
246
|
+
except YAMLError as e:
|
|
247
|
+
print(f"YAML error: {e}")
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
| Exception | Raised when |
|
|
251
|
+
|---|---|
|
|
252
|
+
| `YAMLError` | Base class for all yamlsmith errors |
|
|
253
|
+
| `ScannerError` | Invalid character or token in the input stream |
|
|
254
|
+
| `ParserError` | Structurally invalid YAML (bad nesting, missing values) |
|
|
255
|
+
| `ComposerError` | Undefined alias reference (`*anchor` without `&anchor`) |
|
|
256
|
+
| `ConstructorError` | Unknown YAML tag or type conversion failure |
|
|
257
|
+
| `EmitterError` | Serialization failure during emit |
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Comment Preservation
|
|
262
|
+
|
|
263
|
+
yamlsmith attaches comments to the nearest YAML node:
|
|
264
|
+
|
|
265
|
+
| Comment type | Example | Stored on |
|
|
266
|
+
|---|---|---|
|
|
267
|
+
| Pre-comment | `# header` on its own line before a key | the key's node |
|
|
268
|
+
| Inline comment | `value # note` after a value | the value's node |
|
|
269
|
+
| Post-comment | trailing comment after a block | the block node |
|
|
270
|
+
| Document comment | comment before `---` or after `...` | the document node |
|
|
271
|
+
|
|
272
|
+
Comments survive load → modify → dump cycles as long as the node they are attached to is not replaced with a plain Python object. If you replace a `RoundTripDict` with a plain `dict`, its comments are discarded.
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## YAML 1.2 Strict Mode
|
|
277
|
+
|
|
278
|
+
yamlsmith implements **YAML 1.2 only**. Legacy YAML 1.1 boolean strings are treated as plain strings:
|
|
279
|
+
|
|
280
|
+
| Expression | ruamel.yaml (1.1) | yamlsmith (1.2) |
|
|
281
|
+
|---|---|---|
|
|
282
|
+
| `yes` | `True` | `"yes"` |
|
|
283
|
+
| `no` | `False` | `"no"` |
|
|
284
|
+
| `on` | `True` | `"on"` |
|
|
285
|
+
| `off` | `False` | `"off"` |
|
|
286
|
+
| `true` | `True` | `True` |
|
|
287
|
+
| `false` | `False` | `False` |
|
|
288
|
+
|
|
289
|
+
Only `true`/`True`/`TRUE` and `false`/`False`/`FALSE` are recognised as booleans.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Migration from ruamel.yaml
|
|
294
|
+
|
|
295
|
+
### Import changes
|
|
296
|
+
|
|
297
|
+
| ruamel.yaml | yamlsmith |
|
|
298
|
+
|---|---|
|
|
299
|
+
| `from ruamel.yaml import YAML` | `from yamlsmith import YAML` |
|
|
300
|
+
| `from ruamel.yaml.comments import CommentedMap` | `from yamlsmith import RoundTripDict` |
|
|
301
|
+
| `from ruamel.yaml.comments import CommentedSeq` | `from yamlsmith import RoundTripList` |
|
|
302
|
+
|
|
303
|
+
### API compatibility
|
|
304
|
+
|
|
305
|
+
The core `YAML` class API is identical:
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
# ruamel.yaml
|
|
309
|
+
from ruamel.yaml import YAML
|
|
310
|
+
yaml = YAML()
|
|
311
|
+
data = yaml.load(stream)
|
|
312
|
+
yaml.dump(data, stream)
|
|
313
|
+
|
|
314
|
+
# yamlsmith — same calls
|
|
315
|
+
from yamlsmith import YAML
|
|
316
|
+
yaml = YAML()
|
|
317
|
+
data = yaml.load(stream)
|
|
318
|
+
yaml.dump(data, stream)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Type name changes
|
|
322
|
+
|
|
323
|
+
| ruamel.yaml | yamlsmith | Notes |
|
|
324
|
+
|---|---|---|
|
|
325
|
+
| `CommentedMap` | `RoundTripDict` | Same dict semantics |
|
|
326
|
+
| `CommentedSeq` | `RoundTripList` | Same list semantics |
|
|
327
|
+
| `CommentedSeq` | `RoundTripList` | Same list semantics |
|
|
328
|
+
| `scalarstring.*` | `RoundTripScalar(style=...)` | Unified scalar wrapper |
|
|
329
|
+
|
|
330
|
+
### Behaviour differences
|
|
331
|
+
|
|
332
|
+
| Behaviour | ruamel.yaml | yamlsmith |
|
|
333
|
+
|---|---|---|
|
|
334
|
+
| YAML spec | 1.1 + 1.2 hybrid | 1.2 strict |
|
|
335
|
+
| `yes`/`no`/`on`/`off` | booleans | plain strings |
|
|
336
|
+
| `dump()` return value | `None` (writes to stream) | always returns `str` |
|
|
337
|
+
| Initialisation | `YAML(typ="rt")` for round-trip | round-trip is the only mode |
|
|
338
|
+
| Python object tags | `!!python/object` supported | not supported (safe by default) |
|
|
339
|
+
|
|
340
|
+
### dump() return value
|
|
341
|
+
|
|
342
|
+
ruamel.yaml's `dump()` writes to a stream and returns `None`. yamlsmith's `dump()` always returns the YAML string and optionally also writes to the stream if one is provided:
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
# ruamel.yaml
|
|
346
|
+
import io
|
|
347
|
+
buf = io.StringIO()
|
|
348
|
+
yaml.dump(data, buf)
|
|
349
|
+
text = buf.getvalue()
|
|
350
|
+
|
|
351
|
+
# yamlsmith — simpler
|
|
352
|
+
text = yaml.dump(data) # or:
|
|
353
|
+
text = yaml.dump(data, stream=f) # also writes to f
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Features
|
|
359
|
+
|
|
360
|
+
- YAML 1.2 strict mode (no legacy 1.1 boolean quirks)
|
|
361
|
+
- Round-trip comment preservation (pre, inline, post, document-level)
|
|
362
|
+
- Block and flow style preservation
|
|
363
|
+
- Anchor and alias support
|
|
364
|
+
- Multi-document streams (`load_all` / `dump_all`)
|
|
365
|
+
- Literal (`|`) and folded (`>`) block scalars
|
|
366
|
+
- All standard YAML types: `str`, `int`, `float`, `bool`, `null`, `datetime`, `binary`
|
|
367
|
+
- Octal (`0o777`) and hexadecimal (`0xFF`) integer literals
|
|
368
|
+
- `inf`, `-inf`, `.nan` float values
|
|
369
|
+
- Full type annotations, mypy `--strict` clean
|
|
370
|
+
- PEP 561 typed package
|
|
371
|
+
- Zero dependencies
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## License
|
|
376
|
+
|
|
377
|
+
[MIT](LICENSE)
|
yamlsmith-0.1.1/PLAN.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# yamlsmith — Round-Trip YAML 1.2 Library for Python
|
|
2
|
+
|
|
3
|
+
## Target Library
|
|
4
|
+
|
|
5
|
+
**ruamel.yaml** — the only production-ready Python library for round-trip YAML editing with comment preservation.
|
|
6
|
+
|
|
7
|
+
| Metric | Value |
|
|
8
|
+
|--------|-------|
|
|
9
|
+
| Downloads | ~155M/month |
|
|
10
|
+
| Maintainers | 1 (Anthon van der Neut) |
|
|
11
|
+
| Dependent packages | 3,080 |
|
|
12
|
+
| Latest release | v0.19.1 (Jan 2026) |
|
|
13
|
+
| Hosting | SourceForge/Mercurial |
|
|
14
|
+
| Community fork | ruyaml (stalled, no releases 12+ months) |
|
|
15
|
+
|
|
16
|
+
### Why Replace
|
|
17
|
+
|
|
18
|
+
- **Bus factor = 1.** Single maintainer with no governance structure.
|
|
19
|
+
- **PEP 625 crisis.** Maintainer signaled potential inability to continue uploading to PyPI due to namespace package naming requirements.
|
|
20
|
+
- **Hostile contribution model.** SourceForge/Mercurial hosting makes PRs/community contribution nearly impossible.
|
|
21
|
+
- **Failed fork.** ruyaml (pycontribs) was created specifically to address these risks and has itself stalled.
|
|
22
|
+
- **No alternative.** PyYAML does not support round-trip editing or comment preservation. There is no production-ready substitute.
|
|
23
|
+
|
|
24
|
+
## Package Name
|
|
25
|
+
|
|
26
|
+
**yamlsmith** — verified available on PyPI.
|
|
27
|
+
|
|
28
|
+
## Scope
|
|
29
|
+
|
|
30
|
+
A modern Python library for YAML 1.2 parsing and emitting with full round-trip fidelity: comments, ordering, formatting, and whitespace are preserved through load/modify/dump cycles.
|
|
31
|
+
|
|
32
|
+
## Architecture
|
|
33
|
+
|
|
34
|
+
### Core Components
|
|
35
|
+
|
|
36
|
+
1. **Scanner/Tokenizer** — Stream-based YAML 1.2 tokenizer that captures comments and whitespace as metadata tokens.
|
|
37
|
+
2. **Parser** — Produces an event stream (similar to SAX) from tokens, attaching comment metadata.
|
|
38
|
+
3. **Composer** — Builds a document tree (node graph) from the event stream, with anchor/alias resolution.
|
|
39
|
+
4. **Representer** — Maps Python objects to YAML nodes, preserving type information.
|
|
40
|
+
5. **Constructor** — Maps YAML nodes back to Python objects.
|
|
41
|
+
6. **Emitter** — Serializes the node graph back to YAML text, replaying preserved comments and formatting.
|
|
42
|
+
7. **Public API** — Simple `load()`, `dump()`, `load_all()`, `dump_all()` functions plus a `YAML` class for configuration.
|
|
43
|
+
|
|
44
|
+
### Key Design Decisions
|
|
45
|
+
|
|
46
|
+
- **Pure Python, zero dependencies.** No C extensions, no Rust. Simple `pip install`.
|
|
47
|
+
- **YAML 1.2 only.** No YAML 1.1 legacy support (boolean `yes/no/on/off` quirks).
|
|
48
|
+
- **Round-trip by default.** The primary API preserves comments and formatting. Safe-load semantics by default (no arbitrary Python object construction).
|
|
49
|
+
- **Type-annotated.** Full PEP 561 type stubs, mypy-clean.
|
|
50
|
+
- **Python 3.10+.**
|
|
51
|
+
|
|
52
|
+
### API Surface
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from yamlsmith import YAML
|
|
56
|
+
|
|
57
|
+
yaml = YAML()
|
|
58
|
+
|
|
59
|
+
# Round-trip load and dump
|
|
60
|
+
data = yaml.load(text)
|
|
61
|
+
yaml.dump(data, stream)
|
|
62
|
+
|
|
63
|
+
# Convenience functions
|
|
64
|
+
from yamlsmith import load, dump, load_all, dump_all
|
|
65
|
+
|
|
66
|
+
data = load(text) # round-trip mode
|
|
67
|
+
text = dump(data) # preserves comments
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Comment Preservation Strategy
|
|
71
|
+
|
|
72
|
+
Comments attach to the nearest node:
|
|
73
|
+
- **Pre-comments:** Lines before a mapping key or sequence item.
|
|
74
|
+
- **Inline comments:** `# comment` on the same line as a value.
|
|
75
|
+
- **Post-comments:** Trailing comments after a block.
|
|
76
|
+
- **Document-level:** Comments before `---` or after `...`.
|
|
77
|
+
|
|
78
|
+
Comments are stored as metadata on the node objects, not in a separate side-channel.
|
|
79
|
+
|
|
80
|
+
## Deliverables
|
|
81
|
+
|
|
82
|
+
1. Core YAML 1.2 scanner, parser, composer, emitter with comment preservation
|
|
83
|
+
2. Python object construction/representation (dict, list, str, int, float, bool, None, datetime, binary)
|
|
84
|
+
3. Round-trip API (`YAML` class + convenience functions)
|
|
85
|
+
4. Anchor/alias support
|
|
86
|
+
5. Multi-document support (`load_all` / `dump_all`)
|
|
87
|
+
6. Flow style vs block style preservation
|
|
88
|
+
7. Comprehensive test suite (YAML Test Suite compliance)
|
|
89
|
+
8. PyPI package with PEP 561 type stubs
|
|
90
|
+
9. README with migration guide from ruamel.yaml
|
|
91
|
+
|
|
92
|
+
## Non-Goals (v1)
|
|
93
|
+
|
|
94
|
+
- YAML 1.1 compatibility mode
|
|
95
|
+
- Custom Python object serialization (no `!!python/object`)
|
|
96
|
+
- C extension acceleration
|
|
97
|
+
- Schema validation
|