Module refinery.lib.excel

Library for reading the cells of Microsoft Excel workbooks: sheets, sheet types, cell values, and formula sources across the OOXML, BIFF, and XLSB container families. The refinery.lib.excel.formula package holds the AST model those sources decode into and the synthesizer that prints it back.

Expand source code Browse git
"""
Library for reading the cells of Microsoft Excel workbooks: sheets, sheet types, cell values,
and formula sources across the OOXML, BIFF, and XLSB container families. The `formula` package
holds the AST model those sources decode into and the synthesizer that prints it back.
"""
from __future__ import annotations

from refinery.lib.excel.common import (
    Cell,
    CellKind,
    ExcelFormatError,
    SheetKind,
    datetime_to_serial,
    rc2ref,
    ref2rc,
    serial_to_datetime,
)
from refinery.lib.excel.formula import (
    FormulaSynthesizer,
    International,
    XlA1Reference,
    XlArrayConstant,
    XlBinaryExpression,
    XlBinaryOperator,
    XlBoolean,
    XlDefinedName,
    XlError,
    XlFunctionCall,
    XlLiteral,
    XlMissingArgument,
    XlNumber,
    XlParenExpression,
    XlR1C1Reference,
    XlString,
    XlUnaryExpression,
    XlUnaryOperator,
    XlUnparsedFormula,
    parse_formula,
    synthesize_formula,
)
from refinery.lib.excel.workbook import (
    ExcelFormat,
    ExcelSheet,
    ExcelWorkbook,
    detect_format,
    open_workbook,
)

__all__ = [
    'Cell',
    'CellKind',
    'ExcelFormat',
    'ExcelFormatError',
    'ExcelSheet',
    'ExcelWorkbook',
    'FormulaSynthesizer',
    'International',
    'SheetKind',
    'XlArrayConstant',
    'XlA1Reference',
    'XlBinaryExpression',
    'XlBinaryOperator',
    'XlBoolean',
    'XlDefinedName',
    'XlError',
    'XlFunctionCall',
    'XlLiteral',
    'XlMissingArgument',
    'XlNumber',
    'XlParenExpression',
    'XlR1C1Reference',
    'XlString',
    'XlUnaryExpression',
    'XlUnaryOperator',
    'XlUnparsedFormula',
    'datetime_to_serial',
    'detect_format',
    'open_workbook',
    'parse_formula',
    'rc2ref',
    'ref2rc',
    'serial_to_datetime',
    'synthesize_formula',
]

Sub-modules

refinery.lib.excel.biff

A reader for the cell values of workbooks in the BIFF record format, which covers the XLS file extension. Every version from BIFF2 to BIFF8 is read, …

refinery.lib.excel.common
refinery.lib.excel.formula

The formula substrate of the Excel library: the AST model for formulas, the text parser for OOXML formula sources, the synthesizer that prints the …

refinery.lib.excel.ooxml
refinery.lib.excel.workbook
refinery.lib.excel.xlsb

A reader for the cell values of workbooks in the MS-XLSB format, which covers the XLSB file extension: a zip package of binary record streams. The …

Functions

def datetime_to_serial(value, date_mode_1904)

Convert a datetime or a time into the Excel serial date number that serial_to_datetime() turns back into it. A time names the fraction of a day it covers; a datetime of the 1900 epoch before March 1, 1900 counts from December 31, 1899, so the spurious leap day that Excel inherited from Lotus 1-2-3 keeps serial 60 out of the round trip: February 29, 1900 does not exist, and the February 28 that serials 59 and 60 both resolve to converts back to 59 only. A serial number carries no time zone, so a datetime that names one counts by the wall-clock time it spells.

Expand source code Browse git
def datetime_to_serial(
    value: datetime.datetime | datetime.time,
    date_mode_1904: bool,
) -> int | float:
    """
    Convert a datetime or a time into the Excel serial date number that `serial_to_datetime`
    turns back into it. A time names the fraction of a day it covers; a datetime of the 1900
    epoch before March 1, 1900 counts from December 31, 1899, so the spurious leap day that
    Excel inherited from Lotus 1-2-3 keeps serial 60 out of the round trip: February 29,
    1900 does not exist, and the February 28 that serials 59 and 60 both resolve to converts
    back to 59 only. A serial number carries no time zone, so a datetime that names one counts
    by the wall-clock time it spells.
    """
    if isinstance(value, datetime.time):
        milliseconds = (value.hour * 60 + value.minute) * 60000 + value.second * 1000
        milliseconds += value.microsecond // 1000
        serial = milliseconds / _MILLISECONDS_PER_DAY
    else:
        value = value.replace(tzinfo=None)
        if date_mode_1904:
            epoch = _EPOCH_1904
        elif value < _MARCH_FIRST_1900:
            epoch = _EPOCH_1900
        else:
            epoch = _EPOCH_1900_LEAP
        delta = value - epoch
        milliseconds = delta.seconds * 1000 + delta.microseconds // 1000
        serial = delta.days + milliseconds / _MILLISECONDS_PER_DAY
    if isinstance(serial, float) and serial.is_integer():
        return int(serial)
    return serial
def detect_format(data)

Determine the Excel container family of the input: a zip archive containing an OOXML workbook part, a zip archive containing an XLSB workbook part, an OLE compound file with a workbook stream, or a raw BIFF record stream that is not wrapped in any container. The zip detection accepts a part at its conventional location or one that the content types of the package declare as the main document part, which is exactly what the readers resolve; any other zip is not a workbook.

