diff --git a/.github/workflows/docs-build.yaml b/.github/workflows/docs-build.yaml index a9617549142..f059b90543e 100644 --- a/.github/workflows/docs-build.yaml +++ b/.github/workflows/docs-build.yaml @@ -25,6 +25,7 @@ on: - "src/ethereum/**" - "src/ethereum_spec_tools/docc.py" - "src/ethereum_spec_tools/forks.py" + - "tests/spec_tools/**" - "static/**" - "docs/**" - "packages/testing/**" @@ -235,6 +236,9 @@ jobs: - uses: ./.github/actions/setup-uv + - name: Run docc diff regression test + run: uv run --group test pytest tests/spec_tools/test_docc.py + - name: Build spec documentation run: just docs-spec env: diff --git a/docs/scripts/gen_test_case_reference.py b/docs/scripts/gen_test_case_reference.py index ba2a00b38f5..67cd931fcd3 100644 --- a/docs/scripts/gen_test_case_reference.py +++ b/docs/scripts/gen_test_case_reference.py @@ -58,6 +58,7 @@ "--checklist-doc-gen", "--skip-index", "--ignore=tests/ported_static", + "--ignore=tests/spec_tools", "-m", "not blockchain_test_engine", "-s", diff --git a/pyproject.toml b/pyproject.toml index b89580d7ac0..c4c45856af1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -211,6 +211,7 @@ test = [ "requests-cache>=1.2.1,<2", "libcst>=1.8,<2", "ethereum-execution-testing", + { include-group = "doc" }, ] lint = [ "codespell==2.4.1", @@ -253,7 +254,6 @@ dev = [ { include-group = "test" }, { include-group = "lint" }, { include-group = "actionlint" }, - { include-group = "doc" }, { include-group = "mkdocs" }, "ethereum-execution[optimized]", "psutil>=7.2.2", diff --git a/src/ethereum_spec_tools/docc.py b/src/ethereum_spec_tools/docc.py index c93a943eeb1..0db0b08c2a3 100644 --- a/src/ethereum_spec_tools/docc.py +++ b/src/ethereum_spec_tools/docc.py @@ -148,6 +148,18 @@ def _diff_path(before: Hardfork, after: Hardfork) -> PurePath: return PurePath("diffs") / before.short_name / after.short_name +def _diff_source_paths( + diff_root: PurePath, path: PurePath +) -> Tuple[PurePath, PurePath]: + listing_path = diff_root / path + output_path = listing_path + + if path.name == "__init__.py": + output_path = output_path.with_name("index") + + return listing_path, output_path + + class _ForkOrder: forks: List[PurePath] diffs: List[PurePath] @@ -295,16 +307,16 @@ def discover(self, known: FrozenSet[T]) -> Iterator[Source]: assert before_source or after_source - if path.name == "__init__.py": - path = path.with_name("index") - - output_path = _diff_path(before, after) / path + relative_path, output_path = _diff_source_paths( + _diff_path(before, after), path + ) yield DiffSource( before.name, before_source, after.name, after_source, + relative_path, output_path, ) @@ -320,6 +332,10 @@ def discover(self, known: FrozenSet[T]) -> Iterator[Source]: class DiffSource(Generic[S], Source, Listable): """ A source that represents the difference between two other sources. + + This source is synthetic, so its `relative_path` is synthetic too. We keep + it separate from `output_path` because `docc` directory listings use + `relative_path` as the visible label. """ before_name: str @@ -327,6 +343,7 @@ class DiffSource(Generic[S], Source, Listable): after_name: str after: Optional[S] + _relative_path: PurePath _output_path: PurePath def __init__( @@ -335,6 +352,7 @@ def __init__( before: Optional[S], after_name: str, after: Optional[S], + relative_path: PurePath, output_path: PurePath, ) -> None: self.before_name = before_name @@ -343,6 +361,7 @@ def __init__( self.after_name = after_name self.after = after + self._relative_path = relative_path self._output_path = output_path @property @@ -357,7 +376,7 @@ def relative_path(self) -> Optional[PurePath]: """ Path to the Source (if one exists) relative to the project root. """ - return None + return self._relative_path @property def output_path(self) -> PurePath: diff --git a/tests/spec_tools/test_docc.py b/tests/spec_tools/test_docc.py new file mode 100644 index 00000000000..06d00f53f3f --- /dev/null +++ b/tests/spec_tools/test_docc.py @@ -0,0 +1,83 @@ +"""Regression tests for diff listing labels in docc-generated pages.""" + +from pathlib import PurePath + +from docc.context import Context +from docc.plugins import html +from docc.plugins.listing import ListingNode, ListingSource, render_html +from docc.source import Source + +from ethereum_spec_tools.docc import DiffSource, _diff_source_paths + + +def _render_listing_label(source: DiffSource[Source]) -> str: + """Render a leaf listing for a single source and return its leaf label.""" + listing = ListingSource( + PurePath("diffs/frontier/homestead/vm"), + PurePath("diffs/frontier/homestead/vm/index"), + {source}, + ) + + context = Context({Source: listing}) + root = html.HTMLRoot(context) + render_html(context, root, ListingNode({source})) + + for child in root.children: + if isinstance(child, html.HTMLTag): + text = "".join(child._to_element().itertext()).strip() + return PurePath(text).name + + raise AssertionError("listing render produced no HTML output") + + +def test_diff_source_renders_init_label_but_writes_to_index() -> None: + """Render `__init__.py` in Browse while preserving the `index` output.""" + relative_path, output_path = _diff_source_paths( + PurePath("diffs/frontier/homestead"), + PurePath("vm/__init__.py"), + ) + + old_shape_source: DiffSource[Source] = DiffSource( + "frontier", + None, + "homestead", + None, + PurePath("diffs/frontier/homestead/vm/index"), + PurePath("diffs/frontier/homestead/vm/index"), + ) + diff_source: DiffSource[Source] = DiffSource( + "frontier", + None, + "homestead", + None, + relative_path, + output_path, + ) + + assert _render_listing_label(old_shape_source) == "index" + assert _render_listing_label(diff_source) == "__init__.py" + assert diff_source.output_path == PurePath( + "diffs/frontier/homestead/vm/index" + ) + assert diff_source.index_dir == PurePath("diffs/frontier/homestead/vm") + + +def test_diff_source_renders_normal_module_label() -> None: + """Render normal modules with their filename unchanged.""" + relative_path, output_path = _diff_source_paths( + PurePath("diffs/frontier/homestead"), + PurePath("vm/gas.py"), + ) + + diff_source: DiffSource[Source] = DiffSource( + "frontier", + None, + "homestead", + None, + relative_path, + output_path, + ) + + assert _render_listing_label(diff_source) == "gas.py" + assert diff_source.output_path == diff_source.relative_path + assert diff_source.index_dir is None diff --git a/uv.lock b/uv.lock index 9c6ba50bf78..2c0ce8c87ad 100644 --- a/uv.lock +++ b/uv.lock @@ -927,10 +927,13 @@ mkdocs = [ { name = "pyspelling" }, ] test = [ + { name = "docc" }, { name = "ethereum-execution-testing" }, { name = "filelock" }, + { name = "fladrif" }, { name = "gitpython" }, { name = "libcst" }, + { name = "mistletoe" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-xdist" }, @@ -1035,10 +1038,13 @@ mkdocs = [ { name = "pyspelling", specifier = ">=2.8.2,<3" }, ] test = [ + { name = "docc", specifier = ">=0.6.0,<0.7.0" }, { name = "ethereum-execution-testing", editable = "packages/testing" }, { name = "filelock", specifier = ">=3.15.1,<4" }, + { name = "fladrif", specifier = ">=0.2.0,<0.3.0" }, { name = "gitpython", specifier = ">=3.1.0,<3.2" }, { name = "libcst", specifier = ">=1.8,<2" }, + { name = "mistletoe", specifier = ">=1.5.0,<2" }, { name = "pytest", specifier = ">=8,<9" }, { name = "pytest-cov", specifier = ">=4.1.0,<5" }, { name = "pytest-xdist", specifier = ">=3.3.1,<4" },