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