workbook.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. # Copyright (c) 2010-2024 openpyxl
  2. """Workbook is the top-level container for all document information."""
  3. from copy import copy
  4. from openpyxl.compat import deprecated
  5. from openpyxl.worksheet.worksheet import Worksheet
  6. from openpyxl.worksheet._read_only import ReadOnlyWorksheet
  7. from openpyxl.worksheet._write_only import WriteOnlyWorksheet
  8. from openpyxl.worksheet.copier import WorksheetCopy
  9. from openpyxl.utils import quote_sheetname
  10. from openpyxl.utils.indexed_list import IndexedList
  11. from openpyxl.utils.datetime import WINDOWS_EPOCH, MAC_EPOCH
  12. from openpyxl.utils.exceptions import ReadOnlyWorkbookException
  13. from openpyxl.writer.excel import save_workbook
  14. from openpyxl.styles.cell_style import StyleArray
  15. from openpyxl.styles.named_styles import NamedStyle
  16. from openpyxl.styles.differential import DifferentialStyleList
  17. from openpyxl.styles.alignment import Alignment
  18. from openpyxl.styles.borders import DEFAULT_BORDER
  19. from openpyxl.styles.fills import DEFAULT_EMPTY_FILL, DEFAULT_GRAY_FILL
  20. from openpyxl.styles.fonts import DEFAULT_FONT
  21. from openpyxl.styles.protection import Protection
  22. from openpyxl.styles.colors import COLOR_INDEX
  23. from openpyxl.styles.named_styles import NamedStyleList
  24. from openpyxl.styles.table import TableStyleList
  25. from openpyxl.chartsheet import Chartsheet
  26. from .defined_name import DefinedName, DefinedNameDict
  27. from openpyxl.packaging.core import DocumentProperties
  28. from openpyxl.packaging.custom import CustomPropertyList
  29. from openpyxl.packaging.relationship import RelationshipList
  30. from .child import _WorkbookChild
  31. from .protection import DocumentSecurity
  32. from .properties import CalcProperties
  33. from .views import BookView
  34. from openpyxl.xml.constants import (
  35. XLSM,
  36. XLSX,
  37. XLTM,
  38. XLTX
  39. )
  40. INTEGER_TYPES = (int,)
  41. class Workbook:
  42. """Workbook is the container for all other parts of the document."""
  43. _read_only = False
  44. _data_only = False
  45. template = False
  46. path = "/xl/workbook.xml"
  47. def __init__(self,
  48. write_only=False,
  49. iso_dates=False,
  50. ):
  51. self._sheets = []
  52. self._pivots = []
  53. self._active_sheet_index = 0
  54. self.defined_names = DefinedNameDict()
  55. self._external_links = []
  56. self.properties = DocumentProperties()
  57. self.custom_doc_props = CustomPropertyList()
  58. self.security = DocumentSecurity()
  59. self.__write_only = write_only
  60. self.shared_strings = IndexedList()
  61. self._setup_styles()
  62. self.loaded_theme = None
  63. self.vba_archive = None
  64. self.is_template = False
  65. self.code_name = None
  66. self.epoch = WINDOWS_EPOCH
  67. self.encoding = "utf-8"
  68. self.iso_dates = iso_dates
  69. if not self.write_only:
  70. self._sheets.append(Worksheet(self))
  71. self.rels = RelationshipList()
  72. self.calculation = CalcProperties()
  73. self.views = [BookView()]
  74. def _setup_styles(self):
  75. """Bootstrap styles"""
  76. self._fonts = IndexedList()
  77. self._fonts.add(DEFAULT_FONT)
  78. self._alignments = IndexedList([Alignment()])
  79. self._borders = IndexedList()
  80. self._borders.add(DEFAULT_BORDER)
  81. self._fills = IndexedList()
  82. self._fills.add(DEFAULT_EMPTY_FILL)
  83. self._fills.add(DEFAULT_GRAY_FILL)
  84. self._number_formats = IndexedList()
  85. self._date_formats = {}
  86. self._timedelta_formats = {}
  87. self._protections = IndexedList([Protection()])
  88. self._colors = COLOR_INDEX
  89. self._cell_styles = IndexedList([StyleArray()])
  90. self._named_styles = NamedStyleList()
  91. self.add_named_style(NamedStyle(font=copy(DEFAULT_FONT), border=copy(DEFAULT_BORDER), builtinId=0))
  92. self._table_styles = TableStyleList()
  93. self._differential_styles = DifferentialStyleList()
  94. @property
  95. def epoch(self):
  96. if self._epoch == WINDOWS_EPOCH:
  97. return WINDOWS_EPOCH
  98. return MAC_EPOCH
  99. @epoch.setter
  100. def epoch(self, value):
  101. if value not in (WINDOWS_EPOCH, MAC_EPOCH):
  102. raise ValueError("The epoch must be either 1900 or 1904")
  103. self._epoch = value
  104. @property
  105. def read_only(self):
  106. return self._read_only
  107. @property
  108. def data_only(self):
  109. return self._data_only
  110. @property
  111. def write_only(self):
  112. return self.__write_only
  113. @property
  114. def excel_base_date(self):
  115. return self.epoch
  116. @property
  117. def active(self):
  118. """Get the currently active sheet or None
  119. :type: :class:`openpyxl.worksheet.worksheet.Worksheet`
  120. """
  121. try:
  122. return self._sheets[self._active_sheet_index]
  123. except IndexError:
  124. pass
  125. @active.setter
  126. def active(self, value):
  127. """Set the active sheet"""
  128. if not isinstance(value, (_WorkbookChild, INTEGER_TYPES)):
  129. raise TypeError("Value must be either a worksheet, chartsheet or numerical index")
  130. if isinstance(value, INTEGER_TYPES):
  131. self._active_sheet_index = value
  132. return
  133. #if self._sheets and 0 <= value < len(self._sheets):
  134. #value = self._sheets[value]
  135. #else:
  136. #raise ValueError("Sheet index is outside the range of possible values", value)
  137. if value not in self._sheets:
  138. raise ValueError("Worksheet is not in the workbook")
  139. if value.sheet_state != "visible":
  140. raise ValueError("Only visible sheets can be made active")
  141. idx = self._sheets.index(value)
  142. self._active_sheet_index = idx
  143. def create_sheet(self, title=None, index=None):
  144. """Create a worksheet (at an optional index).
  145. :param title: optional title of the sheet
  146. :type title: str
  147. :param index: optional position at which the sheet will be inserted
  148. :type index: int
  149. """
  150. if self.read_only:
  151. raise ReadOnlyWorkbookException('Cannot create new sheet in a read-only workbook')
  152. if self.write_only :
  153. new_ws = WriteOnlyWorksheet(parent=self, title=title)
  154. else:
  155. new_ws = Worksheet(parent=self, title=title)
  156. self._add_sheet(sheet=new_ws, index=index)
  157. return new_ws
  158. def _add_sheet(self, sheet, index=None):
  159. """Add an worksheet (at an optional index)."""
  160. if not isinstance(sheet, (Worksheet, WriteOnlyWorksheet, Chartsheet)):
  161. raise TypeError("Cannot be added to a workbook")
  162. if sheet.parent != self:
  163. raise ValueError("You cannot add worksheets from another workbook.")
  164. if index is None:
  165. self._sheets.append(sheet)
  166. else:
  167. self._sheets.insert(index, sheet)
  168. def move_sheet(self, sheet, offset=0):
  169. """
  170. Move a sheet or sheetname
  171. """
  172. if not isinstance(sheet, Worksheet):
  173. sheet = self[sheet]
  174. idx = self._sheets.index(sheet)
  175. del self._sheets[idx]
  176. new_pos = idx + offset
  177. self._sheets.insert(new_pos, sheet)
  178. def remove(self, worksheet):
  179. """Remove `worksheet` from this workbook."""
  180. idx = self._sheets.index(worksheet)
  181. self._sheets.remove(worksheet)
  182. @deprecated("Use wb.remove(worksheet) or del wb[sheetname]")
  183. def remove_sheet(self, worksheet):
  184. """Remove `worksheet` from this workbook."""
  185. self.remove(worksheet)
  186. def create_chartsheet(self, title=None, index=None):
  187. if self.read_only:
  188. raise ReadOnlyWorkbookException("Cannot create new sheet in a read-only workbook")
  189. cs = Chartsheet(parent=self, title=title)
  190. self._add_sheet(cs, index)
  191. return cs
  192. @deprecated("Use wb[sheetname]")
  193. def get_sheet_by_name(self, name):
  194. """Returns a worksheet by its name.
  195. :param name: the name of the worksheet to look for
  196. :type name: string
  197. """
  198. return self[name]
  199. def __contains__(self, key):
  200. return key in self.sheetnames
  201. def index(self, worksheet):
  202. """Return the index of a worksheet."""
  203. return self.worksheets.index(worksheet)
  204. @deprecated("Use wb.index(worksheet)")
  205. def get_index(self, worksheet):
  206. """Return the index of the worksheet."""
  207. return self.index(worksheet)
  208. def __getitem__(self, key):
  209. """Returns a worksheet by its name.
  210. :param name: the name of the worksheet to look for
  211. :type name: string
  212. """
  213. for sheet in self.worksheets + self.chartsheets:
  214. if sheet.title == key:
  215. return sheet
  216. raise KeyError("Worksheet {0} does not exist.".format(key))
  217. def __delitem__(self, key):
  218. sheet = self[key]
  219. self.remove(sheet)
  220. def __iter__(self):
  221. return iter(self.worksheets)
  222. @deprecated("Use wb.sheetnames")
  223. def get_sheet_names(self):
  224. return self.sheetnames
  225. @property
  226. def worksheets(self):
  227. """A list of sheets in this workbook
  228. :type: list of :class:`openpyxl.worksheet.worksheet.Worksheet`
  229. """
  230. return [s for s in self._sheets if isinstance(s, (Worksheet, ReadOnlyWorksheet, WriteOnlyWorksheet))]
  231. @property
  232. def chartsheets(self):
  233. """A list of Chartsheets in this workbook
  234. :type: list of :class:`openpyxl.chartsheet.chartsheet.Chartsheet`
  235. """
  236. return [s for s in self._sheets if isinstance(s, Chartsheet)]
  237. @property
  238. def sheetnames(self):
  239. """Returns the list of the names of worksheets in this workbook.
  240. Names are returned in the worksheets order.
  241. :type: list of strings
  242. """
  243. return [s.title for s in self._sheets]
  244. @deprecated("Assign scoped named ranges directly to worksheets or global ones to the workbook. Deprecated in 3.1")
  245. def create_named_range(self, name, worksheet=None, value=None, scope=None):
  246. """Create a new named_range on a worksheet
  247. """
  248. defn = DefinedName(name=name)
  249. if worksheet is not None:
  250. defn.value = "{0}!{1}".format(quote_sheetname(worksheet.title), value)
  251. else:
  252. defn.value = value
  253. self.defined_names[name] = defn
  254. def add_named_style(self, style):
  255. """
  256. Add a named style
  257. """
  258. self._named_styles.append(style)
  259. style.bind(self)
  260. @property
  261. def named_styles(self):
  262. """
  263. List available named styles
  264. """
  265. return self._named_styles.names
  266. @property
  267. def mime_type(self):
  268. """
  269. The mime type is determined by whether a workbook is a template or
  270. not and whether it contains macros or not. Excel requires the file
  271. extension to match but openpyxl does not enforce this.
  272. """
  273. ct = self.template and XLTX or XLSX
  274. if self.vba_archive:
  275. ct = self.template and XLTM or XLSM
  276. return ct
  277. def save(self, filename):
  278. """Save the current workbook under the given `filename`.
  279. Use this function instead of using an `ExcelWriter`.
  280. .. warning::
  281. When creating your workbook using `write_only` set to True,
  282. you will only be able to call this function once. Subsequent attempts to
  283. modify or save the file will raise an :class:`openpyxl.shared.exc.WorkbookAlreadySaved` exception.
  284. """
  285. if self.read_only:
  286. raise TypeError("""Workbook is read-only""")
  287. if self.write_only and not self.worksheets:
  288. self.create_sheet()
  289. save_workbook(self, filename)
  290. @property
  291. def style_names(self):
  292. """
  293. List of named styles
  294. """
  295. return [s.name for s in self._named_styles]
  296. def copy_worksheet(self, from_worksheet):
  297. """Copy an existing worksheet in the current workbook
  298. .. warning::
  299. This function cannot copy worksheets between workbooks.
  300. worksheets can only be copied within the workbook that they belong
  301. :param from_worksheet: the worksheet to be copied from
  302. :return: copy of the initial worksheet
  303. """
  304. if self.__write_only or self._read_only:
  305. raise ValueError("Cannot copy worksheets in read-only or write-only mode")
  306. new_title = u"{0} Copy".format(from_worksheet.title)
  307. to_worksheet = self.create_sheet(title=new_title)
  308. cp = WorksheetCopy(source_worksheet=from_worksheet, target_worksheet=to_worksheet)
  309. cp.copy_worksheet()
  310. return to_worksheet
  311. def close(self):
  312. """
  313. Close workbook file if open. Only affects read-only and write-only modes.
  314. """
  315. if hasattr(self, '_archive'):
  316. self._archive.close()
  317. def _duplicate_name(self, name):
  318. """
  319. Check for duplicate name in defined name list and table list of each worksheet.
  320. Names are not case sensitive.
  321. """
  322. name = name.lower()
  323. for sheet in self.worksheets:
  324. for t in sheet.tables:
  325. if name == t.lower():
  326. return True
  327. if name in self.defined_names:
  328. return True