484b77b3a0e30b6508c2b546eb4e3109459762e7
[sfa.git] / sfa / util / method.py
1 #
2 # Base class for all SfaAPI functions
3 #
4 #
5
6 ### $Id$
7 ### $URL$
8
9 import os, time
10 from types import *
11 from types import StringTypes
12 import traceback
13 import textwrap
14 import xmlrpclib
15
16
17 import sfa.util.sfalogging
18 from sfa.util.faults import * 
19 from sfa.util.parameter import Parameter, Mixed, python_type, xmlrpc_type
20 from sfa.trust.auth import Auth
21 #from sfa.util.debug import profile, log
22
23 # we inherit object because we use new-style classes for legacy methods
24 class Method (object):
25     """
26     Base class for all SfaAPI functions. At a minimum, all SfaAPI
27     functions must define:
28
29     interfaces = [allowed interfaces]
30     accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
31     returns = Parameter(return_type, return_doc)
32     call(arg1, arg2, ...): method body
33
34     Argument types may be Python types (e.g., int, bool, etc.), typed
35     values (e.g., 1, True, etc.), a Parameter, or lists or
36     dictionaries of possibly mixed types, values, and/or Parameters
37     (e.g., [int, bool, ...]  or {'arg1': int, 'arg2': bool}).
38
39     Once function decorators in Python 2.4 are fully supported,
40     consider wrapping calls with accepts() and returns() functions
41     instead of performing type checking manually.
42     """
43
44     interfaces = []
45     accepts = []
46     returns = bool
47     status = "current"
48
49     def call(self, *args):
50         """
51         Method body for all SfaAPI functions. Must override.
52
53         """
54
55         return True
56
57     def __init__(self, api):
58         self.name = self.__class__.__name__
59         self.api = api
60
61         # Auth may set this to a Person instance (if an anonymous
62         # method, will remain None).
63         self.caller = None
64
65         # API may set this to a (addr, port) tuple if known
66         self.source = None
67         
68     def __call__(self, *args, **kwds):
69         """
70         Main entry point for all SfaAPI functions. Type checks
71         arguments, authenticates, and executes call().
72         """
73
74         try:
75             start = time.time()
76             methodname = self.name
77             if not self.api.interface or self.api.interface not in self.interfaces:
78                 raise SfaInvalidAPIMethod(methodname, self.api.interface) 
79
80             # legacy code cannot be type-checked, due to the way Method.args() works
81             if not hasattr(self,"skip_typecheck"):
82                 (min_args, max_args, defaults) = self.args()
83                                 
84                 # Check that the right number of arguments were passed in
85                 if len(args) < len(min_args) or len(args) > len(max_args):
86                     raise SfaInvalidArgumentCount(len(args), len(min_args), len(max_args))
87
88                 for name, value, expected in zip(max_args, args, self.accepts):
89                     self.type_check(name, value, expected, args)
90
91             result = self.call(*args, **kwds)
92             runtime = time.time() - start
93
94             if self.api.config.SFA_API_DEBUG or hasattr(self, 'message'):
95                 msg=getattr(self,'message',"method %s completed"%methodname)
96                 sfa.util.sfalogging.logger.info(msg)
97                 # XX print to some log file
98                 # print >> log, "some output"
99
100             return result
101
102         except SfaFault, fault:
103
104             caller = ""
105
106             # Prepend caller and method name to expected faults
107             fault.faultString = caller + ": " +  self.name + ": " + fault.faultString
108             runtime = time.time() - start
109 #            if self.api.config.SFA_API_DEBUG:
110 #                traceback.print_exc()
111             sfa.util.sfalogging.log_exc("Method %s raised an exception"%self.name) 
112             raise fault
113
114
115     def help(self, indent = "  "):
116         """
117         Text documentation for the method.
118         """
119
120         (min_args, max_args, defaults) = self.args()
121
122         text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
123
124         text += "Description:\n\n"
125         lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
126         text += "\n".join(lines) + "\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, set)):
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, args):
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, args)
222                     return
223                 except SfaInvalidArgument, fault:
224                     pass
225             raise fault
226
227         # If an authentication structure is expected, save it and
228         # authenticate after basic type checking is done.
229         #if isinstance(expected, Auth):
230         #    auth = expected
231         #else:
232         #    auth = None
233
234         # Get actual expected type from within the Parameter structure
235         if isinstance(expected, Parameter):
236             min = expected.min
237             max = expected.max
238             nullok = expected.nullok
239             expected = expected.type
240         else:
241             min = None
242             max = None
243             nullok = False
244
245         expected_type = python_type(expected)
246
247         # If value can be NULL
248         if value is None and nullok:
249             return
250
251         # Strings are a special case. Accept either unicode or str
252         # types if a string is expected.
253         if expected_type in StringTypes and isinstance(value, StringTypes):
254             pass
255
256         # Integers and long integers are also special types. Accept
257         # either int or long types if an int or long is expected.
258         elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
259             pass
260
261         elif not isinstance(value, expected_type):
262             raise SfaInvalidArgument("expected %s, got %s" % \
263                                      (xmlrpc_type(expected_type),
264                                       xmlrpc_type(type(value))),
265                                      name)
266
267         # If a minimum or maximum (length, value) has been specified
268         if expected_type in StringTypes:
269             if min is not None and \
270                len(value.encode(self.api.encoding)) < min:
271                 raise SfaInvalidArgument, "%s must be at least %d bytes long" % (name, min)
272             if max is not None and \
273                len(value.encode(self.api.encoding)) > max:
274                 raise SfaInvalidArgument, "%s must be at most %d bytes long" % (name, max)
275         elif expected_type in (list, tuple, set):
276             if min is not None and len(value) < min:
277                 raise SfaInvalidArgument, "%s must contain at least %d items" % (name, min)
278             if max is not None and len(value) > max:
279                 raise SfaInvalidArgument, "%s must contain at most %d items" % (name, max)
280         else:
281             if min is not None and value < min:
282                 raise SfaInvalidArgument, "%s must be > %s" % (name, str(min))
283             if max is not None and value > max:
284                 raise SfaInvalidArgument, "%s must be < %s" % (name, str(max))
285
286         # If a list with particular types of items is expected
287         if isinstance(expected, (list, tuple, set)):
288             for i in range(len(value)):
289                 if i >= len(expected):
290                     j = len(expected) - 1
291                 else:
292                     j = i
293                 self.type_check(name + "[]", value[i], expected[j], args)
294
295         # If a struct with particular (or required) types of items is
296         # expected.
297         elif isinstance(expected, dict):
298             for key in value.keys():
299                 if key in expected:
300                     self.type_check(name + "['%s']" % key, value[key], expected[key], args)
301             for key, subparam in expected.iteritems():
302                 if isinstance(subparam, Parameter) and \
303                    subparam.optional is not None and \
304                    not subparam.optional and key not in value.keys():
305                     raise SfaInvalidArgument("'%s' not specified" % key, name)
306
307         #if auth is not None:
308         #    auth.check(self, *args)