Repository: pri22296/beautifultable Branch: master Commit: 5bce31dcf0f4 Files: 37 Total size: 245.8 KB Directory structure: gitextract_qaaahoom/ ├── .coveragerc ├── .github/ │ └── workflows/ │ ├── build.yml │ ├── publish-test.yml │ └── publish.yml ├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── beautifultable/ │ ├── __init__.py │ ├── __version__.py │ ├── ansi.py │ ├── base.py │ ├── beautifultable.py │ ├── compat.py │ ├── enums.py │ ├── helpers.py │ ├── meta.py │ ├── styles.py │ └── utils.py ├── coverage.xml ├── docs/ │ ├── Makefile │ ├── badges.rst │ ├── changelog.rst │ ├── conf.py │ ├── donation.rst │ ├── index.rst │ ├── install.rst │ ├── introduction.rst │ ├── links.rst │ ├── make.bat │ ├── quickstart.rst │ └── source/ │ ├── beautifultable.rst │ └── modules.rst ├── requirements.txt ├── setup.cfg ├── setup.py └── test.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .coveragerc ================================================ [run] branch = True source = beautifultable [report] exclude_lines = if self.debug: pragma: no cover raise NotImplementedError if __name__ == .__main__.: pass ignore_errors = True omit = test.py ================================================ FILE: .github/workflows/build.yml ================================================ # This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Build on: push: branches: [ master ] pull_request: branches: [ master ] workflow_dispatch: jobs: black: name: Check formatting runs-on: 'ubuntu-latest' steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 - uses: psf/black@stable coverage: name: Check code coverage runs-on: 'ubuntu-latest' steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install coverage if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Generate coverage report run: | coverage run test.py coverage xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v2 build: runs-on: ${{ matrix.os }} needs: black strategy: fail-fast: false matrix: os: - 'ubuntu-latest' - 'macos-latest' - 'windows-latest' python-version: - '3.7' - '3.8' - '3.9' - '3.10' - 'pypy-3.9' steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install flake8 python -m pip install -r requirements.txt - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Run Test run: | python test.py ================================================ FILE: .github/workflows/publish-test.yml ================================================ # This workflow will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. name: Test Publish on: release: types: [published] workflow_dispatch: jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install build - name: Build package run: python -m build - name: Publish package to TestPyPi uses: pypa/gh-action-pypi-publish@master with: user: __token__ password: ${{ secrets.TEST_PYPI_API_TOKEN }} repository_url: https://test.pypi.org/legacy/ ================================================ FILE: .github/workflows/publish.yml ================================================ # This workflow will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. name: Publish on: release: types: [published] workflow_dispatch: jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install build - name: Build package run: python -m build - name: Publish package to PyPi uses: pypa/gh-action-pypi-publish@master with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} ================================================ FILE: .gitignore ================================================ __pycache__/ *egg-info/ docs/_build/ dist/ build/ .tox/ *.pyc .coverage .vscode/ .venv .idea ================================================ FILE: LICENSE.txt ================================================ MIT License Copyright (c) 2022 Priyam Singh Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MANIFEST.in ================================================ include LICENSE.txt include README.rst include docs/*.rst include test.py ================================================ FILE: README.rst ================================================ ########################################################################## beautifultable ########################################################################## .. inclusion-marker-badges-start .. image:: https://badge.fury.io/py/beautifultable.svg :target: https://badge.fury.io/py/beautifultable .. image:: https://img.shields.io/pypi/pyversions/beautifultable.svg :target: https://pypi.python.org/pypi/beautifultable/ .. image:: https://codecov.io/gh/pri22296/beautifultable/branch/master/graphs/badge.svg :target: https://codecov.io/gh/pri22296/beautifultable/branch/master/ .. image:: https://api.codacy.com/project/badge/Grade/7a76eb35ad4e450eaf00339e98381511 :target: https://www.codacy.com/app/pri22296/beautifultable?utm_source=github.com&utm_medium=referral&utm_content=pri22296/beautifultable&utm_campaign=Badge_Grade .. image:: https://github.com/pri22296/beautifultable/actions/workflows/build.yml/badge.svg?branch=master :target: https://github.com/pri22296/beautifultable/actions/workflows/build.yml .. image:: https://readthedocs.org/projects/beautifultable/badge/?version=latest :alt: Documentation Status :target: http://beautifultable.readthedocs.io/en/latest/?badge=latest .. image:: https://img.shields.io/badge/Donate-PayPal-yellow.svg :target: https://paypal.me/beautifultable .. inclusion-marker-badges-end .. inclusion-marker-introduction-start ************************************************************************** Introduction ************************************************************************** This Package provides BeautifulTable class for easily printing tabular data in a visually appealing format to a terminal. Features included but not limited to: * Full customization of the look and feel of the table * Build the Table as you wish, By adding rows, or by columns or even mixing both these approaches. * Full support for *colors* using ANSI sequences or any library of your choice. It just works. * Plenty of predefined *styles* for multiple use cases and option to create custom ones. * Support for *Unicode* characters. * Supports streaming table when data is slow to retrieve. .. inclusion-marker-introduction-end .. inclusion-marker-links-start ************************************************************************** Links ************************************************************************** * `Documentation `_ * `Source `_ * `API Reference `_ .. inclusion-marker-links-end .. inclusion-marker-usage-start ************************************************************************** Usage ************************************************************************** Here is an example of how you can use beautifultable:: >>> from beautifultable import BeautifulTable >>> table = BeautifulTable() >>> table.rows.append(["Jacob", 1, "boy"]) >>> table.rows.append(["Isabella", 1, "girl"]) >>> table.rows.append(["Ethan", 2, "boy"]) >>> table.rows.append(["Sophia", 2, "girl"]) >>> table.rows.append(["Michael", 3, "boy"]) >>> table.rows.header = ["S1", "S2", "S3", "S4", "S5"] >>> table.columns.header = ["name", "rank", "gender"] >>> print(table) +----+----------+------+--------+ | | name | rank | gender | +----+----------+------+--------+ | S1 | Jacob | 1 | boy | +----+----------+------+--------+ | S2 | Isabella | 1 | girl | +----+----------+------+--------+ | S3 | Ethan | 2 | boy | +----+----------+------+--------+ | S4 | Sophia | 2 | girl | +----+----------+------+--------+ | S5 | Michael | 3 | boy | +----+----------+------+--------+ You can learn more about beautifultable at this `Tutorial `_ .. inclusion-marker-usage-end .. inclusion-marker-install-start ************************************************************************** Installation ************************************************************************** :: python3 -m pip install beautifultable .. inclusion-marker-install-end .. inclusion-marker-changelog-start ************************************************************************** Changelog ************************************************************************** =========== Development =========== ========== v1.1.0 ========== * Drop support for Python 3.4, 3.5 and 3.6 * Add official support for python 3.9 and 3.10 * Added `asdict` and `aslist` method on the row object. (Thanks to `@Agent-Hellboy `_) * Added `from_csv` and `to_csv` methods to export/import a csv file. (Thanks to `@Agent-Hellboy `_) * Added `from_df` and `to_df` methods to export/import a dataframe. (Thanks to `@Agent-Hellboy `_) ========== v1.0.1 ========== * Fixed an issue where appending a column with a header to an empty table left the table instance in an inconsistent state. ========== v1.0.0 ========== * Added two new views ``rows`` and ``columns`` to the ``BeautifulTable`` class. Most of the existing methods have been deprecated. Methods of the form ``{}_row`` and ``{}_column`` have been moved to views ``rows.{}`` and ``columns.{}``(ex. ``append_row`` is now ``rows.append``). Calling older deprecated methods will now raise a ``FutureWarning``. Special methods such as ``__len__``, ``__iter__``, etc. have also been moved to the respective views. For details, refer the API documentation and the Updated Tutorial * The existing styling attributes have also been deprecated. A new ``border`` property can be accessed to control all styling attributes affecting the border. Rest of the attributes can be accessed from it's respective view. * Added support for row headers. As a result rows can now be accessed by their keys similar to columns * Added two new methods ``to_csv`` and ``from_csv`` to directly export/import to a csv file. (Thanks to `@dinko-pehar `_) * Added ``BeautifulTable.rows.filter`` method to generate a new table with only certain rows * Added a new ``shape`` attribute to the ``BeautifulTable`` class which returns a tuple of form (nrow, ncol) * Added new attribute ``BeautifulTable.columns.header.alignment`` which can be used to have a seperate header alignment. The default behaviour is to inherit ``BeautifulTable.columns.alignment`` * Updated ``BeautifulTable.rows.sort`` (earlier ``BeautifulTable.sort``) method to now also accept any callables as a key. * Updated behaviour of ``BeautifulTable.columns.width`` (earlier ``BeautifulTable.column_widths``). It no longer overrides user specified widths by default. You can reset it to default by setting it to **"auto"** * Deprecated attribute ``serialno`` and ``serialno_header``. User can now easily implement this functionality by using row headers if required * Deprecated methods ``get_table_width()``, ``copy()`` and ``get_string()``. * Deprecated constructor arguments and class attributes named ``sign_mode``, ``numeric_precision``, ``max_width`` and renamed to ``sign``, ``precision`` and ``maxwidth`` respectively * Fixed an issue where table was malformed if ``blessings`` module was used to generate colored strings. * Fixed issues with the existing implementation of ``__iter__``, ``__copy__`` and ``__deepcopy__`` which should now work more reliably. * Fixed an issue where default padding could not be set to 0. (Thanks to `@furlongm `_) * Fixed several memory leak issues by ensuring that all internal objects hold only a weak reference to the table instance. * Dropped support for Python 2 ========== v0.8.0 ========== * Dropped support for Python 3.3 * Added support for streaming tables using a generator for cases where data retrieval is slow * Alignment, padding, width can now be set for all columns using a simplified syntax like ``table.column_alignments = beautifultable.ALIGN_LEFT`` ========== v0.7.0 ========== * Added 4 new styles, **STYLE_BOX**, **STYLE_BOX_DOUBLED**, **STYLE_BOX_ROUNDED**, **STYLE_GRID**. * Renamed **STYLE_RESTRUCTURED_TEXT** to **STYLE_RST** * **wcwidth** is now an optional dependency * Updated the algorithm for calculating width of columns(better division of space among columns) * Added support for Paragraphs(using ``\n`` character) * Added finer control for intersection characters using 12 new attributes ``intersect_{top|header|row|bottom}_{left|mid|right}`` * Added the ability to also accept bytestrings instead of unicode * Deprecated attribute ``intersection_char`` * Deprecated methods ``get_top_border()``, ``get_bottom_border()``, ``get_header_separator()``, ``get_row_separator()``, ``auto_calculate_width()`` * Fixed an issue with **WEP_ELLIPSIS** and **WEP_STRIP** when using multibyte characters * Fixed an issue where table would not be in proper form if ``column_width`` is too low ========== v0.6.0 ========== * Added support for handling Multi byte strings * Added support for colored strings using ANSI escape sequences * Added constraint where all strings must be unicode * Fixed an issue where sometimes width was calculated as higher than intended ========== v0.5.3 ========== * Added support for handing color codes using ANSI escape sequences(experimental) * Fixed collections ABCs deprecation warning ========== v0.5.2 ========== * Added new style **STYLE_NONE** * Fixed issue regarding improper conversion of non-string floats ========== v0.5.1 ========== * Added ``detect_numerics`` boolean for toggling automatic numeric conversion ========== v0.5.0 ========== * Added new property ``serialno_header`` * Deprecated methods with misspelled *"seperator"* in their name. * Fixed an issue where table was corrupted when ``column_count`` was too high ========== v0.4.0 ========== * Added predefined styles for easier customization * Added *reverse* argument to ``sort()`` method * Fixed *enum34* dependency for python versions prior to 3.4 ========== v0.3.0 ========== * Added property ``serialno`` for auto printing serial number * Fixed an issue with ``sign_mode`` related to str conversion * Fixed bugs related to python version prior to 3.3 * Fixed exception on **WEP_ELLIPSIS** and token length less than 3 * Fixed printing issues with empty table ========== v0.2.0 ========== * Added python 2 support ========== v0.1.3 ========== * Fixed minor issues ========== v0.1.2 ========== * Added new property ``default_padding`` * Added new method ``update_row`` * Fixed an issue in ``auto_calculate_width()`` ========== v0.1.1 ========== * Initial release on PyPI .. inclusion-marker-changelog-end .. inclusion-marker-contribution-start ************************************************************************** Contribute ************************************************************************** If you have any suggestions or bug reports, Please create a Issue. Pull Requests are always welcome. .. inclusion-marker-contribution-end .. inclusion-marker-license-start ************************************************************************** License ************************************************************************** This project is licensed under the MIT License - see the `LICENSE.txt `_ file for details. .. inclusion-marker-license-end .. inclusion-marker-donation-start ************************************************************************** Donation ************************************************************************** Love *beautifultable*? Consider supporting the development :) .. image:: https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif :target: https://paypal.me/beautifultable .. inclusion-marker-donation-end ================================================ FILE: beautifultable/__init__.py ================================================ from .__version__ import __title__, __description__, __url__, __version__ from .__version__ import __copyright__, __author__, __author_email__ from .__version__ import __license__ from .beautifultable import ( # noqa F401 BeautifulTable, BTRowCollection, BTColumnCollection, BTRowHeader, BTColumnHeader, BTBorder, __all__, ) from . import enums from .enums import * # noqa __all__ = __all__ + [ "__title__", "__description__", "__url__", "__version__", "__copyright__", "__author__", "__author_email__", "__license__", ] # To avoid duplicating enums name, dynamically add them to BeautifulTable # class and __all__ for token in dir(enums): if ( token.startswith("WEP_") or token.startswith("ALIGN_") or token.startswith("SM_") or token.startswith("STYLE_") ): setattr(BeautifulTable, token, getattr(enums, token)) __all__.append(token) ================================================ FILE: beautifultable/__version__.py ================================================ __title__ = "beautifultable" __description__ = "Print text tables for terminals" __url__ = "https://github.com/pri22296/beautifultable" __version__ = "1.1.0" __author__ = "Priyam Singh" __author_email__ = "priyamsingh.22296@gmail.com" __license__ = "MIT" __copyright__ = "Copyright (c) 2022 Priyam Singh" ================================================ FILE: beautifultable/ansi.py ================================================ """Module Containing class required for handling ANSI and east asian chars""" from __future__ import unicode_literals import re try: from wcwidth import wcwidth except ImportError: # pragma: no cover wcwidth = len # pragma: no cover from .compat import to_unicode class ANSIMultiByteString(object): ANSI_REGEX = re.compile(r"(\x1B(?:[()][AB012]|[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]))") ANSI_RESET = "\x1b[0m" def __init__(self, string): self._string = [] self._state = [] self._width = [] self._termwidth = 0 state = set() for token in re.split(self.ANSI_REGEX, to_unicode(string)): if token: if re.match(self.ANSI_REGEX, token): if token == self.ANSI_RESET: state.clear() else: state.add(token) else: s_copy = set(state) for char in token: w = wcwidth(char) if w == -1: raise ValueError( f"Unsupported Literal {repr(char)} in string {repr(token)}" ) self._termwidth += w self._string.append(char) self._width.append(w) self._state.append(s_copy) def __len__(self): return len(self._string) def __getitem__(self, key): if isinstance(key, int): if self._state[key]: return "".join(self._state[key]) + self._string[key] + self.ANSI_RESET return self._string[key] if isinstance(key, slice): return self._slice(key) raise TypeError( f"table indices must be integers or slices, not {type(key).__name__}" ) def _slice(self, key): res = [] prev_state = set() for char, state in zip(self._string[key], self._state[key]): if prev_state == state: pass elif prev_state <= state: res.extend(state - prev_state) else: res.append(self.ANSI_RESET) res.extend(state) prev_state = state res.append(char) if prev_state: res.append(self.ANSI_RESET) return "".join(res) def termwidth(self): """Returns the width of string as when printed to a terminal""" return self._termwidth def wrap(self, width): """Returns a partition of the string based on `width`""" res = [] prev_state = set() part = [] cwidth = 0 for char, _width, state in zip(self._string, self._width, self._state): if cwidth + _width > width: if prev_state: part.append(self.ANSI_RESET) res.append("".join(part)) prev_state = set() part = [] cwidth = 0 cwidth += _width if prev_state == state: pass elif prev_state <= state: part.extend(state - prev_state) else: part.append(self.ANSI_RESET) part.extend(state) prev_state = state part.append(char) if prev_state: part.append(self.ANSI_RESET) if part: res.append("".join(part)) return res ================================================ FILE: beautifultable/base.py ================================================ import abc import weakref class BTBaseList(metaclass=abc.ABCMeta): def __init__(self, table, value): self._table = table self._value = self._validate(list(value)) @property def _table(self): return self._table_ref() @_table.setter def _table(self, value): self._table_ref = weakref.ref(value) @property def value(self): return self._value def __len__(self): return len(self._value) def __iter__(self): return iter(self._value) def __next__(self): return next(self._value) def __repr__(self): class_ = type(self).__name__ data = ", ".join(repr(v) for v in self._value) return "{}<{}>".format(class_, data) def __eq__(self, other): if len(self) != len(other): return False for i, j in zip(self, other): if i != j: return False return True def __contains__(self, item): """Returns whether `item` is present""" return item in self._value def _append(self, item): self._value.append(item) def _insert(self, i, item): self._value.insert(i, item) def _pop(self, i=-1): return self._value.pop(self._get_canonical_key(i)) def _remove(self, item): self._value.remove(item) def _reverse(self): self._value.reverse() def _sort(self, key, reverse=False): self._value.sort(key=key, reverse=reverse) def _clear(self): self._value.clear() def count(self, item): return self._value.count(item) def index(self, item, *args): """Returns the index of `item`""" try: return self._value.index(item, *args) except ValueError: raise KeyError(f"Key {item} is not available") def __getitem__(self, key): """Returns item at index or header `key`""" return self._value[self._get_canonical_key(key)] def __setitem__(self, key, value): """Updates item at index or header `key`""" self._value[self._get_canonical_key(key)] = value def __delitem__(self, key): del self._value[self._get_canonical_key(key)] def _validate(self, value): if len(value) != self._get_ideal_length(): raise ValueError( f"'Expected iterable of length {self._get_ideal_length()}, got {len(value)}" ) return value @abc.abstractmethod def _get_canonical_key(self, key): pass @abc.abstractmethod def _get_ideal_length(self): pass class BTBaseRow(BTBaseList): def _get_canonical_key(self, key): return self._table.columns._canonical_key(key) def _get_ideal_length(self): return self._table._ncol def _validate(self, value): if self._get_ideal_length() == 0 and len(value) > 0: self._table.columns._reset_state(len(value)) return super(BTBaseRow, self)._validate(value) class BTBaseColumn(BTBaseList): def _get_canonical_key(self, key): return self._table.rows._canonical_key(key) def _get_ideal_length(self): return len(self._table._data) def _validate(self, value): if self._get_ideal_length() == 0 and len(value) > 0: self._table.rows._reset_state(len(value)) return super(BTBaseColumn, self)._validate(value) ================================================ FILE: beautifultable/beautifultable.py ================================================ """This module provides BeautifulTable class It is intended for printing Tabular data to terminals. Example ------- >>> from beautifultable import BeautifulTable >>> table = BeautifulTable() >>> table.columns.header = ['1st column', '2nd column'] >>> for i in range(5): ... table.rows.apppend([i, i*i]) ... >>> print(table) +------------+------------+ | 1st column | 2nd column | +------------+------------+ | 0 | 0 | +------------+------------+ | 1 | 1 | +------------+------------+ | 2 | 4 | +------------+------------+ | 3 | 9 | +------------+------------+ | 4 | 16 | +------------+------------+ """ from __future__ import division, unicode_literals import copy import csv import warnings from . import enums from .utils import ( pre_process, termwidth, deprecated, deprecated_param, deprecation_message, ensure_type, ) from .compat import basestring, Iterable, to_unicode from .base import BTBaseList from .helpers import ( BTRowCollection, BTColumnCollection, BTRowHeader, BTColumnHeader, ) __all__ = [ "BeautifulTable", "BTRowCollection", "BTColumnCollection", "BTRowHeader", "BTColumnHeader", "BTBorder", ] class BTBorder: """Class to control how each section of the table's border is rendered. To disable a behaviour, just set its corresponding attribute to an empty string Attributes ---------- top : str Character used to draw the top border. left : str Character used to draw the left border. bottom : str Character used to draw the bottom border. right : str Character used to draw the right border. top_left : str Left most character of the top border. bottom_left : str Left most character of the bottom border. bottom_right : str Right most character of the bottom border. top_right : str Right most character of the top border. header_left : str Left most character of the header separator. header_right : str Right most character of the header separator. top_junction : str Junction character for top border. left_junction : str Junction character for left border. bottom_junction : str Junction character for bottom border. right_junction : str Junction character for right border. """ def __init__( self, top, left, bottom, right, top_left, bottom_left, bottom_right, top_right, header_left, header_right, top_junction, left_junction, bottom_junction, right_junction, ): self.top = top self.left = left self.bottom = bottom self.right = right self.top_left = top_left self.bottom_left = bottom_left self.bottom_right = bottom_right self.top_right = top_right self.header_left = header_left self.header_right = header_right self.top_junction = top_junction self.left_junction = left_junction self.bottom_junction = bottom_junction self.right_junction = right_junction def _make_getter(attr): return lambda self: getattr(self, attr) def _make_setter(attr): return lambda self, value: setattr(self, attr, ensure_type(value, basestring)) for prop, attr in [ (x, "_{}".format(x)) for x in ( "top", "left", "bottom", "right", "top_left", "bottom_left", "bottom_right", "top_right", "header_left", "header_right", "top_junction", "left_junction", "bottom_junction", "right_junction", ) ]: setattr(BTBorder, prop, property(_make_getter(attr), _make_setter(attr))) class BTTableData(BTBaseList): def __init__(self, table, value=None): if value is None: value = [] self._table = table self._value = value def _get_canonical_key(self, key): return self._table.rows._canonical_key(key) def _get_ideal_length(self): pass class BeautifulTable: """Utility Class to print data in tabular format to terminal. Parameters ---------- maxwidth: int, optional maximum width of the table in number of characters. this is ignored when manually setting the width of the columns. if this value is too low with respect to the number of columns and width of padding, the resulting table may override it(default 80). default_alignment : int, optional Default alignment for new columns(default beautifultable.ALIGN_CENTER). default_padding : int, optional Default width of the left and right padding for new columns(default 1). precision : int, optional All float values will have maximum number of digits after the decimal, capped by this value(Default 3). serialno : bool, optional If true, a column will be rendered with serial numbers(**DEPRECATED**). serialno_header: str, optional The header of the serial number column if rendered(**DEPRECATED**). detect_numerics : bool, optional Whether numeric strings should be automatically detected(Default True). sign : SignMode, optional Parameter to control how signs in numeric data are displayed. (default beautifultable.SM_MINUS). Attributes ---------- precision : int All float values will have maximum number of digits after the decimal, capped by this value(Default 3). detect_numerics : bool Whether numeric strings should be automatically detected(Default True). """ @deprecated_param("1.0.0", "1.2.0", "sign_mode", "sign") @deprecated_param("1.0.0", "1.2.0", "numeric_precision", "precision") @deprecated_param("1.0.0", "1.2.0", "max_width", "maxwidth") @deprecated_param("1.0.0", "1.2.0", "serialno") @deprecated_param("1.0.0", "1.2.0", "serialno_header") def __init__( self, maxwidth=80, default_alignment=enums.ALIGN_CENTER, default_padding=1, precision=3, serialno=False, serialno_header="SN", detect_numerics=True, sign=enums.SM_MINUS, **kwargs, ): kwargs.setdefault("max_width", None) if kwargs["max_width"] is not None: maxwidth = kwargs["max_width"] kwargs.setdefault("numeric_precision", None) if kwargs["numeric_precision"] is not None: precision = kwargs["numeric_precision"] kwargs.setdefault("sign_mode", None) if kwargs["sign_mode"] is not None: sign = kwargs["sign_mode"] self.precision = precision self._serialno = serialno self._serialno_header = serialno_header self.detect_numerics = detect_numerics self._sign = sign self.maxwidth = maxwidth self._ncol = 0 self._data = BTTableData(self) self.rows = BTRowCollection(self) self.columns = BTColumnCollection(self, default_alignment, default_padding) self._header_separator = "" self._header_junction = "" self._column_separator = "" self._row_separator = "" self.border = "" self.set_style(enums.STYLE_DEFAULT) def __copy__(self): obj = type(self)() obj.__dict__.update({k: copy.copy(v) for k, v in self.__dict__.items()}) obj.rows._table = obj obj.rows.header._table = obj obj.columns._table = obj obj.columns.header._table = obj obj.columns.alignment._table = obj obj.columns.width._table = obj obj.columns.padding_left._table = obj obj.columns.padding_right._table = obj obj._data._table = obj for row in obj._data: row._table = obj return obj def __deepcopy__(self, memo): obj = type(self)() obj.__dict__.update( {k: copy.deepcopy(v, memo) for k, v in self.__dict__.items()} ) obj.rows._table = obj obj.rows.header._table = obj obj.columns._table = obj obj.columns.header._table = obj obj.columns.alignment._table = obj obj.columns.width._table = obj obj.columns.padding_left._table = obj obj.columns.padding_right._table = obj obj._data._table = obj for row in obj._data: row._table = obj return obj def __setattr__(self, name, value): attrs = ( "left_border_char", "right_border_char", "top_border_char", "bottom_border_char", "header_separator_char", "column_separator_char", "row_separator_char", "intersect_top_left", "intersect_top_mid", "intersect_top_right", "intersect_header_left", "intersect_header_mid", "intersect_header_right", "intersect_row_left", "intersect_row_mid", "intersect_row_right", "intersect_bottom_left", "intersect_bottom_mid", "intersect_bottom_right", ) if to_unicode(name) in attrs: warnings.warn( deprecation_message(name, "1.0.0", "1.2.0", None), FutureWarning, ) value = ensure_type(value, basestring, name) super(BeautifulTable, self).__setattr__(name, value) @deprecated( "1.0.0", "1.2.0", BTRowCollection.__len__, details="Use len(BeautifulTable.rows)' instead.", ) def __len__(self): # pragma: no cover return len(self.rows) @deprecated( "1.0.0" "1.2.0", BTRowCollection.__iter__, details="Use iter(BeautifulTable.rows)' instead.", ) def __iter__(self): # pragma: no cover return iter(self.rows) @deprecated( "1.0.0", "1.2.0", BTColumnCollection.__contains__, details="Use ''value' in BeautifulTable.{columns|rows}' instead.", ) def __contains__(self, key): # pragma: no cover if isinstance(key, basestring): return key in self.columns elif isinstance(key, Iterable): return key in self.rows else: raise TypeError(f"'key' must be str or Iterable, not {type(key).__name__}") def __repr__(self): return repr(self._data) def __str__(self): if len(self.rows) == 0 or len(self.columns) == 0: return "" string_ = [] for line in self._get_string([], append=False): string_.append(line) return "\n".join(string_) # ************************Properties Begin Here************************ @property def shape(self): """Read only attribute which returns the shape of the table.""" return (len(self.rows), len(self.columns)) @property def sign(self): """Attribute to control how signs are displayed for numerical data. It can be one of the following: ======================== ============================================= Option Meaning ======================== ============================================= beautifultable.SM_PLUS A sign should be used for both +ve and -ve numbers. beautifultable.SM_MINUS A sign should only be used for -ve numbers. beautifultable.SM_SPACE A leading space should be used for +ve numbers and a minus sign for -ve numbers. ======================== ============================================= """ return self._sign @sign.setter def sign(self, value): if not isinstance(value, enums.SignMode): allowed = (f"{type(self).__name__}.{i.name}" for i in enums.SignMode) error_msg = "allowed values for sign are: " + ", ".join(allowed) raise ValueError(error_msg) self._sign = value @property def border(self): """Characters used to draw the border of the table. You can set this directly to a character or use it's several attribute to control how each section of the table is rendered. It is an instance of :class:`~.BTBorder` """ return self._border @border.setter def border(self, value): self._border = BTBorder( top=value, left=value, bottom=value, right=value, top_left=value, bottom_left=value, bottom_right=value, top_right=value, header_left=value, header_right=value, top_junction=value, left_junction=value, bottom_junction=value, right_junction=value, ) @property def junction(self): """Character used to draw junctions in the row separator.""" return self._junction @junction.setter def junction(self, value): self._junction = ensure_type(value, basestring) @property @deprecated("1.0.0", "1.2.0", BTRowCollection.header.fget) def serialno(self): # pragma: no cover return self._serialno @serialno.setter @deprecated("1.0.0", "1.2.0", BTRowCollection.header.fget) def serialno(self, value): # pragma: no cover self._serialno = value @property @deprecated("1.0.0", "1.2.0") def serialno_header(self): # pragma: no cover return self._serialno_header @serialno_header.setter @deprecated("1.0.0", "1.2.0") def serialno_header(self, value): # pragma: no cover self._serialno_header = value @property @deprecated("1.0.0", "1.2.0", sign.fget) def sign_mode(self): # pragma: no cover return self.sign @sign_mode.setter @deprecated("1.0.0", "1.2.0", sign.fget) def sign_mode(self, value): # pragma: no cover self.sign = value @property def maxwidth(self): """get/set the maximum width of the table. The width of the table is guaranteed to not exceed this value. If it is not possible to print a given table with the width provided, this value will automatically adjust. """ offset = (len(self.columns) - 1) * termwidth(self.columns.separator) offset += termwidth(self.border.left) offset += termwidth(self.border.right) self._maxwidth = max(self._maxwidth, offset + len(self.columns)) return self._maxwidth @maxwidth.setter def maxwidth(self, value): self._maxwidth = value @property @deprecated("1.0.0", "1.2.0", maxwidth.fget) def max_table_width(self): # pragma: no cover return self.maxwidth @max_table_width.setter @deprecated("1.0.0", "1.2.0", maxwidth.fget) def max_table_width(self, value): # pragma: no cover self.maxwidth = value @property @deprecated( "1.0.0", "1.2.0", BTColumnCollection.__len__, details="Use 'len(self.columns)' instead.", ) def column_count(self): # pragma: no cover return len(self.columns) @property @deprecated("1.0.0", "1.2.0", BTColumnCollection.width_exceed_policy.fget) def width_exceed_policy(self): # pragma: no cover return self.columns.width_exceed_policy @width_exceed_policy.setter @deprecated("1.0.0", "1.2.0", BTColumnCollection.width_exceed_policy.fget) def width_exceed_policy(self, value): # pragma: no cover self.columns.width_exceed_policy = value @property @deprecated("1.0.0", "1.2.0", BTColumnCollection.default_alignment.fget) def default_alignment(self): # pragma: no cover return self.columns.default_alignment @default_alignment.setter @deprecated("1.0.0", "1.2.0", BTColumnCollection.default_alignment.fget) def default_alignment(self, value): # pragma: no cover self.columns.default_alignment = value @property @deprecated("1.0.0", "1.2.0", BTColumnCollection.default_padding.fget) def default_padding(self): # pragma: no cover return self.columns.default_padding @default_padding.setter @deprecated("1.0.0", "1.2.0", BTColumnCollection.default_padding.fget) def default_padding(self, value): # pragma: no cover self.columns.default_padding = value @property @deprecated("1.0.0", "1.2.0", BTColumnCollection.width.fget) def column_widths(self): # pragma: no cover return self.columns.width @column_widths.setter @deprecated("1.0.0", "1.2.0", BTColumnCollection.width.fget) def column_widths(self, value): # pragma: no cover self.columns.width = value @property @deprecated("1.0.0", "1.2.0", BTColumnCollection.header.fget) def column_headers(self): # pragma: no cover return self.columns.header @column_headers.setter @deprecated("1.0.0", "1.2.0", BTColumnCollection.header.fget) def column_headers(self, value): # pragma: no cover self.columns.header = value @property @deprecated("1.0.0", "1.2.0", BTColumnCollection.alignment.fget) def column_alignments(self): # pragma: no cover return self.columns.alignment @column_alignments.setter @deprecated("1.0.0", "1.2.0", BTColumnCollection.alignment.fget) def column_alignments(self, value): # pragma: no cover self.columns.alignment = value @property @deprecated("1.0.0", "1.2.0", BTColumnCollection.padding_left.fget) def left_padding_widths(self): # pragma: no cover return self.columns.padding_left @left_padding_widths.setter @deprecated("1.0.0", "1.2.0", BTColumnCollection.padding_left.fget) def left_padding_widths(self, value): # pragma: no cover self.columns.padding_left = value @property @deprecated("1.0.0", "1.2.0", BTColumnCollection.padding_right.fget) def right_padding_widths(self): # pragma: no cover return self.columns.padding_right @right_padding_widths.setter @deprecated("1.0.0", "1.2.0", BTColumnCollection.padding_right.fget) def right_padding_widths(self, value): # pragma: no cover self.columns.padding_right = value @deprecated( "1.0.0", "1.2.0", BTColumnCollection.__getitem__, details="Use 'BeautifulTable.{columns|rows}[key]' instead.", ) def __getitem__(self, key): # pragma: no cover return self.columns[key] if isinstance(key, basestring) else self.rows[key] @deprecated( "1.0.0", "1.2.0", BTColumnCollection.__setitem__, details="Use 'BeautifulTable.{columns|rows}[key]' instead.", ) def __setitem__(self, key, value): # pragma: no cover if isinstance(key, basestring): self.columns[key] = value else: self.rows[key] = value @deprecated( "1.0.0", "1.2.0", BTColumnCollection.__delitem__, details="Use 'BeautifulTable.{columns|rows}[key]' instead.", ) def __delitem__(self, key): # pragma: no cover if isinstance(key, basestring): del self.columns[key] else: del self.rows[key] # *************************Properties End Here************************* @deprecated( "1.0.0", "1.2.0", BTColumnCollection.__getitem__, details="Use 'BeautifulTable.columns[key]' instead.", ) def get_column(self, key): # pragma: no cover return self.columns[key] @deprecated( "1.0.0", "1.2.0", BTColumnHeader.__getitem__, details="Use 'BeautifulTable.columns.header[key]' instead.", ) def get_column_header(self, index): # pragma: no cover return self.columns.header[index] @deprecated( "1.0.0", "1.2.0", BTColumnHeader.__getitem__, details="Use 'BeautifulTable.columns.header.index(header)' instead.", ) def get_column_index(self, header): # pragma: no cover return self.columns.header.index(header) @deprecated("1.0.0", "1.2.0", BTRowCollection.filter) def filter(self, key): # pragma: no cover return self.rows.filter(key) @deprecated("1.0.0", "1.2.0", BTRowCollection.sort) def sort(self, key, reverse=False): # pragma: no cover self.rows.sort(key, reverse=reverse) @deprecated("1.0.0", "1.2.0", BTRowCollection.reverse) def reverse(self, value): # pragma: no cover self.rows.reverse() @deprecated("1.0.0", "1.2.0", BTRowCollection.pop) def pop_row(self, index=-1): # pragma: no cover return self.rows.pop(index) @deprecated("1.0.0", "1.2.0", BTRowCollection.insert) def insert_row(self, index, row): # pragma: no cover return self.rows.insert(index, row) @deprecated("1.0.0", "1.2.0", BTRowCollection.append) def append_row(self, value): # pragma: no cover self.rows.append(value) @deprecated("1.0.0", "1.2.0", BTRowCollection.update) def update_row(self, key, value): # pragma: no cover self.rows.update(key, value) @deprecated("1.0.0", "1.2.0", BTColumnCollection.pop) def pop_column(self, index=-1): # pragma: no cover return self.columns.pop(index) @deprecated("1.0.0", "1.2.0", BTColumnCollection.insert) def insert_column(self, index, header, column): # pragma: no cover self.columns.insert(index, column, header) @deprecated("1.0.0", "1.2.0", BTColumnCollection.append) def append_column(self, header, column): # pragma: no cover self.columns.append(column, header) @deprecated("1.0.0", "1.2.0", BTColumnCollection.update) def update_column(self, header, column): # pragma: no cover self.columns.update(header, column) def set_style(self, style): """Set the style of the table from a predefined set of styles. Parameters ---------- style: Style It can be one of the following: * beautifultable.STYLE_DEFAULT * beautifultable.STYLE_NONE * beautifultable.STYLE_DOTTED * beautifultable.STYLE_MYSQL * beautifultable.STYLE_SEPARATED * beautifultable.STYLE_COMPACT * beautifultable.STYLE_MARKDOWN * beautifultable.STYLE_RESTRUCTURED_TEXT * beautifultable.STYLE_BOX * beautifultable.STYLE_BOX_DOUBLED * beautifultable.STYLE_BOX_ROUNDED * beautifultable.STYLE_GRID """ if not isinstance(style, enums.Style): allowed = (f"{type(self).__name__}.{i.name}" for i in enums.Style) error_msg = "allowed values for style are: " + ", ".join(allowed) raise ValueError(error_msg) style_template = style.value self.border.left = style_template.left_border_char self.border.right = style_template.right_border_char self.border.top = style_template.top_border_char self.border.bottom = style_template.bottom_border_char self.border.top_left = style_template.intersect_top_left self.border.bottom_left = style_template.intersect_bottom_left self.border.bottom_right = style_template.intersect_bottom_right self.border.top_right = style_template.intersect_top_right self.border.header_left = style_template.intersect_header_left self.border.header_right = style_template.intersect_header_right self.columns.header.separator = style_template.header_separator_char self.columns.separator = style_template.column_separator_char self.rows.separator = style_template.row_separator_char self.border.top_junction = style_template.intersect_top_mid self.border.left_junction = style_template.intersect_row_left self.border.bottom_junction = style_template.intersect_bottom_mid self.border.right_junction = style_template.intersect_row_right self.columns.header.junction = style_template.intersect_header_mid self.junction = style_template.intersect_row_mid def _compute_width(self): """Calculate width of column automatically based on data.""" table_width = self._width lpw, rpw = self.columns.padding_left, self.columns.padding_right pad_widths = [(lpw[i] + rpw[i]) for i in range(len(self.columns))] maxwidths = [0 for index in range(len(self.columns))] offset = table_width - sum(self.columns.width) + sum(pad_widths) self._maxwidth = max(self._maxwidth, offset + len(self.columns)) for index, header in enumerate(self.columns.header): max_length = 0 for i in pre_process( header, self.detect_numerics, self.precision, self.sign.value ).split("\n"): output_str = pre_process( i, self.detect_numerics, self.precision, self.sign.value, ) max_length = max(max_length, termwidth(output_str)) maxwidths[index] += max_length for index, column in enumerate(zip(*self._data)): max_length = maxwidths[index] for i in column: for j in pre_process( i, self.detect_numerics, self.precision, self.sign.value ).split("\n"): output_str = pre_process( j, self.detect_numerics, self.precision, self.sign.value, ) max_length = max(max_length, termwidth(output_str)) maxwidths[index] = max_length sum_ = sum(maxwidths) desired_sum = self._maxwidth - offset # Set flag for columns who are within their fair share temp_sum = 0 flag = [0] * len(maxwidths) for i, width in enumerate(maxwidths): if width <= int(desired_sum / len(self.columns)): temp_sum += width flag[i] = 1 else: # Allocate atleast 1 character width to the column temp_sum += 1 avail_space = desired_sum - temp_sum actual_space = sum_ - temp_sum shrinked_columns = {} # Columns which exceed their fair share should be shrinked based on # how much space is left for the table for i, width in enumerate(maxwidths): self.columns.width[i] = width if not flag[i]: new_width = 1 + int((width - 1) * avail_space / actual_space) if new_width < width: self.columns.width[i] = new_width shrinked_columns[new_width] = i # Divide any remaining space among shrinked columns if shrinked_columns: extra = self._maxwidth - offset - sum(self.columns.width) actual_space = sum(shrinked_columns) if extra > 0: for i, width in enumerate(sorted(shrinked_columns)): index = shrinked_columns[width] extra_width = int(width * extra / actual_space) self.columns.width[i] += extra_width if i == (len(shrinked_columns) - 1): extra = self._maxwidth - offset - sum(self.columns.width) self.columns.width[index] += extra for i in range(len(self.columns)): self.columns.width[i] += pad_widths[i] @deprecated("1.0.0", "1.2.0", BTColumnCollection.padding.fget) def set_padding_widths(self, pad_width): # pragma: no cover self.columns.padding_left = pad_width self.columns.padding_right = pad_width @deprecated("1.0.0", "1.2.0") def copy(self): return copy.copy(self) @deprecated_param("1.0.0", "1.2.0", "clear_metadata", "reset_columns") def clear(self, reset_columns=False, **kwargs): # pragma: no cover """Clear the contents of the table. Clear all rows of the table, and if specified clears all column specific data. Parameters ---------- reset_columns : bool, optional If it is true(default False), all metadata of columns such as their alignment, padding, width, etc. are also cleared and number of columns is set to 0. """ kwargs.setdefault("clear_metadata", None) if kwargs["clear_metadata"]: reset_columns = kwargs["clear_metadata"] self.rows.clear() if reset_columns: self.columns.clear() def _get_horizontal_line( self, char, intersect_left, intersect_mid, intersect_right, mask=None ): """Get a horizontal line for the table. Internal method used to draw all horizontal lines in the table. Column width should be set prior to calling this method. This method detects intersection and handles it according to the values of `intersect_*_*` attributes. Parameters ---------- char : str Character used to draw the line. Returns ------- str String which will be printed as a line in the table. """ width = self._width if mask is None: mask = [True] * len(self.columns) try: line = list(char * (int(width / termwidth(char)) + 1))[:width] except ZeroDivisionError: line = [" "] * width if len(line) == 0: return "" # Only if Special Intersection is enabled and horizontal line is # visible if not char.isspace(): # If left border is enabled and it is visible visible_junc = not intersect_left.isspace() if termwidth(self.border.left) > 0: if not (self.border.left.isspace() and visible_junc): length = min( termwidth(self.border.left), termwidth(intersect_left), ) for i in range(length): line[i] = intersect_left[i] if mask[0] else " " visible_junc = not intersect_right.isspace() # If right border is enabled and it is visible if termwidth(self.border.right) > 0: if not (self.border.right.isspace() and visible_junc): length = min( termwidth(self.border.right), termwidth(intersect_right), ) for i in range(length): line[-i - 1] = intersect_right[-i - 1] if mask[-1] else " " visible_junc = not intersect_mid.isspace() # If column separator is enabled and it is visible if termwidth(self.columns.separator): if not (self.columns.separator.isspace() and visible_junc): index = termwidth(self.border.left) for i in range(len(self.columns) - 1): if not mask[i]: for j in range(self.columns.width[i]): line[index + j] = " " index += self.columns.width[i] length = min( termwidth(self.columns.separator), termwidth(intersect_mid), ) for j in range(length): # TODO: we should also hide junctions based on mask line[index + j] = ( intersect_mid[j] if (mask[i] or mask[i + 1]) else " " ) index += termwidth(self.columns.separator) return "".join(line) def _get_top_border(self, *args, **kwargs): return self._get_horizontal_line( self.border.top, self.border.top_left, self.border.top_junction, self.border.top_right, *args, **kwargs, ) def _get_header_separator(self, *args, **kwargs): return self._get_horizontal_line( self.columns.header.separator, self.border.header_left, self.columns.header.junction, self.border.header_right, *args, **kwargs, ) def _get_row_separator(self, *args, **kwargs): return self._get_horizontal_line( self.rows.separator, self.border.left_junction, self.junction, self.border.right_junction, *args, **kwargs, ) def _get_bottom_border(self, *args, **kwargs): return self._get_horizontal_line( self.border.bottom, self.border.bottom_left, self.border.bottom_junction, self.border.bottom_right, *args, **kwargs, ) @property def _width(self): """Get the actual width of the table as number of characters. Column width should be set prior to calling this method. Returns ------- int Width of the table as number of characters. """ if len(self.columns) == 0: return 0 width = sum(self.columns.width) width += (len(self.columns) - 1) * termwidth(self.columns.separator) width += termwidth(self.border.left) width += termwidth(self.border.right) return width @deprecated("1.0.0", "1.2.0", _width.fget) def get_table_width(self): # pragma: no cover return self._width def _get_string(self, rows=None, append=False, recalculate_width=True): row_header_visible = bool( "".join(x if x is not None else "" for x in self.rows.header).strip() ) and (len(self.columns) > 0) column_header_visible = bool( "".join(x if x is not None else "" for x in self.columns.header).strip() ) and (len(self.rows) > 0 or rows is not None) # Preparing table for printing serialno, row headers and column headers if len(self.columns) > 0: if self._serialno: self.columns.insert( 0, range(1, len(self.rows) + 1), self._serialno_header ) if row_header_visible: self.columns.insert(0, self.rows.header) if column_header_visible: self.rows.insert(0, self.columns.header) if (self.columns._auto_width and recalculate_width) or sum( self.columns.width ) == 0: self._compute_width() try: # Rendering the top border if self.border.top: yield self._get_top_border() # Print column headers if not empty or only spaces row_iterator = iter(self.rows) if column_header_visible: yield next(row_iterator)._get_string( align=self.columns.header.alignment ) if self.columns.header.separator: yield self._get_header_separator() # Printing rows first_row_encountered = False for i, row in enumerate(row_iterator): if first_row_encountered and self.rows.separator: yield self._get_row_separator() first_row_encountered = True content = to_unicode(row) yield content if rows is not None: # Printing additional rows prev_length = len(self.rows) for i, row in enumerate(rows, start=1): if first_row_encountered and self.rows.separator: yield self._get_row_separator() first_row_encountered = True if self._serialno: row.insert(0, prev_length + i) if row_header_visible: self.rows.append([None] + list(row)) else: self.rows.append(row) content = to_unicode(self.rows[-1]) if not append: self.rows.pop() yield content # Rendering the bottom border if self.border.bottom: yield self._get_bottom_border() except Exception: raise finally: # Cleanup if column_header_visible: self.rows.pop(0) if row_header_visible: self.columns.pop(0) if len(self.columns) > 0: if self._serialno: self.columns.pop(0) return def stream(self, rows, append=False): """Get a generator for the table. This should be used in cases where data takes time to retrieve and it is required to be displayed as soon as possible. Any existing rows in the table shall also be returned. It is essential that atleast one of column header, width or existing rows set before calling this method. Parameters ---------- rows : iterable A generator which yields one row at a time. append : bool, optional If rows should also be appended to the table.(Default False) Returns ------- iterable: string representation of the table as a generators """ for line in self._get_string(rows, append=append, recalculate_width=False): yield line @deprecated("1.0.0", "1.2.0", str) def get_string(self): return str(self) def to_csv(self, file_name, *args, **kwargs): """Export table to CSV format. Parameters ---------- file_name : str Path to CSV file. """ if not isinstance(file_name, str): raise ValueError( f"Expected 'file_name' to be string, got {type(file_name).__name__}" ) with open(file_name, mode="wt", newline="") as csv_file: csv_writer = csv.writer(csv_file, *args, **kwargs) if bool( "".join(x if x is not None else "" for x in self.columns.header).strip() ): csv_writer.writerow(self.columns.header) csv_writer.writerows(self.rows) def from_csv(self, file_name, header=True, **kwargs): """Create table from CSV file. Parameters ---------- file_name : str Path to CSV file. header : bool, optional Whether First row in CSV file should be parsed as table header. Raises ------ ValueError If `file_name` is not str type. FileNotFoundError If `file_name` is not valid path to file. """ if not isinstance(file_name, str): raise ValueError( f"Expected 'file_name' to be string, got {type(file_name).__name__}" ) with open(file_name, mode="rt", newline="") as csv_file: csv_reader = csv.reader(csv_file, **kwargs) if header: self.columns.header = next(csv_reader) for row in csv_reader: self.rows.append(row) return self def to_df(self): """Export table to dataframe. This method requires that you have `pandas` already installed in your machine. Returns ------- pandas.Dataframe: The exported dataframe """ try: import pandas as pd except ImportError: warnings.warn( "This method requires that 'pandas' is installed.", RuntimeWarning ) raise # If there are column headers then it will act as a column of dataframe headers = list(self.columns.header) if headers.count(None) == len(headers): headers = None # If there are row headers then it will act as an Index index = list(self.rows.header) if index.count(None) == len(index): index = None return pd.DataFrame( [list(row) for row in self.rows], columns=headers, index=index ) def from_df(self, df): """Import table from dataframe. Parameters ---------- df : pandas.Dataframe input dataframe """ data = df.to_dict() # Dataframe columns will act as a column headers headers = list(data.keys()) # Index of dataframe will act as a row headers row_header = list(df.index) for header in headers: self.columns.append( [data[header][indx] for indx in row_header], header=str(header) ) self.rows.header = row_header return self ================================================ FILE: beautifultable/compat.py ================================================ from itertools import zip_longest # noqa: F401 from collections.abc import Iterable # noqa: F401 to_unicode = str basestring = (str, bytes) ================================================ FILE: beautifultable/enums.py ================================================ from __future__ import unicode_literals import enum from .styles import ( DefaultStyle, NoStyle, DottedStyle, MySQLStyle, SeparatedStyle, CompactStyle, MarkdownStyle, RestructuredTextStyle, BoxStyle, DoubledBoxStyle, RoundedStyle, GridStyle, ) class WidthExceedPolicy(enum.Enum): WEP_WRAP = 1 WEP_STRIP = 2 WEP_ELLIPSIS = 3 def __repr__(self): return self.name class SignMode(enum.Enum): SM_PLUS = "+" SM_MINUS = "-" SM_SPACE = " " def __repr__(self): return self.name class Alignment(enum.Enum): ALIGN_LEFT = "<" ALIGN_CENTER = "^" ALIGN_RIGHT = ">" def __repr__(self): return self.name class Style(enum.Enum): STYLE_DEFAULT = DefaultStyle STYLE_NONE = NoStyle STYLE_DOTTED = DottedStyle STYLE_MYSQL = MySQLStyle STYLE_SEPARATED = SeparatedStyle STYLE_COMPACT = CompactStyle STYLE_MARKDOWN = MarkdownStyle STYLE_RST = RestructuredTextStyle STYLE_BOX = BoxStyle STYLE_BOX_DOUBLED = DoubledBoxStyle STYLE_BOX_ROUNDED = RoundedStyle STYLE_GRID = GridStyle def __repr__(self): return self.name WEP_WRAP = WidthExceedPolicy.WEP_WRAP WEP_STRIP = WidthExceedPolicy.WEP_STRIP WEP_ELLIPSIS = WidthExceedPolicy.WEP_ELLIPSIS SM_PLUS = SignMode.SM_PLUS SM_MINUS = SignMode.SM_MINUS SM_SPACE = SignMode.SM_SPACE ALIGN_LEFT = Alignment.ALIGN_LEFT ALIGN_CENTER = Alignment.ALIGN_CENTER ALIGN_RIGHT = Alignment.ALIGN_RIGHT STYLE_DEFAULT = Style.STYLE_DEFAULT STYLE_NONE = Style.STYLE_NONE STYLE_DOTTED = Style.STYLE_DOTTED STYLE_SEPARATED = Style.STYLE_SEPARATED STYLE_COMPACT = Style.STYLE_COMPACT STYLE_MYSQL = Style.STYLE_MYSQL STYLE_MARKDOWN = Style.STYLE_MARKDOWN STYLE_RST = Style.STYLE_RST STYLE_BOX = Style.STYLE_BOX STYLE_BOX_DOUBLED = Style.STYLE_BOX_DOUBLED STYLE_BOX_ROUNDED = Style.STYLE_BOX_ROUNDED STYLE_GRID = Style.STYLE_GRID ================================================ FILE: beautifultable/helpers.py ================================================ import copy import weakref import operator from . import enums from .base import BTBaseRow, BTBaseColumn from .utils import pre_process, termwidth, textwrap, ensure_type from .compat import basestring, Iterable, to_unicode, zip_longest from .meta import AlignmentMetaData, NonNegativeIntegerMetaData class BTRowHeader(BTBaseColumn): def __init__(self, table, value): for i in value: self._validate_item(i) super(BTRowHeader, self).__init__(table, value) def __setitem__(self, key, value): self._validate_item(value) super(BTRowHeader, self).__setitem__(key, value) def _validate_item(self, value): if not (isinstance(value, basestring) or value is None): raise TypeError(f"header must be of type 'str', got {type(value).__name__}") class BTColumnHeader(BTBaseRow): def __init__(self, table, value): for i in value: self._validate_item(i) super(BTColumnHeader, self).__init__(table, value) self.alignment = None @property def alignment(self): """get/set alignment of the column header of the table. It can be any iterable containing only the following: * beautifultable.ALIGN_LEFT * beautifultable.ALIGN_CENTER * beautifultable.ALIGN_RIGHT """ return self._alignment @alignment.setter def alignment(self, value): if value is None: self._alignment = None return if isinstance(value, enums.Alignment): value = [value] * len(self) self._alignment = AlignmentMetaData(self._table, value) @property def separator(self): """Character used to draw the line seperating header from the table.""" return self._table._header_separator @separator.setter def separator(self, value): self._table._header_separator = ensure_type(value, basestring) @property def junction(self): """Character used to draw junctions in the header separator.""" return self._table._header_junction @junction.setter def junction(self, value): self._table._header_junction = ensure_type(value, basestring) def __setitem__(self, key, value): self._validate_item(value) super(BTColumnHeader, self).__setitem__(key, value) def _validate_item(self, value): if not (isinstance(value, basestring) or value is None): raise TypeError(f"header must be of type 'str', got {type(value).__name__}") class BTRowData(BTBaseRow): def _get_padding(self): return ( self._table.columns.padding_left, self._table.columns.padding_right, ) def _clamp_row(self, row): """Process a row so that it is clamped by column_width. Parameters ---------- row : array_like A single row. Returns ------- list of list: List representation of the `row` after it has been processed according to width exceed policy. """ table = self._table lpw, rpw = self._get_padding() wep = table.columns.width_exceed_policy result = [] if ( wep is enums.WidthExceedPolicy.WEP_STRIP or wep is enums.WidthExceedPolicy.WEP_ELLIPSIS ): # Let's strip the row delimiter = "" if wep is enums.WidthExceedPolicy.WEP_STRIP else "..." row_item_list = [] for index, row_item in enumerate(row): left_pad = table.columns._pad_character * lpw[index] right_pad = table.columns._pad_character * rpw[index] clmp_str = ( left_pad + self._clamp_string(row_item, index, delimiter) + right_pad ) row_item_list.append(clmp_str) result.append(row_item_list) elif wep is enums.WidthExceedPolicy.WEP_WRAP: # Let's wrap the row string_partition = [] for index, row_item in enumerate(row): width = table.columns.width[index] - lpw[index] - rpw[index] string_partition.append(textwrap(row_item, width)) for row_items in zip_longest(*string_partition, fillvalue=""): row_item_list = [] for index, row_item in enumerate(row_items): left_pad = table.columns._pad_character * lpw[index] right_pad = table.columns._pad_character * rpw[index] row_item_list.append(left_pad + row_item + right_pad) result.append(row_item_list) return result or [[""] * len(table.columns)] def _clamp_string(self, row_item, index, delimiter=""): """Clamp `row_item` to fit in column referred by index. This method considers padding and appends the delimiter if `row_item` needs to be truncated. Parameters ---------- row_item: str String which should be clamped. index: int Index of the column `row_item` belongs to. delimiter: str String which is to be appended to the clamped string. Returns ------- str The modified string which fits in it's column. """ lpw, rpw = self._get_padding() width = self._table.columns.width[index] - lpw[index] - rpw[index] if termwidth(row_item) <= width: return row_item else: if width - len(delimiter) >= 0: clamped_string = ( textwrap(row_item, width - len(delimiter))[0] + delimiter ) else: clamped_string = delimiter[:width] return clamped_string def _get_string( self, align=None, mask=None, draw_left_border=True, draw_right_border=True, ): """Return a string representation of a row.""" rows = [] table = self._table width = table.columns.width sign = table.sign if align is None: align = table.columns.alignment if mask is None: mask = [True] * len(table.columns) lpw, rpw = self._get_padding() string = [] for i, item in enumerate(self._value): if isinstance(item, type(table)): # temporarily change the max width of the table curr_maxwidth = item.maxwidth item.maxwidth = width[i] - lpw[i] - rpw[i] rows.append( pre_process( item, table.detect_numerics, table.precision, sign.value, ).split("\n") ) item.maxwidth = curr_maxwidth else: rows.append( pre_process( item, table.detect_numerics, table.precision, sign.value, ).split("\n") ) for row in map(list, zip_longest(*rows, fillvalue="")): for i in range(len(row)): row[i] = pre_process( row[i], table.detect_numerics, table.precision, sign.value, ) for row_ in self._clamp_row(row): for i in range(len(table.columns)): # str.format method doesn't work for multibyte strings # hence, we need to manually align the texts instead # of using the align property of the str.format method pad_len = width[i] - termwidth(row_[i]) if align[i].value == "<": right_pad = " " * pad_len row_[i] = to_unicode(row_[i]) + right_pad elif align[i].value == ">": left_pad = " " * pad_len row_[i] = left_pad + to_unicode(row_[i]) else: left_pad = " " * (pad_len // 2) right_pad = " " * (pad_len - pad_len // 2) row_[i] = left_pad + to_unicode(row_[i]) + right_pad content = [] for j, item in enumerate(row_): if j > 0: content.append( table.columns.separator if (mask[j - 1] or mask[j]) else " " * termwidth(table.columns.separator) ) content.append(item) content = "".join(content) content = ( table.border.left if mask[0] else " " * termwidth(table.border.left) ) + content content += ( table.border.right if mask[-1] else " " * termwidth(table.border.right) ) string.append(content) return "\n".join(string) def __str__(self): return self._get_string() def aslist(self): """Return list of row values.""" return self.value def asdict(self): """ Return dictionary where key is column header and value as row value and raise a Warning if coulmn header invalid(not provided) or empty. """ header_rowval_map = {} for header, row_val in zip(self._table.columns.header, self.value): if header is None or header == "": raise Warning("Column header is not provided or invalid") header_rowval_map[header] = row_val return header_rowval_map class BTColumnData(BTBaseColumn): def aslist(self): """Return list of column values.""" return self.value def asdict(self): """ Raise a NotImplementedError as currently it is not implemented """ raise NotImplementedError("Currently supported for rows only") class BTRowCollection(object): def __init__(self, table): self._table = table self._reset_state(0) @property def _table(self): return self._table_ref() @_table.setter def _table(self, value): self._table_ref = weakref.ref(value) def _reset_state(self, nrow): self._table._data = type(self._table._data)( self._table, [BTRowData(self._table, [None] * self._table._ncol) for i in range(nrow)], ) self.header = BTRowHeader(self._table, [None] * nrow) @property def header(self): return self._header @header.setter def header(self, value): self._header = BTRowHeader(self._table, value) @property def separator(self): """Character used to draw the line seperating two rows.""" return self._table._row_separator @separator.setter def separator(self, value): self._table._row_separator = ensure_type(value, basestring) def _canonical_key(self, key): if isinstance(key, (int, slice)): return key elif isinstance(key, basestring): return self.header.index(key) raise TypeError( f"row indices must be int, str or slices, not {type(key).__name__}" ) def __len__(self): return len(self._table._data) def __getitem__(self, key): """Get a particular row, or a new table by slicing. Parameters ---------- key : int, slice, str If key is an `int`, returns a row at index `key`. If key is an `str`, returns the first row with heading `key`. If key is a slice object, returns a new sliced table. Raises ------ TypeError If key is not of type int, slice or str. IndexError If `int` index is out of range. KeyError If `str` key is not found in header. """ if isinstance(key, slice): new_table = copy.deepcopy(self._table) new_table.rows.clear() new_table.rows.header = self._table.rows.header[key] for i, r in enumerate(self._table._data[key]): new_table.rows[i] = r.value return new_table if isinstance(key, (int, basestring)): return self._table._data[key] raise TypeError( f"row indices must be int, str or a slice object, not {type(key).__name__}" ) def __delitem__(self, key): """Delete a row, or multiple rows by slicing. Parameters ---------- key : int, slice, str If key is an `int`, deletes a row at index `key`. If key is an `str`, deletes the first row with heading `key`. If key is a slice object, deletes multiple rows. Raises ------ TypeError If key is not of type int, slice or str. IndexError If `int` key is out of range. KeyError If `str` key is not in header. """ if isinstance(key, (int, basestring, slice)): del self._table._data[key] del self.header[key] else: raise TypeError( f"row indices must be int, str or a slice object, not {type(key).__name__}" ) def __setitem__(self, key, value): """Update a row, or multiple rows by slicing. Parameters ---------- key : int, slice, str If key is an `int`, updates a row. If key is an `str`, updates the first row with heading `key`. If key is a slice object, updates multiple rows. Raises ------ TypeError If key is not of type int, slice or str. IndexError If `int` key is out of range. KeyError If `str` key is not in header. """ if isinstance(key, (int, basestring)): self._table._data[key] = BTRowData(self._table, value) elif isinstance(key, slice): value = [list(row) for row in value] if len(self._table.columns) == 0: self._table.columns._initialize(len(value[0])) self._table._data[key] = [BTRowData(self._table, row) for row in value] else: raise TypeError("key must be int, str or a slice object") def __contains__(self, key): if isinstance(key, basestring): return key in self.header elif isinstance(key, Iterable): return key in self._table._data else: raise TypeError(f"'key' must be str or Iterable, not {type(key).__name__}") def __iter__(self): return BTCollectionIterator(self) def __repr__(self): return repr(self._table._data) def __str__(self): return str(self._table._data) def reverse(self): """Reverse the table row-wise *IN PLACE*.""" self._table._data._reverse() def pop(self, index=-1): """Remove and return row at index (default last). Parameters ---------- index : int, str index or heading of the row. Normal list rules apply. """ if not isinstance(index, (int, basestring)): raise TypeError(f"row index must be int or str, not {type(index).__name__}") if len(self._table._data) == 0: raise IndexError("pop from empty table") else: res = self._table._data._pop(index) self.header._pop(index) return res def insert(self, index, row, header=None): """Insert a row before index in the table. Parameters ---------- index : int List index rules apply row : iterable Any iterable of appropriate length. header : str, optional Heading of the row Raises ------ TypeError: If `row` is not an iterable. ValueError: If size of `row` is inconsistent with the current number of columns. """ if self._table._ncol == 0: row = list(row) self._table.columns._reset_state(len(row)) self.header._insert(index, header) self._table._data._insert(index, BTRowData(self._table, row)) def append(self, row, header=None): """Append a row to end of the table. Parameters ---------- row : iterable Any iterable of appropriate length. header : str, optional Heading of the row """ self.insert(len(self), row, header) def update(self, key, value): """Update row(s) identified with `key` in the table. `key` can be a index or a slice object. Parameters ---------- key : int or slice index of the row, or a slice object. value : iterable If an index is specified, `value` should be an iterable of appropriate length. Instead if a slice object is passed as key, value should be an iterable of rows. Raises ------ IndexError: If index specified is out of range. TypeError: If `value` is of incorrect type. ValueError: If length of row does not matches number of columns. """ self[key] = value def clear(self): self._reset_state(0) def sort(self, key, reverse=False): """Stable sort of the table *IN-PLACE* with respect to a column. Parameters ---------- key: int, str index or header of the column. Normal list rules apply. reverse : bool If `True` then table is sorted as if each comparison was reversed. """ if isinstance(key, (int, basestring)): key = operator.itemgetter(key) elif callable(key): pass else: raise TypeError("'key' must either be 'int' or 'str' or a 'callable'") indices = sorted( range(len(self)), key=lambda x: key(self._table._data[x]), reverse=reverse, ) self._table._data._sort(key=key, reverse=reverse) self.header = [self.header[i] for i in indices] def filter(self, key): """Return a copy of the table with only those rows which satisfy a certain condition. Returns ------- BeautifulTable: Filtered copy of the BeautifulTable instance. """ new_table = self._table.rows[:] new_table.rows.clear() for row in filter(key, self): new_table.rows.append(row) return new_table class BTCollectionIterator(object): def __init__(self, collection): self._collection = collection self._index = -1 def __iter__(self): return self def __next__(self): self._index += 1 if self._index == len(self._collection): raise StopIteration return self._collection[self._index] class BTColumnCollection(object): def __init__(self, table, default_alignment, default_padding): self._table = table self._width_exceed_policy = enums.WEP_WRAP self._pad_character = " " self.default_alignment = default_alignment self.default_padding = default_padding self._reset_state(0) @property def _table(self): return self._table_ref() @_table.setter def _table(self, value): self._table_ref = weakref.ref(value) @property def padding(self): """Set width for left and rigth padding of the columns of the table.""" raise AttributeError( "cannot read attribute 'padding'. use 'padding_{left|right}'" ) @padding.setter def padding(self, value): self.padding_left = value self.padding_right = value def _reset_state(self, ncol): self._table._ncol = ncol self._header = BTColumnHeader(self._table, [None] * ncol) self._auto_width = True self._alignment = AlignmentMetaData( self._table, [self.default_alignment] * ncol ) self._width = NonNegativeIntegerMetaData(self._table, [0] * ncol) self._padding_left = NonNegativeIntegerMetaData( self._table, [self.default_padding] * ncol ) self._padding_right = NonNegativeIntegerMetaData( self._table, [self.default_padding] * ncol ) self._table._data = type(self._table._data)( self._table, [ BTRowData(self._table, [None] * ncol) for i in range(len(self._table._data)) ], ) def _canonical_key(self, key): if isinstance(key, (int, slice)): return key elif isinstance(key, basestring): return self.header.index(key) raise TypeError( f"column indices must be int, str or slices, not {type(key).__name__}" ) @property def header(self): """get/set headings for the columns of the table. It can be any iterable with all members an instance of `str` or None. """ return self._header @header.setter def header(self, value): self._header = BTColumnHeader(self._table, value) @property def alignment(self): """get/set alignment of the columns of the table. It can be any iterable containing only the following: * beautifultable.ALIGN_LEFT * beautifultable.ALIGN_CENTER * beautifultable.ALIGN_RIGHT """ return self._alignment @alignment.setter def alignment(self, value): if isinstance(value, enums.Alignment): value = [value] * len(self) self._alignment = AlignmentMetaData(self._table, value) @property def width(self): """get/set width for the columns of the table. Width of the column specifies the max number of characters a column can contain. Larger characters are handled according to `width_exceed_policy`. This can be one of `'auto'`, a non-negative integer or an iterable of the same length as the number of columns. If set to anything other than 'auto', the user is responsible for updating it if new columns are added or existing ones are updated. """ return self._width @width.setter def width(self, value): if isinstance(value, str): if value == "auto": self._auto_width = True return raise ValueError(f"Invalid value '{value}'") if isinstance(value, int): value = [value] * len(self) self._width = NonNegativeIntegerMetaData(self._table, value) self._auto_width = False @property def padding_left(self): """get/set width for left padding of the columns of the table. Left Width of the padding specifies the number of characters on the left of a column reserved for padding. By Default It is 1. """ return self._padding_left @padding_left.setter def padding_left(self, value): if isinstance(value, int): value = [value] * len(self) self._padding_left = NonNegativeIntegerMetaData(self._table, value) @property def padding_right(self): """get/set width for right padding of the columns of the table. Right Width of the padding specifies the number of characters on the rigth of a column reserved for padding. By default It is 1. """ return self._padding_right @padding_right.setter def padding_right(self, value): if isinstance(value, int): value = [value] * len(self) self._padding_right = NonNegativeIntegerMetaData(self._table, value) @property def width_exceed_policy(self): """Attribute to control how exceeding column width should be handled. It can be one of the following: ============================ ========================================= Option Meaning ============================ ========================================= beautifultable.WEP_WRAP An item is wrapped so every line fits within it's column width. beautifultable.WEP_STRIP An item is stripped to fit in it's column. beautifultable.WEP_ELLIPSIS An item is stripped to fit in it's column and appended with ...(Ellipsis). ============================ ========================================= """ return self._width_exceed_policy @width_exceed_policy.setter def width_exceed_policy(self, value): if not isinstance(value, enums.WidthExceedPolicy): allowed = ( f"{type(self).__name__}.{i.name}" for i in enums.WidthExceedPolicy ) error_msg = "allowed values for width_exceed_policy are: " + ", ".join( allowed ) raise ValueError(error_msg) self._width_exceed_policy = value @property def default_alignment(self): """Attribute to control the alignment of newly created columns. It can be one of the following: ============================ ========================================= Option Meaning ============================ ========================================= beautifultable.ALIGN_LEFT New columns are left aligned. beautifultable.ALIGN_CENTER New columns are center aligned. beautifultable.ALIGN_RIGHT New columns are right aligned. ============================ ========================================= """ return self._default_alignment @default_alignment.setter def default_alignment(self, value): if not isinstance(value, enums.Alignment): allowed = (f"{type(self).__name__}.{i.name}" for i in enums.Alignment) error_msg = "allowed values for default_alignment are: " + ", ".join( allowed ) raise ValueError(error_msg) self._default_alignment = value @property def default_padding(self): """Initial value for Left and Right padding widths for new columns.""" return self._default_padding @default_padding.setter def default_padding(self, value): if not isinstance(value, int): raise TypeError("default_padding must be an integer") elif value < 0: raise ValueError("default_padding must be a non-negative integer") else: self._default_padding = value @property def separator(self): """Character used to draw the line seperating two columns.""" return self._table._column_separator @separator.setter def separator(self, value): self._table._column_separator = ensure_type(value, basestring) def __len__(self): return self._table._ncol def __getitem__(self, key): """Get a column, or a new table by slicing. Parameters ---------- key : int, slice, str If key is an `int`, returns column at index `key`. If key is an `str`, returns first column with heading `key`. If key is a slice object, returns a new sliced table. Raises ------ TypeError If key is not of type int, slice or str. IndexError If `int` key is out of range. KeyError If `str` key is not in header. """ if isinstance(key, int): pass elif isinstance(key, slice): new_table = copy.deepcopy(self._table) new_table.columns.clear() new_table.columns.header = self.header[key] new_table.columns.alignment = self.alignment[key] new_table.columns.padding_left = self.padding_left[key] new_table.columns.padding_right = self.padding_right[key] new_table.columns.width = self.width[key] new_table.columns._auto_width = self._auto_width for i, r in enumerate(self._table._data): new_table.rows[i] = r.value[key] return new_table elif isinstance(key, basestring): key = self.header.index(key) else: raise TypeError( f"column indices must be integers, strings or slices, not {type(key).__name__}" ) return BTColumnData(self._table, [row[key] for row in self._table._data]) def __delitem__(self, key): """Delete a column, or multiple columns by slicing. Parameters ---------- key : int, slice, str If key is an `int`, deletes column at index `key`. If key is a slice object, deletes multiple columns. If key is an `str`, deletes the first column with heading `key` Raises ------ TypeError If key is not of type int, slice or str. IndexError If `int` key is out of range. KeyError If `str` key is not in header. """ if isinstance(key, (int, basestring, slice)): key = self._canonical_key(key) del self.alignment[key] del self.width[key] del self.padding_left[key] del self.padding_right[key] for row in self._table.rows: del row[key] del self.header[key] if self.header.alignment is not None: del self.header.alignment[key] self._table._ncol = len(self.header) if self._table._ncol == 0: del self._table.rows[:] else: raise TypeError( f"table indices must be int, str or slices, not {type(key).__name__}" ) def __setitem__(self, key, value): """Update a column, or multiple columns by slicing. Parameters ---------- key : int, slice, str If key is an `int`, updates column at index `key`. If key is an `str`, updates first column with heading `key`. If key is a slice object, updates multiple columns. Raises ------ TypeError If key is not of type int, slice or str. IndexError If `int` key is out of range. KeyError If `str` key is not in header """ if not isinstance(key, (int, basestring, slice)): raise TypeError("column indices must be of type int, str or a slice object") for row, new_item in zip(self._table.rows, value): row[key] = new_item def __contains__(self, key): if isinstance(key, basestring): return key in self.header elif isinstance(key, Iterable): key = list(key) return any(key == column for column in self) else: raise TypeError(f"'key' must be str or Iterable, not {type(key).__name__}") def __iter__(self): return BTCollectionIterator(self) def __repr__(self): return repr(self._table) def __str__(self): return str(self._table._data) def clear(self): self._reset_state(0) def pop(self, index=-1): """Remove and return column at index (default last). Parameters ---------- index : int, str index of the column, or the header of the column. If index is specified, then normal list rules apply. Raises ------ TypeError: If index is not an instance of `int`, or `str`. IndexError: If Table is empty. """ if not isinstance(index, (int, basestring)): raise TypeError( f"column index must be int or str, not {type(index).__name__}" ) if self._table._ncol == 0: raise IndexError("pop from empty table") else: res = [] index = self._canonical_key(index) for row in self._table.rows: res.append(row._pop(index)) res = BTColumnData(self._table, res) self.alignment._pop(index) self.width._pop(index) self.padding_left._pop(index) self.padding_right._pop(index) self.header._pop(index) self._table._ncol = len(self.header) if self._table._ncol == 0: del self._table.rows[:] return res def update(self, key, value): """Update a column named `header` in the table. If length of column is smaller than number of rows, lets say `k`, only the first `k` values in the column is updated. Parameters ---------- key : int, str If `key` is int, column at index `key` is updated. If `key` is str, the first column with heading `key` is updated. value : iterable Any iterable of appropriate length. Raises ------ TypeError: If length of `column` is shorter than number of rows. ValueError: If no column exists with heading `header`. """ self[key] = value def insert( self, index, column, header=None, padding_left=None, padding_right=None, alignment=None, ): """Insert a column before `index` in the table. If length of column is bigger than number of rows, lets say `k`, only the first `k` values of `column` is considered. If column is shorter than 'k', ValueError is raised. Note that Table remains in consistent state even if column is too short. Any changes made by this method is rolled back before raising the exception. Parameters ---------- index : int List index rules apply. column : iterable Any iterable of appropriate length. header : str, optional Heading of the column. padding_left : int, optional Left padding of the column. padding_right : int, optional Right padding of the column. alignment : Alignment, optional alignment of the column. Raises ------ TypeError: If `header` is not of type `str`. ValueError: If length of `column` is shorter than number of rows. """ padding_left = self.default_padding if padding_left is None else padding_left padding_right = self.default_padding if padding_right is None else padding_right alignment = self.default_alignment if alignment is None else alignment if not isinstance(padding_left, int): raise TypeError( f"'padding_left' should be of type 'int' not '{type(padding_left).__name__}'" ) if not isinstance(padding_right, int): raise TypeError( f"'padding_right' should be of type 'int' not '{type(padding_right).__name__}'" ) if not isinstance(alignment, enums.Alignment): raise TypeError( f"alignment should be of type '{enums.Alignment.__name__}' not '{type(alignment).__name__}'" ) if self._table._ncol == 0: self.header = [header] self.padding_left = [padding_left] self.padding_right = [padding_right] self.alignment = [alignment] self._table._data = type(self._table._data)( self._table, [BTRowData(self._table, [i]) for i in column] ) else: if (not isinstance(header, basestring)) and (header is not None): raise TypeError( f"header must be of type 'str' not '{type(header).__name__}'" ) column_length = 0 for row, new_item in zip(self._table.rows, column): row._insert(index, new_item) column_length += 1 if column_length == len(self._table.rows): self._table._ncol += 1 self.header._insert(index, header) self.width._insert(index, 0) self.alignment._insert(index, alignment) self.padding_left._insert(index, padding_left) self.padding_right._insert(index, padding_right) if self.header.alignment is not None: self.header.alignment._insert(index, alignment) else: # Roll back changes so that table remains in consistent state for j in range(column_length, -1, -1): self._table.rows[j]._pop(index) raise ValueError( f"length of 'column' should be atleast {len(self._table.rows)}, got {column_length}" ) def append( self, column, header=None, padding_left=None, padding_right=None, alignment=None, ): """Append a column to end of the table. Parameters ---------- column : iterable Any iterable of appropriate length. header : str, optional Heading of the column padding_left : int, optional Left padding of the column padding_right : int, optional Right padding of the column alignment : Alignment, optional alignment of the column """ self.insert( self._table._ncol, column, header, padding_left, padding_right, alignment, ) ================================================ FILE: beautifultable/meta.py ================================================ from .base import BTBaseRow from .enums import Alignment class MetaData(BTBaseRow): def __init__(self, table, row): for i in row: self.validate(i) super(MetaData, self).__init__(table, row) def __setitem__(self, key, value): self.validate(value) super(MetaData, self).__setitem__(key, value) def validate(self, value): pass class AlignmentMetaData(MetaData): def validate(self, value): if not isinstance(value, Alignment): allowed = (f"{type(self).__name__}.{i.name}" for i in Alignment) error_msg = ( "allowed values for alignment are: " + ", ".join(allowed) + f", was {value}" ) raise TypeError(error_msg) class NonNegativeIntegerMetaData(MetaData): def validate(self, value): if isinstance(value, int) and value >= 0: pass else: raise TypeError( ("Value must a non-negative integer, " "was {}").format(value) ) ================================================ FILE: beautifultable/styles.py ================================================ # -*- coding: utf-8 -*- from __future__ import unicode_literals class NoStyle: left_border_char = "" right_border_char = "" top_border_char = "" bottom_border_char = "" header_separator_char = "" column_separator_char = "" row_separator_char = "" intersect_top_left = "" intersect_top_mid = "" intersect_top_right = "" intersect_header_left = "" intersect_header_mid = "" intersect_header_right = "" intersect_row_left = "" intersect_row_mid = "" intersect_row_right = "" intersect_bottom_left = "" intersect_bottom_mid = "" intersect_bottom_right = "" class DefaultStyle(NoStyle): left_border_char = "|" right_border_char = "|" top_border_char = "-" bottom_border_char = "-" header_separator_char = "-" column_separator_char = "|" row_separator_char = "-" intersect_top_left = "+" intersect_top_mid = "+" intersect_top_right = "+" intersect_header_left = "+" intersect_header_mid = "+" intersect_header_right = "+" intersect_row_left = "+" intersect_row_mid = "+" intersect_row_right = "+" intersect_bottom_left = "+" intersect_bottom_mid = "+" intersect_bottom_right = "+" class MySQLStyle(DefaultStyle): pass class SeparatedStyle(DefaultStyle): top_border_char = "=" header_separator_char = "=" class CompactStyle(NoStyle): header_separator_char = "-" column_separator_char = " " intersect_top_left = " " intersect_top_mid = " " intersect_top_right = " " intersect_header_left = " " intersect_header_mid = " " intersect_header_right = " " intersect_row_left = " " intersect_row_mid = " " intersect_row_right = " " intersect_bottom_left = " " intersect_bottom_mid = " " intersect_bottom_right = " " class DottedStyle(NoStyle): left_border_char = ":" right_border_char = ":" top_border_char = "." bottom_border_char = "." header_separator_char = "." column_separator_char = ":" class MarkdownStyle(NoStyle): left_border_char = "|" right_border_char = "|" header_separator_char = "-" column_separator_char = "|" intersect_header_left = "|" intersect_header_mid = "|" intersect_header_right = "|" class RestructuredTextStyle(CompactStyle): top_border_char = "=" bottom_border_char = "=" header_separator_char = "=" class BoxStyle(NoStyle): left_border_char = "│" right_border_char = "│" top_border_char = "─" bottom_border_char = "─" header_separator_char = "─" column_separator_char = "│" row_separator_char = "─" intersect_top_left = "┌" intersect_top_mid = "┬" intersect_top_right = "┐" intersect_header_left = "├" intersect_header_mid = "┼" intersect_header_right = "┤" intersect_row_left = "├" intersect_row_mid = "┼" intersect_row_right = "┤" intersect_bottom_left = "└" intersect_bottom_mid = "┴" intersect_bottom_right = "┘" class DoubledBoxStyle(NoStyle): left_border_char = "║" right_border_char = "║" top_border_char = "═" bottom_border_char = "═" header_separator_char = "═" column_separator_char = "║" row_separator_char = "═" intersect_top_left = "╔" intersect_top_mid = "╦" intersect_top_right = "╗" intersect_header_left = "╠" intersect_header_mid = "╬" intersect_header_right = "╣" intersect_row_left = "╠" intersect_row_mid = "╬" intersect_row_right = "╣" intersect_bottom_left = "╚" intersect_bottom_mid = "╩" intersect_bottom_right = "╝" class RoundedStyle(BoxStyle): intersect_top_left = "╭" intersect_top_right = "╮" intersect_bottom_left = "╰" intersect_bottom_right = "╯" class GridStyle(BoxStyle): left_border_char = "║" right_border_char = "║" top_border_char = "═" bottom_border_char = "═" intersect_top_left = "╔" intersect_top_mid = "╤" intersect_top_right = "╗" intersect_header_left = "╟" intersect_header_right = "╢" intersect_row_left = "╟" intersect_row_right = "╢" intersect_bottom_left = "╚" intersect_bottom_mid = "╧" intersect_bottom_right = "╝" ================================================ FILE: beautifultable/utils.py ================================================ """Module containing some utility methods""" import warnings import functools from .ansi import ANSIMultiByteString from .compat import to_unicode def to_numeric(item): """ Helper method to convert a string to float or int if possible. If the conversion is not possible, it simply returns the string. """ num_types = (int, float) # We don't wan't to perform any conversions if item is already a number if isinstance(item, num_types): return item # First try for an int conversion so that strings like "5" are converted # to 5 instead of 5.0 . This is safe as a direct int cast for a non integer # string raises a ValueError. try: num = int(to_unicode(item)) except ValueError: try: num = float(to_unicode(item)) except ValueError: return item else: return num except TypeError: return item else: return num def ensure_type(value, *types, varname="value"): if not isinstance(value, types): expected_types_str = "/".join([t.__name__ for t in types]) raise TypeError( ("Expected '{}' to be of type '{}', " "got '{}'").format( varname, expected_types_str, type(value).__name__ ) ) return value def pre_process(item, detect_numerics, precision, sign_value): """Returns the final string which should be displayed""" if item is None: return "" if detect_numerics: item = to_numeric(item) if isinstance(item, float): item = round(item, precision) try: item = "{:{sign}}".format(item, sign=sign_value) except (ValueError, TypeError): pass return to_unicode(item) def termwidth(item): """Returns the visible width of the string as shown on the terminal""" obj = ANSIMultiByteString(to_unicode(item)) return obj.termwidth() def textwrap(item, width): obj = ANSIMultiByteString(to_unicode(item)) return obj.wrap(width) def deprecation_message( old_name, deprecated_in, removed_in, extra_msg ): # pragma: no cover return f"'{old_name}' has been deprecated in 'v{deprecated_in}' and will be removed in 'v{removed_in}'. {extra_msg}" def deprecated( deprecated_in, removed_in, replacement=None, details=None, ): # pragma: no cover def decorator(f): @functools.wraps(f) def wrapper(*args, **kwds): nonlocal details if not details: if replacement: details = replacement.__qualname__ details = details.replace( "BTColumns", "BeautifulTable.columns", ) details = details.replace( "BTRows", "BeautifulTable.rows", ) details = details.replace( "BTColumnHeader", "BeautifulTable.columns.header", ) details = details.replace( "BTRowHeader", "BeautifulTable.rows.header", ) details = f"Use '{details}' instead." else: details = "" message = deprecation_message( f.__qualname__, deprecated_in, removed_in, details, ) if replacement: f.__doc__ = "{}\n\n{}".format(replacement.__doc__, message) warnings.warn(message, FutureWarning) return f(*args, **kwds) return wrapper return decorator def deprecated_param( deprecated_in, removed_in, old_name, new_name=None, details=None, ): # pragma: no cover def decorator(f): @functools.wraps(f) def wrapper(*args, **kwargs): nonlocal details if not details: details = f"Use '{new_name}' instead." if new_name else "" message = deprecation_message( old_name, deprecated_in, removed_in, details, ) if old_name in kwargs: warnings.warn(message, FutureWarning) return f(*args, **kwargs) return wrapper return decorator ================================================ FILE: coverage.xml ================================================ D:\Workspace\beautifultable\beautifultable ================================================ FILE: docs/Makefile ================================================ # Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXPROJ = BeautifulTable SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ================================================ FILE: docs/badges.rst ================================================ .. include:: ../README.rst :start-after: inclusion-marker-badges-start :end-before: inclusion-marker-badges-end ================================================ FILE: docs/changelog.rst ================================================ .. include:: ../README.rst :start-after: inclusion-marker-changelog-start :end-before: inclusion-marker-changelog-end ================================================ FILE: docs/conf.py ================================================ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # # BeautifulTable documentation build configuration file, created by # sphinx-quickstart on Sun Dec 18 15:59:32 2016. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys sys.path.insert(0, os.path.abspath("../")) import beautifultable # noqa E402 # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.todo", "sphinx.ext.coverage", "sphinx.ext.ifconfig", "sphinx.ext.viewcode", "sphinx.ext.napoleon", ] napoleon_google_docstring = False napoleon_include_special_with_doc = False # napoleon_use_param = False # napoleon_use_ivar = True autodoc_member_order = "bysource" # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = ".rst" # The master toctree document. master_doc = "index" # General information about the project. project = "BeautifulTable" copyright = "2022, Priyam Singh" author = beautifultable.__author__ # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = beautifultable.__version__ # The full version, including alpha/beta/rc tags. release = beautifultable.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "default" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. htmlhelp_basename = "BeautifulTabledoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ( master_doc, "BeautifulTable.tex", "BeautifulTable Documentation", "Priyam Singh", "manual", ), ] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, "beautifultable", "BeautifulTable Documentation", [author], 1) ] # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "BeautifulTable", "BeautifulTable Documentation", author, "BeautifulTable", "One line description of project.", "Miscellaneous", ), ] # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {"https://docs.python.org/": None} ================================================ FILE: docs/donation.rst ================================================ .. include:: ../README.rst :start-after: inclusion-marker-donation-start :end-before: inclusion-marker-donation-end ================================================ FILE: docs/index.rst ================================================ .. BeautifulTable documentation master file, created by sphinx-quickstart on Sun Dec 18 15:59:32 2016. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. ########################################## Welcome to beautifultable's documentation! ########################################## .. include:: badges.rst .. include:: introduction.rst .. include:: links.rst ****************************************** Contents ****************************************** .. toctree:: :maxdepth: 3 install quickstart changelog .. include:: donation.rst ================================================ FILE: docs/install.rst ================================================ .. include:: ../README.rst :start-after: inclusion-marker-install-start :end-before: inclusion-marker-install-end ================================================ FILE: docs/introduction.rst ================================================ .. include:: ../README.rst :start-after: inclusion-marker-introduction-start :end-before: inclusion-marker-introduction-end ================================================ FILE: docs/links.rst ================================================ .. include:: ../README.rst :start-after: inclusion-marker-links-start :end-before: inclusion-marker-links-end ================================================ FILE: docs/make.bat ================================================ @ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build set SPHINXPROJ=PrinterTools if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd ================================================ FILE: docs/quickstart.rst ================================================ ************************************************************************* Quickstart ************************************************************************* ========================================================================= Building the Table ========================================================================= Building a table is very easy. :class:`.BeautifulTable` provides two views ``rows`` and ``columns``. You can use them to modify their respective properties. Let's create our first table and add some rows. .. code:: python >>> from beautifultable import BeautifulTable >>> table = BeautifulTable() >>> table.rows.append(["Jacob", 1, "boy"]) >>> table.rows.append(["Isabella", 1, "girl"]) >>> table.rows.append(["Ethan", 2, "boy"]) >>> table.rows.append(["Sophia", 2, "girl"]) >>> table.rows.append(["Michael", 3, "boy"]) >>> table.columns.header = ["name", "rank", "gender"] >>> table.rows.header = ["S1", "S2", "S3", "S4", "S5"] >>> print(table) +----+----------+------+--------+ | | name | rank | gender | +----+----------+------+--------+ | S1 | Jacob | 1 | boy | +----+----------+------+--------+ | S2 | Isabella | 1 | girl | +----+----------+------+--------+ | S3 | Ethan | 2 | boy | +----+----------+------+--------+ | S4 | Sophia | 2 | girl | +----+----------+------+--------+ | S5 | Michael | 3 | boy | +----+----------+------+--------+ BeautifulTable initializes the shape lazily. Here when you appended the first row, the number of columns was set to 3. Further rows had to be of length 3. If you had set the columns and/or row headers beforehand as follows, the table shape would already be set to (5, 3). Hence you would just set the rows directly using their indices or keys. .. code:: python >>> from beautifultable import BeautifulTable >>> table = BeautifulTable() >>> table.columns.header = ["name", "rank", "gender"] >>> table.rows.header = ["S1", "S2", "S3", "S4", "S5"] >>> table.rows[0] = ["Jacob", 1, "boy"] >>> table.rows[1] = ["Isabella", 1, "girl"] >>> table.rows[2] = ["Ethan", 2, "boy"] >>> table.rows[3] = ["Sophia", 2, "girl"] >>> table.rows[4] =["Michael", 3, "boy"] >>> print(table) +----+----------+------+--------+ | | name | rank | gender | +----+----------+------+--------+ | S1 | Jacob | 1 | boy | +----+----------+------+--------+ | S2 | Isabella | 1 | girl | +----+----------+------+--------+ | S3 | Ethan | 2 | boy | +----+----------+------+--------+ | S4 | Sophia | 2 | girl | +----+----------+------+--------+ | S5 | Michael | 3 | boy | +----+----------+------+--------+ So, We created our first table. Let's add a new column. .. code:: python >>> table.columns.append(["2010", "2012", "2008", "2010", "2011"], header="year") >>> print(table) +----+----------+------+--------+------+ | | name | rank | gender | year | +----+----------+------+--------+------+ | S1 | Jacob | 1 | boy | 2010 | +----+----------+------+--------+------+ | S2 | Isabella | 1 | girl | 2012 | +----+----------+------+--------+------+ | S3 | Ethan | 2 | boy | 2008 | +----+----------+------+--------+------+ | S4 | Sophia | 2 | girl | 2010 | +----+----------+------+--------+------+ | S5 | Michael | 3 | boy | 2011 | +----+----------+------+--------+------+ You can also build a :class:`.BeautifulTable` using slicing. Slicing creates a new table with it's own copy of data. But it retains the properties of the original object. You can slice both rows or columns. .. code:: python >>> new_table = table.rows[:3] >>> print(new_table) +----+----------+------+--------+------+ | | name | rank | gender | year | +----+----------+------+--------+------+ | S1 | Jacob | 1 | boy | 2010 | +----+----------+------+--------+------+ | S2 | Isabella | 1 | girl | 2012 | +----+----------+------+--------+------+ | S3 | Ethan | 2 | boy | 2008 | +----+----------+------+--------+------+ .. code:: python >>> new_table = table.columns[:3] >>> print(new_table) +----+----------+------+--------+ | | name | rank | gender | +----+----------+------+--------+ | S1 | Jacob | 1 | boy | +----+----------+------+--------+ | S2 | Isabella | 1 | girl | +----+----------+------+--------+ | S3 | Ethan | 2 | boy | +----+----------+------+--------+ | S4 | Sophia | 2 | girl | +----+----------+------+--------+ | S5 | Michael | 3 | boy | +----+----------+------+--------+ As you can see how easy it is to create a Table with **beautifultable**. Now lets move on to see some common use cases. For details, please refer the API Documentation. ========================================================================= Accessing Rows ========================================================================= You can access a row using it's index or it's header. It returns a **BTRowData** object. .. code:: python >>> print(list(table.rows[3])) ['Sophia', 2, 'girl', '2010'] To access a particular field of a row, you can again use the index, or the header of the required column. .. code:: python >>> print(table.rows[3][2]) girl >>> print(table.rows[3]['gender']) girl ========================================================================= Accessing Columns ========================================================================= You can access a column using it's index or it's header. It returns a **BTColumnData** object. .. code:: python >>> print(list(table.columns['name'])) ['Jacob', 'Isabella', 'Ethan', 'Sophia', 'Michael'] To access a particular field of a column, you can again use the index, or the header of the required row. .. code:: python >>> print(table.columns[2][3]) girl >>> print(table.columns[2]['S4']) girl ========================================================================= Counting Rows and Columns ========================================================================= You can get the number of columns or rows in the table by using the ``len`` function. You can also use the :attr:`.BeautifulTable.shape` attribute. .. code:: python >>> print(len(table.columns)) 3 >>> print(len(table.rows)) 5 >>> print(table.shape) (5,3) ========================================================================= Inserting Rows and Columns ========================================================================= BeautifulTable provides 2 methods, :meth:`.BTRowCollection.insert` and :meth:`.BTColumnCollection.insert` for this purpose. .. code:: python >>> table.rows.insert(3, ['Gary', 2, 'boy', 2009], header='S6') >>> table.columns.insert(2, [78, 67, 82, 56, 86, 74], header='marks') >>> print(table) +----+----------+------+-------+--------+------+ | | name | rank | marks | gender | year | +----+----------+------+-------+--------+------+ | S1 | Jacob | 1 | 78 | boy | 2010 | +----+----------+------+-------+--------+------+ | S2 | Isabella | 1 | 67 | girl | 2012 | +----+----------+------+-------+--------+------+ | S3 | Ethan | 2 | 82 | boy | 2008 | +----+----------+------+-------+--------+------+ | S6 | Gary | 2 | 56 | boy | 2009 | +----+----------+------+-------+--------+------+ | S4 | Sophia | 2 | 86 | girl | 2010 | +----+----------+------+-------+--------+------+ | S5 | Michael | 3 | 74 | boy | 2011 | +----+----------+------+-------+--------+------+ ========================================================================= Removing Rows and Columns ========================================================================= Removing a row or column is very easy. Just delete it using ``del`` statement. .. code:: python >>> del table.rows[3] >>> del table.columns['year'] >>> print(table) +----+----------+------+-------+--------+ | | name | rank | marks | gender | +----+----------+------+-------+--------+ | S1 | Jacob | 1 | 78 | boy | +----+----------+------+-------+--------+ | S2 | Isabella | 1 | 67 | girl | +----+----------+------+-------+--------+ | S3 | Ethan | 2 | 82 | boy | +----+----------+------+-------+--------+ | S4 | Sophia | 2 | 86 | girl | +----+----------+------+-------+--------+ | S5 | Michael | 3 | 74 | boy | +----+----------+------+-------+--------+ You can also use the helper methods :meth:`.BTRowCollection.pop`, :meth:`.BTColumnCollection.pop` to do the same thing. Both these methods take the index or header of the row/column to be removed. Therefore the following 2 snippets are equivalent. .. code:: python >>> table.columns.pop('marks') .. code:: python >>> table.columns.pop(2) ========================================================================= Updating data in the Table ========================================================================= Let's change the name in the 4th row to ``'Sophie'``. .. code:: python >>> table.rows[3][0] = 'Sophie' # index of 4th row is 3 >>> print(list(table.rows[3])) ['Sophie', 2, 86, 'girl'] You could have done the same thing using the header. .. code:: python >>> table.rows[3]['name'] = 'Sophie' Or, you can also change the entire row, or even multiple rows using slicing. .. code:: python >>> table.rows[3] = ['Sophie', 2, 56, 'girl'] You can also update existing columns as shown below. .. code:: python >>> table.columns['marks'] = [75, 46, 89, 56, 82] >>> print(table) +----+----------+------+-------+--------+ | | name | rank | marks | gender | +----+----------+------+-------+--------+ | S1 | Jacob | 1 | 75 | boy | +----+----------+------+-------+--------+ | S2 | Isabella | 1 | 46 | girl | +----+----------+------+-------+--------+ | S3 | Ethan | 2 | 89 | boy | +----+----------+------+-------+--------+ | S4 | Sophie | 2 | 56 | girl | +----+----------+------+-------+--------+ | S5 | Michael | 3 | 82 | boy | +----+----------+------+-------+--------+ The methods :meth:`.BTRowCollection.update` and :meth:`.BTColumnCollection.update` can be used to perform the operations discussed in this section. Note that you can only update existing columns but can't create a new column using this method. For that you need to use the methods :meth:`.BTRowCollection.append`, :meth:`.BTRowCollection.insert`, :meth:`.BTColumnCollection.append` or :meth:`.BTColumnCollection.insert`. ========================================================================= Searching for rows or columns headers ========================================================================= Cheking if a column header is in the table. .. code:: python >>> 'rank' in table.columns.header True Cheking if a row header is in the table. .. code:: python >>> 'S2' in table.rows.header True Cheking if a row is in table .. code:: python >>> ["Ethan", 2, 89, "boy"] in table.rows True Cheking if a column is in table .. code:: python >>> ["Jacob", "Isabella", "Ethan", "Sophie", "Michael"] in table.columns True ========================================================================= Sorting based on a Column ========================================================================= You can also :meth:`.BTRowCollection.sort` the table based on a column by specifying it's index or it's header. .. code:: python >>> table.rows.sort('marks') >>> print(table) +----+----------+------+-------+--------+ | | name | rank | marks | gender | +----+----------+------+-------+--------+ | S2 | Isabella | 1 | 46 | girl | +----+----------+------+-------+--------+ | S4 | Sophia | 2 | 56 | girl | +----+----------+------+-------+--------+ | S1 | Jacob | 1 | 75 | boy | +----+----------+------+-------+--------+ | S5 | Michael | 3 | 82 | boy | +----+----------+------+-------+--------+ | S3 | Ethan | 2 | 89 | boy | +----+----------+------+-------+--------+ ========================================================================= Customizing the look of the Table ========================================================================= ------------------------------------------------------------------------- Alignment ------------------------------------------------------------------------- Let's change the way some columns are aligned in our table. .. code:: python >>> table.columns.alignment['name'] = BeautifulTable.ALIGN_LEFT >>> table.columns.alignment['gender'] = BeautifulTable.ALIGN_RIGHT >>> print(table) +----+----------+------+-------+--------+ | | name | rank | marks | gender | +----+----------+------+-------+--------+ | S2 | Isabella | 1 | 46 | girl | +----+----------+------+-------+--------+ | S4 | Sophia | 2 | 56 | girl | +----+----------+------+-------+--------+ | S1 | Jacob | 1 | 75 | boy | +----+----------+------+-------+--------+ | S5 | Michael | 3 | 82 | boy | +----+----------+------+-------+--------+ | S3 | Ethan | 2 | 89 | boy | +----+----------+------+-------+--------+ You can also set all columns to a specific alignment .. code:: python >>> table.columns.alignment = BeautifulTable.ALIGN_RIGHT >>> print(table) +----+----------+------+-------+--------+ | | name | rank | marks | gender | +----+----------+------+-------+--------+ | S2 | Isabella | 1 | 46 | girl | +----+----------+------+-------+--------+ | S4 | Sophia | 2 | 56 | girl | +----+----------+------+-------+--------+ | S1 | Jacob | 1 | 75 | boy | +----+----------+------+-------+--------+ | S5 | Michael | 3 | 82 | boy | +----+----------+------+-------+--------+ | S3 | Ethan | 2 | 89 | boy | +----+----------+------+-------+--------+ Headers can have a different alignment that the column. .. code:: python >>> table.columns.header.alignment= BeautifulTable.ALIGN_RIGHT >>> table.columns.alignment = BeautifulTable.ALIGN_LEFT >>> print(table) +----+----------+------+-------+--------+ | | name | rank | marks | gender | +----+----------+------+-------+--------+ | S2 | Isabella | 1 | 46 | girl | +----+----------+------+-------+--------+ | S4 | Sophia | 2 | 56 | girl | +----+----------+------+-------+--------+ | S1 | Jacob | 1 | 75 | boy | +----+----------+------+-------+--------+ | S5 | Michael | 3 | 82 | boy | +----+----------+------+-------+--------+ | S3 | Ethan | 2 | 89 | boy | +----+----------+------+-------+--------+ ------------------------------------------------------------------------- Padding ------------------------------------------------------------------------- You can change the padding for individual column similar to the alignment. .. code:: python >>> table.columns.padding_left['rank'] = 5 >>> table.columns.padding_right['rank'] = 3 >>> print(table) +----+----------+------------+--------+ | | name | rank | gender | +----+----------+------------+--------+ | S1 | Jacob | 1 | boy | +----+----------+------------+--------+ | S2 | Isabella | 1 | girl | +----+----------+------------+--------+ | S3 | Ethan | 2 | boy | +----+----------+------------+--------+ | S4 | Sophia | 2 | girl | +----+----------+------------+--------+ | S5 | Michael | 3 | boy | +----+----------+------------+--------+ You can use a helper attribute :attr:`.BTColumnCollection.padding` to set the left and right padding to a common value. ------------------------------------------------------------------------- Styling ------------------------------------------------------------------------- **beautifultable** comes with several predefined styles for various use cases. You can use the :meth:`.BeautifulTable.set_style` method to set the style of the table. The following styles are available: * **STYLE_DEFAULT** .. code:: python >>> table.set_style(BeautifulTable.STYLE_DEFAULT) >>> print(table) +----+----------+------+--------+ | | name | rank | gender | +----+----------+------+--------+ | S1 | Jacob | 1 | boy | +----+----------+------+--------+ | S2 | Isabella | 1 | girl | +----+----------+------+--------+ | S3 | Ethan | 2 | boy | +----+----------+------+--------+ | S4 | Sophia | 2 | girl | +----+----------+------+--------+ | S5 | Michael | 3 | boy | +----+----------+------+--------+ * **STYLE_NONE** .. code:: python >>> table.set_style(BeautifulTable.STYLE_NONE) >>> print(table) name rank gender S1 Jacob 1 boy S2 Isabella 1 girl S3 Ethan 2 boy S4 Sophia 2 girl S5 Michael 3 boy * **STYLE_DOTTED** .. code:: python >>> table.set_style(BeautifulTable.STYLE_DOTTED) >>> print(table) ................................. : : name : rank : gender : ................................. : S1 : Jacob : 1 : boy : : S2 : Isabella : 1 : girl : : S3 : Ethan : 2 : boy : : S4 : Sophia : 2 : girl : : S5 : Michael : 3 : boy : ................................. * **STYLE_SEPARATED** .. code:: python >>> table.set_style(BeautifulTable.STYLE_SEPARATED) >>> print(table) +====+==========+======+========+ | | name | rank | gender | +====+==========+======+========+ | S1 | Jacob | 1 | boy | +----+----------+------+--------+ | S2 | Isabella | 1 | girl | +----+----------+------+--------+ | S3 | Ethan | 2 | boy | +----+----------+------+--------+ | S4 | Sophia | 2 | girl | +----+----------+------+--------+ | S5 | Michael | 3 | boy | +----+----------+------+--------+ * **STYLE_COMPACT** .. code:: python >>> table.set_style(BeautifulTable.STYLE_COMPACT) >>> print(table) name rank gender ---- ---------- ------ -------- S1 Jacob 1 boy S2 Isabella 1 girl S3 Ethan 2 boy S4 Sophia 2 girl S5 Michael 3 boy * **STYLE_MYSQL** .. code:: python >>> table.set_style(BeautifulTable.STYLE_MYSQL) >>> print(table) # Yes, the default style is same as this style +----+----------+------+--------+ | | name | rank | gender | +----+----------+------+--------+ | S1 | Jacob | 1 | boy | +----+----------+------+--------+ | S2 | Isabella | 1 | girl | +----+----------+------+--------+ | S3 | Ethan | 2 | boy | +----+----------+------+--------+ | S4 | Sophia | 2 | girl | +----+----------+------+--------+ | S5 | Michael | 3 | boy | +----+----------+------+--------+ * **STYLE_MARKDOWN** .. code:: python >>> table.set_style(BeautifulTable.STYLE_MARKDOWN) >>> print(table) # Markdown alignment not supported currently | | name | rank | gender | |----|----------|------|--------| | S1 | Jacob | 1 | boy | | S2 | Isabella | 1 | girl | | S3 | Ethan | 2 | boy | | S4 | Sophia | 2 | girl | | S5 | Michael | 3 | boy | * **STYLE_RST** .. code:: python >>> table.set_style(BeautifulTable.STYLE_RST) >>> print(table) ==== ========== ====== ======== name rank gender ==== ========== ====== ======== S1 Jacob 1 boy S2 Isabella 1 girl S3 Ethan 2 boy S4 Sophia 2 girl S5 Michael 3 boy ==== ========== ====== ======== * **STYLE_BOX** .. code:: python >>> table.set_style(BeautifulTable.STYLE_BOX) >>> print(table) ┌────┬──────────┬──────┬────────┐ │ │ name │ rank │ gender │ ├────┼──────────┼──────┼────────┤ │ S1 │ Jacob │ 1 │ boy │ ├────┼──────────┼──────┼────────┤ │ S2 │ Isabella │ 1 │ girl │ ├────┼──────────┼──────┼────────┤ │ S3 │ Ethan │ 2 │ boy │ ├────┼──────────┼──────┼────────┤ │ S4 │ Sophia │ 2 │ girl │ ├────┼──────────┼──────┼────────┤ │ S5 │ Michael │ 3 │ boy │ └────┴──────────┴──────┴────────┘ * **STYLE_BOX_DOUBLED** .. code:: python >>> table.set_style(BeautifulTable.STYLE_BOX_DOUBLED) >>> print(table) ╔════╦══════════╦══════╦════════╗ ║ ║ name ║ rank ║ gender ║ ╠════╬══════════╬══════╬════════╣ ║ S1 ║ Jacob ║ 1 ║ boy ║ ╠════╬══════════╬══════╬════════╣ ║ S2 ║ Isabella ║ 1 ║ girl ║ ╠════╬══════════╬══════╬════════╣ ║ S3 ║ Ethan ║ 2 ║ boy ║ ╠════╬══════════╬══════╬════════╣ ║ S4 ║ Sophia ║ 2 ║ girl ║ ╠════╬══════════╬══════╬════════╣ ║ S5 ║ Michael ║ 3 ║ boy ║ ╚════╩══════════╩══════╩════════╝ * **STYLE_BOX_ROUNDED** .. code:: python >>> table.set_style(BeautifulTable.STYLE_BOX_ROUNDED) >>> print(table) ╭────┬──────────┬──────┬────────╮ │ │ name │ rank │ gender │ ├────┼──────────┼──────┼────────┤ │ S1 │ Jacob │ 1 │ boy │ ├────┼──────────┼──────┼────────┤ │ S2 │ Isabella │ 1 │ girl │ ├────┼──────────┼──────┼────────┤ │ S3 │ Ethan │ 2 │ boy │ ├────┼──────────┼──────┼────────┤ │ S4 │ Sophia │ 2 │ girl │ ├────┼──────────┼──────┼────────┤ │ S5 │ Michael │ 3 │ boy │ ╰────┴──────────┴──────┴────────╯ * **STYLE_GRID** .. code:: python >>> table.set_style(BeautifulTable.STYLE_GRID) >>> print(table) ╔════╤══════════╤══════╤════════╗ ║ │ name │ rank │ gender ║ ╟────┼──────────┼──────┼────────╢ ║ S1 │ Jacob │ 1 │ boy ║ ╟────┼──────────┼──────┼────────╢ ║ S2 │ Isabella │ 1 │ girl ║ ╟────┼──────────┼──────┼────────╢ ║ S3 │ Ethan │ 2 │ boy ║ ╟────┼──────────┼──────┼────────╢ ║ S4 │ Sophia │ 2 │ girl ║ ╟────┼──────────┼──────┼────────╢ ║ S5 │ Michael │ 3 │ boy ║ ╚════╧══════════╧══════╧════════╝ For more finer customization, you can change what characters are used to draw various parts of the table. Here we show you an example of how you can use this feature. You can read the API Reference for more details. .. code:: python >>> table.set_style(BeautifulTable.STYLE_NONE) # clear all formatting >>> table.border.left = 'o' >>> table.border.right = 'o' >>> table.border.top = '<~>' >>> table.border.bottom = '=' >>> table.columns.header.separator = '^' >>> table.columns.separator = ':' >>> table.rows.separator = '~' >>> print(table) <~><~><~><~><~><~><~><~><~><~><~> o : name : rank : gender o ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ o S1 : Jacob : 1 : boy o ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ o S2 : Isabella : 1 : girl o ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ o S3 : Ethan : 2 : boy o ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ o S4 : Sophia : 2 : girl o ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ o S5 : Michael : 3 : boy o ================================= As you can see, you can change quite a lot about your *BeautifulTable* instance. For further sections, We switch the look of the table to *default* again. ------------------------------------------------------------------------- Colored Tables ------------------------------------------------------------------------- **beautifultable** comes with out of the box support for colored tables using ansi escape sequences. You can also use any library which makes use of these sequences to produce colored text output. :: python3 -m pip install termcolor .. code:: python >>> from termcolor import colored >>> table.rows.append([colored("John", 'red'), 4, colored("boy", 'blue')]) >>> print(table) .. raw:: html

