diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2022-09-16 09:09:35 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2022-09-16 09:09:35 +0000 |
commit | 0dfe1c9e2780469e3a4696e8fb3e6f717a7ebeb7 (patch) | |
tree | a0b651b55ea02e3b00bbc5eedba566fdd6bd7c08 | |
parent | Initial commit. (diff) | |
download | terminaltables-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>
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! @@ -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 Binary files differnew file mode 100644 index 0000000..97ae271 --- /dev/null +++ b/docs/asciitable.png 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 Binary files differnew file mode 100644 index 0000000..ce532a1 --- /dev/null +++ b/docs/doubletable.png 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 Binary files differnew file mode 100644 index 0000000..62a623c --- /dev/null +++ b/docs/examples.png diff --git a/docs/favicon.ico b/docs/favicon.ico Binary files differnew file mode 100644 index 0000000..8cf8947 --- /dev/null +++ b/docs/favicon.ico diff --git a/docs/githubtable.png b/docs/githubtable.png Binary files differnew file mode 100644 index 0000000..0b2a6a3 --- /dev/null +++ b/docs/githubtable.png 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 Binary files differnew file mode 100644 index 0000000..319bbcb --- /dev/null +++ b/docs/githubtable_rendered.png 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 Binary files differnew file mode 100644 index 0000000..f485db2 --- /dev/null +++ b/docs/key.enc 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 Binary files differnew file mode 100644 index 0000000..cc595ff --- /dev/null +++ b/docs/singletable.png 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 Binary files differnew file mode 100644 index 0000000..fe21fa7 --- /dev/null +++ b/tests/test_all_tables_e2e/sub_ascii_win10.bmp diff --git a/tests/test_all_tables_e2e/sub_ascii_winxp.bmp b/tests/test_all_tables_e2e/sub_ascii_winxp.bmp Binary files differnew file mode 100644 index 0000000..4105d11 --- /dev/null +++ b/tests/test_all_tables_e2e/sub_ascii_winxp.bmp diff --git a/tests/test_all_tables_e2e/sub_double_win10.bmp b/tests/test_all_tables_e2e/sub_double_win10.bmp Binary files differnew file mode 100644 index 0000000..e6b00ae --- /dev/null +++ b/tests/test_all_tables_e2e/sub_double_win10.bmp diff --git a/tests/test_all_tables_e2e/sub_double_win10b.bmp b/tests/test_all_tables_e2e/sub_double_win10b.bmp Binary files differnew file mode 100644 index 0000000..a527959 --- /dev/null +++ b/tests/test_all_tables_e2e/sub_double_win10b.bmp diff --git a/tests/test_all_tables_e2e/sub_double_winxp.bmp b/tests/test_all_tables_e2e/sub_double_winxp.bmp Binary files differnew file mode 100644 index 0000000..aae7b24 --- /dev/null +++ b/tests/test_all_tables_e2e/sub_double_winxp.bmp diff --git a/tests/test_all_tables_e2e/sub_single_win10.bmp b/tests/test_all_tables_e2e/sub_single_win10.bmp Binary files differnew file mode 100644 index 0000000..ff6f272 --- /dev/null +++ b/tests/test_all_tables_e2e/sub_single_win10.bmp diff --git a/tests/test_all_tables_e2e/sub_single_win10b.bmp b/tests/test_all_tables_e2e/sub_single_win10b.bmp Binary files differnew file mode 100644 index 0000000..c8d1e36 --- /dev/null +++ b/tests/test_all_tables_e2e/sub_single_win10b.bmp diff --git a/tests/test_all_tables_e2e/sub_single_winxp.bmp b/tests/test_all_tables_e2e/sub_single_winxp.bmp Binary files differnew file mode 100644 index 0000000..c4f5873 --- /dev/null +++ b/tests/test_all_tables_e2e/sub_single_winxp.bmp 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 Binary files differnew file mode 100644 index 0000000..638d0a3 --- /dev/null +++ b/tests/test_terminal_io/sub_title_ascii_win10.bmp diff --git a/tests/test_terminal_io/sub_title_ascii_win2012.bmp b/tests/test_terminal_io/sub_title_ascii_win2012.bmp Binary files differnew file mode 100644 index 0000000..04f0f2a --- /dev/null +++ b/tests/test_terminal_io/sub_title_ascii_win2012.bmp diff --git a/tests/test_terminal_io/sub_title_ascii_winxp.bmp b/tests/test_terminal_io/sub_title_ascii_winxp.bmp Binary files differnew file mode 100644 index 0000000..c40a2d2 --- /dev/null +++ b/tests/test_terminal_io/sub_title_ascii_winxp.bmp diff --git a/tests/test_terminal_io/sub_title_cjk_win10.bmp b/tests/test_terminal_io/sub_title_cjk_win10.bmp Binary files differnew file mode 100644 index 0000000..052e6b5 --- /dev/null +++ b/tests/test_terminal_io/sub_title_cjk_win10.bmp diff --git a/tests/test_terminal_io/sub_title_cjk_win2012.bmp b/tests/test_terminal_io/sub_title_cjk_win2012.bmp Binary files differnew file mode 100644 index 0000000..ec48a85 --- /dev/null +++ b/tests/test_terminal_io/sub_title_cjk_win2012.bmp diff --git a/tests/test_terminal_io/sub_title_cjk_winxp.bmp b/tests/test_terminal_io/sub_title_cjk_winxp.bmp Binary files differnew file mode 100644 index 0000000..349f685 --- /dev/null +++ b/tests/test_terminal_io/sub_title_cjk_winxp.bmp 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 @@ -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 |