Cleanup : removed unused functions.
[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 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), xmlrpc_type(type(value))),
249                                      name)
250
251         # If a minimum or maximum (length, value) has been specified
252         if expected_type in StringTypes:
253             if min is not None and \
254                len(value.encode(self.api.encoding)) < min:
255                 raise SfaInvalidArgument, "%s must be at least %d bytes long" % (name, min)
256             if max is not None and \
257                len(value.encode(self.api.encoding)) > max:
258                 raise SfaInvalidArgument, "%s must be at most %d bytes long" % (name, max)
259         elif expected_type in (list, tuple, set):
260             if min is not None and len(value) < min:
261                 raise SfaInvalidArgument, "%s must contain at least %d items" % (name, min)
262             if max is not None and len(value) > max:
263                 raise SfaInvalidArgument, "%s must contain at most %d items" % (name, max)
264         else:
265             if min is not None and value < min:
266                 raise SfaInvalidArgument, "%s must be > %s" % (name, str(min))
267             if max is not None and value > max:
268                 raise SfaInvalidArgument, "%s must be < %s" % (name, str(max))
269
270         # If a list with particular types of items is expected
271         if isinstance(expected, (list, tuple, set)):
272             for i in range(len(value)):
273                 if i >= len(expected):
274                     j = len(expected) - 1
275                 else:
276                     j = i
277                 self.type_check(name + "[]", value[i], expected[j], args)
278
279         # If a struct with particular (or required) types of items is
280         # expected.
281         elif isinstance(expected, dict):
282             for key in value.keys():
283                 if key in expected:
284                     self.type_check(name + "['%s']" % key, value[key], expected[key], args)
285             for key, subparam in expected.iteritems():
286                 if isinstance(subparam, Parameter) and \
287                    subparam.optional is not None and \
288                    not subparam.optional and key not in value.keys():
289                     raise SfaInvalidArgument("'%s' not specified" % key, name)
290
291         #if auth is not None:
292         #    auth.check(self, *args)