Expand source code Browse git
def detect_format(data: bytes | bytearray | memoryview) -> ExcelFormat | None:
    """
    Determine the Excel container family of the input: a zip archive containing an OOXML
    workbook part, a zip archive containing an XLSB workbook part, an OLE compound file with
    a workbook stream, or a raw BIFF record stream that is not wrapped in any container.
    The zip detection accepts a part at its conventional location or one that the content
    types of the package declare as the main document part, which is exactly what the readers
    resolve; any other zip is not a workbook.
    """
    view = memoryview(data)
    if view[:2] == b'PK':
        try:
            package = _Package(view)
        except zipfile.BadZipFile:
            return None
        if 'xl/workbook.xml' in package:
            return ExcelFormat.OOXML
        if 'xl/workbook.bin' in package:
            return ExcelFormat.XLSB
        try:
            content_types = package.open('[Content_Types].xml')
        except ExcelFormatError:
            return None
        if content_types is not None:
            try:
                root = fromstring(content_types.read())
            except (*XML_DEFECTS, ExcelFormatError):
                return None
            for element in root.iter():
                if (
                    local_name(element.tag) == 'Override'
                    and 'sheet.main+xml' in (element.get('ContentType') or '')
                ):
                    return ExcelFormat.OOXML
        return None
    if get_microsoft_format(view) == Fmt.XLS:
        return ExcelFormat.BIFF
    if (
        view[:1] == b'\x09'
        and view[1:2] in _RAW_BIFF_VERSIONS
        and int.from_bytes(view[2:4], 'little') in _RAW_BIFF_LENGTHS
    ):
        return ExcelFormat.BIFF
    return None
def open_workbook(data)

Read a workbook of any supported Excel format. Raises ExcelFormatError when the input is not a workbook at all; sheets that are defective are reported while iterating their cells rather than when opening the workbook.

Expand source code Browse git
def open_workbook(data: bytes | bytearray | memoryview) -> ExcelWorkbook:
    """
    Read a workbook of any supported Excel format. Raises `ExcelFormatError` when the input
    is not a workbook at all; sheets that are defective are reported while iterating their
    cells rather than when opening the workbook.
    """
    detected = detect_format(data)
    if detected is ExcelFormat.OOXML:
        from refinery.lib.excel.ooxml import OoxmlWorkbook
        return OoxmlWorkbook(data)
    if detected is ExcelFormat.BIFF:
        from refinery.lib.excel.biff import BiffWorkbook
        return BiffWorkbook(data)
    if detected is ExcelFormat.XLSB:
        from refinery.lib.excel.xlsb import XlsbWorkbook
        return XlsbWorkbook(data)
    if detected is None:
        raise ExcelFormatError('Input not recognized as Excel workbook.')
    raise ExcelFormatError(F'The {detected.name} format is not implemented yet.')
def parse_formula(text, international=None)

Parse formula text into the AST model. The leading = of a stored formula is optional. Any input that does not read as one whole formula becomes an XlUnparsedFormula carrying the text, never an exception. The parse runs under TREE_RECURSION_DEPTH, so how deeply a formula may nest does not depend on the recursion limit of the caller.

Expand source code Browse git
def parse_formula(
    text: str,
    international: International | None = None,
) -> Expression:
    """
    Parse formula text into the AST model. The leading `=` of a stored formula is optional. Any
    input that does not read as one whole formula becomes an `XlUnparsedFormula` carrying the
    text, never an exception. The parse runs under `refinery.lib.scripts.TREE_RECURSION_DEPTH`,
    so how deeply a formula may nest does not depend on the recursion limit of the caller.
    """
    if not text or not text.strip():
        return XlUnparsedFormula(text=text)
    parser = _Parser(text.strip(), international or International())
    try:
        with RecursionDepth(TREE_RECURSION_DEPTH):
            expression = parser.parse()
    except (_ParseFailure, RecursionError, ValueError, IndexError):
        return XlUnparsedFormula(text=text)
    return expression
def rc2ref(row, col)

Convert one-based row and column numbers into a cell reference like B12.

Expand source code Browse git
def rc2ref(row: int, col: int) -> str:
    """
    Convert one-based row and column numbers into a cell reference like `B12`.
    """
    if row <= 0:
        raise ValueError
    if col <= 0:
        raise ValueError
    return F'{column_letters(col)}{row}'
def ref2rc(ref)

Convert a cell reference like B12 into its one-based row and column.

Expand source code Browse git
def ref2rc(ref: str) -> tuple[int, int]:
    """
    Convert a cell reference like `B12` into its one-based row and column.
    """
    match = re.match(R'^([A-Za-z]+)(\d+)$', ref)
    if not match:
        raise ValueError
    col = functools.reduce(
        lambda acc, c: (acc * 26) + c,
        (ord(c.upper()) - 0x40 for c in match[1]),
        0,
    )
    row = int(match[2], 10)
    if row <= 0:
        raise ValueError
    return row, col
def serial_to_datetime(serial, date_mode_1904)

Convert an Excel serial date number into a datetime, or into a time when the serial number carries only a fraction of a day. The 1900 epoch accounts for the spurious leap day that Excel inherited from Lotus 1-2-3: serial 60 is the non-existent date of February 29, 1900, and serials up to 59 are shifted back by one day.

Expand source code Browse git
def serial_to_datetime(
    serial: int | float,
    date_mode_1904: bool,
) -> datetime.datetime | datetime.time:
    """
    Convert an Excel serial date number into a datetime, or into a time when the serial number
    carries only a fraction of a day. The 1900 epoch accounts for the spurious leap day that
    Excel inherited from Lotus 1-2-3: serial 60 is the non-existent date of February 29, 1900,
    and serials up to 59 are shifted back by one day.
    """
    days, fraction = divmod(float(serial), 1.0)
    milliseconds = round(fraction * _MILLISECONDS_PER_DAY)
    if 0 <= serial < 1 and milliseconds < _MILLISECONDS_PER_DAY:
        return (datetime.datetime.min + datetime.timedelta(milliseconds=milliseconds)).time()
    if date_mode_1904:
        epoch = _EPOCH_1904
    elif 0 < serial < 60:
        epoch = _EPOCH_1900
    else:
        epoch = _EPOCH_1900_LEAP
    return epoch + datetime.timedelta(days=days, milliseconds=milliseconds)
def synthesize_formula(expression, international=None)

