Skip to content

feat!: rewrite toon_format with full TOON 4.1 support - #68

Merged
alesanfra merged 10 commits into
mainfrom
rewrite-v1
Oct 2, 2026
Merged

alesanfra merged 10 commits into
mainfrom
rewrite-v1

Conversation

@alesanfra

@alesanfra alesanfra commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Rewrites toon_format against TOON specification 4.1.

  • json-style API: dumps, dump, loads, load with keyword-only options; encode and decode are aliases.
  • Every official fixture of spec v4.1.1 passes.
  • Dataclasses, attrs classes, enums, UUIDs, Decimal, and Pydantic models encode directly; unsupported values raise TypeError instead of becoming null.
  • Pure Python, no runtime dependencies, fully typed, Python 3.10–3.14.
  • The toon command is removed in favor of npx @toon-format/cli.

Breaking changes and the upgrade path from 0.9 are in docs/migration.md.

Before the first release, the toon-format project on PyPI needs a trusted publisher for publish.yml with the pypi environment.

Closes #33, closes #47, closes #61, closes #62, closes #63, closes #64, closes #65.

🤖 Generated with Claude Code

Rewrite the package from scratch as a standard uv project:

- New `toon` module with a json-style API: dumps, dump, loads, load,
  ToonDecodeError (with line/source), Delimiter, __toon_spec__.
- Full TOON specification 4.1: keyed tabular objects, nested field groups,
  comment lines, canonical empty arrays, and all strict-mode checks. Every
  official fixture of spec v4.1.1 passes.
- json-style options: default and sort_keys for encoding; parse_float,
  parse_int, object_hook and object_pairs_hook for decoding.
- Host types: dataclasses, enums, UUIDs, sets, lossless Decimal, Pydantic.
- CLI: --check, --json-indent, python -m toon; UTF-8 and LF on every platform.
- `toon_format` stays as a deprecated wrapper over the new API.
- Differential tests against toons and Hypothesis round-trip tests.
- MkDocs documentation for Read the Docs (not published yet).
- uv_build backend, CI on Python 3.10-3.14, trusted publishing with uv.
- Simplified dev container; 88-character lines, explained in CONTRIBUTING.md.
- AGENTS.md documents the project rules.

Fixes #33, #47, #61, #62, #63, #64, #65.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alesanfra
alesanfra requested review from a team and johannschopplich as code owners September 28, 2026 22:01
alesanfra and others added 5 commits September 29, 2026 09:15
- Raise ValueError when mapping keys collide after conversion to strings
  ({1: "a", "1": "b"}) instead of silently dropping a value.
- Quote strings starting with U+FEFF so that a root string survives the
  byte-order-mark removal of the decoder (§12).
- Pass `strict` from ToonPydanticModel.model_validate_toon to both the TOON
  decoder and pydantic; None keeps strict decoding and the model's own
  validation settings.
- Make the CLI refuse to write ±inf from non-strict decoding as invalid JSON.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/benchmark.py times encoding and decoding of the working tree, and
compares it with any git revision checked out in a temporary worktree
(0.9 revisions are driven through toon_format.encode/decode).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `toon --stats` prints token counts of compact JSON, indented JSON, and
  TOON to standard error. Counting is offline through tiktoken (the tokens
  extra), so the figures are exact for OpenAI models only; the 0.9 token
  helpers now share the same code in toon._tokens.
- Instances of attrs classes encode as objects of their fields, detected
  through __attrs_attrs__ without importing attrs.
- Document how to encode pandas DataFrames and NumPy values.
- Add packaging/toon-python, an alias distribution that only depends on
  toon-format, and publish it from the Publish workflow, which checks that
  its version matches.
- Replace the per-person copyright lines with "The toon-python authors"
  and list them, including earlier contributors, in AUTHORS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The repository root is now the toon-python distribution (module `toon`,
  the `toon` command, the pydantic and tokens extras).
- packaging/toon-format builds toon-format, which depends on toon-python at
  the same version and carries the deprecated toon_format module, its tests,
  and the `toon` command. Keeping those files in toon-format is what lets
  `pip install -U toon-format` from 0.9 keep `toon_format` and the command:
  pip removes the files of the old toon-format after installing toon-python.
- CI builds both, smoke-tests both wheels, and runs the toon_format tests
  from packaging/toon-format. The Publish workflow uploads toon-python and
  then toon-format, and checks that the versions and pins match.
- Documentation, badges, and extras now name toon-python.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add `toon encode`, `toon decode`, `toon check`, and `toon stats`, each with
only the options it needs. `stats` prints the token counts of JSON and of
TOON with every delimiter, compared with compact JSON; `check` validates
several files and reports `file:line: message`. The unreleased `--stats`,
`--check`, and `--json-indent` flags are removed. `toon FILE [options]` keeps
the 0.9 interface unchanged.

Rework the README: absolute links for PyPI, measured token savings, usage
examples, and the CLI reduced to `toon stats`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alesanfra

Copy link
Copy Markdown
Contributor Author

Hi @johannschopplich, friendly ping on this one. Would you have a moment to take a look? Happy to adjust anything.

