Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# Copyright (c) 2010-2020 openpyxl 

2 

3"""Workbook is the top-level container for all document information.""" 

4from copy import copy 

5 

6from openpyxl.compat import deprecated 

7from openpyxl.worksheet.worksheet import Worksheet 

8from openpyxl.worksheet._read_only import ReadOnlyWorksheet 

9from openpyxl.worksheet._write_only import WriteOnlyWorksheet 

10from openpyxl.worksheet.copier import WorksheetCopy 

11 

12from openpyxl.utils import quote_sheetname 

13from openpyxl.utils.indexed_list import IndexedList 

14from openpyxl.utils.datetime import CALENDAR_WINDOWS_1900 

15from openpyxl.utils.exceptions import ReadOnlyWorkbookException 

16 

17from openpyxl.writer.excel import save_workbook 

18 

19from openpyxl.styles.cell_style import StyleArray 

20from openpyxl.styles.named_styles import NamedStyle 

21from openpyxl.styles.differential import DifferentialStyleList 

22from openpyxl.styles.alignment import Alignment 

23from openpyxl.styles.borders import DEFAULT_BORDER 

24from openpyxl.styles.fills import DEFAULT_EMPTY_FILL, DEFAULT_GRAY_FILL 

25from openpyxl.styles.fonts import DEFAULT_FONT 

26from openpyxl.styles.protection import Protection 

27from openpyxl.styles.colors import COLOR_INDEX 

28from openpyxl.styles.named_styles import NamedStyleList 

29from openpyxl.styles.table import TableStyleList 

30 

31from openpyxl.chartsheet import Chartsheet 

32from .defined_name import DefinedName, DefinedNameList 

33from openpyxl.packaging.core import DocumentProperties 

34from openpyxl.packaging.relationship import RelationshipList 

35from .child import _WorkbookChild 

36from .protection import DocumentSecurity 

37from .properties import CalcProperties 

38from .views import BookView 

39 

40 

41from openpyxl.xml.constants import ( 

42 XLSM, 

43 XLSX, 

44 XLTM, 

45 XLTX 

46) 

47 

48INTEGER_TYPES = (int,) 

49 

50class Workbook(object): 

51 """Workbook is the container for all other parts of the document.""" 

52 

53 _read_only = False 

54 _data_only = False 

55 template = False 

56 path = "/xl/workbook.xml" 

57 

58 def __init__(self, 

59 write_only=False, 

60 iso_dates=False, 

61 ): 

62 self._sheets = [] 

63 self._pivots = [] 

64 self._active_sheet_index = 0 

65 self.defined_names = DefinedNameList() 

66 self._external_links = [] 

67 self.properties = DocumentProperties() 

68 self.security = DocumentSecurity() 

69 self.__write_only = write_only 

70 self.shared_strings = IndexedList() 

71 

72 self._setup_styles() 

73 

74 self.loaded_theme = None 

75 self.vba_archive = None 

76 self.is_template = False 

77 self.code_name = None 

78 self.epoch = CALENDAR_WINDOWS_1900 

79 self.encoding = "utf-8" 

80 self.iso_dates = iso_dates 

81 

82 if not self.write_only: 

83 self._sheets.append(Worksheet(self)) 

84 

85 self.rels = RelationshipList() 

86 self.calculation = CalcProperties() 

87 self.views = [BookView()] 

88 

89 

90 def _setup_styles(self): 

91 """Bootstrap styles""" 

92 

93 self._fonts = IndexedList() 

94 self._fonts.add(DEFAULT_FONT) 

95 

96 self._alignments = IndexedList([Alignment()]) 

97 

98 self._borders = IndexedList() 

99 self._borders.add(DEFAULT_BORDER) 

100 

101 self._fills = IndexedList() 

102 self._fills.add(DEFAULT_EMPTY_FILL) 

103 self._fills.add(DEFAULT_GRAY_FILL) 

104 

105 self._number_formats = IndexedList() 

106 self._date_formats = {} 

107 

108 self._protections = IndexedList([Protection()]) 

109 

110 self._colors = COLOR_INDEX 

111 self._cell_styles = IndexedList([StyleArray()]) 

112 self._named_styles = NamedStyleList() 

113 self.add_named_style(NamedStyle(font=copy(DEFAULT_FONT), builtinId=0)) 

114 self._table_styles = TableStyleList() 

115 self._differential_styles = DifferentialStyleList() 

116 

117 

118 @property 

119 def read_only(self): 

120 return self._read_only 

121 

122 @property 

123 def data_only(self): 

124 return self._data_only 

125 

126 @property 

127 def write_only(self): 

128 return self.__write_only 

129 

130 

131 @property 

132 def excel_base_date(self): 

133 return self.epoch 

134 

135 @property 

136 def active(self): 

137 """Get the currently active sheet or None 

138 

139 :type: :class:`openpyxl.worksheet.worksheet.Worksheet` 

140 """ 

141 try: 

142 return self._sheets[self._active_sheet_index] 

143 except IndexError: 

