property.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984
  1. # -*- coding: utf-8
  2. """
  3. This module provides the Base Property class.
  4. """
  5. import uuid
  6. import warnings
  7. from . import base
  8. from . import dtypes
  9. from . import validation
  10. from . import format as frmt
  11. from .tools.doc_inherit import inherit_docstring, allow_inherit_docstring
  12. from .util import format_cardinality
  13. MSG_VALUE_DEPRECATION = "The attribute 'value' is deprecated and will be removed, " \
  14. "use 'values' instead."
  15. def odml_tuple_import(t_count, new_value):
  16. """
  17. Checks via a heuristic if the values in a string fit the general
  18. odml style tuple format and the individual items match the
  19. required number of tuple values.
  20. Legacy Python2 code required to parse unicode strings to a list
  21. of odml style tuples.
  22. :param t_count: integer, required values within a single odml style tuple.
  23. :param new_value: string containing an odml style tuple list.
  24. :return: list of odml style tuples.
  25. """
  26. try:
  27. unicode = unicode
  28. except NameError:
  29. unicode = str
  30. if not isinstance(new_value, (list, tuple)) and \
  31. not isinstance(new_value[0], (list, tuple)):
  32. new_value = [new_value]
  33. return_value = []
  34. for n_val in new_value:
  35. if isinstance(n_val, (list, tuple)):
  36. if len(n_val) == t_count:
  37. n_val_str = "("
  38. for tuple_val in n_val:
  39. n_val_str += str(tuple_val) + "; "
  40. return_value += [n_val_str[:-2] + ")"]
  41. else:
  42. #non-unicode handling needed for python2
  43. if len(n_val) != 1 and not isinstance(n_val[0], unicode):
  44. n_val = n_val.encode('utf-8')
  45. cln = n_val.strip()
  46. br_check = cln.count("(") == cln.count(")")
  47. sep_check = t_count == 1 or cln.count("(") == (cln.count(";") / (t_count - 1))
  48. if len(new_value) == 1 and cln.startswith("["):
  49. l_check = cln.startswith("[") and cln.endswith("]")
  50. com_check = cln.count("(") == (cln.count(",") + 1)
  51. if l_check and br_check and com_check and sep_check:
  52. return_value = cln[1:-1].split(",")
  53. elif br_check and sep_check:
  54. return_value += [cln]
  55. if not return_value:
  56. return_value = new_value
  57. return return_value
  58. @allow_inherit_docstring
  59. class BaseProperty(base.BaseObject):
  60. """
  61. An odML Property.
  62. If a value without an explicitly stated dtype has been provided, dtype will
  63. be inferred from the value.
  64. Example:
  65. >>> p = Property("property1", "a string")
  66. >>> p.dtype
  67. >>> str
  68. >>> p = Property("property1", 2)
  69. >>> p.dtype
  70. >>> int
  71. >>> p = Property("prop", [2, 3, 4])
  72. >>> p.dtype
  73. >>> int
  74. :param name: The name of the Property.
  75. :param values: Some data value, it can be a single value or
  76. a list of homogeneous values.
  77. :param parent: the parent object of the new Property. If the object is not an
  78. odml.Section a ValueError is raised.
  79. :param unit: The unit of the stored data.
  80. :param uncertainty: The uncertainty (e.g. the standard deviation)
  81. associated with a measure value.
  82. :param reference: A reference (e.g. an URL) to an external definition
  83. of the value.
  84. :param definition: The definition of the Property.
  85. :param dependency: Another Property this Property depends on.
  86. :param dependency_value: Dependency on a certain value.
  87. :param dtype: The data type of the values stored in the property,
  88. if dtype is not given, the type is deduced from the values.
  89. Check odml.DType for supported data types.
  90. :param value_origin: Reference where the value originated from e.g. a file name.
  91. :param oid: object id, UUID string as specified in RFC 4122. If no id is provided,
  92. an id will be generated and assigned. An id has to be unique
  93. within an odML Document.
  94. :param val_cardinality: Value cardinality defines how many values are allowed for this Property.
  95. By default unlimited values can be set.
  96. A required number of values can be set by assigning a tuple of the
  97. format "(min, max)".
  98. :param value: Legacy code to the 'values' attribute. If 'values' is provided,
  99. any data provided via 'value' will be ignored.
  100. """
  101. _format = frmt.Property
  102. def __init__(self, name=None, values=None, parent=None, unit=None,
  103. uncertainty=None, reference=None, definition=None,
  104. dependency=None, dependency_value=None, dtype=None,
  105. value_origin=None, oid=None, val_cardinality=None, value=None):
  106. try:
  107. if oid is not None:
  108. self._id = str(uuid.UUID(oid))
  109. else:
  110. self._id = str(uuid.uuid4())
  111. except ValueError as exc:
  112. print(exc)
  113. self._id = str(uuid.uuid4())
  114. # Use id if no name was provided.
  115. if not name:
  116. name = self._id
  117. self._parent = None
  118. self._name = name
  119. self._value_origin = value_origin
  120. self._unit = unit
  121. self._uncertainty = uncertainty
  122. self._reference = reference
  123. self._definition = definition
  124. self._dependency = dependency
  125. self._dependency_value = dependency_value
  126. self._val_cardinality = None
  127. self._dtype = None
  128. if dtypes.valid_type(dtype):
  129. self._dtype = dtype
  130. else:
  131. print("Warning: Unknown dtype '%s'." % dtype)
  132. self._values = []
  133. self.values = values
  134. if not values and (value or isinstance(value, (bool, int))):
  135. # Using stacklevel=2 to avoid file name and code line in the message output.
  136. warnings.warn(MSG_VALUE_DEPRECATION, category=DeprecationWarning, stacklevel=2)
  137. self.values = value
  138. self.parent = parent
  139. # Cardinality should always be set after values have been added
  140. # since it is always tested against values when it is set.
  141. self.val_cardinality = val_cardinality
  142. for err in validation.Validation(self).errors:
  143. if err.is_error:
  144. use_name = err.obj.name if err.obj.id != err.obj.name else None
  145. prop_formatted = "Property[id=%s|%s]" % (err.obj.id, use_name)
  146. msg = "%s\n Validation[%s]: %s" % (prop_formatted, err.rank, err.msg)
  147. print(msg)
  148. def __len__(self):
  149. return len(self._values)
  150. def __getitem__(self, key):
  151. return self._values[key]
  152. def __setitem__(self, key, item):
  153. if int(key) < 0 or int(key) > self.__len__():
  154. raise IndexError("odml.Property.__setitem__: key %i invalid for "
  155. "array of length %i" % (int(key), self.__len__()))
  156. try:
  157. val = dtypes.get(item, self.dtype)
  158. self._values[int(key)] = val
  159. except Exception:
  160. raise ValueError("odml.Property.__setitem__: passed value cannot be "
  161. "converted to data type \'%s\'!" % self._dtype)
  162. def __repr__(self):
  163. return "Property: {name = %s}" % self._name
  164. @property
  165. def oid(self):
  166. """
  167. The uuid of the Property. Required for entity creation and comparison,
  168. saving and loading.
  169. """
  170. return self.id
  171. @property
  172. def id(self):
  173. """
  174. The uuid of the Property.
  175. """
  176. return self._id
  177. def new_id(self, oid=None):
  178. """
  179. new_id sets the object id of the current object to an RFC 4122 compliant UUID.
  180. If an id was provided, it is assigned if it is RFC 4122 UUID format compliant.
  181. If no id was provided, a new UUID is generated and assigned.
  182. :param oid: UUID string as specified in RFC 4122.
  183. """
  184. if oid is not None:
  185. self._id = str(uuid.UUID(oid))
  186. else:
  187. self._id = str(uuid.uuid4())
  188. @property
  189. def name(self):
  190. """
  191. The name of the Property.
  192. """
  193. return self._name
  194. @name.setter
  195. def name(self, new_name):
  196. if self.name == new_name:
  197. return
  198. # Make sure name cannot be set to None or empty
  199. if not new_name:
  200. self._name = self._id
  201. return
  202. curr_parent = self.parent
  203. if hasattr(curr_parent, "properties") and new_name in curr_parent.properties:
  204. raise KeyError("Object with the same name already exists!")
  205. self._name = new_name
  206. @property
  207. def dtype(self):
  208. """
  209. The data type of the value. Check odml.DType for supported data types.
  210. """
  211. return self._dtype
  212. @dtype.setter
  213. def dtype(self, new_type):
  214. """
  215. If the data type of a property value is changed, it is tried
  216. to convert existing values to the new type. If this doesn't work,
  217. the change is refused. The dtype can always be changed, if
  218. a Property does not contain values.
  219. """
  220. # check if this is a valid type
  221. if not dtypes.valid_type(new_type):
  222. raise AttributeError("'%s' is not a valid type." % new_type)
  223. # we convert the value if possible
  224. old_type = self._dtype
  225. old_values = self._values
  226. try:
  227. self._dtype = new_type
  228. self.values = old_values
  229. except:
  230. self._dtype = old_type # If conversion failed, restore old dtype
  231. raise ValueError("cannot convert from '%s' to '%s'" %
  232. (old_type, new_type))
  233. @property
  234. def parent(self):
  235. """
  236. The Section containing this Property.
  237. """
  238. return self._parent
  239. @parent.setter
  240. def parent(self, new_parent):
  241. if new_parent is None and self._parent is None:
  242. return
  243. if new_parent is None and self._parent is not None:
  244. self._parent.remove(self)
  245. self._parent = None
  246. elif self._validate_parent(new_parent):
  247. if self._parent is not None:
  248. self._parent.remove(self)
  249. self._parent = new_parent
  250. self._parent.append(self)
  251. else:
  252. raise ValueError(
  253. "odml.Property.parent: passed value is not of consistent type!"
  254. "odml.Section expected")
  255. @staticmethod
  256. def _validate_parent(new_parent):
  257. """
  258. Checks whether a provided object is a valid odml.Section.
  259. :param new_parent: object to check whether it is an odml.Section.
  260. :returns: Boolean whether the object is an odml.Section or not.
  261. """
  262. from odml.section import BaseSection
  263. if isinstance(new_parent, BaseSection):
  264. return True
  265. return False
  266. @property
  267. def value(self):
  268. """
  269. Deprecated alias of 'values'. Will be removed with the next minor release.
  270. """
  271. # Using stacklevel=2 to avoid file name and code line in the message output.
  272. warnings.warn(MSG_VALUE_DEPRECATION, category=DeprecationWarning, stacklevel=2)
  273. return self.values
  274. @value.setter
  275. def value(self, new_value):
  276. """
  277. Deprecated alias of 'values'. Will be removed with the next minor release.
  278. :param new_value: a single value or list of values.
  279. """
  280. # Using stacklevel=2 to avoid file name and code line in the message output.
  281. warnings.warn(MSG_VALUE_DEPRECATION, category=DeprecationWarning, stacklevel=2)
  282. self.values = new_value
  283. def value_str(self, index=0):
  284. """
  285. Used to access typed data of the value at a specific
  286. index position as a string.
  287. """
  288. return dtypes.set(self._values[index], self._dtype)
  289. def _validate_values(self, values):
  290. """
  291. Method ensures that the passed value(s) can be cast to the
  292. same dtype, i.e. that are associated with this property or the
  293. inferred dtype of the first entry of the values list.
  294. :param values: an iterable that contains the values.
  295. """
  296. for val in values:
  297. try:
  298. dtypes.get(val, self.dtype)
  299. except Exception:
  300. return False
  301. return True
  302. @staticmethod
  303. def _convert_value_input(new_value):
  304. """
  305. This method ensures, that the passed new value is a list.
  306. If new_value is a string, it will convert it to a list of
  307. strings if the new_value contains embracing brackets.
  308. :return: list of new_value
  309. """
  310. if isinstance(new_value, str):
  311. if new_value[0] == "[" and new_value[-1] == "]":
  312. new_value = list(map(str.strip, new_value[1:-1].split(",")))
  313. else:
  314. new_value = [new_value]
  315. elif isinstance(new_value, dict):
  316. new_value = [str(new_value)]
  317. elif hasattr(new_value, '__iter__') or hasattr(new_value, '__next__'):
  318. new_value = list(new_value)
  319. elif not isinstance(new_value, list):
  320. new_value = [new_value]
  321. else:
  322. raise ValueError("odml.Property._convert_value_input: "
  323. "unsupported data type for values: %s" % type(new_value))
  324. return new_value
  325. @property
  326. def values(self):
  327. """
  328. Returns the value(s) stored in this property. Method always returns a list
  329. that is a copy (!) of the stored value. Changing this list will NOT change
  330. the property.
  331. For manipulation of the stored values use the append, extend, and direct
  332. access methods (using brackets).
  333. For example:
  334. >>> p = odml.Property("prop", values=[1, 2, 3])
  335. >>> print(p.values)
  336. [1, 2, 3]
  337. >>> p.values.append(4)
  338. >>> print(p.values)
  339. [1, 2, 3]
  340. Individual values can be accessed and manipulated like this:
  341. >>> print(p[0])
  342. [1]
  343. >>> p[0] = 4
  344. >>> print(p[0])
  345. [4]
  346. The values can be iterated e.g. with a loop:
  347. >>> for v in p.values:
  348. >>> print(v)
  349. 4
  350. 2
  351. 3
  352. """
  353. return list(self._values)
  354. @values.setter
  355. def values(self, new_value):
  356. """
  357. Set the values of the property discarding any previous information.
  358. Method will try to convert the passed value to the dtype of
  359. the property and raise a ValueError if not possible.
  360. :param new_value: a single value or list of values.
  361. """
  362. # Make sure boolean value 'False' gets through as well...
  363. if new_value is None or \
  364. (isinstance(new_value, (list, tuple, str)) and len(new_value) == 0):
  365. self._values = []
  366. return
  367. new_value = self._convert_value_input(new_value)
  368. if self._dtype is None:
  369. self._dtype = dtypes.infer_dtype(new_value[0])
  370. # Python2 legacy code for loading odml style tuples from YAML or JSON.
  371. # Works from Python 3 onwards.
  372. if self._dtype.endswith("-tuple") and not self._validate_values(new_value):
  373. t_count = int(self._dtype.split("-")[0])
  374. new_value = odml_tuple_import(t_count, new_value)
  375. if not self._validate_values(new_value):
  376. msg = "odml.Property.values: passed values are not of consistent type"
  377. if self._dtype in ("date", "time", "datetime"):
  378. req_format = dtypes.default_values(self._dtype)
  379. msg += " \'%s\'! Format should be \'%s\'." % (self._dtype, req_format)
  380. raise ValueError(msg)
  381. self._values = [dtypes.get(v, self.dtype) for v in new_value]
  382. # Validate and inform user if the current values cardinality is violated
  383. self._values_cardinality_validation()
  384. @property
  385. def value_origin(self):
  386. """
  387. Reference where the value originated from e.g. a file name.
  388. :returns: the value_origin of the Property.
  389. """
  390. return self._value_origin
  391. @value_origin.setter
  392. def value_origin(self, new_value):
  393. if new_value == "":
  394. new_value = None
  395. self._value_origin = new_value
  396. @property
  397. def uncertainty(self):
  398. """
  399. The uncertainty (e.g. the standard deviation) associated with
  400. the values of the Property.
  401. :returns: the uncertainty of the Property.
  402. """
  403. return self._uncertainty
  404. @uncertainty.setter
  405. def uncertainty(self, new_value):
  406. if new_value == "":
  407. new_value = None
  408. if new_value and not isinstance(new_value, (int, float)):
  409. try:
  410. new_value = float(new_value)
  411. except ValueError:
  412. raise ValueError("odml.Property.uncertainty: passed uncertainty '%s' "
  413. "is not float or int." % new_value)
  414. self._uncertainty = new_value
  415. @property
  416. def unit(self):
  417. """
  418. The unit associated with the values of the Property.
  419. :returns: the unit of the Property.
  420. """
  421. return self._unit
  422. @unit.setter
  423. def unit(self, new_value):
  424. if new_value == "":
  425. new_value = None
  426. self._unit = new_value
  427. @property
  428. def reference(self):
  429. """
  430. A reference (e.g. an URL) to an external definition of the value.
  431. :returns: the reference of the Property.
  432. """
  433. return self._reference
  434. @reference.setter
  435. def reference(self, new_value):
  436. if new_value == "":
  437. new_value = None
  438. self._reference = new_value
  439. @property
  440. def definition(self):
  441. """
  442. :returns the definition of the Property:
  443. """
  444. return self._definition
  445. @definition.setter
  446. def definition(self, new_value):
  447. if new_value == "":
  448. new_value = None
  449. self._definition = new_value
  450. @property
  451. def dependency(self):
  452. """
  453. Another Property this Property depends on.
  454. :returns: the dependency of the Property.
  455. """
  456. return self._dependency
  457. @dependency.setter
  458. def dependency(self, new_value):
  459. if new_value == "":
  460. new_value = None
  461. self._dependency = new_value
  462. @property
  463. def dependency_value(self):
  464. """
  465. Dependency on a certain value in a dependency Property.
  466. :returns: the required value to be found in a dependency Property.
  467. """
  468. return self._dependency_value
  469. @dependency_value.setter
  470. def dependency_value(self, new_value):
  471. if new_value == "":
  472. new_value = None
  473. self._dependency_value = new_value
  474. @property
  475. def val_cardinality(self):
  476. """
  477. The value cardinality of a Property. It defines how many values
  478. are minimally required and how many values should be maximally
  479. stored. Use the 'set_values_cardinality' method to set.
  480. """
  481. return self._val_cardinality
  482. @val_cardinality.setter
  483. def val_cardinality(self, new_value):
  484. """
  485. Sets the values cardinality of a Property.
  486. The following cardinality cases are supported:
  487. (n, n) - default, no restriction
  488. (d, n) - minimally d entries, no maximum
  489. (n, d) - maximally d entries, no minimum
  490. (d, d) - minimally d entries, maximally d entries
  491. Only positive integers are supported. 'None' is used to denote
  492. no restrictions on a maximum or minimum.
  493. :param new_value: Can be either 'None', a positive integer, which will set
  494. the maximum or an integer 2-tuple of the format '(min, max)'.
  495. """
  496. self._val_cardinality = format_cardinality(new_value)
  497. # Validate and inform user if the current values cardinality is violated
  498. self._values_cardinality_validation()
  499. def _values_cardinality_validation(self):
  500. """
  501. Runs a validation to check whether the values cardinality
  502. is respected and prints a warning message otherwise.
  503. """
  504. # This check is run quite frequently so do not run all checks via the default validation
  505. # but use a custom validation instead.
  506. valid = validation.Validation(self, validate=False, reset=True)
  507. valid.register_custom_handler("property", validation.property_values_cardinality)
  508. valid.run_validation()
  509. val_id = validation.IssueID.property_values_cardinality
  510. # Make sure to display only warnings of the current property
  511. for curr in valid.errors:
  512. if curr.validation_id == val_id and self.id == curr.obj.id:
  513. print("%s: %s" % (curr.rank.capitalize(), curr.msg))
  514. def set_values_cardinality(self, min_val=None, max_val=None):
  515. """
  516. Sets the values cardinality of a Property.
  517. :param min_val: Required minimal number of values elements. None denotes
  518. no restrictions on values elements minimum. Default is None.
  519. :param max_val: Allowed maximal number of values elements. None denotes
  520. no restrictions on values elements maximum. Default is None.
  521. """
  522. self.val_cardinality = (min_val, max_val)
  523. def remove(self, value):
  524. """
  525. Remove a value from this property. Only the first encountered
  526. occurrence of the passed in value is removed from the properties
  527. list of values.
  528. """
  529. if value in self._values:
  530. self._values.remove(value)
  531. def get_path(self):
  532. """
  533. Return the absolute path to this object.
  534. """
  535. if not self.parent:
  536. return "/"
  537. return self.parent.get_path() + ":" + self.name
  538. def clone(self, keep_id=False):
  539. """
  540. Clone this property to copy it independently to another document.
  541. By default the id of the cloned object will be set to a different uuid.
  542. :param keep_id: If this attribute is set to True, the uuid of the
  543. object will remain unchanged.
  544. :return: The cloned property
  545. """
  546. obj = super(BaseProperty, self).clone()
  547. obj._parent = None
  548. obj.values = self._values
  549. if not keep_id:
  550. obj.new_id()
  551. return obj
  552. def merge_check(self, source, strict=True):
  553. """
  554. Checks whether a source Property can be merged with self as destination and
  555. raises a ValueError if the values of source and destination are not compatible.
  556. With parameter *strict=True* a ValueError is also raised, if any of the
  557. attributes unit, definition, uncertainty, reference or value_origin and dtype
  558. differ in source and destination.
  559. :param source: an odML Property.
  560. :param strict: If True, the attributes dtype, unit, uncertainty, definition,
  561. reference and value_origin of source and destination
  562. must be identical.
  563. """
  564. if not isinstance(source, BaseProperty):
  565. raise ValueError("odml.Property.merge: odML Property required.")
  566. # Catch unmerge-able values at this point to avoid
  567. # failing Section tree merges which cannot easily be rolled back.
  568. new_value = self._convert_value_input(source.values)
  569. if not self._validate_values(new_value):
  570. raise ValueError("odml.Property.merge: passed value(s) cannot "
  571. "be converted to data type '%s'!" % self._dtype)
  572. if not strict:
  573. return
  574. if (self.dtype is not None and source.dtype is not None and
  575. self.dtype != source.dtype):
  576. raise ValueError("odml.Property.merge: src and dest dtypes do not match!")
  577. if self.unit is not None and source.unit is not None and self.unit != source.unit:
  578. raise ValueError("odml.Property.merge: "
  579. "src and dest units (%s, %s) do not match!" %
  580. (source.unit, self.unit))
  581. if (self.uncertainty is not None and source.uncertainty is not None and
  582. self.uncertainty != source.uncertainty):
  583. raise ValueError("odml.Property.merge: "
  584. "src and dest uncertainty both set and do not match!")
  585. if self.definition is not None and source.definition is not None:
  586. self_def = ''.join(map(str.strip, self.definition.split())).lower()
  587. other_def = ''.join(map(str.strip, source.definition.split())).lower()
  588. if self_def != other_def:
  589. raise ValueError("odml.Property.merge: "
  590. "src and dest definitions do not match!")
  591. if self.reference is not None and source.reference is not None:
  592. self_ref = ''.join(map(str.strip, self.reference.lower().split()))
  593. other_ref = ''.join(map(str.strip, source.reference.lower().split()))
  594. if self_ref != other_ref:
  595. raise ValueError("odml.Property.merge: "
  596. "src and dest references are in conflict!")
  597. if self.value_origin is not None and source.value_origin is not None:
  598. self_ori = ''.join(map(str.strip, self.value_origin.lower().split()))
  599. other_ori = ''.join(map(str.strip, source.value_origin.lower().split()))
  600. if self_ori != other_ori:
  601. raise ValueError("odml.Property.merge: "
  602. "src and dest value_origin are in conflict!")
  603. def merge(self, other, strict=True):
  604. """
  605. Merges the Property 'other' into self, if possible. Information
  606. will be synchronized. By default the method will raise a ValueError when the
  607. information in this property and the passed property are in conflict.
  608. :param other: an odML Property.
  609. :param strict: Bool value to indicate whether types should be implicitly converted
  610. even when information may be lost. Default is True, i.e. no conversion,
  611. and a ValueError will be raised if types or other attributes do not match.
  612. If a conflict arises with strict=False, the attribute value of self will
  613. be kept, while the attribute value of other will be lost.
  614. """
  615. if not isinstance(other, BaseProperty):
  616. raise TypeError("odml.Property.merge: odml Property required.")
  617. self.merge_check(other, strict)
  618. if self.value_origin is None and other.value_origin is not None:
  619. self.value_origin = other.value_origin
  620. if self.uncertainty is None and other.uncertainty is not None:
  621. self.uncertainty = other.uncertainty
  622. if self.reference is None and other.reference is not None:
  623. self.reference = other.reference
  624. if self.definition is None and other.definition is not None:
  625. self.definition = other.definition
  626. if self.unit is None and other.unit is not None:
  627. self.unit = other.unit
  628. to_add = [v for v in other.values if v not in self._values]
  629. self.extend(to_add, strict=strict)
  630. def unmerge(self, other):
  631. """
  632. Stub that doesn't do anything for this class.
  633. """
  634. pass
  635. def get_merged_equivalent(self):
  636. """
  637. Return the merged object (i.e. if the parent section is linked to another one,
  638. return the corresponding property of the linked section) or None.
  639. """
  640. if self.parent is None or not self.parent.is_merged:
  641. return None
  642. return self.parent.get_merged_equivalent().contains(self)
  643. @inherit_docstring
  644. def get_terminology_equivalent(self):
  645. if self._parent is None:
  646. return None
  647. sec = self._parent.get_terminology_equivalent()
  648. if sec is None:
  649. return None
  650. try:
  651. return sec.properties[self.name]
  652. except KeyError:
  653. return None
  654. def _reorder(self, childlist, new_index):
  655. lst = childlist
  656. old_index = lst.index(self)
  657. # 2 cases: insert after old_index / insert before
  658. if new_index > old_index:
  659. new_index += 1
  660. lst.insert(new_index, self)
  661. if new_index < old_index:
  662. del lst[old_index + 1]
  663. else:
  664. del lst[old_index]
  665. return old_index
  666. def reorder(self, new_index):
  667. """
  668. Move this object in its parent child-list to the position *new_index*.
  669. :return: The old index at which the object was found.
  670. """
  671. if not self.parent:
  672. raise ValueError("odml.Property.reorder: "
  673. "Property has no parent, cannot reorder in parent list.")
  674. return self._reorder(self.parent.properties, new_index)
  675. def extend(self, obj, strict=True):
  676. """
  677. Extend the list of values stored in this property by the passed values. Method
  678. will raise a ValueError, if values cannot be converted to the current dtype.
  679. One can also pass another Property to append all values stored in that one.
  680. In this case units must match!
  681. :param obj: single value, list of values or a Property.
  682. :param strict: a Bool that controls whether dtypes must match. Default is True.
  683. """
  684. if isinstance(obj, BaseProperty):
  685. if obj.unit != self.unit:
  686. raise ValueError("odml.Property.extend: src and dest units (%s, %s) "
  687. "do not match!" % (obj.unit, self.unit))
  688. self.extend(obj.values)
  689. return
  690. if self.__len__() == 0:
  691. self.values = obj
  692. return
  693. new_value = self._convert_value_input(obj)
  694. if self._dtype.endswith("-tuple"):
  695. t_count = int(self._dtype.split("-")[0])
  696. new_value = odml_tuple_import(t_count, new_value)
  697. if len(new_value) > 0 and strict and \
  698. dtypes.infer_dtype(new_value[0]) != self.dtype:
  699. type_check = dtypes.infer_dtype(new_value[0])
  700. if not (type_check == "string" and self.dtype in dtypes.special_dtypes) \
  701. and not self.dtype.endswith("-tuple"):
  702. msg = "odml.Property.extend: passed value data type found "
  703. msg += "(\"%s\") does not match expected dtype \"%s\"!" % (type_check,
  704. self._dtype)
  705. raise ValueError(msg)
  706. if not self._validate_values(new_value):
  707. raise ValueError("odml.Property.extend: passed value(s) cannot be converted "
  708. "to data type \'%s\'!" % self._dtype)
  709. self._values.extend([dtypes.get(v, self.dtype) for v in new_value])
  710. def append(self, obj, strict=True):
  711. """
  712. Append a single value to the list of stored values. Method will raise
  713. a ValueError if the passed value cannot be converted to the current dtype.
  714. :param obj: the additional value.
  715. :param strict: a Bool that controls whether dtypes must match. Default is True.
  716. """
  717. # Ignore empty values before nasty stuff happens, but make sure
  718. # 0 and False get through.
  719. if obj in [None, "", [], {}]:
  720. return
  721. if not self.values:
  722. self.values = obj
  723. return
  724. new_value = self._convert_value_input(obj)
  725. if len(new_value) > 1:
  726. raise ValueError("odml.property.append: Use extend to add a list of values!")
  727. if self._dtype.endswith("-tuple"):
  728. t_count = int(self._dtype.split("-")[0])
  729. new_value = odml_tuple_import(t_count, new_value)
  730. if len(new_value) > 0 and strict and \
  731. dtypes.infer_dtype(new_value[0]) != self.dtype:
  732. type_check = dtypes.infer_dtype(new_value[0])
  733. if not (type_check == "string" and self.dtype in dtypes.special_dtypes) \
  734. and not self.dtype.endswith("-tuple"):
  735. msg = "odml.Property.append: passed value data type found "
  736. msg += "(\"%s\") does not match expected dtype \"%s\"!" % (type_check,
  737. self._dtype)
  738. raise ValueError(msg)
  739. if not self._validate_values(new_value):
  740. raise ValueError("odml.Property.append: passed value(s) cannot be converted "
  741. "to data type \'%s\'!" % self._dtype)
  742. self._values.append(dtypes.get(new_value[0], self.dtype))
  743. def insert(self, index, obj, strict=True):
  744. """
  745. Insert a single value to the list of stored values. Method will raise
  746. a ValueError if the passed value cannot be converted to the current dtype.
  747. :param obj: the additional value.
  748. :param index: position of the new value
  749. :param strict: a Bool that controls whether dtypes must match. Default is True.
  750. """
  751. # Ignore empty values before nasty stuff happens, but make sure
  752. # 0 and False get through.
  753. if obj in [None, "", [], {}]:
  754. return
  755. if not self.values:
  756. self.values = obj
  757. return
  758. new_value = self._convert_value_input(obj)
  759. if len(new_value) > 1:
  760. raise ValueError("odml.property.insert: Use extend to add a list of values!")
  761. new_value = self._convert_value_input(obj)
  762. if len(new_value) > 1:
  763. raise ValueError("odml.property.insert: Use extend to add a list of values!")
  764. if self._dtype.endswith("-tuple"):
  765. t_count = int(self._dtype.split("-")[0])
  766. new_value = odml_tuple_import(t_count, new_value)
  767. if len(new_value) > 0 and strict and \
  768. dtypes.infer_dtype(new_value[0]) != self.dtype:
  769. type_check = dtypes.infer_dtype(new_value[0])
  770. if not (type_check == "string" and self.dtype in dtypes.special_dtypes) \
  771. and not self.dtype.endswith("-tuple"):
  772. msg = "odml.Property.insert: passed value data type found "
  773. msg += "(\"%s\") does not match expected dtype \"%s\"!" % (type_check,
  774. self._dtype)
  775. raise ValueError(msg)
  776. if not self._validate_values(new_value):
  777. raise ValueError("odml.Property.insert: passed value(s) cannot be converted "
  778. "to data type \'%s\'!" % self._dtype)
  779. if index > len(self._values):
  780. warnings.warn("odml.Property.insert: Index %i larger than length of property.values. "
  781. "Added value at end of list." % index, stacklevel=2)
  782. self._values.insert(index, dtypes.get(new_value[0], self.dtype))
  783. def pprint(self, indent=2, max_length=80, current_depth=-1):
  784. """
  785. Pretty print method to visualize Properties and Section-Property trees.
  786. :param indent: number of leading spaces for every child Property.
  787. :param max_length: maximum number of characters printed in one line.
  788. :param current_depth: number of hierarchical levels printed from the
  789. starting Section.
  790. """
  791. property_spaces = ""
  792. prefix = ""
  793. if current_depth >= 0:
  794. property_spaces = " " * ((current_depth + 2) * indent)
  795. prefix = "|-"
  796. if self.unit is None:
  797. value_string = str(self.values)
  798. else:
  799. value_string = "{}{}".format(self.values, self.unit)
  800. p_len = len(property_spaces) + len(self.name) + len(value_string)
  801. if p_len >= max_length - 4:
  802. split_len = int((max_length - len(property_spaces)
  803. + len(self.name) - len(prefix))/2)
  804. str1 = value_string[0: split_len]
  805. str2 = value_string[-split_len:]
  806. print(("{}{} {}: {} ... {}".format(property_spaces, prefix,
  807. self.name, str1, str2)))
  808. else:
  809. print(("{}{} {}: {}".format(property_spaces, prefix, self.name,
  810. value_string)))
  811. def export_leaf(self):
  812. """
  813. Export the path including all direct parents from this Property
  814. to the root of the document. Section properties are included,
  815. Subsections are not included.
  816. :returns: Cloned odml tree to the root of the current document.
  817. """
  818. export = self
  819. if export.parent:
  820. # Section.export_leaf will take care of the full export and
  821. # include the current Property.
  822. export = export.parent.export_leaf()
  823. return export