base.py 23 KB

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