summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2022-09-16 09:09:35 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2022-09-16 09:09:35 +0000
commit0dfe1c9e2780469e3a4696e8fb3e6f717a7ebeb7 (patch)
treea0b651b55ea02e3b00bbc5eedba566fdd6bd7c08
parentInitial commit. (diff)
downloadterminaltables-0dfe1c9e2780469e3a4696e8fb3e6f717a7ebeb7.tar.xz
terminaltables-0dfe1c9e2780469e3a4696e8fb3e6f717a7ebeb7.zip
Adding upstream version 3.1.0.upstream/3.1.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.gitignore96
-rw-r--r--.travis.yml48
-rw-r--r--CONTRIBUTING.md47
-rw-r--r--LICENSE21
-rw-r--r--README.rst162
-rw-r--r--appveyor.yml33
-rw-r--r--docs/_templates/layout.html6
-rw-r--r--docs/asciitable.pngbin0 -> 44856 bytes
-rw-r--r--docs/asciitable.rst16
-rw-r--r--docs/changelog.rst5
-rw-r--r--docs/conf.py53
-rw-r--r--docs/doubletable.pngbin0 -> 45812 bytes
-rw-r--r--docs/doubletable.rst33
-rw-r--r--docs/examples.pngbin0 -> 72612 bytes
-rw-r--r--docs/favicon.icobin0 -> 1150 bytes
-rw-r--r--docs/githubtable.pngbin0 -> 40167 bytes
-rw-r--r--docs/githubtable.rst27
-rw-r--r--docs/githubtable_rendered.pngbin0 -> 53983 bytes
-rw-r--r--docs/index.rst77
-rw-r--r--docs/install.rst38
-rw-r--r--docs/key.encbin0 -> 3248 bytes
-rw-r--r--docs/quickstart.rst110
-rw-r--r--docs/settings.rst79
-rw-r--r--docs/singletable.pngbin0 -> 44507 bytes
-rw-r--r--docs/singletable.rst26
-rwxr-xr-xexample1.py42
-rwxr-xr-xexample2.py86
-rwxr-xr-xexample3.py36
-rwxr-xr-xsetup.py111
-rw-r--r--terminaltables/__init__.py17
-rw-r--r--terminaltables/ascii_table.py55
-rw-r--r--terminaltables/base_table.py217
-rw-r--r--terminaltables/build.py151
-rw-r--r--terminaltables/github_table.py70
-rw-r--r--terminaltables/other_tables.py177
-rw-r--r--terminaltables/terminal_io.py98
-rw-r--r--terminaltables/width_and_alignment.py160
-rw-r--r--tests/__init__.py5
-rw-r--r--tests/screenshot.py292
-rw-r--r--tests/test_all_tables_e2e/__init__.py1
-rw-r--r--tests/test_all_tables_e2e/sub_ascii_win10.bmpbin0 -> 36870 bytes
-rw-r--r--tests/test_all_tables_e2e/sub_ascii_winxp.bmpbin0 -> 49350 bytes
-rw-r--r--tests/test_all_tables_e2e/sub_double_win10.bmpbin0 -> 36090 bytes
-rw-r--r--tests/test_all_tables_e2e/sub_double_win10b.bmpbin0 -> 36090 bytes
-rw-r--r--tests/test_all_tables_e2e/sub_double_winxp.bmpbin0 -> 48726 bytes
-rw-r--r--tests/test_all_tables_e2e/sub_single_win10.bmpbin0 -> 34854 bytes
-rw-r--r--tests/test_all_tables_e2e/sub_single_win10b.bmpbin0 -> 34854 bytes
-rw-r--r--tests/test_all_tables_e2e/sub_single_winxp.bmpbin0 -> 45954 bytes
-rw-r--r--tests/test_all_tables_e2e/test_ascii_table.py145
-rw-r--r--tests/test_all_tables_e2e/test_double_table.py245
-rw-r--r--tests/test_all_tables_e2e/test_github_table.py77
-rw-r--r--tests/test_all_tables_e2e/test_porcelain_table.py59
-rw-r--r--tests/test_all_tables_e2e/test_single_table.py171
-rw-r--r--tests/test_all_tables_e2e/test_single_table_windows.py246
-rw-r--r--tests/test_ascii_table.py108
-rw-r--r--tests/test_base_table/test_gen_row_lines.py86
-rw-r--r--tests/test_base_table/test_gen_table.py225
-rw-r--r--tests/test_base_table/test_horizontal_border.py98
-rw-r--r--tests/test_base_table/test_table.py196
-rw-r--r--tests/test_build/test_build_border.py312
-rw-r--r--tests/test_build/test_build_row.py104
-rw-r--r--tests/test_build/test_combine.py37
-rw-r--r--tests/test_build/test_flatten.py25
-rw-r--r--tests/test_examples.py32
-rw-r--r--tests/test_terminal_io/__init__.py45
-rw-r--r--tests/test_terminal_io/sub_title_ascii_win10.bmpbin0 -> 1998 bytes
-rw-r--r--tests/test_terminal_io/sub_title_ascii_win2012.bmpbin0 -> 3048 bytes
-rw-r--r--tests/test_terminal_io/sub_title_ascii_winxp.bmpbin0 -> 2070 bytes
-rw-r--r--tests/test_terminal_io/sub_title_cjk_win10.bmpbin0 -> 4278 bytes
-rw-r--r--tests/test_terminal_io/sub_title_cjk_win2012.bmpbin0 -> 6896 bytes
-rw-r--r--tests/test_terminal_io/sub_title_cjk_winxp.bmpbin0 -> 4322 bytes
-rw-r--r--tests/test_terminal_io/test_get_console_info.py28
-rw-r--r--tests/test_terminal_io/test_set_terminal_title.py110
-rw-r--r--tests/test_terminal_io/test_terminal_size.py54
-rw-r--r--tests/test_width_and_alignment/test_align_and_pad_cell.py202
-rw-r--r--tests/test_width_and_alignment/test_column_max_width.py107
-rw-r--r--tests/test_width_and_alignment/test_max_dimensions.py100
-rw-r--r--tests/test_width_and_alignment/test_table_width.py70
-rw-r--r--tests/test_width_and_alignment/test_visible_width.py59
-rw-r--r--tox.ini77
80 files changed, 5413 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..53889c8
--- /dev/null
+++ b/.gitignore
@@ -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
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..1f9f3ed
--- /dev/null
+++ b/.travis.yml
@@ -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
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..bc22847
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -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!
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d314c3c
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..fe9044f
--- /dev/null
+++ b/README.rst
@@ -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
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000..5b0e517
--- /dev/null
+++ b/appveyor.yml
@@ -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
diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html
new file mode 100644
index 0000000..f78ddc4
--- /dev/null
+++ b/docs/_templates/layout.html
@@ -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'] %}
diff --git a/docs/asciitable.png b/docs/asciitable.png
new file mode 100644
index 0000000..97ae271
--- /dev/null
+++ b/docs/asciitable.png
Binary files differ
diff --git a/docs/asciitable.rst b/docs/asciitable.rst
new file mode 100644
index 0000000..d5120f3
--- /dev/null
+++ b/docs/asciitable.rst
@@ -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
diff --git a/docs/changelog.rst b/docs/changelog.rst
new file mode 100644
index 0000000..0b9ff7b
--- /dev/null
+++ b/docs/changelog.rst
@@ -0,0 +1,5 @@
+.. _changelog:
+
+.. include:: ../README.rst
+ :start-after: changelog-section-start
+ :end-before: changelog-section-end
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..fb33f09
--- /dev/null
+++ b/docs/conf.py
@@ -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')
diff --git a/docs/doubletable.png b/docs/doubletable.png
new file mode 100644
index 0000000..ce532a1
--- /dev/null
+++ b/docs/doubletable.png
Binary files differ
diff --git a/docs/doubletable.rst b/docs/doubletable.rst
new file mode 100644
index 0000000..da322ab
--- /dev/null
+++ b/docs/doubletable.rst
@@ -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
diff --git a/docs/examples.png b/docs/examples.png
new file mode 100644
index 0000000..62a623c
--- /dev/null
+++ b/docs/examples.png
Binary files differ
diff --git a/docs/favicon.ico b/docs/favicon.ico
new file mode 100644
index 0000000..8cf8947
--- /dev/null
+++ b/docs/favicon.ico
Binary files differ
diff --git a/docs/githubtable.png b/docs/githubtable.png
new file mode 100644
index 0000000..0b2a6a3
--- /dev/null
+++ b/docs/githubtable.png
Binary files differ
diff --git a/docs/githubtable.rst b/docs/githubtable.rst
new file mode 100644
index 0000000..ba53218
--- /dev/null
+++ b/docs/githubtable.rst
@@ -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
diff --git a/docs/githubtable_rendered.png b/docs/githubtable_rendered.png
new file mode 100644
index 0000000..319bbcb
--- /dev/null
+++ b/docs/githubtable_rendered.png
Binary files differ
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..002ca17
--- /dev/null
+++ b/docs/index.rst
@@ -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
diff --git a/docs/install.rst b/docs/install.rst
new file mode 100644
index 0000000..ea0bfae
--- /dev/null
+++ b/docs/install.rst
@@ -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
diff --git a/docs/key.enc b/docs/key.enc
new file mode 100644
index 0000000..f485db2
--- /dev/null
+++ b/docs/key.enc
Binary files differ
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
new file mode 100644
index 0000000..83e9ee8
--- /dev/null
+++ b/docs/quickstart.rst
@@ -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.
diff --git a/docs/settings.rst b/docs/settings.rst
new file mode 100644
index 0000000..c609d1f
--- /dev/null
+++ b/docs/settings.rst
@@ -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.
diff --git a/docs/singletable.png b/docs/singletable.png
new file mode 100644
index 0000000..cc595ff
--- /dev/null
+++ b/docs/singletable.png
Binary files differ
diff --git a/docs/singletable.rst b/docs/singletable.rst
new file mode 100644
index 0000000..9d6313b
--- /dev/null
+++ b/docs/singletable.rst
@@ -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
diff --git a/example1.py b/example1.py
new file mode 100755
index 0000000..daf1fbf
--- /dev/null
+++ b/example1.py
@@ -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()
diff --git a/example2.py b/example2.py
new file mode 100755
index 0000000..51644f8
--- /dev/null
+++ b/example2.py
@@ -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()
diff --git a/example3.py b/example3.py
new file mode 100755
index 0000000..bec5500
--- /dev/null
+++ b/example3.py
@@ -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()
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..0bc7bf2
--- /dev/null
+++ b/setup.py
@@ -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,
+ )
diff --git a/terminaltables/__init__.py b/terminaltables/__init__.py
new file mode 100644
index 0000000..6cea813
--- /dev/null
+++ b/terminaltables/__init__.py
@@ -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'
diff --git a/terminaltables/ascii_table.py b/terminaltables/ascii_table.py
new file mode 100644
index 0000000..3623918
--- /dev/null
+++ b/terminaltables/ascii_table.py
@@ -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)
diff --git a/terminaltables/base_table.py b/terminaltables/base_table.py
new file mode 100644
index 0000000..281d5a3
--- /dev/null
+++ b/terminaltables/base_table.py
@@ -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))
diff --git a/terminaltables/build.py b/terminaltables/build.py
new file mode 100644
index 0000000..6b23b2f
--- /dev/null
+++ b/terminaltables/build.py
@@ -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)
diff --git a/terminaltables/github_table.py b/terminaltables/github_table.py
new file mode 100644
index 0000000..7eb1be7
--- /dev/null
+++ b/terminaltables/github_table.py
@@ -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)
diff --git a/terminaltables/other_tables.py b/terminaltables/other_tables.py
new file mode 100644
index 0000000..50c0bcd
--- /dev/null
+++ b/terminaltables/other_tables.py
@@ -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
diff --git a/terminaltables/terminal_io.py b/terminaltables/terminal_io.py
new file mode 100644
index 0000000..8b8c10d
--- /dev/null
+++ b/terminaltables/terminal_io.py
@@ -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
diff --git a/terminaltables/width_and_alignment.py b/terminaltables/width_and_alignment.py
new file mode 100644
index 0000000..057e800
--- /dev/null
+++ b/terminaltables/width_and_alignment.py
@@ -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
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..b91337b
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,5 @@
+"""Allows importing from screenshot."""
+
+import py
+
+PROJECT_ROOT = py.path.local(__file__).dirpath().join('..')
diff --git a/tests/screenshot.py b/tests/screenshot.py
new file mode 100644
index 0000000..6ccb593
--- /dev/null
+++ b/tests/screenshot.py
@@ -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)
diff --git a/tests/test_all_tables_e2e/__init__.py b/tests/test_all_tables_e2e/__init__.py
new file mode 100644
index 0000000..785cc5a
--- /dev/null
+++ b/tests/test_all_tables_e2e/__init__.py
@@ -0,0 +1 @@
+"""Allows importing from screenshot."""
diff --git a/tests/test_all_tables_e2e/sub_ascii_win10.bmp b/tests/test_all_tables_e2e/sub_ascii_win10.bmp
new file mode 100644
index 0000000..fe21fa7
--- /dev/null
+++ b/tests/test_all_tables_e2e/sub_ascii_win10.bmp
Binary files differ
diff --git a/tests/test_all_tables_e2e/sub_ascii_winxp.bmp b/tests/test_all_tables_e2e/sub_ascii_winxp.bmp
new file mode 100644
index 0000000..4105d11
--- /dev/null
+++ b/tests/test_all_tables_e2e/sub_ascii_winxp.bmp
Binary files differ
diff --git a/tests/test_all_tables_e2e/sub_double_win10.bmp b/tests/test_all_tables_e2e/sub_double_win10.bmp
new file mode 100644
index 0000000..e6b00ae
--- /dev/null
+++ b/tests/test_all_tables_e2e/sub_double_win10.bmp
Binary files differ
diff --git a/tests/test_all_tables_e2e/sub_double_win10b.bmp b/tests/test_all_tables_e2e/sub_double_win10b.bmp
new file mode 100644
index 0000000..a527959
--- /dev/null
+++ b/tests/test_all_tables_e2e/sub_double_win10b.bmp
Binary files differ
diff --git a/tests/test_all_tables_e2e/sub_double_winxp.bmp b/tests/test_all_tables_e2e/sub_double_winxp.bmp
new file mode 100644
index 0000000..aae7b24
--- /dev/null
+++ b/tests/test_all_tables_e2e/sub_double_winxp.bmp
Binary files differ
diff --git a/tests/test_all_tables_e2e/sub_single_win10.bmp b/tests/test_all_tables_e2e/sub_single_win10.bmp
new file mode 100644
index 0000000..ff6f272
--- /dev/null
+++ b/tests/test_all_tables_e2e/sub_single_win10.bmp
Binary files differ
diff --git a/tests/test_all_tables_e2e/sub_single_win10b.bmp b/tests/test_all_tables_e2e/sub_single_win10b.bmp
new file mode 100644
index 0000000..c8d1e36
--- /dev/null
+++ b/tests/test_all_tables_e2e/sub_single_win10b.bmp
Binary files differ
diff --git a/tests/test_all_tables_e2e/sub_single_winxp.bmp b/tests/test_all_tables_e2e/sub_single_winxp.bmp
new file mode 100644
index 0000000..c4f5873
--- /dev/null
+++ b/tests/test_all_tables_e2e/sub_single_winxp.bmp
Binary files differ
diff --git a/tests/test_all_tables_e2e/test_ascii_table.py b/tests/test_all_tables_e2e/test_ascii_table.py
new file mode 100644
index 0000000..51ebc2a
--- /dev/null
+++ b/tests/test_all_tables_e2e/test_ascii_table.py
@@ -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)
diff --git a/tests/test_all_tables_e2e/test_double_table.py b/tests/test_all_tables_e2e/test_double_table.py
new file mode 100644
index 0000000..892357a
--- /dev/null
+++ b/tests/test_all_tables_e2e/test_double_table.py
@@ -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)
diff --git a/tests/test_all_tables_e2e/test_github_table.py b/tests/test_all_tables_e2e/test_github_table.py
new file mode 100644
index 0000000..6176215
--- /dev/null
+++ b/tests/test_all_tables_e2e/test_github_table.py
@@ -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
diff --git a/tests/test_all_tables_e2e/test_porcelain_table.py b/tests/test_all_tables_e2e/test_porcelain_table.py
new file mode 100644
index 0000000..7677188
--- /dev/null
+++ b/tests/test_all_tables_e2e/test_porcelain_table.py
@@ -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
diff --git a/tests/test_all_tables_e2e/test_single_table.py b/tests/test_all_tables_e2e/test_single_table.py
new file mode 100644
index 0000000..f4fa6b9
--- /dev/null
+++ b/tests/test_all_tables_e2e/test_single_table.py
@@ -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
diff --git a/tests/test_all_tables_e2e/test_single_table_windows.py b/tests/test_all_tables_e2e/test_single_table_windows.py
new file mode 100644
index 0000000..a15fa3a
--- /dev/null
+++ b/tests/test_all_tables_e2e/test_single_table_windows.py
@@ -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)
diff --git a/tests/test_ascii_table.py b/tests/test_ascii_table.py
new file mode 100644
index 0000000..020a443
--- /dev/null
+++ b/tests/test_ascii_table.py
@@ -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
diff --git a/tests/test_base_table/test_gen_row_lines.py b/tests/test_base_table/test_gen_row_lines.py
new file mode 100644
index 0000000..0d0f43c
--- /dev/null
+++ b/tests/test_base_table/test_gen_row_lines.py
@@ -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
diff --git a/tests/test_base_table/test_gen_table.py b/tests/test_base_table/test_gen_table.py
new file mode 100644
index 0000000..54d5fe1
--- /dev/null
+++ b/tests/test_base_table/test_gen_table.py
@@ -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
diff --git a/tests/test_base_table/test_horizontal_border.py b/tests/test_base_table/test_horizontal_border.py
new file mode 100644
index 0000000..e162261
--- /dev/null
+++ b/tests/test_base_table/test_horizontal_border.py
@@ -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
diff --git a/tests/test_base_table/test_table.py b/tests/test_base_table/test_table.py
new file mode 100644
index 0000000..c5b5a89
--- /dev/null
+++ b/tests/test_base_table/test_table.py
@@ -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
diff --git a/tests/test_build/test_build_border.py b/tests/test_build/test_build_border.py
new file mode 100644
index 0000000..9c410fd
--- /dev/null
+++ b/tests/test_build/test_build_border.py
@@ -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
diff --git a/tests/test_build/test_build_row.py b/tests/test_build/test_build_row.py
new file mode 100644
index 0000000..ce55944
--- /dev/null
+++ b/tests/test_build/test_build_row.py
@@ -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
diff --git a/tests/test_build/test_combine.py b/tests/test_build/test_combine.py
new file mode 100644
index 0000000..b296ffd
--- /dev/null
+++ b/tests/test_build/test_combine.py
@@ -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 == ['>', '<']
diff --git a/tests/test_build/test_flatten.py b/tests/test_build/test_flatten.py
new file mode 100644
index 0000000..aacfdbd
--- /dev/null
+++ b/tests/test_build/test_flatten.py
@@ -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
diff --git a/tests/test_examples.py b/tests/test_examples.py
new file mode 100644
index 0000000..f0799f9
--- /dev/null
+++ b/tests/test_examples.py
@@ -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
diff --git a/tests/test_terminal_io/__init__.py b/tests/test_terminal_io/__init__.py
new file mode 100644
index 0000000..93738fc
--- /dev/null
+++ b/tests/test_terminal_io/__init__.py
@@ -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
diff --git a/tests/test_terminal_io/sub_title_ascii_win10.bmp b/tests/test_terminal_io/sub_title_ascii_win10.bmp
new file mode 100644
index 0000000..638d0a3
--- /dev/null
+++ b/tests/test_terminal_io/sub_title_ascii_win10.bmp
Binary files differ
diff --git a/tests/test_terminal_io/sub_title_ascii_win2012.bmp b/tests/test_terminal_io/sub_title_ascii_win2012.bmp
new file mode 100644
index 0000000..04f0f2a
--- /dev/null
+++ b/tests/test_terminal_io/sub_title_ascii_win2012.bmp
Binary files differ
diff --git a/tests/test_terminal_io/sub_title_ascii_winxp.bmp b/tests/test_terminal_io/sub_title_ascii_winxp.bmp
new file mode 100644
index 0000000..c40a2d2
--- /dev/null
+++ b/tests/test_terminal_io/sub_title_ascii_winxp.bmp
Binary files differ
diff --git a/tests/test_terminal_io/sub_title_cjk_win10.bmp b/tests/test_terminal_io/sub_title_cjk_win10.bmp
new file mode 100644
index 0000000..052e6b5
--- /dev/null
+++ b/tests/test_terminal_io/sub_title_cjk_win10.bmp
Binary files differ
diff --git a/tests/test_terminal_io/sub_title_cjk_win2012.bmp b/tests/test_terminal_io/sub_title_cjk_win2012.bmp
new file mode 100644
index 0000000..ec48a85
--- /dev/null
+++ b/tests/test_terminal_io/sub_title_cjk_win2012.bmp
Binary files differ
diff --git a/tests/test_terminal_io/sub_title_cjk_winxp.bmp b/tests/test_terminal_io/sub_title_cjk_winxp.bmp
new file mode 100644
index 0000000..349f685
--- /dev/null
+++ b/tests/test_terminal_io/sub_title_cjk_winxp.bmp
Binary files differ
diff --git a/tests/test_terminal_io/test_get_console_info.py b/tests/test_terminal_io/test_get_console_info.py
new file mode 100644
index 0000000..1a9b98f
--- /dev/null
+++ b/tests/test_terminal_io/test_get_console_info.py
@@ -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
diff --git a/tests/test_terminal_io/test_set_terminal_title.py b/tests/test_terminal_io/test_set_terminal_title.py
new file mode 100644
index 0000000..6d58301
--- /dev/null
+++ b/tests/test_terminal_io/test_set_terminal_title.py
@@ -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)
diff --git a/tests/test_terminal_io/test_terminal_size.py b/tests/test_terminal_io/test_terminal_size.py
new file mode 100644
index 0000000..ba14d18
--- /dev/null
+++ b/tests/test_terminal_io/test_terminal_size.py
@@ -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
diff --git a/tests/test_width_and_alignment/test_align_and_pad_cell.py b/tests/test_width_and_alignment/test_align_and_pad_cell.py
new file mode 100644
index 0000000..e0a928e
--- /dev/null
+++ b/tests/test_width_and_alignment/test_align_and_pad_cell.py
@@ -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
diff --git a/tests/test_width_and_alignment/test_column_max_width.py b/tests/test_width_and_alignment/test_column_max_width.py
new file mode 100644
index 0000000..696c9bf
--- /dev/null
+++ b/tests/test_width_and_alignment/test_column_max_width.py
@@ -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
diff --git a/tests/test_width_and_alignment/test_max_dimensions.py b/tests/test_width_and_alignment/test_max_dimensions.py
new file mode 100644
index 0000000..fc97883
--- /dev/null
+++ b/tests/test_width_and_alignment/test_max_dimensions.py
@@ -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])
diff --git a/tests/test_width_and_alignment/test_table_width.py b/tests/test_width_and_alignment/test_table_width.py
new file mode 100644
index 0000000..5818789
--- /dev/null
+++ b/tests/test_width_and_alignment/test_table_width.py
@@ -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
diff --git a/tests/test_width_and_alignment/test_visible_width.py b/tests/test_width_and_alignment/test_visible_width.py
new file mode 100644
index 0000000..79cebcb
--- /dev/null
+++ b/tests/test_width_and_alignment/test_visible_width.py
@@ -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
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..f5b8ad4
--- /dev/null
+++ b/tox.ini
@@ -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