Skip to content

7.0: slim binding APIs, compatibility policy, and installable wheels on every platform - #142

Merged
GabrielMarquezMatte merged 25 commits into
masterfrom
develop
Oct 15, 2026
Merged

GabrielMarquezMatte merged 25 commits into
masterfrom
develop

Conversation

@GabrielMarquezMatte

@GabrielMarquezMatte GabrielMarquezMatte commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

What does this change?

Everything on develop since 7.0.0-rc.2. It does three things:

  • Python wheels now install on the systems they support, and more platforms get one.
  • 7.x has a written compatibility policy that CI enforces.
  • The Python, C++ and Rust APIs are cut down to one way per operation before 7.0 freezes them.

docs/guide/migrating-to-v7.md has a v6 → v7 table per binding.

The C ABI is unchanged and XL_ABI_VERSION stays 6.

Wheels and native builds

  • The wheel's platform tag now comes from the bundled library, not from the build runner. The runner's own tags were macosx_26_0 and manylinux_2_39, so pip refused 6.0.0 and the 7.0 release candidates on macOS 14/15, Debian 12, Ubuntu 22.04, RHEL 9 and Amazon Linux 2023. The library itself needs macOS 12.0 and GLIBC 2.34.
  • New targets: Linux aarch64 (ubuntu-24.04-arm), macOS x86_64 (macos-15-intel), and musllinux x64/aarch64 built in Alpine containers (python/scripts/build_musl_wheel.sh).
  • check_wheel_tag.py fails CI when a wheel does not install on the runner that built it.
  • Arch detection now accepts aarch64. The old check would have published the Linux ARM assets and CLI as x64.
  • Wheel tags produced by CI: manylinux_2_34_{x86_64,aarch64}, musllinux_1_2_{x86_64,aarch64}, macosx_12_0_{arm64,x86_64}, win_amd64.

CI

  • CodeQL always creates both Analyze (csharp) and Analyze (python). Before, a PR without C# changes never created the required Analyze (csharp) check, so it stayed pending forever.
  • ExcelReader.NET and ExcelReader.Arrow enable package validation against 7.0.0-rc.2, and Build & Test (ubuntu-latest) packs both. A removed or changed public member fails the build: a deliberate break was caught with CP0002.

Compatibility policy

  • New guide page docs/guide/versioning.md says what a minor version may change:

    • new members on the interfaces only the library implements, with default implementations;
    • new enum values;
    • new options.

    Interfaces that users implement never gain a member they would have to implement.

  • XML docs on those interfaces and on CellType and ExcelColumnType link to the policy. CONTRIBUTING lists the rules.

Python API

  • open_workbook(source) is the only opener. It takes a path, bytes/bytearray/memoryview/io.BytesIO, a Source, or a readable stream. open_bytes, open_source and open_stream are removed.
  • write_workbook(destination, data, types=None) takes a path or a binary object with write(), and a TypedTable or any Arrow-compatible table, including pandas and polars DataFrames. Removed: write_arrow, write_pandas, write_polars and every *_to_bytes.
  • open_writer(destination) and encrypt_package(package, destination, password) follow the same pattern. open_writer_to_memory, SheetWriter.bytes() and encrypt_package_bytes are removed.
  • A sheet reads as rows(), parse_typed()/iter_parse_typed(), to_arrow() (now a RecordBatch)/to_arrow_stream(), or to_pandas()/to_polars().
    • Removed: read_all, read_all_columnar, ColumnarSheet, decode_cell, to_record_batch, to_record_batch_reader, iter_pandas, iter_polars, sheet_count, sheet_names, sheet_name_at.
  • SheetWriter writes whole rows only, with write_row. A typed blank is written as (None, ColumnType.X).

C++ API

  • Workbook::open is one overload set: path, byte span, Source or InputStream.
  • write_columns, write_sheet and WriterHandle::open take a path or a std::vector<std::uint8_t>&. WriterHandle::finish() reports errors.
  • Options are const Opts& = {}, with the format inside them.
  • One aggregate_csv; CsvParallelOptions is removed.
  • C handles, from_raw, handle() and to_c() are no longer public. The dead payload path in RowView is removed.
  • The README is current: it pins v7.0.0 and documents WriterHandle.

Rust API

  • The raw FFI is private: the ~130 Xl*/xl_*/XL_* items.
  • Every item lives at the crate root, with one path each.
  • Integer codes are now enums: Format, ColumnType, ErrorKind.
  • Opening is Workbook::open(path) plus OpenOptions::{open, open_bytes, open_source, open_reader}. OpenOptions and WriteOptions are builder-only.
  • Writing has path and impl Write pairs (write_columns/write_columns_to, write_sheet/write_sheet_to). WriterHandle::new(writer) ends with finish().
  • Reading has Sheet::{parse, parse_arrow, parse_arrow_stream, read_all} and one aggregate_csv.

