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
24 changes: 23 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,35 @@ repos:
- id: pyupgrade
args: ["--py310-plus"]

- repo: https://github.com/frostming/fix-future-annotations
rev: 0.5.0
hooks:
- id: fix-future-annotations

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.12
rev: v0.13.2
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.18.2
hooks:
- id: mypy
args: [--config-file=pyproject.toml]
additional_dependencies:
- boto3-stubs[essential,lambda]==1.40.41
- decouple-types==1.0.2
- types-PyYAML==6.0.12.20250915
- types-beautifulsoup4==4.12.0.20250516
- types-html5lib==1.1.11.20250917
- types-openpyxl==3.1.5.20250919
- types-python-dateutil==2.9.0.20250822
- types-toml==0.10.8.20240310
- types-xmltodict==1.0.1.20250920
exclude: "tests"

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
Expand Down
1 change: 1 addition & 0 deletions bandit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
skips: ['B101']
38 changes: 28 additions & 10 deletions benedict/core/clean.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
def _clean_dict(d, strings, collections):
from __future__ import annotations

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

_K = TypeVar("_K")
_V = TypeVar("_V")
_T = TypeVar("_T")


def _clean_dict(
d: MutableMapping[_K, _V], strings: bool, collections: bool
) -> MutableMapping[_K, _V]:
keys = list(d.keys())
for key in keys:
d[key] = _clean_value(d[key], strings=strings, collections=collections)
Expand All @@ -7,41 +19,47 @@ def _clean_dict(d, strings, collections):
return d


def _clean_list(ls, strings, collections):
def _clean_list(
ls: MutableSequence[_T], strings: bool, collections: bool
) -> MutableSequence[_T]:
for i in range(len(ls) - 1, -1, -1):
ls[i] = _clean_value(ls[i], strings=strings, collections=collections)
if ls[i] is None:
ls.pop(i)
return ls


def _clean_set(values, strings, collections):
def _clean_set(values: set[_T], strings: bool, collections: bool) -> set[_T]:
return {
value
for value in values
if _clean_value(value, strings=strings, collections=collections) is not None
}


def _clean_str(s, strings, collections):
def _clean_str(s: str, strings: bool, collections: bool) -> str | None:
return s if s and s.strip() else None


def _clean_tuple(values, strings, collections):
def _clean_tuple(
values: tuple[_T, ...], strings: bool, collections: bool
) -> tuple[_T, ...]:
return tuple(
value
for value in values
if _clean_value(value, strings=strings, collections=collections) is not None
)


def _clean_value(value, strings, collections):
def _clean_value(value: Any, strings: bool, collections: bool) -> Any:
if value is None:
return value
elif isinstance(value, list) and collections:
elif isinstance(value, MutableSequence) and collections:
value = _clean_list(value, strings=strings, collections=collections) or None
elif isinstance(value, dict) and collections:
value = _clean_dict(value, strings=strings, collections=collections) or None
elif isinstance(value, MutableMapping) and collections:
value = (
_clean_dict(dict(value), strings=strings, collections=collections) or None
)
elif isinstance(value, set) and collections:
value = _clean_set(value, strings=strings, collections=collections) or None
elif isinstance(value, str) and strings:
Expand All @@ -51,5 +69,5 @@ def _clean_value(value, strings, collections):
return value


def clean(d, strings=True, collections=True):
def clean(d: Any, strings: bool = True, collections: bool = True) -> Any:
return _clean_dict(d, strings=strings, collections=collections)
14 changes: 12 additions & 2 deletions benedict/core/clone.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
from __future__ import annotations

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

_T = TypeVar("_T")


def clone(obj, empty=False, memo=None):
def clone(
obj: _T,
empty: bool = False,
memo: dict[int, Any] | None = None,
) -> _T:
d = copy.deepcopy(obj, memo)
if empty:
if empty and isinstance(d, MutableMapping):
d.clear()
return d
4 changes: 3 additions & 1 deletion benedict/core/dump.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from typing import Any

