Skip to content

Auto-Generated Reference

This page renders the full public API from source docstrings via mkdocstrings. All symbols are importable from the top-level ezxl package.


Exceptions

EzXlError

EzXlError(message: str, cause: BaseException | None = None)

Bases: Exception

Base exception for all EzXl errors.

All exceptions raised by the EzXl library inherit from this class. Catching EzXlError is sufficient to handle any EzXl-originated failure without importing subclasses.

PARAMETER DESCRIPTION
message

Human-readable description of the error.

TYPE: str

cause

Original exception that triggered this error, if any.

TYPE: BaseException | None DEFAULT: None

Example

try: ... raise EzXlError("something went wrong") ... except EzXlError as e: ... print(e) something went wrong

Source code in src/ezxl/exceptions.py
def __init__(self, message: str, cause: BaseException | None = None) -> None:
    super().__init__(message)
    self.cause = cause
    if cause is not None:
        self.__cause__ = cause

options: show_source: false

ExcelNotAvailableError

ExcelNotAvailableError(message: str, cause: BaseException | None = None)

Bases: EzXlError

Raised when Excel is not open or the COM server is unreachable.

Typically thrown when win32com.client.Dispatch or win32com.client.GetActiveObject fails because no Excel process is running, or because the COM registration is broken.

PARAMETER DESCRIPTION
message

Human-readable description of the error.

TYPE: str

cause

Original exception that triggered this error, if any.

TYPE: BaseException | None DEFAULT: None

Example

raise ExcelNotAvailableError( ... "No running Excel instance found", cause=original_err ... )

Source code in src/ezxl/exceptions.py
def __init__(self, message: str, cause: BaseException | None = None) -> None:
    super().__init__(message, cause)

options: show_source: false

ExcelSessionLostError

ExcelSessionLostError(message: str, cause: BaseException | None = None)

Bases: EzXlError

Raised when an established COM connection is lost mid-operation.

This can happen when the user closes Excel while an automation session is active, or when Excel crashes. Unlike ExcelNotAvailableError, this implies a previously valid connection existed.

PARAMETER DESCRIPTION
message

Human-readable description of the error.

TYPE: str

cause

Original exception that triggered this error, if any.

TYPE: BaseException | None DEFAULT: None

Source code in src/ezxl/exceptions.py
def __init__(self, message: str, cause: BaseException | None = None) -> None:
    super().__init__(message, cause)

options: show_source: false

ExcelThreadViolationError

ExcelThreadViolationError(message: str, cause: BaseException | None = None)

Bases: EzXlError

Raised when a COM call is attempted from the wrong thread.

Excel COM operates under the Single-Threaded Apartment (STA) model. All COM calls must originate from the thread that created the ExcelApp instance. This exception is raised proactively before the COM call reaches the dispatcher to give a clear diagnostic.

PARAMETER DESCRIPTION
message

Human-readable description of the error.

TYPE: str

cause

Original exception that triggered this error, if any.

TYPE: BaseException | None DEFAULT: None

Source code in src/ezxl/exceptions.py
def __init__(self, message: str, cause: BaseException | None = None) -> None:
    super().__init__(message, cause)

options: show_source: false

WorkbookNotFoundError

WorkbookNotFoundError(message: str, cause: BaseException | None = None)

Bases: EzXlError

Raised when a workbook cannot be found by name in the Excel session.

PARAMETER DESCRIPTION
message

Human-readable description of the error.

TYPE: str

cause

Original exception that triggered this error, if any.

TYPE: BaseException | None DEFAULT: None

Example

raise WorkbookNotFoundError("No workbook named 'report.xlsx'")

Source code in src/ezxl/exceptions.py
def __init__(self, message: str, cause: BaseException | None = None) -> None:
    super().__init__(message, cause)

options: show_source: false

SheetNotFoundError

SheetNotFoundError(message: str, cause: BaseException | None = None)

Bases: EzXlError

Raised when a worksheet cannot be found by name in a workbook.

PARAMETER DESCRIPTION
message

Human-readable description of the error.

TYPE: str

cause

Original exception that triggered this error, if any.

TYPE: BaseException | None DEFAULT: None

Example

raise SheetNotFoundError("No sheet named 'Summary' in 'report.xlsx'")

Source code in src/ezxl/exceptions.py
def __init__(self, message: str, cause: BaseException | None = None) -> None:
    super().__init__(message, cause)

options: show_source: false

COMOperationError

COMOperationError(message: str, cause: BaseException | None = None)

Bases: EzXlError

Raised for unclassified COM errors that do not map to a specific subclass.

This is the catch-all wrapper for pywintypes.com_error exceptions. If a COM error can be identified as a lost session or unavailability issue, the more specific subclass should be used instead.

PARAMETER DESCRIPTION
message

Human-readable description of the error.

TYPE: str

cause

Original exception that triggered this error, if any.

TYPE: BaseException | None DEFAULT: None

Source code in src/ezxl/exceptions.py
def __init__(self, message: str, cause: BaseException | None = None) -> None:
    super().__init__(message, cause)

options: show_source: false

GUIOperationError

GUIOperationError(message: str, cause: BaseException | None = None)

Bases: EzXlError

Raised when a GUI-level COM operation fails for ribbon, menu, or dialog.

Distinct from COMOperationError to allow consumer code to differentiate between generic COM failures and failures that occur specifically within GUI interaction surfaces (ribbon, CommandBars, file dialogs, message boxes).

PARAMETER DESCRIPTION
message

Human-readable description of the error.

TYPE: str

cause

Original exception that triggered this error, if any.

TYPE: BaseException | None DEFAULT: None

Example

raise GUIOperationError( ... "Failed to execute ribbon command 'FileSave'", cause=exc ... )

Source code in src/ezxl/exceptions.py
def __init__(self, message: str, cause: BaseException | None = None) -> None:
    super().__init__(message, cause)

options: show_source: false

FormatterError

FormatterError(message: str, cause: BaseException | None = None)

Bases: EzXlError

Raised when an openpyxl-based formatting operation fails.

This exception covers errors that occur during closed-file formatting via ExcelFormatter, such as invalid cell references, unsupported style properties, or file I/O failures.

PARAMETER DESCRIPTION
message

Human-readable description of the error.

TYPE: str

cause

Original exception that triggered this error, if any.

TYPE: BaseException | None DEFAULT: None

Source code in src/ezxl/exceptions.py
def __init__(self, message: str, cause: BaseException | None = None) -> None:
    super().__init__(message, cause)

options: show_source: false


COM automation

ExcelApp

ExcelApp(mode: Literal['dispatch', 'attach'] = 'dispatch', visible: bool = True)

COM automation session for a single Excel Application instance.

Provides a unified interface for opening, navigating, and controlling Excel via COM, regardless of whether the Application object was dispatched (new process) or attached (existing process).

Threading

This class is not thread-safe. Excel COM uses the STA model. All method calls must originate from the thread that constructed the instance. Calls from other threads raise ExcelThreadViolationError immediately.

Lifecycle

Two usage patterns are supported:

Context manager (recommended for short, bounded sessions) — cleanup is automatic on exit::

with ExcelApp(mode="dispatch", visible=False) as xl:
    wb = xl.open("C:/reports/budget.xlsx")
    wb.save()
# Excel quit automatically

Manual lifecycle (long-running sessions, e.g. consumer libraries) — call :meth:quit explicitly when done. In attach mode, quit is optional; Excel stays running and the Python reference is released::

xl = ExcelApp(mode="attach")
wb = xl.open("C:/reports/budget.xlsx")
# ... many operations spread across multiple calls ...
xl.quit()   # optional in attach mode — Excel stays running

The COM object is resolved lazily on the first public method call; constructing an ExcelApp instance does not connect to COM.

PARAMETER DESCRIPTION
mode

"dispatch" to start a new Excel instance, or "attach" to bind to the already-running one.

TYPE: Literal['dispatch', 'attach'] DEFAULT: 'dispatch'

visible

Whether to make the Excel window visible. Only relevant in dispatch mode; ignored when attaching.

TYPE: bool DEFAULT: True

RAISES DESCRIPTION
ExcelNotAvailableError

If mode="attach" and no Excel instance is currently running.

Source code in src/ezxl/core/_excel_app.py
def __init__(
    self,
    mode: Literal["dispatch", "attach"] = "dispatch",
    visible: bool = True,
) -> None:
    self._mode: Literal["dispatch", "attach"] = mode
    self._visible: bool = visible
    # COM object resolved lazily on first access via _get_app().
    self._app: Any = None
    # Record the creating thread — enforced on every COM call.
    self._thread_id: int = threading.get_ident()
    logger.debug(
        "ExcelApp created (mode=%s, visible=%s, thread=%d)",
        mode,
        visible,
        self._thread_id,
    )

gui property

gui: GUIProxy

Access GUI interaction helpers (ribbon, menus, dialogs, keys).

Returns a GUIProxy that bundles all GUI automation surfaces. The proxy is created fresh on each access; it holds no state of its own beyond a reference to this ExcelApp instance.

Surfaces available via the proxy:

  • gui.ribbon — MSO ribbon execution and state queries.
  • gui.menu — Legacy CommandBar traversal and execution.
  • gui.dialog — File-open, file-save, and alert dialogs.
  • gui.send_keys(…)Application.SendKeys pass-through.
RETURNS DESCRIPTION
GUIProxy

Facade bound to this ExcelApp instance.

TYPE: GUIProxy

RAISES DESCRIPTION
ExcelThreadViolationError

If accessed from the wrong thread.

Example

with ExcelApp(mode="attach") as xl: ... xl.gui.ribbon.execute("FileSave") ... path = xl.gui.dialog.get_file_open()

hwnd property

hwnd: int

Win32 window handle of this Excel Application instance.

Returns the Application.Hwnd COM property. Used to bind pywinauto backends to the exact same Excel window managed by this ExcelApp session, preventing cross-instance interference when multiple Excel processes are running simultaneously.

RETURNS DESCRIPTION
int

The Win32 HWND of the Excel main window.

TYPE: int

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

COMOperationError

If the COM call fails.

Example

hwnd = xl.hwnd gui = GUIProxy(xl, keys=PywinautoKeysBackend(hwnd=hwnd))

open

open(path: str | Path) -> WorkbookProxy

Open a workbook file and return a proxy for it.

PARAMETER DESCRIPTION
path

Absolute path to the workbook file.

TYPE: str | Path

RETURNS DESCRIPTION
WorkbookProxy

A proxy bound to the opened workbook.

TYPE: WorkbookProxy

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

COMOperationError

If Excel cannot open the file.

Example

wb = xl.open("C:/data/report.xlsx")

Source code in src/ezxl/core/_excel_app.py
@wrap_com_error
def open(self, path: str | Path) -> WorkbookProxy:
    """Open a workbook file and return a proxy for it.

    Args:
        path: Absolute path to the workbook file.

    Returns:
        WorkbookProxy: A proxy bound to the opened workbook.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        COMOperationError: If Excel cannot open the file.

    Example:
        >>> wb = xl.open("C:/data/report.xlsx")
    """
    from ._workbook import WorkbookProxy  # local import avoids circular dep

    self._check_thread()
    resolved = Path(path).resolve()
    logger.debug("Opening workbook: %s", resolved)
    xl = self._get_app()
    xl.Workbooks.Open(str(resolved))
    # The newly opened workbook becomes the ActiveWorkbook.
    wb_name: str = xl.ActiveWorkbook.Name
    logger.debug("Workbook opened: %s", wb_name)
    printer.system(f"Workbook opened: {wb_name}")
    return WorkbookProxy(self, wb_name)

workbook

workbook(name: str | None = None) -> WorkbookProxy

Return a proxy for an already-open workbook.

PARAMETER DESCRIPTION
name

The workbook name as displayed in Excel's title bar (e.g. "report.xlsx"). Pass None to get the active workbook.

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
WorkbookProxy

A proxy bound to the named workbook.

TYPE: WorkbookProxy

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

WorkbookNotFoundError

If no workbook with that name is open.

COMOperationError

If the COM call fails.

Example

wb = xl.workbook("report.xlsx")

Source code in src/ezxl/core/_excel_app.py
@wrap_com_error
def workbook(self, name: str | None = None) -> WorkbookProxy:
    """Return a proxy for an already-open workbook.

    Args:
        name: The workbook name as displayed in Excel's title bar
            (e.g. ``"report.xlsx"``). Pass ``None`` to get the active
            workbook.

    Returns:
        WorkbookProxy: A proxy bound to the named workbook.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        WorkbookNotFoundError: If no workbook with that name is open.
        COMOperationError: If the COM call fails.

    Example:
        >>> wb = xl.workbook("report.xlsx")
    """
    from ._workbook import WorkbookProxy

    self._check_thread()
    xl = self._get_app()

    if name is None:
        wb_name: str = xl.ActiveWorkbook.Name
        logger.debug("Using active workbook: %s", wb_name)
        return WorkbookProxy(self, wb_name)

    # Validate the name exists before returning the proxy, so callers
    # get a clean WorkbookNotFoundError rather than a late COM fault.
    from ..exceptions import WorkbookNotFoundError

    for i in range(1, xl.Workbooks.Count + 1):
        if xl.Workbooks(i).Name == name:
            logger.debug("Resolved workbook by name: %s", name)
            return WorkbookProxy(self, name)

    raise WorkbookNotFoundError(
        f"No open workbook named '{name}'. "
        f"Open workbooks: {[xl.Workbooks(i).Name for i in range(1, xl.Workbooks.Count + 1)]}"
    )

run_macro

run_macro(name: str, *args: Any) -> Any

Execute a VBA macro by name with optional arguments.

PARAMETER DESCRIPTION
name

Fully qualified macro name (e.g. "Module1.MyMacro" or "'report.xlsm'!Module1.MyMacro").

TYPE: str

*args

Positional arguments forwarded to the macro.

TYPE: Any DEFAULT: ()

RETURNS DESCRIPTION
Any

The return value of the macro, if any.

TYPE: Any

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

COMOperationError

If the macro call fails.

Example

xl.run_macro("Module1.FormatSheet", "Sheet1")

Source code in src/ezxl/core/_excel_app.py
@wrap_com_error
def run_macro(self, name: str, *args: Any) -> Any:
    """Execute a VBA macro by name with optional arguments.

    Args:
        name: Fully qualified macro name (e.g. ``"Module1.MyMacro"`` or
            ``"'report.xlsm'!Module1.MyMacro"``).
        *args: Positional arguments forwarded to the macro.

    Returns:
        Any: The return value of the macro, if any.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        COMOperationError: If the macro call fails.

    Example:
        >>> xl.run_macro("Module1.FormatSheet", "Sheet1")
    """
    self._check_thread()
    logger.debug("Running macro: %s (args=%s)", name, args)
    xl = self._get_app()
    return xl.Run(name, *args)

execute_ribbon

execute_ribbon(mso_id: str) -> None

Execute a built-in ribbon command by its MSO identifier.

Uses Application.CommandBars.ExecuteMso to trigger any built-in Excel ribbon button programmatically without navigating a menu tree.

PARAMETER DESCRIPTION
mso_id

The MSO control identifier string (e.g. "FileSave", "Copy", "PasteValues").

