Geen omschrijving
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.

leobject.py 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. #-*- coding: utf-8 -*-
  2. import importlib
  3. class LeApiErrors(Exception):
  4. ##@brief Instanciate a new exceptions handling multiple exceptions
  5. # @param msg str : Exception message
  6. # @param exceptions dict : A list of data check Exception with concerned field (or stuff) as key
  7. def __init__(self, msg = "Unknow error", exceptions = None):
  8. self._msg = msg
  9. self._exceptions = dict() if exceptions is None else exceptions
  10. def __repr__(self):
  11. return self.__str__()
  12. def __str__(self):
  13. msg = self._msg
  14. for_iter = self._exceptions.items() if isinstance(self._exceptions, dict) else enumerate(self.__exceptions)
  15. for obj, expt in for_iter:
  16. msg += "\n\t{expt_obj} : ({expt_name}) {expt_msg}; ".format(
  17. expt_obj = obj,
  18. expt_name=expt.__class__.__name__,
  19. expt_msg=str(expt)
  20. )
  21. return msg
  22. ##@brief When an error concern a query
  23. class LeApiQueryError(LeApiErrors):
  24. pass
  25. ##@brief When an error concerns a datas
  26. class LeApiDataCheckError(LeApiErrors):
  27. pass
  28. ##@brief Wrapper class for LeObject getter & setter
  29. #
  30. # This class intend to provide easy & friendly access to LeObject fields values
  31. # without name collision problems
  32. # @note Wrapped methods are : LeObject.data() & LeObject.set_data()
  33. class LeObjectValues(object):
  34. ##@brief Construct a new LeObjectValues
  35. # @param set_callback method : The LeObject.set_datas() method of corresponding LeObject class
  36. # @param get_callback method : The LeObject.get_datas() method of corresponding LeObject class
  37. def __init__(self, fieldnames_callback, set_callback, get_callback):
  38. self.__setter = set_callback
  39. self.__getter = get_callback
  40. ##@brief Provide read access to datas values
  41. # @note Read access should be provided for all fields
  42. # @param fname str : Field name
  43. def __getattribute__(self, fname):
  44. return self.__getter(fname)
  45. ##@brief Provide write access to datas values
  46. # @note Write acces shouldn't be provided for internal or immutable fields
  47. # @param fname str : Field name
  48. # @param fval * : the field value
  49. def __setattribute__(self, fname, fval):
  50. return self.__setter(fname, fval)
  51. class LeObject(object):
  52. ##@brief boolean that tells if an object is abtract or not
  53. _abstract = None
  54. ##@brief A dict that stores DataHandler instances indexed by field name
  55. _fields = None
  56. ##@brief A tuple of fieldname (or a uniq fieldname) representing uid
  57. _uid = None
  58. ##@brief Construct an object representing an Editorial component
  59. # @note Can be considered as EmClass instance
  60. def __init__(self, **kwargs):
  61. if self._abstract:
  62. raise NotImplementedError("%s is abstract, you cannot instanciate it." % self.__class__.__name__ )
  63. ##@brief A dict that stores fieldvalues indexed by fieldname
  64. self.__datas = { fname:None for fname in self._fields }
  65. ##@brief Store a list of initianilized fields when instanciation not complete else store True
  66. self.__initialized = list()
  67. ##@brief Datas accessor. Instance of @ref LeObjectValues
  68. self.d = LeObjectValues(self.fieldnames, self.set_data, self.data)
  69. # Checks that uid is given
  70. for uid_name in self._uid:
  71. if uid_name not in kwargs:
  72. raise AttributeError("Cannot instanciate a LeObject without it's identifier")
  73. self.__datas[uid_name] = kwargs[uid_name]
  74. del(kwargs[uid_name])
  75. self.__initialized.append(uid_name)
  76. # Processing given fields
  77. allowed_fieldnames = self.fieldnames(include_ro = False)
  78. err_list = list()
  79. for fieldname, fieldval in kwargs.items():
  80. if fieldname not in allowed_fieldnames:
  81. if fieldname in self._fields:
  82. err_list.append(
  83. AttributeError("Value given for internal field : '%s'" % fieldname)
  84. )
  85. else:
  86. err_list.append(
  87. AttributeError("Unknown fieldname : '%s'" % fieldname)
  88. )
  89. else:
  90. self.__datas[fieldame] = fieldval
  91. self.__initialized = list()
  92. self.set_initialized()
  93. #-----------------------------------#
  94. # Fields datas handling methods #
  95. #-----------------------------------#
  96. ##@brief @property True if LeObject is initialized else False
  97. @property
  98. def initialized(self):
  99. return not isinstance(self.__initialized, list)
  100. ##@return The uid field name
  101. @classmethod
  102. def uid_fieldname(cls):
  103. return cls._uid
  104. ##@brief Return a list of fieldnames
  105. # @param include_ro bool : if True include read only field names
  106. # @return a list of str
  107. @classmethod
  108. def fieldnames(cls, include_ro = False):
  109. if not include_ro:
  110. return [ fname for fname in self._fields if not self._fields[fname].is_internal() ]
  111. else:
  112. return list(self._fields.keys())
  113. @classmethod
  114. def name2objname(cls, name):
  115. return name.title()
  116. ##@brief Return the datahandler asssociated with a LeObject field
  117. # @param fieldname str : The fieldname
  118. # @return A data handler instance
  119. @classmethod
  120. def data_handler(cls, fieldname):
  121. if not fieldname in cls._fields:
  122. raise NameError("No field named '%s' in %s" % (fieldname, cls.__name__))
  123. return cls._fields[fieldname]
  124. ##@brief Return a LeObject child class from a name
  125. # @warning This method has to be called from dynamically generated LeObjects
  126. # @param leobject_name str : LeObject name
  127. # @return A LeObject child class
  128. # @throw NameError if invalid name given
  129. @classmethod
  130. def name2class(cls, leobject_name):
  131. if cls.__module__ == 'lodel.leapi.leobject':
  132. raise NotImplementedError("Abstract method")
  133. mod = importlib.import_module(cls.__module__)
  134. try:
  135. return getattr(mod, leobject_name)
  136. except AttributeError:
  137. raise NameError("No LeObject named '%s'" % leobject_name)
  138. @classmethod
  139. def is_abstract(cls):
  140. return cls._abstract
  141. ##@brief Field data handler gettet
  142. #@param fieldname str : The field name
  143. #@return A datahandler instance
  144. #@throw NameError if the field doesn't exist
  145. @classmethod
  146. def field(cls, fieldname):
  147. try:
  148. return cls._fields[field_uid]
  149. except KeyError:
  150. raise NameError("No field named '%s' in %s" % ( field_uid,
  151. cls.__name__))
  152. ##@brief Read only access to all datas
  153. # @note for fancy data accessor use @ref LeObject.g attribute @ref LeObjectValues instance
  154. # @param name str : field name
  155. # @return the Value
  156. # @throw RuntimeError if the field is not initialized yet
  157. # @throw NameError if name is not an existing field name
  158. def data(self, field_name):
  159. if field_name not in self._fields.keys():
  160. raise NameError("No such field in %s : %s" % (self.__class__.__name__, name))
  161. if not self.initialized and name not in self.__initialized:
  162. raise RuntimeError("The field %s is not initialized yet (and have no value)" % name)
  163. return self.__datas[name]
  164. ##@brief Datas setter
  165. # @note for fancy data accessor use @ref LeObject.g attribute @ref LeObjectValues instance
  166. # @param fname str : field name
  167. # @param fval * : field value
  168. # @return the value that is really set
  169. # @throw NameError if fname is not valid
  170. # @throw AttributeError if the field is not writtable
  171. def set_data(self, fname, fval):
  172. if field_name not in self.fieldnames(include_ro = False):
  173. if field_name not in self._fields.keys():
  174. raise NameError("No such field in %s : %s" % (self.__class__.__name__, name))
  175. else:
  176. raise AttributeError("The field %s is read only" % fname)
  177. self.__datas[fname] = fval
  178. if not self.initialized and fname not in self.__initialized:
  179. # Add field to initialized fields list
  180. self.__initialized.append(fname)
  181. self.__set_initialized()
  182. if self.initialized:
  183. # Running full value check
  184. ret = self.__check_modified_values()
  185. if ret is None:
  186. return self.__datas[fname]
  187. else:
  188. raise LeApiErrors("Data check error", ret)
  189. else:
  190. # Doing value check on modified field
  191. # We skip full validation here because the LeObject is not fully initialized yet
  192. val, err = self._fields[fname].check_data_value(fval)
  193. if isinstance(err, Exception):
  194. #Revert change to be in valid state
  195. del(self.__datas[fname])
  196. del(self.__initialized[-1])
  197. raise LeApiErrors("Data check error", {fname:err})
  198. else:
  199. self.__datas[fname] = val
  200. ##@brief Update the __initialized attribute according to LeObject internal state
  201. #
  202. # Check the list of initialized fields and set __initialized to True if all fields initialized
  203. def __set_initialized(self):
  204. if isinstance(self.__initialized, list):
  205. expected_fields = self.fieldnames(include_ro = False) + self._uid
  206. if set(expected_fields) == set(self.__initialized):
  207. self.__initialized = True
  208. ##@brief Designed to be called when datas are modified
  209. #
  210. # Make different checks on the LeObject given it's state (fully initialized or not)
  211. # @return None if checks succeded else return an exception list
  212. def __check_modified_values(self):
  213. err_list = dict()
  214. if self.__initialized is True:
  215. # Data value check
  216. for fname in self.fieldnames(include_ro = False):
  217. val, err = self._fields[fname].check_data_value(self.__datas[fname])
  218. if err is not None:
  219. err_list[fname] = err
  220. else:
  221. self.__datas[fname] = val
  222. # Data construction
  223. if len(err_list) == 0:
  224. for fname in self.fieldnames(include_ro = True):
  225. try:
  226. field = self._fields[fname]
  227. self.__datas[fname] = fields.construct_data( self,
  228. fname,
  229. self.__datas,
  230. self.__datas[fname]
  231. )
  232. except Exception as e:
  233. err_list[fname] = e
  234. # Datas consistency check
  235. if len(err_list) == 0:
  236. for fname in self.fieldnames(include_ro = True):
  237. field = self._fields[fname]
  238. ret = field.check_data_consistency(self, fname, self.__datas)
  239. if isinstance(ret, Exception):
  240. err_list[fname] = ret
  241. else:
  242. # Data value check for initialized datas
  243. for fname in self.__initialized:
  244. val, err = self._fields[fname].check_data_value(self.__datas[fname])
  245. if err is not None:
  246. err_list[fname] = err
  247. else:
  248. self.__datas[fname] = val
  249. return err_list if len(err_list) > 0 else None
  250. #--------------------#
  251. # Other methods #
  252. #--------------------#
  253. ##@brief Temporary method to set private fields attribute at dynamic code generation
  254. #
  255. # This method is used in the generated dynamic code to set the _fields attribute
  256. # at the end of the dyncode parse
  257. # @warning This method is deleted once the dynamic code loaded
  258. # @param field_list list : list of EmField instance
  259. # @param cls
  260. @classmethod
  261. def _set__fields(cls, field_list):
  262. cls._fields = field_list
  263. ## @brief Check that datas are valid for this type
  264. # @param datas dict : key == field name value are field values
  265. # @param complete bool : if True expect that datas provide values for all non internal fields
  266. # @param allow_internal bool : if True don't raise an error if a field is internal
  267. # @param cls
  268. # @return Checked datas
  269. # @throw LeApiDataCheckError if errors reported during check
  270. @classmethod
  271. def check_datas_value(cls, datas, complete = False, allow_internal = True):
  272. err_l = dict() #Error storing
  273. correct = set() #valid fields name
  274. mandatory = set() #mandatory fields name
  275. for fname, datahandler in cls._fields.items():
  276. if allow_internal or not datahandler.is_internal():
  277. correct.add(fname)
  278. if complete and not hasattr(datahandler, 'default'):
  279. mandatory.add(fname)
  280. provided = set(datas.keys())
  281. # searching for unknow fields
  282. for u_f in provided - correct:
  283. #Here we can check if the field is invalid or rejected because
  284. # it is internel
  285. err_l[u_f] = AttributeError("Unknown or unauthorized field '%s'" % u_f)
  286. # searching for missing mandatory fieldsa
  287. for missing in mandatory - provided:
  288. err_l[miss_field] = AttributeError("The data for field '%s' is missing" % missing)
  289. #Checks datas
  290. checked_datas = dict()
  291. for name, value in [ (name, value) for name, value in datas.items() if name in correct ]:
  292. dh = cls._fields[name]
  293. res = dh.check_data_value(value)
  294. checked_datas[name], err = res
  295. if err:
  296. err_l[name] = err
  297. if len(err_l) > 0:
  298. raise LeApiDataCheckError("Error while checking datas", err_l)
  299. return checked_datas
  300. ##@brief Check and prepare datas
  301. #
  302. # @warning when complete = False we are not able to make construct_datas() and _check_data_consistency()
  303. #
  304. # @param datas dict : {fieldname : fieldvalue, ...}
  305. # @param complete bool : If True you MUST give all the datas
  306. # @param allow_internal : Wether or not interal fields are expected in datas
  307. # @param cls
  308. # @return Datas ready for use
  309. # @todo: complete is very unsafe, find a way to get rid of it
  310. @classmethod
  311. def prepare_datas(cls, datas, complete=False, allow_internal=True):
  312. if not complete:
  313. warnings.warn("\nActual implementation can make datas construction and consitency unsafe when datas are not complete\n")
  314. ret_datas = cls.check_datas_value(datas, complete, allow_internal)
  315. if isinstance(ret_datas, Exception):
  316. raise ret_datas
  317. if complete:
  318. ret_datas = cls._construct_datas(ret_datas)
  319. cls._check_datas_consistency(ret_datas)
  320. return ret_datas
  321. ## @brief Construct datas values
  322. #
  323. # @param cls
  324. # @param datas dict : Datas that have been returned by LeCrud.check_datas_value() methods
  325. # @return A new dict of datas
  326. # @todo IMPLEMENTATION
  327. @classmethod
  328. def _construct_datas(cls, datas):
  329. """
  330. constructor = DatasConstructor(cls, datas, cls.fieldtypes())
  331. ret = {
  332. fname:constructor[fname]
  333. for fname, ftype in cls.fieldtypes().items()
  334. if not ftype.is_internal() or ftype.internal != 'autosql'
  335. }
  336. return ret
  337. """
  338. pass
  339. ## @brief Check datas consistency
  340. # @warning assert that datas is complete
  341. # @param cls
  342. # @param datas dict : Datas that have been returned by LeCrud._construct_datas() method
  343. # @throw LeApiDataCheckError if fails
  344. @classmethod
  345. def _check_datas_consistency(cls, datas):
  346. err_l = []
  347. err_l = dict()
  348. for fname, dh in cls._fields.items():
  349. ret = dh.check_data_consistency(cls, fname, datas)
  350. if isinstance(ret, Exception):
  351. err_l[fname] = ret
  352. if len(err_l) > 0:
  353. raise LeApiDataCheckError("Datas consistency checks fails", err_l)