Skip to content

Commit 3167c93

Browse files
committed
Merge pull request #61 from MarcCote/command_parsing
Command parsing
2 parents 0900d20 + 9a2f918 commit 3167c93

File tree

6 files changed

+194
-57
lines changed

6 files changed

+194
-57
lines changed

scripts/smart_dispatch.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ def main():
3535
jobname = args.commandsFile.name
3636
commands = smartdispatch.get_commands_from_file(args.commandsFile)
3737
else:
38-
# Commands that needs to be parsed and unfolded.
39-
arguments = map(smartdispatch.unfold_argument, args.commandAndOptions)
40-
jobname = smartdispatch.generate_name_from_arguments(arguments, max_length=235)
41-
commands = smartdispatch.get_commands_from_arguments(arguments)
38+
# Command that needs to be parsed and unfolded.
39+
command = " ".join(args.commandAndOptions)
40+
jobname = smartdispatch.generate_name_from_command(command, max_length=235)
41+
commands = smartdispatch.unfold_command(command)
4242

4343
commands = smartdispatch.replace_uid_tag(commands)
4444

smartdispatch/argument_template.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import re
2+
from collections import OrderedDict
3+
4+
5+
def build_argument_templates_dictionnary():
6+
# Order matter, if some regex is more greedy than another, the it should go after
7+
argument_templates = OrderedDict()
8+
argument_templates[RangeArgumentTemplate.__name__] = RangeArgumentTemplate()
9+
argument_templates[ListArgumentTemplate.__name__] = ListArgumentTemplate()
10+
return argument_templates
11+
12+
13+
class ArgumentTemplate(object):
14+
def __init__(self):
15+
self.regex = ""
16+
17+
def unfold(self, match):
18+
raise NotImplementedError("Subclass must implement method `unfold(self, match)`!")
19+
20+
21+
class ListArgumentTemplate(ArgumentTemplate):
22+
def __init__(self):
23+
self.regex = "\[[^]]*\]"
24+
25+
def unfold(self, match):
26+
return match[1:-1].split(' ')
27+
28+
29+
class RangeArgumentTemplate(ArgumentTemplate):
30+
def __init__(self):
31+
self.regex = "\[(\d+):(\d+)(?::(\d+))?\]"
32+
33+
def unfold(self, match):
34+
groups = re.search(self.regex, match).groups()
35+
start = int(groups[0])
36+
end = int(groups[1])
37+
step = 1 if groups[2] is None else int(groups[2])
38+
return map(str, range(start, end, step))
39+
40+
41+
argument_templates = build_argument_templates_dictionnary()

smartdispatch/smartdispatch.py

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from __future__ import absolute_import
22

33
import os
4+
import re
45
import itertools
56
from datetime import datetime
67

78
import smartdispatch
89
from smartdispatch import utils
10+
from smartdispatch.argument_template import argument_templates
911

1012
UID_TAG = "{UID}"
1113

@@ -98,45 +100,47 @@ def get_commands_from_file(fileobj):
98100
return fileobj.read().strip().split('\n')
99101

100102

101-
def get_commands_from_arguments(arguments):
102-
''' Obtains commands from the product of every unfolded arguments.
103+
def unfold_command(command):
104+
''' Unfolds a command into a list of unfolded commands.
105+
106+
Unfolding is performed for every folded arguments (see *Arguments templates*)
107+
found in `command`. Then, resulting commands are generated using the product
108+
of every unfolded arguments.
103109
104110
Parameters
105111
----------
106-
arguments : list of list of str
107-
list of unfolded arguments
112+
command : list of str
113+
command to unfold
108114
109115
Returns
110116
-------
111117
commands : list of str
112-
commands resulting from the product of every unfolded arguments
113-
'''
114-
return [" ".join(argvalues) for argvalues in itertools.product(*arguments)]
115-
116-
117-
def unfold_argument(argument):
118-
''' Unfolds a folded argument into a list of unfolded arguments.
118+
commands obtained after unfolding `command`
119119
120-
An argument can be folded e.g. a list of unfolded arguments separated by spaces.
121-
An unfolded argument unfolds to itself.
120+
Arguments template
121+
------------------
122+
*list*: "[item1 item2 ... itemN]"
123+
*range*: "[start:end]" or "[start:end:step]"
124+
'''
125+
text = utils.encode_escaped_characters(command)
122126

123-
Parameters
124-
----------
125-
argument : str
126-
argument to unfold
127+
# Build the master regex with all argument's regex
128+
regex = "(" + "|".join(["(?P<{0}>{1})".format(name, arg.regex) for name, arg in argument_templates.items()]) + ")"
127129

128-
Returns
129-
-------
130-
unfolded_arguments : list of str
131-
result of the unfolding
130+
pos = 0
131+
arguments = []
132+
for match in re.finditer(regex, text):
133+
# Add already unfolded argument
134+
arguments.append([text[pos:match.start()]])
132135

133-
Complex arguments
134-
-----------------
135-
*list (space)*: "item1 item2 ... itemN"
136-
'''
136+
# Unfold argument
137+
argument_template_name, matched_text = next((k, v) for k, v in match.groupdict().items() if v is not None)
138+
arguments.append(argument_templates[argument_template_name].unfold(matched_text))
139+
pos = match.end()
137140