144 pass 

145 

146 @active.setter 

147 def active(self, value): 

148 """Set the active sheet""" 

149 if not isinstance(value, (_WorkbookChild, INTEGER_TYPES)): 

150 raise TypeError("Value must be either a worksheet, chartsheet or numerical index") 

151 if isinstance(value, INTEGER_TYPES): 

152 self._active_sheet_index = value 

153 return 

154 #if self._sheets and 0 <= value < len(self._sheets): 

155 #value = self._sheets[value] 

156 #else: 

157 #raise ValueError("Sheet index is outside the range of possible values", value) 

158 if value not in self._sheets: 

159 raise ValueError("Worksheet is not in the workbook") 

160 if value.sheet_state != "visible": 

161 raise ValueError("Only visible sheets can be made active") 

162 

163 idx = self._sheets.index(value) 

164 self._active_sheet_index = idx 

165 

166 

167 def create_sheet(self, title=None, index=None): 

168 """Create a worksheet (at an optional index). 

169 

170 :param title: optional title of the sheet 

171 :type title: str 

172 :param index: optional position at which the sheet will be inserted 

173 :type index: int 

174 

175 """ 

176 if self.read_only: 

177 raise ReadOnlyWorkbookException('Cannot create new sheet in a read-only workbook') 

178 

179 if self.write_only : 

180 new_ws = WriteOnlyWorksheet(parent=self, title=title) 

181 else: 

182 new_ws = Worksheet(parent=self, title=title) 

183 

184 self._add_sheet(sheet=new_ws, index=index) 

185 return new_ws 

186 

187 

188 def _add_sheet(self, sheet, index=None): 

189 """Add an worksheet (at an optional index).""" 

190 

191 if not isinstance(sheet, (Worksheet, WriteOnlyWorksheet, Chartsheet)): 

192 raise TypeError("Cannot be added to a workbook") 

193 

194 if sheet.parent != self: 

195 raise ValueError("You cannot add worksheets from another workbook.") 

196 

197 if index is None: 

198 self._sheets.append(sheet) 

199 else: 

200 self._sheets.insert(index, sheet) 

201 

202 

203 def move_sheet(self, sheet, offset=0): 

204 """ 

205 Move a sheet or sheetname 

206 """ 

207 if not isinstance(sheet, Worksheet): 

208 sheet = self[sheet] 

209 idx = self._sheets.index(sheet) 

210 del self._sheets[idx] 

211 new_pos = idx + offset 

212 self._sheets.insert(new_pos, sheet) 

213 

214 

215 def remove(self, worksheet): 

216 """Remove `worksheet` from this workbook.""" 

217 idx = self._sheets.index(worksheet) 

218 localnames = self.defined_names.localnames(scope=idx) 

219 for name in localnames: 

220 self.defined_names.delete(name, scope=idx) 

221 self._sheets.remove(worksheet) 

222 

223 

224 @deprecated("Use wb.remove(worksheet) or del wb[sheetname]") 

225 def remove_sheet(self, worksheet): 

226 """Remove `worksheet` from this workbook.""" 

227 self.remove(worksheet) 

228 

229 

230 def create_chartsheet(self, title=None, index=None): 

231 if self.read_only: 

232 raise ReadOnlyWorkbookException("Cannot create new sheet in a read-only workbook") 

233 cs = Chartsheet(parent=self, title=title) 

234 

235 self._add_sheet(cs, index) 

236 return cs 

237 

238 

239 @deprecated("Use wb[sheetname]") 

240 def get_sheet_by_name(self, name): 

241 """Returns a worksheet by its name. 

242 

243 :param name: the name of the worksheet to look for 

244 :type name: string 

245 

246 """ 

247 return self[name] 

248 

249 def __contains__(self, key): 

250 return key in self.sheetnames 

251 

252 

253 def index(self, worksheet): 

254 """Return the index of a worksheet.""" 

255 return self.worksheets.index(worksheet) 

256 

257 

258 @deprecated("Use wb.index(worksheet)") 

259 def get_index(self, worksheet): 

260 """Return the index of the worksheet.""" 

261 return self.index(worksheet) 

262 

263 def __getitem__(self, key): 

264 """Returns a worksheet by its name. 

265 

266 :param name: the name of the worksheet to look for 

267 :type name: string 

268 

269 """ 

270 for sheet in self.worksheets + self.chartsheets: 

271 if sheet.title == key: 

272 return sheet 

273 raise KeyError("Worksheet {0} does not exist.".format(key)) 

274 

275 def __delitem__(self, key): 

276 sheet = self[key] 

277 self.remove(sheet) 

278 

279 def __iter__(self): 

280 return iter(self.worksheets) 

281 

282 

283 @deprecated("Use wb.sheetnames") 

284 def get_sheet_names(self): 

285 return self.sheetnames 

286 

287 @property 

288 def worksheets(self): 

289 """A list of sheets in this workbook 

290 

291 :type: list of :class:`openpyxl.worksheet.worksheet.Worksheet` 

292 """ 

