Skip to content

Classes

craftable.ColDef dataclass

Source code in src/craftable/craftable.py
@dataclass
class ColDef:
    width: int = 0
    align: str = "<"
    prefix: str = ""
    prefix_align: str = ">"
    suffix: str = ""
    suffix_align: str = "<"
    auto_fill: bool = False
    truncate: bool = False
    strict: bool = False
    format_spec: FormatSpec | None = None
    preprocessor: PreprocessorCallback | None = None
    postprocessor: PostprocessorCallback | None = None
    none_text: str = ""

    def set_width(self, value: int) -> None:
        self.width = value
        if self.format_spec:
            if self.prefix_align == "<" or self.suffix_align == ">":
                adj_width = value - len(self.prefix) - len(self.suffix)
                if adj_width > 0:
                    self.format_spec.width = adj_width

    def format(self, value: Any) -> str:
        # "Inner" format
        if self.format_spec:
            format_string = f"{{:{self.format_spec}}}"
        else:
            format_string = "{}"

        # convert None to user-configurable text
        val = value if value is not None else self.none_text
        try:
            text = format_string.format(val)
        except:  # noqa: E722
            # On failure, fall back to string unless strict mode
            if self.strict:
                raise
            else:
                text = str(val)
        # add prefix and suffix if there is a value
        if value is not None:
            text = f"{self.prefix}{text}{self.suffix}"

        # "Outer" format
        return self.format_text(text)

    # ------------------------------------------------------------------
    # Processor helpers
    # ------------------------------------------------------------------
    def preprocess(self, value: Any, row: list[Any], col_idx: int) -> Any:
        """Apply the column's preprocessor callback if present.

        Swallows exceptions and returns the original value on failure.
        """
        if self.preprocessor and callable(self.preprocessor):
            try:
                return self.preprocessor(value, row, col_idx)
            except Exception:
                return value
        return value

    def postprocess(
        self, original_value: Any, text: str, row: list[Any], col_idx: int
    ) -> str:
        """Apply the column's postprocessor callback if present.

        Runs after width sizing, wrapping and alignment. Should not
        alter the displayed width. Swallows exceptions and returns the
        original text on failure.
        """
        if self.postprocessor and callable(self.postprocessor):
            try:
                return self.postprocessor(original_value, text, row, col_idx)
            except Exception:
                return text
        return text

    def format_text(self, text: str) -> str:
        if len(text) > self.width and self.truncate:
            text = text[: self.width - 1] + "…"

        if self.align == "^":
            text = text.center(self.width)
        elif self.align == ">":
            text = text.rjust(self.width)
        else:
            text = text.ljust(self.width)

        return text

    @staticmethod
    def parse(text) -> "ColDef":
        match = _FORMAT_SPEC_PATTERN.match(text)
        if not match:
            raise InvalidColDefError(f"Invalid format specifier for column: {text}")
        spec = match.groupdict()
        # Use .get() with default instead of ternary expressions
        prefix = spec.get("prefix") or ""
        prefix_align = spec.get("prefix_align") or ""
        fill = spec.get("fill") or ""
        align = spec["align"]
        if not align or align == "=":
            align = ""
            fill = ""
        sign = spec.get("sign") or ""
        alternate = spec.get("alternate") or ""
        zero = spec.get("zero") or ""
        width = int(spec["width"]) if spec.get("width") else 0
        grouping = spec.get("grouping_option") or ""
        precision = spec.get("precision") or ""
        type_ = spec.get("type") or ""
        suffix_align = spec.get("suffix_align") or ""
        suffix = spec.get("suffix") or ""

        auto_size = False
        truncate = False

        table_config = spec["table_config"]
        if table_config:
            if "A" in table_config:
                auto_size = True
            if "T" in table_config:
                truncate = True

        format_spec = FormatSpec(
            fill=fill,
            align=align,
            sign=sign,
            alternate=alternate,
            zero=zero,
            grouping=grouping,
            precision=precision,
            type=type_,
        )

        if width and (prefix_align == "<" or suffix_align == ">"):
            adj_width = width - len(prefix) - len(suffix)
            if adj_width > 0:
                format_spec.width = adj_width
        else:
            format_spec.align = ""

        # if format spec is just a number, then just toss it to avoid
        # inadvertent right-aligned numbers.
        try:
            _ = int(str(format_spec))
            format_spec = None
        except ValueError:
            pass

        return ColDef(
            prefix=prefix,
            prefix_align=prefix_align,
            suffix=suffix,
            suffix_align=suffix_align,
            width=width,
            align=align,
            auto_fill=auto_size,
            truncate=truncate,
            format_spec=format_spec,
        )