Print a formula Expression as one line of formula text without the leading =.

Expand source code Browse git
def synthesize_formula(
    expression: Expression,
    international: International | None = None,
) -> str:
    """
    Print a formula `Expression` as one line of formula text without the leading `=`.
    """
    return FormulaSynthesizer(international).convert(expression)

Classes

class Cell (row, col, kind, value, formula, assignment=False)

A single spreadsheet cell at a one-based row and col. The value is typed by kind; refinery.lib.excel.formula is the formula source as stored by the format, without interpretation, and is None for cells that are not formulas. assignment marks a formula the container says assigns a value to a name — the bx attribute of an OOXML macrosheet formula, whose text spells the assignment as a comparison; the formats that store the assignment as the SET.NAME call it already spells leave the flag unset.

Expand source code Browse git
class Cell(NamedTuple):
    """
    A single spreadsheet cell at a one-based `row` and `col`. The `value` is typed by `kind`;
    `formula` is the formula source as stored by the format, without interpretation, and is
    `None` for cells that are not formulas. `assignment` marks a formula the container says
    assigns a value to a name — the `bx` attribute of an OOXML macrosheet formula, whose text
    spells the assignment as a comparison; the formats that store the assignment as the
    `SET.NAME` call it already spells leave the flag unset.
    """
    row: int
    col: int
    kind: CellKind
    value: CellValue
    formula: FormulaSource
    assignment: bool = False

Ancestors

  • builtins.tuple

Instance variables

var row

Alias for field number 0

Expand source code Browse git
class Cell(NamedTuple):
    """
    A single spreadsheet cell at a one-based `row` and `col`. The `value` is typed by `kind`;
    `formula` is the formula source as stored by the format, without interpretation, and is
    `None` for cells that are not formulas. `assignment` marks a formula the container says
    assigns a value to a name — the `bx` attribute of an OOXML macrosheet formula, whose text
    spells the assignment as a comparison; the formats that store the assignment as the
    `SET.NAME` call it already spells leave the flag unset.
    """
    row: int
    col: int
    kind: CellKind
    value: CellValue
    formula: FormulaSource
    assignment: bool = False
var col

Alias for field number 1

Expand source code Browse git
class Cell(NamedTuple):
    """
    A single spreadsheet cell at a one-based `row` and `col`. The `value` is typed by `kind`;
    `formula` is the formula source as stored by the format, without interpretation, and is
    `None` for cells that are not formulas. `assignment` marks a formula the container says
    assigns a value to a name — the `bx` attribute of an OOXML macrosheet formula, whose text
    spells the assignment as a comparison; the formats that store the assignment as the
    `SET.NAME` call it already spells leave the flag unset.
    """
    row: int
    col: int
    kind: CellKind
    value: CellValue
    formula: FormulaSource
    assignment: bool = False
var kind

Alias for field number 2

Expand source code Browse git
class Cell(NamedTuple):
    """
    A single spreadsheet cell at a one-based `row` and `col`. The `value` is typed by `kind`;
    `formula` is the formula source as stored by the format, without interpretation, and is
    `None` for cells that are not formulas. `assignment` marks a formula the container says
    assigns a value to a name — the `bx` attribute of an OOXML macrosheet formula, whose text
    spells the assignment as a comparison; the formats that store the assignment as the
    `SET.NAME` call it already spells leave the flag unset.
    """
    row: int
    col: int
    kind: CellKind
    value: CellValue
    formula: FormulaSource
    assignment: bool = False
var value

Alias for field number 3

Expand source code Browse git
class Cell(NamedTuple):
    """
    A single spreadsheet cell at a one-based `row` and `col`. The `value` is typed by `kind`;
    `formula` is the formula source as stored by the format, without interpretation, and is
    `None` for cells that are not formulas. `assignment` marks a formula the container says
    assigns a value to a name — the `bx` attribute of an OOXML macrosheet formula, whose text
    spells the assignment as a comparison; the formats that store the assignment as the
    `SET.NAME` call it already spells leave the flag unset.
    """
    row: int
    col: int
    kind: CellKind
    value: CellValue
    formula: FormulaSource
    assignment: bool = False
var formula

Alias for field number 4

Expand source code Browse git
class Cell(NamedTuple):
    """
    A single spreadsheet cell at a one-based `row` and `col`. The `value` is typed by `kind`;
    `formula` is the formula source as stored by the format, without interpretation, and is
    `None` for cells that are not formulas. `assignment` marks a formula the container says
    assigns a value to a name — the `bx` attribute of an OOXML macrosheet formula, whose text
    spells the assignment as a comparison; the formats that store the assignment as the
    `SET.NAME` call it already spells leave the flag unset.
    """
    row: int
    col: int
    kind: CellKind
    value: CellValue
    formula: FormulaSource
    assignment: bool = False
var assignment

Alias for field number 5

Expand source code Browse git
class Cell(NamedTuple):
    """
    A single spreadsheet cell at a one-based `row` and `col`. The `value` is typed by `kind`;
    `formula` is the formula source as stored by the format, without interpretation, and is
    `None` for cells that are not formulas. `assignment` marks a formula the container says
    assigns a value to a name — the `bx` attribute of an OOXML macrosheet formula, whose text
    spells the assignment as a comparison; the formats that store the assignment as the
    `SET.NAME` call it already spells leave the flag unset.
    """
    row: int
    col: int
    kind: CellKind
    value: CellValue
    formula: FormulaSource
    assignment: bool = False
class CellKind (*args, **kwds)

The kind of value a spreadsheet cell holds. A formula cell with a cached result carries the kind of that result together with the formula source; a formula cell without a cached result carries FORMULA and no value.

Expand source code Browse git
class CellKind(enum.Enum):
    """
    The kind of value a spreadsheet cell holds. A formula cell with a cached result carries the
    kind of that result together with the formula source; a formula cell without a cached
    result carries `FORMULA` and no value.
    """
    TEXT = enum.auto()
    NUMBER = enum.auto()
    DATE = enum.auto()
    BOOLEAN = enum.auto()
    ERROR = enum.auto()
    BLANK = enum.auto()
    FORMULA = enum.auto()

