proxy.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. import sys
  2. import odml
  3. import odml.tools.event as event
  4. import odml.tools.weakmeth as weakmeth
  5. import odml.tools.nodes as nodes
  6. # events are required for proxy objects
  7. odml.setMinimumImplementation('event')
  8. class Proxy(object):
  9. """common interface"""
  10. pass
  11. # here we hook the base-class-comparism functionality to
  12. # prefer comparism provided by the proxy obj
  13. _odml_base___eq__ = odml.base.baseobject.__eq__
  14. def _proxy___eq__(self, obj):
  15. if isinstance(obj, Proxy) and not isinstance(self, Proxy):
  16. return obj == self
  17. return _odml_base___eq__(self, obj)
  18. odml.base.baseobject.__eq__ = _proxy___eq__
  19. class BaseProxy(Proxy):
  20. """
  21. A nonfunctional proxy, having only the delegate
  22. self._proxy_obj
  23. Comparism is hooked, so the following always holds
  24. >>> BaseProxy(o) == o
  25. True
  26. """
  27. def __init__(self, obj):
  28. object.__setattr__(self, "_proxy_obj", obj)
  29. def __eq__(self, obj):
  30. """
  31. return True if both reference to equal objects
  32. or there referenced object equals *obj*
  33. """
  34. if not isinstance(obj, BaseProxy):
  35. return self._proxy_obj == obj
  36. return self._proxy_obj == obj._proxy_obj
  37. def __neq__(self, obj):
  38. return not self == obj
  39. def __repr__(self):
  40. return "P(%s)" % repr(self._proxy_obj)
  41. class EqualityBaseProxy(BaseProxy):
  42. """
  43. A BaseProxy that uses a set of attributes (defined in self._format)
  44. to compare to other objects
  45. """
  46. def __eq__(self, obj):
  47. for key in self._format:
  48. if not hasattr(self, key) or not hasattr(obj, key):
  49. return False
  50. if getattr(self, key) != getattr(obj, key):
  51. return False
  52. return True
  53. class PassProxy(BaseProxy):
  54. """
  55. A simple Proxy forwarding all attribute access to the delegate
  56. It can have inaccessable values (write protected)::
  57. inaccessable = {'name': 'default name'}
  58. Attributes indicated in the dictionary *personal* are stored in the proxy
  59. and not in the delegate, however note: functions not defined in the proxy
  60. are actually attributes of the delegate and executed on it, so they will not
  61. work on the *personal* attributes, but on the delegates ones.
  62. """
  63. PASS_THROUGH = "PASS_THROUGH"
  64. inaccessable = {}
  65. personal = set()
  66. def __getattr__(self, name):
  67. val = self.inaccessable.get(name, self.PASS_THROUGH)
  68. if val != self.PASS_THROUGH:
  69. return val
  70. return getattr(self._proxy_obj, name)
  71. def __hasattr__(self, name):
  72. return getattr(self._proxy_obj, name)
  73. def __setattr__(self, name, value):
  74. if name in self.inaccessable:
  75. raise AttributeError("Cannot set '%s' attribute on Proxy object" % name)
  76. if name in self.personal:
  77. return object.__setattr__(self, name, value)
  78. return setattr(self._proxy_obj, name, value)
  79. def __len__(self):
  80. return self._proxy_obj.__len__()
  81. class NotifyingProxy(event.ModificationNotifier, PassProxy):
  82. """
  83. Hooks the personal attributes to cause change events
  84. """
  85. def __setattr__(self, name, value):
  86. if name in self.personal:
  87. return super(NotifyingProxy, self).__setattr__(name, value)
  88. return PassProxy.__setattr__(self, name, value)
  89. class HookProxy(BaseProxy):
  90. """
  91. A proxy that only intercepts certain attributes
  92. """
  93. hooks = set()
  94. def __getattribute__(self, name):
  95. if name in object.__getattribute__(self, "hooks"):
  96. return self._proxy_obj.__getattribute__(name)
  97. return super(HookProxy, self).__getattribute__(name)
  98. def __setattr__(self, name, value):
  99. if name in object.__getattribute__(self, "hooks"):
  100. setattr(self._proxy_obj, name, value)
  101. else:
  102. super(HookProxy, self).__setattr__(name, value)
  103. class ChangeAbleProxy(NotifyingProxy):
  104. """
  105. A notifying PassProxy with a change handler, attached to original object
  106. and passing events on modifying them in such that they appear to originate
  107. from the Proxy object.
  108. Change handlers are installed as weak references, s.t. the installed change
  109. handlers don't solely cause the Proxy object to stay in memory
  110. """
  111. personal = set(["_change_handler"])
  112. _change_handler = None
  113. def __init__(self, obj):
  114. super(ChangeAbleProxy, self).__init__(obj)
  115. # use a weak ref and detach at some point
  116. weak_change_handler = weakmeth.WeakMethod(self.change_handler)
  117. def call(context):
  118. if weak_change_handler() is None:
  119. return
  120. return weak_change_handler()(context)
  121. self._weak_change_handler = call
  122. if hasattr(obj, "add_change_handler"):
  123. obj.add_change_handler(call)
  124. @property
  125. def _Changed(self):
  126. return self._proxy_obj._Changed
  127. def change_handler(self, context):
  128. """
  129. redo the change handler but replace the proxied object
  130. with our instance
  131. """
  132. cur = context._obj.pop()
  133. context.passOn(self)
  134. context._obj.append(cur)
  135. def __del__(self):
  136. """
  137. our object is no longer needed, so we can also remove the change handler
  138. from the original object
  139. """
  140. if hasattr(self._proxy_obj, "remove_change_handler"):
  141. self._proxy_obj.remove_change_handler(self._weak_change_handler)
  142. class PropertyProxy(EqualityBaseProxy, ChangeAbleProxy, odml.property.Property, nodes.PropertyNode):
  143. """
  144. A PropertyProxy provides transparent access to a Property,
  145. blocking only the 'mapping' attribute.
  146. It has however to take care, that when accessing its values, appropriate
  147. proxy objects are returned, as otherwise gui functionality breaks
  148. """
  149. inaccessable = {"mapping": None}#, "name": PassProxy.PASS_THROUGH}
  150. personal = set(["_section", "name", "_p_values"]) | ChangeAbleProxy.personal
  151. _Changed = event.Property._Changed
  152. _p_values = []
  153. # TODO make this generic
  154. @property
  155. def parent(self):
  156. return self._section
  157. @property
  158. def values(self):
  159. """
  160. provide transparent access to values as Proxy objects
  161. use "caching" to return the same proxy object for the same value
  162. as other functionality needs to be able to identify a certain object
  163. exactly (TODO append/insert/remove invalidates the cache and might still
  164. break stuff)
  165. """
  166. # do some caching
  167. if len(self._p_values) == len(self._proxy_obj.values):
  168. cached = True
  169. for i, val in enumerate(self._p_values):
  170. if val._proxy_obj is not self._proxy_obj.values[i]:
  171. cached = False
  172. break
  173. if cached:
  174. return self._p_values
  175. # transparently create proxy objects
  176. self._p_values = list(map(ValueProxy, self._proxy_obj.values))
  177. for i in self._p_values: # and make them remember to which property they belong
  178. i._property = self
  179. return self._p_values
  180. def change_handler(self, context):
  181. """
  182. we need to fix append/insert/remove notifications
  183. so that they deal with ProxyObjects instead
  184. """
  185. # TODO this needs to be double checked
  186. oval = None
  187. if isinstance(context.val, odml.value.Value):
  188. oval = context.val
  189. if context.postChange ^ (context.action == "remove"):
  190. assert oval.parent is not None
  191. i = self.values.index(oval)
  192. context.val = self.values[i]
  193. else: # the val does not yet/anymore exist
  194. assert oval.parent is None
  195. context.val = ValueProxy(oval)
  196. context.val._property = self
  197. super(PropertyProxy, self).change_handler(context)
  198. if oval is not None:
  199. context.val = oval
  200. def path_to(self, child):
  201. """
  202. override this, to account for proxy objects
  203. """
  204. if not isinstance(child, PassProxy):
  205. return super(PropertyProxy, self).path_to(child)
  206. for (i, obj) in enumerate(self._values):
  207. if obj is child._proxy_obj: return (i,)
  208. raise ValueError("%s does not contain the item %s" % (repr(self), repr(child)))
  209. class ValueProxy(EqualityBaseProxy, ChangeAbleProxy, odml.value.Value, nodes.ValueNode):
  210. """
  211. A ValueProxy provides transparent access to a Value,
  212. but has its own parent (_property)
  213. """
  214. personal = set(["_property"]) | ChangeAbleProxy.personal
  215. _Changed = event.Value._Changed
  216. # TODO make this generic
  217. @property
  218. def parent(self):
  219. return self._property
  220. class ReadOnlyObject(object):
  221. """
  222. An odml-object whose properties (according to *_format*)
  223. cannot be modified if *enable_protection* is set.
  224. """
  225. enable_protection = False
  226. def __setattr__(self, name, value):
  227. if self.enable_protection and name in self._format._args:
  228. raise AttributeError("attribute '%s' is read-only" % name)
  229. object.__setattr__(self, name, value)
  230. class SectionProxy(odml.getImplementation().Section, Proxy):
  231. """
  232. A Section that will also pass as a Proxy
  233. """
  234. def proxy_remove(self, obj):
  235. """
  236. explicitly remove a proxy object without side-effects
  237. """
  238. assert isinstance(obj, Proxy)
  239. super(SectionProxy, self).remove(obj)
  240. def proxy_append(self, obj):
  241. """
  242. explicitly append a proxy object without side-effects
  243. """
  244. assert isinstance(obj, Proxy)
  245. super(SectionProxy, self).append(obj)
  246. class BaseSectionProxy(SectionProxy):
  247. pass
  248. class ReadOnlySection(SectionProxy, ReadOnlyObject):
  249. """
  250. A protected section, that cannot be modified
  251. """
  252. # @property
  253. # def sections(self):
  254. # return []
  255. # TODO make this generic
  256. @property
  257. def parent(self):
  258. return self._parent
  259. def append(self, obj):
  260. if self.enable_protection:
  261. raise TypeError("Cannot append to a proxy section")
  262. super(SectionProxy, self).append(obj)
  263. def insert(self, index, obj):
  264. raise TypeError("Cannot insert into a proxy section")
  265. def remove(self, obj):
  266. if self.enable_protection:
  267. raise TypeError("Cannot remove from a proxy section")
  268. super(SectionProxy, self).remove(obj)
  269. def _unprotected(self, func):
  270. """
  271. briefly disable write protection for the execution
  272. of *func*
  273. """
  274. p = self.enable_protection
  275. self.enable_protection = False
  276. ret = func()
  277. self.enable_protection = p
  278. return ret
  279. class NonexistantSection(ReadOnlySection):
  280. """
  281. A NonexistantSection is a fake section that only exists
  282. in the mapping. However it has real children.
  283. """
  284. # there is an explicit method now: proxy_append
  285. # def append(self, obj):
  286. # # always allow to insert proxy objects
  287. # if isinstance(obj, Proxy):
  288. # super(SectionProxy, self).append(obj)
  289. # else:
  290. # super(NonexistantSection, self).append(obj)
  291. # def proxy_remove(self, obj):
  292. # # also allow to remove proxy objects
  293. # # unmap the original property and then remove it (this is done in mapping)
  294. # #if not isinstance(obj, PropertyProxy):
  295. # return super(NonexistantSection, self).remove(obj)
  296. #
  297. # raise NotImplementedError("""
  298. # we could do this, but this would break undo functionality, so
  299. # better remove the original property from its parent section
  300. # """)
  301. # # prop = obj._proxy_objremove_proxy
  302. # # mapping.unmap_property(prop=prop, mprop=obj)
  303. # # prop.parent.remove(prop)
  304. def merge(self, section=None):
  305. """
  306. performs the merge operation, briefly disables read_protection
  307. """
  308. self._unprotected(lambda: super(NonexistantSection, self).merge(section))
  309. def unmerge(self, section):
  310. """
  311. performs the unmerge operation, briefly disables read_protection
  312. """
  313. self._unprotected(lambda: super(NonexistantSection, self).unmerge(section))
  314. def __repr__(self):
  315. return super(NonexistantSection, self).__repr__().replace("<Section", "<?Section")
  316. class DocumentProxy(EqualityBaseProxy, odml.getImplementation().Document):
  317. """
  318. A bare bones DocumentProxy not yet proxing actually much. TODO make it a HookProxy?
  319. """
  320. def __init__(self, doc):
  321. super(DocumentProxy, self).__init__(doc)
  322. odml.getImplementation().Document.__init__(self) # TODO this might already be a different implementation now. and we're not an instance
  323. for name in odml.format.Document:
  324. if name in odml.format.Document._args: # skip 'sections'
  325. setattr(self, name, getattr(doc, name))
  326. # TODO append also needs to append to original document and care about mappings and stuff
  327. # TODO same for insert/remove
  328. def proxy_remove(self, obj):
  329. self.remove(obj)
  330. def proxy_append(self, obj):
  331. self.append(obj)
  332. #class DocumentProxy(EqualityBaseProxy, ChangeAbleProxy, odml.document.Document, nodes.DocumentNode):
  333. # """
  334. # A ValueProxy provides transparent access to a Value
  335. # """
  336. # personal = set(["_sections"]) | ChangeAbleProxy.personal
  337. # _Changed = event.Document._Changed
  338. #
  339. # # TODO make this generic
  340. # @property
  341. # def sections(self):
  342. # return self._sections
  343. #
  344. # def proxy_remove(self, obj):
  345. # self.remove(obj)
  346. #
  347. # def proxy_append(self, obj):
  348. # self.append(obj)
  349. class MappedSection(EqualityBaseProxy, HookProxy, ReadOnlySection):
  350. """
  351. Like a fake section, but we reference a concrete section
  352. and forward changes to it
  353. TODO stuff works, but clear to account for proxy_append/insert/remove
  354. """
  355. hooks = set(["name"])
  356. def __init__(self, obj, template=None):
  357. super(MappedSection, self).__init__(obj)
  358. if template is not None:
  359. obj = template
  360. super(ReadOnlySection, self).__init__(obj.name, obj.type)
  361. if hasattr(obj, "add_change_handler"):
  362. obj.add_change_handler(self.change_handler)
  363. def change_handler(self, context):
  364. if context.postChange and context.obj is context.cur:
  365. # intercept append and remove actions
  366. if context.action == "append" or context.action == "insert":
  367. self.proxy_append(self.mkproxy(context.val))
  368. return # don't pass this event further
  369. elif context.action == "remove":
  370. obj = self.contains(context.val) # find an equal one
  371. # the source section removed the object
  372. # if it was explicitly mapped, it might already be gone
  373. # if apparently not: handle it
  374. if obj is not None:
  375. self.proxy_remove(obj)
  376. # don't pass this event further: either it does not affect us anyway
  377. # (removing the mapping causes a seperate change event for this section)
  378. # or we explicitly remove the obj ourselves causing a seperate event too
  379. return
  380. # TODO this could cause conflicts in gui (maybe)
  381. # pass on the event replacing the original object with us (the proxy equivalent)
  382. cur = context._obj.pop()
  383. context.passOn(self)
  384. context._obj.append(cur)
  385. def mkproxy(self, obj):
  386. """
  387. create a proxy equivalent of obj
  388. obj is either a Section (-> MappedSection) or a Property (-> PropertyProxy)
  389. """
  390. if isinstance(obj, odml.property.Property):
  391. return PropertyProxy(obj)
  392. return MappedSection(obj)
  393. def append(self, obj):
  394. assert not isinstance(obj, Proxy) # proxies should always go through the proxy_append method. TODO also when setting links?
  395. # a normal object needs to be appended to the proxied section and then
  396. # we will handle (hopefully) everything else that might be necessary in
  397. # the change handler (i.e. append a proxy version to ourselves)
  398. return self._proxy_obj.append(obj)
  399. # won't support insert. it'd be crazy
  400. def remove(self, obj):
  401. # we should only contain proxy objects (unless a link is present TODO)
  402. if not isinstance(obj, Proxy):
  403. print("%s should only contain proxy objects, but then look at this" % repr(self), obj)
  404. obj = self.contains(obj)
  405. assert isinstance(obj, Proxy)
  406. # trying to remove a Section only present in the mapping is prohibited
  407. if isinstance(obj, NonexistantSection):
  408. # can't remove what doesn't really exist
  409. raise TypeError("Cannot remove from a proxy section")
  410. # remove the mapped object, which may be somewhere else!
  411. parent = obj._proxy_obj.parent
  412. if parent is not None:
  413. parent.remove(obj._proxy_obj)
  414. try:
  415. # use the normal remove method of Section
  416. super(SectionProxy, self).remove(obj)
  417. except ValueError: # we might have already removed the property in the change handler
  418. pass