align = '<' class-attribute instance-attribute

auto_fill = False class-attribute instance-attribute

format_spec = None class-attribute instance-attribute

none_text = '' class-attribute instance-attribute

postprocessor = None class-attribute instance-attribute

prefix = '' class-attribute instance-attribute

prefix_align = '>' class-attribute instance-attribute

preprocessor = None class-attribute instance-attribute

strict = False class-attribute instance-attribute

suffix = '' class-attribute instance-attribute

suffix_align = '<' class-attribute instance-attribute

truncate = False class-attribute instance-attribute

width = 0 class-attribute instance-attribute

__init__

format

Source code in src/craftable/craftable.py
def format(self, value: Any) -> str:
    # "Inner" format
    if self.format_spec:
        format_string = f"{{:{self.format_spec}}}"
    else:
        format_string = "{}"

    # convert None to user-configurable text
    val = value if value is not None else self.none_text
    try:
        text = format_string.format(val)
    except:  # noqa: E722
        # On failure, fall back to string unless strict mode
        if self.strict:
            raise
        else:
            text = str(val)
    # add prefix and suffix if there is a value
    if value is not None:
        text = f"{self.prefix}{text}{self.suffix}"

    # "Outer" format
    return self.format_text(text)

format_text

Source code in src/craftable/craftable.py
def format_text(self, text: str) -> str:
    if len(text) > self.width and self.truncate:
        text = text[: self.width - 1] + "…"

    if self.align == "^":
        text = text.center(self.width)
    elif self.align == ">":
        text = text.rjust(self.width)
    else:
        text = text.ljust(self.width)

    return text

parse staticmethod

Source code in src/craftable/craftable.py
@staticmethod
def parse(text) -> "ColDef":
    match = _FORMAT_SPEC_PATTERN.match(text)
    if not match:
        raise InvalidColDefError(f"Invalid format specifier for column: {text}")
    spec = match.groupdict()
    # Use .get() with default instead of ternary expressions
    prefix = spec.get("prefix") or ""
    prefix_align = spec.get("prefix_align") or ""
    fill = spec.get("fill") or ""
    align = spec["align"]
    if not align or align == "=":
        align = ""
        fill = ""
    sign = spec.get("sign") or ""
    alternate = spec.get("alternate") or ""
    zero = spec.get("zero") or ""
    width = int(spec["width"]) if spec.get("width") else 0
    grouping = spec.get("grouping_option") or ""
    precision = spec.get("precision") or ""
    type_ = spec.get("type") or ""
    suffix_align = spec.get("suffix_align") or ""
    suffix = spec.get("suffix") or ""

    auto_size = False
    truncate = False

    table_config = spec["table_config"]
    if table_config:
        if "A" in table_config:
            auto_size = True
        if "T" in table_config:
            truncate = True

    format_spec = FormatSpec(
        fill=fill,
        align=align,
        sign=sign,
        alternate=alternate,
        zero=zero,
        grouping=grouping,
        precision=precision,
        type=type_,
    )

    if width and (prefix_align == "<" or suffix_align == ">"):
        adj_width = width - len(prefix) - len(suffix)
        if adj_width > 0:
            format_spec.width = adj_width
    else:
        format_spec.align = ""

    # if format spec is just a number, then just toss it to avoid
    # inadvertent right-aligned numbers.
    try:
        _ = int(str(format_spec))
        format_spec = None
    except ValueError:
        pass

    return ColDef(
        prefix=prefix,
        prefix_align=prefix_align,
        suffix=suffix,
        suffix_align=suffix_align,
        width=width,
        align=align,
        auto_fill=auto_size,
        truncate=truncate,
        format_spec=format_spec,
    )

postprocess

Apply the column's postprocessor callback if present.

Runs after width sizing, wrapping and alignment. Should not alter the displayed width. Swallows exceptions and returns the original text on failure.

