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 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. #-*- coding: utf-8 -*-
  2. ## @package lodel.leapi.datahandlers.base_classes Define all base/abstract class for data handlers
  3. #
  4. # Contains custom exceptions too
  5. import copy
  6. import importlib
  7. import inspect
  8. import warnings
  9. from lodel.context import LodelContext
  10. LodelContext.expose_modules(globals(), {
  11. 'lodel.exceptions': [
  12. 'LodelException',
  13. 'LodelExceptions',
  14. 'LodelFatalError',
  15. 'DataNoneValid',
  16. 'FieldValidationError'
  17. ],
  18. 'lodel.mlnamedobject.mlnamedobject': ['MlNamedObject'],
  19. 'lodel.leapi.datahandlers.exceptions': [
  20. 'LodelDataHandlerConsistencyException',
  21. 'LodelDataHandlerException'
  22. ],
  23. 'lodel.validator.validator': [
  24. 'ValidationError'
  25. ],
  26. 'lodel.logger': 'logger'})
  27. ## @brief Base class for all data handlers
  28. # @ingroup lodel2_datahandlers
  29. class DataHandler(MlNamedObject):
  30. base_type = "type"
  31. _HANDLERS_MODULES = ('datas_base', 'datas', 'references')
  32. ## @brief Stores the DataHandler childs classes indexed by name
  33. _base_handlers = None
  34. ## @brief Stores custom datahandlers classes indexed by name
  35. # @todo do it ! (like plugins, register handlers... blablabla)
  36. __custom_handlers = dict()
  37. help_text = 'Generic Field Data Handler'
  38. display_name = "Generic Field"
  39. options_spec = dict()
  40. options_values = dict()
  41. ## @brief List fields that will be exposed to the construct_data_method
  42. _construct_datas_deps = []
  43. directly_editable = True
  44. ## @brief constructor
  45. #
  46. # @param internal False | str : define whether or not a field is internal
  47. # @param immutable bool : indicates if the fieldtype has to be defined in child classes of
  48. # LeObject or if it is designed globally and immutable
  49. # @throw NotImplementedError if it is instanciated directly
  50. def __init__(self, **kwargs):
  51. if self.__class__ == DataHandler:
  52. raise NotImplementedError("Abstract class")
  53. self.__arguments = kwargs
  54. self.nullable = True
  55. self.uniq = False
  56. self.immutable = False
  57. self.primary_key = False
  58. self.internal = False
  59. if 'default' in kwargs:
  60. self.default, error = self.check_data_value(kwargs['default'])
  61. if error:
  62. raise error
  63. del kwargs['default']
  64. for argname, argval in kwargs.items():
  65. setattr(self, argname, argval)
  66. self.check_options()
  67. super().__init__(self.display_name, self.help_text)
  68. ## @brief Sets properly casted and checked options for the datahandler
  69. # @raises LodelDataHandlerNotAllowedOptionException when a passed option is not in the option
  70. # specifications of the datahandler
  71. def check_options(self):
  72. for option_name, option_datas in self.options_spec.items():
  73. if option_name in self.options_values:
  74. # There is a configured option, we check its value
  75. try:
  76. self.options_values[option_name] = option_datas[1].check_value(
  77. self.options_values[option_name])
  78. except ValueError:
  79. pass # TODO Deal with the case where the value used for an option is invalid
  80. else:
  81. # This option was not configured, we get the default value from the specs
  82. self.options_values[option_name] = option_datas[0]
  83. ## Fieldtype name
  84. @classmethod
  85. def name(cls):
  86. return cls.__module__.split('.')[-1]
  87. @classmethod
  88. def is_reference(cls):
  89. return issubclass(cls, Reference)
  90. @classmethod
  91. def is_singlereference(cls):
  92. return issubclass(cls, SingleRef)
  93. def is_primary_key(self):
  94. return self.primary_key
  95. ## @brief checks if a fieldtype is internal
  96. # @return bool
  97. def is_internal(self):
  98. return self.internal is not False
  99. ## @brief check if a value can be nullable
  100. # @param value *
  101. # @throw DataNoneValid if value is None and nullable. LodelExceptions if not nullable
  102. # @return value (if not None)
  103. # @return value
  104. def _check_data_value(self, value):
  105. if value is None:
  106. if not self.nullable:
  107. raise LodelExceptions("None value is forbidden for this data field")
  108. raise DataNoneValid("None with a nullable. This exeption is allowed")
  109. return value
  110. ## @brief calls the data_field (defined in derived class) _check_data_value() method
  111. # @param value *
  112. # @return tuple (value|None, None|error) value can be cast if NoneError
  113. def check_data_value(self, value):
  114. try:
  115. value = self._check_data_value(value)
  116. except DataNoneValid as expt:
  117. return value, None
  118. except (LodelExceptions, FieldValidationError) as expt:
  119. return None, expt
  120. return value, None
  121. ## @brief checks if this class can override the given data handler
  122. # @param data_handler DataHandler
  123. # @return bool
  124. def can_override(self, data_handler):
  125. if data_handler.__class__.base_type != self.__class__.base_type:
  126. return False
  127. return True
  128. ## @brief Build field value
  129. # @ingroup lodel2_dh_checks
  130. # @warning DO NOT REIMPLEMENT THIS METHOD IN A CUSTOM DATAHANDLER (see
  131. # @ref _construct_data() and @ref lodel2_dh_check_impl )
  132. # @param emcomponent EmComponent : An EmComponent child class instance
  133. # @param fname str : The field name
  134. # @param datas dict : dict storing fields values (from the component)
  135. # @param cur_value : the value from the current field (identified by fieldname)
  136. # @return the value
  137. # @throw RunTimeError if data construction fails
  138. # @todo raise something else
  139. def construct_data(self, emcomponent, fname, datas, cur_value):
  140. emcomponent_fields = emcomponent.fields()
  141. data_handler = None
  142. if fname in emcomponent_fields:
  143. data_handler = emcomponent_fields[fname]
  144. new_val = cur_value
  145. if fname in datas.keys():
  146. pass
  147. elif data_handler is not None and hasattr(data_handler, 'default'):
  148. new_val = data_handler.default
  149. elif data_handler is not None and data_handler.nullable:
  150. new_val = None
  151. return self._construct_data(emcomponent, fname, datas, new_val)
  152. ## @brief Designed to be reimplemented by child classes
  153. # @param emcomponent EmComponent : An EmComponent child class instance
  154. # @param fname str : The field name
  155. # @param datas dict : dict storing fields values (from the component)
  156. # @param cur_value : the value from the current field (identified by fieldname)
  157. # @return the value
  158. # @see construct_data() lodel2_dh_check_impl
  159. def _construct_data(self, empcomponent, fname, datas, cur_value):
  160. return cur_value
  161. ## @brief Check datas consistency
  162. # @ingroup lodel2_dh_checks
  163. # @warning DO NOT REIMPLEMENT THIS METHOD IN A CUSTOM DATAHANDLER (see
  164. # @ref _construct_data() and @ref lodel2_dh_check_impl )
  165. # @warning the datas argument looks like a dict but is not a dict
  166. # see @ref base_classes.DatasConstructor "DatasConstructor" and
  167. # @ref lodel2_dh_datas_construction "Datas construction section"
  168. # @param emcomponent EmComponent : An EmComponent child class instance
  169. # @param fname : the field name
  170. # @param datas dict : dict storing fields values
  171. # @return an Exception instance if fails else True
  172. # @todo A implémenter
  173. def check_data_consistency(self, emcomponent, fname, datas):
  174. return self._check_data_consistency(emcomponent, fname, datas)
  175. ## @brief Designed to be reimplemented by child classes
  176. # @param emcomponent EmComponent : An EmComponent child class instance
  177. # @param fname : the field name
  178. # @param datas dict : dict storing fields values
  179. # @return an Exception instance if fails else True
  180. # @see check_data_consistency() lodel2_dh_check_impl
  181. def _check_data_consistency(self, emcomponent, fname, datas):
  182. return True
  183. ## @brief make consistency after a query
  184. # @param emcomponent EmComponent : An EmComponent child class instance
  185. # @param fname : the field name
  186. # @param datas dict : dict storing fields values
  187. # @return an Exception instance if fails else True
  188. # @todo A implémenter
  189. def make_consistency(self, emcomponent, fname, datas):
  190. pass
  191. ## @brief This method is use by plugins to register new data handlers
  192. @classmethod
  193. def register_new_handler(cls, name, data_handler):
  194. if not inspect.isclass(data_handler):
  195. raise ValueError("A class was expected but %s given" % type(data_handler))
  196. if not issubclass(data_handler, DataHandler):
  197. raise ValueError("A data handler HAS TO be a child class of DataHandler")
  198. cls.__custom_handlers[name] = data_handler
  199. ## @brief Load all datahandlers
  200. @classmethod
  201. def load_base_handlers(cls):
  202. if cls._base_handlers is None:
  203. cls._base_handlers = dict()
  204. for module_name in cls._HANDLERS_MODULES:
  205. module = importlib.import_module('lodel.leapi.datahandlers.%s' % module_name)
  206. for name, obj in inspect.getmembers(module):
  207. if inspect.isclass(obj):
  208. logger.debug("Load data handler %s.%s" % (obj.__module__, obj.__name__))
  209. cls._base_handlers[name.lower()] = obj
  210. return copy.copy(cls._base_handlers)
  211. ## @brief given a field type name, returns the associated python class
  212. # @param fieldtype_name str : A field type name (not case sensitive)
  213. # @return DataField child class
  214. # @note To access custom data handlers it can be cool to prefix the handler name by plugin
  215. # name for example ? (to ensure name unicity)
  216. @classmethod
  217. def from_name(cls, name):
  218. cls.load_base_handlers()
  219. all_handlers = dict(cls._base_handlers, **cls.__custom_handlers)
  220. name = name.lower()
  221. if name not in all_handlers:
  222. raise NameError("No data handlers named '%s'" % (name,))
  223. return all_handlers[name]
  224. ## @brief Return the module name to import in order to use the datahandler
  225. # @param data_handler_name str : Data handler name
  226. # @return a str
  227. @classmethod
  228. def module_name(cls, name):
  229. name = name.lower()
  230. handler_class = cls.from_name(name)
  231. return '{module_name}.{class_name}'.format(
  232. module_name=handler_class.__module__,
  233. class_name=handler_class.__name__
  234. )
  235. ## @brief __hash__ implementation for fieldtypes
  236. def __hash__(self):
  237. hash_dats = [self.__class__.__module__]
  238. for kdic in sorted([k for k in self.__dict__.keys() if not k.startswith('_')]):
  239. hash_dats.append((kdic, getattr(self, kdic)))
  240. return hash(tuple(hash_dats))
  241. ## @brief Base class for datas data handler (by opposition with references)
  242. # @ingroup lodel2_datahandlers
  243. class DataField(DataHandler):
  244. pass
  245. ## @brief Abstract class for all references
  246. # @ingroup lodel2_datahandlers
  247. #
  248. # References are fields that stores a reference to another
  249. # editorial object
  250. # @todo Construct data implementation : transform the data into a LeObject instance
  251. class Reference(DataHandler):
  252. base_type = "ref"
  253. ## @brief Instanciation
  254. # @param allowed_classes list | None : list of allowed em classes if None no restriction
  255. # @param back_reference tuple | None : tuple containing (LeObject child class, fieldname)
  256. # @param internal bool : if False, the field is not internal
  257. # @param **kwargs : other arguments
  258. def __init__(self, allowed_classes=None, back_reference=None, internal=False, **kwargs):
  259. self.__allowed_classes = set() if allowed_classes is None else set(allowed_classes)
  260. # For now usefull to jinja 2
  261. self.allowed_classes = list() if allowed_classes is None else allowed_classes
  262. if back_reference is not None:
  263. if len(back_reference) != 2:
  264. raise ValueError("A tuple (classname, fieldname) expected but got '%s'" % back_reference)
  265. # if not issubclass(lodel.leapi.leobject.LeObject, back_reference[0])
  266. # or not isinstance(back_reference[1], str):
  267. # raise TypeError("Back reference was expected to be a tuple(<class LeObject>, str)
  268. # but got : (%s, %s)" % (back_reference[0], back_reference[1]))
  269. self.__back_reference = back_reference
  270. super().__init__(internal=internal, **kwargs)
  271. ## @brief Method designed to return an empty value for this kind of
  272. # multipleref
  273. @classmethod
  274. def empty(cls):
  275. return None
  276. ## @brief Property that takes value of a copy of the back_reference tuple
  277. @property
  278. def back_reference(self):
  279. return copy.copy(self.__back_reference)
  280. ## @brief Property that takes value of datahandler of the backreference or
  281. # None
  282. @property
  283. def back_ref_datahandler(self):
  284. if self.__back_reference is None:
  285. return None
  286. return self.__back_reference[0].data_handler(self.__back_reference[1])
  287. @property
  288. def linked_classes(self):
  289. return copy.copy(self.__allowed_classes)
  290. ## @brief Set the back reference for this field.
  291. def _set_back_reference(self, back_reference):
  292. self.__back_reference = back_reference
  293. ## @brief Check and cast value in appropriate type
  294. # @param value *
  295. # @throw FieldValidationError if value is an appropriate type
  296. # @return value
  297. # @todo implement the check when we have LeObject uid check value
  298. def _check_data_value(self, value):
  299. from lodel.leapi.leobject import LeObject
  300. value = super()._check_data_value(value)
  301. if not (hasattr(value, '__class__') and
  302. issubclass(value.__class__, LeObject)):
  303. if self.__allowed_classes:
  304. rcls = list(self.__allowed_classes)[0]
  305. uidname = rcls.uid_fieldname()[0]# TODO multiple uid is broken
  306. uiddh = rcls.data_handler(uidname)
  307. value = uiddh._check_data_value(value)
  308. else:
  309. raise FieldValidationError("Reference datahandler can not check this value %s if any allowed_class is allowed." % value)
  310. return value
  311. ## @brief Check datas consistency
  312. # @param emcomponent EmComponent : An EmComponent child class instance
  313. # @param fname : the field name
  314. # @param datas dict : dict storing fields values
  315. # @return an Exception instance if fails else True
  316. # @todo check for performance issue and check logics
  317. # @warning composed uid capabilities broken here
  318. def check_data_consistency(self, emcomponent, fname, datas):
  319. rep = super().check_data_consistency(emcomponent, fname, datas)
  320. if isinstance(rep, Exception):
  321. return rep
  322. if self.back_reference is None:
  323. return True
  324. # !! Reimplement instance fetching in construct data !!
  325. target_class = self.back_reference[0]
  326. if target_class not in self.__allowed_classes:
  327. logger.warning('Class of the back_reference given is not an allowed class')
  328. return False
  329. value = datas[fname]
  330. if not target_class.is_exist(value):
  331. logger.warning('Object referenced does not exist')
  332. return False
  333. # target_uidfield = target_class.uid_fieldname()[0] #multi uid broken here
  334. # obj = target_class.get([(target_uidfield, '=', value)])
  335. # if len(obj) == 0:
  336. # logger.warning('Object referenced does not exist')
  337. # return False
  338. return True
  339. ## @brief Utility method designed to fetch referenced objects
  340. # @param value mixed : the field value
  341. # @throw NotImplementedError
  342. def get_referenced(self, value):
  343. raise NotImplementedError
  344. ## @brief This class represent a data_handler for single reference to another object
  345. #
  346. # The fields using this data handlers are like "foreign key" on another object
  347. class SingleRef(Reference):
  348. def __init__(self, allowed_classes=None, **kwargs):
  349. super().__init__(allowed_classes=allowed_classes, **kwargs)
  350. ## @brief Check and cast value in appropriate type
  351. # @param value: *
  352. # @throw FieldValidationError if value is unappropriate or can not be cast
  353. # @return value
  354. def _check_data_value(self, value):
  355. value = super()._check_data_value(value)
  356. return value
  357. ## @brief Utility method designed to fetch referenced objects
  358. # @param value mixed : the field value
  359. # @return A LeObject child class instance
  360. # @throw LodelDataHandlerConsistencyException if no referenced object found
  361. def get_referenced(self, value):
  362. for leo_cls in self.linked_classes:
  363. res = leo_cls.get_from_uid(value)
  364. if res is not None:
  365. return res
  366. raise LodelDataHandlerConsistencyException("Unable to find \
  367. referenced object with uid %s" % value)
  368. ## @brief This class represent a data_handler for multiple references to another object
  369. # @ingroup lodel2_datahandlers
  370. #
  371. # The fields using this data handlers are like SingleRef but can store multiple references in one field
  372. # @note for the moment split on ',' chars
  373. class MultipleRef(Reference):
  374. ## @brief Constructor
  375. # @param max_item int | None : indicate the maximum number of item referenced by this field, None mean no limit
  376. def __init__(self, max_item=None, **kwargs):
  377. self.max_item = max_item
  378. super().__init__(**kwargs)
  379. ## @brief Method designed to return an empty value for this kind of
  380. # multipleref
  381. @classmethod
  382. def empty(cls):
  383. return []
  384. ## @brief Check and cast value in appropriate type
  385. # @param value *
  386. # @throw FieldValidationError if value is unappropriate or can not be cast
  387. # @return value
  388. # @TODO Writing test error for errors when stored multiple references in one field
  389. def _check_data_value(self, value):
  390. value = DataHandler._check_data_value(self, value)
  391. if not hasattr(value, '__iter__'):
  392. raise FieldValidationError("MultipleRef has to be an iterable or a string, '%s' found" % value)
  393. if self.max_item is not None:
  394. if self.max_item < len(value):
  395. raise FieldValidationError("Too many items")
  396. new_val = list()
  397. error_list = list()
  398. for i, v in enumerate(value):
  399. try:
  400. v = super()._check_data_value(v)
  401. new_val.append(v)
  402. except (FieldValidationError):
  403. error_list.append(repr(v))
  404. if len(error_list) > 0:
  405. raise FieldValidationError("MultipleRef have for invalid values [%s] :" % (",".join(error_list)))
  406. return new_val
  407. ## @brief Utility method designed to fetch referenced objects
  408. # @param values mixed : the field values
  409. # @return A list of LeObject child class instance
  410. # @throw LodelDataHandlerConsistencyException if some referenced objects
  411. # were not found
  412. def get_referenced(self, values):
  413. if values is None or len(values) == 0:
  414. return list()
  415. left = set(values)
  416. values = set(values)
  417. res = list()
  418. for leo_cls in self.linked_classes:
  419. uidname = leo_cls.uid_fieldname()[0] # MULTIPLE UID BROKEN HERE
  420. tmp_res = leo_cls.get(('%s in (%s)' % (uidname, ','.join(
  421. [str(l) for l in left]))))
  422. left ^= set((leo.uid() for leo in tmp_res))
  423. res += tmp_res
  424. if len(left) == 0:
  425. return res
  426. raise LodelDataHandlerConsistencyException("Unable to find \
  427. some referenced objects. Following uids were not found : %s" % ','.join(left))
  428. ## @brief Class designed to handle datas access will fieldtypes are constructing datas
  429. # @ingroup lodel2_datahandlers
  430. #
  431. # This class is designed to allow automatic scheduling of construct_data calls.
  432. #
  433. # In theory it's able to detect circular dependencies
  434. # @todo test circular deps detection
  435. # @todo test circular deps false positive
  436. class DatasConstructor(object):
  437. ## @brief Init a DatasConstructor
  438. # @param leobject LeCrud : @ref LeObject child class
  439. # @param datas dict : dict with field name as key and field values as value
  440. # @param fields_handler dict : dict with field name as key and data handler instance as value
  441. def __init__(self, leobject, datas, fields_handler):
  442. # Stores concerned class
  443. self._leobject = leobject
  444. # Stores datas and constructed datas
  445. self._datas = copy.copy(datas)
  446. # Stores fieldtypes
  447. self._fields_handler = fields_handler
  448. # Stores list of fieldname for constructed datas
  449. self._constructed = []
  450. # Stores construct calls list
  451. self._construct_calls = []
  452. ## @brief Implements the dict.keys() method on instance
  453. def keys(self):
  454. return self._datas.keys()
  455. ## @brief Allows to access the instance like a dict
  456. def __getitem__(self, fname):
  457. if fname not in self._constructed:
  458. if fname in self._construct_calls:
  459. raise RuntimeError('Probably circular dependencies in fieldtypes')
  460. cur_value = self._datas[fname] if fname in self._datas else None
  461. self._datas[fname] = self._fields_handler[fname].construct_data(self._leobject, fname, self, cur_value)
  462. self._constructed.append(fname)
  463. return self._datas[fname]
  464. ## @brief Allows to set instance values like a dict
  465. # @warning Should not append in theory
  466. def __setitem__(self, fname, value):
  467. self._datas[fname] = value
  468. warnings.warn("Setting value of an DatasConstructor instance")
  469. ## @brief Class designed to handle an option of a DataHandler
  470. class DatahandlerOption(MlNamedObject):
  471. ## @brief instanciates a new Datahandler option object
  472. #
  473. # @param id str
  474. # @param display_name MlString
  475. # @param help_text MlString
  476. # @param validator function
  477. def __init__(self, id, display_name, help_text, validator):
  478. self.__id = id
  479. self.__validator = validator
  480. super().__init__(display_name, help_text)
  481. @property
  482. def id(self):
  483. return self.__id
  484. ## @brief checks a value corresponding to this option is valid
  485. # @param value
  486. # @return casted value
  487. def check_value(self, value):
  488. try:
  489. return self.__validator(value)
  490. except ValidationError:
  491. raise ValueError()