File: conftest.py

package info (click to toggle)
python-wrapt 2.1.1-1
  • links: PTS, VCS
  • area: main
  • in suites: experimental
  • size: 1,592 kB
  • sloc: python: 8,452; ansic: 2,978; makefile: 168; sh: 46
file content (185 lines) | stat: -rw-r--r-- 5,770 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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
import os
import pathlib
import re
import sys
import warnings

import pytest

try:
    from pytest import File as FileCollector
except ImportError:
    from pytest.collect import File as FileCollector

version = tuple(sys.version_info[:2])


# Set MYPYPATH to the src directory relative to this conftest.py file. This
# allows mypy to find the source code for type checking.
_conftest_dir = pathlib.Path(__file__).parent
_src_dir = _conftest_dir.parent / "src"
os.environ["MYPYPATH"] = str(_src_dir)


class DummyCollector(FileCollector):
    def collect(self):
        return []


def construct_dummy(path, parent):
    if hasattr(DummyCollector, "from_parent"):
        item = DummyCollector.from_parent(parent, path=path)
        return item
    else:
        return DummyCollector(path, parent=parent)


def pytest_pycollect_makemodule(module_path, parent):
    basename = module_path.name

    # Handle Python 2/3 general cases
    if "_py2" in basename and version >= (3, 0):
        return construct_dummy(module_path, parent)
    if "_py3" in basename and version < (3, 0):
        return construct_dummy(module_path, parent)

    # Handle specific Python version cases using regex
    # Match patterns like "_py33", "_py34", "_py310", etc.
    version_match = re.search(r"_py(\d)(\d*)", basename)
    if version_match:
        major = int(version_match.group(1))
        minor_str = version_match.group(2)
        minor = int(minor_str) if minor_str else 0

        # Check if current version is less than the required version
        if version < (major, minor):
            return construct_dummy(module_path, parent)

    return None


# -----------------------------
# Custom mypy_*.py + .out tests
# -----------------------------


def run_custom_action(py_file: pathlib.Path) -> str:
    """
    Run mypy on the given file with the current interpreter's major.minor version
    and return the combined stdout/stderr output as text.
    """
    import platform
    import subprocess

    major, minor = version
    cmd = [
        "mypy",
        "--strict",
        "--show-error-codes",
        "--python-version",
        f"{major}.{minor}",
        str(py_file),
    ]
    proc = subprocess.run(
        cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        check=False,
    )
    output = proc.stdout

    # On Windows, convert backslash paths to forward slashes for consistency
    # with .out files
    if platform.system() == "Windows":
        output = re.sub(r"\btests\\mypy\\", "tests/mypy/", output)

    return output


class MypyPairItem(pytest.Item):
    def __init__(self, name, parent, py_path: pathlib.Path, out_path: pathlib.Path):
        super().__init__(name, parent)
        self.py_path = py_path
        self.out_path = out_path

    def runtest(self):
        # Try to run mypy; if it's not found, skip the test
        try:
            actual_output = run_custom_action(self.py_path)
        except FileNotFoundError:
            warnings.warn(
                f"mypy not found; skipping test {self.py_path.name}",
                UserWarning,
                stacklevel=2,
            )
            pytest.skip("mypy command not found")

        expected_output = self.out_path.read_text(encoding="utf-8")

        # Normalize line endings to avoid platform discrepancies
        if actual_output.replace("\r\n", "\n") != expected_output.replace("\r\n", "\n"):
            raise AssertionError(
                f"Output did not match expected for {self.py_path.name}\n"
                f"Expected (from {self.out_path.name}):\n{expected_output}\n"
                f"Actual:\n{actual_output}"
            )

    def reportinfo(self):
        return self.py_path, 0, f"mypy-pair: {self.py_path.name}"


class MypyPairCollector(pytest.File):
    """
    A collector that discovers mypy*.py files with corresponding .out files in
    the same directory and creates test items for them.
    """

    def collect(self):
        # Only run this custom collection on Python 3.10+
        if version < (3, 10):
            return

        # Skip mypy tests if running on PyPy
        if sys.implementation.name == "pypy":
            return

        path = pathlib.Path(str(self.fspath))
        # Only operate in a tests directory context
        if path.name != "conftest.py":
            return

        tests_dir = path.parent

        for py_file in sorted(tests_dir.glob("mypy/mypy_*.py")):
            out_file = py_file.with_suffix(".out")
            if out_file.exists():
                name = f"{py_file.stem}"
                # Create a test item for the pair
                yield MypyPairItem.from_parent(
                    parent=self, name=name, py_path=py_file, out_path=out_file
                )


def pytest_collect_file(file_path, parent):
    """
    Hook that allows adding our MypyPairCollector when pytest collects files.
    We attach the collector to tests/conftest.py so the discovery runs once per tests session.
    """
    # Guard early so we don't attach the collector on older Pythons
    if version < (3, 10):
        return

    # Newer pytest passes pathlib.Path-like objects; ensure we can get a string/Path
    try:
        p = pathlib.Path(str(file_path))
    except Exception:
        p = pathlib.Path(getattr(file_path, "strpath", str(file_path)))

    # Only hook our collector on the tests/conftest.py file to avoid multiple runs
    if p.name == "conftest.py" and p.parent.name == "tests":
        if hasattr(MypyPairCollector, "from_parent"):
            return MypyPairCollector.from_parent(parent, path=file_path)
        else:
            # Fallback for very old pytest versions
            return MypyPairCollector(file_path, parent=parent)