named_styles.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. from __future__ import absolute_import
  2. # Copyright (c) 2010-2019 openpyxl
  3. from openpyxl.compat import safe_string
  4. from openpyxl.descriptors import (
  5. Typed,
  6. Integer,
  7. Bool,
  8. String,
  9. Sequence,
  10. )
  11. from openpyxl.descriptors.excel import ExtensionList
  12. from openpyxl.descriptors.serialisable import Serialisable
  13. from .fills import PatternFill, Fill
  14. from .fonts import Font
  15. from .borders import Border
  16. from .alignment import Alignment
  17. from .protection import Protection
  18. from .numbers import (
  19. NumberFormatDescriptor,
  20. BUILTIN_FORMATS_MAX_SIZE,
  21. BUILTIN_FORMATS_REVERSE,
  22. )
  23. from .cell_style import (
  24. StyleArray,
  25. CellStyle,
  26. )
  27. class NamedStyle(Serialisable):
  28. """
  29. Named and editable styles
  30. """
  31. font = Typed(expected_type=Font)
  32. fill = Typed(expected_type=Fill)
  33. border = Typed(expected_type=Border)
  34. alignment = Typed(expected_type=Alignment)
  35. number_format = NumberFormatDescriptor()
  36. protection = Typed(expected_type=Protection)
  37. builtinId = Integer(allow_none=True)
  38. hidden = Bool(allow_none=True)
  39. xfId = Integer(allow_none=True)
  40. name = String()
  41. _wb = None
  42. _style = StyleArray()
  43. def __init__(self,
  44. name="Normal",
  45. font=Font(),
  46. fill=PatternFill(),
  47. border=Border(),
  48. alignment=Alignment(),
  49. number_format=None,
  50. protection=Protection(),
  51. builtinId=None,
  52. hidden=False,
  53. xfId=None,
  54. ):
  55. self.name = name
  56. self.font = font
  57. self.fill = fill
  58. self.border = border
  59. self.alignment = alignment
  60. self.number_format = number_format
  61. self.protection = protection
  62. self.builtinId = builtinId
  63. self.hidden = hidden
  64. self._wb = None
  65. self._style = StyleArray()
  66. def __setattr__(self, attr, value):
  67. super(NamedStyle, self).__setattr__(attr, value)
  68. if getattr(self, '_wb', None) and attr in (
  69. 'font', 'fill', 'border', 'alignment', 'number_format', 'protection',
  70. ):
  71. self._recalculate()
  72. def __iter__(self):
  73. for key in ('name', 'builtinId', 'hidden', 'xfId'):
  74. value = getattr(self, key, None)
  75. if value is not None:
  76. yield key, safe_string(value)
  77. @property
  78. def xfId(self):
  79. """
  80. Index of the style in the list of named styles
  81. """
  82. return self._style.xfId
  83. def _set_index(self, idx):
  84. """
  85. Allow the containing list to set the index
  86. """
  87. self._style.xfId = idx
  88. def bind(self, wb):
  89. """
  90. Bind a named style to a workbook
  91. """
  92. self._wb = wb
  93. self._recalculate()
  94. def _recalculate(self):
  95. self._style.fontId = self._wb._fonts.add(self.font)
  96. self._style.borderId = self._wb._borders.add(self.border)
  97. self._style.fillId = self._wb._fills.add(self.fill)
  98. self._style.protectionId = self._wb._protections.add(self.protection)
  99. self._style.alignmentId = self._wb._alignments.add(self.alignment)
  100. fmt = self.number_format
  101. if fmt in BUILTIN_FORMATS_REVERSE:
  102. fmt = BUILTIN_FORMATS_REVERSE[fmt]
  103. else:
  104. fmt = self._wb._number_formats.add(self.number_format) + (
  105. BUILTIN_FORMATS_MAX_SIZE)
  106. self._style.numFmtId = fmt
  107. def as_tuple(self):
  108. """Return a style array representing the current style"""
  109. return self._style
  110. def as_xf(self):
  111. """
  112. Return equivalent XfStyle
  113. """
  114. xf = CellStyle.from_array(self._style)
  115. xf.xfId = None
  116. xf.pivotButton = None
  117. xf.quotePrefix = None
  118. if self.alignment != Alignment():
  119. xf.alignment = self.alignment
  120. if self.protection != Protection():
  121. xf.protection = self.protection
  122. return xf
  123. def as_name(self):
  124. """
  125. Return relevant named style
  126. """
  127. named = _NamedCellStyle(
  128. name=self.name,
  129. builtinId=self.builtinId,
  130. hidden=self.hidden,
  131. xfId=self.xfId
  132. )
  133. return named
  134. class NamedStyleList(list):
  135. """
  136. Named styles are editable and can be applied to multiple objects
  137. As only the index is stored in referencing objects the order mus
  138. be preserved.
  139. """
  140. @property
  141. def names(self):
  142. return [s.name for s in self]
  143. def __getitem__(self, key):
  144. if isinstance(key, int):
  145. return super(NamedStyleList, self).__getitem__(key)
  146. names = self.names
  147. if key not in names:
  148. raise KeyError("No named style with the name{0} exists".format(key))
  149. for idx, name in enumerate(names):
  150. if name == key:
  151. return self[idx]
  152. def append(self, style):
  153. if not isinstance(style, NamedStyle):
  154. raise TypeError("""Only NamedStyle instances can be added""")
  155. elif style.name in self.names:
  156. raise ValueError("""Style {0} exists already""".format(style.name))
  157. style._set_index(len(self))
  158. super(NamedStyleList, self).append(style)
  159. class _NamedCellStyle(Serialisable):
  160. """
  161. Pointer-based representation of named styles in XML
  162. xfId refers to the corresponding CellStyleXfs
  163. Not used in client code.
  164. """
  165. tagname = "cellStyle"
  166. name = String()
  167. xfId = Integer()
  168. builtinId = Integer(allow_none=True)
  169. iLevel = Integer(allow_none=True)
  170. hidden = Bool(allow_none=True)
  171. customBuiltin = Bool(allow_none=True)
  172. extLst = Typed(expected_type=ExtensionList, allow_none=True)
  173. __elements__ = ()
  174. def __init__(self,
  175. name=None,
  176. xfId=None,
  177. builtinId=None,
  178. iLevel=None,
  179. hidden=None,
  180. customBuiltin=None,
  181. extLst=None,
  182. ):
  183. self.name = name
  184. self.xfId = xfId
  185. self.builtinId = builtinId
  186. self.iLevel = iLevel
  187. self.hidden = hidden
  188. self.customBuiltin = customBuiltin
  189. class _NamedCellStyleList(Serialisable):
  190. """
  191. Container for named cell style objects
  192. Not used in client code
  193. """
  194. tagname = "cellStyles"
  195. count = Integer(allow_none=True)
  196. cellStyle = Sequence(expected_type=_NamedCellStyle)
  197. __attrs__ = ("count",)
  198. def __init__(self,
  199. count=None,
  200. cellStyle=(),
  201. ):
  202. self.cellStyle = cellStyle
  203. @property
  204. def count(self):
  205. return len(self.cellStyle)
  206. @property
  207. def names(self):
  208. """
  209. Convert to NamedStyle objects and remove duplicates.
  210. In theory the highest xfId wins but in practice they are duplicates
  211. so it doesn't matter.
  212. """
  213. def sort_fn(v):
  214. return v.xfId
  215. styles = []
  216. names = set()
  217. for ns in sorted(self.cellStyle, key=sort_fn):
  218. if ns.name in names:
  219. continue
  220. style = NamedStyle(
  221. name=ns.name,
  222. hidden=ns.hidden,
  223. builtinId = ns.builtinId
  224. )
  225. names.add(ns.name)
  226. style._set_index(len(styles)) # assign xfId
  227. styles.append(style)
  228. return NamedStyleList(styles)