- type_check(): don't type check Mixed types twice
[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: Method.py,v 1.11 2006/10/19 21:38:08 tmack Exp $
8 #
9
10 import xmlrpclib
11 from types import *
12 import textwrap
13 import os
14 import time
15
16 from PLC.Faults import *
17 from PLC.Parameter import Parameter, Mixed
18 from PLC.Auth import Auth
19 from PLC.Debug import profile, log
20
21 class Method:
22     """
23     Base class for all PLCAPI functions. At a minimum, all PLCAPI
24     functions must define:
25
26     roles = [list of roles]
27     accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
28     returns = Parameter(return_type, return_doc)
29     call(arg1, arg2, ...): method body
30
31     Argument types may be Python types (e.g., int, bool, etc.), typed
32     values (e.g., 1, True, etc.), a Parameter, or lists or
33     dictionaries of possibly mixed types, values, and/or Parameters
34     (e.g., [int, bool, ...]  or {'arg1': int, 'arg2': bool}).
35
36     Once function decorators in Python 2.4 are fully supported,
37     consider wrapping calls with accepts() and returns() functions
38     instead of performing type checking manually.
39     """
40
41     # Defaults. Could implement authentication and type checking with
42     # decorators, but they are not supported in Python 2.3 and it
43     # would be hard to generate documentation without writing a code
44     # parser.
45
46     roles = []
47     accepts = []
48     returns = bool
49     status = "current"
50
51     def call(self, *args):
52         """
53         Method body for all PLCAPI functions. Must override.
54         """
55
56         return True
57
58     def __init__(self, api):
59         self.name = self.__class__.__name__
60         self.api = api
61
62         # Auth may set this to a Person instance (if an anonymous
63         # method, will remain None).
64         self.caller = None
65
66         # API may set this to a (addr, port) tuple if known
67         self.source = None
68
69         
70     def __call__(self, *args, **kwds):
71         """
72         Main entry point for all PLCAPI functions. Type checks
73         arguments, authenticates, and executes call().
74         """
75
76         try:
77             start = time.time()
78             (min_args, max_args, defaults) = self.args()
79                                 
80             # Check that the right number of arguments were passed in
81             if len(args) < len(min_args) or len(args) > len(max_args):
82                 raise PLCInvalidArgumentCount(len(args), len(min_args), len(max_args))
83
84             for name, value, expected in zip(max_args, args, self.accepts):
85                 self.type_check(name, value, expected, args)
86         
87             result = self.call(*args, **kwds)
88             runtime = time.time() - start
89
90             if self.api.config.PLC_API_DEBUG:
91                 self.log(0, runtime, *args)
92                 
93             return result
94
95         except PLCFault, fault:
96             # Prepend method name to expected faults
97             fault.faultString = self.name + ": " + fault.faultString
98             runtime = time.time() - start
99             self.log(fault.faultCode, runtime, *args)
100             raise fault
101
102
103     def log(self, fault_code, runtime, *args):
104         """
105         Log the transaction 
106         """     
107         # Gather necessary logging variables
108         event_type = 'Unknown'
109         object_type = 'Unknown'
110         person_id = None
111         object_ids = []
112         call_name = self.name
113         call_args = ", ".join([unicode(arg) for arg in list(args)[1:]])
114         call = "%s(%s)" % (call_name, call_args)
115                 
116         if hasattr(self, 'event_type'):
117                 event_type = self.event_type
118         if hasattr(self, 'object_type'):
119                 object_type = self.object_type
120         if self.caller:
121                 person_id = self.caller['person_id']
122         if hasattr(self, 'object_ids'):
123                 object_ids = self.object_ids 
124
125         # do not log system calls
126         if call_name.startswith('system'):
127                 return False
128         # do not log get calls
129         if call_name.startswith('Get'):
130                 return False
131         
132         sql_event = "INSERT INTO events " \
133               " (person_id, event_type, object_type, fault_code, call, runtime) VALUES" \
134               " (%(person_id)s, %(event_type)s, %(object_type)s," \
135               "  %(fault_code)d, %(call)s, %(runtime)f)" 
136         self.api.db.do(sql_event, locals())     
137
138         # log objects affected
139         event_id =  self.api.db.last_insert_id('events', 'event_id')
140         for object_id in object_ids:
141                 sql_objects = "INSERT INTO event_object (event_id, object_id) VALUES" \
142                         " (%(event_id)d, %(object_id)d) " 
143                 self.api.db.do(sql_objects, locals())
144                         
145         self.api.db.commit()            
146         
147
148     def help(self, indent = "  "):
149         """
150         Text documentation for the method.
151         """
152
153         (min_args, max_args, defaults) = self.args()
154
155         text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
156
157         text += "Description:\n\n"
158         lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
159         text += "\n".join(lines) + "\n\n"
160
161         text += "Allowed Roles:\n\n"
162         if not self.roles:
163             roles = ["any"]
164         else:
165             roles = self.roles
166         text += indent + ", ".join(roles) + "\n\n"
167
168         def param_text(name, param, indent, step):
169             """
170             Format a method parameter.
171             """
172
173             text = indent
174
175             # Print parameter name
176             if name:
177                 param_offset = 32
178                 text += name.ljust(param_offset - len(indent))
179             else:
180                 param_offset = len(indent)
181
182             # Print parameter type
183             param_type = python_type(param)
184             text += xmlrpc_type(param_type) + "\n"
185
186             # Print parameter documentation right below type
187             if isinstance(param, Parameter):
188                 wrapper = textwrap.TextWrapper(width = 70,
189                                                initial_indent = " " * param_offset,
190                                                subsequent_indent = " " * param_offset)
191                 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
192                 param = param.type
193
194             text += "\n"
195
196             # Indent struct fields and mixed types
197             if isinstance(param, dict):
198                 for name, subparam in param.iteritems():
199                     text += param_text(name, subparam, indent + step, step)
200             elif isinstance(param, Mixed):
201                 for subparam in param:
202                     text += param_text(name, subparam, indent + step, step)
203             elif isinstance(param, (list, tuple)):
204                 for subparam in param:
205                     text += param_text("", subparam, indent + step, step)
206
207             return text
208
209         text += "Parameters:\n\n"
210         for name, param in zip(max_args, self.accepts):
211             text += param_text(name, param, indent, indent)
212
213         text += "Returns:\n\n"
214         text += param_text("", self.returns, indent, indent)
215
216         return text
217
218     def args(self):
219         """
220         Returns a tuple:
221
222         ((arg1_name, arg2_name, ...),
223          (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
224          (None, None, ..., optional1_default, optional2_default, ...))
225
226         That represents the minimum and maximum sets of arguments that
227         this function accepts and the defaults for the optional arguments.
228         """
229
230         # Inspect call. Remove self from the argument list.
231         max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
232         defaults = self.call.func_defaults
233         if defaults is None:
234             defaults = ()
235
236         min_args = max_args[0:len(max_args) - len(defaults)]
237         defaults = tuple([None for arg in min_args]) + defaults
238         
239         return (min_args, max_args, defaults)
240
241     def type_check(self, name, value, expected, args):
242         """
243         Checks the type of the named value against the expected type,
244         which may be a Python type, a typed value, a Parameter, a
245         Mixed type, or a list or dictionary of possibly mixed types,
246         values, Parameters, or Mixed types.
247         
248         Extraneous members of lists must be of the same type as the
249         last specified type. For example, if the expected argument
250         type is [int, bool], then [1, False] and [14, True, False,
251         True] are valid, but [1], [False, 1] and [14, True, 1] are
252         not.
253
254         Extraneous members of dictionaries are ignored.
255         """
256
257         # If any of a number of types is acceptable
258         if isinstance(expected, Mixed):
259             for item in expected:
260                 try:
261                     self.type_check(name, value, item, args)
262                     return
263                 except PLCInvalidArgument, fault:
264                     pass
265             xmlrpc_types = [xmlrpc_type(item) for item in expected]
266             raise PLCInvalidArgument("expected %s, got %s" % \
267                                      (" or ".join(xmlrpc_types),
268                                       xmlrpc_type(type(value))),
269                                      name)
270
271         # If an authentication structure is expected, save it and
272         # authenticate after basic type checking is done.
273         if isinstance(expected, Auth):
274             auth = expected
275         else:
276             auth = None
277
278         # Get actual expected type from within the Parameter structure
279         if isinstance(expected, Parameter):
280             min = expected.min
281             max = expected.max
282             expected = expected.type
283         else:
284             min = None
285             max = None
286
287         expected_type = python_type(expected)
288
289         # Strings are a special case. Accept either unicode or str
290         # types if a string is expected.
291         if expected_type in StringTypes and isinstance(value, StringTypes):
292             pass
293
294         # Integers and long integers are also special types. Accept
295         # either int or long types if an int or long is expected.
296         elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
297             pass
298
299         elif not isinstance(value, expected_type):
300             raise PLCInvalidArgument("expected %s, got %s" % \
301                                      (xmlrpc_type(expected_type),
302                                       xmlrpc_type(type(value))),
303                                      name)
304
305         # If a minimum or maximum (length, value) has been specified
306         if expected_type in StringTypes:
307             if min is not None and \
308                len(value.encode(self.api.encoding)) < min:
309                 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
310             if max is not None and \
311                len(value.encode(self.api.encoding)) > max:
312                 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
313         else:
314             if min is not None and value < min:
315                 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
316             if max is not None and value > max:
317                 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
318
319         # If a list with particular types of items is expected
320         if isinstance(expected, (list, tuple)):
321             for i in range(len(value)):
322                 if i >= len(expected):
323                     i = len(expected) - 1
324                 self.type_check(name + "[]", value[i], expected[i], args)
325
326         # If a struct with particular (or required) types of items is
327         # expected.
328         elif isinstance(expected, dict):
329             for key in value.keys():
330                 if key in expected:
331                     self.type_check(name + "['%s']" % key, value[key], expected[key], args)
332             for key, subparam in expected.iteritems():
333                 if isinstance(subparam, Parameter) and \
334                    subparam.optional is not None and \
335                    not subparam.optional and key not in value.keys():
336                     raise PLCInvalidArgument("'%s' not specified" % key, name)
337
338         if auth is not None:
339             auth.check(self, *args)
340
341 def python_type(arg):
342     """
343     Returns the Python type of the specified argument, which may be a
344     Python type, a typed value, or a Parameter.
345     """
346
347     if isinstance(arg, Parameter):
348         arg = arg.type
349
350     if isinstance(arg, type):
351         return arg
352     else:
353         return type(arg)
354
355 def xmlrpc_type(arg):
356     """
357     Returns the XML-RPC type of the specified argument, which may be a
358     Python type, a typed value, or a Parameter.
359     """
360
361     arg_type = python_type(arg)
362
363     if arg_type == NoneType:
364         return "nil"
365     elif arg_type == IntType or arg_type == LongType:
366         return "int"
367     elif arg_type == bool:
368         return "boolean"
369     elif arg_type == FloatType:
370         return "double"
371     elif arg_type in StringTypes:
372         return "string"
373     elif arg_type == ListType or arg_type == TupleType:
374         return "array"
375     elif arg_type == DictType:
376         return "struct"
377     elif arg_type == Mixed:
378         # Not really an XML-RPC type but return "mixed" for
379         # documentation purposes.
380         return "mixed"
381     else:
382         raise PLCAPIError, "XML-RPC cannot marshal %s objects" % arg_type