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
15 changes: 7 additions & 8 deletions benedict/core/items_sorted.py
Original file line number Diff line number Diff line change
@@ -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)
26 changes: 19 additions & 7 deletions benedict/dicts/io/io_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@
import boto3

s3_installed = True
except ModuleNotFoundError:
except ModuleNotFoundError: # pragma: no cover
s3_installed = False

import fsutil
try:
import fsutil

fsutil_installed = True
except ModuleNotFoundError: # pragma: no cover
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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Comment thread
fabiocaccamo marked this conversation as resolved.


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()
Expand All @@ -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:
Expand All @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion benedict/dicts/parse/parse_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from phonenumbers import PhoneNumberFormat, phonenumberutil

parse_installed = True
except ModuleNotFoundError:
except ModuleNotFoundError: # pragma: no cover
parse_installed = False


Expand Down
5 changes: 5 additions & 0 deletions benedict/extras.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from benedict.exceptions import ExtrasRequireModuleNotFoundError

__all__ = [
"require_fsutil",
"require_html",
"require_parse",
"require_s3",
Expand All @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion benedict/serializers/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion benedict/serializers/toml.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import toml

toml_installed = True
except ModuleNotFoundError:
except ModuleNotFoundError: # pragma: no cover
toml_installed = False

try:
Expand Down
12 changes: 9 additions & 3 deletions benedict/serializers/xls.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
from __future__ import annotations

import fsutil
try:
import fsutil

fsutil_installed = True
except ModuleNotFoundError: # pragma: no cover
fsutil_installed = False

try:
from openpyxl import load_workbook
from xlrd import open_workbook

xls_installed = True
except ModuleNotFoundError:
except ModuleNotFoundError: # pragma: no cover
xls_installed = False

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

from slugify import slugify

from benedict.extras import require_xls
from benedict.extras import require_fsutil, require_xls
from benedict.serializers.abstract import AbstractSerializer


Expand Down Expand Up @@ -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)
Comment thread
fabiocaccamo marked this conversation as resolved.
if extension in ["xlsx", "xlsm"]:
return self._decode(s, **kwargs)
Expand Down
2 changes: 1 addition & 1 deletion benedict/serializers/xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import xmltodict

xml_installed = True
except ModuleNotFoundError:
except ModuleNotFoundError: # pragma: no cover
xml_installed = False


Expand Down
2 changes: 1 addition & 1 deletion benedict/serializers/yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from yaml.representer import SafeRepresenter

yaml_installed = True
except ModuleNotFoundError:
except ModuleNotFoundError: # pragma: no cover
yaml_installed = False


Expand Down
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,8 @@ 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",
"useful-types >= 0.2.1, < 0.3.0"
]
dynamic = ["version"]
maintainers = [
Expand Down Expand Up @@ -126,6 +123,8 @@ html = [
"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 = [
Expand All @@ -136,12 +135,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 = [
Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
19 changes: 19 additions & 0 deletions tests/dicts/io/test_io_dict_xls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
28 changes: 28 additions & 0 deletions tests/dicts/io/test_io_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading