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
45 changes: 31 additions & 14 deletions src/usethis/_integrations/file/toml/io_.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,9 @@ def set_value(
except KeyError:
_set_value_in_existing(
toml_document=toml_document,
current_container=d,
shared_container=d,
shared_keys=shared_keys,
keys=keys,
current_keys=shared_keys,
value=value,
)
except ValidationError:
Expand All @@ -179,9 +179,9 @@ def set_value(
else:
_set_value_in_existing(
toml_document=toml_document,
current_container=d,
shared_keys=shared_keys,
shared_container=d,
keys=keys,
current_keys=shared_keys,
value=value,
)
else:
Expand Down Expand Up @@ -269,12 +269,14 @@ def extend_list(self, *, keys: Sequence[Key], values: list[Any]) -> None:

toml_document = copy.copy(self.get())

shared_keys: list[str] = []
d = toml_document
try:
d = toml_document
for key in keys[:-1]:
TypeAdapter(dict).validate_python(d)
assert isinstance(d, dict)
d = d[key]
shared_keys.append(key)
p_parent = d
TypeAdapter(dict).validate_python(p_parent)
assert isinstance(p_parent, dict)
Expand All @@ -284,7 +286,13 @@ def extend_list(self, *, keys: Sequence[Key], values: list[Any]) -> None:
for key in reversed(keys):
contents = {key: contents}
assert isinstance(contents, dict)
toml_document = mergedeep.merge(toml_document, contents)
_set_value_in_existing(
toml_document=toml_document,
shared_keys=shared_keys,
shared_container=d,
keys=keys,
value=values,
)
assert isinstance(toml_document, TOMLDocument)
except ValidationError:
msg = (
Expand Down Expand Up @@ -349,11 +357,20 @@ def remove_from_list(self, *, keys: Sequence[Key], values: Collection[Any]) -> N
def _set_value_in_existing(
*,
toml_document: TOMLDocument,
current_container: TOMLDocument | Item | Container,
shared_keys: Sequence[Key],
shared_container: TOMLDocument | Item | Container,
keys: Sequence[Key],
current_keys: Sequence[Key],
value: Any,
) -> None:
"""Set a new value in an existing container.

Args:
toml_document: The overall document.
shared_keys: Keys to the deepest container that actually exists.
shared_container: The shared container itself that needs new contents.
keys: Keys to the new value from the root of the document.
value: The value at the keys.
"""
# The old configuration should be kept for all ID keys except the
# final/deepest one which shouldn't exist anyway since we checked as much,
# above. For example, if there is [tool.ruff] then we shouldn't overwrite it
Expand All @@ -369,12 +386,12 @@ def _set_value_in_existing(
else:
# Note that this alternative logic is just to avoid a bug:
# https://github.com/usethis-python/usethis-python/issues/507
TypeAdapter(dict).validate_python(current_container)
assert isinstance(current_container, dict)
TypeAdapter(dict).validate_python(shared_container)
assert isinstance(shared_container, dict)

unshared_keys = keys[len(current_keys) :]
unshared_keys = keys[len(shared_keys) :]

if len(current_keys) == 1:
if len(shared_keys) == 1:
# In this case, we need to "seed" the section to avoid another bug:
# https://github.com/usethis-python/usethis-python/issues/558

Expand All @@ -385,9 +402,9 @@ def _set_value_in_existing(
for key in reversed(unshared_keys[1:]):
contents = {key: contents}

current_container[keys[1]] = contents # type: ignore[reportAssignmentType]
shared_container[keys[1]] = contents # type: ignore[reportAssignmentType]
else:
current_container[_get_unified_key(unshared_keys)] = value
shared_container[_get_unified_key(unshared_keys)] = value


def _validate_keys(keys: Sequence[Key]) -> list[str]:
Expand Down
5 changes: 2 additions & 3 deletions src/usethis/_tool/rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ class RuleConfig(BaseModel):
"""Configuration for linter rules associated with a tool.

There is a distinction between selected and ignored rules. Selected rules are those
which are enabled and will be run by the tool unless ignored. Ignored rules are
those which are not run by the tool, even if they are selected. This follows the
Ruff paradigm.
which are enabled for the tool and will be used unless ignored. Ignored rules are
those which are not run, even if they are selected. This follows the Ruff paradigm.

There is also a distinction between managed and unmanaged rule config. Managed
selections (and ignores) are those which are managed exclusively by the one tool,
Expand Down
34 changes: 34 additions & 0 deletions tests/usethis/_core/test_core_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -1373,6 +1373,40 @@ def test_inp_rules_not_selected_for_tests_dir(self, tmp_path: Path):
"""
)

@pytest.mark.usefixtures("_vary_network_conn")
def test_inp_rules_dont_break_config(self, uv_init_dir: Path):
# Arrange
(uv_init_dir / "pyproject.toml").write_text("""\
[project]
name = "test"
version = "0.1.0"

[tool.ruff]
line-length = 88
format.docstring-code-format = true
lint.select = [ "A", "C4", "E4", "E7", "E9", "F", "FLY", "FURB", "I", "INP", "PLE", "PLR", "PT", "RUF", "SIM", "UP" ]
lint.ignore = [ "PLR2004", "SIM108" ]
""")

with change_cwd(uv_init_dir), files_manager():
# Act
use_import_linter()

# Assert
contents = (uv_init_dir / "pyproject.toml").read_text()
assert contents.startswith("""\
[project]
name = "test"
version = "0.1.0"

[tool.ruff]
line-length = 88
format.docstring-code-format = true
lint.select = [ "A", "C4", "E4", "E7", "E9", "F", "FLY", "FURB", "I", "INP", "PLE", "PLR", "PT", "RUF", "SIM", "UP" ]
lint.per-file-ignores."tests/**" = ["INP"]
lint.ignore = [ "PLR2004", "SIM108" ]
""")

@pytest.mark.usefixtures("_vary_network_conn")
def test_ruff_passes(self, tmp_path: Path):
with change_cwd(tmp_path), files_manager():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from usethis._integrations.file.pyproject_toml.requires_python import (
get_requires_python,
)
from usethis._test import is_offline
from usethis._test import change_cwd, is_offline


class TestStep2:
Expand Down Expand Up @@ -42,7 +42,7 @@ def test_matches_schema_store(self):
# TIP: go into debug mode to copy-and-paste into updated schema.json
assert local_schema_json == online_schema_json

def test_target_python_version(self):
def test_target_python_version(self, usethis_dev_dir: Path):
# If this test fails, we should bump the version in the command in schema.py
with PyprojectTOMLManager():
with change_cwd(usethis_dev_dir), PyprojectTOMLManager():
assert get_requires_python() == ">=3.10"
Original file line number Diff line number Diff line change
Expand Up @@ -392,5 +392,32 @@ def test_add_one(self, tmp_path: Path):
== """\
[tool.usethis]
key = ["value1", "value2"]
"""
)

def test_deep_nesting_doesnt_break_config(self, tmp_path: Path):
# https://github.com/usethis-python/usethis-python/issues/862
# The issue is basically the same as this one though:
# https://github.com/usethis-python/usethis-python/issues/507
(tmp_path / "pyproject.toml").write_text("""\
[tool.ruff]
lint.select = [ "INP" ]
""")

with change_cwd(tmp_path), PyprojectTOMLManager() as mgr:
# Act
mgr.extend_list(
keys=["tool", "ruff", "lint", "per-file-ignores", "tests/**"],
values=["INP"],
)

# Assert
contents = (tmp_path / "pyproject.toml").read_text()
assert (
contents
== """\
[tool.ruff]
lint.select = [ "INP" ]
lint.per-file-ignores."tests/**" = ["INP"]
"""
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
get_requires_python,
)
from usethis._integrations.pre_commit.schema import HookDefinition, LocalRepo
from usethis._test import is_offline
from usethis._test import change_cwd, is_offline


def test_multiple_per_repo():
Expand Down Expand Up @@ -44,7 +44,7 @@ def test_matches_schema_store(self):
# TIP: go into debug mode to copy-and-paste into updated schema.json
assert local_schema_json == online_schema_json.replace("\r\n", "\n\n")

def test_target_python_version(self):
def test_target_python_version(self, usethis_dev_dir: Path):
# If this test fails, we should bump the version in the command in schema.py
with PyprojectTOMLManager():
with change_cwd(usethis_dev_dir), PyprojectTOMLManager():
assert get_requires_python() == ">=3.10"
4 changes: 2 additions & 2 deletions tests/usethis/_interface/test_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ def test_dependencies_added(self, tmp_path: Path):

# Check Ruff formatter is not added
txt = (tmp_path / "pyproject.toml").read_text()
assert "ruff.format" not in txt
assert "ruff.lint" in txt
assert "format" not in txt
assert "lint" in txt
26 changes: 26 additions & 0 deletions tests/usethis/_tool/impl/test_ruff.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,3 +312,29 @@ def test_ruff_toml(self, tmp_path: Path):
# Act
with change_cwd(tmp_path), DotRuffTOMLManager():
assert RuffTool().is_formatter_used()

class TestIgnoreRulesInGlob:
def test_dont_break_config(self, tmp_path: Path):
# https://github.com/usethis-python/usethis-python/issues/862
# The issue is basically the same as this one though:
# https://github.com/usethis-python/usethis-python/issues/507
# Arrange
(tmp_path / "pyproject.toml").write_text("""\
[tool.ruff]
lint.select = [ "INP" ]
""")

with change_cwd(tmp_path), files_manager():
# Act
RuffTool().ignore_rules_in_glob(["INP"], glob="tests/**")

# Assert
contents = (tmp_path / "pyproject.toml").read_text()
assert (
contents
== """\
[tool.ruff]
lint.select = [ "INP" ]
lint.per-file-ignores."tests/**" = ["INP"]
"""
)
Loading