Skip to content

Commit

Permalink
Fix pre-commit hook when called with multiple files (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
abravalheri committed May 17, 2022
2 parents 5eeb787 + 9b3ecae commit 802c455
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 28 deletions.
11 changes: 11 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,14 @@ repos:
hooks:
- id: flake8
additional_dependencies: [flake8-bugbear]


- repo: local # self-test for `validate-pyproject` hook
hooks:
- id: validate-pyproject
name: Validate pyproject.toml
language: python
files: ^tests/examples/pretend-setuptools/07-pyproject.toml$
entry: validate-pyproject
additional_dependencies:
- validate-pyproject[all]
2 changes: 1 addition & 1 deletion .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
description: Validation library for a simple check on pyproject.toml,
including optional dependencies
language: python
files: pyproject.toml
files: ^pyproject.toml$
entry: validate-pyproject
additional_dependencies:
- .[all]
9 changes: 8 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@
Changelog
=========

Version 0.8
===========

- New :pypi:`pre-commit` hook, #40
- Allow multiple TOML files to be validated at once via **CLI**
(*no changes regarding the Python API*).

Version 0.7.2
=============

- ``setuptools`` plugin:
- Allow ``dependencies``/``optional-dependencies`` to use file directives (#37)
- Allow ``dependencies``/``optional-dependencies`` to use file directives, #37

Version 0.7.1
=============
Expand Down
7 changes: 7 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ pre-commit
hooks:
- id: validate-pyproject
By default, this ``pre-commit`` hook will only validate the ``pyproject.toml``
file at the root of the project repository.
You can customize that by defining a `custom regular expression pattern`_ using
the ``files`` parameter.


Note
====

Expand All @@ -164,6 +170,7 @@ For details and usage information on PyScaffold see https://pyscaffold.org/.


.. _contribution guides: https://validate-pyproject.readthedocs.io/en/latest/contributing.html
.. _custom regular expression pattern: https://pre-commit.com/#regular-expressions
.. _our docs: https://validate-pyproject.readthedocs.io
.. _ini2toml: https://ini2toml.readthedocs.io
.. _JSON Schema: https://json-schema.org/
Expand Down
83 changes: 68 additions & 15 deletions src/validate_pyproject/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,17 @@
from contextlib import contextmanager
from itertools import chain
from textwrap import dedent, wrap
from typing import Callable, Dict, List, NamedTuple, Sequence, Type, TypeVar
from typing import (
Callable,
Dict,
Iterator,
List,
NamedTuple,
Sequence,
Tuple,
Type,
TypeVar,
)

from . import __version__
from .api import Validator
Expand All @@ -23,13 +33,16 @@


try:
from tomli import loads
from tomli import TOMLDecodeError, loads
except ImportError: # pragma: no cover
try:
from toml import TomlDecodeError as TOMLDecodeError # type: ignore
from toml import loads # type: ignore
except ImportError as ex:
raise ImportError("Please install a TOML parser (e.g. `tomli`)") from ex

_REGULAR_EXCEPTIONS = (ValidationError, TOMLDecodeError)


@contextmanager
def critical_logging():
Expand All @@ -50,8 +63,8 @@ def critical_logging():
),
"input_file": dict(
dest="input_file",
nargs="?",
default="-",
nargs="*",
default=[argparse.FileType("r")("-")],
type=argparse.FileType("r"),
help="TOML file to be verified (`stdin` by default)",
),
Expand Down Expand Up @@ -94,7 +107,7 @@ def critical_logging():


class CliParams(NamedTuple):
input_file: io.TextIOBase
input_file: List[io.TextIOBase]
plugins: List[PluginWrapper]
loglevel: int = logging.WARNING
dump_json: bool = False
Expand Down Expand Up @@ -164,13 +177,18 @@ def setup_logging(loglevel: int):


@contextmanager
def exceptisons2exit():
def exceptions2exit():
try:
yield
except ValidationError as ex:
except _ExceptionGroup as group:
for prefix, ex in group:
print(prefix)
_logger.error(str(ex) + "\n")
raise SystemExit(1)
except _REGULAR_EXCEPTIONS as ex:
_logger.error(str(ex))
raise SystemExit(1)
except Exception as ex:
except Exception as ex: # pragma: no cover
_logger.error(f"{ex.__class__.__name__}: {ex}\n")
_logger.debug("Please check the following information:", exc_info=True)
raise SystemExit(1)
Expand All @@ -191,16 +209,25 @@ def run(args: Sequence[str] = ()):
params: CliParams = parse_args(args, plugins)
setup_logging(params.loglevel)
validator = Validator(plugins=params.plugins)
toml_equivalent = loads(params.input_file.read())
validator(toml_equivalent)
if params.dump_json:
print(json.dumps(toml_equivalent, indent=2))
else:
print("Valid file")

exceptions = _ExceptionGroup()
for file in params.input_file:
try:
toml_equivalent = loads(file.read())
validator(toml_equivalent)
if params.dump_json:
print(json.dumps(toml_equivalent, indent=2))
else:
print(f"Valid {_format_file(file)}")
except _REGULAR_EXCEPTIONS as ex:
exceptions.add(f"Invalid {_format_file(file)}", ex)

exceptions.raise_if_any()

return 0


main = exceptisons2exit()(run)
main = exceptions2exit()(run)


class Formatter(argparse.RawTextHelpFormatter):
Expand All @@ -226,3 +253,29 @@ def _format_plugin_help(plugin: PluginWrapper) -> str:
help_text = plugin.help_text
help_text = f": {_flatten_str(help_text)}" if help_text else ""
return f'* "{plugin.tool}"{help_text}'


def _format_file(file: io.TextIOBase) -> str:
if hasattr(file, "name") and file.name: # type: ignore[attr-defined]
return f"file: {file.name}" # type: ignore[attr-defined]
return "file" # pragma: no cover


class _ExceptionGroup(Exception):
def __init__(self):
self._members: List[Tuple[str, Exception]] = []
super().__init__()

def add(self, prefix: str, ex: Exception):
self._members.append((prefix, ex))

def __iter__(self) -> Iterator[Tuple[str, Exception]]:
return iter(self._members)

def raise_if_any(self):
number = len(self._members)
if number == 1:
print(self._members[0][0])
raise self._members[0][1]
if number > 0:
raise self
4 changes: 2 additions & 2 deletions src/validate_pyproject/extra_validations.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

from typing import Mapping, TypeVar

from ._vendor.fastjsonschema import JsonSchemaValueException
from .error_reporting import ValidationError

T = TypeVar("T", bound=Mapping)


class RedefiningStaticFieldAsDynamic(JsonSchemaValueException):
class RedefiningStaticFieldAsDynamic(ValidationError):
"""According to PEP 621:
Build back-ends MUST raise an error if the metadata specifies a field
Expand Down
2 changes: 1 addition & 1 deletion src/validate_pyproject/pre_compile/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def run(args: Sequence[str] = ()):
return 0


main = cli.exceptisons2exit()(run)
main = cli.exceptions2exit()(run)


if __name__ == "__main__":
Expand Down
47 changes: 39 additions & 8 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
from pathlib import Path
from uuid import uuid4

import pytest
from validate_pyproject._vendor.fastjsonschema import JsonSchemaValueException
Expand Down Expand Up @@ -46,23 +47,25 @@ def parse_args(args):
"""


def write_example(dir_path, text):
path = Path(dir_path, "pyproject.toml")
path.write_text(text, "UTF-8")
def write_example(dir_path, *, name="pyproject.toml", _text=simple_example):
path = Path(dir_path, name)
path.write_text(_text, "UTF-8")
return path


def write_invalid_example(dir_path, *, name="pyproject.toml"):
text = simple_example.replace("zip-safe = false", "zip-safe = { hello = 'world' }")
return write_example(dir_path, name=name, _text=text)


@pytest.fixture
def valid_example(tmp_path):
return write_example(tmp_path, simple_example)
return write_example(tmp_path)


@pytest.fixture
def invalid_example(tmp_path):
example = simple_example.replace(
"zip-safe = false", "zip-safe = { hello = 'world' }"
)
return write_example(tmp_path, example)
return write_invalid_example(tmp_path)


class TestEnable:
Expand Down Expand Up @@ -130,3 +133,31 @@ def test_invalid(self, caplog, invalid_example):
assert "offending rule" in captured
assert "given value" in captured
assert '"type": "boolean"' in captured


def test_multiple_files(tmp_path, capsys):
N = 3

valid_files = [
write_example(tmp_path, name=f"valid-pyproject{i}.toml") for i in range(N)
]
cli.run(map(str, valid_files))
captured = capsys.readouterr().out.lower()
number_valid = captured.count("valid file:")
assert number_valid == N

invalid_files = [
write_invalid_example(tmp_path, name=f"invalid-pyproject{i}.toml")
for i in range(N + 3)
]
with pytest.raises(SystemExit):
cli.main(map(str, valid_files + invalid_files))

repl = str(uuid4())
captured = capsys.readouterr().out.lower()
captured = captured.replace("invalid file:", repl)
number_invalid = captured.count(repl)
number_valid = captured.count("valid file:")
captured = captured.replace(repl, "invalid file:")
assert number_valid == N
assert number_invalid == N + 3

0 comments on commit 802c455

Please sign in to comment.