Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added cli_serializer & from_cli function #311

Merged
merged 3 commits into from
Jul 15, 2023
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
10 changes: 10 additions & 0 deletions benedict/dicts/io/io_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,16 @@ def from_yaml(cls, s, **kwargs):
"""
return cls(s, format="yaml", **kwargs)

@classmethod
def from_cli(cls, s, **kwargs):
"""
Load and decode data from a string of CLI arguments.
ArgumentParser specific options can be passed using kwargs:
https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser
Return a new dict instance. A ValueError is raised in case of failure.
"""
return cls(s, format="cli", **kwargs)

def to_base64(self, subformat="json", encoding="utf-8", **kwargs):
"""
Encode the current dict instance in Base64 format
Expand Down
4 changes: 4 additions & 0 deletions benedict/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from benedict.serializers.abstract import AbstractSerializer
from benedict.serializers.base64 import Base64Serializer
from benedict.serializers.cli import CLISerializer
from benedict.serializers.csv import CSVSerializer
from benedict.serializers.ini import INISerializer
from benedict.serializers.json import JSONSerializer
Expand All @@ -16,6 +17,7 @@
__all__ = [
"AbstractSerializer",
"Base64Serializer",
"CLISerializer",
"CSVSerializer",
"INISerializer",
"JSONSerializer",
Expand All @@ -29,6 +31,7 @@
]

_BASE64_SERIALIZER = Base64Serializer()
_CLI_SERIALIZER = CLISerializer()
_CSV_SERIALIZER = CSVSerializer()
_INI_SERIALIZER = INISerializer()
_JSON_SERIALIZER = JSONSerializer()
Expand All @@ -42,6 +45,7 @@

_SERIALIZERS_LIST = [
_BASE64_SERIALIZER,
_CLI_SERIALIZER,
_CSV_SERIALIZER,
_INI_SERIALIZER,
_JSON_SERIALIZER,
Expand Down
104 changes: 104 additions & 0 deletions benedict/serializers/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from argparse import ArgumentError, ArgumentParser
from collections import Counter
from re import finditer

from benedict.serializers.abstract import AbstractSerializer
from benedict.utils import type_util


class CLISerializer(AbstractSerializer):
"""
This class describes a CLI serializer.
"""

regex_keys_with_values = r"-+\w+(?=\s[^\s-])"
"""
Regex string.
Used to search for keys (e.g. -STRING or --STRING)
that *aren't* followed by another key

Example input: script.py --username example --verbose -d -e email@example.com
- Matches: --username, -e
- Doesn't match: script.py, example, --verbose, -d, email@example.com
"""

regex_all_keys = r"-+\w+"
"""
Regex string.
Used to search for keys (e.g. -STRING or --STRING)
no matter if they are followed by another key

Example input: script.py --username example --verbose -d -e email@example.com
- Matches: --username, --verbose, -d, -e
- Doesn't match: script.py, example, email@example.com
"""

def __init__(self):
super().__init__(
extensions=["cli"],
)

@staticmethod
def parse_keys(regex, string):
# For some reason findall didn't work
results = [match.group(0) for match in finditer(regex, string)]
return results

"""Helper method, returns a list of --keys based on the regex used"""

@staticmethod
def _get_parser(options):
parser = ArgumentParser(**options)
return parser

def decode(self, s=None, **kwargs):
parser = self._get_parser(options=kwargs)

keys_with_values = set(self.parse_keys(self.regex_keys_with_values, s))
all_keys = Counter(self.parse_keys(self.regex_all_keys, s))
for key in all_keys:
count = all_keys[key]

try:
# If the key has a value...
if key in keys_with_values:
# and is defined once, collect the values
if count == 1:
parser.add_argument(
key,
nargs="*",
# This puts multiple values in a list
# even though this won't always be wanted
# This is adressed after the dict is generated
required=False,
)
# and is defined multiple times, collect the values
else:
parser.add_argument(key, action="append", required=False)

# If the key doesn't have a value...
else:
# and is defined once, store as bool
if count <= 1:
parser.add_argument(key, action="store_true", required=False)
# and is defined multiple times, count how many times
else:
parser.add_argument(key, action="count", required=False)

except ArgumentError as error:
raise ValueError from error

try:
args = parser.parse_args(s.split())
except BaseException as error:
raise ValueError from error

dict = vars(args)
for key in dict:
value = dict[key]
# If only one value was written,
# return that value instead of a list
if type_util.is_list(value) and len(value) == 1:
dict[key] = value[0]

return dict
47 changes: 47 additions & 0 deletions tests/dicts/io/test_io_dict_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from benedict.dicts.io import IODict

from .test_io_dict import io_dict_test_case


class io_dict_cli_test_case(io_dict_test_case):
"""
This class describes an IODict / cli test case.
"""

def test_from_cli_with_valid_data(self):
s = """--url "https://github.com" --usernames another handle --languages Python --languages JavaScript -v --count --count --count"""
# static method
r = {
"url": '"https://github.com"',
"usernames": ["another", "handle"],
"languages": ["Python", "JavaScript"],
"v": True,
"count": 3,
}

d = IODict.from_cli(s)
self.assertTrue(isinstance(d, dict))
self.assertEqual(d, r)
# constructor
d = IODict(s, format="cli")
self.assertTrue(isinstance(d, dict))
self.assertEqual(d, r)

def test_from_cli_with_invalid_arguments(self):
s = """--help -h"""

# static method
with self.assertRaises(ValueError):
IODict.from_cli(s)
# constructor
with self.assertRaises(ValueError):
IODict(s, format="cli")

def test_from_cli_with_invalid_data(self):
s = "Lorem ipsum est in ea occaecat nisi officia."
# static method
with self.assertRaises(ValueError):
IODict.from_cli(s)
# constructor
with self.assertRaises(ValueError):
IODict(s, format="cli")