review on imports & svn keywords
[sfa.git] / geni / util / method.py
1 #
2 # Base class for all GeniAPI functions
3 #
4 #
5
6 ### $Id$
7 ### $URL$
8
9 import xmlrpclib
10 from types import *
11 import textwrap
12 import os
13 import time
14 import pprint
15
16 from types import StringTypes
17
18 from geni.util.faults import * 
19 from geni.util.parameter import Parameter, Mixed, python_type, xmlrpc_type
20 from geni.util.auth import Auth
21 from geni.util.debug import profile, log
22
23 # we inherit object because we use new-style classes for legacy methods
24 class Method (object):
25     """
26     Base class for all GeniAPI functions. At a minimum, all GeniAPI
27     functions must define:
28
29     interfaces = [allowed interfaces]
30     accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
31     returns = Parameter(return_type, return_doc)
32     call(arg1, arg2, ...): method body
33
34     Argument types may be Python types (e.g., int, bool, etc.), typed
35     values (e.g., 1, True, etc.), a Parameter, or lists or
36     dictionaries of possibly mixed types, values, and/or Parameters
37     (e.g., [int, bool, ...]  or {'arg1': int, 'arg2': bool}).
38
39     Once function decorators in Python 2.4 are fully supported,
40     consider wrapping calls with accepts() and returns() functions
41     instead of performing type checking manually.
42     """
43
44     interfaces = []
45     accepts = []
46     returns = bool
47     status = "current"
48
49     def call(self, *args):
50         """
51         Method body for all GeniAPI functions. Must override.
52
53         """
54
55         return True
56
57     def __init__(self, api):
58         self.name = self.__class__.__name__
59         self.api = api
60
61         # Auth may set this to a Person instance (if an anonymous
62         # method, will remain None).
63         self.caller = None
64
65         # API may set this to a (addr, port) tuple if known
66         self.source = None
67         
68     def __call__(self, *args, **kwds):
69         """
70         Main entry point for all GeniAPI functions. Type checks
71         arguments, authenticates, and executes call().
72         """
73
74         try:
75             start = time.time()
76             methodname = self.name
77             if not self.api.interface or self.api.interface not in self.interfaces:
78                 raise GeniInvalidAPIMethod, methodname, self.api.interface 
79
80             # legacy code cannot be type-checked, due to the way Method.args() works
81             if not hasattr(self,"skip_typecheck"):
82                 (min_args, max_args, defaults) = self.args()
83                                 
84                 # Check that the right number of arguments were passed in
85                 if len(args) < len(min_args) or len(args) > len(max_args):
86                     raise GeniInvalidArgumentCount(len(args), len(min_args), len(max_args))
87
88                 for name, value, expected in zip(max_args, args, self.accepts):
89                     self.type_check(name, value, expected, args)
90
91             result = self.call(*args, **kwds)
92             runtime = time.time() - start
93
94             if self.api.config.GENI_API_DEBUG or hasattr(self, 'message'):
95                 # XX print to some log file
96                 # print >> log, "some output"
97                     pass
98
99             return result
100
101         except GeniFault, fault:
102
103             caller = ""
104
105             # Prepend caller and method name to expected faults
106             fault.faultString = caller + ": " +  self.name + ": " + fault.faultString
107             runtime = time.time() - start
108             
109             if self.api.config.GENI_API_DEBUG:
110                 # XX print to some log file
111                 #print >> log, "Some debugging output"              
112                 pass 
113             raise fault
114
115
116     def help(self, indent = "  "):
117         """
118         Text documentation for the method.
119         """
120
121         (min_args, max_args, defaults) = self.args()
122
123         text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
124
125         text += "Description:\n\n"
126         lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
127         text += "\n".join(lines) + "\n\n"
128
129         def param_text(name, param, indent, step):
130             """
131             Format a method parameter.
132             """
133
134             text = indent
135
136             # Print parameter name
137             if name:
138                 param_offset = 32
139                 text += name.ljust(param_offset - len(indent))
140             else:
141                 param_offset = len(indent)
142
143             # Print parameter type
144             param_type = python_type(param)
145             text += xmlrpc_type(param_type) + "\n"
146
147             # Print parameter documentation right below type
148             if isinstance(param, Parameter):
149                 wrapper = textwrap.TextWrapper(width = 70,
150                                                initial_indent = " " * param_offset,
151                                                subsequent_indent = " " * param_offset)
152                 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
153                 param = param.type
154
155             text += "\n"
156
157             # Indent struct fields and mixed types
158             if isinstance(param, dict):
159                 for name, subparam in param.iteritems():
160                     text += param_text(name, subparam, indent + step, step)
161             elif isinstance(param, Mixed):
162                 for subparam in param:
163                     text += param_text(name, subparam, indent + step, step)
164             elif isinstance(param, (list, tuple, set)):
165                 for subparam in param:
166                     text += param_text("", subparam, indent + step, step)
167
168             return text
169
170         text += "Parameters:\n\n"
171         for name, param in zip(max_args, self.accepts):
172             text += param_text(name, param, indent, indent)
173
174         text += "Returns:\n\n"
175         text += param_text("", self.returns, indent, indent)
176
177         return text
178
179     def args(self):
180         """
181         Returns a tuple:
182
183         ((arg1_name, arg2_name, ...),
184          (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
185          (None, None, ..., optional1_default, optional2_default, ...))
186
187         That represents the minimum and maximum sets of arguments that
188         this function accepts and the defaults for the optional arguments.
189         """
190         
191         # Inspect call. Remove self from the argument list.
192         max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
193         defaults = self.call.func_defaults
194         if defaults is None:
195             defaults = ()
196
197         min_args = max_args[0:len(max_args) - len(defaults)]
198         defaults = tuple([None for arg in min_args]) + defaults
199         
200         return (min_args, max_args, defaults)
201
202     def type_check(self, name, value, expected, args):
203         """
204         Checks the type of the named value against the expected type,
205         which may be a Python type, a typed value, a Parameter, a
206         Mixed type, or a list or dictionary of possibly mixed types,
207         values, Parameters, or Mixed types.
208         
209         Extraneous members of lists must be of the same type as the
210         last specified type. For example, if the expected argument
211         type is [int, bool], then [1, False] and [14, True, False,
212         True] are valid, but [1], [False, 1] and [14, True, 1] are
213         not.
214
215         Extraneous members of dictionaries are ignored.
216         """
217
218         # If any of a number of types is acceptable
219         if isinstance(expected, Mixed):
220             for item in expected:
221                 try:
222                     self.type_check(name, value, item, args)
223                     return
224                 except GeniInvalidArgument, fault:
225                     pass
226             raise fault
227
228         # If an authentication structure is expected, save it and
229         # authenticate after basic type checking is done.
230         #if isinstance(expected, Auth):
231         #    auth = expected
232         #else:
233         #    auth = None
234
235         # Get actual expected type from within the Parameter structure
236         if isinstance(expected, Parameter):
237             min = expected.min
238             max = expected.max
239             nullok = expected.nullok
240             expected = expected.type
241         else:
242             min = None
243             max = None
244             nullok = False
245
246         expected_type = python_type(expected)
247
248         # If value can be NULL
249         if value is None and nullok:
250             return
251
252         # Strings are a special case. Accept either unicode or str
253         # types if a string is expected.
254         if expected_type in StringTypes and isinstance(value, StringTypes):
255             pass
256
257         # Integers and long integers are also special types. Accept
258         # either int or long types if an int or long is expected.
259         elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
260             pass
261
262         elif not isinstance(value, expected_type):
263             raise GeniInvalidArgument("expected %s, got %s" % \
264                                      (xmlrpc_type(expected_type),
265                                       xmlrpc_type(type(value))),
266                                      name)
267
268         # If a minimum or maximum (length, value) has been specified
269         if expected_type in StringTypes:
270             if min is not None and \
271                len(value.encode(self.api.encoding)) < min:
272                 raise GeniInvalidArgument, "%s must be at least %d bytes long" % (name, min)
273             if max is not None and \
274                len(value.encode(self.api.encoding)) > max:
275                 raise GeniInvalidArgument, "%s must be at most %d bytes long" % (name, max)
276         elif expected_type in (list, tuple, set):
277             if min is not None and len(value) < min:
278                 raise GeniInvalidArgument, "%s must contain at least %d items" % (name, min)
279             if max is not None and len(value) > max:
280                 raise GeniInvalidArgument, "%s must contain at most %d items" % (name, max)
281         else:
282             if min is not None and value < min:
283                 raise GeniInvalidArgument, "%s must be > %s" % (name, str(min))
284             if max is not None and value > max:
285                 raise GeniInvalidArgument, "%s must be < %s" % (name, str(max))
286
287         # If a list with particular types of items is expected
288         if isinstance(expected, (list, tuple, set)):
289             for i in range(len(value)):
290                 if i >= len(expected):
291                     j = len(expected) - 1
292                 else:
293                     j = i
294                 self.type_check(name + "[]", value[i], expected[j], args)
295
296         # If a struct with particular (or required) types of items is
297         # expected.
298         elif isinstance(expected, dict):
299             for key in value.keys():
300                 if key in expected:
301                     self.type_check(name + "['%s']" % key, value[key], expected[key], args)
302             for key, subparam in expected.iteritems():
303                 if isinstance(subparam, Parameter) and \
304                    subparam.optional is not None and \
305                    not subparam.optional and key not in value.keys():
306                     raise GeniInvalidArgument("'%s' not specified" % key, name)
307
308         #if auth is not None:
309         #    auth.check(self, *args)