1# https://pyrocko.org - GPLv3
2#
3# The Pyrocko Developers, 21st Century
4# ---|P------/S----------~Lg----------
6import difflib
7from collections import defaultdict
9from pyrocko import util
10from pyrocko.guts import Object, List, clone
11from weakref import ref
14class listdict(dict):
15 def __missing__(self, k):
16 self[k] = []
17 return self[k]
20def root_and_path(obj, name=None):
21 root = obj
22 path = []
23 if name is not None:
24 path.append(name)
26 while True:
27 try:
28 root, name_at_parent = root._talkie_parent
29 path.append(name_at_parent)
30 except AttributeError:
31 break
33 return root, '.'.join(path[::-1])
36def lclone(xs):
37 [clone(x) for x in xs]
40g_uid = 0
43def new_uid():
44 global g_uid
45 g_uid += 1
46 return '#%i' % g_uid
49def has_computed(cls):
50 cls._computed = defaultdict(list)
51 cls._computed_rev = defaultdict(list)
53 for prop_name in dir(cls):
54 try:
55 prop = getattr(cls, prop_name)
56 depends_on = prop.fget._computed_depends_on
57 for name in depends_on:
58 cls._computed_rev[name].append(prop_name)
59 cls._computed[prop_name].append(name)
61 except AttributeError:
62 pass
64 return cls
67def computed(depends_on):
68 def wrapper(func):
69 func._computed_depends_on = depends_on
70 return property(func)
72 return wrapper
75class Talkie(Object):
76 _computed = None
77 _computed_rev = None
79 def __setattr__(self, name, value):
80 try:
81 t = self.T.get_property(name)
82 except ValueError:
83 Object.__setattr__(self, name, value)
84 return
86 if isinstance(t, List.T):
87 value = TalkieList(value)
89 oldvalue = getattr(self, name, None)
90 if oldvalue:
91 if isinstance(oldvalue, (Talkie, TalkieList)):
92 oldvalue.unset_parent()
94 if isinstance(value, (Talkie, TalkieList)):
95 value.set_parent(self, name)
97 Object.__setattr__(self, name, value)
98 self.fire([name], value)
99 if self._computed_rev is not None:
100 for dname in self._computed_rev[name]:
101 self.fire([dname], None)
103 def fire(self, path, value):
104 self.fire_event(path, value)
105 if hasattr(self, '_talkie_parent'):
106 root, name_at_parent = self._talkie_parent
107 path.append(name_at_parent)
108 root.fire(path, value)
110 def set_parent(self, parent, name):
111 Object.__setattr__(self, '_talkie_parent', (parent, name))
113 def unset_parent(self):
114 Object.__delattr__(self, '_talkie_parent')
116 def fire_event(self, path, value):
117 pass
119 def diff(self, other, path=()):
120 assert type(self) is type(other), '%s %s' % (type(self), type(other))
122 for (s_prop, s_val), (o_prop, o_val) in zip(
123 self.T.ipropvals(self), other.T.ipropvals(other)):
125 if not s_prop.multivalued:
126 if isinstance(s_val, Talkie) \
127 and type(s_val) is type(o_val):
129 for x in s_val.diff(o_val, path + (s_prop.name,)):
130 yield x
131 else:
132 if not equal(s_val, o_val):
133 yield 'set', path + (s_prop.name,), clone(o_val)
134 else:
135 if issubclass(s_prop.content_t.cls, Talkie):
136 sm = difflib.SequenceMatcher(
137 None,
138 type_eq_proxy_seq(s_val),
139 type_eq_proxy_seq(o_val))
140 mode = 1
142 else:
143 sm = difflib.SequenceMatcher(
144 None,
145 eq_proxy_seq(s_val),
146 eq_proxy_seq(o_val))
147 mode = 2
149 for tag, i1, i2, j1, j2 in list(sm.get_opcodes()):
150 if tag == 'equal' and mode == 1:
151 for koff, (s_element, o_element) in enumerate(zip(
152 s_val[i1:i2], o_val[j1:j2])):
154 for x in s_element.diff(
155 o_element,
156 path + ((s_prop.name, i1+koff),)):
157 yield x
159 if tag == 'replace':
160 yield (
161 'replace',
162 path + ((s_prop.name, i1, i2),),
163 lclone(o_val[j1:j2]))
165 elif tag == 'delete':
166 yield (
167 'delete',
168 path + ((s_prop.name, i1, i2),),
169 None)
171 elif tag == 'insert':
172 yield (
173 'insert',
174 path + ((s_prop.name, i1, i2)),
175 lclone(o_val[j1:j2]))
177 def diff_update(self, other, path=()):
178 assert type(self) is type(other), '%s %s' % (type(self), type(other))
180 for (s_prop, s_val), (o_prop, o_val) in zip(
181 self.T.ipropvals(self), other.T.ipropvals(other)):
183 if not s_prop.multivalued:
184 if isinstance(s_val, Talkie) \
185 and type(s_val) is type(o_val):
187 s_val.diff_update(o_val, path + (s_prop.name,))
188 else:
189 if not equal(s_val, o_val):
190 setattr(self, s_prop.name, clone(o_val))
191 else:
192 if issubclass(s_prop.content_t.cls, Talkie):
193 sm = difflib.SequenceMatcher(
194 None,
195 type_eq_proxy_seq(s_val),
196 type_eq_proxy_seq(o_val))
197 mode = 1
199 else:
200 sm = difflib.SequenceMatcher(
201 None,
202 eq_proxy_seq(s_val),
203 eq_proxy_seq(o_val))
204 mode = 2
206 ioff = 0
207 for tag, i1, i2, j1, j2 in list(sm.get_opcodes()):
208 if tag == 'equal' and mode == 1:
209 for koff, (s_element, o_element) in enumerate(zip(
210 s_val[i1+ioff:i2+ioff], o_val[j1:j2])):
212 s_element.diff_update(
213 o_element,
214 path + ((s_prop.name, i1+ioff+koff),))
216 elif tag == 'replace':
217 for _ in range(i1, i2):
218 s_val.pop(i1+ioff)
220 for j in range(j1, j2):
221 s_val.insert(i1+ioff, clone(o_val[j]))
223 ioff += (j2 - j1) - (i2 - i1)
225 elif tag == 'delete':
226 for _ in range(i1, i2):
227 s_val.pop(i1 + ioff)
229 ioff -= (i2 - i1)
231 elif tag == 'insert':
232 for j in range(j1, j2):
233 s_val.insert(i1+ioff, clone(o_val[j]))
235 ioff += (j2 - j1)
238def equal(a, b):
239 return str(a) == str(b) # to be replaced by recursive guts.equal
242def ghash(obj):
243 return hash(str(obj))
246class GutsEqProxy(object):
247 def __init__(self, obj):
248 self._obj = obj
250 def __eq__(self, other):
251 return equal(self._obj, other._obj)
253 def __hash__(self):
254 return ghash(self._obj)
257class TypeEqProxy(object):
258 def __init__(self, obj):
259 self._obj = obj
261 def __eq__(self, other):
262 return type(self._obj) is type(other._obj) # noqa
264 def __hash__(self):
265 return hash(type(self._obj))
268def eq_proxy_seq(seq):
269 return list(GutsEqProxy(x) for x in seq)
272def type_eq_proxy_seq(seq):
273 return list(TypeEqProxy(x) for x in seq)
276class TalkieConnection(object):
277 def __init__(self, talkie_root, path, listener):
278 self._talkie_root = talkie_root
279 self._listener = listener
280 self._path = path
281 self._ref_listener = ref(listener)
283 def release(self):
284 self._talkie_root.disconnect(self)
287class TalkieRoot(Talkie):
289 def __init__(self, **kwargs):
290 self._listeners = listdict()
291 Talkie.__init__(self, **kwargs)
293 def talkie_connect(self, path, listener):
294 connection = TalkieConnection(self, path, listener)
295 self._listeners[path].append(connection._ref_listener)
296 return connection
298 def talkie_disconnect(self, connection):
299 try:
300 self._listeners[connection._path].remove(
301 connection._ref_listener)
302 except ValueError:
303 pass
305 def fire_event(self, path, value):
306 path = '.'.join(path[::-1])
307 # print('fire_event:', path, value)
308 parts = path.split('.')
309 for i in range(len(parts)+1):
310 subpath = '.'.join(parts[:i])
311 target_refs = self._listeners[subpath]
312 delete = []
313 for target_ref in target_refs:
314 target = target_ref()
315 if target:
316 target(path, value)
317 else:
318 delete.append(target_ref)
320 for target_ref in delete:
321 target_refs.remove(target_ref)
323 def get(self, path):
324 x = self
325 for s in path.split('.'):
326 x = getattr(x, s)
328 return x
330 def set(self, path, value):
331 x = self
332 p = path.split('.')
333 for s in p[:-1]:
334 x = getattr(x, s)
336 setattr(x, p[-1], value)
339class TalkieList(list):
341 def fire(self, path, value):
342 if self._talkie_parent:
343 root, name_at_parent = self._talkie_parent
344 path.append(name_at_parent)
345 root.fire(path, value)
347 def set_parent(self, parent, name):
348 list.__setattr__(self, '_talkie_parent', (parent, name))
350 def unset_parent(self):
351 list.__delattr__(self, '_talkie_parent')
353 def append(self, element):
354 retval = list.append(self, element)
355 name = new_uid()
356 if isinstance(element, (Talkie, TalkieList)):
357 element.set_parent(self, name)
359 self.fire([], self)
360 return retval
362 def insert(self, index, element):
363 retval = list.insert(self, index, element)
364 name = new_uid()
365 if isinstance(element, (Talkie, TalkieList)):
366 element.set_parent(self, name)
368 self.fire([], self)
369 return retval
371 def remove(self, element):
372 list.remove(self, element)
373 if isinstance(element, (Talkie, TalkieList)):
374 element.unset_parent()
376 self.fire([], self)
378 def pop(self, index=-1):
379 element = list.pop(self, index)
380 if isinstance(element, (Talkie, TalkieList)):
381 element.unset_parent()
383 self.fire([], self)
384 return element
386 def extend(self, elements):
387 for element in elements:
388 self.append(element)
390 self.fire([], self)
392 def __setitem__(self, key, value):
393 try:
394 element = self[key]
395 if isinstance(element, (Talkie, TalkieList)):
396 element.unset_parent()
398 except IndexError:
399 pass
401 list.__setitem__(self, key, value)
402 self.fire([], self)
404 def __setslice__(self, *args, **kwargs):
405 raise Exception('not implemented')
407 def __iadd__(self, *args, **kwargs):
408 raise Exception('not implemented')
410 def __imul__(self, *args, **kwargs):
411 raise Exception('not implemented')
414for method_name in ['reverse', 'sort']:
416 def x():
417 list_meth = getattr(list, method_name)
419 def meth(self, *args, **kwargs):
420 retval = list_meth(self, *args, **kwargs)
421 self.fire([], self)
422 return retval
424 return meth
426 try:
427 setattr(TalkieList, method_name, x())
428 except AttributeError:
429 pass
432def drop_args_wrapper(f):
433 def f_drop_args(*args):
434 f()
436 return f_drop_args
439class TalkieConnectionOwner(object):
440 def __init__(self):
441 self._connections = []
443 def talkie_connect(self, state, path, listener, drop_args=False):
444 if drop_args:
445 listener = drop_args_wrapper(listener)
447 if not isinstance(path, str):
448 return [
449 self.talkie_connect(state, path_, listener, False)
450 for path_ in path]
452 connection = state.talkie_connect(path, listener)
453 self._connections.append(connection)
454 return connection
456 def talkie_disconnect_all(self):
457 while self._connections:
458 try:
459 self._connections.pop().release()
460 except Exception:
461 pass
464def none_or(f):
465 def g(x):
466 if x is None:
467 return ''
468 else:
469 return f(x)
471 return g
474def noop(x):
475 return x
478class TalkieStringer:
479 def __init__(self, root):
480 self._root = root
481 self._formatters = {
482 'date': none_or(lambda v: util.time_to_str(v, format='%Y-%m-%d')),
483 'datetime': none_or(lambda v: util.time_to_str(v))}
485 self._paths = []
487 def __getitem__(self, key):
489 stringer = self
491 class Proxy:
492 def __init__(self, val, formatter, path, talkie_root):
493 self._formatter = formatter
494 self._val = val
495 self._path = path
496 self._talkie_root = talkie_root
498 def register_path(self):
499 stringer._paths.append(
500 (self._talkie_root, '.'.join(self._path)))
502 def _computed(self):
503 return getattr(self._val, '_computed', ())
505 def __getattr__(self, key):
506 if key in self._val.T.propnames \
507 or key in self._computed():
509 self._path.append(key)
510 val = getattr(self._val, key)
511 pval = self._proxy(val)
512 if not isinstance(pval, (Proxy, ListProxy)):
513 self.register_path()
515 return pval
516 else:
517 return '{' + key + '}'
519 def __format__(self, *args, **kwargs):
520 self.register_path()
521 return self._val.__format__(*args, **kwargs)
523 def _proxy(self, val):
524 if isinstance(val, TalkieRoot):
525 return Proxy(
526 val, self._formatter, [], val)
528 elif isinstance(val, Object):
529 return Proxy(
530 val, self._formatter, self._path, self._talkie_root)
532 elif isinstance(val, list):
533 return ListProxy(
534 val, self._formatter, self._path, self._talkie_root)
536 else:
537 return self._formatter(val)
539 class ListProxy(Proxy):
540 def __getitem__(self, i):
541 self._path[-1] += '[%i]' % i
542 return self._proxy(self._val[i])
544 key = key.split('|', 1)
545 if len(key) == 2:
546 key, formatter = key[0], self._formatters[key[1]]
547 else:
548 key, formatter = key[0], noop
550 return getattr(Proxy(self._root, formatter, [], self._root), key)
552 def get_paths(self):
553 return self._paths