Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- name: Build wheel and install
run: |
maturin build --release --out dist
pip install --find-links dist wolfxl
pip install --no-index --find-links dist wolfxl

- name: Run tests
run: pytest tests/ -v
Expand Down
12 changes: 12 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ classifiers = [
"Topic :: Office/Business :: Financial :: Spreadsheet",
]

[project.optional-dependencies]
calc = ["formulas>=1.3.3,<2.0"]

[project.urls]
Homepage = "https://github.com/SynthGL/wolfxl"
Repository = "https://github.com/SynthGL/wolfxl"
Expand All @@ -29,9 +32,18 @@ bindings = "pyo3"
module-name = "wolfxl._rust"
python-source = "python"

[tool.pyright]
pythonVersion = "3.12"
extraPaths = ["python"]

[tool.ruff]
line-length = 100

[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP"]

[tool.pytest.ini_options]
markers = ["slow: marks tests that are sensitive to CI timing"]

[tool.mypy]
strict = true
2 changes: 2 additions & 0 deletions python/wolfxl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
wb.save("out.xlsx")
"""

from __future__ import annotations

import os

from wolfxl._rust import __version__
Expand Down
52 changes: 51 additions & 1 deletion python/wolfxl/_workbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
from __future__ import annotations

import os
from typing import Any
from typing import TYPE_CHECKING, Any

from wolfxl._worksheet import Worksheet

if TYPE_CHECKING:
from wolfxl.calc._protocol import RecalcResult


class Workbook:
"""openpyxl-compatible workbook backed by Rust."""
Expand All @@ -23,6 +26,7 @@ def __init__(self) -> None:
self._rust_writer: Any = _rust.RustXlsxWriterBook()
self._rust_reader: Any = None
self._rust_patcher: Any = None
self._evaluator: Any = None
self._sheet_names: list[str] = ["Sheet"]
self._sheets: dict[str, Worksheet] = {}
self._sheets["Sheet"] = Worksheet(self, "Sheet")
Expand All @@ -36,6 +40,7 @@ def _from_reader(cls, path: str) -> Workbook:
wb = object.__new__(cls)
wb._rust_writer = None
wb._rust_patcher = None
wb._evaluator = None
wb._rust_reader = _rust.CalamineStyledBook.open(path)
names = [str(n) for n in wb._rust_reader.sheet_names()]
wb._sheet_names = names
Expand All @@ -51,6 +56,7 @@ def _from_patcher(cls, path: str) -> Workbook:

wb = object.__new__(cls)
wb._rust_writer = None
wb._evaluator = None
wb._rust_reader = _rust.CalamineStyledBook.open(path)
wb._rust_patcher = _rust.XlsxPatcher.open(path)
names = [str(n) for n in wb._rust_reader.sheet_names()]
Expand Down Expand Up @@ -118,6 +124,50 @@ def save(self, filename: str | os.PathLike[str]) -> None:
else:
raise RuntimeError("save requires write or modify mode")

# ------------------------------------------------------------------
# Formula evaluation (requires wolfxl.calc)
# ------------------------------------------------------------------

def calculate(self) -> dict[str, Any]:
"""Evaluate all formulas in the workbook.

Returns a dict of cell_ref -> computed value for all formula cells.
Requires the ``wolfxl.calc`` module (install via ``pip install wolfxl[calc]``).

The internal evaluator is cached so that a subsequent
:meth:`recalculate` call can reuse it without rescanning.
"""
from wolfxl.calc._evaluator import WorkbookEvaluator

ev = WorkbookEvaluator()
ev.load(self)
result = ev.calculate()
self._evaluator = ev # cache for recalculate()
return result
Comment on lines +140 to +146
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workbook.calculate() assigns self._evaluator, but _evaluator is never declared/initialized on the class. With [tool.mypy] strict = true, this typically triggers an "attribute-defined" error, and it also means reader/patcher constructors won't have the attribute. Initialize self._evaluator (e.g., to None) in __init__ and also set it on instances created via _from_reader/_from_patcher.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 668fd6e. _evaluator is now initialized to None in init, _from_reader, and _from_patcher. Strict mypy should be happy.


def recalculate(
self,
perturbations: dict[str, float | int],
tolerance: float = 1e-10,
) -> RecalcResult:
"""Perturb input cells and recompute affected formulas.
Comment on lines 148 to 153
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type of Workbook.recalculate() is annotated as Any, but the docstring says it returns RecalcResult, and callers/tests treat it as such. Please tighten the annotation to RecalcResult (using TYPE_CHECKING import to avoid hard runtime dependency if needed) so type checkers and IDEs get the correct contract.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 668fd6e. Return type tightened to RecalcResult using TYPE_CHECKING import to avoid hard runtime dependency on wolfxl.calc.


Returns a ``RecalcResult`` describing which cells changed.
Requires the ``wolfxl.calc`` module.

If :meth:`calculate` was called first, the cached evaluator is
reused (avoiding a full rescan + recalculate).
"""
ev = self._evaluator
if ev is None:
from wolfxl.calc._evaluator import WorkbookEvaluator

ev = WorkbookEvaluator()
ev.load(self)
ev.calculate()
self._evaluator = ev
return ev.recalculate(perturbations, tolerance)

# ------------------------------------------------------------------
# Context manager + cleanup
# ------------------------------------------------------------------
Expand Down
21 changes: 21 additions & 0 deletions python/wolfxl/calc/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""wolfxl.calc - Formula evaluation engine for wolfxl workbooks."""

from wolfxl.calc._evaluator import WorkbookEvaluator
from wolfxl.calc._functions import FUNCTION_WHITELIST_V1, FunctionRegistry, is_supported
from wolfxl.calc._graph import DependencyGraph
from wolfxl.calc._parser import FormulaParser, all_references, expand_range
from wolfxl.calc._protocol import CalcEngine, CellDelta, RecalcResult

__all__ = [
"CalcEngine",
"CellDelta",
"DependencyGraph",
"FUNCTION_WHITELIST_V1",
"FormulaParser",
"FunctionRegistry",
"RecalcResult",
"WorkbookEvaluator",
"all_references",
"expand_range",
"is_supported",
]
Loading