TYPE: str

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

COMOperationError

If the MSO ID is unknown or execution fails.

Example

xl.execute_ribbon("FileSave")

Source code in src/ezxl/core/_excel_app.py
@wrap_com_error
def execute_ribbon(self, mso_id: str) -> None:
    """Execute a built-in ribbon command by its MSO identifier.

    Uses ``Application.CommandBars.ExecuteMso`` to trigger any built-in
    Excel ribbon button programmatically without navigating a menu tree.

    Args:
        mso_id: The MSO control identifier string
            (e.g. ``"FileSave"``, ``"Copy"``, ``"PasteValues"``).

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        COMOperationError: If the MSO ID is unknown or execution fails.

    Example:
        >>> xl.execute_ribbon("FileSave")
    """
    self._check_thread()
    logger.debug("Executing ribbon command: %s", mso_id)
    xl = self._get_app()
    xl.CommandBars.ExecuteMso(mso_id)

wait_ready

wait_ready(timeout: float = 30.0) -> None

Block until Excel reports it is ready.

Delegates to _com_utils.wait_until_ready. Useful after operations that trigger asynchronous recalculation or file I/O.

PARAMETER DESCRIPTION
timeout

Maximum seconds to wait. Defaults to 30.

TYPE: float DEFAULT: 30.0

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

COMOperationError

If the timeout is exceeded.

Example

xl.wait_ready(timeout=60.0)

Source code in src/ezxl/core/_excel_app.py
@wrap_com_error
def wait_ready(self, timeout: float = 30.0) -> None:
    """Block until Excel reports it is ready.

    Delegates to ``_com_utils.wait_until_ready``. Useful after
    operations that trigger asynchronous recalculation or file I/O.

    Args:
        timeout: Maximum seconds to wait. Defaults to 30.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        COMOperationError: If the timeout is exceeded.

    Example:
        >>> xl.wait_ready(timeout=60.0)
    """
    self._check_thread()
    logger.debug("Waiting for Excel to become ready (timeout=%.1fs).", timeout)
    wait_until_ready(self._get_app(), timeout)

quit

quit(save_changes: bool = False) -> None

Quit the Excel Application.

This method is safe to call multiple times; subsequent calls after the first are no-ops.

PARAMETER DESCRIPTION
save_changes

If True, Excel will prompt to save unsaved workbooks. If False, all unsaved changes are discarded. Defaults to False.

TYPE: bool DEFAULT: False

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

COMOperationError

If the quit call fails unexpectedly.

Example

xl.quit(save_changes=False)

Source code in src/ezxl/core/_excel_app.py
@wrap_com_error
def quit(self, save_changes: bool = False) -> None:
    """Quit the Excel Application.

    This method is safe to call multiple times; subsequent calls after
    the first are no-ops.

    Args:
        save_changes: If ``True``, Excel will prompt to save unsaved
            workbooks. If ``False``, all unsaved changes are discarded.
            Defaults to ``False``.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        COMOperationError: If the quit call fails unexpectedly.

    Example:
        >>> xl.quit(save_changes=False)
    """
    if self._app is None:
        return
    self._check_thread()
    logger.debug("Quitting Excel (save_changes=%s).", save_changes)
    try:
        self._app.Quit()
        printer.system("Excel.Application quit.")
    except Exception as exc:
        # Swallow errors on quit — the process may already be gone.
        logger.warning("Error during Excel quit (may be harmless): %s", exc)
    finally:
        self._app = None

options: show_source: false members: - init - enter - exit - open - workbook - run_macro - execute_ribbon - wait_ready - quit - gui - hwnd

WorkbookProxy

WorkbookProxy(app: ExcelApp, name: str)

COM proxy for a single Excel Workbook.

Instances are created by ExcelApp.open() or ExcelApp.workbook(). Do not instantiate directly.

All methods enforce COM thread safety by delegating to the parent ExcelApp's thread identity.

PARAMETER DESCRIPTION
app

The ExcelApp that owns this session.

TYPE: ExcelApp

name

The workbook name as shown in Excel's title bar (e.g. "budget.xlsx").

TYPE: str

Example

with ExcelApp() as xl: ... wb = xl.open("C:/data/report.xlsx") ... print(wb.name) ... wb.save() ... wb.close()

Source code in src/ezxl/core/_workbook.py
def __init__(self, app: ExcelApp, name: str) -> None:
    self._app = app
    self._name = name

name property

name: str

The workbook filename as shown in Excel's title bar.

RETURNS DESCRIPTION
str

Workbook name (e.g. "report.xlsx").

TYPE: str

sheets property

sheets: list[str]

List of all worksheet names in this workbook.

RETURNS DESCRIPTION
list[str]

list[str]: Sheet names in tab order.

RAISES DESCRIPTION
WorkbookNotFoundError

If the workbook is no longer open.

sheet

sheet(name: str) -> SheetProxy

Return a proxy for a named worksheet.

PARAMETER DESCRIPTION
name

The worksheet name (case-sensitive, as shown on the tab).

TYPE: str

RETURNS DESCRIPTION
SheetProxy

A proxy bound to the named worksheet.

TYPE: SheetProxy

RAISES DESCRIPTION
SheetNotFoundError

If no sheet with that name exists.

WorkbookNotFoundError

If the workbook is no longer open.

Example

ws = wb.sheet("Summary")

Source code in src/ezxl/core/_workbook.py
@wrap_com_error
def sheet(self, name: str) -> SheetProxy:
    """Return a proxy for a named worksheet.

    Args:
        name: The worksheet name (case-sensitive, as shown on the tab).

    Returns:
        SheetProxy: A proxy bound to the named worksheet.

    Raises:
        SheetNotFoundError: If no sheet with that name exists.
        WorkbookNotFoundError: If the workbook is no longer open.

    Example:
        >>> ws = wb.sheet("Summary")
    """
    from ._sheet import SheetProxy  # local import avoids circular dep

    self._check_thread()
    wb = self._get_wb()

    # Validate existence before constructing the proxy.
    for i in range(1, wb.Sheets.Count + 1):
        if wb.Sheets(i).Name == name:
            logger.debug("Resolved sheet '%s' in '%s'.", name, self._name)
            return SheetProxy(self, name)

    available = [wb.Sheets(i).Name for i in range(1, wb.Sheets.Count + 1)]
    raise SheetNotFoundError(
        f"No sheet named '{name}' in '{self._name}'. Available sheets: {available}"
    )

save

save() -> None

Save the workbook in place (equivalent to Ctrl+S).

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

WorkbookNotFoundError

If the workbook is no longer open.

COMOperationError

If the save fails.

Example

wb.save()

Source code in src/ezxl/core/_workbook.py
@wrap_com_error
def save(self) -> None:
    """Save the workbook in place (equivalent to Ctrl+S).

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        WorkbookNotFoundError: If the workbook is no longer open.
        COMOperationError: If the save fails.

    Example:
        >>> wb.save()
    """
    self._check_thread()
    logger.debug("Saving workbook: %s", self._name)
    self._get_wb().Save()
    printer.success(f"Workbook saved: {self._name}")

save_as

save_as(path: str | Path, fmt: str | None = None) -> None

Save the workbook to a new path, optionally changing its format.

Uses COM Workbook.SaveAs which keeps Excel open. Suitable for format conversion (e.g. xlsx → csv) via an active Excel session.

PARAMETER DESCRIPTION
path

Destination file path. The extension determines the format when fmt is None.

TYPE: str | Path

fmt

Explicit format override. Must be a key from the internal format map (e.g. ".csv", ".xlsx"). If omitted the extension of path is used.

TYPE: str | None DEFAULT: None

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

WorkbookNotFoundError

If the workbook is no longer open.

COMOperationError

If SaveAs fails.

ValueError

If the file extension cannot be mapped to a COM format.

Example

wb.save_as("C:/output/report.csv") wb.save_as("C:/output/report_backup.xlsx", fmt=".xlsx")

Source code in src/ezxl/core/_workbook.py
@wrap_com_error
def save_as(self, path: str | Path, fmt: str | None = None) -> None:
    """Save the workbook to a new path, optionally changing its format.

    Uses COM ``Workbook.SaveAs`` which keeps Excel open. Suitable for
    format conversion (e.g. xlsx → csv) via an active Excel session.

    Args:
        path: Destination file path. The extension determines the format
            when ``fmt`` is ``None``.
        fmt: Explicit format override. Must be a key from the internal
            format map (e.g. ``".csv"``, ``".xlsx"``). If omitted the
            extension of ``path`` is used.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        WorkbookNotFoundError: If the workbook is no longer open.
        COMOperationError: If SaveAs fails.
        ValueError: If the file extension cannot be mapped to a COM format.

    Example:
        >>> wb.save_as("C:/output/report.csv")
        >>> wb.save_as("C:/output/report_backup.xlsx", fmt=".xlsx")
    """
    self._check_thread()
    dest = Path(path).resolve()
    extension = fmt if fmt is not None else dest.suffix.lower()

    file_format: int | None = _FORMAT_MAP.get(extension)
    if file_format is None:
        raise ValueError(
            f"Unsupported file format '{extension}'. "
            f"Supported: {list(_FORMAT_MAP.keys())}"
        )

    logger.debug(
        "SaveAs workbook '%s' → '%s' (format=%d).", self._name, dest, file_format
    )
    wb = self._get_wb()

    # PDF export uses a different COM method.
    if extension == ".pdf":
        wb.ExportAsFixedFormat(0, str(dest))
    else:
        wb.SaveAs(str(dest), FileFormat=file_format)

    printer.success(f"Workbook saved as: {dest}")

close

close(save: bool = False) -> None

Close the workbook.

PARAMETER DESCRIPTION
save

If True, save changes before closing. Defaults to False (discard unsaved changes).

TYPE: bool DEFAULT: False

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

WorkbookNotFoundError

If the workbook is no longer open.

COMOperationError

If the close operation fails.

Example

wb.close(save=True)

Source code in src/ezxl/core/_workbook.py
@wrap_com_error
def close(self, save: bool = False) -> None:
    """Close the workbook.

    Args:
        save: If ``True``, save changes before closing. Defaults to
            ``False`` (discard unsaved changes).

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        WorkbookNotFoundError: If the workbook is no longer open.
        COMOperationError: If the close operation fails.

    Example:
        >>> wb.close(save=True)
    """
    self._check_thread()
    logger.debug("Closing workbook '%s' (save=%s).", self._name, save)
    self._get_wb().Close(SaveChanges=save)
    printer.system(f"Workbook closed: {self._name}")

options: show_source: false members: - name - sheets - sheet - save - save_as - close

SheetProxy

SheetProxy(workbook: WorkbookProxy, name: str)

COM proxy for a single Excel Worksheet.

Instances are created by WorkbookProxy.sheet(). Do not instantiate directly.

PARAMETER DESCRIPTION
workbook

The parent WorkbookProxy.

TYPE: WorkbookProxy

name

The worksheet name (as shown on the tab).

TYPE: str

Example

ws = wb.sheet("Data") ws.cell("A1").value = "Hello" ws.calculate()

Source code in src/ezxl/core/_sheet.py
def __init__(self, workbook: WorkbookProxy, name: str) -> None:
    self._workbook = workbook
    self._name = name

name property

name: str

The worksheet name as shown on the tab.

RETURNS DESCRIPTION
str

Sheet name.

TYPE: str

used_range property

used_range: RangeProxy

The smallest rectangle that contains all used cells.

RETURNS DESCRIPTION
RangeProxy

Proxy over the Worksheet.UsedRange COM range.

TYPE: RangeProxy

RAISES DESCRIPTION
SheetNotFoundError

If the sheet is no longer available.

cell

cell(ref: str) -> CellProxy

Return a proxy for a single cell.

PARAMETER DESCRIPTION
ref

Cell address in A1 notation (e.g. "B3").

TYPE: str

RETURNS DESCRIPTION
CellProxy

A proxy bound to the cell.

TYPE: CellProxy

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

SheetNotFoundError

If the sheet is no longer available.

Example

ws.cell("A1").value = 42

Source code in src/ezxl/core/_sheet.py
@wrap_com_error
def cell(self, ref: str) -> CellProxy:
    """Return a proxy for a single cell.

    Args:
        ref: Cell address in A1 notation (e.g. ``"B3"``).

    Returns:
        CellProxy: A proxy bound to the cell.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        SheetNotFoundError: If the sheet is no longer available.

    Example:
        >>> ws.cell("A1").value = 42
    """
    self._check_thread()
    # Validate the sheet exists before constructing the proxy.
    self._get_ws()
    return CellProxy(self, ref)

range

range(ref: str) -> RangeProxy

Return a proxy for a cell range.

PARAMETER DESCRIPTION
ref

Range address in A1 notation (e.g. "A1:D10").

TYPE: str

RETURNS DESCRIPTION
RangeProxy

A proxy bound to the range.

TYPE: RangeProxy

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

SheetNotFoundError

If the sheet is no longer available.

Example

rng = ws.range("A1:C5") data = rng.values

Source code in src/ezxl/core/_sheet.py
@wrap_com_error
def range(self, ref: str) -> RangeProxy:
    """Return a proxy for a cell range.

    Args:
        ref: Range address in A1 notation (e.g. ``"A1:D10"``).

    Returns:
        RangeProxy: A proxy bound to the range.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        SheetNotFoundError: If the sheet is no longer available.

    Example:
        >>> rng = ws.range("A1:C5")
        >>> data = rng.values
    """
    self._check_thread()
    self._get_ws()
    return RangeProxy(self, ref)

calculate

calculate() -> None

Trigger recalculation of all formulas on this sheet.

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

SheetNotFoundError

If the sheet is no longer available.

Example

ws.calculate()

Source code in src/ezxl/core/_sheet.py
@wrap_com_error
def calculate(self) -> None:
    """Trigger recalculation of all formulas on this sheet.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        SheetNotFoundError: If the sheet is no longer available.

    Example:
        >>> ws.calculate()
    """
    self._check_thread()
    logger.debug("Calculating sheet '%s'.", self._name)
    self._get_ws().Calculate()

options: show_source: false members: - name - used_range - cell - range - calculate

CellProxy

CellProxy(sheet: SheetProxy, ref: str)

COM proxy for a single cell in a worksheet.

Instances are created by SheetProxy.cell(). Do not instantiate directly.

PARAMETER DESCRIPTION
sheet

The parent SheetProxy.

TYPE: SheetProxy

ref

Cell address in A1 notation (e.g. "B3").

TYPE: str

Example

cell = ws.cell("C7") cell.value = 100 print(cell.formula) 100

Source code in src/ezxl/core/_sheet.py
def __init__(self, sheet: SheetProxy, ref: str) -> None:
    self._sheet = sheet
    self._ref = ref

address property

address: str

The absolute address of this cell (e.g. "$B$3").

RETURNS DESCRIPTION
str

Cell address in absolute notation.

TYPE: str

value property writable

value: Any

The cell's current value, normalised to a Python type.