293 return [s for s in self._sheets if isinstance(s, (Worksheet, ReadOnlyWorksheet, WriteOnlyWorksheet))] 

294 

295 @property 

296 def chartsheets(self): 

297 """A list of Chartsheets in this workbook 

298 

299 :type: list of :class:`openpyxl.chartsheet.chartsheet.Chartsheet` 

300 """ 

301 return [s for s in self._sheets if isinstance(s, Chartsheet)] 

302 

303 @property 

304 def sheetnames(self): 

305 """Returns the list of the names of worksheets in this workbook. 

306 

307 Names are returned in the worksheets order. 

308 

309 :type: list of strings 

310 

311 """ 

312 return [s.title for s in self._sheets] 

313 

314 def create_named_range(self, name, worksheet=None, value=None, scope=None): 

315 """Create a new named_range on a worksheet""" 

316 defn = DefinedName(name=name, localSheetId=scope) 

317 if worksheet is not None: 

318 defn.value = "{0}!{1}".format(quote_sheetname(worksheet.title), value) 

319 else: 

320 defn.value = value 

321 

322 self.defined_names.append(defn) 

323 

324 

325 def add_named_style(self, style): 

326 """ 

327 Add a named style 

328 """ 

329 self._named_styles.append(style) 

330 style.bind(self) 

331 

332 

333 @property 

334 def named_styles(self): 

335 """ 

336 List available named styles 

337 """ 

338 return self._named_styles.names 

339 

340 

341 @deprecated("Use workbook.defined_names.definedName") 

342 def get_named_ranges(self): 

343 """Return all named ranges""" 

344 return self.defined_names.definedName 

345 

346 

347 @deprecated("Use workbook.defined_names.append") 

348 def add_named_range(self, named_range): 

349 """Add an existing named_range to the list of named_ranges.""" 

350 self.defined_names.append(named_range) 

351 

352 

353 @deprecated("Use workbook.defined_names[name]") 

354 def get_named_range(self, name): 

355 """Return the range specified by name.""" 

356 return self.defined_names[name] 

357 

358 

359 @deprecated("Use del workbook.defined_names[name]") 

360 def remove_named_range(self, named_range): 

361 """Remove a named_range from this workbook.""" 

362 del self.defined_names[named_range] 

363 

364 

365 @property 

366 def mime_type(self): 

367 """ 

368 The mime type is determined by whether a workbook is a template or 

369 not and whether it contains macros or not. Excel requires the file 

370 extension to match but openpyxl does not enforce this. 

371 

372 """ 

373 ct = self.template and XLTX or XLSX 

374 if self.vba_archive: 

375 ct = self.template and XLTM or XLSM 

376 return ct 

377 

378 

379 def save(self, filename): 

380 """Save the current workbook under the given `filename`. 

381 Use this function instead of using an `ExcelWriter`. 

382 

383 .. warning:: 

384 When creating your workbook using `write_only` set to True, 

385 you will only be able to call this function once. Subsequents attempts to 

386 modify or save the file will raise an :class:`openpyxl.shared.exc.WorkbookAlreadySaved` exception. 

387 """ 

388 if self.read_only: 

389 raise TypeError("""Workbook is read-only""") 

390 if self.write_only and not self.worksheets: 

391 self.create_sheet() 

392 save_workbook(self, filename) 

393 

394 

395 @property 

396 def style_names(self): 

397 """ 

398 List of named styles 

399 """ 

400 return [s.name for s in self._named_styles] 

401 

402 

403 def copy_worksheet(self, from_worksheet): 

404 """Copy an existing worksheet in the current workbook 

405 

406 .. warning:: 

407 This function cannot copy worksheets between workbooks. 

408 worksheets can only be copied within the workbook that they belong 

409 

410 :param from_worksheet: the worksheet to be copied from 

411 :return: copy of the initial worksheet 

412 """ 

413 if self.__write_only or self._read_only: 

414 raise ValueError("Cannot copy worksheets in read-only or write-only mode") 

415 

416 new_title = u"{0} Copy".format(from_worksheet.title) 

417 to_worksheet = self.create_sheet(title=new_title) 

418 cp = WorksheetCopy(source_worksheet=from_worksheet, target_worksheet=to_worksheet) 

419 cp.copy_worksheet() 

420 return to_worksheet 

421 

422 

423 def close(self): 

424 """ 

425 Close workbook file if open. Only affects read-only and write-only modes. 

426 """ 

427 if hasattr(self, '_archive'): 

428 self._archive.close() 

429 

430 

431 def _duplicate_name(self, name): 

432 """ 

433 Check for duplicate name in defined name list and table list of each worksheet. 

434 Names are not case sensitive. 

435 """ 

436 lwr = name.lower() 

437 for sheet in self._sheets: 

438 tables = [key.lower() for key in sheet._tables.keys()] 

439 if lwr in tables: 

440 return True 

441 

442 if lwr in [dfn.name.lower() for dfn in self.defined_names.definedName]: 

443 return True 

444 return False