"""Tests para read_xlsx. Se importa el modulo por path directo (sin tocar __init__.py) para no depender del re-export del paquete. write_xlsx_sheets se importa igual para el round-trip. """ import importlib.util import os _HERE = os.path.dirname(os.path.abspath(__file__)) def _load(name): spec = importlib.util.spec_from_file_location(name, os.path.join(_HERE, f"{name}.py")) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod read_xlsx = _load("read_xlsx").read_xlsx write_xlsx_sheets = _load("write_xlsx_sheets").write_xlsx_sheets def test_round_trip_escribe_lee_compara(tmp_path): """Escribir con write_xlsx_sheets y leer con read_xlsx devuelve los mismos datos.""" out = str(tmp_path / "rt.xlsx") write_xlsx_sheets( out, { "Ventas": [ ["Producto", "Unidades", "Precio", "Activo"], ["Teclado", 12, 29.99, True], ["Raton", 30, 14.5, False], ["Monitor", None, 199.0, True], ], }, ) res = read_xlsx(out) assert res["status"] == "ok" assert list(res["sheets"].keys()) == ["Ventas"] ventas = res["sheets"]["Ventas"] assert ventas["headers"] == ["Producto", "Unidades", "Precio", "Activo"] assert ventas["rows"] == [ ["Teclado", 12, 29.99, True], ["Raton", 30, 14.5, False], ["Monitor", None, 199.0, True], ] def test_lee_solo_la_hoja_indicada(tmp_path): out = str(tmp_path / "multi.xlsx") write_xlsx_sheets( out, { "A": [["x"], [1]], "B": [["y"], [2]], }, ) res = read_xlsx(out, sheet="B") assert res["status"] == "ok" assert list(res["sheets"].keys()) == ["B"] assert res["sheets"]["B"]["headers"] == ["y"] assert res["sheets"]["B"]["rows"] == [[2]] def test_max_rows_trunca_filas_de_datos(tmp_path): out = str(tmp_path / "trunc.xlsx") write_xlsx_sheets( out, {"S": [["n"], [1], [2], [3], [4], [5]]}, ) res = read_xlsx(out, sheet="S", max_rows=2) assert res["status"] == "ok" assert res["sheets"]["S"]["headers"] == ["n"] assert res["sheets"]["S"]["rows"] == [[1], [2]] def test_header_false_no_consume_cabecera(tmp_path): out = str(tmp_path / "nohdr.xlsx") write_xlsx_sheets(out, {"S": [["a", "b"], [1, 2]]}) res = read_xlsx(out, sheet="S", header=False) assert res["status"] == "ok" assert res["sheets"]["S"]["headers"] == [] assert res["sheets"]["S"]["rows"] == [["a", "b"], [1, 2]] def test_fecha_se_devuelve_como_iso(tmp_path): import datetime from openpyxl import Workbook out = str(tmp_path / "fechas.xlsx") wb = Workbook() ws = wb.active ws.title = "F" ws.append(["evento", "cuando"]) ws.append(["solo_fecha", datetime.date(2026, 6, 16)]) ws.append(["con_hora", datetime.datetime(2026, 6, 16, 14, 30, 0)]) wb.save(out) res = read_xlsx(out, sheet="F") assert res["status"] == "ok" rows = res["sheets"]["F"]["rows"] assert rows[0] == ["solo_fecha", "2026-06-16"] assert rows[1] == ["con_hora", "2026-06-16T14:30:00"] def test_formula_se_lee_como_valor_calculado(tmp_path): """data_only lee el valor cacheado de la formula si Excel/openpyxl lo guardo. openpyxl no calcula formulas; cuando escribimos la formula con openpyxl el valor cacheado es None hasta que un motor (Excel/LibreOffice) la evalua y guarda. El round-trip valido es escribir el VALOR (no la formula). """ out = str(tmp_path / "calc.xlsx") # Escribimos el valor resultante directamente: read_xlsx con data_only lo lee. write_xlsx_sheets(out, {"C": [["total"], [42]]}) res = read_xlsx(out, sheet="C") assert res["status"] == "ok" assert res["sheets"]["C"]["rows"] == [[42]] def test_archivo_inexistente_devuelve_error(): res = read_xlsx("/tmp/no_existe_seguro_123456.xlsx") assert res["status"] == "error" assert "no encontrado" in res["error"] def test_hoja_inexistente_devuelve_error(tmp_path): out = str(tmp_path / "h.xlsx") write_xlsx_sheets(out, {"Real": [["x"], [1]]}) res = read_xlsx(out, sheet="Fantasma") assert res["status"] == "error" assert "no existe" in res["error"] def test_path_vacio_devuelve_error(): res = read_xlsx("") assert res["status"] == "error"