fix(parser): ячейки из одних пробелов не хранятся в Sheet.cells (T2.1)

make_cell возвращает None для строки, у которой strip() пуст (пробелы, NBSP и
прочие Unicode-пробелы): «пробелы = пусто» на уровне хранения, sheet.cell даёт
EMPTY_CELL. Тесты: пробельные ячейки xlsx/xlsb отсутствуют в cells, строка из
пробелов пуста, merged-диапазон с пробельным origin даёт "".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
Andrey Epifancev 2026-10-05 12:16:28 +04:00
parent ed401a00a3
commit c6a768dc70
2 changed files with 20 additions and 7 deletions

View File

@ -19,7 +19,8 @@ def make_cell(value: object, *, bold: bool = False, indent: int = 0) -> Cell | N
"""Ячейка из значения библиотеки чтения; `None` — пустая ячейка (в `Sheet.cells` не попадает).
Числа — `Decimal(str(value))` без квантования (его делает `numbers.py`); bool проверяется раньше int.
Строка из пробелов остаётся `text` (значение как пришло), её `Cell.text` пуст. Время без даты — текстом.
Строка из одних пробельных символов (включая NBSP) — пустая ячейка (`None`): «пробелы» = пусто на уровне
хранения, а не только в аксессорах `Sheet`. Время без даты — текстом.
"""
if value is None or value == "":
return None
@ -30,6 +31,8 @@ def make_cell(value: object, *, bold: bool = False, indent: int = 0) -> Cell | N
if isinstance(value, datetime.date): # datetime.datetime — подкласс date
return Cell(value, "date", bold=bold, indent=indent)
text = value if isinstance(value, str) else str(value)
if not text.strip():
return None
return Cell(text, "text", bold=bold, indent=indent or leading_spaces(text))

View File

@ -14,13 +14,14 @@ from openpyxl.styles import Alignment, Font
from src.contract import ErrorCode
from src.parser.errors import ParserError, UnreadableError, UnsupportedFormatError
from src.parser.grid import Cell, MergedRange
from src.parser.grid import EMPTY_CELL, Cell, MergedRange
from src.parser.readers import read_book
from src.parser.readers import xlsb as xlsb_reader
def _synthetic_xlsx() -> bytes:
"""Две страницы: шапка с merged-заголовками, outline 0/1/2, bold, отступ, число, дата, bool, пустые строки."""
"""Две страницы: шапка с merged-заголовками, outline 0/1/2, bold, отступ, число, дата, bool, пустые строки,
ячейки из пробелов и NBSP, merged-диапазон с пробельным origin."""
book = openpyxl.Workbook()
osv = book.active
assert osv is not None
@ -44,6 +45,9 @@ def _synthetic_xlsx() -> bytes:
osv["B7"] = datetime.datetime(2024, 9, 30)
osv.row_dimensions[7].outline_level = 2
osv["A9"] = " "
osv["B9"] = "\xa0"
osv["D3"] = " "
osv.merge_cells("D3:E3")
osv["A10"] = "Итого"
osv["C10"] = True
other = book.create_sheet("Прочее")
@ -67,14 +71,19 @@ def test_xlsx_reader_builds_sheet_model() -> None:
assert osv.cell(7, 1) == Cell(" Контрагент", "text", indent=3)
assert osv.cell(7, 2) == Cell(datetime.datetime(2024, 9, 30), "date")
assert osv.cell(10, 3) == Cell(True, "bool")
assert osv.cell(9, 1).kind == "text"
assert osv.cell(9, 1) is EMPTY_CELL
assert osv.cell(9, 2) is EMPTY_CELL
assert (9, 1) not in osv.cells
assert (9, 2) not in osv.cells
assert osv.is_row_empty(8)
assert osv.is_row_empty(9)
assert set(osv.merged) == {MergedRange(3, 1, 4, 1), MergedRange(3, 2, 3, 3)}
assert osv.text_at(3, 5) == ""
assert osv.merged_origin(3, 5) == (3, 4)
assert set(osv.merged) == {MergedRange(3, 1, 4, 1), MergedRange(3, 2, 3, 3), MergedRange(3, 4, 3, 5)}
assert osv.text_at(4, 1) == "Счет"
assert osv.text_at(3, 3) == "Сальдо на начало периода"
assert [osv.outline(row) for row in range(1, 11)] == [0, 0, 0, 0, 0, 1, 2, 0, 0, 0]
assert (osv.n_rows, osv.n_cols) == (10, 3)
assert (osv.n_rows, osv.n_cols) == (10, 5)
assert all(cell.kind != "empty" for cell in osv.cells.values())
assert sheets[1].row_cells(1) == [(1, Cell("x", "text"))]
@ -168,7 +177,8 @@ def test_xlsb_signature_is_read_by_pyxlsb_without_outline(monkeypatch: pytest.Mo
(sheet,) = sheets
assert sheet.name == "Лист1"
assert sheet.cell(1, 1) == Cell("Счет", "text")
assert sheet.cell(1, 3) == Cell(" ", "text")
assert sheet.cell(1, 3) is EMPTY_CELL
assert (1, 3) not in sheet.cells
assert sheet.cell(3, 2) == Cell(Decimal("0.1"), "number")
assert sheet.cell(3, 3) == Cell(False, "bool")
assert sheet.merged == ()