base.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. # -*- coding: utf-8
  2. """
  3. collects common base functionality
  4. """
  5. import sys
  6. import posixpath
  7. from odml import terminology
  8. from odml import mapping
  9. # from odml import doc
  10. from odml.tools.doc_inherit import inherit_docstring, allow_inherit_docstring
  11. class _baseobj(object):
  12. pass
  13. class baseobject(_baseobj):
  14. _format = None
  15. @property
  16. def document(self):
  17. """returns the Document object in which this object is contained"""
  18. if self.parent is None:
  19. return None
  20. return self.parent.document
  21. def get_terminology_equivalent(self):
  22. """
  23. returns the equivalent object in an terminology (should there be one
  24. defined) or None
  25. """
  26. return None
  27. def __eq__(self, obj):
  28. """
  29. do a deep comparison of this object and its odml properties
  30. """
  31. # cannot compare totally different stuff
  32. if not isinstance(obj, _baseobj):
  33. return False
  34. if not isinstance(self, obj.__class__):
  35. return False
  36. for key in self._format:
  37. if getattr(self, key) != getattr(obj, key):
  38. return False
  39. return True
  40. def __ne__(self, obj):
  41. """
  42. use the __eq__ function to determine if both objects are equal
  43. """
  44. return not self == obj
  45. def clean(self):
  46. """
  47. stub that doesn't do anything for this class
  48. """
  49. pass
  50. def clone(self, children=True):
  51. """
  52. clone this object recursively (if children is True) allowing to copy it independently
  53. to another document. If children is False, this acts as a template cloner, creating
  54. a copy of the object without any children
  55. """
  56. # TODO don't we need some recursion / deepcopy here?
  57. import copy
  58. obj = copy.copy(self)
  59. return obj
  60. def _reorder(self, childlist, new_index):
  61. l = childlist
  62. old_index = l.index(self)
  63. # 2 cases: insert after old_index / insert before
  64. if new_index > old_index:
  65. new_index += 1
  66. l.insert(new_index, self)
  67. if new_index < old_index:
  68. del l[old_index+1]
  69. else:
  70. del l[old_index]
  71. return old_index
  72. def reorder(self, new_index):
  73. """
  74. move this object in its parent child-list to the position *new_index*
  75. returns the old index at which the object was found
  76. """
  77. raise NotImplementedError
  78. class SafeList(list):
  79. def index(self, obj):
  80. """
  81. find obj in list
  82. be sure to use "is" based comparison (instead of __eq__)
  83. """
  84. for i, e in enumerate(self):
  85. if e is obj:
  86. return i
  87. raise ValueError("remove: %s not in list" % repr(obj))
  88. def remove(self, obj):
  89. """
  90. remove an element from this list
  91. be sure to use "is" based comparison (instead of __eq__)
  92. """
  93. del self[self.index(obj)]
  94. class SmartList(SafeList):
  95. def __getitem__(self, key):
  96. """
  97. provides element index also by searching for an element with a given name
  98. """
  99. # try normal list index first (for integers)
  100. if isinstance(key, int):
  101. return super(SmartList, self).__getitem__(key)
  102. # otherwise search the list
  103. for obj in self:
  104. if (hasattr(obj, "name") and obj.name == key) or key == obj:
  105. return obj
  106. # and fail eventually
  107. raise KeyError(key)
  108. def __contains__(self, key):
  109. for obj in self:
  110. if (hasattr(obj, "name") and obj.name == key) or key == obj:
  111. return True
  112. def append(self, obj):
  113. if obj.name in self:
  114. raise KeyError("Object with the same name already exists! " + str(obj))
  115. else:
  116. super(SmartList, self).append(obj)
  117. @allow_inherit_docstring
  118. class sectionable(baseobject, mapping.mapped):
  119. def __init__(self):
  120. self._sections = SmartList()
  121. self._repository = None
  122. @property
  123. def document(self):
  124. """
  125. returns the parent-most node (if its a document instance) or None
  126. """
  127. p = self
  128. while p.parent:
  129. p = p.parent
  130. import odml.doc as doc
  131. if isinstance(p, doc.Document):
  132. return p
  133. @property
  134. def sections(self):
  135. """the list of sections contained in this section/document"""
  136. return self._sections
  137. @mapping.remapable_insert
  138. def insert(self, position, section):
  139. """
  140. adds the section to the section-list and makes this document the section’s parent
  141. currently just appends the section and does not insert at the specified *position*
  142. """
  143. self._sections.append(section)
  144. section._parent = self
  145. @mapping.remapable_append
  146. def append(self, section):
  147. """adds the section to the section-list and makes this document the section’s parent"""
  148. self._sections.append(section)
  149. section._parent = self
  150. @inherit_docstring
  151. def reorder(self, new_index):
  152. return self._reorder(self.parent.sections, new_index)
  153. @mapping.remapable_remove
  154. def remove(self, section):
  155. """removes the specified child-section"""
  156. self._sections.remove(section)
  157. section._parent = None
  158. def __getitem__(self, key):
  159. return self._sections[key]
  160. def __len__(self):
  161. return len(self._sections)
  162. def __iter__(self):
  163. return self._sections.__iter__()
  164. def itersections(self, recursive=True, yield_self=False, filter_func=lambda x: True, max_depth=None):
  165. """
  166. iterate each child section
  167. >>> # example: return all subsections which name contains "foo"
  168. >>> filter_func = lambda x: getattr(x, 'name').find("foo") > -1
  169. >>> sec_or_doc.itersections(filter_func=filter_func)
  170. :param recursive: iterate all child sections recursively (deprecated)
  171. :type recursive: bool
  172. :param yield_self: includes itself in the iteration
  173. :type yield_self: bool
  174. :param filter_func: accepts a function that will be applied to each iterable. Yields
  175. iterable if function returns True
  176. :type filter_func: function
  177. """
  178. stack = []
  179. # below: never yield self if self is a Document
  180. if self == self.document and ((max_depth is None) or (max_depth > 0)):
  181. for sec in self.sections:
  182. stack.append((sec, 1)) # (<section>, <level in a tree>)
  183. elif not self == self.document:
  184. stack.append((self, 0)) # (<section>, <level in a tree>)
  185. while len(stack) > 0:
  186. (sec, level) = stack.pop(0)
  187. if filter_func(sec) and (yield_self if level == 0 else True):
  188. yield sec
  189. if max_depth is None or level < max_depth:
  190. for sec in sec.sections:
  191. stack.append((sec, level + 1))
  192. def iterproperties(self, max_depth=None, filter_func=lambda x: True):
  193. """
  194. iterate each related property (recursively)
  195. >>> # example: return all children properties which name contains "foo"
  196. >>> filter_func = lambda x: getattr(x, 'name').find("foo") > -1
  197. >>> sec_or_doc.iterproperties(filter_func=filter_func)
  198. :param max_depth: iterate all properties recursively if None, only to a certain
  199. level otherwise
  200. :type max_depth: bool
  201. :param filter_func: accepts a function that will be applied to each iterable. Yields
  202. iterable if function returns True
  203. :type filter_func: function
  204. """
  205. for sec in [s for s in self.itersections(max_depth=max_depth, yield_self=True)]:
  206. if hasattr(sec, "properties"): # not to fail if odml.Document
  207. for i in sec.properties:
  208. if filter_func(i):
  209. yield i
  210. def itervalues(self, max_depth=None, filter_func=lambda x: True):
  211. """
  212. iterate each related value (recursively)
  213. >>> # example: return all children values which string converted version has "foo"
  214. >>> filter_func = lambda x: str(getattr(x, 'data')).find("foo") > -1
  215. >>> sec_or_doc.itervalues(filter_func=filter_func)
  216. :param max_depth: iterate all properties recursively if None, only to a certain
  217. level otherwise
  218. :type max_depth: bool
  219. :param filter_func: accepts a function that will be applied to each iterable. Yields
  220. iterable if function returns True
  221. :type filter_func: function
  222. """
  223. for prop in [p for p in self.iterproperties(max_depth=max_depth)]:
  224. for v in prop.values:
  225. if filter_func(v):
  226. yield v
  227. def contains(self, obj):
  228. """
  229. checks if a subsection of name&type of *obj* is a child of this section
  230. if so return this child
  231. """
  232. for i in self._sections:
  233. if obj.name == i.name and obj.type == i.type:
  234. return i
  235. def _matches(self, obj, key=None, type=None, include_subtype=False):
  236. """
  237. find out
  238. * if the *key* matches obj.name (if key is not None)
  239. * or if *type* matches obj.type (if type is not None)
  240. * if type does not match exactly, test for subtype. (e.g.stimulus/white_noise)
  241. comparisons are case-insensitive, however both key and type
  242. MUST be lower-case.
  243. """
  244. name_match = (key is None or (key is not None and hasattr(obj, "name") and obj.name == key))
  245. exact_type_match = (type is None or (type is not None and hasattr(obj, "type") and obj.type.lower() == type))
  246. if not include_subtype:
  247. return name_match and exact_type_match
  248. subtype_match = type is None or (type is not None and hasattr(obj, "type") and
  249. type in obj.type.lower().split('/')[:-1])
  250. return name_match and (exact_type_match or subtype_match)
  251. def get_section_by_path(self, path):
  252. """
  253. find a Section through a path like "../name1/name2"
  254. :param path: path like "../name1/name2"
  255. :type path: str
  256. """
  257. return self._get_section_by_path(path)
  258. def get_property_by_path(self, path):
  259. """
  260. find a Property through a path like "../name1/name2:property_name"
  261. :param path: path like "../name1/name2:property_name"
  262. :type path: str
  263. """
  264. laststep = path.split(":") # assuming section names do not contain :
  265. found = self._get_section_by_path(laststep[0])
  266. return self._match_iterable(found.properties, ":".join(laststep[1:]))
  267. def _match_iterable(self, iterable, key):
  268. """
  269. Searches for a key match within a given iterable.
  270. Raises ValueError if not found.
  271. """
  272. for obj in iterable:
  273. if self._matches(obj, key):
  274. return obj
  275. raise ValueError("Object named '%s' does not exist" % key)
  276. def _get_section_by_path(self, path):
  277. """
  278. Returns a Section by a given path.
  279. Raises ValueError if not found.
  280. """
  281. if path.startswith("/"):
  282. if len(path) == 1:
  283. raise ValueError("Not a valid path")
  284. doc = self.document
  285. if doc is not None:
  286. return doc._get_section_by_path(path[1:])
  287. raise ValueError("A section with no Document cannot resolve absolute path")
  288. pathlist = path.split("/")
  289. if len(pathlist) > 1:
  290. if pathlist[0] == "..":
  291. found = self.parent
  292. elif pathlist[0] == ".":
  293. found = self
  294. else:
  295. found = self._match_iterable(self.sections, pathlist[0])
  296. if found:
  297. return found._get_section_by_path("/".join(pathlist[1:]))
  298. raise ValueError("Section named '%s' does not exist" % pathlist[0])
  299. else:
  300. return self._match_iterable(self.sections, pathlist[0])
  301. def find(self, key=None, type=None, findAll=False, include_subtype=False):
  302. """return the first subsection named *key* of type *type*"""
  303. ret = []
  304. if type:
  305. type = type.lower()
  306. for s in self._sections:
  307. if self._matches(s, key, type, include_subtype=include_subtype):
  308. if findAll:
  309. ret.append(s)
  310. else:
  311. return s
  312. if ret:
  313. return ret
  314. def find_related(self, key=None, type=None, children=True, siblings=True, parents=True, recursive=True, findAll=False):
  315. """
  316. finds a related section named *key* and/or *type*
  317. * by searching its children’s children if *children* is True
  318. if *recursive* is true all leave nodes will be searched
  319. * by searching its siblings if *siblings* is True
  320. * by searching the parent element if *parents* is True
  321. if *recursive* is True all parent nodes until the root are searched
  322. * if *findAll* is True, returns a list of all matching objects
  323. """
  324. ret = []
  325. if type:
  326. type = type.lower()
  327. if children:
  328. for section in self._sections:
  329. if self._matches(section, key, type):
  330. if findAll:
  331. ret.append(section)
  332. else:
  333. return section
  334. if recursive:
  335. obj = section.find_related(key, type, children, siblings=False, parents=False, recursive=recursive, findAll=findAll)
  336. if obj is not None:
  337. if findAll:
  338. ret += obj
  339. else:
  340. return obj
  341. if siblings and self.parent is not None:
  342. obj = self.parent.find(key, type, findAll)
  343. if obj is not None:
  344. if findAll:
  345. ret += obj
  346. else:
  347. return obj
  348. if parents:
  349. obj = self
  350. while obj.parent is not None:
  351. obj = obj.parent
  352. if self._matches(obj, key, type):
  353. if findAll:
  354. ret.append(obj)
  355. else:
  356. return obj
  357. if not recursive:
  358. break
  359. if ret:
  360. return ret
  361. return None
  362. def get_path(self):
  363. """
  364. returns the absolute path of this section
  365. """
  366. node = self
  367. path = []
  368. while node.parent is not None:
  369. path.insert(0, node.name)
  370. node = node.parent
  371. return "/" + "/".join(path)
  372. @staticmethod
  373. def _get_relative_path(a, b):
  374. """
  375. returns a relative path for navigation from dir *a* to dir *b*
  376. if the common parent of both is "/", return an absolute path
  377. """
  378. a += "/"
  379. b += "/"
  380. parent = posixpath.dirname(posixpath.commonprefix([a,b]))
  381. if parent == "/":
  382. return b[:-1]
  383. a = posixpath.relpath(a, parent)
  384. b = posixpath.relpath(b, parent)
  385. if a == ".":
  386. return b
  387. return posixpath.normpath("../" * (a.count("/")+1) + b)
  388. def get_relative_path(self, section):
  389. """
  390. returns a relative (file)path to point to section (e.g. ../other_section)
  391. if the common parent of both sections is the document (i.e. /), return an absolute path
  392. """
  393. a = self.get_path()
  394. b = section.get_path()
  395. return self._get_relative_path(a, b)
  396. def clean(self):
  397. """
  398. Runs clean() on all immediate child-sections causing any resolved links
  399. or includes to be unresolved.
  400. This should be called for the document prior to saving.
  401. """
  402. for i in self:
  403. i.clean()
  404. def clone(self, children=True):
  405. """
  406. clone this object recursively allowing to copy it independently
  407. to another document
  408. """
  409. obj = super(sectionable, self).clone(children)
  410. obj._parent = None
  411. obj._sections = SmartList()
  412. if children:
  413. for s in self._sections:
  414. obj.append(s.clone())
  415. return obj
  416. @property
  417. def repository(self):
  418. """An url to a terminology."""
  419. return self._repository
  420. @repository.setter
  421. def repository(self, url):
  422. if not url:
  423. url = None
  424. self._repository = url
  425. if url:
  426. terminology.deferred_load(url)
  427. def get_repository(self):
  428. """
  429. return the current applicable repository (may be inherited from a
  430. parent) or None
  431. """
  432. return self._repository