Last active
November 26, 2024 03:19
-
-
Save elibroftw/365444deff699175add08e78559ba0bf to your computer and use it in GitHub Desktop.
Openpyxl Utility Functions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.