trimmed useless imports, unstarred all imports
[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
11 from sfa.util.sfalogging import logger
12 from sfa.util.faults import SfaFault, SfaInvalidAPIMethod, SfaInvalidArgumentCount, SfaInvalidArgument
13 from sfa.util.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 SfaAPI 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             if self.api.config.SFA_API_DEBUG:
79                 logger.debug("method.__call__ [%s] : BEG %s"%(self.api.interface,methodname))
80             result = self.call(*args, **kwds)
81
82             runtime = time.time() - start
83             if self.api.config.SFA_API_DEBUG or hasattr(self, 'message'):
84                 logger.debug("method.__call__ [%s] : END %s in %02f s (%s)"%\
85                                        (self.api.interface,methodname,runtime,getattr(self,'message',"[no-msg]")))
86
87             return result
88
89         except SfaFault, fault:
90
91             caller = ""
92
93             # Prepend caller and method name to expected faults
94             fault.faultString = caller + ": " +  self.name + ": " + fault.faultString
95             runtime = time.time() - start
96             logger.log_exc("Method %s raised an exception"%self.name) 
97             raise fault
98
99
100     def help(self, indent = "  "):
101         """
102         Text documentation for the method.
103         """
104
105         (min_args, max_args, defaults) = self.args()
106
107         text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
108
109         text += "Description:\n\n"
110         lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
111         text += "\n".join(lines) + "\n\n"
112
113         def param_text(name, param, indent, step):
114             """
115             Format a method parameter.
116             """
117
118             text = indent
119
120             # Print parameter name
121             if name:
122                 param_offset = 32
123                 text += name.ljust(param_offset - len(indent))
124             else:
125                 param_offset = len(indent)
126
127             # Print parameter type
128             param_type = python_type(param)
129             text += xmlrpc_type(param_type) + "\n"
130
131             # Print parameter documentation right below type
132             if isinstance(param, Parameter):
133                 wrapper = textwrap.TextWrapper(width = 70,
134                                                initial_indent = " " * param_offset,
135                                                subsequent_indent = " " * param_offset)
136                 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
137                 param = param.type
138
139             text += "\n"
140
141             # Indent struct fields and mixed types
142             if isinstance(param, dict):
143                 for name, subparam in param.iteritems():
144                     text += param_text(name, subparam, indent + step, step)
145             elif isinstance(param, Mixed):
146                 for subparam in param:
147                     text += param_text(name, subparam, indent + step, step)
148             elif isinstance(param, (list, tuple, set)):
149                 for subparam in param:
150                     text += param_text("", subparam, indent + step, step)
151
152             return text
153
154         text += "Parameters:\n\n"
155         for name, param in zip(max_args, self.accepts):
156             text += param_text(name, param, indent, indent)
157
158         text += "Returns:\n\n"
159         text += param_text("", self.returns, indent, indent)
160
161         return text
162
163     def args(self):
164         """
165         Returns a tuple:
166
167         ((arg1_name, arg2_name, ...),
168          (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
169          (None, None, ..., optional1_default, optional2_default, ...))
170
171         That represents the minimum and maximum sets of arguments that
172         this function accepts and the defaults for the optional arguments.
173         """
174         
175         # Inspect call. Remove self from the argument list.
176         max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
177         defaults = self.call.func_defaults
178         if defaults is None:
179             defaults = ()
180
181         min_args = max_args[0:len(max_args) - len(defaults)]
182         defaults = tuple([None for arg in min_args]) + defaults
183         
184         return (min_args, max_args, defaults)
185
186     def type_check(self, name, value, expected, args):
187         """
188         Checks the type of the named value against the expected type,
189         which may be a Python type, a typed value, a Parameter, a
190         Mixed type, or a list or dictionary of possibly mixed types,
191         values, Parameters, or Mixed types.
192         
193         Extraneous members of lists must be of the same type as the
194         last specified type. For example, if the expected argument
195         type is [int, bool], then [1, False] and [14, True, False,
196         True] are valid, but [1], [False, 1] and [14, True, 1] are
197         not.
198
199         Extraneous members of dictionaries are ignored.
200         """
201
202         # If any of a number of types is acceptable
203         if isinstance(expected, Mixed):
204             for item in expected:
205                 try:
206                     self.type_check(name, value, item, args)
207                     return
208                 except SfaInvalidArgument, fault:
209                     pass
210             raise fault
211
212         # If an authentication structure is expected, save it and
213         # authenticate after basic type checking is done.
214         #if isinstance(expected, Auth):
215         #    auth = expected
216         #else:
217         #    auth = None
218
219         # Get actual expected type from within the Parameter structure
220         if isinstance(expected, Parameter):
221             min = expected.min
222             max = expected.max
223             nullok = expected.nullok
224             expected = expected.type
225         else:
226             min = None
227             max = None
228             nullok = False
229
230         expected_type = python_type(expected)
231
232         # If value can be NULL
233         if value is None and nullok:
234             return
235
236         # Strings are a special case. Accept either unicode or str
237         # types if a string is expected.
238         if expected_type in StringTypes and isinstance(value, StringTypes):
239             pass
240
241         # Integers and long integers are also special types. Accept
242         # either int or long types if an int or long is expected.
243         elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
244             pass
245
246         elif not isinstance(value, expected_type):
247             raise SfaInvalidArgument("expected %s, got %s" % \
248                                      (xmlrpc_type(expected_type),
249                                       xmlrpc_type(type(value))),
250                                      name)
251
252         # If a minimum or maximum (length, value) has been specified
253         if expected_type in StringTypes:
254             if min is not None and \
255                len(value.encode(self.api.encoding)) < min:
256                 raise SfaInvalidArgument, "%s must be at least %d bytes long" % (name, min)
257             if max is not None and \
258                len(value.encode(self.api.encoding)) > max:
259                 raise SfaInvalidArgument, "%s must be at most %d bytes long" % (name, max)
260         elif expected_type in (list, tuple, set):
261             if min is not None and len(value) < min:
262                 raise SfaInvalidArgument, "%s must contain at least %d items" % (name, min)
263             if max is not None and len(value) > max:
264                 raise SfaInvalidArgument, "%s must contain at most %d items" % (name, max)
265         else:
266             if min is not None and value < min:
267                 raise SfaInvalidArgument, "%s must be > %s" % (name, str(min))
268             if max is not None and value > max:
269                 raise SfaInvalidArgument, "%s must be < %s" % (name, str(max))
270
271         # If a list with particular types of items is expected
272         if isinstance(expected, (list, tuple, set)):
273             for i in range(len(value)):
274                 if i >= len(expected):
275                     j = len(expected) - 1
276                 else:
277                     j = i
278                 self.type_check(name + "[]", value[i], expected[j], args)
279
280         # If a struct with particular (or required) types of items is
281         # expected.
282         elif isinstance(expected, dict):
283             for key in value.keys():
284                 if key in expected:
285                     self.type_check(name + "['%s']" % key, value[key], expected[key], args)
286             for key, subparam in expected.iteritems():
287                 if isinstance(subparam, Parameter) and \
288                    subparam.optional is not None and \
289                    not subparam.optional and key not in value.keys():
290                     raise SfaInvalidArgument("'%s' not specified" % key, name)
291
292         #if auth is not None:
293         #    auth.check(self, *args)