section.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. # -*- coding: utf-8
  2. import collections
  3. import uuid
  4. from . import base
  5. from . import format
  6. from . import terminology
  7. from .doc import BaseDocument
  8. # this is supposedly ok, as we only use it for an isinstance check
  9. from .property import BaseProperty
  10. # it MUST however not be used to create any Property objects
  11. from .tools.doc_inherit import inherit_docstring, allow_inherit_docstring
  12. @allow_inherit_docstring
  13. class BaseSection(base.Sectionable):
  14. """ An odML Section """
  15. type = None
  16. reference = None # the *import* property
  17. _link = None
  18. _include = None
  19. _merged = None
  20. _format = format.Section
  21. def __init__(self, name=None, type=None, parent=None,
  22. definition=None, reference=None,
  23. repository=None, link=None, include=None, oid=None):
  24. # Sets _sections Smartlist and _repository to None, so run first.
  25. super(BaseSection, self).__init__()
  26. self._props = base.SmartList(BaseProperty)
  27. try:
  28. if oid is not None:
  29. self._id = str(uuid.UUID(oid))
  30. else:
  31. self._id = str(uuid.uuid4())
  32. except ValueError as e:
  33. print(e)
  34. self._id = str(uuid.uuid4())
  35. # Use id if no name was provided.
  36. if not name:
  37. name = self._id
  38. self._parent = None
  39. self._name = name
  40. self._definition = definition
  41. self._reference = reference
  42. self._repository = repository
  43. self._link = link
  44. self._include = include
  45. # this may fire a change event, so have the section setup then
  46. self.type = type
  47. self.parent = parent
  48. def __repr__(self):
  49. return "Section[%d|%d] {name = %s, type = %s, id = %s}" % (len(self._sections),
  50. len(self._props),
  51. self._name,
  52. self.type,
  53. self.id)
  54. def __iter__(self):
  55. """
  56. Iterate over each section and property contained in this section
  57. """
  58. for section in self._sections:
  59. yield section
  60. for prop in self._props:
  61. yield prop
  62. def __len__(self):
  63. """
  64. Number of children (sections AND properties)
  65. """
  66. return len(self._sections) + len(self._props)
  67. @property
  68. def oid(self):
  69. """
  70. The uuid for the section. Required for entity creation and comparison,
  71. saving and loading.
  72. """
  73. return self.id
  74. @property
  75. def id(self):
  76. """
  77. The uuid for the section.
  78. """
  79. return self._id
  80. def new_id(self, oid=None):
  81. """
  82. new_id sets the id of the current object to a RFC 4122 compliant UUID.
  83. If an id was provided, it is assigned if it is RFC 4122 UUID format compliant.
  84. If no id was provided, a new UUID is generated and assigned.
  85. :param oid: UUID string as specified in RFC 4122.
  86. """
  87. if oid is not None:
  88. self._id = str(uuid.UUID(oid))
  89. else:
  90. self._id = str(uuid.uuid4())
  91. @property
  92. def name(self):
  93. return self._name
  94. @name.setter
  95. def name(self, new_value):
  96. if self.name == new_value:
  97. return
  98. curr_parent = self.parent
  99. if hasattr(curr_parent, "sections") and new_value in curr_parent.sections:
  100. raise KeyError("Object with the same name already exists!")
  101. self._name = new_value
  102. @property
  103. def include(self):
  104. """
  105. The same as :py:attr:`odml.section.BaseSection.link`, except that
  106. include specifies an arbitrary url instead of a local path within
  107. the same document
  108. """
  109. return self._include
  110. @include.setter
  111. def include(self, new_value):
  112. if self._link is not None:
  113. raise TypeError("%s.include: You can either set link or include, "
  114. "but not both." % repr(self))
  115. if not new_value:
  116. self._include = None
  117. self.clean()
  118. return
  119. if '#' in new_value:
  120. url, path = new_value.split('#', 1)
  121. else:
  122. url, path = new_value, None
  123. terminology.deferred_load(url)
  124. if self.parent is None:
  125. self._include = new_value
  126. return
  127. term = terminology.load(url)
  128. new_section = term.get_section_by_path(
  129. path) if path is not None else term.sections[0]
  130. if self._include is not None:
  131. self.clean()
  132. self._include = new_value
  133. # strict needs to be False, otherwise finalizing a document will
  134. # basically always fail.
  135. self.merge(new_section, strict=False)
  136. @property
  137. def link(self):
  138. """
  139. Specifies a softlink, i.e. a path within the document
  140. When the merge()-method is called, the link will be resolved creating
  141. according copies of the section referenced by the link attribute.
  142. When the unmerge() method is called (happens when running clean())
  143. the link is unresolved, i.e. all properties and sections that are
  144. completely equivalent to the merged object will be removed.
  145. (They will be restored accordingly when calling merge()).
  146. When changing the *link* attribute, the previously merged section is
  147. unmerged, and the new reference will be immediately resolved. To avoid
  148. this side-effect, directly change the *_link* attribute.
  149. """
  150. return self._link
  151. @link.setter
  152. def link(self, new_value):
  153. if self._include is not None:
  154. raise TypeError("%s.link: You can either set link or include,"
  155. " but not both." % repr(self))
  156. if self.parent is None: # we cannot possibly know where the link goes
  157. self._link = new_value
  158. return
  159. if not new_value:
  160. self._link = None
  161. self.clean()
  162. return
  163. # raises exception if path cannot be found
  164. new_section = self.get_section_by_path(new_value)
  165. if self._link is not None:
  166. self.clean()
  167. self._link = new_value
  168. # strict needs to be False, otherwise finalizing a document will
  169. # basically always fail.
  170. self.merge(new_section, strict=False)
  171. @property
  172. def definition(self):
  173. """ Name Definition of the section """
  174. if hasattr(self, "_definition"):
  175. return self._definition
  176. else:
  177. return None
  178. @definition.setter
  179. def definition(self, new_value):
  180. if new_value == "":
  181. new_value = None
  182. self._definition = new_value
  183. @definition.deleter
  184. def definition(self):
  185. del self._definition
  186. @property
  187. def reference(self):
  188. return self._reference
  189. @reference.setter
  190. def reference(self, new_value):
  191. if new_value == "":
  192. new_value = None
  193. self._reference = new_value
  194. # API (public)
  195. #
  196. # properties
  197. @property
  198. def properties(self):
  199. """ The list of all properties contained in this section """
  200. return self._props
  201. @property
  202. def props(self):
  203. """ The list of all properties contained in this section;
  204. NIXpy format style alias for 'properties'."""
  205. return self._props
  206. @property
  207. def sections(self):
  208. """ The list of all child-sections of this section """
  209. return self._sections
  210. @property
  211. def parent(self):
  212. """ The parent section, the parent document or None """
  213. return self._parent
  214. @parent.setter
  215. def parent(self, new_parent):
  216. if new_parent is None and self._parent is None:
  217. return
  218. elif new_parent is None and self._parent is not None:
  219. self._parent.remove(self)
  220. self._parent = None
  221. elif self._validate_parent(new_parent):
  222. if self._parent is not None:
  223. self._parent.remove(self)
  224. self._parent = new_parent
  225. self._parent.append(self)
  226. else:
  227. raise ValueError(
  228. "odml.Section.parent: passed value is not of consistent type!"
  229. "\nodml.Document or odml.Section expected")
  230. def _validate_parent(self, new_parent):
  231. if isinstance(new_parent, BaseDocument) or \
  232. isinstance(new_parent, BaseSection):
  233. return True
  234. return False
  235. def get_repository(self):
  236. """
  237. Returns the repository responsible for this section,
  238. which might not be the *repository* attribute, but may
  239. be inherited from a parent section / the document
  240. """
  241. if self._repository is None and self.parent is not None:
  242. return self.parent.get_repository()
  243. return super(BaseSection, self).repository
  244. @base.Sectionable.repository.setter
  245. def repository(self, url):
  246. base.Sectionable.repository.fset(self, url)
  247. @inherit_docstring
  248. def get_terminology_equivalent(self):
  249. repo = self.get_repository()
  250. if repo is None:
  251. return None
  252. term = terminology.load(repo)
  253. if term is None:
  254. return None
  255. return term.find_related(type=self.type)
  256. def get_merged_equivalent(self):
  257. """
  258. Return the merged object or None
  259. """
  260. return self._merged
  261. def append(self, obj):
  262. """
  263. Method adds single Sections and Properties to the respective child-lists
  264. of the current Section.
  265. :param obj: Section or Property object.
  266. """
  267. if isinstance(obj, BaseSection):
  268. self._sections.append(obj)
  269. obj._parent = self
  270. elif isinstance(obj, BaseProperty):
  271. self._props.append(obj)
  272. obj._parent = self
  273. elif isinstance(obj, collections.Iterable) and not isinstance(obj, str):
  274. raise ValueError("odml.Section.append: "
  275. "Use extend to add a list of Sections or Properties.")
  276. else:
  277. raise ValueError("odml.Section.append: "
  278. "Can only append Sections or Properties.")
  279. def extend(self, obj_list):
  280. """
  281. Method adds Sections and Properties to the respective child-lists
  282. of the current Section.
  283. :param obj_list: Iterable containing Section and Property entries.
  284. """
  285. if not isinstance(obj_list, collections.Iterable):
  286. raise TypeError("'%s' object is not iterable" % type(obj_list).__name__)
  287. # Make sure only Sections and Properties with unique names will be added.
  288. for obj in obj_list:
  289. if not isinstance(obj, BaseSection) and not isinstance(obj, BaseProperty):
  290. raise ValueError("odml.Section.extend: "
  291. "Can only extend sections and properties.")
  292. elif isinstance(obj, BaseSection) and obj.name in self.sections:
  293. raise KeyError("odml.Section.extend: "
  294. "Section with name '%s' already exists." % obj.name)
  295. elif isinstance(obj, BaseProperty) and obj.name in self.properties:
  296. raise KeyError("odml.Section.extend: "
  297. "Property with name '%s' already exists." % obj.name)
  298. for obj in obj_list:
  299. self.append(obj)
  300. def insert(self, position, obj):
  301. """
  302. Insert a Section or a Property at the respective child-list position.
  303. A ValueError will be raised, if a Section or a Property with the same
  304. name already exists in the respective child-list.
  305. :param position: index at which the object should be inserted.
  306. :param obj: Section or Property object.
  307. """
  308. if isinstance(obj, BaseSection):
  309. if obj.name in self.sections:
  310. raise ValueError("odml.Section.insert: "
  311. "Section with name '%s' already exists." % obj.name)
  312. self._sections.insert(position, obj)
  313. obj._parent = self
  314. elif isinstance(obj, BaseProperty):
  315. if obj.name in self.properties:
  316. raise ValueError("odml.Section.insert: "
  317. "Property with name '%s' already exists." % obj.name)
  318. self._props.insert(position, obj)
  319. obj._parent = self
  320. else:
  321. raise ValueError("Can only insert sections and properties")
  322. def remove(self, obj):
  323. """
  324. Remove a Section or a Property from the respective child-lists of the current
  325. Section and sets the parent attribute of the handed in object to None.
  326. Raises a ValueError if the object is not a Section or a Property or if
  327. the object is not contained in the child-lists.
  328. :param obj: Section or Property object.
  329. """
  330. if isinstance(obj, BaseSection):
  331. self._sections.remove(obj)
  332. obj._parent = None
  333. elif isinstance(obj, BaseProperty):
  334. self._props.remove(obj)
  335. obj._parent = None
  336. else:
  337. raise ValueError("Can only remove sections and properties")
  338. def clone(self, children=True, keep_id=False):
  339. """
  340. Clone this Section allowing to copy it independently
  341. to another document. By default the id of any cloned
  342. object will be set to a new uuid.
  343. :param children: If True, also clone child sections and properties
  344. recursively.
  345. :param keep_id: If this attribute is set to True, the uuids of the
  346. Section and all child objects will remain unchanged.
  347. :return: The cloned Section.
  348. """
  349. obj = super(BaseSection, self).clone(children, keep_id)
  350. if not keep_id:
  351. obj.new_id()
  352. obj._props = base.SmartList(BaseProperty)
  353. if children:
  354. for p in self._props:
  355. obj.append(p.clone(keep_id))
  356. return obj
  357. def contains(self, obj):
  358. """
  359. If the child-lists of the current Section contain a Section with
  360. the same *name* and *type* or a Property with the same *name* as
  361. the provided object, the found Section or Property is returned.
  362. :param obj: Section or Property object.
  363. """
  364. if isinstance(obj, BaseSection):
  365. return super(BaseSection, self).contains(obj)
  366. elif isinstance(obj, BaseProperty):
  367. for i in self._props:
  368. if obj.name == i.name:
  369. return i
  370. else:
  371. raise ValueError("odml.Section.contains:"
  372. "Section or Property object expected.")
  373. def merge_check(self, source_section, strict=True):
  374. """
  375. Recursively checks whether a source Section and all its children can be merged
  376. with self and all its children as destination and raises a ValueError if any of
  377. the Section attributes definition and reference differ in source and destination.
  378. :param source_section: an odML Section.
  379. :param strict: If True, definition and reference attributes of any merged Sections
  380. as well as most attributes of merged Properties on the same
  381. tree level in source and destination have to be identical.
  382. """
  383. if strict and self.definition is not None and source_section.definition is not None:
  384. self_def = ''.join(map(str.strip, self.definition.split())).lower()
  385. other_def = ''.join(map(str.strip, source_section.definition.split())).lower()
  386. if self_def != other_def:
  387. raise ValueError(
  388. "odml.Section.merge: src and dest definitions do not match!")
  389. if strict and self.reference is not None and source_section.reference is not None:
  390. self_ref = ''.join(map(str.strip, self.reference.lower().split()))
  391. other_ref = ''.join(map(str.strip, source_section.reference.lower().split()))
  392. if self_ref != other_ref:
  393. raise ValueError(
  394. "odml.Section.merge: src and dest references are in conflict!")
  395. # Check all the way down the rabbit hole / Section tree.
  396. for obj in source_section:
  397. mine = self.contains(obj)
  398. if mine is not None:
  399. mine.merge_check(obj, strict)
  400. def merge(self, section=None, strict=True):
  401. """
  402. Merges this section with another *section*.
  403. See also: :py:attr:`odml.section.BaseSection.link`
  404. If section is none, sets the link/include attribute (if _link or
  405. _include are set), causing the section to be automatically merged
  406. to the referenced section.
  407. :param section: an odML Section. If section is None, *link* or *include*
  408. will be resolved instead.
  409. :param strict: Bool value to indicate whether the attributes of affected
  410. child Properties except their ids and values have to be identical
  411. to be merged. Default is True.
  412. """
  413. if section is None:
  414. # for the high level interface
  415. if self._link is not None:
  416. self.link = self._link
  417. elif self._include is not None:
  418. self.include = self._include
  419. return
  420. # Check all the way down the tree if the destination source and
  421. # its children can be merged with self and its children since
  422. # there is no rollback in case of a downstream merge error.
  423. self.merge_check(section, strict)
  424. if self.definition is None and section.definition is not None:
  425. self.definition = section.definition
  426. if self.reference is None and section.reference is not None:
  427. self.reference = section.reference
  428. for obj in section:
  429. mine = self.contains(obj)
  430. if mine is not None:
  431. mine.merge(obj, strict)
  432. else:
  433. mine = obj.clone()
  434. mine._merged = obj
  435. self.append(mine)
  436. self._merged = section
  437. @inherit_docstring
  438. def clean(self):
  439. if self._merged is not None:
  440. self.unmerge(self._merged)
  441. super(BaseSection, self).clean()
  442. def unmerge(self, section):
  443. """
  444. Clean up a merged section by removing objects that are totally equal
  445. to the linked object
  446. """
  447. if self == section:
  448. raise RuntimeError("cannot unmerge myself?")
  449. removals = []
  450. for obj in section:
  451. mine = self.contains(obj)
  452. if mine is None:
  453. continue
  454. if mine == obj:
  455. removals.append(mine)
  456. else:
  457. mine.unmerge(obj)
  458. for obj in removals:
  459. self.remove(obj)
  460. # The path may not be valid anymore, so make sure to update it
  461. # however this does not reflect changes happening while the section
  462. # is unmerged
  463. if self._link is not None:
  464. # TODO get_absolute_path, # TODO don't change if the section can
  465. # still be reached using the old link
  466. self._link = self.get_relative_path(section)
  467. self._merged = None
  468. @property
  469. def is_merged(self):
  470. """
  471. Returns True if the section is merged with another one (e.g. through
  472. :py:attr:`odml.section.BaseSection.link` or
  473. :py:attr:`odml.section.BaseSection.include`)
  474. The merged object can be accessed through the *_merged* attribute.
  475. """
  476. return self._merged is not None
  477. @property
  478. def can_be_merged(self):
  479. """
  480. Returns True if either a *link* or an *include* attribute is specified
  481. """
  482. return self._link is not None or self._include is not None
  483. def _reorder(self, childlist, new_index):
  484. l = childlist
  485. old_index = l.index(self)
  486. # 2 cases: insert after old_index / insert before
  487. if new_index > old_index:
  488. new_index += 1
  489. l.insert(new_index, self)
  490. if new_index < old_index:
  491. del l[old_index + 1]
  492. else:
  493. del l[old_index]
  494. return old_index
  495. def reorder(self, new_index):
  496. """
  497. Move this object in its parent child-list to the position *new_index*.
  498. :return: The old index at which the object was found.
  499. """
  500. if not self.parent:
  501. raise ValueError("odml.Section.reorder: "
  502. "Section has no parent, cannot reorder in parent list.")
  503. return self._reorder(self.parent.sections, new_index)
  504. def create_property(self, name, value=None, dtype=None, oid=None):
  505. """
  506. Create a new property that is a child of this section.
  507. :param name: The name of the property.
  508. :param value: Some data value, it can be a single value or
  509. a list of homogeneous values.
  510. :param dtype: The data type of the values stored in the property,
  511. if dtype is not given, the type is deduced from the values.
  512. Check odml.DType for supported data types.
  513. :param oid: object id, UUID string as specified in RFC 4122. If no id
  514. is provided, an id will be generated and assigned.
  515. :return: The new property.
  516. """
  517. prop = BaseProperty(name=name, value=value, dtype=dtype, oid=oid)
  518. prop.parent = self
  519. return prop
  520. def pprint(self, indent=2, max_depth=1, max_length=80, current_depth=0):
  521. """
  522. Pretty print method to visualize Section-Property trees.
  523. :param indent: number of leading spaces for every child Section or Property.
  524. :param max_length: maximum number of characters printed in one line.
  525. :param current_depth: number of hierarchical levels printed from the
  526. starting Section.
  527. """
  528. spaces = " " * (current_depth * indent)
  529. sec_str = "{} {} [{}]".format(spaces, self.name, self.type)
  530. print(sec_str)
  531. for p in self.props:
  532. p.pprint(current_depth=current_depth, indent=indent,
  533. max_length=max_length)
  534. if max_depth == -1 or current_depth < max_depth:
  535. for s in self.sections:
  536. s.pprint(current_depth=current_depth+1, max_depth=max_depth,
  537. indent=indent, max_length=max_length)
  538. elif max_depth == current_depth:
  539. child_sec_indent = spaces + " " * indent
  540. more_indent = spaces + " " * (current_depth + 2 * indent)
  541. for s in self.sections:
  542. print("{} {} [{}]\n{}[...]".format(child_sec_indent,
  543. s.name, s.type,
  544. more_indent))