diff --git a/src/validate_pyproject/api.py b/src/validate_pyproject/api.py index 5b9f1bd..779daf6 100644 --- a/src/validate_pyproject/api.py +++ b/src/validate_pyproject/api.py @@ -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) diff --git a/src/validate_pyproject/error_reporting.py b/src/validate_pyproject/error_reporting.py index 62c827f..c54bb49 100644 --- a/src/validate_pyproject/error_reporting.py +++ b/src/validate_pyproject/error_reporting.py @@ -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( @@ -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, ' ')}", diff --git a/src/validate_pyproject/errors.py b/src/validate_pyproject/errors.py index 057c4e4..c0f1395 100644 --- a/src/validate_pyproject/errors.py +++ b/src/validate_pyproject/errors.py @@ -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)) diff --git a/src/validate_pyproject/extra_validations.py b/src/validate_pyproject/extra_validations.py index 4130a42..760acf9 100644 --- a/src/validate_pyproject/extra_validations.py +++ b/src/validate_pyproject/extra_validations.py @@ -3,6 +3,7 @@ JSON Schema library). """ +from inspect import cleandoc from typing import Mapping, TypeVar from .error_reporting import ValidationError @@ -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: @@ -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 diff --git a/src/validate_pyproject/plugins/__init__.py b/src/validate_pyproject/plugins/__init__.py index e3e2016..56d22a1 100644 --- a/src/validate_pyproject/plugins/__init__.py +++ b/src/validate_pyproject/plugins/__init__.py @@ -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:])}") diff --git a/src/validate_pyproject/pre_compile/__init__.py b/src/validate_pyproject/pre_compile/__init__.py index b2f693c..71b7bf7 100644 --- a/src/validate_pyproject/pre_compile/__init__.py +++ b/src/validate_pyproject/pre_compile/__init__.py @@ -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) @@ -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( @@ -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]: @@ -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", @@ -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 diff --git a/src/validate_pyproject/pre_compile/main_file.template b/src/validate_pyproject/pre_compile/main_file.template index dbe6cb4..4f612bd 100644 --- a/src/validate_pyproject/pre_compile/main_file.template +++ b/src/validate_pyproject/pre_compile/main_file.template @@ -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 diff --git a/tests/invalid-examples/pep621/dynamic/static_entry_points_listed_as_dynamic.errors.txt b/tests/invalid-examples/pep621/dynamic/static_entry_points_listed_as_dynamic.errors.txt new file mode 100644 index 0000000..5ab1320 --- /dev/null +++ b/tests/invalid-examples/pep621/dynamic/static_entry_points_listed_as_dynamic.errors.txt @@ -0,0 +1 @@ +cannot provide a value for `project.entry-points` and list it under `project.dynamic` at the same time diff --git a/tests/invalid-examples/pep621/dynamic/static_entry_points_listed_as_dynamic.toml b/tests/invalid-examples/pep621/dynamic/static_entry_points_listed_as_dynamic.toml new file mode 100644 index 0000000..e493d91 --- /dev/null +++ b/tests/invalid-examples/pep621/dynamic/static_entry_points_listed_as_dynamic.toml @@ -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" diff --git a/tests/test_examples.py b/tests/test_examples.py index 8fc850a..50908b3 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -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_ @@ -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())