Ancestors

  • enum.Enum

Class variables

var TEXT

The type of the None singleton.

var NUMBER

The type of the None singleton.

var DATE

The type of the None singleton.

var BOOLEAN

The type of the None singleton.

var ERROR

The type of the None singleton.

var BLANK

The type of the None singleton.

var FORMULA

The type of the None singleton.

class ExcelFormat (*args, **kwds)

The container family of an Excel workbook.

Expand source code Browse git
class ExcelFormat(enum.Enum):
    """
    The container family of an Excel workbook.
    """
    OOXML = enum.auto()
    XLSB = enum.auto()
    BIFF = enum.auto()

Ancestors

  • enum.Enum

Class variables

var OOXML

The type of the None singleton.

var XLSB

The type of the None singleton.

var BIFF

The type of the None singleton.

class ExcelFormatError (*args, **kwargs)

The structure of a workbook is defective; the cells that were read before the defect are still available to the caller while anything after it is not.

Expand source code Browse git
class ExcelFormatError(Exception):
    """
    The structure of a workbook is defective; the cells that were read before the defect are
    still available to the caller while anything after it is not.
    """

Ancestors

  • builtins.Exception
  • builtins.BaseException
class ExcelSheet

A single sheet of an ExcelWorkbook.

Expand source code Browse git
class ExcelSheet(ABC):
    """
    A single sheet of an `ExcelWorkbook`.
    """
    name: str
    kind: SheetKind

    @abstractmethod
    def cells(self) -> Iterator[Cell]:
        """
        Iterate over every cell of the sheet, in document order.
        """

Ancestors

  • abc.ABC

Subclasses

Class variables

var name

The type of the None singleton.

var kind

The type of the None singleton.

Methods

def cells(self)

Iterate over every cell of the sheet, in document order.

Expand source code Browse git
@abstractmethod
def cells(self) -> Iterator[Cell]:
    """
    Iterate over every cell of the sheet, in document order.
    """
class ExcelWorkbook

A workbook of arbitrary Excel format, exposing its sheets without interpreting or rendering any of the values they contain.

Expand source code Browse git
class ExcelWorkbook(ABC):
    """
    A workbook of arbitrary Excel format, exposing its sheets without interpreting or
    rendering any of the values they contain.
    """

    format: ExcelFormat

    #: Whether the serial date numbers of this workbook count from the 1904 epoch.
    date_mode_1904: bool

    @abstractmethod
    def sheets(self) -> Sequence[ExcelSheet]:
        """
        All sheets of the workbook in document order.
        """

    def sheet_index(self, name: str) -> int | None:
        """
        The position of a sheet in the sequence the format's defined-name scopes count: the
        full sheet table of the workbook, chartsheets included, matched case-insensitively as
        Excel matches sheet names. `None` when the workbook has no such sheet.
        """
        key = name.lower()
        for index, sheet in enumerate(self.sheets()):
            if sheet.name.lower() == key:
                return index
        return None

    def formula(self, source: FormulaSource) -> Expression | None:
        """
        Decode the formula source of a cell or a defined name of this workbook into the
        formula model. A source the format cannot decode yields the unparsed carrier rather
        than an error, and `None` — a cell that is not a formula — yields `None`.
        """
        raise NotImplementedError

    def defined_names(self) -> Sequence[DefinedName]:
        """
        All defined names of the workbook, in the order their records appear; the position
        of a name in this list is the index its format's name token carries.
        """
        raise NotImplementedError

Ancestors

  • abc.ABC

Subclasses

Class variables

var format

The type of the None singleton.

var date_mode_1904

Whether the serial date numbers of this workbook count from the 1904 epoch.

Methods

def sheets(self)

All sheets of the workbook in document order.

Expand source code Browse git
@abstractmethod
def sheets(self) -> Sequence[ExcelSheet]:
    """
    All sheets of the workbook in document order.
    """
def sheet_index(self, name)

The position of a sheet in the sequence the format's defined-name scopes count: the full sheet table of the workbook, chartsheets included, matched case-insensitively as Excel matches sheet names. None when the workbook has no such sheet.

Expand source code Browse git
def sheet_index(self, name: str) -> int | None:
    """
    The position of a sheet in the sequence the format's defined-name scopes count: the
    full sheet table of the workbook, chartsheets included, matched case-insensitively as
    Excel matches sheet names. `None` when the workbook has no such sheet.
    """
    key = name.lower()
    for index, sheet in enumerate(self.sheets()):
        if sheet.name.lower() == key:
            return index
    return None
def formula(self, source)

Decode the formula source of a cell or a defined name of this workbook into the formula model. A source the format cannot decode yields the unparsed carrier rather than an error, and None — a cell that is not a formula — yields None.

Expand source code Browse git
def formula(self, source: FormulaSource) -> Expression | None:
    """
    Decode the formula source of a cell or a defined name of this workbook into the
    formula model. A source the format cannot decode yields the unparsed carrier rather
    than an error, and `None` — a cell that is not a formula — yields `None`.
    """
    raise NotImplementedError
def defined_names(self)

All defined names of the workbook, in the order their records appear; the position of a name in this list is the index its format's name token carries.

Expand source code Browse git
def defined_names(self) -> Sequence[DefinedName]:
    """
    All defined names of the workbook, in the order their records appear; the position
    of a name in this list is the index its format's name token carries.
    """
    raise NotImplementedError
class FormulaSynthesizer (international=None)

Print a formula Expression as formula text. The list separator of the output follows the International it is given, so a workbook stored under another locale round-trips too.

