Source code for gridient.stacks

# excelalchemy/stacks.py
import logging
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, List, Optional, Tuple

# Use TYPE_CHECKING to avoid circular imports at runtime
if TYPE_CHECKING:
    import xlsxwriter.worksheet

    from .layout import ExcelLayout  # Assuming ExcelLayout is needed for type hint
    from .workbook import ExcelWorkbook
    # Assuming components have get_size. Actual imports might be needed if type hints are strict.
    # from .values import ExcelValue, ExcelSeries
    # from .tables import ExcelTable, ExcelParameterTable

logger = logging.getLogger(__name__)

# Define a base type for components that can be placed in a layout
LayoutComponent = Any  # Replace with a Protocol or ABC later if needed


[docs] @dataclass class ExcelStack: """A container for arranging layout components vertically or horizontally.""" orientation: str # "vertical" or "horizontal" children: List[LayoutComponent] = field(default_factory=list) padding: int = 0 # Rows/columns relative to the stack's top-left corner spacing: int = 1 # Rows/columns between elements name: Optional[str] = None # Optional name for debugging # Keep track of calculated size _calculated_size: Optional[Tuple[int, int]] = field(default=None, init=False, repr=False) def __post_init__(self): if self.orientation not in ("vertical", "horizontal"): raise ValueError(f"Invalid stack orientation: '{self.orientation}'. Must be 'vertical' or 'horizontal'.")
[docs] def add(self, component: LayoutComponent): """Add a component (ExcelValue, ExcelTable, ExcelSeries, ExcelStack) to the stack.""" self.children.append(component) self._calculated_size = None # Reset size cache
[docs] def get_size(self) -> Tuple[int, int]: """Calculate and return the total size (rows, columns) of the stack including padding.""" if self._calculated_size is not None: return self._calculated_size if not self.children: # Return padding even if empty self._calculated_size = (self.padding, self.padding) return self._calculated_size total_inner_rows = 0 total_inner_cols = 0 child_sizes = [] # Recursively get sizes of children for child in self.children: if hasattr(child, "get_size") and callable(child.get_size): child_sizes.append(child.get_size()) else: logger.warning( f"Component {type(child)} in stack '{self.name}' does not have get_size method. Assuming size (1, 1)." ) child_sizes.append((1, 1)) # Default fallback size num_children = len(self.children) effective_spacing = self.spacing * (num_children - 1) if num_children > 1 else 0 if self.orientation == "vertical": total_inner_rows = sum(h for h, w in child_sizes) + effective_spacing total_inner_cols = max((w for h, w in child_sizes), default=0) elif self.orientation == "horizontal": total_inner_rows = max((h for h, w in child_sizes), default=0) total_inner_cols = sum(w for h, w in child_sizes) + effective_spacing else: # Should be caught by __post_init__ raise ValueError(f"Invalid stack orientation: {self.orientation}") # Final size includes padding final_rows = total_inner_rows + self.padding final_cols = total_inner_cols + self.padding self._calculated_size = (final_rows, final_cols) # logger.debug(f"Stack '{self.name}' size calculated: {self._calculated_size}") return self._calculated_size
def _assign_child_references( self, start_row: int, start_col: int, sheet_name: str, layout_manager: "ExcelLayout", ref_map: dict, ): """Recursively assign references to children within the stack.""" # Start placing children *after* the padding current_row = start_row + self.padding current_col = start_col + self.padding for i, child in enumerate(self.children): child_start_row = current_row child_start_col = current_col layout_manager._assign_references_recursive(child, child_start_row, child_start_col, sheet_name, ref_map) # Update position for the next child based on orientation and spacing child_rows, child_cols = (0, 0) if hasattr(child, "get_size") and callable(child.get_size): child_rows, child_cols = child.get_size() else: child_rows, child_cols = ( 1, 1, ) # Use fallback size from get_size warning if self.orientation == "vertical": current_row += child_rows + (self.spacing if i < len(self.children) - 1 else 0) elif self.orientation == "horizontal": current_col += child_cols + (self.spacing if i < len(self.children) - 1 else 0)
[docs] def write( self, worksheet: "xlsxwriter.worksheet.Worksheet", start_row: int, start_col: int, workbook_wrapper: "ExcelWorkbook", ref_map: dict, column_widths: dict, ): """Write the stack's children to the worksheet.""" logger.debug( f"Writing Stack ('{self.name or 'unnamed'}') starting at ({start_row}, {start_col}) Orientation: {self.orientation}" ) # Start placing children *after* the padding current_row = start_row + self.padding current_col = start_col + self.padding for i, child in enumerate(self.children): child_start_row = current_row child_start_col = current_col logger.debug(f" Writing child {i} ({type(child)}) at ({child_start_row}, {child_start_col})") # Use the component's own write method if hasattr(child, "write") and callable(child.write): child.write( worksheet, child_start_row, child_start_col, workbook_wrapper, ref_map, column_widths, ) else: logger.warning( f" Component {type(child)} in stack '{self.name}' at ({child_start_row},{child_start_col}) has no write method." ) worksheet.write(child_start_row, child_start_col, f"Unhandled: {type(child)}") # Update position for the next child child_rows, child_cols = (0, 0) if hasattr(child, "get_size") and callable(child.get_size): child_rows, child_cols = child.get_size() else: child_rows, child_cols = ( 1, 1, ) # Use fallback size from get_size warning if self.orientation == "vertical": current_row += child_rows + (self.spacing if i < len(self.children) - 1 else 0) elif self.orientation == "horizontal": current_col += child_cols + (self.spacing if i < len(self.children) - 1 else 0)
def __repr__(self) -> str: return f"ExcelStack(name='{self.name}', orientation='{self.orientation}', children={len(self.children)})"