tomlrt 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.
@@ -0,0 +1,131 @@
1
+ # Instructions for AI coding agents
2
+
3
+ This file is read by GitHub Copilot, Copilot CLI, and similar agents
4
+ when they work in this repository. Humans are welcome to read it too —
5
+ it doubles as a high-signal contributor guide.
6
+
7
+ ## What this project is
8
+
9
+ `tomlrt` is a **pure-Python, format-preserving** TOML parser and
10
+ writer. The non-negotiable invariant is:
11
+
12
+ > Parsing a document and dumping it again, with no mutations in
13
+ > between, must return the **exact same bytes** — including comments,
14
+ > whitespace, string style (literal vs basic, single vs multiline),
15
+ > number formatting, and line endings (LF vs CRLF).
16
+
17
+ If a change you are about to make could break that, stop and rethink.
18
+
19
+ ## Toolchain
20
+
21
+ - **`uv`** is the only supported package/dependency manager. Do not
22
+ introduce `pip`, `poetry`, `pipenv`, `tox`, `nox`, `setuptools`, or
23
+ `requirements.txt`.
24
+ - Build backend is **`hatchling`**.
25
+ - Supported Python versions: **3.10 – 3.14**.
26
+
27
+ ## Common commands
28
+
29
+ ```bash
30
+ uv sync # install dev deps
31
+ uv run pytest -q # run the test suite (~1s)
32
+ uv run pytest --cov # tests + branch coverage
33
+ uv run mypy # strict type-check src/ and tests/
34
+ uv run ruff check . # lint
35
+ uv run ruff format . # apply formatting
36
+ uv run ruff format --check . # CI-style format check
37
+ ```
38
+
39
+ All four checks (`pytest`, `mypy`, `ruff check`, `ruff format --check`)
40
+ must pass before any commit. CI runs the same set on Python 3.10–3.14.
41
+
42
+ ## Coding standards
43
+
44
+ - **`mypy --strict`** clean — no `# type: ignore` without a specific
45
+ error code, and ideally no ignores at all. Prefer fixing the type.
46
+ - **`ruff` with `select = ["ALL"]`** clean — see `[tool.ruff.lint.ignore]`
47
+ in `pyproject.toml` for the curated exceptions. Do not add new
48
+ per-line `# noqa` without a strong reason.
49
+ - **`ruff format`** is the source of truth for formatting. Run it
50
+ before committing.
51
+ - **No runtime dependencies.** The project is and will remain pure
52
+ stdlib. Only `dependency-groups.dev` may grow, and only with care.
53
+ - **No `cast()` in user-facing code paths.** Tests should not need
54
+ `cast()` either; the typed accessors `Table.array(k)`,
55
+ `Table.table(k)`, `Table.aot(k)`, `Array.array(i)`, `Array.table(i)`
56
+ exist precisely to avoid this.
57
+ - **`from __future__ import annotations`** at the top of every module
58
+ (enforced by ruff's isort `required-imports`).
59
+ - Do not add comments that merely restate the code. Comment intent and
60
+ invariants, not mechanics.
61
+
62
+ ## Architecture (in `src/tomlrt/`)
63
+
64
+ The codebase is small and deliberately layered. Read in this order:
65
+
66
+ - **`_nodes.py`** — the **concrete syntax tree (CST)**: dataclasses
67
+ that hold every byte of the original source (including trivia,
68
+ comments, and the literal lexeme of every value). Mutate these only
69
+ through helpers that maintain the round-trip invariant.
70
+ - **`_parser.py`** — hand-written recursive-descent parser, TOML 1.0 +
71
+ 1.1, that produces the CST. Performance-sensitive: prefer bulk
72
+ `str` scans over per-character loops.
73
+ - **`_synthesise.py`** — converts plain Python values (`str`, `int`,
74
+ `bool`, `datetime`, `list`, `dict`, …) into newly synthesised CST
75
+ nodes when the user assigns into the document.
76
+ - **`_document.py`** — the **logical view layer**: `Document`, `Table`,
77
+ `Array`, `AoT` wrappers that present a dict/list-shaped API while
78
+ delegating all mutation to the CST. The Comment API and typed
79
+ accessors live here. This is by far the largest file.
80
+ - **`_public.py`** — the top-level `parse` / `loads` / `load` /
81
+ `dumps` / `dump` functions. `load` and `dump` require **binary**
82
+ file objects (`IO[bytes]`); text mode would silently translate
83
+ newlines on Windows and break round-tripping.
84
+ - **`_errors.py`** — public exception hierarchy.
85
+ - **`__init__.py`** — re-exports the public API; keep `__all__`
86
+ alphabetised.
87
+
88
+ When in doubt: a change that touches only one of these layers is
89
+ usually right; a change that has to touch all of them is usually wrong.
90
+
91
+ ## Tests
92
+
93
+ - `tests/test_basic.py`, `test_spacing.py`, `test_edit_golden.py` —
94
+ parser and writer regressions, including byte-exact round-trip
95
+ fixtures.
96
+ - `tests/test_comments.py` — the comment manipulation API.
97
+ - `tests/test_compliance.py` — the official **`toml-test`** suite
98
+ (vendored under `vendor/`). Do not edit fixtures there to make
99
+ failures pass.
100
+ - `tests/test_toml11.py` — TOML 1.1-specific coverage.
101
+ - `tests/test_hypothesis.py` — property-based round-trip tests. If you
102
+ break round-tripping, this will usually catch it; add new strategies
103
+ here when you add a new construct.
104
+ - `tests/test_mutation.py` — the dict/list mutation API.
105
+ - `tests/test_synthesise_and_io.py` — value synthesis and binary I/O.
106
+
107
+ When adding behaviour, add a focused unit test in the relevant file
108
+ **and** consider whether the property tests should grow.
109
+
110
+ ## Things to avoid
111
+
112
+ - Adding a runtime dependency.
113
+ - Reaching into `_nodes` from user-facing code instead of going through
114
+ `_document`.
115
+ - "Fixing" formatting differences in the writer's output without
116
+ adding a round-trip test that proves it.
117
+ - Touching `vendor/` (it is third-party, vendored verbatim).
118
+ - Editing `uv.lock` by hand — let `uv` regenerate it.
119
+ - Bumping action versions in `.github/workflows/*.yml` to a tag instead
120
+ of a 40-char commit SHA. The workflows are **`zizmor` clean** and
121
+ must stay that way (`uv tool run zizmor .`).
122
+
123
+ ## Commit conventions
124
+
125
+ - Subject line: imperative mood, ≤ ~70 chars, no trailing period.
126
+ - Body: wrap around 72 chars; explain *why* not *what*. Bullets are
127
+ fine.
128
+ - One logical change per commit. Keep mechanical reformat passes
129
+ separate from substantive changes.
130
+ - Append a `Co-authored-by` trailer when an AI agent did the work, e.g.
131
+ `Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>`.
@@ -0,0 +1,68 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ permissions: {}
10
+
11
+ jobs:
12
+ test:
13
+ name: test (py${{ matrix.python-version }} on ${{ matrix.os }})
14
+ runs-on: ${{ matrix.os }}
15
+ permissions:
16
+ contents: read
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ os:
21
+ - ubuntu-latest
22
+ python-version:
23
+ - "3.10"
24
+ - "3.11"
25
+ - "3.12"
26
+ - "3.13"
27
+ - "3.14"
28
+ - "3.15"
29
+ include:
30
+ - os: macos-latest
31
+ python-version: "3.14"
32
+ - os: windows-latest
33
+ python-version: "3.14"
34
+ steps:
35
+ - name: Check out repository
36
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
37
+ with:
38
+ persist-credentials: false
39
+
40
+ - name: Set up python {{ matrix.python-version }}
41
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
42
+ with:
43
+ python-version: ${{ matrix.python-version }}
44
+ allow-prereleases: true
45
+
46
+ - name: Install uv
47
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
48
+
49
+ - name: Check lockfile
50
+ run: uv lock --check
51
+
52
+ - name: Install project
53
+ run: uv sync -p ${{ matrix.python-version }}
54
+
55
+ - name: Install ruff
56
+ uses: astral-sh/ruff-action@0ce1b0bf8b818ef400413f810f8a11cdbda0034b # v4.0.0
57
+
58
+ - name: ruff check
59
+ run: ruff check .
60
+
61
+ - name: ruff format
62
+ run: ruff format --check .
63
+
64
+ - name: pytest
65
+ run: uv run pytest
66
+
67
+ - name: mypy
68
+ run: uv run mypy
@@ -0,0 +1,78 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ pull_request:
8
+ branches:
9
+ - main
10
+ workflow_dispatch:
11
+
12
+ permissions: {}
13
+
14
+ jobs:
15
+ build:
16
+ name: Build distribution
17
+ runs-on: ubuntu-latest
18
+ permissions:
19
+ contents: read
20
+ steps:
21
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
22
+ with:
23
+ persist-credentials: false
24
+
25
+ - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
26
+ with:
27
+ python-version: "3.14"
28
+
29
+ - name: Install uv
30
+ uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
31
+ with:
32
+ enable-cache: false
33
+
34
+ - name: Check tag matches pyproject version
35
+ if: startsWith(github.ref, 'refs/tags/v')
36
+ env:
37
+ REF_NAME: ${{ github.ref_name }}
38
+ run: |
39
+ TAG_VERSION="${REF_NAME#v}"
40
+ PROJECT_VERSION=$(python -c '
41
+ import tomllib
42
+ with open("pyproject.toml", "rb") as f:
43
+ print(tomllib.load(f)["project"]["version"])
44
+ ')
45
+ if [ "$TAG_VERSION" != "$PROJECT_VERSION" ]; then
46
+ echo "Tag $REF_NAME (version $TAG_VERSION) does not match pyproject version $PROJECT_VERSION" >&2
47
+ exit 1
48
+ fi
49
+
50
+ - name: Build sdist and wheel
51
+ run: uv build
52
+
53
+ - name: Upload distributions
54
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
55
+ with:
56
+ name: dist
57
+ path: dist/
58
+
59
+ publish:
60
+ name: Publish to PyPI
61
+ if: startsWith(github.ref, 'refs/tags/v')
62
+ needs: build
63
+ runs-on: ubuntu-latest
64
+ environment:
65
+ name: pypi
66
+ url: https://pypi.org/p/tomlrt
67
+ permissions:
68
+ id-token: write
69
+ steps:
70
+ - name: Download distributions
71
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
72
+ with:
73
+ path: dist
74
+
75
+ - name: Publish package distributions to PyPI
76
+ uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
77
+ with:
78
+ packages-dir: dist/
@@ -0,0 +1,14 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ vendor/
12
+ .coverage
13
+ dist/
14
+ .hypothesis/
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-04-20
11
+
12
+ Initial release.
13
+
14
+ [Unreleased]: https://github.com/dimbleby/tomlrt/compare/v0.1.0...HEAD
15
+ [0.1.0]: https://github.com/dimbleby/tomlrt/releases/tag/v0.1.0
tomlrt-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tomlrt contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
tomlrt-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: tomlrt
3
+ Version: 0.1.0
4
+ Summary: A format-preserving TOML parser and writer for Python.
5
+ Project-URL: Repository, https://github.com/dimbleby/tomlrt
6
+ Project-URL: Issues, https://github.com/dimbleby/tomlrt/issues
7
+ Project-URL: Changelog, https://github.com/dimbleby/tomlrt/blob/main/CHANGELOG.md
8
+ Author-email: David Hotham <david.hotham@blueyonder.co.uk>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: config,configuration,format-preserving,pyproject,round-trip,toml,toml-parser,toml-writer,tomlrt
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: typing-extensions>=4; python_version < '3.12'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # tomlrt
22
+
23
+ [![PyPI](https://img.shields.io/pypi/v/tomlrt.svg)](https://pypi.org/project/tomlrt/)
24
+ [![Python versions](https://img.shields.io/pypi/pyversions/tomlrt.svg)](https://pypi.org/project/tomlrt/)
25
+ [![License](https://img.shields.io/pypi/l/tomlrt.svg)](https://github.com/dimbleby/tomlrt/blob/main/LICENSE)
26
+ [![CI](https://github.com/dimbleby/tomlrt/actions/workflows/ci.yml/badge.svg)](https://github.com/dimbleby/tomlrt/actions/workflows/ci.yml)
27
+
28
+ A format-preserving TOML parser and writer for Python.
29
+
30
+ ## Usage
31
+
32
+ ```python
33
+ import tomlrt
34
+
35
+ # Files must be opened in binary mode.
36
+ with open("pyproject.toml", "rb") as f:
37
+ doc = tomlrt.load(f)
38
+
39
+ doc["project"]["version"] = "0.2.0"
40
+ doc["project"]["dependencies"].append("requests>=2")
41
+
42
+ print(tomlrt.dumps(doc)) # comments and layout are preserved
43
+ ```
44
+
45
+ ### Comment API
46
+
47
+ ```python
48
+ doc = tomlrt.loads("""
49
+ [server]
50
+ host = "localhost" # default
51
+ port = 8080
52
+ """)
53
+
54
+ server = doc.table("server")
55
+ server.comments["port"] = "override with $PORT"
56
+ server.comments["host"] = None # clear
57
+
58
+ print(tomlrt.dumps(doc))
59
+ # [server]
60
+ # host = "localhost"
61
+ # port = 8080 # override with $PORT
62
+ ```
tomlrt-0.1.0/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # tomlrt
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/tomlrt.svg)](https://pypi.org/project/tomlrt/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/tomlrt.svg)](https://pypi.org/project/tomlrt/)
5
+ [![License](https://img.shields.io/pypi/l/tomlrt.svg)](https://github.com/dimbleby/tomlrt/blob/main/LICENSE)
6
+ [![CI](https://github.com/dimbleby/tomlrt/actions/workflows/ci.yml/badge.svg)](https://github.com/dimbleby/tomlrt/actions/workflows/ci.yml)
7
+
8
+ A format-preserving TOML parser and writer for Python.
9
+
10
+ ## Usage
11
+
12
+ ```python
13
+ import tomlrt
14
+
15
+ # Files must be opened in binary mode.
16
+ with open("pyproject.toml", "rb") as f:
17
+ doc = tomlrt.load(f)
18
+
19
+ doc["project"]["version"] = "0.2.0"
20
+ doc["project"]["dependencies"].append("requests>=2")
21
+
22
+ print(tomlrt.dumps(doc)) # comments and layout are preserved
23
+ ```
24
+
25
+ ### Comment API
26
+
27
+ ```python
28
+ doc = tomlrt.loads("""
29
+ [server]
30
+ host = "localhost" # default
31
+ port = 8080
32
+ """)
33
+
34
+ server = doc.table("server")
35
+ server.comments["port"] = "override with $PORT"
36
+ server.comments["host"] = None # clear
37
+
38
+ print(tomlrt.dumps(doc))
39
+ # [server]
40
+ # host = "localhost"
41
+ # port = 8080 # override with $PORT
42
+ ```
@@ -0,0 +1,111 @@
1
+ [project]
2
+ name = "tomlrt"
3
+ version = "0.1.0"
4
+ description = "A format-preserving TOML parser and writer for Python."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = "MIT"
8
+ authors = [{ name = "David Hotham", email = "david.hotham@blueyonder.co.uk" }]
9
+ keywords = [
10
+ "toml",
11
+ "toml-parser",
12
+ "toml-writer",
13
+ "tomlrt",
14
+ "format-preserving",
15
+ "round-trip",
16
+ "config",
17
+ "configuration",
18
+ "pyproject",
19
+ ]
20
+ dependencies = ["typing_extensions>=4; python_version < '3.12'"]
21
+ classifiers = [
22
+ "Intended Audience :: Developers",
23
+ "License :: OSI Approved :: MIT License",
24
+ "Operating System :: OS Independent",
25
+ "Programming Language :: Python :: 3",
26
+ "Typing :: Typed",
27
+ ]
28
+
29
+ [project.urls]
30
+ Repository = "https://github.com/dimbleby/tomlrt"
31
+ Issues = "https://github.com/dimbleby/tomlrt/issues"
32
+ Changelog = "https://github.com/dimbleby/tomlrt/blob/main/CHANGELOG.md"
33
+
34
+ [dependency-groups]
35
+ dev = [
36
+ "pytest>=9",
37
+ "pytest-cov>=5",
38
+ "hypothesis>=6",
39
+ "mypy>=1.11",
40
+ "tomli>=2; python_version < '3.11'",
41
+ ]
42
+
43
+ [build-system]
44
+ requires = ["hatchling"]
45
+ build-backend = "hatchling.build"
46
+
47
+ [tool.pytest]
48
+ addopts = ["-ra"]
49
+ testpaths = ["tests"]
50
+ strict = true
51
+
52
+ [tool.coverage.run]
53
+ source = ["src/tomlrt"]
54
+ branch = true
55
+
56
+ [tool.coverage.report]
57
+ skip_covered = false
58
+ exclude_also = [
59
+ "if TYPE_CHECKING:",
60
+ "raise NotImplementedError",
61
+ "@overload",
62
+ "pragma: no cover",
63
+ ]
64
+
65
+ [tool.mypy]
66
+ files = ["src", "tests"]
67
+ strict = true
68
+ warn_unreachable = true
69
+ disallow_any_unimported = true
70
+ enable_error_code = [
71
+ "deprecated",
72
+ "explicit-override",
73
+ "ignore-without-code",
74
+ "possibly-undefined",
75
+ "redundant-expr",
76
+ "redundant-self",
77
+ "truthy-bool",
78
+ "truthy-iterable",
79
+ "unused-awaitable",
80
+ ]
81
+
82
+ [tool.ruff]
83
+ src = ["src", "tests"]
84
+
85
+ [tool.ruff.lint]
86
+ select = ["ALL"]
87
+ ignore = [
88
+ "C90", # mccabe
89
+ "COM", # flake8-commas
90
+ "CPY", # flake8-copyright
91
+ "D", # pydocstyle
92
+ "DOC", # pydoclint
93
+ "ERA", # eradicate
94
+ "PD", # pandas-vet
95
+ "PLR", # pylint-refactor
96
+ "T20", # flake8-print
97
+ "ANN401", # any-type
98
+ "E203", # whitespace before ':'
99
+ "S101", # assert-used
100
+ ]
101
+ extend-safe-fixes = ["TC"]
102
+ unfixable = ["F841"]
103
+
104
+ [tool.ruff.lint.flake8-tidy-imports]
105
+ ban-relative-imports = "all"
106
+
107
+ [tool.ruff.lint.flake8-type-checking]
108
+ strict = true
109
+
110
+ [tool.ruff.lint.isort]
111
+ required-imports = ["from __future__ import annotations"]
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "extends": [
4
+ "config:recommended"
5
+ ],
6
+ "lockFileMaintenance": {
7
+ "enabled": true
8
+ }
9
+ }
@@ -0,0 +1,23 @@
1
+ """tomlrt: a format-preserving TOML parser and writer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from tomlrt._document import AoT, Array, Document, Table
6
+ from tomlrt._errors import TOMLError, TOMLParseError
7
+ from tomlrt._public import dump, dumps, load, loads, parse
8
+
9
+ __all__ = [
10
+ "AoT",
11
+ "Array",
12
+ "Document",
13
+ "TOMLError",
14
+ "TOMLParseError",
15
+ "Table",
16
+ "dump",
17
+ "dumps",
18
+ "load",
19
+ "loads",
20
+ "parse",
21
+ ]
22
+
23
+ __version__ = "0.1.0"