diff --git a/README.md b/README.md
index f6b62b2..f102925 100644
--- a/README.md
+++ b/README.md
@@ -225,7 +225,7 @@ off.
Three functions cover the common path:
```python
-from legaldown import load, validate, serialize_document
+from legaldown import load, serialize, validate
document = load("contract.lgd")
result = validate(document)
@@ -234,7 +234,7 @@ for diagnostic in result.diagnostics:
print(diagnostic.level, diagnostic.rule, diagnostic.message)
if result.is_valid: # no Error-level diagnostics
- print(serialize_document(document))
+ print(serialize(document)) # the source text; save(document) writes it to the file
```
`load(path)` takes a `str` or `os.PathLike`, reads the file as UTF-8 and returns the `Document`.
@@ -262,9 +262,10 @@ file's text (or `None`), to read them from elsewhere — a database, an upload.
is the same function for files on disk, and the one `parse_template` takes as `resolve=` (`LoadFile`).
> **Deprecated:** `parse_document` is now `parse` (string; same arguments) or `load` (file),
-> `validate_document` is now `validate`, and the importer callbacks `import_definitions=` and
-> `import_attachment_definitions=` are replaced by `resolve=`. They still work and raise a
-> `DeprecationWarning`; they are deprecated since 0.4.0 and will be removed in 0.5.0.
+> `validate_document` is now `validate`, `serialize_document` is now `serialize` (text) or `save`
+> (file), and the importer callbacks `import_definitions=` and `import_attachment_definitions=` are
+> replaced by `resolve=`. They still work and raise a `DeprecationWarning`; they are deprecated
+> since 0.4.0 and will be removed in 0.5.0.
>
> **Changed in 0.4.0:** `ValidationResult` is a plain value of two parts, `diagnostics` and `index`.
> `errors`, `warnings` and `infos` are read-only lists taken from `diagnostics` (changing them
@@ -317,15 +318,25 @@ the validator's own helpers ([Tooling API](#tooling-api)).
`Side`, `Party`, `Attachment` — that you can inspect, edit, and write back out:
```python
-from legaldown import load, serialize_document
+from legaldown import load, save
document = load("contract.lgd")
document.metadata.governing_law = "Czech Republic"
-with open("contract.lgd", "w", encoding="utf-8") as handle:
- handle.write(serialize_document(document))
+save(document) # to document.path, where load found it
```
+`save(document, path=None)` mirrors `load`: it writes the document to `path` (a `str` or
+`os.PathLike`), or to `document.path` when it has none to be given, and returns the absolute path.
+A document from `parse(text)` or built in code has no path, so `save` raises `ValueError` until you
+pass one. The write is atomic — the file is either as it was or the new text, never half written —
+a symbolic link is written through, and a file keeps its permissions. Missing directories on the way
+are created. The text is UTF-8 with LF line endings and no byte-order mark, so a file that was read
+with CRLF line endings or a byte-order mark is written back without them. Afterwards `document.path`
+is the path written, as after `load`: that is where `validate` looks for the files the document
+refers to (and `document.filename`, which diagnostics name, is its name). `serialize(document)` is the same text as a `str`, the inverse of `parse`, for when the
+file is not what you want to write (an HTTP response, a database).
+
Content before the first heading — typically the sentence identifying the parties — is the
document's preamble (§4.4): it is unnumbered, so it lives in `document.preamble` rather than in
`sections`. `document.iter_blocks()` walks every body block, preamble first.
diff --git a/src/legaldown/__init__.py b/src/legaldown/__init__.py
index 0d29e9c..4675c9f 100644
--- a/src/legaldown/__init__.py
+++ b/src/legaldown/__init__.py
@@ -6,14 +6,14 @@
Quick start::
- from legaldown import load, serialize_document, validate
+ from legaldown import load, serialize, validate
doc = load("contract.lgd")
result = validate(doc)
for diagnostic in result.diagnostics:
print(diagnostic.level, diagnostic.rule, diagnostic.message)
if result.is_valid:
- print(serialize_document(doc))
+ print(serialize(doc)) # save(doc) writes it back to the file
"""
from __future__ import annotations
@@ -78,7 +78,7 @@
# Parser & serializer
from .parser import FrontmatterError, collect_source_directives, load, parse, parse_document
-from .serializer import render_block, render_item, serialize_document
+from .serializer import render_block, render_item, save, serialize, serialize_document
# The LegalDown specification version this implementation targets.
from .specification import SPEC_VERSION
@@ -125,7 +125,9 @@
"parse",
"parse_document", # deprecated since 0.4.0, removed in 0.5.0: use ``parse`` or ``load``
"FrontmatterError",
- "serialize_document",
+ "save",
+ "serialize",
+ "serialize_document", # deprecated since 0.4.0, removed in 0.5.0: use ``serialize``
"validate",
"validate_document", # deprecated since 0.4.0, removed in 0.5.0: use ``validate``
# Assembly (§15.7, §17.6)
diff --git a/src/legaldown/cli.py b/src/legaldown/cli.py
index 5aa1bec..e91a018 100644
--- a/src/legaldown/cli.py
+++ b/src/legaldown/cli.py
@@ -15,12 +15,9 @@
from __future__ import annotations
import argparse
-import contextlib
import json
import os
-import stat
import sys
-import tempfile
from pathlib import Path
from typing import Any
@@ -28,7 +25,7 @@
from . import SPEC_VERSION, __version__
from .assembly import AssemblyResult, frontmatter_diagnostic
-from .files import within
+from .files import within, write_atomically
from .parser import FrontmatterError, load
from .template import AnswersError, Form, Template, load_answers, load_template
from .validator import validate
@@ -176,31 +173,6 @@ def _write_stdout(text: str, errors: str = "strict") -> None:
sys.stdout.write(text)
-def _write_atomically(path: Path, text: str) -> None:
- """*text* to *path*: all of it, or the file as it was — written beside it and
- moved into place, since the file may be the answers a person typed. A symbolic
- link is written through, and the file keeps its permissions (a new one gets
- those a file made now would have)."""
- target = path.resolve()
- target.parent.mkdir(parents=True, exist_ok=True)
- if target.exists():
- mode = stat.S_IMODE(target.stat().st_mode)
- else:
- mask = os.umask(0)
- os.umask(mask)
- mode = 0o666 & ~mask
- descriptor, name = tempfile.mkstemp(dir=target.parent, prefix=f".{target.name}.", suffix=".tmp")
- try:
- with os.fdopen(descriptor, "w", encoding="utf-8", newline="") as handle:
- handle.write(text)
- os.chmod(name, mode)
- os.replace(name, target)
- except BaseException:
- with contextlib.suppress(OSError):
- os.unlink(name)
- raise
-
-
def _write(path: Path, text: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8", newline="") as handle:
@@ -296,7 +268,7 @@ def _save_answers(path: Path, answers: dict) -> str | None:
try:
text = yaml.safe_dump(answers, sort_keys=False, allow_unicode=True, default_flow_style=False)
text.encode("utf-8")
- _write_atomically(path, text)
+ write_atomically(path, text)
except (OSError, ValueError, RuntimeError, yaml.YAMLError) as exc: # a UnicodeEncodeError is a ValueError; a link loop a RuntimeError
return f"cannot write the answers to {path}: {exc}"
return None
diff --git a/src/legaldown/definitions.py b/src/legaldown/definitions.py
index c2531ea..a9e9c13 100644
--- a/src/legaldown/definitions.py
+++ b/src/legaldown/definitions.py
@@ -311,7 +311,7 @@ def _own_fragments(block: Block) -> list[tuple[str, bool]]:
):
# Raw HTML and indented code: no directive or marker is recognized
# (§11.4). A source block is not read at all: its text is read once
- # written and parsed (``serialize_document``). A fenced block's text
+ # written and parsed (``serialize``). A fenced block's text
# is read with its fence blanked (below): text a model puts after its
# closing fence is checked -- which needs
# at least the fence's own line and one more, so a fence with nothing
diff --git a/src/legaldown/files.py b/src/legaldown/files.py
index dcb8fa6..724e390 100644
--- a/src/legaldown/files.py
+++ b/src/legaldown/files.py
@@ -7,8 +7,11 @@
"""
from __future__ import annotations
+import contextlib
import os
import posixpath
+import stat
+import tempfile
from collections.abc import Callable
from pathlib import Path
@@ -61,3 +64,29 @@ def within(base: Path, relative: str) -> Path | None:
except (OSError, ValueError, RuntimeError):
return None
return target if target.is_relative_to(root) else None
+
+
+def write_atomically(path: Path, text: str) -> None:
+ """*text* to *path* as UTF-8: all of it, or the file as it was — written
+ beside it and moved into place, since the file may be a document or the
+ answers a person wrote. A symbolic link is written through, the file keeps
+ its permissions (a new one gets those a file made now would have), and
+ missing directories on the way are created."""
+ target = path.resolve()
+ target.parent.mkdir(parents=True, exist_ok=True)
+ if target.exists():
+ mode = stat.S_IMODE(target.stat().st_mode)
+ else:
+ mask = os.umask(0)
+ os.umask(mask)
+ mode = 0o666 & ~mask
+ descriptor, name = tempfile.mkstemp(dir=target.parent, prefix=f".{target.name}.", suffix=".tmp")
+ try:
+ with os.fdopen(descriptor, "w", encoding="utf-8", newline="") as handle:
+ handle.write(text)
+ os.chmod(name, mode)
+ os.replace(name, target)
+ except BaseException:
+ with contextlib.suppress(OSError):
+ os.unlink(name)
+ raise
diff --git a/src/legaldown/models.py b/src/legaldown/models.py
index 29e0bbd..35a59a0 100644
--- a/src/legaldown/models.py
+++ b/src/legaldown/models.py
@@ -271,7 +271,7 @@ def iter_blocks(self) -> Iterator[tuple[Section | None, int, Block]]:
# not numbered and without an identifier. Its text is one line.
"heading": {"kind": "heading", "text": "", "level": 1},
# Markdown source a caller built, such as an editor's row: written as it
- # stands (``serialize_document``), never produced by the parser and not
+ # stands (``serialize``), never produced by the parser and not
# read by the validator. Read back, it is the blocks it holds.
"source": {"kind": "source", "text": ""},
}
diff --git a/src/legaldown/serializer.py b/src/legaldown/serializer.py
index 92a900e..8a140f2 100644
--- a/src/legaldown/serializer.py
+++ b/src/legaldown/serializer.py
@@ -5,12 +5,16 @@
"""
from __future__ import annotations
+import os
import re
+import warnings
+from pathlib import Path
from typing import Any
import yaml
from .directives import format_value
+from .files import write_atomically
from .markdown import (
FENCE_OPEN_RE,
close_fences,
@@ -555,9 +559,14 @@ def ignore_aliases(self, data: Any) -> bool:
# ── Public API ────────────────────────────────────────────────────
-def serialize_document(document: Document) -> str:
+def serialize(document: Document) -> str:
"""Serialize a Document object to LegalDown (.legal.md) source text.
+ The inverse of ``parse``: what it returns parses back as the document
+ (what the document says, not the source's own details such as line
+ endings or a byte-order mark). The text is LF-terminated and always ends
+ with a line break; ``save`` is the call for a file.
+
A document parsed without frontmatter (§3.1) is written without it, as
long as its metadata is still empty; a thematic break opening it is then
written ``***``, which cannot open frontmatter."""
@@ -591,6 +600,52 @@ def serialize_document(document: Document) -> str:
return text.rstrip("\n") + "\n"
+def save(document: Document, path: str | os.PathLike[str] | None = None) -> Path:
+ """Write *document* as LegalDown source to *path*, or, without one, to the
+ file it came from (``Document.path``).
+
+ The call for a document that lives in a file, as ``load`` is for reading
+ it: it writes ``serialize(document)`` as UTF-8 with LF line endings and no
+ byte-order mark, so a file that was read with CRLF line endings or a
+ byte-order mark is written back without them. The write is atomic: the
+ file is either as it was or the new text, never half written, since the
+ text goes to a temporary file beside it and is moved into place. A
+ symbolic link is written through (the link stays one, its target changes);
+ the file keeps its permissions, and a new one gets those a file made now
+ would have. Directories that are missing on the way to *path* are created.
+
+ The document now lives at *path*, as after ``load``: ``Document.path`` is
+ set to its absolute path (symbolic links are not followed), which is where
+ ``validate`` looks for the files it refers to (§7.5, §12.4), and
+ ``Document.filename`` to its name, which diagnostics name. That path is
+ returned.
+
+ Raises ``ValueError`` when both *path* and ``Document.path`` are None (a
+ document parsed from text or built in code has no path; pass one), and
+ ``OSError`` when the file cannot be written; the document's path and
+ filename are then unchanged.
+ """
+ target = document.path if path is None else path
+ if target is None:
+ raise ValueError("a document parsed from text or built in code has no path; pass one")
+ file = Path(target).absolute()
+ write_atomically(file, serialize(document))
+ document.path = file
+ document.filename = file.name
+ return file
+
+
+def serialize_document(document: Document) -> str:
+ """Deprecated since 0.4.0, removed in 0.5.0. Same as ``serialize``."""
+ warnings.warn(
+ "legaldown.serialize_document() is deprecated since 0.4.0 and will be removed in 0.5.0; "
+ "use legaldown.serialize()",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return serialize(document)
+
+
def render_item(item: ListItem) -> str:
diff --git a/tests/conformance/test_legaldown_fixtures.py b/tests/conformance/test_legaldown_fixtures.py
index 4f3283b..d3b900f 100644
--- a/tests/conformance/test_legaldown_fixtures.py
+++ b/tests/conformance/test_legaldown_fixtures.py
@@ -249,14 +249,14 @@ def test_a_fixture_keeps_its_diagnostics_when_written_back(path: Path):
holds, an entry not yet complete included (#83)."""
from collections import Counter
- from legaldown import serialize_document
+ from legaldown import serialize
from legaldown.parser import FrontmatterError
try:
document = parse(_read(path), filename=path.name)
except FrontmatterError:
pytest.skip("unreadable frontmatter: nothing to write back")
- again = parse(serialize_document(document), filename=path.name)
+ again = parse(serialize(document), filename=path.name)
# How the source writes its YAML is not the model's (block style when
# written back).
assert replace(again.metadata, not_line_editable=[]) == replace(document.metadata, not_line_editable=[])
diff --git a/tests/test_conditions.py b/tests/test_conditions.py
index 65233dd..4282072 100644
--- a/tests/test_conditions.py
+++ b/tests/test_conditions.py
@@ -4,7 +4,7 @@
import pytest
-from legaldown import serialize_document
+from legaldown import serialize
from legaldown.markers import Marker, parse_marker, split_heading
from legaldown.parser import parse
from legaldown.validator import validate
@@ -77,7 +77,7 @@ def test_a_conditional_heading_round_trips_and_its_identifier_is_generated():
document = _parse("# Services {when=vat}\n\nText.")
section = document.sections[0]
assert (section.title, section.identifier, section.condition) == ("Services", "", "vat")
- assert "\n# Services {when=vat}\n" in serialize_document(document)
+ assert "\n# Services {when=vat}\n" in serialize(document)
result = validate(document)
assert result.index.sections[0].identifier == "services"
assert document.sections[0].identifier == "" # validation leaves the document as it was
@@ -389,7 +389,7 @@ def test_a_comment_may_follow_a_heading_marker():
document = _parse("# Termination {when=vat} \n\nText.")
[section] = document.sections
assert (section.title, section.condition) == ("Termination ", "vat")
- assert parse(serialize_document(document)).sections[0].condition == "vat"
+ assert parse(serialize(document)).sections[0].condition == "vat"
def test_a_dotted_path_is_not_a_reference():
diff --git a/tests/test_public_api.py b/tests/test_public_api.py
index b1e16c4..20e3245 100644
--- a/tests/test_public_api.py
+++ b/tests/test_public_api.py
@@ -292,6 +292,20 @@ def test_parse_document_is_a_deprecated_alias_of_parse():
assert record[0].filename == __file__ # the caller is named, not the shim
+def test_serialize_document_is_a_deprecated_alias_of_serialize():
+ document = parse(_SOURCE)
+ with pytest.warns(DeprecationWarning, match=r"removed in 0\.5\.0") as record:
+ text = legaldown.serialize_document(document)
+ assert text == legaldown.serialize(document)
+ assert record[0].filename == __file__ # the caller is named, not the shim
+
+
+def test_writing_mirrors_reading():
+ for name in ("load", "parse", "save", "serialize"):
+ assert name in legaldown.__all__
+ assert "write_atomically" not in legaldown.__all__
+
+
def test_a_symbolic_link_is_named_as_it_was_given(tmp_path):
target = tmp_path / "v3-final.lgd"
target.write_text(_SOURCE, encoding="utf-8")
diff --git a/tests/test_save.py b/tests/test_save.py
new file mode 100644
index 0000000..5ec8174
--- /dev/null
+++ b/tests/test_save.py
@@ -0,0 +1,246 @@
+"""``save``: writing a document to a file, the mirror of ``load``."""
+from __future__ import annotations
+
+import os
+import stat
+import sys
+import warnings
+from pathlib import Path
+
+import pytest
+
+import legaldown
+from legaldown import Document, load, parse, save, serialize, validate
+
+_SOURCE = "---\ntitle: Services Agreement\nlanguage: en\n---\n\n# Scope\n\nThe Supplier provides the services.\n"
+
+
+def _write(path: Path, text: str = _SOURCE) -> Path:
+ path.write_bytes(text.encode("utf-8"))
+ return path
+
+
+def _leftovers(directory: Path, keep: set[str]) -> list[str]:
+ return sorted(entry.name for entry in directory.iterdir() if entry.name not in keep)
+
+
+# the round trip --------------------------------------------------------------
+
+def test_a_loaded_document_edited_and_saved_loads_back_equal(tmp_path):
+ file = _write(tmp_path / "contract.lgd")
+ document = load(file)
+ document.metadata.governing_law = "Czech Republic"
+ document.metadata.title = "Amended Agreement"
+ save(document)
+ again = load(file)
+ assert again == document
+ assert again.metadata.governing_law == "Czech Republic"
+ assert again.path == file.absolute()
+ assert file.read_text(encoding="utf-8") == serialize(document)
+
+
+def test_save_without_a_path_writes_back_to_where_the_document_was_loaded_from(tmp_path):
+ file = _write(tmp_path / "contract.lgd")
+ document = load(file)
+ document.sections[0].title = "Subject"
+ assert save(document) == file.absolute()
+ assert load(file).sections[0].title == "Subject"
+ assert _leftovers(tmp_path, {"contract.lgd"}) == []
+
+
+def test_save_to_a_new_path_moves_the_document_there(tmp_path):
+ original = _write(tmp_path / "contract.lgd")
+ document = load(original)
+ copy = tmp_path / "copy.lgd"
+ assert save(document, copy) == copy.absolute()
+ assert document.path == copy.absolute()
+ assert original.read_text(encoding="utf-8") == _SOURCE # the first file is untouched
+ assert load(copy).sections == document.sections
+ assert load(copy).metadata == document.metadata
+ document.metadata.title = "Later"
+ save(document) # now writes to the copy
+ assert load(copy).metadata.title == "Later"
+ assert original.read_text(encoding="utf-8") == _SOURCE
+
+
+def test_a_document_parsed_from_text_has_no_path_to_save_to(tmp_path):
+ document = parse(_SOURCE)
+ with pytest.raises(ValueError, match="no path; pass one"):
+ save(document)
+ assert document.path is None
+ target = tmp_path / "new.lgd"
+ save(document, target)
+ assert document.path == target.absolute()
+ assert document.filename == "new.lgd" # as load names it: diagnostics name the file
+ assert load(target) == document
+
+
+def test_a_document_built_in_code_has_no_path_to_save_to():
+ with pytest.raises(ValueError, match="no path; pass one"):
+ save(legaldown.empty_document())
+
+
+def test_the_path_returned_and_set_is_absolute(tmp_path, monkeypatch):
+ monkeypatch.chdir(tmp_path)
+ document = parse(_SOURCE)
+ result = save(document, "relative.lgd")
+ assert isinstance(result, Path)
+ assert result.is_absolute()
+ assert result == tmp_path / "relative.lgd"
+ assert document.path == result
+ assert (tmp_path / "relative.lgd").exists()
+
+
+def test_a_str_and_a_path_like_are_accepted(tmp_path):
+ document = parse(_SOURCE)
+ assert save(document, str(tmp_path / "a.lgd")) == tmp_path / "a.lgd"
+
+ class Like:
+ def __fspath__(self) -> str:
+ return str(tmp_path / "b.lgd")
+
+ assert save(document, Like()) == tmp_path / "b.lgd"
+ assert (tmp_path / "a.lgd").read_bytes() == (tmp_path / "b.lgd").read_bytes()
+
+
+def test_missing_directories_are_created(tmp_path):
+ target = tmp_path / "a" / "b" / "contract.lgd"
+ save(parse(_SOURCE), target)
+ assert load(target).metadata.title == "Services Agreement"
+
+
+def test_a_saved_document_is_validated_against_files_beside_its_new_path(tmp_path):
+ amendment = (
+ "---\ntitle: A\namends:\n title: Original\n file: original.lgd\n---\n\n# S\n\nUses {{term: services}}.\n"
+ )
+ original = '---\ntitle: O\n---\n\n# S\n\n"Services" {{def: services}} means x.\n'
+ (tmp_path / "work").mkdir()
+ _write(tmp_path / "work" / "original.lgd", original)
+ document = parse(amendment)
+ assert "amend-term-unresolvable" in validate(document).rules() # no path, nothing to read
+ save(document, tmp_path / "work" / "amendment.lgd")
+ rules = validate(document).rules()
+ assert "amend-term-unresolvable" not in rules and "amend-term-undefined" not in rules
+
+
+# line endings and byte-order mark --------------------------------------------
+
+@pytest.mark.parametrize("prefix", [b"", b"\xef\xbb\xbf"])
+@pytest.mark.parametrize("ending", ["\r\n", "\r"])
+def test_crlf_and_a_byte_order_mark_are_written_back_as_lf_without_one(tmp_path, prefix, ending):
+ file = tmp_path / "contract.lgd"
+ file.write_bytes(prefix + _SOURCE.replace("\n", ending).encode("utf-8"))
+ document = load(file)
+ save(document)
+ data = file.read_bytes()
+ assert data == _SOURCE.encode("utf-8")
+ assert b"\r" not in data
+ assert not data.startswith(b"\xef\xbb\xbf")
+ assert load(file) == document
+
+
+def test_text_is_utf_8(tmp_path):
+ document = parse("---\ntitle: Smlouva o dílo\n---\n\n# Předmět\n\nZhotovitel provede dílo — „řádně“.\n")
+ file = tmp_path / "cs.lgd"
+ save(document, file)
+ assert "Zhotovitel provede dílo — „řádně“." in file.read_bytes().decode("utf-8")
+
+
+# atomicity ---------------------------------------------------------------------
+
+def test_a_failure_while_moving_into_place_leaves_the_file_as_it_was(tmp_path, monkeypatch):
+ file = _write(tmp_path / "contract.lgd")
+ document = load(file)
+ document.metadata.title = "Changed"
+
+ def broken(source, destination):
+ raise OSError("disk on fire")
+
+ monkeypatch.setattr(os, "replace", broken)
+ with pytest.raises(OSError, match="disk on fire"):
+ save(document)
+ assert file.read_text(encoding="utf-8") == _SOURCE
+ assert _leftovers(tmp_path, {"contract.lgd"}) == []
+ assert document.path == file.absolute()
+
+
+def test_a_failure_to_save_a_new_file_leaves_no_file_and_no_path(tmp_path, monkeypatch):
+ document = parse(_SOURCE)
+
+ def broken(source, destination):
+ raise OSError("disk on fire")
+
+ monkeypatch.setattr(os, "replace", broken)
+ with pytest.raises(OSError):
+ save(document, tmp_path / "new.lgd")
+ assert list(tmp_path.iterdir()) == []
+ assert document.path is None
+
+
+def test_a_document_that_cannot_be_encoded_leaves_the_file_as_it_was(tmp_path):
+ file = _write(tmp_path / "contract.lgd")
+ document = load(file)
+ document.sections[0].title = "Lone surrogate \ud800"
+ with pytest.raises(ValueError):
+ save(document)
+ assert file.read_text(encoding="utf-8") == _SOURCE
+ assert _leftovers(tmp_path, {"contract.lgd"}) == []
+
+
+def test_a_serializing_failure_writes_nothing(tmp_path, monkeypatch):
+ file = _write(tmp_path / "contract.lgd")
+ document = load(file)
+
+ def broken(_document: Document) -> str:
+ raise RuntimeError("cannot serialize")
+
+ monkeypatch.setattr("legaldown.serializer.serialize", broken)
+ with pytest.raises(RuntimeError):
+ save(document)
+ assert file.read_text(encoding="utf-8") == _SOURCE
+ assert _leftovers(tmp_path, {"contract.lgd"}) == []
+
+
+@pytest.mark.skipif(sys.platform == "win32", reason="symbolic links need privileges on Windows")
+def test_a_symbolic_link_is_written_through(tmp_path):
+ target = _write(tmp_path / "v3-final.lgd")
+ link = tmp_path / "current.lgd"
+ try:
+ link.symlink_to(target.name)
+ except OSError:
+ pytest.skip("symbolic links are not available")
+ document = load(link)
+ document.metadata.title = "Through the link"
+ assert save(document) == link.absolute()
+ assert link.is_symlink()
+ assert os.readlink(link) == target.name
+ assert load(target).metadata.title == "Through the link"
+ assert _leftovers(tmp_path, {"v3-final.lgd", "current.lgd"}) == []
+
+
+@pytest.mark.skipif(sys.platform == "win32", reason="permission bits are not kept on Windows")
+def test_the_permissions_of_an_existing_file_are_kept(tmp_path):
+ file = _write(tmp_path / "contract.lgd")
+ file.chmod(0o640)
+ document = load(file)
+ document.metadata.title = "Changed"
+ save(document)
+ assert stat.S_IMODE(file.stat().st_mode) == 0o640
+ assert load(file).metadata.title == "Changed"
+
+
+@pytest.mark.skipif(sys.platform == "win32", reason="permission bits are not kept on Windows")
+def test_a_new_file_gets_the_permissions_a_file_made_now_would(tmp_path):
+ mask = os.umask(0)
+ os.umask(mask)
+ save(parse(_SOURCE), tmp_path / "new.lgd")
+ assert stat.S_IMODE((tmp_path / "new.lgd").stat().st_mode) == 0o666 & ~mask
+
+
+# no warnings -----------------------------------------------------------------
+
+def test_serialize_and_save_do_not_warn(tmp_path):
+ with warnings.catch_warnings():
+ warnings.simplefilter("error")
+ save(parse(_SOURCE), tmp_path / "x.lgd")
+ serialize(parse(_SOURCE))
diff --git a/tests/test_spec_alignment.py b/tests/test_spec_alignment.py
index fca7dd5..4f6911a 100644
--- a/tests/test_spec_alignment.py
+++ b/tests/test_spec_alignment.py
@@ -19,7 +19,7 @@
iter_directives,
render_block,
render_item,
- serialize_document,
+ serialize,
)
from legaldown.definitions import text_fragments
from legaldown.directives import format_value, lex
@@ -257,7 +257,7 @@ def test_parameters_on_ref_and_def_are_reported():
result = _validate(source)
assert [d.rule for d in result.diagnostics] == ["directive-unknown-param"] * 2
assert result.index.definition_lookup["foo"] == "Foo"
- assert "{{ref: terms, format=long}}" in serialize_document(parse(_FRONTMATTER + source))
+ assert "{{ref: terms, format=long}}" in serialize(parse(_FRONTMATTER + source))
def test_directive_in_code_span_is_not_lifted_or_checked():
@@ -271,7 +271,7 @@ def test_lifted_term_label_round_trips():
source = _FRONTMATTER + '"Svc" {{def: svc}} x.\n\nSee {{term: svc, label="Services, as amended"}}.\n'
block = parse(source).sections[0].blocks[1]
assert (block.kind, block.target, block.label) == ("term", "svc", "Services, as amended")
- assert '{{term: svc, label="Services, as amended"}}' in serialize_document(parse(source))
+ assert '{{term: svc, label="Services, as amended"}}' in serialize(parse(source))
def test_placeholder_currency_is_defined_only_for_money():
@@ -325,7 +325,7 @@ def test_a_curly_quoted_reference_stays_paragraph_text():
validator warns about is not lifted."""
document = parse(_FRONTMATTER + "See {{ref: “terms”}}.\n")
assert document.sections[0].blocks[0].kind == "paragraph"
- assert parse(serialize_document(document)).sections == document.sections
+ assert parse(serialize(document)).sections == document.sections
assert "value-curly-quote" in validate(document).rules("warning")
@@ -335,7 +335,7 @@ def test_a_curly_quoted_reference_stays_paragraph_text():
)
def test_a_quoted_curly_value_stays_quoted_through_a_round_trip(text):
document = parse(_FRONTMATTER + text + "\n")
- reparsed = parse(serialize_document(document))
+ reparsed = parse(serialize(document))
assert reparsed.sections == document.sections
assert "value-curly-quote" not in validate(reparsed).rules()
@@ -359,7 +359,7 @@ def test_collect_source_directives_sees_every_parameter_shape():
def test_explicitly_empty_label_round_trips():
source = _FRONTMATTER + '"X" {{def: x}} y.\n\nSee {{term: x, label=""}}.\n'
- assert '{{term: x, label=""}}' in serialize_document(parse(source))
+ assert '{{term: x, label=""}}' in serialize(parse(source))
def test_malformed_directive_with_unknown_name_is_malformed():
@@ -433,7 +433,7 @@ def test_quoted_value_may_contain_backticks():
def test_lifted_values_are_kept_as_written():
source = _FRONTMATTER + '"X" {{def: x}} y.\n\nSee {{term: x, label="a `b` c"}}.\n'
assert parse(source).sections[0].blocks[1].label == "a `b` c"
- reparsed = parse(serialize_document(parse(source)))
+ reparsed = parse(serialize(parse(source)))
assert reparsed.sections[0].blocks[1].label == "a `b` c"
@@ -461,7 +461,7 @@ def test_malformed_directive_ends_at_the_next_opener(body):
def test_definition_with_other_quotation_marks_round_trips(paragraph):
"""The serializer writes straight quotes, so other delimiters stay text."""
source = _FRONTMATTER + paragraph + "\n"
- assert paragraph in serialize_document(parse(source))
+ assert paragraph in serialize(parse(source))
def test_ref_inside_a_defined_term_is_not_lifted():
@@ -504,7 +504,7 @@ def test_unclosed_directive_does_not_swallow_the_next():
@pytest.mark.parametrize("paragraph", ['"" {{def: foo}} means x.', '" Foo " {{def: foo}} means x.'])
def test_definition_the_block_cannot_hold_stays_text(paragraph):
source = _FRONTMATTER + paragraph + "\n"
- assert paragraph in serialize_document(parse(source))
+ assert paragraph in serialize(parse(source))
def test_escaped_placeholder_in_metadata_is_not_a_placeholder():
@@ -923,7 +923,7 @@ def test_directives_in_the_preamble_are_checked():
def test_preamble_round_trips():
document = parse(_PREAMBLE_SOURCE)
- reparsed = parse(serialize_document(document))
+ reparsed = parse(serialize(document))
assert reparsed.preamble == document.preamble
assert reparsed.sections == document.sections
@@ -1113,7 +1113,7 @@ def test_frontmatter_that_cannot_be_read_raises_frontmatter_error(frontmatter):
)
def test_a_document_without_frontmatter_is_written_without_it(source):
document = parse(source)
- written = serialize_document(document)
+ written = serialize(document)
assert not written.startswith("---")
assert parse(written) == document
assert validate(parse(written)).rules() <= {"frontmatter-absent"}
@@ -1122,7 +1122,7 @@ def test_a_document_without_frontmatter_is_written_without_it(source):
def test_metadata_set_on_a_bare_document_is_written_as_frontmatter():
document = parse("# A\n\nText.\n")
document.metadata.title = "Terms"
- assert serialize_document(document).startswith("---\ntitle: Terms\n")
+ assert serialize(document).startswith("---\ntitle: Terms\n")
def test_frontmatter_absent_is_not_read_from_frontmatter_or_a_dict():
@@ -1170,7 +1170,7 @@ def test_only_spaces_and_tabs_and_nine_digits_make_a_heading_or_an_item(line):
assert [(b.kind, b.text) for b in document.sections[0].blocks] == [
("paragraph", line.strip(" \t\n\r"))
]
- assert parse(serialize_document(document)) == document
+ assert parse(serialize(document)) == document
@pytest.mark.parametrize(("line", "kind"), [
@@ -1256,7 +1256,7 @@ def test_an_anchor_after_a_heading_in_an_item_is_misplaced():
@pytest.mark.parametrize("text", [" See {{ref: x}}.", "See {{ref: x}} ", " Use {{term: x}}."])
def test_a_no_break_space_round_trips_around_a_lifted_directive(text):
document = parse(_FRONTMATTER + text + "\n")
- assert parse(serialize_document(document)) == document
+ assert parse(serialize(document)) == document
def test_a_paragraph_of_only_spaces_is_empty():
@@ -1268,7 +1268,7 @@ def test_a_paragraph_of_a_no_break_space_is_text():
# Not blank in CommonMark (#47): written back, it reads the same.
document = document_from_dict({"sections": [{"title": "A", "blocks": [{"kind": "paragraph", "text": "\u00a0"}]}]})
assert document.sections[0].blocks[0].text == "\u00a0"
- written = parse(serialize_document(document))
+ written = parse(serialize(document))
assert [(b.kind, b.text) for b in written.sections[0].blocks] == [("paragraph", "\u00a0")]
@@ -1300,7 +1300,7 @@ def test_where_a_list_does_not_continue_past_a_blank_line(body, kinds, titles):
assert [b.kind for b in document.sections[0].blocks] == kinds
assert [s.title for s in document.sections] == titles
if "- \n" not in body: # an empty item is not written back (#46)
- assert parse(serialize_document(document)) == document
+ assert parse(serialize(document)) == document
@pytest.mark.parametrize(("body", "items", "after"), [
@@ -1326,7 +1326,7 @@ def test_a_line_short_of_every_open_items_content_is_code(body):
document = parse(_FRONTMATTER + body)
assert [b.kind for b in document.sections[0].blocks] == ["ordered_list" if body[0] == "1" else "unordered_list", "code"]
assert "ref-broken" not in _validate(body).rules()
- assert parse(serialize_document(document)) == document
+ assert parse(serialize(document)) == document
@pytest.mark.parametrize("body", [
@@ -1338,13 +1338,13 @@ def test_a_block_after_a_wide_list_marker_round_trips(body):
"""The list is written back with its last item's content past the
block's indentation, so the block stays after it."""
document = parse(_FRONTMATTER + body)
- assert parse(serialize_document(document)) == document
+ assert parse(serialize(document)) == document
@pytest.mark.parametrize("text", ["
", "
", ""])
def test_a_tag_is_split_only_where_both_lines_stay_paragraph_text(text):
document = document_from_dict({"sections": [{"title": "A", "blocks": [{"kind": "paragraph", "text": text}]}]})
- blocks = parse(serialize_document(document)).sections[0].blocks
+ blocks = parse(serialize(document)).sections[0].blocks
assert [b.kind for b in blocks] == ["paragraph"]
@@ -1363,7 +1363,7 @@ def test_what_follows_a_list_item_that_holds_no_open_paragraph(body, kinds, titl
document = parse(_FRONTMATTER + body)
assert [b.kind for b in document.sections[0].blocks] == kinds
assert [s.title for s in document.sections] == titles
- assert parse(serialize_document(document)) == document
+ assert parse(serialize(document)) == document
def test_wide_marker_spacing_sets_the_items_column_as_commonmark_does():
@@ -1380,12 +1380,12 @@ def test_an_unclosed_fence_after_a_list_is_closed_when_written():
{"title": "A", "blocks": [{"kind": "unordered_list", "items": ["a"]}, {"kind": "code", "text": "```\nx"}]},
{"title": "B", "blocks": []},
]})
- assert [s.title for s in parse(serialize_document(document)).sections] == ["A", "B"]
+ assert [s.title for s in parse(serialize(document)).sections] == ["A", "B"]
def test_an_empty_list_does_not_take_the_code_after_it():
document = parse(_FRONTMATTER + "- a\n\n* \n\n code\n")
- reparsed = parse(serialize_document(document))
+ reparsed = parse(serialize(document))
assert [(b.kind, _texts(b), b.text) for b in reparsed.sections[0].blocks] == [
("unordered_list", ["a"], ""), ("unordered_list", [""], ""), ("code", [], " code")
]
@@ -1426,7 +1426,7 @@ def test_an_items_later_content_round_trips(body):
"""A `#` line in it is written in far enough not to read as a heading;
a fence left open in it ends with the item and is not closed."""
document = parse(_FRONTMATTER + body)
- assert parse(serialize_document(document)) == document
+ assert parse(serialize(document)) == document
def test_a_table_cell_opening_like_a_fence_is_text():
@@ -1441,7 +1441,7 @@ def test_frontmatter_with_cr_line_endings_is_frontmatter():
def test_a_quote_round_trips_a_character_that_is_not_a_line_ending():
document = parse(_FRONTMATTER + "> One\x0ctwo.\n> Three.\n")
- assert serialize_document(document).endswith("> One\x0ctwo.\n> Three.\n")
+ assert serialize(document).endswith("> One\x0ctwo.\n> Three.\n")
@pytest.mark.parametrize(
@@ -1468,7 +1468,7 @@ def test_a_signature_block_heading_is_an_ordinary_section(hashes):
assert [d.message for d in result.diagnostics if d.rule == "ref-broken"] == [
"Broken section reference: 'nowhere'."
]
- serialized = serialize_document(document)
+ serialized = serialize(document)
assert "Signed by the parties." in serialized
assert parse(serialized) == document
@@ -1481,7 +1481,7 @@ def test_a_second_signature_block_identifier_is_a_duplicate():
def test_setext_heading_round_trips_as_an_atx_heading():
"""Its generated identifier is not written: it stays generated."""
source = _BARE + "Scope\n=====\n\nText.\n"
- assert "\n# Scope\n" in serialize_document(parse(source))
+ assert "\n# Scope\n" in serialize(parse(source))
def test_fenced_code_is_one_literal_block():
@@ -1493,7 +1493,7 @@ def test_fenced_code_is_one_literal_block():
assert _outline(source) == [("Terms", 1, "terms"), ("Next", 1, "next")]
assert [(b.kind, b.text) for b in document.sections[0].blocks] == [("code", fence)]
assert validate(document).diagnostics == []
- assert fence in serialize_document(document)
+ assert fence in serialize(document)
@pytest.mark.parametrize(
@@ -1563,7 +1563,7 @@ def test_tab_indented_backticks_neither_open_nor_close_a_fence():
def test_indented_fence_round_trips_unchanged():
fence = " ```\n code\n ```"
- assert fence in serialize_document(parse(_FRONTMATTER + "Para.\n\n" + fence + "\n"))
+ assert fence in serialize(parse(_FRONTMATTER + "Para.\n\n" + fence + "\n"))
def test_paragraph_directly_above_dashes_is_a_setext_heading():
@@ -1586,7 +1586,7 @@ def test_fence_in_a_list_item_is_literal_and_round_trips():
body = "- Example:\n ~~~\n {{ref: nope}}\n\n {#zz}\n ~~~\n"
document = parse(_FRONTMATTER + body)
assert validate(document).diagnostics == []
- assert body.strip() in serialize_document(document)
+ assert body.strip() in serialize(document)
def test_blank_line_ends_the_lazy_context_even_inside_a_list_fence():
@@ -1637,7 +1637,7 @@ def test_item_text_after_its_closed_fence_is_checked():
document = parse(_FRONTMATTER + "- ```\n code\n ```\n more {{ref: nowhere}}\n")
assert _texts(document.sections[0].blocks[0]) == ["```\ncode\n```\nmore {{ref: nowhere}}"]
assert "ref-broken" in validate(document).rules("error")
- assert parse(serialize_document(document)).sections == document.sections
+ assert parse(serialize(document)).sections == document.sections
def test_unclosed_fence_in_an_item_ends_at_the_next_item():
@@ -1673,7 +1673,7 @@ def test_code_block_text_after_its_closing_fence_is_checked():
def test_serializer_closes_an_unclosed_fence():
document = parse(_FRONTMATTER + "Text.\n\n# Next {#next}\n")
document.sections[0].blocks.append(Block(kind="code", text="```\nx"))
- reparsed = parse(serialize_document(document))
+ reparsed = parse(serialize(document))
assert [s.identifier for s in reparsed.sections] == ["terms", "next"]
@@ -1681,7 +1681,7 @@ def test_code_in_a_list_item_keeps_trailing_spaces():
source = _FRONTMATTER + "- ```\n keep \n ```\n"
item = parse(source).sections[0].blocks[0].items[0]
assert [(block.kind, block.text) for block in item.blocks] == [("code", "```\nkeep \n```")]
- assert parse(serialize_document(parse(source))).sections[0].blocks[0].items[0] == item
+ assert parse(serialize(parse(source))).sections[0].blocks[0].items[0] == item
@pytest.mark.parametrize(
@@ -1699,7 +1699,7 @@ def test_backticks_in_directive_values_open_nothing(text, targets):
@pytest.mark.parametrize("paragraph", [" ~~~\n x", " # not a heading"])
def test_paragraph_that_would_open_a_block_stays_indented(paragraph):
source = _FRONTMATTER + paragraph + "\n\n# Next {#next}\n\nBody.\n"
- reparsed = parse(serialize_document(parse(source)))
+ reparsed = parse(serialize(parse(source)))
assert reparsed.sections == parse(source).sections
@@ -1716,7 +1716,7 @@ def test_a_fence_left_open_in_a_list_item_ends_with_its_list():
Block(kind="unordered_list", items=["x\n```\ncode"]),
Block(kind="unordered_list", items=["y"]),
]
- blocks = parse(serialize_document(document)).sections[0].blocks
+ blocks = parse(serialize(document)).sections[0].blocks
assert [_texts(b) for b in blocks[1:]] == [["x\n```\ncode"], ["y"]]
@@ -1746,7 +1746,7 @@ def test_only_some_list_items_interrupt_a_paragraph(second_line, kinds, headings
def _blocks(body: str) -> list[tuple[str, str | list[str]]]:
document = parse(_FRONTMATTER + body)
- assert parse(serialize_document(document)).sections == document.sections
+ assert parse(serialize(document)).sections == document.sections
return [(b.kind, _texts(b) if b.kind.endswith("list") else b.text) for b in document.sections[0].blocks]
@@ -1828,7 +1828,7 @@ def _table(body: str) -> Block:
def _round_trips(body: str) -> None:
document = parse(_FRONTMATTER + body)
- assert parse(serialize_document(document)).sections == document.sections
+ assert parse(serialize(document)).sections == document.sections
def test_an_escaped_pipe_is_cell_text_and_alignment_is_kept():
@@ -1837,7 +1837,7 @@ def test_an_escaped_pipe_is_cell_text_and_alignment_is_kept():
assert block.headers == ["a | b", "c", "d", "e"]
assert block.align == ["left", "right", "center", ""]
assert block.rows == [["1", "2", "3", "4"]]
- assert "| a \\| b | c | d | e |\n| :--- | ---: | :---: | --- |" in serialize_document(
+ assert "| a \\| b | c | d | e |\n| :--- | ---: | :---: | --- |" in serialize(
parse(_FRONTMATTER + body)
)
_round_trips(body)
@@ -1908,7 +1908,7 @@ def test_table_cells_are_positional_in_the_model():
def test_the_serializer_escapes_pipes_in_model_built_cells():
def written(block: Block) -> list[str]:
document = document_from_dict({"sections": [{"title": "A", "blocks": [block]}]})
- return serialize_document(document).split("# A\n\n")[1].splitlines()
+ return serialize(document).split("# A\n\n")[1].splitlines()
assert written({"kind": "table", "headers": ["a|b"], "rows": [["c|d"]]}) == [
"| a\\|b |", "| --- |", "| c\\|d |"
@@ -1921,10 +1921,10 @@ def written(block: Block) -> list[str]:
assert written({"kind": "table", "headers": [], "rows": [[]]}) == ["| |", "| --- |", "| |"]
block = {"kind": "table", "headers": ["a\\|b", "c\\\\|"], "rows": [["\\", "|"]]}
document = document_from_dict({"sections": [{"title": "A", "blocks": [block]}]})
- assert parse(serialize_document(document)).sections == document.sections
+ assert parse(serialize(document)).sections == document.sections
# Nothing to take a width from: one empty column, still a table.
document = document_from_dict({"sections": [{"title": "A", "blocks": [{"kind": "table", "headers": [], "rows": [[]]}]}]})
- assert parse(serialize_document(document)).sections[0].blocks[0].kind == "table"
+ assert parse(serialize(document)).sections[0].blocks[0].kind == "table"
def test_default_headers_widen_to_the_widest_row():
@@ -1951,7 +1951,7 @@ def _html(body: str, *, bare: bool = False) -> tuple[list[str], list[tuple[str,
"""Section titles, and the (kind, text) of every block, preamble first."""
source = (_BARE if bare else _FRONTMATTER) + body
document = parse(source)
- assert parse(serialize_document(document)) == document
+ assert parse(serialize(document)) == document
blocks = [(b.kind, b.text) for _section, _index, b in document.iter_blocks()]
return [s.title for s in document.sections], blocks
@@ -2037,7 +2037,7 @@ def test_an_html_block_in_the_model_keeps_its_indentation():
{"sections": [{"title": "A", "blocks": [{"kind": "html", "text": "\n\n
\n x\n
\n\n"}]}]}
)
assert document.sections[0].blocks[0].text == "
\n x\n
"
- assert parse(serialize_document(document)).sections == document.sections
+ assert parse(serialize(document)).sections == document.sections
@pytest.mark.parametrize("prefix", ["
see", "# see", "```"])
@@ -2050,7 +2050,7 @@ def test_a_model_built_reference_that_would_open_a_block_is_escaped(prefix):
{"kind": "paragraph", "text": f"{prefix} text"},
]}]}
)
- reparsed = parse(serialize_document(document)).sections[0].blocks
+ reparsed = parse(serialize(document)).sections[0].blocks
assert [(b.kind, b.prefix or b.text) for b in reparsed] == [("ref", f"\\{prefix} "), ("paragraph", f"\\{prefix} text")]
@@ -2059,7 +2059,7 @@ def test_a_model_built_reference_that_would_open_a_block_is_escaped(prefix):
def _code_blocks(body: str) -> list[tuple[str, str]]:
document = parse(_FRONTMATTER + body)
- assert parse(serialize_document(document)) == document
+ assert parse(serialize(document)) == document
return [(b.kind, b.text) for b in document.sections[0].blocks]
@@ -2103,7 +2103,7 @@ def test_a_heading_indented_up_to_three_spaces_interrupts_a_paragraph():
def test_a_document_can_open_with_indented_code():
document = parse(" code\n\n# A\n")
assert [(b.kind, b.text) for b in document.preamble] == [("code", " code")]
- assert parse(serialize_document(document)) == document
+ assert parse(serialize(document)) == document
def test_indented_code_after_a_list_is_written_as_it_is():
@@ -2113,7 +2113,7 @@ def test_indented_code_after_a_list_is_written_as_it_is():
{"kind": "unordered_list", "items": ["one"]},
{"kind": "code", "text": " x = `y`\n\n z"},
]}]})
- written = serialize_document(document)
+ written = serialize(document)
assert "- one\n\n x = `y`\n\n z" in written
assert parse(written) == document
@@ -2135,7 +2135,7 @@ def test_an_indented_line_after_a_list_round_trips_whatever_it_begins_with(opene
closer = f"\n {opener}" if opener in ("```", "~~~") else ""
body = f"- a\n\n b\n\n {opener}{closer}\n\nd\n\n code\n"
document = parse(_FRONTMATTER + body)
- assert parse(serialize_document(document)) == document
+ assert parse(serialize(document)) == document
last = {
"# foo": ("heading", "foo"), "
": ("html", "
"), "```": ("code", " ```\n ```"),
"~~~": ("code", " ~~~\n ~~~"), "": ("html", " "),
@@ -2175,7 +2175,7 @@ def test_code_directly_after_a_list_is_written_so_it_stays_code():
item's; only code with no paragraph between it and the list needs
fencing to stay code rather than become the item's own text."""
document = parse(_FRONTMATTER + "- a\n\n
\n\n code {{ref: nope}}\n")
- reparsed = parse(serialize_document(document))
+ reparsed = parse(serialize(document))
assert [(b.kind, b.text) for b in reparsed.sections[0].blocks] == [
("unordered_list", ""), ("paragraph", ""), ("code", " code {{ref: nope}}")
]
@@ -2183,12 +2183,12 @@ def test_code_directly_after_a_list_is_written_so_it_stays_code():
document = document_from_dict({"sections": [{"title": "A", "blocks": [
{"kind": "unordered_list", "items": ["a"]}, {"kind": "code", "text": " x"},
]}]})
- blocks = parse(serialize_document(document)).sections[0].blocks
+ blocks = parse(serialize(document)).sections[0].blocks
assert [(b.kind, b.text) for b in blocks] == [("unordered_list", ""), ("code", " x")]
document = document_from_dict({"sections": [{"title": "A", "blocks": [
{"kind": "unordered_list", "items": ["a"]}, {"kind": "paragraph", "text": "# foo"}, {"kind": "code", "text": " x"},
]}]})
- blocks = parse(serialize_document(document)).sections[0].blocks
+ blocks = parse(serialize(document)).sections[0].blocks
assert [(b.kind, b.text) for b in blocks] == [("unordered_list", ""), ("paragraph", "\\# foo"), ("code", " x")]
@@ -2197,7 +2197,7 @@ def test_text_after_a_list_that_would_open_a_block_is_escaped():
{"kind": "unordered_list", "items": ["a"]}, {"kind": "paragraph", "text": "> q"},
{"kind": "paragraph", "text": "# foo"},
]}]})
- blocks = parse(serialize_document(document)).sections[0].blocks
+ blocks = parse(serialize(document)).sections[0].blocks
assert (blocks[-1].kind, blocks[-1].text) == ("paragraph", "\\# foo")
@@ -2233,7 +2233,7 @@ def test_a_pipe_paragraph_does_not_end_the_list_item(first):
together with the heading-looking line after it."""
body = f"- a\n\n {first}\n\n # foo\n"
document = parse(_FRONTMATTER + body)
- assert parse(serialize_document(document)) == document
+ assert parse(serialize(document)) == document
assert [_shape(b) for b in document.sections[0].blocks] == [("unordered_list", [["a", first, ("heading", "foo")]])]
assert "ref-broken" not in validate(document).rules()
@@ -2275,7 +2275,7 @@ def _list(body: str) -> Block:
def test_a_nested_item_is_in_the_item_it_is_nested_in(body, shape):
assert _shape(_list(body))[1] == shape
document = parse(_FRONTMATTER + body)
- assert parse(serialize_document(document)) == document
+ assert parse(serialize(document)) == document
@pytest.mark.parametrize(
@@ -2293,7 +2293,7 @@ def test_a_nested_item_is_in_the_item_it_is_nested_in(body, shape):
)
def test_a_nested_list_is_written_as_it_was(body):
document = parse(_FRONTMATTER + body)
- written = serialize_document(document)
+ written = serialize(document)
assert written.endswith("{#terms}\n\n" + body)
assert parse(written) == document
@@ -2326,7 +2326,7 @@ def test_code_after_a_nested_list_stays_code():
# indentation, which puts the items nested in it past it too.
document = parse(_FRONTMATTER + "- a\n - b\n\n\n\n code\n")
del document.sections[0].blocks[1] # the comment: the code now follows the list
- written = serialize_document(document)
+ written = serialize(document)
assert "- a\n - b\n\n code" in written
assert parse(written) == document
@@ -2338,7 +2338,7 @@ def test_an_item_written_as_a_string_is_read_as_its_content():
]}]})
[block] = document.sections[0].blocks
assert _shape(block)[1] == [["a", (_U, [["b"]]), "more"], [], ["c"]]
- assert serialize_document(document).endswith("# A\n\n- a\n - b\n\n more\n-\n- c\n")
+ assert serialize(document).endswith("# A\n\n- a\n - b\n\n more\n-\n- c\n")
# A block built in code may hold them too.
assert _texts(Block(kind=_U, items=["x", "y"])) == ["x", "y"]
@@ -2380,7 +2380,7 @@ def test_a_list_indented_into_an_earlier_items_content_is_the_items():
def test_a_marker_short_of_the_items_content_but_four_columns_in_is_lazy_text(body, shape):
block = _list(body)
assert _shape(block)[1] == shape
- written = serialize_document(parse(_FRONTMATTER + body))
+ written = serialize(parse(_FRONTMATTER + body))
assert parse(written).sections[0].blocks == [block]
@@ -2409,7 +2409,7 @@ def test_a_line_short_of_an_items_content_ends_it_after_anything_but_a_paragraph
def test_leaving_signatures_out_survives_a_round_trip(value):
document = parse(f"---\ntitle: T\ninclude_signatures: {value}\n---\n\n# A\n\nx\n")
assert document.metadata.include_signatures is False
- written = serialize_document(document)
+ written = serialize(document)
assert "include_signatures: false" in written
assert parse(written).metadata.include_signatures is False
assert document_from_dict(document_to_dict(document)).metadata.include_signatures is False
@@ -2417,12 +2417,12 @@ def test_leaving_signatures_out_survives_a_round_trip(value):
def test_the_default_include_signatures_is_not_written():
document = parse("---\ntitle: T\ninclude_signatures: true\n---\n\n# A\n\nx\n")
- assert "include_signatures" not in serialize_document(document)
+ assert "include_signatures" not in serialize(document)
# A document without frontmatter stays without it.
bare = parse("# A\n\nx\n")
- assert serialize_document(bare) == "# A\n\nx\n"
+ assert serialize(bare) == "# A\n\nx\n"
bare.metadata.include_signatures = False
- assert parse(serialize_document(bare)).metadata.include_signatures is False
+ assert parse(serialize(bare)).metadata.include_signatures is False
# ── Empty list items (#46) ────────────────────────────────────────
@@ -2455,7 +2455,7 @@ def test_an_empty_item_keeps_its_place(body, shape):
document = parse(_FRONTMATTER + body)
[block] = document.sections[0].blocks
assert _shape(block)[1] == shape
- assert parse(serialize_document(document)) == document
+ assert parse(serialize(document)) == document
@pytest.mark.parametrize(
@@ -2472,7 +2472,7 @@ def test_an_empty_item_keeps_its_place(body, shape):
def test_where_a_bare_marker_starts_a_list(body, kinds):
document = parse(_FRONTMATTER + body)
assert [block.kind for section in document.sections for block in section.blocks] == kinds
- assert parse(serialize_document(document)) == document
+ assert parse(serialize(document)) == document
def test_an_empty_item_with_items_nested_after_it_does_not_take_code():
@@ -2480,7 +2480,7 @@ def test_an_empty_item_with_items_nested_after_it_does_not_take_code():
# the code out of it, so the code is written fenced.
document = parse(_FRONTMATTER + "1.\n -\n\n\n\n code\n")
del document.sections[0].blocks[1]
- reparsed = parse(serialize_document(document))
+ reparsed = parse(serialize(document))
assert [block.kind for block in reparsed.sections[0].blocks] == [_O, "code"]
assert reparsed.sections[0].blocks[0] == document.sections[0].blocks[0]
@@ -2498,7 +2498,7 @@ def test_conditions_pass_an_empty_item_by():
@pytest.mark.parametrize(("text", "written"), [("-", "\\-"), ("1.", "1\\."), ("2) x", "2\\) x"), ("+ y", "\\+ y")])
def test_a_model_built_paragraph_that_reads_as_a_list_item_is_escaped(text, written):
document = document_from_dict({"sections": [{"title": "A", "blocks": [{"kind": "paragraph", "text": text}]}]})
- output = serialize_document(document)
+ output = serialize(document)
assert output.endswith("# A\n\n" + written + "\n")
assert [block.kind for block in parse(output).sections[0].blocks] == ["paragraph"]
@@ -2512,7 +2512,7 @@ def test_a_model_built_paragraph_that_reads_as_a_list_item_is_escaped(text, writ
)
def test_a_model_built_paragraph_that_reads_as_another_block_is_escaped(text, written):
document = document_from_dict({"sections": [{"title": "A", "blocks": [{"kind": "paragraph", "text": text}]}]})
- output = serialize_document(document)
+ output = serialize(document)
assert output.endswith("# A\n\n" + written + "\n")
assert [block.kind for block in parse(output).sections[0].blocks] == ["paragraph"]
@@ -2521,7 +2521,7 @@ def _lists(*blocks: tuple[str, list[str]]) -> str:
document = document_from_dict({"sections": [{"title": "A", "blocks": [
{"kind": kind, "items": items} for kind, items in blocks
]}]})
- output = serialize_document(document)
+ output = serialize(document)
assert parse(output) == document
return output.split("# A\n\n", 1)[1]
@@ -2550,7 +2550,7 @@ def test_adjacent_lists_whose_items_rule_out_two_bullets_still_differ():
document = document_from_dict({"sections": [{"title": "A", "blocks": [
{"kind": _U, "items": ["--", "**\n```\ncode"]}, {"kind": _U, "items": ["--", "**"]},
]}]})
- assert parse(serialize_document(document)) == document
+ assert parse(serialize(document)) == document
# ── A fence left open at the end (#67) ────────────────────────────
@@ -2563,7 +2563,7 @@ def test_adjacent_lists_whose_items_rule_out_two_bullets_still_differ():
@pytest.mark.parametrize("before", ["# A\n\n", ""]) # in a section, or in the preamble
def test_a_fence_left_open_at_the_end_is_written_as_it_is(body, before):
document = parse("---\ntitle: T\nlanguage: en\n---\n\n" + before + body)
- written = serialize_document(document)
+ written = serialize(document)
assert written.endswith("\n\n" + body)
assert parse(written) == document
@@ -2571,7 +2571,7 @@ def test_a_fence_left_open_at_the_end_is_written_as_it_is(body, before):
def test_a_fence_left_open_after_a_list_at_the_end_stays_open():
# The item's fence ends with the item; the last one runs to the end.
document = parse(_FRONTMATTER + "- a\n\n ```\n code\n ```\n")
- written = serialize_document(document)
+ written = serialize(document)
assert written.endswith("\n ```\n")
assert parse(written) == document
@@ -2581,7 +2581,7 @@ def test_a_fence_left_open_before_a_heading_is_closed():
document = document_from_dict({"sections": [
{"title": "A", "blocks": [{"kind": "code", "text": "```\nx"}]}, {"title": "B"},
]})
- written = serialize_document(document)
+ written = serialize(document)
assert "```\nx\n```\n\n# B" in written
assert [section.title for section in parse(written).sections] == ["A", "B"]
@@ -2602,12 +2602,12 @@ def test_a_nested_list_that_changes_its_marker_is_another_list(body, shape):
document = parse(_FRONTMATTER + body)
[block] = document.sections[0].blocks
assert _shape(block)[1] == shape
- assert parse(serialize_document(document)) == document
+ assert parse(serialize(document)) == document
def test_the_second_nested_list_is_numbered_from_one():
document = parse(_FRONTMATTER + "- one\n 1. a\n 2. b\n 1) c\n 2) d\n")
- assert serialize_document(document).endswith("- one\n 1. a\n 2. b\n 1) c\n 2) d\n")
+ assert serialize(document).endswith("- one\n 1. a\n 2. b\n 1) c\n 2) d\n")
def test_an_item_that_would_make_a_thematic_break_starts_on_the_next_line():
@@ -2615,7 +2615,7 @@ def test_an_item_that_would_make_a_thematic_break_starts_on_the_next_line():
document = document_from_dict({"sections": [{"title": "A", "blocks": [
{"kind": _U, "items": ["a"]}, {"kind": _U, "items": ["--", "**"]}, {"kind": _U, "items": ["--", "**"]},
]}]})
- written = serialize_document(document)
+ written = serialize(document)
assert parse(written) == document
assert "\n-\n --\n" in written # the third list's: the second took "+"
@@ -2638,7 +2638,7 @@ def test_an_item_that_would_make_a_thematic_break_starts_on_the_next_line():
def test_an_items_content_is_blocks(body, shape):
document = parse(_FRONTMATTER + body)
assert _shape(document.sections[0].blocks[0])[1] == shape
- assert parse(serialize_document(document)) == document
+ assert parse(serialize(document)) == document
def test_directives_in_an_items_raw_html_are_not_read():
@@ -2663,7 +2663,7 @@ def test_a_nested_item_after_a_later_paragraph_is_in_its_item():
def test_a_chain_of_items_each_opening_with_the_next_is_written_quickly():
# Each nested list's first lines are read once, not once per level above.
document = parse(_FRONTMATTER + "- " * 40 + "a\n")
- assert parse(serialize_document(document)) == document
+ assert parse(serialize(document)) == document
def test_lists_nested_past_the_limit_are_read_as_text():
@@ -2750,7 +2750,7 @@ def test_a_quote_built_with_a_tab_is_written_as_its_content_reads():
document = parse(_FRONTMATTER + "Text.\n")
document.sections[0].blocks = [Block(kind="quote", text="\t{{ref: nowhere}}")]
assert "ref-broken" not in validate(document).rules() # four columns: code
- written = parse(serialize_document(document))
+ written = parse(serialize(document))
assert "ref-broken" not in validate(written).rules()
@@ -2789,7 +2789,7 @@ def test_quotes_nested_past_the_limit_are_read_as_text():
assert "ref-broken" in _validate(f"{deep} {{{{ref: nowhere}}}}\n").rules() # its text, lexed
assert "ref-broken" not in _validate(f"{'>' * 3} {{{{ref: nowhere}}}}\n").rules()
document = parse(_FRONTMATTER + ">" * 10000 + " a\n")
- assert parse(serialize_document(document)) == document
+ assert parse(serialize(document)) == document
_validate(">" * 10000 + " a\n")
@@ -2868,7 +2868,7 @@ def test_a_no_break_space_after_a_fence_does_not_close_it():
def test_a_no_break_space_ending_a_setext_headings_line_is_kept():
document = parse(f"---\ntitle: T\n---\n\nTitle{_NBSP}\nMore\n===\n")
assert document.sections[0].title == f"Title{_NBSP} More"
- assert parse(serialize_document(document)) == document
+ assert parse(serialize(document)) == document
# ── Raw HTML in a list item ends where CommonMark ends it ─────────
@@ -3107,7 +3107,7 @@ def _tree(block: Block):
def _trees(body: str) -> list:
document = parse(_FRONTMATTER + body)
- assert parse(serialize_document(document)) == document
+ assert parse(serialize(document)) == document
return [_tree(block) for block in document.sections[0].blocks]
@@ -3256,7 +3256,7 @@ def test_a_model_built_heading_block_is_written_as_one_in_an_item(heading, writt
{"kind": "unordered_list", "items": [{"blocks": [{"kind": "paragraph", "text": "a"}]}]},
]}]})
list_items(document.sections[0].blocks[0])[0].blocks.append(heading)
- source = serialize_document(document)
+ source = serialize(document)
# Four columns in, where a heading after a blank line stays the item's.
assert f"- a\n {written}\n" in source
[reread] = list_items(parse(source).sections[0].blocks[0])[0].blocks[1:]
@@ -3266,7 +3266,7 @@ def test_a_model_built_heading_block_is_written_as_one_in_an_item(heading, writt
def test_a_model_built_heading_outside_an_item_is_written_as_text():
"""At the margin it would be a section's heading."""
document = document_from_dict({"sections": [{"title": "A", "blocks": [{"kind": "heading", "text": "T", "level": 3}]}]})
- source = serialize_document(document)
+ source = serialize(document)
assert "\n\\### T\n" in source
assert [(b.kind, b.text) for b in parse(source).sections[0].blocks] == [("paragraph", "\\### T")]
@@ -3276,7 +3276,7 @@ def test_a_paragraph_in_an_item_that_reads_as_a_heading_is_escaped(text):
document = document_from_dict({"sections": [{"title": "A", "blocks": [{"kind": "unordered_list", "items": [
{"blocks": [{"kind": "paragraph", "text": "a"}, {"kind": "paragraph", "text": text}]},
]}]}]})
- [item] = list_items(parse(serialize_document(document)).sections[0].blocks[0])
+ [item] = list_items(parse(serialize(document)).sections[0].blocks[0])
assert [(b.kind, b.text) for b in item.blocks] == [("paragraph", "a"), ("paragraph", "\\" + text)]
@@ -3302,7 +3302,7 @@ def _with_sources(*rows: str, preamble: tuple[str, ...] = ()) -> str:
"preamble": [{"kind": "source", "text": row} for row in preamble],
"sections": [{"title": "A", "blocks": [{"kind": "source", "text": row} for row in rows]}],
})
- return serialize_document(document)
+ return serialize(document)
def test_source_blocks_are_written_as_they_stand():
@@ -3337,21 +3337,21 @@ def test_a_fence_a_source_leaves_open_is_closed_unless_it_ends_the_document():
document = document_from_dict({"sections": [
{"title": "A", "blocks": [{"kind": "source", "text": "~~~\ncode"}]}, {"title": "B"},
]})
- assert [s.title for s in parse(serialize_document(document)).sections] == ["A", "B"]
+ assert [s.title for s in parse(serialize(document)).sections] == ["A", "B"]
def test_a_source_after_a_list_is_not_read_into_it():
document = document_from_dict({"sections": [{"title": "A", "blocks": [
{"kind": "unordered_list", "items": ["a"]}, {"kind": "source", "text": " code"},
]}]})
- blocks = parse(serialize_document(document)).sections[0].blocks
+ blocks = parse(serialize(document)).sections[0].blocks
assert [(b.kind, b.text) for b in blocks] == [("unordered_list", ""), ("code", " code")]
def test_a_document_without_frontmatter_opening_with_a_rule_in_a_source():
document = document_from_dict({"preamble": [{"kind": "source", "text": "---\nText."}]})
document.metadata.frontmatter_absent = True
- source = serialize_document(document)
+ source = serialize(document)
assert source == "***\nText.\n"
reread = parse(source)
assert reread.metadata.frontmatter_absent and [b.kind for b in reread.preamble] == ["rule", "paragraph"]
@@ -3363,14 +3363,14 @@ def test_a_source_block_is_not_validated():
{"kind": "source", "text": "See {{ref: nope}}."},
]}]})
assert "ref-broken" not in validate(document).rules()
- assert "ref-broken" in validate(parse(serialize_document(document))).rules()
+ assert "ref-broken" in validate(parse(serialize(document))).rules()
def test_a_source_block_in_a_list_item_is_its_content():
document = document_from_dict({"sections": [{"title": "A", "blocks": [{"kind": "unordered_list", "items": [
{"blocks": [{"kind": "paragraph", "text": "a"}, {"kind": "source", "text": "* x\n* y"}]},
]}]}]})
- source = serialize_document(document)
+ source = serialize(document)
assert "- a\n\n * x\n * y\n" in source
[item] = list_items(parse(source).sections[0].blocks[0])
assert [(b.kind, len(b.items)) for b in item.blocks] == [("paragraph", 0), ("unordered_list", 2)]
@@ -3406,7 +3406,7 @@ def test_a_paragraph_keeps_its_lines(body, text):
"[a]: https://example.com/a\n[b]: https://example.com/b\n\nSee [a] and [b].\n",
])
def test_a_hard_line_break_is_written_back(body):
- assert serialize_document(parse(_FRONTMATTER + body)).endswith("\n" + body)
+ assert serialize(parse(_FRONTMATTER + body)).endswith("\n" + body)
def test_an_items_and_a_quotes_paragraphs_keep_their_lines():
@@ -3438,7 +3438,7 @@ def test_a_definition_over_lines_is_lifted_with_its_lines():
body = '"Term" {{def: term}} means\nthe thing.\n'
[block] = parse(_FRONTMATTER + body).sections[0].blocks
assert (block.kind, block.term, block.text) == ("definition", "Term", "means\nthe thing.")
- assert serialize_document(parse(_FRONTMATTER + body)).endswith("\n" + body)
+ assert serialize(parse(_FRONTMATTER + body)).endswith("\n" + body)
def test_a_marker_ends_a_paragraph_over_lines_only_at_its_end():
@@ -3495,7 +3495,7 @@ def test_a_models_paragraph_is_written_as_its_lines(text, read, in_item):
paragraph = {"kind": "paragraph", "text": text}
blocks = [{"kind": "unordered_list", "items": [{"blocks": [paragraph]}]}] if in_item else [paragraph]
document = document_from_dict({"sections": [{"title": "A", "blocks": blocks}]})
- [block] = parse(serialize_document(document)).sections[0].blocks
+ [block] = parse(serialize(document)).sections[0].blocks
if in_item:
[block] = list_items(block)[0].blocks
assert (block.kind, block.text) == ("paragraph", read)
@@ -3538,7 +3538,7 @@ def test_an_entry_not_yet_complete_is_written_back(frontmatter, rule):
yet filled in keeps its Error, rather than vanishing and leaving others
in its place (attach-undeclared, sides-absent)."""
document = parse(f"---\ntitle: T\n{frontmatter}---\n\n# A\n\nText.\n")
- again = parse(serialize_document(document))
+ again = parse(serialize(document))
assert again.metadata == document.metadata
assert rule in validate(document).rules()
assert validate(again).rules() == validate(document).rules()
@@ -3555,7 +3555,7 @@ def test_a_partys_custom_fields_are_its_other_keys():
document = parse(source)
party = document.metadata.sides[0].parties[0]
assert [(f.label, f.value) for f in party.custom_fields] == [("tax_id", "CZ123"), ("vat", ""), ("count", "0")]
- written = serialize_document(document)
+ written = serialize(document)
assert "tax_id: CZ123\n" in written
assert parse(written).metadata == document.metadata
# A model's own, in the dict form it holds them in.
@@ -3573,11 +3573,11 @@ def test_custom_fields_no_keys_can_hold_are_written_as_a_list(fields):
``custom_fields`` list holds them, in order, and its fields stay."""
document = parse(f"---\ntitle: T\nsides:\n{_SIDE_B}---\n\n# A\n\nText.\n")
document.metadata.sides[0].parties[0].custom_fields = fields
- written = serialize_document(document)
+ written = serialize(document)
again = parse(written).metadata.sides[0].parties[0]
assert (again.name, again.type) == ("y", "legal_entity")
assert [(f.label, f.value) for f in again.custom_fields] == [(f.label.strip(), f.value) for f in fields]
- assert serialize_document(parse(written)) == written
+ assert serialize(parse(written)) == written
def test_a_placeholder_in_a_custom_fields_name_is_reported():
@@ -3595,9 +3595,9 @@ def test_a_custom_field_row_not_yet_labelled_is_written_back():
"""An editor's new row (§3.4): kept, as the key ``''``, until labelled."""
document = parse(f"---\ntitle: T\nsides:\n{_SIDE_B}---\n\n# A\n\nText.\n")
document.metadata.sides[0].parties[0].custom_fields = [CustomField(), CustomField("vat", "")]
- written = serialize_document(document)
+ written = serialize(document)
assert parse(written).metadata == document.metadata
- assert serialize_document(parse(written)) == written
+ assert serialize(parse(written)) == written
@pytest.mark.parametrize("key", ["' name'", "'type '", "' custom_fields'"])
@@ -3605,7 +3605,7 @@ def test_a_key_naming_a_party_field_with_spaces_is_no_custom_field(key):
source = f"---\ntitle: T\nsides:\n - name: a\n parties:\n - name: x\n {key}: z\n{_SIDE_B}---\n\n# A\n\nText.\n"
document = parse(source)
assert document.metadata.sides[0].parties[0].custom_fields == []
- assert parse(serialize_document(document)).metadata == document.metadata
+ assert parse(serialize(document)).metadata == document.metadata
def test_a_custom_field_in_the_list_form_is_reported_on_its_line():
@@ -3726,9 +3726,9 @@ def test_a_fence_left_open_in_a_list_item_keeps_its_blank_lines_whatever_the_sib
)
def test_a_fence_left_open_in_a_nested_item_survives_the_round_trip(source):
document = parse("---\ntitle: T\n---\n\n" + source)
- again = parse(serialize_document(document))
+ again = parse(serialize(document))
assert again == document
- assert serialize_document(again) == serialize_document(document)
+ assert serialize(again) == serialize(document)
def test_an_item_nested_after_the_first_line_text_of_an_item_that_only_nests_ends_a_paragraph_no_more_than_before():
diff --git a/tests/test_templates.py b/tests/test_templates.py
index ecb048e..a113f8d 100644
--- a/tests/test_templates.py
+++ b/tests/test_templates.py
@@ -4,7 +4,7 @@
import pytest
-from legaldown import Amends, document_from_dict, document_to_dict, item_text, serialize_document
+from legaldown import Amends, document_from_dict, document_to_dict, item_text, serialize
from legaldown.parser import parse
from legaldown.validator import validate
from legaldown.validator.templates import answer_problem
@@ -205,7 +205,7 @@ def test_questions_and_conditional_attachments_round_trip():
)
source = f"---\ntitle: Fixture\n{_SIDES}{frontmatter}---\n\n# Terms {{#terms}}\n\nText.\n"
document = parse(source)
- again = parse(serialize_document(document))
+ again = parse(serialize(document))
assert again.metadata == document.metadata
assert again.metadata.legaldown == "0.2"
assert again.metadata.attachments[0].when == "personal-data"
@@ -280,7 +280,7 @@ def test_supersedes_object_form_is_modelled_and_its_title_required():
f"---\ntitle: Fixture\n{_SIDES}supersedes:\n title: Old NDA\n file: old.pdf\n---\n"
)
assert document.metadata.supersedes == Amends(title="Old NDA", file="old.pdf")
- assert parse(serialize_document(document)).metadata == document.metadata
+ assert parse(serialize(document)).metadata == document.metadata
# ── Include-only paragraphs (§12.2) ───────────────────────────────
@@ -324,7 +324,7 @@ def test_keys_yaml_reads_as_booleans_stay_distinct_and_as_written():
source = f"---\ntitle: Fixture\n{_SIDES}{frontmatter}---\n"
document = parse(source)
assert list(document.metadata.questions) == ["yes", "true"]
- assert parse(serialize_document(document)).metadata == document.metadata
+ assert parse(serialize(document)).metadata == document.metadata
def test_an_empty_attachments_key_in_a_template_is_line_editable():
@@ -434,7 +434,7 @@ def test_an_attachment_condition_is_read_as_written(when):
)
document = parse(source)
assert document.metadata.attachments[0].when == when
- assert parse(serialize_document(document)).metadata == document.metadata
+ assert parse(serialize(document)).metadata == document.metadata
def test_choice_labels_are_read_as_written():
@@ -522,7 +522,7 @@ def test_the_serializer_writes_shared_declarations_in_full():
source = (
f"---\ntitle: Fixture\n{_SIDES}questions:\n q: &a\n type: text\n r: *a\n---\n"
)
- written = serialize_document(parse(source))
+ written = serialize(parse(source))
assert "&" not in written and "*" not in written
assert parse(written).metadata.questions == {"q": {"type": "text"}, "r": {"type": "text"}}
@@ -778,7 +778,7 @@ def test_a_lazy_line_continues_its_quote():
("quote", "quoted\ncontinued"),
("paragraph", "After."),
]
- assert parse(serialize_document(document)).sections == document.sections
+ assert parse(serialize(document)).sections == document.sections
@pytest.mark.parametrize(
@@ -852,7 +852,7 @@ def test_a_drafting_note_in_a_list_item_is_recognized():
assert "drafting-note-def" in result.rules("error")
assert "drafting-note-unrecognized" in result.rules("warning")
assert "template-construct-present" in result.rules("error")
- assert parse(serialize_document(document)).sections == document.sections
+ assert parse(serialize(document)).sections == document.sections
def test_a_quote_whose_first_line_is_blank_is_not_a_drafting_note():
@@ -861,7 +861,7 @@ def test_a_quote_whose_first_line_is_blank_is_not_a_drafting_note():
assert not {"drafting-note-def", "template-construct-present"} & validate(
document, final=True
).rules()
- assert parse(serialize_document(document)).sections == document.sections
+ assert parse(serialize(document)).sections == document.sections
@pytest.mark.parametrize(
@@ -876,7 +876,7 @@ def test_a_lazy_line_after_a_quote_in_a_list_item_stays_in_the_quote():
body = '- item\n > [!DRAFTING]\n > note\n"Fee" {{def: fee}} means the fee.\n\nPay the {{term: fee}}.'
document = parse(f"---\ntitle: T\n{_SIDES}---\n\n# A\n\n{body}\n")
assert "drafting-note-def" in validate(document).rules("error")
- assert parse(serialize_document(document)).sections == document.sections
+ assert parse(serialize(document)).sections == document.sections
# ── Quote extents, one scanner for every quote (second review) ────
@@ -902,7 +902,7 @@ def test_a_lazy_line_after_a_quote_in_a_list_item_stays_in_the_quote():
def test_a_drafting_note_extends_as_its_quote_does(body, in_note):
document = parse(f"---\ntitle: T\n{_SIDES}---\n\n# A\n\n{body}\n\nPay the {{{{term: fee}}}}.\n")
assert ("drafting-note-def" in validate(document).rules()) == in_note
- assert parse(serialize_document(document)).sections == document.sections
+ assert parse(serialize(document)).sections == document.sections
def test_quote_lines_in_an_items_code_are_code():
@@ -937,7 +937,7 @@ def test_an_items_text_after_a_closed_quote_is_not_quoted(first):
document = parse(f"---\ntitle: T\n---\n\n# A\n\n- {first}\n text after quote\n")
item = document.sections[0].blocks[0].items[0]
assert (item.blocks[-1].kind, item.blocks[-1].text) == ("paragraph", "text after quote")
- assert parse(serialize_document(document)).sections == document.sections
+ assert parse(serialize(document)).sections == document.sections
@pytest.mark.parametrize(