section.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. #-*- coding: utf-8
  2. import odml.base as base
  3. import odml.format as format
  4. import odml.terminology as terminology
  5. import odml.mapping as mapping
  6. from odml.property import Property # this is supposedly ok, as we only use it for an isinstance check
  7. # it MUST however not be used to create any Property objects
  8. from odml.tools.doc_inherit import inherit_docstring, allow_inherit_docstring
  9. class Section(base._baseobj):
  10. pass
  11. @allow_inherit_docstring
  12. class BaseSection(base.sectionable, mapping.mapableSection, Section):
  13. """An odML Section"""
  14. type = None
  15. id = None
  16. _link = None
  17. _include = None
  18. _mapping = None
  19. reference = None # the *import* property
  20. _merged = None
  21. _format = format.Section
  22. def __init__(self, name, type="undefined", parent=None, definition=None, mapping=None):
  23. self._parent = parent
  24. self._name = name
  25. self._props = base.SmartList()
  26. self._definition = definition
  27. self._mapping = mapping
  28. super(BaseSection, self).__init__()
  29. # this may fire a change event, so have the section setup then
  30. self.type = type
  31. def __repr__(self):
  32. return "<Section %s[%s] (%d)>" % (self._name, self.type, len(self._sections))
  33. @property
  34. def name(self):
  35. return self._name
  36. @name.setter
  37. def name(self, new_value):
  38. self._name = new_value
  39. @property
  40. def include(self):
  41. """
  42. the same as :py:attr:`odml.section.BaseSection.link`, except that include specifies an arbitrary url
  43. instead of a local path within the same document
  44. """
  45. return self._include
  46. @include.setter
  47. def include(self, new_value):
  48. if self._link is not None:
  49. raise TypeError("%s.include: You can either set link or include, but not both." % repr(self))
  50. if not new_value:
  51. self._include = None
  52. self.clean()
  53. return
  54. if '#' in new_value:
  55. url, path = new_value.split('#', 1)
  56. else:
  57. url, path = new_value, None
  58. terminology.deferred_load(url)
  59. if self.parent is None:
  60. self._include = new_value
  61. return
  62. term = terminology.load(url)
  63. new_section = term.get_section_by_path(path) if path is not None else term.sections[0]
  64. if self._include is not None:
  65. self.clean()
  66. self._include = new_value
  67. self.merge(new_section)
  68. @property
  69. def link(self):
  70. """
  71. specifies a softlink, i.e. a path within the document
  72. When the merge()-method is called, the link will be resolved creating
  73. according copies of the section referenced by the link attribute.
  74. When the unmerge() method is called (which happens when running clean())
  75. the link is unresolved, i.e. all properties and sections that are completely
  76. equivalent to the merged object will be removed. (They will be restored
  77. accordingly when calling merge()).
  78. When changing the *link* attribute, the previously merged section is
  79. unmerged, and the new reference will be immediately resolved. To avoid
  80. this side-effect, directly change the *_link* attribute.
  81. """
  82. return self._link
  83. @link.setter
  84. def link(self, new_value):
  85. if self._include is not None:
  86. raise TypeError("%s.link: You can either set link or include, but not both." % repr(self))
  87. if self.parent is None: # we cannot possibly know where the link is going
  88. self._link = new_value
  89. return
  90. if not new_value:
  91. self._link = None
  92. self.clean()
  93. return
  94. new_section = self.get_section_by_path(new_value) # raises exception if path cannot be found
  95. if self._link is not None:
  96. self.clean()
  97. self._link = new_value
  98. self.merge(new_section)
  99. @property
  100. def definition(self):
  101. """Name Definition of the section"""
  102. if hasattr(self, "_definition"):
  103. return self._definition
  104. else:
  105. return None
  106. @definition.setter
  107. def definition(self, val):
  108. self._definition = val
  109. @definition.deleter
  110. def definition(self):
  111. del self._definition
  112. # API (public)
  113. #
  114. # properties
  115. @property
  116. def properties(self):
  117. """the list of all properties contained in this section"""
  118. return self._props
  119. @property
  120. def sections(self):
  121. """the list of all child-sections of this section"""
  122. return self._sections
  123. @property
  124. def parent(self):
  125. """the parent section, the parent document or None"""
  126. return self._parent
  127. def get_repository(self):
  128. """
  129. returns the repository responsible for this section,
  130. which might not be the *repository* attribute, but may
  131. be inherited from a parent section / the document
  132. """
  133. if self._repository is None and self.parent is not None:
  134. return self.parent.get_repository()
  135. return super(BaseSection, self).repository
  136. @base.sectionable.repository.setter
  137. def repository(self, url):
  138. if self._active_mapping is not None:
  139. raise ValueError("cannot edit repsitory while a mapping is active")
  140. base.sectionable.repository.fset(self, url)
  141. @inherit_docstring
  142. def get_terminology_equivalent(self):
  143. repo = self.get_repository()
  144. if repo is None: return None
  145. term = terminology.load(repo)
  146. if term is None: return None
  147. return term.find_related(type=self.type)
  148. def get_merged_equivalent(self):
  149. """
  150. return the merged object or None
  151. """
  152. return self._merged
  153. @mapping.remapable_append
  154. def append(self, obj):
  155. """append a Section or Property"""
  156. if isinstance(obj, Section):
  157. self._sections.append(obj)
  158. obj._parent = self
  159. elif isinstance(obj, Property):
  160. self._props.append(obj)
  161. obj._section = self
  162. else:
  163. raise ValueError("Can only append sections and properties")
  164. @mapping.remapable_insert
  165. def insert(self, position, obj):
  166. """insert a Section or Property at the respective position"""
  167. if isinstance(obj, Section):
  168. self._sections.insert(position, obj)
  169. obj._parent = self
  170. elif isinstance(obj, Property):
  171. self._props.insert(position, obj)
  172. obj._section = self
  173. else:
  174. raise ValueError("Can only insert sections and properties")
  175. @mapping.remapable_remove
  176. def remove(self, obj):
  177. if isinstance(obj, Section): # TODO make sure this is not compare based
  178. self._sections.remove(obj)
  179. obj._parent = None
  180. elif isinstance(obj, Property):
  181. self._props.remove(obj)
  182. obj._section = None
  183. # also: TODO unmap the property
  184. else:
  185. raise ValueError("Can only remove sections and properties")
  186. def __iter__(self):
  187. """iterate over each section and property contained in this section"""
  188. for section in self._sections:
  189. yield section
  190. for prop in self._props:
  191. yield prop
  192. def __len__(self):
  193. """number of children (sections AND properties)"""
  194. return len(self._sections) + len(self._props)
  195. def clone(self, children=True):
  196. """
  197. clone this object recursively allowing to copy it independently
  198. to another document
  199. """
  200. obj = super(BaseSection, self).clone(children)
  201. obj._props = base.SmartList()
  202. if children:
  203. for p in self._props:
  204. obj.append(p.clone())
  205. return obj
  206. def contains(self, obj):
  207. """
  208. finds a property or section with the same name&type properties or None
  209. """
  210. if isinstance(obj, Section):
  211. return super(BaseSection, self).contains(obj)
  212. for i in self._props:
  213. if obj.name == i.name:
  214. return i
  215. def merge(self, section=None):
  216. """
  217. merges this section with another *section*
  218. See also: :py:attr:`odml.section.BaseSection.link`
  219. If section is none, sets the link/include attribute (if _link or
  220. _include are set), causing the section to be automatically merged
  221. to the referenced section.
  222. """
  223. if section is None:
  224. # for the high level interface
  225. if self._link is not None:
  226. self.link = self._link
  227. elif self._include is not None:
  228. self.include = self._include
  229. return
  230. for obj in section:
  231. mine = self.contains(obj)
  232. if mine is not None:
  233. mine.merge(obj)
  234. else:
  235. mine = obj.clone()
  236. mine._merged = obj
  237. self.append(mine)
  238. self._merged = section
  239. @inherit_docstring
  240. def clean(self):
  241. if self._merged is not None:
  242. self.unmerge(self._merged)
  243. super(BaseSection, self).clean()
  244. def unmerge(self, section):
  245. """
  246. clean up a merged section by removing objects that are totally equal
  247. to the linked object
  248. """
  249. if self == section:
  250. raise RuntimeException("cannot unmerge myself?")
  251. removals = []
  252. for obj in section:
  253. mine = self.contains(obj)
  254. if mine is None:
  255. continue
  256. if mine == obj:
  257. removals.append(mine)
  258. else:
  259. mine.unmerge(obj)
  260. for obj in removals:
  261. self.remove(obj)
  262. # the path may not be valid anymore, so make sure to update it
  263. # however this does not reflect changes happening while the section
  264. # is unmerged
  265. if self._link is not None:
  266. # TODO get_absolute_path, # TODO don't change if the section can still be reached using the old link
  267. self._link = self.get_relative_path(section)
  268. self._merged = None
  269. @property
  270. def is_merged(self):
  271. """
  272. returns True if the section is merged with another one (e.g. through
  273. :py:attr:`odml.section.BaseSection.link` or :py:attr:`odml.section.BaseSection.include`)
  274. The merged object can be accessed through the *_merged* attribute.
  275. """
  276. return self._merged is not None
  277. @property
  278. def can_be_merged(self):
  279. """returns True if either a *link* or an *include* attribute is specified"""
  280. return self._link is not None or self._include is not None