Skip to content

Commit b67a9aa

Browse files
committed
Implement declarative argument definitions and logic to bundle values.
Expand the arguments module with the notions of a defined grouping of arguments (ArgumentBundleDefinition) and bundles of particular arguments (ArgumentBundle). Use this as the mechanism by which payloads are checked for validity. As part of declaring a bundle definition the expected positional arguments are declared so implement basic length checks that catch missing positional arguments which were required. Each argument itself is also tested aganst an optional validity function which can be specified at the point of definition.
1 parent e7a3852 commit b67a9aa

File tree

2 files changed

+116
-1
lines changed

2 files changed

+116
-1
lines changed

mig/shared/arguments.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,88 @@
11
import argparse
2+
from collections import OrderedDict
23

34
from mig.shared.compat import PY2
45

56
_EMPTY_DICT = {}
7+
_EMPTY_LIST = {}
68
_NO_DEFAULT = object()
79
_POSITIONAL_MARKER = '__args__'
810

911

12+
class ArgumentBundleDefinition:
13+
def __init__(self, name, positional=_EMPTY_LIST):
14+
self._definition_name = name
15+
self._expected_positions = 0
16+
self._item_checks = []
17+
self._item_names = []
18+
19+
if positional is not _EMPTY_LIST:
20+
self._define_positional(positional)
21+
22+
@property
23+
def _fields(self):
24+
return self._item_names
25+
26+
@property
27+
def _validators(self):
28+
return self._item_checks
29+
30+
def __call__(self, *args):
31+
return self._extract_and_bundle(args, extract_by='position')
32+
33+
def _define_positional(self, positional):
34+
for flag, name, validator_fn in positional:
35+
assert flag is None
36+
self._item_names.append(name)
37+
self._item_checks.append(validator_fn)
38+
self._expected_positions = len(positional)
39+
40+
def _extract_and_bundle(self, args, extract_by=None):
41+
if extract_by == 'position':
42+
actual_positions = len(args)
43+
if actual_positions < self._expected_positions:
44+
raise ValueError('Error: too few arguments given (expected %d got %d)' % (
45+
self._expected_positions, actual_positions))
46+
keys_to_bundle = list(range(actual_positions))
47+
elif extract_by == 'name':
48+
keys_to_bundle = self._item_names
49+
elif extract_by == 'short':
50+
keys_to_bundle = self._item_short
51+
else:
52+
raise RuntimeError()
53+
54+
return ArgumentBundle.from_args(self, args, keys_to_bundle)
55+
56+
def ensure_bundle(self, bundle_or_args):
57+
assert isinstance(self, ArgumentBundleDefinition)
58+
59+
bundle_definition = self
60+
61+
if isinstance(bundle_or_args, ArgumentBundle):
62+
assert bundle_or_args.name == bundle_definition._definition_name
63+
return bundle_or_args
64+
else:
65+
return bundle_definition(*bundle_or_args)
66+
67+
68+
class ArgumentBundle(OrderedDict):
69+
def __init__(self, definition, dictionary):
70+
super(ArgumentBundle, self).__init__(dictionary)
71+
self._definition = definition
72+
73+
@property
74+
def name(self):
75+
return self._definition._definition_name
76+
77+
def __iter__(self):
78+
return iter(self.values())
79+
80+
@classmethod
81+
def from_args(cls, definition, args, keys):
82+
dictionary = {key: args[key] for key in keys}
83+
return cls(definition, dictionary)
84+
85+
1086
class GetoptCompatNamespace(argparse.Namespace):
1187
"""Small glue abstraction to provide an object that when iterated yields
1288
tuples of cli-like options and their corresponding values thus emulating

tests/test_mig_shared_arguments.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,46 @@
33

44
from tests.support import MigTestCase, testmain
55

6-
from mig.shared.arguments import parse_getopt_args
6+
from mig.shared.arguments import parse_getopt_args, \
7+
ArgumentBundle, ArgumentBundleDefinition
8+
9+
10+
def _is_not_none(value):
11+
return value is not None
12+
13+
14+
class TestMigSharedArguments__bundles(MigTestCase):
15+
ThingsBundle = ArgumentBundleDefinition('Things', [
16+
(None, 'some_thing', _is_not_none),
17+
(None, 'other_thing', _is_not_none),
18+
])
19+
20+
def assertBundleOfKind(self, value, bundle_kind=None):
21+
assert isinstance(bundle_kind, str) and bundle_kind
22+
self.assertIsInstance(value, ArgumentBundle, "value is not an argument bundle")
23+
self.assertEqual(value.name, bundle_kind, "expected %s bundle, got %s" % (bundle_kind, value.name))
24+
25+
def test_bundling_arguments_produces_a_bundle(self):
26+
bundle = self.ThingsBundle('abc', 'def')
27+
28+
self.assertBundleOfKind(bundle, bundle_kind='Things')
29+
30+
def test_raises_on_missing_positional_arguments(self):
31+
with self.assertRaises(ValueError) as raised:
32+
self.ThingsBundle(['a'])
33+
self.assertEqual(str(raised.exception), 'Error: too few arguments given (expected 2 got 1)')
34+
35+
def test_ensuring_an_existing_bundle_returns_it_unchanged(self):
36+
existing_bundle = self.ThingsBundle('abc', 'def')
37+
38+
bundle = self.ThingsBundle.ensure_bundle(existing_bundle)
39+
40+
self.assertIs(bundle, existing_bundle)
41+
42+
def test_ensuring_an_list_of_arguments_returns_a_bundle(self):
43+
bundle = self.ThingsBundle.ensure_bundle(['abc', 'def'])
44+
45+
self.assertBundleOfKind(bundle, bundle_kind='Things')
746

847

948
class TestMigSharedArguments__getopt(MigTestCase):

0 commit comments

Comments
 (0)