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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  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 import logger
  10. class FieldValidationError(Exception):
  11. pass
  12. ##@brief Base class for all data handlers
  13. class DataHandler(object):
  14. _HANDLERS_MODULES = ('datas_base', 'datas', 'references')
  15. ##@brief Stores the DataHandler childs classes indexed by name
  16. _base_handlers = None
  17. ##@brief Stores custom datahandlers classes indexed by name
  18. # @todo do it ! (like plugins, register handlers... blablabla)
  19. __custom_handlers = dict()
  20. help_text = 'Generic Field Data Handler'
  21. ##@brief List fields that will be exposed to the construct_data_method
  22. _construct_datas_deps = []
  23. directly_editable = True
  24. ##@brief constructor
  25. # @param internal False | str : define whether or not a field is internal
  26. # @param immutable bool : indicates if the fieldtype has to be defined in child classes of LeObject or if it is
  27. # designed globally and immutable
  28. # @param **args
  29. # @throw NotImplementedError if it is instanciated directly
  30. def __init__(self, **kwargs):
  31. if self.__class__ == DataHandler:
  32. raise NotImplementedError("Abstract class")
  33. self.__arguments = kwargs
  34. self.nullable = True
  35. self.uniq = False
  36. self.immutable = False
  37. self.primary_key = False
  38. self.internal = False
  39. if 'default' in kwargs:
  40. self.default, error = self.check_data_value(kwargs['default'])
  41. if error:
  42. raise error
  43. del(kwargs['default'])
  44. for argname, argval in kwargs.items():
  45. setattr(self, argname, argval)
  46. ## Fieldtype name
  47. @staticmethod
  48. def name(cls):
  49. return cls.__module__.split('.')[-1]
  50. @classmethod
  51. def is_reference(cls):
  52. return issubclass(cls, Reference)
  53. def is_primary_key(self):
  54. return self.primary_key
  55. ##@brief checks if a fieldtype is internal
  56. # @return bool
  57. def is_internal(self):
  58. return self.internal is not False
  59. ##@brief calls the data_field defined _check_data_value() method
  60. # @return tuple (value, error|None)
  61. def check_data_value(self, value):
  62. if value is None:
  63. if not self.nullable:
  64. return None, TypeError("'None' value but field is not nullable")
  65. return None, None
  66. return self._check_data_value(value)
  67. ##@brief checks if this class can override the given data handler
  68. # @param data_handler DataHandler
  69. # @return bool
  70. def can_override(self, data_handler):
  71. if data_handler.__class__.base_type != self.__class__.base_type:
  72. return False
  73. return True
  74. ##@brief Build field value
  75. # @param emcomponent EmComponent : An EmComponent child class instance
  76. # @param fname str : The field name
  77. # @param datas dict : dict storing fields values (from the component)
  78. # @param cur_value : the value from the current field (identified by fieldname)
  79. # @return the value
  80. # @throw RunTimeError if data construction fails
  81. def construct_data(self, emcomponent, fname, datas, cur_value):
  82. emcomponent_fields = emcomponent.fields()
  83. data_handler = None
  84. if fname in emcomponent_fields:
  85. data_handler = emcomponent_fields[fname]
  86. if fname in datas.keys():
  87. return cur_value
  88. elif data_handler is not None and hasattr(data_handler, 'default'):
  89. return data_handler.default
  90. elif data_handler is not None and data_handler.nullable:
  91. return None
  92. return cur_value
  93. ##@brief Check datas consistency
  94. # @param emcomponent EmComponent : An EmComponent child class instance
  95. # @param fname : the field name
  96. # @param datas dict : dict storing fields values
  97. # @return an Exception instance if fails else True
  98. # @todo A implémenter
  99. def check_data_consistency(self, emcomponent, fname, datas):
  100. return True
  101. ##@brief This method is use by plugins to register new data handlers
  102. @classmethod
  103. def register_new_handler(cls, name, data_handler):
  104. if not inspect.isclass(data_handler):
  105. raise ValueError("A class was expected but %s given" % type(data_handler))
  106. if not issubclass(data_handler, DataHandler):
  107. raise ValueError("A data handler HAS TO be a child class of DataHandler")
  108. cls.__custom_handlers[name] = data_handler
  109. @classmethod
  110. def load_base_handlers(cls):
  111. if cls._base_handlers is None:
  112. cls._base_handlers = dict()
  113. for module_name in cls._HANDLERS_MODULES:
  114. module = importlib.import_module('lodel.leapi.datahandlers.%s' % module_name)
  115. for name, obj in inspect.getmembers(module):
  116. if inspect.isclass(obj):
  117. logger.debug("Load data handler %s.%s" % (obj.__module__, obj.__name__))
  118. cls._base_handlers[name.lower()] = obj
  119. return copy.copy(cls._base_handlers)
  120. ##@brief given a field type name, returns the associated python class
  121. # @param fieldtype_name str : A field type name (not case sensitive)
  122. # @return DataField child class
  123. # @todo implements custom handlers fetch
  124. # @note To access custom data handlers it can be cool to prefix the handler name by plugin name for example ? (to ensure name unicity)
  125. @classmethod
  126. def from_name(cls, name):
  127. cls.load_base_handlers()
  128. name = name.lower()
  129. if name not in cls._base_handlers:
  130. raise NameError("No data handlers named '%s'" % (name,))
  131. return cls._base_handlers[name]
  132. ##@brief Return the module name to import in order to use the datahandler
  133. # @param data_handler_name str : Data handler name
  134. # @return a str
  135. @classmethod
  136. def module_name(cls, name):
  137. name = name.lower()
  138. handler_class = cls.from_name(name)
  139. return '{module_name}.{class_name}'.format(
  140. module_name = handler_class.__module__,
  141. class_name = handler_class.__name__
  142. )
  143. ##@brief __hash__ implementation for fieldtypes
  144. def __hash__(self):
  145. hash_dats = [self.__class__.__module__]
  146. for kdic in sorted([k for k in self.__dict__.keys() if not k.startswith('_')]):
  147. hash_dats.append((kdic, getattr(self, kdic)))
  148. return hash(tuple(hash_dats))
  149. ##@brief Base class for datas data handler (by opposition with references)
  150. class DataField(DataHandler):
  151. pass
  152. ##@brief Abstract class for all references
  153. #
  154. # References are fields that stores a reference to another
  155. # editorial object
  156. class Reference(DataHandler):
  157. base_type="ref"
  158. ##@brief Instanciation
  159. # @param allowed_classes list | None : list of allowed em classes if None no restriction
  160. # @param back_reference tuple | None : tuple containing (LeObject child class, fieldname)
  161. # @param internal bool : if False, the field is not internal
  162. # @param **kwargs : other arguments
  163. def __init__(self, allowed_classes = None, back_reference = None, internal=False, **kwargs):
  164. self.__allowed_classes = [] if allowed_classes is None else set(allowed_classes)
  165. logger.warning("We're going to inialize an temporary attribute, don't forget to fix this issue")
  166. if back_reference is not None:
  167. if len(back_reference) != 2:
  168. raise ValueError("A tuple (classname, fieldname) expected but got '%s'" % back_reference)
  169. #if not issubclass(back_reference[0], LeObject) or not isinstance(back_reference[1], str):
  170. # raise TypeError("Back reference was expected to be a tuple(<class LeObject>, str) but got : (%s, %s)" % (back_reference[0], back_reference[1]))
  171. self.__back_reference = back_reference
  172. super().__init__(internal=internal, **kwargs)
  173. @property
  174. def back_reference(self):
  175. return copy.copy(self.__back_reference)
  176. @property
  177. def linked_classes(self):
  178. return copy.copy(self.__allowed_classes)
  179. ##@brief Set the back reference for this field.
  180. def _set_back_reference(self, back_reference):
  181. self.__back_reference = back_reference
  182. ##@brief Check value
  183. # @param value *
  184. # @return tuple(value, exception)
  185. # @todo implement the check when we have LeObject to check value
  186. def _check_data_value(self, value):
  187. return value, None
  188. if isinstance(value, lodel.editorial_model.components.EmClass):
  189. value = [value]
  190. for elt in value:
  191. if not issubclass(elt.__class__, EmClass):
  192. return None, FieldValidationError("Some elements of this references are not EmClass instances")
  193. if self.__allowed_classes is not None:
  194. if not isinstance(elt, self.__allowed_classes):
  195. return None, FieldValidationError("Some element of this references are not valids (don't fit with allowed_classes")
  196. return value
  197. ##@brief This class represent a data_handler for single reference to another object
  198. #
  199. # The fields using this data handlers are like "foreign key" on another object
  200. class SingleRef(Reference):
  201. def __init__(self, allowed_classes = None, **kwargs):
  202. super().__init__(allowed_classes = allowed_classes)
  203. def _check_data_value(self, value):
  204. val, expt = super()._check_data_value(value)
  205. if not isinstance(expt, Exception):
  206. if len(val) > 1:
  207. return None, FieldValidationError("Only single values are allowed for SingleRef fields")
  208. return val, expt
  209. ##@brief This class represent a data_handler for multiple references to another object
  210. #
  211. # The fields using this data handlers are like SingleRef but can store multiple references in one field
  212. # @note for the moment split on ',' chars
  213. class MultipleRef(Reference):
  214. ##
  215. # @param max_item int | None : indicate the maximum number of item referenced by this field, None mean no limit
  216. def __init__(self, max_item = None, **kwargs):
  217. self.max_item = max_item
  218. super().__init__(**kwargs)
  219. def _check_data_value(self, value):
  220. expt = None
  221. if isinstance(value, str):
  222. value, expt = super()._check_data_value(value)
  223. elif not hasattr(value, '__iter__'):
  224. return None, FieldValidationError("MultipleRef has to be an iterable or a string")
  225. if self.max_item is not None:
  226. if self.max_item < len(value):
  227. return None, FieldValidationError("Too many items")
  228. return value, expt
  229. def check_data_consistency(self, emcomponent, fname, datas):
  230. return True
  231. ## @brief Class designed to handle datas access will fieldtypes are constructing datas
  232. #
  233. # This class is designed to allow automatic scheduling of construct_data calls.
  234. #
  235. # In theory it's able to detect circular dependencies
  236. # @todo test circular deps detection
  237. # @todo test circulat deps false positiv
  238. class DatasConstructor(object):
  239. ## @brief Init a DatasConstructor
  240. # @param lec LeCrud : @ref LeObject child class
  241. # @param datas dict : dict with field name as key and field values as value
  242. # @param fields_handler dict : dict with field name as key and data handler instance as value
  243. def __init__(self, leobject, datas, fields_handler):
  244. ## Stores concerned class
  245. self._leobject = leobject
  246. ## Stores datas and constructed datas
  247. self._datas = copy.copy(datas)
  248. ## Stores fieldtypes
  249. self._fields_handler = fields_handler
  250. ## Stores list of fieldname for constructed datas
  251. self._constructed = []
  252. ## Stores construct calls list
  253. self._construct_calls = []
  254. ## @brief Implements the dict.keys() method on instance
  255. def keys(self):
  256. return self._datas.keys()
  257. ## @brief Allows to access the instance like a dict
  258. def __getitem__(self, fname):
  259. if fname not in self._constructed:
  260. if fname in self._construct_calls:
  261. raise RuntimeError('Probably circular dependencies in fieldtypes')
  262. cur_value = self._datas[fname] if fname in self._datas else None
  263. self._datas[fname] = self._fields_handler[fname].construct_data(self._leobject, fname, self, cur_value)
  264. self._constructed.append(fname)
  265. return self._datas[fname]
  266. ## @brief Allows to set instance values like a dict
  267. # @warning Should not append in theory
  268. def __setitem__(self, fname, value):
  269. self._datas[fname] = value
  270. warnings.warn("Setting value of an DatasConstructor instance")