Skip to content

Commit 7d4c3f5

Browse files
committed
Workaround for bypassing conversion of YAML-block directives
Refs: executablebooks/mdformat-myst#21 executablebooks/mdformat-myst#49
1 parent 3327614 commit 7d4c3f5

2 files changed

Lines changed: 217 additions & 4 deletions

File tree

repomatic/tool_runner.py

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import hashlib
3737
import json
3838
import logging
39+
import re
3940
import subprocess
4041
import sys
4142
import tarfile
@@ -211,12 +212,68 @@ class ToolSpec:
211212
``requires-python``). ``None`` if no computed params.
212213
"""
213214

215+
post_process: Callable[[Sequence[str]], None] | None = None
216+
"""Callback invoked on ``extra_args`` after the tool exits successfully.
217+
218+
Intended for temporary workarounds that fix known upstream formatting bugs
219+
in-place. Remove the callback once upstream ships the fix.
220+
"""
221+
214222
binary: BinarySpec | None = None
215223
"""Platform-specific binary download spec. When set, the tool is downloaded
216224
as a binary instead of installed via ``uvx`` or ``uv run``.
217225
"""
218226

219227

228+
# ---------------------------------------------------------------------------
229+
# Post-process callbacks
230+
# ---------------------------------------------------------------------------
231+
232+
_DIRECTIVE_YAML_OPTIONS_RE = re.compile(
233+
r"^((?:`{3,}|:{3,})\{[^}]+\}[^\n]*\n)"
234+
r"---\n"
235+
r"((?:[^\n]+\n)+?)"
236+
r"---\n",
237+
re.MULTILINE,
238+
)
239+
"""Match YAML-block directive options immediately after a MyST fence opening.
240+
241+
.. note::
242+
Workaround for `executablebooks/mdformat-myst#21
243+
<https://github.com/executablebooks/mdformat-myst/issues/21>`_ where
244+
``mdformat-myst`` unconditionally converts ``:key: value`` directive
245+
options to YAML blocks (``---`` / ``key: value`` / ``---``). Remove when
246+
upstream merges `executablebooks/mdformat-myst#49
247+
<https://github.com/executablebooks/mdformat-myst/pull/49>`_.
248+
"""
249+
250+
251+
def _yaml_block_to_field_list(match: re.Match[str]) -> str:
252+
"""Convert a single YAML-block directive option to field-list syntax."""
253+
directive_line = match.group(1)
254+
yaml_lines = match.group(2)
255+
# Prepend ":" to each non-empty line: ``key: value`` → ``:key: value``.
256+
field_lines = re.sub(r"^(?=\S)", ":", yaml_lines, flags=re.MULTILINE)
257+
return directive_line + field_lines
258+
259+
260+
def _fix_myst_directive_options(extra_args: Sequence[str]) -> None:
261+
"""Rewrite YAML-block directive options back to field-list syntax.
262+
263+
Operates in-place on every file in *extra_args* that exists on disk.
264+
Files without matching patterns are left untouched.
265+
"""
266+
for arg in extra_args:
267+
path = Path(arg)
268+
if not path.is_file():
269+
continue
270+
content = path.read_text(encoding="utf-8")
271+
fixed = _DIRECTIVE_YAML_OPTIONS_RE.sub(_yaml_block_to_field_list, content)
272+
if fixed != content:
273+
path.write_text(fixed, encoding="utf-8")
274+
logging.debug("Fixed MyST directive options in %s", path)
275+
276+
220277
# ---------------------------------------------------------------------------
221278
# Tool registry
222279
# ---------------------------------------------------------------------------
@@ -387,6 +444,7 @@ class ToolSpec:
387444
"mdformat-web==0.2.0",
388445
"ruff==0.15.5",
389446
),
447+
post_process=_fix_myst_directive_options,
390448
),
391449
# mypy configuration reference:
392450
# - Config discovery: https://mypy.readthedocs.io/en/stable/config_file.html
@@ -928,13 +986,15 @@ def run_tool(
928986
cmd.extend(extra_args)
929987
logging.debug("Running: %s", " ".join(cmd))
930988
result = subprocess.run(cmd, check=False)
931-
return result.returncode
932989
else:
933990
cmd.extend(config_args)
991+
cmd.extend(extra_args)
992+
logging.debug("Running: %s", " ".join(cmd))
993+
result = subprocess.run(cmd, check=False)
994+
995+
if result.returncode == 0 and spec.post_process:
996+
spec.post_process(extra_args)
934997

935-
cmd.extend(extra_args)
936-
logging.debug("Running: %s", " ".join(cmd))
937-
result = subprocess.run(cmd, check=False)
938998
return result.returncode
939999

9401000
finally:

tests/test_tool_runner.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,13 @@
4343
BinarySpec,
4444
NativeFormat,
4545
ToolSpec,
46+
_DIRECTIVE_YAML_OPTIONS_RE,
4647
_download_and_verify,
4748
_extract_binary,
49+
_fix_myst_directive_options,
4850
_get_platform_key,
4951
_install_binary,
52+
_yaml_block_to_field_list,
5053
binary_tool_context,
5154
find_unmodified_configs,
5255
get_data_file_path,
@@ -1166,3 +1169,153 @@ def test_find_unmodified_configs_alternative_filename(tmp_path, monkeypatch):
11661169
result = find_unmodified_configs()
11671170
paths = [p for _, p in result]
11681171
assert ".yamllint.yml" in paths
1172+
1173+
1174+
# ---------------------------------------------------------------------------
1175+
# MyST directive options post-processing
1176+
# ---------------------------------------------------------------------------
1177+
1178+
1179+
@pytest.mark.parametrize(
1180+
("before", "after"),
1181+
[
1182+
pytest.param(
1183+
"```{py:module} extra_platforms.detection\n"
1184+
"---\n"
1185+
"no-typesetting:\n"
1186+
"no-contents-entry:\n"
1187+
"---\n"
1188+
"```\n",
1189+
"```{py:module} extra_platforms.detection\n"
1190+
":no-typesetting:\n"
1191+
":no-contents-entry:\n"
1192+
"```\n",
1193+
id="backtick-fence-flags",
1194+
),
1195+
pytest.param(
1196+
"```{directive} arg\n"
1197+
"---\n"
1198+
"class: my-class\n"
1199+
"name: my-name\n"
1200+
"---\n"
1201+
"```\n",
1202+
"```{directive} arg\n"
1203+
":class: my-class\n"
1204+
":name: my-name\n"
1205+
"```\n",
1206+
id="backtick-fence-key-value",
1207+
),
1208+
pytest.param(
1209+
":::{note}\n"
1210+
"---\n"
1211+
"class: special\n"
1212+
"---\n"
1213+
":::\n",
1214+
":::{note}\n"
1215+
":class: special\n"
1216+
":::\n",
1217+
id="colon-fence",
1218+
),
1219+
pytest.param(
1220+
"````{directive} arg\n"
1221+
"---\n"
1222+
"key: value\n"
1223+
"---\n"
1224+
"````\n",
1225+
"````{directive} arg\n"
1226+
":key: value\n"
1227+
"````\n",
1228+
id="four-backtick-fence",
1229+
),
1230+
],
1231+
)
1232+
def test_directive_yaml_options_regex(before, after):
1233+
"""YAML-block directive options are converted to field-list syntax."""
1234+
assert _DIRECTIVE_YAML_OPTIONS_RE.sub(_yaml_block_to_field_list, before) == after
1235+
1236+
1237+
@pytest.mark.parametrize(
1238+
"content",
1239+
[
1240+
pytest.param(
1241+
"---\ntitle: My Doc\n---\n\n# Hello\n",
1242+
id="yaml-frontmatter",
1243+
),
1244+
pytest.param(
1245+
"Some text\n\n---\n\nMore text\n",
1246+
id="horizontal-rule",
1247+
),
1248+
pytest.param(
1249+
"```python\nprint('hello')\n```\n",
1250+
id="plain-code-fence",
1251+
),
1252+
],
1253+
)
1254+
def test_directive_yaml_options_regex_no_false_positives(content):
1255+
"""Non-directive YAML blocks and horizontal rules are left untouched."""
1256+
assert _DIRECTIVE_YAML_OPTIONS_RE.sub(_yaml_block_to_field_list, content) == content
1257+
1258+
1259+
def test_fix_myst_directive_options_in_place(tmp_path):
1260+
"""Post-processor rewrites files in-place and skips unchanged files."""
1261+
affected = tmp_path / "affected.md"
1262+
affected.write_text(
1263+
"# Title\n\n"
1264+
"```{py:module} mymod\n"
1265+
"---\n"
1266+
"no-typesetting:\n"
1267+
"---\n"
1268+
"```\n",
1269+
encoding="utf-8",
1270+
)
1271+
1272+
untouched = tmp_path / "untouched.md"
1273+
original = "# Plain markdown\n\nNo directives here.\n"
1274+
untouched.write_text(original, encoding="utf-8")
1275+
1276+
_fix_myst_directive_options([str(affected), str(untouched), "/nonexistent/path"])
1277+
1278+
assert affected.read_text(encoding="utf-8") == (
1279+
"# Title\n\n"
1280+
"```{py:module} mymod\n"
1281+
":no-typesetting:\n"
1282+
"```\n"
1283+
)
1284+
assert untouched.read_text(encoding="utf-8") == original
1285+
1286+
1287+
def test_fix_myst_directive_options_multiple_directives(tmp_path):
1288+
"""Multiple directive blocks in the same file are all fixed."""
1289+
md = tmp_path / "multi.md"
1290+
md.write_text(
1291+
"```{py:module} mod_a\n"
1292+
"---\n"
1293+
"no-typesetting:\n"
1294+
"no-contents-entry:\n"
1295+
"---\n"
1296+
"```\n"
1297+
"\n"
1298+
"Some text.\n"
1299+
"\n"
1300+
"```{py:module} mod_b\n"
1301+
"---\n"
1302+
"no-typesetting:\n"
1303+
"---\n"
1304+
"```\n",
1305+
encoding="utf-8",
1306+
)
1307+
1308+
_fix_myst_directive_options([str(md)])
1309+
1310+
assert md.read_text(encoding="utf-8") == (
1311+
"```{py:module} mod_a\n"
1312+
":no-typesetting:\n"
1313+
":no-contents-entry:\n"
1314+
"```\n"
1315+
"\n"
1316+
"Some text.\n"
1317+
"\n"
1318+
"```{py:module} mod_b\n"
1319+
":no-typesetting:\n"
1320+
"```\n"
1321+
)

0 commit comments

Comments
 (0)