Skip to content

Commit

Permalink
fix: support pre-compile too, fix tests
Browse files Browse the repository at this point in the history
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
  • Loading branch information
henryiii committed Nov 10, 2023
1 parent e9dc142 commit 4ba6a65
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 23 deletions.
4 changes: 3 additions & 1 deletion src/validate_pyproject/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ def __getitem__(self, key: str) -> Callable[[str], Schema]:
return self._registry.__getitem__


def load_from_uri(tool_uri: str) -> tuple[str, Any]:
def load_from_uri(tool_uri: str) -> Tuple[str, Any]:
tool_info = urllib.parse.urlparse(tool_uri)
if tool_info.netloc:
url = f"{tool_info.scheme}://{tool_info.netloc}/{tool_info.path}"
Expand Down Expand Up @@ -228,6 +228,8 @@ def __init__(

for tool in load_tools:
tool_name, _, tool_uri = tool.partition("=")
if not tool_uri:
raise errors.URLMissingTool(tool)
self._external[tool_name] = load_from_uri(tool_uri)

# Let's make the following options readonly
Expand Down
15 changes: 15 additions & 0 deletions src/validate_pyproject/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@
from .error_reporting import ValidationError


class URLMissingTool(RuntimeError):
_DESC = """\
The '--tool' option requires a tool name.
Correct form is '--tool=<tool-name>={url}', with an optional
'#json/pointer' at the end.
"""
__doc__ = _DESC

def __init__(self, url: str):
msg = dedent(self._DESC).strip()
msg = msg.format(url=url)
super().__init__(msg)


class InvalidSchemaVersion(JsonSchemaDefinitionException):
_DESC = """\
All schemas used in the validator should be specified using the same version \
Expand Down
6 changes: 4 additions & 2 deletions src/validate_pyproject/pre_compile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@
)


def pre_compile(
def pre_compile( # noqa: PLR0913
output_dir: Union[str, os.PathLike] = ".",
main_file: str = "__init__.py",
original_cmd: str = "",
plugins: Union[api.AllPlugins, Sequence["PluginWrapper"]] = api.ALL_PLUGINS,
text_replacements: Mapping[str, str] = TEXT_REPLACEMENTS,
*,
load_tools: Sequence[str] = (),
) -> Path:
"""Populate the given ``output_dir`` with all files necessary to perform
the validation.
Expand All @@ -45,7 +47,7 @@ def pre_compile(
out.mkdir(parents=True, exist_ok=True)
replacements = {**TEXT_REPLACEMENTS, **text_replacements}

validator = api.Validator(plugins)
validator = api.Validator(plugins, load_tools=load_tools)
header = "\n".join(NOCHECK_HEADERS)
code = replace_text(validator.generated_code, replacements)
_write(out / "fastjsonschema_validations.py", header + code)
Expand Down
17 changes: 16 additions & 1 deletion src/validate_pyproject/pre_compile/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ def JSON_dict(name: str, value: str):
"for example: \n"
'-R \'{"from packaging import": "from .._vendor.packaging import"}\'',
),
"tool": dict(
flags=("-t", "--tool"),
nargs="+",
dest="tool",
default=(),
help="External tools file/url(s) to load, of the form name=URL#path",
),
}


Expand All @@ -74,6 +81,7 @@ class CliParams(NamedTuple):
main_file: str = "__init__.py"
replacements: Mapping[str, str] = MappingProxyType({})
loglevel: int = logging.WARNING
tool: Sequence[str] = ()


def parser_spec(plugins: Sequence[PluginWrapper]) -> Dict[str, dict]:
Expand All @@ -91,7 +99,14 @@ def run(args: Sequence[str] = ()):
desc = 'Generate files for "pre-compiling" `validate-pyproject`'
prms = cli.parse_args(args, plugins, desc, parser_spec, CliParams)
cli.setup_logging(prms.loglevel)
pre_compile(prms.output_dir, prms.main_file, cmd, prms.plugins, prms.replacements)
pre_compile(
prms.output_dir,
prms.main_file,
cmd,
prms.plugins,
prms.replacements,
load_tools=prms.tool,
)
return 0


Expand Down
9 changes: 9 additions & 0 deletions tests/helpers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
from pathlib import Path

HERE = Path(__file__).parent.resolve()
Expand All @@ -9,3 +10,11 @@ def error_file(p: Path) -> Path:
return next(f for f in files if f.exists())
except StopIteration:
raise FileNotFoundError(f"No error file found for {p}") from None


def get_test_config(example: Path) -> dict[str, str]:
test_config = example.with_name("test_config.json")
if test_config.is_file():
with test_config.open(encoding="utf-8") as f:
return json.load(f)
return {}
2 changes: 2 additions & 0 deletions tests/invalid-examples/poetry/poetry-bad-multiline.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
`tool.poetry.description` must match pattern ^[^
]*$
27 changes: 15 additions & 12 deletions tests/test_examples.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import json
import logging
from pathlib import Path