@johannschopplich

Copy link
Copy Markdown
Contributor

Hi there! I'm aware. I'll follow up as soon as I can in the next days. Currently busy with a work project.

@alesanfra

Copy link
Copy Markdown
Contributor Author

@johannschopplich Thank you!

alesanfra and others added 2 commits September 30, 2026 22:30
Drop the AUTHORS file and the keywords and classifiers from toon-format,
which only carries the deprecated 0.9 API. Its LICENSE no longer points to
an AUTHORS file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before each commit, prek runs ruff check --fix, ruff format, mypy, and
mkdocs build --strict (only when docs/, src/, or mkdocs.yml change). The
manual stage also runs both test suites, so
`uv run prek run --all-files --hook-stage manual` covers every CI check.

The dev container installs the hooks from a post-create script, kept in a
file because Zed joins lifecycle command arguments without quoting them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@johannschopplich johannschopplich left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there! Thanks for bringing spec v4.1.1 to this TOON port.

What blocks the merge is the name. #50 and #51 asked for toon-python, but every port publishes as toon-format (npm, crates.io, pub.dev).

To get it in:

  • Build toon-format with the module toon_format from the repo root, and drop the packaging/toon-format shim and the two-step publish.
  • Keep encode/decode as the public names, like the other ports.
  • Yank toon-python 1.0.0rc1 and leave that name without releases.
  • Move the license rewrite and the CLI redesign into separate PRs.

Details are inline.

Comment thread pyproject.toml Outdated
Comment thread pyproject.toml Outdated
Comment thread src/toon/__init__.py Outdated
Comment thread packaging/toon-format/pyproject.toml Outdated
Comment thread .github/workflows/publish.yml
Comment thread src/toon/pydantic.py Outdated
Comment thread LICENSE Outdated
Comment thread CHANGELOG.md Outdated
Comment thread src/toon/cli.py Outdated
Comment thread README.md Outdated
Address the review of #68:

- Build toon-format, module toon_format, from the repository root and drop
  the packaging/toon-format shim. toon-python becomes an alias with no code
  that depends on toon-format, so that the repository's name stays taken.
- Keep dumps/loads/dump/load as the primary API with keyword-only options,
  and add encode/decode as aliases. The 0.9 options dictionary and its types
  are gone.
- Remove the toon command and the token helpers with the tokens extra; the
  README points to @toon-format/cli.
- Import Self from typing on 3.11+ and declare typing-extensions in the
  pydantic extra for 3.10.
- Write U+FEFF as an escape, document that Decimal values beyond the double
  range need parse_float=Decimal to decode, and drop "independent" for toons.
- Restore the named copyright lines in LICENSE and remove AUTHORS; drop the
  Read the Docs badge until the site is live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alesanfra

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I've pushed 4d55d9f, which addresses it. Here is what changed and where I see things differently.

Done

  • Name. The distribution is toon-format, built from the repository root, and the module is toon_format. Honestly, I find import toon_format ugly: Python modules are usually short single words like json, yaml, or toml. But the collision with other toon modules on PyPI is a real problem, so I accept it.
  • packaging/toon-format is gone. The two-step publish now builds toon-format from the root (see the toon-python point below).
  • version("toon-format") in __init__.py.
  • README. Install instructions use toon-format and toon-format[pydantic]. The Read the Docs badge is gone until the project is live.
  • pydantic extra. Self now comes from typing on 3.11+ and from typing_extensions on 3.10, and typing_extensions is declared in the extra for 3.10. It worked before only because pydantic itself depends on typing_extensions. Relying on a transitive dependency was wrong, thanks.
  • "" is written as an escape.
  • Decimal. docs/data-types.md now says that Decimal("1e400") encodes but does not decode back with the default strict decoder, and that parse_float=Decimal reads it.
  • CHANGELOG. "independent" is gone.
  • LICENSE. The named copyright lines are back, with mine added below. AUTHORS is removed.

Where I disagree

The spec defines the format. How a library exposes that format is up to the host language. This is the Python implementation, and its users are Python developers: it has to feel like Python, not like TypeScript translated into Python. Consistency across ports matters for the format, which is identical byte for byte and checked against the official fixtures. It does not matter for calling conventions, which no Python user compares across languages.

1. dumps/loads are the primary API, and encode/decode are aliases.

toon_format.dumps(value, *, delimiter=",", indent_size=2)
toon_format.loads(text, *, strict=True)
toon_format.dump(value, fp, ...)
toon_format.load(fp, ...)

toon_format.encode(value, ...)  # alias of dumps
toon_format.decode(text, ...)   # alias of loads

dumps/loads is the interface of every Python serialization library: json, pickle, marshal, tomllib, orjson. Python developers know it without reading the docs, and it replaces json.dumps with no other change. encode and decode are still there for people coming from other ports, and simple calls written for 0.9 keep working.

Options are keyword-only arguments, not an options dictionary. Passing options as a dict is a JavaScript idiom; no Python library does it. Keyword arguments give type checking, IDE completion, a useful help(), and an immediate TypeError on a misspelled option, while a dict gives none of these. The option names and meanings match the spec and the other ports. Only the calling convention follows Python. I won't move this to an options dict.

