From a55dc7594aafc831ab1e725c9f129e67a16bd9a5 Mon Sep 17 00:00:00 2001 From: xuri Date: Mon, 9 Dec 2024 09:53:30 +0800 Subject: [PATCH] Python version 3.10 or later required - Add 386 arch support - Update GitHub Action config - New functions auto_filter, calc_cell_value and set_cell_formula has been added - Improve compatible with Python 3.9 - Update unit tests and docs of the function - Add pull request template --- .github/workflows/build.yml | 98 +++++++++++++++++++++++++- PULL_REQUEST_TEMPLATE.md | 45 ++++++++++++ README.md | 2 +- excelize.py | 134 ++++++++++++++++++++++++++++++++++-- main.go | 97 +++++++++++++++++++++++--- setup.py | 9 ++- test_excelize.py | 48 ++++++++++++- types_c.h | 15 +++- types_go.py | 16 ++++- types_py.py | 8 ++- 10 files changed, 443 insertions(+), 29 deletions(-) create mode 100644 PULL_REQUEST_TEMPLATE.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 529c834..2ada56e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,8 +7,8 @@ jobs: strategy: matrix: go-version: [1.23.x] - os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [3.13] + os: [ubuntu-24.04, macos-latest, windows-latest] + python-version: ["3.10", "3.11", "3.12", "3.13"] targetplatform: [x64] runs-on: ${{ matrix.os }} @@ -56,9 +56,101 @@ jobs: run: go build -buildmode=c-shared -o libexcelize.arm64.darwin.dylib main.go && coverage run -m unittest - name: Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: flags: unittests name: codecov-umbrella + + build: + runs-on: macos-latest + needs: [test] + if: github.event_name == 'release' && github.event.action == 'published' + + steps: + + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: 1.23.x + cache: false + + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get dependencies + run: | + env GO111MODULE=on go vet ./... + pip install coverage + + - name: Build Shared Library + env: + CGO_ENABLED: 1 + run: | + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + brew tap messense/macos-cross-toolchains + brew install FiloSottile/musl-cross/musl-cross i686-unknown-linux-gnu mingw-w64 + wget https://github.com/mstorsjo/llvm-mingw/releases/download/20241203/llvm-mingw-20241203-ucrt-macos-universal.tar.xz + tar -xzf llvm-mingw-20241203-ucrt-macos-universal.tar.xz + export PATH="$(pwd)/llvm-mingw-20241203-ucrt-macos-universal/bin:$PATH" + CC=i686-linux-gnu-gcc GOOS=linux GOARCH=386 go build -ldflags "-s -w" -buildmode=c-shared -o libexcelize.386.linux.so main.go + CC=x86_64-linux-musl-gcc GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -buildmode=c-shared -o libexcelize.amd64.linux.so main.go + CC=aarch64-linux-musl-gcc GOOS=linux GOARCH=arm64 go build -ldflags "-s -w" -buildmode=c-shared -o libexcelize.arm64.linux.so main.go + CC=x86_64-w64-mingw32-gcc GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -buildmode=c-shared -o libexcelize.amd64.windows.dll main.go + CC=i686-w64-mingw32-gcc GOOS=windows GOARCH=386 go build -ldflags "-s -w" -buildmode=c-shared -o libexcelize.386.windows.dll main.go + CC=aarch64-w64-mingw32-gcc GOOS=windows GOARCH=arm64 go build -ldflags "-s -w" -buildmode=c-shared -o libexcelize.arm64.windows.dll main.go + CC=gcc GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w" -buildmode=c-shared -o libexcelize.arm64.darwin.dylib main.go + CC=gcc GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w" -buildmode=c-shared -o libexcelize.amd64.darwin.dylib main.go + rm -f libexcelize.*.h + + - uses: actions/upload-artifact@v4 + with: + name: libexcelize + path: | + libexcelize.386.linux.so + libexcelize.amd64.linux.so + libexcelize.arm64.linux.so + libexcelize.amd64.windows.dll + libexcelize.386.windows.dll + libexcelize.arm64.windows.dll + libexcelize.arm64.darwin.dylib + libexcelize.amd64.darwin.dylib + + publish: + runs-on: ubuntu-latest + needs: [build] + environment: + name: pypi + url: https://pypi.org/p/excelize + permissions: + id-token: write + + if: github.event_name == 'release' && github.event.action == 'published' + steps: + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download Artifacts + uses: actions/download-artifact@v4 + with: + merge-multiple: true + path: ./ + + - name: Build Python Package + run: | + pip install build setuptools wheel + python -m build + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..d2ac755 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,45 @@ +# PR Details + + + +## Description + + + +## Related Issue + + + + + + +## Motivation and Context + + + +## How Has This Been Tested + + + + + +## Types of changes + + + +- [ ] Docs change / refactoring / dependency upgrade +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) + +## Checklist + + + + +- [ ] My code follows the code style of this project. +- [ ] My change requires a change to the documentation. +- [ ] I have updated the documentation accordingly. +- [ ] I have read the **CONTRIBUTING** document. +- [ ] I have added tests to cover my changes. +- [ ] All new and existing tests passed. diff --git a/README.md b/README.md index 92c7efc..658d913 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Donate