COM date objects are converted to datetime. Error values (#N/A etc.) are returned as None with a WARNING log.

RETURNS DESCRIPTION
Any

The cell value, or None for empty/error cells.

TYPE: Any

formula property writable

formula: str

The formula string in the cell (e.g. "=SUM(A1:A5)").

Returns an empty string for cells with no formula.

RETURNS DESCRIPTION
str

Formula string, or empty string if none.

TYPE: str

options: show_source: false members: - address - value - formula

RangeProxy

RangeProxy(sheet: SheetProxy, ref: str)

COM proxy for a rectangular range of cells in a worksheet.

Instances are created by SheetProxy.range() or accessed via SheetProxy.used_range. Do not instantiate directly.

PARAMETER DESCRIPTION
sheet

The parent SheetProxy.

TYPE: SheetProxy

ref

Range address in A1 notation (e.g. "A1:D10").

TYPE: str

Example

rng = ws.range("A1:C3") data = rng.values # list[list[Any]] rng.values = [[1, 2, 3], ... [4, 5, 6], ... [7, 8, 9]]

Source code in src/ezxl/core/_sheet.py
def __init__(self, sheet: SheetProxy, ref: str) -> None:
    self._sheet = sheet
    self._ref = ref

address property

address: str

The address of this range (e.g. "A1:D10").

RETURNS DESCRIPTION
str

Range address.

TYPE: str

values property writable

values: list[list[Any]]

All cell values in the range as a list of rows.

Single-row or single-column ranges are normalised to a 2D list-of-lists for a consistent return type. COM dates are converted to datetime; error cells return None.

RETURNS DESCRIPTION
list[list[Any]]

list[list[Any]]: Row-major 2D list of cell values.

Example

data = ws.range("A1:C3").values data[0][] # row 1, col A

options: show_source: false members: - address - values


GUI interaction

GUIProxy

GUIProxy(app: ExcelApp, ribbon: AbstractRibbonBackend | None = None, menu: AbstractMenuBackend | None = None, dialog: AbstractDialogBackend | None = None, keys: AbstractKeysBackend | None = None, backstage: AbstractBackstageFileOps | None = None, backstage_nav: AbstractBackstageNavigator | None = None)

Unified facade for GUI-level Excel interaction.

Instantiated by ExcelApp.gui and bundles all six automation surfaces (ribbon, menu, dialog, keys, backstage, backstage_nav) under a single object.

Backend injection

Each surface can be replaced at construction time by passing an alternative implementation that satisfies the corresponding abstract protocol. This is the primary extension point for non-COM backends such as pywinauto. Passing None (the default) selects the standard COM implementation for that surface — except for backstage_nav, which defaults to None (no UIA navigator).

The backstage and backstage_nav surfaces are intentionally separate:

  • backstage (AbstractBackstageFileOps) — file I/O operations via COM. Focus-independent, locale-independent. Always present.
  • backstage_nav (AbstractBackstageNavigator | None) — visual panel navigation via UIA. Required for open_options and open_save_as_panel. Inject explicitly when needed.
PARAMETER DESCRIPTION
app

The active ExcelApp instance that owns this proxy.

TYPE: ExcelApp

ribbon

Optional ribbon backend. Defaults to :class:~ezxl.gui.RibbonProxy when None.

TYPE: AbstractRibbonBackend | None DEFAULT: None

menu

Optional menu backend. Defaults to :class:~ezxl.gui.MenuProxy when None.

TYPE: AbstractMenuBackend | None DEFAULT: None

dialog

Optional dialog backend. Defaults to :class:~ezxl.gui.DialogProxy when None.

TYPE: AbstractDialogBackend | None DEFAULT: None

keys

Optional keys backend. Defaults to :class:~ezxl.gui._gui_proxy._COMKeysBackend when None.

TYPE: AbstractKeysBackend | None DEFAULT: None

backstage

Optional Backstage file-ops backend. Defaults to :class:~ezxl.gui.win32com._backstage.COMBackstageBackend when None.

TYPE: AbstractBackstageFileOps | None DEFAULT: None

backstage_nav

Optional Backstage UIA navigator. Defaults to None (no UIA navigator). Pass a :class:~ezxl.gui.pywinauto.PywinautoBackstageBackend to enable open_options and open_save_as_panel.

TYPE: AbstractBackstageNavigator | None DEFAULT: None

Security note

When injecting pywinauto backends, always pass hwnd=app.hwnd to bind the backend to the exact Excel window managed by this session. Omitting hwnd causes pywinauto to attach to the first Excel window it finds, which may not be the correct one when multiple Excel instances are running.

Example

with ExcelApp(mode="attach") as xl: ... xl.gui.ribbon.execute("FileSave") ... xl.gui.backstage.save() ... path = xl.gui.dialog.get_file_open() ... xl.gui.send_keys("^{HOME}")

Example — with UIA navigator::

gui = GUIProxy(
    xl,
    backstage=COMBackstageBackend(xl),
    backstage_nav=PywinautoBackstageBackend(hwnd=xl.hwnd, locale="fr"),
)
gui.backstage.save()                        # COM: direct save
gui.backstage.save_as(path="out.xlsx")      # COM: format-aware save
gui.backstage_nav.open_options()            # UIA: opens Options panel
gui.backstage_nav.open_save_as_panel()      # UIA: opens panel only
Source code in src/ezxl/gui/_gui_proxy.py
def __init__(
    self,
    app: ExcelApp,
    ribbon: AbstractRibbonBackend | None = None,
    menu: AbstractMenuBackend | None = None,
    dialog: AbstractDialogBackend | None = None,
    keys: AbstractKeysBackend | None = None,
    backstage: AbstractBackstageFileOps | None = None,
    backstage_nav: AbstractBackstageNavigator | None = None,
) -> None:
    self._app = app
    # Construct default COM implementations when no override is provided.
    self._ribbon: AbstractRibbonBackend = (
        ribbon if ribbon is not None else RibbonProxy(app)
    )
    self._menu: AbstractMenuBackend = menu if menu is not None else MenuProxy(app)
    self._dialog: AbstractDialogBackend = (
        dialog if dialog is not None else DialogProxy(app)
    )
    self._keys: AbstractKeysBackend = (
        keys if keys is not None else _COMKeysBackend(app)
    )
    # COM backend for file operations — always present.
    self._backstage: AbstractBackstageFileOps = (
        backstage if backstage is not None else COMBackstageBackend(app)
    )
    # UIA navigator — optional; no default COM equivalent exists.
    self._backstage_nav: AbstractBackstageNavigator | None = backstage_nav

ribbon property

Return the ribbon backend for MSO ribbon command interaction.

RETURNS DESCRIPTION
AbstractRibbonBackend

The configured ribbon backend (default: :class:~ezxl.gui.RibbonProxy).

TYPE: AbstractRibbonBackend

Example

xl.gui.ribbon.execute("FileSave") xl.gui.ribbon.is_enabled("Copy") True

menu property

Return the menu backend for legacy CommandBar interaction.

RETURNS DESCRIPTION
AbstractMenuBackend

The configured menu backend (default: :class:~ezxl.gui.MenuProxy).

TYPE: AbstractMenuBackend

Example

xl.gui.menu.list_bars() ['Standard', 'Formatting', ...]

dialog property

Return the dialog backend for file picker and alert dialogs.

RETURNS DESCRIPTION
AbstractDialogBackend

The configured dialog backend (default: :class:~ezxl.gui.DialogProxy).

TYPE: AbstractDialogBackend

Example

path = xl.gui.dialog.get_file_open(title="Select report")

backstage property

Return the Backstage file-ops backend.

Handles save, save_as, open_file, and close_workbook via the COM object model — focus-independent and locale-independent.

The default backend is :class:~ezxl.gui.win32com._backstage.COMBackstageBackend. Replace it at construction time to swap the implementation::

gui = GUIProxy(xl, backstage=MyCustomFileOpsBackend(xl))
RETURNS DESCRIPTION
AbstractBackstageFileOps

The configured file-ops backend.

TYPE: AbstractBackstageFileOps

Example

xl.gui.backstage.save() xl.gui.backstage.save_as(path="C:\output.xlsx")

backstage_nav property

backstage_nav: AbstractBackstageNavigator | None

Return the Backstage UIA navigator, or None if not injected.

Handles open_options, open_save_as_panel, open_file, and close_workbook via UIA direct click. This backend must be injected explicitly — there is no COM default::

gui = GUIProxy(
    xl,
    backstage_nav=PywinautoBackstageBackend(
        hwnd=xl.hwnd, locale="fr"
    ),
)
RETURNS DESCRIPTION
AbstractBackstageNavigator | None

AbstractBackstageNavigator | None: The configured UIA navigator, or None if none was provided.

Example

if xl.gui.backstage_nav is not None: ... xl.gui.backstage_nav.open_options()

send_keys

send_keys(keys: str, wait: bool = True) -> None

Send a keystroke sequence to the Excel Application window.

Delegates to the configured :class:~ezxl.gui._protocols.AbstractKeysBackend. The keys string must use standard VBA SendKeys notation (e.g. "{ENTER}", "^s" for Ctrl+S).

PARAMETER DESCRIPTION
keys

Keystroke string in VBA SendKeys notation.

TYPE: str

wait

If True, block until Excel processes the keystrokes before returning. Defaults to True.

TYPE: bool DEFAULT: True

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

COMOperationError

If the SendKeys call fails.

Example

xl.gui.send_keys("^s") # Ctrl+S xl.gui.send_keys("{ESCAPE}")

Source code in src/ezxl/gui/_gui_proxy.py
def send_keys(self, keys: str, wait: bool = True) -> None:
    """Send a keystroke sequence to the Excel Application window.

    Delegates to the configured :class:`~ezxl.gui._protocols.AbstractKeysBackend`.
    The ``keys`` string must use standard VBA SendKeys notation
    (e.g. ``"{ENTER}"``, ``"^s"`` for Ctrl+S).

    Args:
        keys: Keystroke string in VBA SendKeys notation.
        wait: If ``True``, block until Excel processes the keystrokes
            before returning. Defaults to ``True``.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        COMOperationError: If the SendKeys call fails.

    Example:
        >>> xl.gui.send_keys("^s")           # Ctrl+S
        >>> xl.gui.send_keys("{ESCAPE}")
    """
    logger.debug("GUIProxy.send_keys: keys=%r, wait=%r", keys, wait)
    self._keys.send_keys(keys, wait)

options: show_source: false members: - init - ribbon - menu - dialog - backstage - backstage_nav - send_keys


GUI protocols (ABCs)

AbstractRibbonBackend

Bases: ABC

Contract for ribbon command execution and state queries.

Any class that implements this interface can be injected into :class:~ezxl.gui.GUIProxy as the ribbon backend, replacing the default COM-based implementation.

Implementations are responsible for thread-safety; the caller (:class:~ezxl.gui.GUIProxy) does not perform additional thread checks.

execute abstractmethod

execute(mso_id: str) -> None

Execute a built-in ribbon command by its MSO identifier.

PARAMETER DESCRIPTION
mso_id

MSO control identifier string (e.g. "FileSave", "Copy", "PasteValues").

TYPE: str

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

GUIOperationError

If the MSO ID is unknown or the command cannot be executed in the current application state.

Source code in src/ezxl/gui/_protocols.py
@abstractmethod
def execute(self, mso_id: str) -> None:
    """Execute a built-in ribbon command by its MSO identifier.

    Args:
        mso_id: MSO control identifier string
            (e.g. ``"FileSave"``, ``"Copy"``, ``"PasteValues"``).

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        GUIOperationError: If the MSO ID is unknown or the command
            cannot be executed in the current application state.
    """
    ...

is_enabled abstractmethod

is_enabled(mso_id: str) -> bool

Return whether a ribbon command is currently enabled.

PARAMETER DESCRIPTION
mso_id

MSO control identifier string.

TYPE: str

RETURNS DESCRIPTION
bool

True if the command is enabled; False otherwise.

TYPE: bool

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

GUIOperationError

If the MSO ID is unknown or the query fails.

Source code in src/ezxl/gui/_protocols.py
@abstractmethod
def is_enabled(self, mso_id: str) -> bool:
    """Return whether a ribbon command is currently enabled.

    Args:
        mso_id: MSO control identifier string.

    Returns:
        bool: ``True`` if the command is enabled; ``False`` otherwise.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        GUIOperationError: If the MSO ID is unknown or the query fails.
    """
    ...

is_pressed abstractmethod

is_pressed(mso_id: str) -> bool

Return whether a ribbon toggle command is currently pressed.

PARAMETER DESCRIPTION
mso_id

MSO control identifier string.

TYPE: str

RETURNS DESCRIPTION
bool

True if the toggle command is in the pressed/active state; False if not pressed or if the control does not support the pressed-state query.

TYPE: bool

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

Source code in src/ezxl/gui/_protocols.py
@abstractmethod
def is_pressed(self, mso_id: str) -> bool:
    """Return whether a ribbon toggle command is currently pressed.

    Args:
        mso_id: MSO control identifier string.

    Returns:
        bool: ``True`` if the toggle command is in the pressed/active
            state; ``False`` if not pressed or if the control does not
            support the pressed-state query.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
    """
    ...

is_visible abstractmethod

is_visible(mso_id: str) -> bool

Return whether a ribbon command is currently visible.

PARAMETER DESCRIPTION
mso_id

MSO control identifier string.

TYPE: str

RETURNS DESCRIPTION
bool

True if the command is visible in the current ribbon state; False otherwise.

TYPE: bool

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

GUIOperationError

If the MSO ID is unknown or the query fails.

Source code in src/ezxl/gui/_protocols.py
@abstractmethod
def is_visible(self, mso_id: str) -> bool:
    """Return whether a ribbon command is currently visible.

    Args:
        mso_id: MSO control identifier string.

    Returns:
        bool: ``True`` if the command is visible in the current ribbon
            state; ``False`` otherwise.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        GUIOperationError: If the MSO ID is unknown or the query fails.
    """
    ...

options: show_source: false members: - execute - is_enabled - is_pressed - is_visible

AbstractMenuBackend

Bases: ABC

Contract for legacy CommandBar traversal and control execution.

Any class that implements this interface can be injected into :class:~ezxl.gui.GUIProxy as the menu backend, replacing the default COM-based implementation.

click abstractmethod

click(bar_name: str, *item_path: str) -> None

Traverse a CommandBar by caption path and execute the final control.

PARAMETER DESCRIPTION
bar_name

The name of the CommandBar to start from.

TYPE: str

*item_path

One or more control captions forming the path to the target control. At least one caption is required.

TYPE: str DEFAULT: ()

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

GUIOperationError

If the bar, any intermediate control, or the final control cannot be found, or if execution fails.

Source code in src/ezxl/gui/_protocols.py
@abstractmethod
def click(self, bar_name: str, *item_path: str) -> None:
    """Traverse a CommandBar by caption path and execute the final control.

    Args:
        bar_name: The name of the CommandBar to start from.
        *item_path: One or more control captions forming the path to
            the target control. At least one caption is required.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        GUIOperationError: If the bar, any intermediate control, or
            the final control cannot be found, or if execution fails.
    """
    ...

list_bars abstractmethod

list_bars() -> list[str]

Return the names of all CommandBars registered with Excel.

RETURNS DESCRIPTION
list[str]

list[str]: Sorted list of CommandBar names.

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

GUIOperationError

If the COM collection cannot be iterated.

Source code in src/ezxl/gui/_protocols.py
@abstractmethod
def list_bars(self) -> list[str]:
    """Return the names of all CommandBars registered with Excel.

    Returns:
        list[str]: Sorted list of CommandBar names.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        GUIOperationError: If the COM collection cannot be iterated.
    """
    ...

list_controls abstractmethod

list_controls(bar_name: str) -> list[str]

Return the captions of all top-level controls in a CommandBar.

PARAMETER DESCRIPTION
bar_name

The name of the CommandBar to inspect.

TYPE: str

RETURNS DESCRIPTION
list[str]

list[str]: Captions of the bar's top-level controls, in order.

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

GUIOperationError

If the bar cannot be found or the controls collection cannot be iterated.

Source code in src/ezxl/gui/_protocols.py
@abstractmethod
def list_controls(self, bar_name: str) -> list[str]:
    """Return the captions of all top-level controls in a CommandBar.

    Args:
        bar_name: The name of the CommandBar to inspect.

    Returns:
        list[str]: Captions of the bar's top-level controls, in order.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        GUIOperationError: If the bar cannot be found or the controls
            collection cannot be iterated.
    """
    ...

options: show_source: false members: - click - list_bars - list_controls

AbstractDialogBackend

Bases: ABC

Contract for file-open, file-save, and message-box dialogs.

Any class that implements this interface can be injected into :class:~ezxl.gui.GUIProxy as the dialog backend, replacing the default COM/Win32-based implementation.

Default parameter values in implementing methods must match those declared on :class:~ezxl.gui.DialogProxy.

get_file_open abstractmethod

get_file_open(title: str = 'Open', initial_dir: str | None = None, filter: str = 'Excel Files (*.xls*), *.xls*') -> str | None

Show a file-open picker dialog and return the selected path.

PARAMETER DESCRIPTION
title

Dialog title bar text. Defaults to "Open".

TYPE: str DEFAULT: 'Open'

initial_dir

Directory to open the dialog in. If None, the backend chooses the initial directory.

TYPE: str | None DEFAULT: None

filter

File-type filter string. Defaults to Excel files.

TYPE: str DEFAULT: 'Excel Files (*.xls*), *.xls*'

RETURNS DESCRIPTION
str | None

str | None: Absolute path chosen by the user, or None if the dialog was cancelled.

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

GUIOperationError

If the underlying call fails.

Source code in src/ezxl/gui/_protocols.py
@abstractmethod
def get_file_open(
    self,
    title: str = "Open",
    initial_dir: str | None = None,
    filter: str = "Excel Files (*.xls*), *.xls*",
) -> str | None:
    """Show a file-open picker dialog and return the selected path.

    Args:
        title: Dialog title bar text. Defaults to ``"Open"``.
        initial_dir: Directory to open the dialog in. If ``None``,
            the backend chooses the initial directory.
        filter: File-type filter string. Defaults to Excel files.

    Returns:
        str | None: Absolute path chosen by the user, or ``None``
            if the dialog was cancelled.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        GUIOperationError: If the underlying call fails.
    """
    ...

get_file_save abstractmethod

get_file_save(title: str = 'Save As', initial_dir: str | None = None, filter: str = 'Excel Files (*.xlsx), *.xlsx') -> str | None

Show a file-save picker dialog and return the selected path.

PARAMETER DESCRIPTION
title

Dialog title bar text. Defaults to "Save As".

TYPE: str DEFAULT: 'Save As'

initial_dir

Directory to open the dialog in. If None, the backend chooses the initial directory.

TYPE: str | None DEFAULT: None

filter

File-type filter string. Defaults to .xlsx.

TYPE: str DEFAULT: 'Excel Files (*.xlsx), *.xlsx'

RETURNS DESCRIPTION
str | None

str | None: Absolute path chosen by the user, or None if the dialog was cancelled.

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

GUIOperationError

If the underlying call fails.

Source code in src/ezxl/gui/_protocols.py
@abstractmethod
def get_file_save(
    self,
    title: str = "Save As",
    initial_dir: str | None = None,
    filter: str = "Excel Files (*.xlsx), *.xlsx",
) -> str | None:
    """Show a file-save picker dialog and return the selected path.

    Args:
        title: Dialog title bar text. Defaults to ``"Save As"``.
        initial_dir: Directory to open the dialog in. If ``None``,
            the backend chooses the initial directory.
        filter: File-type filter string. Defaults to ``.xlsx``.

    Returns:
        str | None: Absolute path chosen by the user, or ``None``
            if the dialog was cancelled.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        GUIOperationError: If the underlying call fails.
    """
    ...

alert abstractmethod

alert(message: str, title: str = 'EzXl') -> None

Display a modal information message box.

PARAMETER DESCRIPTION
message

The body text displayed in the message box.

TYPE: str

title

Caption for the message box title bar. Defaults to "EzXl".

TYPE: str DEFAULT: 'EzXl'

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

GUIOperationError

If the underlying call fails.

Source code in src/ezxl/gui/_protocols.py
@abstractmethod
def alert(self, message: str, title: str = "EzXl") -> None:
    """Display a modal information message box.

    Args:
        message: The body text displayed in the message box.
        title: Caption for the message box title bar.
            Defaults to ``"EzXl"``.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        GUIOperationError: If the underlying call fails.
    """
    ...

options: show_source: false members: - get_file_open - get_file_save - alert

AbstractKeysBackend

Bases: ABC

Contract for keystroke injection into Excel.

Any class that implements this interface can be injected into :class:~ezxl.gui.GUIProxy as the keys backend, replacing the default COM-based implementation.

send_keys abstractmethod

send_keys(keys: str, wait: bool = True) -> None

Send a keystroke sequence to the Excel Application window.

PARAMETER DESCRIPTION
keys

Keystroke string in VBA SendKeys notation (e.g. "{ENTER}", "^s" for Ctrl+S).

TYPE: str

wait

If True, block until Excel processes the keystrokes before returning. Defaults to True.

TYPE: bool DEFAULT: True

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

COMOperationError

If the keystroke injection call fails.

Source code in src/ezxl/gui/_protocols.py
@abstractmethod
def send_keys(self, keys: str, wait: bool = True) -> None:
    """Send a keystroke sequence to the Excel Application window.

    Args:
        keys: Keystroke string in VBA SendKeys notation
            (e.g. ``"{ENTER}"``, ``"^s"`` for Ctrl+S).
        wait: If ``True``, block until Excel processes the keystrokes
            before returning. Defaults to ``True``.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        COMOperationError: If the keystroke injection call fails.
    """
    ...

options: show_source: false members: - send_keys

AbstractBackstageFileOps

Bases: ABC

Contract for Excel Backstage file operations via COM.

Covers the four operations that the COM object model executes reliably, focus-independently, and locale-independently: save, save_as, open_file, and close_workbook.

This contract is implemented by :class:~ezxl.gui.win32com.COMBackstageBackend. Inject it into :class:~ezxl.gui.GUIProxy via the backstage parameter::

gui = GUIProxy(xl, backstage=COMBackstageBackend(xl))

save abstractmethod

save() -> None

Save the active workbook.

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

WorkbookNotFoundError

If no workbook is currently open.

GUIOperationError

If the action cannot be completed.

Source code in src/ezxl/gui/_protocols.py
@abstractmethod
def save(self) -> None:
    """Save the active workbook.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        WorkbookNotFoundError: If no workbook is currently open.
        GUIOperationError: If the action cannot be completed.
    """
    ...

save_as abstractmethod

save_as(path: str | None = None) -> None

Save the active workbook under a new path, or open the Save As dialog.

PARAMETER DESCRIPTION
path

Absolute path for the new file, including extension. If None, the built-in Save As dialog is displayed for manual path selection.

TYPE: str | None DEFAULT: None

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

WorkbookNotFoundError

If no workbook is currently open.

GUIOperationError

If the panel cannot be opened or path entry fails.

Source code in src/ezxl/gui/_protocols.py
@abstractmethod
def save_as(self, path: str | None = None) -> None:
    """Save the active workbook under a new path, or open the Save As dialog.

    Args:
        path: Absolute path for the new file, including extension.
            If ``None``, the built-in Save As dialog is displayed for
            manual path selection.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        WorkbookNotFoundError: If no workbook is currently open.
        GUIOperationError: If the panel cannot be opened or path entry
            fails.
    """
    ...

open_file abstractmethod

open_file() -> None

Show the built-in Excel Open dialog.

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

GUIOperationError

If the dialog cannot be opened.

Source code in src/ezxl/gui/_protocols.py
@abstractmethod
def open_file(self) -> None:
    """Show the built-in Excel Open dialog.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        GUIOperationError: If the dialog cannot be opened.
    """
    ...

close_workbook abstractmethod

close_workbook() -> None

Close the active workbook without saving.

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

WorkbookNotFoundError

If no workbook is currently open.

GUIOperationError

If the action cannot be completed.

Source code in src/ezxl/gui/_protocols.py
@abstractmethod
def close_workbook(self) -> None:
    """Close the active workbook without saving.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        WorkbookNotFoundError: If no workbook is currently open.
        GUIOperationError: If the action cannot be completed.
    """
    ...

options: show_source: false members: - save - save_as - open_file - close_workbook

AbstractBackstageNavigator

Bases: ABC

Contract for Excel Backstage visual navigation via UI Automation.

Covers operations that require UIA-level interaction with the Backstage overlay: navigating to the Options panel, opening the Save As panel without confirming, and UIA-driven open/close actions.

This contract is implemented by :class:~ezxl.gui.pywinauto.PywinautoBackstageBackend. Inject it into :class:~ezxl.gui.GUIProxy via the backstage_nav parameter::

gui = GUIProxy(
    xl,
    backstage=COMBackstageBackend(xl),
    backstage_nav=PywinautoBackstageBackend(hwnd=xl.hwnd, locale="fr"),
)
Note

backstage_nav is optional — :class:~ezxl.gui.GUIProxy defaults it to None. Access it via xl.gui.backstage_nav; guard with an is not None check before calling if it may be absent.

open_options abstractmethod

open_options() -> None

Navigate to the Excel Options panel via the Backstage.

RAISES DESCRIPTION
GUIOperationError

If the Options panel cannot be reached.

Source code in src/ezxl/gui/_protocols.py
@abstractmethod
def open_options(self) -> None:
    """Navigate to the Excel Options panel via the Backstage.

    Raises:
        GUIOperationError: If the Options panel cannot be reached.
    """
    ...

open_save_as_panel abstractmethod

open_save_as_panel() -> None

Open the Save As panel in the Backstage without confirming a save.

Clicks the "Enregistrer sous" (or locale equivalent) ListItem and leaves the panel open for manual interaction.

RAISES DESCRIPTION
GUIOperationError

If the panel cannot be reached.

Source code in src/ezxl/gui/_protocols.py
@abstractmethod
def open_save_as_panel(self) -> None:
    """Open the Save As panel in the Backstage without confirming a save.

    Clicks the ``"Enregistrer sous"`` (or locale equivalent) ListItem
    and leaves the panel open for manual interaction.

    Raises:
        GUIOperationError: If the panel cannot be reached.
    """
    ...

open_file abstractmethod

open_file() -> None

Open the Open panel via the Backstage.

RAISES DESCRIPTION
GUIOperationError

If the panel cannot be reached.

Source code in src/ezxl/gui/_protocols.py
@abstractmethod
def open_file(self) -> None:
    """Open the Open panel via the Backstage.

    Raises:
        GUIOperationError: If the panel cannot be reached.
    """
    ...

close_workbook abstractmethod

close_workbook() -> None

Close the active workbook via a Backstage UIA click.

RAISES DESCRIPTION
GUIOperationError

If the action cannot be completed.

Source code in src/ezxl/gui/_protocols.py
@abstractmethod
def close_workbook(self) -> None:
    """Close the active workbook via a Backstage UIA click.

    Raises:
        GUIOperationError: If the action cannot be completed.
    """
    ...

options: show_source: false members: - open_options - open_save_as_panel - open_file - close_workbook


GUI backends — COM (win32com)

RibbonProxy

RibbonProxy(app: ExcelApp)

Bases: AbstractRibbonBackend

Ribbon command execution and state queries via MSO identifiers.

Wraps Application.CommandBars methods to execute and inspect built-in Excel ribbon commands without navigating the UI manually.

PARAMETER DESCRIPTION
app

The active ExcelApp instance that owns this proxy.

TYPE: ExcelApp

Example

proxy = RibbonProxy(xl) proxy.execute("FileSave") proxy.is_enabled("FileSave") True

Source code in src/ezxl/gui/win32com/_ribbon.py
def __init__(self, app: ExcelApp) -> None:
    self._app = app

execute

execute(mso_id: str) -> None

Execute a built-in ribbon command by its MSO identifier.

Calls Application.CommandBars.ExecuteMso(mso_id). Use this to trigger any standard Excel ribbon button programmatically.

PARAMETER DESCRIPTION
mso_id

MSO control identifier string (e.g. "FileSave", "Copy", "PasteValues").

TYPE: str

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

GUIOperationError

If the MSO ID is unknown or the command cannot be executed in the current application state.

Example

ribbon.execute("FileSave")

Source code in src/ezxl/gui/win32com/_ribbon.py
@wrap_com_error
def execute(self, mso_id: str) -> None:
    """Execute a built-in ribbon command by its MSO identifier.

    Calls ``Application.CommandBars.ExecuteMso(mso_id)``. Use this
    to trigger any standard Excel ribbon button programmatically.

    Args:
        mso_id: MSO control identifier string
            (e.g. ``"FileSave"``, ``"Copy"``, ``"PasteValues"``).

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        GUIOperationError: If the MSO ID is unknown or the command
            cannot be executed in the current application state.

    Example:
        >>> ribbon.execute("FileSave")
    """
    self._check_thread()
    logger.debug("RibbonProxy.execute: mso_id=%r", mso_id)
    try:
        self._command_bars().ExecuteMso(mso_id)
    except Exception as exc:
        # wrap_com_error handles pywintypes.com_error; this re-raise
        # catches any other unexpected error and surfaces it as a
        # GUIOperationError with full context.
        raise GUIOperationError(
            f"Failed to execute ribbon command {mso_id!r}: {exc}", cause=exc
        ) from exc

is_enabled

is_enabled(mso_id: str) -> bool

Return whether a ribbon command is currently enabled.

Calls Application.CommandBars.GetEnabledMso(mso_id).

PARAMETER DESCRIPTION
mso_id

MSO control identifier string.

TYPE: str

RETURNS DESCRIPTION
bool

True if the command is enabled; False otherwise.

TYPE: bool

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

GUIOperationError

If the MSO ID is unknown or the query fails.

Example

ribbon.is_enabled("FileSave") True

Source code in src/ezxl/gui/win32com/_ribbon.py
@wrap_com_error
def is_enabled(self, mso_id: str) -> bool:
    """Return whether a ribbon command is currently enabled.

    Calls ``Application.CommandBars.GetEnabledMso(mso_id)``.

    Args:
        mso_id: MSO control identifier string.

    Returns:
        bool: ``True`` if the command is enabled; ``False`` otherwise.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        GUIOperationError: If the MSO ID is unknown or the query fails.

    Example:
        >>> ribbon.is_enabled("FileSave")
        True
    """
    self._check_thread()
    logger.debug("RibbonProxy.is_enabled: mso_id=%r", mso_id)
    try:
        return bool(self._command_bars().GetEnabledMso(mso_id))
    except Exception as exc:
        raise GUIOperationError(
            f"Failed to query enabled state for {mso_id!r}: {exc}", cause=exc
        ) from exc

is_pressed

is_pressed(mso_id: str) -> bool

Return whether a ribbon toggle command is currently pressed.

Calls Application.CommandBars.GetPressedMso(mso_id).

PARAMETER DESCRIPTION
mso_id

MSO control identifier string.

TYPE: str

RETURNS DESCRIPTION
bool

True if the toggle command is in the pressed/active state; False if not pressed or if the control does not support the pressed-state query (e.g. regular buttons such as "FileSave" always return False).

TYPE: bool

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

Example

ribbon.is_pressed("Bold") False

Source code in src/ezxl/gui/win32com/_ribbon.py
@wrap_com_error
def is_pressed(self, mso_id: str) -> bool:
    """Return whether a ribbon toggle command is currently pressed.

    Calls ``Application.CommandBars.GetPressedMso(mso_id)``.

    Args:
        mso_id: MSO control identifier string.

    Returns:
        bool: ``True`` if the toggle command is in the pressed/active
            state; ``False`` if not pressed **or** if the control does not
            support the pressed-state query (e.g. regular buttons such as
            ``"FileSave"`` always return ``False``).

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.

    Example:
        >>> ribbon.is_pressed("Bold")
        False
    """
    self._check_thread()
    logger.debug("RibbonProxy.is_pressed: mso_id=%r", mso_id)
    try:
        return bool(self._command_bars().GetPressedMso(mso_id))
    except Exception:
        # GetPressedMso raises COM E_INVALIDARG for non-toggle controls
        # (regular buttons, split buttons, etc.). Treat as "not pressed"
        # rather than propagating an error — the control simply has no
        # pressed state.
        logger.debug(
            "RibbonProxy.is_pressed: %r does not support pressed-state query"
            " — returning False.",
            mso_id,
        )
        return False

is_visible

is_visible(mso_id: str) -> bool

Return whether a ribbon command is currently visible.

Calls Application.CommandBars.GetVisibleMso(mso_id).

PARAMETER DESCRIPTION
mso_id

MSO control identifier string.

TYPE: str

RETURNS DESCRIPTION
bool

True if the command is visible in the current ribbon state; False otherwise.

TYPE: bool

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

GUIOperationError

If the MSO ID is unknown or the query fails.

Example

ribbon.is_visible("FileSave") True

Source code in src/ezxl/gui/win32com/_ribbon.py
@wrap_com_error
def is_visible(self, mso_id: str) -> bool:
    """Return whether a ribbon command is currently visible.

    Calls ``Application.CommandBars.GetVisibleMso(mso_id)``.

    Args:
        mso_id: MSO control identifier string.

    Returns:
        bool: ``True`` if the command is visible in the current ribbon
            state; ``False`` otherwise.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        GUIOperationError: If the MSO ID is unknown or the query fails.

    Example:
        >>> ribbon.is_visible("FileSave")
        True
    """
    self._check_thread()
    logger.debug("RibbonProxy.is_visible: mso_id=%r", mso_id)
    try:
        return bool(self._command_bars().GetVisibleMso(mso_id))
    except Exception as exc:
        raise GUIOperationError(
            f"Failed to query visible state for {mso_id!r}: {exc}", cause=exc
        ) from exc

options: show_source: false members: - execute - is_enabled - is_pressed - is_visible

MenuProxy

MenuProxy(app: ExcelApp)

Bases: AbstractMenuBackend

Legacy CommandBar menu traversal and control execution.

Allows navigating an Excel CommandBar by name and executing controls by their caption path. Also exposes discovery helpers to list all available bars and the top-level controls of any given bar.

PARAMETER DESCRIPTION
app

The active ExcelApp instance that owns this proxy.

TYPE: ExcelApp

Example

proxy = MenuProxy(xl) proxy.list_bars() ['Standard', 'Formatting', ...] proxy.click("Standard", "Open")

Source code in src/ezxl/gui/win32com/_menu.py
def __init__(self, app: ExcelApp) -> None:
    self._app = app

click

click(bar_name: str, *item_path: str) -> None

Traverse a CommandBar by caption path and execute the final control.

Looks up the CommandBar named bar_name, then iterates through item_path — each element being the caption of a nested control — descending into sub-controls at each step. Calls .Execute() on the control identified by the last element.

PARAMETER DESCRIPTION
bar_name

The name of the CommandBar to start from (e.g. "Standard").

TYPE: str

*item_path

One or more control captions forming the path to the target control. At least one caption is required.

TYPE: str DEFAULT: ()

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

GUIOperationError

If the bar, any intermediate control, or the final control cannot be found, or if Execute fails.

Example

menu.click("Standard", "Open") menu.click("Tools", "Macros", "Visual Basic Editor")

Source code in src/ezxl/gui/win32com/_menu.py
@wrap_com_error
def click(self, bar_name: str, *item_path: str) -> None:
    """Traverse a CommandBar by caption path and execute the final control.

    Looks up the CommandBar named ``bar_name``, then iterates through
    ``item_path`` — each element being the caption of a nested control —
    descending into sub-controls at each step. Calls ``.Execute()`` on
    the control identified by the last element.

    Args:
        bar_name: The name of the CommandBar to start from
            (e.g. ``"Standard"``).
        *item_path: One or more control captions forming the path to
            the target control. At least one caption is required.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        GUIOperationError: If the bar, any intermediate control, or
            the final control cannot be found, or if ``Execute``
            fails.

    Example:
        >>> menu.click("Standard", "Open")
        >>> menu.click("Tools", "Macros", "Visual Basic Editor")
    """
    self._check_thread()
    if not item_path:
        raise GUIOperationError(
            "click() requires at least one item caption in item_path."
        )
    logger.debug("MenuProxy.click: bar=%r, path=%r", bar_name, item_path)
    try:
        bars = self._command_bars()
        bar = bars(bar_name)
    except Exception as exc:
        raise GUIOperationError(
            f"CommandBar {bar_name!r} not found: {exc}", cause=exc
        ) from exc

    # Traverse the control path, descending into sub-controls.
    current_controls: Any = bar.Controls
    for caption in item_path[:-1]:
        control = _find_control(current_controls, caption)
        if control is None:
            raise GUIOperationError(
                f"Control {caption!r} not found in bar {bar_name!r} "
                f"at caption path {item_path!r}."
            )
        # Sub-menu controls expose a .Controls collection.
        ctrl: Any = control
        try:
            current_controls = ctrl.Controls
        except Exception as exc:
            raise GUIOperationError(
                f"Control {caption!r} has no sub-controls "
                f"(cannot descend further in path {item_path!r}): {exc}",
                cause=exc,
            ) from exc

    final_caption = item_path[-1]
    final_control = _find_control(current_controls, final_caption)
    if final_control is None:
        raise GUIOperationError(
            f"Final control {final_caption!r} not found "
            f"in bar {bar_name!r} at path {item_path!r}."
        )
    logger.debug("MenuProxy.click: executing control %r", final_caption)
    final_ctrl: Any = final_control
    try:
        final_ctrl.Execute()
    except Exception as exc:
        raise GUIOperationError(
            f"Execute() failed for control {final_caption!r} "
            f"in bar {bar_name!r}: {exc}",
            cause=exc,
        ) from exc

list_bars

list_bars() -> list[str]

Return the names of all CommandBars registered with Excel.

Iterates the Application.CommandBars collection and collects each bar's .Name attribute. Bars with no name are skipped.

RETURNS DESCRIPTION
list[str]

list[str]: Sorted list of CommandBar names.

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

GUIOperationError

If the COM collection cannot be iterated.

Example

menu.list_bars() ['3-D Settings', 'Borders', 'Cell', ...]

Source code in src/ezxl/gui/win32com/_menu.py
@wrap_com_error
def list_bars(self) -> list[str]:
    """Return the names of all CommandBars registered with Excel.

    Iterates the ``Application.CommandBars`` collection and collects
    each bar's ``.Name`` attribute. Bars with no name are skipped.

    Returns:
        list[str]: Sorted list of CommandBar names.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        GUIOperationError: If the COM collection cannot be iterated.

    Example:
        >>> menu.list_bars()
        ['3-D Settings', 'Borders', 'Cell', ...]
    """
    self._check_thread()
    logger.debug("MenuProxy.list_bars")
    try:
        bars = self._command_bars()
        names: list[str] = []
        for i in range(1, bars.Count + 1):
            try:
                name: str = bars(i).Name
                if name:
                    names.append(name)
            except Exception as exc:
                # Individual bars may be inaccessible (e.g., protected
                # add-in toolbars). Skip and log at DEBUG.
                logger.debug(
                    "list_bars: skipping bar %d (inaccessible): %s", i, exc
                )
                continue
        return sorted(names)
    except GUIOperationError:
        raise
    except Exception as exc:
        raise GUIOperationError(
            f"Failed to list CommandBars: {exc}", cause=exc
        ) from exc

list_controls

list_controls(bar_name: str) -> list[str]

Return the captions of all top-level controls in a CommandBar.

Iterates the .Controls collection of the named bar. Controls that have no Caption (e.g. separator lines) are skipped.

PARAMETER DESCRIPTION
bar_name

The name of the CommandBar to inspect.

TYPE: str

RETURNS DESCRIPTION
list[str]

list[str]: Captions of the bar's top-level controls, in order.

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

GUIOperationError

If the bar cannot be found or the controls collection cannot be iterated.

Example

menu.list_controls("Standard") ['New', 'Open', 'Save', ...]

Source code in src/ezxl/gui/win32com/_menu.py
@wrap_com_error
def list_controls(self, bar_name: str) -> list[str]:
    """Return the captions of all top-level controls in a CommandBar.

    Iterates the ``.Controls`` collection of the named bar. Controls
    that have no ``Caption`` (e.g. separator lines) are skipped.

    Args:
        bar_name: The name of the CommandBar to inspect.

    Returns:
        list[str]: Captions of the bar's top-level controls, in order.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        GUIOperationError: If the bar cannot be found or the controls
            collection cannot be iterated.

    Example:
        >>> menu.list_controls("Standard")
        ['New', 'Open', 'Save', ...]
    """
    self._check_thread()
    logger.debug("MenuProxy.list_controls: bar=%r", bar_name)
    try:
        bars = self._command_bars()
        bar = bars(bar_name)
    except Exception as exc:
        raise GUIOperationError(
            f"CommandBar {bar_name!r} not found: {exc}", cause=exc
        ) from exc
    try:
        controls = bar.Controls
        captions: list[str] = []
        for i in range(1, controls.Count + 1):
            try:
                caption: str = controls(i).Caption
                if caption:
                    captions.append(caption)
            except Exception as exc:
                logger.debug(
                    "list_controls: skipping control %d (inaccessible): %s", i, exc
                )
                continue
        return captions
    except GUIOperationError:
        raise
    except Exception as exc:
        raise GUIOperationError(
            f"Failed to list controls for bar {bar_name!r}: {exc}", cause=exc
        ) from exc

options: show_source: false members: - click - list_bars - list_controls

DialogProxy

DialogProxy(app: ExcelApp)

Bases: AbstractDialogBackend

File and message dialog helpers backed by Excel COM and Win32.

Provides a clean Python interface for the three most common GUI dialogs needed during Excel automation: open-file picker, save-file picker, and a simple information alert.

PARAMETER DESCRIPTION
app

The active ExcelApp instance that owns this proxy.

TYPE: ExcelApp

Example

proxy = DialogProxy(xl) path = proxy.get_file_open(title="Select a report") if path: ... wb = xl.open(path)

Source code in src/ezxl/gui/win32com/_dialog.py
def __init__(self, app: ExcelApp) -> None:
    self._app = app

get_file_open

get_file_open(title: str = 'Open', initial_dir: str | None = None, filter: str = 'Excel Files (*.xls*), *.xls*') -> str | None

Show Excel's built-in Open file picker dialog.

Calls Application.GetOpenFilename. The dialog is modal; this method blocks until the user confirms or cancels.

PARAMETER DESCRIPTION
title

Dialog title bar text. Defaults to "Open".

TYPE: str DEFAULT: 'Open'

initial_dir

Directory to open the dialog in. If None, Excel uses its current working directory.

TYPE: str | None DEFAULT: None

filter

File-type filter string in Excel's two-part format: "<description>, <wildcard>". Defaults to Excel files.

TYPE: str DEFAULT: 'Excel Files (*.xls*), *.xls*'

RETURNS DESCRIPTION
str | None

str | None: Absolute path chosen by the user, or None if the dialog was cancelled.

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

GUIOperationError

If the COM call fails.

Example

path = dialog.get_file_open(title="Pick a workbook") if path is not None: ... xl.open(path)

Source code in src/ezxl/gui/win32com/_dialog.py
@wrap_com_error
def get_file_open(
    self,
    title: str = "Open",
    initial_dir: str | None = None,
    filter: str = "Excel Files (*.xls*), *.xls*",
) -> str | None:
    """Show Excel's built-in Open file picker dialog.

    Calls ``Application.GetOpenFilename``. The dialog is modal;
    this method blocks until the user confirms or cancels.

    Args:
        title: Dialog title bar text. Defaults to ``"Open"``.
        initial_dir: Directory to open the dialog in. If ``None``,
            Excel uses its current working directory.
        filter: File-type filter string in Excel's two-part format:
            ``"<description>, <wildcard>"``. Defaults to Excel files.

    Returns:
        str | None: Absolute path chosen by the user, or ``None``
            if the dialog was cancelled.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        GUIOperationError: If the COM call fails.

    Example:
        >>> path = dialog.get_file_open(title="Pick a workbook")
        >>> if path is not None:
        ...     xl.open(path)
    """
    self._check_thread()
    logger.debug(
        "DialogProxy.get_file_open: title=%r, initial_dir=%r, filter=%r",
        title,
        initial_dir,
        filter,
    )
    xl = self._app._get_app()

    # Optionally change the initial directory for the duration of the
    # dialog, then restore it to avoid side-effects.
    original_dir: str | None = None
    if initial_dir is not None:
        try:
            original_dir = xl.DefaultFilePath
            xl.DefaultFilePath = initial_dir
        except Exception:
            # Non-critical; continue without setting the directory.
            original_dir = None

    try:
        result = xl.GetOpenFilename(
            FileFilter=filter,
            Title=title,
        )
    except Exception as exc:
        raise GUIOperationError(
            f"GetOpenFilename failed: {exc}", cause=exc
        ) from exc
    finally:
        if original_dir is not None:
            with contextlib.suppress(Exception):
                xl.DefaultFilePath = original_dir

    # Excel returns False (boolean) when the user cancels.
    if result is False or result == "False":
        logger.debug("DialogProxy.get_file_open: cancelled by user.")
        return None

    logger.debug("DialogProxy.get_file_open: selected=%r", result)
    return str(result)

get_file_save

get_file_save(title: str = 'Save As', initial_dir: str | None = None, filter: str = 'Excel Files (*.xlsx), *.xlsx') -> str | None

Show Excel's built-in Save As file picker dialog.

Calls Application.GetSaveAsFilename. The dialog is modal; this method blocks until the user confirms or cancels.

PARAMETER DESCRIPTION
title

Dialog title bar text. Defaults to "Save As".

TYPE: str DEFAULT: 'Save As'

initial_dir

Directory to open the dialog in. If None, Excel uses its current working directory.

TYPE: str | None DEFAULT: None

filter

File-type filter string in Excel's two-part format: "<description>, <wildcard>". Defaults to .xlsx.

TYPE: str DEFAULT: 'Excel Files (*.xlsx), *.xlsx'

RETURNS DESCRIPTION
str | None

str | None: Absolute path chosen by the user, or None if the dialog was cancelled.

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

GUIOperationError

If the COM call fails.

Example

path = dialog.get_file_save(title="Save report as") if path is not None: ... wb.save_as(path)

Source code in src/ezxl/gui/win32com/_dialog.py
@wrap_com_error
def get_file_save(
    self,
    title: str = "Save As",
    initial_dir: str | None = None,
    filter: str = "Excel Files (*.xlsx), *.xlsx",
) -> str | None:
    """Show Excel's built-in Save As file picker dialog.

    Calls ``Application.GetSaveAsFilename``. The dialog is modal;
    this method blocks until the user confirms or cancels.

    Args:
        title: Dialog title bar text. Defaults to ``"Save As"``.
        initial_dir: Directory to open the dialog in. If ``None``,
            Excel uses its current working directory.
        filter: File-type filter string in Excel's two-part format:
            ``"<description>, <wildcard>"``. Defaults to ``.xlsx``.

    Returns:
        str | None: Absolute path chosen by the user, or ``None``
            if the dialog was cancelled.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        GUIOperationError: If the COM call fails.

    Example:
        >>> path = dialog.get_file_save(title="Save report as")
        >>> if path is not None:
        ...     wb.save_as(path)
    """
    self._check_thread()
    logger.debug(
        "DialogProxy.get_file_save: title=%r, initial_dir=%r, filter=%r",
        title,
        initial_dir,
        filter,
    )
    xl = self._app._get_app()

    original_dir: str | None = None
    if initial_dir is not None:
        try:
            original_dir = xl.DefaultFilePath
            xl.DefaultFilePath = initial_dir
        except Exception:
            original_dir = None

    try:
        result = xl.GetSaveAsFilename(
            FileFilter=filter,
            Title=title,
        )
    except Exception as exc:
        raise GUIOperationError(
            f"GetSaveAsFilename failed: {exc}", cause=exc
        ) from exc
    finally:
        if original_dir is not None:
            with contextlib.suppress(Exception):
                xl.DefaultFilePath = original_dir

    if result is False or result == "False":
        logger.debug("DialogProxy.get_file_save: cancelled by user.")
        return None

    logger.debug("DialogProxy.get_file_save: selected=%r", result)
    return str(result)

alert

alert(message: str, title: str = 'EzXl') -> None

Display a modal information message box.

Uses ctypes.windll.user32.MessageBoxW (Win32 API) directly. This approach avoids any dependency on a running VBA environment and does not require Excel to have a document open.

The dialog shows a single OK button with an information icon. This method blocks until the user dismisses the dialog.

PARAMETER DESCRIPTION
message

The body text displayed in the message box.

TYPE: str

title

Caption for the message box title bar. Defaults to "EzXl".

TYPE: str DEFAULT: 'EzXl'

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

GUIOperationError

If the Win32 MessageBoxW call fails.

Example

dialog.alert("Export complete.", title="Success")

Source code in src/ezxl/gui/win32com/_dialog.py
def alert(self, message: str, title: str = "EzXl") -> None:
    """Display a modal information message box.

    Uses ``ctypes.windll.user32.MessageBoxW`` (Win32 API) directly.
    This approach avoids any dependency on a running VBA environment
    and does not require Excel to have a document open.

    The dialog shows a single OK button with an information icon.
    This method blocks until the user dismisses the dialog.

    Args:
        message: The body text displayed in the message box.
        title: Caption for the message box title bar.
            Defaults to ``"EzXl"``.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        GUIOperationError: If the Win32 MessageBoxW call fails.

    Example:
        >>> dialog.alert("Export complete.", title="Success")
    """
    self._check_thread()
    logger.debug("DialogProxy.alert: title=%r, message=%r", title, message)
    try:
        # HWND=0 → desktop owner (no parent window).
        ctypes.windll.user32.MessageBoxW(
            0,
            str(message),
            str(title),
            _MB_OK | _MB_ICONINFORMATION,
        )
    except Exception as exc:
        raise GUIOperationError(f"MessageBoxW failed: {exc}", cause=exc) from exc

options: show_source: false members: - get_file_open - get_file_save - alert

COMBackstageBackend

COMBackstageBackend(app: ExcelApp)

Bases: AbstractBackstageFileOps

Excel Backstage file operations via the COM object model.

Implements :class:~ezxl.gui._protocols.AbstractBackstageFileOps using Excel's COM API. All operations are focus-independent and locale-independent.

This is the default backstage backend for :class:~ezxl.gui.GUIProxy. For UIA-based Backstage navigation (Options panel, visual panel opening), inject a :class:~ezxl.gui.pywinauto.PywinautoBackstageBackend as the backstage_nav argument::

gui = GUIProxy(
    xl,
    backstage=COMBackstageBackend(xl),
    backstage_nav=PywinautoBackstageBackend(hwnd=xl.hwnd, locale="fr"),
)
PARAMETER DESCRIPTION
app

The active ExcelApp instance that owns this backend.

TYPE: ExcelApp

Example

backend = COMBackstageBackend(xl) backend.save() backend.save_as(path="C:\Reports\output.xlsx") backend.open_file() backend.close_workbook()

Source code in src/ezxl/gui/win32com/_backstage.py
def __init__(self, app: ExcelApp) -> None:
    self._app = app

save

save() -> None

Save the active workbook via ActiveWorkbook.Save().

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

WorkbookNotFoundError

If no workbook is currently open.

COMOperationError

If the COM call fails.

Example

backend.save()

Source code in src/ezxl/gui/win32com/_backstage.py
@wrap_com_error
def save(self) -> None:
    """Save the active workbook via ``ActiveWorkbook.Save()``.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        WorkbookNotFoundError: If no workbook is currently open.
        COMOperationError: If the COM call fails.

    Example:
        >>> backend.save()
    """
    self._check_thread()
    logger.debug("COMBackstageBackend.save")
    self._active_workbook().Save()

save_as

save_as(path: str | None = None) -> None

Save the active workbook under a new path, or show the Save As dialog.

If path is provided, calls ActiveWorkbook.SaveAs(Filename=path) directly — no dialog is shown. If path is None, opens the built-in Excel Save As dialog (xlDialogSaveAs), which blocks until the user dismisses it.

PARAMETER DESCRIPTION
path

Absolute path for the new file. If None, the built-in dialog is displayed for manual path selection.

TYPE: str | None DEFAULT: None

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

WorkbookNotFoundError

If no workbook is currently open.

COMOperationError

If the COM call fails.

Example

backend.save_as() # opens dialog backend.save_as(path="C:\output.xlsx") # direct save

Source code in src/ezxl/gui/win32com/_backstage.py
@wrap_com_error
def save_as(self, path: str | None = None) -> None:
    """Save the active workbook under a new path, or show the Save As dialog.

    If *path* is provided, calls ``ActiveWorkbook.SaveAs(Filename=path)``
    directly — no dialog is shown.  If *path* is ``None``, opens the
    built-in Excel Save As dialog (``xlDialogSaveAs``), which blocks until
    the user dismisses it.

    Args:
        path: Absolute path for the new file.  If ``None``, the built-in
            dialog is displayed for manual path selection.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        WorkbookNotFoundError: If no workbook is currently open.
        COMOperationError: If the COM call fails.

    Example:
        >>> backend.save_as()                              # opens dialog
        >>> backend.save_as(path="C:\\\\output.xlsx")     # direct save
    """
    import pathlib

    self._check_thread()
    logger.debug("COMBackstageBackend.save_as: path=%r", path)
    if path is not None:
        app = self._get_app()
        fmt = _EXT_TO_FILE_FORMAT.get(pathlib.Path(path).suffix.lower())
        app.DisplayAlerts = False
        try:
            if fmt is not None:
                self._active_workbook().SaveAs(Filename=path, FileFormat=fmt)
            else:
                self._active_workbook().SaveAs(Filename=path)
        finally:
            app.DisplayAlerts = True
    else:
        self._get_app().Dialogs(_XL_DIALOG_SAVE_AS).Show()

open_file

open_file() -> None

Show the built-in Excel Open dialog (xlDialogOpen).

The dialog blocks until the user selects a file or cancels.

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

COMOperationError

If the COM call fails.

Example

backend.open_file()

Source code in src/ezxl/gui/win32com/_backstage.py
@wrap_com_error
def open_file(self) -> None:
    """Show the built-in Excel Open dialog (``xlDialogOpen``).

    The dialog blocks until the user selects a file or cancels.

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        COMOperationError: If the COM call fails.

    Example:
        >>> backend.open_file()
    """
    self._check_thread()
    logger.debug("COMBackstageBackend.open_file")
    self._get_app().Dialogs(_XL_DIALOG_OPEN).Show()

close_workbook

close_workbook() -> None

Close the active workbook without saving (SaveChanges=False).

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

WorkbookNotFoundError

If no workbook is currently open.

COMOperationError

If the COM call fails.

Example

backend.close_workbook()

Source code in src/ezxl/gui/win32com/_backstage.py
@wrap_com_error
def close_workbook(self) -> None:
    """Close the active workbook without saving (``SaveChanges=False``).

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        WorkbookNotFoundError: If no workbook is currently open.
        COMOperationError: If the COM call fails.

    Example:
        >>> backend.close_workbook()
    """
    self._check_thread()
    logger.debug("COMBackstageBackend.close_workbook")
    self._active_workbook().Close(SaveChanges=False)

open_options

open_options() -> None

Open the Excel Options dialog via CommandBars.ExecuteMso.

This method is not part of :class:AbstractBackstageFileOps. It is provided as a convenience fallback when :class:~ezxl.gui.pywinauto.PywinautoBackstageBackend (the preferred implementation via :class:AbstractBackstageNavigator) is unavailable.

Uses the "ApplicationOptionsDialog" MSO identifier. This may fail in restricted environments (e.g. when a macro is running or the ribbon is disabled).

RAISES DESCRIPTION
ExcelThreadViolationError

If called from the wrong thread.

GUIOperationError

If the Options dialog cannot be opened via COM.

Example

backend.open_options()

Source code in src/ezxl/gui/win32com/_backstage.py
@wrap_com_error
def open_options(self) -> None:
    """Open the Excel Options dialog via ``CommandBars.ExecuteMso``.

    This method is **not** part of :class:`AbstractBackstageFileOps`.
    It is provided as a convenience fallback when
    :class:`~ezxl.gui.pywinauto.PywinautoBackstageBackend` (the
    preferred implementation via :class:`AbstractBackstageNavigator`)
    is unavailable.

    Uses the ``"ApplicationOptionsDialog"`` MSO identifier.  This may
    fail in restricted environments (e.g. when a macro is running or the
    ribbon is disabled).

    Raises:
        ExcelThreadViolationError: If called from the wrong thread.
        GUIOperationError: If the Options dialog cannot be opened via COM.

    Example:
        >>> backend.open_options()
    """
    self._check_thread()
    logger.debug("COMBackstageBackend.open_options")
    try:
        self._get_app().CommandBars.ExecuteMso("ApplicationOptionsDialog")
    except Exception as exc:
        raise GUIOperationError(
            "Could not open Excel Options dialog via COM "
            "(CommandBars.ExecuteMso('ApplicationOptionsDialog') failed). "
            "Consider using PywinautoBackstageBackend.open_options() via "
            "GUIProxy.backstage_nav as the preferred implementation. "
            f"Original error: {exc}",
            cause=exc,
        ) from exc

options: show_source: false members: - save - save_as - open_file - close_workbook


GUI backends — pywinauto

PywinautoKeysBackend

PywinautoKeysBackend(hwnd: int | None = None)

Bases: AbstractKeysBackend

Keystroke injection via pywinauto.keyboard.send_keys.

Translates VBA SendKeys notation to pywinauto notation using :func:_translate_keys, then delegates to pywinauto.keyboard.send_keys.

This backend is a standalone alternative to the COM-based _COMKeysBackend. It does not require an :class:~ezxl.core.ExcelApp instance and carries no COM STA thread constraint.

PARAMETER DESCRIPTION
hwnd

Win32 window handle for the Excel main window. Currently unused — pywinauto.keyboard.send_keys injects keystrokes into the currently focused window. Ensure the Excel window has focus before calling :meth:send_keys. The parameter is accepted for API consistency with the other pywinauto backends and reserved for future use.

TYPE: int | None DEFAULT: None

Example

from ezxl.gui.pywinauto import PywinautoKeysBackend keys = PywinautoKeysBackend() keys.send_keys("^s") # Ctrl+S keys.send_keys("{ESCAPE}") # maps to {ESC} internally keys.send_keys("^{HOME}")

Inject into GUIProxy:

from ezxl import ExcelApp, GUIProxy with ExcelApp(mode="attach") as xl: ... gui = GUIProxy(xl, keys=PywinautoKeysBackend()) ... gui.send_keys("^{HOME}")

Source code in src/ezxl/gui/pywinauto/_keys.py
def __init__(self, hwnd: int | None = None) -> None:
    # hwnd is reserved for future focus-management logic.
    self._hwnd = hwnd

send_keys

send_keys(keys: str, wait: bool = True) -> None

Send a keystroke sequence using pywinauto.keyboard.

Translates keys from VBA SendKeys notation to pywinauto notation via :func:_translate_keys, then calls pywinauto.keyboard.send_keys. If wait is True, a brief pause of 50 ms is inserted after injection as a best-effort approximation of the VBA wait=True semantics.

Note

pywinauto.keyboard.send_keys injects keystrokes into the currently focused window. Call window.set_focus() on the Excel window before using this backend if focus cannot be guaranteed.

PARAMETER DESCRIPTION
keys

Keystroke string in VBA SendKeys notation (e.g. "{ENTER}", "^s" for Ctrl+S, "{ESCAPE}" for Escape).

TYPE: str

wait

If True, insert a 50 ms pause after sending. This is a best-effort approximation; it does not guarantee Excel has finished processing. Defaults to True.

TYPE: bool DEFAULT: True

RAISES DESCRIPTION
GUIOperationError

If the pywinauto keystroke injection raises an unexpected error.

Example

backend.send_keys("^s") # Ctrl+S backend.send_keys("{ESCAPE}") # → {ESC} internally backend.send_keys("%{F4}", wait=False) # Alt+F4, no pause

Source code in src/ezxl/gui/pywinauto/_keys.py
def send_keys(self, keys: str, wait: bool = True) -> None:
    """Send a keystroke sequence using ``pywinauto.keyboard``.

    Translates *keys* from VBA SendKeys notation to pywinauto notation
    via :func:`_translate_keys`, then calls
    ``pywinauto.keyboard.send_keys``.  If *wait* is ``True``, a brief
    pause of 50 ms is inserted after injection as a best-effort
    approximation of the VBA ``wait=True`` semantics.

    Note:
        ``pywinauto.keyboard.send_keys`` injects keystrokes into
        the **currently focused window**.  Call
        ``window.set_focus()`` on the Excel window before using
        this backend if focus cannot be guaranteed.

    Args:
        keys: Keystroke string in VBA SendKeys notation
            (e.g. ``"{ENTER}"``, ``"^s"`` for Ctrl+S,
            ``"{ESCAPE}"`` for Escape).
        wait: If ``True``, insert a 50 ms pause after sending.
            This is a best-effort approximation; it does not
            guarantee Excel has finished processing.
            Defaults to ``True``.

    Raises:
        GUIOperationError: If the pywinauto keystroke injection
            raises an unexpected error.

    Example:
        >>> backend.send_keys("^s")             # Ctrl+S
        >>> backend.send_keys("{ESCAPE}")       # → {ESC} internally
        >>> backend.send_keys("%{F4}", wait=False)  # Alt+F4, no pause
    """
    translated = _translate_keys(keys)
    logger.debug(
        "PywinautoKeysBackend.send_keys: keys=%r, translated=%r, wait=%r",
        keys,
        translated,
        wait,
    )
    _pw_send_keys(translated)
    if wait:
        time.sleep(_WAIT_PAUSE)

options: show_source: false members: - send_keys

PywinautoBackstageBackend

PywinautoBackstageBackend(hwnd: int | None = None, locale: str = 'en')

Bases: AbstractBackstageNavigator

Excel Backstage visual navigation via UIA direct click with Alt-sequence fallback.

Implements :class:~ezxl.gui._protocols.AbstractBackstageNavigator using pywinauto. The primary strategy for every action is a UIA direct click — the backend opens the Backstage via Button "Onglet Fichier" (or locale equivalent), then clicks the target ListItem by its localised UIA Name. This approach is focus-independent.

An Alt-sequence fallback is attempted only when the UIA click fails and the element spec carries a non-empty alt_sequence.

This backend does not perform file I/O. Operations that write to disk (save, save_as with an explicit path) belong to :class:~ezxl.gui.win32com.COMBackstageBackend and are exposed through GUIProxy.backstage. This backend is composed alongside it via GUIProxy.backstage_nav.

PARAMETER DESCRIPTION
hwnd

Win32 window handle for the Excel main window. Pass None to auto-detect the first visible Excel instance. Always pass xl.hwnd in production to avoid targeting the wrong window when multiple Excel instances are open.

TYPE: int | None DEFAULT: None

locale

Locale code used for UIA Name-based searches. Accepted values: "en" (default), "fr".

TYPE: str DEFAULT: 'en'

Example

backend = PywinautoBackstageBackend(hwnd=xl.hwnd, locale="fr") backend.open_options() # UIA: opens Options panel backend.open_save_as_panel() # UIA: opens Save As panel, leaves open backend.open_file() # UIA: opens Open panel backend.close_workbook() # UIA: clicks Close in Backstage

Source code in src/ezxl/gui/pywinauto/_backstage.py
def __init__(
    self,
    hwnd: int | None = None,
    locale: str = "en",
) -> None:
    self._hwnd = hwnd
    self._locale = locale

open_options

open_options() -> None

Navigate to the Excel Options panel via the Backstage.

RAISES DESCRIPTION
GUIOperationError

If the Options panel cannot be reached.

Example

backend.open_options()

Source code in src/ezxl/gui/pywinauto/_backstage.py
def open_options(self) -> None:
    """Navigate to the Excel Options panel via the Backstage.

    Raises:
        GUIOperationError: If the Options panel cannot be reached.

    Example:
        >>> backend.open_options()
    """
    logger.debug("PywinautoBackstageBackend.open_options")
    self._execute_by_spec(self._get_spec("file_options"))

open_save_as_panel

open_save_as_panel() -> None

Open the Save As panel in the Backstage without confirming a save.

Clicks the "Enregistrer sous" (or locale equivalent) ListItem and leaves the panel open. Does not write to disk — use GUIProxy.backstage.save_as(path=...) for programmatic saves.

RAISES DESCRIPTION
GUIOperationError

If the panel cannot be reached.

Example

backend.open_save_as_panel()

Source code in src/ezxl/gui/pywinauto/_backstage.py
def open_save_as_panel(self) -> None:
    """Open the Save As panel in the Backstage without confirming a save.

    Clicks the ``"Enregistrer sous"`` (or locale equivalent) ListItem
    and leaves the panel open.  Does not write to disk — use
    ``GUIProxy.backstage.save_as(path=...)`` for programmatic saves.

    Raises:
        GUIOperationError: If the panel cannot be reached.

    Example:
        >>> backend.open_save_as_panel()
    """
    logger.debug("PywinautoBackstageBackend.open_save_as_panel")
    self._execute_by_spec(self._get_spec("file_save_as"))

open_file

open_file() -> None

Open the Open panel via the Backstage.

RAISES DESCRIPTION
GUIOperationError

If the panel cannot be reached.

Example

backend.open_file()

Source code in src/ezxl/gui/pywinauto/_backstage.py
def open_file(self) -> None:
    """Open the Open panel via the Backstage.

    Raises:
        GUIOperationError: If the panel cannot be reached.

    Example:
        >>> backend.open_file()
    """
    logger.debug("PywinautoBackstageBackend.open_file")
    self._execute_by_spec(self._get_spec("file_open"))

close_workbook

close_workbook() -> None

Close the active workbook via a Backstage UIA click.

RAISES DESCRIPTION
GUIOperationError

If the action cannot be completed.

Example

backend.close_workbook()

Source code in src/ezxl/gui/pywinauto/_backstage.py
def close_workbook(self) -> None:
    """Close the active workbook via a Backstage UIA click.

    Raises:
        GUIOperationError: If the action cannot be completed.

    Example:
        >>> backend.close_workbook()
    """
    logger.debug("PywinautoBackstageBackend.close_workbook")
    self._execute_by_spec(self._get_spec("file_close"))

options: show_source: false members: - init - open_options - open_save_as_panel - open_file - close_workbook


File I/O

read_excel

read_excel(source: str | Path, sheet: str | None = None) -> DataFrame

Read an Excel workbook sheet into a polars DataFrame.

Delegates to polars.read_excel which uses fastexcel (Rust) under the hood. No running Excel process is required.

PARAMETER DESCRIPTION
source

Path to the source .xlsx / .xlsm file.

TYPE: str | Path

sheet

Worksheet name to read. Pass None to read the first sheet (polars default when sheet_name is omitted).

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
DataFrame

pl.DataFrame: Contents of the requested sheet as a polars

DataFrame

DataFrame, with the first row used as column headers.

RAISES DESCRIPTION
FileNotFoundError

If source does not exist.

ImportError

If polars (or its fastexcel extra) is not installed.

Example

df = read_excel("report.xlsx", sheet="Data") print(df.head())

Source code in src/ezxl/io/_converters.py
def read_excel(
    source: str | Path,
    sheet: str | None = None,
) -> pl.DataFrame:
    """Read an Excel workbook sheet into a polars DataFrame.

    Delegates to ``polars.read_excel`` which uses ``fastexcel`` (Rust)
    under the hood.  No running Excel process is required.

    Args:
        source: Path to the source ``.xlsx`` / ``.xlsm`` file.
        sheet: Worksheet name to read.  Pass ``None`` to read the first
            sheet (polars default when ``sheet_name`` is omitted).

    Returns:
        pl.DataFrame: Contents of the requested sheet as a polars
        DataFrame, with the first row used as column headers.

    Raises:
        FileNotFoundError: If ``source`` does not exist.
        ImportError: If polars (or its ``fastexcel`` extra) is not
            installed.

    Example:
        >>> df = read_excel("report.xlsx", sheet="Data")
        >>> print(df.head())
    """
    source_path = Path(source).resolve()

    if not source_path.exists():
        raise FileNotFoundError(f"Source file not found: {source_path}")

    logger.debug("read_excel: %s (sheet=%r)", source_path, sheet)

    df: pl.DataFrame = pl.read_excel(source_path, sheet_name=sheet)

    logger.debug("read_excel: read %d rows from '%s'.", len(df), source_path)
    return df

options: show_source: false

read_csv

read_csv(source: str | Path, separator: str = ',', encoding: str = 'utf-8') -> DataFrame

Read a CSV file into a polars DataFrame.

PARAMETER DESCRIPTION
source

Path to the source .csv file.

TYPE: str | Path

separator

Column delimiter character. Defaults to "," (standard CSV). Use "\t" for TSV files.

TYPE: str DEFAULT: ','

encoding

File encoding passed through to polars. Defaults to "utf-8".

TYPE: str DEFAULT: 'utf-8'

RETURNS DESCRIPTION
DataFrame

pl.DataFrame: Parsed contents of the CSV file.

RAISES DESCRIPTION
FileNotFoundError

If source does not exist.

Example

df = read_csv("transactions.csv", separator=";") print(df.schema)

Source code in src/ezxl/io/_converters.py
def read_csv(
    source: str | Path,
    separator: str = ",",
    encoding: str = "utf-8",
) -> pl.DataFrame:
    """Read a CSV file into a polars DataFrame.

    Args:
        source: Path to the source ``.csv`` file.
        separator: Column delimiter character.  Defaults to ``","``
            (standard CSV).  Use ``"\\t"`` for TSV files.
        encoding: File encoding passed through to polars.  Defaults to
            ``"utf-8"``.

    Returns:
        pl.DataFrame: Parsed contents of the CSV file.

    Raises:
        FileNotFoundError: If ``source`` does not exist.

    Example:
        >>> df = read_csv("transactions.csv", separator=";")
        >>> print(df.schema)
    """
    source_path = Path(source).resolve()

    if not source_path.exists():
        raise FileNotFoundError(f"Source file not found: {source_path}")

    logger.debug("read_csv: %s (sep=%r, enc=%r)", source_path, separator, encoding)

    df: pl.DataFrame = pl.read_csv(
        source_path,
        separator=separator,
        encoding=encoding,
    )

    logger.debug("read_csv: read %d rows from '%s'.", len(df), source_path)
    return df

options: show_source: false

xlsx_to_csv

xlsx_to_csv(source: str | Path, dest: str | Path, sheet: str | None = None, separator: str = ',') -> None

Convert an Excel workbook sheet to a CSV file using polars.

Supersedes both the former xlsx_to_csv (openpyxl) and xlsx_to_csv_fast (python-calamine) functions. polars uses fastexcel (Rust) for the read step, providing the same high-throughput characteristics as the former fast path.

PARAMETER DESCRIPTION
source

Path to the source .xlsx / .xlsm file.

TYPE: str | Path

dest

Destination .csv file path. Parent directories must exist.

TYPE: str | Path

sheet

Worksheet name to export. Pass None to use the first sheet.

TYPE: str | None DEFAULT: None

separator

Column delimiter for the CSV output. Defaults to "," (standard CSV).

TYPE: str DEFAULT: ','

RAISES DESCRIPTION
FileNotFoundError

If source does not exist.

Example

xlsx_to_csv("data.xlsx", "data.csv", sheet="Transactions") xlsx_to_csv("data.xlsx", "data.tsv", separator="\t")

Source code in src/ezxl/io/_converters.py
def xlsx_to_csv(
    source: str | Path,
    dest: str | Path,
    sheet: str | None = None,
    separator: str = ",",
) -> None:
    """Convert an Excel workbook sheet to a CSV file using polars.

    Supersedes both the former ``xlsx_to_csv`` (openpyxl) and
    ``xlsx_to_csv_fast`` (python-calamine) functions.  polars uses
    ``fastexcel`` (Rust) for the read step, providing the same
    high-throughput characteristics as the former fast path.

    Args:
        source: Path to the source ``.xlsx`` / ``.xlsm`` file.
        dest: Destination ``.csv`` file path.  Parent directories must
            exist.
        sheet: Worksheet name to export.  Pass ``None`` to use the
            first sheet.
        separator: Column delimiter for the CSV output.  Defaults to
            ``","`` (standard CSV).

    Raises:
        FileNotFoundError: If ``source`` does not exist.

    Example:
        >>> xlsx_to_csv("data.xlsx", "data.csv", sheet="Transactions")
        >>> xlsx_to_csv("data.xlsx", "data.tsv", separator="\\t")
    """
    dest_path = Path(dest).resolve()

    logger.debug(
        "xlsx_to_csv: %s%s (sheet=%r, sep=%r)",
        Path(source).resolve(),
        dest_path,
        sheet,
        separator,
    )

    df = read_excel(source, sheet=sheet)
    df.write_csv(dest_path, separator=separator)

    logger.debug("xlsx_to_csv: completed — wrote %s", dest_path)
    printer.success(f"xlsx_to_csv: conversion complete — {dest_path}")

options: show_source: false

csv_to_xlsx

csv_to_xlsx(source: str | Path, dest: str | Path, sheet_name: str = 'Sheet1') -> None

Convert a CSV file to an Excel workbook using polars.

Reads the CSV with polars and writes it as an .xlsx file. polars delegates the Excel write step to xlsxwriter or openpyxl depending on which is installed; no additional configuration is required.

PARAMETER DESCRIPTION
source

Path to the source .csv file.

TYPE: str | Path

dest

Destination .xlsx file path. Parent directories must exist.

TYPE: str | Path

sheet_name

Name of the worksheet to create in the output workbook. Defaults to "Sheet1".

TYPE: str DEFAULT: 'Sheet1'

RAISES DESCRIPTION
FileNotFoundError

If source does not exist.

Example

csv_to_xlsx("transactions.csv", "transactions.xlsx", sheet_name="Data")

Source code in src/ezxl/io/_converters.py
def csv_to_xlsx(
    source: str | Path,
    dest: str | Path,
    sheet_name: str = "Sheet1",
) -> None:
    """Convert a CSV file to an Excel workbook using polars.

    Reads the CSV with polars and writes it as an ``.xlsx`` file.
    polars delegates the Excel write step to ``xlsxwriter`` or
    ``openpyxl`` depending on which is installed; no additional
    configuration is required.

    Args:
        source: Path to the source ``.csv`` file.
        dest: Destination ``.xlsx`` file path.  Parent directories must
            exist.
        sheet_name: Name of the worksheet to create in the output
            workbook.  Defaults to ``"Sheet1"``.

    Raises:
        FileNotFoundError: If ``source`` does not exist.

    Example:
        >>> csv_to_xlsx("transactions.csv", "transactions.xlsx", sheet_name="Data")
    """
    dest_path = Path(dest).resolve()

    logger.debug(
        "csv_to_xlsx: %s%s (sheet=%r)",
        Path(source).resolve(),
        dest_path,
        sheet_name,
    )

    df = read_csv(source)
    df.write_excel(dest_path, worksheet=sheet_name)

    logger.debug("csv_to_xlsx: completed — wrote %s", dest_path)
    printer.success(f"csv_to_xlsx: conversion complete — {dest_path}")

options: show_source: false

read_sheet

read_sheet(source: str | Path, sheet: str | None = None) -> list[list[Any]]

Read a worksheet into a row-major list of lists (compatibility shim).

Wraps read_excel and converts the resulting polars DataFrame to a list[list[Any]] via DataFrame.rows(). The first row contains the column headers as extracted by polars.

This function exists for backwards compatibility with callers that pre-date the polars migration. New code should use read_excel directly to benefit from the full polars API.

PARAMETER DESCRIPTION
source

Path to the source .xlsx / .xlsm file.

TYPE: str | Path

sheet

Worksheet name to read. Pass None to use the first sheet.

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
list[list[Any]]

list[list[Any]]: Row-major 2D list of cell values. The first

list[list[Any]]

row contains column headers; subsequent rows contain data

list[list[Any]]

values. Empty cells are represented as None.

RAISES DESCRIPTION
FileNotFoundError

If source does not exist.

Example

data = read_sheet("report.xlsx", sheet="Data") headers = data[0] rows = data[1:]

Source code in src/ezxl/io/_converters.py
def read_sheet(
    source: str | Path,
    sheet: str | None = None,
) -> list[list[Any]]:
    """Read a worksheet into a row-major list of lists (compatibility shim).

    Wraps ``read_excel`` and converts the resulting polars DataFrame to
    a ``list[list[Any]]`` via ``DataFrame.rows()``.  The first row
    contains the column headers as extracted by polars.

    This function exists for backwards compatibility with callers that
    pre-date the polars migration.  New code should use ``read_excel``
    directly to benefit from the full polars API.

    Args:
        source: Path to the source ``.xlsx`` / ``.xlsm`` file.
        sheet: Worksheet name to read.  Pass ``None`` to use the first
            sheet.

    Returns:
        list[list[Any]]: Row-major 2D list of cell values.  The first
        row contains column headers; subsequent rows contain data
        values.  Empty cells are represented as ``None``.

    Raises:
        FileNotFoundError: If ``source`` does not exist.

    Example:
        >>> data = read_sheet("report.xlsx", sheet="Data")
        >>> headers = data[0]
        >>> rows = data[1:]
    """
    logger.debug("read_sheet: %s (sheet=%r) — delegating to read_excel", source, sheet)

    df = read_excel(source, sheet=sheet)

    # Prepend column names as the first row to preserve the legacy contract
    # where callers expected headers in row 0.
    header_row: list[Any] = list(df.columns)
    data_rows: list[list[Any]] = [list(row) for row in df.rows()]

    result: list[list[Any]] = [header_row, *data_rows]

    logger.debug("read_sheet: returning %d rows (incl. header).", len(result))
    return result

options: show_source: false


Closed-file formatting

ExcelFormatter

ExcelFormatter(path: str | Path)

Fluent formatter for closed Excel workbook files.

All formatting operations are buffered and applied in a single write pass when save() is called. The workbook is opened with openpyxl only at save time, minimising I/O overhead.

The API is intentionally flat: no sheet selector is exposed here. The formatter operates on the active sheet of the workbook. Consumer libraries that need multi-sheet formatting should instantiate one ExcelFormatter per sheet operation.

PARAMETER DESCRIPTION
path

Path to an existing .xlsx workbook file.

TYPE: str | Path

RAISES DESCRIPTION
FileNotFoundError

If path does not exist.

ImportError

If openpyxl is not installed.

Example

( ... ExcelFormatter("report.xlsx") ... .column_width("A", 20) ... .font("A1", bold=True, size=14, color="FFFFFF") ... .fill("A1", "4F81BD") ... .save() ... )

Source code in src/ezxl/io/_formatters.py
def __init__(self, path: str | Path) -> None:
    self._path = Path(path).resolve()
    if not self._path.exists():
        raise FileNotFoundError(f"ExcelFormatter: file not found: {self._path}")
    # Buffer of pending operations applied in order at save().
    self._ops: list[_Operation] = []

column_width

column_width(col: str, width: float) -> ExcelFormatter

Set the width of a column.

PARAMETER DESCRIPTION
col

Column letter (e.g. "A", "BC").

TYPE: str

width

Column width in Excel character units.

TYPE: float

RETURNS DESCRIPTION
ExcelFormatter

self for method chaining.

TYPE: ExcelFormatter

Example

formatter.column_width("A", 20).column_width("B", 15)

Source code in src/ezxl/io/_formatters.py
def column_width(self, col: str, width: float) -> ExcelFormatter:
    """Set the width of a column.

    Args:
        col: Column letter (e.g. ``"A"``, ``"BC"``).
        width: Column width in Excel character units.

    Returns:
        ExcelFormatter: ``self`` for method chaining.

    Example:
        >>> formatter.column_width("A", 20).column_width("B", 15)
    """
    self._ops.append(_ColumnWidthOp(col=col, width=width))
    return self

row_height

row_height(row: int, height: float) -> ExcelFormatter

Set the height of a row.

PARAMETER DESCRIPTION
row

1-based row index.

TYPE: int

height

Row height in points.

TYPE: float

RETURNS DESCRIPTION
ExcelFormatter

self for method chaining.

TYPE: ExcelFormatter

Example

formatter.row_height(1, 30)

Source code in src/ezxl/io/_formatters.py
def row_height(self, row: int, height: float) -> ExcelFormatter:
    """Set the height of a row.

    Args:
        row: 1-based row index.
        height: Row height in points.

    Returns:
        ExcelFormatter: ``self`` for method chaining.

    Example:
        >>> formatter.row_height(1, 30)
    """
    self._ops.append(_RowHeightOp(row=row, height=height))
    return self

font

font(ref: str, *, bold: bool = False, italic: bool = False, size: int | None = None, color: str | None = None) -> ExcelFormatter

Apply font formatting to a cell or range.

PARAMETER DESCRIPTION
ref

Cell or range address in A1 notation (e.g. "A1" or "A1:D1").

TYPE: str

bold

Apply bold weight. Defaults to False.

TYPE: bool DEFAULT: False

italic

Apply italic style. Defaults to False.

TYPE: bool DEFAULT: False

size

Font size in points. None leaves the size unchanged.

TYPE: int | None DEFAULT: None

color

Font colour as a 6-character hex string without # (e.g. "FF0000" for red). None leaves colour unchanged.

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION
ExcelFormatter

self for method chaining.

TYPE: ExcelFormatter

Example

formatter.font("A1", bold=True, size=12, color="FF0000")

Source code in src/ezxl/io/_formatters.py
def font(
    self,
    ref: str,
    *,
    bold: bool = False,
    italic: bool = False,
    size: int | None = None,
    color: str | None = None,
) -> ExcelFormatter:
    """Apply font formatting to a cell or range.

    Args:
        ref: Cell or range address in A1 notation (e.g. ``"A1"`` or
            ``"A1:D1"``).
        bold: Apply bold weight. Defaults to ``False``.
        italic: Apply italic style. Defaults to ``False``.
        size: Font size in points. ``None`` leaves the size unchanged.
        color: Font colour as a 6-character hex string without ``#``
            (e.g. ``"FF0000"`` for red). ``None`` leaves colour unchanged.

    Returns:
        ExcelFormatter: ``self`` for method chaining.

    Example:
        >>> formatter.font("A1", bold=True, size=12, color="FF0000")
    """
    self._ops.append(
        _FontOp(ref=ref, bold=bold, italic=italic, size=size, color=color)
    )
    return self

fill

fill(ref: str, color: str) -> ExcelFormatter

Apply a solid background fill to a cell or range.

PARAMETER DESCRIPTION
ref

Cell or range address in A1 notation.

TYPE: str

color

Background colour as a 6-character hex string without # (e.g. "4F81BD" for a medium blue).

TYPE: str

RETURNS DESCRIPTION
ExcelFormatter

self for method chaining.

TYPE: ExcelFormatter

Example

formatter.fill("A1:D1", "4F81BD")

Source code in src/ezxl/io/_formatters.py
def fill(self, ref: str, color: str) -> ExcelFormatter:
    """Apply a solid background fill to a cell or range.

    Args:
        ref: Cell or range address in A1 notation.
        color: Background colour as a 6-character hex string without
            ``#`` (e.g. ``"4F81BD"`` for a medium blue).

    Returns:
        ExcelFormatter: ``self`` for method chaining.

    Example:
        >>> formatter.fill("A1:D1", "4F81BD")
    """
    self._ops.append(_FillOp(ref=ref, color=color))
    return self

border

border(ref: str, style: str = 'thin') -> ExcelFormatter

Apply a border to all edges of a cell or range.

PARAMETER DESCRIPTION
ref

Cell or range address in A1 notation.

TYPE: str

style

Border style name as understood by openpyxl (e.g. "thin", "medium", "thick", "dashed"). Defaults to "thin".

TYPE: str DEFAULT: 'thin'

RETURNS DESCRIPTION
ExcelFormatter

self for method chaining.

TYPE: ExcelFormatter

Example

formatter.border("A1:D5", style="thin")

Source code in src/ezxl/io/_formatters.py
def border(self, ref: str, style: str = "thin") -> ExcelFormatter:
    """Apply a border to all edges of a cell or range.

    Args:
        ref: Cell or range address in A1 notation.
        style: Border style name as understood by openpyxl
            (e.g. ``"thin"``, ``"medium"``, ``"thick"``, ``"dashed"``).
            Defaults to ``"thin"``.

    Returns:
        ExcelFormatter: ``self`` for method chaining.

    Example:
        >>> formatter.border("A1:D5", style="thin")
    """
    self._ops.append(_BorderOp(ref=ref, style=style))
    return self

align

align(ref: str, *, horizontal: str | None = None, vertical: str | None = None, wrap: bool = False) -> ExcelFormatter

Apply alignment to a cell or range.

PARAMETER DESCRIPTION
ref

Cell or range address in A1 notation.

TYPE: str

horizontal

Horizontal alignment. Accepted values: "left", "center", "right", "fill", "justify", "centerContinuous", "distributed". None leaves the setting unchanged.

TYPE: str | None DEFAULT: None

vertical

Vertical alignment. Accepted values: "top", "center", "bottom", "justify", "distributed". None leaves the setting unchanged.

TYPE: str | None DEFAULT: None

wrap

Enable text wrapping. Defaults to False.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
ExcelFormatter

self for method chaining.

TYPE: ExcelFormatter

Example

formatter.align("A1", horizontal="center", vertical="top", wrap=True)

Source code in src/ezxl/io/_formatters.py
def align(
    self,
    ref: str,
    *,
    horizontal: str | None = None,
    vertical: str | None = None,
    wrap: bool = False,
) -> ExcelFormatter:
    """Apply alignment to a cell or range.

    Args:
        ref: Cell or range address in A1 notation.
        horizontal: Horizontal alignment. Accepted values:
            ``"left"``, ``"center"``, ``"right"``, ``"fill"``,
            ``"justify"``, ``"centerContinuous"``, ``"distributed"``.
            ``None`` leaves the setting unchanged.
        vertical: Vertical alignment. Accepted values:
            ``"top"``, ``"center"``, ``"bottom"``, ``"justify"``,
            ``"distributed"``. ``None`` leaves the setting unchanged.
        wrap: Enable text wrapping. Defaults to ``False``.

    Returns:
        ExcelFormatter: ``self`` for method chaining.

    Example:
        >>> formatter.align("A1", horizontal="center", vertical="top", wrap=True)
    """
    self._ops.append(
        _AlignOp(ref=ref, horizontal=horizontal, vertical=vertical, wrap=wrap)
    )
    return self

save

save(dest: str | Path | None = None) -> None

Apply all buffered operations and write the workbook.

PARAMETER DESCRIPTION
dest

Destination path. Pass None to overwrite the source file in place. Parent directories of dest must exist.

TYPE: str | Path | None DEFAULT: None

RAISES DESCRIPTION
FormatterError

If any openpyxl operation fails.

ImportError

If openpyxl is not installed.

Example

formatter.save() # overwrite source formatter.save("output/report.xlsx") # write to new path

Source code in src/ezxl/io/_formatters.py
def save(self, dest: str | Path | None = None) -> None:
    """Apply all buffered operations and write the workbook.

    Args:
        dest: Destination path. Pass ``None`` to overwrite the source
            file in place. Parent directories of ``dest`` must exist.

    Raises:
        FormatterError: If any openpyxl operation fails.
        ImportError: If openpyxl is not installed.

    Example:
        >>> formatter.save()                        # overwrite source
        >>> formatter.save("output/report.xlsx")   # write to new path
    """
    from ..exceptions import FormatterError

    logger.debug(
        "ExcelFormatter.save: applying %d operations to '%s'.",
        len(self._ops),
        self._path,
    )

    try:
        wb = openpyxl.load_workbook(self._path)
    except Exception as exc:
        raise FormatterError(
            f"Failed to open workbook for formatting: {self._path}{exc}",
            cause=exc,
        ) from exc

    try:
        ws = wb.active
        if ws is None:
            raise FormatterError(
                f"Workbook '{self._path}' has no active sheet.",
                cause=None,
            )

        for op in self._ops:
            if isinstance(op, _ColumnWidthOp):
                ws.column_dimensions[op.col].width = op.width

            elif isinstance(op, _RowHeightOp):
                ws.row_dimensions[op.row].height = op.height

            elif isinstance(op, _FontOp):
                font_kwargs: dict[str, Any] = {
                    "bold": op.bold,
                    "italic": op.italic,
                }
                if op.size is not None:
                    font_kwargs["size"] = op.size
                if op.color is not None:
                    font_kwargs["color"] = op.color
                font_style = Font(**font_kwargs)
                for cell in _iter_cells(ws, op.ref):
                    cell.font = font_style

            elif isinstance(op, _FillOp):
                fill_style = PatternFill(fill_type="solid", fgColor=op.color)
                for cell in _iter_cells(ws, op.ref):
                    cell.fill = fill_style

            elif isinstance(op, _BorderOp):
                side = Side(border_style=cast(Any, op.style))
                border_style = Border(left=side, right=side, top=side, bottom=side)
                for cell in _iter_cells(ws, op.ref):
                    cell.border = border_style

            elif isinstance(op, _AlignOp):
                align_kwargs: dict[str, Any] = {"wrap_text": op.wrap}
                if op.horizontal is not None:
                    align_kwargs["horizontal"] = op.horizontal
                if op.vertical is not None:
                    align_kwargs["vertical"] = op.vertical
                align_style = Alignment(**align_kwargs)
                for cell in _iter_cells(ws, op.ref):
                    cell.alignment = align_style

    except Exception as exc:
        raise FormatterError(
            f"Error applying formatting operation to '{self._path}': {exc}",
            cause=exc,
        ) from exc

    out_path = Path(dest).resolve() if dest is not None else self._path

    try:
        wb.save(str(out_path))
    except Exception as exc:
        raise FormatterError(
            f"Failed to save formatted workbook to '{out_path}': {exc}",
            cause=exc,
        ) from exc

    logger.debug("ExcelFormatter.save: written to '%s'.", out_path)
    printer.success(
        f"ExcelFormatter: formatting applied and saved to '{out_path}'."
    )

options: show_source: false members: - init - column_width - row_height - font - fill - border - align - save