from benedict.serializers import JSONSerializer


def dump(obj, **kwargs):
def dump(obj: Any, **kwargs: Any) -> str:
serializer = JSONSerializer()
options = {"indent": 4, "sort_keys": True}
options.update(**kwargs)
Expand Down
14 changes: 11 additions & 3 deletions benedict/core/filter.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
from benedict.core import clone
from collections.abc import Callable, MutableMapping
from typing import TypeVar

from benedict.core.clone import clone

def filter(d, predicate):
_K = TypeVar("_K")
_V = TypeVar("_V")


def filter(
d: MutableMapping[_K, _V], predicate: Callable[[_K, _V], bool]
) -> MutableMapping[_K, _V]:
if not callable(predicate):
raise ValueError("predicate argument must be a callable.")
new_dict = clone(d, empty=True)
keys = list(d.keys())
for key in keys:
value = d.get(key, None)
value = d[key]
if predicate(key, value):
new_dict[key] = value
return new_dict
15 changes: 13 additions & 2 deletions benedict/core/find.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
def find(d, keys, default=None):
from __future__ import annotations

from collections.abc import Iterable, Mapping
from typing import TypeVar

_K = TypeVar("_K")
_V = TypeVar("_V")


def find(
d: Mapping[_K, _V], keys: Iterable[_K], default: _V | None = None
) -> _V | None:
for key in keys:
if key in d:
return d.get(key, default)
return d[key]
return default
19 changes: 13 additions & 6 deletions benedict/core/flatten.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
from benedict.core import clone
from benedict.utils import type_util
from collections.abc import Mapping
from typing import Any

from benedict.core.clone import clone

def _flatten_key(base_key, key, separator):

def _flatten_key(base_key: str, key: str, separator: str) -> str:
if base_key and separator:
return f"{base_key}{separator}{key}"
return key


def _flatten_item(d, base_dict, base_key, separator):
def _flatten_item(
d: Any,
base_dict: Any,
base_key: str,
separator: str,
) -> Any:
new_dict = base_dict
keys = list(d.keys())
for key in keys:
new_key = _flatten_key(base_key, key, separator)
value = d.get(key, None)
if type_util.is_dict(value):
if isinstance(value, Mapping):
new_value = _flatten_item(
value, base_dict=new_dict, base_key=new_key, separator=separator
)
Expand All @@ -26,6 +33,6 @@ def _flatten_item(d, base_dict, base_key, separator):
return new_dict


def flatten(d, separator="_"):
def flatten(d: Any, separator: str = "_") -> Any:
new_dict = clone(d, empty=True)
return _flatten_item(d, base_dict=new_dict, base_key="", separator=separator)
12 changes: 10 additions & 2 deletions benedict/core/groupby.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
from __future__ import annotations

from collections.abc import Mapping, MutableSequence, Sequence
from typing import Any, TypeVar

from benedict.utils import type_util

_K = TypeVar("_K")
_V = TypeVar("_V", bound=MutableSequence[Any])


def groupby(items, key):
def groupby(items: Sequence[Mapping[_K, Any]], key: _K) -> dict[Any, Any]:
if not type_util.is_list(items):
raise ValueError("items should be a list of dicts.")
items_grouped = {}
items_grouped: dict[Any, Any] = {}
for item in items:
if not type_util.is_dict(item):
raise ValueError("item should be a dict.")
Expand Down
16 changes: 12 additions & 4 deletions benedict/core/invert.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
from benedict.core import clone
from collections.abc import MutableMapping, Sequence
from typing import Any, TypeVar

from benedict.core.clone import clone
from benedict.utils import type_util

_K = TypeVar("_K")
_V = TypeVar("_V")


def _invert_item(d, key, value, flat):
def _invert_item(d: MutableMapping[Any, Any], key: _K, value: _V, flat: bool) -> None:
if flat:
d.setdefault(value, key)
else:
d.setdefault(value, []).append(key)


