sheetdelta 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - run: python -m pip install --upgrade pip
20
+ - run: python -m pip install -e ".[dev]"
21
+ - run: python -m ruff check src tests
22
+ - run: python -m mypy
23
+ - run: python -m pytest
@@ -0,0 +1,20 @@
1
+ name: Release
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ environment: pypi
11
+ permissions:
12
+ id-token: write # mint the OIDC token PyPI trusts
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+ - run: python -m pip install --upgrade build
19
+ - run: python -m build
20
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ htmlcov/
@@ -0,0 +1,61 @@
1
+ # AGENTS.md
2
+
3
+ Repository notes for anyone (human or agent) working on sheetdelta.
4
+
5
+ ## What this is
6
+
7
+ A headless `.xlsx` differ. It compares two workbooks cell by cell, including
8
+ the formulas and the values Excel cached, then follows each change through the
9
+ workbook's dependency graph to report what it reaches downstream. Read-only,
10
+ standard library only, no Excel.
11
+
12
+ ## Commands
13
+
14
+ ```console
15
+ python -m pip install -e ".[dev]" # install with dev tools
16
+ python -m pytest # tests
17
+ python -m ruff check src tests # lint
18
+ python -m mypy # types (strict on src)
19
+ python -m build # wheel + sdist
20
+ ```
21
+
22
+ Run all three checks before pushing; CI runs them on 3.10 through 3.13.
23
+
24
+ ## Layout
25
+
26
+ ```
27
+ src/sheetdelta/
28
+ model.py dataclasses: CellRef, RangeRef, Cell, Sheet, Workbook
29
+ errors.py the exception hierarchy, all under SheetDeltaError
30
+ references.py pulls cell/range references out of a formula
31
+ reader.py parses .xlsx (zipfile + xml.etree)
32
+ differ.py compares two workbooks, builds the dependency graph
33
+ audit.py checks one workbook for broken and circular references
34
+ report.py text and JSON rendering, exit codes
35
+ cli.py argparse entry point
36
+ ```
37
+
38
+ ## Things worth knowing before you change something
39
+
40
+ - **The reader has no dependencies on purpose.** It is `zipfile` and
41
+ `xml.etree`. Do not add openpyxl as a runtime dependency; it is a dev-only
42
+ tool used to write reference workbooks for `tests/test_interop.py`.
43
+ - **Formulas are never evaluated.** Only the value Excel cached is compared.
44
+ There is no calculation engine and adding one would defeat the point.
45
+ - **Ranges are never expanded.** `SUM(A:A)` covers a million cells; the graph
46
+ asks `RangeRef.contains` instead of materialising them.
47
+ - **The dependency graph spans the whole workbook**, not one sheet, so a change
48
+ in one sheet is followed into the sheet that reads it. A formula that reads
49
+ its own cell (a total inside its own range) is excluded from the graph.
50
+ - **Strings are not always in `<v>`.** openpyxl writes text as
51
+ `<is><t>...</t></is>`. Both shapes are handled in `_raw_value`; the interop
52
+ test is what caught this.
53
+ - **Shared formulas must be shifted.** Excel stores a repeated formula once and
54
+ refers to it from other cells with relative offsets applied. See
55
+ `shift_formula`.
56
+
57
+ ## Test fixtures
58
+
59
+ `tests/xlsx_fixtures.py` writes `.xlsx` files with the standard library, so the
60
+ suite runs with no dependencies. `tests/test_interop.py` checks the same reader
61
+ against workbooks written by openpyxl, and skips if openpyxl is absent.
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.1.0] - 2026-10-07
8
+
9
+ First release.
10
+
11
+ ### Added
12
+
13
+ - `sheetdelta diff OLD NEW`: compares two `.xlsx` workbooks cell by cell,
14
+ including formulas and the values Excel cached, and follows each change
15
+ through the workbook's dependency graph to the cells it reaches.
16
+ - `sheetdelta audit FILE`: reports formulas whose references do not resolve --
17
+ a missing sheet, or a range outside the used area.
18
+ - `--fail-on {never,any,breaking}` to set the exit code, so the tool works as a
19
+ CI check without extra scripting.
20
+ - `--json` output for both subcommands.
21
+ - A standard-library `.xlsx` reader: no runtime dependencies.
22
+ - Rename detection, so renaming a sheet does not report every cell as changed.
23
+ - Stale-cell detection, for a formula edited without the workbook being
24
+ recalculated.
25
+
26
+ [0.1.0]: https://github.com/sqmyou/sheetdelta/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sqm
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,250 @@
1
+ Metadata-Version: 2.5
2
+ Name: sheetdelta
3
+ Version: 0.1.0
4
+ Summary: Diff Excel workbooks without Excel: formulas, values and dependency impact.
5
+ Project-URL: Homepage, https://github.com/sqmyou/sheetdelta
6
+ Project-URL: Repository, https://github.com/sqmyou/sheetdelta
7
+ Project-URL: Changelog, https://github.com/sqmyou/sheetdelta/blob/main/CHANGELOG.md
8
+ Author: sqm
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ci,cli,dependency-graph,diff,excel,spreadsheet,xlsx,xlsx-diff
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Financial and Insurance Industry
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
24
+ Classifier: Topic :: Software Development :: Quality Assurance
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.10
27
+ Provides-Extra: dev
28
+ Requires-Dist: mypy>=1.11; extra == 'dev'
29
+ Requires-Dist: openpyxl>=3.1; extra == 'dev'
30
+ Requires-Dist: pytest>=8.0; extra == 'dev'
31
+ Requires-Dist: ruff>=0.6; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # sheetdelta
35
+
36
+ Compare two Excel workbooks and find out what actually changed.
37
+
38
+ `sheetdelta` reads two `.xlsx` files, shows you the difference cell by cell, and
39
+ then tells you which of those changes reach other cells. It runs on Linux, in
40
+ CI, with no copy of Excel anywhere near it.
41
+
42
+ ```console
43
+ $ sheetdelta diff budget_v1.xlsx budget_v2.xlsx
44
+ budget_v1.xlsx -> budget_v2.xlsx
45
+
46
+ Sheet 'Revenue' (1 change, 1 breaking)
47
+ !! D12 formula changed
48
+ - =SUM(D2:D11)
49
+ + =SUM(D2:D13)
50
+ affects 3 cells downstream: Revenue!E12, Revenue!F12, Summary!B4
51
+
52
+ Sheet 'Summary' (1 change)
53
+ ! B7 value changed
54
+ - 1200
55
+ + 1450
56
+
57
+ 1 formula; 1 value across 2 edited sheet(s). 1 breaking change reaching 3 downstream cells.
58
+ ```
59
+
60
+ ## Why
61
+
62
+ Two things go wrong when a spreadsheet changes and nobody looks closely.
63
+
64
+ The first is the change itself. `git diff` on an `.xlsx` gives you binary
65
+ noise, so the usual answer is to open both files side by side and squint.
66
+
67
+ The second is worse. A formula edit looks harmless on the sheet you are
68
+ looking at, but the cell feeds a summary three sheets away. The number that
69
+ matters is now wrong, and nothing tells you.
70
+
71
+ `sheetdelta` answers both. Every change is followed through the dependency
72
+ graph, so a formula edit reports the cells it reaches, and a CI job can fail
73
+ the build on exactly that.
74
+
75
+ ## Install
76
+
77
+ ```console
78
+ pip install sheetdelta
79
+ ```
80
+
81
+ No dependencies. The reader is `zipfile` and `xml.etree` from the standard
82
+ library, so it drops into any environment, including a build container that
83
+ has nothing but Python.
84
+
85
+ ## Usage
86
+
87
+ ### Compare two workbooks
88
+
89
+ ```console
90
+ sheetdelta diff old.xlsx new.xlsx
91
+ ```
92
+
93
+ Exit status is `0` when nothing breaking changed, `1` when something did, `2`
94
+ on an error. That makes it a CI check without any extra scripting:
95
+
96
+ ```yaml
97
+ - run: pip install sheetdelta
98
+ - run: sheetdelta diff before.xlsx after.xlsx --fail-on breaking
99
+ ```
100
+
101
+ `--fail-on` takes three values:
102
+
103
+ | Value | Fails when |
104
+ |---|---|
105
+ | `breaking` (default) | a change reaches another cell |
106
+ | `any` | anything changed at all |
107
+ | `never` | never; useful for a report-only run |
108
+
109
+ ### Machine-readable output
110
+
111
+ ```console
112
+ sheetdelta diff old.xlsx new.xlsx --json
113
+ ```
114
+
115
+ ```json
116
+ {
117
+ "old": "budget_v1.xlsx",
118
+ "new": "budget_v2.xlsx",
119
+ "has_changes": true,
120
+ "summary": {
121
+ "cell_changes": {"formula": 1, "value": 1},
122
+ "breaking": 1
123
+ },
124
+ "sheets": [
125
+ {
126
+ "kind": "changed",
127
+ "name": "Revenue",
128
+ "old_name": "Revenue",
129
+ "changes": [
130
+ {
131
+ "cell": "Revenue!D12",
132
+ "a1": "D12",
133
+ "kind": "formula",
134
+ "severity": "breaking",
135
+ "detail": "formula changed",
136
+ "old": "=SUM(D2:D11)",
137
+ "new": "=SUM(D2:D13)",
138
+ "affected": ["Revenue!E12", "Revenue!F12", "Summary!B4"]
139
+ }
140
+ ]
141
+ }
142
+ ]
143
+ }
144
+ ```
145
+
146
+ ### Check one workbook
147
+
148
+ ```console
149
+ sheetdelta audit workbook.xlsx
150
+ ```
151
+
152
+ An audit looks for the two defects that make a workbook unsound:
153
+
154
+ - a formula pointing at a sheet that does not exist, which Excel turns into
155
+ `#REF!` when it recalculates; and
156
+ - a circular reference, a cell that depends on itself through a chain of other
157
+ cells, which Excel refuses to calculate at all.
158
+
159
+ ```console
160
+ $ sheetdelta audit model.xlsx
161
+ model.xlsx
162
+ 3 sheet(s), 412 cell(s), 118 formula(s)
163
+
164
+ 1 broken reference(s):
165
+ !! C4 Removed Sheet!B2
166
+ sheet 'Removed Sheet' does not exist
167
+ ='Removed Sheet'!B2*1.2
168
+
169
+ 1 circular reference(s):
170
+ !! Revenue!D12 -> Revenue!E12 -> Summary!B4 -> Revenue!D12
171
+ ```
172
+
173
+ A reference to a cell that is simply empty is not reported. That is normal in
174
+ a spreadsheet, and flagging it would make the audit useless on real files.
175
+
176
+ ## What it reports
177
+
178
+ | Change | Severity |
179
+ |---|---|
180
+ | Formula changed, and other cells read it | breaking |
181
+ | Value changed, and other cells read it | breaking |
182
+ | Cell deleted, and other cells read it | breaking |
183
+ | Formula or value changed, nothing reads it | warning |
184
+ | Formula changed but the cached value did not move | stale |
185
+ | New cell | info |
186
+ | Sheet added, removed or renamed | info |
187
+
188
+ The **stale** case is worth explaining. Excel stores both a formula and the
189
+ last value it calculated for it. When a file is edited by something that does
190
+ not recalculate, the two disagree. Excel will happily show you the new
191
+ formula next to the old number, which is a quiet way to ship a wrong total.
192
+ `sheetdelta` flags it.
193
+
194
+ A **rename** is detected by matching the contents of a removed sheet against
195
+ an added one. Without that, renaming a sheet would look like every cell in it
196
+ changed and bury the real diff.
197
+
198
+ ## What it does not do
199
+
200
+ Being clear about this saves you time.
201
+
202
+ - **It does not evaluate formulas.** There is no calculation engine, and
203
+ there will not be one. Excel already stored the value it last computed, and
204
+ that is what gets compared. This is why the tool needs no Excel and no
205
+ dependencies, and why it is fast on large files.
206
+ - **It does not write or edit workbooks.** Read-only, by design.
207
+ - **It only reads `.xlsx` and `.xlsm`.** The old `.xls` and binary `.xlsb`
208
+ formats are rejected with a clear message rather than half-parsed.
209
+ - **It does not align rows across an insertion.** If a row is inserted near
210
+ the top, every cell below it reports as changed. Matching rows by content
211
+ is the hard part of spreadsheet diffing and is not implemented yet.
212
+ - **It does not read VBA macros** inside `.xlsm` files.
213
+
214
+ ## Using it as a library
215
+
216
+ ```python
217
+ from sheetdelta import diff_workbooks, read_workbook
218
+
219
+ result = diff_workbooks(read_workbook("old.xlsx"), read_workbook("new.xlsx"))
220
+
221
+ for change in result.breaking:
222
+ print(change.ref, "->", change.affected)
223
+ print(" was", change.old)
224
+ print(" now", change.new)
225
+ ```
226
+
227
+ Every type is exported from the package root, and the whole thing is typed.
228
+
229
+ ## How it works
230
+
231
+ An `.xlsx` file is a zip containing XML. `sheetdelta` reads the sheet list, the
232
+ shared string table, the number formats and each sheet's cells, then stops.
233
+
234
+ Each formula is scanned for the cells and ranges it mentions. That scan is
235
+ more careful than it looks: `LOG10(...)` looks exactly like a reference to
236
+ column LOG row 10, and `"see A1"` looks like a reference to A1. Both are
237
+ handled, because a dependency graph built on false references is worse than
238
+ no graph.
239
+
240
+ References are kept as ranges rather than expanded. `SUM(A:A)` covers a
241
+ million cells, and expanding it would cost more than the entire diff; the
242
+ graph asks whether a range contains a cell instead.
243
+
244
+ ## Requirements
245
+
246
+ Python 3.10 or newer. No runtime dependencies.
247
+
248
+ ## License
249
+
250
+ MIT.
@@ -0,0 +1,217 @@
1
+ # sheetdelta
2
+
3
+ Compare two Excel workbooks and find out what actually changed.
4
+
5
+ `sheetdelta` reads two `.xlsx` files, shows you the difference cell by cell, and
6
+ then tells you which of those changes reach other cells. It runs on Linux, in
7
+ CI, with no copy of Excel anywhere near it.
8
+
9
+ ```console
10
+ $ sheetdelta diff budget_v1.xlsx budget_v2.xlsx
11
+ budget_v1.xlsx -> budget_v2.xlsx
12
+
13
+ Sheet 'Revenue' (1 change, 1 breaking)
14
+ !! D12 formula changed
15
+ - =SUM(D2:D11)
16
+ + =SUM(D2:D13)
17
+ affects 3 cells downstream: Revenue!E12, Revenue!F12, Summary!B4
18
+
19
+ Sheet 'Summary' (1 change)
20
+ ! B7 value changed
21
+ - 1200
22
+ + 1450
23
+
24
+ 1 formula; 1 value across 2 edited sheet(s). 1 breaking change reaching 3 downstream cells.
25
+ ```
26
+
27
+ ## Why
28
+
29
+ Two things go wrong when a spreadsheet changes and nobody looks closely.
30
+
31
+ The first is the change itself. `git diff` on an `.xlsx` gives you binary
32
+ noise, so the usual answer is to open both files side by side and squint.
33
+
34
+ The second is worse. A formula edit looks harmless on the sheet you are
35
+ looking at, but the cell feeds a summary three sheets away. The number that
36
+ matters is now wrong, and nothing tells you.
37
+
38
+ `sheetdelta` answers both. Every change is followed through the dependency
39
+ graph, so a formula edit reports the cells it reaches, and a CI job can fail
40
+ the build on exactly that.
41
+
42
+ ## Install
43
+
44
+ ```console
45
+ pip install sheetdelta
46
+ ```
47
+
48
+ No dependencies. The reader is `zipfile` and `xml.etree` from the standard
49
+ library, so it drops into any environment, including a build container that
50
+ has nothing but Python.
51
+
52
+ ## Usage
53
+
54
+ ### Compare two workbooks
55
+
56
+ ```console
57
+ sheetdelta diff old.xlsx new.xlsx
58
+ ```
59
+
60
+ Exit status is `0` when nothing breaking changed, `1` when something did, `2`
61
+ on an error. That makes it a CI check without any extra scripting:
62
+
63
+ ```yaml
64
+ - run: pip install sheetdelta
65
+ - run: sheetdelta diff before.xlsx after.xlsx --fail-on breaking
66
+ ```
67
+
68
+ `--fail-on` takes three values:
69
+
70
+ | Value | Fails when |
71
+ |---|---|
72
+ | `breaking` (default) | a change reaches another cell |
73
+ | `any` | anything changed at all |
74
+ | `never` | never; useful for a report-only run |
75
+
76
+ ### Machine-readable output
77
+
78
+ ```console
79
+ sheetdelta diff old.xlsx new.xlsx --json
80
+ ```
81
+
82
+ ```json
83
+ {
84
+ "old": "budget_v1.xlsx",
85
+ "new": "budget_v2.xlsx",
86
+ "has_changes": true,
87
+ "summary": {
88
+ "cell_changes": {"formula": 1, "value": 1},
89
+ "breaking": 1
90
+ },
91
+ "sheets": [
92
+ {
93
+ "kind": "changed",
94
+ "name": "Revenue",
95
+ "old_name": "Revenue",
96
+ "changes": [
97
+ {
98
+ "cell": "Revenue!D12",
99
+ "a1": "D12",
100
+ "kind": "formula",
101
+ "severity": "breaking",
102
+ "detail": "formula changed",
103
+ "old": "=SUM(D2:D11)",
104
+ "new": "=SUM(D2:D13)",
105
+ "affected": ["Revenue!E12", "Revenue!F12", "Summary!B4"]
106
+ }
107
+ ]
108
+ }
109
+ ]
110
+ }
111
+ ```
112
+
113
+ ### Check one workbook
114
+
115
+ ```console
116
+ sheetdelta audit workbook.xlsx
117
+ ```
118
+
119
+ An audit looks for the two defects that make a workbook unsound:
120
+
121
+ - a formula pointing at a sheet that does not exist, which Excel turns into
122
+ `#REF!` when it recalculates; and
123
+ - a circular reference, a cell that depends on itself through a chain of other
124
+ cells, which Excel refuses to calculate at all.
125
+
126
+ ```console
127
+ $ sheetdelta audit model.xlsx
128
+ model.xlsx
129
+ 3 sheet(s), 412 cell(s), 118 formula(s)
130
+
131
+ 1 broken reference(s):
132
+ !! C4 Removed Sheet!B2
133
+ sheet 'Removed Sheet' does not exist
134
+ ='Removed Sheet'!B2*1.2
135
+
136
+ 1 circular reference(s):
137
+ !! Revenue!D12 -> Revenue!E12 -> Summary!B4 -> Revenue!D12
138
+ ```
139
+
140
+ A reference to a cell that is simply empty is not reported. That is normal in
141
+ a spreadsheet, and flagging it would make the audit useless on real files.
142
+
143
+ ## What it reports
144
+
145
+ | Change | Severity |
146
+ |---|---|
147
+ | Formula changed, and other cells read it | breaking |
148
+ | Value changed, and other cells read it | breaking |
149
+ | Cell deleted, and other cells read it | breaking |
150
+ | Formula or value changed, nothing reads it | warning |
151
+ | Formula changed but the cached value did not move | stale |
152
+ | New cell | info |
153
+ | Sheet added, removed or renamed | info |
154
+
155
+ The **stale** case is worth explaining. Excel stores both a formula and the
156
+ last value it calculated for it. When a file is edited by something that does
157
+ not recalculate, the two disagree. Excel will happily show you the new
158
+ formula next to the old number, which is a quiet way to ship a wrong total.
159
+ `sheetdelta` flags it.
160
+
161
+ A **rename** is detected by matching the contents of a removed sheet against
162
+ an added one. Without that, renaming a sheet would look like every cell in it
163
+ changed and bury the real diff.
164
+
165
+ ## What it does not do
166
+
167
+ Being clear about this saves you time.
168
+
169
+ - **It does not evaluate formulas.** There is no calculation engine, and
170
+ there will not be one. Excel already stored the value it last computed, and
171
+ that is what gets compared. This is why the tool needs no Excel and no
172
+ dependencies, and why it is fast on large files.
173
+ - **It does not write or edit workbooks.** Read-only, by design.
174
+ - **It only reads `.xlsx` and `.xlsm`.** The old `.xls` and binary `.xlsb`
175
+ formats are rejected with a clear message rather than half-parsed.
176
+ - **It does not align rows across an insertion.** If a row is inserted near
177
+ the top, every cell below it reports as changed. Matching rows by content
178
+ is the hard part of spreadsheet diffing and is not implemented yet.
179
+ - **It does not read VBA macros** inside `.xlsm` files.
180
+
181
+ ## Using it as a library
182
+
183
+ ```python
184
+ from sheetdelta import diff_workbooks, read_workbook
185
+
186
+ result = diff_workbooks(read_workbook("old.xlsx"), read_workbook("new.xlsx"))
187
+
188
+ for change in result.breaking:
189
+ print(change.ref, "->", change.affected)
190
+ print(" was", change.old)
191
+ print(" now", change.new)
192
+ ```
193
+
194
+ Every type is exported from the package root, and the whole thing is typed.
195
+
196
+ ## How it works
197
+
198
+ An `.xlsx` file is a zip containing XML. `sheetdelta` reads the sheet list, the
199
+ shared string table, the number formats and each sheet's cells, then stops.
200
+
201
+ Each formula is scanned for the cells and ranges it mentions. That scan is
202
+ more careful than it looks: `LOG10(...)` looks exactly like a reference to
203
+ column LOG row 10, and `"see A1"` looks like a reference to A1. Both are
204
+ handled, because a dependency graph built on false references is worse than
205
+ no graph.
206
+
207
+ References are kept as ranges rather than expanded. `SUM(A:A)` covers a
208
+ million cells, and expanding it would cost more than the entire diff; the
209
+ graph asks whether a range contains a cell instead.
210
+
211
+ ## Requirements
212
+
213
+ Python 3.10 or newer. No runtime dependencies.
214
+
215
+ ## License
216
+
217
+ MIT.