138-
# Suppose `argument`is a space separated list
139-
return argument.split(" ")
141+
arguments.append([text[pos:]]) # Add remaining unfolded arguments
142+
arguments = [map(utils.decode_escaped_characters, argvalues) for argvalues in arguments]
143+
return ["".join(argvalues) for argvalues in itertools.product(*arguments)]
140144

141145

142146
def replace_uid_tag(commands):
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import re
2+
from nose.tools import assert_true
3+
from numpy.testing import assert_array_equal
4+
5+
from smartdispatch.argument_template import ListArgumentTemplate, RangeArgumentTemplate
6+
7+
8+
def test_list_argument_template():
9+
# Test valid enumeration folded arguments
10+
arg = ListArgumentTemplate()
11+
folded_arguments = [("[]", [""]),
12+
("[1]", ["1"]),
13+
("[1 ]", ["1", ""]),
14+
("[1 2 3]", ["1", "2", "3"]),
15+
]
16+
17+
for folded_argument, unfolded_arguments in folded_arguments:
18+
match = re.match(arg.regex, folded_argument)
19+
assert_true(match is not None)
20+
assert_array_equal(arg.unfold(match.group()), unfolded_arguments)
21+
22+
# Test invalid enumeration folded arguments
23+
assert_true(re.match(arg.regex, "[1 2 3") is None)
24+
25+
26+
def test_range_argument_template():
27+
arg = RangeArgumentTemplate()
28+
folded_arguments = [("[1:4]", ["1", "2", "3"]),
29+
("[1:4:2]", ["1", "3"]),
30+
]
31+
32+
for folded_argument, unfolded_arguments in folded_arguments:
33+
match = re.match(arg.regex, folded_argument)
34+
assert_true(match is not None)
35+
assert_array_equal(arg.unfold(match.group()), unfolded_arguments)

smartdispatch/tests/test_smartdispatch.py

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -77,32 +77,71 @@ def test_get_commands_from_file():
7777
assert_array_equal(smartdispatch.get_commands_from_file(fileobj), commands)
7878

7979

