table.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. # Copyright (c) 2010-2024 openpyxl
  2. from openpyxl.descriptors.serialisable import Serialisable
  3. from openpyxl.descriptors import (
  4. Descriptor,
  5. Alias,
  6. Typed,
  7. Bool,
  8. Integer,
  9. NoneSet,
  10. String,
  11. Sequence,
  12. )
  13. from openpyxl.descriptors.excel import ExtensionList, CellRange
  14. from openpyxl.descriptors.sequence import NestedSequence
  15. from openpyxl.xml.constants import SHEET_MAIN_NS, REL_NS
  16. from openpyxl.xml.functions import tostring
  17. from openpyxl.utils import range_boundaries
  18. from openpyxl.utils.escape import escape, unescape
  19. from .related import Related
  20. from .filters import (
  21. AutoFilter,
  22. SortState,
  23. )
  24. TABLESTYLES = tuple(
  25. ["TableStyleMedium{0}".format(i) for i in range(1, 29)]
  26. + ["TableStyleLight{0}".format(i) for i in range(1, 22)]
  27. + ["TableStyleDark{0}".format(i) for i in range(1, 12)]
  28. )
  29. PIVOTSTYLES = tuple(
  30. ["PivotStyleMedium{0}".format(i) for i in range(1, 29)]
  31. + ["PivotStyleLight{0}".format(i) for i in range(1, 29)]
  32. + ["PivotStyleDark{0}".format(i) for i in range(1, 29)]
  33. )
  34. class TableStyleInfo(Serialisable):
  35. tagname = "tableStyleInfo"
  36. name = String(allow_none=True)
  37. showFirstColumn = Bool(allow_none=True)
  38. showLastColumn = Bool(allow_none=True)
  39. showRowStripes = Bool(allow_none=True)
  40. showColumnStripes = Bool(allow_none=True)
  41. def __init__(self,
  42. name=None,
  43. showFirstColumn=None,
  44. showLastColumn=None,
  45. showRowStripes=None,
  46. showColumnStripes=None,
  47. ):
  48. self.name = name
  49. self.showFirstColumn = showFirstColumn
  50. self.showLastColumn = showLastColumn
  51. self.showRowStripes = showRowStripes
  52. self.showColumnStripes = showColumnStripes
  53. class XMLColumnProps(Serialisable):
  54. tagname = "xmlColumnPr"
  55. mapId = Integer()
  56. xpath = String()
  57. denormalized = Bool(allow_none=True)
  58. xmlDataType = String()
  59. extLst = Typed(expected_type=ExtensionList, allow_none=True)
  60. __elements__ = ()
  61. def __init__(self,
  62. mapId=None,
  63. xpath=None,
  64. denormalized=None,
  65. xmlDataType=None,
  66. extLst=None,
  67. ):
  68. self.mapId = mapId
  69. self.xpath = xpath
  70. self.denormalized = denormalized
  71. self.xmlDataType = xmlDataType
  72. class TableFormula(Serialisable):
  73. tagname = "tableFormula"
  74. ## Note formula is stored as the text value
  75. array = Bool(allow_none=True)
  76. attr_text = Descriptor()
  77. text = Alias('attr_text')
  78. def __init__(self,
  79. array=None,
  80. attr_text=None,
  81. ):
  82. self.array = array
  83. self.attr_text = attr_text
  84. class TableColumn(Serialisable):
  85. tagname = "tableColumn"
  86. id = Integer()
  87. uniqueName = String(allow_none=True)
  88. name = String()
  89. totalsRowFunction = NoneSet(values=(['sum', 'min', 'max', 'average',
  90. 'count', 'countNums', 'stdDev', 'var', 'custom']))
  91. totalsRowLabel = String(allow_none=True)
  92. queryTableFieldId = Integer(allow_none=True)
  93. headerRowDxfId = Integer(allow_none=True)
  94. dataDxfId = Integer(allow_none=True)
  95. totalsRowDxfId = Integer(allow_none=True)
  96. headerRowCellStyle = String(allow_none=True)
  97. dataCellStyle = String(allow_none=True)
  98. totalsRowCellStyle = String(allow_none=True)
  99. calculatedColumnFormula = Typed(expected_type=TableFormula, allow_none=True)
  100. totalsRowFormula = Typed(expected_type=TableFormula, allow_none=True)
  101. xmlColumnPr = Typed(expected_type=XMLColumnProps, allow_none=True)
  102. extLst = Typed(expected_type=ExtensionList, allow_none=True)
  103. __elements__ = ('calculatedColumnFormula', 'totalsRowFormula',
  104. 'xmlColumnPr', 'extLst')
  105. def __init__(self,
  106. id=None,
  107. uniqueName=None,
  108. name=None,
  109. totalsRowFunction=None,
  110. totalsRowLabel=None,
  111. queryTableFieldId=None,
  112. headerRowDxfId=None,
  113. dataDxfId=None,
  114. totalsRowDxfId=None,
  115. headerRowCellStyle=None,
  116. dataCellStyle=None,
  117. totalsRowCellStyle=None,
  118. calculatedColumnFormula=None,
  119. totalsRowFormula=None,
  120. xmlColumnPr=None,
  121. extLst=None,
  122. ):
  123. self.id = id
  124. self.uniqueName = uniqueName
  125. self.name = name
  126. self.totalsRowFunction = totalsRowFunction
  127. self.totalsRowLabel = totalsRowLabel
  128. self.queryTableFieldId = queryTableFieldId
  129. self.headerRowDxfId = headerRowDxfId
  130. self.dataDxfId = dataDxfId
  131. self.totalsRowDxfId = totalsRowDxfId
  132. self.headerRowCellStyle = headerRowCellStyle
  133. self.dataCellStyle = dataCellStyle
  134. self.totalsRowCellStyle = totalsRowCellStyle
  135. self.calculatedColumnFormula = calculatedColumnFormula
  136. self.totalsRowFormula = totalsRowFormula
  137. self.xmlColumnPr = xmlColumnPr
  138. self.extLst = extLst
  139. def __iter__(self):
  140. for k, v in super().__iter__():
  141. if k == 'name':
  142. v = escape(v)
  143. yield k, v
  144. @classmethod
  145. def from_tree(cls, node):
  146. self = super().from_tree(node)
  147. self.name = unescape(self.name)
  148. return self
  149. class TableNameDescriptor(String):
  150. """
  151. Table names cannot have spaces in them
  152. """
  153. def __set__(self, instance, value):
  154. if value is not None and " " in value:
  155. raise ValueError("Table names cannot have spaces")
  156. super().__set__(instance, value)
  157. class Table(Serialisable):
  158. _path = "/tables/table{0}.xml"
  159. mime_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml"
  160. _rel_type = REL_NS + "/table"
  161. _rel_id = None
  162. tagname = "table"
  163. id = Integer()
  164. name = String(allow_none=True)
  165. displayName = TableNameDescriptor()
  166. comment = String(allow_none=True)
  167. ref = CellRange()
  168. tableType = NoneSet(values=(['worksheet', 'xml', 'queryTable']))
  169. headerRowCount = Integer(allow_none=True)
  170. insertRow = Bool(allow_none=True)
  171. insertRowShift = Bool(allow_none=True)
  172. totalsRowCount = Integer(allow_none=True)
  173. totalsRowShown = Bool(allow_none=True)
  174. published = Bool(allow_none=True)
  175. headerRowDxfId = Integer(allow_none=True)
  176. dataDxfId = Integer(allow_none=True)
  177. totalsRowDxfId = Integer(allow_none=True)
  178. headerRowBorderDxfId = Integer(allow_none=True)
  179. tableBorderDxfId = Integer(allow_none=True)
  180. totalsRowBorderDxfId = Integer(allow_none=True)
  181. headerRowCellStyle = String(allow_none=True)
  182. dataCellStyle = String(allow_none=True)
  183. totalsRowCellStyle = String(allow_none=True)
  184. connectionId = Integer(allow_none=True)
  185. autoFilter = Typed(expected_type=AutoFilter, allow_none=True)
  186. sortState = Typed(expected_type=SortState, allow_none=True)
  187. tableColumns = NestedSequence(expected_type=TableColumn, count=True)
  188. tableStyleInfo = Typed(expected_type=TableStyleInfo, allow_none=True)
  189. extLst = Typed(expected_type=ExtensionList, allow_none=True)
  190. __elements__ = ('autoFilter', 'sortState', 'tableColumns',
  191. 'tableStyleInfo')
  192. def __init__(self,
  193. id=1,
  194. displayName=None,
  195. ref=None,
  196. name=None,
  197. comment=None,
  198. tableType=None,
  199. headerRowCount=1,
  200. insertRow=None,
  201. insertRowShift=None,
  202. totalsRowCount=None,
  203. totalsRowShown=None,
  204. published=None,
  205. headerRowDxfId=None,
  206. dataDxfId=None,
  207. totalsRowDxfId=None,
  208. headerRowBorderDxfId=None,
  209. tableBorderDxfId=None,
  210. totalsRowBorderDxfId=None,
  211. headerRowCellStyle=None,
  212. dataCellStyle=None,
  213. totalsRowCellStyle=None,
  214. connectionId=None,
  215. autoFilter=None,
  216. sortState=None,
  217. tableColumns=(),
  218. tableStyleInfo=None,
  219. extLst=None,
  220. ):
  221. self.id = id
  222. self.displayName = displayName
  223. if name is None:
  224. name = displayName
  225. self.name = name
  226. self.comment = comment
  227. self.ref = ref
  228. self.tableType = tableType
  229. self.headerRowCount = headerRowCount
  230. self.insertRow = insertRow
  231. self.insertRowShift = insertRowShift
  232. self.totalsRowCount = totalsRowCount
  233. self.totalsRowShown = totalsRowShown
  234. self.published = published
  235. self.headerRowDxfId = headerRowDxfId
  236. self.dataDxfId = dataDxfId
  237. self.totalsRowDxfId = totalsRowDxfId
  238. self.headerRowBorderDxfId = headerRowBorderDxfId
  239. self.tableBorderDxfId = tableBorderDxfId
  240. self.totalsRowBorderDxfId = totalsRowBorderDxfId
  241. self.headerRowCellStyle = headerRowCellStyle
  242. self.dataCellStyle = dataCellStyle
  243. self.totalsRowCellStyle = totalsRowCellStyle
  244. self.connectionId = connectionId
  245. self.autoFilter = autoFilter
  246. self.sortState = sortState
  247. self.tableColumns = tableColumns
  248. self.tableStyleInfo = tableStyleInfo
  249. def to_tree(self):
  250. tree = super().to_tree()
  251. tree.set("xmlns", SHEET_MAIN_NS)
  252. return tree
  253. @property
  254. def path(self):
  255. """
  256. Return path within the archive
  257. """
  258. return "/xl" + self._path.format(self.id)
  259. def _write(self, archive):
  260. """
  261. Serialise to XML and write to archive
  262. """
  263. xml = self.to_tree()
  264. archive.writestr(self.path[1:], tostring(xml))
  265. def _initialise_columns(self):
  266. """
  267. Create a list of table columns from a cell range
  268. Always set a ref if we have headers (the default)
  269. Column headings must be strings and must match cells in the worksheet.
  270. """
  271. min_col, min_row, max_col, max_row = range_boundaries(self.ref)
  272. for idx in range(min_col, max_col+1):
  273. col = TableColumn(id=idx, name="Column{0}".format(idx))
  274. self.tableColumns.append(col)
  275. if self.headerRowCount and not self.autoFilter:
  276. self.autoFilter = AutoFilter(ref=self.ref)
  277. @property
  278. def column_names(self):
  279. return [column.name for column in self.tableColumns]
  280. class TablePartList(Serialisable):
  281. tagname = "tableParts"
  282. count = Integer(allow_none=True)
  283. tablePart = Sequence(expected_type=Related)
  284. __elements__ = ('tablePart',)
  285. __attrs__ = ('count',)
  286. def __init__(self,
  287. count=None,
  288. tablePart=(),
  289. ):
  290. self.tablePart = tablePart
  291. def append(self, part):
  292. self.tablePart.append(part)
  293. @property
  294. def count(self):
  295. return len(self.tablePart)
  296. def __bool__(self):
  297. return bool(self.tablePart)
  298. class TableList(dict):
  299. def add(self, table):
  300. if not isinstance(table, Table):
  301. raise TypeError("You can only add tables")
  302. self[table.name] = table
  303. def get(self, name=None, table_range=None):
  304. if name is not None:
  305. return super().get(name)
  306. for table in self.values():
  307. if table_range == table.ref:
  308. return table
  309. def items(self):
  310. return [(name, table.ref) for name, table in super().items()]