1# https://pyrocko.org - GPLv3 

2# 

3# The Pyrocko Developers, 21st Century 

4# ---|P------/S----------~Lg---------- 

5 

6import difflib 

7from collections import defaultdict 

8 

9from pyrocko import util 

10from pyrocko.guts import Object, List, clone 

11from weakref import ref 

12 

13 

14class listdict(dict): 

15 def __missing__(self, k): 

16 self[k] = [] 

17 return self[k] 

18 

19 

20def root_and_path(obj, name=None): 

21 root = obj 

22 path = [] 

23 if name is not None: 

24 path.append(name) 

25 

26 while True: 

27 try: 

28 root, name_at_parent = root._talkie_parent 

29 path.append(name_at_parent) 

30 except AttributeError: 

31 break 

32 

33 return root, '.'.join(path[::-1]) 

34 

35 

36def lclone(xs): 

37 [clone(x) for x in xs] 

38 

39 

40g_uid = 0 

41 

42 

43def new_uid(): 

44 global g_uid 

45 g_uid += 1 

46 return '#%i' % g_uid 

47 

48 

49def has_computed(cls): 

50 cls._computed = defaultdict(list) 

51 cls._computed_rev = defaultdict(list) 

52 

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) 

60 

61 except AttributeError: 

62 pass 

63 

64 return cls 

65 

66 

67def computed(depends_on): 

68 def wrapper(func): 

69 func._computed_depends_on = depends_on 

70 return property(func) 

71 

72 return wrapper 

73 

74 

75class Talkie(Object): 

76 _computed = None 

77 _computed_rev = None 

78 

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 

85 

86 if isinstance(t, List.T): 

87 value = TalkieList(value) 

88 

89 oldvalue = getattr(self, name, None) 

90 if oldvalue: 

91 if isinstance(oldvalue, (Talkie, TalkieList)): 

92 oldvalue.unset_parent() 

93 

94 if isinstance(value, (Talkie, TalkieList)): 

95 value.set_parent(self, name) 

96 

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) 

102 

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) 

109 

110 def set_parent(self, parent, name): 

111 Object.__setattr__(self, '_talkie_parent', (parent, name)) 

112 

113 def unset_parent(self): 

114 Object.__delattr__(self, '_talkie_parent') 

115 

116 def fire_event(self, path, value): 

117 pass 

118 

119 def diff(self, other, path=()): 

120 assert type(self) is type(other), '%s %s' % (type(self), type(other)) 

121 

122 for (s_prop, s_val), (o_prop, o_val) in zip( 

123 self.T.ipropvals(self), other.T.ipropvals(other)): 

124 

125 if not s_prop.multivalued: 

126 if isinstance(s_val, Talkie) \ 

127 and type(s_val) is type(o_val): 

128 

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 

141 

142 else: 

143 sm = difflib.SequenceMatcher( 

144 None, 

145 eq_proxy_seq(s_val), 

146 eq_proxy_seq(o_val)) 

147 mode = 2 

148 

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])): 

153 

154 for x in s_element.diff( 

155 o_element, 

156 path + ((s_prop.name, i1+koff),)): 

157 yield x 

158 

159 if tag == 'replace': 

160 yield ( 

161 'replace', 

162 path + ((s_prop.name, i1, i2),), 

163 lclone(o_val[j1:j2])) 

164 

165 elif tag == 'delete': 

166 yield ( 

167 'delete', 

168 path + ((s_prop.name, i1, i2),), 

169 None) 

170 

171 elif tag == 'insert': 

172 yield ( 

173 'insert', 

174 path + ((s_prop.name, i1, i2)), 

175 lclone(o_val[j1:j2])) 

176 

177 def diff_update(self, other, path=()): 

178 assert type(self) is type(other), '%s %s' % (type(self), type(other)) 

179 

180 for (s_prop, s_val), (o_prop, o_val) in zip( 

181 self.T.ipropvals(self), other.T.ipropvals(other)): 

182 

183 if not s_prop.multivalued: 

