File: test_sphinx_gallery.py

package info (click to toggle)
python-pyvista 0.46.4-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 176,968 kB
  • sloc: python: 94,346; sh: 216; makefile: 70
file content (144 lines) | stat: -rw-r--r-- 5,276 bytes parent folder | download
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
from __future__ import annotations

from collections import Counter
from pathlib import Path
import re
from typing import NamedTuple
from typing import Optional

import pytest

ROOT_DIR = str(Path(__file__).parent.parent.parent)
EXAMPLES_DIR = str(Path(ROOT_DIR) / 'examples')
PYVISTA_DIR = str(Path(ROOT_DIR) / 'pyvista')
DOC_DIR = str(Path(ROOT_DIR) / 'doc')


class _TestCaseTuple(NamedTuple):
    test_id: str
    file_path: str
    has_crossref_to_api: bool
    has_crossref_from_api: bool
    anchor: Optional[str]


def find_files_with_extension(root_dir: str, ext: str) -> list[str]:
    """Return a list of files with the given extension under root_dir."""
    return [str(p) for p in Path(root_dir).rglob(f'*{ext}')]


def count_ref_tags(py_root: str, rst_root: str) -> dict[str, int]:
    """Count :ref:`...` targets in both .py and .rst files."""
    pattern = re.compile(r':ref:`(?:[^<`]*<)?([^>`]+)>?`')
    ref_counter = Counter()

    # Process .py files
    for file_path in find_files_with_extension(py_root, '.py'):
        ref_counter.update(extract_ref_targets_from_file(file_path, pattern))

    # Process .rst files
    for file_path in find_files_with_extension(rst_root, '.rst'):
        ref_counter.update(extract_ref_targets_from_file(file_path, pattern))

    return dict(ref_counter)


def extract_ref_targets_from_file(file_path: str, pattern: re.Pattern) -> list[str]:
    """Extract all :ref:`...` targets from a single file using the given pattern."""
    with Path(file_path).open(encoding='utf-8') as f:
        content = f.read()
    return pattern.findall(content)


def analyze_gallery_example_file(file_path: str) -> tuple[bool, str]:
    """Check a file for cross-references and return the first anchor."""
    with Path(file_path).open(encoding='utf-8') as f:
        content = f.read()

    crossref_pattern = re.compile(r':(meth|func|class|mod|attr|exc|data|ref|obj):`[^`]+`')
    anchor_pattern = re.compile(r'^\s*\.\.\s+_(.+?):\s*$', re.MULTILINE)

    has_crossref = bool(crossref_pattern.search(content))
    anchor_match = anchor_pattern.search(content)
    anchor = anchor_match.group(1) if anchor_match else None

    return has_crossref, anchor


def generate_test_cases() -> list[_TestCaseTuple]:
    test_cases = []
    example_files = find_files_with_extension(EXAMPLES_DIR, '.py')
    ref_tags = count_ref_tags(py_root=PYVISTA_DIR, rst_root=DOC_DIR)
    for file_path in example_files:
        has_crossref_to_api, anchor = analyze_gallery_example_file(file_path)
        has_crossref_from_api = bool(ref_tags.get(anchor, None))
        test_id = str(Path(file_path).relative_to(ROOT_DIR))
        test_cases.append(
            _TestCaseTuple(
                test_id=test_id,
                file_path=file_path,
                has_crossref_to_api=has_crossref_to_api,
                has_crossref_from_api=has_crossref_from_api,
                anchor=anchor,
            )
        )
    return test_cases


TEST_CASES = generate_test_cases()
TEST_CASE_IDS = [case.test_id for case in TEST_CASES]


@pytest.mark.parametrize('test_case', TEST_CASES, ids=TEST_CASE_IDS)
def test_example_has_cross_reference_to_api(test_case):
    if not test_case.has_crossref_to_api:
        msg = (
            "Example must include at least one cross-reference to PyVista's core or "
            'plotting API.\n '
            'E.g. if the example shows how to use `my_function`, then include a reference to '
            '`my_function`.\n'
            'E.g. use :class:`~pyvista.Plotter` to reference the `Plotter` class.\n'
            'E.g. use :meth:`~pyvista.DataSetFilters.transform` to reference the '
            '`transform` filter.\n'
        )
        pytest.fail(msg)


@pytest.mark.parametrize('test_case', TEST_CASES, ids=TEST_CASE_IDS)
def test_example_has_cross_reference_from_api(test_case):
    if test_case.file_path.endswith('add_example.py'):
        pytest.skip('This is a meta-example for dev purposes.')

    if not test_case.has_crossref_from_api:
        msg = (
            "Example must include at least one cross-reference from PyVista's core or "
            'plotting API.\n'
            'E.g. if the example shows how to use `my_function` with dataset '
            '`download_some_dataset`\n'
            f'then consider including a reference:\n'
            f'    :ref:`{test_case.anchor}`\n'
            f'in the docstring of `my_function` and/or `download_some_dataset`.'
        )
        pytest.fail(msg)


@pytest.mark.parametrize('test_case', TEST_CASES, ids=TEST_CASE_IDS)
def test_example_anchor(test_case):
    def format_anchor(anchor):
        return f'.. _{anchor}:'

    expected_anchor = f'{Path(test_case.file_path).stem}_example'
    if test_case.anchor is None:
        msg = (
            'Example is missing a reference anchor. Expected to find the anchor\n'
            f'{format_anchor(expected_anchor)!r} at the top of the file.'
        )
        raise pytest.fail(msg)

    if test_case.anchor != expected_anchor:
        msg = (
            f'Example has an incorrect reference anchor at the top of the file.\n'
            f'Actual: {format_anchor(test_case.anchor)!r}\n'
            f'Expected: {format_anchor(expected_anchor)!r}'
        )
        raise pytest.fail(msg)