No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

base_classes.py 27KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772
  1. #-*- coding: utf-8 -*-
  2. ##
  3. #  @package lodel.leapi.datahandlers.base_classes Defines all base/abstract
  4. # classes for DataHandlers
  5. #
  6. # Contains custom exceptions too
  7. import copy
  8. import importlib
  9. import inspect
  10. import warnings
  11. from lodel.context import LodelContext
  12. LodelContext.expose_modules(globals(), {
  13. 'lodel.exceptions': [
  14. 'LodelException',
  15. 'LodelExceptions',
  16. 'LodelFatalError',
  17. 'DataNoneValid',
  18. 'FieldValidationError'
  19. ],
  20. 'lodel.mlnamedobject.mlnamedobject': ['MlNamedObject'],
  21. 'lodel.leapi.datahandlers.exceptions': [
  22. 'LodelDataHandlerConsistencyException',
  23. 'LodelDataHandlerException'
  24. ],
  25. 'lodel.validator.validator': [
  26. 'ValidationError'
  27. ],
  28. 'lodel.logger': 'logger',
  29. 'lodel.utils.mlstring': ['MlString']})
  30. ##
  31. # @brief Base class for all DataHandlers
  32. # @ingroup lodel2_datahandlers
  33. #
  34. # @remarks Some of the methods and properties in this "abstract" class are
  35. # bounded to its children. This implies that the parent
  36. # is aware of its children, which is an absolute anti-pattern
  37. # (Liskov / OC violation), a source of confusion and a decrased
  38. # maintainability. Aggregation =/= Inheritance
  39. # Concerned methods are: is_reference; is_singlereference.
  40. # Concerned properties are __custom_datahandlers; base_handlers.
  41. # @remarks What is the purpose of an internal property being set to a
  42. # string (namely 'automatic')
  43. # @remarks Two sets of methods appears a little strange in regards to their
  44. # visibility.
  45. # - @ref _construct_data / @ref construct_data
  46. # - @ref _check_data_consistency / @ref check_data_consistency
  47. class DataHandler(MlNamedObject):
  48. base_type = "type"
  49. _HANDLERS_MODULES = ('datas_base', 'datas', 'references')
  50. ##
  51. # @brief Stores the DataHandler child classes indexed by name
  52. _base_handlers = None
  53. ##
  54. # @brief Stores custom DataHandlers classes indexed by name
  55. # @todo do it ! (like plugins, register handlers... blablabla)
  56. __custom_handlers = dict()
  57. help_text = 'Generic Field Data Handler'
  58. display_name = "Generic Field"
  59. options_spec = dict()
  60. options_values = dict()
  61. ##
  62. # @brief Lists fields that will be exposed to the construct_data method
  63. _construct_datas_deps = []
  64. directly_editable = True
  65. ##
  66. # @brief constructor
  67. #
  68. # @param internal False | str : define whether or not a field is internal
  69. # @param immutable bool : Indicates if the fieldtype has to be defined in child classes of
  70. # LeObject or if it is designed globally and immutable
  71. # @throw NotImplementedError If it is instantiated directly
  72. # @remarks Shouldn't the class be declared abstract? No need to check if it
  73. # is instantiated directly, no exception to throw, cleaner code.
  74. def __init__(self, **kwargs):
  75. if self.__class__ == DataHandler:
  76. raise NotImplementedError("Abstract class")
  77. self.__arguments = kwargs
  78. self.nullable = True
  79. self.uniq = False
  80. self.immutable = False
  81. self.primary_key = False
  82. self.internal = False
  83. if 'default' in kwargs:
  84. self.default, error = self.check_data_value(kwargs['default'])
  85. if error:
  86. raise error
  87. del kwargs['default']
  88. for argname, argval in kwargs.items():
  89. setattr(self, argname, argval)
  90. self.check_options()
  91. display_name = kwargs.get('display_name', MlString(self.display_name))
  92. help_text = kwargs.get('help_text', MlString(self.help_text))
  93. super().__init__(display_name, help_text)
  94. ##
  95. # @brief Sets properly cast and checked options for the DataHandler
  96. #
  97. # @throw LodelDataHandlerNotAllowedOptionException when a passed option
  98. # is not in the option specifications of the DataHandler
  99. def check_options(self):
  100. for option_name, option_datas in self.options_spec.items():
  101. if option_name in self.options_values:
  102. # There is a configured option, we check its value
  103. try:
  104. self.options_values[option_name] = option_datas[1].check_value(
  105. self.options_values[option_name])
  106. except ValueError:
  107. pass # TODO Deal with the case where the value used for an option is invalid
  108. else:
  109. # This option was not configured, we get the default value from the specs
  110. self.options_values[option_name] = option_datas[0]
  111. ##
  112. # @return string: Field type name
  113. @classmethod
  114. def name(cls):
  115. return cls.__module__.split('.')[-1]
  116. ##
  117. # @return bool: True if subclass is of Reference type, False otherwise.
  118. @classmethod
  119. def is_reference(cls):
  120. return issubclass(cls, Reference)
  121. ##
  122. # @return bool: True if subclass is of SingleRef type, False otherwise.
  123. @classmethod
  124. def is_singlereference(cls):
  125. return issubclass(cls, SingleRef)
  126. ##
  127. # @return bool: True if the field is a primary_key, False otherwise.
  128. def is_primary_key(self):
  129. return self.primary_key
  130. ##
  131. # @brief checks if a field type is internal
  132. # @return bool: True if the field is internal, False otherwise.
  133. def is_internal(self):
  134. return self.internal is not False
  135. ##
  136. # @brief check if a value can be nullable
  137. #
  138. # @param value *
  139. # @throw DataNoneValid if value is None and nullable.
  140. # @throw LodelExceptions if not nullable
  141. # @return value (if not None)
  142. # @return value
  143. #
  144. # @remarks why are there an thrown exception if it is allowed?
  145. # Exceptions are no message brokers
  146. def _check_data_value(self, value):
  147. if value is None:
  148. if not self.nullable:
  149. raise LodelExceptions("None value is forbidden for this data field")
  150. raise DataNoneValid("None with a nullable. This exception is allowed")
  151. return value
  152. ##
  153. # @brief calls the data_field (defined in derived class) _check_data_value() method
  154. # @param value *
  155. # @return tuple (value|None, None|error) value can be cast if NoneError
  156. # @remarks Consider renaming this method, such as '_is_data_nullable'.
  157. # @remarks Exceptions ARE NOT message brokers! Moreover, those two methods
  158. # are more complicated than required. In case 'value' is None,
  159. # the returned value is the same as the input value. This is the
  160. # same behavior as when the value is not None!
  161. # @return What's a "NoneError"? Value can be cast to what?
  162. def check_data_value(self, value):
  163. try:
  164. value = self._check_data_value(value)
  165. except DataNoneValid as expt:
  166. return value, None
  167. except (LodelExceptions, FieldValidationError) as expt:
  168. return None, expt
  169. return value, None
  170. ##
  171. # @brief Checks if this class can override the given data handler.
  172. # i.e. both class having the same base_type.
  173. # @param data_handler DataHandler
  174. # @return bool
  175. # @remarks Simplify by "return data_handler.__class__.base_type == self.__class__.base_type"?
  176. def can_override(self, data_handler):
  177. if data_handler.__class__.base_type != self.__class__.base_type:
  178. return False
  179. return True
  180. ##
  181. # @brief Build field value
  182. #
  183. # @ingroup lodel2_dh_checks
  184. # @ref _construct_data() and @ref lodel2_dh_check_impl )
  185. #
  186. # @param emcomponent EmComponent : An EmComponent child class instance
  187. # @param fname str : The field name
  188. # @param datas dict : dict storing fields values (from the component)
  189. # @param cur_value : the value from the current field (identified by fieldname)
  190. # @return the value
  191. # @throw RunTimeError if data construction fails
  192. #
  193. # @warning DO NOT REIMPLEMENT THIS METHOD IN A CUSTOM DATAHANDLER (see
  194. # @todo raise something else
  195. #
  196. # @remarks What the todo up right here means? Raise what? When?
  197. # @remarks Nothing is being raised in this method, should it?
  198. def construct_data(self, emcomponent, fname, datas, cur_value):
  199. emcomponent_fields = emcomponent.fields()
  200. data_handler = None
  201. if fname in emcomponent_fields:
  202. data_handler = emcomponent_fields[fname]
  203. new_val = cur_value
  204. if fname in datas.keys():
  205. pass
  206. elif data_handler is not None and hasattr(data_handler, 'default'):
  207. new_val = data_handler.default
  208. elif data_handler is not None and data_handler.nullable:
  209. new_val = None
  210. return self._construct_data(emcomponent, fname, datas, new_val)
  211. ##
  212. # @brief Designed to be reimplemented by child classes
  213. #
  214. # @param emcomponent EmComponent : An EmComponent child class instance
  215. # @param fname str : The field name
  216. # @param datas dict : dict storing fields values (from the component)
  217. # @param cur_value : the value from the current field (identified by fieldname)
  218. # @return the value
  219. # @see construct_data() lodel2_dh_check_impl
  220. def _construct_data(self, emcomponent, fname, datas, cur_value):
  221. return cur_value
  222. ##
  223. # @brief Check data consistency
  224. # @ingroup lodel2_dh_checks
  225. #
  226. # @ref lodel2_dh_datas_construction "Data construction section"
  227. # @param emcomponent EmComponent : An EmComponent child class instance
  228. # @param fname : the field name
  229. # @param datas dict : dict storing fields values
  230. # @return an Exception instance if fails else True
  231. #
  232. # @warning DO NOT REIMPLEMENT THIS METHOD IN A CUSTOM DATAHANDLER (see
  233. # @ref _construct_data() and @ref lodel2_dh_check_impl )
  234. # @warning the data argument looks like a dict but is not a dict
  235. # see @ref base_classes.DatasConstructor "DatasConstructor" and
  236. # @todo A implémenter
  237. def check_data_consistency(self, emcomponent, fname, datas):
  238. return self._check_data_consistency(emcomponent, fname, datas)
  239. ##
  240. # @brief Designed to be reimplemented by child classes
  241. #
  242. # @param emcomponent EmComponent : An EmComponent child class instance
  243. # @param fname : the field name
  244. # @param datas dict : dict storing fields values
  245. # @return an Exception instance if fails else True
  246. #
  247. # @see check_data_consistency() lodel2_dh_check_impl
  248. def _check_data_consistency(self, emcomponent, fname, datas):
  249. return True
  250. ##
  251. # @brief Makes consistency after a query
  252. #
  253. # @param emcomponent EmComponent : An EmComponent child class instance
  254. # @param fname : the field name
  255. # @param datas dict : dict storing fields values
  256. # @return an Exception instance if fails else True
  257. #
  258. # @todo To be implemented
  259. # @remarks It not clear what is the intent of this method...
  260. def make_consistency(self, emcomponent, fname, datas):
  261. pass
  262. ##
  263. # @brief Registers a new data handlers
  264. #
  265. # @note Used by plugins.
  266. # @remarks This method is actually never used anywhere. May consider removing it.
  267. @classmethod
  268. def register_new_handler(cls, name, data_handler):
  269. if not inspect.isclass(data_handler):
  270. raise ValueError("A class was expected but %s given" % type(data_handler))
  271. if not issubclass(data_handler, DataHandler):
  272. raise ValueError("A data handler HAS TO be a child class of DataHandler")
  273. cls.__custom_handlers[name] = data_handler
  274. ##
  275. # @brief Loads all DataHandlers
  276. @classmethod
  277. def load_base_handlers(cls):
  278. if cls._base_handlers is None:
  279. cls._base_handlers = dict()
  280. for module_name in cls._HANDLERS_MODULES:
  281. module = importlib.import_module('lodel.leapi.datahandlers.%s' % module_name)
  282. for name, obj in inspect.getmembers(module):
  283. if inspect.isclass(obj):
  284. logger.debug("Load data handler %s.%s" % (obj.__module__, obj.__name__))
  285. cls._base_handlers[name.lower()] = obj
  286. return copy.copy(cls._base_handlers)
  287. ##
  288. # @brief given a field type name, returns the associated python class
  289. #
  290. # @param name str : A field type name (not case sensitive)
  291. # @return DataField child class
  292. # @throw NameError
  293. #
  294. # @note Would not it be better to prefix the DataHandler name with the
  295. # plugin's one so that it is ensured names are unique?
  296. # @remarks "do/get what from name?" Consider renaming this method (e.g.
  297. # 'get_datafield_from_name')
  298. @classmethod
  299. def from_name(cls, name):
  300. cls.load_base_handlers()
  301. all_handlers = dict(cls._base_handlers, **cls.__custom_handlers)
  302. name = name.lower()
  303. if name not in all_handlers:
  304. raise NameError("No data handlers named '%s'" % (name,))
  305. return all_handlers[name]
  306. ##
  307. # @brief List all DataHandlers
  308. # @return a dict with, display_name for keys, and a dict for value
  309. # @remarks ATM, solely used by the EditorialModel.
  310. # @remarks EditorialModel own class does nothing but calls this class.
  311. # Moreover, nothing calls it anyway.
  312. # @remarks It also seems like it is an EM related concern, and has
  313. # nothing to do with this class. That list appears to be doing
  314. # a purely presentational job. Isn't that a serialization instead?
  315. @classmethod
  316. def list_data_handlers(cls):
  317. cls.load_base_handlers()
  318. all_handlers = dict(cls._base_handlers, **cls.__custom_handlers)
  319. list_dh = dict()
  320. for hdl in all_handlers:
  321. options = dict({'nullable': hdl.nullable,
  322. 'internal': hdl.internal,
  323. 'immutable': hdl.immutable,
  324. 'primary_key': hdl.primary_key}, hdl.options_spec)
  325. list_dh[hdl.display_name] = {'help_text': hdl.help_text, 'options': options}
  326. return list_dh
  327. ##
  328. # @brief Return the module name to import in order to use the DataHandler
  329. # @param datahandler_name str : Data handler name
  330. # @return str
  331. # @remarks consider renaming this (e.g. "datahandler_module_name")
  332. @classmethod
  333. def module_name(cls, datahandler_name):
  334. datahandler_name = datahandler_name.lower()
  335. handler_class = cls.from_name(datahandler_name)
  336. return '{module_name}.{class_name}'.format(
  337. module_name=handler_class.__module__,
  338. class_name=handler_class.__name__
  339. )
  340. ##
  341. # @brief __hash__ implementation for field types
  342. def __hash__(self):
  343. hash_dats = [self.__class__.__module__]
  344. for kdic in sorted([k for k in self.__dict__.keys() if not k.startswith('_')]):
  345. hash_dats.append((kdic, getattr(self, kdic)))
  346. return hash(tuple(hash_dats))
  347. ##
  348. # @brief Base class for data data handler (by opposition with references)
  349. # @ingroup lodel2_datahandlers
  350. class DataField(DataHandler):
  351. pass
  352. ##
  353. # @brief Abstract class for all references
  354. # @ingroup lodel2_datahandlers
  355. #
  356. # References are fields that stores a reference to another
  357. # editorial object
  358. # @todo Construct data implementation : transform the data into a LeObject instance
  359. class Reference(DataHandler):
  360. base_type = "ref"
  361. ##
  362. # @brief Instantiation
  363. # @param allowed_classes list | None : list of allowed em classes if None no restriction
  364. # @param back_reference tuple | None : tuple containing (LeObject child class, field name)
  365. # @param internal bool | string: if False, the field is not internal
  366. # @param **kwargs : other arguments
  367. # @throw ValueError
  368. # @remarks internal may hold the string value 'automatic'. So far, nothing
  369. # mentions what that means, and nothing seems to be aware
  370. # of an 'automatic' value (at least not in leapi package)
  371. def __init__(self, allowed_classes=None, back_reference=None, internal=False, **kwargs):
  372. self.__allowed_classes = set() if allowed_classes is None else set(allowed_classes)
  373. ##
  374. # @note what is "useful to Jinja 2"?
  375. # For now useful to jinja 2
  376. self.allowed_classes = list() if allowed_classes is None else allowed_classes
  377. if back_reference is not None:
  378. if len(back_reference) != 2:
  379. raise ValueError(
  380. "A tuple (classname, fieldname) expected but got '%s'" % back_reference)
  381. ##
  382. # @note Why is there commented out code? Should it be deleted? Ractivated?
  383. # if not issubclass(lodel.leapi.leobject.LeObject, back_reference[0])
  384. # or not isinstance(back_reference[1], str):
  385. # raise TypeError("Back reference was expected to be a tuple(<class LeObject>, str)
  386. # but got : (%s, %s)" % (back_reference[0], back_reference[1]))
  387. self.__back_reference = back_reference
  388. super().__init__(internal=internal, **kwargs)
  389. ##
  390. # @brief Method designed to return an empty value for this kind of
  391. # multipleref
  392. # @remarks purpose!?
  393. @classmethod
  394. def empty(cls):
  395. return None
  396. ##
  397. # @brief Property that takes value of a copy of the back_reference tuple
  398. @property
  399. def back_reference(self):
  400. return copy.copy(self.__back_reference)
  401. ##
  402. # @brief Property that takes value of datahandler of the backreference or
  403. # None
  404. @property
  405. def back_ref_datahandler(self):
  406. if self.__back_reference is None:
  407. return None
  408. return self.__back_reference[0].data_handler(self.__back_reference[1])
  409. @property
  410. def linked_classes(self):
  411. return copy.copy(self.__allowed_classes)
  412. ##
  413. # @brief Sets a back reference.
  414. def _set_back_reference(self, back_reference):
  415. self.__back_reference = back_reference
  416. ##
  417. # @brief Check and cast value in the appropriate type
  418. #
  419. # @param value
  420. # @throw FieldValidationError if value is an appropriate type
  421. # @return value
  422. # @todo implement the check when we have LeObject uid check value
  423. def _check_data_value(self, value):
  424. from lodel.leapi.leobject import LeObject
  425. value = super()._check_data_value(value)
  426. if not (hasattr(value, '__class__') and
  427. issubclass(value.__class__, LeObject)):
  428. if self.__allowed_classes:
  429. rcls = list(self.__allowed_classes)[0]
  430. uidname = rcls.uid_fieldname()[0] # TODO multiple uid is broken
  431. uiddh = rcls.data_handler(uidname)
  432. value = uiddh._check_data_value(value)
  433. else:
  434. raise FieldValidationError(
  435. "Reference datahandler can not check this value %s if any allowed_class is allowed. " % value)
  436. return value
  437. ##
  438. # @brief Check data consistency
  439. #
  440. # @param emcomponent EmComponent :
  441. # @param fname string : the field name
  442. # @param datas dict : dict storing fields values
  443. # @return bool | Exception :
  444. #
  445. # @todo check for performance issues and checks logic
  446. # @warning composed uid capabilities are broken
  447. # @remarks Is that really a legitimate case of retuning an Exception object?
  448. def check_data_consistency(self, emcomponent, fname, datas):
  449. rep = super().check_data_consistency(emcomponent, fname, datas)
  450. if isinstance(rep, Exception):
  451. return rep
  452. if self.back_reference is None:
  453. return True
  454. ##
  455. # @todo Reimplement instance fetching in construct data
  456. # @remarks Set the previous todo as one, looked like it was intended to be.
  457. target_class = self.back_reference[0]
  458. if target_class not in self.__allowed_classes:
  459. logger.warning('Class of the back_reference given is not an allowed class')
  460. return False
  461. value = datas[fname]
  462. ##
  463. # @warning multi uid broken here
  464. # @remarks Why is that broken? Any clue? Set as a warning.
  465. target_uidfield = target_class.uid_fieldname()[0]
  466. obj = target_class.get([(target_uidfield, '=', value)])
  467. if len(obj) == 0:
  468. logger.warning('Object referenced does not exist')
  469. return False
  470. return True
  471. ##
  472. # @brief Utility method designed to fetch referenced objects
  473. #
  474. # @param value mixed : the field value
  475. # @throw NotImplementedError
  476. # @remarks Not implemented? Consider renaming?
  477. def get_referenced(self, value):
  478. raise NotImplementedError
  479. ##
  480. # @brief DataHandler for single reference to another object
  481. #
  482. # An instance of this class acts like a "foreign key" to another object
  483. class SingleRef(Reference):
  484. def __init__(self, allowed_classes=None, **kwargs):
  485. super().__init__(allowed_classes=allowed_classes, **kwargs)
  486. ##
  487. # @brief Checks and casts value to the appropriate type
  488. #
  489. # @param value: mixed
  490. # @throw FieldValidationError if value is inappropriate or can not be cast
  491. # @return mixed
  492. def _check_data_value(self, value):
  493. value = super()._check_data_value(value)
  494. return value
  495. ##
  496. # @brief Utility method to fetch referenced objects
  497. #
  498. # @param value mixed : the field value
  499. # @return A LeObject child class instance
  500. # @throw LodelDataHandlerConsistencyException if no referenced object found
  501. # @remarks Consider renaming (e.g. get_referenced_object)?
  502. def get_referenced(self, value):
  503. for leo_cls in self.linked_classes:
  504. res = leo_cls.get_from_uid(value)
  505. if res is not None:
  506. return res
  507. raise LodelDataHandlerConsistencyException("Unable to find \
  508. referenced object with uid %s" % value)
  509. ##
  510. # @brief DataHandler for multiple references to another object
  511. # @ingroup lodel2_datahandlers
  512. #
  513. # The fields using this data handlers are like SingleRef but can store multiple
  514. # references in one field.
  515. # @note for the moment split on ',' chars
  516. class MultipleRef(Reference):
  517. ##
  518. # @brief Constructor
  519. #
  520. # @param max_item int | None : indicate the maximum number of item referenced
  521. # by this field, None mean no limit
  522. def __init__(self, max_item=None, **kwargs):
  523. self.max_item = max_item
  524. super().__init__(**kwargs)
  525. ##
  526. # @brief Method designed to return an empty value for this kind of
  527. # multipleref
  528. # @remarks Purpose!?
  529. @classmethod
  530. def empty(cls):
  531. return []
  532. ##
  533. # @brief Check and cast value in appropriate type
  534. # @param value mixed
  535. # @throw FieldValidationError if value is unappropriate or can not be cast
  536. # @return value
  537. # @todo Writing test error for errors when stored multiple references in one field
  538. def _check_data_value(self, value):
  539. value = DataHandler._check_data_value(self, value)
  540. if not hasattr(value, '__iter__'):
  541. raise FieldValidationError(
  542. "MultipleRef has to be an iterable or a string, '%s' found" % value)
  543. if self.max_item is not None:
  544. if self.max_item < len(value):
  545. raise FieldValidationError("Too many items")
  546. new_val = list()
  547. error_list = list()
  548. for i, v in enumerate(value):
  549. try:
  550. v = super()._check_data_value(v)
  551. new_val.append(v)
  552. except (FieldValidationError):
  553. error_list.append(repr(v))
  554. if len(error_list) > 0:
  555. raise FieldValidationError(
  556. "MultipleRef have for invalid values [%s] :" % (",".join(error_list)))
  557. return new_val
  558. ##
  559. # @brief Utility method designed to fetch referenced objects
  560. #
  561. # @param values mixed : the field values
  562. # @return A list of LeObject child class instance
  563. # @throw LodelDataHandlerConsistencyException if some referenced objects
  564. # were not found
  565. def get_referenced(self, values):
  566. if values is None or len(values) == 0:
  567. return list()
  568. left = set(values)
  569. values = set(values)
  570. res = list()
  571. for leo_cls in self.linked_classes:
  572. uidname = leo_cls.uid_fieldname()[0] # MULTIPLE UID BROKEN HERE
  573. tmp_res = leo_cls.get(('%s in (%s)' % (uidname, ','.join(
  574. [str(l) for l in left]))))
  575. left ^= set((leo.uid() for leo in tmp_res))
  576. res += tmp_res
  577. if len(left) == 0:
  578. return res
  579. raise LodelDataHandlerConsistencyException("Unable to find \
  580. some referenced objects. Following uids were not found : %s" % ','.join(left))
  581. ##
  582. # @brief Class designed to handle data access while field types are constructing data
  583. # @ingroup lodel2_datahandlers
  584. #
  585. # This class is designed to allow automatic scheduling of construct_data calls.
  586. #
  587. # In theory it has the ability to detect circular dependencies
  588. # @todo test circular deps detection
  589. # @todo test circular deps false positive
  590. # @remarks Would not it be better to make sure what the code actually is doing?
  591. class DatasConstructor(object):
  592. ##
  593. # @brief Init a DatasConstructor
  594. #
  595. # @param leobject LeObject
  596. # @param datas dict : dict with field name as key and field values as value
  597. # @param fields_handler dict : dict with field name as key and data handler instance as value
  598. def __init__(self, leobject, datas, fields_handler):
  599. self._leobject = leobject
  600. self._datas = copy.copy(datas)
  601. # Stores fieldtypes
  602. self._fields_handler = fields_handler
  603. # Stores list of fieldname for constructed
  604. self._constructed = []
  605. # Stores construct calls list
  606. self._construct_calls = []
  607. ##
  608. # @brief Implements the dict.keys() method on instance
  609. #
  610. # @return list
  611. def keys(self):
  612. return self._datas.keys()
  613. ##
  614. # @brief Allows to access the instance like a dict
  615. #
  616. # @param fname string: The field name
  617. # @return field values
  618. # @throw RuntimeError
  619. #
  620. # @note Determine return type
  621. def __getitem__(self, fname):
  622. if fname not in self._constructed:
  623. if fname in self._construct_calls:
  624. raise RuntimeError('Probably circular dependencies in fieldtypes')
  625. cur_value = self._datas[fname] if fname in self._datas else None
  626. self._datas[fname] = self._fields_handler[fname].construct_data(
  627. self._leobject, fname, self, cur_value)
  628. self._constructed.append(fname)
  629. return self._datas[fname]
  630. ##
  631. # @brief Allows to set instance values like a dict
  632. #
  633. # @warning Should not append in theory
  634. #
  635. # @remarks Why is a warning issued any time we call this method?
  636. def __setitem__(self, fname, value):
  637. self._datas[fname] = value
  638. warnings.warn("Setting value of an DatasConstructor instance")
  639. ##
  640. # @brief Class designed to handle a DataHandler option
  641. class DatahandlerOption(MlNamedObject):
  642. ##
  643. # @brief instantiates a new DataHandlerOption object
  644. #
  645. # @param id str
  646. # @param display_name MlString
  647. # @param help_text MlString
  648. # @param validator function
  649. def __init__(self, id, display_name, help_text, validator):
  650. self.__id = id
  651. self.__validator = validator
  652. super().__init__(display_name, help_text)
  653. ##
  654. # @brief Accessor to the id property.
  655. @property
  656. def id(self):
  657. return self.__id
  658. ##
  659. # @brief checks a value corresponding to this option is valid
  660. #
  661. # @param value mixed
  662. # @return cast value
  663. # @throw ValueError
  664. def check_value(self, value):
  665. try:
  666. return self.__validator(value)
  667. except ValidationError:
  668. raise ValueError()