1
0
Fork 0

Adding upstream version 3.1.0.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-02-12 13:04:33 +01:00
parent ba59263e72
commit a35389c891
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
80 changed files with 5413 additions and 0 deletions

96
.gitignore vendored Normal file
View file

@ -0,0 +1,96 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# IPython Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# dotenv
.env
# virtualenv
venv/
ENV/
# Spyder project settings
.spyderproject
# Rope project settings
.ropeproject
# Robpol86
test*.png
*.rpm
.idea/
requirements*.txt
.DS_Store

48
.travis.yml Normal file
View file

@ -0,0 +1,48 @@
# Configure.
env: TOX_ENV=py
language: python
matrix:
include:
- python: 3.5
env: TOX_ENV=lint
after_success:
- echo
- python: 3.5
env: TOX_ENV=docs
after_success:
- eval "$(ssh-agent -s)"; touch docs/key; chmod 0600 docs/key
- openssl aes-256-cbc -d -K "$encrypted_c89fed6a587d_key" -iv "$encrypted_c89fed6a587d_iv"
< docs/key.enc > docs/key && ssh-add docs/key
- git config --global user.email "builds@travis-ci.com"
- git config --global user.name "Travis CI"
- git remote set-url --push origin "git@github.com:$TRAVIS_REPO_SLUG"
- export ${!TRAVIS*}
- tox -e docsV
python:
- 3.5
- 3.4
- 3.3
- pypy3
- pypy
- 2.7
- 2.6
sudo: false
# Run.
install: pip install tox
script: tox -e $TOX_ENV
after_success:
- bash <(curl -s https://codecov.io/bash)
# Deploy.
deploy:
provider: pypi
user: Robpol86
password:
secure:
"aj+Hl25+NbtmKpHcqxxNJhaMmawgzEPdLX+NwxwAZuTrvUCdiMtYhF9qxN0USHIlXSGDNc\
7ua6nNpYPhjRv7j5YM4uLlK+4Fv/iU+iQcVfy89BS4vlXzUoje6nLIhogsxytb+FjdGZ0PK\
JzzxfYr0relUjui/gPYmTQoZ1IiT8A="
on:
condition: $TRAVIS_PYTHON_VERSION = 3.4
tags: true

47
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,47 @@
# Contributing
Everyone that wants to contribute to the project should read this document.
## Getting Started
You may follow these steps if you wish to create a pull request. Fork the repo and clone it on your local machine. Then
in the project's directory:
```bash
virtualenv env # Create a virtualenv for the project's dependencies.
source env/bin/activate # Activate the virtualenv.
pip install tox # Install tox, which runs linting and tests.
tox # This runs all tests on your local machine. Make sure they pass.
```
If you don't have Python 2.6, 2.7, or 3.4 installed you can manually run tests on one specific version by running
`tox -e lint,py35` (for Python 3.5) instead.
## Updating Docs
You don't need to but if you wish to update the [Sphinx](http://sphinx-doc.org/) documentation for this project you can
get started by running these commands:
```bash
source env/bin/activate
pip install tox
tox -e docs
open docs/_build/html/index.html # Opens this file in your browser.
```
## Consistency and Style
Keep code style consistent with the rest of the project. Some suggestions:
1. **Write tests for your new features.** `if new_feature else` **Write tests for bug-causing scenarios.**
2. Write docstrings for all classes, functions, methods, modules, etc.
3. Document all function/method arguments and return values.
4. Document all class variables instance variables.
5. Documentation guidelines also apply to tests, though not as strict.
6. Keep code style consistent, such as the kind of quotes to use and spacing.
7. Don't use `except:` or `except Exception:` unless you have a `raise` in the block. Be specific about error handling.
8. Don't use `isinstance()` (it breaks [duck typing](https://en.wikipedia.org/wiki/Duck_typing#In_Python)).
## Thanks
Thanks for fixing bugs or adding features to the project!

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Robpol86
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.

162
README.rst Normal file
View file

@ -0,0 +1,162 @@
==============
terminaltables
==============
Easily draw tables in terminal/console applications from a list of lists of strings. Supports multi-line rows.
* Python 2.6, 2.7, PyPy, PyPy3, 3.3, 3.4, and 3.5 supported on Linux and OS X.
* Python 2.7, 3.3, 3.4, and 3.5 supported on Windows (both 32 and 64 bit versions of Python).
📖 Full documentation: https://robpol86.github.io/terminaltables
.. image:: https://img.shields.io/appveyor/ci/Robpol86/terminaltables/master.svg?style=flat-square&label=AppVeyor%20CI
:target: https://ci.appveyor.com/project/Robpol86/terminaltables
:alt: Build Status Windows
.. image:: https://img.shields.io/travis/Robpol86/terminaltables/master.svg?style=flat-square&label=Travis%20CI
:target: https://travis-ci.org/Robpol86/terminaltables
:alt: Build Status
.. image:: https://img.shields.io/codecov/c/github/Robpol86/terminaltables/master.svg?style=flat-square&label=Codecov
:target: https://codecov.io/gh/Robpol86/terminaltables
:alt: Coverage Status
.. image:: https://img.shields.io/pypi/v/terminaltables.svg?style=flat-square&label=Latest
:target: https://pypi.python.org/pypi/terminaltables
:alt: Latest Version
Quickstart
==========
Install:
.. code:: bash
pip install terminaltables
Usage:
.. code::
from terminaltables import AsciiTable
table_data = [
['Heading1', 'Heading2'],
['row1 column1', 'row1 column2'],
['row2 column1', 'row2 column2'],
['row3 column1', 'row3 column2']
]
table = AsciiTable(table_data)
print table.table
+--------------+--------------+
| Heading1 | Heading2 |
+--------------+--------------+
| row1 column1 | row1 column2 |
| row2 column1 | row2 column2 |
| row3 column1 | row3 column2 |
+--------------+--------------+
Example Implementations
=======================
.. image:: docs/examples.png?raw=true
:alt: Example Scripts Screenshot
Source code for examples: `example1.py <https://github.com/Robpol86/terminaltables/blob/master/example1.py>`_,
`example2.py <https://github.com/Robpol86/terminaltables/blob/master/example2.py>`_, and
`example3.py <https://github.com/Robpol86/terminaltables/blob/master/example3.py>`_
.. changelog-section-start
Changelog
=========
This project adheres to `Semantic Versioning <http://semver.org/>`_.
3.1.0 - 2016-10-16
------------------
Added
* ``git --porcelain``-like table by liiight: https://github.com/Robpol86/terminaltables/pull/31
3.0.0 - 2016-05-30
------------------
Added
* Support for https://pypi.python.org/pypi/colorama
* Support for https://pypi.python.org/pypi/termcolor
* Support for RTL characters (Arabic and Hebrew).
* Support for non-string items in ``table_data`` like integers.
Changed
* Refactored again, but this time entire project including tests.
Removed
* ``padded_table_data`` property and ``join_row()``. Moving away from repeated string joining/splitting.
Fixed
* ``set_terminal_title()`` Unicode handling on Windows.
* https://github.com/Robpol86/terminaltables/issues/18
* https://github.com/Robpol86/terminaltables/issues/20
* https://github.com/Robpol86/terminaltables/issues/23
* https://github.com/Robpol86/terminaltables/issues/26
2.1.0 - 2015-11-02
------------------
Added
* GitHub Flavored Markdown table by bcho: https://github.com/Robpol86/terminaltables/pull/12
* Python 3.5 support (Linux/OS X and Windows).
2.0.0 - 2015-10-11
------------------
Changed
* Refactored code. No new features.
* Breaking changes: ``UnixTable``/``WindowsTable``/``WindowsTableDouble`` moved. Use ``SingleTable``/``DoubleTable``
instead.
1.2.1 - 2015-09-03
------------------
Fixed
* CJK character width fixed by zqqf16 and bcho: https://github.com/Robpol86/terminaltables/pull/9
1.2.0 - 2015-05-31
------------------
Added
* Bottom row separator.
1.1.1 - 2014-11-03
------------------
Fixed
* Python 2.7 64-bit terminal width bug on Windows.
1.1.0 - 2014-11-02
------------------
Added
* Windows support.
* Double-lined table.
1.0.2 - 2014-09-18
------------------
Added
* ``table_width`` and ``ok`` properties.
1.0.1 - 2014-09-12
------------------
Added
* Terminal width/height defaults for testing.
* ``terminaltables.DEFAULT_TERMINAL_WIDTH``
* ``terminaltables.DEFAULT_TERMINAL_HEIGHT``
1.0.0 - 2014-09-11
------------------
* Initial release.
.. changelog-section-end

33
appveyor.yml Normal file
View file

@ -0,0 +1,33 @@
# Configure.
environment:
PYTHON: Python35
matrix:
- TOX_ENV: lint
- TOX_ENV: py35
- TOX_ENV: py34
- TOX_ENV: py33
- TOX_ENV: py27
- TOX_ENV: py
PYTHON: Python35-x64
- TOX_ENV: py
PYTHON: Python34-x64
- TOX_ENV: py
PYTHON: Python33-x64
- TOX_ENV: py
PYTHON: Python27-x64
# Run.
init: set PATH=C:\%PYTHON%;C:\%PYTHON%\Scripts;%PATH%
install:
- appveyor DownloadFile https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-desktop.ps1
- ps: .\enable-desktop
build_script: pip install tox
test_script: tox -e %TOX_ENV%
on_success: IF %TOX_ENV% NEQ lint pip install codecov & codecov
# Post.
# on_finish: https://github.com/Robpol86/terminaltables/issues/30
#- appveyor PushArtifact test_ascii_table.png https://github.com/Robpol86/terminaltables/issues/30
#- appveyor PushArtifact test_double_table.png https://github.com/Robpol86/terminaltables/issues/30
#- appveyor PushArtifact test_single_table.png https://github.com/Robpol86/terminaltables/issues/30
#- appveyor PushArtifact test_terminal_io.png https://github.com/Robpol86/terminaltables/issues/30

6
docs/_templates/layout.html vendored Normal file
View file

@ -0,0 +1,6 @@
{# From https://github.com/snide/sphinx_rtd_theme/issues/166 #}
{# Import the theme's layout. #}
{% extends "!layout.html" %}
{% set css_files = css_files + ['_static/pygments.css'] %}

BIN
docs/asciitable.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

16
docs/asciitable.rst Normal file
View file

@ -0,0 +1,16 @@
.. _asciitable:
==========
AsciiTable
==========
AsciiTable is the simplest table. It uses ``+``, ``|``, and ``-`` characters to build the borders.
.. image:: asciitable.png
:target: _images/asciitable.png
API
===
.. autoclass:: terminaltables.AsciiTable
:members: column_max_width, column_widths, ok, table_width, table

5
docs/changelog.rst Normal file
View file

@ -0,0 +1,5 @@
.. _changelog:
.. include:: ../README.rst
:start-after: changelog-section-start
:end-before: changelog-section-end

53
docs/conf.py Normal file
View file

@ -0,0 +1,53 @@
"""Sphinx configuration file."""
import os
import sys
import time
# General configuration.
sys.path.append(os.path.realpath(os.path.join(os.path.dirname(__file__), '..')))
author = '@Robpol86'
copyright = '{}, {}'.format(time.strftime('%Y'), author)
master_doc = 'index'
project = __import__('setup').NAME
pygments_style = 'friendly'
release = version = __import__('setup').VERSION
templates_path = ['_templates']
extensions = list()
# Options for HTML output.
html_context = dict(
conf_py_path='/docs/',
display_github=True,
github_repo=os.environ.get('TRAVIS_REPO_SLUG', '/' + project).split('/', 1)[1],
github_user=os.environ.get('TRAVIS_REPO_SLUG', 'robpol86/').split('/', 1)[0],
github_version=os.environ.get('TRAVIS_BRANCH', 'master'),
source_suffix='.rst',
)
html_copy_source = False
html_favicon = 'favicon.ico'
html_theme = 'sphinx_rtd_theme'
html_title = project
# autodoc
extensions.append('sphinx.ext.autodoc')
# extlinks.
extensions.append('sphinx.ext.extlinks')
extlinks = {'github': ('https://github.com/robpol86/{0}/blob/v{1}/%s'.format(project, version), '')}
# google analytics
extensions.append('sphinxcontrib.googleanalytics')
googleanalytics_id = 'UA-82627369-1'
# SCVersioning.
scv_banner_greatest_tag = True
scv_grm_exclude = ('.gitignore', '.nojekyll', 'README.rst')
scv_show_banner = True
scv_sort = ('semver', 'time')

BIN
docs/doubletable.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

33
docs/doubletable.rst Normal file
View file

@ -0,0 +1,33 @@
.. _doubletable:
===========
DoubleTable
===========
DoubleTable uses `box drawing characters`_ for table borders. On Windows terminaltables uses `code page 437`_
characters. However there is no equivalent character set for POSIX (Linux/OS X). Python automatically converts CP437
double-line box characters to Unicode and displays that instead.
.. image:: doubletable.png
:target: _images/doubletable.png
Gaps on Windows 10
==================
Like SingleTable the console on Windows 10 changed the default font face to ``Consolas``. This new font seems to show
gaps between lines. Switching the font back to ``Lucida Console`` eliminates the gaps.
Gaps on POSIX
=============
There is no easy trick for POSIX like there is on Windows. I can't seem to find out how to force terminals to eliminate
gaps vertically between Unicode characters.
API
===
.. autoclass:: terminaltables.DoubleTable
:members: column_max_width, column_widths, ok, table_width, table
.. _box drawing characters: https://en.wikipedia.org/wiki/Box-drawing_character
.. _code page 437: https://en.wikipedia.org/wiki/Code_page_437

BIN
docs/examples.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
docs/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
docs/githubtable.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

27
docs/githubtable.rst Normal file
View file

@ -0,0 +1,27 @@
.. _githubtable:
===========================
GithubFlavoredMarkdownTable
===========================
GithubFlavoredMarkdownTable was initially implemented bcho_. It produces a `GitHub Flavored Markdown`_ formatted table.
Because there are no outer table borders:
* Table titles are ignored.
* Border display toggles are also ignored.
.. image:: githubtable.png
:target: _images/githubtable.png
.. image:: githubtable_rendered.png
:target: _images/githubtable_rendered.png
API
===
.. autoclass:: terminaltables.GithubFlavoredMarkdownTable
:members: column_max_width, column_widths, ok, table_width, table
.. _bcho: https://github.com/Robpol86/terminaltables/pull/12
.. _GitHub Flavored Markdown: https://help.github.com/categories/writing-on-github

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

77
docs/index.rst Normal file
View file

@ -0,0 +1,77 @@
========================
terminaltables |version|
========================
Easily draw tables in terminal/console applications from a list of lists of strings. As easy as:
.. code-block:: pycon
>>> from terminaltables import AsciiTable
>>> table_data = [
... ['Heading1', 'Heading2'],
... ['row1 column1', 'row1 column2'],
... ['row2 column1', 'row2 column2'],
... ['row3 column1', 'row3 column2'],
... ]
>>> table = AsciiTable(table_data)
>>> print table.table
+--------------+--------------+
| Heading1 | Heading2 |
+--------------+--------------+
| row1 column1 | row1 column2 |
| row2 column1 | row2 column2 |
| row3 column1 | row3 column2 |
+--------------+--------------+
.. figure:: examples.png
:target: _images/examples.png
Windows 10, Windows XP, and OS X are also supported. View source: :github:`example1.py`, :github:`example2.py`,
:github:`example3.py`
Features
========
* Multi-line rows: add newlines to table cells and terminatables will handle the rest.
* Table titles: show a title embedded in the top border of the table.
* POSIX: Python 2.6, 2.7, PyPy, PyPy3, 3.3, 3.4, and 3.5 supported on Linux and OS X.
* Windows: Python 2.7, 3.3, 3.4, and 3.5 supported on Windows XP through 10.
* CJK: Wide Chinese/Japanese/Korean characters displayed correctly.
* RTL: Arabic and Hebrew characters aligned correctly.
* Alignment/Justification: Align individual columns left, center, or right.
* Colored text: colorclass_, colorama_, termcolor_, or just plain `ANSI escape codes`_.
Project Links
=============
* Documentation: https://robpol86.github.io/terminaltables
* Source code: https://github.com/Robpol86/terminaltables
* PyPI homepage: https://pypi.python.org/pypi/terminaltables
.. toctree::
:maxdepth: 2
:caption: General
install
quickstart
settings
.. toctree::
:maxdepth: 2
:caption: Table Styles
asciitable
singletable
doubletable
githubtable
.. toctree::
:maxdepth: 1
:caption: Appendix
changelog
.. _colorclass: https://github.com/Robpol86/colorclass
.. _colorama: https://github.com/tartley/colorama
.. _termcolor: https://pypi.python.org/pypi/termcolor
.. _ANSI escape codes: http://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/x329.html

38
docs/install.rst Normal file
View file

@ -0,0 +1,38 @@
.. _install:
============
Installation
============
Getting started is pretty simple. The first step is to install the library.
Pip Install
===========
The easiest way to get terminaltables is to use `pip <https://pip.pypa.io>`_. Simply run this command.
.. code-block:: bash
pip install terminaltables
Latest from GitHub
==================
You can also elect to install the latest bleeding-edge version by using pip to install directly from the GitHub
repository.
.. code-block:: bash
pip install git+https://github.com/Robpol86/terminaltables.git
Clone and Install
=================
Lastly you can also just clone the repo and install from it. Usually you only need to do this if you plan on
`contributing <https://github.com/Robpol86/terminaltables/blob/master/CONTRIBUTING.md>`_ to the project.
.. code-block:: bash
git clone https://github.com/Robpol86/terminaltables.git
cd terminaltables
python setup.py install

BIN
docs/key.enc Normal file

Binary file not shown.

110
docs/quickstart.rst Normal file
View file

@ -0,0 +1,110 @@
.. _quickstart:
==========
Quickstart
==========
This section will go over the basics of terminaltables.
Make sure that you've already :ref:`installed <install>` it.
Table with Default Settings
===========================
Let's begin by importing AsciiTable, which just uses ``+``, ``-``, and ``|`` characters.
.. code-block:: pycon
>>> from terminaltables import AsciiTable
Now let's define the table data in a variable called ``data``. We'll do it the long way by creating an empty list
representing the entire table. Then we'll add rows one by one. Each row is a list representing table cells.
.. code-block:: pycon
>>> data = []
>>> data.append(['Row one column one', 'Row one column two'])
>>> data.append(['Row two column one', 'Row two column two'])
>>> data.append(['Row three column one', 'Row three column two'])
Next we can use AsciiTable to format the table properly and then we can just print it. ``table.table`` gives you just
one long string with newline characters so you can easily print it.
.. code-block:: pycon
>>> table = AsciiTable(data)
>>> print table.table
+----------------------+----------------------+
| Row one column one | Row one column two |
+----------------------+----------------------+
| Row two column one | Row two column two |
| Row three column one | Row three column two |
+----------------------+----------------------+
By default the first row of the table is considered the heading. This can be turned off.
Changing Table Settings
=======================
There are more options available to change how your tables are formatted. Say your table doesn't really have a heading
row; all rows are just data.
.. code-block:: pycon
>>> table.inner_heading_row_border = False
>>> print table.table
+----------------------+----------------------+
| Row one column one | Row one column two |
| Row two column one | Row two column two |
| Row three column one | Row three column two |
+----------------------+----------------------+
Now you want to add a title to the table:
.. code-block:: pycon
>>> table.title = 'My Table'
>>> print table.table
+My Table--------------+----------------------+
| Row one column one | Row one column two |
| Row two column one | Row two column two |
| Row three column one | Row three column two |
+----------------------+----------------------+
Maybe you want lines in between all rows:
.. code-block:: pycon
>>> table.inner_row_border = True
>>> print table.table
+My Table--------------+----------------------+
| Row one column one | Row one column two |
+----------------------+----------------------+
| Row two column one | Row two column two |
+----------------------+----------------------+
| Row three column one | Row three column two |
+----------------------+----------------------+
There are many more settings available. You can find out more by reading the :ref:`settings` section. Each table style
pretty much shares the same settings but there are a few minor exceptions. Refer to each table style's documentation on
the sidebar.
Other Table Styles
==================
Terminaltables comes with a few other table styles than just ``AsciiTable``. All table styles more or less have the same
API.
.. code-block:: pycon
>>> from terminaltables import SingleTable
>>> table = SingleTable(data)
>>> print table.table
┌──────────────────────┬──────────────────────┐
│ Row one column one │ Row one column two │
├──────────────────────┼──────────────────────┤
│ Row two column one │ Row two column two │
│ Row three column one │ Row three column two │
└──────────────────────┴──────────────────────┘
You can find documentation for all table styles on the sidebar.

79
docs/settings.rst Normal file
View file

@ -0,0 +1,79 @@
.. _settings:
========
Settings
========
All tables (except :ref:`githubtable`) have the same settings to change the way the table is displayed. These attributes
are available after instantiation.
.. py:attribute:: Table.table_data
The actual table data to render. This must be a list (or tuple) of lists of strings. The outer list holds the rows
and the inner lists holds the cells (aka columns in that row).
Example:
.. code-block:: python
table.table_data = [
['Name', 'Color', 'Type'],
['Avocado', 'green', 'nut'],
['Tomato', 'red', 'fruit'],
['Lettuce', 'green', 'vegetable'],
]
.. py:attribute:: Table.title
Optional title to show within the top border of the table. This is ignored if None or a blank string.
.. py:attribute:: Table.inner_column_border
Toggles the column dividers. Set to **False** to disable these vertically dividing borders.
.. py:attribute:: Table.inner_footing_row_border
Show a horizontal dividing border before the last row. If **True** this defines the last row as the table footer.
.. py:attribute:: Table.inner_heading_row_border
Show a horizontal dividing border after the first row. If **False** this removes the border so the first row is no
longer considered a header row. It'll look just like any other row.
.. py:attribute:: Table.inner_row_border
If **True** terminaltables will show dividing borders between every row.
.. py:attribute:: Table.outer_border
Toggles the four outer borders. If **False** the top, left, right, and bottom borders will not be shown.
.. py:attribute:: Table.justify_columns
Aligns text in entire columns. The keys in this dict are column integers (0 for the first column) and the values
are either 'left', 'right', or 'center'. Left is the default.
Example:
.. code-block:: pycon
>>> table.justify_columns[0] = 'right' # Name column.
>>> table.justify_columns[1] = 'center' # Color column.
>>> print table.table
+---------+-------+-----------+
| Name | Color | Type |
+---------+-------+-----------+
| Avocado | green | nut |
| Tomato | red | fruit |
| Lettuce | green | vegetable |
+---------+-------+-----------+
.. py:attribute:: Table.padding_left
Number of spaces to pad on the left side of every cell. Default is **1**. Padding adds spacing between the cell text
and the column border.
.. py:attribute:: Table.padding_right
Number of spaces to pad on the right side of every cell. Default is **1**. Padding adds spacing between the cell
text and the column border.

BIN
docs/singletable.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

26
docs/singletable.rst Normal file
View file

@ -0,0 +1,26 @@
.. _singletable:
===========
SingleTable
===========
SingleTable uses `box drawing characters`_ for table borders. On POSIX (Linux/OS X) terminaltables uses ``Esc ( 0``
characters while on Windows it uses `code page 437`_ characters.
.. image:: singletable.png
:target: _images/singletable.png
Gaps on Windows 10
==================
Unfortunately the console on Windows 10 changed the default font face to ``Consolas``. This new font seems to show gaps
between lines. Switching the font back to ``Lucida Console`` eliminates the gaps.
API
===
.. autoclass:: terminaltables.SingleTable
:members: column_max_width, column_widths, ok, table_width, table
.. _box drawing characters: https://en.wikipedia.org/wiki/Box-drawing_character
.. _code page 437: https://en.wikipedia.org/wiki/Code_page_437

42
example1.py Executable file
View file

@ -0,0 +1,42 @@
#!/usr/bin/env python
"""Simple example usage of terminaltables without any other dependencies.
Just prints sample text and exits.
"""
from __future__ import print_function
from terminaltables import AsciiTable, DoubleTable, SingleTable
TABLE_DATA = (
('Platform', 'Years', 'Notes'),
('Mk5', '2007-2009', 'The Golf Mk5 Variant was\nintroduced in 2007.'),
('MKVI', '2009-2013', 'Might actually be Mk5.'),
)
def main():
"""Main function."""
title = 'Jetta SportWagen'
# AsciiTable.
table_instance = AsciiTable(TABLE_DATA, title)
table_instance.justify_columns[2] = 'right'
print(table_instance.table)
print()
# SingleTable.
table_instance = SingleTable(TABLE_DATA, title)
table_instance.justify_columns[2] = 'right'
print(table_instance.table)
print()
# DoubleTable.
table_instance = DoubleTable(TABLE_DATA, title)
table_instance.justify_columns[2] = 'right'
print(table_instance.table)
print()
if __name__ == '__main__':
main()

86
example2.py Executable file
View file

@ -0,0 +1,86 @@
#!/usr/bin/env python
"""Example usage of terminaltables with colorclass.
Just prints sample text and exits.
"""
from __future__ import print_function
from colorclass import Color, Windows
from terminaltables import SingleTable
def table_server_timings():
"""Return table string to be printed."""
table_data = [
[Color('{autogreen}<10ms{/autogreen}'), '192.168.0.100, 192.168.0.101'],
[Color('{autoyellow}10ms <= 100ms{/autoyellow}'), '192.168.0.102, 192.168.0.103'],
[Color('{autored}>100ms{/autored}'), '192.168.0.105'],
]
table_instance = SingleTable(table_data)
table_instance.inner_heading_row_border = False
return table_instance.table
def table_server_status():
"""Return table string to be printed."""
table_data = [
[Color('Low Space'), Color('{autocyan}Nominal Space{/autocyan}'), Color('Excessive Space')],
[Color('Low Load'), Color('Nominal Load'), Color('{autored}High Load{/autored}')],
[Color('{autocyan}Low Free RAM{/autocyan}'), Color('Nominal Free RAM'), Color('High Free RAM')],
]
table_instance = SingleTable(table_data, '192.168.0.105')
table_instance.inner_heading_row_border = False
table_instance.inner_row_border = True
table_instance.justify_columns = {0: 'center', 1: 'center', 2: 'center'}
return table_instance.table
def table_abcd():
"""Return table string to be printed. Two tables on one line."""
table_instance = SingleTable([['A', 'B'], ['C', 'D']])
# Get first table lines.
table_instance.outer_border = False
table_inner_borders = table_instance.table.splitlines()
# Get second table lines.
table_instance.outer_border = True
table_instance.inner_heading_row_border = False
table_instance.inner_column_border = False
table_outer_borders = table_instance.table.splitlines()
# Combine.
smallest, largest = sorted([table_inner_borders, table_outer_borders], key=len)
smallest += [''] * (len(largest) - len(smallest)) # Make both same size.
combined = list()
for i, row in enumerate(largest):
combined.append(row.ljust(10) + ' ' + smallest[i])
return '\n'.join(combined)
def main():
"""Main function."""
Windows.enable(auto_colors=True, reset_atexit=True) # Does nothing if not on Windows.
# Server timings.
print(table_server_timings())
print()
# Server status.
print(table_server_status())
print()
# Two A B C D tables.
print(table_abcd())
print()
# Instructions.
table_instance = SingleTable([['Obey Obey Obey Obey']], 'Instructions')
print(table_instance.table)
print()
if __name__ == '__main__':
main()

36
example3.py Executable file
View file

@ -0,0 +1,36 @@
#!/usr/bin/env python
"""Simple example usage of terminaltables and column_max_width().
Just prints sample text and exits.
"""
from __future__ import print_function
from textwrap import wrap
from terminaltables import SingleTable
LONG_STRING = ('Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore '
'et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut '
'aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum '
'dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui '
'officia deserunt mollit anim id est laborum.')
def main():
"""Main function."""
table_data = [
['Long String', ''], # One row. Two columns. Long string will replace this empty string.
]
table = SingleTable(table_data)
# Calculate newlines.
max_width = table.column_max_width(1)
wrapped_string = '\n'.join(wrap(LONG_STRING, max_width))
table.table_data[0][1] = wrapped_string
print(table.table)
if __name__ == '__main__':
main()

111
setup.py Executable file
View file

@ -0,0 +1,111 @@
#!/usr/bin/env python
"""Setup script for the project."""
from __future__ import print_function
import codecs
import os
import re
from setuptools import Command, setup
INSTALL_REQUIRES = []
LICENSE = 'MIT'
NAME = IMPORT = 'terminaltables'
VERSION = '3.1.0'
def readme(path='README.rst'):
"""Try to read README.rst or return empty string if failed.
:param str path: Path to README file.
:return: File contents.
:rtype: str
"""
path = os.path.realpath(os.path.join(os.path.dirname(__file__), path))
handle = None
url_prefix = 'https://raw.githubusercontent.com/Robpol86/{name}/v{version}/'.format(name=NAME, version=VERSION)
try:
handle = codecs.open(path, encoding='utf-8')
return handle.read(131072).replace('.. image:: docs', '.. image:: {0}docs'.format(url_prefix))
except IOError:
return ''
finally:
getattr(handle, 'close', lambda: None)()
class CheckVersion(Command):
"""Make sure version strings and other metadata match here, in module/package, tox, and other places."""
description = 'verify consistent version/etc strings in project'
user_options = []
@classmethod
def initialize_options(cls):
"""Required by distutils."""
pass
@classmethod
def finalize_options(cls):
"""Required by distutils."""
pass
@classmethod
def run(cls):
"""Check variables."""
project = __import__(IMPORT, fromlist=[''])
for expected, var in [('@Robpol86', '__author__'), (LICENSE, '__license__'), (VERSION, '__version__')]:
if getattr(project, var) != expected:
raise SystemExit('Mismatch: {0}'.format(var))
# Check changelog.
if not re.compile(r'^%s - \d{4}-\d{2}-\d{2}[\r\n]' % VERSION, re.MULTILINE).search(readme()):
raise SystemExit('Version not found in readme/changelog file.')
# Check tox.
if INSTALL_REQUIRES:
contents = readme('tox.ini')
section = re.compile(r'[\r\n]+install_requires =[\r\n]+(.+?)[\r\n]+\w', re.DOTALL).findall(contents)
if not section:
raise SystemExit('Missing install_requires section in tox.ini.')
in_tox = re.findall(r' ([^=]+)==[\w\d.-]+', section[0])
if INSTALL_REQUIRES != in_tox:
raise SystemExit('Missing/unordered pinned dependencies in tox.ini.')
if __name__ == '__main__':
setup(
author='@Robpol86',
author_email='robpol86@gmail.com',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
'Environment :: MacOS X',
'Environment :: Win32 (MS Windows)',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: MacOS :: MacOS X',
'Operating System :: Microsoft :: Windows',
'Operating System :: POSIX :: Linux',
'Operating System :: POSIX',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Software Development :: Libraries',
'Topic :: Terminals',
'Topic :: Text Processing :: Markup',
],
cmdclass=dict(check_version=CheckVersion),
description='Generate simple tables in terminals from a nested list of strings.',
install_requires=INSTALL_REQUIRES,
keywords='Shell Bash ANSI ASCII terminal tables',
license=LICENSE,
long_description=readme(),
name=NAME,
packages=[IMPORT],
url='https://github.com/Robpol86/' + NAME,
version=VERSION,
zip_safe=True,
)

View file

@ -0,0 +1,17 @@
"""Generate simple tables in terminals from a nested list of strings.
Use SingleTable or DoubleTable instead of AsciiTable for box-drawing characters.
https://github.com/Robpol86/terminaltables
https://pypi.python.org/pypi/terminaltables
"""
from terminaltables.ascii_table import AsciiTable # noqa
from terminaltables.github_table import GithubFlavoredMarkdownTable # noqa
from terminaltables.other_tables import DoubleTable # noqa
from terminaltables.other_tables import SingleTable # noqa
from terminaltables.other_tables import PorcelainTable # noqa
__author__ = '@Robpol86'
__license__ = 'MIT'
__version__ = '3.1.0'

View file

@ -0,0 +1,55 @@
"""AsciiTable is the main table class. To be inherited by other tables. Define convenience methods here."""
from terminaltables.base_table import BaseTable
from terminaltables.terminal_io import terminal_size
from terminaltables.width_and_alignment import column_max_width, max_dimensions, table_width
class AsciiTable(BaseTable):
"""Draw a table using regular ASCII characters, such as ``+``, ``|``, and ``-``.
:ivar iter table_data: List (empty or list of lists of strings) representing the table.
:ivar str title: Optional title to show within the top border of the table.
:ivar bool inner_column_border: Separates columns.
:ivar bool inner_footing_row_border: Show a border before the last row.
:ivar bool inner_heading_row_border: Show a border after the first row.
:ivar bool inner_row_border: Show a border in between every row.
:ivar bool outer_border: Show the top, left, right, and bottom border.
:ivar dict justify_columns: Horizontal justification. Keys are column indexes (int). Values are right/left/center.
:ivar int padding_left: Number of spaces to pad on the left side of every cell.
:ivar int padding_right: Number of spaces to pad on the right side of every cell.
"""
def column_max_width(self, column_number):
"""Return the maximum width of a column based on the current terminal width.
:param int column_number: The column number to query.
:return: The max width of the column.
:rtype: int
"""
inner_widths = max_dimensions(self.table_data)[0]
outer_border = 2 if self.outer_border else 0
inner_border = 1 if self.inner_column_border else 0
padding = self.padding_left + self.padding_right
return column_max_width(inner_widths, column_number, outer_border, inner_border, padding)
@property
def column_widths(self):
"""Return a list of integers representing the widths of each table column without padding."""
if not self.table_data:
return list()
return max_dimensions(self.table_data)[0]
@property
def ok(self): # Too late to change API. # pylint: disable=invalid-name
"""Return True if the table fits within the terminal width, False if the table breaks."""
return self.table_width <= terminal_size()[0]
@property
def table_width(self):
"""Return the width of the table including padding and borders."""
outer_widths = max_dimensions(self.table_data, self.padding_left, self.padding_right)[2]
outer_border = 2 if self.outer_border else 0
inner_border = 1 if self.inner_column_border else 0
return table_width(outer_widths, outer_border, inner_border)

View file

@ -0,0 +1,217 @@
"""Base table class. Define just the bare minimum to build tables."""
from terminaltables.build import build_border, build_row, flatten
from terminaltables.width_and_alignment import align_and_pad_cell, max_dimensions
class BaseTable(object):
"""Base table class.
:ivar iter table_data: List (empty or list of lists of strings) representing the table.
:ivar str title: Optional title to show within the top border of the table.
:ivar bool inner_column_border: Separates columns.
:ivar bool inner_footing_row_border: Show a border before the last row.
:ivar bool inner_heading_row_border: Show a border after the first row.
:ivar bool inner_row_border: Show a border in between every row.
:ivar bool outer_border: Show the top, left, right, and bottom border.
:ivar dict justify_columns: Horizontal justification. Keys are column indexes (int). Values are right/left/center.
:ivar int padding_left: Number of spaces to pad on the left side of every cell.
:ivar int padding_right: Number of spaces to pad on the right side of every cell.
"""
CHAR_F_INNER_HORIZONTAL = '-'
CHAR_F_INNER_INTERSECT = '+'
CHAR_F_INNER_VERTICAL = '|'
CHAR_F_OUTER_LEFT_INTERSECT = '+'
CHAR_F_OUTER_LEFT_VERTICAL = '|'
CHAR_F_OUTER_RIGHT_INTERSECT = '+'
CHAR_F_OUTER_RIGHT_VERTICAL = '|'
CHAR_H_INNER_HORIZONTAL = '-'
CHAR_H_INNER_INTERSECT = '+'
CHAR_H_INNER_VERTICAL = '|'
CHAR_H_OUTER_LEFT_INTERSECT = '+'
CHAR_H_OUTER_LEFT_VERTICAL = '|'
CHAR_H_OUTER_RIGHT_INTERSECT = '+'
CHAR_H_OUTER_RIGHT_VERTICAL = '|'
CHAR_INNER_HORIZONTAL = '-'
CHAR_INNER_INTERSECT = '+'
CHAR_INNER_VERTICAL = '|'
CHAR_OUTER_BOTTOM_HORIZONTAL = '-'
CHAR_OUTER_BOTTOM_INTERSECT = '+'
CHAR_OUTER_BOTTOM_LEFT = '+'
CHAR_OUTER_BOTTOM_RIGHT = '+'
CHAR_OUTER_LEFT_INTERSECT = '+'
CHAR_OUTER_LEFT_VERTICAL = '|'
CHAR_OUTER_RIGHT_INTERSECT = '+'
CHAR_OUTER_RIGHT_VERTICAL = '|'
CHAR_OUTER_TOP_HORIZONTAL = '-'
CHAR_OUTER_TOP_INTERSECT = '+'
CHAR_OUTER_TOP_LEFT = '+'
CHAR_OUTER_TOP_RIGHT = '+'
def __init__(self, table_data, title=None):
"""Constructor.
:param iter table_data: List (empty or list of lists of strings) representing the table.
:param title: Optional title to show within the top border of the table.
"""
self.table_data = table_data
self.title = title
self.inner_column_border = True
self.inner_footing_row_border = False
self.inner_heading_row_border = True
self.inner_row_border = False
self.outer_border = True
self.justify_columns = dict() # {0: 'right', 1: 'left', 2: 'center'}
self.padding_left = 1
self.padding_right = 1
def horizontal_border(self, style, outer_widths):
"""Build any kind of horizontal border for the table.
:param str style: Type of border to return.
:param iter outer_widths: List of widths (with padding) for each column.
:return: Prepared border as a tuple of strings.
:rtype: tuple
"""
if style == 'top':
horizontal = self.CHAR_OUTER_TOP_HORIZONTAL
left = self.CHAR_OUTER_TOP_LEFT
intersect = self.CHAR_OUTER_TOP_INTERSECT if self.inner_column_border else ''
right = self.CHAR_OUTER_TOP_RIGHT
title = self.title
elif style == 'bottom':
horizontal = self.CHAR_OUTER_BOTTOM_HORIZONTAL
left = self.CHAR_OUTER_BOTTOM_LEFT
intersect = self.CHAR_OUTER_BOTTOM_INTERSECT if self.inner_column_border else ''
right = self.CHAR_OUTER_BOTTOM_RIGHT
title = None
elif style == 'heading':
horizontal = self.CHAR_H_INNER_HORIZONTAL
left = self.CHAR_H_OUTER_LEFT_INTERSECT if self.outer_border else ''
intersect = self.CHAR_H_INNER_INTERSECT if self.inner_column_border else ''
right = self.CHAR_H_OUTER_RIGHT_INTERSECT if self.outer_border else ''
title = None
elif style == 'footing':
horizontal = self.CHAR_F_INNER_HORIZONTAL
left = self.CHAR_F_OUTER_LEFT_INTERSECT if self.outer_border else ''
intersect = self.CHAR_F_INNER_INTERSECT if self.inner_column_border else ''
right = self.CHAR_F_OUTER_RIGHT_INTERSECT if self.outer_border else ''
title = None
else:
horizontal = self.CHAR_INNER_HORIZONTAL
left = self.CHAR_OUTER_LEFT_INTERSECT if self.outer_border else ''
intersect = self.CHAR_INNER_INTERSECT if self.inner_column_border else ''
right = self.CHAR_OUTER_RIGHT_INTERSECT if self.outer_border else ''
title = None
return build_border(outer_widths, horizontal, left, intersect, right, title)
def gen_row_lines(self, row, style, inner_widths, height):
r"""Combine cells in row and group them into lines with vertical borders.
Caller is expected to pass yielded lines to ''.join() to combine them into a printable line. Caller must append
newline character to the end of joined line.
In:
['Row One Column One', 'Two', 'Three']
Out:
[
('|', ' Row One Column One ', '|', ' Two ', '|', ' Three ', '|'),
]
In:
['Row One\nColumn One', 'Two', 'Three'],
Out:
[
('|', ' Row One ', '|', ' Two ', '|', ' Three ', '|'),
('|', ' Column One ', '|', ' ', '|', ' ', '|'),
]
:param iter row: One row in the table. List of cells.
:param str style: Type of border characters to use.
:param iter inner_widths: List of widths (no padding) for each column.
:param int height: Inner height (no padding) (number of lines) to expand row to.
:return: Yields lines split into components in a list. Caller must ''.join() line.
"""
cells_in_row = list()
# Resize row if it doesn't have enough cells.
if len(row) != len(inner_widths):
row = row + [''] * (len(inner_widths) - len(row))
# Pad and align each cell. Split each cell into lines to support multi-line cells.
for i, cell in enumerate(row):
align = (self.justify_columns.get(i),)
inner_dimensions = (inner_widths[i], height)
padding = (self.padding_left, self.padding_right, 0, 0)
cells_in_row.append(align_and_pad_cell(cell, align, inner_dimensions, padding))
# Determine border characters.
if style == 'heading':
left = self.CHAR_H_OUTER_LEFT_VERTICAL if self.outer_border else ''
center = self.CHAR_H_INNER_VERTICAL if self.inner_column_border else ''
right = self.CHAR_H_OUTER_RIGHT_VERTICAL if self.outer_border else ''
elif style == 'footing':
left = self.CHAR_F_OUTER_LEFT_VERTICAL if self.outer_border else ''
center = self.CHAR_F_INNER_VERTICAL if self.inner_column_border else ''
right = self.CHAR_F_OUTER_RIGHT_VERTICAL if self.outer_border else ''
else:
left = self.CHAR_OUTER_LEFT_VERTICAL if self.outer_border else ''
center = self.CHAR_INNER_VERTICAL if self.inner_column_border else ''
right = self.CHAR_OUTER_RIGHT_VERTICAL if self.outer_border else ''
# Yield each line.
for line in build_row(cells_in_row, left, center, right):
yield line
def gen_table(self, inner_widths, inner_heights, outer_widths):
"""Combine everything and yield every line of the entire table with borders.
:param iter inner_widths: List of widths (no padding) for each column.
:param iter inner_heights: List of heights (no padding) for each row.
:param iter outer_widths: List of widths (with padding) for each column.
:return:
"""
# Yield top border.
if self.outer_border:
yield self.horizontal_border('top', outer_widths)
# Yield table body.
row_count = len(self.table_data)
last_row_index, before_last_row_index = row_count - 1, row_count - 2
for i, row in enumerate(self.table_data):
# Yield the row line by line (e.g. multi-line rows).
if self.inner_heading_row_border and i == 0:
style = 'heading'
elif self.inner_footing_row_border and i == last_row_index:
style = 'footing'
else:
style = 'row'
for line in self.gen_row_lines(row, style, inner_widths, inner_heights[i]):
yield line
# If this is the last row then break. No separator needed.
if i == last_row_index:
break
# Yield heading separator.
if self.inner_heading_row_border and i == 0:
yield self.horizontal_border('heading', outer_widths)
# Yield footing separator.
elif self.inner_footing_row_border and i == before_last_row_index:
yield self.horizontal_border('footing', outer_widths)
# Yield row separator.
elif self.inner_row_border:
yield self.horizontal_border('row', outer_widths)
# Yield bottom border.
if self.outer_border:
yield self.horizontal_border('bottom', outer_widths)
@property
def table(self):
"""Return a large string of the entire table ready to be printed to the terminal."""
dimensions = max_dimensions(self.table_data, self.padding_left, self.padding_right)[:3]
return flatten(self.gen_table(*dimensions))

151
terminaltables/build.py Normal file
View file

@ -0,0 +1,151 @@
"""Combine cells into rows."""
from terminaltables.width_and_alignment import visible_width
def combine(line, left, intersect, right):
"""Zip borders between items in `line`.
e.g. ('l', '1', 'c', '2', 'c', '3', 'r')
:param iter line: List to iterate.
:param left: Left border.
:param intersect: Column separator.
:param right: Right border.
:return: Yields combined objects.
"""
# Yield left border.
if left:
yield left
# Yield items with intersect characters.
if intersect:
try:
for j, i in enumerate(line, start=-len(line) + 1):
yield i
if j:
yield intersect
except TypeError: # Generator.
try:
item = next(line)
except StopIteration: # Was empty all along.
pass
else:
while True:
yield item
try:
peek = next(line)
except StopIteration:
break
yield intersect
item = peek
else:
for i in line:
yield i
# Yield right border.
if right:
yield right
def build_border(outer_widths, horizontal, left, intersect, right, title=None):
"""Build the top/bottom/middle row. Optionally embed the table title within the border.
Title is hidden if it doesn't fit between the left/right characters/edges.
Example return value:
('<', '-----', '+', '------', '+', '-------', '>')
('<', 'My Table', '----', '+', '------->')
:param iter outer_widths: List of widths (with padding) for each column.
:param str horizontal: Character to stretch across each column.
:param str left: Left border.
:param str intersect: Column separator.
:param str right: Right border.
:param title: Overlay the title on the border between the left and right characters.
:return: Returns a generator of strings representing a border.
:rtype: iter
"""
length = 0
# Hide title if it doesn't fit.
if title is not None and outer_widths:
try:
length = visible_width(title)
except TypeError:
title = str(title)
length = visible_width(title)
if length > sum(outer_widths) + len(intersect) * (len(outer_widths) - 1):
title = None
# Handle no title.
if title is None or not outer_widths or not horizontal:
return combine((horizontal * c for c in outer_widths), left, intersect, right)
# Handle title fitting in the first column.
if length == outer_widths[0]:
return combine([title] + [horizontal * c for c in outer_widths[1:]], left, intersect, right)
if length < outer_widths[0]:
columns = [title + horizontal * (outer_widths[0] - length)] + [horizontal * c for c in outer_widths[1:]]
return combine(columns, left, intersect, right)
# Handle wide titles/narrow columns.
columns_and_intersects = [title]
for width in combine(outer_widths, None, bool(intersect), None):
# If title is taken care of.
if length < 1:
columns_and_intersects.append(intersect if width is True else horizontal * width)
# If title's last character overrides an intersect character.
elif width is True and length == 1:
length = 0
# If this is an intersect character that is overridden by the title.
elif width is True:
length -= 1
# If title's last character is within a column.
elif width >= length:
columns_and_intersects[0] += horizontal * (width - length) # Append horizontal chars to title.
length = 0
# If remainder of title won't fit in a column.
else:
length -= width
return combine(columns_and_intersects, left, None, right)
def build_row(row, left, center, right):
"""Combine single or multi-lined cells into a single row of list of lists including borders.
Row must already be padded and extended so each cell has the same number of lines.
Example return value:
[
['>', 'Left ', '|', 'Center', '|', 'Right', '<'],
['>', 'Cell1', '|', 'Cell2 ', '|', 'Cell3', '<'],
]
:param iter row: List of cells for one row.
:param str left: Left border.
:param str center: Column separator.
:param str right: Right border.
:return: Yields other generators that yield strings.
:rtype: iter
"""
if not row or not row[0]:
yield combine((), left, center, right)
return
for row_index in range(len(row[0])):
yield combine((c[row_index] for c in row), left, center, right)
def flatten(table):
"""Flatten table data into a single string with newlines.
:param iter table: Padded and bordered table data.
:return: Joined rows/cells.
:rtype: str
"""
return '\n'.join(''.join(r) for r in table)

View file

@ -0,0 +1,70 @@
"""GithubFlavoredMarkdownTable class."""
from terminaltables.ascii_table import AsciiTable
from terminaltables.build import combine
class GithubFlavoredMarkdownTable(AsciiTable):
"""Github flavored markdown table.
https://help.github.com/articles/github-flavored-markdown/#tables
:ivar iter table_data: List (empty or list of lists of strings) representing the table.
:ivar dict justify_columns: Horizontal justification. Keys are column indexes (int). Values are right/left/center.
"""
def __init__(self, table_data):
"""Constructor.
:param iter table_data: List (empty or list of lists of strings) representing the table.
"""
# Github flavored markdown table won't support title.
super(GithubFlavoredMarkdownTable, self).__init__(table_data)
def horizontal_border(self, _, outer_widths):
"""Handle the GitHub heading border.
E.g.:
|:---|:---:|---:|----|
:param _: Unused.
:param iter outer_widths: List of widths (with padding) for each column.
:return: Prepared border strings in a generator.
:rtype: iter
"""
horizontal = str(self.CHAR_INNER_HORIZONTAL)
left = self.CHAR_OUTER_LEFT_VERTICAL
intersect = self.CHAR_INNER_VERTICAL
right = self.CHAR_OUTER_RIGHT_VERTICAL
columns = list()
for i, width in enumerate(outer_widths):
justify = self.justify_columns.get(i)
width = max(3, width) # Width should be at least 3 so justification can be applied.
if justify == 'left':
columns.append(':' + horizontal * (width - 1))
elif justify == 'right':
columns.append(horizontal * (width - 1) + ':')
elif justify == 'center':
columns.append(':' + horizontal * (width - 2) + ':')
else:
columns.append(horizontal * width)
return combine(columns, left, intersect, right)
def gen_table(self, inner_widths, inner_heights, outer_widths):
"""Combine everything and yield every line of the entire table with borders.
:param iter inner_widths: List of widths (no padding) for each column.
:param iter inner_heights: List of heights (no padding) for each row.
:param iter outer_widths: List of widths (with padding) for each column.
:return:
"""
for i, row in enumerate(self.table_data):
# Yield the row line by line (e.g. multi-line rows).
for line in self.gen_row_lines(row, 'row', inner_widths, inner_heights[i]):
yield line
# Yield heading separator.
if i == 0:
yield self.horizontal_border(None, outer_widths)

View file

@ -0,0 +1,177 @@
"""Additional simple tables defined here."""
from terminaltables.ascii_table import AsciiTable
from terminaltables.terminal_io import IS_WINDOWS
class UnixTable(AsciiTable):
"""Draw a table using box-drawing characters on Unix platforms. Table borders won't have any gaps between lines.
Similar to the tables shown on PC BIOS boot messages, but not double-lined.
"""
CHAR_F_INNER_HORIZONTAL = '\033(0\x71\033(B'
CHAR_F_INNER_INTERSECT = '\033(0\x6e\033(B'
CHAR_F_INNER_VERTICAL = '\033(0\x78\033(B'
CHAR_F_OUTER_LEFT_INTERSECT = '\033(0\x74\033(B'
CHAR_F_OUTER_LEFT_VERTICAL = '\033(0\x78\033(B'
CHAR_F_OUTER_RIGHT_INTERSECT = '\033(0\x75\033(B'
CHAR_F_OUTER_RIGHT_VERTICAL = '\033(0\x78\033(B'
CHAR_H_INNER_HORIZONTAL = '\033(0\x71\033(B'
CHAR_H_INNER_INTERSECT = '\033(0\x6e\033(B'
CHAR_H_INNER_VERTICAL = '\033(0\x78\033(B'
CHAR_H_OUTER_LEFT_INTERSECT = '\033(0\x74\033(B'
CHAR_H_OUTER_LEFT_VERTICAL = '\033(0\x78\033(B'
CHAR_H_OUTER_RIGHT_INTERSECT = '\033(0\x75\033(B'
CHAR_H_OUTER_RIGHT_VERTICAL = '\033(0\x78\033(B'
CHAR_INNER_HORIZONTAL = '\033(0\x71\033(B'
CHAR_INNER_INTERSECT = '\033(0\x6e\033(B'
CHAR_INNER_VERTICAL = '\033(0\x78\033(B'
CHAR_OUTER_BOTTOM_HORIZONTAL = '\033(0\x71\033(B'
CHAR_OUTER_BOTTOM_INTERSECT = '\033(0\x76\033(B'
CHAR_OUTER_BOTTOM_LEFT = '\033(0\x6d\033(B'
CHAR_OUTER_BOTTOM_RIGHT = '\033(0\x6a\033(B'
CHAR_OUTER_LEFT_INTERSECT = '\033(0\x74\033(B'
CHAR_OUTER_LEFT_VERTICAL = '\033(0\x78\033(B'
CHAR_OUTER_RIGHT_INTERSECT = '\033(0\x75\033(B'
CHAR_OUTER_RIGHT_VERTICAL = '\033(0\x78\033(B'
CHAR_OUTER_TOP_HORIZONTAL = '\033(0\x71\033(B'
CHAR_OUTER_TOP_INTERSECT = '\033(0\x77\033(B'
CHAR_OUTER_TOP_LEFT = '\033(0\x6c\033(B'
CHAR_OUTER_TOP_RIGHT = '\033(0\x6b\033(B'
@property
def table(self):
"""Return a large string of the entire table ready to be printed to the terminal."""
ascii_table = super(UnixTable, self).table
optimized = ascii_table.replace('\033(B\033(0', '')
return optimized
class WindowsTable(AsciiTable):
"""Draw a table using box-drawing characters on Windows platforms. This uses Code Page 437. Single-line borders.
From: http://en.wikipedia.org/wiki/Code_page_437#Characters
"""
CHAR_F_INNER_HORIZONTAL = b'\xc4'.decode('ibm437')
CHAR_F_INNER_INTERSECT = b'\xc5'.decode('ibm437')
CHAR_F_INNER_VERTICAL = b'\xb3'.decode('ibm437')
CHAR_F_OUTER_LEFT_INTERSECT = b'\xc3'.decode('ibm437')
CHAR_F_OUTER_LEFT_VERTICAL = b'\xb3'.decode('ibm437')
CHAR_F_OUTER_RIGHT_INTERSECT = b'\xb4'.decode('ibm437')
CHAR_F_OUTER_RIGHT_VERTICAL = b'\xb3'.decode('ibm437')
CHAR_H_INNER_HORIZONTAL = b'\xc4'.decode('ibm437')
CHAR_H_INNER_INTERSECT = b'\xc5'.decode('ibm437')
CHAR_H_INNER_VERTICAL = b'\xb3'.decode('ibm437')
CHAR_H_OUTER_LEFT_INTERSECT = b'\xc3'.decode('ibm437')
CHAR_H_OUTER_LEFT_VERTICAL = b'\xb3'.decode('ibm437')
CHAR_H_OUTER_RIGHT_INTERSECT = b'\xb4'.decode('ibm437')
CHAR_H_OUTER_RIGHT_VERTICAL = b'\xb3'.decode('ibm437')
CHAR_INNER_HORIZONTAL = b'\xc4'.decode('ibm437')
CHAR_INNER_INTERSECT = b'\xc5'.decode('ibm437')
CHAR_INNER_VERTICAL = b'\xb3'.decode('ibm437')
CHAR_OUTER_BOTTOM_HORIZONTAL = b'\xc4'.decode('ibm437')
CHAR_OUTER_BOTTOM_INTERSECT = b'\xc1'.decode('ibm437')
CHAR_OUTER_BOTTOM_LEFT = b'\xc0'.decode('ibm437')
CHAR_OUTER_BOTTOM_RIGHT = b'\xd9'.decode('ibm437')
CHAR_OUTER_LEFT_INTERSECT = b'\xc3'.decode('ibm437')
CHAR_OUTER_LEFT_VERTICAL = b'\xb3'.decode('ibm437')
CHAR_OUTER_RIGHT_INTERSECT = b'\xb4'.decode('ibm437')
CHAR_OUTER_RIGHT_VERTICAL = b'\xb3'.decode('ibm437')
CHAR_OUTER_TOP_HORIZONTAL = b'\xc4'.decode('ibm437')
CHAR_OUTER_TOP_INTERSECT = b'\xc2'.decode('ibm437')
CHAR_OUTER_TOP_LEFT = b'\xda'.decode('ibm437')
CHAR_OUTER_TOP_RIGHT = b'\xbf'.decode('ibm437')
class WindowsTableDouble(AsciiTable):
"""Draw a table using box-drawing characters on Windows platforms. This uses Code Page 437. Double-line borders."""
CHAR_F_INNER_HORIZONTAL = b'\xcd'.decode('ibm437')
CHAR_F_INNER_INTERSECT = b'\xce'.decode('ibm437')
CHAR_F_INNER_VERTICAL = b'\xba'.decode('ibm437')
CHAR_F_OUTER_LEFT_INTERSECT = b'\xcc'.decode('ibm437')
CHAR_F_OUTER_LEFT_VERTICAL = b'\xba'.decode('ibm437')
CHAR_F_OUTER_RIGHT_INTERSECT = b'\xb9'.decode('ibm437')
CHAR_F_OUTER_RIGHT_VERTICAL = b'\xba'.decode('ibm437')
CHAR_H_INNER_HORIZONTAL = b'\xcd'.decode('ibm437')
CHAR_H_INNER_INTERSECT = b'\xce'.decode('ibm437')
CHAR_H_INNER_VERTICAL = b'\xba'.decode('ibm437')
CHAR_H_OUTER_LEFT_INTERSECT = b'\xcc'.decode('ibm437')
CHAR_H_OUTER_LEFT_VERTICAL = b'\xba'.decode('ibm437')
CHAR_H_OUTER_RIGHT_INTERSECT = b'\xb9'.decode('ibm437')
CHAR_H_OUTER_RIGHT_VERTICAL = b'\xba'.decode('ibm437')
CHAR_INNER_HORIZONTAL = b'\xcd'.decode('ibm437')
CHAR_INNER_INTERSECT = b'\xce'.decode('ibm437')
CHAR_INNER_VERTICAL = b'\xba'.decode('ibm437')
CHAR_OUTER_BOTTOM_HORIZONTAL = b'\xcd'.decode('ibm437')
CHAR_OUTER_BOTTOM_INTERSECT = b'\xca'.decode('ibm437')
CHAR_OUTER_BOTTOM_LEFT = b'\xc8'.decode('ibm437')
CHAR_OUTER_BOTTOM_RIGHT = b'\xbc'.decode('ibm437')
CHAR_OUTER_LEFT_INTERSECT = b'\xcc'.decode('ibm437')
CHAR_OUTER_LEFT_VERTICAL = b'\xba'.decode('ibm437')
CHAR_OUTER_RIGHT_INTERSECT = b'\xb9'.decode('ibm437')
CHAR_OUTER_RIGHT_VERTICAL = b'\xba'.decode('ibm437')
CHAR_OUTER_TOP_HORIZONTAL = b'\xcd'.decode('ibm437')
CHAR_OUTER_TOP_INTERSECT = b'\xcb'.decode('ibm437')
CHAR_OUTER_TOP_LEFT = b'\xc9'.decode('ibm437')
CHAR_OUTER_TOP_RIGHT = b'\xbb'.decode('ibm437')
class SingleTable(WindowsTable if IS_WINDOWS else UnixTable):
"""Cross-platform table with single-line box-drawing characters.
:ivar iter table_data: List (empty or list of lists of strings) representing the table.
:ivar str title: Optional title to show within the top border of the table.
:ivar bool inner_column_border: Separates columns.
:ivar bool inner_footing_row_border: Show a border before the last row.
:ivar bool inner_heading_row_border: Show a border after the first row.
:ivar bool inner_row_border: Show a border in between every row.
:ivar bool outer_border: Show the top, left, right, and bottom border.
:ivar dict justify_columns: Horizontal justification. Keys are column indexes (int). Values are right/left/center.
:ivar int padding_left: Number of spaces to pad on the left side of every cell.
:ivar int padding_right: Number of spaces to pad on the right side of every cell.
"""
pass
class DoubleTable(WindowsTableDouble):
"""Cross-platform table with box-drawing characters. On Windows it's double borders, on Linux/OSX it's unicode.
:ivar iter table_data: List (empty or list of lists of strings) representing the table.
:ivar str title: Optional title to show within the top border of the table.
:ivar bool inner_column_border: Separates columns.
:ivar bool inner_footing_row_border: Show a border before the last row.
:ivar bool inner_heading_row_border: Show a border after the first row.
:ivar bool inner_row_border: Show a border in between every row.
:ivar bool outer_border: Show the top, left, right, and bottom border.
:ivar dict justify_columns: Horizontal justification. Keys are column indexes (int). Values are right/left/center.
:ivar int padding_left: Number of spaces to pad on the left side of every cell.
:ivar int padding_right: Number of spaces to pad on the right side of every cell.
"""
pass
class PorcelainTable(AsciiTable):
"""An AsciiTable stripped to a minimum.
Meant to be machine passable and roughly follow format set by git --porcelain option (hence the name).
:ivar iter table_data: List (empty or list of lists of strings) representing the table.
"""
def __init__(self, table_data):
"""Constructor.
:param iter table_data: List (empty or list of lists of strings) representing the table.
"""
# Porcelain table won't support title since it has no outer birders.
super(PorcelainTable, self).__init__(table_data)
# Removes outer border, and inner footing and header row borders.
self.inner_footing_row_border = False
self.inner_heading_row_border = False
self.outer_border = False

View file

@ -0,0 +1,98 @@
"""Get info about the current terminal window/screen buffer."""
import ctypes
import struct
import sys
DEFAULT_HEIGHT = 24
DEFAULT_WIDTH = 79
INVALID_HANDLE_VALUE = -1
IS_WINDOWS = sys.platform == 'win32'
STD_ERROR_HANDLE = -12
STD_OUTPUT_HANDLE = -11
def get_console_info(kernel32, handle):
"""Get information about this current console window (Windows only).
https://github.com/Robpol86/colorclass/blob/ab42da59/colorclass/windows.py#L111
:raise OSError: When handle is invalid or GetConsoleScreenBufferInfo API call fails.
:param ctypes.windll.kernel32 kernel32: Loaded kernel32 instance.
:param int handle: stderr or stdout handle.
:return: Width (number of characters) and height (number of lines) of the terminal.
:rtype: tuple
"""
if handle == INVALID_HANDLE_VALUE:
raise OSError('Invalid handle.')
# Query Win32 API.
lpcsbi = ctypes.create_string_buffer(22) # Populated by GetConsoleScreenBufferInfo.
if not kernel32.GetConsoleScreenBufferInfo(handle, lpcsbi):
raise ctypes.WinError() # Subclass of OSError.
# Parse data.
left, top, right, bottom = struct.unpack('hhhhHhhhhhh', lpcsbi.raw)[5:-2]
width, height = right - left, bottom - top
return width, height
def terminal_size(kernel32=None):
"""Get the width and height of the terminal.
http://code.activestate.com/recipes/440694-determine-size-of-console-window-on-windows/
http://stackoverflow.com/questions/17993814/why-the-irrelevant-code-made-a-difference
:param kernel32: Optional mock kernel32 object. For testing.
:return: Width (number of characters) and height (number of lines) of the terminal.
:rtype: tuple
"""
if IS_WINDOWS:
kernel32 = kernel32 or ctypes.windll.kernel32
try:
return get_console_info(kernel32, kernel32.GetStdHandle(STD_ERROR_HANDLE))
except OSError:
try:
return get_console_info(kernel32, kernel32.GetStdHandle(STD_OUTPUT_HANDLE))
except OSError:
return DEFAULT_WIDTH, DEFAULT_HEIGHT
try:
device = __import__('fcntl').ioctl(0, __import__('termios').TIOCGWINSZ, '\0\0\0\0\0\0\0\0')
except IOError:
return DEFAULT_WIDTH, DEFAULT_HEIGHT
height, width = struct.unpack('hhhh', device)[:2]
return width, height
def set_terminal_title(title, kernel32=None):
"""Set the terminal title.
:param title: The title to set (string, unicode, bytes accepted).
:param kernel32: Optional mock kernel32 object. For testing.
:return: If title changed successfully (Windows only, always True on Linux/OSX).
:rtype: bool
"""
try:
title_bytes = title.encode('utf-8')
except AttributeError:
title_bytes = title
if IS_WINDOWS:
kernel32 = kernel32 or ctypes.windll.kernel32
try:
is_ascii = all(ord(c) < 128 for c in title) # str/unicode.
except TypeError:
is_ascii = all(c < 128 for c in title) # bytes.
if is_ascii:
return kernel32.SetConsoleTitleA(title_bytes) != 0
else:
return kernel32.SetConsoleTitleW(title) != 0
# Linux/OSX.
sys.stdout.write(b'\033]0;' + title_bytes + b'\007')
return True

View file

@ -0,0 +1,160 @@
"""Functions that handle alignment, padding, widths, etc."""
import re
import unicodedata
from terminaltables.terminal_io import terminal_size
RE_COLOR_ANSI = re.compile(r'(\033\[[\d;]+m)')
def visible_width(string):
"""Get the visible width of a unicode string.
Some CJK unicode characters are more than one byte unlike ASCII and latin unicode characters.
From: https://github.com/Robpol86/terminaltables/pull/9
:param str string: String to measure.
:return: String's width.
:rtype: int
"""
if '\033' in string:
string = RE_COLOR_ANSI.sub('', string)
# Convert to unicode.
try:
string = string.decode('u8')
except (AttributeError, UnicodeEncodeError):
pass
width = 0
for char in string:
if unicodedata.east_asian_width(char) in ('F', 'W'):
width += 2
else:
width += 1
return width
def align_and_pad_cell(string, align, inner_dimensions, padding, space=' '):
"""Align a string horizontally and vertically. Also add additional padding in both dimensions.
:param str string: Input string to operate on.
:param tuple align: Tuple that contains one of left/center/right and/or top/middle/bottom.
:param tuple inner_dimensions: Width and height ints to expand string to without padding.
:param iter padding: Number of space chars for left, right, top, and bottom (4 ints).
:param str space: Character to use as white space for resizing/padding (use single visible chars only).
:return: Padded cell split into lines.
:rtype: list
"""
if not hasattr(string, 'splitlines'):
string = str(string)
# Handle trailing newlines or empty strings, str.splitlines() does not satisfy.
lines = string.splitlines() or ['']
if string.endswith('\n'):
lines.append('')
# Vertically align and pad.
if 'bottom' in align:
lines = ([''] * (inner_dimensions[1] - len(lines) + padding[2])) + lines + ([''] * padding[3])
elif 'middle' in align:
delta = inner_dimensions[1] - len(lines)
lines = ([''] * (delta // 2 + delta % 2 + padding[2])) + lines + ([''] * (delta // 2 + padding[3]))
else:
lines = ([''] * padding[2]) + lines + ([''] * (inner_dimensions[1] - len(lines) + padding[3]))
# Horizontally align and pad.
for i, line in enumerate(lines):
new_width = inner_dimensions[0] + len(line) - visible_width(line)
if 'right' in align:
lines[i] = line.rjust(padding[0] + new_width, space) + (space * padding[1])
elif 'center' in align:
lines[i] = (space * padding[0]) + line.center(new_width, space) + (space * padding[1])
else:
lines[i] = (space * padding[0]) + line.ljust(new_width + padding[1], space)
return lines
def max_dimensions(table_data, padding_left=0, padding_right=0, padding_top=0, padding_bottom=0):
"""Get maximum widths of each column and maximum height of each row.
:param iter table_data: List of list of strings (unmodified table data).
:param int padding_left: Number of space chars on left side of cell.
:param int padding_right: Number of space chars on right side of cell.
:param int padding_top: Number of empty lines on top side of cell.
:param int padding_bottom: Number of empty lines on bottom side of cell.
:return: 4-item tuple of n-item lists. Inner column widths and row heights, outer column widths and row heights.
:rtype: tuple
"""
inner_widths = [0] * (max(len(r) for r in table_data) if table_data else 0)
inner_heights = [0] * len(table_data)
# Find max width and heights.
for j, row in enumerate(table_data):
for i, cell in enumerate(row):
if not hasattr(cell, 'count') or not hasattr(cell, 'splitlines'):
cell = str(cell)
if not cell:
continue
inner_heights[j] = max(inner_heights[j], cell.count('\n') + 1)
inner_widths[i] = max(inner_widths[i], *[visible_width(l) for l in cell.splitlines()])
# Calculate with padding.
outer_widths = [padding_left + i + padding_right for i in inner_widths]
outer_heights = [padding_top + i + padding_bottom for i in inner_heights]
return inner_widths, inner_heights, outer_widths, outer_heights
def column_max_width(inner_widths, column_number, outer_border, inner_border, padding):
"""Determine the maximum width of a column based on the current terminal width.
:param iter inner_widths: List of widths (no padding) for each column.
:param int column_number: The column number to query.
:param int outer_border: Sum of left and right outer border visible widths.
:param int inner_border: Visible width of the inner border character.
:param int padding: Total padding per cell (left + right padding).
:return: The maximum width the column can be without causing line wrapping.
"""
column_count = len(inner_widths)
terminal_width = terminal_size()[0]
# Count how much space padding, outer, and inner borders take up.
non_data_space = outer_border
non_data_space += inner_border * (column_count - 1)
non_data_space += column_count * padding
# Exclude selected column's width.
data_space = sum(inner_widths) - inner_widths[column_number]
return terminal_width - data_space - non_data_space
def table_width(outer_widths, outer_border, inner_border):
"""Determine the width of the entire table including borders and padding.
:param iter outer_widths: List of widths (with padding) for each column.
:param int outer_border: Sum of left and right outer border visible widths.
:param int inner_border: Visible width of the inner border character.
:return: The width of the table.
:rtype: int
"""
column_count = len(outer_widths)
# Count how much space outer and inner borders take up.
non_data_space = outer_border
if column_count:
non_data_space += inner_border * (column_count - 1)
# Space of all columns and their padding.
data_space = sum(outer_widths)
return data_space + non_data_space

5
tests/__init__.py Normal file
View file

@ -0,0 +1,5 @@
"""Allows importing from screenshot."""
import py
PROJECT_ROOT = py.path.local(__file__).dirpath().join('..')

292
tests/screenshot.py Normal file
View file

@ -0,0 +1,292 @@
"""Take screenshots and search for subimages in images."""
import ctypes
import os
import random
import struct
import subprocess
import time
try:
from itertools import izip
except ImportError:
izip = zip # Py3
from tests import PROJECT_ROOT
STARTF_USESHOWWINDOW = getattr(subprocess, 'STARTF_USESHOWWINDOW', 1)
STILL_ACTIVE = 259
SW_MAXIMIZE = 3
class StartupInfo(ctypes.Structure):
"""STARTUPINFO structure."""
_fields_ = [
('cb', ctypes.c_ulong),
('lpReserved', ctypes.c_char_p),
('lpDesktop', ctypes.c_char_p),
('lpTitle', ctypes.c_char_p),
('dwX', ctypes.c_ulong),
('dwY', ctypes.c_ulong),
('dwXSize', ctypes.c_ulong),
('dwYSize', ctypes.c_ulong),
('dwXCountChars', ctypes.c_ulong),
('dwYCountChars', ctypes.c_ulong),
('dwFillAttribute', ctypes.c_ulong),
('dwFlags', ctypes.c_ulong),
('wShowWindow', ctypes.c_ushort),
('cbReserved2', ctypes.c_ushort),
('lpReserved2', ctypes.c_char_p),
('hStdInput', ctypes.c_ulong),
('hStdOutput', ctypes.c_ulong),
('hStdError', ctypes.c_ulong),
]
def __init__(self, maximize=False, title=None):
"""Constructor.
:param bool maximize: Start process in new console window, maximized.
:param bytes title: Set new window title to this instead of exe path.
"""
super(StartupInfo, self).__init__()
self.cb = ctypes.sizeof(self)
if maximize:
self.dwFlags |= STARTF_USESHOWWINDOW
self.wShowWindow = SW_MAXIMIZE
if title:
self.lpTitle = ctypes.c_char_p(title)
class ProcessInfo(ctypes.Structure):
"""PROCESS_INFORMATION structure."""
_fields_ = [
('hProcess', ctypes.c_void_p),
('hThread', ctypes.c_void_p),
('dwProcessId', ctypes.c_ulong),
('dwThreadId', ctypes.c_ulong),
]
class RunNewConsole(object):
"""Run the command in a new console window. Windows only. Use in a with statement.
subprocess sucks and really limits your access to the win32 API. Its implementation is half-assed. Using this so
that STARTUPINFO.lpTitle actually works and STARTUPINFO.dwFillAttribute produce the expected result.
"""
def __init__(self, command, maximized=False, title=None):
"""Constructor.
:param iter command: Command to run.
:param bool maximized: Start process in new console window, maximized.
:param bytes title: Set new window title to this. Needed by user32.FindWindow.
"""
if title is None:
title = 'pytest-{0}-{1}'.format(os.getpid(), random.randint(1000, 9999)).encode('ascii')
self.startup_info = StartupInfo(maximize=maximized, title=title)
self.process_info = ProcessInfo()
self.command_str = subprocess.list2cmdline(command).encode('ascii')
self._handles = list()
self._kernel32 = ctypes.LibraryLoader(ctypes.WinDLL).kernel32
self._kernel32.GetExitCodeProcess.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_ulong)]
self._kernel32.GetExitCodeProcess.restype = ctypes.c_long
def __del__(self):
"""Close win32 handles."""
while self._handles:
try:
self._kernel32.CloseHandle(self._handles.pop(0)) # .pop() is thread safe.
except IndexError:
break
def __enter__(self):
"""Entering the `with` block. Runs the process."""
if not self._kernel32.CreateProcessA(
None, # lpApplicationName
self.command_str, # lpCommandLine
None, # lpProcessAttributes
None, # lpThreadAttributes
False, # bInheritHandles
subprocess.CREATE_NEW_CONSOLE, # dwCreationFlags
None, # lpEnvironment
str(PROJECT_ROOT).encode('ascii'), # lpCurrentDirectory
ctypes.byref(self.startup_info), # lpStartupInfo
ctypes.byref(self.process_info) # lpProcessInformation
):
raise ctypes.WinError()
# Add handles added by the OS.
self._handles.append(self.process_info.hProcess)
self._handles.append(self.process_info.hThread)
# Get hWnd.
self.hwnd = 0
for _ in range(int(5 / 0.1)):
# Takes time for console window to initialize.
self.hwnd = ctypes.windll.user32.FindWindowA(None, self.startup_info.lpTitle)
if self.hwnd:
break
time.sleep(0.1)
assert self.hwnd
# Return generator that yields window size/position.
return self._iter_pos()
def __exit__(self, *_):
"""Cleanup."""
try:
# Verify process exited 0.
status = ctypes.c_ulong(STILL_ACTIVE)
while status.value == STILL_ACTIVE:
time.sleep(0.1)
if not self._kernel32.GetExitCodeProcess(self.process_info.hProcess, ctypes.byref(status)):
raise ctypes.WinError()
assert status.value == 0
finally:
# Close handles.
self.__del__()
def _iter_pos(self):
"""Yield new console window's current position and dimensions.
:return: Yields region the new window is in (left, upper, right, lower).
:rtype: tuple
"""
rect = ctypes.create_string_buffer(16) # To be written to by GetWindowRect. RECT structure.
while ctypes.windll.user32.GetWindowRect(self.hwnd, rect):
left, top, right, bottom = struct.unpack('llll', rect.raw)
width, height = right - left, bottom - top
assert width > 1
assert height > 1
yield left, top, right, bottom
raise StopIteration
def iter_rows(pil_image):
"""Yield tuple of pixels for each row in the image.
itertools.izip in Python 2.x and zip in Python 3.x are writen in C. Much faster than anything else I've found
written in pure Python.
From:
http://stackoverflow.com/questions/1624883/alternative-way-to-split-a-list-into-groups-of-n/1625023#1625023
:param PIL.Image.Image pil_image: Image to read from.
:return: Yields rows.
:rtype: tuple
"""
iterator = izip(*(iter(pil_image.getdata()),) * pil_image.width)
for row in iterator:
yield row
def get_most_interesting_row(pil_image):
"""Look for a row in the image that has the most unique pixels.
:param PIL.Image.Image pil_image: Image to read from.
:return: Row (tuple of pixel tuples), row as a set, first pixel tuple, y offset from top.
:rtype: tuple
"""
final = (None, set(), None, None) # row, row_set, first_pixel, y_pos
for y_pos, row in enumerate(iter_rows(pil_image)):
row_set = set(row)
if len(row_set) > len(final[1]):
final = row, row_set, row[0], y_pos
if len(row_set) == pil_image.width:
break # Can't get bigger.
return final
def count_subimages(screenshot, subimg):
"""Check how often subimg appears in the screenshot image.
:param PIL.Image.Image screenshot: Screen shot to search through.
:param PIL.Image.Image subimg: Subimage to search for.
:return: Number of times subimg appears in the screenshot.
:rtype: int
"""
# Get row to search for.
si_pixels = list(subimg.getdata()) # Load entire subimg into memory.
si_width = subimg.width
si_height = subimg.height
si_row, si_row_set, si_pixel, si_y = get_most_interesting_row(subimg)
occurrences = 0
# Look for subimg row in screenshot, then crop and compare pixel arrays.
for y_pos, row in enumerate(iter_rows(screenshot)):
if si_row_set - set(row):
continue # Some pixels not found.
for x_pos in range(screenshot.width - si_width + 1):
if row[x_pos] != si_pixel:
continue # First pixel does not match.
if row[x_pos:x_pos + si_width] != si_row:
continue # Row does not match.
# Found match for interesting row of subimg in screenshot.
y_corrected = y_pos - si_y
with screenshot.crop((x_pos, y_corrected, x_pos + si_width, y_corrected + si_height)) as cropped:
if list(cropped.getdata()) == si_pixels:
occurrences += 1
return occurrences
def try_candidates(screenshot, subimg_candidates, expected_count):
"""Call count_subimages() for each subimage candidate until.
If you get ImportError run "pip install pillow". Only OSX and Windows is supported.
:param PIL.Image.Image screenshot: Screen shot to search through.
:param iter subimg_candidates: Subimage paths to look for. List of strings.
:param int expected_count: Try until any a subimage candidate is found this many times.
:return: Number of times subimg appears in the screenshot.
:rtype: int
"""
from PIL import Image
count_found = 0
for subimg_path in subimg_candidates:
with Image.open(subimg_path) as rgba_s:
with rgba_s.convert(mode='RGB') as subimg:
# Make sure subimage isn't too large.
assert subimg.width < 256
assert subimg.height < 256
# Count.
count_found = count_subimages(screenshot, subimg)
if count_found == expected_count:
break # No need to try other candidates.
return count_found
def screenshot_until_match(save_to, timeout, subimg_candidates, expected_count, gen):
"""Take screenshots until one of the 'done' subimages is found. Image is saved when subimage found or at timeout.
If you get ImportError run "pip install pillow". Only OSX and Windows is supported.
:param str save_to: Save screenshot to this PNG file path when expected count found or timeout.
:param int timeout: Give up after these many seconds.
:param iter subimg_candidates: Subimage paths to look for. List of strings.
:param int expected_count: Keep trying until any of subimg_candidates is found this many times.
:param iter gen: Generator yielding window position and size to crop screenshot to.
"""
from PIL import ImageGrab
assert save_to.endswith('.png')
stop_after = time.time() + timeout
# Take screenshots until subimage is found.
while True:
with ImageGrab.grab(next(gen)) as rgba:
with rgba.convert(mode='RGB') as screenshot:
count_found = try_candidates(screenshot, subimg_candidates, expected_count)
if count_found == expected_count or time.time() > stop_after:
screenshot.save(save_to)
assert count_found == expected_count
return
time.sleep(0.5)

View file

@ -0,0 +1 @@
"""Allows importing from screenshot."""

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View file

@ -0,0 +1,145 @@
"""AsciiTable end to end testing."""
import sys
from textwrap import dedent
import py
import pytest
from terminaltables import AsciiTable
from terminaltables.terminal_io import IS_WINDOWS
from tests import PROJECT_ROOT
from tests.screenshot import RunNewConsole, screenshot_until_match
HERE = py.path.local(__file__).dirpath()
def test_single_line():
"""Test single-lined cells."""
table_data = [
['Name', 'Color', 'Type'],
['Avocado', 'green', 'nut'],
['Tomato', 'red', 'fruit'],
['Lettuce', 'green', 'vegetable'],
['Watermelon', 'green'],
[],
]
table = AsciiTable(table_data, 'Example')
table.inner_footing_row_border = True
table.justify_columns[0] = 'left'
table.justify_columns[1] = 'center'
table.justify_columns[2] = 'right'
actual = table.table
expected = (
'+Example-----+-------+-----------+\n'
'| Name | Color | Type |\n'
'+------------+-------+-----------+\n'
'| Avocado | green | nut |\n'
'| Tomato | red | fruit |\n'
'| Lettuce | green | vegetable |\n'
'| Watermelon | green | |\n'
'+------------+-------+-----------+\n'
'| | | |\n'
'+------------+-------+-----------+'
)
assert actual == expected
def test_multi_line():
"""Test multi-lined cells."""
table_data = [
['Show', 'Characters'],
['Rugrats', 'Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles,\nDil Pickles'],
['South Park', 'Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick']
]
table = AsciiTable(table_data)
# Test defaults.
actual = table.table
expected = (
'+------------+-------------------------------------------------------------------------------------+\n'
'| Show | Characters |\n'
'+------------+-------------------------------------------------------------------------------------+\n'
'| Rugrats | Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles, |\n'
'| | Dil Pickles |\n'
'| South Park | Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick |\n'
'+------------+-------------------------------------------------------------------------------------+'
)
assert actual == expected
# Test inner row border.
table.inner_row_border = True
actual = table.table
expected = (
'+------------+-------------------------------------------------------------------------------------+\n'
'| Show | Characters |\n'
'+------------+-------------------------------------------------------------------------------------+\n'
'| Rugrats | Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles, |\n'
'| | Dil Pickles |\n'
'+------------+-------------------------------------------------------------------------------------+\n'
'| South Park | Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick |\n'
'+------------+-------------------------------------------------------------------------------------+'
)
assert actual == expected
# Justify right.
table.justify_columns = {1: 'right'}
actual = table.table
expected = (
'+------------+-------------------------------------------------------------------------------------+\n'
'| Show | Characters |\n'
'+------------+-------------------------------------------------------------------------------------+\n'
'| Rugrats | Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles, |\n'
'| | Dil Pickles |\n'
'+------------+-------------------------------------------------------------------------------------+\n'
'| South Park | Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick |\n'
'+------------+-------------------------------------------------------------------------------------+'
)
assert actual == expected
@pytest.mark.skipif(str(not IS_WINDOWS))
@pytest.mark.skipif('True') # https://github.com/Robpol86/terminaltables/issues/30
def test_windows_screenshot(tmpdir):
"""Test on Windows in a new console window. Take a screenshot to verify it works.
:param tmpdir: pytest fixture.
"""
script = tmpdir.join('script.py')
command = [sys.executable, str(script)]
screenshot = PROJECT_ROOT.join('test_ascii_table.png')
if screenshot.check():
screenshot.remove()
# Generate script.
script_template = dedent(u"""\
from __future__ import print_function
import os, time
from colorclass import Color, Windows
from terminaltables import AsciiTable
Windows.enable(auto_colors=True)
stop_after = time.time() + 20
table_data = [
[Color('{b}Name{/b}'), Color('{b}Color{/b}'), Color('{b}Misc{/b}')],
['Avocado', Color('{autogreen}green{/fg}'), 100],
['Tomato', Color('{autored}red{/fg}'), 0.5],
['Lettuce', Color('{autogreen}green{/fg}'), None],
]
print(AsciiTable(table_data).table)
print('Waiting for screenshot_until_match()...')
while not os.path.exists(r'%s') and time.time() < stop_after:
time.sleep(0.5)
""")
script_contents = script_template % str(screenshot)
script.write(script_contents.encode('utf-8'), mode='wb')
# Setup expected.
sub_images = [str(p) for p in HERE.listdir('sub_ascii_*.bmp')]
assert sub_images
# Run.
with RunNewConsole(command) as gen:
screenshot_until_match(str(screenshot), 15, sub_images, 1, gen)

View file

@ -0,0 +1,245 @@
"""DoubleTable end to end testing."""
import sys
from textwrap import dedent
import py
import pytest
from terminaltables import DoubleTable
from terminaltables.terminal_io import IS_WINDOWS
from tests import PROJECT_ROOT
from tests.screenshot import RunNewConsole, screenshot_until_match
HERE = py.path.local(__file__).dirpath()
def test_single_line():
"""Test single-lined cells."""
table_data = [
['Name', 'Color', 'Type'],
['Avocado', 'green', 'nut'],
['Tomato', 'red', 'fruit'],
['Lettuce', 'green', 'vegetable'],
['Watermelon', 'green'],
[],
]
table = DoubleTable(table_data, 'Example')
table.inner_footing_row_border = True
table.justify_columns[0] = 'left'
table.justify_columns[1] = 'center'
table.justify_columns[2] = 'right'
actual = table.table
expected = (
u'\u2554Example\u2550\u2550\u2550\u2550\u2550\u2566\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2566\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\n'
u'\u2551 Name \u2551 Color \u2551 Type \u2551\n'
u'\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u256c\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u256c\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563\n'
u'\u2551 Avocado \u2551 green \u2551 nut \u2551\n'
u'\u2551 Tomato \u2551 red \u2551 fruit \u2551\n'
u'\u2551 Lettuce \u2551 green \u2551 vegetable \u2551\n'
u'\u2551 Watermelon \u2551 green \u2551 \u2551\n'
u'\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u256c\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u256c\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563\n'
u'\u2551 \u2551 \u2551 \u2551\n'
u'\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2569\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2569\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d'
)
assert actual == expected
def test_multi_line():
"""Test multi-lined cells."""
table_data = [
['Show', 'Characters'],
['Rugrats', 'Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles,\nDil Pickles'],
['South Park', 'Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick']
]
table = DoubleTable(table_data)
# Test defaults.
actual = table.table
expected = (
u'\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2566\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\n'
u'\u2551 Show \u2551 Characters '
u'\u2551\n'
u'\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u256c\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563\n'
u'\u2551 Rugrats \u2551 Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles, '
u'\u2551\n'
u'\u2551 \u2551 Dil Pickles '
u'\u2551\n'
u'\u2551 South Park \u2551 Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick '
u'\u2551\n'
u'\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2569\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d'
)
assert actual == expected
# Test inner row border.
table.inner_row_border = True
actual = table.table
expected = (
u'\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2566\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\n'
u'\u2551 Show \u2551 Characters '
u'\u2551\n'
u'\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u256c\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563\n'
u'\u2551 Rugrats \u2551 Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles, '
u'\u2551\n'
u'\u2551 \u2551 Dil Pickles '
u'\u2551\n'
u'\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u256c\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563\n'
u'\u2551 South Park \u2551 Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick '
u'\u2551\n'
u'\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2569\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d'
)
assert actual == expected
# Justify right.
table.justify_columns = {1: 'right'}
actual = table.table
expected = (
u'\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2566\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\n'
u'\u2551 Show \u2551 Characters '
u'\u2551\n'
u'\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u256c\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563\n'
u'\u2551 Rugrats \u2551 Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles, '
u'\u2551\n'
u'\u2551 \u2551 Dil Pickles '
u'\u2551\n'
u'\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u256c\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563\n'
u'\u2551 South Park \u2551 Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick '
u'\u2551\n'
u'\u255a\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2569\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550'
u'\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255d'
)
assert actual == expected
@pytest.mark.skipif(str(not IS_WINDOWS))
@pytest.mark.skipif('True') # https://github.com/Robpol86/terminaltables/issues/30
def test_windows_screenshot(tmpdir):
"""Test on Windows in a new console window. Take a screenshot to verify it works.
:param tmpdir: pytest fixture.
"""
script = tmpdir.join('script.py')
command = [sys.executable, str(script)]
screenshot = PROJECT_ROOT.join('test_double_table.png')
if screenshot.check():
screenshot.remove()
# Generate script.
script_template = dedent(u"""\
from __future__ import print_function
import os, time
from colorclass import Color, Windows
from terminaltables import DoubleTable
Windows.enable(auto_colors=True)
stop_after = time.time() + 20
table_data = [
[Color('{b}Name{/b}'), Color('{b}Color{/b}'), Color('{b}Misc{/b}')],
['Avocado', Color('{autogreen}green{/fg}'), 100],
['Tomato', Color('{autored}red{/fg}'), 0.5],
['Lettuce', Color('{autogreen}green{/fg}'), None],
]
print(DoubleTable(table_data).table)
print('Waiting for screenshot_until_match()...')
while not os.path.exists(r'%s') and time.time() < stop_after:
time.sleep(0.5)
""")
script_contents = script_template % str(screenshot)
script.write(script_contents.encode('utf-8'), mode='wb')
# Setup expected.
sub_images = [str(p) for p in HERE.listdir('sub_double_*.bmp')]
assert sub_images
# Run.
with RunNewConsole(command) as gen:
screenshot_until_match(str(screenshot), 15, sub_images, 1, gen)

View file

@ -0,0 +1,77 @@
"""GithubFlavoredMarkdownTable end to end testing."""
from terminaltables import GithubFlavoredMarkdownTable
def test_single_line():
"""Test single-lined cells."""
table_data = [
['Name', 'Color', 'Type'],
['Avocado', 'green', 'nut'],
['Tomato', 'red', 'fruit'],
['Lettuce', 'green', 'vegetable'],
['Watermelon', 'green'],
[],
]
table = GithubFlavoredMarkdownTable(table_data)
table.inner_footing_row_border = True
table.justify_columns[0] = 'left'
table.justify_columns[1] = 'center'
table.justify_columns[2] = 'right'
actual = table.table
expected = (
'| Name | Color | Type |\n'
'|:-----------|:-----:|----------:|\n'
'| Avocado | green | nut |\n'
'| Tomato | red | fruit |\n'
'| Lettuce | green | vegetable |\n'
'| Watermelon | green | |\n'
'| | | |'
)
assert actual == expected
def test_multi_line():
"""Test multi-lined cells."""
table_data = [
['Show', 'Characters'],
['Rugrats', 'Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles,\nDil Pickles'],
['South Park', 'Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick']
]
table = GithubFlavoredMarkdownTable(table_data)
# Test defaults.
actual = table.table
expected = (
'| Show | Characters |\n'
'|------------|-------------------------------------------------------------------------------------|\n'
'| Rugrats | Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles, |\n'
'| | Dil Pickles |\n'
'| South Park | Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick |'
)
assert actual == expected
# Test inner row border.
table.inner_row_border = True
actual = table.table
expected = (
'| Show | Characters |\n'
'|------------|-------------------------------------------------------------------------------------|\n'
'| Rugrats | Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles, |\n'
'| | Dil Pickles |\n'
'| South Park | Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick |'
)
assert actual == expected
# Justify right.
table.justify_columns = {1: 'right'}
actual = table.table
expected = (
'| Show | Characters |\n'
'|------------|------------------------------------------------------------------------------------:|\n'
'| Rugrats | Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles, |\n'
'| | Dil Pickles |\n'
'| South Park | Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick |'
)
assert actual == expected

View file

@ -0,0 +1,59 @@
"""PorcelainTable end to end testing."""
from terminaltables import PorcelainTable
def test_single_line():
"""Test single-lined cells."""
table_data = [
['Name', 'Color', 'Type'],
['Avocado', 'green', 'nut'],
['Tomato', 'red', 'fruit'],
['Lettuce', 'green', 'vegetable'],
['Watermelon', 'green']
]
table = PorcelainTable(table_data)
table.justify_columns[0] = 'left'
table.justify_columns[1] = 'center'
table.justify_columns[2] = 'right'
actual = table.table
expected = (
' Name | Color | Type \n'
' Avocado | green | nut \n'
' Tomato | red | fruit \n'
' Lettuce | green | vegetable \n'
' Watermelon | green | '
)
assert actual == expected
def test_multi_line():
"""Test multi-lined cells."""
table_data = [
['Show', 'Characters'],
['Rugrats', 'Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles,\nDil Pickles'],
['South Park', 'Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick']
]
table = PorcelainTable(table_data)
# Test defaults.
actual = table.table
expected = (
' Show | Characters \n'
' Rugrats | Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles, \n'
' | Dil Pickles \n'
' South Park | Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick '
)
assert actual == expected
# Justify right.
table.justify_columns = {1: 'right'}
actual = table.table
expected = (
' Show | Characters \n'
' Rugrats | Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles, \n'
' | Dil Pickles \n'
' South Park | Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick '
)
assert actual == expected

View file

@ -0,0 +1,171 @@
"""SingleTable end to end testing on Linux/OSX."""
import pytest
from terminaltables import SingleTable
from terminaltables.terminal_io import IS_WINDOWS
pytestmark = pytest.mark.skipif(str(IS_WINDOWS))
def test_single_line():
"""Test single-lined cells."""
table_data = [
['Name', 'Color', 'Type'],
['Avocado', 'green', 'nut'],
['Tomato', 'red', 'fruit'],
['Lettuce', 'green', 'vegetable'],
['Watermelon', 'green'],
[],
]
table = SingleTable(table_data, 'Example')
table.inner_footing_row_border = True
table.justify_columns[0] = 'left'
table.justify_columns[1] = 'center'
table.justify_columns[2] = 'right'
actual = table.table
expected = (
'\033(0\x6c\033(BExample\033(0\x71\x71\x71\x71\x71\x77\x71\x71\x71\x71\x71\x71\x71\x77\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x6b\033(B\n'
'\033(0\x78\033(B Name \033(0\x78\033(B Color \033(0\x78\033(B Type \033(0\x78\033(B\n'
'\033(0\x74\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x6e\x71\x71\x71\x71\x71\x71\x71\x6e\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x75\033(B\n'
'\033(0\x78\033(B Avocado \033(0\x78\033(B green \033(0\x78\033(B nut \033(0\x78\033(B\n'
'\033(0\x78\033(B Tomato \033(0\x78\033(B red \033(0\x78\033(B fruit \033(0\x78\033(B\n'
'\033(0\x78\033(B Lettuce \033(0\x78\033(B green \033(0\x78\033(B vegetable \033(0\x78\033(B\n'
'\033(0\x78\033(B Watermelon \033(0\x78\033(B green \033(0\x78\033(B \033(0\x78\033(B\n'
'\033(0\x74\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x6e\x71\x71\x71\x71\x71\x71\x71\x6e\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x75\033(B\n'
'\033(0\x78\033(B \033(0\x78\033(B \033(0\x78\033(B \033(0\x78\033(B\n'
'\033(0\x6d\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x76\x71\x71\x71\x71\x71\x71\x71\x76\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x6a\033(B'
)
assert actual == expected
def test_multi_line():
"""Test multi-lined cells."""
table_data = [
['Show', 'Characters'],
['Rugrats', 'Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles,\nDil Pickles'],
['South Park', 'Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick']
]
table = SingleTable(table_data)
# Test defaults.
actual = table.table
expected = (
'\033(0\x6c\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x77\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x6b\033(B\n'
'\033(0\x78\033(B Show \033(0\x78\033(B Characters '
' \033(0\x78\033(B\n'
'\033(0\x74\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x6e\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x75\033(B\n'
'\033(0\x78\033(B Rugrats \033(0\x78\033(B Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille,'
' Angelica Pickles, \033(0\x78\033(B\n'
'\033(0\x78\033(B \033(0\x78\033(B Dil Pickles '
' \033(0\x78\033(B\n'
'\033(0\x78\033(B South Park \033(0\x78\033(B Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick '
' \033(0\x78\033(B\n'
'\033(0\x6d\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x76\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x6a\033(B'
)
assert actual == expected
# Test inner row border.
table.inner_row_border = True
actual = table.table
expected = (
'\033(0\x6c\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x77\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x6b\033(B\n'
'\033(0\x78\033(B Show \033(0\x78\033(B Characters '
' \033(0\x78\033(B\n'
'\033(0\x74\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x6e\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x75\033(B\n'
'\033(0\x78\033(B Rugrats \033(0\x78\033(B Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille,'
' Angelica Pickles, \033(0\x78\033(B\n'
'\033(0\x78\033(B \033(0\x78\033(B Dil Pickles '
' \033(0\x78\033(B\n'
'\033(0\x74\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x6e\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x75\033(B\n'
'\033(0\x78\033(B South Park \033(0\x78\033(B Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick '
' \033(0\x78\033(B\n'
'\033(0\x6d\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x76\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x6a\033(B'
)
assert actual == expected
# Justify right.
table.justify_columns = {1: 'right'}
actual = table.table
expected = (
'\033(0\x6c\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x77\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x6b\033(B\n'
'\033(0\x78\033(B Show \033(0\x78\033(B '
' Characters \033(0\x78\033(B\n'
'\033(0\x74\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x6e\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x75\033(B\n'
'\033(0\x78\033(B Rugrats \033(0\x78\033(B Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille,'
' Angelica Pickles, \033(0\x78\033(B\n'
'\033(0\x78\033(B \033(0\x78\033(B '
' Dil Pickles \033(0\x78\033(B\n'
'\033(0\x74\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x6e\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x75\033(B\n'
'\033(0\x78\033(B South Park \033(0\x78\033(B Stan Marsh, Kyle Broflovski, '
'Eric Cartman, Kenny McCormick \033(0\x78\033(B\n'
'\033(0\x6d\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x76\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71'
'\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x71\x6a\033(B'
)
assert actual == expected

View file

@ -0,0 +1,246 @@
"""SingleTable end to end testing on Windows."""
import sys
from textwrap import dedent
import py
import pytest
from terminaltables import SingleTable
from terminaltables.terminal_io import IS_WINDOWS
from tests import PROJECT_ROOT
from tests.screenshot import RunNewConsole, screenshot_until_match
HERE = py.path.local(__file__).dirpath()
pytestmark = pytest.mark.skipif(str(not IS_WINDOWS))
def test_single_line():
"""Test single-lined cells."""
table_data = [
['Name', 'Color', 'Type'],
['Avocado', 'green', 'nut'],
['Tomato', 'red', 'fruit'],
['Lettuce', 'green', 'vegetable'],
['Watermelon', 'green'],
[],
]
table = SingleTable(table_data, 'Example')
table.inner_footing_row_border = True
table.justify_columns[0] = 'left'
table.justify_columns[1] = 'center'
table.justify_columns[2] = 'right'
actual = table.table
expected = (
u'\u250cExample\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n'
u'\u2502 Name \u2502 Color \u2502 Type \u2502\n'
u'\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n'
u'\u2502 Avocado \u2502 green \u2502 nut \u2502\n'
u'\u2502 Tomato \u2502 red \u2502 fruit \u2502\n'
u'\u2502 Lettuce \u2502 green \u2502 vegetable \u2502\n'
u'\u2502 Watermelon \u2502 green \u2502 \u2502\n'
u'\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n'
u'\u2502 \u2502 \u2502 \u2502\n'
u'\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518'
)
assert actual == expected
def test_multi_line():
"""Test multi-lined cells."""
table_data = [
['Show', 'Characters'],
['Rugrats', 'Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles,\nDil Pickles'],
['South Park', 'Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick']
]
table = SingleTable(table_data)
# Test defaults.
actual = table.table
expected = (
u'\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n'
u'\u2502 Show \u2502 Characters '
u'\u2502\n'
u'\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n'
u'\u2502 Rugrats \u2502 Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles, '
u'\u2502\n'
u'\u2502 \u2502 Dil Pickles '
u'\u2502\n'
u'\u2502 South Park \u2502 Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick '
u'\u2502\n'
u'\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518'
)
assert actual == expected
# Test inner row border.
table.inner_row_border = True
actual = table.table
expected = (
u'\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n'
u'\u2502 Show \u2502 Characters '
u'\u2502\n'
u'\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n'
u'\u2502 Rugrats \u2502 Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles, '
u'\u2502\n'
u'\u2502 \u2502 Dil Pickles '
u'\u2502\n'
u'\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n'
u'\u2502 South Park \u2502 Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick '
u'\u2502\n'
u'\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518'
)
assert actual == expected
# Justify right.
table.justify_columns = {1: 'right'}
actual = table.table
expected = (
u'\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n'
u'\u2502 Show \u2502 Characters '
u'\u2502\n'
u'\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n'
u'\u2502 Rugrats \u2502 Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles, '
u'\u2502\n'
u'\u2502 \u2502 Dil Pickles '
u'\u2502\n'
u'\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n'
u'\u2502 South Park \u2502 Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick '
u'\u2502\n'
u'\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'
u'\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518'
)
assert actual == expected
@pytest.mark.skipif(str(not IS_WINDOWS))
@pytest.mark.skipif('True') # https://github.com/Robpol86/terminaltables/issues/30
def test_windows_screenshot(tmpdir):
"""Test on Windows in a new console window. Take a screenshot to verify it works.
:param tmpdir: pytest fixture.
"""
script = tmpdir.join('script.py')
command = [sys.executable, str(script)]
screenshot = PROJECT_ROOT.join('test_single_table.png')
if screenshot.check():
screenshot.remove()
# Generate script.
script_template = dedent(u"""\
from __future__ import print_function
import os, time
from colorclass import Color, Windows
from terminaltables import SingleTable
Windows.enable(auto_colors=True)
stop_after = time.time() + 20
table_data = [
[Color('{b}Name{/b}'), Color('{b}Color{/b}'), Color('{b}Misc{/b}')],
['Avocado', Color('{autogreen}green{/fg}'), 100],
['Tomato', Color('{autored}red{/fg}'), 0.5],
['Lettuce', Color('{autogreen}green{/fg}'), None],
]
print(SingleTable(table_data).table)
print('Waiting for screenshot_until_match()...')
while not os.path.exists(r'%s') and time.time() < stop_after:
time.sleep(0.5)
""")
script_contents = script_template % str(screenshot)
script.write(script_contents.encode('utf-8'), mode='wb')
# Setup expected.
sub_images = [str(p) for p in HERE.listdir('sub_single_*.bmp')]
assert sub_images
# Run.
with RunNewConsole(command) as gen:
screenshot_until_match(str(screenshot), 15, sub_images, 1, gen)

108
tests/test_ascii_table.py Normal file
View file

@ -0,0 +1,108 @@
"""Test AsciiTable class."""
import pytest
from terminaltables.other_tables import AsciiTable
SINGLE_LINE = (
('Name', 'Color', 'Type'),
('Avocado', 'green', 'nut'),
('Tomato', 'red', 'fruit'),
('Lettuce', 'green', 'vegetable'),
)
MULTI_LINE = (
('Show', 'Characters'),
('Rugrats', 'Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles,\nDil Pickles'),
('South Park', 'Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick'),
)
@pytest.fixture(autouse=True)
def patch(monkeypatch):
"""Monkeypatch before every test function in this module.
:param monkeypatch: pytest fixture.
"""
monkeypatch.setattr('terminaltables.ascii_table.terminal_size', lambda: (79, 24))
monkeypatch.setattr('terminaltables.width_and_alignment.terminal_size', lambda: (79, 24))
@pytest.mark.parametrize('table_data,column_number,expected', [
([], 0, IndexError),
([[]], 0, IndexError),
([['']], 1, IndexError),
(SINGLE_LINE, 0, 55),
(SINGLE_LINE, 1, 53),
(SINGLE_LINE, 2, 57),
(MULTI_LINE, 0, -11),
(MULTI_LINE, 1, 62),
])
def test_column_max_width(table_data, column_number, expected):
"""Test method in class.
:param iter table_data: Passed to AsciiTable.__init__().
:param int column_number: Passed to AsciiTable.column_max_width().
:param int expected: Expected return value of AsciiTable.column_max_width().
"""
table = AsciiTable(table_data)
if expected == IndexError:
with pytest.raises(IndexError):
table.column_max_width(column_number)
return
actual = table.column_max_width(column_number)
assert actual == expected
def test_column_widths():
"""Test method in class."""
assert AsciiTable([]).column_widths == list()
table = AsciiTable(SINGLE_LINE)
actual = table.column_widths
assert actual == [7, 5, 9]
@pytest.mark.parametrize('table_data,terminal_width,expected', [
([], None, True),
([[]], None, True),
([['']], None, True),
(SINGLE_LINE, None, True),
(SINGLE_LINE, 30, False),
(MULTI_LINE, None, False),
(MULTI_LINE, 100, True),
])
def test_ok(monkeypatch, table_data, terminal_width, expected):
"""Test method in class.
:param monkeypatch: pytest fixture.
:param iter table_data: Passed to AsciiTable.__init__().
:param int terminal_width: Monkeypatch width of terminal_size() if not None.
:param bool expected: Expected return value.
"""
if terminal_width is not None:
monkeypatch.setattr('terminaltables.ascii_table.terminal_size', lambda: (terminal_width, 24))
table = AsciiTable(table_data)
actual = table.ok
assert actual is expected
@pytest.mark.parametrize('table_data,expected', [
([], 2),
([[]], 2),
([['']], 4),
([[' ']], 5),
(SINGLE_LINE, 31),
(MULTI_LINE, 100),
])
def test_table_width(table_data, expected):
"""Test method in class.
:param iter table_data: Passed to AsciiTable.__init__().
:param int expected: Expected return value.
"""
table = AsciiTable(table_data)
actual = table.table_width
assert actual == expected

View file

@ -0,0 +1,86 @@
"""Test method in BaseTable class."""
import pytest
from terminaltables.base_table import BaseTable
@pytest.mark.parametrize('style', ['heading', 'footing', 'row'])
def test_single_line(style):
"""Test with single-line row.
:param str style: Passed to method.
"""
row = ['Row One Column One', 'Two', 'Three']
table = BaseTable([row])
actual = [tuple(i) for i in table.gen_row_lines(row, style, [18, 3, 5], 1)]
expected = [
('|', ' Row One Column One ', '|', ' Two ', '|', ' Three ', '|'),
]
assert actual == expected
@pytest.mark.parametrize('style', ['heading', 'footing', 'row'])
def test_multi_line(style):
"""Test with multi-line row.
:param str style: Passed to method.
"""
row = ['Row One\nColumn One', 'Two', 'Three']
table = BaseTable([row])
actual = [tuple(i) for i in table.gen_row_lines(row, style, [10, 3, 5], 2)]
expected = [
('|', ' Row One ', '|', ' Two ', '|', ' Three ', '|'),
('|', ' Column One ', '|', ' ', '|', ' ', '|'),
]
assert actual == expected
@pytest.mark.parametrize('style', ['heading', 'footing', 'row'])
def test_no_padding_no_borders(style):
"""Test without padding or borders.
:param str style: Passed to method.
"""
row = ['Row One\nColumn One', 'Two', 'Three']
table = BaseTable([row])
table.inner_column_border = False
table.outer_border = False
table.padding_left = 0
table.padding_right = 0
actual = [tuple(i) for i in table.gen_row_lines(row, style, [10, 3, 5], 2)]
expected = [
('Row One ', 'Two', 'Three'),
('Column One', ' ', ' '),
]
assert actual == expected
@pytest.mark.parametrize('style', ['heading', 'footing', 'row'])
def test_uneven(style):
"""Test with row missing cells.
:param str style: Passed to method.
"""
row = ['Row One Column One']
table = BaseTable([row])
actual = [tuple(i) for i in table.gen_row_lines(row, style, [18, 3, 5], 1)]
expected = [
('|', ' Row One Column One ', '|', ' ', '|', ' ', '|'),
]
assert actual == expected
@pytest.mark.parametrize('style', ['heading', 'footing', 'row'])
def test_empty_table(style):
"""Test empty table.
:param str style: Passed to method.
"""
row = []
table = BaseTable([row])
actual = [tuple(i) for i in table.gen_row_lines(row, style, [], 0)]
expected = [
('|', '|'),
]
assert actual == expected

View file

@ -0,0 +1,225 @@
"""Test method in BaseTable class."""
import pytest
from terminaltables.base_table import BaseTable
from terminaltables.build import flatten
from terminaltables.width_and_alignment import max_dimensions
@pytest.mark.parametrize('inner_heading_row_border', [True, False])
@pytest.mark.parametrize('inner_footing_row_border', [True, False])
@pytest.mark.parametrize('inner_row_border', [True, False])
def test_inner_row_borders(inner_heading_row_border, inner_footing_row_border, inner_row_border):
"""Test heading/footing/row borders.
:param bool inner_heading_row_border: Passed to table.
:param bool inner_footing_row_border: Passed to table.
:param bool inner_row_border: Passed to table.
"""
table_data = [
['Name', 'Color', 'Type'],
['Avocado', 'green', 'nut'],
['Tomato', 'red', 'fruit'],
['Lettuce', 'green', 'vegetable'],
]
table = BaseTable(table_data)
table.inner_heading_row_border = inner_heading_row_border
table.inner_footing_row_border = inner_footing_row_border
table.inner_row_border = inner_row_border
inner_widths, inner_heights, outer_widths = max_dimensions(table_data, table.padding_left, table.padding_right)[:3]
actual = flatten(table.gen_table(inner_widths, inner_heights, outer_widths))
# Determine expected.
if inner_row_border:
expected = (
'+---------+-------+-----------+\n'
'| Name | Color | Type |\n'
'+---------+-------+-----------+\n'
'| Avocado | green | nut |\n'
'+---------+-------+-----------+\n'
'| Tomato | red | fruit |\n'
'+---------+-------+-----------+\n'
'| Lettuce | green | vegetable |\n'
'+---------+-------+-----------+'
)
elif inner_heading_row_border and inner_footing_row_border:
expected = (
'+---------+-------+-----------+\n'
'| Name | Color | Type |\n'
'+---------+-------+-----------+\n'
'| Avocado | green | nut |\n'
'| Tomato | red | fruit |\n'
'+---------+-------+-----------+\n'
'| Lettuce | green | vegetable |\n'
'+---------+-------+-----------+'
)
elif inner_heading_row_border:
expected = (
'+---------+-------+-----------+\n'
'| Name | Color | Type |\n'
'+---------+-------+-----------+\n'
'| Avocado | green | nut |\n'
'| Tomato | red | fruit |\n'
'| Lettuce | green | vegetable |\n'
'+---------+-------+-----------+'
)
elif inner_footing_row_border:
expected = (
'+---------+-------+-----------+\n'
'| Name | Color | Type |\n'
'| Avocado | green | nut |\n'
'| Tomato | red | fruit |\n'
'+---------+-------+-----------+\n'
'| Lettuce | green | vegetable |\n'
'+---------+-------+-----------+'
)
else:
expected = (
'+---------+-------+-----------+\n'
'| Name | Color | Type |\n'
'| Avocado | green | nut |\n'
'| Tomato | red | fruit |\n'
'| Lettuce | green | vegetable |\n'
'+---------+-------+-----------+'
)
assert actual == expected
@pytest.mark.parametrize('outer_border', [True, False])
def test_outer_borders(outer_border):
"""Test left/right/top/bottom table borders.
:param bool outer_border: Passed to table.
"""
table_data = [
['Name', 'Color', 'Type'],
['Avocado', 'green', 'nut'],
['Tomato', 'red', 'fruit'],
['Lettuce', 'green', 'vegetable'],
]
table = BaseTable(table_data, 'Example Table')
table.outer_border = outer_border
inner_widths, inner_heights, outer_widths = max_dimensions(table_data, table.padding_left, table.padding_right)[:3]
actual = flatten(table.gen_table(inner_widths, inner_heights, outer_widths))
# Determine expected.
if outer_border:
expected = (
'+Example Table----+-----------+\n'
'| Name | Color | Type |\n'
'+---------+-------+-----------+\n'
'| Avocado | green | nut |\n'
'| Tomato | red | fruit |\n'
'| Lettuce | green | vegetable |\n'
'+---------+-------+-----------+'
)
else:
expected = (
' Name | Color | Type \n'
'---------+-------+-----------\n'
' Avocado | green | nut \n'
' Tomato | red | fruit \n'
' Lettuce | green | vegetable '
)
assert actual == expected
@pytest.mark.parametrize('mode', ['row', 'one', 'blank', 'empty', 'none'])
@pytest.mark.parametrize('bare', [False, True])
def test_one_no_rows(mode, bare):
"""Test with one or no rows.
:param str mode: Type of table contents to test.
:param bool bare: Disable padding/borders.
"""
if mode == 'row':
table_data = [
['Avocado', 'green', 'nut'],
]
elif mode == 'one':
table_data = [
['Avocado'],
]
elif mode == 'blank':
table_data = [
[''],
]
elif mode == 'empty':
table_data = [
[],
]
else:
table_data = [
]
table = BaseTable(table_data)
if bare:
table.inner_column_border = False
table.inner_footing_row_border = False
table.inner_heading_row_border = False
table.inner_row_border = False
table.outer_border = False
table.padding_left = 0
table.padding_right = 0
inner_widths, inner_heights, outer_widths = max_dimensions(table_data, table.padding_left, table.padding_right)[:3]
actual = flatten(table.gen_table(inner_widths, inner_heights, outer_widths))
# Determine expected.
if mode == 'row':
if bare:
expected = (
'Avocadogreennut'
)
else:
expected = (
'+---------+-------+-----+\n'
'| Avocado | green | nut |\n'
'+---------+-------+-----+'
)
elif mode == 'one':
if bare:
expected = (
'Avocado'
)
else:
expected = (
'+---------+\n'
'| Avocado |\n'
'+---------+'
)
elif mode == 'blank': # Remember there's still padding.
if bare:
expected = (
''
)
else:
expected = (
'+--+\n'
'| |\n'
'+--+'
)
elif mode == 'empty':
if bare:
expected = (
''
)
else:
expected = (
'++\n'
'||\n'
'++'
)
else:
if bare:
expected = (
''
)
else:
expected = (
'++\n'
'++'
)
assert actual == expected

View file

@ -0,0 +1,98 @@
"""Test method in BaseTable class."""
import pytest
from terminaltables.base_table import BaseTable
from terminaltables.width_and_alignment import max_dimensions
SINGLE_LINE = (
('Name', 'Color', 'Type'),
('Avocado', 'green', 'nut'),
('Tomato', 'red', 'fruit'),
('Lettuce', 'green', 'vegetable'),
)
@pytest.mark.parametrize('inner_column_border', [True, False])
@pytest.mark.parametrize('style', ['top', 'bottom'])
def test_top_bottom(inner_column_border, style):
"""Test top and bottom borders.
:param bool inner_column_border: Passed to table class.
:param str style: Passed to method.
"""
table = BaseTable(SINGLE_LINE, 'Example')
table.inner_column_border = inner_column_border
outer_widths = max_dimensions(table.table_data, table.padding_left, table.padding_right)[2]
# Determine expected.
if style == 'top' and inner_column_border:
expected = '+Example--+-------+-----------+'
elif style == 'top':
expected = '+Example--------------------+'
elif style == 'bottom' and inner_column_border:
expected = '+---------+-------+-----------+'
else:
expected = '+---------------------------+'
# Test.
actual = ''.join(table.horizontal_border(style, outer_widths))
assert actual == expected
@pytest.mark.parametrize('inner_column_border', [True, False])
@pytest.mark.parametrize('outer_border', [True, False])
@pytest.mark.parametrize('style', ['heading', 'footing'])
def test_heading_footing(inner_column_border, outer_border, style):
"""Test heading and footing borders.
:param bool inner_column_border: Passed to table class.
:param bool outer_border: Passed to table class.
:param str style: Passed to method.
"""
table = BaseTable(SINGLE_LINE)
table.inner_column_border = inner_column_border
table.outer_border = outer_border
outer_widths = max_dimensions(table.table_data, table.padding_left, table.padding_right)[2]
# Determine expected.
if style == 'heading' and outer_border:
expected = '+---------+-------+-----------+' if inner_column_border else '+---------------------------+'
elif style == 'heading':
expected = '---------+-------+-----------' if inner_column_border else '---------------------------'
elif style == 'footing' and outer_border:
expected = '+---------+-------+-----------+' if inner_column_border else '+---------------------------+'
else:
expected = '---------+-------+-----------' if inner_column_border else '---------------------------'
# Test.
actual = ''.join(table.horizontal_border(style, outer_widths))
assert actual == expected
@pytest.mark.parametrize('inner_column_border', [True, False])
@pytest.mark.parametrize('outer_border', [True, False])
def test_row(inner_column_border, outer_border):
"""Test inner borders.
:param bool inner_column_border: Passed to table class.
:param bool outer_border: Passed to table class.
"""
table = BaseTable(SINGLE_LINE)
table.inner_column_border = inner_column_border
table.outer_border = outer_border
outer_widths = max_dimensions(table.table_data, table.padding_left, table.padding_right)[2]
# Determine expected.
if inner_column_border and outer_border:
expected = '+---------+-------+-----------+'
elif inner_column_border:
expected = '---------+-------+-----------'
elif outer_border:
expected = '+---------------------------+'
else:
expected = '---------------------------'
# Test.
actual = ''.join(table.horizontal_border('row', outer_widths))
assert actual == expected

View file

@ -0,0 +1,196 @@
# coding: utf-8
"""Test property in BaseTable class."""
from colorama import Fore
from colorclass import Color
from termcolor import colored
from terminaltables.base_table import BaseTable
def test_ascii():
"""Test with ASCII characters."""
table_data = [
['Name', 'Color', 'Type'],
['Avocado', 'green', 'nut'],
['Tomato', 'red', 'fruit'],
['Lettuce', 'green', 'vegetable'],
]
table = BaseTable(table_data)
actual = table.table
expected = (
'+---------+-------+-----------+\n'
'| Name | Color | Type |\n'
'+---------+-------+-----------+\n'
'| Avocado | green | nut |\n'
'| Tomato | red | fruit |\n'
'| Lettuce | green | vegetable |\n'
'+---------+-------+-----------+'
)
assert actual == expected
def test_int():
"""Test with integers instead of strings."""
table_data = [
[100, 10, 1],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
]
table = BaseTable(table_data, 1234567890)
actual = table.table
expected = (
'+1234567890+---+\n'
'| 100 | 10 | 1 |\n'
'+-----+----+---+\n'
'| 0 | 3 | 6 |\n'
'| 1 | 4 | 7 |\n'
'| 2 | 5 | 8 |\n'
'+-----+----+---+'
)
assert actual == expected
def test_float():
"""Test with floats instead of strings."""
table_data = [
[1.0, 22.0, 333.0],
[0.1, 3.1, 6.1],
[1.1, 4.1, 7.1],
[2.1, 5.1, 8.1],
]
table = BaseTable(table_data, 0.12345678)
actual = table.table
expected = (
'+0.12345678--+-------+\n'
'| 1.0 | 22.0 | 333.0 |\n'
'+-----+------+-------+\n'
'| 0.1 | 3.1 | 6.1 |\n'
'| 1.1 | 4.1 | 7.1 |\n'
'| 2.1 | 5.1 | 8.1 |\n'
'+-----+------+-------+'
)
assert actual == expected
def test_bool_none():
"""Test with NoneType/boolean instead of strings."""
table_data = [
[True, False, None],
[True, False, None],
[False, None, True],
[None, True, False],
]
table = BaseTable(table_data, True)
actual = table.table
expected = (
'+True---+-------+-------+\n'
'| True | False | None |\n'
'+-------+-------+-------+\n'
'| True | False | None |\n'
'| False | None | True |\n'
'| None | True | False |\n'
'+-------+-------+-------+'
)
assert actual == expected
def test_cjk():
"""Test with CJK characters."""
table_data = [
['CJK'],
['蓝色'],
['世界你好'],
]
table = BaseTable(table_data)
actual = table.table
expected = (
'+----------+\n'
'| CJK |\n'
'+----------+\n'
'| 蓝色 |\n'
'| 世界你好 |\n'
'+----------+'
)
assert actual == expected
def test_rtl():
"""Test with RTL characters."""
table_data = [
['RTL'],
['שלום'],
['معرب'],
]
table = BaseTable(table_data)
actual = table.table
expected = (
'+------+\n'
'| RTL |\n'
'+------+\n'
'| שלום |\n'
'| معرب |\n'
'+------+'
)
assert actual == expected
def test_rtl_large():
"""Test large table of RTL characters."""
table_data = [
['اكتب', 'اللون', 'اسم'],
['البندق', 'أخضر', 'أفوكادو'],
['ثمرة', 'أحمر', 'بندورة'],
['الخضروات', 'أخضر', 'الخس'],
]
table = BaseTable(table_data, 'جوجل المترجم')
actual = table.table
expected = (
'+جوجل المترجم------+---------+\n'
'| اكتب | اللون | اسم |\n'
'+----------+-------+---------+\n'
'| البندق | أخضر | أفوكادو |\n'
'| ثمرة | أحمر | بندورة |\n'
'| الخضروات | أخضر | الخس |\n'
'+----------+-------+---------+'
)
assert actual == expected
def test_color():
"""Test with color characters."""
table_data = [
['ansi', '\033[31mRed\033[39m', '\033[32mGreen\033[39m', '\033[34mBlue\033[39m'],
['colorclass', Color('{red}Red{/red}'), Color('{green}Green{/green}'), Color('{blue}Blue{/blue}')],
['colorama', Fore.RED + 'Red' + Fore.RESET, Fore.GREEN + 'Green' + Fore.RESET, Fore.BLUE + 'Blue' + Fore.RESET],
['termcolor', colored('Red', 'red'), colored('Green', 'green'), colored('Blue', 'blue')],
]
table = BaseTable(table_data)
table.inner_heading_row_border = False
actual = table.table
expected = (
u'+------------+-----+-------+------+\n'
u'| ansi | \033[31mRed\033[39m | \033[32mGreen\033[39m | \033[34mBlue\033[39m |\n'
u'| colorclass | \033[31mRed\033[39m | \033[32mGreen\033[39m | \033[34mBlue\033[39m |\n'
u'| colorama | \033[31mRed\033[39m | \033[32mGreen\033[39m | \033[34mBlue\033[39m |\n'
u'| termcolor | \033[31mRed\033[0m | \033[32mGreen\033[0m | \033[34mBlue\033[0m |\n'
u'+------------+-----+-------+------+'
)
assert actual == expected

View file

@ -0,0 +1,312 @@
# coding: utf-8
"""Test function in module."""
import pytest
from colorama import Fore, Style
from colorclass import Color
from termcolor import colored
from terminaltables.build import build_border
@pytest.mark.parametrize('outer_widths,horizontal,left,intersect,right,expected', [
([5, 6, 7], '-', '<', '+', '>', '<-----+------+------->'),
([1, 1, 1], '-', '', '', '', '---'),
([1, 1, 1], '', '', '', '', ''),
([1], '-', '<', '+', '>', '<->'),
([], '-', '<', '+', '>', '<>'),
])
def test_no_title(outer_widths, horizontal, left, intersect, right, expected):
"""Test without title.
:param iter outer_widths: List of integers representing column widths with padding.
:param str horizontal: Character to stretch across each column.
:param str left: Left border.
:param str intersect: Column separator.
:param str right: Right border.
:param str expected: Expected output.
"""
actual = build_border(outer_widths, horizontal, left, intersect, right)
assert ''.join(actual) == expected
@pytest.mark.parametrize('outer_widths,intersect,expected', [
([20], '+', 'Applications--------'),
([20], '', 'Applications--------'),
([15, 5], '+', 'Applications---+-----'),
([15, 5], '', 'Applications--------'),
([12], '+', 'Applications'),
([12], '', 'Applications'),
([12, 1], '+', 'Applications+-'),
([12, 1], '', 'Applications-'),
([12, 0], '+', 'Applications+'),
([12, 0], '', 'Applications'),
])
@pytest.mark.parametrize('left,right', [('', ''), ('<', '>')])
def test_first_column_fit(outer_widths, left, intersect, right, expected):
"""Test with title that fits in the first column.
:param iter outer_widths: List of integers representing column widths with padding.
:param str left: Left border.
:param str intersect: Column separator.
:param str right: Right border.
:param str expected: Expected output.
"""
if left and right:
expected = left + expected + right
actual = build_border(outer_widths, '-', left, intersect, right, title='Applications')
assert ''.join(actual) == expected
@pytest.mark.parametrize('outer_widths,expected', [
([20], 'Applications--------'),
([10, 10], 'Applications--------'),
([5, 5, 5, 5], 'Applications--------'),
([3, 2, 3, 2, 3, 2, 3, 2], 'Applications--------'),
([1] * 20, 'Applications--------'),
([10, 5], 'Applications---'),
([9, 5], 'Applications--'),
([8, 5], 'Applications-'),
([7, 5], 'Applications'),
([6, 5], '-----------'),
])
@pytest.mark.parametrize('left,right', [('', ''), ('<', '>')])
def test_no_intersect(outer_widths, left, right, expected):
"""Test with no column dividers.
:param iter outer_widths: List of integers representing column widths.
:param str left: Left border.
:param str right: Right border.
:param str expected: Expected output.
"""
if left and right:
expected = left + expected + right
actual = build_border(outer_widths, '-', left, '', right, title='Applications')
assert ''.join(actual) == expected
@pytest.mark.parametrize('outer_widths,expected', [
([20], 'Applications--------'),
([0, 20], 'Applications---------'),
([20, 0], 'Applications--------+'),
([0, 0, 20], 'Applications----------'),
([20, 0, 0], 'Applications--------++'),
([10, 10], 'Applications---------'),
([11, 9], 'Applications---------'),
([12, 8], 'Applications+--------'),
([13, 7], 'Applications-+-------'),
([5, 5, 5, 5], 'Applications-----+-----'),
([4, 4, 6, 6], 'Applications----+------'),
([3, 3, 7, 7], 'Applications---+-------'),
([2, 2, 7, 9], 'Applications-+---------'),
([1, 1, 9, 9], 'Applications-+---------'),
([2, 2, 2, 2, 2, 2, 2], 'Applications--+--+--'),
([1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 'Applications-+-+-+-'),
([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'Applications++++++++'),
([2, 2, 2, 2], '--+--+--+--'),
([1, 1, 1, 1, 1], '-+-+-+-+-'),
([0, 0, 0, 0, 0, 0, 0, 0, 0, 0], '+++++++++'),
])
@pytest.mark.parametrize('left,right', [('', ''), ('<', '>')])
def test_intersect(outer_widths, left, right, expected):
"""Test with column dividers.
:param iter outer_widths: List of integers representing column widths.
:param str left: Left border.
:param str right: Right border.
:param str expected: Expected output.
"""
if left and right:
expected = left + expected + right
actual = build_border(outer_widths, '-', left, '+', right, title='Applications')
assert ''.join(actual) == expected
@pytest.mark.parametrize('outer_widths,intersect,expected', [
([12], '+', u'蓝色--------'),
([12], '', u'蓝色--------'),
([7, 5], '+', u'蓝色---+-----'),
([7, 5], '', u'蓝色--------'),
([4], '+', u'蓝色'),
([4], '', u'蓝色'),
([4, 1], '+', u'蓝色+-'),
([4, 1], '', u'蓝色-'),
([4, 0], '+', u'蓝色+'),
([4, 0], '', u'蓝色'),
([12], '', u'蓝色--------'),
([6, 6], '', u'蓝色--------'),
([3, 3, 3, 3], '', u'蓝色--------'),
([2, 1, 2, 1, 2, 1, 2, 1], '', u'蓝色--------'),
([1] * 12, '', u'蓝色--------'),
([2, 4], '', u'蓝色--'),
([1, 4], '', u'蓝色-'),
([1, 3], '', u'蓝色'),
([1, 2], '', u'---'),
([2], '', u'--'),
([12], '+', u'蓝色--------'),
([0, 12], '+', u'蓝色---------'),
([12, 0], '+', u'蓝色--------+'),
([0, 0, 12], '+', u'蓝色----------'),
([12, 0, 0], '+', u'蓝色--------++'),
([3, 3], '+', u'蓝色---'),
([4, 2], '+', u'蓝色+--'),
([5, 1], '+', u'蓝色-+-'),
([3, 3, 3, 3], '+', u'蓝色---+---+---'),
([2, 2, 4, 4], '+', u'蓝色-+----+----'),
([1, 1, 5, 5], '+', u'蓝色-----+-----'),
([2, 2, 2, 2], '+', u'蓝色-+--+--'),
([1, 1, 1, 1, 1], '+', u'蓝色-+-+-'),
([0, 0, 0, 0, 0, 0, 0], '+', u'蓝色++'),
([1, 1], '+', u'-+-'),
])
@pytest.mark.parametrize('left,right', [('', ''), ('<', '>')])
def test_cjk(outer_widths, left, intersect, right, expected):
"""Test with CJK characters in title.
:param iter outer_widths: List of integers representing column widths.
:param str left: Left border.
:param str intersect: Column separator.
:param str right: Right border.
:param str expected: Expected output.
"""
if left and right:
expected = left + expected + right
actual = build_border(outer_widths, '-', left, intersect, right, title=u'蓝色')
assert ''.join(actual) == expected
@pytest.mark.parametrize('outer_widths,intersect,expected', [
([12], '+', u'معرب--------'),
([12], '', u'معرب--------'),
([7, 5], '+', u'معرب---+-----'),
([7, 5], '', u'معرب--------'),
([4], '+', u'معرب'),
([4], '', u'معرب'),
([4, 1], '+', u'معرب+-'),
([4, 1], '', u'معرب-'),
([4, 0], '+', u'معرب+'),
([4, 0], '', u'معرب'),
([12], '', u'معرب--------'),
([6, 6], '', u'معرب--------'),
([3, 3, 3, 3], '', u'معرب--------'),
([2, 1, 2, 1, 2, 1, 2, 1], '', u'معرب--------'),
([1] * 12, '', u'معرب--------'),
([2, 4], '', u'معرب--'),
([1, 4], '', u'معرب-'),
([1, 3], '', u'معرب'),
([1, 2], '', u'---'),
([2], '', u'--'),
([12], '+', u'معرب--------'),
([0, 12], '+', u'معرب---------'),
([12, 0], '+', u'معرب--------+'),
([0, 0, 12], '+', u'معرب----------'),
([12, 0, 0], '+', u'معرب--------++'),
([3, 3], '+', u'معرب---'),
([4, 2], '+', u'معرب+--'),
([5, 1], '+', u'معرب-+-'),
([3, 3, 3, 3], '+', u'معرب---+---+---'),
([2, 2, 4, 4], '+', u'معرب-+----+----'),
([1, 1, 5, 5], '+', u'معرب-----+-----'),
([2, 2, 2, 2], '+', u'معرب-+--+--'),
([1, 1, 1, 1, 1], '+', u'معرب-+-+-'),
([0, 0, 0, 0, 0, 0, 0], '+', u'معرب++'),
([1, 1], '+', u'-+-'),
])
@pytest.mark.parametrize('left,right', [('', ''), ('<', '>')])
def test_rtl(outer_widths, left, intersect, right, expected):
"""Test with RTL characters in title.
:param iter outer_widths: List of integers representing column widths.
:param str left: Left border.
:param str intersect: Column separator.
:param str right: Right border.
:param str expected: Expected output.
"""
if left and right:
expected = left + expected + right
actual = build_border(outer_widths, '-', left, intersect, right, title=u'معرب')
assert ''.join(actual) == expected
@pytest.mark.parametrize('outer_widths,intersect,expected', [
([12], '+', '\x1b[34mTEST\x1b[0m--------'),
([12], '', '\x1b[34mTEST\x1b[0m--------'),
([7, 5], '+', '\x1b[34mTEST\x1b[0m---+-----'),
([7, 5], '', '\x1b[34mTEST\x1b[0m--------'),
([4], '+', '\x1b[34mTEST\x1b[0m'),
([4], '', '\x1b[34mTEST\x1b[0m'),
([4, 1], '+', '\x1b[34mTEST\x1b[0m+-'),
([4, 1], '', '\x1b[34mTEST\x1b[0m-'),
([4, 0], '+', '\x1b[34mTEST\x1b[0m+'),
([4, 0], '', '\x1b[34mTEST\x1b[0m'),
([12], '', '\x1b[34mTEST\x1b[0m--------'),
([6, 6], '', '\x1b[34mTEST\x1b[0m--------'),
([3, 3, 3, 3], '', '\x1b[34mTEST\x1b[0m--------'),
([2, 1, 2, 1, 2, 1, 2, 1], '', '\x1b[34mTEST\x1b[0m--------'),
([1] * 12, '', '\x1b[34mTEST\x1b[0m--------'),
([2, 4], '', '\x1b[34mTEST\x1b[0m--'),
([1, 4], '', '\x1b[34mTEST\x1b[0m-'),
([1, 3], '', '\x1b[34mTEST\x1b[0m'),
([1, 2], '', '---'),
([12], '+', '\x1b[34mTEST\x1b[0m--------'),
([0, 12], '+', '\x1b[34mTEST\x1b[0m---------'),
([12, 0], '+', '\x1b[34mTEST\x1b[0m--------+'),
([0, 0, 12], '+', '\x1b[34mTEST\x1b[0m----------'),
([12, 0, 0], '+', '\x1b[34mTEST\x1b[0m--------++'),
([3, 3], '+', '\x1b[34mTEST\x1b[0m---'),
([4, 2], '+', '\x1b[34mTEST\x1b[0m+--'),
([5, 1], '+', '\x1b[34mTEST\x1b[0m-+-'),
([3, 3, 3, 3], '+', '\x1b[34mTEST\x1b[0m---+---+---'),
([2, 2, 4, 4], '+', '\x1b[34mTEST\x1b[0m-+----+----'),
([1, 1, 5, 5], '+', '\x1b[34mTEST\x1b[0m-----+-----'),
([2, 2, 2, 2], '+', '\x1b[34mTEST\x1b[0m-+--+--'),
([1, 1, 1, 1, 1], '+', '\x1b[34mTEST\x1b[0m-+-+-'),
([0, 0, 0, 0, 0, 0, 0], '+', '\x1b[34mTEST\x1b[0m++'),
([1, 1], '+', '-+-'),
])
@pytest.mark.parametrize('left,right', [('', ''), ('<', '>')])
@pytest.mark.parametrize('title', [
'\x1b[34mTEST\x1b[0m',
Color('{blue}TEST{/all}'),
Fore.BLUE + 'TEST' + Style.RESET_ALL,
colored('TEST', 'blue'),
])
def test_colors(outer_widths, left, intersect, right, title, expected):
"""Test with color title characters.
:param iter outer_widths: List of integers representing column widths with padding.
:param str left: Left border.
:param str intersect: Column separator.
:param str right: Right border.
:param title: Title in border with color codes.
:param str expected: Expected output.
"""
if left and right:
expected = left + expected + right
actual = build_border(outer_widths, '-', left, intersect, right, title=title)
assert ''.join(actual) == expected
@pytest.mark.parametrize('outer_widths,title,expected', [
([3, 3, 3], 123, '<123+---+--->'),
([3, 3, 3], 0.9, '<0.9+---+--->'),
([3, 3, 3], True, '<True---+--->'),
([3, 3, 3], False, '<False--+--->'),
])
def test_non_string(outer_widths, title, expected):
"""Test with non-string values.
:param iter outer_widths: List of integers representing column widths with padding.
:param title: Title in border.
:param str expected: Expected output.
"""
actual = build_border(outer_widths, '-', '<', '+', '>', title=title)
assert ''.join(actual) == expected

View file

@ -0,0 +1,104 @@
"""Test function in module."""
from terminaltables.build import build_row
def test_one_line():
"""Test with one line cells."""
row = [
['Left Cell'], ['Center Cell'], ['Right Cell'],
]
actual = [tuple(i) for i in build_row(row, '>', '|', '<')]
expected = [
('>', 'Left Cell', '|', 'Center Cell', '|', 'Right Cell', '<'),
]
assert actual == expected
def test_two_line():
"""Test with two line cells."""
row = [
[
'Left ',
'Cell1',
],
[
'Center',
'Cell2 ',
],
[
'Right',
'Cell3',
],
]
actual = [tuple(i) for i in build_row(row, '>', '|', '<')]
expected = [
('>', 'Left ', '|', 'Center', '|', 'Right', '<'),
('>', 'Cell1', '|', 'Cell2 ', '|', 'Cell3', '<'),
]
assert actual == expected
def test_three_line():
"""Test with three line cells."""
row = [
[
'Left ',
'Cell1',
' ',
],
[
'Center',
'Cell2 ',
' ',
],
[
'Right',
'Cell3',
' ',
],
]
actual = [tuple(i) for i in build_row(row, '>', '|', '<')]
expected = [
('>', 'Left ', '|', 'Center', '|', 'Right', '<'),
('>', 'Cell1', '|', 'Cell2 ', '|', 'Cell3', '<'),
('>', ' ', '|', ' ', '|', ' ', '<'),
]
assert actual == expected
def test_single():
"""Test with single cell."""
actual = [tuple(i) for i in build_row([['Cell']], '>', '|', '<')]
expected = [
('>', 'Cell', '<'),
]
assert actual == expected
def test_empty():
"""Test with empty cell."""
actual = [tuple(i) for i in build_row([['']], '>', '|', '<')]
expected = [
('>', '', '<'),
]
assert actual == expected
def test_no_cells():
"""Test with no cells."""
actual = [tuple(i) for i in build_row([[]], '>', '|', '<')]
expected = [
('>', '<'),
]
assert actual == expected
actual = [tuple(i) for i in build_row([], '>', '|', '<')]
expected = [
('>', '<'),
]
assert actual == expected

View file

@ -0,0 +1,37 @@
"""Test function in module."""
import pytest
from terminaltables.build import combine
@pytest.mark.parametrize('generator', [False, True])
def test_borders(generator):
"""Test with borders.
:param bool generator: Test with generator instead of list.
"""
line = ['One', 'Two', 'Three']
actual = list(combine(iter(line) if generator else line, '>', '|', '<'))
assert actual == ['>', 'One', '|', 'Two', '|', 'Three', '<']
@pytest.mark.parametrize('generator', [False, True])
def test_no_border(generator):
"""Test without borders.
:param bool generator: Test with generator instead of list.
"""
line = ['One', 'Two', 'Three']
actual = list(combine(iter(line) if generator else line, '', '', ''))
assert actual == ['One', 'Two', 'Three']
@pytest.mark.parametrize('generator', [False, True])
def test_no_items(generator):
"""Test with empty list.
:param bool generator: Test with generator instead of list.
"""
actual = list(combine(iter([]) if generator else [], '>', '|', '<'))
assert actual == ['>', '<']

View file

@ -0,0 +1,25 @@
"""Test function in module."""
from terminaltables.build import flatten
def test_one_line():
"""Test with one line cells."""
table = [
['>', 'Left Cell', '|', 'Center Cell', '|', 'Right Cell', '<'],
]
actual = flatten(table)
expected = '>Left Cell|Center Cell|Right Cell<'
assert actual == expected
def test_two_line():
"""Test with two line cells."""
table = [
['>', 'Left ', '|', 'Center', '|', 'Right', '<'],
['>', 'Cell1', '|', 'Cell2 ', '|', 'Cell3', '<'],
]
actual = flatten(table)
expected = ('>Left |Center|Right<\n'
'>Cell1|Cell2 |Cell3<')
assert actual == expected

32
tests/test_examples.py Normal file
View file

@ -0,0 +1,32 @@
"""Test example scripts."""
from __future__ import print_function
import os
import subprocess
import sys
import pytest
from tests import PROJECT_ROOT
@pytest.mark.parametrize('filename', map('example{0}.py'.format, (1, 2, 3)))
def test(filename):
"""Test with subprocess.
:param str filename: Example script filename to run.
"""
command = [sys.executable, str(PROJECT_ROOT.join(filename))]
env = dict(os.environ, PYTHONIOENCODING='utf-8')
# Run.
proc = subprocess.Popen(command, env=env, stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
output = proc.communicate()[0]
# Verify.
try:
assert proc.poll() == 0
except AssertionError:
print(output)
raise

View file

@ -0,0 +1,45 @@
"""Common objects used by tests in directory."""
from terminaltables import terminal_io
class MockKernel32(object):
"""Mock kernel32."""
def __init__(self, stderr=terminal_io.INVALID_HANDLE_VALUE, stdout=terminal_io.INVALID_HANDLE_VALUE):
"""Constructor."""
self.stderr = stderr
self.stdout = stdout
self.csbi_err = b'x\x00)#\x00\x00\x87\x05\x07\x00\x00\x00j\x05w\x00\x87\x05x\x00J\x00' # 119 x 29
self.csbi_out = b'L\x00,\x01\x00\x00*\x01\x07\x00\x00\x00\x0e\x01K\x00*\x01L\x00L\x00' # 75 x 28
self.setConsoleTitleA_called = False
self.setConsoleTitleW_called = False
def GetConsoleScreenBufferInfo(self, handle, lpcsbi): # noqa
"""Mock GetConsoleScreenBufferInfo.
:param handle: Unused handle.
:param lpcsbi: ctypes.create_string_buffer() return value.
"""
if handle == self.stderr:
lpcsbi.raw = self.csbi_err
else:
lpcsbi.raw = self.csbi_out
return 1
def GetStdHandle(self, handle): # noqa
"""Mock GetStdHandle.
:param int handle: STD_ERROR_HANDLE or STD_OUTPUT_HANDLE.
"""
return self.stderr if handle == terminal_io.STD_ERROR_HANDLE else self.stdout
def SetConsoleTitleA(self, _): # noqa
"""Mock SetConsoleTitleA."""
self.setConsoleTitleA_called = True
return 1
def SetConsoleTitleW(self, _): # noqa
"""Mock SetConsoleTitleW."""
self.setConsoleTitleW_called = True
return 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -0,0 +1,28 @@
# coding: utf-8
"""Test function in module."""
import ctypes
import pytest
from terminaltables.terminal_io import get_console_info, INVALID_HANDLE_VALUE, IS_WINDOWS
from tests.test_terminal_io import MockKernel32
def test():
"""Test function."""
# Test live WinError.
if IS_WINDOWS:
with pytest.raises(OSError):
get_console_info(ctypes.windll.kernel32, 0)
# Test INVALID_HANDLE_VALUE.
kernel32 = MockKernel32(stderr=1)
with pytest.raises(OSError):
get_console_info(kernel32, INVALID_HANDLE_VALUE)
# Test no error with mock methods.
width, height = get_console_info(kernel32, 1)
assert width == 119
assert height == 29

View file

@ -0,0 +1,110 @@
# coding: utf-8
"""Test function in module."""
import sys
from textwrap import dedent
import py
import pytest
from terminaltables.terminal_io import IS_WINDOWS, set_terminal_title
from tests import PROJECT_ROOT
from tests.screenshot import RunNewConsole, screenshot_until_match
from tests.test_terminal_io import MockKernel32
HERE = py.path.local(__file__).dirpath()
@pytest.mark.parametrize('is_windows', [False, True])
@pytest.mark.parametrize('mode', ['ascii', 'unicode', 'bytes'])
def test(monkeypatch, is_windows, mode):
"""Test function.
:param monkeypatch: pytest fixture.
:param bool is_windows: Monkeypatch terminal_io.IS_WINDOWS
:param str mode: Scenario to test for.
"""
monkeypatch.setattr('terminaltables.terminal_io.IS_WINDOWS', is_windows)
kernel32 = MockKernel32()
# Title.
if mode == 'ascii':
title = 'Testing terminaltables.'
elif mode == 'unicode':
title = u'Testing terminaltables with unicode: 世界你好蓝色'
else:
title = b'Testing terminaltables with bytes.'
# Run.
assert set_terminal_title(title, kernel32)
if not is_windows:
return
# Verify.
if mode == 'ascii':
assert kernel32.setConsoleTitleA_called
assert not kernel32.setConsoleTitleW_called
elif mode == 'unicode':
assert not kernel32.setConsoleTitleA_called
assert kernel32.setConsoleTitleW_called
else:
assert kernel32.setConsoleTitleA_called
assert not kernel32.setConsoleTitleW_called
@pytest.mark.skipif(str(not IS_WINDOWS))
@pytest.mark.skipif('True') # https://github.com/Robpol86/terminaltables/issues/30
@pytest.mark.parametrize('mode', ['ascii', 'unicode', 'bytes'])
def test_windows_screenshot(tmpdir, mode):
"""Test function on Windows in a new console window. Take a screenshot to verify it works.
:param tmpdir: pytest fixture.
:param str mode: Scenario to test for.
"""
script = tmpdir.join('script.py')
command = [sys.executable, str(script)]
change_title = tmpdir.join('change_title')
screenshot = PROJECT_ROOT.join('test_terminal_io.png')
if screenshot.check():
screenshot.remove()
# Determine title.
if mode == 'ascii':
title = "'test ASCII test'"
elif mode == 'unicode':
title = u"u'test 世界你好蓝色 test'"
else:
title = "b'test ASCII test'"
# Generate script.
script_template = dedent(u"""\
# coding: utf-8
from __future__ import print_function
import os, time
from terminaltables.terminal_io import set_terminal_title
stop_after = time.time() + 20
print('Waiting for FindWindowA() in RunNewConsole.__enter__()...')
while not os.path.exists(r'{change_title}') and time.time() < stop_after:
time.sleep(0.5)
assert set_terminal_title({title}) is True
print('Waiting for screenshot_until_match()...')
while not os.path.exists(r'{screenshot}') and time.time() < stop_after:
time.sleep(0.5)
""")
script_contents = script_template.format(change_title=str(change_title), title=title, screenshot=str(screenshot))
script.write(script_contents.encode('utf-8'), mode='wb')
# Setup expected.
if mode == 'unicode':
sub_images = [str(p) for p in HERE.listdir('sub_title_cjk_*.bmp')]
else:
sub_images = [str(p) for p in HERE.listdir('sub_title_ascii_*.bmp')]
assert sub_images
# Run.
with RunNewConsole(command) as gen:
change_title.ensure(file=True) # Touch file.
screenshot_until_match(str(screenshot), 15, sub_images, 1, gen)

View file

@ -0,0 +1,54 @@
# coding: utf-8
"""Test function in module."""
import pytest
from terminaltables.terminal_io import DEFAULT_HEIGHT, DEFAULT_WIDTH, INVALID_HANDLE_VALUE, IS_WINDOWS, terminal_size
from tests.test_terminal_io import MockKernel32
@pytest.mark.parametrize('stderr', [1, INVALID_HANDLE_VALUE])
@pytest.mark.parametrize('stdout', [2, INVALID_HANDLE_VALUE])
def test_windows(monkeypatch, stderr, stdout):
"""Test function with IS_WINDOWS=True.
:param monkeypatch: pytest fixture.
:param int stderr: Mock handle value.
:param int stdout: Mock handle value.
"""
monkeypatch.setattr('terminaltables.terminal_io.IS_WINDOWS', True)
kernel32 = MockKernel32(stderr=stderr, stdout=stdout)
width, height = terminal_size(kernel32)
if stderr == INVALID_HANDLE_VALUE and stdout == INVALID_HANDLE_VALUE:
assert width == DEFAULT_WIDTH
assert height == DEFAULT_HEIGHT
elif stdout == INVALID_HANDLE_VALUE:
assert width == 119
assert height == 29
elif stderr == INVALID_HANDLE_VALUE:
assert width == 75
assert height == 28
else:
assert width == 119
assert height == 29
@pytest.mark.skipif(str(IS_WINDOWS))
def test_nix(monkeypatch):
"""Test function with IS_WINDOWS=False.
:param monkeypatch: pytest fixture.
"""
# Test exception (no terminal within pytest).
width, height = terminal_size()
assert width == DEFAULT_WIDTH
assert height == DEFAULT_HEIGHT
# Test mocked.
monkeypatch.setattr('fcntl.ioctl', lambda *_: b'\x1d\x00w\x00\xca\x02\x96\x01')
width, height = terminal_size()
assert width == 119
assert height == 29

View file

@ -0,0 +1,202 @@
# coding: utf-8
"""Test function in module."""
import pytest
from colorama import Fore
from colorclass import Color
from termcolor import colored
from terminaltables.width_and_alignment import align_and_pad_cell
@pytest.mark.parametrize('string,align,width,expected', [
('test', '', 4, ['test']),
(123, '', 3, ['123']),
(0.9, '', 3, ['0.9']),
(None, '', 4, ['None']),
(True, '', 4, ['True']),
(False, '', 5, ['False']),
(Color('{blue}Test{/blue}'), '', 4, ['\x1b[34mTest\x1b[39m']),
(Fore.BLUE + 'Test' + Fore.RESET, '', 4, ['\x1b[34mTest\x1b[39m']),
(colored('Test', 'blue'), '', 4, ['\x1b[34mTest\x1b[0m']),
('蓝色', '', 4, ['蓝色']),
(u'שלום', '', 4, [u'\u05e9\u05dc\u05d5\u05dd']),
(u'معرب', '', 4, [u'\u0645\u0639\u0631\u0628']),
('test', '', 5, ['test ']),
(123, '', 4, ['123 ']),
(0.9, '', 4, ['0.9 ']),
(None, '', 5, ['None ']),
(True, '', 5, ['True ']),
(False, '', 6, ['False ']),
(Color('{blue}Test{/blue}'), '', 5, ['\x1b[34mTest\x1b[39m ']),
(Fore.BLUE + 'Test' + Fore.RESET, '', 5, ['\x1b[34mTest\x1b[39m ']),
(colored('Test', 'blue'), '', 5, ['\x1b[34mTest\x1b[0m ']),
('蓝色', '', 5, ['蓝色 ']),
(u'שלום', '', 5, [u'\u05e9\u05dc\u05d5\u05dd ']),
(u'معرب', '', 5, [u'\u0645\u0639\u0631\u0628 ']),
('test', 'left', 5, ['test ']),
(123, 'left', 4, ['123 ']),
(0.9, 'left', 4, ['0.9 ']),
(None, 'left', 5, ['None ']),
(True, 'left', 5, ['True ']),
(False, 'left', 6, ['False ']),
(Color('{blue}Test{/blue}'), 'left', 5, ['\x1b[34mTest\x1b[39m ']),
(Fore.BLUE + 'Test' + Fore.RESET, 'left', 5, ['\x1b[34mTest\x1b[39m ']),
(colored('Test', 'blue'), 'left', 5, ['\x1b[34mTest\x1b[0m ']),
('蓝色', 'left', 5, ['蓝色 ']),
(u'שלום', 'left', 5, [u'\u05e9\u05dc\u05d5\u05dd ']),
(u'معرب', 'left', 5, [u'\u0645\u0639\u0631\u0628 ']),
('test', 'right', 5, [' test']),
(123, 'right', 4, [' 123']),
(0.9, 'right', 4, [' 0.9']),
(None, 'right', 5, [' None']),
(True, 'right', 5, [' True']),
(False, 'right', 6, [' False']),
(Color('{blue}Test{/blue}'), 'right', 5, [' \x1b[34mTest\x1b[39m']),
(Fore.BLUE + 'Test' + Fore.RESET, 'right', 5, [' \x1b[34mTest\x1b[39m']),
(colored('Test', 'blue'), 'right', 5, [' \x1b[34mTest\x1b[0m']),
('蓝色', 'right', 5, [' 蓝色']),
(u'שלום', 'right', 5, [u' \u05e9\u05dc\u05d5\u05dd']),
(u'معرب', 'right', 5, [u' \u0645\u0639\u0631\u0628']),
('test', 'center', 6, [' test ']),
(123, 'center', 5, [' 123 ']),
(0.9, 'center', 5, [' 0.9 ']),
(None, 'center', 6, [' None ']),
(True, 'center', 6, [' True ']),
(False, 'center', 7, [' False ']),
(Color('{blue}Test{/blue}'), 'center', 6, [' \x1b[34mTest\x1b[39m ']),
(Fore.BLUE + 'Test' + Fore.RESET, 'center', 6, [' \x1b[34mTest\x1b[39m ']),
(colored('Test', 'blue'), 'center', 6, [' \x1b[34mTest\x1b[0m ']),
('蓝色', 'center', 6, [' 蓝色 ']),
(u'שלום', 'center', 6, [u' \u05e9\u05dc\u05d5\u05dd ']),
(u'معرب', 'center', 6, [u' \u0645\u0639\u0631\u0628 ']),
])
def test_width(string, align, width, expected):
"""Test width and horizontal alignment.
:param str string: String to test.
:param str align: Horizontal alignment.
:param int width: Expand string to this width without padding.
:param list expected: Expected output string.
"""
actual = align_and_pad_cell(string, (align,), (width, 1), (0, 0, 0, 0))
assert actual == expected
@pytest.mark.parametrize('string,align,height,expected', [
('test', '', 1, ['test']),
(Color('{blue}Test{/blue}'), '', 1, ['\x1b[34mTest\x1b[39m']),
(Fore.BLUE + 'Test' + Fore.RESET, '', 1, ['\x1b[34mTest\x1b[39m']),
(colored('Test', 'blue'), '', 1, ['\x1b[34mTest\x1b[0m']),
('蓝色', '', 1, ['蓝色']),
(u'שלום', '', 1, [u'\u05e9\u05dc\u05d5\u05dd']),
(u'معرب', '', 1, [u'\u0645\u0639\u0631\u0628']),
('test', '', 2, ['test', ' ']),
(Color('{blue}Test{/blue}'), '', 2, ['\x1b[34mTest\x1b[39m', ' ']),
(Fore.BLUE + 'Test' + Fore.RESET, '', 2, ['\x1b[34mTest\x1b[39m', ' ']),
(colored('Test', 'blue'), '', 2, ['\x1b[34mTest\x1b[0m', ' ']),
('蓝色', '', 2, ['蓝色', ' ']),
(u'שלום', '', 2, [u'\u05e9\u05dc\u05d5\u05dd', ' ']),
(u'معرب', '', 2, [u'\u0645\u0639\u0631\u0628', ' ']),
('test', 'top', 2, ['test', ' ']),
(Color('{blue}Test{/blue}'), 'top', 2, ['\x1b[34mTest\x1b[39m', ' ']),
(Fore.BLUE + 'Test' + Fore.RESET, 'top', 2, ['\x1b[34mTest\x1b[39m', ' ']),
(colored('Test', 'blue'), 'top', 2, ['\x1b[34mTest\x1b[0m', ' ']),
('蓝色', 'top', 2, ['蓝色', ' ']),
(u'שלום', 'top', 2, [u'\u05e9\u05dc\u05d5\u05dd', ' ']),
(u'معرب', 'top', 2, [u'\u0645\u0639\u0631\u0628', ' ']),
('test', 'bottom', 2, [' ', 'test']),
(Color('{blue}Test{/blue}'), 'bottom', 2, [' ', '\x1b[34mTest\x1b[39m']),
(Fore.BLUE + 'Test' + Fore.RESET, 'bottom', 2, [' ', '\x1b[34mTest\x1b[39m']),
(colored('Test', 'blue'), 'bottom', 2, [' ', '\x1b[34mTest\x1b[0m']),
('蓝色', 'bottom', 2, [' ', '蓝色']),
(u'שלום', 'bottom', 2, [' ', u'\u05e9\u05dc\u05d5\u05dd']),
(u'معرب', 'bottom', 2, [' ', u'\u0645\u0639\u0631\u0628']),
('test', 'middle', 3, [' ', 'test', ' ']),
(Color('{blue}Test{/blue}'), 'middle', 3, [' ', '\x1b[34mTest\x1b[39m', ' ']),
(Fore.BLUE + 'Test' + Fore.RESET, 'middle', 3, [' ', '\x1b[34mTest\x1b[39m', ' ']),
(colored('Test', 'blue'), 'middle', 3, [' ', '\x1b[34mTest\x1b[0m', ' ']),
('蓝色', 'middle', 3, [' ', '蓝色', ' ']),
(u'שלום', 'middle', 3, [' ', u'\u05e9\u05dc\u05d5\u05dd', ' ']),
(u'معرب', 'middle', 3, [' ', u'\u0645\u0639\u0631\u0628', ' ']),
])
def test_height(string, align, height, expected):
"""Test height and vertical alignment.
:param str string: String to test.
:param str align: Horizontal alignment.
:param int height: Expand string to this height without padding.
:param list expected: Expected output string.
"""
actual = align_and_pad_cell(string, (align,), (4, height), (0, 0, 0, 0))
assert actual == expected
@pytest.mark.parametrize('string,align,expected', [
('', '', ['.......', '.......', '.......', '.......', '.......']),
('\n', '', ['.......', '.......', '.......', '.......', '.......']),
('a\nb\nc', '', ['.......', '.a.....', '.b.....', '.c.....', '.......']),
('test', '', ['.......', '.test..', '.......', '.......', '.......']),
('', 'left', ['.......', '.......', '.......', '.......', '.......']),
('\n', 'left', ['.......', '.......', '.......', '.......', '.......']),
('a\nb\nc', 'left', ['.......', '.a.....', '.b.....', '.c.....', '.......']),
('test', 'left', ['.......', '.test..', '.......', '.......', '.......']),
('', 'right', ['.......', '.......', '.......', '.......', '.......']),
('\n', 'right', ['.......', '.......', '.......', '.......', '.......']),
('a\nb\nc', 'right', ['.......', '.....a.', '.....b.', '.....c.', '.......']),
('test', 'right', ['.......', '..test.', '.......', '.......', '.......']),
('', 'center', ['.......', '.......', '.......', '.......', '.......']),
('\n', 'center', ['.......', '.......', '.......', '.......', '.......']),
('a\nb\nc', 'center', ['.......', '...a...', '...b...', '...c...', '.......']),
('test', 'center', ['.......', '..test.', '.......', '.......', '.......']),
('', 'top', ['.......', '.......', '.......', '.......', '.......']),
('\n', 'top', ['.......', '.......', '.......', '.......', '.......']),
('a\nb\nc', 'top', ['.......', '.a.....', '.b.....', '.c.....', '.......']),
('test', 'top', ['.......', '.test..', '.......', '.......', '.......']),
('', 'bottom', ['.......', '.......', '.......', '.......', '.......']),
('\n', 'bottom', ['.......', '.......', '.......', '.......', '.......']),
('a\nb\nc', 'bottom', ['.......', '.a.....', '.b.....', '.c.....', '.......']),
('test', 'bottom', ['.......', '.......', '.......', '.test..', '.......']),
('', 'middle', ['.......', '.......', '.......', '.......', '.......']),
('\n', 'middle', ['.......', '.......', '.......', '.......', '.......']),
('a\nb\nc', 'middle', ['.......', '.a.....', '.b.....', '.c.....', '.......']),
('test', 'middle', ['.......', '.......', '.test..', '.......', '.......']),
(
u'蓝色\nשלום\nمعرب',
'',
['.......', u'.蓝色..', u'.\u05e9\u05dc\u05d5\u05dd..', u'.\u0645\u0639\u0631\u0628..', '.......']
),
(
'\n'.join((Color('{blue}Test{/blue}'), Fore.BLUE + 'Test' + Fore.RESET, colored('Test', 'blue'))),
'',
['.......', '.\x1b[34mTest\x1b[39m..', '.\x1b[34mTest\x1b[39m..', '.\x1b[34mTest\x1b[0m..', '.......']
),
# (Color('{blue}A\nB{/blue}'), '', '.......\n.\x1b[34mA\x1b[39m.....\n.\x1b[34mB\x1b[39m.....\n.......\n.......'),
])
def test_odd_width_height_pad_space(string, align, expected):
"""Test odd number width, height, padding, and dots for whitespaces.
:param str string: String to test.
:param str align: Alignment in any dimension but one at a time.
:param list expected: Expected output string.
"""
actual = align_and_pad_cell(string, (align,), (5, 3), (1, 1, 1, 1), '.')
assert actual == expected

View file

@ -0,0 +1,107 @@
"""Test function in module."""
import pytest
from terminaltables.width_and_alignment import column_max_width, max_dimensions
@pytest.fixture(autouse=True)
def patch(monkeypatch):
"""Monkeypatch before every test function in this module.
:param monkeypatch: pytest fixture.
"""
monkeypatch.setattr('terminaltables.width_and_alignment.terminal_size', lambda: (79, 24))
def test_empty():
"""Test with zero-length cells."""
assert column_max_width(max_dimensions([['']])[0], 0, 0, 0, 0) == 79
assert column_max_width(max_dimensions([['', '', '']])[0], 0, 0, 0, 0) == 79
assert column_max_width(max_dimensions([['', '', ''], ['', '', '']])[0], 0, 0, 0, 0) == 79
assert column_max_width(max_dimensions([['']])[0], 0, 2, 1, 2) == 75
assert column_max_width(max_dimensions([['', '', '']])[0], 0, 2, 1, 2) == 69
assert column_max_width(max_dimensions([['', '', ''], ['', '', '']])[0], 0, 2, 1, 2) == 69
def test_single_line():
"""Test with single-line cells."""
table_data = [
['Name', 'Color', 'Type'],
['Avocado', 'green', 'nut'],
['Tomato', 'red', 'fruit'],
['Lettuce', 'green', 'vegetable'],
]
inner_widths = max_dimensions(table_data)[0]
# '| Lettuce | green | vegetable |'
outer, inner, padding = 2, 1, 2
assert column_max_width(inner_widths, 0, outer, inner, padding) == 55
assert column_max_width(inner_widths, 1, outer, inner, padding) == 53
assert column_max_width(inner_widths, 2, outer, inner, padding) == 57
# ' Lettuce | green | vegetable '
outer = 0
assert column_max_width(inner_widths, 0, outer, inner, padding) == 57
assert column_max_width(inner_widths, 1, outer, inner, padding) == 55
assert column_max_width(inner_widths, 2, outer, inner, padding) == 59
# '| Lettuce green vegetable |'
outer, inner = 2, 0
assert column_max_width(inner_widths, 0, outer, inner, padding) == 57
assert column_max_width(inner_widths, 1, outer, inner, padding) == 55
assert column_max_width(inner_widths, 2, outer, inner, padding) == 59
# ' Lettuce green vegetable '
outer = 0
assert column_max_width(inner_widths, 0, outer, inner, padding) == 59
assert column_max_width(inner_widths, 1, outer, inner, padding) == 57
assert column_max_width(inner_widths, 2, outer, inner, padding) == 61
# '|Lettuce |green |vegetable |'
outer, inner, padding = 2, 1, 1
assert column_max_width(inner_widths, 0, outer, inner, padding) == 58
assert column_max_width(inner_widths, 1, outer, inner, padding) == 56
assert column_max_width(inner_widths, 2, outer, inner, padding) == 60
# '|Lettuce |green |vegetable |'
padding = 5
assert column_max_width(inner_widths, 0, outer, inner, padding) == 46
assert column_max_width(inner_widths, 1, outer, inner, padding) == 44
assert column_max_width(inner_widths, 2, outer, inner, padding) == 48
table_data = [
['Name', 'Color', 'Type'],
['Avocado', 'green', 'nut'],
['Tomato', 'red', 'fruit'],
['Lettuce', 'green', 'vegetable'],
['Watermelon', 'green', 'fruit'],
]
inner_widths = max_dimensions(table_data)[0]
outer, inner, padding = 2, 1, 2
assert column_max_width(inner_widths, 0, outer, inner, padding) == 55
assert column_max_width(inner_widths, 1, outer, inner, padding) == 50
assert column_max_width(inner_widths, 2, outer, inner, padding) == 54
def test_multi_line(monkeypatch):
"""Test with multi-line cells.
:param monkeypatch: pytest fixture.
"""
table_data = [
['Show', 'Characters'],
['Rugrats', ('Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles,\n'
'Susie Carmichael, Dil Pickles, Kimi Finster, Spike')],
['South Park', 'Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick']
]
inner_widths = max_dimensions(table_data)[0]
outer, inner, padding = 2, 1, 2
assert column_max_width(inner_widths, 0, outer, inner, padding) == -11
assert column_max_width(inner_widths, 1, outer, inner, padding) == 62
monkeypatch.setattr('terminaltables.width_and_alignment.terminal_size', lambda: (100, 24))
assert column_max_width(inner_widths, 0, outer, inner, padding) == 10
assert column_max_width(inner_widths, 1, outer, inner, padding) == 83

View file

@ -0,0 +1,100 @@
# coding: utf-8
"""Test function in module."""
import pytest
from colorama import Fore
from colorclass import Color
from termcolor import colored
from terminaltables.width_and_alignment import max_dimensions
@pytest.mark.parametrize('table_data,expected_w,expected_h', [
([[]], [], [0]),
([['']], [0], [0]),
([['', '']], [0, 0], [0]),
([[], []], [], [0, 0]),
([[''], ['']], [0], [0, 0]),
([['', ''], ['', '']], [0, 0], [0, 0]),
])
def test_zero_length(table_data, expected_w, expected_h):
"""Test zero-length or empty tables.
:param list table_data: Input table data to test.
:param list expected_w: Expected widths.
:param list expected_h: Expected heights.
"""
actual = max_dimensions(table_data)
assert actual == (expected_w, expected_h, expected_w, expected_h)
def test_single_line():
"""Test widths."""
table_data = [
['Name', 'Color', 'Type'],
['Avocado', 'green', 'nut'],
['Tomato', 'red', 'fruit'],
['Lettuce', 'green', 'vegetable'],
]
assert max_dimensions(table_data, 1, 1) == ([7, 5, 9], [1, 1, 1, 1], [9, 7, 11], [1, 1, 1, 1])
table_data.append(['Watermelon', 'green', 'fruit'])
assert max_dimensions(table_data, 2, 2) == ([10, 5, 9], [1, 1, 1, 1, 1], [14, 9, 13], [1, 1, 1, 1, 1])
def test_multi_line():
"""Test heights."""
table_data = [
['One\nTwo', 'Buckle\nMy\nShoe'],
]
assert max_dimensions(table_data, 0, 0, 1, 1) == ([3, 6], [3], [3, 6], [5])
table_data = [
['Show', 'Characters'],
['Rugrats', ('Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles,\n'
'Susie Carmichael, Dil Pickles, Kimi Finster, Spike')],
['South Park', 'Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick']
]
assert max_dimensions(table_data, 0, 0, 2, 2) == ([10, 83], [1, 2, 1], [10, 83], [5, 6, 5])
def test_trailing_newline():
r"""Test with trailing \n."""
table_data = [
['Row One\n<blank>'],
['<blank>\nRow Two'],
['Row Three\n'],
['\nRow Four'],
]
assert max_dimensions(table_data) == ([9], [2, 2, 2, 2], [9], [2, 2, 2, 2])
def test_colors_cjk_rtl():
"""Test color text, CJK characters, and RTL characters."""
table_data = [
[Color('{blue}Test{/blue}')],
[Fore.BLUE + 'Test' + Fore.RESET],
[colored('Test', 'blue')],
]
assert max_dimensions(table_data) == ([4], [1, 1, 1], [4], [1, 1, 1])
table_data = [
['蓝色'],
['世界你好'],
]
assert max_dimensions(table_data) == ([8], [1, 1], [8], [1, 1])
table_data = [
['שלום'],
['معرب'],
]
assert max_dimensions(table_data) == ([4], [1, 1], [4], [1, 1])
def test_non_string():
"""Test with non-string values."""
table_data = [
[123, 0.9, None, True, False],
]
assert max_dimensions(table_data) == ([3, 3, 4, 4, 5], [1], [3, 3, 4, 4, 5], [1])

View file

@ -0,0 +1,70 @@
"""Test function in module."""
from terminaltables.width_and_alignment import max_dimensions, table_width
def test_empty():
"""Test with zero-length cells."""
assert table_width(max_dimensions([['']])[2], 0, 0) == 0
assert table_width(max_dimensions([['', '', '']])[2], 0, 0) == 0
assert table_width(max_dimensions([['', '', ''], ['', '', '']])[2], 0, 0) == 0
assert table_width(max_dimensions([['']], 1, 1)[2], 2, 1) == 4
assert table_width(max_dimensions([['', '', '']], 1, 1)[2], 2, 1) == 10
assert table_width(max_dimensions([['', '', ''], ['', '', '']], 1, 1)[2], 2, 1) == 10
def test_single_line():
"""Test with single-line cells."""
table_data = [
['Name', 'Color', 'Type'],
['Avocado', 'green', 'nut'],
['Tomato', 'red', 'fruit'],
['Lettuce', 'green', 'vegetable'],
]
# '| Lettuce | green | vegetable |'
outer, inner, outer_widths = 2, 1, max_dimensions(table_data, 1, 1)[2]
assert table_width(outer_widths, outer, inner) == 31
# ' Lettuce | green | vegetable '
outer = 0
assert table_width(outer_widths, outer, inner) == 29
# '| Lettuce green vegetable |'
outer, inner = 2, 0
assert table_width(outer_widths, outer, inner) == 29
# ' Lettuce green vegetable '
outer = 0
assert table_width(outer_widths, outer, inner) == 27
# '|Lettuce |green |vegetable |'
outer, inner, outer_widths = 2, 1, max_dimensions(table_data, 1)[2]
assert table_width(outer_widths, outer, inner) == 28
# '|Lettuce |green |vegetable |'
outer_widths = max_dimensions(table_data, 3, 2)[2]
assert table_width(outer_widths, outer, inner) == 40
table_data = [
['Name', 'Color', 'Type'],
['Avocado', 'green', 'nut'],
['Tomato', 'red', 'fruit'],
['Lettuce', 'green', 'vegetable'],
['Watermelon', 'green', 'fruit'],
]
outer, inner, outer_widths = 2, 1, max_dimensions(table_data, 1, 1)[2]
assert table_width(outer_widths, outer, inner) == 34
def test_multi_line():
"""Test with multi-line cells."""
table_data = [
['Show', 'Characters'],
['Rugrats', ('Tommy Pickles, Chuckie Finster, Phillip DeVille, Lillian DeVille, Angelica Pickles,\n'
'Susie Carmichael, Dil Pickles, Kimi Finster, Spike')],
['South Park', 'Stan Marsh, Kyle Broflovski, Eric Cartman, Kenny McCormick']
]
outer, inner, outer_widths = 2, 1, max_dimensions(table_data, 1, 1)[2]
assert table_width(outer_widths, outer, inner) == 100

View file

@ -0,0 +1,59 @@
# coding: utf-8
"""Test function in module."""
import pytest
from colorama import Fore
from colorclass import Color
from termcolor import colored
from terminaltables.width_and_alignment import visible_width
@pytest.mark.parametrize('string,expected', [
# str
('hello, world', 12),
('世界你好', 8),
('蓝色', 4),
('שלום', 4),
('معرب', 4),
('hello 世界', 10),
# str+ansi
('\x1b[34mhello, world\x1b[39m', 12),
('\x1b[34m世界你好\x1b[39m', 8),
('\x1b[34m蓝色\x1b[39m', 4),
('\x1b[34mשלום\x1b[39m', 4),
('\x1b[34mمعرب\x1b[39m', 4),
('\x1b[34mhello 世界\x1b[39m', 10),
# colorclass
(Color(u'{blue}hello, world{/blue}'), 12),
(Color(u'{blue}世界你好{/blue}'), 8),
(Color(u'{blue}蓝色{/blue}'), 4),
(Color(u'{blue}שלום{/blue}'), 4),
(Color(u'{blue}معرب{/blue}'), 4),
(Color(u'{blue}hello 世界{/blue}'), 10),
# colorama
(Fore.BLUE + 'hello, world' + Fore.RESET, 12),
(Fore.BLUE + '世界你好' + Fore.RESET, 8),
(Fore.BLUE + '蓝色' + Fore.RESET, 4),
(Fore.BLUE + 'שלום' + Fore.RESET, 4),
(Fore.BLUE + 'معرب' + Fore.RESET, 4),
(Fore.BLUE + 'hello 世界' + Fore.RESET, 10),
# termcolor
(colored('hello, world', 'blue'), 12),
(colored('世界你好', 'blue'), 8),
(colored('蓝色', 'blue'), 4),
(colored('שלום', 'blue'), 4),
(colored('معرب', 'blue'), 4),
(colored('hello 世界', 'blue'), 10),
])
def test(string, expected):
"""Test function with different color libraries.
:param str string: Input string to measure.
:param int expected: Expected visible width of string (some characters are len() == 1 but take up 2 spaces).
"""
assert visible_width(string) == expected

77
tox.ini Normal file
View file

@ -0,0 +1,77 @@
[general]
name = terminaltables
[tox]
envlist = lint,py{34,27,26}
[testenv]
commands =
python -c "import os, sys; sys.platform == 'win32' and os.system('easy_install pillow')"
py.test --cov-report term-missing --cov-report xml --cov {[general]name} --cov-config tox.ini {posargs:tests}
deps =
colorama==0.3.7
colorclass==2.2.0
pytest-cov==2.4.0
termcolor==1.1.0
passenv =
WINDIR
setenv =
PYTHON_EGG_CACHE = {envtmpdir}
usedevelop = True
[testenv:lint]
commands =
python setup.py check --strict
python setup.py check --strict -m
python setup.py check --strict -s
python setup.py check_version
flake8 --application-import-names={[general]name},tests
pylint --rcfile=tox.ini setup.py {[general]name}
deps =
flake8-docstrings==1.0.2
flake8-import-order==0.9.2
flake8==3.0.4
pep8-naming==0.4.1
pylint==1.6.4
[testenv:docs]
changedir = {toxinidir}/docs
commands =
sphinx-build . _build/html {posargs}
deps =
robpol86-sphinxcontrib-googleanalytics==0.1
sphinx-rtd-theme==0.1.10a0
sphinx==1.4.8
usedevelop = False
[testenv:docsV]
commands =
sphinx-versioning push docs gh-pages .
deps =
{[testenv:docs]deps}
sphinxcontrib-versioning==2.2.0
passenv =
HOME
HOSTNAME
SSH_AUTH_SOCK
TRAVIS*
USER
[flake8]
exclude = .tox/*,build/*,docs/*,env/*,get-pip.py
import-order-style = smarkets
max-line-length = 120
statistics = True
[pylint]
disable =
locally-disabled,
too-few-public-methods,
too-many-instance-attributes,
ignore = .tox/*,build/*,docs/*,env/*,get-pip.py
max-args = 6
max-line-length = 120
reports = no
[run]
branch = True