def has_columns(df, columns, exact_cols=False, exact_order=False):
    """Asserts that `df` has ``columns``

    Args:
        df (pd.DataFrame): Any pd.DataFrame.
        columns (list or tuple): Columns that are expected to be in ``df``.
        exact_cols (bool): Whether or not ``columns`` need to be the only columns in ``df``.
        exact_order (bool): Whether or not ``columns`` need to be in
                            the same order as the columns in ``df``.

    Returns:
        Original `df`.

    """
    df_cols = df.columns
    msg = []
    missing_cols = list(set(columns).difference(df_cols))
    if missing_cols:
        msg.append('`df` is missing columns: {}.'.format(missing_cols))
    if exact_cols:
        unexpected_extra_cols = list(set(df_cols).difference(columns))
        if unexpected_extra_cols:
            msg.append('`df` has extra columns: {}.'.format(
                unexpected_extra_cols))
    if exact_order:
        if missing_cols:
            msg.append(
                '`df` column order does not match given `columns` order, because columns are missing.'
                )
        else:
            idx_order = []
            for i in range(len(columns)):
                try:
                    idx_order.append(columns.index(df.columns[i]))
                except ValueError:
                    pass
            if idx_order != sorted(idx_order):
                msg.append(
                    '`df` column order does not match given `columns` order.')
    if msg:
        raise AssertionError(' '.join(msg))
    return df
