Skip to content

Commit 1feb11c

Browse files
authored
Added cli_serializer & from_cli function (#311)
* Added cli_serializer & from_cli function * Removed cli serializer exit_on_error option * Invalid arg test, static CLISerializer func & vars
1 parent 83e4853 commit 1feb11c

File tree

4 files changed

+165
-0
lines changed

4 files changed

+165
-0
lines changed

benedict/dicts/io/io_dict.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,16 @@ def from_yaml(cls, s, **kwargs):
173173
"""
174174
return cls(s, format="yaml", **kwargs)
175175

176+
@classmethod
177+
def from_cli(cls, s, **kwargs):
178+
"""
179+
Load and decode data from a string of CLI arguments.
180+
ArgumentParser specific options can be passed using kwargs:
181+
https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser
182+
Return a new dict instance. A ValueError is raised in case of failure.
183+
"""
184+
return cls(s, format="cli", **kwargs)
185+
176186
def to_base64(self, subformat="json", encoding="utf-8", **kwargs):
177187
"""
178188
Encode the current dict instance in Base64 format

benedict/serializers/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from benedict.serializers.abstract import AbstractSerializer
44
from benedict.serializers.base64 import Base64Serializer
5+
from benedict.serializers.cli import CLISerializer
56
from benedict.serializers.csv import CSVSerializer
67
from benedict.serializers.ini import INISerializer
78
from benedict.serializers.json import JSONSerializer
@@ -16,6 +17,7 @@
1617
__all__ = [
1718
"AbstractSerializer",
1819
"Base64Serializer",
20+
"CLISerializer",
1921
"CSVSerializer",
2022
"INISerializer",
2123
"JSONSerializer",
@@ -29,6 +31,7 @@
2931
]
3032

3133
_BASE64_SERIALIZER = Base64Serializer()
34+
_CLI_SERIALIZER = CLISerializer()
3235
_CSV_SERIALIZER = CSVSerializer()
3336
_INI_SERIALIZER = INISerializer()
3437
_JSON_SERIALIZER = JSONSerializer()
@@ -42,6 +45,7 @@
4245

4346
_SERIALIZERS_LIST = [
4447
_BASE64_SERIALIZER,
48+
_CLI_SERIALIZER,
4549
_CSV_SERIALIZER,
4650
_INI_SERIALIZER,
4751
_JSON_SERIALIZER,

benedict/serializers/cli.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
from argparse import ArgumentError, ArgumentParser
2+
from collections import Counter
3+
from re import finditer
4+
5+
from benedict.serializers.abstract import AbstractSerializer
6+
from benedict.utils import type_util
7+
8+
9+
class CLISerializer(AbstractSerializer):
10+
"""
11+
This class describes a CLI serializer.
12+
"""
13+
14+
regex_keys_with_values = r"-+\w+(?=\s[^\s-])"
15+
"""
16+
Regex string.
17+
Used to search for keys (e.g. -STRING or --STRING)
18+
that *aren't* followed by another key
19+
20+
Example input: script.py --username example --verbose -d -e [email protected]
21+
- Matches: --username, -e
22+
- Doesn't match: script.py, example, --verbose, -d, [email protected]
23+
"""
24+
25+
regex_all_keys = r"-+\w+"
26+
"""
27+
Regex string.
28+
Used to search for keys (e.g. -STRING or --STRING)
29+
no matter if they are followed by another key
30+
31+
Example input: script.py --username example --verbose -d -e [email protected]
32+
- Matches: --username, --verbose, -d, -e
33+
- Doesn't match: script.py, example, [email protected]
34+
"""
35+
36+
def __init__(self):
37+
super().__init__(
38+
extensions=["cli"],
39+
)
40+
41+
@staticmethod
42+
def parse_keys(regex, string):
43+
# For some reason findall didn't work
44+
results = [match.group(0) for match in finditer(regex, string)]
45+
return results
46+
47+
"""Helper method, returns a list of --keys based on the regex used"""
48+
49+
@staticmethod
50+
def _get_parser(options):
51+
parser = ArgumentParser(**options)
52+
return parser
53+
54+
def decode(self, s=None, **kwargs):
55+
parser = self._get_parser(options=kwargs)
56+
57+
keys_with_values = set(self.parse_keys(self.regex_keys_with_values, s))
58+
all_keys = Counter(self.parse_keys(self.regex_all_keys, s))
59+
for key in all_keys:
60+
count = all_keys[key]
61+
62+
try:
63+
# If the key has a value...
64+
if key in keys_with_values:
65+
# and is defined once, collect the values
66+
if count == 1:
67+
parser.add_argument(
68+
key,
69+
nargs="*",
70+
# This puts multiple values in a list
71+
# even though this won't always be wanted
72+
# This is adressed after the dict is generated
73+
required=False,
74+
)
75+
# and is defined multiple times, collect the values
76+
else:
77+
parser.add_argument(key, action="append", required=False)
78+
79+
# If the key doesn't have a value...
80+
else:
81+
# and is defined once, store as bool
82+
if count <= 1:
83+
parser.add_argument(key, action="store_true", required=False)
84+
# and is defined multiple times, count how many times
85+
else:
86+
parser.add_argument(key, action="count", required=False)
87+
88+
except ArgumentError as error:
89+
raise ValueError from error
90+
91+
try:
92+
args = parser.parse_args(s.split())
93+
except BaseException as error:
94+
raise ValueError from error
95+
96+
dict = vars(args)
97+
for key in dict:
98+
value = dict[key]
99+
# If only one value was written,
100+
# return that value instead of a list
101+
if type_util.is_list(value) and len(value) == 1:
102+
dict[key] = value[0]
103+
104+
return dict

tests/dicts/io/test_io_dict_cli.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from benedict.dicts.io import IODict
2+
3+
from .test_io_dict import io_dict_test_case
4+
5+
6+
class io_dict_cli_test_case(io_dict_test_case):
7+
"""
8+
This class describes an IODict / cli test case.
9+
"""
10+
11+
def test_from_cli_with_valid_data(self):
12+
s = """--url "https://github.com" --usernames another handle --languages Python --languages JavaScript -v --count --count --count"""
13+
# static method
14+
r = {
15+
"url": '"https://github.com"',
16+
"usernames": ["another", "handle"],
17+
"languages": ["Python", "JavaScript"],
18+
"v": True,
19+
"count": 3,
20+
}
21+
22+
d = IODict.from_cli(s)
23+
self.assertTrue(isinstance(d, dict))
24+
self.assertEqual(d, r)
25+
# constructor
26+
d = IODict(s, format="cli")
27+
self.assertTrue(isinstance(d, dict))
28+
self.assertEqual(d, r)
29+
30+
def test_from_cli_with_invalid_arguments(self):
31+
s = """--help -h"""
32+
33+
# static method
34+
with self.assertRaises(ValueError):
35+
IODict.from_cli(s)
36+
# constructor
37+
with self.assertRaises(ValueError):
38+
IODict(s, format="cli")
39+
40+
def test_from_cli_with_invalid_data(self):
41+
s = "Lorem ipsum est in ea occaecat nisi officia."
42+
# static method
43+
with self.assertRaises(ValueError):
44+
IODict.from_cli(s)
45+
# constructor
46+
with self.assertRaises(ValueError):
47+
IODict(s, format="cli")

0 commit comments

Comments
 (0)