Behavior change in all three bindings

max_cell_bytes and intern_strings now apply to CSV too. Before, a CSV read used only csv_max_cell_bytes/csv_intern_strings, so the general limit never reached it. The csv_* pair is removed from the bindings.

Checklist

  • dotnet build ExcelReader.slnx --configuration Release builds clean (warnings are errors)
  • dotnet test tests/ExcelReader.Tests/ExcelReader.Tests.csproj --configuration Release passes
  • If this changes the public API: PublicAPI.Unshipped.txt updated (.NET public API unchanged; XML docs only)
  • Tests added/updated for the behavior change
  • One focused change: this is the 7.0 release-candidate batch, split into one commit per area

Test plan

  • CI on develop is green on every job: .NET on three OSes, ISA fallbacks, AOT, CodeQL, docs, the native build on five runners, Python smoke on five runners, musl on x64 and arm64, Rust and C++ on three OSes.
  • Locally:
    • Python: 195 passed.
    • C++: 8/8 ctest, benchmark smoke, and the find_package consumer.
    • Rust: clippy -D warnings and 133 tests plus 19 doctests.
    • Package validation passes against rc.2.

Known gaps

  • The release.yml jobs added here (musl, macOS Intel CLI and wheel) first run on the next tag.
  • After 7.0.0 ships, move PackageValidationBaselineVersion to 7.0.0.
  • The read benchmark tables predate checksum verification, and the Python README keeps earlier timings under the new method names.

Wheels took the build runner's own platform tag, so 6.0.0 shipped as
macosx_26_0_arm64 and manylinux_2_39_x86_64 although the library needs
only macOS 12.0 and GLIBC 2.34: pip refused it on macOS 14/15, Debian 12,
Ubuntu 22.04, RHEL 9 and Amazon Linux 2023. The build hook now reads the
floor from the library (LC_BUILD_VERSION minos, newest GLIBC_2.N symbol),
and CI checks each wheel installs on the runner that built it.

Adds ubuntu-24.04-arm to the native, CLI and wheel builds, and fixes the
arch detection that labelled Linux aarch64 (uname -m) as x64.
… wheels

CodeQL picked its matrix from the languages a pull request touched, so a
PR without C# changes never created "Analyze (csharp)" and the required
check stayed pending. Both languages now always get a job, and an
unchanged one skips its steps, as Build & Test already does.

macos-15-intel joins the native, CLI and wheel builds. musl builds run in
Alpine containers on x64 and arm64: build_native.py picks the linux-musl
RID off glibc, and the wheel takes the musllinux tag when the library
links musl.
…t the last release

A versioning guide says what a minor version may change: new members on the
interfaces only the library implements (with default implementations), new
enum values, new options. The interfaces users implement gain nothing they
would have to implement. Those library-implemented interfaces and the two
growing enums now say so in their docs, and CONTRIBUTING lists the rules.

ExcelReader.NET and ExcelReader.Arrow enable package validation against
7.0.0-rc.2, and CI packs both on every pull request, so a removed or
changed member fails the build. The development version derives from the
baseline, since validation requires an assembly version at least the
baseline's; release builds still override it from the tag.
@codecov-commenter

codecov-commenter commented Oct 14, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.06%. Comparing base (114f5ae) to head (daad018).
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #142   +/-   ##
=======================================
  Coverage   91.06%   91.06%           
=======================================
  Files         251      251           
  Lines       17472    17472           
  Branches     3153     3153           
=======================================
  Hits        15911    15911           
  Misses       1070     1070           
  Partials      491      491           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

GabrielMarquezMatte and others added 9 commits October 14, 2026 15:55
open_workbook() now takes a path, bytes, bytearray, memoryview, io.BytesIO,
a Source, or any object with readinto()/read(), and picks the native open
from the input's type. open_bytes(), open_source() and open_stream() are
removed; the last two only existed in the 7.0 release candidates.

BREAKING CHANGE: open_bytes(data, ...) becomes open_workbook(data, ...).
…unction each

write_workbook(), write_arrow(), write_pandas() and write_polars() take
the destination first: a path, or any binary object with write(), such
as io.BytesIO or an open file. The four *_to_bytes() twins are removed.
A destination with no path needs format=, and the frame writers take
format and options as keywords instead of forwarding **kwargs.

