Pure-Python decoder for DMS, a data syntax with strong typing, ordered maps, and heredocs.
Project description
dms-py
Python implementations of DMS, a data syntax with strong typing, ordered maps, multi-line heredocs, and front-matter metadata.
Two packages live in this repo:
| dir | PyPI name | description |
|---|---|---|
dms_py/ |
dms-py |
pure-Python reference decoder |
dms-c/ |
dms-c |
CPython extension wrapping the C decoder (much faster) |
The dms-c package compiles external/dms-c/dms.c (a git submodule). After
cloning, run:
git submodule update --init --recursive
Install (pure Python)
pip install dms-py
import dms_py
with open("config.dms") as f:
src = f.read()
# Body-only (drops front matter and comments after decode).
body = dms_py.decode(src)
# Full document (preserves comments + literal forms for encode round-trip).
doc = dms_py.decode_document(src)
doc.meta # OrderedDict | None — None when there is no `+++` block
doc.body # decoded root value
doc.comments # list[AttachedComment]
doc.original_forms # list[(path, OriginalLiteral)]
# Re-emit DMS source.
output = dms_py.encode(doc)
The legacy
parse,parse_document,to_dms, andParseErrornames from SPEC v0.13 still work in 0.3 as deprecated thin aliases that emitDeprecationWarningand forward to the new entry points (decode/encode/DecodeError). They will be removed one release later — switch to the new names now. Encoder errors now raiseEncodeError(subclass ofException) instead of bareValueError.
Install (C-extension)
pip install dms-c
import dms_c
doc = dms_c.decode(open("config.dms").read())
Same API surface; the C variant is several times faster on wide-flat documents.
Performance
50,000-key flat document (~700 KB), best-of-5, startup-subtracted, CPython 3.13 on Windows 11:
| tier | DMS port | time | JSON peer | time | YAML peer | time | DMS / JSON | DMS / YAML |
|---|---|---|---|---|---|---|---|---|
| pure Python | dms-py |
360 ms | n/a | — | PyYAML SafeLoader |
2,388 ms | n/a | 0.15× — DMS ~6.6× faster |
| native (C) | dms-c |
51 ms | json (stdlib) |
11 ms | PyYAML CSafeLoader |
395 ms | 4.73× | 0.13× — DMS ~7.7× faster |
The C extension is ~7× faster than pure Python. Against C-backed peers DMS sits at ~4.7× the JSON cost — the standard cost of carrying comments, ordered keys, and source-form metadata — and ~7× faster than libyaml.
Python's stdlib json is C-backed and there's no widely-used
pure-Python JSON parser to compare against, so JSON only appears in
the FFI tier (same situation as Ruby and Node). The pure-Python YAML
peer is PyYAML with SafeLoader (the pure-Python loader); the
C-backed YAML peer is the same PyYAML library with CSafeLoader
(libyaml-bound).
Reproduce with:
pip install pyyaml # YAML baselines
python C:/Users/<you>/projects/dms-tests/gen_bench_fixtures.py
py bench/bench_decoders.py --iters 5 --warmup 2
Value shape
| DMS type | Python type |
|---|---|
| bool | bool |
| integer | int |
| float | float |
| string | str |
| local-date | dms_py.LocalDate |
| local-time | dms_py.LocalTime |
| local-datetime | dms_py.LocalDateTime |
| offset-datetime | dms_py.OffsetDateTime |
| table | OrderedDict[str, value] |
| list | list |
Datetime classes wrap the source lexeme as a str (already
SPEC-validated), so inspecting them never re-parses. Tables use
collections.OrderedDict to make the insertion-order requirement
explicit (CPython 3.7+ dicts already preserve order, but the
spec mandates it).
Working with comments and heredocs
DMS preserves comments through decode → mutate → re-emit (SPEC
§Comments). The Document carries them on a side-channel keyed by
breadcrumb path; the same shape lets you attach a comment to a value
after decoding and have it round-trip through encode:
import dms_py
from dms_py import Comment, AttachedComment
doc = dms_py.decode_document("db:\n port: 8080\n")
# Mutate a value in place.
doc.body["db"]["port"] = 5432
# Attach a leading line comment to db.port.
doc.comments.append(AttachedComment(
comment=Comment(content="# bumped after LB change", kind="line"),
position="leading",
path=("db", "port"),
))
print(dms_py.encode(doc))
Forcing a heredoc on emit
Strings parse and re-emit in their source form. To switch a basic-quoted
string to a heredoc (or to construct one from scratch), append an
OriginalLiteral.String record to doc.original_forms keyed by the
value's path:
from dms_py import OriginalLiteral, StringForm, HeredocFlavor
doc.body["db"]["greeting"] = "Hello, friend.\nWelcome aboard.\n"
doc.original_forms.append((
("db", "greeting"),
OriginalLiteral.String(
StringForm.Heredoc(
flavor=HeredocFlavor.BasicTriple, # or .LiteralTriple for '''
label=None, # None = unlabeled
modifiers=[], # _trim(...), _fold_paragraphs(), …
)
),
))
Round-trip rules (SPEC §Round-trip semantics): comments stick to
still-present nodes; deleting a node drops its comments; newly
inserted nodes start with no comments. The first original_forms
entry per path wins, so override a parser-recorded form by replacing
rather than appending if the key is already present.
Build & test (development)
python -m pip install -e . # pure-py, editable
python -m pip install -e ./dms-c # C-ext, editable
python -m pytest tests/ # round-trip + comment tests
Conformance
The fixture corpus lives in dms-tests (4500+ pairs). Clone it once as a sibling:
git clone https://gitlab.com/flo-labs/pub/dms-tests.git ../dms-tests
Then run the sweep:
python3 ../dms-tests/run_conformance.py "python3 encoder.py"
dms-tests can also drive every implementation in one shot — see its
README for the cross-language workflow.
Publish
# pure Python
python -m build # creates dist/dms_py-0.1.0*
twine upload dist/dms_py-*
# C extension
cd dms-c && python -m build && twine upload dist/*
You'll need pip install build twine and a PyPI token configured (~/.pypirc).
The C-ext sdist must include external/dms-c/dms.c — either initialise the
submodule before python -m build, or use a build-time hook to copy the file.
License
MIT OR Apache-2.0
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dms_py-0.5.0.tar.gz.
File metadata
- Download URL: dms_py-0.5.0.tar.gz
- Upload date:
- Size: 59.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b6645e4405300ae3d9a949579287d3c2625aef0568846740409cc680da2c3ec6
|
|
| MD5 |
9d1e06aeb41c0caedd27a575f2c0873e
|
|
| BLAKE2b-256 |
5aeba7655d1dcf6514048cb753942b03b7ec89fb80c314a50ab8c1415bd593d2
|
File details
Details for the file dms_py-0.5.0-py3-none-any.whl.
File metadata
- Download URL: dms_py-0.5.0-py3-none-any.whl
- Upload date:
- Size: 52.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ed0095aaa50a5947ac9bff56fc67e1cb1c1065c955f7c56ebbe74f813c1c8d5b
|
|
| MD5 |
93ecfd0ccb5c3d31de77a9c4c042dc6c
|
|
| BLAKE2b-256 |
8811e5c534258070aa33e4c971f831518d981f99c164acb9bdfbd61a5c59dc5d
|