123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335 |
- #-*- coding: utf-8
- import odml.base as base
- import odml.format as format
- import odml.terminology as terminology
- import odml.mapping as mapping
- from odml.property import Property # this is supposedly ok, as we only use it for an isinstance check
- # it MUST however not be used to create any Property objects
- from odml.tools.doc_inherit import inherit_docstring, allow_inherit_docstring
- class Section(base._baseobj):
- pass
- @allow_inherit_docstring
- class BaseSection(base.sectionable, mapping.mapableSection, Section):
- """An odML Section"""
- type = None
- id = None
- _link = None
- _include = None
- _mapping = None
- reference = None # the *import* property
- _merged = None
- _format = format.Section
- def __init__(self, name, type="undefined", parent=None, definition=None, mapping=None):
- self._parent = parent
- self._name = name
- self._props = base.SmartList()
- self._definition = definition
- self._mapping = mapping
- super(BaseSection, self).__init__()
- # this may fire a change event, so have the section setup then
- self.type = type
- def __repr__(self):
- return "<Section %s[%s] (%d)>" % (self._name, self.type, len(self._sections))
- @property
- def name(self):
- return self._name
- @name.setter
- def name(self, new_value):
- self._name = new_value
- @property
- def include(self):
- """
- the same as :py:attr:`odml.section.BaseSection.link`, except that include specifies an arbitrary url
- instead of a local path within the same document
- """
- return self._include
- @include.setter
- def include(self, new_value):
- if self._link is not None:
- raise TypeError("%s.include: You can either set link or include, but not both." % repr(self))
- if not new_value:
- self._include = None
- self.clean()
- return
- if '#' in new_value:
- url, path = new_value.split('#', 1)
- else:
- url, path = new_value, None
- terminology.deferred_load(url)
- if self.parent is None:
- self._include = new_value
- return
- term = terminology.load(url)
- new_section = term.get_section_by_path(path) if path is not None else term.sections[0]
- if self._include is not None:
- self.clean()
- self._include = new_value
- self.merge(new_section)
- @property
- def link(self):
- """
- specifies a softlink, i.e. a path within the document
- When the merge()-method is called, the link will be resolved creating
- according copies of the section referenced by the link attribute.
- When the unmerge() method is called (which happens when running clean())
- the link is unresolved, i.e. all properties and sections that are completely
- equivalent to the merged object will be removed. (They will be restored
- accordingly when calling merge()).
- When changing the *link* attribute, the previously merged section is
- unmerged, and the new reference will be immediately resolved. To avoid
- this side-effect, directly change the *_link* attribute.
- """
- return self._link
- @link.setter
- def link(self, new_value):
- if self._include is not None:
- raise TypeError("%s.link: You can either set link or include, but not both." % repr(self))
- if self.parent is None: # we cannot possibly know where the link is going
- self._link = new_value
- return
- if not new_value:
- self._link = None
- self.clean()
- return
- new_section = self.get_section_by_path(new_value) # raises exception if path cannot be found
- if self._link is not None:
- self.clean()
- self._link = new_value
- self.merge(new_section)
- @property
- def definition(self):
- """Name Definition of the section"""
- if hasattr(self, "_definition"):
- return self._definition
- else:
- return None
- @definition.setter
- def definition(self, val):
- self._definition = val
- @definition.deleter
- def definition(self):
- del self._definition
- # API (public)
- #
- # properties
- @property
- def properties(self):
- """the list of all properties contained in this section"""
- return self._props
- @property
- def sections(self):
- """the list of all child-sections of this section"""
- return self._sections
- @property
- def parent(self):
- """the parent section, the parent document or None"""
- return self._parent
- def get_repository(self):
- """
- returns the repository responsible for this section,
- which might not be the *repository* attribute, but may
- be inherited from a parent section / the document
- """
- if self._repository is None and self.parent is not None:
- return self.parent.get_repository()
- return super(BaseSection, self).repository
- @base.sectionable.repository.setter
- def repository(self, url):
- if self._active_mapping is not None:
- raise ValueError("cannot edit repsitory while a mapping is active")
- base.sectionable.repository.fset(self, url)
- @inherit_docstring
- def get_terminology_equivalent(self):
- repo = self.get_repository()
- if repo is None: return None
- term = terminology.load(repo)
- if term is None: return None
- return term.find_related(type=self.type)
- def get_merged_equivalent(self):
- """
- return the merged object or None
- """
- return self._merged
- @mapping.remapable_append
- def append(self, obj):
- """append a Section or Property"""
- if isinstance(obj, Section):
- self._sections.append(obj)
- obj._parent = self
- elif isinstance(obj, Property):
- self._props.append(obj)
- obj._section = self
- else:
- raise ValueError("Can only append sections and properties")
- @mapping.remapable_insert
- def insert(self, position, obj):
- """insert a Section or Property at the respective position"""
- if isinstance(obj, Section):
- self._sections.insert(position, obj)
- obj._parent = self
- elif isinstance(obj, Property):
- self._props.insert(position, obj)
- obj._section = self
- else:
- raise ValueError("Can only insert sections and properties")
- @mapping.remapable_remove
- def remove(self, obj):
- if isinstance(obj, Section): # TODO make sure this is not compare based
- self._sections.remove(obj)
- obj._parent = None
- elif isinstance(obj, Property):
- self._props.remove(obj)
- obj._section = None
- # also: TODO unmap the property
- else:
- raise ValueError("Can only remove sections and properties")
- def __iter__(self):
- """iterate over each section and property contained in this section"""
- for section in self._sections:
- yield section
- for prop in self._props:
- yield prop
- def __len__(self):
- """number of children (sections AND properties)"""
- return len(self._sections) + len(self._props)
- def clone(self, children=True):
- """
- clone this object recursively allowing to copy it independently
- to another document
- """
- obj = super(BaseSection, self).clone(children)
- obj._props = base.SmartList()
- if children:
- for p in self._props:
- obj.append(p.clone())
- return obj
- def contains(self, obj):
- """
- finds a property or section with the same name&type properties or None
- """
- if isinstance(obj, Section):
- return super(BaseSection, self).contains(obj)
- for i in self._props:
- if obj.name == i.name:
- return i
- def merge(self, section=None):
- """
- merges this section with another *section*
- See also: :py:attr:`odml.section.BaseSection.link`
- If section is none, sets the link/include attribute (if _link or
- _include are set), causing the section to be automatically merged
- to the referenced section.
- """
- if section is None:
- # for the high level interface
- if self._link is not None:
- self.link = self._link
- elif self._include is not None:
- self.include = self._include
- return
- for obj in section:
- mine = self.contains(obj)
- if mine is not None:
- mine.merge(obj)
- else:
- mine = obj.clone()
- mine._merged = obj
- self.append(mine)
- self._merged = section
- @inherit_docstring
- def clean(self):
- if self._merged is not None:
- self.unmerge(self._merged)
- super(BaseSection, self).clean()
- def unmerge(self, section):
- """
- clean up a merged section by removing objects that are totally equal
- to the linked object
- """
- if self == section:
- raise RuntimeException("cannot unmerge myself?")
- removals = []
- for obj in section:
- mine = self.contains(obj)
- if mine is None:
- continue
- if mine == obj:
- removals.append(mine)
- else:
- mine.unmerge(obj)
- for obj in removals:
- self.remove(obj)
- # the path may not be valid anymore, so make sure to update it
- # however this does not reflect changes happening while the section
- # is unmerged
- if self._link is not None:
- # TODO get_absolute_path, # TODO don't change if the section can still be reached using the old link
- self._link = self.get_relative_path(section)
- self._merged = None
- @property
- def is_merged(self):
- """
- returns True if the section is merged with another one (e.g. through
- :py:attr:`odml.section.BaseSection.link` or :py:attr:`odml.section.BaseSection.include`)
- The merged object can be accessed through the *_merged* attribute.
- """
- return self._merged is not None
- @property
- def can_be_merged(self):
- """returns True if either a *link* or an *include* attribute is specified"""
- return self._link is not None or self._include is not None
|