Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure reporting show more detailed error messages for RedefiningStaticFieldAsDynamic #104

Merged
merged 3 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/validate_pyproject/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,4 +248,4 @@ def __call__(self, pyproject: T) -> T:

with detailed_errors():
self._cache(pyproject)
return reduce(lambda acc, fn: fn(acc), self.extra_validations, pyproject)
return reduce(lambda acc, fn: fn(acc), self.extra_validations, pyproject)
7 changes: 4 additions & 3 deletions src/validate_pyproject/error_reporting.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,9 @@ def _expand_summary(self):

def _expand_details(self) -> str:
optional = []
desc_lines = self.ex.definition.pop("$$description", [])
desc = self.ex.definition.pop("description", None) or " ".join(desc_lines)
definition = self.ex.definition or {}
desc_lines = definition.pop("$$description", [])
desc = definition.pop("description", None) or " ".join(desc_lines)
if desc:
description = "\n".join(
wrap(
Expand All @@ -142,7 +143,7 @@ def _expand_details(self) -> str:
)
)
optional.append(f"DESCRIPTION:\n{description}")
schema = json.dumps(self.ex.definition, indent=4)
schema = json.dumps(definition, indent=4)
value = json.dumps(self.ex.value, indent=4)
defaults = [
f"GIVEN VALUE:\n{indent(value, ' ')}",
Expand Down
15 changes: 9 additions & 6 deletions src/validate_pyproject/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,41 @@


class InvalidSchemaVersion(JsonSchemaDefinitionException):
"""\
_DESC = """\
All schemas used in the validator should be specified using the same version \
as the toplevel schema ({version!r}).

Schema for {name!r} has version {given!r}.
"""
__doc__ = _DESC

def __init__(self, name: str, given_version: str, required_version: str):
msg = dedent(self.__doc__ or "").strip()
msg = dedent(self._DESC).strip()
msg = msg.format(name=name, version=required_version, given=given_version)
super().__init__(msg)


class SchemaMissingId(JsonSchemaDefinitionException):
"""\
_DESC = """\
All schemas used in the validator MUST define a unique toplevel `"$id"`.
No `"$id"` was found for schema associated with {reference!r}.
"""
__doc__ = _DESC

def __init__(self, reference: str):
msg = dedent(self.__doc__ or "").strip()
msg = dedent(self._DESC).strip()
super().__init__(msg.format(reference=reference))


class SchemaWithDuplicatedId(JsonSchemaDefinitionException):
"""\
_DESC = """\
All schemas used in the validator MUST define a unique toplevel `"$id"`.
`$id = {schema_id!r}` was found at least twice.
"""
__doc__ = _DESC

def __init__(self, schema_id: str):
msg = dedent(self.__doc__ or "").strip()
msg = dedent(self._DESC).strip()
super().__init__(msg.format(schema_id=schema_id))


Expand Down
28 changes: 22 additions & 6 deletions src/validate_pyproject/extra_validations.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
JSON Schema library).
"""

from inspect import cleandoc
from typing import Mapping, TypeVar

from .error_reporting import ValidationError
Expand All @@ -11,11 +12,16 @@


class RedefiningStaticFieldAsDynamic(ValidationError):
"""According to PEP 621:
_DESC = """According to PEP 621:

Build back-ends MUST raise an error if the metadata specifies a field
statically as well as being listed in dynamic.
"""
__doc__ = _DESC
_URL = (
"https://packaging.python.org/en/latest/specifications/"
"declaring-project-metadata/#dynamic"
)


def validate_project_dynamic(pyproject: T) -> T:
Expand All @@ -24,11 +30,21 @@ def validate_project_dynamic(pyproject: T) -> T:

for field in dynamic:
if field in project_table:
msg = f"You cannot provide a value for `project.{field}` and "
msg += "list it under `project.dynamic` at the same time"
name = f"data.project.{field}"
value = {field: project_table[field], "...": " # ...", "dynamic": dynamic}
raise RedefiningStaticFieldAsDynamic(msg, value, name, rule="PEP 621")
raise RedefiningStaticFieldAsDynamic(
message=f"You cannot provide a value for `project.{field}` and "
"list it under `project.dynamic` at the same time",
value={
field: project_table[field],
"...": " # ...",
"dynamic": dynamic,
},
name=f"data.project.{field}",
definition={
"description": cleandoc(RedefiningStaticFieldAsDynamic._DESC),
"see": RedefiningStaticFieldAsDynamic._URL,
},
rule="PEP 621",
)

return pyproject

Expand Down
5 changes: 3 additions & 2 deletions src/validate_pyproject/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,16 @@ def list_from_entry_points(


class ErrorLoadingPlugin(RuntimeError):
"""There was an error loading '{plugin}'.
_DESC = """There was an error loading '{plugin}'.
Please make sure you have installed a version of the plugin that is compatible
with {package} {version}. You can also try uninstalling it.
"""
__doc__ = _DESC

def __init__(self, plugin: str = "", entry_point: Optional[EntryPoint] = None):
if entry_point and not plugin:
plugin = getattr(entry_point, "module", entry_point.name)

sub = dict(package=__package__, version=__version__, plugin=plugin)
msg = dedent(self.__doc__ or "").format(**sub).splitlines()
msg = dedent(self._DESC).format(**sub).splitlines()
super().__init__(f"{msg[0]}\n{' '.join(msg[1:])}")
25 changes: 11 additions & 14 deletions src/validate_pyproject/pre_compile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def pre_compile(
validator = api.Validator(plugins)
header = "\n".join(NOCHECK_HEADERS)
code = replace_text(validator.generated_code, replacements)
(out / "fastjsonschema_validations.py").write_text(header + code, "UTF-8")
_write(out / "fastjsonschema_validations.py", header + code)

copy_fastjsonschema_exceptions(out, replacements)
copy_module("extra_validations", out, replacements)
Expand All @@ -70,27 +70,20 @@ def replace_text(text: str, replacements: Dict[str, str]) -> str:
def copy_fastjsonschema_exceptions(
output_dir: Path, replacements: Dict[str, str]
) -> Path:
file = output_dir / "fastjsonschema_exceptions.py"
code = replace_text(api.read_text(FJS.__name__, "exceptions.py"), replacements)
file.write_text(code, "UTF-8")
return file
return _write(output_dir / "fastjsonschema_exceptions.py", code)


def copy_module(name: str, output_dir: Path, replacements: Dict[str, str]) -> Path:
file = output_dir / f"{name}.py"
code = api.read_text(api.__package__, f"{name}.py")
code = replace_text(code, replacements)
file.write_text(code, "UTF-8")
return file
return _write(output_dir / f"{name}.py", replace_text(code, replacements))


def write_main(
file_path: Path, schema: types.Schema, replacements: Dict[str, str]
) -> Path:
code = api.read_text(__name__, "main_file.template")
code = replace_text(code, replacements)
file_path.write_text(code, "UTF-8")
return file_path
return _write(file_path, replace_text(code, replacements))


def write_notice(
Expand All @@ -105,9 +98,7 @@ def write_notice(
notice = notice.format(notice=opening, main_file=main_file, **load_licenses())
notice = replace_text(notice, replacements)

file = out / "NOTICE"
file.write_text(notice, "UTF-8")
return file
return _write(out / "NOTICE", notice)


def load_licenses() -> Dict[str, str]:
Expand All @@ -120,6 +111,7 @@ def load_licenses() -> Dict[str, str]:
NOCHECK_HEADERS = (
"# noqa",
"# type: ignore",
"# ruff: noqa",
"# flake8: noqa",
"# pylint: skip-file",
"# mypy: ignore-errors",
Expand All @@ -142,3 +134,8 @@ def _find_and_load_licence(files: Optional[Sequence[_M.PackagePath]]) -> str:
)
_logger.warning(msg)
raise


def _write(file: Path, text: str) -> Path:
file.write_text(text.rstrip() + "\n", encoding="utf-8") # POSIX convention
return file
2 changes: 1 addition & 1 deletion src/validate_pyproject/pre_compile/main_file.template
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@ def validate(data: Any) -> bool:
"""
with detailed_errors():
_validate(data, custom_formats=FORMAT_FUNCTIONS)
reduce(lambda acc, fn: fn(acc), EXTRA_VALIDATIONS, data)
reduce(lambda acc, fn: fn(acc), EXTRA_VALIDATIONS, data)
return True
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cannot provide a value for `project.entry-points` and list it under `project.dynamic` at the same time
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[build-system]
requires = ["setuptools>=67.5"]
build-backend = "setuptools.build_meta"

[project]
name = "timmins"
dynamic = ["version", "entry-points"]

[project.entry-points."timmins.display"]
excl = "timmins_plugin_fancy:excl_display"
6 changes: 4 additions & 2 deletions tests/test_examples.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import logging

import pytest
from fastjsonschema import JsonSchemaValueException

from validate_pyproject import api, cli
from validate_pyproject.error_reporting import ValidationError

from .helpers import EXAMPLES, INVALID, error_file, examples, invalid_examples, toml_

Expand All @@ -26,11 +26,13 @@ def test_invalid_examples_api(example):
expected_error = error_file(example_file).read_text("utf-8")
toml_equivalent = toml_.loads(example_file.read_text())
validator = api.Validator()
with pytest.raises(JsonSchemaValueException) as exc_info:
with pytest.raises(ValidationError) as exc_info:
validator(toml_equivalent)
exception_message = str(exc_info.value)
summary = exc_info.value.summary
for error in expected_error.splitlines():
assert error in exception_message
assert error in summary


@pytest.mark.parametrize("example", invalid_examples())
Expand Down