80-
def test_get_commands_from_arguments():
81-
# Test single unfolded arguments
82-
args = [["arg"]]
83-
assert_equal(smartdispatch.get_commands_from_arguments(args), ["arg"])
84-
85-
args = [["args1_a", "args1_b"]]
86-
assert_equal(smartdispatch.get_commands_from_arguments(args), ["args1_a", "args1_b"])
87-
88-
# Test multiple unfolded arguments
89-
args = [["args1"], ["args2"]]
90-
assert_equal(smartdispatch.get_commands_from_arguments(args), ["args1 args2"])
91-
92-
args = [["args1_a", "args1_b", "args1_c"], ["args2_a", "args2_b"]]
93-
assert_equal(smartdispatch.get_commands_from_arguments(args), ["args1_a args2_a", "args1_a args2_b",
94-
"args1_b args2_a", "args1_b args2_b",
95-
"args1_c args2_a", "args1_c args2_b"])
96-
97-
98-
def test_unfold_argument():
99-
# Test simple argument
100-
for arg in ["arg1", "[arg1"]:
101-
assert_array_equal(smartdispatch.unfold_argument(arg), [arg])
102-
103-
# Test list (space)
104-
for arg in ["arg1 arg2", "arg1 ", " arg1"]:
105-
assert_array_equal(smartdispatch.unfold_argument(arg), arg.split(" "))
80+
def test_unfold_command():
81+
# Test with one argument
82+
cmd = "ls"
83+
assert_equal(smartdispatch.unfold_command(cmd), ["ls"])
84+
85+
cmd = "echo 1"
86+
assert_equal(smartdispatch.unfold_command(cmd), ["echo 1"])
87+
88+
# Test two arguments
89+
cmd = "echo [1 2]"
90+
assert_equal(smartdispatch.unfold_command(cmd), ["echo 1", "echo 2"])
91+
92+
cmd = "echo test [1 2] yay"
93+
assert_equal(smartdispatch.unfold_command(cmd), ["echo test 1 yay", "echo test 2 yay"])
94+
95+
cmd = "echo test[1 2]"
96+
assert_equal(smartdispatch.unfold_command(cmd), ["echo test1", "echo test2"])
97+
98+
cmd = "echo test[1 2]yay"
99+
assert_equal(smartdispatch.unfold_command(cmd), ["echo test1yay", "echo test2yay"])
100+
101+
# Test multiple folded arguments
102+
cmd = "python my_command.py [0.01 0.000001 0.00000000001] -1 [omicron mu]"
103+
assert_equal(smartdispatch.unfold_command(cmd), ["python my_command.py 0.01 -1 omicron",
104+
"python my_command.py 0.01 -1 mu",
105+
"python my_command.py 0.000001 -1 omicron",
106+
"python my_command.py 0.000001 -1 mu",
107+
"python my_command.py 0.00000000001 -1 omicron",
108+
"python my_command.py 0.00000000001 -1 mu"])
109+
110+
# Test multiple folded arguments and not unfoldable brackets
111+
cmd = "python my_command.py [0.01 0.000001 0.00000000001] -1 \[[42 133,666]\] slow [omicron mu]"
112+
assert_equal(smartdispatch.unfold_command(cmd), ["python my_command.py 0.01 -1 [42] slow omicron",
113+
"python my_command.py 0.01 -1 [42] slow mu",
114+
"python my_command.py 0.01 -1 [133,666] slow omicron",
115+
"python my_command.py 0.01 -1 [133,666] slow mu",
116+
"python my_command.py 0.000001 -1 [42] slow omicron",
117+
"python my_command.py 0.000001 -1 [42] slow mu",
118+
"python my_command.py 0.000001 -1 [133,666] slow omicron",
119+
"python my_command.py 0.000001 -1 [133,666] slow mu",
120+
"python my_command.py 0.00000000001 -1 [42] slow omicron",
121+
"python my_command.py 0.00000000001 -1 [42] slow mu",
122+
"python my_command.py 0.00000000001 -1 [133,666] slow omicron",
123+
"python my_command.py 0.00000000001 -1 [133,666] slow mu"])
124+
125+
# Test multiple different folded arguments
126+
cmd = "python my_command.py [0.01 0.001] -[1:5] slow"
127+
assert_equal(smartdispatch.unfold_command(cmd), ["python my_command.py 0.01 -1 slow",
128+
"python my_command.py 0.01 -2 slow",
129+
"python my_command.py 0.01 -3 slow",
130+
"python my_command.py 0.01 -4 slow",
131+
"python my_command.py 0.001 -1 slow",
132+
"python my_command.py 0.001 -2 slow",
133+
"python my_command.py 0.001 -3 slow",
134+
"python my_command.py 0.001 -4 slow"])
135+
136+
cmd = "python my_command.py -[1:5] slow [0.01 0.001]"
137+
assert_equal(smartdispatch.unfold_command(cmd), ["python my_command.py -1 slow 0.01",
138+
"python my_command.py -1 slow 0.001",
139+
"python my_command.py -2 slow 0.01",
140+
"python my_command.py -2 slow 0.001",
141+
"python my_command.py -3 slow 0.01",
142+
"python my_command.py -3 slow 0.001",
143+
"python my_command.py -4 slow 0.01",
144+
"python my_command.py -4 slow 0.001"])
106145

107146

108147
def test_replace_uid_tag():

smartdispatch/utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,24 @@ def slugify(value):
3535
return str(re.sub('[-\s]+', '_', value))
3636

3737

38+
def encode_escaped_characters(text, escaping_character="\\"):
39+
""" Escape the escaped character using its hex representation """
40+
def hexify(match):
41+
return "\\x{0}".format(match.group()[-1].encode("hex"))
42+
43+
return re.sub(r"\\.", hexify, text)
44+
45+
46+
def decode_escaped_characters(text):
47+
""" Convert hex representation to the character it represents """
48+
if len(text) == 0:
49+
return ''
50+
51+
def unhexify(match):
52+
return match.group()[2:].decode("hex")
53+
54+
return re.sub(r"\\x..", unhexify, text)
55+
3856
@contextmanager
3957
def open_with_lock(*args, **kwargs):
4058
""" Context manager for opening file with an exclusive lock. """

0 commit comments

Comments
 (0)