def _invert_list(d, key, value, flat):
def _invert_list(
d: MutableMapping[Any, Any], key: _K, value: Sequence[Any], flat: bool
) -> None:
for value_item in value:
_invert_item(d, key, value_item, flat)


def invert(d, flat=False):
def invert(d: MutableMapping[_K, _V], flat: bool = False) -> MutableMapping[Any, Any]:
new_dict = clone(d, empty=True)
for key, value in d.items():
if type_util.is_list_or_tuple(value):
Expand Down
21 changes: 18 additions & 3 deletions benedict/core/items_sorted.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
def _items_sorted_by_item_at_index(d, index, reverse):
from __future__ import annotations

from collections.abc import Mapping

from useful_types import SupportsRichComparisonT


def _items_sorted_by_item_at_index(
d: Mapping[SupportsRichComparisonT, SupportsRichComparisonT],
index: int,
reverse: bool,
) -> list[tuple[SupportsRichComparisonT, SupportsRichComparisonT]]:
return sorted(d.items(), key=lambda item: item[index], reverse=reverse)


def items_sorted_by_keys(d, reverse=False):
def items_sorted_by_keys(
d: Mapping[SupportsRichComparisonT, SupportsRichComparisonT], reverse: bool = False
) -> list[tuple[SupportsRichComparisonT, SupportsRichComparisonT]]:
return _items_sorted_by_item_at_index(d, 0, reverse)


def items_sorted_by_values(d, reverse=False):
def items_sorted_by_values(
d: Mapping[SupportsRichComparisonT, SupportsRichComparisonT], reverse: bool = False
) -> list[tuple[SupportsRichComparisonT, SupportsRichComparisonT]]:
return _items_sorted_by_item_at_index(d, 1, reverse)
21 changes: 17 additions & 4 deletions benedict/core/keylists.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
from __future__ import annotations

from collections.abc import Mapping, Sequence
from typing import Any

from benedict.utils import type_util


def _get_keylist_for_dict(d, parent_keys, indexes):
def _get_keylist_for_dict(
d: Mapping[Any, Any], parent_keys: list[Any], indexes: bool
) -> list[list[Any]]:
keylist = []
for key, value in d.items():
keys = parent_keys + [key]
Expand All @@ -10,7 +17,9 @@ def _get_keylist_for_dict(d, parent_keys, indexes):
return keylist


def _get_keylist_for_list(ls, parent_keys, indexes):
def _get_keylist_for_list(
ls: Sequence[Any], parent_keys: list[Any], indexes: bool
) -> list[list[Any]]:
keylist = []
for key, value in enumerate(ls):
keys = list(parent_keys)
Expand All @@ -20,13 +29,17 @@ def _get_keylist_for_list(ls, parent_keys, indexes):
return keylist


def _get_keylist_for_value(value, parent_keys, indexes):
def _get_keylist_for_value(
value: Mapping[Any, Any] | Sequence[Any], parent_keys: list[Any], indexes: bool
) -> list[list[Any]]:
if type_util.is_dict(value):
return _get_keylist_for_dict(value, parent_keys, indexes)
elif type_util.is_list(value) and indexes:
return _get_keylist_for_list(value, parent_keys, indexes)
return []


def keylists(d, indexes=False):
def keylists(
d: Mapping[Any, Any] | Sequence[Any], indexes: bool = False
) -> list[list[Any]]:
return _get_keylist_for_value(d, [], indexes)
12 changes: 11 additions & 1 deletion benedict/core/keypaths.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
from __future__ import annotations

from collections.abc import Mapping
from typing import Any

from benedict.core.keylists import keylists
from benedict.utils import type_util


def keypaths(d, separator=".", indexes=False, sort=True):
def keypaths(
d: Mapping[Any, Any],
separator: str | None = ".",
indexes: bool = False,
sort: bool = True,
) -> list[str]:
separator = separator or "."
if not type_util.is_string(separator):
raise ValueError("separator argument must be a (non-empty) string.")
Expand Down
Loading