Skip to content

Commit

Permalink
Validate pyproject.toml configuration more stringently
Browse files Browse the repository at this point in the history
Co-authored-by: Amethyst Reese <amy@n7.gg>
  • Loading branch information
akx and amyreese committed Mar 27, 2023
1 parent 1c5ba5a commit 5b44451
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 10 deletions.
35 changes: 25 additions & 10 deletions ufmt/config.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# Copyright 2022 Amethyst Reese
# Licensed under the MIT license

import logging
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional
from typing import List, Optional, Sequence

import tomlkit
from trailrunner import project_root

LOG = logging.getLogger(__name__)


@dataclass
class UfmtConfig:
Expand All @@ -22,13 +24,26 @@ def ufmt_config(path: Optional[Path] = None) -> UfmtConfig:
config_path = root / "pyproject.toml"
if config_path.is_file():
pyproject = tomlkit.loads(config_path.read_text())
config: Dict[str, Any] = {}

if "tool" in pyproject and "ufmt" in pyproject["tool"]: # type: ignore
config.update(pyproject["tool"]["ufmt"]) # type: ignore

config["project_root"] = root
config["pyproject_path"] = config_path
return UfmtConfig(**config)
config = pyproject.get("tool", {}).get("ufmt", {})
if not isinstance(config, dict):
LOG.warning("%s: tool.ufmt is not a mapping, ignoring", config_path)
config = {}

config_excludes = config.pop("excludes", [])
if isinstance(config_excludes, Sequence) and not isinstance(
config_excludes, str
):
excludes = [str(x) for x in config_excludes]
else:
raise ValueError(f"{config_path}: excludes must be a list of strings")

if config:
LOG.warning("%s: unknown values ignored: %r", config_path, sorted(config))

return UfmtConfig(
project_root=root,
pyproject_path=config_path,
excludes=excludes,
)

return UfmtConfig()
117 changes: 117 additions & 0 deletions ufmt/tests/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from tempfile import TemporaryDirectory
from textwrap import dedent
from unittest import TestCase
from unittest.mock import ANY, patch

from trailrunner.tests.core import cd

Expand Down Expand Up @@ -93,3 +94,119 @@ def test_ufmt_config(self):
),
config,
)

@patch("ufmt.config.LOG")
def test_invalid_config(self, log_mock):
with self.subTest("string"):
self.pyproject.write_text(
dedent(
"""
[tool]
ufmt = "hello"
"""
)
)
expected = UfmtConfig(project_root=self.td, pyproject_path=self.pyproject)
result = ufmt_config(self.td / "fake.py")
self.assertEqual(expected, result)

log_mock.warning.assert_called_once()
log_mock.reset_mock()

with self.subTest("array"):
self.pyproject.write_text(
dedent(
"""
[[tool.ufmt]]
excludes = ["fixtures/"]
"""
)
)
expected = UfmtConfig(project_root=self.td, pyproject_path=self.pyproject)
result = ufmt_config(self.td / "fake.py")
self.assertEqual(expected, result)

log_mock.warning.assert_called_once()
log_mock.reset_mock()

with self.subTest("extra"):
self.pyproject.write_text(
dedent(
"""
[tool.ufmt]
unknown_element = true
hello_world = "my name is"
"""
)
)
expected = UfmtConfig(project_root=self.td, pyproject_path=self.pyproject)
result = ufmt_config(self.td / "fake.py")
self.assertEqual(expected, result)

log_mock.warning.assert_called_with(
ANY, self.pyproject, ["hello_world", "unknown_element"]
)
log_mock.reset_mock()

@patch("ufmt.config.LOG")
def test_config_excludes(self, log_mock):
with self.subTest("missing"):
self.pyproject.write_text(
dedent(
"""
[tool.ufmt]
"""
)
)
expected = UfmtConfig(project_root=self.td, pyproject_path=self.pyproject)
result = ufmt_config(self.td / "fake.py")
self.assertEqual(expected, result)
log_mock.assert_not_called()

with self.subTest("empty"):
self.pyproject.write_text(
dedent(
"""
[tool.ufmt]
excludes = []
"""
)
)
expected = UfmtConfig(
project_root=self.td, pyproject_path=self.pyproject, excludes=[]
)
result = ufmt_config(self.td / "fake.py")
self.assertEqual(expected, result)
log_mock.assert_not_called()

with self.subTest("list"):
self.pyproject.write_text(
dedent(
"""
[tool.ufmt]
excludes = ["fixtures/"]
"""
)
)
expected = UfmtConfig(
project_root=self.td,
pyproject_path=self.pyproject,
excludes=["fixtures/"],
)
result = ufmt_config(self.td / "fake.py")
self.assertEqual(expected, result)
log_mock.assert_not_called()

with self.subTest("string"):
self.pyproject.write_text(
dedent(
"""
[tool.ufmt]
excludes = "fixtures/"
"""
)
)
with self.assertRaisesRegex(
ValueError, "excludes must be a list of strings"
):
ufmt_config(self.td / "fake.py")

0 comments on commit 5b44451

Please sign in to comment.