-Package excelize-py is a Python port of Go [Excelize](https://github.com/xuri/excelize) library, providing a set of functions that allow you to write and read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. Supports complex components by high compatibility, and provided streaming API for generating or reading data from a worksheet with huge amounts of data. This library needs Python version 3.9 or later. The full API docs can be found at [docs reference](https://xuri.me/excelize/). +Package excelize-py is a Python port of Go [Excelize](https://github.com/xuri/excelize) library, providing a set of functions that allow you to write and read from XLAM / XLSM / XLSX / XLTM / XLTX files. Supports reading and writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. Supports complex components by high compatibility, and provided streaming API for generating or reading data from a worksheet with huge amounts of data. This library needs Python version 3.10 or later. The full API docs can be found at [docs reference](https://xuri.me/excelize/). ## Platform Compatibility diff --git a/excelize.py b/excelize.py index 1480341..2d3fbf1 100644 --- a/excelize.py +++ b/excelize.py @@ -6,7 +6,7 @@ files. Supports reading and writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. Supports complex components by high compatibility, and provided streaming API for generating or reading data from a worksheet with huge -amounts of data. This library needs Python version 3.9 or later. +amounts of data. This library needs Python version 3.10 or later. """ from dataclasses import fields @@ -45,6 +45,10 @@ def load_lib(): "amd64": "amd64", "aarch64": "arm64", }, + "32bit": { + "i386": "386", + "i686": "386", + }, }, "darwin": { "64bit": { @@ -59,6 +63,10 @@ def load_lib(): "amd64": "amd64", "arm64": "arm64", }, + "32bit": { + "i386": "386", + "i686": "386", + }, }, } if system in ext_map and arch in arch_map.get(system, {}): @@ -434,11 +442,13 @@ def save(self, *opts: Options) -> Exception | None: otherwise returns an Exception with the message. """ err, lib.Save.restype = None, c_char_p - if len(opts) > 0: - options = py_value_to_c(opts[0], types_go._Options()) - err = lib.Save(self.file_index, byref(options)).decode(ENCODE) - return None if err == "" else err - err = lib.Save(self.file_index, POINTER(types_go._Options)()).decode(ENCODE) + options = POINTER(types_go._Options)() + options = ( + byref(py_value_to_c(opts[0], types_go._Options())) + if opts + else POINTER(types_go._Options)() + ) + err = lib.Save(self.file_index, options).decode(ENCODE) return None if err == "" else Exception(err) def save_as(self, filename: str, *opts: Options) -> Exception | None: @@ -866,6 +876,78 @@ def add_table(self, sheet: str, table: Table) -> Exception | None: ).decode(ENCODE) return None if err == "" else Exception(err) + def auto_filter( + self, + sheet: str, + range_ref: str, + opts: list[AutoFilterOptions], + ) -> Exception | None: + """ + Add auto filter in a worksheet by given worksheet name, range reference + and settings. An auto filter in Excel is a way of filtering a 2D range + of data based on some simple criteria. + + Column defines the filter columns in an auto filter range based on simple + criteria + + It isn't sufficient to just specify the filter condition. You must also + hide any rows that don't match the filter condition. Rows are hidden using + the SetRowVisible function. Excelize can't filter rows automatically since + this isn't part of the file format. + + Args: + sheet (str): The worksheet name + range_ref (str): The top-left and right-bottom cell range reference + opts (list[AutoFilterOptions]): The auto filter options + + Returns: + Exception | None: Returns None if no error occurred, + otherwise returns an Exception with the message. + """ + lib.AutoFilter.restype = c_char_p + options = (types_go._AutoFilterOptions * len(opts))() + for i, opt in enumerate(opts): + options[i] = py_value_to_c(opt, types_go._AutoFilterOptions()) + err = lib.AutoFilter( + self.file_index, + sheet.encode(ENCODE), + range_ref.encode(ENCODE), + byref(options), + len(options), + ).decode(ENCODE) + return None if err == "" else Exception(err) + + def calc_cell_value( + self, sheet: str, cell: str, *opts: Options + ) -> Tuple[str, Exception | None]: + """ + Get calculated cell value. This feature is currently in working + processing. Iterative calculation, implicit intersection, explicit + intersection, array formula, table formula and some other formulas are + not supported currently. + + Args: + sheet (str): The worksheet name + cell (str): The cell reference + *opts (Options): Optional parameters for get cell value + + Returns: + Tuple[str, Exception | None]: A tuple containing the calculation + result as a string and an exception if an error occurred, otherwise + None. + """ + lib.CalcCellValue.restype = types_go._CalcCellValueResult + options = ( + byref(py_value_to_c(opts[0], types_go._Options())) + if opts + else POINTER(types_go._Options)() + ) + res = lib.CalcCellValue( + self.file_index, sheet.encode(ENCODE), cell.encode(ENCODE), options + ) + err = res.err.decode(ENCODE) + return res.val.decode(ENCODE), None if err == "" else Exception(err) + def close(self) -> Exception | None: """ Closes and cleanup the open temporary file for the spreadsheet. @@ -1203,6 +1285,43 @@ def set_active_sheet(self, index: int) -> Exception | None: err = lib.SetActiveSheet(self.file_index, index).decode(ENCODE) return None if err == "" else Exception(err) + def set_cell_formula( + self, sheet: str, cell: str, formula: str, *opts: FormulaOpts + ) -> Exception | None: + """ + Set formula on the cell is taken according to the given worksheet name + and cell formula settings. The result of the formula cell can be + calculated when the worksheet is opened by the Office Excel application + or can be using the "CalcCellValue" function also can get the calculated + cell value. If the Excel application doesn't calculate the formula + automatically when the workbook has been opened, please call + "update_linked_value" after setting the cell formula functions. + + Parameters: + sheet (str): The worksheet name + cell (str): The cell reference + formula (str): The cell formula + *opts (FormulaOpts): The formula options + + Returns: + Exception | None: Returns None if no error occurred, + otherwise returns an Exception with the message. + """ + err, lib.SetCellFormula.restype = None, c_char_p + options = ( + byref(py_value_to_c(opts[0], types_go._FormulaOpts())) + if opts + else POINTER(types_go._FormulaOpts)() + ) + err = lib.SetCellFormula( + self.file_index, + sheet.encode(ENCODE), + cell.encode(ENCODE), + formula.encode(ENCODE), + options, + ).decode(ENCODE) + return None if err == "" else Exception(err) + def set_cell_style( self, sheet: str, top_left_cell: str, bottom_right_cell: str, style_id: int ) -> Exception | None: @@ -1305,7 +1424,8 @@ def set_sheet_row( Args: sheet (str): The worksheet name cell (str): The cell reference - values (bytes): The cell values + values (list[None | int | str | bool | datetime | date]): The cell + values Returns: Exception | None: Returns None if no error occurred, diff --git a/main.go b/main.go index 51d827c..61932d4 100644 --- a/main.go +++ b/main.go @@ -6,7 +6,7 @@ // / XLTX files. Supports reading and writing spreadsheet documents generated // by Microsoft Excel™ 2007 and later. Supports complex components by high // compatibility, and provided streaming API for generating or reading data from -// a worksheet with huge amounts of data. This library needs Python version 3.9 +// a worksheet with huge amounts of data. This library needs Python version 3.10 // or later. package main @@ -666,6 +666,56 @@ func AddTable(idx int, sheet *C.char, table *C.struct_Table) *C.char { return C.CString(errNil) } +// AutoFilter provides the method to add auto filter in a worksheet by given +// worksheet name, range reference and settings. An auto filter in Excel is a +// way of filtering a 2D range of data based on some simple criteria. +// +//export AutoFilter +func AutoFilter(idx int, sheet, rangeRef *C.char, opts *C.struct_AutoFilterOptions, length int) *C.char { + f, ok := files.Load(idx) + if !ok { + return C.CString(errFilePtr) + } + options := make([]excelize.AutoFilterOptions, length) + for i, val := range unsafe.Slice(opts, length) { + goVal, err := cValueToGo(reflect.ValueOf(val), reflect.TypeOf(excelize.AutoFilterOptions{})) + if err != nil { + return C.CString(err.Error()) + } + options[i] = goVal.Elem().Interface().(excelize.AutoFilterOptions) + } + if err := f.(*excelize.File).AutoFilter(C.GoString(sheet), C.GoString(rangeRef), options); err != nil { + return C.CString(err.Error()) + } + return C.CString(errNil) +} + +// CalcCellValue provides a function to get calculated cell value. This feature +// is currently in working processing. Iterative calculation, implicit +// intersection, explicit intersection, array formula, table formula and some +// other formulas are not supported currently. +// +//export CalcCellValue +func CalcCellValue(idx int, sheet, cell *C.char, opts *C.struct_Options) C.struct_CalcCellValueResult { + var options excelize.Options + f, ok := files.Load(idx) + if !ok { + return C.struct_CalcCellValueResult{val: C.CString(""), err: C.CString(errFilePtr)} + } + if opts != nil { + goVal, err := cValueToGo(reflect.ValueOf(*opts), reflect.TypeOf(excelize.Options{})) + if err != nil { + return C.struct_CalcCellValueResult{val: C.CString(""), err: C.CString(err.Error())} + } + options = goVal.Elem().Interface().(excelize.Options) + } + val, err := f.(*excelize.File).CalcCellValue(C.GoString(sheet), C.GoString(cell), options) + if err != nil { + return C.struct_CalcCellValueResult{val: C.CString(val), err: C.CString(err.Error())} + } + return C.struct_CalcCellValueResult{val: C.CString(val), err: C.CString(errNil)} +} + // CellNameToCoordinates converts alphanumeric cell name to [X, Y] coordinates // or returns an error. // @@ -1137,14 +1187,43 @@ func SetActiveSheet(idx, index int) *C.char { return C.CString(errNil) } -// SetCellHyperLink provides a function to set cell hyperlink by given -// worksheet name and link URL address. LinkType defines three types of -// hyperlink "External" for website or "Location" for moving to one of cell in -// this workbook or "None" for remove hyperlink. Maximum limit hyperlinks in a -// worksheet is 65530. This function is only used to set the hyperlink of the -// cell and doesn't affect the value of the cell. If you need to set the value -// of the cell, please use the other functions such as `SetCellStyle` or -// `SetSheetRow`. +// SetCellFormula provides a function to set formula on the cell is taken +// according to the given worksheet name and cell formula settings. The result +// of the formula cell can be calculated when the worksheet is opened by the +// Office Excel application or can be using the "CalcCellValue" function also +// can get the calculated cell value. If the Excel application doesn't +// calculate the formula automatically when the workbook has been opened, +// please call "UpdateLinkedValue" after setting the cell formula functions. +// +//export SetCellFormula +func SetCellFormula(idx int, sheet, cell, formula *C.char, opts *C.struct_FormulaOpts) *C.char { + f, ok := files.Load(idx) + if !ok { + return C.CString("") + } + if opts != nil { + var options excelize.FormulaOpts + goVal, err := cValueToGo(reflect.ValueOf(*opts), reflect.TypeOf(excelize.FormulaOpts{})) + if err != nil { + return C.CString(err.Error()) + } + options = goVal.Elem().Interface().(excelize.FormulaOpts) + if err := f.(*excelize.File).SetCellFormula(C.GoString(sheet), C.GoString(cell), C.GoString(formula), options); err != nil { + return C.CString(err.Error()) + } + return C.CString(errNil) + } + if err := f.(*excelize.File).SetCellFormula(C.GoString(sheet), C.GoString(cell), C.GoString(formula)); err != nil { + return C.CString(err.Error()) + } + return C.CString(errNil) +} + +// SetCellStyle provides a function to add style attribute for cells by given +// worksheet name, range reference and style ID. This function is concurrency +// safe. Note that diagonalDown and diagonalUp type border should be use same +// color in the same range. SetCellStyle will overwrite the existing +// styles for the cell, it won't append or merge style with existing styles. // //export SetCellStyle func SetCellStyle(idx int, sheet, topLeftCell, bottomRightCell *C.char, styleID int) *C.char { diff --git a/setup.py b/setup.py index 77f9cde..153185a 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ files. Supports reading and writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. Supports complex components by high compatibility, and provided streaming API for generating or reading data from a worksheet with huge -amounts of data. This library needs Python version 3.9 or later. +amounts of data. This library needs Python version 3.10 or later. """ from __future__ import annotations @@ -17,8 +17,8 @@ from setuptools import setup from setuptools.command.install import install -if sys.version_info < (3, 9): - warn("The minimum Python version supported by excelize is 3.9") +if sys.version_info < (3, 10): + warn("The minimum Python version supported by excelize is 3.10") exit() @@ -60,7 +60,7 @@ def run(self): "types_go", "types_py", ], - python_requires=">=3.9", + python_requires=">=3.10", keywords=[ "excelize", "excel", @@ -80,7 +80,6 @@ def run(self): "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", diff --git a/test_excelize.py b/test_excelize.py index bb4250a..916540c 100644 --- a/test_excelize.py +++ b/test_excelize.py @@ -6,7 +6,7 @@ files. Supports reading and writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. Supports complex components by high compatibility, and provided streaming API for generating or reading data from a worksheet with huge -amounts of data. This library needs Python version 3.9 or later. +amounts of data. This library needs Python version 3.10 or later. """ import excelize @@ -585,6 +585,52 @@ def test_add_sparkline(self): self.assertIsNone(f.save_as("TestAddSparkline.xlsx")) self.assertIsNone(f.close()) + def test_auto_filter(self): + f = excelize.new_file() + self.assertIsNone( + f.auto_filter( + "Sheet1", + "A2:D4", + [], + ) + ) + self.assertIsNone( + f.auto_filter( + "Sheet1", + "F1:D4", + [ + excelize.AutoFilterOptions( + column="F", + expression="x != blanks", + ) + ], + ) + ) + self.assertIsNone(f.save_as("TestAutoFilter.xlsx")) + self.assertIsNone(f.close()) + + def test_calc_cell_formula(self): + f = excelize.new_file() + self.assertIsNone(f.set_sheet_row("Sheet1", "A1", [1, 2])) + self.assertIsNone(f.set_cell_formula("Sheet1", "C1", "A1+B1")) + self.assertIsNone( + f.set_cell_formula( + "Sheet1", + "D1", + "A1+B1", + excelize.FormulaOpts( + type="shared", + ref="D1:D5", + ), + ) + ) + val, err = f.calc_cell_value("Sheet1", "C1") + self.assertEqual(val, "3") + self.assertIsNone(err) + val, err = f.calc_cell_value("Sheet1", "D2") + self.assertEqual(val, "0") + self.assertIsNone(err) + def test_cell_name_to_coordinates(self): col, row, err = excelize.cell_name_to_coordinates("Z3") self.assertEqual(col, 26) diff --git a/types_c.h b/types_c.h index 6b29be3..c7044f5 100644 --- a/types_c.h +++ b/types_c.h @@ -6,7 +6,7 @@ // / XLTX files. Supports reading and writing spreadsheet documents generated // by Microsoft Excel™ 2007 and later. Supports complex components by high // compatibility, and provided streaming API for generating or reading data from -// a worksheet with huge amounts of data. This library needs Python version 3.9 +// a worksheet with huge amounts of data. This library needs Python version 3.10 // or later. #include @@ -136,6 +136,13 @@ struct AutoFilterOptions char *Expression; }; +// FormulaOpts can be passed to SetCellFormula to use other formula types. +struct FormulaOpts +{ + char **Type; + char **Ref; +}; + // Protection directly maps the protection settings of the cells. struct Protection { @@ -457,6 +464,12 @@ struct Table bool *ShowRowStripes; }; +struct CalcCellValueResult +{ + char *val; + char *err; +}; + struct CellNameToCoordinatesResult { int col; diff --git a/types_go.py b/types_go.py index 8cafd69..fa1b154 100644 --- a/types_go.py +++ b/types_go.py @@ -6,7 +6,7 @@ files. Supports reading and writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. Supports complex components by high compatibility, and provided streaming API for generating or reading data from a worksheet with huge -amounts of data. This library needs Python version 3.9 or later. +amounts of data. This library needs Python version 3.10 or later. """ from ctypes import ( @@ -120,6 +120,13 @@ class _AutoFilterOptions(Structure): ] +class _FormulaOpts(Structure): + _fields_ = [ + ("Type", POINTER(c_char_p)), + ("Ref", POINTER(c_char_p)), + ] + + class _Style(Structure): _fields_ = [ ("BorderLen", c_int), @@ -434,6 +441,13 @@ class _Table(Structure): ] +class _CalcCellValueResult(Structure): + _fields_ = [ + ("val", c_char_p), + ("err", c_char_p), + ] + + class _CellNameToCoordinatesResult(Structure): _fields_ = [ ("col", c_int), diff --git a/types_py.py b/types_py.py index 9f1af0c..ad3a242 100644 --- a/types_py.py +++ b/types_py.py @@ -6,7 +6,7 @@ files. Supports reading and writing spreadsheet documents generated by Microsoft Excel™ 2007 and later. Supports complex components by high compatibility, and provided streaming API for generating or reading data from a worksheet with huge -amounts of data. This library needs Python version 3.9 or later. +amounts of data. This library needs Python version 3.10 or later. """ from dataclasses import dataclass @@ -258,6 +258,12 @@ class AutoFilterOptions: expression: str = "" +@dataclass +class FormulaOpts: + type: Optional[str] = None + ref: Optional[str] = None + + @dataclass class Style: border: Optional[list[Border]] = None