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
27 changes: 20 additions & 7 deletions src/usethis/_file/ini/io_.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,11 @@ def __getitem__(self, item: Sequence[Key]) -> object:
raise KeyError(msg)

def set_value(
self, *, keys: Sequence[Key], value: Any, exists_ok: bool = False
self,
*,
keys: Sequence[Key],
value: object,
exists_ok: bool = False,
) -> None:
"""Set a value in the INI file.

Expand All @@ -156,19 +160,24 @@ def set_value(
root = self.get()

if len(keys) == 0:
value = TypeAdapter(dict[str, dict[str, str | list[str]]]).validate_python(
value
)
self._set_value_in_root(root=root, value=value, exists_ok=exists_ok)
elif len(keys) == 1:
(section_key,) = keys
value = TypeAdapter(dict[str, str | list[str]]).validate_python(value)
self._set_value_in_section(
root=root, section_key=keys[0], value=value, exists_ok=exists_ok
)
elif len(keys) == 2:
(section_key, option_key) = keys
cast_value = TypeAdapter(str | list[str]).validate_python(value)
self._set_value_in_option(
root=root,
section_key=section_key,
option_key=option_key,
value=value,
value=cast_value,
exists_ok=exists_ok,
)
else:
Expand Down Expand Up @@ -271,7 +280,7 @@ def _set_value_in_option(
root: INIDocument,
section_key: Key,
option_key: Key,
value: str,
value: str | list[str],
exists_ok: bool,
) -> None:
if not isinstance(section_key, str) or not isinstance(option_key, str):
Expand Down Expand Up @@ -408,7 +417,7 @@ def _delete_strkeys(self, strkeys: Sequence[str]) -> None:

self.commit(root)

def extend_list(self, *, keys: Sequence[Key], values: list[str]) -> None:
def extend_list(self, *, keys: Sequence[Key], values: Sequence[object]) -> None:
"""Extend a list in the INI file.

An empty list of keys corresponds to the root of the document.
Expand All @@ -429,6 +438,7 @@ def extend_list(self, *, keys: Sequence[Key], values: list[str]) -> None:
raise InvalidINITypeError(msg)
elif len(keys) == 2:
section_key, option_key = keys
values = TypeAdapter(list[str]).validate_python(values)
self._extend_list_in_option(
root=root, section_key=section_key, option_key=option_key, values=values
)
Expand All @@ -443,7 +453,7 @@ def extend_list(self, *, keys: Sequence[Key], values: list[str]) -> None:

