section.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876
  1. # -*- coding: utf-8
  2. """
  3. This module provides the Base Section class.
  4. """
  5. import uuid
  6. import warnings
  7. try:
  8. from collections.abc import Iterable
  9. except ImportError:
  10. from collections import Iterable
  11. from . import base
  12. from . import format as fmt
  13. from . import terminology
  14. from . import validation
  15. from .doc import BaseDocument
  16. # this is supposedly ok, as we only use it for an isinstance check
  17. from .property import BaseProperty
  18. # it MUST however not be used to create any Property objects
  19. from .tools.doc_inherit import inherit_docstring, allow_inherit_docstring
  20. from .util import format_cardinality
  21. @allow_inherit_docstring
  22. class BaseSection(base.Sectionable):
  23. """
  24. An odML Section.
  25. :param name: string providing the name of the Section. If the name is not
  26. provided, the object id of the Section is assigned as its name.
  27. Section name is a required attribute.
  28. :param type: String providing a grouping description for similar Sections.
  29. Section type is a required attribute and will be set to the string
  30. 'n.s.' by default.
  31. :param parent: the parent object of the new Section. If the object is not
  32. an odml.Section or an odml.Document, a ValueError is raised.
  33. :param definition: String defining this Section.
  34. :param reference: A reference (e.g. an URL) to an external definition
  35. of the Section.
  36. :param repository: URL to a repository in which the Section is defined.
  37. :param link: Specifies a soft link, i.e. a path within the document.
  38. :param include: Specifies an arbitrary URL. Can only be used if *link* is not set.
  39. :param oid: object id, UUID string as specified in RFC 4122. If no id is provided,
  40. an id will be generated and assigned. An id has to be unique
  41. within an odML Document.
  42. :param sec_cardinality: Section cardinality defines how many Sub-Sections are allowed for this
  43. Section. By default unlimited Sections can be set.
  44. A required number of Sections can be set by assigning a tuple of the
  45. format "(min, max)"
  46. :param prop_cardinality: Property cardinality defines how many Properties are allowed for this
  47. Section. By default unlimited Properties can be set.
  48. A required number of Properties can be set by assigning a tuple of the
  49. format "(min, max)".
  50. """
  51. type = None
  52. _link = None
  53. _include = None
  54. _merged = None
  55. _format = fmt.Section
  56. def __init__(self, name=None, type="n.s.", parent=None,
  57. definition=None, reference=None,
  58. repository=None, link=None, include=None, oid=None,
  59. sec_cardinality=None, prop_cardinality=None):
  60. # Sets _sections Smartlist and _repository to None, so run first.
  61. super(BaseSection, self).__init__()
  62. self._props = base.SmartList(BaseProperty)
  63. try:
  64. if oid is not None:
  65. self._id = str(uuid.UUID(oid))
  66. else:
  67. self._id = str(uuid.uuid4())
  68. except ValueError as exc:
  69. print(exc)
  70. self._id = str(uuid.uuid4())
  71. # Use id if no name was provided.
  72. if not name:
  73. name = self._id
  74. self._parent = None
  75. self._name = name
  76. self._definition = definition
  77. self._reference = reference
  78. self._repository = repository
  79. self._link = link
  80. self._include = include
  81. self._sec_cardinality = None
  82. self._prop_cardinality = None
  83. # this may fire a change event, so have the section setup then
  84. self.type = type
  85. self.parent = parent
  86. # This might lead to a validation warning, since properties are set
  87. # at a later point in time.
  88. self.sec_cardinality = sec_cardinality
  89. self.prop_cardinality = prop_cardinality
  90. for err in validation.Validation(self).errors:
  91. if err.is_error:
  92. use_name = err.obj.name if err.obj.id != err.obj.name else None
  93. sec_formatted = "Section[id=%s|%s/%s]" % (err.obj.id, use_name, err.obj.type)
  94. msg = "%s\n Validation[%s]: %s" % (sec_formatted, err.rank, err.msg)
  95. print(msg)
  96. def __repr__(self):
  97. return "Section[%d|%d] {name = %s, type = %s, id = %s}" % (len(self._sections),
  98. len(self._props),
  99. self._name,
  100. self.type,
  101. self.id)
  102. def __iter__(self):
  103. """
  104. Iterate over each Section and Property contained in this Section.
  105. """
  106. for section in self._sections:
  107. yield section
  108. for prop in self._props:
  109. yield prop
  110. def __len__(self):
  111. """
  112. Number of children (Sections AND Properties).
  113. """
  114. return len(self._sections) + len(self._props)
  115. @property
  116. def oid(self):
  117. """
  118. The uuid for the Section. Required for entity creation and comparison,
  119. saving and loading.
  120. """
  121. return self.id
  122. @property
  123. def id(self):
  124. """
  125. The uuid for the section.
  126. """
  127. return self._id
  128. def new_id(self, oid=None):
  129. """
  130. new_id sets the id of the current object to a RFC 4122 compliant UUID.
  131. If an id was provided, it is assigned if it is RFC 4122 UUID format compliant.
  132. If no id was provided, a new UUID is generated and assigned.
  133. :param oid: UUID string as specified in RFC 4122.
  134. """
  135. if oid is not None:
  136. self._id = str(uuid.UUID(oid))
  137. else:
  138. self._id = str(uuid.uuid4())
  139. @property
  140. def name(self):
  141. """
  142. The name of the Section.
  143. """
  144. return self._name
  145. @name.setter
  146. def name(self, new_value):
  147. if self.name == new_value:
  148. return
  149. # Make sure name cannot be set to None or empty
  150. if not new_value:
  151. self._name = self._id
  152. return
  153. curr_parent = self.parent
  154. if hasattr(curr_parent, "sections") and new_value in curr_parent.sections:
  155. raise KeyError("Object with the same name already exists!")
  156. self._name = new_value
  157. @property
  158. def include(self):
  159. """
  160. The same as :py:attr:`odml.section.BaseSection.link`, except that
  161. include specifies an arbitrary url instead of a local path within
  162. the same document.
  163. """
  164. return self._include
  165. @include.setter
  166. def include(self, new_value):
  167. if self._link is not None:
  168. raise TypeError("%s.include: You can either set link or include, "
  169. "but not both." % repr(self))
  170. if not new_value:
  171. self._include = None
  172. self.clean()
  173. return
  174. if '#' in new_value:
  175. url, path = new_value.split('#', 1)
  176. else:
  177. url, path = new_value, None
  178. terminology.deferred_load(url)
  179. if self.parent is None:
  180. self._include = new_value
  181. return
  182. term = terminology.load(url)
  183. new_section = term.get_section_by_path(
  184. path) if path is not None else term.sections[0]
  185. if self._include is not None:
  186. self.clean()
  187. self._include = new_value
  188. # strict needs to be False, otherwise finalizing a document will
  189. # basically always fail.
  190. self.merge(new_section, strict=False)
  191. @property
  192. def link(self):
  193. """
  194. A softlink, i.e. a path within the document.
  195. When the merge()-method is called, the link will be resolved creating
  196. according copies of the section referenced by the link attribute.
  197. When the unmerge() method is called (happens when running clean())
  198. the link is unresolved, i.e. all properties and sections that are
  199. completely equivalent to the merged object will be removed.
  200. (They will be restored accordingly when calling merge()).
  201. When changing the *link* attribute, the previously merged section is
  202. unmerged, and the new reference will be immediately resolved. To avoid
  203. this side-effect, directly change the *_link* attribute.
  204. """
  205. return self._link
  206. @link.setter
  207. def link(self, new_value):
  208. if self._include is not None:
  209. raise TypeError("%s.link: You can either set link or include,"
  210. " but not both." % repr(self))
  211. if self.parent is None: # we cannot possibly know where the link goes
  212. self._link = new_value
  213. return
  214. if not new_value:
  215. self._link = None
  216. self.clean()
  217. return
  218. # raises exception if path cannot be found
  219. new_section = self.get_section_by_path(new_value)
  220. if self._link is not None:
  221. self.clean()
  222. self._link = new_value
  223. # strict needs to be False, otherwise finalizing a document will
  224. # basically always fail.
  225. self.merge(new_section, strict=False)
  226. @property
  227. def definition(self):
  228. """
  229. The definition of the Section.
  230. """
  231. return self._definition
  232. @definition.setter
  233. def definition(self, new_value):
  234. if new_value == "":
  235. new_value = None
  236. self._definition = new_value
  237. @definition.deleter
  238. def definition(self):
  239. del self._definition
  240. @property
  241. def reference(self):
  242. """
  243. A reference (e.g. an URL) to an external definition of the Section.
  244. :returns: The reference of the Section.
  245. """
  246. return self._reference
  247. @reference.setter
  248. def reference(self, new_value):
  249. if new_value == "":
  250. new_value = None
  251. self._reference = new_value
  252. # API (public)
  253. #
  254. # properties
  255. @property
  256. def properties(self):
  257. """
  258. The list of all properties contained in this Section,
  259. """
  260. return self._props
  261. @property
  262. def props(self):
  263. """
  264. The list of all properties contained in this Section;
  265. NIXpy format style alias for 'properties'.
  266. """
  267. return self._props
  268. @property
  269. def sections(self):
  270. """
  271. The list of all child-sections of this Section.
  272. """
  273. return self._sections
  274. @property
  275. def parent(self):
  276. """
  277. The parent Section, Document or None.
  278. """
  279. return self._parent
  280. @parent.setter
  281. def parent(self, new_parent):
  282. if new_parent is None and self._parent is None:
  283. return
  284. if new_parent is None and self._parent is not None:
  285. self._parent.remove(self)
  286. self._parent = None
  287. elif self._validate_parent(new_parent):
  288. if self._parent is not None:
  289. self._parent.remove(self)
  290. self._parent = new_parent
  291. self._parent.append(self)
  292. else:
  293. raise ValueError(
  294. "odml.Section.parent: passed value is not of consistent type!"
  295. "\nodml.Document or odml.Section expected")
  296. @staticmethod
  297. def _validate_parent(new_parent):
  298. """
  299. Checks whether a provided object is a valid odml.Section or odml.Document..
  300. :param new_parent: object to check whether it is an odml.Section or odml.Document.
  301. :returns: Boolean whether the object is an odml.Section, odml.Document or not.
  302. """
  303. if isinstance(new_parent, (BaseDocument, BaseSection)):
  304. return True
  305. return False
  306. def get_repository(self):
  307. """
  308. Returns the repository responsible for this Section,
  309. which might not be the *repository* attribute, but may
  310. be inherited from a parent Section / the Document.
  311. """
  312. if self._repository is None and self.parent is not None:
  313. return self.parent.get_repository()
  314. return super(BaseSection, self).repository
  315. @base.Sectionable.repository.setter
  316. def repository(self, url):
  317. base.Sectionable.repository.fset(self, url)
  318. @property
  319. def sec_cardinality(self):
  320. """
  321. The Section cardinality of a Section. It defines how many Sections
  322. are minimally required and how many Sections should be maximally
  323. stored. Use the 'set_sections_cardinality' method to set.
  324. """
  325. return self._sec_cardinality
  326. @sec_cardinality.setter
  327. def sec_cardinality(self, new_value):
  328. """
  329. Sets the Sections cardinality of a Section.
  330. The following cardinality cases are supported:
  331. (n, n) - default, no restriction
  332. (d, n) - minimally d entries, no maximum
  333. (n, d) - maximally d entries, no minimum
  334. (d, d) - minimally d entries, maximally d entries
  335. Only positive integers are supported. 'None' is used to denote
  336. no restrictions on a maximum or minimum.
  337. :param new_value: Can be either 'None', a positive integer, which will set
  338. the maximum or an integer 2-tuple of the format '(min, max)'.
  339. """
  340. self._sec_cardinality = format_cardinality(new_value)
  341. # Validate and inform user if the current cardinality is violated
  342. self._sections_cardinality_validation()
  343. def set_sections_cardinality(self, min_val=None, max_val=None):
  344. """
  345. Sets the Sections cardinality of a Section.
  346. :param min_val: Required minimal number of values elements. None denotes
  347. no restrictions on sections elements minimum. Default is None.
  348. :param max_val: Allowed maximal number of values elements. None denotes
  349. no restrictions on sections elements maximum. Default is None.
  350. """
  351. self.sec_cardinality = (min_val, max_val)
  352. def _sections_cardinality_validation(self):
  353. """
  354. Runs a validation to check whether the sections cardinality
  355. is respected and prints a warning message otherwise.
  356. """
  357. # This check is run quite frequently so do not run all checks via the default validation
  358. # but use a custom validation instead.
  359. valid = validation.Validation(self, validate=False, reset=True)
  360. valid.register_custom_handler("section", validation.section_sections_cardinality)
  361. valid.run_validation()
  362. val_id = validation.IssueID.section_sections_cardinality
  363. # Make sure to display only warnings of the current section
  364. for curr in valid.errors:
  365. if curr.validation_id == val_id and self.id == curr.obj.id:
  366. print("%s: %s" % (curr.rank.capitalize(), curr.msg))
  367. @property
  368. def prop_cardinality(self):
  369. """
  370. The Property cardinality of a Section. It defines how many Properties
  371. are minimally required and how many Properties should be maximally
  372. stored. Use the 'set_properties_cardinality' method to set.
  373. """
  374. return self._prop_cardinality
  375. @prop_cardinality.setter
  376. def prop_cardinality(self, new_value):
  377. """
  378. Sets the Properties cardinality of a Section.
  379. The following cardinality cases are supported:
  380. (n, n) - default, no restriction
  381. (d, n) - minimally d entries, no maximum
  382. (n, d) - maximally d entries, no minimum
  383. (d, d) - minimally d entries, maximally d entries
  384. Only positive integers are supported. 'None' is used to denote
  385. no restrictions on a maximum or minimum.
  386. :param new_value: Can be either 'None', a positive integer, which will set
  387. the maximum or an integer 2-tuple of the format '(min, max)'.
  388. """
  389. self._prop_cardinality = format_cardinality(new_value)
  390. # Validate and inform user if the current cardinality is violated
  391. self._properties_cardinality_validation()
  392. def set_properties_cardinality(self, min_val=None, max_val=None):
  393. """
  394. Sets the Properties cardinality of a Section.
  395. :param min_val: Required minimal number of values elements. None denotes
  396. no restrictions on properties elements minimum. Default is None.
  397. :param max_val: Allowed maximal number of values elements. None denotes
  398. no restrictions on properties elements maximum. Default is None.
  399. """
  400. self.prop_cardinality = (min_val, max_val)
  401. def _properties_cardinality_validation(self):
  402. """
  403. Runs a validation to check whether the properties cardinality
  404. is respected and prints a warning message otherwise.
  405. """
  406. # This check is run quite frequently so do not run all checks via the default validation
  407. # but use a custom validation instead.
  408. valid = validation.Validation(self, validate=False, reset=True)
  409. valid.register_custom_handler("section", validation.section_properties_cardinality)
  410. valid.run_validation()
  411. val_id = validation.IssueID.section_properties_cardinality
  412. # Make sure to display only warnings of the current section
  413. for curr in valid.errors:
  414. if curr.validation_id == val_id and self.id == curr.obj.id:
  415. print("%s: %s" % (curr.rank.capitalize(), curr.msg))
  416. @inherit_docstring
  417. def get_terminology_equivalent(self):
  418. repo = self.get_repository()
  419. if repo is None:
  420. return None
  421. term = terminology.load(repo)
  422. if term is None:
  423. return None
  424. return term.find_related(type=self.type)
  425. def get_merged_equivalent(self):
  426. """
  427. Returns the merged object or None.
  428. """
  429. return self._merged
  430. def append(self, obj):
  431. """
  432. Method adds single Sections and Properties to the respective child-lists
  433. of the current Section.
  434. :param obj: Section or Property object.
  435. """
  436. if isinstance(obj, BaseSection):
  437. self._sections.append(obj)
  438. obj._parent = self
  439. elif isinstance(obj, BaseProperty):
  440. self._props.append(obj)
  441. obj._parent = self
  442. elif isinstance(obj, Iterable) and not isinstance(obj, str):
  443. raise ValueError("odml.Section.append: "
  444. "Use extend to add a list of Sections or Properties.")
  445. else:
  446. raise ValueError("odml.Section.append: "
  447. "Can only append Sections or Properties.")
  448. def extend(self, obj_list):
  449. """
  450. Method adds Sections and Properties to the respective child-lists
  451. of the current Section.
  452. :param obj_list: Iterable containing Section and Property entries.
  453. """
  454. if not isinstance(obj_list, Iterable):
  455. raise TypeError("'%s' object is not iterable" % type(obj_list).__name__)
  456. # Make sure only Sections and Properties with unique names will be added.
  457. for obj in obj_list:
  458. if not isinstance(obj, BaseSection) and not isinstance(obj, BaseProperty):
  459. msg = "odml.Section.extend: Can only extend sections and properties."
  460. raise ValueError(msg)
  461. if isinstance(obj, BaseSection) and obj.name in self.sections:
  462. msg = "odml.Section.extend: Section with name '%s' already exists." % obj.name
  463. raise KeyError(msg)
  464. if isinstance(obj, BaseProperty) and obj.name in self.properties:
  465. msg = "odml.Section.extend: Property with name '%s' already exists." % obj.name
  466. raise KeyError(msg)
  467. for obj in obj_list:
  468. self.append(obj)
  469. def insert(self, position, obj):
  470. """
  471. Insert a Section or a Property at the respective child-list position.
  472. A ValueError will be raised, if a Section or a Property with the same
  473. name already exists in the respective child-list.
  474. :param position: index at which the object should be inserted.
  475. :param obj: Section or Property object.
  476. """
  477. if isinstance(obj, BaseSection):
  478. if obj.name in self.sections:
  479. raise ValueError("odml.Section.insert: "
  480. "Section with name '%s' already exists." % obj.name)
  481. self._sections.insert(position, obj)
  482. obj._parent = self
  483. elif isinstance(obj, BaseProperty):
  484. if obj.name in self.properties:
  485. raise ValueError("odml.Section.insert: "
  486. "Property with name '%s' already exists." % obj.name)
  487. self._props.insert(position, obj)
  488. obj._parent = self
  489. else:
  490. raise ValueError("Can only insert sections and properties")
  491. def remove(self, obj):
  492. """
  493. Remove a Section or a Property from the respective child-lists of the current
  494. Section and sets the parent attribute of the handed in object to None.
  495. Raises a ValueError if the object is not a Section or a Property or if
  496. the object is not contained in the child-lists.
  497. :param obj: Section or Property object.
  498. """
  499. if isinstance(obj, BaseSection):
  500. self._sections.remove(obj)
  501. obj._parent = None
  502. elif isinstance(obj, BaseProperty):
  503. self._props.remove(obj)
  504. obj._parent = None
  505. else:
  506. raise ValueError("Can only remove sections and properties")
  507. def clone(self, children=True, keep_id=False):
  508. """
  509. Clone this Section allowing to copy it independently
  510. to another document. By default the id of any cloned
  511. object will be set to a new uuid.
  512. :param children: If True, also clone child sections and properties
  513. recursively.
  514. :param keep_id: If this attribute is set to True, the uuids of the
  515. Section and all child objects will remain unchanged.
  516. :return: The cloned Section.
  517. """
  518. obj = super(BaseSection, self).clone(children, keep_id)
  519. if not keep_id:
  520. obj.new_id()
  521. obj._props = base.SmartList(BaseProperty)
  522. if children:
  523. for prop in self._props:
  524. obj.append(prop.clone(keep_id))
  525. return obj
  526. def contains(self, obj):
  527. """
  528. If the child-lists of the current Section contain a Section with
  529. the same *name* and *type* or a Property with the same *name* as
  530. the provided object, the found Section or Property is returned.
  531. :param obj: Section or Property object.
  532. """
  533. if isinstance(obj, BaseSection):
  534. return super(BaseSection, self).contains(obj)
  535. if isinstance(obj, BaseProperty):
  536. for i in self._props:
  537. if obj.name == i.name:
  538. return i
  539. return None
  540. raise ValueError("odml.Section.contains: Section or Property object expected.")
  541. def merge_check(self, source_section, strict=True):
  542. """
  543. Recursively checks whether a source Section and all its children can be merged
  544. with self and all its children as destination and raises a ValueError if any of
  545. the Section attributes definition and reference differ in source and destination.
  546. :param source_section: an odML Section.
  547. :param strict: If True, definition and reference attributes of any merged Sections
  548. as well as most attributes of merged Properties on the same
  549. tree level in source and destination have to be identical.
  550. """
  551. if strict and self.definition is not None and source_section.definition is not None:
  552. self_def = ''.join(map(str.strip, self.definition.split())).lower()
  553. other_def = ''.join(map(str.strip, source_section.definition.split())).lower()
  554. if self_def != other_def:
  555. raise ValueError(
  556. "odml.Section.merge: src and dest definitions do not match!")
  557. if strict and self.reference is not None and source_section.reference is not None:
  558. self_ref = ''.join(map(str.strip, self.reference.lower().split()))
  559. other_ref = ''.join(map(str.strip, source_section.reference.lower().split()))
  560. if self_ref != other_ref:
  561. raise ValueError(
  562. "odml.Section.merge: src and dest references are in conflict!")
  563. # Check all the way down the rabbit hole / Section tree.
  564. for obj in source_section:
  565. mine = self.contains(obj)
  566. if mine is not None:
  567. mine.merge_check(obj, strict)
  568. def merge(self, section=None, strict=True):
  569. """
  570. Merges this section with another *section*.
  571. See also: :py:attr:`odml.section.BaseSection.link`
  572. If section is none, sets the link/include attribute (if _link or
  573. _include are set), causing the section to be automatically merged
  574. to the referenced section.
  575. :param section: an odML Section. If section is None, *link* or *include*
  576. will be resolved instead.
  577. :param strict: Bool value to indicate whether the attributes of affected
  578. child Properties except their ids and values have to be identical
  579. to be merged. Default is True.
  580. """
  581. if section is None:
  582. # for the high level interface
  583. if self._link is not None:
  584. self.link = self._link
  585. elif self._include is not None:
  586. self.include = self._include
  587. return
  588. # Check all the way down the tree if the destination source and
  589. # its children can be merged with self and its children since
  590. # there is no rollback in case of a downstream merge error.
  591. self.merge_check(section, strict)
  592. if self.definition is None and section.definition is not None:
  593. self.definition = section.definition
  594. if self.reference is None and section.reference is not None:
  595. self.reference = section.reference
  596. for obj in section:
  597. mine = self.contains(obj)
  598. if mine is not None:
  599. mine.merge(obj, strict)
  600. else:
  601. mine = obj.clone()
  602. mine._merged = obj
  603. self.append(mine)
  604. self._merged = section
  605. @inherit_docstring
  606. def clean(self):
  607. if self._merged is not None:
  608. self.unmerge(self._merged)
  609. super(BaseSection, self).clean()
  610. def unmerge(self, section):
  611. """
  612. Clean up a merged section by removing objects that are totally equal
  613. to the linked object
  614. """
  615. if self == section:
  616. raise RuntimeError("cannot unmerge myself?")
  617. removals = []
  618. for obj in section:
  619. mine = self.contains(obj)
  620. if mine is None:
  621. continue
  622. if mine == obj:
  623. removals.append(mine)
  624. else:
  625. mine.unmerge(obj)
  626. for obj in removals:
  627. self.remove(obj)
  628. # The path may not be valid anymore, so make sure to update it.
  629. # However this does not reflect changes happening while the section
  630. # is unmerged.
  631. if self._link is not None:
  632. # TODO get_absolute_path
  633. # TODO don't change if the section can still be reached using the old link
  634. self._link = self.get_relative_path(section)
  635. self._merged = None
  636. @property
  637. def is_merged(self):
  638. """
  639. Returns True if the section is merged with another one (e.g. through
  640. :py:attr:`odml.section.BaseSection.link` or
  641. :py:attr:`odml.section.BaseSection.include`)
  642. The merged object can be accessed through the *_merged* attribute.
  643. """
  644. return self._merged is not None
  645. @property
  646. def can_be_merged(self):
  647. """
  648. Returns True if either a *link* or an *include* attribute is specified
  649. """
  650. return self._link is not None or self._include is not None
  651. def _reorder(self, childlist, new_index):
  652. lst = childlist
  653. old_index = lst.index(self)
  654. # 2 cases: insert after old_index / insert before
  655. if new_index > old_index:
  656. new_index += 1
  657. lst.insert(new_index, self)
  658. if new_index < old_index:
  659. del lst[old_index + 1]
  660. else:
  661. del lst[old_index]
  662. return old_index
  663. def reorder(self, new_index):
  664. """
  665. Move this object in its parent child-list to the position *new_index*.
  666. :return: The old index at which the object was found.
  667. """
  668. if not self.parent:
  669. raise ValueError("odml.Section.reorder: "
  670. "Section has no parent, cannot reorder in parent list.")
  671. return self._reorder(self.parent.sections, new_index)
  672. def create_property(self, name, values=None, dtype=None, oid=None, value=None):
  673. """
  674. Create a new property that is a child of this section.
  675. :param name: The name of the property.
  676. :param values: Some data value, it can be a single value or
  677. a list of homogeneous values.
  678. :param dtype: The data type of the values stored in the property,
  679. if dtype is not given, the type is deduced from the values.
  680. Check odml.DType for supported data types.
  681. :param oid: object id, UUID string as specified in RFC 4122. If no id
  682. is provided, an id will be generated and assigned.
  683. :param value: Deprecated alias of 'values'. Any content of 'value' is ignored,
  684. if 'values' is set.
  685. :return: The new property.
  686. """
  687. if value and values:
  688. print("Warning: Both 'values' and 'value' were set; ignoring 'value'.")
  689. if not values and (value or isinstance(value, (bool, int))):
  690. msg = "The attribute 'value' is deprecated and will be removed, use 'values' instead."
  691. warnings.warn(msg, category=DeprecationWarning, stacklevel=2)
  692. values = value
  693. prop = BaseProperty(name=name, values=values, dtype=dtype, oid=oid)
  694. prop.parent = self
  695. return prop
  696. def pprint(self, indent=2, max_depth=1, max_length=80, current_depth=0):
  697. """
  698. Pretty prints Section-Property trees for nicer visualization.
  699. :param indent: number of leading spaces for every child Section or Property.
  700. :param max_depth: number of maximum child section layers to traverse and print.
  701. :param max_length: maximum number of characters printed in one line.
  702. :param current_depth: number of hierarchical levels printed from the
  703. starting Section.
  704. """
  705. spaces = " " * (current_depth * indent)
  706. sec_str = "{} {} [{}]".format(spaces, self.name, self.type)
  707. print(sec_str)
  708. for prop in self.props:
  709. prop.pprint(current_depth=current_depth, indent=indent,
  710. max_length=max_length)
  711. if max_depth == -1 or current_depth < max_depth:
  712. for sec in self.sections:
  713. sec.pprint(current_depth=current_depth+1, max_depth=max_depth,
  714. indent=indent, max_length=max_length)
  715. elif max_depth == current_depth:
  716. child_sec_indent = spaces + " " * indent
  717. more_indent = spaces + " " * (current_depth + 2 * indent)
  718. for sec in self.sections:
  719. print("{} {} [{}]\n{}[...]".format(child_sec_indent, sec.name,
  720. sec.type, more_indent))
  721. def export_leaf(self):
  722. """
  723. Exports only the path from this section to the root.
  724. Include all properties for all sections, but no other subsections.
  725. :returns: cloned odml tree to the root of the current document.
  726. """
  727. curr = self
  728. par = self
  729. child = self
  730. while curr is not None:
  731. par = curr.clone(children=False, keep_id=True)
  732. if curr != self:
  733. par.append(child)
  734. if hasattr(curr, 'properties'):
  735. for prop in curr.properties:
  736. par.append(prop.clone(keep_id=True))
  737. child = par
  738. curr = curr.parent
  739. return par