base.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  1. # -*- coding: utf-8
  2. """
  3. Collects common base functionality
  4. """
  5. import collections
  6. import posixpath
  7. from . import terminology
  8. from .tools.doc_inherit import allow_inherit_docstring
  9. class BaseObject(object):
  10. _format = None
  11. def __hash__(self):
  12. """
  13. Allow all odML objects to be hash-able.
  14. """
  15. return id(self)
  16. def __eq__(self, obj):
  17. """
  18. Do a deep comparison of this object and its odml properties.
  19. The 'id' attribute of an object is excluded, since it is
  20. unique within a document.
  21. """
  22. # cannot compare totally different stuff
  23. if not isinstance(self, obj.__class__):
  24. return False
  25. for key in self._format:
  26. if key == "id" or key == "oid":
  27. continue
  28. elif getattr(self, key) != getattr(obj, key):
  29. return False
  30. return True
  31. def __ne__(self, obj):
  32. """
  33. Use the __eq__ function to determine if both objects are equal
  34. """
  35. return not self == obj
  36. def format(self):
  37. return self._format
  38. @property
  39. def document(self):
  40. """ Returns the Document object in which this object is contained """
  41. if self.parent is None:
  42. return None
  43. return self.parent.document
  44. def get_terminology_equivalent(self):
  45. """
  46. Returns the equivalent object in an terminology (should there be one
  47. defined) or None
  48. """
  49. return None
  50. def clean(self):
  51. """
  52. Stub that doesn't do anything for this class
  53. """
  54. pass
  55. def clone(self, children=True):
  56. """
  57. Clone this object recursively (if children is True) allowing to copy it
  58. independently to another document. If children is False, this acts as
  59. a template cloner, creating a copy of the object without any children
  60. """
  61. # TODO don't we need some recursion / deepcopy here?
  62. import copy
  63. obj = copy.copy(self)
  64. return obj
  65. class SmartList(list):
  66. def __init__(self, content_type):
  67. """
  68. Only values of the instance *content_type* can be added to the SmartList.
  69. """
  70. self._content_type = content_type
  71. super(SmartList, self).__init__()
  72. def __getitem__(self, key):
  73. """
  74. Provides element index also by searching for an element with a given name.
  75. """
  76. # Try normal list index first (for integers)
  77. if isinstance(key, int):
  78. return super(SmartList, self).__getitem__(key)
  79. # Otherwise search the list
  80. for obj in self:
  81. if (hasattr(obj, "name") and obj.name == key) or key == obj:
  82. return obj
  83. # and fail eventually
  84. raise KeyError(key)
  85. def __setitem__(self, key, value):
  86. """
  87. Replaces item at list[*key*] with *value*.
  88. :param key: index position
  89. :param value: object that replaces item at *key* position.
  90. value has to be of the same content type as the list.
  91. In this context usually a Section or a Property.
  92. """
  93. if not isinstance(value, self._content_type):
  94. raise ValueError("List only supports elements of type '%s'" %
  95. self._content_type)
  96. # If required remove new object from its old parents child-list
  97. if hasattr(value, "_parent") and (value._parent and value in value._parent):
  98. value._parent.remove(value)
  99. # If required move parent reference from replaced to new object
  100. # and set parent reference on replaced object None.
  101. if hasattr(self[key], "_parent"):
  102. value._parent = self[key]._parent
  103. self[key]._parent = None
  104. super(SmartList, self).__setitem__(key, value)
  105. def __contains__(self, key):
  106. for obj in self:
  107. if (hasattr(obj, "name") and obj.name == key) or key == obj:
  108. return True
  109. def __eq__(self, obj):
  110. """
  111. SmartList attributes of 'sections' and 'properties' are
  112. handled specially: We want to make sure that the lists'
  113. objects are properly compared without changing the order
  114. of the individual lists.
  115. """
  116. # This special case was introduced only due to the fact
  117. # that RDF files will be loaded with randomized list
  118. # order. With any other file format the list order
  119. # remains unchanged.
  120. if sorted(self, key=lambda x: x.name) != sorted(obj, key=lambda x: x.name):
  121. return False
  122. return True
  123. def __ne__(self, obj):
  124. """
  125. Use the __eq__ function to determine if both objects are equal
  126. """
  127. return not self == obj
  128. def index(self, obj):
  129. """
  130. Find obj in list
  131. """
  132. for i, e in enumerate(self):
  133. if e is obj:
  134. return i
  135. raise ValueError("remove: %s not in list" % repr(obj))
  136. def remove(self, obj):
  137. """
  138. Remove an element from this list.
  139. """
  140. del self[self.index(obj)]
  141. def append(self, *obj_tuple):
  142. for obj in obj_tuple:
  143. if obj.name in self:
  144. raise KeyError(
  145. "Object with the same name already exists! " + str(obj))
  146. if not isinstance(obj, self._content_type):
  147. raise ValueError("List only supports elements of type '%s'" %
  148. self._content_type)
  149. super(SmartList, self).append(obj)
  150. def sort(self, key=lambda x: x.name, reverse=False):
  151. """
  152. If not otherwise defined, sort by the *name* attribute
  153. of the lists *_content_type* object.
  154. """
  155. super(SmartList, self).sort(key=key, reverse=reverse)
  156. @allow_inherit_docstring
  157. class Sectionable(BaseObject):
  158. def __init__(self):
  159. from odml.section import BaseSection
  160. self._sections = SmartList(BaseSection)
  161. self._repository = None
  162. def __getitem__(self, key):
  163. return self._sections[key]
  164. def __len__(self):
  165. return len(self._sections)
  166. def __iter__(self):
  167. return self._sections.__iter__()
  168. @property
  169. def document(self):
  170. """
  171. Returns the parent-most node (if its a document instance) or None
  172. """
  173. from odml.doc import BaseDocument
  174. p = self
  175. while p.parent:
  176. p = p.parent
  177. if isinstance(p, BaseDocument):
  178. return p
  179. @property
  180. def sections(self):
  181. """ The list of sections contained in this section/document """
  182. return self._sections
  183. def insert(self, position, section):
  184. """
  185. Insert a Section at the child-list position. A ValueError will be raised,
  186. if a Section with the same name already exists in the child-list.
  187. :param position: index at which the object should be inserted.
  188. :param section: odML Section object.
  189. """
  190. from odml.section import BaseSection
  191. if isinstance(section, BaseSection):
  192. if section.name in self._sections:
  193. raise ValueError("Section with name '%s' already exists." % section.name)
  194. self._sections.insert(position, section)
  195. section._parent = self
  196. else:
  197. raise ValueError("Can only insert objects of type Section.")
  198. def append(self, section):
  199. """
  200. Method appends a single Section to the section child-lists of the current Object.
  201. :param section: odML Section object.
  202. """
  203. from odml.section import BaseSection
  204. if isinstance(section, BaseSection):
  205. self._sections.append(section)
  206. section._parent = self
  207. elif isinstance(section, collections.Iterable) and not isinstance(section, str):
  208. raise ValueError("Use extend to add a list of Sections.")
  209. else:
  210. raise ValueError("Can only append objects of type Section.")
  211. def extend(self, sec_list):
  212. """
  213. Method adds Sections to the section child-list of the current object.
  214. :param sec_list: Iterable containing odML Section entries.
  215. """
  216. from odml.section import BaseSection
  217. if not isinstance(sec_list, collections.Iterable):
  218. raise TypeError("'%s' object is not iterable" % type(sec_list).__name__)
  219. # Make sure only Sections with unique names will be added.
  220. for sec in sec_list:
  221. if not isinstance(sec, BaseSection):
  222. raise ValueError("Can only extend objects of type Section.")
  223. elif isinstance(sec, BaseSection) and sec.name in self._sections:
  224. raise KeyError("Section with name '%s' already exists." % sec.name)
  225. for sec in sec_list:
  226. self.append(sec)
  227. def remove(self, section):
  228. """ Removes the specified child-section """
  229. self._sections.remove(section)
  230. section._parent = None
  231. def itersections(self, recursive=True, yield_self=False,
  232. filter_func=lambda x: True, max_depth=None):
  233. """
  234. Iterate each child section
  235. Example: return all subsections which name contains "foo"
  236. >>> filter_func = lambda x: getattr(x, 'name').find("foo") > -1
  237. >>> sec_or_doc.itersections(filter_func=filter_func)
  238. :param recursive: iterate all child sections recursively (deprecated)
  239. :type recursive: bool
  240. :param yield_self: includes itself in the iteration
  241. :type yield_self: bool
  242. :param filter_func: accepts a function that will be applied to each
  243. iterable. Yields iterable if function returns True
  244. :type filter_func: function
  245. """
  246. stack = []
  247. # Below: never yield self if self is a Document
  248. if self == self.document and ((max_depth is None) or (max_depth > 0)):
  249. for sec in self.sections:
  250. stack.append((sec, 1)) # (<section>, <level in a tree>)
  251. elif self != self.document:
  252. stack.append((self, 0)) # (<section>, <level in a tree>)
  253. while len(stack) > 0:
  254. (sec, level) = stack.pop(0)
  255. if filter_func(sec) and (yield_self if level == 0 else True):
  256. yield sec
  257. if max_depth is None or level < max_depth:
  258. for sec in sec.sections:
  259. stack.append((sec, level + 1))
  260. def iterproperties(self, max_depth=None, filter_func=lambda x: True):
  261. """
  262. Iterate each related property (recursively)
  263. Example: return all children properties which name contains "foo"
  264. >>> filter_func = lambda x: getattr(x, 'name').find("foo") > -1
  265. >>> sec_or_doc.iterproperties(filter_func=filter_func)
  266. :param max_depth: iterate all properties recursively if None, only to
  267. a certain level otherwise
  268. :type max_depth: bool
  269. :param filter_func: accepts a function that will be applied to each
  270. iterable. Yields iterable if function returns True
  271. :type filter_func: function
  272. """
  273. for sec in [s for s in self.itersections(max_depth=max_depth,
  274. yield_self=True)]:
  275. if hasattr(sec, "properties"): # not to fail if odml.Document
  276. for i in sec.properties:
  277. if filter_func(i):
  278. yield i
  279. def itervalues(self, max_depth=None, filter_func=lambda x: True):
  280. """
  281. Iterate each related value (recursively)
  282. # Example: return all children values which string converted version
  283. has "foo"
  284. >>> filter_func = lambda x: str(x).find("foo") > -1
  285. >>> sec_or_doc.itervalues(filter_func=filter_func)
  286. :param max_depth: iterate all properties recursively if None, only to
  287. a certain level otherwise
  288. :type max_depth: bool
  289. :param filter_func: accepts a function that will be applied to each
  290. iterable. Yields iterable if function returns True
  291. :type filter_func: function
  292. """
  293. for prop in [p for p in self.iterproperties(max_depth=max_depth)]:
  294. if filter_func(prop.values):
  295. yield prop.values
  296. def contains(self, obj):
  297. """
  298. Checks if a subsection of name&type of *obj* is a child of this section
  299. if so return this child
  300. """
  301. for i in self._sections:
  302. if obj.name == i.name and obj.type == i.type:
  303. return i
  304. def _matches(self, obj, key=None, otype=None, include_subtype=False):
  305. """
  306. Find out
  307. * if the *key* matches obj.name (if key is not None)
  308. * or if *otype* matches obj.type (if type is not None)
  309. * if type does not match exactly, test for subtype.
  310. (e.g.stimulus/white_noise)
  311. comparisons are case-insensitive, however both key and type
  312. MUST be lower-case.
  313. """
  314. name_match = (key is None or (
  315. key is not None and hasattr(obj, "name") and obj.name == key))
  316. exact_type_match = (otype is None or (otype is not None and
  317. hasattr(obj, "type") and
  318. obj.type.lower() == otype))
  319. if not include_subtype:
  320. return name_match and exact_type_match
  321. subtype_match = (otype is None or
  322. (otype is not None and hasattr(obj, "type") and
  323. otype in obj.type.lower().split('/')[:-1]))
  324. return name_match and (exact_type_match or subtype_match)
  325. def get_section_by_path(self, path):
  326. """
  327. Find a Section through a path like "../name1/name2"
  328. :param path: path like "../name1/name2"
  329. :type path: str
  330. """
  331. return self._get_section_by_path(path)
  332. def get_property_by_path(self, path):
  333. """
  334. Find a Property through a path like "../name1/name2:property_name"
  335. :param path: path like "../name1/name2:property_name"
  336. :type path: str
  337. """
  338. laststep = path.split(":") # assuming section names do not contain :
  339. found = self._get_section_by_path(laststep[0])
  340. return self._match_iterable(found.properties, ":".join(laststep[1:]))
  341. def _match_iterable(self, iterable, key):
  342. """
  343. Searches for a key match within a given iterable.
  344. Raises ValueError if not found.
  345. """
  346. for obj in iterable:
  347. if self._matches(obj, key):
  348. return obj
  349. raise ValueError("Object named '%s' does not exist" % key)
  350. def _get_section_by_path(self, path):
  351. """
  352. Returns a Section by a given path.
  353. Raises ValueError if not found.
  354. """
  355. if path.startswith("/"):
  356. if len(path) == 1:
  357. raise ValueError("Not a valid path")
  358. doc = self.document
  359. if doc is not None:
  360. return doc._get_section_by_path(path[1:])
  361. raise ValueError(
  362. "A section with no Document cannot resolve absolute path")
  363. pathlist = path.split("/")
  364. if len(pathlist) > 1:
  365. if pathlist[0] == "..":
  366. found = self.parent
  367. elif pathlist[0] == ".":
  368. found = self
  369. else:
  370. found = self._match_iterable(self.sections, pathlist[0])
  371. if found:
  372. return found._get_section_by_path("/".join(pathlist[1:]))
  373. raise ValueError("Section named '%s' does not exist" % pathlist[0])
  374. else:
  375. return self._match_iterable(self.sections, pathlist[0])
  376. def find(self, key=None, type=None, findAll=False, include_subtype=False):
  377. """ Return the first subsection named *key* of type *type* """
  378. ret = []
  379. if type:
  380. type = type.lower()
  381. for s in self._sections:
  382. if self._matches(s, key, type, include_subtype=include_subtype):
  383. if findAll:
  384. ret.append(s)
  385. else:
  386. return s
  387. if ret:
  388. return ret
  389. def find_related(self, key=None, type=None, children=True, siblings=True,
  390. parents=True, recursive=True, findAll=False):
  391. """
  392. Finds a related section named *key* and/or *type*
  393. * by searching its children’s children if *children* is True
  394. if *recursive* is true all leave nodes will be searched
  395. * by searching its siblings if *siblings* is True
  396. * by searching the parent element if *parents* is True
  397. if *recursive* is True all parent nodes until the root are searched
  398. * if *findAll* is True, returns a list of all matching objects
  399. """
  400. ret = []
  401. if type:
  402. type = type.lower()
  403. if children:
  404. for section in self._sections:
  405. if self._matches(section, key, type):
  406. if findAll:
  407. ret.append(section)
  408. else:
  409. return section
  410. if recursive:
  411. obj = section.find_related(key, type, children,
  412. siblings=False, parents=False,
  413. recursive=recursive,
  414. findAll=findAll)
  415. if obj is not None:
  416. if findAll:
  417. ret += obj
  418. else:
  419. return obj
  420. if siblings and self.parent is not None:
  421. obj = self.parent.find(key, type, findAll)
  422. if obj is not None:
  423. if findAll:
  424. ret += obj
  425. else:
  426. return obj
  427. if parents:
  428. obj = self
  429. while obj.parent is not None:
  430. obj = obj.parent
  431. if self._matches(obj, key, type):
  432. if findAll:
  433. ret.append(obj)
  434. else:
  435. return obj
  436. if not recursive:
  437. break
  438. if ret:
  439. return ret
  440. return None
  441. def get_path(self):
  442. """
  443. Returns the absolute path of this section
  444. """
  445. node = self
  446. path = []
  447. while node.parent is not None:
  448. path.insert(0, node.name)
  449. node = node.parent
  450. return "/" + "/".join(path)
  451. @staticmethod
  452. def _get_relative_path(a, b):
  453. """
  454. Returns a relative path for navigation from dir *a* to dir *b*
  455. If the common parent of both is "/", return an absolute path
  456. """
  457. a += "/"
  458. b += "/"
  459. parent = posixpath.dirname(posixpath.commonprefix([a, b]))
  460. if parent == "/":
  461. return b[:-1]
  462. a = posixpath.relpath(a, parent)
  463. b = posixpath.relpath(b, parent)
  464. if a == ".":
  465. return b
  466. return posixpath.normpath("../" * (a.count("/") + 1) + b)
  467. def get_relative_path(self, section):
  468. """
  469. Returns a relative (file)path to point to section
  470. like (e.g. ../other_section)
  471. If the common parent of both sections is the document (i.e. /),
  472. return an absolute path
  473. """
  474. a = self.get_path()
  475. b = section.get_path()
  476. return self._get_relative_path(a, b)
  477. def clean(self):
  478. """
  479. Runs clean() on all immediate child-sections causing any resolved links
  480. or includes to be unresolved.
  481. This should be called for the document prior to saving.
  482. """
  483. for i in self:
  484. i.clean()
  485. def clone(self, children=True, keep_id=False):
  486. """
  487. Clone this object recursively allowing to copy it independently
  488. to another document
  489. """
  490. from odml.section import BaseSection
  491. obj = super(Sectionable, self).clone(children)
  492. obj._parent = None
  493. obj._sections = SmartList(BaseSection)
  494. if children:
  495. for s in self._sections:
  496. obj.append(s.clone(keep_id=keep_id))
  497. return obj
  498. @property
  499. def repository(self):
  500. """ A URL to a terminology. """
  501. return self._repository
  502. @repository.setter
  503. def repository(self, url):
  504. if not url:
  505. url = None
  506. self._repository = url
  507. if url:
  508. terminology.deferred_load(url)
  509. def get_repository(self):
  510. """
  511. Return the current applicable repository (may be inherited from a
  512. parent) or None
  513. """
  514. return self._repository
  515. def create_section(self, name, type="undefined", oid=None):
  516. """
  517. Creates a new subsection that is a child of this section.
  518. :param name: The name of the section to create.
  519. :param type: The type of the section.
  520. :param oid: object id, UUID string as specified in RFC 4122. If no id
  521. is provided, an id will be generated and assigned.
  522. :return: The new section.
  523. """
  524. from odml.section import BaseSection
  525. sec = BaseSection(name=name, type=type, oid=oid)
  526. sec.parent = self
  527. return sec