2 # Base class for all PLCAPI functions
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
7 # $Id: Method.py,v 1.1 2006/09/06 15:36:07 mlhuang Exp $
15 from PLC.Faults import *
16 from PLC.Parameter import Parameter, Mixed
17 from PLC.Auth import Auth
21 Base class for all PLCAPI functions. At a minimum, all PLCAPI
22 functions must define:
24 roles = [list of roles]
25 accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
26 returns = Parameter(return_type, return_doc)
27 call(arg1, arg2, ...): method body
29 Argument types may be Python types (e.g., int, bool, etc.), typed
30 values (e.g., 1, True, etc.), a Parameter, or lists or
31 dictionaries of possibly mixed types, values, and/or Parameters
32 (e.g., [int, bool, ...] or {'arg1': int, 'arg2': bool}).
34 Once function decorators in Python 2.4 are fully supported,
35 consider wrapping calls with accepts() and returns() functions
36 instead of performing type checking manually.
39 # Defaults. Could implement authentication and type checking with
40 # decorators, but they are not supported in Python 2.3 and it
41 # would be hard to generate documentation without writing a code
51 Method body for all PLCAPI 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):
69 Main entry point for all PLCAPI functions. Type checks
70 arguments, authenticates, and executes call().
74 (min_args, max_args, defaults) = self.args()
76 # Check that the right number of arguments were passed in
77 if len(args) < len(min_args) or len(args) > len(max_args):
78 raise PLCInvalidArgumentCount(len(args), len(min_args), len(max_args))
80 for name, value, expected in zip(max_args, args, self.accepts):
81 self.type_check(name, value, expected)
83 # The first argument to all methods that require
84 # authentication, should be an Auth structure. The rest of the
85 # arguments to the call may also be used in the authentication
86 # check. For example, calls made by the Boot Manager are
87 # verified by comparing a hash of the message parameters to
88 # the value in the authentication structure.
92 if isinstance(self.accepts[0], Auth):
93 auth = self.accepts[0]
94 elif isinstance(self.accepts[0], Mixed):
95 for auth in self.accepts[0]:
96 if isinstance(auth, Auth):
98 if isinstance(auth, Auth):
99 auth.check(self, *args)
101 return self.call(*args)
103 except PLCFault, fault:
104 # Prepend method name to expected faults
105 fault.faultString = self.name + ": " + fault.faultString
108 def help(self, indent = " "):
110 Text documentation for the method.
113 (min_args, max_args, defaults) = self.args()
115 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
117 text += "Description:\n\n"
118 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
119 text += "\n".join(lines) + "\n\n"
121 text += "Allowed Roles:\n\n"
126 text += indent + ", ".join(roles) + "\n\n"
128 def param_text(name, param, indent, step):
130 Format a method parameter.
135 # Print parameter name
138 text += name.ljust(param_offset - len(indent))
140 param_offset = len(indent)
142 # Print parameter type
143 param_type = python_type(param)
144 text += xmlrpc_type(param_type) + "\n"
146 # Print parameter documentation right below type
147 if isinstance(param, Parameter):
148 wrapper = textwrap.TextWrapper(width = 70,
149 initial_indent = " " * param_offset,
150 subsequent_indent = " " * param_offset)
151 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
156 # Indent struct fields and mixed types
157 if isinstance(param, dict):
158 for name, subparam in param.iteritems():
159 text += param_text(name, subparam, indent + step, step)
160 elif isinstance(param, Mixed):
161 for subparam in param:
162 text += param_text(name, subparam, indent + step, step)
163 elif isinstance(param, (list, tuple)):
164 for subparam in param:
165 text += param_text("", subparam, indent + step, step)
169 text += "Parameters:\n\n"
170 for name, param in zip(max_args, self.accepts):
171 text += param_text(name, param, indent, indent)
173 text += "Returns:\n\n"
174 text += param_text("", self.returns, indent, indent)
182 ((arg1_name, arg2_name, ...),
183 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
184 (None, None, ..., optional1_default, optional2_default, ...))
186 That represents the minimum and maximum sets of arguments that
187 this function accepts and the defaults for the optional arguments.
190 # Inspect call. Remove self from the argument list.
191 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
192 defaults = self.call.func_defaults
196 min_args = max_args[0:len(max_args) - len(defaults)]
197 defaults = tuple([None for arg in min_args]) + defaults
199 return (min_args, max_args, defaults)
201 def type_check(self, name, value, expected, min = None, max = None):
203 Checks the type of the named value against the expected type,
204 which may be a Python type, a typed value, a Parameter, a
205 Mixed type, or a list or dictionary of possibly mixed types,
206 values, Parameters, or Mixed types.
208 Extraneous members of lists must be of the same type as the
209 last specified type. For example, if the expected argument
210 type is [int, bool], then [1, False] and [14, True, False,
211 True] are valid, but [1], [False, 1] and [14, True, 1] are
214 Extraneous members of dictionaries are ignored.
217 # If any of a number of types is acceptable
218 if isinstance(expected, Mixed):
219 for item in expected:
221 self.type_check(name, value, item)
224 except PLCInvalidArgument, fault:
227 xmlrpc_types = [xmlrpc_type(item) for item in expected]
228 raise PLCInvalidArgument("expected %s, got %s" % \
229 (" or ".join(xmlrpc_types),
230 xmlrpc_type(type(value))),
233 # Get actual expected type from within the Parameter structure
234 elif isinstance(expected, Parameter):
237 expected = expected.type
239 expected_type = python_type(expected)
241 # Strings are a special case. Accept either unicode or str
242 # types if a string is expected.
243 if expected_type in StringTypes and isinstance(value, StringTypes):
246 # Integers and long integers are also special types. Accept
247 # either int or long types if an int or long is expected.
248 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
251 elif not isinstance(value, expected_type):
252 raise PLCInvalidArgument("expected %s, got %s" % \
253 (xmlrpc_type(expected_type),
254 xmlrpc_type(type(value))),
257 # If a minimum or maximum (length, value) has been specified
258 if expected_type in StringTypes:
259 if min is not None and \
260 len(value.encode(self.api.encoding)) < min:
261 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
262 if max is not None and \
263 len(value.encode(self.api.encoding)) > max:
264 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
266 if min is not None and value < min:
267 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
268 if max is not None and value > max:
269 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
271 # If a list with particular types of items is expected
272 if isinstance(expected, (list, tuple)):
273 for i in range(len(value)):
274 if i >= len(expected):
275 i = len(expected) - 1
276 self.type_check(name + "[]", value[i], expected[i])
278 # If a struct with particular (or required) types of items is
280 elif isinstance(expected, dict):
281 for key in value.keys():
283 self.type_check(name + "['%s']" % key, value[key], expected[key])
284 for key, subparam in expected.iteritems():
285 if isinstance(subparam, Parameter) and \
286 not subparam.optional and key not in value.keys():
287 raise PLCInvalidArgument("'%s' not specified" % key, name)
289 def python_type(arg):
291 Returns the Python type of the specified argument, which may be a
292 Python type, a typed value, or a Parameter.
295 if isinstance(arg, Parameter):
298 if isinstance(arg, type):
303 def xmlrpc_type(arg):
305 Returns the XML-RPC type of the specified argument, which may be a
306 Python type, a typed value, or a Parameter.
309 arg_type = python_type(arg)
311 if arg_type == NoneType:
313 elif arg_type == IntType or arg_type == LongType:
315 elif arg_type == bool:
317 elif arg_type == FloatType:
319 elif arg_type in StringTypes:
321 elif arg_type == ListType or arg_type == TupleType:
323 elif arg_type == DictType:
325 elif arg_type == Mixed:
326 # Not really an XML-RPC type but return "mixed" for
327 # documentation purposes.
330 raise PLCAPIError, "XML-RPC cannot marshal %s objects" % arg_type