1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
|
"""Snapshot suite for every ``.. code-block:: python`` block shipped in the docs.
Each block is executed in a fresh edify-populated namespace; any ``Regex``
or ``Pattern`` bound to a top-level name has its ``.to_regex_string()``
snapshotted under ``tests/snapshots/docs/<file-stem>/<block-index>.regex``.
The snapshot pins the docs against silent drift: an intentional compile-path
change surfaces every doc example the change touches so their prose stays
truthful.
"""
import re
import sys
from collections.abc import Iterator
from pathlib import Path
import pytest
import edify
import edify.library as edify_library
from edify import Pattern
from edify.result import Regex
from edify.testing import assert_snapshot
_ON_PYPY = hasattr(sys, "pypy_version_info")
_REPO_ROOT = Path(__file__).parent.parent.parent
_DOCS_ROOT = _REPO_ROOT / "docs"
_SNAPSHOT_ROOT = _REPO_ROOT / "tests" / "snapshots" / "docs"
_CODE_BLOCK_HEADER = ".. code-block:: python"
def _collect_python_code_blocks(rst_path: Path) -> Iterator[tuple[int, str]]:
lines = rst_path.read_text().splitlines()
line_index = 0
while line_index < len(lines):
stripped = lines[line_index].strip()
if stripped != _CODE_BLOCK_HEADER:
line_index += 1
continue
block_start, block_lines = _consume_indented_block(lines, line_index + 1)
if block_lines:
yield block_start, "\n".join(block_lines)
line_index = block_start + len(block_lines)
def _consume_indented_block(lines: list[str], start_index: int) -> tuple[int, list[str]]:
block_lines: list[str] = []
line_index = start_index
while line_index < len(lines) and lines[line_index].strip() == "":
line_index += 1
if line_index >= len(lines):
return line_index, block_lines
indent_prefix = lines[line_index][: len(lines[line_index]) - len(lines[line_index].lstrip())]
if indent_prefix == "":
return line_index, block_lines
while line_index < len(lines):
current_line = lines[line_index]
if current_line.strip() == "":
block_lines.append("")
line_index += 1
continue
if not current_line.startswith(indent_prefix):
break
block_lines.append(current_line[len(indent_prefix) :])
line_index += 1
return line_index - len(block_lines), block_lines
def _discover_blocks() -> Iterator[tuple[Path, int, str, Path]]:
for rst_path in sorted(_DOCS_ROOT.rglob("*.rst")):
for block_start, block_source in _collect_python_code_blocks(rst_path):
relative_stem = rst_path.relative_to(_DOCS_ROOT).with_suffix("")
yield rst_path, block_start, block_source, relative_stem
def _snapshot_bodies_for_block(namespace: dict[str, object], pre_exec_names: frozenset[str]) -> str:
interesting_pairs: list[tuple[str, str, str]] = []
for identifier, value in namespace.items():
if identifier in pre_exec_names:
continue
if identifier.startswith("_") or identifier in {"edify", "Pattern", "Regex"}:
continue
if isinstance(value, Regex):
interesting_pairs.append((identifier, "regex", value.source))
elif isinstance(value, Pattern):
interesting_pairs.append((identifier, "pattern", value.to_regex_string()))
interesting_pairs.sort()
rendered_lines = [f"{name} :: {kind} :: {body}" for name, kind, body in interesting_pairs]
return "\n".join(rendered_lines) + ("\n" if rendered_lines else "")
DISCOVERED_BLOCKS: list[tuple[Path, int, str, Path]] = list(_discover_blocks())
_BLOCKS_DEFERRED_TO_DOCS_REWRITE = frozenset(
{
("built-in/index", 46),
("built-in/index", 83),
("built-in/index", 98),
("built-in/index", 142),
("built-in/index", 175),
("built-in/index", 184),
("built-in/index", 936),
("regex-builder/builder/index", 396),
}
)
_ILLUSTRATIVE_NON_EXECUTABLE_BLOCKS = frozenset(
{
("upgrading/0.3-to-1.0", 140),
}
)
_BLOCKS_SKIPPED_ON_PYPY = frozenset(
{
("regex-builder/flags/index", 38),
}
)
@pytest.mark.parametrize(
("rst_path", "block_start", "block_source", "relative_stem"),
DISCOVERED_BLOCKS,
ids=[
f"{relative_stem}:{block_start}" for _, block_start, _, relative_stem in DISCOVERED_BLOCKS
],
)
def test_doc_code_block_produces_the_snapshotted_regex(
rst_path: Path, block_start: int, block_source: str, relative_stem: Path
) -> None:
stem_string = str(relative_stem)
if (stem_string, block_start) in _BLOCKS_DEFERRED_TO_DOCS_REWRITE:
pytest.skip("doc block references validators / kwargs slated for the docs rewrite")
if (stem_string, block_start) in _ILLUSTRATIVE_NON_EXECUTABLE_BLOCKS:
pytest.skip("doc block shows pre/post-migration code that is intentionally not executable")
if _ON_PYPY and (stem_string, block_start) in _BLOCKS_SKIPPED_ON_PYPY:
pytest.skip("doc block hits PyPy's re.DEBUG upstream disassembler bug")
namespace = _prepared_exec_namespace()
pre_exec_names = frozenset(namespace)
exec(compile(block_source, str(rst_path), "exec"), namespace)
rendered = _snapshot_bodies_for_block(namespace, pre_exec_names)
snapshot_path = _SNAPSHOT_ROOT / stem_string / f"{block_start:04d}.regex"
assert_snapshot(rendered, snapshot_path)
def _prepared_exec_namespace() -> dict[str, object]:
namespace: dict[str, object] = {"edify": edify, "Pattern": Pattern, "Regex": Regex, "re": re}
for edify_export in dir(edify):
if edify_export.startswith("_"):
continue
namespace[edify_export] = getattr(edify, edify_export)
for library_export in dir(edify_library):
if library_export.startswith("_"):
continue
namespace[library_export] = getattr(edify_library, library_export)
return namespace
|