Skip to content

Commit 17cbeab

Browse files
committed
Fix TreeRoot interface to accept single ActionArgsPack argument
Add to_python_name and from_python_name methods to NodePath
1 parent 18d6aeb commit 17cbeab

12 files changed

+295
-40
lines changed

contextshell/Action.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from abc import ABC, abstractmethod
22
from contextshell.NodePath import NodePath
3+
from contextshell.TreeRoot import ActionArgsPack
34
from typing import Dict, Union, Any
45

56

@@ -10,5 +11,5 @@ def __init__(self, name: NodePath):
1011
self.name: NodePath = name
1112

1213
@abstractmethod
13-
def invoke(self, target: NodePath, action: NodePath, arguments: Dict[Union[NodePath, int], Any]):
14+
def invoke(self, target: NodePath, action: NodePath, arguments: ActionArgsPack):
1415
raise NotImplementedError()

contextshell/CallableAction.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from collections import OrderedDict
22
from typing import Callable, Dict, Union, Any, Tuple, List
3-
3+
from contextshell.TreeRoot import unpack_argument_tree, ActionArgsPack
44
from contextshell.Action import Action
55
from contextshell.NodePath import NodePath
66

@@ -10,22 +10,11 @@ def __init__(self, implementation: Callable, name: NodePath):
1010
super().__init__(name)
1111
self.implementation = implementation
1212

13-
def invoke(self, target: NodePath, action: NodePath, arguments: Dict[Union[NodePath, int], Any]):
13+
def invoke(self, target: NodePath, action: NodePath, arguments: ActionArgsPack):
1414
#print("Invoked with:", *args)
15-
args, kwargs = CallableAction.unpack_argument_tree(arguments)
15+
args, kwargs = unpack_argument_tree(arguments)
1616
return self.implementation(target, *args, **kwargs)
1717

18-
@staticmethod
19-
def unpack_argument_tree(arguments: Dict[Union[NodePath, int], Any]) -> Tuple[List[Any], Dict[NodePath, Any]]:
20-
args = list()
21-
kwargs = OrderedDict()
22-
for key, value in arguments.items():
23-
if isinstance(key, int):
24-
args.append(value)
25-
else:
26-
kwargs[key] = value
27-
return args, kwargs
28-
2918

3019
def action_from_function(method_to_wrap: Callable) -> Action:
3120
action_name: str = method_to_wrap.__name__

contextshell/CommandInterpreter.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from contextshell.Command import Command
22
from contextshell.NodePath import NodePath
3-
from contextshell.TreeRoot import TreeRoot
3+
from contextshell.TreeRoot import TreeRoot, parse_argument_tree
4+
from typing import Dict, Union, Any
45

56

67
class CommandInterpreter:
@@ -15,9 +16,9 @@ def execute(self, command: Command):
1516
raise RuntimeError("No action target specified")
1617
target_path = NodePath.cast(target_path)
1718
action_path = NodePath.cast(self._evaluate(command.name))
18-
arguments = map(self._evaluate, command.arguments)
19-
# TODO: parse arguments into OrderedDict[Union[int, NodePath], Any] as in Action.invoke interface
20-
return self.tree.execute(target_path, action_path, *arguments)
19+
arguments = list(map(self._evaluate, command.arguments))
20+
arguments = parse_argument_tree(arguments)
21+
return self.tree.execute(target_path, action_path, arguments)
2122

2223
def _evaluate(self, part):
2324
if isinstance(part, Command):

contextshell/CommandParser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def convert_token_type(token):
1313
pass
1414
return token
1515

16+
1617
def tokenize(text: str) -> List[str]:
1718
tokens = []
1819
tok = ''

contextshell/NodePath.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,36 @@
11

2+
# TODO: add from_python_name and to_python_name met
3+
24
class NodePath(list):
35
# CHECK: if this class could be replaced by build-in os.path
46
separator = '.'
57

68
@staticmethod
7-
def join(first, *rest):
9+
def join(first, *rest) -> 'NodePath':
810
path = NodePath(first)
911
for name in rest:
1012
part = NodePath.cast(name)
1113
path.extend(part)
1214
return path
1315

1416
@staticmethod
15-
def cast(path):
17+
def cast(path) -> 'NodePath':
1618
"""Converts passed argument to NodePath (if needed)"""
1719
# TODO: remove this method - be aware what types are passed instead
1820
if path is None:
1921
return NodePath()
2022
return NodePath(path)
2123

