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)
|