184 if isinstance(s_val, Talkie) \ 

185 and type(s_val) is type(o_val): 

186 

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 

198 

199 else: 

200 sm = difflib.SequenceMatcher( 

201 None, 

202 eq_proxy_seq(s_val), 

203 eq_proxy_seq(o_val)) 

204 mode = 2 

205 

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])): 

211 

212 s_element.diff_update( 

213 o_element, 

214 path + ((s_prop.name, i1+ioff+koff),)) 

215 

216 elif tag == 'replace': 

217 for _ in range(i1, i2): 

218 s_val.pop(i1+ioff) 

219 

220 for j in range(j1, j2): 

221 s_val.insert(i1+ioff, clone(o_val[j])) 

222 

223 ioff += (j2 - j1) - (i2 - i1) 

224 

225 elif tag == 'delete': 

226 for _ in range(i1, i2): 

227 s_val.pop(i1 + ioff) 

228 

229 ioff -= (i2 - i1) 

230 

231 elif tag == 'insert': 

232 for j in range(j1, j2): 

233 s_val.insert(i1+ioff, clone(o_val[j])) 

234 

235 ioff += (j2 - j1) 

236 

237 

238def equal(a, b): 

239 return str(a) == str(b) # to be replaced by recursive guts.equal 

240 

241 

242def ghash(obj): 

243 return hash(str(obj)) 

244 

245 

246class GutsEqProxy(object): 

247 def __init__(self, obj): 

248 self._obj = obj 

249 

250 def __eq__(self, other): 

251 return equal(self._obj, other._obj) 

252 

253 def __hash__(self): 

254 return ghash(self._obj) 

255 

256 

257class TypeEqProxy(object): 

258 def __init__(self, obj): 

259 self._obj = obj 

260 

261 def __eq__(self, other): 

262 return type(self._obj) is type(other._obj) # noqa 

263 

264 def __hash__(self): 

265 return hash(type(self._obj)) 

266 

267 

268def eq_proxy_seq(seq): 

269 return list(GutsEqProxy(x) for x in seq) 

270 

271 

272def type_eq_proxy_seq(seq): 

273 return list(TypeEqProxy(x) for x in seq) 

274 

275 

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) 

282 

283 def release(self): 

284 self._talkie_root.disconnect(self) 

285 

286 

287class TalkieRoot(Talkie): 

288 

289 def __init__(self, **kwargs): 

290 self._listeners = listdict() 

291 Talkie.__init__(self, **kwargs) 

292 

293 def talkie_connect(self, path, listener): 

294 connection = TalkieConnection(self, path, listener) 

295 self._listeners[path].append(connection._ref_listener) 

296 return connection 

297 

298 def talkie_disconnect(self, connection): 

299 try: 

300 self._listeners[connection._path].remove( 

301 connection._ref_listener) 

302 except ValueError: 

303 pass 

304 

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) 

319 

320 for target_ref in delete: 

321 target_refs.remove(target_ref) 

322 

323 def get(self, path): 

324 x = self 

325 for s in path.split('.'): 

326 x = getattr(x, s) 

327 

328 return x 

329 

330 def set(self, path, value): 

331 x = self 

332 p = path.split('.') 

333 for s in p[:-1]: 

334 x = getattr(x, s) 

335 

336 setattr(x, p[-1], value) 

337 

338 

339class TalkieList(list): 

340 

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) 

346 

347 def set_parent(self, parent, name): 

348 list.__setattr__(self, '_talkie_parent', (parent, name)) 

349 

350 def unset_parent(self): 

351 list.__delattr__(self, '_talkie_parent') 

352 

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) 

358 

359 self.fire([], self) 

360 return retval 

361 

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) 

367 

368 self.fire([], self) 

369 return retval 

370 

371 def remove(self, element): 

372 list.remove(self, element) 

373 if isinstance(element, (Talkie, TalkieList)): 

374 element.unset_parent() 

375 

376 self.fire([], self) 

377 

378 def pop(self, index=-1): 