24+
@staticmethod
25+
def from_python_name(name: str) -> 'NodePath':
26+
name = name.lstrip('_').replace('_', NodePath.separator)
27+
return NodePath.cast(name)
28+
29+
def to_python_name(self) -> str:
30+
name = str(self)
31+
name = name.lstrip(NodePath.separator).replace(NodePath.separator, '_')
32+
return name
33+
2234
def __init__(self, representation=[], absolute=False):
2335
super().__init__()
2436
self.is_absolute = absolute

contextshell/NodeTreeRoot.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from contextshell.Node import Node
22
from contextshell.NodePath import NodePath
3-
from contextshell.TreeRoot import TreeRoot
3+
from contextshell.TreeRoot import TreeRoot, ActionArgsPack
44
from contextshell.Action import Action
55
from contextshell.CallableAction import action_from_function
66
from typing import Callable, List, Optional, Dict, Union, Any
@@ -105,14 +105,14 @@ def is_action(self, path: NodePath):
105105
node_value = self.get(path)
106106
return self._is_action_implementation(node_value)
107107

108-
def execute(self, target: NodePath, action: NodePath, *args):
108+
def execute(self, target: NodePath, action: NodePath, args: ActionArgsPack):
109109
#print("Execute: {}: {} {}".format(target, action, args))
110110
action_impl = self.find_action(target, action)
111111
if action_impl is None:
112112
raise NameError("Could not find action named '{}'".format(action))
113-
from collections import OrderedDict # FIXME: Temporary, until execute interface changes
114-
arguments = OrderedDict(enumerate(args))
115-
return action_impl.invoke(target, action, arguments)
113+
#from collections import OrderedDict # FIXME: Temporary, until execute interface changes
114+
#arguments = OrderedDict(enumerate(args))
115+
return action_impl.invoke(target, action, args)
116116

117117
def create_node(self, value):
118118
return Node(value)

contextshell/TreeRoot.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,50 @@
11
from abc import ABC, abstractmethod
22
from contextshell.NodePath import NodePath
3+
from typing import Dict, Union, Any, Tuple, List
4+
from collections import OrderedDict
5+
6+
7+
ActionArgsPack = Dict[Union[NodePath, int], Any]
38

49

510
class TreeRoot(ABC):
611
@abstractmethod
7-
def execute(self, target: NodePath, action: NodePath, *args): #FIXME: use Dict[Union[NodePath, int], Any] as arguments
12+
def execute(self, target: NodePath, action: NodePath, args: ActionArgsPack):
813
raise NotImplementedError()
14+
15+
def unpack_argument_tree(action_args: ActionArgsPack) -> Tuple[List[Any], Dict[str, Any]]:
16+
args = dict()
17+
kwargs = OrderedDict()
18+
for key, value in action_args.items():
19+
if isinstance(key, int):
20+
args[key] = value
21+
else:
22+
kwargs[key.to_python_name()] = value
23+
assert len(args) == 0 or max(args.keys()) < len(args)+len(kwargs)
24+
args = [a[1] for a in sorted(args.items())]
25+
return args, kwargs
26+
27+
28+
def pack_argument_tree(args: List[Any], kwargs: Dict[str, Any]) -> ActionArgsPack:
29+
pack_list = []
30+
for i, arg in enumerate(args):
31+
pack_list.append((i, arg))
32+
for key, value in kwargs.items():
33+
pack_list.append((NodePath.from_python_name(key), value))
34+
return OrderedDict(pack_list)
35+
36+
37+
def parse_argument_tree(raw_arguments: List[str]) -> ActionArgsPack:
38+
from contextshell.CommandParser import convert_token_type
39+
pack_list = []
40+
for i, arg in enumerate(raw_arguments):
41+
if isinstance(arg, str) and '=' in arg:
42+
key, value = arg.split('=')
43+
key_path = NodePath.from_python_name(key)
44+
if key_path.is_absolute:
45+
raise ValueError("Named argument path must be relative - {}".format(key_path))
46+
typed_value = convert_token_type(value)
47+
pack_list.append((key_path, typed_value))
48+
else:
49+
pack_list.append((i, arg))
50+
return OrderedDict(pack_list)

