Full Code of ines/wasabi for AI

master ddcb05ac1e27 cached
25 files
99.8 KB
27.7k tokens
116 symbols
1 requests
Download .txt
Repository: ines/wasabi
Branch: master
Commit: ddcb05ac1e27
Files: 25
Total size: 99.8 KB

Directory structure:
gitextract_vfdcdftg/

├── .flake8
├── .github/
│   └── workflows/
│       └── tests.yml
├── .gitignore
├── LICENSE
├── MANIFEST.in
├── README.md
├── requirements.txt
├── setup.cfg
├── setup.py
└── wasabi/
    ├── __init__.py
    ├── compat.py
    ├── markdown.py
    ├── printer.py
    ├── py.typed
    ├── tables.py
    ├── tests/
    │   ├── __init__.py
    │   ├── test-data/
    │   │   └── wasabi-test-notebook.ipynb
    │   ├── test_jupyter.py
    │   ├── test_markdown.py
    │   ├── test_printer.py
    │   ├── test_tables.py
    │   ├── test_traceback.py
    │   └── test_util.py
    ├── traceback_printer.py
    └── util.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .flake8
================================================
[flake8]
ignore = E203, E266, E501, E731, W503
max-line-length = 80
select = B,C,E,F,W,T4,B9


================================================
FILE: .github/workflows/tests.yml
================================================
name: tests

on:
  push:
    paths-ignore:
      - "*.md"
  pull_request:
    types: [opened, synchronize, reopened, edited]
    paths-ignore:
      - "*.md"

permissions:
  contents: read

env:
  MODULE_NAME: 'wasabi'
  RUN_MYPY: 'true'

jobs:
  tests:
    name: Test
    if: github.repository_owner == 'explosion'
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        python_version: ["3.12"]
        include:
          - os: windows-2019
            python_version: "3.6"
          - os: windows-latest
            python_version: "3.7"
          - os: macos-latest
            python_version: "3.8"
          - os: ubuntu-latest
            python_version: "3.9"
          - os: windows-latest
            python_version: "3.10"
          - os: macos-latest
            python_version: "3.11"
    runs-on: ${{ matrix.os }}

    steps:
      - name: Check out repo
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: Configure Python version
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
        with:
          python-version: ${{ matrix.python_version }}

      - name: Build sdist
        run: |
          python -m pip install -U build pip setuptools
          python -m pip install -U -r requirements.txt
          python -m build --sdist

      - name: Run mypy
        shell: bash
        if: ${{ env.RUN_MYPY == 'true' }}
        run: |
          python -m mypy $MODULE_NAME

      - name: Delete source directory
        shell: bash
        run: |
          rm -rf $MODULE_NAME

      - name: Uninstall all packages
        run: |
          python -m pip freeze > installed.txt
          python -m pip uninstall -y -r installed.txt

      - name: Install from sdist
        shell: bash
        run: |
          SDIST=$(python -c "import os;print(os.listdir('./dist')[-1])" 2>&1)
          python -m pip install dist/$SDIST

      - name: Test import
        shell: bash
        run: |
          python -c "import $MODULE_NAME" -Werror

      - name: Install test requirements
        run: |
          python -m pip install -U -r requirements.txt

      - name: Run tests
        shell: bash
        run: |
          python -m pytest --pyargs $MODULE_NAME -Werror


================================================
FILE: .gitignore
================================================
.vscode/
tmp/

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# 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/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# PyCharm
.idea


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (C) 2018 Ines Montani

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: MANIFEST.in
================================================
include LICENSE
recursive-include wasabi/tests/test-data *.ipynb


================================================
FILE: README.md
================================================
# wasabi: A lightweight console printing and formatting toolkit

