def fmt(
    self: GTSelf,
    fns: FormatFn | FormatFns,
    columns: SelectExpr = None,
    rows: int | list[int] | None = None,
    is_substitution: bool = False,
) -> GTSelf:
    """
    Set a column format with a formatter function.

    The `fmt()` method provides a way to execute custom formatting functionality with raw data
    values in a way that can consider all output contexts.

    Along with the `columns` and `rows` arguments that provide some precision in targeting data
    cells, the `fns` argument allows you to define one or more functions for manipulating the
    raw data.

    Parameters
    ----------
    fns
        Either a single formatting function or a named list of functions.
    columns
        The columns to target. Can either be a single column name or a series of column names
        provided in a list.
    rows
        In conjunction with `columns=`, we can specify which of their rows should undergo
        formatting. The default is all rows, resulting in all rows in `columns` being formatted.
        Alternatively, we can supply a list of row indices.
    is_substitution
        Whether the formatter is a substitution. Substitutions are run last, after other formatters.

    Returns
    -------
    GT
        The GT object is returned. This is the same object that the method is called on so that we
        can facilitate method chaining.

    Examples
    --------
    Let's use the `exibble` dataset to create a table. With the `fmt()` method, we'll add a prefix
    `^` and a suffix `$` to the `row` and `group` columns.

    ```{python}
    from great_tables import GT, exibble

    (
        GT(exibble)
        .fmt(lambda x: f"^{x}$", columns=["row", "group"])
    )
    ```
    """

    # If a single function is supplied to `fns` then
    # repackage that into a list as the `default` function
    if isinstance(fns, Callable):
        fns = FormatFns(default=fns)

    row_res = resolve_rows_i(self, rows)
    row_pos = [name_pos[1] for name_pos in row_res]

    col_res = resolve_cols_c(self, columns)

    formatter = FormatInfo(fns, col_res, row_pos)

    if is_substitution:
        return self._replace(_substitutions=[*self._substitutions, formatter])

    return self._replace(_formats=[*self._formats, formatter])

    Set a column format with a formatter function.

    The `fmt()` method provides a way to execute custom formatting functionality with raw data
    values in a way that can consider all output contexts.

    Along with the `columns` and `rows` arguments that provide some precision in targeting data
    cells, the `fns` argument allows you to define one or more functions for manipulating the
    raw data.

    Parameters
    ----------
    fns
        Either a single formatting function or a named list of functions.
    columns
        The columns to target. Can either be a single column name or a series of column names
        provided in a list.
    rows
        In conjunction with `columns=`, we can specify which of their rows should undergo
        formatting. The default is all rows, resulting in all rows in `columns` being formatted.
        Alternatively, we can supply a list of row indices.
    is_substitution
        Whether the formatter is a substitution. Substitutions are run last, after other formatters.

    Returns
    -------
    GT
        The GT object is returned. This is the same object that the method is called on so that we
        can facilitate method chaining.

    Examples
    --------
    Let's use the `exibble` dataset to create a table. With the `fmt()` method, we'll add a prefix
    `^` and a suffix `$` to the `row` and `group` columns.

    ```{python}
    from great_tables import GT, exibble

    (
        GT(exibble)
        .fmt(lambda x: f"^{x}$", columns=["row", "group"])
    )
    ```
    
great_tables/_formats.py