tests/unit/ArgumentPackerTests.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import unittest
2+
from contextshell.TreeRoot import pack_argument_tree, unpack_argument_tree, parse_argument_tree
3+
from contextshell.NodePath import NodePath as np
4+
from collections import OrderedDict
5+
6+
7+
class UnpackArgumentTreeTests(unittest.TestCase):
8+
def test_unpack_ordered(self):
9+
packed = OrderedDict([
10+
(0, 'foo'),
11+
(1, 'bar'),
12+
])
13+
14+
args, _ = unpack_argument_tree(packed)
15+
16+
self.assertSequenceEqual(['foo', 'bar'], args)
17+
18+
def test_unpack_mixed_order(self):
19+
packed = OrderedDict([
20+
(1, 'bar'),
21+
(0, 'foo'),
22+
])
23+
24+
args, _ = unpack_argument_tree(packed)
25+
26+
self.assertSequenceEqual(['foo', 'bar'], args)
27+
28+
def test_unpack_invalid_index(self):
29+
packed = OrderedDict([
30+
(1, 'bar'),
31+
])
32+
33+
with self.assertRaises(AssertionError):
34+
unpack_argument_tree(packed)
35+
36+
def test_unpack_keyed(self):
37+
packed = OrderedDict([
38+
(np('foo'), 1),
39+
(np('bar'), 2),
40+
])
41+
42+
_, kwargs = unpack_argument_tree(packed)
43+
44+
self.assertDictEqual({
45+
np('foo'): 1,
46+
np('bar'): 2,
47+
}, kwargs)
48+
49+
def test_unpack_mixed(self):
50+
packed = OrderedDict([
51+
(np('foo'), 1),
52+
(1, 'bar'),
53+
])
54+
55+
args, kwargs = unpack_argument_tree(packed)
56+
57+
self.assertSequenceEqual(['bar'], args)
58+
self.assertDictEqual({
59+
np('foo'): 1,
60+
}, kwargs)
61+
62+
def test_unpack_path_to_python_name(self):
63+
packed = OrderedDict([
64+
(np('foo.bar'), 1)
65+
])
66+
67+
_, kwargs = unpack_argument_tree(packed)
68+
self.assertDictEqual({
69+
'foo_bar': 1,
70+
}, kwargs)
71+
72+
73+
class PackArgumentTreeTests(unittest.TestCase):
74+
def test_pack_ordered(self):
75+
args = ['foo', 'bar']
76+
kwargs = {}
77+
78+
packed = pack_argument_tree(args, kwargs)
79+
80+
self.assertEqual(OrderedDict([
81+
(0, 'foo'),
82+
(1, 'bar')
83+
]), packed)
84+
85+
def test_pack_keyed(self):
86+
args = []
87+
kwargs = {
88+
'foo': 2,
89+
'bar': 1
90+
}
91+
92+
packed = pack_argument_tree(args, kwargs)
93+
94+
self.assertDictEqual({
95+
np('bar'): 1,
96+
np('foo'): 2
97+
}, packed)
98+
99+
def test_key_is_converted_to_path(self):
100+
args = []
101+
kwargs = {
102+
'foo_bar': 1,
103+
}
104+
105+
packed = pack_argument_tree(args, kwargs)
106+
107+
self.assertDictEqual({
108+
np('foo.bar'): 1
109+
}, packed)
110+
111+
112+
class ParseArgumentTreeTests(unittest.TestCase):
113+
def test_parse_empty(self):
114+
args = []
115+
116+
packed = parse_argument_tree(args)
117+
118+
self.assertEqual(0, len(packed))
119+
120+
def test_parse_positional(self):
121+
args = ['foo', 'bar']
122+
123+
packed = parse_argument_tree(args)
124+
125+
self.assertEqual(OrderedDict([
126+
(0, 'foo'),
127+
(1, 'bar'),
128+
]), packed)
129+
130+
def test_parse_keyword_string(self):
131+
args = ['foo=rabarbar']
132+
133+
packed = parse_argument_tree(args)
134+
135+
self.assertEqual(OrderedDict([
136+
(np('foo'), 'rabarbar'),
137+
]), packed)
138+
139+
def test_parse_keyword_int(self):
140+
args = ['foo=3']
141+
142+
packed = parse_argument_tree(args)
143+
144+
self.assertEqual(OrderedDict([
145+
(np('foo'), 3),
146+
]), packed)
147+
148+
def test_parse_keyword_path(self):
149+
args = ['foo.bar=spam']
150+
151+
packed = parse_argument_tree(args)
152+
153+
self.assertEqual(OrderedDict([
154+
(np('foo.bar'), 'spam'),
155+
]), packed)
156+
157+
def test_parse_absolute_path_raises(self):
158+
args = ['.foo=spam']
159+
160+
with self.assertRaises(ValueError):
161+
parse_argument_tree(args)

tests/unit/CallableActionTests.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ def test_arguments_are_unpacked_to_keywords(self):
3434
implementation = Mock()
3535
action = create_action(implementation)
3636
arguments = OrderedDict([
37-
('foo', 'spam'),
38-
('bar', 2),
37+
(np('foo'), 'spam'),
38+
(np('bar'), 2),
3939
])
4040

4141
action.invoke(np('.target'), np('action'), arguments)

0 commit comments

Comments
 (0)