import datetime
import os
import re
import zipfile
import openpyxl
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
NORMAL_DIR = os.path.join(ROOT, "tests", "fixtures", "normal")
COMPLEX_DIR = os.path.join(ROOT, "tests", "fixtures", "complex")
ERROR_DIR = os.path.join(ROOT, "tests", "fixtures", "error")
LOAD_DIR = os.path.join(ROOT, "tests", "fixtures", "load")
def _read_zip_entries(path):
with zipfile.ZipFile(path) as z:
return {name: z.read(name) for name in z.namelist()}
def _write_zip_entries(path, entries):
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as z:
for name, data in entries.items():
z.writestr(name, data)
def _mutate_zip_entries(path, mutate_fn):
entries = _read_zip_entries(path)
mutate_fn(entries)
_write_zip_entries(path, entries)
def _add_shared_strings_part(entries, strings):
sst_xml = (
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
'<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" '
f'count="{len(strings)}" uniqueCount="{len(strings)}">'
+ "".join(f"<si><t>{s}</t></si>" for s in strings)
+ "</sst>"
)
entries["xl/sharedStrings.xml"] = sst_xml.encode("utf-8")
rels_xml = entries["xl/_rels/workbook.xml.rels"].decode("utf-8")
rels_xml = rels_xml.replace(
"</Relationships>",
'<Relationship Id="rIdSharedStrings" '
'Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" '
'Target="sharedStrings.xml"/></Relationships>',
)
entries["xl/_rels/workbook.xml.rels"] = rels_xml.encode("utf-8")
def basic_types():
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Sheet1"
ws["A1"] = "日本語Text"
ws["B1"] = 42
ws["C1"] = 19.99
ws["D1"] = datetime.date(2023, 6, 15)
ws["D1"].number_format = "yyyy-mm-dd"
ws["E1"] = True
ws["F1"] = False
ws["G1"] = "#N/A"
ws["G1"].data_type = "e"
path = os.path.join(NORMAL_DIR, "basic_types.xlsx")
wb.save(path)
return path
def houganshi_merged():
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Sheet1"
ws["A1"] = "houganshi"
ws.merge_cells("A1:C3")
path = os.path.join(COMPLEX_DIR, "houganshi_merged.xlsx")
wb.save(path)
return path
def multi_sheet_states():
wb = openpyxl.Workbook()
visible = wb.active
visible.title = "Visible"
visible["A1"] = 1
hidden = wb.create_sheet("Hidden")
hidden["A1"] = 1
hidden.sheet_state = "hidden"
very_hidden = wb.create_sheet("VeryHidden")
very_hidden["A1"] = 1
very_hidden.sheet_state = "veryHidden"
wb.create_sheet("Empty")
path = os.path.join(COMPLEX_DIR, "multi_sheet_states.xlsx")
wb.save(path)
return path
def extreme_sparse():
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Sheet1"
ws.cell(row=1, column=1, value=1)
ws.cell(row=1_048_576, column=16_384, value=2)
path = os.path.join(COMPLEX_DIR, "extreme_sparse.xlsx")
wb.save(path)
return path
def corrupted_xml():
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Sheet1"
ws["A1"] = "will be truncated"
ws["B1"] = 123
path = os.path.join(ERROR_DIR, "corrupted_xml.xlsx")
wb.save(path)
def mutate(entries):
sheet_xml = entries["xl/worksheets/sheet1.xml"]
entries["xl/worksheets/sheet1.xml"] = sheet_xml[: len(sheet_xml) * 2 // 3]
_mutate_zip_entries(path, mutate)
return path
def missing_relations():
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Sheet1"
ws["A1"] = 1
path = os.path.join(ERROR_DIR, "missing_relations.xlsx")
wb.save(path)
def mutate(entries):
rels_xml = entries["xl/_rels/workbook.xml.rels"].decode("utf-8")
broken = rels_xml.replace('Id="rId1"', 'Id="rIdRenumbered"', 1)
assert broken != rels_xml, "expected to find Id=\"rId1\" to break"
entries["xl/_rels/workbook.xml.rels"] = broken.encode("utf-8")
_mutate_zip_entries(path, mutate)
return path
def invalid_merge_ref():
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Sheet1"
ws["A1"] = "merged"
ws.merge_cells("A1:C3")
path = os.path.join(ERROR_DIR, "invalid_merge_ref.xlsx")
wb.save(path)
def mutate(entries):
sheet_xml = entries["xl/worksheets/sheet1.xml"].decode("utf-8")
corrupted, count = re.subn(
r'(<mergeCell ref=")A1:C3(")', r"\1C3:A1\2", sheet_xml
)
assert count == 1, "expected exactly one <mergeCell ref=\"A1:C3\"/>"
entries["xl/worksheets/sheet1.xml"] = corrupted.encode("utf-8")
_mutate_zip_entries(path, mutate)
return path
def out_of_bounds_sst():
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Sheet1"
ws["A1"] = "placeholder"
path = os.path.join(ERROR_DIR, "out_of_bounds_sst.xlsx")
wb.save(path)
def mutate(entries):
_add_shared_strings_part(entries, ["only one entry"])
sheet_xml = entries["xl/worksheets/sheet1.xml"].decode("utf-8")
rewritten, count = re.subn(
r'<c r="A1"[^>]*>.*?</c>',
'<c r="A1" t="s"><v>99999</v></c>',
sheet_xml,
count=1,
flags=re.DOTALL,
)
assert count == 1, "expected exactly one <c r=\"A1\"> to rewrite"
entries["xl/worksheets/sheet1.xml"] = rewritten.encode("utf-8")
_mutate_zip_entries(path, mutate)
return path
def massive_dense_accounting():
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Ledger"
for row in range(1, 10_001):
for col in range(1, 31):
ws.cell(row=row, column=col, value=row * 100 + col)
path = os.path.join(LOAD_DIR, "massive_dense_accounting.xlsx")
wb.save(path)
return path
def thousand_sheets():
wb = openpyxl.Workbook()
wb.remove(wb.active)
for i in range(1, 1001):
ws = wb.create_sheet(f"Sheet{i}")
ws["A1"] = i
path = os.path.join(LOAD_DIR, "thousand_sheets.xlsx")
wb.save(path)
return path
def massive_sst():
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Sheet1"
ws["A1"] = "placeholder-a"
ws["B1"] = "placeholder-b"
ws["C1"] = "placeholder-c"
path = os.path.join(LOAD_DIR, "massive_sst.xlsx")
wb.save(path)
string_count = 50_000
def mutate(entries):
strings = [f"unique-string-{i}" for i in range(string_count)]
_add_shared_strings_part(entries, strings)
sheet_xml = entries["xl/worksheets/sheet1.xml"].decode("utf-8")
for ref, index in (
("A1", 0),
("B1", string_count // 2),
("C1", string_count - 1),
):
sheet_xml, count = re.subn(
rf'<c r="{ref}"[^>]*>.*?</c>',
f'<c r="{ref}" t="s"><v>{index}</v></c>',
sheet_xml,
count=1,
flags=re.DOTALL,
)
assert count == 1, f"expected exactly one <c r=\"{ref}\"> to rewrite"
entries["xl/worksheets/sheet1.xml"] = sheet_xml.encode("utf-8")
_mutate_zip_entries(path, mutate)
return path
def main():
for directory in (NORMAL_DIR, COMPLEX_DIR, ERROR_DIR, LOAD_DIR):
os.makedirs(directory, exist_ok=True)
for fn in (
basic_types,
houganshi_merged,
multi_sheet_states,
extreme_sparse,
corrupted_xml,
missing_relations,
invalid_merge_ref,
out_of_bounds_sst,
massive_dense_accounting,
thousand_sheets,
massive_sst,
):
path = fn()
print(f"wrote {os.path.relpath(path, ROOT)}")
if __name__ == "__main__":
main()