python3 - 2to3 + miscell obvious tweaks
[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.py23 import StringType
12 from sfa.util.faults import SfaFault, SfaInvalidAPIMethod, SfaInvalidArgumentCount, SfaInvalidArgument
13
14 from sfa.storage.parameter import Parameter, Mixed, python_type, xmlrpc_type
15
16
17 class Method:
18     """
19     Base class for all SfaAPI functions. At a minimum, all SfaAPI
20     functions must define:
21
22     interfaces = [allowed interfaces]
23     accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
24     returns = Parameter(return_type, return_doc)
25     call(arg1, arg2, ...): method body
26
27     Argument types may be Python types (e.g., int, bool, etc.), typed
28     values (e.g., 1, True, etc.), a Parameter, or lists or
29     dictionaries of possibly mixed types, values, and/or Parameters
30     (e.g., [int, bool, ...]  or {'arg1': int, 'arg2': bool}).
31
32     Once function decorators in Python 2.4 are fully supported,
33     consider wrapping calls with accepts() and returns() functions
34     instead of performing type checking manually.
35     """
36
37     interfaces = []
38     accepts = []
39     returns = bool
40     status = "current"
41
42     def call(self, *args):
43         """
44         Method body for all SfaAPI functions. Must override.
45         """
46         return None
47
48     def __init__(self, api):
49         self.name = self.__class__.__name__
50         self.api = api
51
52         # Auth may set this to a Person instance (if an anonymous
53         # method, will remain None).
54         self.caller = None
55
56         # API may set this to a (addr, port) tuple if known
57         self.source = None
58
59     def __call__(self, *args, **kwds):
60         """
61         Main entry point for all SFA API functions. Type checks
62         arguments, authenticates, and executes call().
63         """
64
65         try:
66             start = time.time()
67             methodname = self.name
68             if not self.api.interface or self.api.interface not in self.interfaces:
69                 raise SfaInvalidAPIMethod(methodname, self.api.interface)
70
71             (min_args, max_args, defaults) = self.args()
72
73             # Check that the right number of arguments were passed in
74             if len(args) < len(min_args) or len(args) > len(max_args):
75                 raise SfaInvalidArgumentCount(
76                     len(args), len(min_args), len(max_args))
77
78             for name, value, expected in zip(max_args, args, self.accepts):
79                 self.type_check(name, value, expected, args)
80
81             logger.debug("method.__call__ [%s] : BEG %s" % (
82                 self.api.interface, methodname))
83             result = self.call(*args, **kwds)
84
85             runtime = time.time() - start
86             logger.debug("method.__call__ [%s] : END %s in %02f s (%s)" %
87                          (self.api.interface, methodname, runtime, getattr(self, 'message', "[no-msg]")))
88
89             return result
90
91         except SfaFault as fault:
92
93             caller = ""
94
95             # Prepend caller and method name to expected faults
96             fault.faultString = caller + ": " + self.name + ": " + fault.faultString
97             runtime = time.time() - start
98             logger.log_exc("Method %s raised an exception" % self.name)
99             raise fault
100
101     def help(self, indent="  "):
102         """
103         Text documentation for the method.
104         """
105
106         (min_args, max_args, defaults) = self.args()
107
108         text = "%s(%s) -> %s\n\n" % (self.name,
109                                      ", ".join(max_args), xmlrpc_type(self.returns))
110
111         text += "Description:\n\n"
112         lines = [indent + line.strip()
113                  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.items():
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.__code__.co_varnames[
180             1:self.call.__code__.co_argcount]
181         defaults = self.call.__defaults__
182         if defaults is None:
183             defaults = ()
184
185         min_args = max_args[0:len(max_args) - len(defaults)]
186         defaults = tuple([None for arg in min_args]) + defaults
187
188         return (min_args, max_args, defaults)
189
190     def type_check(self, name, value, expected, args):
191         """
192         Checks the type of the named value against the expected type,
193         which may be a Python type, a typed value, a Parameter, a
194         Mixed type, or a list or dictionary of possibly mixed types,
195         values, Parameters, or Mixed types.
196
197         Extraneous members of lists must be of the same type as the
198         last specified type. For example, if the expected argument
199         type is [int, bool], then [1, False] and [14, True, False,
200         True] are valid, but [1], [False, 1] and [14, True, 1] are
201         not.
202
203         Extraneous members of dictionaries are ignored.
204         """
205
206         # If any of a number of types is acceptable
207         if isinstance(expected, Mixed):
208             for item in expected:
209                 try:
210                     self.type_check(name, value, item, args)
211                     return
212                 except SfaInvalidArgument as fault:
213                     pass
214             raise fault
215
216         # If an authentication structure is expected, save it and
217         # authenticate after basic type checking is done.
218         # if isinstance(expected, Auth):
219         #    auth = expected
220         # else:
221         #    auth = None
222
223         # Get actual expected type from within the Parameter structure
224         if isinstance(expected, Parameter):
225             min = expected.min
226             max = expected.max
227             nullok = expected.nullok
228             expected = expected.type
229         else:
230             min = None
231             max = None
232             nullok = False
233
234         expected_type = python_type(expected)
235
236         # If value can be NULL
237         if value is None and nullok:
238             return
239
240         # Strings are a special case. Accept either unicode or str
241         # types if a string is expected.
242         if issubclass(expected_type, StringType) and isinstance(value, StringType):
243             pass
244
245         # Integers and long integers are also special types. Accept
246         # either int or long types if an int or long is expected.
247         elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
248             pass
249
250         elif not isinstance(value, expected_type):
251             raise SfaInvalidArgument("expected %s, got %s" %
252                                      (xmlrpc_type(expected_type),
253                                       xmlrpc_type(type(value))),
254                                      name)
255
256         # If a minimum or maximum (length, value) has been specified
257         if issubclass(expected_type, StringType):
258             if min is not None and \
259                len(value.encode(self.api.encoding)) < min:
260                 raise SfaInvalidArgument(
261                     "%s must be at least %d bytes long" % (name, min))
262             if max is not None and \
263                len(value.encode(self.api.encoding)) > max:
264                 raise SfaInvalidArgument(
265                     "%s must be at most %d bytes long" % (name, max))
266         elif expected_type in (list, tuple, set):
267             if min is not None and len(value) < min:
268                 raise SfaInvalidArgument(
269                     "%s must contain at least %d items" % (name, min))
270             if max is not None and len(value) > max:
271                 raise SfaInvalidArgument(
272                     "%s must contain at most %d items" % (name, max))
273         else:
274             if min is not None and value < min:
275                 raise SfaInvalidArgument("%s must be > %s" % (name, str(min)))
276             if max is not None and value > max:
277                 raise SfaInvalidArgument("%s must be < %s" % (name, str(max)))
278
279         # If a list with particular types of items is expected
280         if isinstance(expected, (list, tuple, set)):
281             for i in range(len(value)):
282                 if i >= len(expected):
283                     j = len(expected) - 1
284                 else:
285                     j = i
286                 self.type_check(name + "[]", value[i], expected[j], args)
287
288         # If a struct with particular (or required) types of items is
289         # expected.
290         elif isinstance(expected, dict):
291             for key in list(value.keys()):
292                 if key in expected:
293                     self.type_check(name + "['%s']" %
294                                     key, value[key], expected[key], args)
295             for key, subparam in expected.items():
296                 if isinstance(subparam, Parameter) and \
297                    subparam.optional is not None and \
298                    not subparam.optional and key not in list(value.keys()):
299                     raise SfaInvalidArgument("'%s' not specified" % key, name)
300
301         # if auth is not None:
302         #    auth.check(self, *args)