Source code in src/craftable/craftable.py
def postprocess(
    self, original_value: Any, text: str, row: list[Any], col_idx: int
) -> str:
    """Apply the column's postprocessor callback if present.

    Runs after width sizing, wrapping and alignment. Should not
    alter the displayed width. Swallows exceptions and returns the
    original text on failure.
    """
    if self.postprocessor and callable(self.postprocessor):
        try:
            return self.postprocessor(original_value, text, row, col_idx)
        except Exception:
            return text
    return text

preprocess

Apply the column's preprocessor callback if present.

Swallows exceptions and returns the original value on failure.

Source code in src/craftable/craftable.py
def preprocess(self, value: Any, row: list[Any], col_idx: int) -> Any:
    """Apply the column's preprocessor callback if present.

    Swallows exceptions and returns the original value on failure.
    """
    if self.preprocessor and callable(self.preprocessor):
        try:
            return self.preprocessor(value, row, col_idx)
        except Exception:
            return value
    return value

set_width

Source code in src/craftable/craftable.py
def set_width(self, value: int) -> None:
    self.width = value
    if self.format_spec:
        if self.prefix_align == "<" or self.suffix_align == ">":
            adj_width = value - len(self.prefix) - len(self.suffix)
            if adj_width > 0:
                self.format_spec.width = adj_width

craftable.ColDefList

Bases: list[ColDef]

A list of ColDef objects.

Source code in src/craftable/craftable.py
class ColDefList(list[ColDef]):
    """
    A list of ColDef objects.
    """

    def __init__(self, iterable: Iterable | None = None) -> None:
        super().__init__()
        if iterable is not None:
            for val in iterable:
                self.append(val)
        self._adjusted = False
        self._cached_list = None

    @overload
    def __setitem__(self, key: SupportsIndex, value: str | ColDef, /) -> None: ...

    @overload
    def __setitem__(self, key: slice, value: Iterable[str | ColDef], /) -> None: ...

    def __setitem__(self, key, value) -> None:
        if isinstance(key, SupportsIndex):
            if isinstance(value, str):
                super().__setitem__(key, ColDef.parse(value))
            elif isinstance(value, ColDef):
                super().__setitem__(key, value)
            else:
                raise ValueError("Column definition contain an invalid value")
        elif isinstance(key, slice) and isinstance(value, Iterable):
            values = ColDefList(value)
            super().__setitem__(key, values)
        else:
            raise ValueError("Column definitions contain an invalid value")

    @overload
    def __getitem__(self, i: SupportsIndex) -> ColDef: ...

    @overload
    def __getitem__(self, i: slice) -> "ColDefList": ...

    def __getitem__(self, i):
        result = super().__getitem__(i)
        if isinstance(i, slice):
            return ColDefList(result)
        else:
            return result

    def __iter__(self) -> Iterator[ColDef]:
        return super().__iter__()

    def append(self, object) -> None:
        if isinstance(object, str):
            super().append(ColDef.parse(object))
        elif isinstance(object, ColDef):
            super().append(object)
        else:
            raise ValueError("Column definitions contain an invalid value")

    def as_list(self, clear_cache: bool = False) -> list[ColDef]:
        """
        Convert to a native list and cache it for performance.
        Returns cached list to avoid repeated __getitem__ overhead.
        """
        if self._cached_list is None or clear_cache:
            self._cached_list = list(self)
        return self._cached_list

    @staticmethod
    def parse(specs: Iterable[str]) -> "ColDefList":
        """Parse a list of format spec strings into a ColDefList.

        This is a convenience for building per-column definitions from
        plain strings. Each spec is parsed via ColDef.parse().

        Example:
            specs = ["A", ">8.2f", "^10"]
            col_defs = ColDefList.parse(specs)

        Args:
            specs: Iterable of column definition strings.

        Returns:
            ColDefList: A list-like collection of ColDef objects.
        """
        col_defs = ColDefList()
        for s in specs:
            col_defs.append(ColDef.parse(s))
        return col_defs

    def adjust_to_table(
        self,
        table_data: list[list[Any]],
        table_width: int,
        style: TableStyle,
        has_header: bool = False,
        clear_cache: bool = False,
    ) -> None:
        # Skip if already adjusted
        if self._adjusted and not clear_cache:
            return
        self._adjusted = True

        # ADD MISSING COL DEFS
        max_cols = max([len(row) for row in table_data])
        diff = max_cols - len(self)
        if diff:
            for _ in range(diff):
                self.append(ColDef())

        # ADJUST WIDTHS OF FIELDS TO MATCH REALITY
        for col_idx in range(max_cols):
            col_def = self[col_idx]
            if not col_def.width:
                max_width = 0
                is_header = has_header
                for row in table_data:
                    if is_header:
                        is_header = False
                        cell = str(row[col_idx])
                    else:
                        value = col_def.preprocess(row[col_idx], row, col_idx)
                        cell = col_def.format(value)
                    max_width = max(max_width, len(cell))

                col_def.set_width(max_width)

            if col_def.width < style.min_width:
                col_def.set_width(style.min_width)

        # ADJUST AUTO-FILL COLS TO FILL REMAINING SPACE AVAILABLE IN TOTAL TABLE_WIDTH
        if not table_width:
            return

        padding_len = style.cell_padding * 2 * len(self)
        border_len = len(str(style.values_left)) + len(str(style.values_right))
        delims_len = len(str(style.values_delimiter)) * (len(self) - 1)
        non_text_len = padding_len + border_len + delims_len
        total_len = non_text_len + sum([c.width for c in self])

        fill_cols = [col_idx for col_idx, col in enumerate(self) if col.auto_fill]
        if not fill_cols:
            if total_len <= table_width:
                return
            else:
                # Find the column with the largest width
                largest_col_idx = max(range(len(self)), key=lambda i: self[i].width)
                self[largest_col_idx].auto_fill = True
                fill_cols.append(largest_col_idx)

        fixed_len = sum([c.width for c in self if not c.auto_fill])

        remaining_width = table_width - fixed_len - non_text_len
        fill_width = remaining_width // len(fill_cols)

        if fill_width < style.min_width:
            raise ValueError(
                "Unable to expand columns to fit table width because existing columns are too wide"
            )

        remainder = remaining_width % len(fill_cols)
        for col_idx in fill_cols:
            new_width = fill_width
            if remainder:
                new_width += 1
                remainder -= 1
            self[col_idx].set_width(new_width)

    @staticmethod
    def assert_valid_table(table: Any) -> None:
        if not isinstance(table, list):
            raise ValueError("Table data must be a list of rows")
        for row in table:
            if not isinstance(row, list):
                raise ValueError("Each row in a table must be a list of cells")

    @staticmethod
    def for_table(table: list[list[Any]]) -> "ColDefList":
        ColDefList.assert_valid_table(table)
        max_cols = max([len(row) for row in table])
        col_defs = ColDefList([ColDef() for _ in range(max_cols)])
        return col_defs

