""" DictCall primarily exports the function `dictCall`, which may raise the exceptions `NotFound` or `Invalid` (both of which are inherited from `DictCallError`. .. include: dictCall .. ignore: DictCallError, NotFound, Invalid .. ignore: parseCallTypes, parseDocString, fixupSpec, parseArgList .. ignore: extractVariable, extractVariableConverted .. ignore: convertDict, unconvertDict """ import re import inspect True, False = 1==1, 1==0 __all__ = ['DictCallError', 'NotFound', 'Invalid', 'dictCall'] class DictCallError(Exception): pass class NotFound(DictCallError): def __init__(self, var, dict=None, *args): # @@: do something with these variables... DictCallError(self, *args) self._var = var self._dict = dict class Invalid(DictCallError): pass def dictCall(func, d): """ Calls the function `func` with the information from dictionary `d`. `d` is expected to be a dictionary of strings, possibly with lists of strings for some keys, as might be generated by the `cgi` module. `dictCall` will match the keys of the dictionary with the arguments of the function. The function can provide type information, so that the input is verified and coerced in some fashion. However, type information is not required. **Defining Parameter Types** There are two ways of providing type information: by the parameter names themselves, or in the docstring. Parameter names can be typed by appending the types to the name, like ``var_int``. The dictionary will still be searched for a variable ``var``, but then it will be converted into an int. However, note that your parameter is still named var_int! This can be annoying. Instead you can put the type information in the docstring. You should begin this information with a line like:: call types: on a line by itself. Later lines should be of the format:: var: dict, int where ``dict, int`` equivalent to var_dict_int (types are described later). You end this section of information with a blank line. Indentation is not significant. Variables that are simply strings may be omited. You can also put the information in a function attribute, like:: def myFunc(a, b, c): pass myFunc.callTypes={'a': 'dict,int', 'c': 'int'} This is equivalent to ``myFunc(a_dict_int, b, c_int)``. Note Python 2.3 you can use:: myFunc.callTypes=dict(a='dict,int', c='int') **Parameter Types** The basic types, besides the implicit string type, are ``int`` and ``float``. In addition to these types there are three compound types: ``set``: Sets are actually returned as lists, but they are not generally ordered. When a dictionary is passed in like ``{'var': ['a', 'b']}``, this returns ``['a', 'b']`` (it's Invalid if you don't indicate that it's a set). It also promotes non-lists to a list, and if nothing is present returns the empty list (i.e., ``{'var': 'a'}`` returns ``['a']``, and ``{}`` returns ``[]``). Sets can be used with ``int`` and ``float``, i.e., a type of ``set, int`` will return a list of integers. You cannot put a ``dict`` or ``list`` inside a set, though a set can go inside them. ``dict``: A dictionary takes keys of the form ``var1:key`` and turns them into nested dictionaries. So with a type dict, ``{'var:a': 'apple', 'var:b': 'banana'}`` will return ``{'a': 'apple', 'b': 'banana'}``. Dict can be nested, so that keys like ``var1:key1:key2`` are possible, and can also be nested with ``list`` and contain ``set``, ``int``, or ``float``. ``list``: Essentially the same as dict, only it uses the keys only for ordering, and expects those keys to be integers. So ``{'var:1': 'first', 'var:2': 'second'}`` returns ``['first', 'second']``. **Using Types Together** If you are, for instance, collecting a list of first and last names, you can use compound types to manage the fields. Generate the form like:: for i in range(len(names)): self.write('' % (i, htmlEncode(names[i]['fname']))) self.write('' % (i, htmlEncode(names[i]['fname']))) Then the method that accepts the form looks like:: def saveNames(self, names_list_dict) ``names_list_dict`` will look like ``[{'fname': ..., 'lname': ...}, ...]`` """ spec = specDict(func) d = convertDict(d) callDict = {} for variable, spec in spec.items(): if variable == '**': continue targetName = spec['targetName'] t = spec['type'] try: callDict[targetName], d = extractVariableConverted(variable, t, d) except NotFound: if not spec['default']: raise if d and not spec.has_key('**'): raise TypeError, "Arguments unconverted: %s" % repr(d) for key in d.keys(): if callDict.has_key(key): raise TypeError, "Argument unparsed: %s" % key callDict.update(d) return func(**callDict) def specDict(func): """ Returns a dictionary that contains whatever type specification there is for the function. That dictionary has a key for each value expected or allowed in the request dictionary -- essentially a key for each argument variable. For each key there's another dictionary. There's keys ``targetName`` and ``type``, where targetName gives the variable name in the function definition (which may contain type information embedded, e.g. var1_int would be considered the variable ``"var1"`` with a type ``["int"]`` and a targetName ``"var1_int"``. Also has the key ``default`` which is a boolean, whether this key has a default value. The presence of the key ``**`` means that extra keyword arguments are allowed (always as strings, unparsed). """ if hasattr(func, 'specDict'): pass elif hasattr(func, 'callTypes'): func.specDict = parseCallTypes(func) elif func.__doc__ and specDictDocRE.search(func.__doc__): func.specDict = parseDocString(func) else: func.specDict = parseArgList(func) return func.specDict def parseCallTypes(func): callTypes = func.callTypes spec = {} for arg, typeList in func.callTypes.items(): info = {} info['targetName'] = arg if type(typeList) is not type([]): typeList = [typeList] info['type'] = [] for subT in typeList: info['type'].extend([t.strip() for t in subT.split(',')]) spec[arg] = info return fixupSpec(func, spec) specDictDocRE = re.compile(r'call types:\s*\n', re.I) specLineRE = re.compile(r'([a-zA-Z_][a-zA-Z_0-9]*): (.*)') def parseDocString(func): doc = func.__doc__ match = specDictDocRE.search(doc) doc = doc[match.end():].strip() lines = [l.strip() for l in doc.split('\n')] while lines and not lines[0]: # get rid of leading blank lines lines.pop(0) try: lines = lines[:lines.index('')] except ValueError: pass spec = {} for line in lines: match = specLineRE.search(line) if not match: raise TypeError, "Doc string line not understood: %s" % repr(line) info = {} info['targetName'] = match.group(1) info['type'] = [t.strip() for t in match.group(2).split(',')] spec[match.group(1)] = info return fixupSpec(func, spec) def fixupSpec(func, spec): args, varargs, varkw, defaults = inspect.getargspec(func) if varkw: spec['**'] = 1 for i in range(len(args)): arg = args[i] if not spec.has_key(arg): spec[arg] = {'targetName': arg, 'type': []} spec[arg]['default'] = i < len(args) - len(defaults) return spec _specialEndings = ['int', 'float', 'dict', 'list', 'set'] def parseArgList(func): args, varargs, varkw, defaults = inspect.getargspec(func) spec = {} if varkw: spec['**'] = 1 for i in range(len(args)): info = {} t = [] arg = args[i] info['targetName'] = arg info['default'] = i < len(args) - len(defaults) while 1: changed = 0 for ending in _specialEndings: if arg.endswith('_' + ending): arg = arg[:-(len(ending)+1)] t = [ending] + t changed = 1 break if not changed: break info['type'] = t spec[arg] = info return spec def extractVariable(var, t, d): """ Extracts variable `var`, which is of type `t`, from dictionary `d`. `t` should be a list (possibly empty) of types, such as ``["list", "int"]`` which means list-of-integers. String is implied in absense of a type. ``d`` is a dictionary of strings (keys and values). Values, however, may be a list of strings. These basic types are supported: ``string`` (or nothing): A string, if empty returns ``''``. If a list is in the value it will throw an exception, like all the basic types (use ``['set', 'string']`` instead). ``int``: Integer, if empty string returns None. ``float``: Float, if empty string returns None. And these compound types: ``set``: Returns an list of values, ordered arbitrarily. Will take lists that are in the value of the dictionary, or promote to a list a normal string. ``dict``: Expects variables like var:key. ``list``: Like ``dict``, with var:index, and then sorts by index (as an integer), and returns just the values sorted that way. Raises NotFound if a variable can't be found, Invalid if there's some other parsing problem. Returns a tuple of the parsed-out values, and the portion of the dictionary that wasn't used. """ value, d = extractVariableConverted(var, t, convertDict(d)) return value, unconvertDict(d) def extractVariableConverted(var, t, d): # basic types if not t or t[0] in _basicConvertTypes: try: val = d[var] except KeyError: raise NotFound(var=var, dict=d) del d[var] val = _basicConvertValue(t, val) return val, d # compound types t, rest = t[0], t[1:] if t == 'set': try: val = d[var] except KeyError: return [], d del d[var] if type(val) is not type([]): val = [val] if not rest or rest[0] in _basicConvertTypes: for v in val: if type(v) is type({}): # @@: better error raise Invalid, 'Nested dictionaries not expected (got: %s)' % v if rest: val = [_basicConvertValue(rest, v) for v in val] return val, d raise TypeError, 'set cannot contain other compound types (like %s)' % rest elif t in ('dict', 'list'): try: val = d[var] except KeyError: return {}, d del d[var] if type(val) is not type({}): # @@: better error raise Invalid if rest and rest[0] in _basicConvertTypes: for key in val.keys(): val[key] = _basicConvertValue(rest, val[key]) elif rest: for key in val.keys(): val[key], ignore = extractVariableConverted(key, rest, val) if t == 'list': val = [(int(key), value) for key, value in val.items()] val.sort() val = [value for key, value in val] return val, d _basicConvertTypes = ['int', 'float'] def _basicConvertValue(t, val): """ Convert a value based on [] (string), [``int``], [``float``]. """ if t and len(t) > 1: raise TypeError, '"%s" must be the last type in a type list (got: %s)' % (t[0], repr(t)) if not t: # string if not type(val) is type(""): # @@: better error raise Invalid return val elif t[0] == 'int': if val == '': return None try: return int(val) except: # @@: better error raise Invalid elif t[0] == 'float': if val == '': return None try: return float(val) except: # @@: better error raise Invalid def convertDict(d): """ Takes a flat dictionary and makes it deeper, where keys are strings and may be separated by ``:``, so that a key ``val1:val2`` which mean that ``val1`` will be a dictionary with the key ``val2`` in it. """ result = {} for key in d.keys(): if key.find(':') == -1: # Just a quick optimization for this common case result[key] = d[key] continue parts = key.split(':') inner = result for part in parts[:-1]: try: inner = inner[part] except KeyError: inner[part] = {} inner = inner[part] inner[parts[-1]] = d[key] return result def unconvertDict(d): """ Flattens a dictionary, undoing `convertDict`. """ result = {} for key, value in d.items(): if type(value) is type({}): value = unconvertDict(value) for key2, value2 in value.items(): result["%s:%s" % (key, key2)] = value2 else: result[key] = value return result if __name__ == '__main__': def callMe(a, b, c, d=None): print 'a=%r\nb=%r\nc=%r\nd=%r' % (a, b, c, d) callMe.specDict = { 'a': {'targetName': 'a', 'type': ['int'], 'default': False}, 'b': {'targetName': 'b', 'type': ['list', 'float'], 'default': False}, 'c': {'targetName': 'c', 'type': ['set'], 'default': False}, 'd': {'targetName': 'd', 'type': ['dict', 'dict'], 'default': True}} def callMe2(a_int, b_list_float, c_set, d_dict_dict=None): print 'a=%r\nb=%r\nc=%r\nd=%r' % (a_int, b_list_float, c_set, d_dict_dict) def callMe3(a, b, c, d=10): """ call types: a: int b: list, float c: set d: dict, dict """ print 'a=%r\nb=%r\nc=%r\nd=%r' % (a, b, c, d) def callMe4(a, b, c, d=None): print 'a=%r\nb=%r\nc=%r\nd=%r' % (a, b, c, d) callMe4.callTypes = { 'a': 'int', 'b': 'list,float', 'c': 'set', 'd': ['dict', 'dict']} t = """ >>> convertDict({'a': 'A', 'b:c': 'C', 'b:d': 'D', '1:2:3': '4'}) >>> unconvertDict(convertDict({'a': 'A', 'b:c': 'C', 'b:d': 'D', '1:2:3': '4'})) >>> extractVariable('happy', ['int'], {'happy': '102'}) >>> extractVariable('t', ['set', 'int'], {}) >>> extractVariable('t', ['set', 'int'], {'t': '20'}) >>> extractVariable('t', ['set', 'int'], {'t': ['20', '30', '20']}) >>> extractVariable('t', ['dict', 'int'], {'t:a': '20', 't:b': '30'}) >>> extractVariable('t', ['dict', 'int'], {'t:a': '20', 't:b': '30', 'c': 'hey'}) >>> extractVariable('t', ['dict', 'set', 'int'], {'t:a': '20', 't:b': ['30', '40']}) >>> extractVariable('t', ['dict', 'dict', 'int'], {'t:a:c': '20', 't:b:d': '40'}) >>> extractVariable('t', ['list', 'int'], {'t:2': '20', 't:1': '40', 't:3': '60'}) >>> extractVariable('t', ['list', 'dict', 'int'], {'t:2:c': '20', 't:1:d': '40'}) >>> dictCall(callMe, {'a': '20', 'b:2': '10.2', 'b:5': '0', 'b:7': '', 'c': 'hey', 'c': 'you', 'c': 'whatever', 'd:d:d': 'yep'}) >>> dictCall(callMe2, {'a': '20', 'b:2': '10.2', 'b:5': '0', 'b:7': '', 'c': 'hey', 'c': 'you', 'c': 'whatever', 'd:d:d': 'yep'}) >>> dictCall(callMe3, {'a': '20', 'b:2': '10.2', 'b:5': '0', 'b:7': '', 'c': 'hey', 'c': 'you', 'c': 'whatever', 'd:d:d': 'yep'}) >>> dictCall(callMe4, {'a': '20', 'b:2': '10.2', 'b:5': '0', 'b:7': '', 'c': 'hey', 'c': 'you', 'c': 'whatever', 'd:d:d': 'yep'}) """ for line in t.split('\n'): if line.startswith('>>>'): print line line = line[3:].strip() val = eval(line) if val is not None: print val