123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660 |
- # -*- coding: utf-8
- import uuid
- from . import base
- from . import dtypes
- from . import format as frmt
- from .tools.doc_inherit import inherit_docstring, allow_inherit_docstring
- @allow_inherit_docstring
- class BaseProperty(base.BaseObject):
- """An odML Property"""
- _format = frmt.Property
- def __init__(self, name=None, values=None, parent=None, unit=None,
- uncertainty=None, reference=None, definition=None,
- dependency=None, dependency_value=None, dtype=None,
- value_origin=None, oid=None, value=None):
- """
- Create a new Property. If a value without an explicitly stated dtype
- has been provided, the method will try to infer the value's dtype.
- Example:
- >>> p = Property("property1", "a string")
- >>> p.dtype
- >>> str
- >>> p = Property("property1", 2)
- >>> p.dtype
- >>> int
- >>> p = Property("prop", [2, 3, 4])
- >>> p.dtype
- >>> int
- :param name: The name of the property.
- :param values: Some data value, it can be a single value or
- a list of homogeneous values.
- :param unit: The unit of the stored data.
- :param uncertainty: The uncertainty (e.g. the standard deviation)
- associated with a measure value.
- :param reference: A reference (e.g. an URL) to an external definition
- of the value.
- :param definition: The definition of the property.
- :param dependency: Another property this property depends on.
- :param dependency_value: Dependency on a certain value.
- :param dtype: The data type of the values stored in the property,
- if dtype is not given, the type is deduced from the values.
- Check odml.DType for supported data types.
- :param value_origin: Reference where the value originated from e.g. a file name.
- :param oid: object id, UUID string as specified in RFC 4122. If no id is provided,
- an id will be generated and assigned. An id has to be unique
- within an odML Document.
- :param value: Legacy code to the 'values' attribute. If 'values' is provided,
- any data provided via 'value' will be ignored.
- """
- try:
- if oid is not None:
- self._id = str(uuid.UUID(oid))
- else:
- self._id = str(uuid.uuid4())
- except ValueError as e:
- print(e)
- self._id = str(uuid.uuid4())
- # Use id if no name was provided.
- if not name:
- name = self._id
- self._parent = None
- self._name = name
- self._value_origin = value_origin
- self._unit = unit
- self._uncertainty = uncertainty
- self._reference = reference
- self._definition = definition
- self._dependency = dependency
- self._dependency_value = dependency_value
- self._dtype = None
- if dtypes.valid_type(dtype):
- self._dtype = dtype
- else:
- print("Warning: Unknown dtype '%s'." % dtype)
- self._values = []
- self.values = values
- if not values and (value or isinstance(value, bool)):
- self.values = value
- self.parent = parent
- def __len__(self):
- return len(self._values)
- def __getitem__(self, key):
- return self._values[key]
- def __setitem__(self, key, item):
- if int(key) < 0 or int(key) > self.__len__():
- raise IndexError("odml.Property.__setitem__: key %i invalid for "
- "array of length %i" % (int(key), self.__len__()))
- try:
- val = dtypes.get(item, self.dtype)
- self._values[int(key)] = val
- except Exception:
- raise ValueError("odml.Property.__setitem__: passed value cannot be "
- "converted to data type \'%s\'!" % self._dtype)
- def __repr__(self):
- return "Property: {name = %s}" % self._name
- @property
- def oid(self):
- """
- The uuid for the property. Required for entity creation and comparison,
- saving and loading.
- """
- return self.id
- @property
- def id(self):
- """
- The uuid for the property.
- """
- return self._id
- def new_id(self, oid=None):
- """
- new_id sets the object id of the current object to an RFC 4122 compliant UUID.
- If an id was provided, it is assigned if it is RFC 4122 UUID format compliant.
- If no id was provided, a new UUID is generated and assigned.
- :param oid: UUID string as specified in RFC 4122.
- """
- if oid is not None:
- self._id = str(uuid.UUID(oid))
- else:
- self._id = str(uuid.uuid4())
- @property
- def name(self):
- return self._name
- @name.setter
- def name(self, new_name):
- if self.name == new_name:
- return
- curr_parent = self.parent
- if hasattr(curr_parent, "properties") and new_name in curr_parent.properties:
- raise KeyError("Object with the same name already exists!")
- self._name = new_name
- @property
- def dtype(self):
- """
- The data type of the value. Check odml.DType for supported data types.
- """
- return self._dtype
- @dtype.setter
- def dtype(self, new_type):
- """
- If the data type of a property value is changed, it is tried
- to convert existing values to the new type. If this doesn't work,
- the change is refused. The dtype can always be changed, if
- a Property does not contain values.
- """
- # check if this is a valid type
- if not dtypes.valid_type(new_type):
- raise AttributeError("'%s' is not a valid type." % new_type)
- # we convert the value if possible
- old_type = self._dtype
- old_values = self._values
- try:
- self._dtype = new_type
- self.values = old_values
- except:
- self._dtype = old_type # If conversion failed, restore old dtype
- raise ValueError("cannot convert from '%s' to '%s'" %
- (old_type, new_type))
- @property
- def parent(self):
- """
- The section containing this property.
- """
- return self._parent
- @parent.setter
- def parent(self, new_parent):
- if new_parent is None and self._parent is None:
- return
- elif new_parent is None and self._parent is not None:
- self._parent.remove(self)
- self._parent = None
- elif self._validate_parent(new_parent):
- if self._parent is not None:
- self._parent.remove(self)
- self._parent = new_parent
- self._parent.append(self)
- else:
- raise ValueError(
- "odml.Property.parent: passed value is not of consistent type!"
- "odml.Section expected")
- @staticmethod
- def _validate_parent(new_parent):
- from odml.section import BaseSection
- if isinstance(new_parent, BaseSection):
- return True
- return False
- @property
- def value(self):
- """
- Deprecated alias of 'values'. Will be removed with the next minor release.
- """
- print("The attribute 'value' is deprecated. Please use 'values' instead.")
- return self.values
- @value.setter
- def value(self, new_value):
- """
- Deprecated alias of 'values'. Will be removed with the next minor release.
- :param new_value: a single value or list of values.
- """
- print("The attribute 'value' is deprecated. Please use 'values' instead.")
- self.values = new_value
- def value_str(self, index=0):
- """
- Used to access typed data of the value at a specific
- index position as a string.
- """
- return dtypes.set(self._values[index], self._dtype)
- def _validate_values(self, values):
- """
- Method ensures that the passed value(s) can be cast to the
- same dtype, i.e. that are associated with this property or the
- inferred dtype of the first entry of the values list.
- :param values: an iterable that contains the values.
- """
- for v in values:
- try:
- dtypes.get(v, self.dtype)
- except Exception:
- return False
- return True
- def _convert_value_input(self, new_value):
- """
- This method ensures, that the passed new value is a list.
- If new_value is a string, it will convert it to a list of
- strings if the new_value contains embracing brackets.
- :return: list of new_value
- """
- if isinstance(new_value, str):
- if new_value[0] == "[" and new_value[-1] == "]":
- new_value = list(map(str.strip, new_value[1:-1].split(",")))
- else:
- new_value = [new_value]
- elif isinstance(new_value, dict):
- new_value = [str(new_value)]
- elif hasattr(new_value, '__iter__') or hasattr(new_value, '__next__'):
- new_value = list(new_value)
- elif not isinstance(new_value, list):
- new_value = [new_value]
- else:
- raise ValueError("odml.Property._convert_value_input: "
- "unsupported data type for values: %s" % type(new_value))
- return new_value
- @property
- def values(self):
- """
- Returns the value(s) stored in this property. Method always returns a list
- that is a copy (!) of the stored value. Changing this list will NOT change
- the property.
- For manipulation of the stored values use the append, extend, and direct
- access methods (using brackets).
- For example:
- >>> p = odml.Property("prop", values=[1, 2, 3])
- >>> print(p.values)
- [1, 2, 3]
- >>> p.values.append(4)
- >>> print(p.values)
- [1, 2, 3]
- Individual values can be accessed and manipulated like this:
- >>> print(p[0])
- [1]
- >>> p[0] = 4
- >>> print(p[0])
- [4]
- The values can be iterated e.g. with a loop:
- >>> for v in p.values:
- >>> print(v)
- 4
- 2
- 3
- """
- return list(self._values)
- @values.setter
- def values(self, new_value):
- """
- Set the values of the property discarding any previous information.
- Method will try to convert the passed value to the dtype of
- the property and raise a ValueError if not possible.
- :param new_value: a single value or list of values.
- """
- # Make sure boolean value 'False' gets through as well...
- if new_value is None or \
- (isinstance(new_value, (list, tuple, str)) and len(new_value) == 0):
- self._values = []
- return
- new_value = self._convert_value_input(new_value)
- if self._dtype is None:
- self._dtype = dtypes.infer_dtype(new_value[0])
- if not self._validate_values(new_value):
- raise ValueError("odml.Property.values: passed values are not of "
- "consistent type!")
- self._values = [dtypes.get(v, self.dtype) for v in new_value]
- @property
- def value_origin(self):
- return self._value_origin
- @value_origin.setter
- def value_origin(self, new_value):
- if new_value == "":
- new_value = None
- self._value_origin = new_value
- @property
- def uncertainty(self):
- return self._uncertainty
- @uncertainty.setter
- def uncertainty(self, new_value):
- if new_value == "":
- new_value = None
- if new_value and not isinstance(new_value, (int, float)):
- try:
- new_value = float(new_value)
- except ValueError:
- raise ValueError("odml.Property.uncertainty: passed uncertainty '%s' "
- "is not float or int." % new_value)
- self._uncertainty = new_value
- @property
- def unit(self):
- return self._unit
- @unit.setter
- def unit(self, new_value):
- if new_value == "":
- new_value = None
- self._unit = new_value
- @property
- def reference(self):
- return self._reference
- @reference.setter
- def reference(self, new_value):
- if new_value == "":
- new_value = None
- self._reference = new_value
- @property
- def definition(self):
- return self._definition
- @definition.setter
- def definition(self, new_value):
- if new_value == "":
- new_value = None
- self._definition = new_value
- @property
- def dependency(self):
- return self._dependency
- @dependency.setter
- def dependency(self, new_value):
- if new_value == "":
- new_value = None
- self._dependency = new_value
- @property
- def dependency_value(self):
- return self._dependency_value
- @dependency_value.setter
- def dependency_value(self, new_value):
- if new_value == "":
- new_value = None
- self._dependency_value = new_value
- def remove(self, value):
- """
- Remove a value from this property. Only the first encountered
- occurrence of the passed in value is removed from the properties
- list of values.
- """
- if value in self._values:
- self._values.remove(value)
- def get_path(self):
- """
- Return the absolute path to this object.
- """
- if not self.parent:
- return "/"
- return self.parent.get_path() + ":" + self.name
- def clone(self, keep_id=False):
- """
- Clone this property to copy it independently to another document.
- By default the id of the cloned object will be set to a different uuid.
- :param keep_id: If this attribute is set to True, the uuid of the
- object will remain unchanged.
- :return: The cloned property
- """
- obj = super(BaseProperty, self).clone()
- obj._parent = None
- obj.values = self._values
- if not keep_id:
- obj.new_id()
- return obj
- def merge_check(self, source, strict=True):
- """
- Checks whether a source Property can be merged with self as destination and
- raises a ValueError if the values of source and destination are not compatible.
- With parameter *strict=True* a ValueError is also raised, if any of the
- attributes unit, definition, uncertainty, reference or value_origin and dtype
- differ in source and destination.
- :param source: an odML Property.
- :param strict: If True, the attributes dtype, unit, uncertainty, definition,
- reference and value_origin of source and destination
- must be identical.
- """
- if not isinstance(source, BaseProperty):
- raise ValueError("odml.Property.merge: odML Property required.")
- # Catch unmerge-able values at this point to avoid
- # failing Section tree merges which cannot easily be rolled back.
- new_value = self._convert_value_input(source.values)
- if not self._validate_values(new_value):
- raise ValueError("odml.Property.merge: passed value(s) cannot "
- "be converted to data type '%s'!" % self._dtype)
- if not strict:
- return
- if (self.dtype is not None and source.dtype is not None and
- self.dtype != source.dtype):
- raise ValueError("odml.Property.merge: src and dest dtypes do not match!")
- if self.unit is not None and source.unit is not None and self.unit != source.unit:
- raise ValueError("odml.Property.merge: "
- "src and dest units (%s, %s) do not match!" %
- (source.unit, self.unit))
- if (self.uncertainty is not None and source.uncertainty is not None and
- self.uncertainty != source.uncertainty):
- raise ValueError("odml.Property.merge: "
- "src and dest uncertainty both set and do not match!")
- if self.definition is not None and source.definition is not None:
- self_def = ''.join(map(str.strip, self.definition.split())).lower()
- other_def = ''.join(map(str.strip, source.definition.split())).lower()
- if self_def != other_def:
- raise ValueError("odml.Property.merge: "
- "src and dest definitions do not match!")
- if self.reference is not None and source.reference is not None:
- self_ref = ''.join(map(str.strip, self.reference.lower().split()))
- other_ref = ''.join(map(str.strip, source.reference.lower().split()))
- if self_ref != other_ref:
- raise ValueError("odml.Property.merge: "
- "src and dest references are in conflict!")
- if self.value_origin is not None and source.value_origin is not None:
- self_ori = ''.join(map(str.strip, self.value_origin.lower().split()))
- other_ori = ''.join(map(str.strip, source.value_origin.lower().split()))
- if self_ori != other_ori:
- raise ValueError("odml.Property.merge: "
- "src and dest value_origin are in conflict!")
- def merge(self, other, strict=True):
- """
- Merges the Property 'other' into self, if possible. Information
- will be synchronized. By default the method will raise a ValueError when the
- information in this property and the passed property are in conflict.
- :param other: an odML Property.
- :param strict: Bool value to indicate whether types should be implicitly converted
- even when information may be lost. Default is True, i.e. no conversion,
- and a ValueError will be raised if types or other attributes do not match.
- If a conflict arises with strict=False, the attribute value of self will
- be kept, while the attribute value of other will be lost.
- """
- if not isinstance(other, BaseProperty):
- raise TypeError("odml.Property.merge: odml Property required.")
- self.merge_check(other, strict)
- if self.value_origin is None and other.value_origin is not None:
- self.value_origin = other.value_origin
- if self.uncertainty is None and other.uncertainty is not None:
- self.uncertainty = other.uncertainty
- if self.reference is None and other.reference is not None:
- self.reference = other.reference
- if self.definition is None and other.definition is not None:
- self.definition = other.definition
- if self.unit is None and other.unit is not None:
- self.unit = other.unit
- to_add = [v for v in other.values if v not in self._values]
- self.extend(to_add, strict=strict)
- def unmerge(self, other):
- """
- Stub that doesn't do anything for this class.
- """
- pass
- def get_merged_equivalent(self):
- """
- Return the merged object (i.e. if the parent section is linked to another one,
- return the corresponding property of the linked section) or None.
- """
- if self.parent is None or self.parent._merged is None:
- return None
- return self.parent._merged.contains(self)
- @inherit_docstring
- def get_terminology_equivalent(self):
- if self._parent is None:
- return None
- sec = self._parent.get_terminology_equivalent()
- if sec is None:
- return None
- try:
- return sec.properties[self.name]
- except KeyError:
- return None
- def extend(self, obj, strict=True):
- """
- Extend the list of values stored in this property by the passed values. Method
- will raise a ValueError, if values cannot be converted to the current dtype.
- One can also pass another Property to append all values stored in that one.
- In this case units must match!
- :param obj: single value, list of values or a Property.
- :param strict: a Bool that controls whether dtypes must match. Default is True.
- """
- if isinstance(obj, BaseProperty):
- if obj.unit != self.unit:
- raise ValueError("odml.Property.extend: src and dest units (%s, %s) "
- "do not match!" % (obj.unit, self.unit))
- self.extend(obj.values)
- return
- if self.__len__() == 0:
- self.values = obj
- return
- new_value = self._convert_value_input(obj)
- if len(new_value) > 0 and strict and dtypes.infer_dtype(new_value[0]) != self.dtype:
- raise ValueError("odml.Property.extend: "
- "passed value data type does not match dtype!")
- if not self._validate_values(new_value):
- raise ValueError("odml.Property.extend: passed value(s) cannot be converted "
- "to data type \'%s\'!" % self._dtype)
- self._values.extend([dtypes.get(v, self.dtype) for v in new_value])
- def append(self, obj, strict=True):
- """
- Append a single value to the list of stored values. Method will raise
- a ValueError if the passed value cannot be converted to the current dtype.
- :param obj: the additional value.
- :param strict: a Bool that controls whether dtypes must match. Default is True.
- """
- # Ignore empty values before nasty stuff happens, but make sure
- # 0 and False get through.
- if obj in [None, "", [], {}]:
- return
- if not self.values:
- self.values = obj
- return
- new_value = self._convert_value_input(obj)
- if len(new_value) > 1:
- raise ValueError("odml.property.append: Use extend to add a list of values!")
- if len(new_value) > 0 and strict and dtypes.infer_dtype(new_value[0]) != self.dtype:
- raise ValueError("odml.Property.append: "
- "passed value data type does not match dtype!")
- if not self._validate_values(new_value):
- raise ValueError("odml.Property.append: passed value(s) cannot be converted "
- "to data type \'%s\'!" % self._dtype)
- self._values.append(dtypes.get(new_value[0], self.dtype))
- def pprint(self, indent=2, max_length=80, current_depth=-1):
- """
- Pretty print method to visualize Properties and Section-Property trees.
- :param indent: number of leading spaces for every child Property.
- :param max_length: maximum number of characters printed in one line.
- :param current_depth: number of hierarchical levels printed from the
- starting Section.
- """
- property_spaces = ""
- prefix = ""
- if current_depth >= 0:
- property_spaces = " " * ((current_depth + 2) * indent)
- prefix = "|-"
- if self.unit is None:
- value_string = str(self.values)
- else:
- value_string = "{}{}".format(self.values, self.unit)
- p_len = len(property_spaces) + len(self.name) + len(value_string)
- if p_len >= max_length - 4:
- split_len = int((max_length - len(property_spaces)
- + len(self.name) - len(prefix))/2)
- str1 = value_string[0: split_len]
- str2 = value_string[-split_len:]
- print(("{}{} {}: {} ... {}".format(property_spaces, prefix,
- self.name, str1, str2)))
- else:
- print(("{}{} {}: {}".format(property_spaces, prefix, self.name,
- value_string)))
|