b3d7d1a7808ad23a81f75324b6c8476ada0f2e44
[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
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
16 class Method:
17     """
18     Base class for all SfaAPI functions. At a minimum, all SfaAPI
19     functions must define:
20
21     interfaces = [allowed interfaces]
22     accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
23     returns = Parameter(return_type, return_doc)
24     call(arg1, arg2, ...): method body
25
26     Argument types may be Python types (e.g., int, bool, etc.), typed
27     values (e.g., 1, True, etc.), a Parameter, or lists or
28     dictionaries of possibly mixed types, values, and/or Parameters
29     (e.g., [int, bool, ...]  or {'arg1': int, 'arg2': bool}).
30
31     Once function decorators in Python 2.4 are fully supported,
32     consider wrapping calls with accepts() and returns() functions
33     instead of performing type checking manually.
34     """
35
36     interfaces = []
37     accepts = []
38     returns = bool
39     status = "current"
40
41     def call(self, *args):
42         """
43         Method body for all SfaAPI functions. Must override.
44         """
45         return None
46
47     def __init__(self, api):
48         self.name = self.__class__.__name__
49         self.api = api
50
51         # Auth may set this to a Person instance (if an anonymous
52         # method, will remain None).
53         self.caller = None
54
55         # API may set this to a (addr, port) tuple if known
56         self.source = None
57
58     def __call__(self, *args, **kwds):
59         """
60         Main entry point for all SFA API functions. Type checks
61         arguments, authenticates, and executes call().
62         """
63
64         try:
65             start = time.time()
66             methodname = self.name
67             if not self.api.interface or self.api.interface not in self.interfaces:
68                 raise SfaInvalidAPIMethod(methodname, self.api.interface)
69
70             (min_args, max_args, defaults) = self.args()
71
72             # Check that the right number of arguments were passed in
73             if len(args) < len(min_args) or len(args) > len(max_args):
74                 raise SfaInvalidArgumentCount(
75                     len(args), len(min_args), len(max_args))
76
77             for name, value, expected in zip(max_args, args, self.accepts):
78                 self.type_check(name, value, expected, args)
79
80             logger.debug("method.__call__ [%s] : BEG %s" % (
81                 self.api.interface, methodname))
82             result = self.call(*args, **kwds)
83
84             runtime = time.time() - start
85             logger.debug("method.__call__ [%s] : END %s in %02f s (%s)" %
86                          (self.api.interface, methodname, runtime, getattr(self, 'message', "[no-msg]")))
87
88             return result
89
90         except SfaFault as fault:
91
92             caller = ""
93
94             # Prepend caller and method name to expected faults
95             fault.faultString = caller + ": " + self.name + ": " + fault.faultString
96             runtime = time.time() - start
97             logger.log_exc("Method %s raised an exception" % self.name)
98             raise fault
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,
108                                      ", ".join(max_args), xmlrpc_type(self.returns))
109
110         text += "Description:\n\n"
111         lines = [indent + line.strip()
112                  for line in self.__doc__.strip().split("\n")]
113         text += "\n".join(lines) + "\n\n"
114
115         def param_text(name, param, indent, step):
116             """
117             Format a method parameter.
118             """
119
120             text = indent
121
122             # Print parameter name
123             if name:
124                 param_offset = 32
125                 text += name.ljust(param_offset - len(indent))
126             else:
127                 param_offset = len(indent)
128
129             # Print parameter type
130             param_type = python_type(param)
131             text += xmlrpc_type(param_type) + "\n"
132
133             # Print parameter documentation right below type
134             if isinstance(param, Parameter):
135                 wrapper = textwrap.TextWrapper(width=70,
136                                                initial_indent=" " * param_offset,
137                                                subsequent_indent=" " * param_offset)
138                 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
139                 param = param.type
140
141             text += "\n"
142
143             # Indent struct fields and mixed types
144             if isinstance(param, dict):
145                 for name, subparam in param.items():
146                     text += param_text(name, subparam, indent + step, step)
147             elif isinstance(param, Mixed):
148                 for subparam in param:
149                     text += param_text(name, subparam, indent + step, step)
150             elif isinstance(param, (list, tuple, set)):
151                 for subparam in param:
152                     text += param_text("", subparam, indent + step, step)
153
154             return text
155
156         text += "Parameters:\n\n"
157         for name, param in zip(max_args, self.accepts):
158             text += param_text(name, param, indent, indent)
159
160         text += "Returns:\n\n"
161         text += param_text("", self.returns, indent, indent)
162
163         return text
164
165     def args(self):
166         """
167         Returns a tuple:
168
169         ((arg1_name, arg2_name, ...),
170          (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
171          (None, None, ..., optional1_default, optional2_default, ...))
172
173         That represents the minimum and maximum sets of arguments that
174         this function accepts and the defaults for the optional arguments.
175         """
176
177         # Inspect call. Remove self from the argument list.
178         max_args = self.call.__code__.co_varnames[
179             1:self.call.__code__.co_argcount]
180         defaults = self.call.__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 as 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 issubclass(expected_type, str) and isinstance(value, str):
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 issubclass(expected_type, str):
257             if min is not None and \
258                len(value.encode(self.api.encoding)) < min:
259                 raise SfaInvalidArgument(
260                     "%s must be at least %d bytes long" % (name, min))
261             if max is not None and \
262                len(value.encode(self.api.encoding)) > max:
263                 raise SfaInvalidArgument(
264                     "%s must be at most %d bytes long" % (name, max))
265         elif expected_type in (list, tuple, set):
266             if min is not None and len(value) < min:
267                 raise SfaInvalidArgument(
268                     "%s must contain at least %d items" % (name, min))
269             if max is not None and len(value) > max:
270                 raise SfaInvalidArgument(
271                     "%s must contain at most %d items" % (name, max))
272         else:
273             if min is not None and value < min:
274                 raise SfaInvalidArgument("%s must be > %s" % (name, str(min)))
275             if max is not None and value > max:
276                 raise SfaInvalidArgument("%s must be < %s" % (name, str(max)))
277
278         # If a list with particular types of items is expected
279         if isinstance(expected, (list, tuple, set)):
280             for i in range(len(value)):
281                 if i >= len(expected):
282                     j = len(expected) - 1
283                 else:
284                     j = i
285                 self.type_check(name + "[]", value[i], expected[j], args)
286
287         # If a struct with particular (or required) types of items is
288         # expected.
289         elif isinstance(expected, dict):
290             for key in list(value.keys()):
291                 if key in expected:
292                     self.type_check(name + "['%s']" %
293                                     key, value[key], expected[key], args)
294             for key, subparam in expected.items():
295                 if isinstance(subparam, Parameter) and \
296                    subparam.optional is not None and \
297                    not subparam.optional and key not in list(value.keys()):
298                     raise SfaInvalidArgument("'%s' not specified" % key, name)
299
300         # if auth is not None:
301         #    auth.check(self, *args)