+----+----------+------+--------+
|    |   name   | rank | gender |
+----+----------+------+--------+
| S1 |  Jacob   |  1   |  boy   |
+----+----------+------+--------+
| S2 | Isabella |  1   |  girl  |
+----+----------+------+--------+
| S3 |  Ethan   |  2   |  boy   |
+----+----------+------+--------+
| S4 |  Sophia  |  2   |  girl  |
+----+----------+------+--------+
| S5 | Michael  |  3   |  boy   |
+----+----------+------+--------+
| S6 |   John   |  4   |  boy   |
+----+----------+------+--------+

You can also use these sequences for making texts bold, italics, etc. ------------------------------------------------------------------------- Paragraphs ------------------------------------------------------------------------- A cell can contain multiple paragraphs such that each one start from a new line. **beautifultable** parses ``\n`` as a paragraph change. .. code:: python >>> new_table = BeautifulTable(max_width=40) >>> new_table.columns.header = ["Heading 1", "Heading 2"] >>> new_table.rows.append(["first Line\nsecond Line", "single line"]) >>> new_table.rows.append(["first Line\nsecond Line\nthird Line", "first Line\nsecond Line"]) >>> new_table.rows.append(["single line", "this is a very long first line\nThis is a very long second line"]) >>> print(new_table) +-------------+------------------------+ | Heading 1 | Heading 2 | +-------------+------------------------+ | first Line | single line | | second Line | | +-------------+------------------------+ | first Line | first Line | | second Line | second Line | | third Line | | +-------------+------------------------+ | single line | this is a very long fi | | | rst line | | | This is a very long se | | | cond line | +-------------+------------------------+ ------------------------------------------------------------------------- Subtables ------------------------------------------------------------------------- You can even render a :class:`.BeautifulTable` instance inside another table. To do that, just pass the table as any regular text and it just works. .. code:: python >>> # Setting up the inner table >>> subtable = BeautifulTable() >>> subtable.rows.append(["Jacob", 1, "boy"]) >>> subtable.rows.append(["Isabella", 1, "girl"]) >>> subtable.border.left = '' >>> subtable.border.right = '' >>> subtable.border.top = '' >>> subtable.border.right = '' >>> >>> # Setting up the outer table >>> table = BeautifulTable() >>> table.columns.header = ["Heading 1", "Heading 2"] >>> table.rows.append(["Sample text", "Another sample text"]) >>> table.rows.append([subtable, "More sample text"]) >>> table.columns.padding_left[0] = 0 >>> table.columns.padding_right[0] = 0 >>> print(table) +---------------------+---------------------+ | Heading 1 | Heading 2 | +---------------------+---------------------+ | Sample text | Another sample text | +---------------------+---------------------+ | Jacob | 1 | boy | More sample text | |----------+---+------| | | Isabella | 1 | girl | | +---------------------+---------------------+ ========================================================================= Streaming Tables ========================================================================= There are situations where data retrieval is slow such as when data is recieved over a network and you want to display the data as soon as possible. In these cases, you can use streaming tables to render the table with the help of a generator. Streaming table do have their limitation. The width calculation routine requires you to either set it manually or specify the column header or add atleast 1 row. You also cannot have row headers for streaming tables. .. code:: python >>> import time >>> def time_taking_process(): ... for i in range(5): ... time.sleep(1) ... yield [i, i**2] ... ... >>> table = BeautifulTable() >>> table.columns.header = ["Number", "It's Square"] >>> for line in table.stream(time_taking_process()): ... print(line) ... +--------+-------------+ | Number | It's Square | +--------+-------------+ | 0 | 0 | +--------+-------------+ | 1 | 1 | +--------+-------------+ | 2 | 4 | +--------+-------------+ | 3 | 9 | +--------+-------------+ | 4 | 16 | +--------+-------------+ ========================================================================= Support for Multibyte Unicode characters ========================================================================= **beautifultable** comes with built-in support for multibyte unicode such as east-asian characters. You can do much more with BeautifulTable but this much should give you a good start. Those of you who are interested to have more control can read the API Documentation. ================================================ FILE: docs/source/beautifultable.rst ================================================ beautifultable package ====================== Module contents --------------- .. automodule:: beautifultable :imported-members: :show-inheritance: .. autoclass:: beautifultable.BeautifulTable :members: .. autoclass:: beautifultable.BTRowCollection :members: .. autoclass:: beautifultable.BTColumnCollection :members: .. autoclass:: beautifultable.BTRowHeader :members: :inherited-members: .. autoclass:: beautifultable.BTColumnHeader :members: :inherited-members: .. autoclass:: beautifultable.BTBorder :members: ================================================ FILE: docs/source/modules.rst ================================================ beautifultable ============== .. toctree:: :maxdepth: 2 beautifultable ================================================ FILE: requirements.txt ================================================ wcwidth pandas ================================================ FILE: setup.cfg ================================================ [bdist_wheel] universal=1 ================================================ FILE: setup.py ================================================ import os import codecs import itertools from setuptools import setup install_requires = ["wcwidth"] extras_require = { "dev": ["pandas"], } extras_require["all"] = list( set(itertools.chain.from_iterable(extras_require.values())) ) this_dir = os.path.abspath(os.path.dirname(__file__)) version_path = os.path.join(this_dir, "beautifultable", "__version__.py") about = {} with codecs.open(version_path, "r", "utf-8") as f: exec(f.read(), about) with codecs.open("README.rst", "r", "utf-8") as f: readme = f.read() download_url = "{}/tarball/{}".format(about["__url__"], about["__version__"]) setup( name=about["__title__"], version=about["__version__"], description=about["__description__"], long_description=readme, author=about["__author__"], author_email=about["__author_email__"], packages=["beautifultable"], url=about["__url__"], download_url=download_url, license=about["__license__"], keywords="table terminal ascii", extras_require=extras_require, install_requires=install_requires, classifiers=[ "Development Status :: 4 - Beta", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Printing", "Topic :: Text Processing", ], ) ================================================ FILE: test.py ================================================ # -*- coding: utf-8 -*- import os import unittest import itertools from beautifultable import BeautifulTable try: import pandas as pd except ImportError: pd = None PANDAS_INSTALLED = False else: PANDAS_INSTALLED = True REQUIRED_PANDAS_MESSAGE = "requires 'pandas' to be installed" class TableOperationsTestCase(unittest.TestCase): def setUp(self): self.create_table() def create_table(self, maxwidth=80): table = BeautifulTable(maxwidth=maxwidth) table.rows.append(["Jacob", 1, "boy"]) table.rows.append(["Isabella", 1, "girl"]) table.rows.append(["Ethan", 2, "boy"]) table.rows.append(["Sophia", 2, "girl"]) table.rows.append(["Michael", 3, "boy"]) table.columns.header = ["name", "rank", "gender"] table.rows.header = ["S1", "S2", "S3", "S4", "S5"] self.table = table def create_dataframe(self): return self.table.to_df() def compare_iterable(self, iterable1, iterable2): for item1, item2 in itertools.zip_longest(iterable1, iterable2): self.assertEqual(item1, item2) # Test for table operations def test_filter(self): new_table = self.table.rows.filter(lambda x: x["rank"] > 1) self.assertEqual(len(self.table.rows), 5) rows = [ ["Ethan", 2, "boy"], ["Sophia", 2, "girl"], ["Michael", 3, "boy"], ] for row_t, row in zip(new_table.rows, rows): self.compare_iterable(row_t, row) def test_sort_by_index(self): self.table.rows.sort(0) rows = [ ["Ethan", 2, "boy"], ["Isabella", 1, "girl"], ["Jacob", 1, "boy"], ["Michael", 3, "boy"], ["Sophia", 2, "girl"], ] for row_t, row in zip(self.table.rows, rows): self.compare_iterable(row_t, row) def test_sort_by_index_reversed(self): self.table.rows.sort(0, reverse=True) rows = [ ["Ethan", 2, "boy"], ["Isabella", 1, "girl"], ["Jacob", 1, "boy"], ["Michael", 3, "boy"], ["Sophia", 2, "girl"], ] for row_t, row in zip(self.table.rows, reversed(rows)): self.compare_iterable(row_t, row) def test_sort_by_header(self): self.table.rows.sort("name") rows = [ ["Ethan", 2, "boy"], ["Isabella", 1, "girl"], ["Jacob", 1, "boy"], ["Michael", 3, "boy"], ["Sophia", 2, "girl"], ] for row_t, row in zip(self.table.rows, rows): self.compare_iterable(row_t, row) def test_sort_by_callable(self): self.table.rows.sort(lambda x: (x[1], x[0])) rows = [ ["Isabella", 1, "girl"], ["Jacob", 1, "boy"], ["Ethan", 2, "boy"], ["Sophia", 2, "girl"], ["Michael", 3, "boy"], ] for row_t, row in zip(self.table.rows, rows): self.compare_iterable(row_t, row) def test_sort_raises_exception(self): with self.assertRaises(TypeError): self.table.rows.sort(None) # Tests for column operations def test_column_aslist(self): self.assertEqual( [column.aslist() for column in self.table.columns], [ ["Jacob", "Isabella", "Ethan", "Sophia", "Michael"], [1, 1, 2, 2, 3], ["boy", "girl", "boy", "girl", "boy"], ], ) def test_column_asdict(self): with self.assertRaises(NotImplementedError): header_colval_map = [column.asdict() for column in self.table.columns] def test_column_count(self): self.assertEqual(len(self.table.columns), 3) def test_access_column_by_header(self): column = ["Jacob", "Isabella", "Ethan", "Sophia", "Michael"] self.compare_iterable(column, self.table.columns["name"]) with self.assertRaises(KeyError): self.table.columns["name1"] def test_access_column_element_by_index(self): self.assertEqual(self.table.columns[0][2], "Ethan") def test_access_column_element_by_header(self): self.assertEqual(self.table.columns["name"][2], "Ethan") def test_get_column_index(self): self.assertEqual(self.table.columns.header.index("gender"), 2) with self.assertRaises(KeyError): self.table.columns.header.index("rank1") def test_get_column_header(self): self.assertEqual(self.table.columns.header[2], "gender") with self.assertRaises(IndexError): self.table.columns.header[len(self.table.columns)] def test_append_column(self): title = "year" column = ["2010", "2012", "2008", "2010", "2011"] self.table.columns.append(column, title) self.assertEqual(len(self.table.columns), 4) last_column = self.table.columns[len(self.table.columns) - 1] self.compare_iterable(column, last_column) def test_append_column_empty_table(self): self.table = BeautifulTable() title = "year" column = ["2010", "2012", "2008", "2010", "2011"] self.table.columns.append(column, header=title) string = """+------+ | year | +------+ | 2010 | +------+ | 2012 | +------+ | 2008 | +------+ | 2010 | +------+ | 2011 | +------+""" self.assertEqual(string, str(self.table)) def test_insert_column(self): column = ["2010", "2012", "2008", "2010", "2011"] title = "year" position = 2 self.table.columns.insert(position, column, title) self.assertEqual(len(self.table.columns), 4) self.compare_iterable(column, self.table.columns[position]) def test_pop_column_by_position(self): position = 2 header = self.table.columns.header[position] self.table.columns.pop(position) self.assertEqual(len(self.table.columns), 2) with self.assertRaises(KeyError): self.table.columns[header] def test_pop_column_by_header(self): header = "gender" self.table.columns.pop(header) self.assertEqual(len(self.table.columns), 2) with self.assertRaises(KeyError): self.table.columns[header] def test_update_column_by_index(self): index = 1 column = [3, 2, 1, 2, 4] self.table.columns.update(index, column) self.assertEqual(len(self.table.columns), 3) self.compare_iterable(column, self.table.columns[index]) def test_update_column_by_header(self): header = "rank" column = [3, 2, 1, 2, 4] self.table.columns.update(header, column) self.assertEqual(len(self.table.columns), 3) self.compare_iterable(column, self.table.columns[header]) def test_update_column_slice(self): columns = [ [5, "girl"], [4, "boy"], [4, "boy"], [2, "girl"], [1, "boy"], ] self.table.columns.update(slice(1, 3, 1), columns) self.assertEqual(len(self.table.columns), 3) self.compare_iterable(self.table.columns[1], [c[0] for c in columns]) self.compare_iterable(self.table.columns[2], [c[1] for c in columns]) # Tests for row operations def test_row_asdict(self): self.assertEqual( [row.asdict() for row in self.table.rows], [ {"name": "Jacob", "rank": 1, "gender": "boy"}, {"name": "Isabella", "rank": 1, "gender": "girl"}, {"name": "Ethan", "rank": 2, "gender": "boy"}, {"name": "Sophia", "rank": 2, "gender": "girl"}, {"name": "Michael", "rank": 3, "gender": "boy"}, ], ) def test_row_aslist(self): self.assertEqual( [row.aslist() for row in self.table.rows], [ ["Jacob", 1, "boy"], ["Isabella", 1, "girl"], ["Ethan", 2, "boy"], ["Sophia", 2, "girl"], ["Michael", 3, "boy"], ], ) def test_row_count(self): self.assertEqual(len(self.table.rows), 5) def test_access_row_by_index(self): row = ["Sophia", 2, "girl"] self.compare_iterable(row, self.table.rows[3]) with self.assertRaises(IndexError): self.table.rows[len(self.table.rows)] def test_access_row_by_header(self): row = ["Sophia", 2, "girl"] self.compare_iterable(row, self.table.rows["S4"]) with self.assertRaises(IndexError): self.table.rows[len(self.table.rows)] def test_access_row_element_by_index(self): self.assertEqual(self.table.rows[2][0], "Ethan") def test_access_row_element_by_header(self): self.assertEqual(self.table.rows[2]["name"], "Ethan") def test_append_row(self): row = ["Gary", 2, "boy"] self.table.rows.append(row, header="S6") self.assertEqual(len(self.table.rows), 6) self.compare_iterable(self.table.rows[5], row) def test_insert_row(self): row = ["Gary", 2, "boy"] position = 2 self.table.rows.insert(position, row, header="S6") self.assertEqual(len(self.table.rows), 6) self.compare_iterable(self.table.rows[position], row) def test_pop_row_by_position(self): position = 2 self.table.rows.pop(position) self.assertEqual(len(self.table.rows), 4) def test_pop_row_by_header(self): header = "S3" self.table.rows.pop(header) self.assertEqual(len(self.table.rows), 4) def test_update_row_by_index(self): row = ["Sophie", 5, "girl"] position = 3 self.table.rows.update(position, row) self.compare_iterable(self.table.rows[position], row) def test_update_row_by_header(self): row = ["Sophie", 5, "girl"] header = "S4" self.table.rows.update(header, row) self.compare_iterable(self.table.rows[header], row) def test_update_row_slice(self): rows = [["Sophie", 5, "girl"], ["Mike", 4, "boy"]] self.table.rows.update(slice(3, 5, 1), rows) self.assertEqual(len(self.table.rows), 5) self.compare_iterable(self.table.rows[3], rows[0]) self.compare_iterable(self.table.rows[4], rows[1]) # Tests for special row methods def test_row_getitem_slice(self): new_table = self.table.rows[:3] self.assertEqual(len(new_table.rows), 3) self.assertEqual(len(self.table.rows), 5) def test_row_delitem_int(self): del self.table.rows[1] self.assertEqual(len(self.table.rows), 4) def test_row_delitem_slice(self): del self.table.rows[2:] self.assertEqual(len(self.table.rows), 2) def test_row_delitem_str(self): del self.table.rows["S2"] self.assertEqual(len(self.table.rows), 4) with self.assertRaises(KeyError): self.table.rows["S2"] def test_row_setitem_int(self): position = 3 row = ["Sophie", 5, "girl"] self.table.rows[position] = row self.compare_iterable(self.table.rows[position], row) def test_row_setitem_slice(self): rows = [["Sophie", 5, "girl"], ["Mike", 4, "boy"]] self.table.rows[3:] = rows self.assertEqual(len(self.table.rows), 5) self.compare_iterable(self.table.rows[3], rows[0]) self.compare_iterable(self.table.rows[4], rows[1]) def test_row_setitem_str(self): header = "S2" row = ["Mike", 4, "Boy"] self.table.rows[header] = row self.assertEqual(len(self.table.rows), 5) self.compare_iterable(row, self.table.rows[header]) def test_row_contains(self): self.assertTrue(["Isabella", 1, "girl"] in self.table.rows) self.assertFalse(["Ethan", 3, "boy"] in self.table.rows) def test_row_header_contains(self): self.assertTrue("S3" in self.table.rows.header) self.assertFalse("S6" in self.table.rows.header) # Test for special column methods def test_column_getitem_slice(self): new_table = self.table.columns[:2] self.assertEqual(len(self.table.columns), 3) self.assertEqual(len(new_table.columns), 2) def test_column_delitem_int(self): del self.table.columns[1] self.assertEqual(len(self.table.columns), 2) def test_column_delitem_slice(self): del self.table.columns[2:] self.assertEqual(len(self.table.columns), 2) def test_column_delitem_str(self): del self.table.columns["rank"] self.assertEqual(len(self.table.columns), 2) with self.assertRaises(KeyError): self.table.columns["rank"] def test_column_setitem_int(self): position = 2 row = [3, 4, 5, 6, 7] self.table.columns[position] = row self.compare_iterable(self.table.columns[position], row) def test_column_setitem_slice(self): columns = [ [5, "girl"], [4, "boy"], [4, "boy"], [2, "girl"], [1, "boy"], ] self.table.columns[1:] = columns self.assertEqual(len(self.table.columns), 3) self.compare_iterable(self.table.columns[1], [c[0] for c in columns]) self.compare_iterable(self.table.columns[2], [c[1] for c in columns]) def test_column_setitem_str(self): header = "rank" column = [3, 2, 1, 2, 4] self.table.columns[header] = column self.assertEqual(len(self.table.columns), 3) self.compare_iterable(column, self.table.columns[header]) def test_column_header_contains(self): self.assertTrue("rank" in self.table.columns.header) self.assertFalse("score" in self.table.columns.header) def test_column_contains(self): self.assertTrue(["boy", "girl", "boy", "girl", "boy"] in self.table.columns) self.assertFalse(["boy", "girl", "girl", "girl", "boy"] in self.table.columns) # Test for printing operations def test_get_string(self): string = """+----+----------+------+--------+ | | name | rank | gender | +----+----------+------+--------+ | S1 | Jacob | 1 | boy | +----+----------+------+--------+ | S2 | Isabella | 1 | girl | +----+----------+------+--------+ | S3 | Ethan | 2 | boy | +----+----------+------+--------+ | S4 | Sophia | 2 | girl | +----+----------+------+--------+ | S5 | Michael | 3 | boy | +----+----------+------+--------+""" self.assertEqual(string, str(self.table)) def test_stream(self): def generator(): for i in range(1, 6): yield [i, i**2] table = BeautifulTable() table.columns.header = ["Number", "It's Square"] self.compare_iterable( table.stream(generator()), [ "+--------+-------------+", "| Number | It's Square |", "+--------+-------------+", "| 1 | 1 |", "+--------+-------------+", "| 2 | 4 |", "+--------+-------------+", "| 3 | 9 |", "+--------+-------------+", "| 4 | 16 |", "+--------+-------------+", "| 5 | 25 |", "+--------+-------------+", ], ) def test_left_align(self): self.table.columns.alignment[0] = self.table.ALIGN_LEFT string = """+----+----------+------+--------+ | | name | rank | gender | +----+----------+------+--------+ | S1 | Jacob | 1 | boy | +----+----------+------+--------+ | S2 | Isabella | 1 | girl | +----+----------+------+--------+ | S3 | Ethan | 2 | boy | +----+----------+------+--------+ | S4 | Sophia | 2 | girl | +----+----------+------+--------+ | S5 | Michael | 3 | boy | +----+----------+------+--------+""" self.assertEqual(string, str(self.table)) def test_right_align(self): self.table.columns.alignment[0] = self.table.ALIGN_RIGHT string = """+----+----------+------+--------+ | | name | rank | gender | +----+----------+------+--------+ | S1 | Jacob | 1 | boy | +----+----------+------+--------+ | S2 | Isabella | 1 | girl | +----+----------+------+--------+ | S3 | Ethan | 2 | boy | +----+----------+------+--------+ | S4 | Sophia | 2 | girl | +----+----------+------+--------+ | S5 | Michael | 3 | boy | +----+----------+------+--------+""" self.assertEqual(string, str(self.table)) def test_mixed_align(self): self.table.columns.alignment = [ self.table.ALIGN_LEFT, self.table.ALIGN_CENTER, self.table.ALIGN_RIGHT, ] string = """+----+----------+------+--------+ | | name | rank | gender | +----+----------+------+--------+ | S1 | Jacob | 1 | boy | +----+----------+------+--------+ | S2 | Isabella | 1 | girl | +----+----------+------+--------+ | S3 | Ethan | 2 | boy | +----+----------+------+--------+ | S4 | Sophia | 2 | girl | +----+----------+------+--------+ | S5 | Michael | 3 | boy | +----+----------+------+--------+""" self.assertEqual(string, str(self.table)) def test_align_all(self): self.table.columns.alignment = self.table.ALIGN_LEFT string = """+----+----------+------+--------+ | | name | rank | gender | +----+----------+------+--------+ | S1 | Jacob | 1 | boy | +----+----------+------+--------+ | S2 | Isabella | 1 | girl | +----+----------+------+--------+ | S3 | Ethan | 2 | boy | +----+----------+------+--------+ | S4 | Sophia | 2 | girl | +----+----------+------+--------+ | S5 | Michael | 3 | boy | +----+----------+------+--------+""" self.assertEqual(string, str(self.table)) def test_sign_plus(self): self.table.sign = self.table.SM_PLUS string = """+----+----------+------+--------+ | | name | rank | gender | +----+----------+------+--------+ | S1 | Jacob | +1 | boy | +----+----------+------+--------+ | S2 | Isabella | +1 | girl | +----+----------+------+--------+ | S3 | Ethan | +2 | boy | +----+----------+------+--------+ | S4 | Sophia | +2 | girl | +----+----------+------+--------+ | S5 | Michael | +3 | boy | +----+----------+------+--------+""" self.assertEqual(string, str(self.table)) def test_wep_wrap(self): self.create_table(20) self.table.columns.width_exceed_policy = self.table.WEP_WRAP string = """+---+------+---+---+ | | name | r | g | | | | a | e | | | | n | n | | | | k | d | | | | | e | | | | | r | +---+------+---+---+ | S | Jaco | 1 | b | | 1 | b | | o | | | | | y | +---+------+---+---+ | S | Isab | 1 | g | | 2 | ella | | i | | | | | r | | | | | l | +---+------+---+---+ | S | Etha | 2 | b | | 3 | n | | o | | | | | y | +---+------+---+---+ | S | Soph | 2 | g | | 4 | ia | | i | | | | | r | | | | | l | +---+------+---+---+ | S | Mich | 3 | b | | 5 | ael | | o | | | | | y | +---+------+---+---+""" self.assertEqual(string, str(self.table)) def test_wep_strip(self): self.create_table(20) self.table.columns.width_exceed_policy = self.table.WEP_STRIP string = """+---+------+---+---+ | | name | r | g | +---+------+---+---+ | S | Jaco | 1 | b | +---+------+---+---+ | S | Isab | 1 | g | +---+------+---+---+ | S | Etha | 2 | b | +---+------+---+---+ | S | Soph | 2 | g | +---+------+---+---+ | S | Mich | 3 | b | +---+------+---+---+""" self.assertEqual(string, str(self.table)) def test_wep_ellipsis(self): self.create_table(20) self.table.columns.width_exceed_policy = self.table.WEP_ELLIPSIS string = """+---+------+---+---+ | | name | . | . | +---+------+---+---+ | . | J... | 1 | . | +---+------+---+---+ | . | I... | 1 | . | +---+------+---+---+ | . | E... | 2 | . | +---+------+---+---+ | . | S... | 2 | . | +---+------+---+---+ | . | M... | 3 | . | +---+------+---+---+""" self.assertEqual(string, str(self.table)) def test_empty_header(self): self.table.columns.header = ["", " ", " "] string = """+----+----------+---+------+ | S1 | Jacob | 1 | boy | +----+----------+---+------+ | S2 | Isabella | 1 | girl | +----+----------+---+------+ | S3 | Ethan | 2 | boy | +----+----------+---+------+ | S4 | Sophia | 2 | girl | +----+----------+---+------+ | S5 | Michael | 3 | boy | +----+----------+---+------+""" self.assertEqual(string, str(self.table)) def test_eastasian_characters(self): string = """+----+------------+------+--------+ | | name | rank | gender | +----+------------+------+--------+ | S1 | Jacob | 1 | boy | +----+------------+------+--------+ | S2 | Isabella | 1 | girl | +----+------------+------+--------+ | S3 | Ethan | 2 | boy | +----+------------+------+--------+ | S4 | Sophia | 2 | girl | +----+------------+------+--------+ | S5 | Michael | 3 | boy | +----+------------+------+--------+ | S6 | こんにちは | 2 | boy | +----+------------+------+--------+""" self.table.rows.append(["こんにちは", 2, "boy"], header="S6") self.assertEqual(string, str(self.table)) def test_newline(self): string = """+---+---+ | 0 | a | | | b | +---+---+""" table = BeautifulTable() table.rows.append(["0", "a\nb"]) self.assertEqual(string, str(table)) def test_newline_multiple_columns(self): string = """+---+---+ | a | p | | b | q | | c | | +---+---+""" table = BeautifulTable() table.rows.append(["a\nb\nc", "p\nq"]) self.assertEqual(string, str(table)) # Test for ANSI sequences def test_ansi_sequences(self): table = BeautifulTable() string = """+------+---+-----+ | \x1b[31mAdam\x1b[0m | 2 | boy | +------+---+-----+""" table.rows.append(["\x1b[31mAdam\x1b[0m", 2, "boy"]) self.assertEqual(string, str(table)) def test_ansi_wrap(self): table = BeautifulTable(maxwidth=30) string = """+-----------------+---+------+ | \x1b[31mThis is a very \x1b[0m | 2 | girl | | \x1b[32mlong name\x1b[0m | | | +-----------------+---+------+""" long_string = "\x1b[31mThis is a very \x1b[0m\x1b[32mlong name\x1b[0m" table.rows.append([long_string, 2, "girl"]) self.assertEqual(string, str(table)) def test_ansi_wrap_mb(self): table = BeautifulTable(maxwidth=30) string = """+-----------------+---+------+ | \x1b[31mこれは非常に長\x1b[0m | 2 | girl | | \x1b[31mい\x1b[0m\x1b[32m名前です\x1b[0m | | | +-----------------+---+------+""" long_string = "\x1b[31mこれは非常に長い\x1b[0m\x1b[32m名前です\x1b[0m" table.rows.append([long_string, 2, "girl"]) self.assertEqual(string, str(table)) def test_ansi_ellipsis(self): table = BeautifulTable(maxwidth=30) table.columns.width_exceed_policy = table.WEP_ELLIPSIS string = """+-----------------+---+------+ | \x1b[31mThis is a ve\x1b[0m... | 2 | girl | +-----------------+---+------+""" long_string = "\x1b[31mThis is a very \x1b[0m\x1b[32mlong name\x1b[0m" table.rows.append([long_string, 2, "girl"]) self.assertEqual(string, str(table)) def test_ansi_ellipsis_mb(self): table = BeautifulTable(maxwidth=30) table.columns.width_exceed_policy = table.WEP_ELLIPSIS string = """+-----------------+---+------+ | \x1b[31mこれは非常に\x1b[0m... | 2 | girl | +-----------------+---+------+""" long_string = "\x1b[31mこれは非常に長い\x1b[0m\x1b[32m名前です\x1b[0m" table.rows.append([long_string, 2, "girl"]) self.assertEqual(string, str(table)) def test_ansi_strip(self): table = BeautifulTable(maxwidth=30) table.columns.width_exceed_policy = table.WEP_STRIP string = """+-----------------+---+------+ | \x1b[31mThis is a very \x1b[0m | 2 | girl | +-----------------+---+------+""" long_string = "\x1b[31mThis is a very \x1b[0m\x1b[32mlong name\x1b[0m" table.rows.append([long_string, 2, "girl"]) self.assertEqual(string, str(table)) def test_ansi_strip_mb(self): table = BeautifulTable(maxwidth=30) table.columns.width_exceed_policy = table.WEP_STRIP string = """+-----------------+---+------+ | \x1b[31mこれは非常に長\x1b[0m | 2 | girl | +-----------------+---+------+""" long_string = "\x1b[31mこれは非常に長い\x1b[0m\x1b[32m名前です\x1b[0m" table.rows.append([long_string, 2, "girl"]) self.assertEqual(string, str(table)) # Test on empty table def test_empty_table_by_column(self): self.create_table(20) for i in range(3): self.table.columns.pop() self.assertEqual(str(self.table), "") def test_empty_table_by_row(self): self.create_table(20) for i in range(5): self.table.rows.pop() self.assertEqual(str(self.table), "") def test_table_width_zero(self): self.create_table(20) self.table.clear(True) self.assertEqual(self.table._width, 0) def test_table_auto_width(self): row_list = ["abcdefghijklmopqrstuvwxyz", 1234, "none"] self.create_table(200) self.table.rows.append(row_list) len_for_max_width_200 = len(str(self.table)) self.create_table(80) self.table.rows.append(row_list) len_for_max_width_80 = len(str(self.table)) self.assertEqual(len_for_max_width_80, len_for_max_width_200) def test_csv_export(self): # Create csv files in path. self.table.to_csv("beautiful_table.csv") self.table.to_csv("./docs/beautiful_table.csv") with self.assertRaises(ValueError): self.table.to_csv(1) # Check if csv files exist. self.assertTrue(os.path.exists("beautiful_table.csv")) self.assertTrue(os.path.exists("./docs/beautiful_table.csv")) # Teardown step. os.remove("beautiful_table.csv") os.remove("./docs/beautiful_table.csv") def test_csv_import(self): # Export table as CSV file and import it back. self.table.to_csv("beautiful_table.csv") test_table = BeautifulTable() test_table.from_csv("beautiful_table.csv") with self.assertRaises(ValueError): self.table.from_csv(1) self.assertEqual(len(self.table.rows), len(test_table.rows)) self.assertEqual(self.table.columns.header, test_table.columns.header) test_table = BeautifulTable() test_table.from_csv("beautiful_table.csv", header=False) self.assertEqual(len(self.table.rows), len(test_table.rows) - 1) # Teardown step. os.remove("beautiful_table.csv") @unittest.skipUnless(PANDAS_INSTALLED, REQUIRED_PANDAS_MESSAGE) def test_df_export(self): df = self.table.to_df() self.assertEqual(self.table.rows.header, df.index) self.assertEqual(self.table.columns.header, list(df.columns)) self.assertEqual( [list(row) for row in list(df.values)], [list(row) for row in list(self.table._data)], ) @unittest.skipUnless(PANDAS_INSTALLED, REQUIRED_PANDAS_MESSAGE) def test_df_import(self): df = self.create_dataframe() table = BeautifulTable() table = table.from_df(df) self.assertEqual(table.rows.header, df.index) self.assertEqual(table.columns.header, list(df.columns)) self.assertEqual( [list(row) for row in list(df.values)], [list(row) for row in list(table.rows)], ) @unittest.skipUnless(PANDAS_INSTALLED, REQUIRED_PANDAS_MESSAGE) def test_df_export_scenario1(self): table = BeautifulTable() table.rows.append(["Jacob", 1, "boy"]) table.rows.append(["Isabella", 2, "girl"]) df = table.to_df() self.assertEqual(table.rows.header, [None, None]) self.assertEqual(table.columns.header, [None, None, None]) self.assertEqual(list(df.index), [0, 1]) self.assertEqual(list(df.columns), [0, 1, 2]) @unittest.skipUnless(PANDAS_INSTALLED, REQUIRED_PANDAS_MESSAGE) def test_df_export_scenario2(self): table = BeautifulTable() table.rows.append(["Jacob", 1, "boy"]) table.rows.append(["Isabella", 2, "girl"]) table.columns.header = [None, "rank", "gender"] df = table.to_df() self.assertEqual(table.rows.header, [None, None]) self.assertEqual(table.columns.header, [None, "rank", "gender"]) self.assertEqual(list(df.index), [0, 1]) self.assertEqual(list(df.columns), [None, "rank", "gender"]) if __name__ == "__main__": unittest.main()