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.

components.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. #-*- coding: utf-8 -*-
  2. import itertools
  3. import warnings
  4. import copy
  5. import hashlib
  6. from lodel.utils.mlstring import MlString
  7. from lodel.leapi.datahandlers.field_data_handler import FieldDataHandler
  8. from lodel.editorial_model.exceptions import *
  9. ## @brief Abstract class to represent editorial model components
  10. # @see EmClass EmField
  11. class EmComponent(object):
  12. ## @brief Instanciate an EmComponent
  13. # @param uid str : uniq identifier
  14. # @param display_name MlString|str|dict : component display_name
  15. # @param help_text MlString|str|dict : help_text
  16. def __init__(self, uid, display_name = None, help_text = None, group = None):
  17. if self.__class__ == EmComponent:
  18. raise NotImplementedError('EmComponent is an abstract class')
  19. self.uid = uid
  20. self.display_name = None if display_name is None else MlString(display_name)
  21. self.help_text = None if help_text is None else MlString(help_text)
  22. self.group = group
  23. def __str__(self):
  24. if self.display_name is None:
  25. return str(self.uid)
  26. return str(self.display_name)
  27. def d_hash(self):
  28. m = hashlib.md5()
  29. for data in (
  30. self.uid,
  31. 'NODISPNAME' if self.display_name is None else str(self.display_name.d_hash()),
  32. 'NOHELP' if self.help_text is None else str(self.help_text.d_hash()),
  33. 'NOGROUP' if self.group is None else str(self.group.d_hash()),
  34. ):
  35. m.update(bytes(data, 'utf-8'))
  36. return int.from_bytes(m.digest(), byteorder='big')
  37. ## @brief Handles editorial model objects classes
  38. #
  39. # @note The inheritance system allow child classes to overwrite parents EmField. But it's maybe not a good idea
  40. class EmClass(EmComponent):
  41. ## @brief Instanciate a new EmClass
  42. # @param uid str : uniq identifier
  43. # @param display_name MlString|str|dict : component display_name
  44. # @param abstract bool : set the class as asbtract if True
  45. # @param parents list: parent EmClass list or uid list
  46. # @param help_text MlString|str|dict : help_text
  47. def __init__(self, uid, display_name = None, help_text = None, abstract = False, parents = None, group = None):
  48. super().__init__(uid, display_name, help_text, group)
  49. self.abstract = bool(abstract)
  50. if parents is not None:
  51. if not isinstance(parents, list):
  52. parents = [parents]
  53. for parent in parents:
  54. if not isinstance(parent, EmClass):
  55. raise ValueError("<class EmClass> expected in parents list, but %s found" % type(parent))
  56. else:
  57. parents = list()
  58. self.parents = parents
  59. ## @brief Stores EmFields instances indexed by field uid
  60. self.__fields = dict()
  61. ## @brief Property that represent a dict of all fields (the EmField defined in this class and all its parents)
  62. @property
  63. def __all_fields(self):
  64. res = dict()
  65. for pfields in [ p.__all_fields for p in self.parents]:
  66. res.update(pfields)
  67. res.update(self.__fields)
  68. return res
  69. ## @brief EmField getter
  70. # @param uid None | str : If None returns an iterator on EmField instances else return an EmField instance
  71. # @param no_parents bool : If True returns only fields defined is this class and not the one defined in parents classes
  72. # @return A list on EmFields instances (if uid is None) else return an EmField instance
  73. def fields(self, uid = None, no_parents = False):
  74. fields = self.__fields if no_parents else self.__all_fields
  75. try:
  76. return list(fields.values()) if uid is None else fields[uid]
  77. except KeyError:
  78. raise EditorialModelError("No such EmField '%s'" % uid)
  79. ## @brief Add a field to the EmClass
  80. # @param emfield EmField : an EmField instance
  81. # @warning do not add an EmField allready in another class !
  82. # @throw EditorialModelException if an EmField with same uid allready in this EmClass (overwritting allowed from parents)
  83. def add_field(self, emfield):
  84. if emfield.uid in self.__fields:
  85. raise EditorialModelException("Duplicated uid '%s' for EmField in this class ( %s )" % (emfield.uid, self))
  86. self.__fields[emfield.uid] = emfield
  87. emfield._emclass = self
  88. return emfield
  89. ## @brief Create a new EmField and add it to the EmClass
  90. # @param uid str : the EmField uniq id
  91. # @param **field_kwargs : EmField constructor parameters ( see @ref EmField.__init__() )
  92. def new_field(self, uid, **field_kwargs):
  93. return self.add_field(EmField(uid, **field_kwargs))
  94. def d_hash(self):
  95. m = hashlib.md5()
  96. payload = str(super().d_hash()) + ("1" if self.abstract else "0")
  97. for p in sorted(self.parents):
  98. payload += str(p.d_hash())
  99. for fuid in sorted(self.__fields.keys()):
  100. payload += str(self.__fields[fuid].d_hash())
  101. m.update(bytes(payload, 'utf-8'))
  102. return int.from_bytes(m.digest(), byteorder='big')
  103. ## @brief Handles editorial model classes fields
  104. class EmField(EmComponent):
  105. ## @brief Instanciate a new EmField
  106. # @param uid str : uniq identifier
  107. # @param display_name MlString|str|dict : field display_name
  108. # @param data_handler str : A DataHandler name
  109. # @param help_text MlString|str|dict : help text
  110. # @param group EmGroup :
  111. # @param **handler_kwargs : data handler arguments
  112. def __init__(self, uid, data_handler, display_name = None, help_text = None, group = None, **handler_kwargs):
  113. super().__init__(uid, display_name, help_text, group)
  114. self.data_handler_name = data_handler
  115. self.data_handler_cls = FieldDataHandler.from_name(data_handler)
  116. self.data_handler_options = handler_kwargs
  117. self.data_handler_instance = self.data_handler_cls(**handler_kwargs)
  118. ## @brief Stores the emclass that contains this field (set by EmClass.add_field() method)
  119. self._emclass = None
  120. ## @warning Not complete !
  121. # @todo Complete the hash when data handlers becomes available
  122. def d_hash(self):
  123. return int.from_bytes(hashlib.md5(
  124. bytes(
  125. "%s%s%s" % ( super().d_hash(),
  126. self.data_handler_name,
  127. self.data_handler_options),
  128. 'utf-8')
  129. ).digest(), byteorder='big')
  130. ## @brief Handles functionnal group of EmComponents
  131. class EmGroup(object):
  132. ## @brief Create a new EmGroup
  133. # @note you should NEVER call the constructor yourself. Use Model.add_group instead
  134. # @param uid str : Uniq identifier
  135. # @param depends list : A list of EmGroup dependencies
  136. # @param display_name MlString|str :
  137. # @param help_text MlString|str :
  138. def __init__(self, uid, depends = None, display_name = None, help_text = None):
  139. self.uid = uid
  140. ## @brief Stores the list of groups that depends on this EmGroup indexed by uid
  141. self.required_by = dict()
  142. ## @brief Stores the list of dependencies (EmGroup) indexed by uid
  143. self.require = dict()
  144. ## @brief Stores the list of EmComponent instances contained in this group
  145. self.__components = set()
  146. self.display_name = None if display_name is None else MlString(display_name)
  147. self.help_text = None if help_text is None else MlString(help_text)
  148. if depends is not None:
  149. for grp in depends:
  150. if not isinstance(grp, EmGroup):
  151. raise ValueError("EmGroup expected in depends argument but %s found" % grp)
  152. self.add_dependencie(grp)
  153. ## @brief Returns EmGroup dependencie
  154. # @param recursive bool : if True return all dependencies and their dependencies
  155. # @return a dict of EmGroup identified by uid
  156. def dependencies(self, recursive = False):
  157. res = copy.copy(self.require)
  158. if not recursive:
  159. return res
  160. to_scan = list(res.values())
  161. while len(to_scan) > 0:
  162. cur_dep = to_scan.pop()
  163. for new_dep in cur_dep.require.values():
  164. if new_dep not in res:
  165. to_scan.append(new_dep)
  166. res[new_dep.uid] = new_dep
  167. return res
  168. ## @brief Add components in a group
  169. # @param components list : EmComponent instance list
  170. def add_components(self, components):
  171. for component in components:
  172. if isinstance(component, EmField):
  173. if component._emclass is None:
  174. warnings.warn("Adding an orphan EmField to an EmGroup")
  175. elif not isinstance(component, EmClass):
  176. raise EditorialModelError("Expecting components to be a list of EmComponent, but %s found in the list" % type(component))
  177. self.__components |= set(components)
  178. ## @brief Add a dependencie
  179. # @param em_group EmGroup|iterable : an EmGroup instance or list of instance
  180. def add_dependencie(self, grp):
  181. try:
  182. for group in grp:
  183. self.add_dependencie(group)
  184. return
  185. except TypeError: pass
  186. if grp.uid in self.require:
  187. return
  188. if self.__circular_dependencie(grp):
  189. raise EditorialModelError("Circular dependencie detected, cannot add dependencie")
  190. self.require[grp.uid] = grp
  191. grp.required_by[self.uid] = self
  192. ## @brief Search for circular dependencie
  193. # @return True if circular dep found else False
  194. def __circular_dependencie(self, new_dep):
  195. return self.uid in new_dep.dependencies(True)
  196. ## @brief Fancy string representation of an EmGroup
  197. # @return a string
  198. def __str__(self):
  199. if self.display_name is None:
  200. return self.uid
  201. else:
  202. return self.display_name.get()
  203. def d_hash(self):
  204. payload = "%s%s%s" % (
  205. self.uid,
  206. 'NODNAME' if self.display_name is None else self.display_name.d_hash(),
  207. 'NOHELP' if self.help_text is None else self.help_text.d_hash()
  208. )
  209. for recurs in (False, True):
  210. deps = self.dependencies(recurs)
  211. for dep_uid in sorted(deps.keys()):
  212. payload += str(deps[dep_uid].d_hash())
  213. for req_by_uid in self.required_by:
  214. payload += req_by_uid
  215. return int.from_bytes(
  216. bytes(payload, 'utf-8'),
  217. byteorder = 'big'
  218. )
  219. ## @brief Complete string representation of an EmGroup
  220. # @return a string
  221. def __repr__(self):
  222. return "<class EmGroup '%s' depends : [%s]>" % (self.uid, ', '.join([duid for duid in self.dependencies(False)]) )