Expand All @@ -8,31 +7,32 @@
from validate_pyproject import api, cli
from validate_pyproject.error_reporting import ValidationError

from .helpers import error_file
from .helpers import error_file, get_test_config


def test_examples_api(example: Path) -> None:
tools = get_test_config(example).get("tools", {})
load_tools = [f"{k}={v}" for k, v in tools.items()]

toml_equivalent = tomllib.loads(example.read_text())
validator = api.Validator()
validator = api.Validator(load_tools=load_tools)
assert validator(toml_equivalent) is not None


def test_examples_cli(example: Path) -> None:
args = []
test_config = example.with_name("test_config.json")
if test_config.is_file():
with test_config.open(encoding="utf-8") as f:
test_config = json.load(f)
tools = test_config.get("tools", {})
args += [f"--tool={k}={v}" for k, v in tools.items()]
tools = get_test_config(example).get("tools", {})
args = [f"--tool={k}={v}" for k, v in tools.items()]

assert cli.run(["--dump-json", str(example), *args]) == 0 # no errors


def test_invalid_examples_api(invalid_example: Path) -> None:
tools = get_test_config(invalid_example).get("tools", {})
load_tools = [f"{k}={v}" for k, v in tools.items()]

expected_error = error_file(invalid_example).read_text("utf-8")
toml_equivalent = tomllib.loads(invalid_example.read_text())
validator = api.Validator()
validator = api.Validator(load_tools=load_tools)
with pytest.raises(ValidationError) as exc_info:
validator(toml_equivalent)
exception_message = str(exc_info.value)
Expand All @@ -43,10 +43,13 @@ def test_invalid_examples_api(invalid_example: Path) -> None:


def test_invalid_examples_cli(invalid_example: Path, caplog) -> None:
tools = get_test_config(invalid_example).get("tools", {})
args = [f"--tool={k}={v}" for k, v in tools.items()]

caplog.set_level(logging.DEBUG)
expected_error = error_file(invalid_example).read_text("utf-8")
with pytest.raises(SystemExit) as exc_info:
cli.main([str(invalid_example)])
cli.main([str(invalid_example), *args])
assert exc_info.value.args == (1,)
for error in expected_error.splitlines():
assert error in caplog.text
31 changes: 24 additions & 7 deletions tests/test_pre_compile.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import builtins
import importlib
import re
import shutil
import subprocess
import sys
from inspect import cleandoc
from pathlib import Path
from typing import Sequence

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
from .helpers import error_file, get_test_config

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

Expand Down Expand Up @@ -96,13 +98,14 @@ def test_vendoring_cli(tmp_path):
PRE_COMPILED_NAME = "_validation"


def api_pre_compile(tmp_path) -> Path:
return pre_compile(Path(tmp_path / PRE_COMPILED_NAME))
def api_pre_compile(tmp_path, *, load_tools: Sequence[str]) -> Path:
return pre_compile(Path(tmp_path / PRE_COMPILED_NAME), load_tools=load_tools)


def cli_pre_compile(tmp_path) -> Path:
def cli_pre_compile(tmp_path, *, load_tools: Sequence[str]) -> Path:
args = [f"--tool={t}" for t in load_tools]
path = Path(tmp_path / PRE_COMPILED_NAME)
cli.run(["-O", str(path)])
cli.run([*args, "-O", str(path)])
return path


Expand All @@ -113,6 +116,7 @@ def cli_pre_compile(tmp_path) -> Path:
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")
Expand All @@ -131,25 +135,38 @@ def _validate(vendored_path, toml_equivalent):
)
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.parametrize("pre_compiled", _PRE_COMPILED)
def test_examples_api(tmp_path, pre_compiled_validate, example, pre_compiled):
tools = get_test_config(example).get("tools", {})
load_tools = [f"{k}={v}" for k, v in tools.items()]

toml_equivalent = tomllib.loads(example.read_text())
pre_compiled_path = pre_compiled(Path(tmp_path))
pre_compiled_path = pre_compiled(Path(tmp_path), load_tools=load_tools)
assert pre_compiled_validate(pre_compiled_path, toml_equivalent) is not None


@pytest.mark.parametrize("pre_compiled", _PRE_COMPILED)
def test_invalid_examples_api(
tmp_path, pre_compiled_validate, invalid_example, pre_compiled
):
tools = get_test_config(invalid_example).get("tools", {})
load_tools = [f"{k}={v}" for k, v in tools.items()]

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))
pre_compiled_path = pre_compiled(Path(tmp_path), load_tools=load_tools)
with pytest.raises(JsonSchemaValueException) as exc_info:
pre_compiled_validate(pre_compiled_path, toml_equivalent)
exception_message = str(exc_info.value)
Expand Down

0 comments on commit 4ba6a65

Please sign in to comment.