_adjusted = False instance-attribute

_cached_list = None instance-attribute

__getitem__

__getitem__
__getitem__
Source code in src/craftable/craftable.py
def __getitem__(self, i):
    result = super().__getitem__(i)
    if isinstance(i, slice):
        return ColDefList(result)
    else:
        return result

__init__

Source code in src/craftable/craftable.py
def __init__(self, iterable: Iterable | None = None) -> None:
    super().__init__()
    if iterable is not None:
        for val in iterable:
            self.append(val)
    self._adjusted = False
    self._cached_list = None

__iter__

Source code in src/craftable/craftable.py
def __iter__(self) -> Iterator[ColDef]:
    return super().__iter__()

__setitem__

__setitem__
__setitem__
Source code in src/craftable/craftable.py
def __setitem__(self, key, value) -> None:
    if isinstance(key, SupportsIndex):
        if isinstance(value, str):
            super().__setitem__(key, ColDef.parse(value))
        elif isinstance(value, ColDef):
            super().__setitem__(key, value)
        else:
            raise ValueError("Column definition contain an invalid value")
    elif isinstance(key, slice) and isinstance(value, Iterable):
        values = ColDefList(value)
        super().__setitem__(key, values)
    else:
        raise ValueError("Column definitions contain an invalid value")

adjust_to_table

