1+ # Copyright 2009-2011 Ram Rachum.
2+ # This program is distributed under the LGPL2.1 license.
3+
4+ '''
5+ Defines the `Emitter` class.
6+
7+ See its documentation for more info.
8+ '''
9+
10+ # todo: there should probably be some circularity check. Maybe actually
11+ # circularity should be permitted?
12+
13+ # todo: make some way to emit from multiple emitters simulataneously, saving
14+ # redundant calls to shared callable outputs.
15+
16+ import itertools
17+ from python_toolbox import cute_iter_tools
18+ from python_toolbox import misc_tools
19+ from python_toolbox import address_tools
20+
21+
22+ class Emitter (object ):
23+ '''
24+ An emitter you can `emit` from to call all its callable outputs.
25+
26+ The emitter idea is a variation on the publisher-subscriber design pattern.
27+
28+ Every emitter has a set of inputs and a set of outputs. The inputs, if
29+ there are any, must be emitters themselves. So when you `emit` on any of
30+ this emitter's inputs, it's as if you `emit`ted on this emitter as well.
31+ (Recursively, of course.)
32+
33+ The outputs are a bit different. An emitter can have as outputs both (a)
34+ other emitters and (b) callable objects. (Which means, functions or
35+ function-like objects.)
36+
37+ There's no need to explain (a): If `emitter_1` has as an output
38+ `emitter_2`, then `emitter_2` has as an input `emitter_1`, which works like
39+ how we explained above about inputs.
40+
41+ But now (b): An emitter can have callables as outputs. (Without these, the
42+ emitter idea won't have much use.) These callables simply get called
43+ whenever the emitter or one of its inputs get `emit`ted.
44+
45+ The callables that you register as outputs are functions that need to be
46+ called when the original event that caused the `emit` action happens.
47+ '''
48+ # todo: Let user put a single input/output
49+
50+
51+ _is_atomically_pickleable = False
52+
53+
54+ def __init__ (self , inputs = (), outputs = (), name = None ):
55+ '''
56+ Construct the emitter.
57+
58+ `inputs` is a list of inputs, all of them must be emitters.
59+
60+ `outputs` is a list of outputs, they must be either emitters or
61+ callables.
62+
63+ `name` is a string name for the emitter.
64+ '''
65+
66+ assert cute_iter_tools .is_iterable (inputs ) and \
67+ cute_iter_tools .is_iterable (outputs )
68+
69+ self ._inputs = set ()
70+ '''The emitter's inputs.'''
71+
72+ self ._outputs = set ()
73+ '''The emitter's inputs.'''
74+
75+ for output in outputs :
76+ self .add_output (output )
77+
78+ self .__total_callable_outputs_cache = None
79+ '''
80+ A cache of total callable outputs.
81+
82+ This means the callable outputs of this emitter and any output
83+ emitters.
84+ '''
85+
86+ self ._recalculate_total_callable_outputs ()
87+
88+ # We made sure to create the callable outputs cache before we add
89+ # inputs, so when we update their cache, it could use ours.
90+ for input in inputs :
91+ self .add_input (input )
92+
93+ self .name = name
94+ '''The emitter's name.'''
95+
96+ def get_inputs (self ):
97+ '''Get the emitter's inputs.'''
98+ return self ._inputs
99+
100+ def get_outputs (self ):
101+ '''Get the emitter's outputs.'''
102+ return self ._outputs
103+
104+ def _get_input_layers (self ):
105+ '''
106+ Get the emitter's inputs as a list of layers.
107+
108+ Every item in the list will be a list of emitters on that layer. For
109+ example, the first item will be a list of direct inputs of our emitter.
110+ The second item will be a list of *their* inputs. Etc.
111+
112+ Every emitter can appear only once in this scheme: It would appear on
113+ the closest layer that it's on.
114+ '''
115+
116+ input_layers = [self ._inputs ]
117+ current_layer = self ._inputs
118+ while current_layer :
119+
120+ next_layer = reduce (
121+ set .union ,
122+ (input ._inputs for input in current_layer ),
123+ set ()
124+ )
125+
126+ for ancestor_layer in input_layers :
127+ assert isinstance (next_layer , set )
128+ next_layer -= ancestor_layer
129+
130+ input_layers .append (next_layer )
131+
132+ current_layer = next_layer
133+
134+
135+ # assert sum(len(layer) for layer in input_layers) == \
136+ # len(reduce(set.union, input_layers, set()))
137+
138+ return input_layers
139+
140+
141+ def _recalculate_total_callable_outputs_recursively (self ):
142+ '''
143+ Recalculate `__total_callable_outputs_cache` recursively.
144+
145+ This will to do the recalculation for this emitter and all its inputs.
146+ '''
147+
148+ # todo: I suspect this wouldn't work for the following case. `self` has
149+ # inputs `A` and `B`. `A` has input `B`. A callable output `func` was
150+ # just removed from `self`, so this function got called. We update the
151+ # cache here, then take the first input layer, which is `A` and `B` in
152+ # some order. Say `B` is first. Now, we do `recalculate` on `B`, but `A`
153+ # still got the cache with `func`, and `B` will take that. I need to
154+ # test this.
155+ #
156+ # I have an idea how to solve it: In the getter of the cache, check the
157+ # cache exists, otherwise rebuild. The reason we didn't do it up to now
158+ # was to optimize for speed, but only `emit` needs to be fast and it
159+ # doesn't use the getter. We'll clear the caches of all inputs, and
160+ # they'll rebuild as they call each other.
161+
162+ self ._recalculate_total_callable_outputs ()
163+ input_layers = self ._get_input_layers ()
164+ for input_layer in input_layers :
165+ for input in input_layer :
166+ input ._recalculate_total_callable_outputs ()
167+
168+
169+ def _recalculate_total_callable_outputs (self ):
170+ '''
171+ Recalculate `__total_callable_outputs_cache` for this emitter.
172+
173+ This will to do the recalculation for this emitter and all its inputs.
174+ '''
175+ children_callable_outputs = reduce (
176+ set .union ,
177+ (emitter .get_total_callable_outputs () for emitter
178+ in self ._get_emitter_outputs () if emitter is not self ),
179+ set ()
180+ )
181+
182+ self .__total_callable_outputs_cache = \
183+ children_callable_outputs .union (self ._get_callable_outputs ())
184+
185+ def add_input (self , emitter ):
186+ '''
187+ Add an emitter as an input to this emitter.
188+
189+ Every time that emitter will emit, it will cause this emitter to emit
190+ as well.
191+ '''
192+ assert isinstance (emitter , Emitter )
193+ self ._inputs .add (emitter )
194+ emitter ._outputs .add (self )
195+ emitter ._recalculate_total_callable_outputs_recursively ()
196+
197+ def remove_input (self , emitter ):
198+ '''Remove an input from this emitter.'''
199+ assert isinstance (emitter , Emitter )
200+ self ._inputs .remove (emitter )
201+ emitter ._outputs .remove (self )
202+ emitter ._recalculate_total_callable_outputs_recursively ()
203+
204+ def add_output (self , thing ):
205+ '''
206+ Add an emitter or a callable as an output to this emitter.
207+
208+ If adding a callable, every time this emitter will emit the callable
209+ will be called.
210+
211+ If adding an emitter, every time this emitter will emit the output
212+ emitter will emit as well.
213+ '''
214+ assert isinstance (thing , Emitter ) or callable (thing )
215+ self ._outputs .add (thing )
216+ if isinstance (thing , Emitter ):
217+ thing ._inputs .add (self )
218+ self ._recalculate_total_callable_outputs_recursively ()
219+
220+ def remove_output (self , thing ):
221+ '''Remove an output from this emitter.'''
222+ assert isinstance (thing , Emitter ) or callable (thing )
223+ self ._outputs .remove (thing )
224+ if isinstance (thing , Emitter ):
225+ thing ._inputs .remove (self )
226+ self ._recalculate_total_callable_outputs_recursively ()
227+
228+ def disconnect_from_all (self ): # todo: use the freeze here
229+ '''Disconnect the emitter from all its inputs and outputs.'''
230+ for input in self ._inputs :
231+ self .remove_input (input )
232+ for output in self ._outputs :
233+ self .remove_output (output )
234+
235+ def _get_callable_outputs (self ):
236+ '''Get the direct callable outputs of this emitter.'''
237+ return set ((
238+ output for output in self ._outputs if callable (output )
239+ ))
240+
241+ def _get_emitter_outputs (self ):
242+ '''Get the direct emitter outputs of this emitter.'''
243+ return set ((
244+ output for output in self ._outputs if isinstance (output , Emitter )
245+ ))
246+
247+ def get_total_callable_outputs (self ):
248+ '''
249+ Get the total of callable outputs of this emitter.
250+
251+ This means the direct callable outputs, and the callable outputs of
252+ emitter outputs.
253+ '''
254+ return self .__total_callable_outputs_cache
255+
256+ def emit (self ):
257+ '''
258+ Call all of the (direct or indirect) callable outputs of this emitter.
259+
260+ This is the most important method of the emitter. When you `emit`, all
261+ the callable outputs get called in succession.
262+ '''
263+ # Note that this function gets called many times, so it should be
264+ # optimized for speed.
265+ for callable_output in self .__total_callable_outputs_cache :
266+ # We are using the cache directly instead of calling the getter,
267+ # for speed.
268+ callable_output ()
269+
270+ def __repr__ (self ):
271+ '''
272+ Get a string representation of the emitter.
273+
274+ Example output:
275+ <python_toolbox.emitters.emitter.Emitter 'tree_modified' at
276+ 0x1c013d0>
277+ '''
278+ return '<%s %sat %s>' % (
279+ address_tools .describe (type (self ), shorten = True ),
280+ '' .join (("'" , self .name , "' " )) if self .name else '' ,
281+ hex (id (self ))
282+ )
283+ """
284+ Unused:
285+
286+ def _get_total_inputs(self):
287+
288+ total_inputs_of_inputs = reduce(
289+ set.union,
290+ (emitter._get_total_inputs() for emitter
291+ in self._inputs if emitter is not self),
292+ set()
293+ )
294+
295+ return total_inputs_of_inputs.union(self._inputs)
296+ """
0 commit comments