Skip to content

Instantly share code, notes, and snippets.

@elibroftw
Last active November 26, 2024 03:19
Show Gist options
  • Save elibroftw/365444deff699175add08e78559ba0bf to your computer and use it in GitHub Desktop.
Save elibroftw/365444deff699175add08e78559ba0bf to your computer and use it in GitHub Desktop.
Openpyxl Utility Functions
# LICENSE: public domain
import warnings
from openpyxl.cell import Cell
from openpyxl.worksheet.worksheet import Worksheet
from openpyxl.styles.stylesheet import Stylesheet, BUILTIN_FORMATS_MAX_SIZE, BUILTIN_FORMATS
from openpyxl.drawing.text import Font as DrawingFont
from openpyxl.descriptors import MinMax
from copy import copy
from contextlib import suppress
def monkey_patch_openpyxl():
'''
Openpyxl does not work if the worksheets it opens has broken style definitions
This code ignores those cell style informations
SEE FIX: https://foss.heptapod.net/openpyxl/openpyxl/-/merge_requests/432
https://foss.heptapod.net/openpyxl/openpyxl/-/merge_requests/432/diffs
'''
def _expand_named_style(self, named_style):
"""
Bind format definitions for a named style from the associated style
record
"""
try:
xf = self.cellStyleXfs[named_style.xfId]
except IndexError:
# ignore this issue
# try:
# warnings.warn(f'Style definitions broken in file {self.parent.file_name}', category=warnings.RuntimeWarning)
# except AttributeError:
# # in case the work book does not have the custom name attribute set
# warnings.warn(f'Style definitions broken in file', category=warnings.RuntimeWarning)
return
named_style.font = self.fonts[xf.fontId]
named_style.fill = self.fills[xf.fillId]
named_style.border = self.borders[xf.borderId]
if xf.numFmtId < BUILTIN_FORMATS_MAX_SIZE:
formats = BUILTIN_FORMATS
else:
formats = self.custom_formats
if xf.numFmtId in formats:
named_style.number_format = formats[xf.numFmtId]
if xf.alignment:
named_style.alignment = xf.alignment
if xf.protection:
named_style.protection = xf.protection
Stylesheet._expand_named_style = _expand_named_style
DrawingFont.pitchFamily = MinMax(min=0, max=82, allow_none=True)
monkey_patch_openpyxl()
def copy_sheet(source_sheet, target_sheet):
copy_cells(source_sheet, target_sheet) # copy all the cel values and styles
copy_sheet_attributes(source_sheet, target_sheet)
def copy_sheet_attributes(source_sheet, target_sheet):
target_sheet.sheet_format = copy(source_sheet.sheet_format)
target_sheet.sheet_properties = copy(source_sheet.sheet_properties)
target_sheet.merged_cells = copy(source_sheet.merged_cells)
target_sheet.page_margins = copy(source_sheet.page_margins)
target_sheet.freeze_panes = copy(source_sheet.freeze_panes)
# set row dimensions
# So you cannot copy the row_dimensions attribute. Does not work (because of meta data in the attribute I think). So we copy every row's row_dimensions. That seems to work.
for rn in range(len(source_sheet.row_dimensions)):
target_sheet.row_dimensions[rn] = copy(source_sheet.row_dimensions[rn])
if source_sheet.sheet_format.defaultColWidth is None:
print('Unable to copy default column wide')
else:
target_sheet.sheet_format.defaultColWidth = copy(source_sheet.sheet_format.defaultColWidth)
# set specific column width and hidden property
# we cannot copy the entire column_dimensions attribute so we copy selected attributes
for key, value in source_sheet.column_dimensions.items():
target_sheet.column_dimensions[key].min = copy(source_sheet.column_dimensions[key].min) # Excel actually groups multiple columns under 1 key. Use the min max attribute to also group the columns in the targetSheet
target_sheet.column_dimensions[key].max = copy(source_sheet.column_dimensions[key].max) # https://stackoverflow.com/questions/36417278/openpyxl-can-not-read-consecutive-hidden-columns discussed the issue. Note that this is also the case for the width, not onl;y the hidden property
target_sheet.column_dimensions[key].width = copy(source_sheet.column_dimensions[key].width) # set width for every column
target_sheet.column_dimensions[key].hidden = copy(source_sheet.column_dimensions[key].hidden)
def copy_cells(source_sheet, target_sheet):
for (row, col), source_cell in source_sheet._cells.items():
copy_cell(target_sheet, col, row, source_cell)
def copy_cell(target_sheet: Worksheet, col, row, source_cell: Cell, copy_conditional_formatting=False):
target_cell: Cell = target_sheet.cell(column=col, row=row)
target_cell.value = source_cell.value
target_cell.data_type = source_cell.data_type
if source_cell.has_style:
target_cell.font = copy(source_cell.font)
target_cell.border = copy(source_cell.border)
target_cell.fill = copy(source_cell.fill)
target_cell.protection = copy(source_cell.protection)
target_cell.alignment = copy(source_cell.alignment)
target_cell.number_format = copy(source_cell.number_format)
if source_cell.parent == target_sheet and copy_conditional_formatting:
pass
if copy_conditional_formatting:
for rule in target_sheet.conditional_formatting[source_cell.coordinate]:
target_sheet.conditional_formatting.add(target_cell.coordinate, copy(rule))
with suppress(AttributeError):
target_cell.hyperlink = source_cell.hyperlink
with suppress(AttributeError):
if source_cell.comment:
target_cell.comment = copy(source_cell.comment)
@elibroftw
Copy link
Author

In Fall 2023, I was working on a project that extensively used Openpyxl .
Sometimes a perfectly valid excel file isn’t accepted by Openpyxl due to some seemingly arbitrary constraint.
At other times, I need to copy some cells or move some cells.
So here are my openpyxl utilities compiled and modified from various sources online.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment