File: test_pre_compile.py

package info (click to toggle)
python-validate-pyproject 0.24.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,340 kB
  • sloc: python: 3,053; makefile: 46; sh: 25
file content (186 lines) | stat: -rw-r--r-- 7,281 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
186
import builtins
import importlib
import re
import shutil
import subprocess
import sys
from inspect import cleandoc
from pathlib import Path

import pytest
from fastjsonschema import JsonSchemaValueException

from validate_pyproject import _tomllib as tomllib
from validate_pyproject.pre_compile import cli, pre_compile

from .helpers import error_file, get_tools, get_tools_as_args

MAIN_FILE = "hello_world.py"  # Let's use something different than `__init__.py`


def _pre_compile_checks(path: Path):
    assert (path / "__init__.py").exists()
    assert (path / "__init__.py").read_text() == ""
    assert (path / MAIN_FILE).exists()
    files = [
        (MAIN_FILE, "def validate("),
        (MAIN_FILE, "from .error_reporting import detailed_errors, ValidationError"),
        ("error_reporting.py", "def detailed_errors("),
        ("fastjsonschema_exceptions.py", "class JsonSchemaValueException"),
        ("fastjsonschema_validations.py", "def validate("),
        ("extra_validations.py", "def validate"),
        ("formats.py", "def pep508("),
        ("NOTICE", "The relevant copyright notes and licenses are included below"),
    ]
    for file, content in files:
        assert (path / file).exists()
        assert content in (path / file).read_text()

    # Make sure standard replacements work
    for file in ("fastjsonschema_validations.py", "error_reporting.py"):
        file_contents = (path / file).read_text()
        assert "from fastjsonschema" not in file_contents
        assert "from ._vendor.fastjsonschema" not in file_contents
        assert "from validate_pyproject._vendor.fastjsonschema" not in file_contents
        assert "from .fastjsonschema_exceptions" in file_contents

    # Make sure the pre-compiled lib works
    script = f"""
    from {path.stem} import {Path(MAIN_FILE).stem} as mod

    assert issubclass(mod.ValidationError, mod.JsonSchemaValueException)

    example = {{
        "project": {{"name": "proj", "version": 42}}
    }}
    assert mod.validate(example) == example
    """
    cmd = [sys.executable, "-c", cleandoc(script)]
    error = r".project\.version. must be string"
    with pytest.raises(subprocess.CalledProcessError) as exc_info:
        subprocess.check_output(cmd, cwd=path.parent, stderr=subprocess.STDOUT)

    assert re.search(error, str(exc_info.value.output, "utf-8"))


@pytest.mark.skip(reason="not needed within Debian, we use the packaged fastjsonschema package")
def test_pre_compile_api(tmp_path):
    path = Path(tmp_path)
    pre_compile(path, MAIN_FILE)
    _pre_compile_checks(path)
    # Let's make sure it also works for __init__
    shutil.rmtree(str(path), ignore_errors=True)
    replacements = {"from fastjsonschema import": "from _vend.fastjsonschema import"}
    pre_compile(path, text_replacements=replacements)
    assert "def validate(" in (path / "__init__.py").read_text()
    assert not (path / MAIN_FILE).exists()
    file_contents = (path / "fastjsonschema_validations.py").read_text()
    assert "from _vend" in file_contents
    assert "from fastjsonschema" not in file_contents


@pytest.mark.skip(reason="not needed within Debian, we use the packaged fastjsonschema package")
def test_vendoring_cli(tmp_path):
    path = Path(tmp_path)
    cli.run(["-O", str(path), "-M", MAIN_FILE])
    _pre_compile_checks(Path(path))
    # Let's also try to test JSON replacements
    shutil.rmtree(str(path), ignore_errors=True)
    replacements = '{"from fastjsonschema import": "from _vend.fastjsonschema import"}'
    cli.run(["-O", str(path), "-R", replacements])
    file_contents = (path / "fastjsonschema_validations.py").read_text()
    assert "from _vend" in file_contents
    assert "from fastjsonschema" not in file_contents


# ---- Examples ----


PRE_COMPILED_NAME = "_validation"


def api_pre_compile(tmp_path, *, example: Path) -> Path:
    plugins = get_tools(example)
    return pre_compile(Path(tmp_path / PRE_COMPILED_NAME), extra_plugins=plugins)


def cli_pre_compile(tmp_path, *, example: Path) -> Path:
    args = get_tools_as_args(example)
    path = Path(tmp_path / PRE_COMPILED_NAME)
    cli.run([*args, "-O", str(path)])
    return path


_PRE_COMPILED = (api_pre_compile, cli_pre_compile)


@pytest.fixture
def pre_compiled_validate(monkeypatch):
    def _validate(vendored_path, toml_equivalent):
        assert PRE_COMPILED_NAME not in sys.modules
        importlib.invalidate_caches()
        with monkeypatch.context() as m:
            # Make sure original imports are not used
            _disable_import(m, "fastjsonschema")
            _disable_import(m, "validate_pyproject")
            # Make newly generated package available for importing
            m.syspath_prepend(str(vendored_path.parent))
            mod = __import__(PRE_COMPILED_NAME)
            print(list(vendored_path.glob("*")))
            print(mod, "\n\n", dir(mod))
            try:
                return mod.validate(toml_equivalent)
            except mod.JsonSchemaValueException as ex:
                # Let's translate the exceptions so we have identical classes
                new_ex = JsonSchemaValueException(
                    ex.message, ex.value, ex.name, ex.definition, ex.rule
                )
                raise new_ex from ex
            finally:
                all_modules = [
                    mod
                    for mod in sys.modules
                    if mod.startswith(f"{PRE_COMPILED_NAME}.")
                ]
                for mod in all_modules:
                    del sys.modules[mod]
                del sys.modules[PRE_COMPILED_NAME]

    return _validate


@pytest.mark.skip(reason="not needed within Debian, we use the packaged fastjsonschema package")
@pytest.mark.parametrize("pre_compiled", _PRE_COMPILED)
def test_examples_api(tmp_path, pre_compiled_validate, example, pre_compiled):
    toml_equivalent = tomllib.loads(example.read_text())
    pre_compiled_path = pre_compiled(Path(tmp_path), example=example)
    assert pre_compiled_validate(pre_compiled_path, toml_equivalent) is not None


@pytest.mark.skip(reason="not needed within Debian, we use the packaged fastjsonschema package")
@pytest.mark.parametrize("pre_compiled", _PRE_COMPILED)
def test_invalid_examples_api(
    tmp_path, pre_compiled_validate, invalid_example, pre_compiled
):
    expected_error = error_file(invalid_example).read_text("utf-8")
    toml_equivalent = tomllib.loads(invalid_example.read_text())
    pre_compiled_path = pre_compiled(Path(tmp_path), example=invalid_example)
    with pytest.raises(JsonSchemaValueException) as exc_info:
        pre_compiled_validate(pre_compiled_path, toml_equivalent)
    exception_message = str(exc_info.value)
    print("rule", "=", exc_info.value.rule)
    print("rule_definition", "=", exc_info.value.rule_definition)
    print("definition", "=", exc_info.value.definition)
    for error in expected_error.splitlines():
        assert error in exception_message


def _disable_import(monkeypatch, name):
    orig = builtins.__import__

    def _import(import_name, *args, **kwargs):
        if import_name == name or import_name.startswith(f"{name}."):
            raise ImportError(name)
        return orig(import_name, *args, **kwargs)

    monkeypatch.setattr(builtins, "__import__", _import)