379 element = list.pop(self, index) 

380 if isinstance(element, (Talkie, TalkieList)): 

381 element.unset_parent() 

382 

383 self.fire([], self) 

384 return element 

385 

386 def extend(self, elements): 

387 for element in elements: 

388 self.append(element) 

389 

390 self.fire([], self) 

391 

392 def __setitem__(self, key, value): 

393 try: 

394 element = self[key] 

395 if isinstance(element, (Talkie, TalkieList)): 

396 element.unset_parent() 

397 

398 except IndexError: 

399 pass 

400 

401 list.__setitem__(self, key, value) 

402 self.fire([], self) 

403 

404 def __setslice__(self, *args, **kwargs): 

405 raise Exception('not implemented') 

406 

407 def __iadd__(self, *args, **kwargs): 

408 raise Exception('not implemented') 

409 

410 def __imul__(self, *args, **kwargs): 

411 raise Exception('not implemented') 

412 

413 

414for method_name in ['reverse', 'sort']: 

415 

416 def x(): 

417 list_meth = getattr(list, method_name) 

418 

419 def meth(self, *args, **kwargs): 

420 retval = list_meth(self, *args, **kwargs) 

421 self.fire([], self) 

422 return retval 

423 

424 return meth 

425 

426 try: 

427 setattr(TalkieList, method_name, x()) 

428 except AttributeError: 

429 pass 

430 

431 

432def drop_args_wrapper(f): 

433 def f_drop_args(*args): 

434 f() 

435 

436 return f_drop_args 

437 

438 

439class TalkieConnectionOwner(object): 

440 def __init__(self): 

441 self._connections = [] 

442 

443 def talkie_connect(self, state, path, listener, drop_args=False): 

444 if drop_args: 

445 listener = drop_args_wrapper(listener) 

446 

447 if not isinstance(path, str): 

448 return [ 

449 self.talkie_connect(state, path_, listener, False) 

450 for path_ in path] 

451 

452 connection = state.talkie_connect(path, listener) 

453 self._connections.append(connection) 

454 return connection 

455 

456 def talkie_disconnect_all(self): 

457 while self._connections: 

458 try: 

459 self._connections.pop().release() 

460 except Exception: 

461 pass 

462 

463 

464def none_or(f): 

465 def g(x): 

466 if x is None: 

467 return '' 

468 else: 

469 return f(x) 

470 

471 return g 

472 

473 

474def noop(x): 

475 return x 

476 

477 

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))} 

484 

485 self._paths = [] 

486 

487 def __getitem__(self, key): 

488 

489 stringer = self 

490 

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 

497 

498 def register_path(self): 

499 stringer._paths.append( 

500 (self._talkie_root, '.'.join(self._path))) 

501 

502 def _computed(self): 

503 return getattr(self._val, '_computed', ()) 

504 

505 def __getattr__(self, key): 

506 if key in self._val.T.propnames \ 

507 or key in self._computed(): 

508 

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() 

514 

515 return pval 

516 else: 

517 return '{' + key + '}' 

518 

519 def __format__(self, *args, **kwargs): 

520 self.register_path() 

521 return self._val.__format__(*args, **kwargs) 

522 

523 def _proxy(self, val): 

524 if isinstance(val, TalkieRoot): 

525 return Proxy( 

526 val, self._formatter, [], val) 

527 

528 elif isinstance(val, Object): 

529 return Proxy( 

530 val, self._formatter, self._path, self._talkie_root) 

531 

532 elif isinstance(val, list): 

533 return ListProxy( 

534 val, self._formatter, self._path, self._talkie_root) 

535 

536 else: 

537 return self._formatter(val) 

538 

539 class ListProxy(Proxy): 

540 def __getitem__(self, i): 

541 self._path[-1] += '[%i]' % i 

542 return self._proxy(self._val[i]) 

543 

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 

549 

550 return getattr(Proxy(self._root, formatter, [], self._root), key) 

551 

552 def get_paths(self): 

553 return self._paths