Source code in src/craftable/craftable.py
def adjust_to_table(
    self,
    table_data: list[list[Any]],
    table_width: int,
    style: TableStyle,
    has_header: bool = False,
    clear_cache: bool = False,
) -> None:
    # Skip if already adjusted
    if self._adjusted and not clear_cache:
        return
    self._adjusted = True

    # ADD MISSING COL DEFS
    max_cols = max([len(row) for row in table_data])
    diff = max_cols - len(self)
    if diff:
        for _ in range(diff):
            self.append(ColDef())

    # ADJUST WIDTHS OF FIELDS TO MATCH REALITY
    for col_idx in range(max_cols):
        col_def = self[col_idx]
        if not col_def.width:
            max_width = 0
            is_header = has_header
            for row in table_data:
                if is_header:
                    is_header = False
                    cell = str(row[col_idx])
                else:
                    value = col_def.preprocess(row[col_idx], row, col_idx)
                    cell = col_def.format(value)
                max_width = max(max_width, len(cell))

            col_def.set_width(max_width)

        if col_def.width < style.min_width:
            col_def.set_width(style.min_width)

    # ADJUST AUTO-FILL COLS TO FILL REMAINING SPACE AVAILABLE IN TOTAL TABLE_WIDTH
    if not table_width:
        return

    padding_len = style.cell_padding * 2 * len(self)
    border_len = len(str(style.values_left)) + len(str(style.values_right))
    delims_len = len(str(style.values_delimiter)) * (len(self) - 1)
    non_text_len = padding_len + border_len + delims_len
    total_len = non_text_len + sum([c.width for c in self])

    fill_cols = [col_idx for col_idx, col in enumerate(self) if col.auto_fill]
    if not fill_cols:
        if total_len <= table_width:
            return
        else:
            # Find the column with the largest width
            largest_col_idx = max(range(len(self)), key=lambda i: self[i].width)
            self[largest_col_idx].auto_fill = True
            fill_cols.append(largest_col_idx)

    fixed_len = sum([c.width for c in self if not c.auto_fill])

    remaining_width = table_width - fixed_len - non_text_len
    fill_width = remaining_width // len(fill_cols)

    if fill_width < style.min_width:
        raise ValueError(
            "Unable to expand columns to fit table width because existing columns are too wide"
        )

    remainder = remaining_width % len(fill_cols)
    for col_idx in fill_cols:
        new_width = fill_width
        if remainder:
            new_width += 1
            remainder -= 1
        self[col_idx].set_width(new_width)

append

Source code in src/craftable/craftable.py
def append(self, object) -> None:
    if isinstance(object, str):
        super().append(ColDef.parse(object))
    elif isinstance(object, ColDef):
        super().append(object)
    else:
        raise ValueError("Column definitions contain an invalid value")

as_list

Convert to a native list and cache it for performance. Returns cached list to avoid repeated getitem overhead.

Source code in src/craftable/craftable.py
def as_list(self, clear_cache: bool = False) -> list[ColDef]:
    """
    Convert to a native list and cache it for performance.
    Returns cached list to avoid repeated __getitem__ overhead.
    """
    if self._cached_list is None or clear_cache:
        self._cached_list = list(self)
    return self._cached_list

assert_valid_table staticmethod

Source code in src/craftable/craftable.py
@staticmethod
def assert_valid_table(table: Any) -> None:
    if not isinstance(table, list):
        raise ValueError("Table data must be a list of rows")
    for row in table:
        if not isinstance(row, list):
            raise ValueError("Each row in a table must be a list of cells")

for_table staticmethod

Source code in src/craftable/craftable.py
@staticmethod
def for_table(table: list[list[Any]]) -> "ColDefList":
    ColDefList.assert_valid_table(table)
    max_cols = max([len(row) for row in table])
    col_defs = ColDefList([ColDef() for _ in range(max_cols)])
    return col_defs

parse staticmethod

Parse a list of format spec strings into a ColDefList.

This is a convenience for building per-column definitions from plain strings. Each spec is parsed via ColDef.parse().

Example

specs = ["A", ">8.2f", "^10"] col_defs = ColDefList.parse(specs)

Parameters:

Name Type Description Default
specs Iterable[str]

Iterable of column definition strings.

required

Returns:

Name Type Description
ColDefList ColDefList

A list-like collection of ColDef objects.

Source code in src/craftable/craftable.py
@staticmethod
def parse(specs: Iterable[str]) -> "ColDefList":
    """Parse a list of format spec strings into a ColDefList.

    This is a convenience for building per-column definitions from
    plain strings. Each spec is parsed via ColDef.parse().

    Example:
        specs = ["A", ">8.2f", "^10"]
        col_defs = ColDefList.parse(specs)

    Args:
        specs: Iterable of column definition strings.

    Returns:
        ColDefList: A list-like collection of ColDef objects.
    """
    col_defs = ColDefList()
    for s in specs:
        col_defs.append(ColDef.parse(s))
    return col_defs