@staticmethod
def _extend_list_in_option(
*, root: INIDocument, section_key: Key, option_key: Key, values: list[str]
*, root: INIDocument, section_key: Key, option_key: Key, values: Sequence[str]
) -> None:
for value in values:
INIFileManager._validated_append(
Expand All @@ -452,7 +462,7 @@ def _extend_list_in_option(

@staticmethod
def _remove_from_list_in_option(
*, root: INIDocument, section_key: Key, option_key: Key, values: list[str]
*, root: INIDocument, section_key: Key, option_key: Key, values: Sequence[str]
) -> None:
if section_key not in root:
return
Expand Down Expand Up @@ -485,7 +495,9 @@ def _remove_from_list_in_option(
elif len(new_values) > 1:
root[section_key][option_key].set_values(new_values)

def remove_from_list(self, *, keys: Sequence[Key], values: list[str]) -> None:
def remove_from_list(
self, *, keys: Sequence[Key], values: Sequence[object]
) -> None:
"""Remove values from a list in the INI file.

An empty list of keys corresponds to the root of the document.
Expand All @@ -506,6 +518,7 @@ def remove_from_list(self, *, keys: Sequence[Key], values: list[str]) -> None:
raise InvalidINITypeError(msg)
elif len(keys) == 2:
section_key, option_key = keys
values = TypeAdapter(list[str]).validate_python(values)
self._remove_from_list_in_option(
root=root, section_key=section_key, option_key=option_key, values=values
)
Expand Down
8 changes: 4 additions & 4 deletions src/usethis/_file/merge.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from __future__ import annotations

from collections.abc import MutableMapping
from typing import Any
from typing import Any, TypeVar

T = TypeVar("T", bound=MutableMapping[Any, Any])

def _deep_merge(
target: MutableMapping[Any, Any], source: MutableMapping[Any, Any]
) -> MutableMapping[Any, Any]:

def _deep_merge(target: T, source: MutableMapping[Any, Any]) -> T:
"""Recursively merge source into target in place, returning target.

For keys present in both mappings, if both values are mappings the merge is
Expand Down
21 changes: 15 additions & 6 deletions src/usethis/_file/toml/io_.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ def set_value(
# The configuration is already present, but we're allowed to overwrite it.
parent[keys[-1]] = value

self.commit(toml_document) # type: ignore[reportAssignmentType]
self.commit(toml_document)

def __delitem__(self, keys: Sequence[Key]) -> None:
"""Delete a value in the TOML file.
Expand Down Expand Up @@ -262,7 +262,7 @@ def __delitem__(self, keys: Sequence[Key]) -> None:

self.commit(toml_document)

def extend_list(self, *, keys: Sequence[Key], values: list[Any]) -> None:
def extend_list(self, *, keys: Sequence[Key], values: Sequence[Any]) -> None:
if not keys:
msg = "At least one ID key must be provided."
raise ValueError(msg)
Expand Down Expand Up @@ -311,7 +311,7 @@ def extend_list(self, *, keys: Sequence[Key], values: list[Any]) -> None:
)
raise TOMLValueInvalidError(msg) from None
assert isinstance(d, list)
p_parent[keys[-1]] = d + values
p_parent[keys[-1]] = d + list(values)

self.commit(toml_document)

Expand Down Expand Up @@ -382,7 +382,7 @@ def _set_value_in_existing(
contents = value
for key in reversed(keys):
contents = {key: contents}
toml_document = _deep_merge(toml_document, contents) # type: ignore[reportAssignmentType]
toml_document = _deep_merge(toml_document, contents)
assert isinstance(toml_document, TOMLDocument)
else:
# Note that this alternative logic is just to avoid a bug:
Expand All @@ -397,13 +397,22 @@ def _set_value_in_existing(
# https://github.com/usethis-python/usethis-python/issues/558

placeholder = {keys[0]: {keys[1]: {}}}
toml_document = _deep_merge(toml_document, placeholder) # type: ignore[reportArgumentType]
toml_document = _deep_merge(toml_document, placeholder)

contents = value
for key in reversed(unshared_keys[1:]):
contents = {key: contents}

shared_container[keys[1]] = contents # pyright: ignore[reportArgumentType]
# Check it's not a pattern-based key, which isn't supported in TOML files.
key = keys[1]
if isinstance(key, re.Pattern):
msg = (
f"Regex-based keys are not currently supported in TOML files: "
f"{print_keys(keys)}"
)
raise NotImplementedError(msg)

shared_container[key] = contents
else:
shared_container[_get_unified_key(unshared_keys)] = value

Expand Down
8 changes: 4 additions & 4 deletions src/usethis/_file/yaml/io_.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ def __delitem__(self, keys: Sequence[Key]) -> None:
)
self.commit(self._content)

def extend_list(self, *, keys: Sequence[Key], values: list[Any]) -> None:
def extend_list(self, *, keys: Sequence[Key], values: Sequence[Any]) -> None:
"""Extend a list in the configuration file."""
if not keys:
msg = "At least one ID key must be provided."
Expand Down Expand Up @@ -317,7 +317,7 @@ def extend_list(self, *, keys: Sequence[Key], values: list[Any]) -> None:
TypeAdapter(list).validate_python(d)
assert isinstance(p_parent, dict)
assert isinstance(d, list)
p_parent[keys[-1]] = d + values
p_parent[keys[-1]] = d + list(values)

assert self._content is not None # We have called .get() already.
update_ruamel_yaml_map(
Expand All @@ -327,7 +327,7 @@ def extend_list(self, *, keys: Sequence[Key], values: list[Any]) -> None:
)
self.commit(self._content)

def remove_from_list(self, *, keys: Sequence[Key], values: list[Any]) -> None:
def remove_from_list(self, *, keys: Sequence[Key], values: Sequence[Any]) -> None:
"""Remove values from a list in the configuration file."""
if not keys:
msg = "At least one ID key must be provided."
Expand Down Expand Up @@ -386,7 +386,7 @@ def _set_value_in_existing(
contents = value
for key in reversed(keys):
contents = {key: contents}
content = _deep_merge(content, contents) # type: ignore[reportAssignmentType]
content = _deep_merge(content, contents)


def _validate_keys(keys: Sequence[Key]) -> list[str]:
Expand Down
5 changes: 3 additions & 2 deletions src/usethis/_file/yaml/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
)

if TYPE_CHECKING:
from collections.abc import Sequence
from typing import Any

from usethis._file.yaml.typing_ import YAMLLiteral

_T = TypeVar("_T")
_T = TypeVar("_T", bound=object)


def update_ruamel_yaml_map(
Expand Down Expand Up @@ -116,7 +117,7 @@ def lcs_list_update(original: list[_T], new: list[_T]) -> None:
original_idx += 1


def _shared_id_sequences(*seqs: list[Any]) -> list[list[int]]:
def _shared_id_sequences(*seqs: Sequence[object]) -> Sequence[list[int]]:
"""Map list elements to integers which are equal iff the objects are with __eq__."""
# Don't use "in" because that would mean the elements must be hashable,
# which we don't want to require. This means we have to loop over every element,
Expand Down
14 changes: 8 additions & 6 deletions src/usethis/_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import re
from abc import ABCMeta, abstractmethod
from typing import TYPE_CHECKING, Generic, TypeAlias, TypeVar
from typing import TYPE_CHECKING, Any, Generic, TypeAlias, TypeVar

from typing_extensions import assert_never

Expand All @@ -13,7 +13,7 @@
from collections.abc import Sequence
from pathlib import Path
from types import TracebackType
from typing import Any, ClassVar
from typing import ClassVar

from typing_extensions import Self

Expand Down Expand Up @@ -192,7 +192,7 @@ def __contains__(self, keys: Sequence[Key]) -> bool:
def __getitem__(self, keys: Sequence[Key]) -> object:
raise NotImplementedError

def __setitem__(self, keys: Sequence[Key], value: Any) -> None:
def __setitem__(self, keys: Sequence[Key], value: object) -> None:
"""Set a value in the configuration file."""
return self.set_value(keys=keys, value=value, exists_ok=True)

Expand All @@ -202,21 +202,23 @@ def __delitem__(self, keys: Sequence[Key]) -> None:

@abstractmethod
def set_value(
self, *, keys: Sequence[Key], value: Any, exists_ok: bool = False
self, *, keys: Sequence[Key], value: object, exists_ok: bool = False
) -> None:
"""Set a value in the configuration file."""
raise NotImplementedError

@abstractmethod
def extend_list(self, *, keys: Sequence[Key], values: list[Any]) -> None:
def extend_list(self, *, keys: Sequence[Key], values: Sequence[object]) -> None:
"""Extend a list in the configuration file.

This method will always extend the list, even if it results in duplicates.
"""
raise NotImplementedError

@abstractmethod
def remove_from_list(self, *, keys: Sequence[Key], values: list[Any]) -> None:
def remove_from_list(
self, *, keys: Sequence[Key], values: Sequence[object]
) -> None:
"""Remove values from a list in the configuration file."""
raise NotImplementedError

Expand Down
10 changes: 2 additions & 8 deletions src/usethis/_pipeweld/containers.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from pydantic import BaseModel, RootModel

if TYPE_CHECKING:
from typing import Any


_HASH_SALT = "e6fdde87-adc6-42f6-8e66-4aabe4ba05f2"


Expand All @@ -21,7 +15,7 @@ def __getitem__(self, item: int) -> Series | Parallel | DepGroup | str:
def __setitem__(self, item: int, value: Series | Parallel | DepGroup | str) -> None:
self.root[item] = value

def __eq__(self, other: Any):
def __eq__(self, other: object):
if not isinstance(other, Series):
return False
return self.root == other.root
Expand All @@ -37,7 +31,7 @@ def __hash__(self):
def __or__(self, other: Parallel) -> Parallel:
return Parallel(self.root | other.root)

def __eq__(self, other: Any):
def __eq__(self, other: object):
if not isinstance(other, Parallel):
return False

Expand Down
10 changes: 5 additions & 5 deletions src/usethis/_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import socket
from contextlib import contextmanager
from pathlib import Path
from typing import IO, TYPE_CHECKING, Any
from typing import IO, TYPE_CHECKING

from typer.testing import CliRunner as TyperCliRunner # noqa: TID251

Expand Down Expand Up @@ -47,10 +47,10 @@ def invoke_safe(
self,
app: Typer,
args: str | Sequence[str] | None = None,
input: bytes | str | IO[Any] | None = None, # noqa: A002
input: bytes | str | IO[str] | None = None, # noqa: A002
env: Mapping[str, str] | None = None,
color: bool = False,
**extra: Any,
**extra: object,
) -> Result:
return self.invoke(
app,
Expand All @@ -66,11 +66,11 @@ def invoke( # noqa: PLR0913
self,
app: Typer,
args: str | Sequence[str] | None = None,
input: bytes | str | IO[Any] | None = None, # noqa: A002
input: bytes | str | IO[str] | None = None, # noqa: A002
env: Mapping[str, str] | None = None,
catch_exceptions: bool = True,
color: bool = False,
**extra: Any,
**extra: object,
) -> Result:
if catch_exceptions:
msg = "`catch_exceptions=True` is forbidden in usethis tests. Use `.invoke_safe()` instead."
Expand Down
8 changes: 5 additions & 3 deletions src/usethis/_tool/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
)

if TYPE_CHECKING:
from collections.abc import Sequence

from usethis._io import KeyValueFileManager
from usethis._tool.config import ConfigItem
from usethis._tool.rule import Rule
Expand Down Expand Up @@ -549,7 +551,7 @@ def _get_select_keys(self, file_manager: KeyValueFileManager[object]) -> list[st
)
raise UnhandledConfigEntryError(msg)

def select_rules(self, rules: list[Rule]) -> bool:
def select_rules(self, rules: Sequence[Rule]) -> bool:
"""Select the rules managed by the tool.

These rules are not validated; it is assumed they are valid rules for the tool,
Expand Down Expand Up @@ -598,7 +600,7 @@ def _get_ignore_keys(self, file_manager: KeyValueFileManager[object]) -> list[st
)
raise UnhandledConfigEntryError(msg)

def ignore_rules(self, rules: list[Rule]) -> bool:
def ignore_rules(self, rules: Sequence[Rule]) -> bool:
"""Ignore rules managed by the tool.

Ignoring a rule is different from deselecting it - it means that even if it
Expand Down Expand Up @@ -664,7 +666,7 @@ def unignore_rules(self, rules: list[str]) -> bool:

return True

def deselect_rules(self, rules: list[Rule]) -> bool:
def deselect_rules(self, rules: Sequence[Rule]) -> bool:
"""Deselect the rules managed by the tool.

Any rules that aren't already selected are ignored.
Expand Down
Loading
Loading