cosmetic
[sfa.git] / sfa / util / method.py
1 #
2 # Base class for all SfaAPI functions
3 #
4 #
5
6 import os, time
7 from types import *
8 from types import StringTypes
9 import traceback
10 import textwrap
11 import xmlrpclib
12
13
14 from sfa.util.sfalogging import sfa_logger
15 from sfa.util.faults import * 
16 from sfa.util.parameter import Parameter, Mixed, python_type, xmlrpc_type
17 from sfa.trust.auth import Auth
18
19 class Method:
20     """
21     Base class for all SfaAPI functions. At a minimum, all SfaAPI
22     functions must define:
23
24     interfaces = [allowed interfaces]
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     interfaces = []
40     accepts = []
41     returns = bool
42     status = "current"
43
44     def call(self, *args):
45         """
46         Method body for all SfaAPI functions. Must override.
47         """
48         return None
49
50     def __init__(self, api):
51         self.name = self.__class__.__name__
52         self.api = api
53
54         # Auth may set this to a Person instance (if an anonymous
55         # method, will remain None).
56         self.caller = None
57
58         # API may set this to a (addr, port) tuple if known
59         self.source = None
60         
61     def __call__(self, *args, **kwds):
62         """
63         Main entry point for all SfaAPI functions. Type checks
64         arguments, authenticates, and executes call().
65         """
66
67         try:
68             start = time.time()
69             methodname = self.name
70             if not self.api.interface or self.api.interface not in self.interfaces:
71                 raise SfaInvalidAPIMethod(methodname, self.api.interface) 
72
73             (min_args, max_args, defaults) = self.args()
74                         
75             # Check that the right number of arguments were passed in
76             if len(args) < len(min_args) or len(args) > len(max_args):
77                 raise SfaInvalidArgumentCount(len(args), len(min_args), len(max_args))
78
79             for name, value, expected in zip(max_args, args, self.accepts):
80                 self.type_check(name, value, expected, args)
81
82             if self.api.config.SFA_API_DEBUG:
83                 sfa_logger().debug("method.__call__ calling method %s"%methodname)
84             result = self.call(*args, **kwds)
85
86             runtime = time.time() - start
87             if self.api.config.SFA_API_DEBUG or hasattr(self, 'message'):
88                 sfa_logger().debug("method.__call__ %s completed in %02f s (%s)"%(methodname,runtime,getattr(self,'message',"[no-msg]")))
89
90             return result
91
92         except SfaFault, fault:
93
94             caller = ""
95
96             # Prepend caller and method name to expected faults
97             fault.faultString = caller + ": " +  self.name + ": " + fault.faultString
98             runtime = time.time() - start
99             sfa_logger().log_exc("Method %s raised an exception"%self.name) 
100             raise fault
101
102
103     def help(self, indent = "  "):
104         """
105         Text documentation for the method.
106         """
107
108         (min_args, max_args, defaults) = self.args()
109
110         text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
111
112         text += "Description:\n\n"
113         lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
114         text += "\n".join(lines) + "\n\n"
115
116         def param_text(name, param, indent, step):
117             """
118             Format a method parameter.
119             """
120
121             text = indent
122
123             # Print parameter name
124             if name:
125                 param_offset = 32
126                 text += name.ljust(param_offset - len(indent))
127             else:
128                 param_offset = len(indent)
129
130             # Print parameter type
131             param_type = python_type(param)
132             text += xmlrpc_type(param_type) + "\n"
133
134             # Print parameter documentation right below type
135             if isinstance(param, Parameter):
136                 wrapper = textwrap.TextWrapper(width = 70,
137                                                initial_indent = " " * param_offset,
138                                                subsequent_indent = " " * param_offset)
139                 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
140                 param = param.type
141
142             text += "\n"
143
144             # Indent struct fields and mixed types
145             if isinstance(param, dict):
146                 for name, subparam in param.iteritems():
147                     text += param_text(name, subparam, indent + step, step)
148             elif isinstance(param, Mixed):
149                 for subparam in param:
150                     text += param_text(name, subparam, indent + step, step)
151             elif isinstance(param, (list, tuple, set)):
152                 for subparam in param:
153                     text += param_text("", subparam, indent + step, step)
154
155             return text
156
157         text += "Parameters:\n\n"
158         for name, param in zip(max_args, self.accepts):
159             text += param_text(name, param, indent, indent)
160
161         text += "Returns:\n\n"
162         text += param_text("", self.returns, indent, indent)
163
164         return text
165
166     def args(self):
167         """
168         Returns a tuple:
169
170         ((arg1_name, arg2_name, ...),
171          (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
172          (None, None, ..., optional1_default, optional2_default, ...))
173
174         That represents the minimum and maximum sets of arguments that
175         this function accepts and the defaults for the optional arguments.
176         """
177         
178         # Inspect call. Remove self from the argument list.
179         max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
180         defaults = self.call.func_defaults
181         if defaults is None:
182             defaults = ()
183
184         min_args = max_args[0:len(max_args) - len(defaults)]
185         defaults = tuple([None for arg in min_args]) + defaults
186         
187         return (min_args, max_args, defaults)
188
189     def type_check(self, name, value, expected, args):
190         """
191         Checks the type of the named value against the expected type,
192         which may be a Python type, a typed value, a Parameter, a
193         Mixed type, or a list or dictionary of possibly mixed types,
194         values, Parameters, or Mixed types.
195         
196         Extraneous members of lists must be of the same type as the
197         last specified type. For example, if the expected argument
198         type is [int, bool], then [1, False] and [14, True, False,
199         True] are valid, but [1], [False, 1] and [14, True, 1] are
200         not.
201
202         Extraneous members of dictionaries are ignored.
203         """
204
205         # If any of a number of types is acceptable
206         if isinstance(expected, Mixed):
207             for item in expected:
208                 try:
209                     self.type_check(name, value, item, args)
210                     return
211                 except SfaInvalidArgument, fault:
212                     pass
213             raise fault
214
215         # If an authentication structure is expected, save it and
216         # authenticate after basic type checking is done.
217         #if isinstance(expected, Auth):
218         #    auth = expected
219         #else:
220         #    auth = None
221
222         # Get actual expected type from within the Parameter structure
223         if isinstance(expected, Parameter):
224             min = expected.min
225             max = expected.max
226             nullok = expected.nullok
227             expected = expected.type
228         else:
229             min = None
230             max = None
231             nullok = False
232
233         expected_type = python_type(expected)
234
235         # If value can be NULL
236         if value is None and nullok:
237             return
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 SfaInvalidArgument("expected %s, got %s" % \
251                                      (xmlrpc_type(expected_type),
252                                       xmlrpc_type(type(value))),
253                                      name)
254
255         # If a minimum or maximum (length, value) has been specified
256         if expected_type in StringTypes:
257             if min is not None and \
258                len(value.encode(self.api.encoding)) < min:
259                 raise SfaInvalidArgument, "%s must be at least %d bytes long" % (name, min)
260             if max is not None and \
261                len(value.encode(self.api.encoding)) > max:
262                 raise SfaInvalidArgument, "%s must be at most %d bytes long" % (name, max)
263         elif expected_type in (list, tuple, set):
264             if min is not None and len(value) < min:
265                 raise SfaInvalidArgument, "%s must contain at least %d items" % (name, min)
266             if max is not None and len(value) > max:
267                 raise SfaInvalidArgument, "%s must contain at most %d items" % (name, max)
268         else:
269             if min is not None and value < min:
270                 raise SfaInvalidArgument, "%s must be > %s" % (name, str(min))
271             if max is not None and value > max:
272                 raise SfaInvalidArgument, "%s must be < %s" % (name, str(max))
273
274         # If a list with particular types of items is expected
275         if isinstance(expected, (list, tuple, set)):
276             for i in range(len(value)):
277                 if i >= len(expected):
278                     j = len(expected) - 1
279                 else:
280                     j = i
281                 self.type_check(name + "[]", value[i], expected[j], args)
282
283         # If a struct with particular (or required) types of items is
284         # expected.
285         elif isinstance(expected, dict):
286             for key in value.keys():
287                 if key in expected:
288                     self.type_check(name + "['%s']" % key, value[key], expected[key], args)
289             for key, subparam in expected.iteritems():
290                 if isinstance(subparam, Parameter) and \
291                    subparam.optional is not None and \
292                    not subparam.optional and key not in value.keys():
293                     raise SfaInvalidArgument("'%s' not specified" % key, name)
294
295         #if auth is not None:
296         #    auth.check(self, *args)