Expand source code Browse git
class FormulaSynthesizer(Synthesizer):
    """
    Print a formula `Expression` as formula text. The list separator of the output follows the
    `International` it is given, so a workbook stored under another locale round-trips too.
    """

    def __init__(self, international: International | None = None):
        international = international or International()
        super().__init__()
        self._sep = international.list_separator
        self._lb = international.left_bracket
        self._rb = international.right_bracket

    def convert(self, node: Node) -> str:
        if isinstance(node, XlBinaryExpression) and node.operator is XlBinaryOperator.UNION:
            node = XlParenExpression(operand=node)
        with RecursionDepth(TREE_RECURSION_DEPTH):
            return super().convert(node)

    def _emit_operand(self, operand: Expression | None, parent: int, left: bool):
        if operand is None:
            return
        if isinstance(operand, XlBinaryExpression):
            if operand.operator is XlBinaryOperator.UNION:
                self._write('(')
                self.visit(operand)
                self._write(')')
                return
            precedence = _PRECEDENCE[operand.operator]
            if precedence < parent or (precedence == parent and not left):
                self._write('(')
                self.visit(operand)
                self._write(')')
                return
        elif isinstance(operand, XlUnaryExpression):
            if _PRECEDENCE[operand.operator] < parent:
                self._write('(')
                self.visit(operand)
                self._write(')')
                return
        self.visit(operand)

    def visit_XlNumber(self, node: XlNumber):
        self._write(self._format_number(node))

    def visit_XlString(self, node: XlString):
        escaped = node.value.replace('"', '""')
        self._write(F'"{escaped}"')

    def visit_XlBoolean(self, node: XlBoolean):
        self._write('TRUE' if node.value else 'FALSE')

    def visit_XlError(self, node: XlError):
        self._write(node.value)

    def visit_XlBinaryExpression(self, node: XlBinaryExpression):
        precedence = _PRECEDENCE[node.operator]
        text = ' ' if node.operator is XlBinaryOperator.ISECT else node.operator.value
        self._emit_operand(node.left, precedence, left=True)
        self._write(text)
        self._emit_operand(node.right, precedence, left=False)

    def visit_XlUnaryExpression(self, node: XlUnaryExpression):
        if node.operator is XlUnaryOperator.PERCENT:
            self._emit_operand(node.operand, _PRECEDENCE[node.operator], left=False)
            self._write(node.operator.value)
            return
        self._write(node.operator.value)
        self._emit_operand(node.operand, _PRECEDENCE[node.operator], left=True)

    def visit_XlParenExpression(self, node: XlParenExpression):
        self._write('(')
        if node.operand is not None:
            self.visit(node.operand)
        self._write(')')

    def visit_XlMissingArgument(self, node: XlMissingArgument):
        pass

    def visit_XlUnparsedFormula(self, node: XlUnparsedFormula):
        self._write(node.text)

    def visit_XlA1Reference(self, node: XlA1Reference):
        if node.sheets is not None:
            self._write(_qualify(node.sheets))
        column = column_letters(node.col)
        row = F'${node.row}' if not node.relative_row else str(node.row)
        col = F'${column}' if not node.relative_col else column
        self._write(F'{col}{row}')

    def visit_XlR1C1Reference(self, node: XlR1C1Reference):
        if node.sheets is not None:
            self._write(_qualify(node.sheets))
        self._write(
            F'{self._axis("R", node.row, node.relative_row)}'
            F'{self._axis("C", node.col, node.relative_col)}'
        )

    def visit_XlDefinedName(self, node: XlDefinedName):
        if node.sheet is not None:
            self._write(_qualify((node.sheet,)))
        self._write(node.name)

    def visit_XlFunctionCall(self, node: XlFunctionCall):
        if isinstance(node.callee, str):
            self._write(node.callee)
        else:
            self.visit(node.callee)
        self._write('(')
        for index, argument in enumerate(node.arguments):
            if index:
                self._write(self._sep)
            # a call argument sits in a fully delimited position, so no precedence asks for
            # parentheses around it — only a union does, because bare it would read as two
            self._emit_operand(argument, 0, left=False)
        self._write(')')

    def visit_XlArrayConstant(self, node: XlArrayConstant):
        self._write('{')
        for rindex, row in enumerate(node.rows):
            if rindex:
                self._write(';')
            for cindex, item in enumerate(row):
                if cindex:
                    self._write(self._sep)
                self.visit(item)
        self._write('}')

    def _axis(self, letter: str, value: int, relative: bool) -> str:
        if not relative:
            return F'{letter}{value}'
        if value:
            return F'{letter}{self._lb}{value}{self._rb}'
        return letter

    def _format_number(self, node: XlNumber) -> str:
        try:
            if float(node.raw) == node.value:
                return node.raw
        except (TypeError, ValueError):
            pass
        if isinstance(node.value, int):
            return str(node.value)
        return repr(node.value)

Ancestors

Methods

def convert(self, node)
Expand source code Browse git
def convert(self, node: Node) -> str:
    if isinstance(node, XlBinaryExpression) and node.operator is XlBinaryOperator.UNION:
        node = XlParenExpression(operand=node)
    with RecursionDepth(TREE_RECURSION_DEPTH):
        return super().convert(node)
def visit_XlNumber(self, node)
Expand source code Browse git
def visit_XlNumber(self, node: XlNumber):
    self._write(self._format_number(node))
def visit_XlString(self, node)
Expand source code Browse git
def visit_XlString(self, node: XlString):
    escaped = node.value.replace('"', '""')
    self._write(F'"{escaped}"')
def visit_XlBoolean(self, node)
Expand source code Browse git
def visit_XlBoolean(self, node: XlBoolean):
    self._write('TRUE' if node.value else 'FALSE')
def visit_XlError(self, node)
Expand source code Browse git
def visit_XlError(self, node: XlError):
    self._write(node.value)
def visit_XlBinaryExpression(self, node)
Expand source code Browse git
def visit_XlBinaryExpression(self, node: XlBinaryExpression):
    precedence = _PRECEDENCE[node.operator]
    text = ' ' if node.operator is XlBinaryOperator.ISECT else node.operator.value
    self._emit_operand(node.left, precedence, left=True)
    self._write(text)
    self._emit_operand(node.right, precedence, left=False)