2. The CLI is removed instead of being made identical to @toon-format/cli.

A Python CLI that copies the TypeScript one flag for flag duplicates a tool that already exists and runs anywhere with npx @toon-format/cli. It adds maintenance work and gives users nothing new. So I removed it. The CHANGELOG and the migration guide mark the removal as breaking, and the README points to @toon-format/cli. If we ever want a Python CLI, it should be designed for Python users, in its own PR.

3. toon-python stays as an alias instead of being yanked.

toon-format is the main name everywhere: package, docs, install instructions. But the repository is toon-python, and people will search PyPI for that name. A name with no releases can be taken over by someone else, who could then ship anything to users who trust this repository. packaging/toon-python is a package with no code that only depends on toon-format at the same version. The publish workflow checks the pins. It costs one extra upload and protects users.

Follow-up

  • A differential test against the TypeScript reference implementation, in its own PR, because it needs Node in CI.

@alesanfra alesanfra changed the title feat!: rewrite as the toon module with full TOON 4.1 support feat!: rewrite toon_format with full TOON 4.1 support Oct 1, 2026
@johannschopplich

Copy link
Copy Markdown
Contributor

Thanks for the quick turn, @alesanfra.

dumps/loads with encode/decode as aliases works for me, and so does dropping the CLI in favor of npx @toon-format/cli.

The toon-python alias stays out, though. A yanked 1.0.0rc1 keeps that project registered to its owners – you and me – so nobody can take the name over, while a second install name is exactly the split #50 and #51 were about. Please drop packaging/toon-python and the second publish job.

Two more, since they ship with this PR:

  • Trim AGENTS.md to what every task needs: what the project is, uv, and the check commands. The layout table and invariants go stale, and the release steps belong in CONTRIBUTING.md.
  • Drop test_toons_compat.py and the toons dev dependency – the spec fixtures are the shared yardstick. No need for the TS differential test as a follow-up either; I take that suggestion back.

@johannschopplich

Copy link
Copy Markdown
Contributor

Thanks for the quick turn on 4d55d9f – the naming and the inline points are all sorted. What's left is subtraction:

  • Drop packaging/toon-python, its workspace entry, and the second build and publish steps – yanking 1.0.0rc1 keeps the name reserved.
  • Drop AGENTS.md and CLAUDE.md – further than my last comment, but we keep agent instructions out of the org repos. The release steps can go into CONTRIBUTING.md.
  • Drop test_toons_compat.py and the toons dev dependency.
  • Drop the tests that repeat the spec fixtures – quoting, forms, error modes, the regressions. A missing case belongs in the spec repo.
  • Drop the MkDocs site, .readthedocs.yaml, and the docs job – docs/data-types.md and docs/migration.md work as plain Markdown.
  • Drop dependabot.yml and scripts/benchmark.py.
  • Vendor the v4.1.2 fixtures – they already pass.
  • Shorten the PR description to match.

@alesanfra

Copy link
Copy Markdown
Contributor Author

So basically I need to drop most of the innovations, nice 😎

Seriously, I don't agree with most of these points. Here's where I stand:

  • test_toons_compat.py and the toons dev dependency: fine, I'll drop them. They helped during the rewrite, but they're useless now.
  • packaging/toon-python and the second publish job: fine, I'll drop them too.
  • The v4.1.2 fixtures: they can go in a separate PR. There's already a lot going on in this one.
  • AGENTS.md: it stays. It's useful for anyone who contributes, and that's the whole point of it.
  • The PR description: it doesn't change anything in the code, so I'll leave it as it is.
  • Everything else (tests, docs, dependabot, benchmark) stays.

This is a community project, so I think some compromise on both sides makes sense. I've already changed a lot since my first proposal, and I honestly believe these additions will make the project better in the long run.

So here's what I propose: I drop toons and the toon-python packaging, and we merge the rest as is.

Let me know how you want to proceed. I'll wait for your answer before pushing any changes.

@johannschopplich

Copy link
Copy Markdown
Contributor

Hey there, fair pushback. Sorry, my last comment moved the goalposts after you'd turned the first round around within a day. I didn't mean to undersell you PR.

Your proposal works for me (toons and the toon-python packaging drop). The fixtures bump can be its own PR, and I'll write the squash message at merge time, so the description can stay. AGENTS.md, the tests, Dependabot and the benchmark are your call as maintainer if you prefer that.

One thing before merging: the README links toon-python.readthedocs.io, which doesn't exist yet. And once 1.0.0 is out, could you yank toon-python 1.0.0rc1? Thanks.

@johannschopplich johannschopplich left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving so you can merge whenever you're ready.

Remove packaging/toon-python and its build and publish steps, the toons
dev dependency and test_toons_compat.py. Point the documentation links to
docs/ until Read the Docs is set up, trim AGENTS.md to rules that do not
go stale, and move the release steps to CONTRIBUTING.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alesanfra
alesanfra merged commit cdb41bf into main Oct 2, 2026
10 checks passed
@alesanfra
alesanfra deleted the rewrite-v1 branch October 2, 2026 20:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment