Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/usethis/_integrations/file/ini/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class INIValueAlreadySetError(INIError):


class INIValueMissingError(KeyError, INIError):
"""Raised when a value is unexpectedly missing from the TOML file."""
"""Raised when a value is unexpectedly missing from the INI file."""


class UnexpectedINIOpenError(INIError):
Expand Down
4 changes: 4 additions & 0 deletions src/usethis/_integrations/file/toml/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ class TOMLValueMissingError(KeyError, TOMLError):
"""Raised when a value is unexpectedly missing from the TOML file."""


class TOMLValueInvalidError(TOMLError):
"""Raised when a value in the TOML file is unexpectedly invalid."""


class TOMLNotFoundError(FileNotFoundError, TOMLError):
"""Raised when a TOML file is unexpectedly not found."""

Expand Down
66 changes: 53 additions & 13 deletions src/usethis/_integrations/file/toml/io_.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import mergedeep
import tomlkit.api
import tomlkit.items
from pydantic import TypeAdapter
from pydantic import TypeAdapter, ValidationError
from tomlkit import TOMLDocument
from tomlkit.container import OutOfOrderTableProxy
from tomlkit.exceptions import TOMLKitError
Expand All @@ -18,6 +18,7 @@
TOMLDecodeError,
TOMLNotFoundError,
TOMLValueAlreadySetError,
TOMLValueInvalidError,
TOMLValueMissingError,
UnexpectedTOMLIOError,
UnexpectedTOMLOpenError,
Expand Down Expand Up @@ -109,7 +110,7 @@ def __contains__(self, keys: Sequence[Key]) -> bool:
TypeAdapter(dict).validate_python(container)
assert isinstance(container, dict)
container = container[key]
except KeyError:
except (KeyError, ValidationError):
return False

return True
Expand All @@ -120,9 +121,16 @@ def __getitem__(self, item: Sequence[Key]) -> Any:

d = self.get()
for key in keys:
TypeAdapter(dict).validate_python(d)
try:
TypeAdapter(dict).validate_python(d)
except ValidationError:
msg = f"Configuration value '{print_keys(keys)}' is missing."
raise TOMLValueMissingError(msg) from None
assert isinstance(d, dict)
d = d[key]
try:
d = d[key]
except KeyError as err:
raise TOMLValueMissingError(err) from None

return d

Expand Down Expand Up @@ -165,14 +173,24 @@ def set_value(
current_keys=shared_keys,
value=value,
)
except ValidationError:
if not exists_ok:
# The configuration is already present, which is not allowed.
_raise_already_set(keys)
else:
_set_value_in_existing(
toml_document=toml_document,
current_container=d,
keys=keys,
current_keys=shared_keys,
value=value,
)
else:
if not exists_ok:
# The configuration is already present, which is not allowed.
_raise_already_set(keys)
else:
# The configuration is already present, but we're allowed to overwrite it.
TypeAdapter(dict).validate_python(parent)
assert isinstance(parent, dict)
parent[keys[-1]] = value

self.commit(toml_document) # type: ignore[reportAssignmentType]
Expand All @@ -197,7 +215,7 @@ def __delitem__(self, keys: Sequence[Key]) -> None:
TypeAdapter(dict).validate_python(d)
assert isinstance(d, dict)
d = d[key]
except KeyError:
except (KeyError, ValidationError):
# N.B. by convention a del call should raise an error if the key is not found.
msg = f"Configuration value '{print_keys(keys)}' is missing."
raise TOMLValueMissingError(msg) from None
Expand Down Expand Up @@ -270,10 +288,21 @@ def extend_list(self, *, keys: Sequence[Key], values: list[Any]) -> None:
assert isinstance(contents, dict)
toml_document = mergedeep.merge(toml_document, contents)
assert isinstance(toml_document, TOMLDocument)
except ValidationError:
msg = (
f"Configuration value '{print_keys(keys[:-1])}' is not a valid mapping in "
f"the TOML file '{self.name}', and does not contain the key '{keys[-1]}'."
)
raise TOMLValueMissingError(msg) from None
else:
TypeAdapter(dict).validate_python(p_parent)
TypeAdapter(list).validate_python(d)
assert isinstance(p_parent, dict)
try:
TypeAdapter(list).validate_python(d)
except ValidationError:
msg = (
f"Configuration value '{print_keys(keys)}' is not a valid list in "
f"the TOML file '{self.name}'."
)
raise TOMLValueInvalidError(msg) from None
assert isinstance(d, list)
p_parent[keys[-1]] = d + values

Expand All @@ -298,13 +327,24 @@ def remove_from_list(self, *, keys: Sequence[Key], values: Collection[Any]) -> N
TypeAdapter(dict).validate_python(p_parent)
assert isinstance(p_parent, dict)
p = p_parent[keys[-1]]
except ValidationError:
msg = (
f"Configuration value '{print_keys(keys[:-1])}' is not a valid mapping in "
f"the TOML file '{self.name}', and does not contain the key '{keys[-1]}'."
)
raise TOMLValueMissingError(msg) from None
except KeyError:
# The configuration is not present - do not modify
return

TypeAdapter(dict).validate_python(p_parent)
TypeAdapter(list).validate_python(p)
assert isinstance(p_parent, dict)
try:
TypeAdapter(list).validate_python(p)
except ValidationError:
msg = (
f"Configuration value '{print_keys(keys)}' is not a valid list in "
f"the TOML file '{self.name}'."
)
raise TOMLValueInvalidError(msg) from None
assert isinstance(p, list)

new_values = [value for value in p if value not in values]
Expand Down
44 changes: 44 additions & 0 deletions tests/usethis/_core/test_author.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
from pathlib import Path

import pytest

from usethis._core.author import add_author
from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager
from usethis._integrations.file.toml.errors import (
TOMLValueInvalidError,
TOMLValueMissingError,
)
from usethis._test import change_cwd


Expand Down Expand Up @@ -138,3 +144,41 @@ def test_doesnt_break_other_sections(self, tmp_path: Path):
with change_cwd(tmp_path), PyprojectTOMLManager() as manager:
assert ["project"] in manager
assert ["project", "scripts"] in manager

def test_project_section_not_a_mapping(self, tmp_path: Path):
# Arrange
(tmp_path / "pyproject.toml").write_text(
"""\
project = ["a"]
"""
)

# Act
with (
change_cwd(tmp_path),
PyprojectTOMLManager(),
pytest.raises(
TOMLValueMissingError,
match="'project' is not a valid mapping .* does not contain the key 'authors'",
),
):
add_author(name="John Cleese")

def test_authors_section_not_a_list(self, tmp_path: Path):
# Arrange
(tmp_path / "pyproject.toml").write_text(
"""\
[project]
authors = { name = "Python Dev" }
"""
)

# Act
with (
change_cwd(tmp_path),
PyprojectTOMLManager(),
pytest.raises(
TOMLValueInvalidError, match="'project.authors' is not a valid list"
),
):
add_author(name="John Cleese")
Loading