def visit_XlUnaryExpression(self, node)
Expand source code Browse git
def visit_XlUnaryExpression(self, node: XlUnaryExpression):
    if node.operator is XlUnaryOperator.PERCENT:
        self._emit_operand(node.operand, _PRECEDENCE[node.operator], left=False)
        self._write(node.operator.value)
        return
    self._write(node.operator.value)
    self._emit_operand(node.operand, _PRECEDENCE[node.operator], left=True)
def visit_XlParenExpression(self, node)
Expand source code Browse git
def visit_XlParenExpression(self, node: XlParenExpression):
    self._write('(')
    if node.operand is not None:
        self.visit(node.operand)
    self._write(')')
def visit_XlMissingArgument(self, node)
Expand source code Browse git
def visit_XlMissingArgument(self, node: XlMissingArgument):
    pass
def visit_XlUnparsedFormula(self, node)
Expand source code Browse git
def visit_XlUnparsedFormula(self, node: XlUnparsedFormula):
    self._write(node.text)
def visit_XlA1Reference(self, node)
Expand source code Browse git
def visit_XlA1Reference(self, node: XlA1Reference):
    if node.sheets is not None:
        self._write(_qualify(node.sheets))
    column = column_letters(node.col)
    row = F'${node.row}' if not node.relative_row else str(node.row)
    col = F'${column}' if not node.relative_col else column
    self._write(F'{col}{row}')
def visit_XlR1C1Reference(self, node)
Expand source code Browse git
def visit_XlR1C1Reference(self, node: XlR1C1Reference):
    if node.sheets is not None:
        self._write(_qualify(node.sheets))
    self._write(
        F'{self._axis("R", node.row, node.relative_row)}'
        F'{self._axis("C", node.col, node.relative_col)}'
    )
def visit_XlDefinedName(self, node)
Expand source code Browse git
def visit_XlDefinedName(self, node: XlDefinedName):
    if node.sheet is not None:
        self._write(_qualify((node.sheet,)))
    self._write(node.name)
def visit_XlFunctionCall(self, node)
Expand source code Browse git
def visit_XlFunctionCall(self, node: XlFunctionCall):
    if isinstance(node.callee, str):
        self._write(node.callee)
    else:
        self.visit(node.callee)
    self._write('(')
    for index, argument in enumerate(node.arguments):
        if index:
            self._write(self._sep)
        # a call argument sits in a fully delimited position, so no precedence asks for
        # parentheses around it — only a union does, because bare it would read as two
        self._emit_operand(argument, 0, left=False)
    self._write(')')
def visit_XlArrayConstant(self, node)
Expand source code Browse git
def visit_XlArrayConstant(self, node: XlArrayConstant):
    self._write('{')
    for rindex, row in enumerate(node.rows):
        if rindex:
            self._write(';')
        for cindex, item in enumerate(row):
            if cindex:
                self._write(self._sep)
            self.visit(item)
    self._write('}')

Inherited members

class International (list_separator=',', left_bracket='[', right_bracket=']')

The locale-dependent characters of formula text: the separator between the arguments of a call and between the columns of an array constant, and the brackets around a relative R1C1 offset.

Expand source code Browse git
class International(NamedTuple):
    """
    The locale-dependent characters of formula text: the separator between the arguments of a
    call and between the columns of an array constant, and the brackets around a relative R1C1
    offset.
    """

    list_separator: str = ','
    left_bracket: str = '['
    right_bracket: str = ']'

Ancestors

  • builtins.tuple

Instance variables

var list_separator

Alias for field number 0

Expand source code Browse git
class International(NamedTuple):
    """
    The locale-dependent characters of formula text: the separator between the arguments of a
    call and between the columns of an array constant, and the brackets around a relative R1C1
    offset.
    """

    list_separator: str = ','
    left_bracket: str = '['
    right_bracket: str = ']'
var left_bracket

Alias for field number 1

Expand source code Browse git
class International(NamedTuple):
    """
    The locale-dependent characters of formula text: the separator between the arguments of a
    call and between the columns of an array constant, and the brackets around a relative R1C1
    offset.
    """

    list_separator: str = ','
    left_bracket: str = '['
    right_bracket: str = ']'
var right_bracket

Alias for field number 2

Expand source code Browse git
class International(NamedTuple):
    """
    The locale-dependent characters of formula text: the separator between the arguments of a
    call and between the columns of an array constant, and the brackets around a relative R1C1
    offset.
    """

    list_separator: str = ','
    left_bracket: str = '['
    right_bracket: str = ']'
class SheetKind (*args, **kwds)

The purpose of a sheet inside a workbook; only worksheets and macrosheets contain cells.

Expand source code Browse git
class SheetKind(enum.Enum):
    """
    The purpose of a sheet inside a workbook; only worksheets and macrosheets contain cells.
    """
    WORKSHEET = enum.auto()
    MACROSHEET = enum.auto()
    CHART = enum.auto()
    MODULE = enum.auto()
    OTHER = enum.auto()

Ancestors

  • enum.Enum

Class variables

var WORKSHEET

The type of the None singleton.

var MACROSHEET

The type of the None singleton.

var CHART

The type of the None singleton.

var MODULE

The type of the None singleton.

var OTHER

The type of the None singleton.

class XlArrayConstant (offset=-1, parent=None, leading_comments=<factory>, trailing_comments=<factory>, rows=<factory>)

A constant array literal such as {1,2;3,4}. Each row is a tuple of literals; the list separator divides columns and the semicolon divides rows. The rows are tuples rather than lists so that the node framework recognizes the nesting as children.

Expand source code Browse git
@dataclass(repr=False, eq=False)
class XlArrayConstant(Expression):
    """
    A constant array literal such as `{1,2;3,4}`. Each row is a tuple of literals; the list
    separator divides columns and the semicolon divides rows. The rows are tuples rather than
    lists so that the node framework recognizes the nesting as children.
    """

    rows: list[tuple[XlLiteral, ...]] = field(default_factory=list)

