1"""DictForArgs.py
2
3See the doc string for the DictForArgs() function.
4
5Also, there is a test suite in Tests/TestDictForArgs.py
6"""
7
8import re
9
10
11class DictForArgsError(Exception):
12    """Error when building dictionary from arguments."""
13
14
15def _SyntaxError(s):
16    raise DictForArgsError('Syntax error: %r' % s)
17
18
19_nameRE = re.compile(r'\w+')
20_equalsRE = re.compile(r'\=')
21_stringRE = re.compile(r'''"[^"]+"|'[^']+'|\S+''')
22_whiteRE = re.compile(r'\s+')
23
24_REs = [_nameRE, _equalsRE, _stringRE, _whiteRE]
25
26
27def DictForArgs(s):
28    """Build dictionary from arguments.
29
30    Takes an input such as:
31        x=3
32        name="foo"
33        first='john' last='doe'
34        required border=3
35
36    And returns a dictionary representing the same. For keys that aren't
37    given an explicit value (such as 'required' above), the value is '1'.
38
39    All values are interpreted as strings. If you want ints and floats,
40    you'll have to convert them yourself.
41
42    This syntax is equivalent to what you find in HTML and close to other
43    ML languages such as XML.
44
45    Returns {} for an empty string.
46
47    The informal grammar is:
48        (NAME [=NAME|STRING])*
49
50    Will raise DictForArgsError if the string is invalid.
51
52    See also: PyDictForArgs() and ExpandDictWithExtras() in this module.
53    """
54
55    s = s.strip()
56
57    # Tokenize
58
59    verbose = False
60    matches = []
61    start = 0
62    sLen = len(s)
63
64    if verbose:
65        print '>> DictForArgs(%s)' % repr(s)
66        print '>> sLen:', sLen
67    while start < sLen:
68        for regEx in _REs:
69            if verbose:
70                print '>> try:', regEx
71            match = regEx.match(s, start)
72            if verbose:
73                print '>> match:', match
74            if match is not None:
75                if match.re is not _whiteRE:
76                    matches.append(match)
77                start = match.end()
78                if verbose:
79                    print '>> new start:', start
80                break
81        else:
82            _SyntaxError(s)
83
84    if verbose:
85        names = []
86        for match in matches:
87            if match.re is _nameRE:
88                name = 'name'
89            elif match.re is _equalsRE:
90                name = 'equals'
91            elif match.re is _stringRE:
92                name = 'string'
93            elif match.re is _whiteRE:
94                name = 'white'
95            names.append(name)
96            #print '>> match =', name, match
97        print '>> names =', names
98
99
100    # Process tokens
101
102    # At this point we have a list of all the tokens (as re.Match objects)
103    # We need to process these into a dictionary.
104
105    d = {}
106    matchesLen = len(matches)
107    i = 0
108    while i < matchesLen:
109        match = matches[i]
110        if i + 1 < matchesLen:
111            peekMatch = matches[i+1]
112        else:
113            peekMatch = None
114        if match.re is _nameRE:
115            if peekMatch is not None:
116                if peekMatch.re is _nameRE:
117                    # We have a name without an explicit value
118                    d[match.group()] = '1'
119                    i += 1
120                    continue
121                if peekMatch.re is _equalsRE:
122                    if i + 2 < matchesLen:
123                        target = matches[i+2]
124                        if target.re is _nameRE or target.re is _stringRE:
125                            value = target.group()
126                            if value[0] == "'" or value[0] == '"':
127                                value = value[1:-1]
128                                # value = "'''%s'''" % value[1:-1]
129                                # value = eval(value)
130                            d[match.group()] = value
131                            i += 3
132                            continue
133            else:
134                d[match.group()] = '1'
135                i += 1
136                continue
137        _SyntaxError(s)
138
139    if verbose:
140        print
141
142    return d
143
144
145def PyDictForArgs(s):
146    """Build dictionary from arguments.
147
148    Takes an input such as:
149        x=3
150        name="foo"
151        first='john'; last='doe'
152        list=[1, 2, 3]; name='foo'
153
154    And returns a dictionary representing the same.
155
156    All values are interpreted as Python expressions. Any error in these
157    expressions will raise the appropriate Python exception. This syntax
158    allows much more power than DictForArgs() since you can include
159    lists, dictionaries, actual ints and floats, etc.
160
161    This could also open the door to hacking your software if the input
162    comes from a tainted source such as an HTML form or an unprotected
163    configuration file.
164
165    Returns {} for an empty string.
166
167    See also: DictForArgs() and ExpandDictWithExtras() in this module.
168    """
169    if s:
170        s = s.strip()
171    if not s:
172        return {}
173
174    # special case: just a name
175    # meaning: name=1
176    # example: isAbstract
177    if ' ' not in s and '=' not in s and s[0].isalpha():
178        s += '=1'
179
180    results = {}
181    exec s in results
182
183    del results['__builtins__']
184    return results
185
186
187def ExpandDictWithExtras(d, key='Extras', delKey=True, dictForArgs=DictForArgs):
188    """Return a dictionary with the 'Extras' column expanded by DictForArgs().
189
190    For example, given:
191        {'Name': 'foo', 'Extras': 'x=1 y=2'}
192    The return value is:
193        {'Name': 'foo', 'x': '1', 'y': '2'}
194    The key argument controls what key in the dictionary is used to hold
195    the extra arguments. The delKey argument controls whether that key and
196    its corresponding value are retained.
197    The same dictionary may be returned if there is no extras key.
198    The most typical use of this function is to pass a row from a DataTable
199    that was initialized from a CSV file (e.g., a spreadsheet or tabular file).
200    FormKit and MiddleKit both use CSV files and allow for an Extras column
201    to specify attributes that occur infrequently.
202    """
203    if key in d:
204        newDict = dict(d)
205        if delKey:
206            del newDict[key]
207        newDict.update(dictForArgs(d[key]))
208        return newDict
209    else:
210        return d
211