property.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  1. # -*- coding: utf-8
  2. import uuid
  3. from . import base
  4. from . import dtypes
  5. from . import format as frmt
  6. from .tools.doc_inherit import inherit_docstring, allow_inherit_docstring
  7. @allow_inherit_docstring
  8. class BaseProperty(base.BaseObject):
  9. """An odML Property"""
  10. _format = frmt.Property
  11. def __init__(self, name=None, values=None, parent=None, unit=None,
  12. uncertainty=None, reference=None, definition=None,
  13. dependency=None, dependency_value=None, dtype=None,
  14. value_origin=None, oid=None, value=None):
  15. """
  16. Create a new Property. If a value without an explicitly stated dtype
  17. has been provided, the method will try to infer the value's dtype.
  18. Example:
  19. >>> p = Property("property1", "a string")
  20. >>> p.dtype
  21. >>> str
  22. >>> p = Property("property1", 2)
  23. >>> p.dtype
  24. >>> int
  25. >>> p = Property("prop", [2, 3, 4])
  26. >>> p.dtype
  27. >>> int
  28. :param name: The name of the property.
  29. :param values: Some data value, it can be a single value or
  30. a list of homogeneous values.
  31. :param unit: The unit of the stored data.
  32. :param uncertainty: The uncertainty (e.g. the standard deviation)
  33. associated with a measure value.
  34. :param reference: A reference (e.g. an URL) to an external definition
  35. of the value.
  36. :param definition: The definition of the property.
  37. :param dependency: Another property this property depends on.
  38. :param dependency_value: Dependency on a certain value.
  39. :param dtype: The data type of the values stored in the property,
  40. if dtype is not given, the type is deduced from the values.
  41. Check odml.DType for supported data types.
  42. :param value_origin: Reference where the value originated from e.g. a file name.
  43. :param oid: object id, UUID string as specified in RFC 4122. If no id is provided,
  44. an id will be generated and assigned. An id has to be unique
  45. within an odML Document.
  46. :param value: Legacy code to the 'values' attribute. If 'values' is provided,
  47. any data provided via 'value' will be ignored.
  48. """
  49. try:
  50. if oid is not None:
  51. self._id = str(uuid.UUID(oid))
  52. else:
  53. self._id = str(uuid.uuid4())
  54. except ValueError as e:
  55. print(e)
  56. self._id = str(uuid.uuid4())
  57. # Use id if no name was provided.
  58. if not name:
  59. name = self._id
  60. self._parent = None
  61. self._name = name
  62. self._value_origin = value_origin
  63. self._unit = unit
  64. self._uncertainty = uncertainty
  65. self._reference = reference
  66. self._definition = definition
  67. self._dependency = dependency
  68. self._dependency_value = dependency_value
  69. self._dtype = None
  70. if dtypes.valid_type(dtype):
  71. self._dtype = dtype
  72. else:
  73. print("Warning: Unknown dtype '%s'." % dtype)
  74. self._values = []
  75. self.values = values
  76. if not values and (value or isinstance(value, bool)):
  77. self.values = value
  78. self.parent = parent
  79. def __len__(self):
  80. return len(self._values)
  81. def __getitem__(self, key):
  82. return self._values[key]
  83. def __setitem__(self, key, item):
  84. if int(key) < 0 or int(key) > self.__len__():
  85. raise IndexError("odml.Property.__setitem__: key %i invalid for "
  86. "array of length %i" % (int(key), self.__len__()))
  87. try:
  88. val = dtypes.get(item, self.dtype)
  89. self._values[int(key)] = val
  90. except Exception:
  91. raise ValueError("odml.Property.__setitem__: passed value cannot be "
  92. "converted to data type \'%s\'!" % self._dtype)
  93. def __repr__(self):
  94. return "Property: {name = %s}" % self._name
  95. @property
  96. def oid(self):
  97. """
  98. The uuid for the property. Required for entity creation and comparison,
  99. saving and loading.
  100. """
  101. return self.id
  102. @property
  103. def id(self):
  104. """
  105. The uuid for the property.
  106. """
  107. return self._id
  108. def new_id(self, oid=None):
  109. """
  110. new_id sets the object id of the current object to an RFC 4122 compliant UUID.
  111. If an id was provided, it is assigned if it is RFC 4122 UUID format compliant.
  112. If no id was provided, a new UUID is generated and assigned.
  113. :param oid: UUID string as specified in RFC 4122.
  114. """
  115. if oid is not None:
  116. self._id = str(uuid.UUID(oid))
  117. else:
  118. self._id = str(uuid.uuid4())
  119. @property
  120. def name(self):
  121. return self._name
  122. @name.setter
  123. def name(self, new_name):
  124. if self.name == new_name:
  125. return
  126. curr_parent = self.parent
  127. if hasattr(curr_parent, "properties") and new_name in curr_parent.properties:
  128. raise KeyError("Object with the same name already exists!")
  129. self._name = new_name
  130. @property
  131. def dtype(self):
  132. """
  133. The data type of the value. Check odml.DType for supported data types.
  134. """
  135. return self._dtype
  136. @dtype.setter
  137. def dtype(self, new_type):
  138. """
  139. If the data type of a property value is changed, it is tried
  140. to convert existing values to the new type. If this doesn't work,
  141. the change is refused. The dtype can always be changed, if
  142. a Property does not contain values.
  143. """
  144. # check if this is a valid type
  145. if not dtypes.valid_type(new_type):
  146. raise AttributeError("'%s' is not a valid type." % new_type)
  147. # we convert the value if possible
  148. old_type = self._dtype
  149. old_values = self._values
  150. try:
  151. self._dtype = new_type
  152. self.values = old_values
  153. except:
  154. self._dtype = old_type # If conversion failed, restore old dtype
  155. raise ValueError("cannot convert from '%s' to '%s'" %
  156. (old_type, new_type))
  157. @property
  158. def parent(self):
  159. """
  160. The section containing this property.
  161. """
  162. return self._parent
  163. @parent.setter
  164. def parent(self, new_parent):
  165. if new_parent is None and self._parent is None:
  166. return
  167. elif new_parent is None and self._parent is not None:
  168. self._parent.remove(self)
  169. self._parent = None
  170. elif self._validate_parent(new_parent):
  171. if self._parent is not None:
  172. self._parent.remove(self)
  173. self._parent = new_parent
  174. self._parent.append(self)
  175. else:
  176. raise ValueError(
  177. "odml.Property.parent: passed value is not of consistent type!"
  178. "odml.Section expected")
  179. @staticmethod
  180. def _validate_parent(new_parent):
  181. from odml.section import BaseSection
  182. if isinstance(new_parent, BaseSection):
  183. return True
  184. return False
  185. @property
  186. def value(self):
  187. """
  188. Deprecated alias of 'values'. Will be removed with the next minor release.
  189. """
  190. print("The attribute 'value' is deprecated. Please use 'values' instead.")
  191. return self.values
  192. @value.setter
  193. def value(self, new_value):
  194. """
  195. Deprecated alias of 'values'. Will be removed with the next minor release.
  196. :param new_value: a single value or list of values.
  197. """
  198. print("The attribute 'value' is deprecated. Please use 'values' instead.")
  199. self.values = new_value
  200. def value_str(self, index=0):
  201. """
  202. Used to access typed data of the value at a specific
  203. index position as a string.
  204. """
  205. return dtypes.set(self._values[index], self._dtype)
  206. def _validate_values(self, values):
  207. """
  208. Method ensures that the passed value(s) can be cast to the
  209. same dtype, i.e. that are associated with this property or the
  210. inferred dtype of the first entry of the values list.
  211. :param values: an iterable that contains the values.
  212. """
  213. for v in values:
  214. try:
  215. dtypes.get(v, self.dtype)
  216. except Exception:
  217. return False
  218. return True
  219. def _convert_value_input(self, new_value):
  220. """
  221. This method ensures, that the passed new value is a list.
  222. If new_value is a string, it will convert it to a list of
  223. strings if the new_value contains embracing brackets.
  224. :return: list of new_value
  225. """
  226. if isinstance(new_value, str):
  227. if new_value[0] == "[" and new_value[-1] == "]":
  228. new_value = list(map(str.strip, new_value[1:-1].split(",")))
  229. else:
  230. new_value = [new_value]
  231. elif isinstance(new_value, dict):
  232. new_value = [str(new_value)]
  233. elif hasattr(new_value, '__iter__') or hasattr(new_value, '__next__'):
  234. new_value = list(new_value)
  235. elif not isinstance(new_value, list):
  236. new_value = [new_value]
  237. else:
  238. raise ValueError("odml.Property._convert_value_input: "
  239. "unsupported data type for values: %s" % type(new_value))
  240. return new_value
  241. @property
  242. def values(self):
  243. """
  244. Returns the value(s) stored in this property. Method always returns a list
  245. that is a copy (!) of the stored value. Changing this list will NOT change
  246. the property.
  247. For manipulation of the stored values use the append, extend, and direct
  248. access methods (using brackets).
  249. For example:
  250. >>> p = odml.Property("prop", values=[1, 2, 3])
  251. >>> print(p.values)
  252. [1, 2, 3]
  253. >>> p.values.append(4)
  254. >>> print(p.values)
  255. [1, 2, 3]
  256. Individual values can be accessed and manipulated like this:
  257. >>> print(p[0])
  258. [1]
  259. >>> p[0] = 4
  260. >>> print(p[0])
  261. [4]
  262. The values can be iterated e.g. with a loop:
  263. >>> for v in p.values:
  264. >>> print(v)
  265. 4
  266. 2
  267. 3
  268. """
  269. return list(self._values)
  270. @values.setter
  271. def values(self, new_value):
  272. """
  273. Set the values of the property discarding any previous information.
  274. Method will try to convert the passed value to the dtype of
  275. the property and raise a ValueError if not possible.
  276. :param new_value: a single value or list of values.
  277. """
  278. # Make sure boolean value 'False' gets through as well...
  279. if new_value is None or \
  280. (isinstance(new_value, (list, tuple, str)) and len(new_value) == 0):
  281. self._values = []
  282. return
  283. new_value = self._convert_value_input(new_value)
  284. if self._dtype is None:
  285. self._dtype = dtypes.infer_dtype(new_value[0])
  286. if not self._validate_values(new_value):
  287. raise ValueError("odml.Property.values: passed values are not of "
  288. "consistent type!")
  289. self._values = [dtypes.get(v, self.dtype) for v in new_value]
  290. @property
  291. def value_origin(self):
  292. return self._value_origin
  293. @value_origin.setter
  294. def value_origin(self, new_value):
  295. if new_value == "":
  296. new_value = None
  297. self._value_origin = new_value
  298. @property
  299. def uncertainty(self):
  300. return self._uncertainty
  301. @uncertainty.setter
  302. def uncertainty(self, new_value):
  303. if new_value == "":
  304. new_value = None
  305. if new_value and not isinstance(new_value, (int, float)):
  306. try:
  307. new_value = float(new_value)
  308. except ValueError:
  309. raise ValueError("odml.Property.uncertainty: passed uncertainty '%s' "
  310. "is not float or int." % new_value)
  311. self._uncertainty = new_value
  312. @property
  313. def unit(self):
  314. return self._unit
  315. @unit.setter
  316. def unit(self, new_value):
  317. if new_value == "":
  318. new_value = None
  319. self._unit = new_value
  320. @property
  321. def reference(self):
  322. return self._reference
  323. @reference.setter
  324. def reference(self, new_value):
  325. if new_value == "":
  326. new_value = None
  327. self._reference = new_value
  328. @property
  329. def definition(self):
  330. return self._definition
  331. @definition.setter
  332. def definition(self, new_value):
  333. if new_value == "":
  334. new_value = None
  335. self._definition = new_value
  336. @property
  337. def dependency(self):
  338. return self._dependency
  339. @dependency.setter
  340. def dependency(self, new_value):
  341. if new_value == "":
  342. new_value = None
  343. self._dependency = new_value
  344. @property
  345. def dependency_value(self):
  346. return self._dependency_value
  347. @dependency_value.setter
  348. def dependency_value(self, new_value):
  349. if new_value == "":
  350. new_value = None
  351. self._dependency_value = new_value
  352. def remove(self, value):
  353. """
  354. Remove a value from this property. Only the first encountered
  355. occurrence of the passed in value is removed from the properties
  356. list of values.
  357. """
  358. if value in self._values:
  359. self._values.remove(value)
  360. def get_path(self):
  361. """
  362. Return the absolute path to this object.
  363. """
  364. if not self.parent:
  365. return "/"
  366. return self.parent.get_path() + ":" + self.name
  367. def clone(self, keep_id=False):
  368. """
  369. Clone this property to copy it independently to another document.
  370. By default the id of the cloned object will be set to a different uuid.
  371. :param keep_id: If this attribute is set to True, the uuid of the
  372. object will remain unchanged.
  373. :return: The cloned property
  374. """
  375. obj = super(BaseProperty, self).clone()
  376. obj._parent = None
  377. obj.values = self._values
  378. if not keep_id:
  379. obj.new_id()
  380. return obj
  381. def merge_check(self, source, strict=True):
  382. """
  383. Checks whether a source Property can be merged with self as destination and
  384. raises a ValueError if the values of source and destination are not compatible.
  385. With parameter *strict=True* a ValueError is also raised, if any of the
  386. attributes unit, definition, uncertainty, reference or value_origin and dtype
  387. differ in source and destination.
  388. :param source: an odML Property.
  389. :param strict: If True, the attributes dtype, unit, uncertainty, definition,
  390. reference and value_origin of source and destination
  391. must be identical.
  392. """
  393. if not isinstance(source, BaseProperty):
  394. raise ValueError("odml.Property.merge: odML Property required.")
  395. # Catch unmerge-able values at this point to avoid
  396. # failing Section tree merges which cannot easily be rolled back.
  397. new_value = self._convert_value_input(source.values)
  398. if not self._validate_values(new_value):
  399. raise ValueError("odml.Property.merge: passed value(s) cannot "
  400. "be converted to data type '%s'!" % self._dtype)
  401. if not strict:
  402. return
  403. if (self.dtype is not None and source.dtype is not None and
  404. self.dtype != source.dtype):
  405. raise ValueError("odml.Property.merge: src and dest dtypes do not match!")
  406. if self.unit is not None and source.unit is not None and self.unit != source.unit:
  407. raise ValueError("odml.Property.merge: "
  408. "src and dest units (%s, %s) do not match!" %
  409. (source.unit, self.unit))
  410. if (self.uncertainty is not None and source.uncertainty is not None and
  411. self.uncertainty != source.uncertainty):
  412. raise ValueError("odml.Property.merge: "
  413. "src and dest uncertainty both set and do not match!")
  414. if self.definition is not None and source.definition is not None:
  415. self_def = ''.join(map(str.strip, self.definition.split())).lower()
  416. other_def = ''.join(map(str.strip, source.definition.split())).lower()
  417. if self_def != other_def:
  418. raise ValueError("odml.Property.merge: "
  419. "src and dest definitions do not match!")
  420. if self.reference is not None and source.reference is not None:
  421. self_ref = ''.join(map(str.strip, self.reference.lower().split()))
  422. other_ref = ''.join(map(str.strip, source.reference.lower().split()))
  423. if self_ref != other_ref:
  424. raise ValueError("odml.Property.merge: "
  425. "src and dest references are in conflict!")
  426. if self.value_origin is not None and source.value_origin is not None:
  427. self_ori = ''.join(map(str.strip, self.value_origin.lower().split()))
  428. other_ori = ''.join(map(str.strip, source.value_origin.lower().split()))
  429. if self_ori != other_ori:
  430. raise ValueError("odml.Property.merge: "
  431. "src and dest value_origin are in conflict!")
  432. def merge(self, other, strict=True):
  433. """
  434. Merges the Property 'other' into self, if possible. Information
  435. will be synchronized. By default the method will raise a ValueError when the
  436. information in this property and the passed property are in conflict.
  437. :param other: an odML Property.
  438. :param strict: Bool value to indicate whether types should be implicitly converted
  439. even when information may be lost. Default is True, i.e. no conversion,
  440. and a ValueError will be raised if types or other attributes do not match.
  441. If a conflict arises with strict=False, the attribute value of self will
  442. be kept, while the attribute value of other will be lost.
  443. """
  444. if not isinstance(other, BaseProperty):
  445. raise TypeError("odml.Property.merge: odml Property required.")
  446. self.merge_check(other, strict)
  447. if self.value_origin is None and other.value_origin is not None:
  448. self.value_origin = other.value_origin
  449. if self.uncertainty is None and other.uncertainty is not None:
  450. self.uncertainty = other.uncertainty
  451. if self.reference is None and other.reference is not None:
  452. self.reference = other.reference
  453. if self.definition is None and other.definition is not None:
  454. self.definition = other.definition
  455. if self.unit is None and other.unit is not None:
  456. self.unit = other.unit
  457. to_add = [v for v in other.values if v not in self._values]
  458. self.extend(to_add, strict=strict)
  459. def unmerge(self, other):
  460. """
  461. Stub that doesn't do anything for this class.
  462. """
  463. pass
  464. def get_merged_equivalent(self):
  465. """
  466. Return the merged object (i.e. if the parent section is linked to another one,
  467. return the corresponding property of the linked section) or None.
  468. """
  469. if self.parent is None or self.parent._merged is None:
  470. return None
  471. return self.parent._merged.contains(self)
  472. @inherit_docstring
  473. def get_terminology_equivalent(self):
  474. if self._parent is None:
  475. return None
  476. sec = self._parent.get_terminology_equivalent()
  477. if sec is None:
  478. return None
  479. try:
  480. return sec.properties[self.name]
  481. except KeyError:
  482. return None
  483. def extend(self, obj, strict=True):
  484. """
  485. Extend the list of values stored in this property by the passed values. Method
  486. will raise a ValueError, if values cannot be converted to the current dtype.
  487. One can also pass another Property to append all values stored in that one.
  488. In this case units must match!
  489. :param obj: single value, list of values or a Property.
  490. :param strict: a Bool that controls whether dtypes must match. Default is True.
  491. """
  492. if isinstance(obj, BaseProperty):
  493. if obj.unit != self.unit:
  494. raise ValueError("odml.Property.extend: src and dest units (%s, %s) "
  495. "do not match!" % (obj.unit, self.unit))
  496. self.extend(obj.values)
  497. return
  498. if self.__len__() == 0:
  499. self.values = obj
  500. return
  501. new_value = self._convert_value_input(obj)
  502. if len(new_value) > 0 and strict and dtypes.infer_dtype(new_value[0]) != self.dtype:
  503. raise ValueError("odml.Property.extend: "
  504. "passed value data type does not match dtype!")
  505. if not self._validate_values(new_value):
  506. raise ValueError("odml.Property.extend: passed value(s) cannot be converted "
  507. "to data type \'%s\'!" % self._dtype)
  508. self._values.extend([dtypes.get(v, self.dtype) for v in new_value])
  509. def append(self, obj, strict=True):
  510. """
  511. Append a single value to the list of stored values. Method will raise
  512. a ValueError if the passed value cannot be converted to the current dtype.
  513. :param obj: the additional value.
  514. :param strict: a Bool that controls whether dtypes must match. Default is True.
  515. """
  516. # Ignore empty values before nasty stuff happens, but make sure
  517. # 0 and False get through.
  518. if obj in [None, "", [], {}]:
  519. return
  520. if not self.values:
  521. self.values = obj
  522. return
  523. new_value = self._convert_value_input(obj)
  524. if len(new_value) > 1:
  525. raise ValueError("odml.property.append: Use extend to add a list of values!")
  526. if len(new_value) > 0 and strict and dtypes.infer_dtype(new_value[0]) != self.dtype:
  527. raise ValueError("odml.Property.append: "
  528. "passed value data type does not match dtype!")
  529. if not self._validate_values(new_value):
  530. raise ValueError("odml.Property.append: passed value(s) cannot be converted "
  531. "to data type \'%s\'!" % self._dtype)
  532. self._values.append(dtypes.get(new_value[0], self.dtype))
  533. def pprint(self, indent=2, max_length=80, current_depth=-1):
  534. """
  535. Pretty print method to visualize Properties and Section-Property trees.
  536. :param indent: number of leading spaces for every child Property.
  537. :param max_length: maximum number of characters printed in one line.
  538. :param current_depth: number of hierarchical levels printed from the
  539. starting Section.
  540. """
  541. property_spaces = ""
  542. prefix = ""
  543. if current_depth >= 0:
  544. property_spaces = " " * ((current_depth + 2) * indent)
  545. prefix = "|-"
  546. if self.unit is None:
  547. value_string = str(self.values)
  548. else:
  549. value_string = "{}{}".format(self.values, self.unit)
  550. p_len = len(property_spaces) + len(self.name) + len(value_string)
  551. if p_len >= max_length - 4:
  552. split_len = int((max_length - len(property_spaces)
  553. + len(self.name) - len(prefix))/2)
  554. str1 = value_string[0: split_len]
  555. str2 = value_string[-split_len:]
  556. print(("{}{} {}: {} ... {}".format(property_spaces, prefix,
  557. self.name, str1, str2)))
  558. else:
  559. print(("{}{} {}: {}".format(property_spaces, prefix, self.name,
  560. value_string)))