diff --git a/tests/test_migration.py b/tests/test_migration.py
index bc069ad..7b18754 100644
--- a/tests/test_migration.py
+++ b/tests/test_migration.py
@@ -1,5 +1,6 @@
"""Tests for migrating documents to another spec version."""
+import re
import sys
import zipfile
from pathlib import Path
@@ -7,6 +8,7 @@
import pytest
from doclang import MigrationError, SchematronBackendNotFound, UnsupportedVersionError, migrate
+from doclang._schemas import _available_versions, _version_key
from doclang.migration._api import _head_version
from doclang.migration._plan import NoMigrationPath, Step, _plan
@@ -16,6 +18,8 @@
CASES_DIR = Path(__file__).parent / "data" / "migration"
NS = "https://www.doclang.ai/ns/v0"
+NEWEST = max(_available_versions(), key=_version_key) # the newest released version
+NEXT = f"{_version_key(NEWEST)[0]}.{_version_key(NEWEST)[1] + 1}" # the head's version, when it has changes
TRACK = ''
@@ -185,16 +189,18 @@ def mark(root, changes):
root[0].text = "marked"
_with_unreleased_head(monkeypatch, mark)
- result = migrate(_doc(tmp_path, "x"), to="0.9")
- assert (result.source_version, result.target_version) == ("0.8", "0.9")
- assert "marked" in result.xml and 'version="0.9"' in result.xml
-
+ result = migrate(_doc(tmp_path, "x", version=NEWEST), to=NEXT)
+ assert (result.source_version, result.target_version) == (NEWEST, NEXT)
+ assert "marked" in result.xml and f'version="{NEXT}"' in result.xml
-HEAD = _head_version("0.8")
+def test_head_has_a_version_of_its_own_only_with_changes_of_its_own(monkeypatch):
+ from doclang.migration import _api
-def test_head_has_a_version_of_its_own():
- assert HEAD == "0.9"
+ monkeypatch.setattr(_api, "_head_description", lambda: "unreleased development version")
+ assert _head_version(NEWEST) == NEXT
+ monkeypatch.setattr(_api, "_head_description", lambda: NEWEST) # as right after a release
+ assert _head_version(NEWEST) is None
def test_texts_are_stripped_and_unneeded_content_unwrapped(tmp_path):
@@ -203,14 +209,14 @@ def test_texts_are_stripped_and_unneeded_content_unwrapped(tmp_path):
" of June, see \n the agenda\n.\n "
"\n x y z\n"
)
- result = migrate(_doc(tmp_path, body), to=HEAD)
+ result = migrate(_doc(tmp_path, body), to="0.9")
- assert (result.source_version, result.target_version, result.changes) == ("0.8", HEAD, [])
+ assert (result.source_version, result.target_version, result.changes) == ("0.8", "0.9", [])
assert ( # the target ignores the whitespace at the start and end of the body, so it stays
"\n Held on the 21st of June, see the agenda.\n "
) in result.xml
assert "xyz" in result.xml # whitespace next to tags was not guaranteed
- assert f'\n ' in result.xml # root layout is kept
+ assert f'\n ' in result.xml # root layout is kept
def test_whitespace_the_target_ignores_is_kept(tmp_path):
@@ -221,7 +227,7 @@ def test_whitespace_the_target_ignores_is_kept(tmp_path):
"\n \n \n c\n \n d\n
"
"\n \n \n e\n \n \n"
)
- result = migrate(_doc(tmp_path, body), to=HEAD)
+ result = migrate(_doc(tmp_path, body), to="0.9")
head = "\n ".join(f'' for v in (1, 2, 3, 4))
assert f"\n {head}\n ab\n " in result.xml
assert "\n \n c\n \n d\n
" in result.xml
@@ -232,63 +238,63 @@ def test_whitespace_between_paragraphs_of_a_list_item_is_kept(tmp_path):
body = (
"\n \n First.\n Second.\n Fruit: apples\n
"
)
- result = migrate(_doc(tmp_path, body), to=HEAD)
+ result = migrate(_doc(tmp_path, body), to="0.9")
assert "\n First.\n Second.\n Fruit:apples" in result.xml
def test_content_comments_and_interior_whitespace_are_kept(tmp_path):
body = "\n def f():\n pass\na b\n c d"
- result = migrate(_doc(tmp_path, body), to=HEAD)
+ result = migrate(_doc(tmp_path, body), to="0.9")
assert "\n def f():\n pass\n" in result.xml
assert "a b\n cd" in result.xml # interior whitespace reads as one space anyway
def test_cdata_sections_the_step_does_not_change_are_kept(tmp_path):
- result = migrate(_doc(tmp_path, "x"), to=HEAD)
+ result = migrate(_doc(tmp_path, "x"), to="0.9")
assert "" in result.xml
def test_whitespace_other_than_space_tab_and_line_breaks_is_kept(tmp_path):
- result = migrate(_doc(tmp_path, "\u00a0x\u2003"), to=HEAD)
+ result = migrate(_doc(tmp_path, "\u00a0x\u2003"), to="0.9")
assert "\u00a0x\u2003" in result.xml
def test_older_documents_get_the_head_steps_too(tmp_path):
- result = migrate(_doc(tmp_path, "x y", version="0.7"), to=HEAD)
- assert "xy" in result.xml and f'version="{HEAD}"' in result.xml
+ result = migrate(_doc(tmp_path, "x y", version="0.7"), to="0.9")
+ assert "xy" in result.xml and 'version="0.9"' in result.xml
def test_head_documents_migrate_down_without_a_fallback_warning(tmp_path, recwarn):
- result = migrate(_doc(tmp_path, "x", version=HEAD), to="0.8")
- assert result.source_version == HEAD
+ result = migrate(_doc(tmp_path, "x", version="0.9"), to="0.8")
+ assert result.source_version == "0.9"
assert not [w for w in recwarn if "No schema" in str(w.message)]
def test_undeclared_version_to_the_head_only_gets_declared(tmp_path):
with pytest.warns(UserWarning, match="No DocLang version declared"):
- result = migrate(_doc(tmp_path, " x ", version=None), to=HEAD)
- assert " x " in result.xml and f'version="{HEAD}"' in result.xml
+ result = migrate(_doc(tmp_path, " x ", version=None), to="0.9")
+ assert " x " in result.xml and 'version="0.9"' in result.xml
def test_undeclared_document_from_an_older_source_gets_the_head_steps(tmp_path, recwarn):
- result = migrate(_doc(tmp_path, "a b", version=None), source="0.8", to=HEAD)
- assert (result.source_version, result.target_version) == ("0.8", HEAD)
- assert "a b" in result.xml and f'version="{HEAD}"' in result.xml
+ result = migrate(_doc(tmp_path, "a b", version=None), source="0.8", to="0.9")
+ assert (result.source_version, result.target_version) == ("0.8", "0.9")
+ assert "a b" in result.xml and 'version="0.9"' in result.xml
assert not [w for w in recwarn if "No DocLang version declared" in str(w.message)]
def test_head_as_source_of_an_undeclared_document(tmp_path, recwarn):
- result = migrate(_doc(tmp_path, " x ", version=None), source=HEAD, to=HEAD)
- assert result.source_version == HEAD and " x " in result.xml
+ result = migrate(_doc(tmp_path, " x ", version=None), source="0.9", to="0.9")
+ assert result.source_version == "0.9" and " x " in result.xml
assert not [w for w in recwarn if "No DocLang version declared" in str(w.message)]
def test_head_is_no_target_without_changes_of_its_own(tmp_path, monkeypatch):
from doclang.migration import _api
- monkeypatch.setattr(_api, "_head_description", lambda: "0.8") # as right after a release
- with pytest.raises(UnsupportedVersionError, match=r"No schema for DocLang spec version '0\.9'"):
- migrate(_doc(tmp_path, "x"), to="0.9")
+ monkeypatch.setattr(_api, "_head_description", lambda: NEWEST) # as right after a release
+ with pytest.raises(UnsupportedVersionError, match=rf"No schema for DocLang spec version '{re.escape(NEXT)}'"):
+ migrate(_doc(tmp_path, "x", version=NEWEST), to=NEXT)
# --- validation of input and output ------------------------------------------------------------------
@@ -502,7 +508,7 @@ def drop_first_track(root, changes):
_with_unreleased_head(monkeypatch, drop_first_track)
members = {"audio/1.mp3": b"a1", "audio/2.ogg": b"a2", "video/3.mp4": b"v3", "assets/frames/2.jpg": b"f"}
out = tmp_path / "out.dclx"
- result = migrate(_archive(tmp_path, f"{TRACK}{FRAME_TRACK}{TRACK}", members), to=HEAD, output=out)
+ result = migrate(_archive(tmp_path, f"{TRACK}{FRAME_TRACK}{TRACK}", members, version=NEWEST), to=NEXT, output=out)
assert [(c["kind"], c["location"]) for c in result.changes] == [
("dropped", "/doclang/track[1]"),
@@ -568,7 +574,7 @@ def test_every_valid_fixture_migrates_from_the_newest_release_to_the_head(fixtur
validate(doc, spec_version="0.8", allow_empty_namespace=True)
except ValidationError:
pytest.skip("uses what the newest release cannot express")
- assert migrate(doc, to=HEAD).target_version == HEAD
+ assert migrate(doc, to="0.9").target_version == "0.9"
# --- file-based cases ----------------------------------------------------------------------------
@@ -647,7 +653,7 @@ def document(out: Path):
def test_text_with_text_level_content_in_a_marker_is_unwrapped(tmp_path):
body = "a)x
"
- result = migrate(_doc(tmp_path, body, version=HEAD), to="0.8")
+ result = migrate(_doc(tmp_path, body, version="0.9"), to="0.8")
assert "a)" in result.xml
assert [(c["kind"], c["location"]) for c in result.changes] == [("mapped", "/doclang/list/ldiv/marker/text")]
@@ -658,7 +664,7 @@ def test_other_semantic_elements_in_a_marker_are_dropped(tmp_path):
f"{head}1xb y"
"x
"
)
- result = migrate(_doc(tmp_path, body, version=HEAD), to="0.8")
+ result = migrate(_doc(tmp_path, body, version="0.9"), to="0.8")
assert f"{head}1" in result.xml # the marker's head stays
assert [(c["kind"], c["location"]) for c in result.changes] == [
("dropped", "/doclang/list/ldiv/marker/picture"),
@@ -669,7 +675,7 @@ def test_other_semantic_elements_in_a_marker_are_dropped(tmp_path):
def test_nested_markers_are_unwrapped_from_the_inside(tmp_path):
body = "ix
"
- result = migrate(_doc(tmp_path, body, version=HEAD), to="0.8")
+ result = migrate(_doc(tmp_path, body, version="0.9"), to="0.8")
assert "i" in result.xml
assert [(c["kind"], c["location"]) for c in result.changes] == [
("mapped", "/doclang/list/ldiv/marker/marker"),
diff --git a/tests/test_prepare_release.py b/tests/test_prepare_release.py
index 6187565..da4e55b 100644
--- a/tests/test_prepare_release.py
+++ b/tests/test_prepare_release.py
@@ -7,7 +7,7 @@
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
-from utils.rotate_migration_steps import rotate_migration_steps # noqa: E402
+from utils.rotate_migration_steps import _FRESH_TO_NEXT, rotate_migration_steps # noqa: E402
from utils.snapshot_schemas import snapshot_schemas # noqa: E402
@@ -128,3 +128,23 @@ def migrated(source_file, target, steps):
for case in cases:
target = case.parent.name.split("_to_")[1]
assert migrated(case, target, after) == migrated(case, target, before), case.name
+
+
+def test_rotate_retargets_tests_importing_the_development_steps(tmp_path):
+ migrate_dir = _project_with_steps(tmp_path, ["0.7"])
+ (migrate_dir / "to_next.py").write_text(_FRESH_TO_NEXT, encoding="utf-8")
+ tests_dir = tmp_path / "tests"
+ tests_dir.mkdir()
+ (tests_dir / "test_steps.py").write_text(
+ "from doclang.migration._steps import from_next, to_next\n\nto_next.migrate(root, from_next._X)\n",
+ encoding="utf-8",
+ )
+ other = 'path = steps_dir / "to_next.py"\n' # no import of a step: left alone
+ (tests_dir / "test_other.py").write_text(other, encoding="utf-8")
+
+ rotate_migration_steps("0.8.0", tmp_path)
+
+ assert (tests_dir / "test_steps.py").read_text(encoding="utf-8") == (
+ "from doclang.migration._steps import v0_8_to_0_7, v0_7_to_0_8\n\nv0_7_to_0_8.migrate(root, v0_8_to_0_7._X)\n"
+ )
+ assert (tests_dir / "test_other.py").read_text(encoding="utf-8") == other
diff --git a/tests/test_validation.py b/tests/test_validation.py
index e524a51..e69d6c8 100644
--- a/tests/test_validation.py
+++ b/tests/test_validation.py
@@ -6,6 +6,7 @@
"""
import builtins
+import re
import sys
from pathlib import Path
from unittest.mock import patch
@@ -296,16 +297,20 @@ def test_08_document_validates_against_the_released_08_schema(tmp_path):
assert validate(track_08, spec_version="0.8") is None
-def test_newer_declared_version_falls_back_to_the_development_head(tmp_path):
- """The head differs from the released 0.8 schema, so the fallback message names it as unreleased."""
- newer = _versioned_doc(tmp_path, "0.9", _TRACK_BODY)
+def test_newer_declared_version_falls_back_to_the_head(tmp_path):
+ """The fallback message names the head by its version if it is identical to that released schema, else as
+ unreleased."""
+ newest = max(_available_versions(), key=lambda v: tuple(map(int, v.split("."))))
+ major, minor = map(int, newest.split("."))
+ newer = f"{major}.{minor + 1}"
+ doc = _versioned_doc(tmp_path, newer, _TRACK_BODY)
+ head = rf"({re.escape(newest)}|unreleased development version)"
with pytest.warns(
- UserWarning,
- match=r"No schema for declared DocLang version 0\.9; .*latest schema \(unreleased development version\)\.",
+ UserWarning, match=rf"No schema for declared DocLang version {re.escape(newer)}; .*latest schema \({head}\)\."
):
- assert validate(newer) is None
- with pytest.raises(ValidationError, match=r"No schema for declared DocLang version '0\.9'"):
- validate(newer, strict_declared_version=True)
+ assert validate(doc) is None
+ with pytest.raises(ValidationError, match=rf"No schema for declared DocLang version '{re.escape(newer)}'"):
+ validate(doc, strict_declared_version=True)
def test_version_attribute_only_constrained_to_format(tmp_path):
diff --git a/utils/rotate_migration_steps.py b/utils/rotate_migration_steps.py
index f4d9f69..7d23ada 100644
--- a/utils/rotate_migration_steps.py
+++ b/utils/rotate_migration_steps.py
@@ -7,6 +7,8 @@
- doclang/migration/_steps/from_next.py -> v_to_.py (e.g. v0_8_to_0_7.py), down
- doclang/migration/_steps/to_next.py -> v_to_.py (e.g. v0_8_to_0_9.py), up
is the newest minor with schemas in doclang/schemas/.
+Tests that import a development step (``from doclang.migration._steps import to_next``) are retargeted to the
+released one, so they keep testing the rules they were written for.
Nothing happens for a patch release of an existing minor, or if there is no previous minor.
Run this before utils/snapshot_schemas.py, which creates doclang/schemas//.
@@ -87,9 +89,11 @@ def rotate_migration_steps(version: str, project_root: Path | None = None) -> Pa
previous = max(previous_versions, key=_key)
released = _rotate(from_next_path, new, previous, _FRESH_FROM_NEXT)
+ renames = {"from_next": released.stem}
to_next_path = from_next_path.with_name("to_next.py")
if to_next_path.exists():
- _rotate(to_next_path, previous, new, _FRESH_TO_NEXT)
+ renames["to_next"] = _rotate(to_next_path, previous, new, _FRESH_TO_NEXT).stem
+ _retarget_tests(project_root / "tests", renames)
return released
@@ -110,6 +114,19 @@ def _rotate(path: Path, source: str, target: str, fresh: str) -> Path:
return released
+def _retarget_tests(tests_dir: Path, renames: dict[str, str]) -> None:
+ """In the tests importing a development step, refer to the released step instead."""
+ if not tests_dir.is_dir():
+ return
+ imports = re.compile(r"^\s*from doclang\.migration\._steps import (" + "|".join(renames) + r")\b", re.MULTILINE)
+ names = re.compile(r"\b(" + "|".join(renames) + r")\b")
+ for path in sorted(tests_dir.rglob("*.py")):
+ content = path.read_text(encoding="utf-8")
+ if imports.search(content):
+ path.write_text(names.sub(lambda m: renames[m.group(1)], content), encoding="utf-8")
+ print(f"Tests retargeted to the released migration steps: {path.name}")
+
+
def main():
parser = argparse.ArgumentParser(description="Rotate the development migration steps for a release")
parser.add_argument("version", help="Release version (e.g. 0.8.0)")