From 1148f3b688b898e63f703ff62fa8068cc8e44473 Mon Sep 17 00:00:00 2001 From: Fabio Caccamo Date: Mon, 27 Apr 2026 23:13:47 +0200 Subject: [PATCH 1/4] Remove `useful-types` dependency. --- benedict/core/items_sorted.py | 15 +++++++-------- pyproject.toml | 1 - requirements.txt | 1 - 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/benedict/core/items_sorted.py b/benedict/core/items_sorted.py index cc587aaf..2096bd16 100644 --- a/benedict/core/items_sorted.py +++ b/benedict/core/items_sorted.py @@ -1,25 +1,24 @@ from __future__ import annotations from collections.abc import Mapping - -from useful_types import SupportsRichComparisonT +from typing import Any def _items_sorted_by_item_at_index( - d: Mapping[SupportsRichComparisonT, SupportsRichComparisonT], + d: Mapping[Any, Any], index: int, reverse: bool, -) -> list[tuple[SupportsRichComparisonT, SupportsRichComparisonT]]: +) -> list[tuple[Any, Any]]: return sorted(d.items(), key=lambda item: item[index], reverse=reverse) def items_sorted_by_keys( - d: Mapping[SupportsRichComparisonT, SupportsRichComparisonT], reverse: bool = False -) -> list[tuple[SupportsRichComparisonT, SupportsRichComparisonT]]: + d: Mapping[Any, Any], reverse: bool = False +) -> list[tuple[Any, Any]]: return _items_sorted_by_item_at_index(d, 0, reverse) def items_sorted_by_values( - d: Mapping[SupportsRichComparisonT, SupportsRichComparisonT], reverse: bool = False -) -> list[tuple[SupportsRichComparisonT, SupportsRichComparisonT]]: + d: Mapping[Any, Any], reverse: bool = False +) -> list[tuple[Any, Any]]: return _items_sorted_by_item_at_index(d, 1, reverse) diff --git a/pyproject.toml b/pyproject.toml index c3676489..ac365e5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,6 @@ dependencies = [ "python-slugify >= 7.0.0, < 9.0.0", "requests >= 2.33.0, < 3.0.0", "typing_extensions >= 4.13.2, < 4.16.0", - "useful-types >= 0.2.1, < 0.3.0" ] dynamic = ["version"] maintainers = [ diff --git a/requirements.txt b/requirements.txt index ad5f0d21..c80163e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,5 @@ requests == 2.33.1 toml == 0.10.2 typing_extensions >= 4.14.1 urllib3 >= 2.6.3 -useful-types == 0.2.1 xlrd == 2.0.2 xmltodict == 1.0.4 From 558530c0df99fde7dc70bd5479a4a9c1d3b4147e Mon Sep 17 00:00:00 2001 From: Fabio Caccamo Date: Mon, 27 Apr 2026 23:18:28 +0200 Subject: [PATCH 2/4] Move `python-fsutil` and `requests` to `[io]` optional dependencies. --- benedict/dicts/io/io_util.py | 24 ++++++++++++++++++------ benedict/extras.py | 5 +++++ benedict/serializers/xls.py | 10 ++++++++-- pyproject.toml | 10 ++++------ 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/benedict/dicts/io/io_util.py b/benedict/dicts/io/io_util.py index 581c99f7..b01b4715 100644 --- a/benedict/dicts/io/io_util.py +++ b/benedict/dicts/io/io_util.py @@ -15,9 +15,14 @@ except ModuleNotFoundError: s3_installed = False -import fsutil +try: + import fsutil + + fsutil_installed = True +except ModuleNotFoundError: + fsutil_installed = False -from benedict.extras import require_s3 +from benedict.extras import require_fsutil, require_s3 from benedict.serializers import ( get_format_by_path, get_serializer_by_format, @@ -88,7 +93,7 @@ def is_data(s: str | bytes) -> bool: def is_filepath(s: Path | str) -> bool: - if fsutil.is_file(s): + if fsutil_installed and fsutil.is_file(s): return True return bool( get_format_by_path(s) @@ -147,15 +152,18 @@ def read_content( def read_content_from_file(filepath: str, format: str | None = None) -> str: + require_fsutil(installed=fsutil_installed) binary_format = is_binary_format(format) if binary_format: return filepath - return fsutil.read_file(filepath) # type: ignore[no-any-return] + content = fsutil.read_file(filepath) + return str(content) def read_content_from_s3( url: str, s3_options: Mapping[str, Any], format: str | None = None ) -> str: + require_fsutil(installed=fsutil_installed) require_s3(installed=s3_installed) s3_url = parse_s3_url(url) dirpath = tempfile.gettempdir() @@ -171,12 +179,14 @@ def read_content_from_s3( def read_content_from_url( url: str, requests_options: Mapping[str, Any], format: str | None = None ) -> str: + require_fsutil(installed=fsutil_installed) binary_format = is_binary_format(format) if binary_format: dirpath = tempfile.gettempdir() filepath = fsutil.download_file(url, dirpath=dirpath, **requests_options) - return filepath # type: ignore[no-any-return] - return fsutil.read_file_from_url(url, **requests_options) # type: ignore[no-any-return] + return str(filepath) + content = fsutil.read_file_from_url(url, **requests_options) + return str(content) def write_content(filepath: str, content: str, **options: Any) -> None: @@ -187,12 +197,14 @@ def write_content(filepath: str, content: str, **options: Any) -> None: def write_content_to_file(filepath: str, content: str, **options: Any) -> None: + require_fsutil(installed=fsutil_installed) fsutil.write_file(filepath, content) def write_content_to_s3( url: str, content: str, s3_options: Mapping[str, Any], **options: Any ) -> None: + require_fsutil(installed=fsutil_installed) require_s3(installed=s3_installed) s3_url = parse_s3_url(url) dirpath = tempfile.gettempdir() diff --git a/benedict/extras.py b/benedict/extras.py index df087aca..f69c3eb3 100644 --- a/benedict/extras.py +++ b/benedict/extras.py @@ -1,6 +1,7 @@ from benedict.exceptions import ExtrasRequireModuleNotFoundError __all__ = [ + "require_fsutil", "require_html", "require_parse", "require_s3", @@ -20,6 +21,10 @@ def require_html(*, installed: bool) -> None: _require_optional_dependencies(target="html", installed=installed) +def require_fsutil(*, installed: bool) -> None: + _require_optional_dependencies(target="io", installed=installed) + + def require_parse(*, installed: bool) -> None: _require_optional_dependencies(target="parse", installed=installed) diff --git a/benedict/serializers/xls.py b/benedict/serializers/xls.py index 748ddfda..cf59c557 100644 --- a/benedict/serializers/xls.py +++ b/benedict/serializers/xls.py @@ -1,6 +1,11 @@ from __future__ import annotations -import fsutil +try: + import fsutil + + fsutil_installed = True +except ModuleNotFoundError: + fsutil_installed = False try: from openpyxl import load_workbook @@ -15,7 +20,7 @@ from slugify import slugify -from benedict.extras import require_xls +from benedict.extras import require_fsutil, require_xls from benedict.serializers.abstract import AbstractSerializer @@ -175,6 +180,7 @@ def _decode(self, s: str, **kwargs: Any) -> list[dict[str, Any]]: def decode(self, s: str, **kwargs: Any) -> list[dict[str, Any]]: require_xls(installed=xls_installed) + require_fsutil(installed=fsutil_installed) extension = fsutil.get_file_extension(s) if extension in ["xlsx", "xlsm"]: return self._decode(s, **kwargs) diff --git a/pyproject.toml b/pyproject.toml index ac365e5f..e6db462a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,9 +90,7 @@ classifiers = [ "Typing :: Typed", ] dependencies = [ - "python-fsutil >= 0.16.0, < 1.0.0", "python-slugify >= 7.0.0, < 9.0.0", - "requests >= 2.33.0, < 3.0.0", "typing_extensions >= 4.13.2, < 4.16.0", ] dynamic = ["version"] @@ -120,11 +118,9 @@ Twitter = "https://twitter.com/fabiocaccamo" all = [ "python-benedict[io,parse,s3]", ] -html = [ - "beautifulsoup4 >= 4.12.0, < 5.0.0", - "python-benedict[xml]", -] io = [ + "python-fsutil >= 0.16.1, < 1.0.0", + "requests >= 2.33.0, < 3.0.0", "python-benedict[html,toml,xls,xml,yaml]", ] parse = [ @@ -135,12 +131,14 @@ parse = [ ] s3 = [ "boto3 >= 1.24.89, < 2.0.0", + "python-fsutil >= 0.16.1, < 1.0.0", ] toml = [ "toml >= 0.10.2, < 1.0.0", ] xls = [ "openpyxl >= 3.0.0, < 4.0.0", + "python-fsutil >= 0.16.1, < 1.0.0", "xlrd >= 2.0.0, < 3.0.0", ] xml = [ From eb8459f6dc2ab101668c475a5fe8ee936744b997 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:50:13 +0000 Subject: [PATCH 3/4] Fix html extra, add fsutil_installed=False tests for io_util and xls serializer Agent-Logs-Url: https://github.com/fabiocaccamo/python-benedict/sessions/841a9dfb-c101-4f41-b8d3-552ef909284b Co-authored-by: fabiocaccamo <1035294+fabiocaccamo@users.noreply.github.com> --- pyproject.toml | 4 ++++ tests/dicts/io/test_io_dict_xls.py | 19 +++++++++++++++++++ tests/dicts/io/test_io_util.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index e6db462a..7cc48cca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -118,6 +118,10 @@ Twitter = "https://twitter.com/fabiocaccamo" all = [ "python-benedict[io,parse,s3]", ] +html = [ + "beautifulsoup4 >= 4.12.0, < 5.0.0", + "python-benedict[xml]", +] io = [ "python-fsutil >= 0.16.1, < 1.0.0", "requests >= 2.33.0, < 3.0.0", diff --git a/tests/dicts/io/test_io_dict_xls.py b/tests/dicts/io/test_io_dict_xls.py index 780adaa9..9dc2baa4 100644 --- a/tests/dicts/io/test_io_dict_xls.py +++ b/tests/dicts/io/test_io_dict_xls.py @@ -91,6 +91,25 @@ def test_from_xls_with_valid_file_valid_content_but_xls_extra_not_installed( with self.assertRaises(ExtrasRequireModuleNotFoundError): _ = IODict(filepath) + @patch("benedict.serializers.xls.fsutil_installed", False) + def test_from_xls_with_valid_file_valid_content_but_io_extra_not_installed( + self, + ) -> None: + for extension in self._extensions: + with self.subTest( + msg=f"test_from_xls_({extension})_with_valid_file_valid_content_but_io_extra_not_installed" + ): + filepath = self.input_path(f"valid-content.{extension}") + # static method + with self.assertRaises(ExtrasRequireModuleNotFoundError): + _ = IODict.from_xls(filepath) + # constructor explicit format + with self.assertRaises(ExtrasRequireModuleNotFoundError): + _ = IODict(filepath, format=extension) + # constructor implicit format + with self.assertRaises(ExtrasRequireModuleNotFoundError): + _ = IODict(filepath) + def test_from_xls_with_valid_url_valid_content(self) -> None: expected_dict = { "values": [ diff --git a/tests/dicts/io/test_io_util.py b/tests/dicts/io/test_io_util.py index 61cb1b1f..62c331dd 100644 --- a/tests/dicts/io/test_io_util.py +++ b/tests/dicts/io/test_io_util.py @@ -136,6 +136,34 @@ def test_parse_s3_url_with_special_characters(self) -> None: } self.assertEqual(result, expected_result) + @patch("benedict.dicts.io.io_util.fsutil_installed", False) + def test_read_content_from_file_with_io_extra_not_installed(self) -> None: + filepath = "/tmp/test-file.json" + with self.assertRaises(ExtrasRequireModuleNotFoundError): + io_util.read_content_from_file(filepath, format="json") + + @patch("benedict.dicts.io.io_util.fsutil_installed", False) + def test_read_content_from_url_with_io_extra_not_installed(self) -> None: + url = "https://example.com/data.json" + with self.assertRaises(ExtrasRequireModuleNotFoundError): + io_util.read_content_from_url(url, {}, format="json") + + @patch("benedict.dicts.io.io_util.fsutil_installed", False) + def test_read_content_from_s3_with_io_extra_not_installed(self) -> None: + s3_options = { + "aws_access_key_id": "", + "aws_secret_access_key": "", + } + s3_url = "s3://my-bucket/my-key.txt" + with self.assertRaises(ExtrasRequireModuleNotFoundError): + io_util.read_content_from_s3(s3_url, s3_options) + + @patch("benedict.dicts.io.io_util.fsutil_installed", False) + def test_write_content_to_file_with_io_extra_not_installed(self) -> None: + filepath = "/tmp/test-file.json" + with self.assertRaises(ExtrasRequireModuleNotFoundError): + io_util.write_content_to_file(filepath, '{"a": 1}') + @patch("benedict.dicts.io.io_util.s3_installed", False) def test_read_content_from_s3_with_s3_extra_not_installed(self) -> None: s3_options = { From bbb81f842c6c45e26cb676a9d4a5cb2944813503 Mon Sep 17 00:00:00 2001 From: Fabio Caccamo Date: Tue, 28 Apr 2026 00:08:32 +0200 Subject: [PATCH 4/4] Add "pragma: no cover" to unreachable except blocks. --- benedict/dicts/io/io_util.py | 4 ++-- benedict/dicts/parse/parse_util.py | 2 +- benedict/serializers/html.py | 2 +- benedict/serializers/toml.py | 2 +- benedict/serializers/xls.py | 4 ++-- benedict/serializers/xml.py | 2 +- benedict/serializers/yaml.py | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/benedict/dicts/io/io_util.py b/benedict/dicts/io/io_util.py index b01b4715..5ec10a23 100644 --- a/benedict/dicts/io/io_util.py +++ b/benedict/dicts/io/io_util.py @@ -12,14 +12,14 @@ import boto3 s3_installed = True -except ModuleNotFoundError: +except ModuleNotFoundError: # pragma: no cover s3_installed = False try: import fsutil fsutil_installed = True -except ModuleNotFoundError: +except ModuleNotFoundError: # pragma: no cover fsutil_installed = False from benedict.extras import require_fsutil, require_s3 diff --git a/benedict/dicts/parse/parse_util.py b/benedict/dicts/parse/parse_util.py index 766a9100..fc993bdc 100644 --- a/benedict/dicts/parse/parse_util.py +++ b/benedict/dicts/parse/parse_util.py @@ -12,7 +12,7 @@ from phonenumbers import PhoneNumberFormat, phonenumberutil parse_installed = True -except ModuleNotFoundError: +except ModuleNotFoundError: # pragma: no cover parse_installed = False diff --git a/benedict/serializers/html.py b/benedict/serializers/html.py index 3c5e7d73..ebdb30b5 100644 --- a/benedict/serializers/html.py +++ b/benedict/serializers/html.py @@ -4,7 +4,7 @@ from bs4 import BeautifulSoup html_installed = True -except ModuleNotFoundError: +except ModuleNotFoundError: # pragma: no cover html_installed = False from typing import Any, NoReturn diff --git a/benedict/serializers/toml.py b/benedict/serializers/toml.py index b7346234..fc3da779 100644 --- a/benedict/serializers/toml.py +++ b/benedict/serializers/toml.py @@ -2,7 +2,7 @@ import toml toml_installed = True -except ModuleNotFoundError: +except ModuleNotFoundError: # pragma: no cover toml_installed = False try: diff --git a/benedict/serializers/xls.py b/benedict/serializers/xls.py index cf59c557..4188dfb8 100644 --- a/benedict/serializers/xls.py +++ b/benedict/serializers/xls.py @@ -4,7 +4,7 @@ import fsutil fsutil_installed = True -except ModuleNotFoundError: +except ModuleNotFoundError: # pragma: no cover fsutil_installed = False try: @@ -12,7 +12,7 @@ from xlrd import open_workbook xls_installed = True -except ModuleNotFoundError: +except ModuleNotFoundError: # pragma: no cover xls_installed = False from collections.abc import Sequence diff --git a/benedict/serializers/xml.py b/benedict/serializers/xml.py index 54dc83f0..1b59b6cd 100644 --- a/benedict/serializers/xml.py +++ b/benedict/serializers/xml.py @@ -4,7 +4,7 @@ import xmltodict xml_installed = True -except ModuleNotFoundError: +except ModuleNotFoundError: # pragma: no cover xml_installed = False diff --git a/benedict/serializers/yaml.py b/benedict/serializers/yaml.py index 6d08363a..4363626c 100644 --- a/benedict/serializers/yaml.py +++ b/benedict/serializers/yaml.py @@ -6,7 +6,7 @@ from yaml.representer import SafeRepresenter yaml_installed = True -except ModuleNotFoundError: +except ModuleNotFoundError: # pragma: no cover yaml_installed = False