/
callables.py
191 lines (154 loc) · 7.1 KB
/
callables.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
"""
Low level functions and classes related to callables.
The AUTO_TOPIC
is the "marker" to use in callables to indicate that when a message
is sent to those callables, the topic object for that message should be
added to the data sent via the call arguments. See the docs in
CallArgsInfo regarding its autoTopicArgName data member.
:copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
:license: BSD, see LICENSE_BSD_Simple.txt for details.
"""
from inspect import getargspec, ismethod, isfunction
from .. import py2and3
AUTO_TOPIC = '## your listener wants topic name ## (string unlikely to be used by caller)'
def getModule(obj):
"""Get the module in which an object was defined. Returns '__main__'
if no module defined (which usually indicates either a builtin, or
a definition within main script). """
if hasattr(obj, '__module__'):
module = obj.__module__
else:
module = '__main__'
return module
def getID(callable_):
"""Get name and module name for a callable, ie function, bound
method or callable instance, by inspecting the callable. E.g.
getID(Foo.bar) returns ('Foo.bar', 'a.b') if Foo.bar was
defined in module a.b. """
sc = callable_
if ismethod(sc):
module = getModule(sc.__self__)
id = '%s.%s' % (sc.__self__.__class__.__name__, sc.__func__.__name__)
elif isfunction(sc):
module = getModule(sc)
id = sc.__name__
else: # must be a functor (instance of a class that has __call__ method)
module = getModule(sc)
id = sc.__class__.__name__
return id, module
def getRawFunction(callable_):
"""Given a callable, return (offset, func) where func is the
function corresponding to callable, and offset is 0 or 1 to
indicate whether the function's first argument is 'self' (1)
or not (0). Raises ValueError if callable_ is not of a
recognized type (function, method or has __call__ method)."""
firstArg = 0
if isfunction(callable_):
#print 'Function', getID(callable_)
func = callable_
elif ismethod(callable_):
#print 'Method', getID(callable_)
func = callable_
if func.__self__ is not None:
# Method is bound, don't care about the self arg
firstArg = 1
elif hasattr(callable_, '__call__'):
#print 'Functor', getID(callable_)
func = callable_.__call__
firstArg = 1 # don't care about the self arg
else:
msg = 'type "%s" not supported' % type(callable_).__name__
raise ValueError(msg)
return func, firstArg
class ListenerMismatchError(ValueError):
"""
Raised when an attempt is made to subscribe a listener to
a topic, but listener does not satisfy the topic's message data
specification (MDS). This specification is inferred from the first
listener subscribed to a topic, or from an imported topic tree
specification (see pub.addTopicDefnProvider()).
"""
def __init__(self, msg, listener, *args):
idStr, module = getID(listener)
msg = 'Listener "%s" (from module "%s") inadequate: %s' % (idStr, module, msg)
ValueError.__init__(self, msg)
self.msg = msg
self.args = args
self.module = module
self.idStr = idStr
def __str__(self):
return self.msg
class CallArgsInfo:
"""
Represent the "signature" or protocol of a listener in the context of
topics.
"""
def __init__(self, func, firstArgIdx): #args, firstArgIdx, defaultVals, acceptsAllKwargs=False):
"""Inputs:
- Args and defaultVals are the complete set of arguments and
default values as obtained form inspect.getargspec();
- The firstArgIdx points to the first item in
args that is of use, so it is typically 0 if listener is a function,
and 1 if listener is a method.
- The acceptsAllKwargs should be true
if the listener has **kwargs in its protocol.
After construction,
- self.allParams will contain the subset of 'args' without first
firstArgIdx items,
- self.numRequired will indicate number of required arguments
(ie self.allParams[:self.numRequired] are the required args names);
- self.acceptsAllKwargs = acceptsAllKwargs
- self.autoTopicArgName will be the name of argument
in which to put the topic object for which pubsub message is
sent, or None. This is identified by the argument that has a
default value of AUTO_TOPIC.
For instance, listener(self, arg1, arg2=AUTO_TOPIC, arg3=None) will
have self.allParams = (arg1, arg2, arg3), self.numRequired=1, and
self.autoTopicArgName = 'arg2', whereas
listener(self, arg1, arg3=None) will have
self.allParams = (arg1, arg3), self.numRequired=1, and
self.autoTopicArgName = None."""
#args, firstArgIdx, defaultVals, acceptsAllKwargs
(allParams, varParamName, varOptParamName, defaultVals) = getargspec(func)
if defaultVals is None:
defaultVals = []
else:
defaultVals = list(defaultVals)
self.acceptsAllKwargs = (varOptParamName is not None)
self.acceptsAllUnnamedArgs = (varParamName is not None)
self.allParams = allParams
del self.allParams[0:firstArgIdx] # does nothing if firstArgIdx == 0
self.numRequired = len(self.allParams) - len(defaultVals)
assert self.numRequired >= 0
# if listener wants topic, remove that arg from args/defaultVals
self.autoTopicArgName = None
if defaultVals:
self.__setupAutoTopic(defaultVals)
def getAllArgs(self):
return tuple( self.allParams )
def getOptionalArgs(self):
return tuple( self.allParams[self.numRequired:] )
def getRequiredArgs(self):
"""Return a tuple of names indicating which call arguments
are required to be present when pub.sendMessage(...) is called. """
return tuple( self.allParams[:self.numRequired] )
def __setupAutoTopic(self, defaults):
"""Does the listener want topic of message? Returns < 0 if not,
otherwise return index of topic kwarg within args."""
for indx, defaultVal in enumerate(defaults):
if defaultVal == AUTO_TOPIC:
#del self.defaults[indx]
firstKwargIdx = self.numRequired
self.autoTopicArgName = self.allParams.pop(firstKwargIdx + indx)
break
def getArgs(callable_):
"""Returns an instance of CallArgsInfo for the given callable_.
Raises ListenerMismatchError if callable_ is not a callable."""
# figure out what is the actual function object to inspect:
try:
func, firstArgIdx = getRawFunction(callable_)
except ValueError:
from .. import py2and3
exc = py2and3.getexcobj()
raise ListenerMismatchError(str(exc), callable_)
return CallArgsInfo(func, firstArgIdx)