Skip to content

Commit

Permalink
Added cli_serializer & from_cli function (#311)
Browse files Browse the repository at this point in the history
* Added cli_serializer & from_cli function

* Removed cli serializer exit_on_error option

* Invalid arg test, static CLISerializer func & vars
  • Loading branch information
Denperidge authored Jul 15, 2023
1 parent 83e4853 commit 1feb11c
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 0 deletions.
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")

0 comments on commit 1feb11c

Please sign in to comment.