BREAKING CHANGE: write_*_to_bytes(x, format=f) becomes write_*(buffer, x, format=f)
on an io.BytesIO.
…h one function each

open_writer() takes a path or any binary object with write(); the latter
receives the finished workbook in one write() call when the writer closes,
even with a sheet left open. open_writer_to_memory() and SheetWriter.bytes()
are removed.

encrypt_package() takes the package as a path, bytes or io.BytesIO and the
destination as a path or an object with write(). Path to path still uses
the native file export; any other pair goes through memory.
encrypt_package_bytes() is removed.

BREAKING CHANGE: open_writer_to_memory(f) becomes open_writer(buffer, f), and
encrypt_package_bytes(p, pw) becomes encrypt_package(p, buffer, pw).
…e function

Removed:
- Workbook.sheet_count, sheet_names, sheet_name_at (use workbook.sheets)
- Sheet.read_all, read_all_columnar, ColumnarSheet, decode_cell
- Sheet.to_record_batch (to_arrow now returns the RecordBatch)
- Sheet.to_record_batch_reader (use
  pyarrow.RecordBatchReader.from_stream(sheet.to_arrow_stream(...)))
- Sheet.iter_pandas, iter_polars
- write_arrow, write_pandas, write_polars (write_workbook takes Arrow data)
- SheetWriter.write_str/i64/f64/bool/date/time/timestamp/null, start_row,
  end_row (write_row infers types; (None, ColumnType.X) is a typed blank)
- OpenOptions.csv_max_cell_bytes, csv_intern_strings (max_cell_bytes and
  intern_strings now reach the CSV reader too)
- ArrowStream from __all__

to_pandas gains parallelism, matching to_polars.

BREAKING CHANGE: the names above are gone from the Python package; each has
a 7.0 replacement listed above.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One way to do each thing, with options passed as `const Opts& = {}`:

- Workbook::open is one overload set (path, byte span, unique_ptr<Source>,
  unique_ptr<InputStream>); open_memory, open_source and open_stream are gone.
  The format moves into OpenOptions::format.
- Workbook::sheet_count, sheet_name_at and sheet_names are removed; use
  sheets(), sheet(i) and Sheet::name().
- Sheet::read_all_decoded is renamed read_all; RowCursor::read_all_decoded is
  removed. typed_reader and arrow_stream reject batch_size <= 0.
- write_columns and write_sheet take a path or a std::vector<uint8_t>& and read
  the format from WriteOptions::format (AUTO = path extension).
  write_columns_to_memory, write_sheet_to_memory and the format overloads are
  removed; format_from_path is internal.
- WriterHandle::open takes a path or a vector; open_memory and bytes() are
  removed and finish() reports the result of completing the workbook.
- aggregate_csv(path | span, seed, OpenOptions, header_row, dop) replaces the
  four aggregate_csv_file/aggregate_csv_memory functions and
  CsvParallelOptions; the seed is a value copied per partition.
- The C ABI is hidden: raw-handle constructors, from_raw and handle() are
  private or removed, to_c() is internal, XlType and FieldBinding move to
  detail, TableView::to_vector is removed.
- ArrowSchemaGuard and ArrowArrayGuard merge into ArrowTable; every stream
  batch carries its schema.
- OpenOptions::password_ and password() become a plain `password` field, and
  csv_max_cell_bytes/csv_intern_strings are dropped: max_cell_bytes and
  intern_strings now apply to CSV too.
- RowView's unused payload decoding is removed.

BREAKING CHANGE: the C++ header API changes as listed above; see the 7.0
migration guide for the mapping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Flatten the crate to one path per item at the root, hide the FFI, and
fold duplicated entry points into option builders and enum arguments.

- Raw bindings (Xl* structs, xl_* functions, XL_* constants) move to a
  private ffi module; derive support moves to #[doc(hidden)] __private.
- Modules workbook, writer, writer_handle, rows, source, aggregate and
  arrow become private; their items are re-exported at the root.
- Format, ColumnType and ErrorKind replace the XL_FORMAT_*, XL_T_* and
  status integers; Error becomes a struct with kind() and message().
- Opening: Workbook::open(path) plus OpenOptions::{open, open_bytes,
  open_source, open_reader}; format is an OpenOptions setting.
  Removed Workbook::{open_with, open_memory, open_source, open_reader,
  sheet_count, sheet_name_at, sheet_names}.
- Reading: Sheet::read_all replaces read_all_decoded/DecodedRows and
  read_all_blob; Sheet::parse replaces parse_sheet/parse_sheet_parallel;
  Sheet::parse_arrow/parse_arrow_stream replace the arrow module's free
  functions; infer_schema takes a parse_text flag.