Ancestors

Instance variables

var rows

The type of the None singleton.

Inherited members

class XlA1Reference (offset=-1, parent=None, leading_comments=<factory>, trailing_comments=<factory>, sheets=None, row=1, col=1, relative_row=True, relative_col=True)

A cell reference in A1 notation. The row and column are one-based absolute numbers, and the relative flags say which axes Excel copies the reference along; sheets holds one name for a qualified reference, two for the 3-D span that Sheet1:Sheet2!A1 spells, and nothing for a reference local to the sheet the formula sits on.

Expand source code Browse git
@dataclass(repr=False, eq=False)
class XlA1Reference(Expression):
    """
    A cell reference in A1 notation. The row and column are one-based absolute numbers, and the
    relative flags say which axes Excel copies the reference along; `sheets` holds one name for
    a qualified reference, two for the 3-D span that `Sheet1:Sheet2!A1` spells, and nothing for
    a reference local to the sheet the formula sits on.
    """

    sheets: tuple[str, ...] | None = None
    row: int = 1
    col: int = 1
    relative_row: bool = True
    relative_col: bool = True

Ancestors

Instance variables

var sheets

The type of the None singleton.

var row

The type of the None singleton.

var col

The type of the None singleton.

var relative_row

The type of the None singleton.

var relative_col

The type of the None singleton.

Inherited members

class XlBinaryExpression (offset=-1, parent=None, leading_comments=<factory>, trailing_comments=<factory>, left=None, operator=XlBinaryOperator.ADD, right=None)

XlBinaryExpression(offset: 'int' = -1, parent: 'Node | None' = None, leading_comments: 'list[str]' = , trailing_comments: 'list[str]' = , left: 'Expression | None' = None, operator: 'XlBinaryOperator' = , right: 'Expression | None' = None)

Expand source code Browse git
@dataclass(repr=False, eq=False)
class XlBinaryExpression(Expression):
    left: Expression | None = None
    operator: XlBinaryOperator = XlBinaryOperator.ADD
    right: Expression | None = None

Ancestors

Instance variables

var left

The type of the None singleton.

var operator

The type of the None singleton.

var right

The type of the None singleton.

Inherited members

class XlBinaryOperator (*args, **kwds)

An operator written between two operands. The three reference operators are members of the same family: a range spans, an intersection narrows to shared cells, and a union gathers.

Expand source code Browse git
class XlBinaryOperator(enum.Enum):
    """
    An operator written between two operands. The three reference operators are members of the
    same family: a range spans, an intersection narrows to shared cells, and a union gathers.
    """

    ADD = '+'
    SUB = '-'
    MUL = '*'
    DIV = '/'
    POW = '^'
    CONCAT = '&'
    RANGE = ':'
    ISECT = ' '
    UNION = ','
    EQ = '='
    NE = '<>'
    LT = '<'
    LE = '<='
    GT = '>'
    GE = '>='

Ancestors

  • enum.Enum

Class variables

var ADD

The type of the None singleton.

var SUB

The type of the None singleton.

var MUL

The type of the None singleton.

var DIV

The type of the None singleton.

var POW

The type of the None singleton.

var CONCAT

The type of the None singleton.

var RANGE

The type of the None singleton.

var ISECT

The type of the None singleton.

var UNION

The type of the None singleton.

var EQ

The type of the None singleton.

var NE

The type of the None singleton.

var LT

The type of the None singleton.

var LE

The type of the None singleton.

var GT

The type of the None singleton.

var GE

The type of the None singleton.

class XlBoolean (offset=-1, parent=None, leading_comments=<factory>, trailing_comments=<factory>, value=False)

XlBoolean(offset: 'int' = -1, parent: 'Node | None' = None, leading_comments: 'list[str]' = , trailing_comments: 'list[str]' = , value: 'bool' = False)

Expand source code Browse git
@dataclass(repr=False, eq=False)
class XlBoolean(Expression):
    value: bool = False

Ancestors

Instance variables

var value

The type of the None singleton.

Inherited members

class XlDefinedName (offset=-1, parent=None, leading_comments=<factory>, trailing_comments=<factory>, name='', sheet=None)

A reference to a defined name, qualified by the sheet that scoped it when it is not global. The _xlfn. and _xlnm. prefixes Excel writes for future and built-in names are part of the name and preserved verbatim.

Expand source code Browse git
@dataclass(repr=False, eq=False)
class XlDefinedName(Expression):
    """
    A reference to a defined name, qualified by the sheet that scoped it when it is not global.
    The `_xlfn.` and `_xlnm.` prefixes Excel writes for future and built-in names are part of
    the name and preserved verbatim.
    """

    name: str = ''
    sheet: str | None = None

Ancestors

Instance variables

var name

The type of the None singleton.

var sheet

The type of the None singleton.

Inherited members

class XlError (offset=-1, parent=None, leading_comments=<factory>, trailing_comments=<factory>, value='#VALUE!')

An error literal such as #N/A; the text domain is the one ERROR_TEXT decodes cell errors into.

Expand source code Browse git
@dataclass(repr=False, eq=False)
class XlError(Expression):
    """
    An error literal such as `#N/A`; the text domain is the one `ERROR_TEXT` decodes cell
    errors into.
    """

    value: str = '#VALUE!'

Ancestors

Instance variables

var value

The type of the None singleton.

Inherited members

class XlFunctionCall (offset=-1, parent=None, leading_comments=<factory>, trailing_comments=<factory>, callee='', arguments=<factory>)

A call to a function or macro. The callee is normally the function name; a defined name or a reference can also be called, because a macro is a cell and R1C1() is the call to the macro in the cell that reference points at.

