1# https://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6import difflib
8from pyrocko.guts import Object, List, clone
9from weakref import ref
12class listdict(dict):
13 def __missing__(self, k):
14 self[k] = []
15 return self[k]
18def root_and_path(obj, name=None):
19 root = obj
20 path = []
21 if name is not None:
22 path.append(name)
24 while True:
25 try:
26 root, name_at_parent = root._talkie_parent
27 path.append(name_at_parent)
28 except AttributeError:
29 break
31 return root, '.'.join(path[::-1])
34def lclone(xs):
35 [clone(x) for x in xs]
38g_uid = 0
41def new_uid():
42 global g_uid
43 g_uid += 1
44 return '#%i' % g_uid
47class Talkie(Object):
49 def __setattr__(self, name, value):
50 try:
51 t = self.T.get_property(name)
52 except ValueError:
53 Object.__setattr__(self, name, value)
54 return
56 if isinstance(t, List.T):
57 value = TalkieList(value)
59 oldvalue = getattr(self, name, None)
60 if oldvalue:
61 if isinstance(oldvalue, (Talkie, TalkieList)):
62 oldvalue.unset_parent()
64 if isinstance(value, (Talkie, TalkieList)):
65 value.set_parent(self, name)
67 Object.__setattr__(self, name, value)
68 self.fire([name], value)
70 def fire(self, path, value):
71 self.fire_event(path, value)
72 if hasattr(self, '_talkie_parent'):
73 root, name_at_parent = self._talkie_parent
74 path.append(name_at_parent)
75 root.fire(path, value)
77 def set_parent(self, parent, name):
78 Object.__setattr__(self, '_talkie_parent', (parent, name))
80 def unset_parent(self):
81 Object.__delattr__(self, '_talkie_parent')
83 def fire_event(self, path, value):
84 pass
86 def diff(self, other, path=()):
87 assert type(self) is type(other), '%s %s' % (type(self), type(other))
89 for (s_prop, s_val), (o_prop, o_val) in zip(
90 self.T.ipropvals(self), other.T.ipropvals(other)):
92 if not s_prop.multivalued:
93 if isinstance(s_val, Talkie) \
94 and type(s_val) is type(o_val):
96 for x in s_val.diff(o_val, path + (s_prop.name,)):
97 yield x
98 else:
99 if not equal(s_val, o_val):
100 yield 'set', path + (s_prop.name,), clone(o_val)
101 else:
102 if issubclass(s_prop.content_t.cls, Talkie):
103 sm = difflib.SequenceMatcher(
104 None,
105 type_eq_proxy_seq(s_val),
106 type_eq_proxy_seq(o_val))
107 mode = 1
109 else:
110 sm = difflib.SequenceMatcher(
111 None,
112 eq_proxy_seq(s_val),
113 eq_proxy_seq(o_val))
114 mode = 2
116 for tag, i1, i2, j1, j2 in list(sm.get_opcodes()):
117 if tag == 'equal' and mode == 1:
118 for koff, (s_element, o_element) in enumerate(zip(
119 s_val[i1:i2], o_val[j1:j2])):
121 for x in s_element.diff(
122 o_element,
123 path + ((s_prop.name, i1+koff),)):
124 yield x
126 if tag == 'replace':
127 yield (
128 'replace',
129 path + ((s_prop.name, i1, i2),),
130 lclone(o_val[j1:j2]))
132 elif tag == 'delete':
133 yield (
134 'delete',
135 path + ((s_prop.name, i1, i2),),
136 None)
138 elif tag == 'insert':
139 yield (
140 'insert',
141 path + ((s_prop.name, i1, i2)),
142 lclone(o_val[j1:j2]))
144 def diff_update(self, other, path=()):
145 assert type(self) is type(other), '%s %s' % (type(self), type(other))
147 for (s_prop, s_val), (o_prop, o_val) in zip(
148 self.T.ipropvals(self), other.T.ipropvals(other)):
150 if not s_prop.multivalued:
151 if isinstance(s_val, Talkie) \
152 and type(s_val) is type(o_val):
154 s_val.diff_update(o_val, path + (s_prop.name,))
155 else:
156 if not equal(s_val, o_val):
157 setattr(self, s_prop.name, clone(o_val))
158 else:
159 if issubclass(s_prop.content_t.cls, Talkie):
160 sm = difflib.SequenceMatcher(
161 None,
162 type_eq_proxy_seq(s_val),
163 type_eq_proxy_seq(o_val))
164 mode = 1
166 else:
167 sm = difflib.SequenceMatcher(
168 None,
169 eq_proxy_seq(s_val),
170 eq_proxy_seq(o_val))
171 mode = 2
173 ioff = 0
174 for tag, i1, i2, j1, j2 in list(sm.get_opcodes()):
175 if tag == 'equal' and mode == 1:
176 for koff, (s_element, o_element) in enumerate(zip(
177 s_val[i1+ioff:i2+ioff], o_val[j1:j2])):
179 s_element.diff_update(
180 o_element,
181 path + ((s_prop.name, i1+ioff+koff),))
183 elif tag == 'replace':
184 for _ in range(i1, i2):
185 s_val.pop(i1+ioff)
187 for j in range(j1, j2):
188 s_val.insert(i1+ioff, clone(o_val[j]))
190 ioff += (j2 - j1) - (i2 - i1)
192 elif tag == 'delete':
193 for _ in range(i1, i2):
194 s_val.pop(i1 + ioff)
196 ioff -= (i2 - i1)
198 elif tag == 'insert':
199 for j in range(j1, j2):
200 s_val.insert(i1+ioff, clone(o_val[j]))
202 ioff += (j2 - j1)
205def equal(a, b):
206 return str(a) == str(b) # to be replaced by recursive guts.equal
209def ghash(obj):
210 return hash(str(obj))
213class GutsEqProxy(object):
214 def __init__(self, obj):
215 self._obj = obj
217 def __eq__(self, other):
218 return equal(self._obj, other._obj)
220 def __hash__(self):
221 return ghash(self._obj)
224class TypeEqProxy(object):
225 def __init__(self, obj):
226 self._obj = obj
228 def __eq__(self, other):
229 return type(self._obj) is type(other._obj) # noqa
231 def __hash__(self):
232 return hash(type(self._obj))
235def eq_proxy_seq(seq):
236 return list(GutsEqProxy(x) for x in seq)
239def type_eq_proxy_seq(seq):
240 return list(TypeEqProxy(x) for x in seq)
243class TalkieConnection(object):
244 def __init__(self, talkie_root, path, listener):
245 self._talkie_root = talkie_root
246 self._listener = listener
247 self._path = path
248 self._ref_listener = ref(listener)
250 def release(self):
251 self._talkie_root.disconnect(self)
254class TalkieRoot(Talkie):
256 def __init__(self, **kwargs):
257 self._listeners = listdict()
258 Talkie.__init__(self, **kwargs)
260 def talkie_connect(self, path, listener):
261 connection = TalkieConnection(self, path, listener)
262 self._listeners[path].append(connection._ref_listener)
263 return connection
265 def talkie_disconnect(self, connection):
266 try:
267 self._listeners[connection._path].remove(
268 connection._ref_listener)
269 except ValueError:
270 pass
272 def fire_event(self, path, value):
273 path = '.'.join(path[::-1])
274 # print('fire_event:', path, value)
275 parts = path.split('.')
276 for i in range(len(parts)+1):
277 subpath = '.'.join(parts[:i])
278 target_refs = self._listeners[subpath]
279 delete = []
280 for target_ref in target_refs:
281 target = target_ref()
282 if target:
283 target(path, value)
284 else:
285 delete.append(target_ref)
287 for target_ref in delete:
288 target_refs.remove(target_ref)
290 def get(self, path):
291 x = self
292 for s in path.split('.'):
293 x = getattr(x, s)
295 return x
297 def set(self, path, value):
298 x = self
299 p = path.split('.')
300 for s in p[:-1]:
301 x = getattr(x, s)
303 setattr(x, p[-1], value)
306class TalkieList(list):
308 def fire(self, path, value):
309 if self._talkie_parent:
310 root, name_at_parent = self._talkie_parent
311 path.append(name_at_parent)
312 root.fire(path, value)
314 def set_parent(self, parent, name):
315 list.__setattr__(self, '_talkie_parent', (parent, name))
317 def unset_parent(self):
318 list.__delattr__(self, '_talkie_parent')
320 def append(self, element):
321 retval = list.append(self, element)
322 name = new_uid()
323 if isinstance(element, (Talkie, TalkieList)):
324 element.set_parent(self, name)
326 self.fire([], self)
327 return retval
329 def insert(self, index, element):
330 retval = list.insert(self, index, element)
331 name = new_uid()
332 if isinstance(element, (Talkie, TalkieList)):
333 element.set_parent(self, name)
335 self.fire([], self)
336 return retval
338 def remove(self, element):
339 list.remove(self, element)
340 if isinstance(element, (Talkie, TalkieList)):
341 element.unset_parent()
343 self.fire([], self)
345 def pop(self, index=-1):
346 element = list.pop(self, index)
347 if isinstance(element, (Talkie, TalkieList)):
348 element.unset_parent()
350 self.fire([], self)
351 return element
353 def extend(self, elements):
354 for element in elements:
355 self.append(element)
357 self.fire([], self)
359 def __setitem__(self, key, value):
360 try:
361 element = self[key]
362 if isinstance(element, (Talkie, TalkieList)):
363 element.unset_parent()
365 except IndexError:
366 pass
368 list.__setitem__(self, key, value)
369 self.fire([], self)
371 def __setslice__(self, *args, **kwargs):
372 raise Exception('not implemented')
374 def __iadd__(self, *args, **kwargs):
375 raise Exception('not implemented')
377 def __imul__(self, *args, **kwargs):
378 raise Exception('not implemented')
381for method_name in ['reverse', 'sort']:
383 def x():
384 list_meth = getattr(list, method_name)
386 def meth(self, *args, **kwargs):
387 retval = list_meth(self, *args, **kwargs)
388 self.fire([], self)
389 return retval
391 return meth
393 try:
394 setattr(TalkieList, method_name, x())
395 except AttributeError:
396 pass
399def drop_args_wrapper(f):
400 def f_drop_args(*args):
401 f()
403 return f_drop_args
406class TalkieConnectionOwner(object):
407 def __init__(self):
408 self._connections = []
410 def talkie_connect(self, state, path, listener, drop_args=False):
411 if drop_args:
412 listener = drop_args_wrapper(listener)
414 if not isinstance(path, str):
415 return [
416 self.talkie_connect(state, path_, listener, False)
417 for path_ in path]
419 connection = state.talkie_connect(path, listener)
420 self._connections.append(connection)
421 return connection
423 def talkie_disconnect_all(self):
424 while self._connections:
425 try:
426 self._connections.pop().release()
427 except Exception:
428 pass