2 # Base class for all SfaAPI functions
11 from types import StringTypes
17 from sfa.util.faults import *
18 from sfa.util.parameter import Parameter, Mixed, python_type, xmlrpc_type
19 from sfa.trust.auth import Auth
20 from sfa.util.debug import profile, log
22 # we inherit object because we use new-style classes for legacy methods
23 class Method (object):
25 Base class for all SfaAPI functions. At a minimum, all SfaAPI
26 functions must define:
28 interfaces = [allowed interfaces]
29 accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
30 returns = Parameter(return_type, return_doc)
31 call(arg1, arg2, ...): method body
33 Argument types may be Python types (e.g., int, bool, etc.), typed
34 values (e.g., 1, True, etc.), a Parameter, or lists or
35 dictionaries of possibly mixed types, values, and/or Parameters
36 (e.g., [int, bool, ...] or {'arg1': int, 'arg2': bool}).
38 Once function decorators in Python 2.4 are fully supported,
39 consider wrapping calls with accepts() and returns() functions
40 instead of performing type checking manually.
48 def call(self, *args):
50 Method body for all SfaAPI functions. Must override.
56 def __init__(self, api):
57 self.name = self.__class__.__name__
60 # Auth may set this to a Person instance (if an anonymous
61 # method, will remain None).
64 # API may set this to a (addr, port) tuple if known
67 def __call__(self, *args, **kwds):
69 Main entry point for all SfaAPI functions. Type checks
70 arguments, authenticates, and executes call().
75 methodname = self.name
76 if not self.api.interface or self.api.interface not in self.interfaces:
77 raise SfaInvalidAPIMethod, methodname, self.api.interface
79 # legacy code cannot be type-checked, due to the way Method.args() works
80 if not hasattr(self,"skip_typecheck"):
81 (min_args, max_args, defaults) = self.args()
83 # Check that the right number of arguments were passed in
84 if len(args) < len(min_args) or len(args) > len(max_args):
85 raise SfaInvalidArgumentCount(len(args), len(min_args), len(max_args))
87 for name, value, expected in zip(max_args, args, self.accepts):
88 self.type_check(name, value, expected, args)
90 result = self.call(*args, **kwds)
91 runtime = time.time() - start
93 if self.api.config.SFA_API_DEBUG or hasattr(self, 'message'):
94 # XX print to some log file
95 # print >> log, "some output"
100 except SfaFault, fault:
104 # Prepend caller and method name to expected faults
105 fault.faultString = caller + ": " + self.name + ": " + fault.faultString
106 runtime = time.time() - start
108 if self.api.config.SFA_API_DEBUG:
109 traceback.print_exc()
113 def help(self, indent = " "):
115 Text documentation for the method.
118 (min_args, max_args, defaults) = self.args()
120 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
122 text += "Description:\n\n"
123 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
124 text += "\n".join(lines) + "\n\n"
126 def param_text(name, param, indent, step):
128 Format a method parameter.
133 # Print parameter name
136 text += name.ljust(param_offset - len(indent))
138 param_offset = len(indent)
140 # Print parameter type
141 param_type = python_type(param)
142 text += xmlrpc_type(param_type) + "\n"
144 # Print parameter documentation right below type
145 if isinstance(param, Parameter):
146 wrapper = textwrap.TextWrapper(width = 70,
147 initial_indent = " " * param_offset,
148 subsequent_indent = " " * param_offset)
149 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
154 # Indent struct fields and mixed types
155 if isinstance(param, dict):
156 for name, subparam in param.iteritems():
157 text += param_text(name, subparam, indent + step, step)
158 elif isinstance(param, Mixed):
159 for subparam in param:
160 text += param_text(name, subparam, indent + step, step)
161 elif isinstance(param, (list, tuple, set)):
162 for subparam in param:
163 text += param_text("", subparam, indent + step, step)
167 text += "Parameters:\n\n"
168 for name, param in zip(max_args, self.accepts):
169 text += param_text(name, param, indent, indent)
171 text += "Returns:\n\n"
172 text += param_text("", self.returns, indent, indent)
180 ((arg1_name, arg2_name, ...),
181 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
182 (None, None, ..., optional1_default, optional2_default, ...))
184 That represents the minimum and maximum sets of arguments that
185 this function accepts and the defaults for the optional arguments.
188 # Inspect call. Remove self from the argument list.
189 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
190 defaults = self.call.func_defaults
194 min_args = max_args[0:len(max_args) - len(defaults)]
195 defaults = tuple([None for arg in min_args]) + defaults
197 return (min_args, max_args, defaults)
199 def type_check(self, name, value, expected, args):
201 Checks the type of the named value against the expected type,
202 which may be a Python type, a typed value, a Parameter, a
203 Mixed type, or a list or dictionary of possibly mixed types,
204 values, Parameters, or Mixed types.
206 Extraneous members of lists must be of the same type as the
207 last specified type. For example, if the expected argument
208 type is [int, bool], then [1, False] and [14, True, False,
209 True] are valid, but [1], [False, 1] and [14, True, 1] are
212 Extraneous members of dictionaries are ignored.
215 # If any of a number of types is acceptable
216 if isinstance(expected, Mixed):
217 for item in expected:
219 self.type_check(name, value, item, args)
221 except SfaInvalidArgument, fault:
225 # If an authentication structure is expected, save it and
226 # authenticate after basic type checking is done.
227 #if isinstance(expected, Auth):
232 # Get actual expected type from within the Parameter structure
233 if isinstance(expected, Parameter):
236 nullok = expected.nullok
237 expected = expected.type
243 expected_type = python_type(expected)
245 # If value can be NULL
246 if value is None and nullok:
249 # Strings are a special case. Accept either unicode or str
250 # types if a string is expected.
251 if expected_type in StringTypes and isinstance(value, StringTypes):
254 # Integers and long integers are also special types. Accept
255 # either int or long types if an int or long is expected.
256 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
259 elif not isinstance(value, expected_type):
260 raise SfaInvalidArgument("expected %s, got %s" % \
261 (xmlrpc_type(expected_type),
262 xmlrpc_type(type(value))),
265 # If a minimum or maximum (length, value) has been specified
266 if expected_type in StringTypes:
267 if min is not None and \
268 len(value.encode(self.api.encoding)) < min:
269 raise SfaInvalidArgument, "%s must be at least %d bytes long" % (name, min)
270 if max is not None and \
271 len(value.encode(self.api.encoding)) > max:
272 raise SfaInvalidArgument, "%s must be at most %d bytes long" % (name, max)
273 elif expected_type in (list, tuple, set):
274 if min is not None and len(value) < min:
275 raise SfaInvalidArgument, "%s must contain at least %d items" % (name, min)
276 if max is not None and len(value) > max:
277 raise SfaInvalidArgument, "%s must contain at most %d items" % (name, max)
279 if min is not None and value < min:
280 raise SfaInvalidArgument, "%s must be > %s" % (name, str(min))
281 if max is not None and value > max:
282 raise SfaInvalidArgument, "%s must be < %s" % (name, str(max))
284 # If a list with particular types of items is expected
285 if isinstance(expected, (list, tuple, set)):
286 for i in range(len(value)):
287 if i >= len(expected):
288 j = len(expected) - 1
291 self.type_check(name + "[]", value[i], expected[j], args)
293 # If a struct with particular (or required) types of items is
295 elif isinstance(expected, dict):
296 for key in value.keys():
298 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
299 for key, subparam in expected.iteritems():
300 if isinstance(subparam, Parameter) and \
301 subparam.optional is not None and \
302 not subparam.optional and key not in value.keys():
303 raise SfaInvalidArgument("'%s' not specified" % key, name)
305 #if auth is not None:
306 # auth.check(self, *args)