Expand source code Browse git
@dataclass(repr=False, eq=False)
class XlFunctionCall(Expression):
    """
    A call to a function or macro. The callee is normally the function name; a defined name or
    a reference can also be called, because a macro is a cell and `R1C1()` is the call to the
    macro in the cell that reference points at.
    """

    callee: str | XlDefinedName | XlA1Reference | XlR1C1Reference = ''
    arguments: list[Expression] = field(default_factory=list)

    def canonical_form(self) -> Expression | None:
        # a defined name in the callee position spells no differently than a function name, so
        # text cannot tell the two apart and comparing programs must not either
        if isinstance(self.callee, XlDefinedName):
            return XlFunctionCall(callee=self.callee.name, arguments=self.arguments)
        return None

Ancestors

Instance variables

var arguments

The type of the None singleton.

var callee

The type of the None singleton.

Inherited members

class XlMissingArgument (offset=-1, parent=None, leading_comments=<factory>, trailing_comments=<factory>)

An argument a call omits: IF(a,,b) passes nothing in the middle. It prints no text.

Expand source code Browse git
@dataclass(repr=False, eq=False)
class XlMissingArgument(Expression):
    """
    An argument a call omits: `IF(a,,b)` passes nothing in the middle. It prints no text.
    """

Ancestors

Inherited members

class XlNumber (offset=-1, parent=None, leading_comments=<factory>, trailing_comments=<factory>, value=0, raw='')

XlNumber(offset: 'int' = -1, parent: 'Node | None' = None, leading_comments: 'list[str]' = , trailing_comments: 'list[str]' = , value: 'int | float' = 0, raw: 'str' = '')

Expand source code Browse git
@dataclass(repr=False, eq=False)
class XlNumber(Expression, spelling='raw'):
    value: int | float = 0
    raw: str = ''

Ancestors

Instance variables

var value

The type of the None singleton.

var raw

The type of the None singleton.

Inherited members

class XlParenExpression (offset=-1, parent=None, leading_comments=<factory>, trailing_comments=<factory>, operand=None)

A parenthesis the source wrote around an operand. It spells nothing of its own, so comparing programs identifies it with what it holds.

Expand source code Browse git
@dataclass(repr=False, eq=False)
class XlParenExpression(Expression):
    """
    A parenthesis the source wrote around an operand. It spells nothing of its own, so comparing
    programs identifies it with what it holds.
    """

    operand: Expression | None = None

    def canonical_form(self) -> Expression | None:
        return self.operand

Ancestors

Instance variables

var operand

The type of the None singleton.

Inherited members

class XlR1C1Reference (offset=-1, parent=None, leading_comments=<factory>, trailing_comments=<factory>, sheets=None, row=1, col=1, relative_row=True, relative_col=True)

A cell reference in R1C1 notation. When a relative flag is set, the number on that axis is an offset from the cell that contains the formula rather than an absolute position, so only the macrosheet layer that knows the containing cell can resolve it. The sheets field carries the same qualification a XlA1Reference does.

Expand source code Browse git
@dataclass(repr=False, eq=False)
class XlR1C1Reference(Expression):
    """
    A cell reference in R1C1 notation. When a relative flag is set, the number on that axis is
    an offset from the cell that contains the formula rather than an absolute position, so only
    the macrosheet layer that knows the containing cell can resolve it. The `sheets` field
    carries the same qualification a `XlA1Reference` does.
    """

    sheets: tuple[str, ...] | None = None
    row: int = 1
    col: int = 1
    relative_row: bool = True
    relative_col: bool = True

Ancestors

Instance variables

var sheets

The type of the None singleton.

var row

The type of the None singleton.

var col

The type of the None singleton.

var relative_row

The type of the None singleton.

var relative_col

The type of the None singleton.

Inherited members

class XlString (offset=-1, parent=None, leading_comments=<factory>, trailing_comments=<factory>, value='')

XlString(offset: 'int' = -1, parent: 'Node | None' = None, leading_comments: 'list[str]' = , trailing_comments: 'list[str]' = , value: 'str' = '')

Expand source code Browse git
@dataclass(repr=False, eq=False)
class XlString(Expression):
    value: str = ''

Ancestors

Instance variables

var value

The type of the None singleton.

Inherited members

class XlUnaryExpression (offset=-1, parent=None, leading_comments=<factory>, trailing_comments=<factory>, operator=XlUnaryOperator.NEG, operand=None)

XlUnaryExpression(offset: 'int' = -1, parent: 'Node | None' = None, leading_comments: 'list[str]' = , trailing_comments: 'list[str]' = , operator: 'XlUnaryOperator' = , operand: 'Expression | None' = None)

Expand source code Browse git
@dataclass(repr=False, eq=False)
class XlUnaryExpression(Expression):
    operator: XlUnaryOperator = XlUnaryOperator.NEG
    operand: Expression | None = None

Ancestors

Instance variables

var operator

The type of the None singleton.

var operand

The type of the None singleton.

Inherited members

class XlUnaryOperator (*args, **kwds)

An operator written before or after one operand.

Expand source code Browse git
class XlUnaryOperator(enum.Enum):
    """
    An operator written before or after one operand.
    """

    NEG = '-'
    POS = '+'
    PERCENT = '%'

Ancestors

  • enum.Enum

Class variables

var NEG

The type of the None singleton.

var POS

The type of the None singleton.

var PERCENT

The type of the None singleton.

class XlUnparsedFormula (offset=-1, parent=None, leading_comments=<factory>, trailing_comments=<factory>, text='')

The carrier for a formula source the decoders or the parser could not read: hostile or truncated text, a token stream that does not decode. It prints the text it was handed.

Expand source code Browse git
@dataclass(repr=False, eq=False)
class XlUnparsedFormula(Expression, unparsed=True):
    """
    The carrier for a formula source the decoders or the parser could not read: hostile or
    truncated text, a token stream that does not decode. It prints the text it was handed.
    """

    text: str = ''

Ancestors

Instance variables

var text

The type of the None singleton.

Inherited members