Over the years, I've written countless implementations of coloring and
formatting utilities to output messages in our libraries like
[spaCy](https://spacy.io), [Thinc](https://github.com/explosion/thinc) and
[Prodigy](https://prodi.gy). While there are many other great open-source
options, I've always ended up wanting something slightly different or slightly
custom.

This package is still a work in progress and aims to bundle those utilities in a
standardised way so they can be shared across our other projects. It's super
lightweight, has zero dependencies and works with Python 3.6+.

[![tests](https://github.com/explosion/wasabi/actions/workflows/tests.yml/badge.svg)](https://github.com/explosion/wasabi/actions/workflows/tests.yml)
[![PyPi](https://img.shields.io/pypi/v/wasabi.svg?style=flat-square&logo=pypi&logoColor=white)](https://pypi.python.org/pypi/wasabi)
[![conda](https://img.shields.io/conda/vn/conda-forge/wasabi.svg?style=flat-square&logo=conda-forge/logoColor=white)](https://anaconda.org/conda-forge/wasabi)
[![GitHub](https://img.shields.io/github/release/ines/wasabi/all.svg?style=flat-square&logo=github)](https://github.com/ines/wasabi)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square)](https://github.com/ambv/black)

<img width="609" src="https://user-images.githubusercontent.com/13643239/48663861-8c9ea000-ea96-11e8-8b04-d120c52276a8.png">

## 💬 FAQ

### Are you going to add more features?

Yes, there's still a few of helpers and features to port over. However, the new
features will be heavily biased by what we (think we) need. I always appreciate
pull requests to improve the existing functionality – but I want to keep this
library as simple, lightweight and specific as possible.

### Can I use this for my projects?

Sure, if you like it, feel free to adopt it! Just keep in mind that the package
is very specific and not intended to be a full-featured and fully customisable
formatting library. If that's what you're looking for, you might want to try
other packages – for example, [`colored`](https://pypi.org/project/colored/),
[`crayons`](https://github.com/kennethreitz/crayons),
[`colorful`](https://github.com/timofurrer/colorful),
[`tabulate`](https://bitbucket.org/astanin/python-tabulate),
[`console`](https://github.com/mixmastamyk/console) or
[`py-term`](https://github.com/gravmatt/py-term), to name a few.

### Why `wasabi`?

I was looking for a short and descriptive name, but everything was already
taken. So I ended up naming this package after one of my rats, Wasabi. 🐀

## ⌛️ Installation

```bash
pip install wasabi
```

## 🎛 API

### <kbd>function</kbd> `msg`

An instance of `Printer`, initialized with the default config. Useful as a quick
shortcut if you don't need to customize initialization.

```python
from wasabi import msg

msg.good("Success!")
```

### <kbd>class</kbd> `Printer`

#### <kbd>method</kbd> `Printer.__init__`

```python
from wasabi import Printer

msg = Printer()
```

| Argument          | Type      | Description                                                   | Default       |
| ----------------- | --------- | ------------------------------------------------------------- | ------------- |
| `pretty`          | bool      | Pretty-print output with colors and icons.                    | `True`        |
| `no_print`        | bool      | Don't actually print, just return.                            | `False`       |
| `colors`          | dict      | Add or overwrite color values, names mapped to `0`-`256`.     | `None`        |
| `icons`           | dict      | Add or overwrite icon. Name mapped to unicode.                | `None`        |
| `line_max`        | int       | Maximum line length (for divider).                            | `80`          |
| `animation`       | str       | Steps of loading animation for `Printer.loading`.             | `"⠙⠹⠸⠼⠴⠦⠧⠇⠏"` |
| `animation_ascii` | str       | Alternative animation for ASCII terminals.                    | `"\|/-\\"`    |
| `hide_animation`  | bool      | Don't display animation, e.g. for logs.                       | `False`       |
| `ignore_warnings` | bool      | Don't output messages of type `MESSAGE.WARN`.                 | `False`       |
| `env_prefix`      | str       | Prefix for environment variables, e.g. `WASABI_LOG_FRIENDLY`. | `"WASABI"`    |
| `timestamp`       | bool      | Add timestamp before output.                                  | `False`       |
| **RETURNS**       | `Printer` | The initialized printer.                                      | -             |

#### <kbd>method</kbd> `Printer.text`

```python
msg = Printer()
msg.text("Hello world!")
```

| Argument   | Type           | Description                                                                                                            | Default |
| ---------- | -------------- | ---------------------------------------------------------------------------------------------------------------------- | ------- |
| `title`    | str            | The main text to print.                                                                                                | `""`    |
| `text`     | str            | Optional additional text to print.                                                                                     | `""`    |
| `color`    |  unicode / int | Color name or value.                                                                                                   | `None`  |
| `icon`     | str            | Name of icon to add.                                                                                                   | `None`  |
| `show`     | bool           | Whether to print or not. Can be used to only output messages under certain condition, e.g. if `--verbose` flag is set. | `True`  |
| `spaced`   | bool           | Whether to add newlines around the output.                                                                             | `False` |
| `no_print` | bool           | Don't actually print, just return. Overwrites global setting.                                                          | `False` |
| `exits`    | int            | If set, perform a system exit with the given code after printing.                                                      | `None`  |

#### <kbd>method</kbd> `Printer.good`, `Printer.fail`, `Printer.warn`, `Printer.info`

Print special formatted messages.

```python
msg = Printer()
msg.good("Success")
msg.fail("Error")
msg.warn("Warning")
msg.info("Info")
```

| Argument | Type | Description                                                                                                            | Default |
| -------- | ---- | ---------------------------------------------------------------------------------------------------------------------- | ------- |
| `title`  | str  | The main text to print.                                                                                                | `""`    |
| `text`   | str  | Optional additional text to print.                                                                                     | `""`    |
| `show`   | bool | Whether to print or not. Can be used to only output messages under certain condition, e.g. if `--verbose` flag is set. | `True`  |
| `exits`  | int  | If set, perform a system exit with the given code after printing.                                                      | `None`  |

#### <kbd>method</kbd> `Printer.divider`

Print a formatted divider.

```python
msg = Printer()
msg.divider("Heading")
```

| Argument | Type | Description                                                                                                            | Default |
| -------- | ---- | ---------------------------------------------------------------------------------------------------------------------- | ------- |
| `text`   | str  | Headline text. If empty, only the line is printed.                                                                     | `""`    |
| `char`   | str  | Single line character to repeat.                                                                                       | `"="`   |
| `show`   | bool | Whether to print or not. Can be used to only output messages under certain condition, e.g. if `--verbose` flag is set. | `True`  |
| `icon`   | str  | Optional icon to use with title.                                                                                       | `None`  |

#### <kbd>contextmanager</kbd> `Printer.loading`

```python
msg = Printer()
with msg.loading("Loading..."):
    # Do something here that takes longer
    time.sleep(10)
msg.good("Successfully loaded something!")
```

| Argument | Type | Description                        | Default        |
| -------- | ---- | ---------------------------------- | -------------- |
| `text`   | str  | The text to display while loading. | `"Loading..."` |

#### <kbd>method</kbd> `Printer.table`, `Printer.row`

See [Tables](#tables).

#### <kbd>property</kbd> `Printer.counts`

Get the counts of how often the special printers were fired, e.g.
`MESSAGES.GOOD`. Can be used to print an overview like "X warnings"

```python
msg = Printer()
msg.good("Success")
msg.fail("Error")
msg.warn("Error")

print(msg.counts)
# Counter({'good': 1, 'fail': 2, 'warn': 0, 'info': 0})
```

| Argument    | Type      | Description                                          |
| ----------- | --------- | ---------------------------------------------------- |
| **RETURNS** | `Counter` | The counts for the individual special message types. |

### Tables

#### <kbd>function</kbd> `table`

Lightweight helper to format tabular data.

```python
from wasabi import table

data = [("a1", "a2", "a3"), ("b1", "b2", "b3")]
header = ("Column 1", "Column 2", "Column 3")
widths = (8, 9, 10)
aligns = ("r", "c", "l")
formatted = table(data, header=header, divider=True, widths=widths, aligns=aligns)
```

```
Column 1   Column 2    Column 3
--------   ---------   ----------
      a1      a2       a3
      b1      b2       b3
```

| Argument       | Type                | Description                                                                                                                         | Default    |
| -------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `data`         | iterable / dict     | The data to render. Either a list of lists (one per row) or a dict for two-column tables.                                           |            |
| `header`       | iterable            | Optional header columns.                                                                                                            | `None`     |
| `footer`       | iterable            | Optional footer columns.                                                                                                            | `None`     |
| `divider`      | bool                | Show a divider line between header/footer and body.                                                                                 | `False`    |
| `widths`       | iterable / `"auto"` | Column widths in order. If `"auto"`, widths will be calculated automatically based on the largest value.                            | `"auto"`   |
| `max_col`      | int                 | Maximum column width.                                                                                                               | `30`       |
| `spacing`      | int                 | Number of spaces between columns.                                                                                                   | `3`        |
| `aligns`       | iterable / unicode  | Columns alignments in order. `"l"` (left, default), `"r"` (right) or `"c"` (center). If If a string, value is used for all columns. | `None`     |
| `multiline`    | bool                | If a cell value is a list of a tuple, render it on multiple lines, with one value per line.                                         | `False`    |
| `env_prefix`   | unicode             | Prefix for environment variables, e.g. WASABI_LOG_FRIENDLY.                                                                         | `"WASABI"` |
| `color_values` | dict                | Add or overwrite color values, name mapped to value.                                                                                | `None`     |
| `fg_colors`    | iterable            | Foreground colors, one per column. None can be specified for individual columns to retain the default background color.             | `None`     |
| `bg_colors`    | iterable            | Background colors, one per column. None can be specified for individual columns to retain the default background color.             | `None`     |
| **RETURNS**    | str                 | The formatted table.                                                                                                                |            |

#### <kbd>function</kbd> `row`

```python
from wasabi import row

data = ("a1", "a2", "a3")
formatted = row(data)
```

```
a1   a2   a3
```

| Argument     | Type                  | Description                                                                                                                                                | Default    |
| ------------ | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `data`       | iterable              | The individual columns to format.                                                                                                                          |            |
| `widths`     | list / int / `"auto"` | Column widths, either one integer for all columns or an iterable of values. If "auto", widths will be calculated automatically based on the largest value. | `"auto"`   |
| `spacing`    | int                   | Number of spaces between columns.                                                                                                                          | `3`        |
| `aligns`     | list                  | Columns alignments in order. `"l"` (left), `"r"` (right) or `"c"` (center).                                                                                | `None`     |
| `env_prefix` | unicode               | Prefix for environment variables, e.g. WASABI_LOG_FRIENDLY.                                                                                                | `"WASABI"` |
| `fg_colors`  | list                  | Foreground colors for the columns, in order. None can be specified for individual columns to retain the default foreground color.                          | `None`     |
| `bg_colors`  | list                  | Background colors for the columns, in order. None can be specified for individual columns to retain the default background color.                          | `None`     |
| **RETURNS**  | str                   | The formatted row.                                                                                                                                         |            |

### <kbd>class</kbd> `TracebackPrinter`

Helper to output custom formatted tracebacks and error messages. Currently used
in [Thinc](https://github.com/explosion/thinc).

#### <kbd>method</kbd> `TracebackPrinter.__init__`

Initialize a traceback printer.

```python
from wasabi import TracebackPrinter

tb = TracebackPrinter(tb_base="thinc", tb_exclude=("check.py",))
```

| Argument          | Type               | Description                                                                                                                                                              | Default    |
| ----------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
| `color_error`     | str / int          | Color name or code for errors (passed to `color` helper).                                                                                                                | `"red"`    |
| `color_tb`        | str / int          | Color name or code for traceback headline (passed to `color` helper).                                                                                                    | `"blue"`   |
| `color_highlight` | str / int          | Color name or code for highlighted text (passed to `color` helper).                                                                                                      | `"yellow"` |
| `indent`          | int                | Number of spaces to use for indentation.                                                                                                                                 | `2`        |
| `tb_base`         | str                | Name of directory to use to show relative paths. For example, `"thinc"` will look for the last occurence of `"/thinc/"` in a path and only show path to the right of it. | `None`     |
| `tb_exclude`      | tuple              | List of filenames to exclude from traceback.                                                                                                                             | `tuple()`  |
| **RETURNS**       | `TracebackPrinter` | The traceback printer.                                                                                                                                                   |            |

#### <kbd>method</kbd> `TracebackPrinter.__call__`

Output custom formatted tracebacks and errors.

```python
from wasabi import TracebackPrinter
import traceback

tb = TracebackPrinter(tb_base="thinc", tb_exclude=("check.py",))

error = tb("Some error", "Error description", highlight="kwargs", tb=traceback.extract_stack())
raise ValueError(error)
```

```
  Some error
  Some error description

  Traceback:
  ├─ <lambda> [61] in .env/lib/python3.6/site-packages/pluggy/manager.py
  ├─── _multicall [187] in .env/lib/python3.6/site-packages/pluggy/callers.py
  └───── pytest_fixture_setup [969] in .env/lib/python3.6/site-packages/_pytest/fixtures.py
         >>> result = call_fixture_func(fixturefunc, request, kwargs)
```

| Argument    | Type     | Description                                                                                | Default |
| ----------- | -------- | ------------------------------------------------------------------------------------------ | ------- |
| `title`     | str      | The message title.                                                                         |         |
| `*texts`    | str      | Optional texts to print (one per line).                                                    |         |
| `highlight` | str      | Optional sequence to highlight in the traceback, e.g. the bad value that caused the error. | `False` |
| `tb`        | iterable | The traceback, e.g. generated by `traceback.extract_stack()`.                              | `None`  |
| **RETURNS** | str      | The formatted traceback. Can be printed or raised by custom exception.                     |         |

### <kbd>class</kbd> `MarkdownRenderer`

Helper to create Markdown-formatted content. Will store the blocks added to the
Markdown document in order.

```python
from wasabi import MarkdownRenderer

md = MarkdownRenderer()
md.add(md.title(1, "Hello world"))
md.add("This is a paragraph")
print(md.text)
```

### <kbd>method</kbd> `MarkdownRenderer.__init__`

Initialize a Markdown renderer.

```python
from wasabi import MarkdownRenderer

md = MarkdownRenderer()
```

| Argument    | Type               | Description                    | Default |
| ----------- | ------------------ | ------------------------------ | ------- |
| `no_emoji`  | bool               | Don't include emoji in titles. | `False` |
| **RETURNS** | `MarkdownRenderer` | The renderer.                  |

### <kbd>method</kbd> `MarkdownRenderer.add`

Add a block to the Markdown document.

```python
from wasabi import MarkdownRenderer

md = MarkdownRenderer()
md.add("This is a paragraph")
```

| Argument | Type | Description         | Default |
| -------- | ---- | ------------------- | ------- |
| `text`   | str  | The content to add. |         |

### <kbd>property</kbd> `MarkdownRenderer.text`

The rendered Markdown document.

```python
md = MarkdownRenderer()
md.add("This is a paragraph")
print(md.text)
```

| Argument    | Type | Description                      | Default |
| ----------- | ---- | -------------------------------- | ------- |
| **RETURNS** | str  | The document as a single string. |         |

### <kbd>method</kbd> `MarkdownRenderer.table`

Create a Markdown-formatted table.

```python
md = MarkdownRenderer()
table = md.table([("a", "b"), ("c", "d")], ["Column 1", "Column 2"])
md.add(table)
```

<!-- prettier-ignore -->
```markdown
| Column 1 | Column 2 |
| --- | --- |
| a | b |
| c | d |
```

| Argument    | Type                    | Description                                                                          | Default |
| ----------- | ----------------------- | ------------------------------------------------------------------------------------ | ------- |
| `data`      | Iterable[Iterable[str]] | The body, one iterable per row, containig an interable of column contents.           |         |
| `header`    | Iterable[str]           | The column names.                                                                    |         |
| `aligns`    | Iterable[str]           | Columns alignments in order. `"l"` (left, default), `"r"` (right) or `"c"` (center). | `None`  |
| **RETURNS** | str                     | The table.                                                                           |         |

### <kbd>method</kbd> `MarkdownRenderer.title`

Create a Markdown-formatted heading.

```python
md = MarkdownRenderer()
md.add(md.title(1, "Hello world"))
md.add(md.title(2, "Subheading", "💖"))
```

```markdown
# Hello world

## 💖 Subheading
```

| Argument    | Type | Description                            | Default |
| ----------- | ---- | -------------------------------------- | ------- |
| `level`     | int  | The heading level, e.g. `3` for `###`. |         |
| `text`      | str  | The heading text.                      |         |
| `emoji`     | str  | Optional emoji to show before heading. | `None`  |
| **RETURNS** | str  | The rendered title.                    |         |

### <kbd>method</kbd> `MarkdownRenderer.list`

Create a Markdown-formatted non-nested list.

```python
md = MarkdownRenderer()
md.add(md.list(["item", "other item"]))
md.add(md.list(["first item", "second item"], numbered=True))
```

```markdown
- item
- other item

1. first item
2. second item
```

| Argument    | Type          | Description                     | Default |
| ----------- | ------------- | ------------------------------- | ------- |
| `items`     | Iterable[str] | The list items.                 |         |
| `numbered`  | bool          | Whether to use a numbered list. | `False` |
| **RETURNS** | str           | The rendered list.              |         |

### <kbd>method</kbd> `MarkdownRenderer.link`

Create a Markdown-formatted link.

```python
md = MarkdownRenderer()
md.add(md.link("Google", "https://google.com"))
```

```markdown
[Google](https://google.com)
```

| Argument    | Type | Description        | Default |
| ----------- | ---- | ------------------ | ------- |
| `text`      | str  | The link text.     |         |
| `url`       | str  | The link URL.      |         |
| **RETURNS** | str  | The rendered link. |         |

### <kbd>method</kbd> `MarkdownRenderer.code_block`

Create a Markdown-formatted code block.

```python
md = MarkdownRenderer()
md.add(md.code_block("import spacy", "python"))
```

````markdown
```python
import spacy
```
````

| Argument    | Type | Description              | Default |
| ----------- | ---- | ------------------------ | ------- |
| `text`      | str  | The code text.           |         |
| `lang`      | str  | Optional code language.  | `""`    |
| **RETURNS** | str  | The rendered code block. |         |

### <kbd>method</kbd> `MarkdownRenderer.code`, `MarkdownRenderer.bold`, `MarkdownRenderer.italic`

Create a Markdown-formatted text.

```python
md = MarkdownRenderer()
md.add(md.code("import spacy"))
md.add(md.bold("Hello!"))
md.add(md.italic("Emphasis"))
```

```markdown
`import spacy`

**Hello!**

_Emphasis_
```

### Utilities

#### <kbd>function</kbd> `color`

```python
from wasabi import color

formatted = color("This is a text", fg="white", bg="green", bold=True)
```

| Argument    | Type      | Description                                   | Default |
| ----------- | --------- | --------------------------------------------- | ------- |
| `text`      | str       | The text to be formatted.                     | -       |
| `fg`        | str / int | Foreground color. String name or `0` - `256`. | `None`  |
| `bg`        | str / int | Background color. String name or `0` - `256`. | `None`  |
| `bold`      | bool      | Format the text in bold.                      | `False` |
| `underline` | bool      | Format the text by underlining.               | `False` |
| **RETURNS** | str       | The formatted string.                         |         |

#### <kbd>function</kbd> `wrap`

```python
from wasabi import wrap

wrapped = wrap("Hello world, this is a text.", indent=2)
```

| Argument    | Type | Description                                | Default |
| ----------- | ---- | ------------------------------------------ | ------- |
| `text`      | str  | The text to wrap.                          | -       |
| `wrap_max`  | int  | Maximum line width, including indentation. | `80`    |
| `indent`    | int  | Number of spaces used for indentation.     | `4`     |
| **RETURNS** | str  | The wrapped text with line breaks.         |         |

#### <kbd>function</kbd> `diff_strings`

```python
from wasabi import diff_strings

diff = diff_strings("hello world!", "helloo world")
```

| Argument    | Type      | Description                                                                  | Default            |
| ----------- | --------- | ---------------------------------------------------------------------------- | ------------------ |
| `a`         | str       | The first string to diff.                                                    |
| `b`         | str       | The second string to diff.                                                   |
| `fg`        | str / int | Foreground color. String name or `0` - `256`.                                | `"black"`          |
| `bg`        | tuple     | Background colors as `(insert, delete)` tuple of string name or `0` - `256`. | `("green", "red")` |
| **RETURNS** | str       | The formatted diff.                                                          |                    |

### Environment variables

Wasabi also respects the following environment variables. The prefix can be
customised on the `Printer` via the `env_prefix` argument. For example, setting
`env_prefix="SPACY"` will expect the environment variable `SPACY_LOG_FRIENDLY`.

| Name                   | Description                                            |
| ---------------------- | ------------------------------------------------------ |
| `ANSI_COLORS_DISABLED` | Disable colors.                                        |
| `WASABI_LOG_FRIENDLY`  | Make output nicer for logs (no colors, no animations). |
| `WASABI_NO_PRETTY`     | Disable pretty printing, e.g. colors and icons.        |

## 🔔 Run tests

Fork or clone the repo, make sure you have `pytest` installed and then run it on
the package directory. The tests are located in
[`/wasabi/tests`](/wasabi/tests).

```bash
pip install pytest
cd wasabi
python -m pytest wasabi
```


================================================
FILE: requirements.txt
================================================
typing_extensions>=3.7.4.1,<5.0.0; python_version < "3.8"
# Development dependencies
pytest
mypy
types-colorama
nbconvert
ipykernel


================================================
FILE: setup.cfg
================================================
[metadata]
version = 1.1.3
description = A lightweight console printing and formatting toolkit
url = https://github.com/explosion/wasabi
author = Explosion
author_email = contact@explosion.ai
license = MIT
long_description = file: README.md
long_description_content_type = text/markdown

[options]
zip_safe = true
include_package_data = true
python_requires = >=3.6
install_requires =
    colorama >= 0.4.6; sys_platform == "win32" and python_version >= "3.7"
    typing_extensions>=3.7.4.1,<5.0.0; python_version < "3.8"

[flake8]
ignore = E203, E266, E501, E731, W503, E741
max-line-length = 80
select = B,C,E,F,W,T4,B9


================================================
FILE: setup.py
================================================
#!/usr/bin/env python

if __name__ == "__main__":
    from setuptools import setup, find_packages

    setup(name="wasabi", packages=find_packages())


================================================
FILE: wasabi/__init__.py
================================================
from .markdown import MarkdownRenderer  # noqa
from .printer import Printer  # noqa
from .tables import row, table  # noqa
from .traceback_printer import TracebackPrinter  # noqa
from .util import MESSAGES  # noqa
from .util import color, diff_strings, format_repr, get_raw_input, wrap  # noqa

msg = Printer()

# fmt: off
__all__ = [
    "color",
    "diff_strings",
    "format_repr",
    "get_raw_input",
    "msg",
    "row",
    "table",
    "wrap",
    "MarkdownRenderer",
    "MESSAGES",
    "Printer",
    "TracebackPrinter",
]
# fmt: on


================================================
FILE: wasabi/compat.py
================================================
import sys


# Use typing_extensions for Python versions < 3.8
if sys.version_info < (3, 8):
    from typing_extensions import Protocol, Literal
else:
    from typing import Protocol, Literal  # noqa: F401


================================================
FILE: wasabi/markdown.py
================================================
from typing import Iterable, List, Optional, Sequence

from .compat import Literal


class MarkdownRenderer:
    """Simple helper for generating raw Markdown."""

    def __init__(self, no_emoji: bool = False):
        """Initialize the renderer.

        no_emoji (bool): Don't show emoji in titles etc.
        """
        self.data: List = []
        self.no_emoji = no_emoji

    @property
    def text(self) -> str:
        """RETURNS (str): The Markdown document."""
        return "\n\n".join(self.data)

    def add(self, content: str):
        """Add a string to the Markdown document.

        content (str): Add content to the document.
        """
        self.data.append(content)

    def table(
        self,
        data: Iterable[Iterable[str]],
        header: Sequence[str],
        aligns: Optional[Sequence[Literal["r", "c", "l"]]] = None,
    ) -> str:
        """Create a Markdown table.

        data (Iterable[Iterable[str]]): The body, one iterable per row,
            containig an interable of column contents.
        header (Sequence[str]): The column names.
        aligns (Optional[Sequence[Literal["r", "c", "l"]]]): Optional alignment-mode for each column. Values should
            either be 'l' (left), 'r' (right), or 'c' (center). Optional.
        RETURNS (str): The rendered table.
        """
        if aligns is None:
            aligns = ["l"] * len(header)
        if len(aligns) != len(header):
            err = "Invalid aligns: {} (header length: {})".format(aligns, len(header))
            raise ValueError(err)
        get_divider = lambda a: ":---:" if a == "c" else "---:" if a == "r" else "---"
        head = "| {} |".format(" | ".join(header))
        divider = "| {} |".format(
            " | ".join(get_divider(aligns[i]) for i in range(len(header)))
        )
        body = "\n".join("| {} |".format(" | ".join(row)) for row in data)
        return "{}\n{}\n{}".format(head, divider, body)

    def title(self, level: int, text: str, emoji: Optional[str] = None) -> str:
        """Create a Markdown heading.

        level (int): The heading level, e.g. 3 for ###
        text (str): The heading text.
        emoji (Optional[str]): Optional emoji to show before heading text, if enabled.
        RETURNS (str): The rendered title.
        """
        prefix = "{} ".format(emoji) if emoji and not self.no_emoji else ""
        return "{} {}{}".format("#" * level, prefix, text)

    def list(self, items: Iterable[str], numbered: bool = False) -> str:
        """Create a non-nested list.

        items (Iterable[str]): The list items.
        numbered (bool): Whether to use a numbered list.
        RETURNS (str): The rendered list.
        """
        content = []
        for i, item in enumerate(items):
            if numbered:
                content.append("{}. {}".format(i + 1, item))
            else:
                content.append("- {}".format(item))
        return "\n".join(content)

    def link(self, text: str, url: str) -> str:
        """Create a Markdown link.

        text (str): The link text.
        url (str): The link URL.
        RETURNS (str): The rendered link.
        """
        return "[{}]({})".format(text, url)

    def code_block(self, text: str, lang: str = "") -> str:
        """Create a Markdown code block.

        text (str): The code text.
        lang (str): Optional code language.
        RETURNS (str): The rendered code block.
        """
        return "```{}\n{}\n```".format(lang, text)

    def code(self, text: str) -> str:
        """Create Markdown inline code.

        text (str): The inline code text.
        RETURNS (str): The rendered code text.
        """
        return self._wrap(text, "`")

    def bold(self, text: str) -> str:
        """Create bold text.

        text (str): The text to format in boldface.
        RETURNS (str): The formatted text.
        """
        return self._wrap(text, "**")

    def italic(self, text: str):
        """Create italic text.

        text (str): The text to italicize.
        RETURNS (str): The formatted text.
        """
        return self._wrap(text, "_")

    def _wrap(self, text, marker):
        return "{}{}{}".format(marker, text, marker)


================================================
FILE: wasabi/printer.py
================================================
import datetime
import itertools
import os
import sys
import time
import traceback
from collections import Counter
from contextlib import contextmanager
from multiprocessing import Process
from typing import Any, Collection, Dict, NoReturn, Optional, Union, cast, overload

from .compat import Literal
from .tables import row, table
from .util import COLORS, ICONS, MESSAGES, can_render
from .util import color as _color
from .util import locale_escape, supports_ansi, wrap


class Printer(object):
    def __init__(
        self,
        pretty: bool = True,
        no_print: bool = False,
        colors: Optional[Dict] = None,
        icons: Optional[Dict] = None,
        line_max: int = 80,
        animation: str = "⠙⠹⠸⠼⠴⠦⠧⠇⠏",
        animation_ascii: str = "|/-\\",
        hide_animation: bool = False,
        ignore_warnings: bool = False,
        env_prefix: str = "WASABI",
        timestamp: bool = False,
    ):
        """Initialize the command-line printer.

        pretty (bool): Pretty-print output (colors, icons).
        no_print (bool): Don't actually print, just return.
        colors (Optional[Dict]): Optional color values to add or overwrite, name mapped to value.
        icons (Optional[Dict]): Optional icons to add or overwrite. Name mapped to unicode icon.
        line_max (int): Maximum line length (for divider).
        animation (str): Steps of loading animation for loading() method.
        animation_ascii (str): Alternative animation for ASCII terminals.
        hide_animation (bool): Don't display animation, e.g. for logs.
        ignore_warnings (bool): Do not output messages of type MESSAGE.WARN.
        env_prefix (str): Prefix for environment variables, e.g.
            WASABI_LOG_FRIENDLY.
        timestamp (bool): Print a timestamp (default False).
        RETURNS (Printer): The initialized printer.
        """
        env_log_friendly = os.getenv("{}_LOG_FRIENDLY".format(env_prefix), False)
        env_no_pretty = os.getenv("{}_NO_PRETTY".format(env_prefix), False)
        self._counts: Counter = Counter()
        self.pretty = pretty and not env_no_pretty
        self.no_print = no_print
        self.show_color = supports_ansi() and not env_log_friendly
        self.hide_animation = hide_animation or env_log_friendly
        self.ignore_warnings = ignore_warnings
        self.line_max = line_max
        self.colors = dict(COLORS)
        self.icons = dict(ICONS)
        self.timestamp = timestamp
        if colors:
            self.colors.update(colors)
        if icons:
            self.icons.update(icons)
        self.anim = animation if can_render(animation) else animation_ascii

    @property
    def counts(self) -> Counter:
        """Get the counts of how often the special printers were fired,
        e.g. MESSAGES.GOOD. Can be used to print an overview like "X warnings".
        """
        return self._counts

    def good(
        self,
        title: Any = "",
        text: Any = "",
        show: bool = True,
        spaced: bool = False,
        exits: Optional[int] = None,
    ):
        """Print a success message.

        title (Any): The main text to print.
        text (Any): Optional additional text to print.
        show (bool): Whether to print or not. Can be used to only output
            messages under certain condition, e.g. if --verbose flag is set.
        spaced (bool): Whether to add newlines around the output.
        exits (Optional[int]): Optional toggle to perform a system exit.
        """
        return self._get_msg(
            title, text, style=MESSAGES.GOOD, show=show, spaced=spaced, exits=exits
        )

    @overload
    def fail(
        self,
        title: Any = "",
        text: Any = "",
        show: bool = True,
        spaced: bool = False,
        exits: Optional[Literal[0, False]] = None,
    ):
        ...

    @overload
    def fail(
        self,
        title: Any = "",
        text: Any = "",
        show: bool = True,
        spaced: bool = False,
        exits: Literal[1, True] = True,
    ) -> NoReturn:
        ...

    def fail(
        self,
        title: Any = "",
        text: Any = "",
        show: bool = True,
        spaced: bool = False,
        exits: Optional[Union[int, bool]] = None,
    ) -> Union[str, None, NoReturn]:
        """Print an error message.

        title (Any): The main text to print.
        text (Any): Optional additional text to print.
        show (bool): Whether to print or not. Can be used to only output
            messages under certain condition, e.g. if --verbose flag is set.
        spaced (bool): Whether to add newlines around the output.
        exits (Optional[int]): Optional toggle to perform a system exit.
        """
        return self._get_msg(
            title, text, style=MESSAGES.FAIL, show=show, spaced=spaced, exits=exits
        )

    def warn(
        self,
        title: Any = "",
        text: Any = "",
        show: bool = True,
        spaced: bool = False,
        exits: Optional[int] = None,
    ):
        """Print a warning message.

        title (Any): The main text to print.
        text (Any): Optional additional text to print.
        show (bool): Whether to print or not. Can be used to only output
            messages under certain condition, e.g. if --verbose flag is set.
        spaced (bool): Whether to add newlines around the output.
        exits (Optional[int]): Optional toggle to perform a system exit.
        """
        return self._get_msg(
            title, text, style=MESSAGES.WARN, show=show, spaced=spaced, exits=exits
        )

    def info(
        self,
        title: Any = "",
        text: Any = "",
        show: bool = True,
        spaced: bool = False,
        exits: Optional[int] = None,
    ):
        """Print an informational message.

        title (Any): The main text to print.
        text (Any): Optional additional text to print.
        show (bool): Whether to print or not. Can be used to only output
            messages under certain condition, e.g. if --verbose flag is set.
        spaced (bool): Whether to add newlines around the output.
        exits (Optional[int]): Optional toggle to perform a system exit.
        """
        return self._get_msg(
            title, text, style=MESSAGES.INFO, show=show, spaced=spaced, exits=exits
        )

    def text(
        self,
        title: Any = "",
        text: Any = "",
        color: Optional[Union[str, int]] = None,
        bg_color: Optional[Union[str, int]] = None,
        icon: Optional[str] = None,
        spaced: bool = False,
        show: bool = True,
        no_print: bool = False,
        exits: Optional[int] = None,
    ):
        """Print a message.

        title (Any): The main text to print.
        text (Any): Optional additional text to print.
        color (Optional[Union[str, int]]): Optional foreground color.
        bg_color (Optional[Union[str, int]]): Optional background color.
        icon (Optional[str]): Optional name of icon to add.
        spaced (bool): Whether to add newlines around the output.
        show (bool): Whether to print or not. Can be used to only output
            messages under certain condition, e.g. if --verbose flag is set.
        no_print (bool): Don't actually print, just return.
        exits (Optional[int]): Perform a system exit. Optional.
        """
        if not show:
            return
        if self.pretty:
            color = self.colors.get(cast(str, color), color)
            bg_color = self.colors.get(cast(str, bg_color), bg_color)
            icon = self.icons.get(cast(str, icon))
            if icon:
                title = locale_escape("{} {}".format(icon, title)).strip()
            if self.show_color:
                title = _color(title, fg=color, bg=bg_color)
            title = wrap(title, indent=0)
        if text:
            title = "{}\n{}".format(title, wrap(text, indent=0))
        if self.timestamp:
            now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            title = "{}\t{}".format(now, title)
        if exits is not None or spaced:
            title = "\n{}\n".format(title)
        if not self.no_print and not no_print:
            print(title)
        if exits is not None:
            sys.stdout.flush()
            sys.stderr.flush()
            if self.no_print or no_print and exits != 0:
                try:
                    raise RuntimeError(title.strip())
                except Exception as e:
                    # Remove wasabi from the traceback and re-raise
                    tb = "\n".join(traceback.format_stack()[:-3])
                    raise SystemExit("{}\n{}".format(tb, e))
            sys.exit(exits)
        if self.no_print or no_print:
            return title

    def divider(
        self,
        text: str = "",
        char: str = "=",
        show: bool = True,
        icon: Optional[str] = None,
    ):
        """Print a divider with a headline:
        ============================ Headline here ===========================

        text (str): Headline text. If empty, only the line is printed.
        char (str): Line character to repeat, e.g. =.
        show (bool): Whether to print or not.
        icon (Optional[str]): Optional icon to display with title.
        """
        if not show:
            return
        if len(char) != 1:
            raise ValueError(
                "Divider chars need to be one character long. "
                "Received: {}".format(char)
            )
        if self.pretty:
            icon = self.icons.get(cast(str, icon))
            if icon:
                text = locale_escape("{} {}".format(icon, text)).strip()
            deco = char * (int(round((self.line_max - len(text))) / 2) - 2)
            text = " {} ".format(text) if text else ""
            text = _color(
                "\n{deco}{text}{deco}".format(deco=deco, text=text), bold=True
            )
            if len(text) < self.line_max:
                text = text + char * (self.line_max - len(text))
        if self.no_print:
            return text
        print(text)

    def table(self, data: Union[Collection, Dict], **kwargs):
        """Print data as a table.

        data (Union[Collection, Dict]): The data to render. Either a list of lists
            (one per row) or a dict for two-column tables.
        kwargs: Table settings. See tables.table for details.
        """
        title = kwargs.pop("title", None)
        text = table(data, **kwargs)
        if title:
            self.divider(title)
        if self.no_print:
            return text
        print(text)

    def row(self, data: Collection, **kwargs):
        """Print a table row.

        data (Collection): The individual columns to format.
        kwargs: Row settings. See tables.row for details.
        """
        text = row(data, **kwargs)
        if self.no_print:
            return text
        print(text)

    @contextmanager
    def loading(self, text: str = "Loading..."):
        if self.no_print:
            yield
        elif self.hide_animation:
            print(text)
            yield
        else:
            sys.stdout.flush()
            t = Process(target=self._spinner, args=(text,))
            t.start()
            try:
                yield
            except Exception as e:
                # Handle exception inside the with block
                t.terminate()
                sys.stdout.write("\n")
                raise (e)
            t.terminate()
            sys.stdout.write("\r\x1b[2K")  # erase line
            sys.stdout.flush()

    def _spinner(self, text: str = "Loading..."):
        for char in itertools.cycle(self.anim):
            sys.stdout.write("\r{} {}".format(char, text))
            sys.stdout.flush()
            time.sleep(0.1)

    def _get_msg(
        self,
        title: Any,
        text: Any,
        style: Optional[str] = None,
        show: bool = False,
        spaced: bool = False,
        exits: Optional[int] = None,
    ):
        if self.ignore_warnings and style == MESSAGES.WARN:
            show = False
        self._counts[style] += 1
        return self.text(
            title, text, color=style, icon=style, show=show, spaced=spaced, exits=exits
        )


================================================
FILE: wasabi/py.typed
================================================


================================================
FILE: wasabi/tables.py
================================================
import os
from itertools import zip_longest
from typing import Collection, Dict, Iterable, List, Optional, Sequence, Union
from typing import cast

from .compat import Literal
from .util import COLORS
from .util import color as _color
from .util import supports_ansi

ALIGN_MAP = {"l": "<", "r": ">", "c": "^"}


def table(
    # fmt: off
    data: Union[Collection, Dict],
    header: Optional[Iterable] = None,
    footer: Optional[Iterable] = None,
    divider: bool = False,
    widths: Union[Iterable[int], Literal["auto"]] = "auto",
    max_col: int = 30,
    spacing: int = 3,
    aligns: Optional[Union[Iterable[Literal["r", "c", "l"]], Literal["r", "c", "l"]]] = None,
    multiline: bool = False,
    env_prefix: str = "WASABI",
    color_values: Optional[Dict] = None,
    fg_colors: Optional[Iterable] = None,
    bg_colors: Optional[Iterable] = None,
    # fmt: on
) -> str:
    """Format tabular data.

    data (Union[Collection, Dict]): The data to render. Either a list of lists (one per
        row) or a dict for two-column tables.
    header (Optional[Iterable]): Optional header columns.
    footer (Optional[Iterable]): Optional footer columns.
    divider (bool): Show a divider line between header/footer and body.
    widths (Union[Iterable[int], Literal['auto']]): Column widths in order. If "auto", widths
        will be calculated automatically based on the largest value.
    max_col (int): Maximum column width.
    spacing (int): Spacing between columns, in spaces.
    aligns (Optional[Union[Iterable[str], str]]): Optional column alignments
        in order. 'l' (left, default), 'r' (right) or 'c' (center). If a string,
        value is used for all columns.
    multiline (bool): If a cell value is a list of a tuple, render it on
        multiple lines, with one value per line.
    env_prefix (str): Prefix for environment variables, e.g.
        WASABI_LOG_FRIENDLY.
    color_values (Optional[Dict]): Optional color values to add or overwrite, name mapped to value.
    fg_colors (Optional[Iterable]): Optional foreground colors, one per column. None can be specified
        for individual columns to retain the default foreground color.
    bg_colors (Optional[Iterable]): Optional background colors, one per column. None can be specified
        for individual columns to retain the default background color.
    RETURNS (str): The formatted table.
    """
    if fg_colors is not None or bg_colors is not None:
        colors = dict(COLORS)
        if color_values is not None:
            colors.update(color_values)
        if fg_colors is not None:
            fg_colors = [colors.get(fg_color, fg_color) for fg_color in fg_colors]
        if bg_colors is not None:
            bg_colors = [colors.get(bg_color, bg_color) for bg_color in bg_colors]
    if isinstance(data, dict):
        data = list(data.items())
    if multiline:
        zipped_data = []
        for i, item in enumerate(data):
            vals = [v if isinstance(v, (list, tuple)) else [v] for v in item]
            zipped_data.extend(list(zip_longest(*vals, fillvalue="")))
            if i < len(data) - 1:
                zipped_data.append(tuple(["" for i in item]))
        data = zipped_data
    if widths == "auto":
        widths = _get_max_widths(data, header, footer, max_col)
    settings = {
        "widths": widths,
        "spacing": spacing,
        "aligns": aligns,
        "env_prefix": env_prefix,
        "fg_colors": fg_colors,
        "bg_colors": bg_colors,
    }
    divider_row = row(["-" * width for width in widths], **settings)  # type: ignore
    rows = []
    if header:
        rows.append(row(header, **settings))  # type: ignore
        if divider:
            rows.append(divider_row)
    for i, item in enumerate(data):
        rows.append(row(item, **settings))  # type: ignore
    if footer:
        if divider:
            rows.append(divider_row)
        rows.append(row(footer, **settings))  # type: ignore
    return "\n{}\n".format("\n".join(rows))


def row(
    data: Collection,
    widths: Union[Sequence[int], int, Literal["auto"]] = "auto",
    spacing: int = 3,
    aligns: Optional[Union[Sequence[Literal["r", "c", "l"]], str]] = None,
    env_prefix: str = "WASABI",
    fg_colors: Optional[Sequence] = None,
    bg_colors: Optional[Sequence] = None,
) -> str:
    """Format data as a table row.

    data (Collection): The individual columns to format.
    widths (Union[Sequence[int], int, Literal['auto']]): Column widths, either one integer for all
        columns or an iterable of values. If "auto", widths will be calculated
        automatically based on the largest value.
    spacing (int): Spacing between columns, in spaces.
    aligns (Optional[Union[Sequence[Literal['r', 'c', 'l']], str]]): Optional column
        alignments in order. 'l' (left, default), 'r' (right) or 'c' (center).
        If a string, value is used for all columns.
    env_prefix (str): Prefix for environment variables, e.g.
        WASABI_LOG_FRIENDLY.
    fg_colors (Optional[Sequence]): Optional foreground colors for the columns, in order. None can be
        specified for individual columns to retain the default foreground color.
    bg_colors (Optional[Sequence]): Optional background colors for the columns, in order. None can be
        specified for individual columns to retain the default background color.
    RETURNS (str): The formatted row.
    """
    env_log_friendly = os.getenv("{}_LOG_FRIENDLY".format(env_prefix), False)
    show_colors = (
        supports_ansi()
        and not env_log_friendly
        and (fg_colors is not None or bg_colors is not None)
    )
    cols: List[str] = []
    _aligns = (
        [aligns for _ in data] if isinstance(aligns, str) else cast(List[str], aligns)
    )
    if not hasattr(widths, "__iter__") and widths != "auto":  # single number
        widths = cast(List[int], [widths for _ in range(len(data))])
    for i, col in enumerate(data):
        align = ALIGN_MAP.get(_aligns[i] if _aligns and i < len(_aligns) else "l")
        col_width = len(col) if widths == "auto" else cast(List[int], widths)[i]
        tpl = "{:%s%d}" % (align, col_width)
        col = tpl.format(str(col))
        if show_colors:
            fg = fg_colors[i] if fg_colors is not None else None
            bg = bg_colors[i] if bg_colors is not None else None
            col = _color(col, fg=fg, bg=bg)
        cols.append(col)
    return (" " * spacing).join(cols)


def _get_max_widths(data, header, footer, max_col):
    all_data = list(data)
    if header:
        all_data.append(header)
    if footer:
        all_data.append(footer)
    widths = [[len(str(col)) for col in item] for item in all_data]
    return [min(max(w), max_col) for w in list(zip(*widths))]


================================================
FILE: wasabi/tests/__init__.py
================================================


================================================
FILE: wasabi/tests/test-data/wasabi-test-notebook.ipynb
================================================
{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "eb5586f8",
   "metadata": {},
   "outputs": [],
   "source": [
    "import sys\n",
    "import wasabi\n",
    "\n",
    "wasabi.msg.warn(\"This is a test. This is only a test.\")\n",
    "if sys.version_info >= (3, 7):\n",
    "    assert wasabi.util.supports_ansi()\n",
    "\n",
    "print(sys.stdout)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}


================================================
FILE: wasabi/tests/test_jupyter.py
================================================
from pathlib import Path
import subprocess
import os
import sys

import wasabi

TEST_DATA = Path(__file__).absolute().parent / "test-data"
WASABI_DIR = Path(wasabi.__file__).absolute().parent.parent


def test_jupyter():
    # This runs some code in a jupyter notebook environment, but without actually
    # starting up the notebook UI. Historically we once had a bug that caused crashes
    # when importing wasabi in a jupyter notebook, because they replace
    # sys.stdout/stderr with custom objects that aren't "real" files/ttys. So this makes
    # sure that we can import and use wasabi inside a notebook without crashing.
    env = dict(os.environ)
    if "PYTHONPATH" in env:
        env["PYTHONPATH"] = f"{WASABI_DIR}{os.pathsep}{env['PYTHONPATH']}"
    else:
        env["PYTHONPATH"] = str(WASABI_DIR)
    subprocess.run(
        [
            sys.executable,
            "-m",
            "nbconvert",
            str(TEST_DATA / "wasabi-test-notebook.ipynb"),
            "--execute",
            "--stdout",
            "--to",
            "notebook",
        ],
        env=env,
        check=True,
    )


================================================
FILE: wasabi/tests/test_markdown.py
================================================
import pytest

from wasabi.markdown import MarkdownRenderer


def test_markdown():
    md = MarkdownRenderer()
    md.add(md.title(1, "Title"))
    md.add("Paragraph with {}".format(md.bold("bold")))
    md.add(md.list(["foo", "bar"]))
    md.add(md.table([("a", "b"), ("c", "d")], ["foo", "bar"]))
    md.add(md.code_block('import spacy\n\nnlp = spacy.blank("en")', "python"))
    md.add(md.list(["first", "second"], numbered=True))
    expected = """# Title\n\nParagraph with **bold**\n\n- foo\n- bar\n\n| foo | bar |\n| --- | --- |\n| a | b |\n| c | d |\n\n```python\nimport spacy\n\nnlp = spacy.blank("en")\n```\n\n1. first\n2. second"""
    assert md.text == expected


def test_markdown_table_aligns():
    md = MarkdownRenderer()
    md.add(md.table([("a", "b", "c")], ["foo", "bar", "baz"], aligns=("c", "r", "l")))
    expected = """| foo | bar | baz |\n| :---: | ---: | --- |\n| a | b | c |"""
    assert md.text == expected
    with pytest.raises(ValueError):
        md.table([("a", "b", "c")], ["foo", "bar", "baz"], aligns=("c", "r"))
    with pytest.raises(ValueError):
        md.table([("a", "b", "c")], ["foo", "bar", "baz"], aligns=("c", "r", "l", "l"))


================================================
FILE: wasabi/tests/test_printer.py
================================================
import os
import re
import time

import pytest

from wasabi.printer import Printer
from wasabi.util import MESSAGES, NO_UTF8, supports_ansi

SUPPORTS_ANSI = supports_ansi()


def test_printer():
    p = Printer(no_print=True)
    text = "This is a test."
    good = p.good(text)
    fail = p.fail(text)
    warn = p.warn(text)
    info = p.info(text)
    assert p.text(text) == text
    if SUPPORTS_ANSI and not NO_UTF8:
        assert good == "\x1b[38;5;2m\u2714 {}\x1b[0m".format(text)
        assert fail == "\x1b[38;5;1m\u2718 {}\x1b[0m".format(text)
        assert warn == "\x1b[38;5;3m\u26a0 {}\x1b[0m".format(text)
        assert info == "\x1b[38;5;4m\u2139 {}\x1b[0m".format(text)
    if SUPPORTS_ANSI and NO_UTF8:
        assert good == "\x1b[38;5;2m[+] {}\x1b[0m".format(text)
        assert fail == "\x1b[38;5;1m[x] {}\x1b[0m".format(text)
        assert warn == "\x1b[38;5;3m[!] {}\x1b[0m".format(text)
        assert info == "\x1b[38;5;4m[i] {}\x1b[0m".format(text)
    if not SUPPORTS_ANSI and not NO_UTF8:
        assert good == "\u2714 {}".format(text)
        assert fail == "\u2718 {}".format(text)
        assert warn == "\u26a0 {}".format(text)
        assert info == "\u2139 {}".format(text)
    if not SUPPORTS_ANSI and NO_UTF8:
        assert good == "[+] {}".format(text)
        assert fail == "[x] {}".format(text)
        assert warn == "[!] {}".format(text)
        assert info == "[i] {}".format(text)


def test_printer_print():
    p = Printer()
    text = "This is a test."
    p.good(text)
    p.fail(text)
    p.info(text)
    p.text(text)


def test_printer_print_timestamp():
    p = Printer(no_print=True, timestamp=True)
    result = p.info("Hello world")
    matches = re.match("^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}", result)
    assert matches


def test_printer_no_pretty():
    p = Printer(no_print=True, pretty=False)
    text = "This is a test."
    assert p.good(text) == text
    assert p.fail(text) == text
    assert p.warn(text) == text
    assert p.info(text) == text
    assert p.text(text) == text


def test_printer_custom():
    colors = {"yellow": 220, "purple": 99}
    icons = {"warn": "\u26a0\ufe0f", "question": "?"}
    p = Printer(no_print=True, colors=colors, icons=icons)
    text = "This is a test."
    purple_question = p.text(text, color="purple", icon="question")
    warning = p.warn(text)
    if SUPPORTS_ANSI and not NO_UTF8:
        assert purple_question == "\x1b[38;5;99m? {}\x1b[0m".format(text)
        assert warning == "\x1b[38;5;3m\u26a0\ufe0f {}\x1b[0m".format(text)
    if SUPPORTS_ANSI and NO_UTF8:
        assert purple_question == "\x1b[38;5;99m? {}\x1b[0m".format(text)
        assert warning == "\x1b[38;5;3m?? {}\x1b[0m".format(text)
    if not SUPPORTS_ANSI and not NO_UTF8:
        assert purple_question == "? {}".format(text)
        assert warning == "\u26a0\ufe0f {}".format(text)
    if not SUPPORTS_ANSI and NO_UTF8:
        assert purple_question == "? {}".format(text)
        assert warning == "?? {}".format(text)


def test_color_as_int():
    p = Printer(no_print=True)
    text = "This is a text."
    result = p.text(text, color=220)
    if SUPPORTS_ANSI:
        assert result == "\x1b[38;5;220mThis is a text.\x1b[0m"
    else:
        assert result == "This is a text."


def test_bg_color():
    p = Printer(no_print=True)
    text = "This is a text."
    result = p.text(text, bg_color="red")
    print(result)
    if SUPPORTS_ANSI:
        assert result == "\x1b[48;5;1mThis is a text.\x1b[0m"
    else:
        assert result == "This is a text."


def test_bg_color_as_int():
    p = Printer(no_print=True)
    text = "This is a text."
    result = p.text(text, bg_color=220)
    print(result)
    if SUPPORTS_ANSI:
        assert result == "\x1b[48;5;220mThis is a text.\x1b[0m"
    else:
        assert result == "This is a text."


def test_color_and_bc_color():
    p = Printer(no_print=True)
    text = "This is a text."
    result = p.text(text, color="green", bg_color="yellow")
    print(result)
    if SUPPORTS_ANSI:
        assert result == "\x1b[38;5;2;48;5;3mThis is a text.\x1b[0m"
    else:
        assert result == "This is a text."


def test_printer_counts():
    p = Printer()
    text = "This is a test."
    for i in range(2):
        p.good(text)
    for i in range(1):
        p.fail(text)
    for i in range(4):
        p.warn(text)
    assert p.counts[MESSAGES.GOOD] == 2
    assert p.counts[MESSAGES.FAIL] == 1
    assert p.counts[MESSAGES.WARN] == 4


def test_printer_spaced():
    p = Printer(no_print=True, pretty=False)
    text = "This is a test."
    assert p.good(text) == text
    assert p.good(text, spaced=True) == "\n{}\n".format(text)


def test_printer_divider():
    p = Printer(line_max=20, no_print=True)
    p.divider() == "\x1b[1m\n================\x1b[0m"
    p.divider("test") == "\x1b[1m\n====== test ======\x1b[0m"
    p.divider("test", char="*") == "\x1b[1m\n****** test ******\x1b[0m"
    assert (
        p.divider("This is a very long text, it is very long")
        == "\x1b[1m\n This is a very long text, it is very long \x1b[0m"
    )
    with pytest.raises(ValueError):
        p.divider("test", char="~.")


@pytest.mark.parametrize("hide_animation", [False, True])
def test_printer_loading(hide_animation):
    p = Printer(hide_animation=hide_animation)
    print("\n")
    with p.loading("Loading..."):
        time.sleep(1)
    p.good("Success!")

    with p.loading("Something else..."):
        time.sleep(2)
    p.good("Yo!")

    with p.loading("Loading..."):
        time.sleep(1)
    p.good("Success!")


def test_printer_loading_raises_exception():
    def loading_with_exception():
        p = Printer()
        print("\n")
        with p.loading():
            raise Exception("This is an error.")

    with pytest.raises(Exception):
        loading_with_exception()


def test_printer_loading_no_print():
    p = Printer(no_print=True)
    with p.loading("Loading..."):
        time.sleep(1)
    p.good("Success!")


def test_printer_log_friendly():
    text = "This is a test."
    ENV_LOG_FRIENDLY = "WASABI_LOG_FRIENDLY"
    os.environ[ENV_LOG_FRIENDLY] = "True"
    p = Printer(no_print=True)
    assert p.good(text) in ("\u2714 This is a test.", "[+] This is a test.")
    del os.environ[ENV_LOG_FRIENDLY]


def test_printer_log_friendly_prefix():
    text = "This is a test."
    ENV_LOG_FRIENDLY = "CUSTOM_LOG_FRIENDLY"
    os.environ[ENV_LOG_FRIENDLY] = "True"
    p = Printer(no_print=True, env_prefix="CUSTOM")
    assert p.good(text) in ("\u2714 This is a test.", "[+] This is a test.")
    print(p.good(text))
    del os.environ[ENV_LOG_FRIENDLY]


@pytest.mark.skip(reason="Now seems to raise TypeError: readonly attribute?")
def test_printer_none_encoding(monkeypatch):
    """Test that printer works even if sys.stdout.encoding is set to None. This
    previously caused a very confusing error."""
    monkeypatch.setattr("sys.stdout.encoding", None)
    p = Printer()  # noqa: F841


def test_printer_no_print_raise_on_exit():
    """Test that the printer raises if a non-zero exit code is provided, even
    if no_print is set to True."""
    err = "This is an error."
    p = Printer(no_print=True, pretty=False)
    with pytest.raises(SystemExit) as e:
        p.fail(err, exits=True)
    assert str(e.value).strip()[-len(err) :] == err


================================================
FILE: wasabi/tests/test_tables.py
================================================
import os

import pytest

from wasabi.tables import row, table
from wasabi.util import supports_ansi

SUPPORTS_ANSI = supports_ansi()


@pytest.fixture()
def data():
    return [("Hello", "World", "12344342"), ("This is a test", "World", "1234")]


@pytest.fixture()
def header():
    return ["COL A", "COL B", "COL 3"]


@pytest.fixture()
def footer():
    return ["", "", "2030203.00"]


@pytest.fixture()
def fg_colors():
    return ["", "yellow", "87"]


@pytest.fixture()
def bg_colors():
    return ["green", "23", ""]


def test_table_default(data):
    result = table(data)
    assert (
        result
        == "\nHello            World   12344342\nThis is a test   World   1234    \n"
    )


def test_table_header(data, header):
    result = table(data, header=header)
    assert (
        result
        == "\nCOL A            COL B   COL 3   \nHello            World   12344342\nThis is a test   World   1234    \n"
    )


def test_table_header_footer_divider(data, header, footer):
    result = table(data, header=header, footer=footer, divider=True)
    assert (
        result
        == "\nCOL A            COL B   COL 3     \n--------------   -----   ----------\nHello            World   12344342  \nThis is a test   World   1234      \n--------------   -----   ----------\n                         2030203.00\n"
    )


def test_table_aligns(data):
    result = table(data, aligns=("r", "c", "l"))
    assert (
        result
        == "\n         Hello   World   12344342\nThis is a test   World   1234    \n"
    )


def test_table_aligns_single(data):
    result = table(data, aligns="r")
    assert (
        result
        == "\n         Hello   World   12344342\nThis is a test   World       1234\n"
    )


def test_table_widths():
    data = [("a", "bb", "ccc"), ("d", "ee", "fff")]
    widths = (5, 2, 10)
    result = table(data, widths=widths)
    assert result == "\na       bb   ccc       \nd       ee   fff       \n"


def test_row_single_widths():
    data = ("a", "bb", "ccc")
    result = row(data, widths=10)
    assert result == "a            bb           ccc       "


def test_table_multiline(header):
    data = [
        ("hello", ["foo", "bar", "baz"], "world"),
        ("hello", "world", ["world 1", "world 2"]),
    ]
    result = table(data, header=header, divider=True, multiline=True)
    assert (
        result
        == "\nCOL A   COL B   COL 3  \n-----   -----   -------\nhello   foo     world  \n        bar            \n        baz            \n                       \nhello   world   world 1\n                world 2\n"
    )


def test_row_fg_colors(fg_colors):
    result = row(("Hello", "World", "12344342"), fg_colors=fg_colors)
    if SUPPORTS_ANSI:
        assert (
            result == "Hello   \x1b[38;5;3mWorld\x1b[0m   \x1b[38;5;87m12344342\x1b[0m"
        )
    else:
        assert result == "Hello   World   12344342"


def test_row_bg_colors(bg_colors):
    result = row(("Hello", "World", "12344342"), bg_colors=bg_colors)
    if SUPPORTS_ANSI:
        assert (
            result == "\x1b[48;5;2mHello\x1b[0m   \x1b[48;5;23mWorld\x1b[0m   12344342"
        )
    else:
        assert result == "Hello   World   12344342"


def test_row_fg_colors_and_bg_colors(fg_colors, bg_colors):
    result = row(
        ("Hello", "World", "12344342"), fg_colors=fg_colors, bg_colors=bg_colors
    )
    if SUPPORTS_ANSI:
        assert (
            result
            == "\x1b[48;5;2mHello\x1b[0m   \x1b[38;5;3;48;5;23mWorld\x1b[0m   \x1b[38;5;87m12344342\x1b[0m"
        )
    else:
        assert result == "Hello   World   12344342"


def test_row_fg_colors_and_bg_colors_log_friendly(fg_colors, bg_colors):
    ENV_LOG_FRIENDLY = "WASABI_LOG_FRIENDLY"
    os.environ[ENV_LOG_FRIENDLY] = "True"
    result = row(
        ("Hello", "World", "12344342"), fg_colors=fg_colors, bg_colors=bg_colors
    )
    assert result == "Hello   World   12344342"
    del os.environ[ENV_LOG_FRIENDLY]


def test_row_fg_colors_and_bg_colors_log_friendly_prefix(fg_colors, bg_colors):
    ENV_LOG_FRIENDLY = "CUSTOM_LOG_FRIENDLY"
    os.environ[ENV_LOG_FRIENDLY] = "True"
    result = row(
        ("Hello", "World", "12344342"),
        fg_colors=fg_colors,
        bg_colors=bg_colors,
        env_prefix="CUSTOM",
    )
    assert result == "Hello   World   12344342"
    del os.environ[ENV_LOG_FRIENDLY]


def test_row_fg_colors_and_bg_colors_supports_ansi_false(fg_colors, bg_colors):
    os.environ["ANSI_COLORS_DISABLED"] = "True"
    result = row(
        ("Hello", "World", "12344342"), fg_colors=fg_colors, bg_colors=bg_colors
    )
    assert result == "Hello   World   12344342"
    del os.environ["ANSI_COLORS_DISABLED"]


def test_colors_whole_table_with_automatic_widths(
    data, header, footer, fg_colors, bg_colors
):
    result = table(
        data,
        header=header,
        footer=footer,
        divider=True,
        fg_colors=fg_colors,
        bg_colors=bg_colors,
    )
    if SUPPORTS_ANSI:
        assert (
            result
            == "\n\x1b[48;5;2mCOL A         \x1b[0m   \x1b[38;5;3;48;5;23mCOL B\x1b[0m   \x1b[38;5;87mCOL 3     \x1b[0m\n\x1b[48;5;2m--------------\x1b[0m   \x1b[38;5;3;48;5;23m-----\x1b[0m   \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2mHello         \x1b[0m   \x1b[38;5;3;48;5;23mWorld\x1b[0m   \x1b[38;5;87m12344342  \x1b[0m\n\x1b[48;5;2mThis is a test\x1b[0m   \x1b[38;5;3;48;5;23mWorld\x1b[0m   \x1b[38;5;87m1234      \x1b[0m\n\x1b[48;5;2m--------------\x1b[0m   \x1b[38;5;3;48;5;23m-----\x1b[0m   \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2m              \x1b[0m   \x1b[38;5;3;48;5;23m     \x1b[0m   \x1b[38;5;87m2030203.00\x1b[0m\n"
        )
    else:
        assert (
            result
            == "\nCOL A            COL B   COL 3     \n--------------   -----   ----------\nHello            World   12344342  \nThis is a test   World   1234      \n--------------   -----   ----------\n                         2030203.00\n"
        )


def test_colors_whole_table_only_fg_colors(data, header, footer, fg_colors):
    result = table(
        data,
        header=header,
        footer=footer,
        divider=True,
        fg_colors=fg_colors,
    )
    if SUPPORTS_ANSI:
        assert (
            result
            == "\nCOL A            \x1b[38;5;3mCOL B\x1b[0m   \x1b[38;5;87mCOL 3     \x1b[0m\n--------------   \x1b[38;5;3m-----\x1b[0m   \x1b[38;5;87m----------\x1b[0m\nHello            \x1b[38;5;3mWorld\x1b[0m   \x1b[38;5;87m12344342  \x1b[0m\nThis is a test   \x1b[38;5;3mWorld\x1b[0m   \x1b[38;5;87m1234      \x1b[0m\n--------------   \x1b[38;5;3m-----\x1b[0m   \x1b[38;5;87m----------\x1b[0m\n                 \x1b[38;5;3m     \x1b[0m   \x1b[38;5;87m2030203.00\x1b[0m\n"
        )
    else:
        assert (
            result
            == "\nCOL A            COL B   COL 3     \n--------------   -----   ----------\nHello            World   12344342  \nThis is a test   World   1234      \n--------------   -----   ----------\n                         2030203.00\n"
        )


def test_colors_whole_table_only_bg_colors(data, header, footer, bg_colors):
    result = table(
        data,
        header=header,
        footer=footer,
        divider=True,
        bg_colors=bg_colors,
    )
    if SUPPORTS_ANSI:
        assert (
            result
            == "\n\x1b[48;5;2mCOL A         \x1b[0m   \x1b[48;5;23mCOL B\x1b[0m   COL 3     \n\x1b[48;5;2m--------------\x1b[0m   \x1b[48;5;23m-----\x1b[0m   ----------\n\x1b[48;5;2mHello         \x1b[0m   \x1b[48;5;23mWorld\x1b[0m   12344342  \n\x1b[48;5;2mThis is a test\x1b[0m   \x1b[48;5;23mWorld\x1b[0m   1234      \n\x1b[48;5;2m--------------\x1b[0m   \x1b[48;5;23m-----\x1b[0m   ----------\n\x1b[48;5;2m              \x1b[0m   \x1b[48;5;23m     \x1b[0m   2030203.00\n"
        )
    else:
        assert (
            result
            == "\nCOL A            COL B   COL 3     \n--------------   -----   ----------\nHello            World   12344342  \nThis is a test   World   1234      \n--------------   -----   ----------\n                         2030203.00\n"
        )


def test_colors_whole_table_with_supplied_spacing(
    data, header, footer, fg_colors, bg_colors
):
    result = table(
        data,
        header=header,
        footer=footer,
        divider=True,
        fg_colors=fg_colors,
        bg_colors=bg_colors,
        spacing=5,
    )
    if SUPPORTS_ANSI:
        assert (
            result
            == "\n\x1b[48;5;2mCOL A         \x1b[0m     \x1b[38;5;3;48;5;23mCOL B\x1b[0m     \x1b[38;5;87mCOL 3     \x1b[0m\n\x1b[48;5;2m--------------\x1b[0m     \x1b[38;5;3;48;5;23m-----\x1b[0m     \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2mHello         \x1b[0m     \x1b[38;5;3;48;5;23mWorld\x1b[0m     \x1b[38;5;87m12344342  \x1b[0m\n\x1b[48;5;2mThis is a test\x1b[0m     \x1b[38;5;3;48;5;23mWorld\x1b[0m     \x1b[38;5;87m1234      \x1b[0m\n\x1b[48;5;2m--------------\x1b[0m     \x1b[38;5;3;48;5;23m-----\x1b[0m     \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2m              \x1b[0m     \x1b[38;5;3;48;5;23m     \x1b[0m     \x1b[38;5;87m2030203.00\x1b[0m\n"
        )
    else:
        assert (
            result
            == "\nCOL A              COL B     COL 3     \n--------------     -----     ----------\nHello              World     12344342  \nThis is a test     World     1234      \n--------------     -----     ----------\n                             2030203.00\n"
        )


def test_colors_whole_table_with_supplied_widths(
    data, header, footer, fg_colors, bg_colors
):
    result = table(
        data,
        header=header,
        footer=footer,
        divider=True,
        fg_colors=fg_colors,
        bg_colors=bg_colors,
        widths=(5, 2, 10),
    )
    if SUPPORTS_ANSI:
        assert (
            result
            == "\n\x1b[48;5;2mCOL A\x1b[0m   \x1b[38;5;3;48;5;23mCOL B\x1b[0m   \x1b[38;5;87mCOL 3     \x1b[0m\n\x1b[48;5;2m-----\x1b[0m   \x1b[38;5;3;48;5;23m--\x1b[0m   \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2mHello\x1b[0m   \x1b[38;5;3;48;5;23mWorld\x1b[0m   \x1b[38;5;87m12344342  \x1b[0m\n\x1b[48;5;2mThis is a test\x1b[0m   \x1b[38;5;3;48;5;23mWorld\x1b[0m   \x1b[38;5;87m1234      \x1b[0m\n\x1b[48;5;2m-----\x1b[0m   \x1b[38;5;3;48;5;23m--\x1b[0m   \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2m     \x1b[0m   \x1b[38;5;3;48;5;23m  \x1b[0m   \x1b[38;5;87m2030203.00\x1b[0m\n"
        )
    else:
        assert (
            result
            == "\nCOL A   COL B   COL 3     \n-----   --   ----------\nHello   World   12344342  \nThis is a test   World   1234      \n-----   --   ----------\n             2030203.00\n"
        )


def test_colors_whole_table_with_single_alignment(
    data, header, footer, fg_colors, bg_colors
):
    result = table(
        data,
        header=header,
        footer=footer,
        divider=True,
        fg_colors=fg_colors,
        bg_colors=bg_colors,
        aligns="r",
    )
    if SUPPORTS_ANSI:
        assert (
            result
            == "\n\x1b[48;5;2m         COL A\x1b[0m   \x1b[38;5;3;48;5;23mCOL B\x1b[0m   \x1b[38;5;87m     COL 3\x1b[0m\n\x1b[48;5;2m--------------\x1b[0m   \x1b[38;5;3;48;5;23m-----\x1b[0m   \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2m         Hello\x1b[0m   \x1b[38;5;3;48;5;23mWorld\x1b[0m   \x1b[38;5;87m  12344342\x1b[0m\n\x1b[48;5;2mThis is a test\x1b[0m   \x1b[38;5;3;48;5;23mWorld\x1b[0m   \x1b[38;5;87m      1234\x1b[0m\n\x1b[48;5;2m--------------\x1b[0m   \x1b[38;5;3;48;5;23m-----\x1b[0m   \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2m              \x1b[0m   \x1b[38;5;3;48;5;23m     \x1b[0m   \x1b[38;5;87m2030203.00\x1b[0m\n"
        )
    else:
        assert (
            result
            == "\n         COL A   COL B        COL 3\n--------------   -----   ----------\n         Hello   World     12344342\nThis is a test   World         1234\n--------------   -----   ----------\n                         2030203.00\n"
        )


def test_colors_whole_table_with_multiple_alignment(
    data, header, footer, fg_colors, bg_colors
):
    result = table(
        data,
        header=header,
        footer=footer,
        divider=True,
        fg_colors=fg_colors,
        bg_colors=bg_colors,
        aligns=("c", "r", "l"),
    )
    if SUPPORTS_ANSI:
        assert (
            result
            == "\n\x1b[48;5;2m    COL A     \x1b[0m   \x1b[38;5;3;48;5;23mCOL B\x1b[0m   \x1b[38;5;87mCOL 3     \x1b[0m\n\x1b[48;5;2m--------------\x1b[0m   \x1b[38;5;3;48;5;23m-----\x1b[0m   \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2m    Hello     \x1b[0m   \x1b[38;5;3;48;5;23mWorld\x1b[0m   \x1b[38;5;87m12344342  \x1b[0m\n\x1b[48;5;2mThis is a test\x1b[0m   \x1b[38;5;3;48;5;23mWorld\x1b[0m   \x1b[38;5;87m1234      \x1b[0m\n\x1b[48;5;2m--------------\x1b[0m   \x1b[38;5;3;48;5;23m-----\x1b[0m   \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2m              \x1b[0m   \x1b[38;5;3;48;5;23m     \x1b[0m   \x1b[38;5;87m2030203.00\x1b[0m\n"
        )
    else:
        assert (
            result
            == "\n    COL A        COL B   COL 3     \n--------------   -----   ----------\n    Hello        World   12344342  \nThis is a test   World   1234      \n--------------   -----   ----------\n                         2030203.00\n"
        )


def test_colors_whole_table_with_multiline(data, header, footer, fg_colors, bg_colors):
    result = table(
        data=((["Charles", "Quinton", "Murphy"], "my", "brother"), ("1", "2", "3")),
        fg_colors=fg_colors,
        bg_colors=bg_colors,
        multiline=True,
    )
    if SUPPORTS_ANSI:
        assert (
            result
            == "\n\x1b[48;5;2mCharles\x1b[0m   \x1b[38;5;3;48;5;23mmy\x1b[0m   \x1b[38;5;87mbrother\x1b[0m\n\x1b[48;5;2mQuinton\x1b[0m   \x1b[38;5;3;48;5;23m  \x1b[0m   \x1b[38;5;87m       \x1b[0m\n\x1b[48;5;2mMurphy \x1b[0m   \x1b[38;5;3;48;5;23m  \x1b[0m   \x1b[38;5;87m       \x1b[0m\n\x1b[48;5;2m       \x1b[0m   \x1b[38;5;3;48;5;23m  \x1b[0m   \x1b[38;5;87m       \x1b[0m\n\x1b[48;5;2m1      \x1b[0m   \x1b[38;5;3;48;5;23m2 \x1b[0m   \x1b[38;5;87m3      \x1b[0m\n"
        )
    else:
        assert (
            result
            == "\nCharles   my   brother\nQuinton               \nMurphy                \n                      \n1         2    3      \n"
        )


def test_colors_whole_table_log_friendly(data, header, footer, fg_colors, bg_colors):
    ENV_LOG_FRIENDLY = "WASABI_LOG_FRIENDLY"
    os.environ[ENV_LOG_FRIENDLY] = "True"
    result = table(
        data,
        header=header,
        footer=footer,
        divider=True,
        fg_colors=fg_colors,
        bg_colors=bg_colors,
    )
    assert (
        result
        == "\nCOL A            COL B   COL 3     \n--------------   -----   ----------\nHello            World   12344342  \nThis is a test   World   1234      \n--------------   -----   ----------\n                         2030203.00\n"
    )
    del os.environ[ENV_LOG_FRIENDLY]


def test_colors_whole_table_log_friendly_prefix(
    data, header, footer, fg_colors, bg_colors
):
    ENV_LOG_FRIENDLY = "CUSTOM_LOG_FRIENDLY"
    os.environ[ENV_LOG_FRIENDLY] = "True"
    result = table(
        data,
        header=header,
        footer=footer,
        divider=True,
        fg_colors=fg_colors,
        bg_colors=bg_colors,
        env_prefix="CUSTOM",
    )
    assert (
        result
        == "\nCOL A            COL B   COL 3     \n--------------   -----   ----------\nHello            World   12344342  \nThis is a test   World   1234      \n--------------   -----   ----------\n                         2030203.00\n"
    )
    del os.environ[ENV_LOG_FRIENDLY]


def test_colors_whole_table_supports_ansi_false(
    data, header, footer, fg_colors, bg_colors
):
    os.environ["ANSI_COLORS_DISABLED"] = "True"
    result = table(
        data,
        header=header,
        footer=footer,
        divider=True,
        fg_colors=fg_colors,
        bg_colors=bg_colors,
    )
    assert (
        result
        == "\nCOL A            COL B   COL 3     \n--------------   -----   ----------\nHello            World   12344342  \nThis is a test   World   1234      \n--------------   -----   ----------\n                         2030203.00\n"
    )
    del os.environ["ANSI_COLORS_DISABLED"]


def test_colors_whole_table_color_values(data, header, footer, fg_colors, bg_colors):
    result = table(
        data,
        header=header,
        footer=footer,
        divider=True,
        fg_colors=fg_colors,
        bg_colors=bg_colors,
        color_values={"yellow": 11},
    )
    if SUPPORTS_ANSI:
        assert (
            result
            == "\n\x1b[48;5;2mCOL A         \x1b[0m   \x1b[38;5;11;48;5;23mCOL B\x1b[0m   \x1b[38;5;87mCOL 3     \x1b[0m\n\x1b[48;5;2m--------------\x1b[0m   \x1b[38;5;11;48;5;23m-----\x1b[0m   \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2mHello         \x1b[0m   \x1b[38;5;11;48;5;23mWorld\x1b[0m   \x1b[38;5;87m12344342  \x1b[0m\n\x1b[48;5;2mThis is a test\x1b[0m   \x1b[38;5;11;48;5;23mWorld\x1b[0m   \x1b[38;5;87m1234      \x1b[0m\n\x1b[48;5;2m--------------\x1b[0m   \x1b[38;5;11;48;5;23m-----\x1b[0m   \x1b[38;5;87m----------\x1b[0m\n\x1b[48;5;2m              \x1b[0m   \x1b[38;5;11;48;5;23m     \x1b[0m   \x1b[38;5;87m2030203.00\x1b[0m\n"
        )
    else:
        assert (
            result
            == "\nCOL A            COL B   COL 3     \n--------------   -----   ----------\nHello            World   12344342  \nThis is a test   World   1234      \n--------------   -----   ----------\n                         2030203.00\n"
        )


================================================
FILE: wasabi/tests/test_traceback.py
================================================
import traceback

import pytest

from wasabi.traceback_printer import TracebackPrinter


@pytest.fixture
def tb():
    return traceback.extract_stack()


def test_traceback_printer(tb):
    tbp = TracebackPrinter(tb_base="wasabi")
    msg = tbp("Hello world", "This is a test", tb=tb)
    print(msg)


def test_traceback_printer_highlight(tb):
    tbp = TracebackPrinter(tb_base="wasabi")
    msg = tbp("Hello world", "This is a test", tb=tb, highlight="kwargs")
    print(msg)


def test_traceback_printer_custom_colors(tb):
    tbp = TracebackPrinter(
        tb_base="wasabi", color_error="blue", color_highlight="green", color_tb="yellow"
    )
    msg = tbp("Hello world", "This is a test", tb=tb, highlight="kwargs")
    print(msg)


def test_traceback_printer_only_title(tb):
    tbp = TracebackPrinter(tb_base="wasabi")
    msg = tbp("Hello world", tb=tb)
    print(msg)


def test_traceback_dot_relative_path_tb_base(tb):
    tbp = TracebackPrinter(tb_base=".")
    msg = tbp("Hello world", tb=tb)
    print(msg)


def test_traceback_tb_base_none(tb):
    tbp = TracebackPrinter()
    msg = tbp("Hello world", tb=tb)
    print(msg)


def test_traceback_printer_no_tb():
    tbp = TracebackPrinter(tb_base="wasabi")
    msg = tbp("Hello world", "This is a test")
    print(msg)


def test_traceback_printer_custom_tb_range():
    tbp = TracebackPrinter(tb_range_start=-10, tb_range_end=-3)
    msg = tbp("Hello world", "This is a test")
    print(msg)


def test_traceback_printer_custom_tb_range_start():
    tbp = TracebackPrinter(tb_range_start=-1)
    msg = tbp("Hello world", "This is a test")
    print(msg)


================================================
FILE: wasabi/tests/test_util.py
================================================
import pytest

from wasabi.util import color, diff_strings, format_repr, locale_escape, wrap


def test_color():
    assert color("test", fg="green") == "\x1b[38;5;2mtest\x1b[0m"
    assert color("test", fg=4) == "\x1b[38;5;4mtest\x1b[0m"
    assert color("test", bold=True) == "\x1b[1mtest\x1b[0m"
    assert color("test", fg="red", underline=True) == "\x1b[4;38;5;1mtest\x1b[0m"
    assert (
        color("test", fg=7, bg="red", bold=True) == "\x1b[1;38;5;7;48;5;1mtest\x1b[0m"
    )


def test_wrap():
    text = "Hello world, this is a test."
    assert wrap(text, indent=0) == text
    assert wrap(text, indent=4) == "    Hello world, this is a test."
    assert wrap(text, wrap_max=10, indent=0) == "Hello\nworld,\nthis is a\ntest."
    assert (
        wrap(text, wrap_max=5, indent=2)
        == "  Hello\n  world,\n  this\n  is\n  a\n  test."
    )


def test_format_repr():
    obj = {"hello": "world", "test": 123}
    formatted = format_repr(obj)
    assert formatted.replace("u'", "'") in [
        "{'hello': 'world', 'test': 123}",
        "{'test': 123, 'hello': 'world'}",
    ]
    formatted = format_repr(obj, max_len=10)
    assert formatted.replace("u'", "'") in [
        "{'hel ...  123}",
        "{'tes ... rld'}",
        "{'te ... rld'}",
    ]
    formatted = format_repr(obj, max_len=10, ellipsis="[...]")
    assert formatted.replace("u'", "'") in [
        "{'hel [...]  123}",
        "{'tes [...] rld'}",
        "{'te [...] rld'}",
    ]


@pytest.mark.parametrize(
    "text,non_ascii",
    [
        ("abc", ["abc"]),
        ("\u2714 abc", ["? abc"]),
        ("👻", ["??", "?"]),  # On Python 3 windows, this becomes "?" instead of "??"
    ],
)
def test_locale_escape(text, non_ascii):
    result = locale_escape(text)
    assert result == text or result in non_ascii
    print(result)


def test_diff_strings():
    a = "hello\nworld\nwide\nweb"
    b = "yo\nwide\nworld\nweb"
    expected = "\x1b[38;5;16;48;5;2myo\x1b[0m\n\x1b[38;5;16;48;5;2mwide\x1b[0m\n\x1b[38;5;16;48;5;1mhello\x1b[0m\nworld\n\x1b[38;5;16;48;5;1mwide\x1b[0m\nweb"
    assert diff_strings(a, b) == expected


def test_diff_strings_with_symbols():
    a = "hello\nworld\nwide\nweb"
    b = "yo\nwide\nworld\nweb"
    expected = "\x1b[38;5;16;48;5;2m+ yo\x1b[0m\n\x1b[38;5;16;48;5;2m+ wide\x1b[0m\n\x1b[38;5;16;48;5;1m- hello\x1b[0m\nworld\n\x1b[38;5;16;48;5;1m- wide\x1b[0m\nweb"
    assert diff_strings(a, b, add_symbols=True) == expected


================================================
FILE: wasabi/traceback_printer.py
================================================
import os
from typing import Optional, Tuple, Union

from .util import NO_UTF8, color, supports_ansi

LINE_EDGE = "└─" if not NO_UTF8 else "|_"
LINE_FORK = "├─" if not NO_UTF8 else "|__"
LINE_PATH = "──" if not NO_UTF8 else "__"


class TracebackPrinter(object):
    def __init__(
        self,
        color_error: Union[str, int] = "red",
        color_tb: Union[str, int] = "blue",
        color_highlight: Union[str, int] = "yellow",
        indent: int = 2,
        tb_base: Optional[str] = None,
        tb_exclude: Tuple = tuple(),
        tb_range_start: int = -5,
        tb_range_end: int = -2,
    ):
        """Initialize a traceback printer.

        color_error (Union[str, int]): Color name or code for errors.
        color_tb (Union[str, int]): Color name or code for traceback headline.
        color_highlight (Union[str, int]): Color name or code for highlights.
        indent (int): Indentation in spaces.
        tb_base (Optional[str]): Optional name of directory to use to show relative paths. For
            example, "thinc" will look for the last occurence of "/thinc/" in
            a path and only show path to the right of it.
        tb_exclude (tuple): List of filenames to exclude from traceback.
        tb_range_start (int): The starting index from a traceback to include.
        tb_range_end (int): The final index from a traceback to include. If None
            the traceback will continue until the last record.
        RETURNS (TracebackPrinter): The traceback printer.
        """
        self.color_error = color_error
        self.color_tb = color_tb
        self.color_highlight = color_highlight
        self.indent = " " * indent
        if tb_base == ".":
            tb_base = "{}{}".format(os.getcwd(), os.path.sep)
        elif tb_base is not None:
            tb_base = "/{}/".format(tb_base)
        self.tb_base = tb_base
        self.tb_exclude = tuple(tb_exclude)
        self.tb_range_start = tb_range_start
        self.tb_range_end = tb_range_end
        self.supports_ansi = supports_ansi()

    def __call__(self, title: str, *texts, **settings) -> str:
        """Output custom formatted tracebacks and errors.

        title (str): The message title.
        *texts (str): The texts to print (one per line).
        RETURNS (str): The formatted traceback. Can be printed or raised
            by custom exception.
        """
        highlight = settings.get("highlight", False)
        tb = settings.get("tb", None)
        if self.supports_ansi:  # use first line as title
            title = color(title, fg=self.color_error, bold=True)
        info = "\n" + "\n".join([self.indent + text for text in texts]) if texts else ""
        tb = self._get_traceback(tb, highlight) if tb else ""
        msg = "\n\n{}{}{}{}\n".format(self.indent, title, info, tb)
        return msg

    def _get_traceback(self, tb, highlight):
        # Exclude certain file names from traceback
        tb = [record for record in tb if not record[0].endswith(self.tb_exclude)]
        tb_range = (
            tb[self.tb_range_start : self.tb_range_end]
            if self.tb_range_end is not None
            else tb[self.tb_range_start :]
        )
        tb_list = [
            self._format_traceback(path, line, fn, text, i, len(tb_range), highlight)
            for i, (path, line, fn, text) in enumerate(tb_range)
        ]
        tb_data = "\n".join(tb_list).strip()
        title = "Traceback:"
        if self.supports_ansi:
            title = color(title, fg=self.color_tb, bold=True)
        return "\n\n{indent}{title}\n{indent}{tb}".format(
            title=title, tb=tb_data, indent=self.indent
        )

    def _format_traceback(self, path, line, fn, text, i, count, highlight):
        template = "{base_indent}{indent} {fn} in {path}:{line}{text}"
        indent = (LINE_EDGE if i == count - 1 else LINE_FORK) + LINE_PATH * i
        if self.tb_base and self.tb_base in path:
            path = path.rsplit(self.tb_base, 1)[1]
        text = self._format_user_error(text, i, highlight) if i == count - 1 else ""
        if self.supports_ansi:
            fn = color(fn, bold=True)
            path = color(path, underline=True)
        return template.format(
            base_indent=self.indent,
            line=line,
            indent=indent,
            text=text,
            fn=fn,
            path=path,
        )

    def _format_user_error(self, text, i, highlight):
        spacing = "  " * i + " >>>"
        if self.supports_ansi:
            spacing = color(spacing, fg=self.color_error)
        if highlight and self.supports_ansi:
            formatted_highlight = color(highlight, fg=self.color_highlight)
            text = text.replace(highlight, formatted_highlight)
        return "\n{}  {} {}".format(self.indent, spacing, text)


================================================
FILE: wasabi/util.py
================================================
import difflib
import os
import sys
import textwrap
from typing import Any, Optional, Tuple, Union

STDOUT_ENCODING = sys.stdout.encoding if hasattr(sys.stdout, "encoding") else None
ENCODING = STDOUT_ENCODING or "ascii"
NO_UTF8 = ENCODING.lower() not in ("utf8", "utf-8")


# Environment variables
ENV_ANSI_DISABLED = "ANSI_COLORS_DISABLED"  # no colors


class MESSAGES(object):
    GOOD = "good"
    FAIL = "fail"
    WARN = "warn"
    INFO = "info"


COLORS = {
    MESSAGES.GOOD: 2,
    MESSAGES.FAIL: 1,
    MESSAGES.WARN: 3,
    MESSAGES.INFO: 4,
    "red": 1,
    "green": 2,
    "yellow": 3,
    "blue": 4,
    "pink": 5,
    "cyan": 6,
    "white": 7,
    "grey": 8,
    "black": 16,
}


ICONS = {
    MESSAGES.GOOD: "\u2714" if not NO_UTF8 else "[+]",
    MESSAGES.FAIL: "\u2718" if not NO_UTF8 else "[x]",
    MESSAGES.WARN: "\u26a0" if not NO_UTF8 else "[!]",
    MESSAGES.INFO: "\u2139" if not NO_UTF8 else "[i]",
}

INSERT_SYMBOL = "+"
DELETE_SYMBOL = "-"


def color(
    text: str,
    fg: Optional[Union[str, int]] = None,
    bg: Optional[Union[str, int]] = None,
    bold: bool = False,
    underline: bool = False,
) -> str:
    """Color text by applying ANSI escape sequence.

    text (str): The text to be formatted.
    fg (Optional[Union[str, int]]): Optional foreground color. String name or 0 - 256 (see COLORS).
    bg (Optional[Union[str, int]]): Optional background color. String name or 0 - 256 (see COLORS).
    bold (bool): Format text in bold.
    underline (bool): Underline text.
    RETURNS (str): The formatted text.
    """
    fg = COLORS.get(fg, fg)  # type: ignore
    bg = COLORS.get(bg, bg)  # type: ignore
    if not any([fg, bg, bold]):
        return text
    styles = []
    if bold:
        styles.append("1")
    if underline:
        styles.append("4")
    if fg:
        styles.append("38;5;{}".format(fg))
    if bg:
        styles.append("48;5;{}".format(bg))
    return "\x1b[{}m{}\x1b[0m".format(";".join(styles), text)


def wrap(text: Any, wrap_max: int = 80, indent: int = 4) -> str:
    """Wrap text at given width using textwrap module.

    text (Any): The text to wrap.
    wrap_max (int): Maximum line width, including indentation. Defaults to 80.
    indent (int): Number of spaces used for indentation. Defaults to 4.
    RETURNS (str): The wrapped text with line breaks.
    """
    indent_str = indent * " "
    wrap_width = wrap_max - len(indent_str)
    text = str(text)
    return textwrap.fill(
        text,
        width=wrap_width,
        initial_indent=indent_str,
        subsequent_indent=indent_str,
        break_long_words=False,
        break_on_hyphens=False,
    )


def format_repr(obj: Any, max_len: int = 50, ellipsis: str = "...") -> str:
    """Wrapper around `repr()` to print shortened and formatted string version.

    obj: The object to represent.
    max_len (int): Maximum string length. Longer strings will be cut in the
        middle so only the beginning and end is displayed, separated by ellipsis.
    ellipsis (str): Ellipsis character(s), e.g. "...".
    RETURNS (str): The formatted representation.
    """
    string = repr(obj)
    if len(string) >= max_len:
        half = int(max_len / 2)
        return "{} {} {}".format(string[:half], ellipsis, string[-half:])
    else:
        return string


def diff_strings(
    a: str,
    b: str,
    fg: Union[str, int] = "black",
    bg: Union[Tuple[str, str], Tuple[int, int]] = ("green", "red"),
    add_symbols: bool = False,
) -> str:
    """Compare two strings and return a colored diff with red/green background
    for deletion and insertions.

    a (str): The first string to diff.
    b (str): The second string to diff.
    fg (Union[str, int]): Foreground color. String name or 0 - 256 (see COLORS).
    bg (Union[Tuple[str, str], Tuple[int, int]]): Background colors as
        (insert, delete) tuple of string name or 0 - 256 (see COLORS).
    add_symbols (bool): Whether to add symbols before the diff lines. Uses '+'
        for inserts and '-' for deletions. Default is False.
    RETURNS (str): The formatted diff.
    """
    a_list = a.split("\n")
    b_list = b.split("\n")
    output = []
    matcher = difflib.SequenceMatcher(None, a_list, b_list)
    for opcode, a0, a1, b0, b1 in matcher.get_opcodes():
        if opcode == "equal":
            for item in a_list[a0:a1]:
                output.append(item)
        if opcode == "insert" or opcode == "replace":
            for item in b_list[b0:b1]:
                item = "{} {}".format(INSERT_SYMBOL, item) if add_symbols else item
                output.append(color(item, fg=fg, bg=bg[0]))
        if opcode == "delete" or opcode == "replace":
            for item in a_list[a0:a1]:
                item = "{} {}".format(DELETE_SYMBOL, item) if add_symbols else item
                output.append(color(item, fg=fg, bg=bg[1]))
    return "\n".join(output)


def get_raw_input(
    description: str, default: Optional[Union[str, bool]] = False, indent: int = 4
) -> str:
    """Get user input from the command line via raw_input / input.

    description (str): Text to display before prompt.
    default (Optional[Union[str, bool]]): Optional default value to display with prompt.
    indent (int): Indentation in spaces.
    RETURNS (str): User input.
    """
    additional = " (default: {})".format(default) if default else ""
    prompt = wrap("{}{}: ".format(description, additional), indent=indent)
    user_input = input(prompt)
    return user_input


def locale_escape(string: Any, errors: str = "replace") -> str:
    """Mangle non-supported characters, for savages with ASCII terminals.

    string (Any): The string to escape.
    errors (str): The str.encode errors setting. Defaults to `"replace"`.
    RETURNS (str): The escaped string.
    """
    string = str(string)
    string = string.encode(ENCODING, errors).decode("utf8")
    return string


def can_render(string: str) -> bool:
    """Check if terminal can render unicode characters, e.g. special loading
    icons. Can be used to display fallbacks for ASCII terminals.

    string (str): The string to render.
    RETURNS (bool): Whether the terminal can render the text.
    """
    try:
        string.encode(ENCODING)
        return True
    except UnicodeEncodeError:
        return False


def supports_ansi() -> bool:
    """Returns True if the running system's terminal supports ANSI escape
    sequences for color, formatting etc. and False otherwise.

    RETURNS (bool): Whether the terminal supports ANSI colors.
    """
    if os.getenv(ENV_ANSI_DISABLED):
        return False
    # We require colorama on Windows Python 3.7+, but we might be running on Unix, or we
    # might be running on Windows Python 3.6. In both cases, colorama might be missing,
    # *or* there might by accident happen to be an install of an old version that
    # doesn't have just_fix_windows_console. So we need to confirm not just that we can
    # import colorama, but that we can import just_fix_windows_console.
    try:
        from colorama import just_fix_windows_console
    except ImportError:
        if sys.platform == "win32" and "ANSICON" not in os.environ:
            return False
    else:
        just_fix_windows_console()
    return True
Download .txt
gitextract_vfdcdftg/

├── .flake8
├── .github/
│   └── workflows/
│       └── tests.yml
├── .gitignore
├── LICENSE
├── MANIFEST.in
├── README.md
├── requirements.txt
├── setup.cfg
├── setup.py
└── wasabi/
    ├── __init__.py
    ├── compat.py
    ├── markdown.py
    ├── printer.py
    ├── py.typed
    ├── tables.py
    ├── tests/
    │   ├── __init__.py
    │   ├── test-data/
    │   │   └── wasabi-test-notebook.ipynb
    │   ├── test_jupyter.py
    │   ├── test_markdown.py
    │   ├── test_printer.py
    │   ├── test_tables.py
    │   ├── test_traceback.py
    │   └── test_util.py
    ├── traceback_printer.py
    └── util.py
Download .txt
SYMBOL INDEX (116 symbols across 11 files)

FILE: wasabi/markdown.py
  class MarkdownRenderer (line 6) | class MarkdownRenderer:
    method __init__ (line 9) | def __init__(self, no_emoji: bool = False):
    method text (line 18) | def text(self) -> str:
    method add (line 22) | def add(self, content: str):
    method table (line 29) | def table(
    method title (line 57) | def title(self, level: int, text: str, emoji: Optional[str] = None) ->...
    method list (line 68) | def list(self, items: Iterable[str], numbered: bool = False) -> str:
    method link (line 83) | def link(self, text: str, url: str) -> str:
    method code_block (line 92) | def code_block(self, text: str, lang: str = "") -> str:
    method code (line 101) | def code(self, text: str) -> str:
    method bold (line 109) | def bold(self, text: str) -> str:
    method italic (line 117) | def italic(self, text: str):
    method _wrap (line 125) | def _wrap(self, text, marker):

FILE: wasabi/printer.py
  class Printer (line 19) | class Printer(object):
    method __init__ (line 20) | def __init__(
    method counts (line 69) | def counts(self) -> Counter:
    method good (line 75) | def good(
    method fail (line 97) | def fail(
    method fail (line 108) | def fail(
    method fail (line 118) | def fail(
    method warn (line 139) | def warn(
    method info (line 160) | def info(
    method text (line 181) | def text(
    method divider (line 240) | def divider(
    method table (line 277) | def table(self, data: Union[Collection, Dict], **kwargs):
    method row (line 292) | def row(self, data: Collection, **kwargs):
    method loading (line 304) | def loading(self, text: str = "Loading..."):
    method _spinner (line 325) | def _spinner(self, text: str = "Loading..."):
    method _get_msg (line 331) | def _get_msg(

FILE: wasabi/tables.py
  function table (line 14) | def table(
  function row (line 99) | def row(
  function _get_max_widths (line 151) | def _get_max_widths(data, header, footer, max_col):

FILE: wasabi/tests/test_jupyter.py
  function test_jupyter (line 12) | def test_jupyter():

FILE: wasabi/tests/test_markdown.py
  function test_markdown (line 6) | def test_markdown():
  function test_markdown_table_aligns (line 18) | def test_markdown_table_aligns():

FILE: wasabi/tests/test_printer.py
  function test_printer (line 13) | def test_printer():
  function test_printer_print (line 43) | def test_printer_print():
  function test_printer_print_timestamp (line 52) | def test_printer_print_timestamp():
  function test_printer_no_pretty (line 59) | def test_printer_no_pretty():
  function test_printer_custom (line 69) | def test_printer_custom():
  function test_color_as_int (line 90) | def test_color_as_int():
  function test_bg_color (line 100) | def test_bg_color():
  function test_bg_color_as_int (line 111) | def test_bg_color_as_int():
  function test_color_and_bc_color (line 122) | def test_color_and_bc_color():
  function test_printer_counts (line 133) | def test_printer_counts():
  function test_printer_spaced (line 147) | def test_printer_spaced():
  function test_printer_divider (line 154) | def test_printer_divider():
  function test_printer_loading (line 168) | def test_printer_loading(hide_animation):
  function test_printer_loading_raises_exception (line 184) | def test_printer_loading_raises_exception():
  function test_printer_loading_no_print (line 195) | def test_printer_loading_no_print():
  function test_printer_log_friendly (line 202) | def test_printer_log_friendly():
  function test_printer_log_friendly_prefix (line 211) | def test_printer_log_friendly_prefix():
  function test_printer_none_encoding (line 222) | def test_printer_none_encoding(monkeypatch):
  function test_printer_no_print_raise_on_exit (line 229) | def test_printer_no_print_raise_on_exit():

FILE: wasabi/tests/test_tables.py
  function data (line 12) | def data():
  function header (line 17) | def header():
  function footer (line 22) | def footer():
  function fg_colors (line 27) | def fg_colors():
  function bg_colors (line 32) | def bg_colors():
  function test_table_default (line 36) | def test_table_default(data):
  function test_table_header (line 44) | def test_table_header(data, header):
  function test_table_header_footer_divider (line 52) | def test_table_header_footer_divider(data, header, footer):
  function test_table_aligns (line 60) | def test_table_aligns(data):
  function test_table_aligns_single (line 68) | def test_table_aligns_single(data):
  function test_table_widths (line 76) | def test_table_widths():
  function test_row_single_widths (line 83) | def test_row_single_widths():
  function test_table_multiline (line 89) | def test_table_multiline(header):
  function test_row_fg_colors (line 101) | def test_row_fg_colors(fg_colors):
  function test_row_bg_colors (line 111) | def test_row_bg_colors(bg_colors):
  function test_row_fg_colors_and_bg_colors (line 121) | def test_row_fg_colors_and_bg_colors(fg_colors, bg_colors):
  function test_row_fg_colors_and_bg_colors_log_friendly (line 134) | def test_row_fg_colors_and_bg_colors_log_friendly(fg_colors, bg_colors):
  function test_row_fg_colors_and_bg_colors_log_friendly_prefix (line 144) | def test_row_fg_colors_and_bg_colors_log_friendly_prefix(fg_colors, bg_c...
  function test_row_fg_colors_and_bg_colors_supports_ansi_false (line 157) | def test_row_fg_colors_and_bg_colors_supports_ansi_false(fg_colors, bg_c...
  function test_colors_whole_table_with_automatic_widths (line 166) | def test_colors_whole_table_with_automatic_widths(
  function test_colors_whole_table_only_fg_colors (line 189) | def test_colors_whole_table_only_fg_colors(data, header, footer, fg_colo...
  function test_colors_whole_table_only_bg_colors (line 209) | def test_colors_whole_table_only_bg_colors(data, header, footer, bg_colo...
  function test_colors_whole_table_with_supplied_spacing (line 229) | def test_colors_whole_table_with_supplied_spacing(
  function test_colors_whole_table_with_supplied_widths (line 253) | def test_colors_whole_table_with_supplied_widths(
  function test_colors_whole_table_with_single_alignment (line 277) | def test_colors_whole_table_with_single_alignment(
  function test_colors_whole_table_with_multiple_alignment (line 301) | def test_colors_whole_table_with_multiple_alignment(
  function test_colors_whole_table_with_multiline (line 325) | def test_colors_whole_table_with_multiline(data, header, footer, fg_colo...
  function test_colors_whole_table_log_friendly (line 344) | def test_colors_whole_table_log_friendly(data, header, footer, fg_colors...
  function test_colors_whole_table_log_friendly_prefix (line 362) | def test_colors_whole_table_log_friendly_prefix(
  function test_colors_whole_table_supports_ansi_false (line 383) | def test_colors_whole_table_supports_ansi_false(
  function test_colors_whole_table_color_values (line 402) | def test_colors_whole_table_color_values(data, header, footer, fg_colors...

FILE: wasabi/tests/test_traceback.py
  function tb (line 9) | def tb():
  function test_traceback_printer (line 13) | def test_traceback_printer(tb):
  function test_traceback_printer_highlight (line 19) | def test_traceback_printer_highlight(tb):
  function test_traceback_printer_custom_colors (line 25) | def test_traceback_printer_custom_colors(tb):
  function test_traceback_printer_only_title (line 33) | def test_traceback_printer_only_title(tb):
  function test_traceback_dot_relative_path_tb_base (line 39) | def test_traceback_dot_relative_path_tb_base(tb):
  function test_traceback_tb_base_none (line 45) | def test_traceback_tb_base_none(tb):
  function test_traceback_printer_no_tb (line 51) | def test_traceback_printer_no_tb():
  function test_traceback_printer_custom_tb_range (line 57) | def test_traceback_printer_custom_tb_range():
  function test_traceback_printer_custom_tb_range_start (line 63) | def test_traceback_printer_custom_tb_range_start():

FILE: wasabi/tests/test_util.py
  function test_color (line 6) | def test_color():
  function test_wrap (line 16) | def test_wrap():
  function test_format_repr (line 27) | def test_format_repr():
  function test_locale_escape (line 56) | def test_locale_escape(text, non_ascii):
  function test_diff_strings (line 62) | def test_diff_strings():
  function test_diff_strings_with_symbols (line 69) | def test_diff_strings_with_symbols():

FILE: wasabi/traceback_printer.py
  class TracebackPrinter (line 11) | class TracebackPrinter(object):
    method __init__ (line 12) | def __init__(
    method __call__ (line 52) | def __call__(self, title: str, *texts, **settings) -> str:
    method _get_traceback (line 69) | def _get_traceback(self, tb, highlight):
    method _format_traceback (line 89) | def _format_traceback(self, path, line, fn, text, i, count, highlight):
    method _format_user_error (line 107) | def _format_user_error(self, text, i, highlight):

FILE: wasabi/util.py
  class MESSAGES (line 16) | class MESSAGES(object):
  function color (line 51) | def color(
  function wrap (line 83) | def wrap(text: Any, wrap_max: int = 80, indent: int = 4) -> str:
  function format_repr (line 104) | def format_repr(obj: Any, max_len: int = 50, ellipsis: str = "...") -> str:
  function diff_strings (line 121) | def diff_strings(
  function get_raw_input (line 159) | def get_raw_input(
  function locale_escape (line 175) | def locale_escape(string: Any, errors: str = "replace") -> str:
  function can_render (line 187) | def can_render(string: str) -> bool:
  function supports_ansi (line 201) | def supports_ansi() -> bool:
Condensed preview — 25 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (109K chars).
[
  {
    "path": ".flake8",
    "chars": 93,
    "preview": "[flake8]\nignore = E203, E266, E501, E731, W503\nmax-line-length = 80\nselect = B,C,E,F,W,T4,B9\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "chars": 2313,
    "preview": "name: tests\n\non:\n  push:\n    paths-ignore:\n      - \"*.md\"\n  pull_request:\n    types: [opened, synchronize, reopened, edi"
  },
  {
    "path": ".gitignore",
    "chars": 1338,
    "preview": ".vscode/\ntmp/\n\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distrib"
  },
  {
    "path": "LICENSE",
    "chars": 1079,
    "preview": "The MIT License (MIT)\n\nCopyright (C) 2018 Ines Montani\n\nPermission is hereby granted, free of charge, to any person obta"
  },
  {
    "path": "MANIFEST.in",
    "chars": 65,
    "preview": "include LICENSE\nrecursive-include wasabi/tests/test-data *.ipynb\n"
  },
  {
    "path": "README.md",
    "chars": 28185,
    "preview": "# wasabi: A lightweight console printing and formatting toolkit\n\nOver the years, I've written countless implementations "
  },
  {
    "path": "requirements.txt",
    "chars": 132,
    "preview": "typing_extensions>=3.7.4.1,<5.0.0; python_version < \"3.8\"\n# Development dependencies\npytest\nmypy\ntypes-colorama\nnbconver"
  },
  {
    "path": "setup.cfg",
    "chars": 622,
    "preview": "[metadata]\nversion = 1.1.3\ndescription = A lightweight console printing and formatting toolkit\nurl = https://github.com/"
  },
  {
    "path": "setup.py",
    "chars": 150,
    "preview": "#!/usr/bin/env python\n\nif __name__ == \"__main__\":\n    from setuptools import setup, find_packages\n\n    setup(name=\"wasab"
  },
  {
    "path": "wasabi/__init__.py",
    "chars": 546,
    "preview": "from .markdown import MarkdownRenderer  # noqa\nfrom .printer import Printer  # noqa\nfrom .tables import row, table  # no"
  },
  {
    "path": "wasabi/compat.py",
    "chars": 206,
    "preview": "import sys\n\n\n# Use typing_extensions for Python versions < 3.8\nif sys.version_info < (3, 8):\n    from typing_extensions "
  },
  {
    "path": "wasabi/markdown.py",
    "chars": 4229,
    "preview": "from typing import Iterable, List, Optional, Sequence\n\nfrom .compat import Literal\n\n\nclass MarkdownRenderer:\n    \"\"\"Simp"
  },
  {
    "path": "wasabi/printer.py",
    "chars": 12287,
    "preview": "import datetime\nimport itertools\nimport os\nimport sys\nimport time\nimport traceback\nfrom collections import Counter\nfrom "
  },
  {
    "path": "wasabi/py.typed",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "wasabi/tables.py",
    "chars": 6773,
    "preview": "import os\nfrom itertools import zip_longest\nfrom typing import Collection, Dict, Iterable, List, Optional, Sequence, Uni"
  },
  {
    "path": "wasabi/tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "wasabi/tests/test-data/wasabi-test-notebook.ipynb",
    "chars": 835,
    "preview": "{\n \"cells\": [\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"id\": \"eb5586f8\",\n   \"metadata\": {},\n   \"output"
  },
  {
    "path": "wasabi/tests/test_jupyter.py",
    "chars": 1122,
    "preview": "from pathlib import Path\nimport subprocess\nimport os\nimport sys\n\nimport wasabi\n\nTEST_DATA = Path(__file__).absolute().pa"
  },
  {
    "path": "wasabi/tests/test_markdown.py",
    "chars": 1173,
    "preview": "import pytest\n\nfrom wasabi.markdown import MarkdownRenderer\n\n\ndef test_markdown():\n    md = MarkdownRenderer()\n    md.ad"
  },
  {
    "path": "wasabi/tests/test_printer.py",
    "chars": 7354,
    "preview": "import os\nimport re\nimport time\n\nimport pytest\n\nfrom wasabi.printer import Printer\nfrom wasabi.util import MESSAGES, NO_"
  },
  {
    "path": "wasabi/tests/test_tables.py",
    "chars": 17497,
    "preview": "import os\n\nimport pytest\n\nfrom wasabi.tables import row, table\nfrom wasabi.util import supports_ansi\n\nSUPPORTS_ANSI = su"
  },
  {
    "path": "wasabi/tests/test_traceback.py",
    "chars": 1622,
    "preview": "import traceback\n\nimport pytest\n\nfrom wasabi.traceback_printer import TracebackPrinter\n\n\n@pytest.fixture\ndef tb():\n    r"
  },
  {
    "path": "wasabi/tests/test_util.py",
    "chars": 2450,
    "preview": "import pytest\n\nfrom wasabi.util import color, diff_strings, format_repr, locale_escape, wrap\n\n\ndef test_color():\n    ass"
  },
  {
    "path": "wasabi/traceback_printer.py",
    "chars": 4822,
    "preview": "import os\nfrom typing import Optional, Tuple, Union\n\nfrom .util import NO_UTF8, color, supports_ansi\n\nLINE_EDGE = \"└─\" i"
  },
  {
    "path": "wasabi/util.py",
    "chars": 7260,
    "preview": "import difflib\nimport os\nimport sys\nimport textwrap\nfrom typing import Any, Optional, Tuple, Union\n\nSTDOUT_ENCODING = sy"
  }
]

About this extraction

This page contains the full source code of the ines/wasabi GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 25 files (99.8 KB), approximately 27.7k tokens, and a symbol index with 116 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!