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.

generic.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. #-*- coding: utf-8 -*-
  2. ## @package EditorialModel.fieldtypes.generic Class definition for fieldtypes
  3. import copy
  4. import types
  5. import importlib
  6. ## @brief Abstract class for all fieldtypes
  7. class GenericFieldType(object):
  8. help_text = 'Generic field type : abstract class for every fieldtype'
  9. ## @brief List fields that will be exposed to the construct_data_method
  10. _construct_datas_deps = []
  11. ## @brief Generic constructor for fieldtypes 
  12. # @param internal False | str : define wheter or not a field is internal
  13. # @param immutable bool : indicate if the fieldtype has to be defined in child classes of LeObject or if it is defined globally and immutable
  14. # @throw NotImplementedError if called from bad class
  15. def __init__(self, internal = False, immutable = False, **args):
  16. if self.__class__ == GenericFieldType:
  17. raise NotImplementedError("Abstract class")
  18. self.internal = internal #Check this value ?
  19. self.immutable = bool(immutable)
  20. for argname, argval in args.items():
  21. setattr(self, argname, argval)
  22. ## Fieldtype name
  23. # @todo should be a staticmethod
  24. @property
  25. def name(self):
  26. return self.__module__.split('.')[-1]
  27. ## @return True if a fieldtype is internal
  28. def is_internal(self):
  29. return self.internal != False
  30. ## @brief Take care to call the fieldtype defined _check_data_value() method
  31. # @return a tuple (value, error|None)
  32. def check_data_value(self, value):
  33. return self._check_data_value(value)
  34. def _check_data_value(self, value):
  35. return (value, None)
  36. ## @brief Build field value
  37. # @param lec LeCrud : A LeCrud child class
  38. # @param fname str : The field name
  39. # @param datas dict : dict storing fields values (from the lec)
  40. # @param cur_value : the value for the current field (identified by fieldname)
  41. # @return constructed datas (for the fname field)
  42. # @throw RuntimeError if unable to construct data
  43. def construct_data(self, lec, fname, datas, cur_value):
  44. if fname in datas.keys():
  45. return cur_value
  46. elif hasattr(lec.fieldtypes()[fname], 'default'):
  47. return lec.fieldtypes()[fname].default
  48. elif lec.fieldtypes()[fname].nullable:
  49. return None
  50. raise RuntimeError("Unable to construct data for field %s", fname)
  51. ## @brief Check datas consistency
  52. # @param leo LeCrud : A LeCrud child class instance
  53. # @param fname str : The field name
  54. # @param datas dict : dict storing fields values
  55. # @return an Exception instance if fails else True
  56. def check_data_consistency(self, lec, fname, datas):
  57. return True
  58. ## @brief Given a fieldtype name return the associated python class
  59. # @param fieldtype_name str : A fieldtype name
  60. # @return An GenericFieldType derivated class
  61. @staticmethod
  62. def from_name(fieldtype_name):
  63. mod = importlib.import_module(GenericFieldType.module_name(fieldtype_name))
  64. return mod.EmFieldType
  65. ## @brief Get a module name given a fieldtype name
  66. # @param fieldtype_name str : A fieldtype name
  67. # @return a string representing a python module name
  68. @staticmethod
  69. def module_name(fieldtype_name):
  70. return 'EditorialModel.fieldtypes.%s' % (fieldtype_name)
  71. ## @brief __hash__ implementation for fieldtypes
  72. def __hash__(self):
  73. hash_dats = [self.__class__.__module__]
  74. for kdic in sorted([k for k in self.__dict__.keys() if not k.startswith('_')]):
  75. hash_dats.append((kdic, getattr(self, kdic)))
  76. return hash(tuple(hash_dats))
  77. ## @brief Represent fieldtypes handling a single value
  78. class SingleValueFieldType(GenericFieldType):
  79. ## @brief Instanciate a new fieldtype
  80. # @param nullable bool : is None allowed as value ?
  81. # @param uniqu bool : Indicate if a field should handle uniq value
  82. # @param primary bool : If True the field is a primary key
  83. # @param **args : Other arguments
  84. # @throw NotImplementedError if called from bad class
  85. def __init__(self, internal = False, nullable = True, uniq = False, primary = False, **args):
  86. if self.__class__ == SingleValueFieldType:
  87. raise NotImplementedError("Asbtract class")
  88. super().__init__(internal, **args)
  89. self.nullable = nullable
  90. self.uniq = uniq
  91. self.primary = primary
  92. if 'default' in args:
  93. self.default, error = self.check_data_value(args['default'])
  94. if error:
  95. raise error
  96. del(args['default'])
  97. def check_data_value(self, value):
  98. if value is None:
  99. if not self.nullable:
  100. return (None, TypeError("'None' value but field is not nullable"))
  101. return (None, None)
  102. return super().check_data_value(value)
  103. ## @brief Abstract class for fieldtype representing references
  104. #
  105. # In MySQL its foreign keys (for example leo fieldtypes)
  106. class ReferenceFieldType(SingleValueFieldType):
  107. ## @brief Instanciate a new fieldtype
  108. #
  109. #
  110. # @param reference str : A string that defines the reference (can be 'object' or 'relation')
  111. # @param nullable bool : is None allowed as value ?
  112. # @param unique bool : Indicate if a field should handle uniq value
  113. # @param primary bool : If True the field is a primary key
  114. # @param **args : Other arguments
  115. # @throw NotImplementedError if called from bad class
  116. def __init__(self, reference, internal=False, nullable = True, uniq = False, primary = False, **args):
  117. if reference.lower() not in ['relation', 'object']:
  118. raise ValueError("Bad value for reference : %s. Excepted on of 'relation', 'object'" % reference)
  119. self.reference = reference.lower()
  120. super().__init__(
  121. internal = internal,
  122. nullable = nullable,
  123. uniq = uniq,
  124. primary = primary,
  125. **args
  126. );
  127. pass
  128. ## @brief Abstract class for fieldtypes handling multiple values identified by a key
  129. #
  130. # For example i18n fieldtype
  131. class MultiValueFieldType(GenericFieldType):
  132. ## @brief Instanciate a new multivalue fieldtype
  133. #
  134. # This fieldtype describe a field that handles multiple values referenced by a key (like a dict).
  135. # A typicall example is the i18n fieldtype
  136. # @param keyname str : The identifier key name
  137. # @param key_fieldtype SingleValueFieldType : A SingleValueFieldType child class instance
  138. # @param value_fieldtype SingleValueFieldType : A SingleValueFieldType child class instance
  139. def __init__(self, keyname, key_fieldtype, value_fieldtype, internal = False, **args):
  140. super().__init__(internal)
  141. ## stores the keyname
  142. self.keyname = keyname
  143. ## stores the fieldtype that handles the key
  144. self.key_fieldtype = key_fieldtype
  145. ## stores the fieldtype that handles the values
  146. self.value_fieldtype = value_fieldtype
  147. ## @brief Class designed to handle datas access will fieldtypes are constructing datas
  148. #
  149. # This class is designed to allow automatic scheduling of construct_data calls.
  150. #
  151. # In theory it's able to detect circular dependencies
  152. # @todo test circular deps detection
  153. # @todo test circulat deps false positiv
  154. class DatasConstructor(object):
  155. ## @brief Init a DatasConstructor
  156. # @param lec LeCrud : LeCrud child class
  157. # @param datas dict : dict with field name as key and field values as value
  158. # @param fieldtypes dict : dict with field name as key and fieldtype as value
  159. def __init__(self, lec, datas, fieldtypes):
  160. ## Stores concerned class
  161. self._lec = lec
  162. ## Stores datas and constructed datas
  163. self._datas = copy.copy(datas)
  164. ## Stores fieldtypes
  165. self._fieldtypes = fieldtypes
  166. ## Stores list of fieldname for constructed datas
  167. self._constructed = []
  168. ## Stores construct calls list
  169. self._construct_calls = []
  170. ## @brief Implements the dict.keys() method on instance
  171. def keys(self):
  172. return self._datas.keys()
  173. ## @brief Allows to access the instance like a dict
  174. def __getitem__(self, fname):
  175. if fname not in self._constructed:
  176. if fname in self._construct_calls:
  177. raise RuntimeError('Probably circular dependencies in fieldtypes')
  178. cur_value = self._datas[fname] if fname in self._datas else None
  179. self._datas[fname] = self._fieldtypes[fname].construct_data(self._lec, fname, self, cur_value)
  180. self._constructed.append(fname)
  181. return self._datas[fname]
  182. ## @brief Allows to set instance values like a dict
  183. # @warning Should not append in theory
  184. def __setitem__(self, fname, value):
  185. self._datas[fname] = value
  186. warnings.warn("Setting value of an DatasConstructor instance")
  187. #
  188. #
  189. # Exceptions
  190. #
  191. #
  192. class FieldTypeError(Exception):
  193. pass
  194. class FieldTypeDataCheckError(FieldTypeError):
  195. ## @brief Instanciate a new data check error
  196. # @param expt_l list : A list of data check Exception
  197. def __init__(self, expt_l):
  198. self._expt_l = expt_l
  199. def __str__(self):
  200. msg = "Data check errors : "
  201. for expt in self._expt_l:
  202. msg += "{expt_name}:{expt_msg}; ".format(expt_name=expt.__class__.__name__, expt_msg=str(expt))
  203. return msg
  204. ## @page lodel2_fieldtypes Lodel2 fieldtypes
  205. #
  206. # @section fieldtypes_features Main features
  207. #
  208. # Lodel2 defines Class and Types containing fields that handle values.
  209. # Fields are defined by FieldTypes. It's objects that are able to check datas value, to construct values (~cast) and to check datas consistency given the list of datas of an Lodel2 object (Class or Type)
  210. #
  211. # @subsection fieldtypes_hierarchy Fieldtypes family
  212. #
  213. # Fieldtypes are python objects. We use inheritance to defines FieldTypes. Here is a list of main FieldTypes inheritance :
  214. # - GenericFieldType
  215. # - SingleValueFieldType <- handles a single value
  216. # - ReferenceFieldType <- handles a reference to another field
  217. # - leo.EmFieldType <- handles references to a LeObject (designed for LeRelation)
  218. # - char.EmFieldType <- handles string
  219. # - integer.EmFieldType <- handles integer
  220. # - pk.EmFieldType <- handles primary keys (identifier)
  221. # - MultiValueFieldType <- handles multiple values identified by a key
  222. # - i18n.EmFieldType <- handles a string and its translations
  223. #
  224. # @subsection fieldtypes_options Fieldtypes main options
  225. #
  226. # There is 2 options that are common for every fieldtypes :
  227. # - internal : that indicate who construct the data. Possible values are
  228. # - False : the field is not internal, its user that provides datas
  229. # - 'automatic' : The field is internal, its leapi that provides datas (see construct in @ref fieldtypes_validator )
  230. # - 'autosql' : BAD NAME. The field is internal but it is the datasource that provide the data
  231. # - immutable : Its a boolean that indicate if a fieldtype defined in EditorialModel.classtypes is immutable or HAVE TO be defined in EmClass
  232. #
  233. # @subsubsection fieldtypes_options_single_value SingleValueFieldType options
  234. #
  235. # SingleValueFieldType have more standart options :
  236. # - nullable (boolean) : is None allowed as value
  237. # - uniq (boolean) : if True the value has to be uniq in all instances of concerned Lodel2 API object
  238. # - primary (boolean) : if True the field is an identifier (primary key)
  239. # - default : if given as argument defines a default value for the FieldType
  240. #
  241. # @subsection fieldtypes_validator Data validation
  242. #
  243. # For each Lodel2 API objects (LeObject, LeRelation, ...) there is a sequence that is run to check datas and transform them. Each step is run for each fieldtypes of a Lodel2 API object
  244. #
  245. # -# Check data : this is a basic data check (for example if an integer is expected and the string 'foo' is given the check will fails)
  246. # -# Construct data : this is a data 'casting' step, this method is called with all other datas from the Lodel2 API object as argument (to be able to construct datas with other parts of the object) @ref fieldtypes_construct_order
  247. # -# Datas consistency checks : this step, as the construct step, has all datas from the Lodel2 API object as argument, and check for the whole datas consistency
  248. #
  249. # @subsubsection fieldtypes_construct_order Data construction dependencies
  250. #
  251. # To handle data construction dependencies there is an object DatasConstructor able to call the data construction when needed and to detect (not tested yet) circular dependencies