- Writing: write_columns/write_sheet take a path and &WriteOptions;
  write_columns_to/write_sheet_to take an io::Write and replace the
  *_to_memory functions; format moves into WriteOptions (Auto infers
  from the extension). WriterHandle::{create, new, finish} replace
  open/open_with/open_memory/bytes; write_null is removed.
- aggregate_csv(input, &OpenOptions, header_row, parallelism, seed)
  replaces aggregate_csv_file/aggregate_csv_memory/CsvParallelOptions.
- OpenOptions/WriteOptions are builder-only; csv_max_cell_bytes and
  csv_intern_strings fold into max_cell_bytes/intern_strings.

BREAKING CHANGE: every module path, the raw FFI surface, integer format
and type codes, Error's enum shape and code(), and the removed functions
listed above are gone; see the 7.0 migration guide.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@GabrielMarquezMatte GabrielMarquezMatte changed the title fix(python): tag wheels from the bundled library; build linux-arm64 7.0: slim binding APIs, compatibility policy, and installable wheels on every platform Oct 14, 2026
GabrielMarquezMatte and others added 12 commits October 14, 2026 17:49
…as_date knows the date system

Sheet.parse_typed is now parse and iter_parse_typed is parse_batches, the
names the C++ and Rust bindings use. header_row, parallelism and
batch_size are keyword-only on every read method. Cell carries its
workbook's date1904, so as_date() takes no argument.

BREAKING CHANGE: parse_typed -> parse, iter_parse_typed -> parse_batches,
read keywords are keyword-only, Cell.as_date(date1904) -> Cell.as_date().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bumps Meziantou.Analyzer from 3.0.294 to 3.0.303
Bumps Microsoft.CodeAnalysis.NetAnalyzers from 10.0.401 to 10.0.402

---
updated-dependencies:
- dependency-name: Meziantou.Analyzer
  dependency-version: 3.0.303
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: analyzers
- dependency-name: Microsoft.CodeAnalysis.NetAnalyzers
  dependency-version: 10.0.402
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: analyzers
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps xunit.runner.visualstudio from 4.0.0 to 4.0.1
Bumps xunit.v3.mtp-v2 from 4.0.1 to 4.0.2

---
updated-dependencies:
- dependency-name: xunit.runner.visualstudio
  dependency-version: 4.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: test
- dependency-name: xunit.v3.mtp-v2
  dependency-version: 4.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: test
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps OfficeIMO.Excel from 3.4.4 to 3.4.5

---
updated-dependencies:
- dependency-name: OfficeIMO.Excel
  dependency-version: 3.4.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: benchmarks
...

Signed-off-by: dependabot[bot] <support@github.com>
… ReadOptions

Sheet::typed_chunks is now parse_batches, parse_arrow is to_arrow and
parse_arrow_stream is to_arrow_stream (TypedChunks/ArrowChunks become
TypedBatches/ArrowBatches). The four take one ReadOptions builder
(header_row, parallelism, batch_size) instead of loose arguments.
Date, Time and Timestamp are built through their public field only.

BREAKING CHANGE: parse(h, n) -> parse(&ReadOptions), typed_chunks ->
parse_batches, parse_arrow -> to_arrow, parse_arrow_stream ->
to_arrow_stream, Date::new(d) -> Date { days_since_epoch: d }.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e ReadOptions

parse_sheet is now parse, typed_reader is parse_batches, parse_arrow is
to_arrow and arrow_stream is to_arrow_stream. The four take one
ReadOptions aggregate (header_row, parallelism, batch_size) instead of
loose arguments.

BREAKING CHANGE: parse_sheet<T>(s, h, n) -> parse<T>(s, {.header_row = h,
.parallelism = n}); typed_reader -> parse_batches; parse_arrow -> to_arrow;
arrow_stream -> to_arrow_stream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts/ExcelReader.Benchmarks/develop/benchmarks-62a6cf4621
…ts/ExcelReader.Tests/develop/test-3d9439c6bb
rows() creates one Cell per cell, and a frozen dataclass's __init__ goes
through object.__setattr__ for every field. A plain slots dataclass is
~2x cheaper to construct (493 ns vs 1023 ns) and makes rows() ~10%
faster on the 65K-row fixture (1.70-1.79 s vs 1.91-2.04 s, min of 3,
interleaved). Cell stays keyword-only, so fields can still be added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@GabrielMarquezMatte
GabrielMarquezMatte merged commit 52aa42e into master Oct 15, 2026
41 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants