Initial checkin of new API implementation
[plcapi.git] / PLC / Method.py
1 #
2 # Base class for all PLCAPI functions
3 #
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
6 #
7 # $Id$
8 #
9
10 import xmlrpclib
11 from types import *
12 import textwrap
13 import os
14
15 from PLC.Faults import *
16 from PLC.Parameter import Parameter, Mixed
17 from PLC.Auth import Auth
18
19 class Method:
20     """
21     Base class for all PLCAPI functions. At a minimum, all PLCAPI
22     functions must define:
23
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
28
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}).
33
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.
37     """
38
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
42     # parser.
43
44     roles = []
45     accepts = []
46     returns = bool
47     status = "current"
48
49     def call(self):
50         """
51         Method body for all PLCAPI functions. Must override.
52         """
53
54         return True
55
56     def __init__(self, api):
57         self.name = self.__class__.__name__
58         self.api = api
59
60         # Auth may set this to a Person instance (if an anonymous
61         # method, will remain None).
62         self.caller = None
63
64         # API may set this to a (addr, port) tuple if known
65         self.source = None
66
67     def __call__(self, *args):
68         """
69         Main entry point for all PLCAPI functions. Type checks
70         arguments, authenticates, and executes call().
71         """
72
73         try:
74             (min_args, max_args, defaults) = self.args()
75
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))
79
80             for name, value, expected in zip(max_args, args, self.accepts):
81                 self.type_check(name, value, expected)
82
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.        
89
90             if len(self.accepts):
91                 auth = None
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):
97                             break
98                 if isinstance(auth, Auth):
99                     auth.check(self, *args)
100
101             return self.call(*args)
102
103         except PLCFault, fault:
104             # Prepend method name to expected faults
105             fault.faultString = self.name + ": " + fault.faultString
106             raise fault
107
108     def help(self, indent = "  "):
109         """
110         Text documentation for the method.
111         """
112
113         (min_args, max_args, defaults) = self.args()
114
115         text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
116
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"
120
121         text += "Allowed Roles:\n\n"
122         if not self.roles:
123             roles = ["any"]
124         else:
125             roles = self.roles
126         text += indent + ", ".join(roles) + "\n\n"
127
128         def param_text(name, param, indent, step):
129             """
130             Format a method parameter.
131             """
132
133             text = indent
134
135             # Print parameter name
136             if name:
137                 param_offset = 32
138                 text += name.ljust(param_offset - len(indent))
139             else:
140                 param_offset = len(indent)
141
142             # Print parameter type
143             param_type = python_type(param)
144             text += xmlrpc_type(param_type) + "\n"
145
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"
152                 param = param.type
153
154             text += "\n"
155
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)
166
167             return text
168
169         text += "Parameters:\n\n"
170         for name, param in zip(max_args, self.accepts):
171             text += param_text(name, param, indent, indent)
172
173         text += "Returns:\n\n"
174         text += param_text("", self.returns, indent, indent)
175
176         return text
177
178     def args(self):
179         """
180         Returns a tuple:
181
182         ((arg1_name, arg2_name, ...),
183          (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
184          (None, None, ..., optional1_default, optional2_default, ...))
185
186         That represents the minimum and maximum sets of arguments that
187         this function accepts and the defaults for the optional arguments.
188         """
189
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
193         if defaults is None:
194             defaults = ()
195
196         min_args = max_args[0:len(max_args) - len(defaults)]
197         defaults = tuple([None for arg in min_args]) + defaults
198         
199         return (min_args, max_args, defaults)
200
201     def type_check(self, name, value, expected):
202         """
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.
207         
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
212         not.
213
214         Extraneous members of dictionaries are ignored.
215         """
216
217         # If any of a number of types is acceptable
218         if isinstance(expected, Mixed):
219             for item in expected:
220                 try:
221                     self.type_check(name, value, item)
222                     expected = item
223                     break
224                 except PLCInvalidArgument, fault:
225                     pass
226             if expected != item:
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))),
231                                          name)
232
233         # Get actual expected type from within the Parameter structure
234         elif isinstance(expected, Parameter):
235             expected = expected.type
236
237         expected_type = python_type(expected)
238
239         # Strings are a special case. Accept either unicode or str
240         # types if a string is expected.
241         if expected_type in StringTypes and isinstance(value, StringTypes):
242             pass
243
244         # Integers and long integers are also special types. Accept
245         # either int or long types if an int or long is expected.
246         elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
247             pass
248
249         elif not isinstance(value, expected_type):
250             raise PLCInvalidArgument("expected %s, got %s" % \
251                                      (xmlrpc_type(expected_type),
252                                       xmlrpc_type(type(value))),
253                                      name)
254
255         # If a list with particular types of items is expected
256         if isinstance(expected, (list, tuple)):
257             for i in range(len(value)):
258                 if i >= len(expected):
259                     i = len(expected) - 1
260                 self.type_check(name + "[]", value[i], expected[i])
261
262         # If a struct with particular (or required) types of items is
263         # expected.
264         elif isinstance(expected, dict):
265             for key in value.keys():
266                 if key in expected:
267                     self.type_check(name + "['%s']" % key, value[key], expected[key])
268             for key, subparam in expected.iteritems():
269                 if isinstance(subparam, Parameter) and \
270                    not subparam.optional and key not in value.keys():
271                     raise PLCInvalidArgument("'%s' not specified" % key, name)
272
273 def python_type(arg):
274     """
275     Returns the Python type of the specified argument, which may be a
276     Python type, a typed value, or a Parameter.
277     """
278
279     if isinstance(arg, Parameter):
280         arg = arg.type
281
282     if isinstance(arg, type):
283         return arg
284     else:
285         return type(arg)
286
287 def xmlrpc_type(arg):
288     """
289     Returns the XML-RPC type of the specified argument, which may be a
290     Python type, a typed value, or a Parameter.
291     """
292
293     arg_type = python_type(arg)
294
295     if arg_type == NoneType:
296         return "nil"
297     elif arg_type == IntType or arg_type == LongType:
298         return "int"
299     elif arg_type == bool:
300         return "boolean"
301     elif arg_type == FloatType:
302         return "double"
303     elif arg_type in StringTypes:
304         return "string"
305     elif arg_type == ListType or arg_type == TupleType:
306         return "array"
307     elif arg_type == DictType:
308         return "struct"
309     elif arg_type == Mixed:
310         # Not really an XML-RPC type but return "mixed" for
311         # documentation purposes.
312         return "mixed"
313     else:
314         raise PLCAPIError, "XML-RPC cannot marshal %s objects" % arg_type