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