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()