Merge branch 'master' into master

This commit is contained in:
Will McGugan 2020-12-11 15:16:31 +00:00 committed by GitHub
commit 02741b795d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 3047 additions and 282 deletions

View file

@ -9,7 +9,9 @@ jobs:
matrix: matrix:
os: [windows-latest, ubuntu-latest, macos-latest] os: [windows-latest, ubuntu-latest, macos-latest]
python-version: [3.6, 3.7, 3.8, 3.9] python-version: [3.6, 3.7, 3.8, 3.9]
defaults:
run:
shell: bash
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
@ -17,22 +19,22 @@ jobs:
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
architecture: x64 architecture: x64
- name: Install and configure Poetry
uses: snok/install-poetry@v1.1.1
with:
version: 1.1.4
virtualenvs-create: false
- name: Install dependencies - name: Install dependencies
run: | run: poetry install
python -m pip install --upgrade pip if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
pip install -r requirements-dev.txt
poetry install
- name: Format check with black - name: Format check with black
run: | run: make format-check
make format-check
- name: Typecheck with mypy - name: Typecheck with mypy
run: | run: make typecheck
make typecheck
- name: Test with pytest - name: Test with pytest
run: | run: |
pip install . pip install .
python -m pytest tests -v --cov=./rich --cov-report=xml:./coverage.xml --cov-report term-missing python -m pytest tests -v --cov=./rich --cov-report=xml:./coverage.xml --cov-report term-missing
- name: Upload code coverage - name: Upload code coverage
uses: codecov/codecov-action@v1.0.10 uses: codecov/codecov-action@v1.0.10
with: with:

1
.gitignore vendored
View file

@ -6,6 +6,7 @@ mypy_report
docs/build docs/build
docs/source/_build docs/source/_build
tools/*.txt tools/*.txt
playground/
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/

View file

@ -5,7 +5,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [9.3.0] - Unreleased ## [9.4.0] - Unreleased
### Added
- Added rich.live https://github.com/willmcgugan/rich/pull/382
- Added algin parameter to Rule and Console.rule
- Added rich.Status class and Console.status
- Added getitem to Text
- Added style parameter to Console.log
### Changed
- Table.add_row style argument now applies to entire line and not just cells
- Added end_section parameter to Table.add_row to force a line underneath row
## [9.3.0] - 2020-12-1
### Added ### Added
@ -13,6 +28,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added get_time parameter to Console - Added get_time parameter to Console
- Added rich.abc.RichRenderable - Added rich.abc.RichRenderable
- Added expand_all to rich.pretty.install() - Added expand_all to rich.pretty.install()
- Added locals_max_length, and locals_max_string to Traceback and logging.RichHandler
- Set defaults of max_length and max_string for Traceback to 10 and 80
- Added disable argument to Progress
### Changed ### Changed
@ -23,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed redirecting of stderr in Progress - Fixed redirecting of stderr in Progress
- Fixed broken expanded tuple of one https://github.com/willmcgugan/rich/issues/445 - Fixed broken expanded tuple of one https://github.com/willmcgugan/rich/issues/445
- Fixed traceback message with `from` exceptions - Fixed traceback message with `from` exceptions
- Fixed justify argument not working in console.log https://github.com/willmcgugan/rich/issues/460
## [9.2.0] - 2020-11-08 ## [9.2.0] - 2020-11-08

View file

@ -1,13 +1,24 @@
# Contributing to Rich # Contributing to Rich
This project welcomes contributions in the form of Pull Requests. For clear bug-fixes / typos etc. just submit a PR. For new features or if there is any doubt in how to fix a bug, you might want to open an issue prior to starting work, or email willmcgugan+rich@gmail.com to discuss it first. This project welcomes contributions in the form of Pull Requests.
For clear bug-fixes / typos etc. just submit a PR.
For new features or if there is any doubt in how to fix a bug, you might want
to open an issue prior to starting work, or email willmcgugan+rich@gmail.com
to discuss it first.
## Development Environment ## Development Environment
To start developing with Rich, first create a _virtual environment_ then run the following to install development requirements: Rich uses [poetry](https://python-poetry.org/docs/) for packaging and
dependency management. To start developing with Rich, install Poetry
using the [recommended method](https://python-poetry.org/docs/#installation) or run:
```
pip install poetry
```
Once Poetry is installed, install the dependencies with the following command:
``` ```
pip install -r requirements-dev.txt
poetry install poetry install
``` ```
@ -29,7 +40,8 @@ New code should ideally have tests and not break existing tests.
### Type Checking ### Type Checking
Rich uses type annotations throughout, and `mypy` to do the checking. Run the following to type check Rich: Rich uses type annotations throughout, and `mypy` to do the checking.
Run the following to type check Rich:
``` ```
make typecheck make typecheck

View file

@ -7,3 +7,4 @@ The following people have contributed to the development of Rich:
- [Oleksis Fraga](https://github.com/oleksis) - [Oleksis Fraga](https://github.com/oleksis)
- [Hedy Li](https://github.com/hedythedev) - [Hedy Li](https://github.com/hedythedev)
- [Will McGugan](https://github.com/willmcgugan) - [Will McGugan](https://github.com/willmcgugan)
- [Nathan Page](https://github.com/nathanrpage97)

View file

@ -205,6 +205,7 @@ Rich 可以将内容通过排列整齐的,具有相等或最佳的宽度的[
```python ```python
import os import os
import sys
from rich import print from rich import print
from rich.columns import Columns from rich.columns import Columns

View file

@ -219,6 +219,7 @@ Rich puede representar contenido en [columnas](https://rich.readthedocs.io/en/la
```python ```python
import os import os
import sys
from rich import print from rich import print
from rich.columns import Columns from rich.columns import Columns

View file

@ -31,6 +31,12 @@ Install with `pip` or your favorite PyPi package manager.
pip install rich pip install rich
``` ```
Run the following to test Rich output on your terminal:
```
python -m rich
```
## Rich print function ## Rich print function
To effortlessly add rich output to your application, you can import the [rich print](https://rich.readthedocs.io/en/latest/introduction.html#quick-start) method, which has the same signature as the builtin Python function. Try this: To effortlessly add rich output to your application, you can import the [rich print](https://rich.readthedocs.io/en/latest/introduction.html#quick-start) method, which has the same signature as the builtin Python function. Try this:
@ -224,12 +230,35 @@ The columns may be configured to show any details you want. Built-in columns inc
To try this out yourself, see [examples/downloader.py](https://github.com/willmcgugan/rich/blob/master/examples/downloader.py) which can download multiple URLs simultaneously while displaying progress. To try this out yourself, see [examples/downloader.py](https://github.com/willmcgugan/rich/blob/master/examples/downloader.py) which can download multiple URLs simultaneously while displaying progress.
## Status
For situations where it is hard to calculate progress, you can use the status method which will display a 'spinner' animation with a status message that won't prevent you from writing to the terminal. Here's an example:
```python
from time import sleep
from rich.console import Console
console = Console()
tasks = [f"task {n}" for n in range(1, 11)]
with console.status("[bold green]Working on tasks...") as status:
while tasks:
task = tasks.pop(0)
sleep(1)
console.log(f"{task} complete")
```
This generates the following output in the terminal.
![status](https://github.com/willmcgugan/rich/raw/master/imgs/status.gif)
## Columns ## Columns
Rich can render content in neat [columns](https://rich.readthedocs.io/en/latest/columns.html) with equal or optimal width. Here's a very basic clone of the (MacOS / Linux) `ls` command which displays a directory listing in columns: Rich can render content in neat [columns](https://rich.readthedocs.io/en/latest/columns.html) with equal or optimal width. Here's a very basic clone of the (MacOS / Linux) `ls` command which displays a directory listing in columns:
```python ```python
import os import os
import sys
from rich import print from rich import print
from rich.columns import Columns from rich.columns import Columns

View file

@ -69,6 +69,38 @@ The :meth:`~rich.console.Console.log` methods offers the same capabilities as pr
To help with debugging, the log() method has a ``log_locals`` parameter. If you set this to ``True``, Rich will display a table of local variables where the method was called. To help with debugging, the log() method has a ``log_locals`` parameter. If you set this to ``True``, Rich will display a table of local variables where the method was called.
Rules
-----
The :meth:`~rich.console.Console.rule` method will draw a horizontal line with an optional title, which is a good way of dividing your terminal output in to sections.
>>> console.rule("[bold red]Chapter 2")
.. raw:: html
<pre style="font-family:Menlo,\'DejaVu Sans Mono\',consolas,\'Courier New\',monospace"><span style="color: #00ff00">─────────────────────────────── </span><span style="color: #800000; font-weight: bold">Chapter 2</span><span style="color: #00ff00"> ───────────────────────────────</span></pre>
The rule method also accepts a ``style`` parameter to set the style of the line, and an ``align`` parameter to align the title ("left", "center", or "right").
Status
------
Rich can display a status message with a 'spinner' animation that won't interfere with regular console output. Run the following command for a demo of this feature::
python -m rich.status
To display a status message call :meth:`~rich.console.Console.status` with the status message (which may be a string, Text, or other renderable). The result is a context manager which starts and stop the status display around a block of code. Here's an example::
with console.status("Working...")
do_work()
You can change the spinner animation via the ``spinner`` parameter. Run the following command to see the available choices::
python -m rich.spinner
Low level output Low level output
---------------- ----------------

View file

@ -25,6 +25,7 @@ Welcome to Rich's documentation!
panel.rst panel.rst
group.rst group.rst
columns.rst columns.rst
live.rst
progress.rst progress.rst
markdown.rst markdown.rst
syntax.rst syntax.rst

172
docs/source/live.rst Normal file
View file

@ -0,0 +1,172 @@
.. _live:
Live Display
============
Rich can display continiuously updated information for any renderable.
To see some live display examples, try this from the command line::
python -m rich.live
.. note::
If you see ellipsis "...", this indicates that the terminal is not tall enough to show the full table.
Basic Usage
-----------
The basic usage can be split into two use cases.
1. Same Renderable
~~~~~~~~~~~~~~~~~~
When keeping the same renderable, you simply pass the :class:`~rich.console.RenderableType` you would like to see updating and provide
a ``refresh_per_second`` parameter. The Live :class:`~rich.live.Live` will automatically update the console at the provided refresh rate.
**Example**::
import time
from rich.live import Live
from rich.table import Table
table = Table()
table.add_column("Row ID")
table.add_column("Description")
table.add_column("Level")
with Live(table, refresh_per_second=4): # update 4 times a second to feel fluid
for row in range(12):
time.sleep(0.4) # arbitrary delay
# update the renderable internally
table.add_row(f"{row}", f"description {row}", "[red]ERROR")
2. New Renderable
~~~~~~~~~~~~~~~~~
You can also provide constant new renderable to :class:`~rich.live.Live` using the :meth:`~rich.live.Live.update` function. This allows you to
completely change what is rendered live.
**Example**::
import random
import time
from rich.live import Live
from rich.table import Table
def generate_table() -> Table:
table = Table()
table.add_column("ID")
table.add_column("Value")
table.add_column("Status")
for row in range(random.randint(2, 6)):
value = random.random() * 100
table.add_row(
f"{row}", f"{value:3.2f}", "[red]ERROR" if value < 50 else "[green]SUCCESS"
)
return table
with Live(refresh_per_second=4) as live:
for _ in range(40):
time.sleep(0.4)
live.update(generate_table())
Advanced Usage
--------------
Transient Display
~~~~~~~~~~~~~~~~~
Normally when you exit live context manager (or call :meth:`~rich.live.Live.stop`) the last refreshed item remains in the terminal with the cursor on the following line.
You can also make the live display disappear on exit by setting ``transient=True`` on the Live constructor. Here's an example::
with Live(transient=True) as live:
...
Auto refresh
~~~~~~~~~~~~
By default, the live display will refresh 4 times a second. You can set the refresh rate with the ``refresh_per_second`` argument on the :class:`~rich.live.Live` constructor.
You should set this to something lower than 4 if you know your updates will not be that frequent or higher for a smoother feeling.
You might want to disable auto-refresh entirely if your updates are not very frequent, which you can do by setting ``auto_refresh=False`` on the constructor.
If you disable auto-refresh you will need to call :meth:`~rich.live.Live.refresh` manually or :meth:`~rich.live.Live.update` with ``refresh=True``.
Vertical Overflow
~~~~~~~~~~~~~~~~~
By default, the live display will display ellipsis if the renderable is too large for the terminal. You can adjust this by setting the
``vertical_overflow`` argument on the :class:`~rich.live.Live` constructor.
- crop: Show renderable up to the terminal height. The rest is hidden.
- ellipsis: Similar to crop except last line of the terminal is replaced with "...". This is the default behavior.
- visible: Will allow the whole renderable to be shown. Note that the display cannot be properly cleared in this mode.
.. note::
Once the live display stops on a non-transient renderable, the last frame will render as **visible** since it doesn't have to be cleared.
Complex Renders
~~~~~~~~~~~~~~~
Refer to the :ref:`Render Groups` about combining multiple :class:`RenderableType` together so that it may be passed into the :class:`~rich.live.Live` constructor
or :meth:`~rich.live.Live.update` method.
For more powerful structuring it is also possible to use nested tables.
Print / log
~~~~~~~~~~~
The Live class will create an internal Console object which you can access via ``live.console``. If you print or log to this console, the output will be displayed *above* the live display. Here's an example::
import time
from rich.live import Live
from rich.table import Table
table = Table()
table.add_column("Row ID")
table.add_column("Description")
table.add_column("Level")
with Live(table, refresh_per_second=4): # update 4 times a second to feel fluid
for row in range(12):
live.console.print("Working on row #{row}")
time.sleep(0.4)
table.add_row(f"{row}", f"description {row}", "[red]ERROR")
If you have another Console object you want to use, pass it in to the :class:`~rich.live.Live` constructor. Here's an example::
from my_project import my_console
with Live(console=my_console) as live:
my_console.print("[bold blue]Starting work!")
...
.. note::
If you are passing in a file console, the live display only show the last item once the live context is left.
Redirecting stdout / stderr
~~~~~~~~~~~~~~~~~~~~~~~~~~~
To avoid breaking the live display visuals, Rich will redirect ``stdout`` and ``stderr`` so that you can use the builtin ``print`` statement.
This feature is enabled by default, but you can disable by setting ``redirect_stdout`` or ``redirect_stderr`` to ``False``.
Examples
--------
See `table_movie.py <https://github.com/willmcgugan/rich/blob/master/examples/table_movie.py>`_ and
`top_lite_simulator.py <https://github.com/willmcgugan/rich/blob/master/examples/top_lite_simulator.py>`_
for deeper examples of live displaying.

View file

@ -114,6 +114,8 @@ The following column objects are available:
- :class:`~rich.progress.TotalFileSizeColumn` Displays total file size (assumes the steps are bytes). - :class:`~rich.progress.TotalFileSizeColumn` Displays total file size (assumes the steps are bytes).
- :class:`~rich.progress.DownloadColumn` Displays download progress (assumes the steps are bytes). - :class:`~rich.progress.DownloadColumn` Displays download progress (assumes the steps are bytes).
- :class:`~rich.progress.TransferSpeedColumn` Displays transfer speed (assumes the steps are bytes. - :class:`~rich.progress.TransferSpeedColumn` Displays transfer speed (assumes the steps are bytes.
- :class:`~rich.progress.SpinnerColumn` Displays a "spinner" animation.
- :class:`~rich.progress.RenderableColumn` Displays an arbitrary Rich renderable in the column.
To implement your own columns, extend the :class:`~rich.progress.Progress` and use it as you would the other columns. To implement your own columns, extend the :class:`~rich.progress.Progress` and use it as you would the other columns.

View file

@ -12,6 +12,7 @@ Reference
reference/emoji.rst reference/emoji.rst
reference/highlighter.rst reference/highlighter.rst
reference/init.rst reference/init.rst
reference/live.rst
reference/logging.rst reference/logging.rst
reference/markdown.rst reference/markdown.rst
reference/markup.rst reference/markup.rst
@ -25,6 +26,8 @@ Reference
reference/protocol.rst reference/protocol.rst
reference/rule.rst reference/rule.rst
reference/segment.rst reference/segment.rst
reference/spinner.rst
reference/status.rst
reference/style.rst reference/style.rst
reference/styled.rst reference/styled.rst
reference/syntax.rst reference/syntax.rst

View file

@ -0,0 +1,5 @@
rich.live
=========
.. automodule:: rich.live
:members:

View file

@ -0,0 +1,5 @@
rich.spinner
============
.. automodule:: rich.spinner
:members:

View file

@ -0,0 +1,5 @@
rich.status
============
.. automodule:: rich.status
:members:

13
examples/status.py Normal file
View file

@ -0,0 +1,13 @@
from time import sleep
from rich.console import Console
console = Console()
console.print()
tasks = [f"task {n}" for n in range(1, 11)]
with console.status("[bold green]Working on tasks...") as status:
while tasks:
task = tasks.pop(0)
sleep(1)
console.log(f"{task} complete")

View file

@ -1,12 +1,16 @@
"""Same as the table_movie.py but uses Live to update"""
from contextlib import contextmanager from contextlib import contextmanager
import time import time
from rich.console import Console from rich.console import Console
from rich.columns import Columns
from rich.table import Table from rich.table import Table
from rich.measure import Measurement from rich.measure import Measurement
from rich import box from rich import box
from rich.text import Text from rich.text import Text
from rich.live import Live
TABLE_DATA = [ TABLE_DATA = [
[ [
"May 25, 1977", "May 25, 1977",
@ -60,136 +64,110 @@ BEAT_TIME = 0.04
@contextmanager @contextmanager
def beat(length: int = 1) -> None: def beat(length: int = 1) -> None:
with console: with console:
console.clear()
yield yield
time.sleep(length * BEAT_TIME) time.sleep(length * BEAT_TIME)
table = Table(show_footer=False) table = Table(show_footer=False)
table_centered = Columns((table,), align="center", expand=True)
console.clear() console.clear()
console.show_cursor(False)
try:
with Live(
table_centered, console=console, refresh_per_second=10, vertical_overflow="ellipsis"
):
with beat(10):
table.add_column("Release Date", no_wrap=True) table.add_column("Release Date", no_wrap=True)
with beat(10):
console.print(table, justify="center")
with beat(10):
table.add_column("Title", Text.from_markup("[b]Total", justify="right")) table.add_column("Title", Text.from_markup("[b]Total", justify="right"))
with beat(10):
console.print(table, justify="center")
with beat(10):
table.add_column("Budget", "[u]$412,000,000", no_wrap=True) table.add_column("Budget", "[u]$412,000,000", no_wrap=True)
with beat(10):
console.print(table, justify="center")
with beat(10):
table.add_column("Opening Weekend", "[u]$577,703,455", no_wrap=True) table.add_column("Opening Weekend", "[u]$577,703,455", no_wrap=True)
with beat(10):
console.print(table, justify="center")
with beat(10):
table.add_column("Box Office", "[u]$4,331,212,357", no_wrap=True) table.add_column("Box Office", "[u]$4,331,212,357", no_wrap=True)
with beat(10):
console.print(table, justify="center")
with beat(10):
table.title = "Star Wars Box Office" table.title = "Star Wars Box Office"
with beat(10):
console.print(table, justify="center")
with beat(10):
table.title = ( table.title = (
"[not italic]:popcorn:[/] Star Wars Box Office [not italic]:popcorn:[/]" "[not italic]:popcorn:[/] Star Wars Box Office [not italic]:popcorn:[/]"
) )
with beat(10):
console.print(table, justify="center")
with beat(10):
table.caption = "Made with Rich" table.caption = "Made with Rich"
with beat(10):
console.print(table, justify="center")
with beat(10):
table.caption = "Made with [b]Rich[/b]" table.caption = "Made with [b]Rich[/b]"
with beat(10):
console.print(table, justify="center")
table.caption = "Made with [b magenta not dim]Rich[/]"
with beat(10): with beat(10):
console.print(table, justify="center") table.caption = "Made with [b magenta not dim]Rich[/]"
for row in TABLE_DATA: for row in TABLE_DATA:
with beat(10):
table.add_row(*row) table.add_row(*row)
with beat(10):
console.print(table, justify="center")
table.show_footer = True
with beat(10): with beat(10):
console.print(table, justify="center") table.show_footer = True
table_width = Measurement.get(console, table, console.width).maximum table_width = Measurement.get(console, table, console.width).maximum
with beat(10):
table.columns[2].justify = "right" table.columns[2].justify = "right"
with beat(10):
console.print(table, justify="center")
with beat(10):
table.columns[3].justify = "right" table.columns[3].justify = "right"
with beat(10):
console.print(table, justify="center")
with beat(10):
table.columns[4].justify = "right" table.columns[4].justify = "right"
with beat(10):
console.print(table, justify="center")
with beat(10):
table.columns[2].header_style = "bold red" table.columns[2].header_style = "bold red"
with beat(10):
console.print(table, justify="center")
with beat(10):
table.columns[3].header_style = "bold green" table.columns[3].header_style = "bold green"
with beat(10):
console.print(table, justify="center")
with beat(10):
table.columns[4].header_style = "bold blue" table.columns[4].header_style = "bold blue"
with beat(10):
console.print(table, justify="center")
with beat(10):
table.columns[2].style = "red" table.columns[2].style = "red"
with beat(10):
console.print(table, justify="center")
with beat(10):
table.columns[3].style = "green" table.columns[3].style = "green"
with beat(10):
console.print(table, justify="center")
with beat(10):
table.columns[4].style = "blue" table.columns[4].style = "blue"
with beat(10):
console.print(table, justify="center")
with beat(10):
table.columns[0].style = "cyan" table.columns[0].style = "cyan"
table.columns[0].header_style = "bold cyan" table.columns[0].header_style = "bold cyan"
with beat(10):
console.print(table, justify="center")
with beat(10):
table.columns[1].style = "magenta" table.columns[1].style = "magenta"
table.columns[1].header_style = "bold magenta" table.columns[1].header_style = "bold magenta"
with beat(10):
console.print(table, justify="center")
with beat(10):
table.columns[2].footer_style = "bright_red" table.columns[2].footer_style = "bright_red"
with beat(10):
console.print(table, justify="center")
with beat(10):
table.columns[3].footer_style = "bright_green" table.columns[3].footer_style = "bright_green"
with beat(10):
console.print(table, justify="center")
with beat(10):
table.columns[4].footer_style = "bright_blue" table.columns[4].footer_style = "bright_blue"
with beat(10):
console.print(table, justify="center")
with beat(10):
table.row_styles = ["none", "dim"] table.row_styles = ["none", "dim"]
with beat(10):
console.print(table, justify="center")
table.border_style = "bright_yellow"
with beat(10): with beat(10):
console.print(table, justify="center") table.border_style = "bright_yellow"
for box in [ for box in [
box.SQUARE, box.SQUARE,
@ -197,39 +175,29 @@ try:
box.SIMPLE, box.SIMPLE,
box.SIMPLE_HEAD, box.SIMPLE_HEAD,
]: ]:
with beat(10):
table.box = box table.box = box
with beat(10):
console.print(table, justify="center")
table.pad_edge = False
with beat(10): with beat(10):
console.print(table, justify="center") table.pad_edge = False
original_width = Measurement.get(console, table).maximum original_width = Measurement.get(console, table).maximum
for width in range(original_width, console.width, 2): for width in range(original_width, console.width, 2):
table.width = width
with beat(2): with beat(2):
console.print(table, justify="center") table.width = width
for width in range(console.width, original_width, -2): for width in range(console.width, original_width, -2):
table.width = width
with beat(2): with beat(2):
console.print(table, justify="center") table.width = width
for width in range(original_width, 90, -2): for width in range(original_width, 90, -2):
table.width = width
with beat(2): with beat(2):
console.print(table, justify="center") table.width = width
for width in range(90, original_width + 1, 2): for width in range(90, original_width + 1, 2):
with beat(2):
table.width = width table.width = width
with beat(2):
console.print(table, justify="center")
with beat(2):
table.width = None table.width = None
with beat(2):
console.print(table, justify="center")
finally:
console.show_cursor(True)

View file

@ -0,0 +1,81 @@
"""Lite simulation of the top linux command."""
import datetime
import random
import time
from dataclasses import dataclass
from rich import box
from rich.console import Console
from rich.live import Live
from rich.table import Table
from typing_extensions import Literal
@dataclass
class Process:
pid: int
command: str
cpu_percent: float
memory: int
start_time: datetime.datetime
thread_count: int
state: Literal["running", "sleeping"]
@property
def memory_str(self) -> str:
if self.memory > 1e6:
return f"{int(self.memory/1e6)}M"
if self.memory > 1e3:
return f"{int(self.memory/1e3)}K"
return str(self.memory)
@property
def time_str(self) -> str:
return str(datetime.datetime.now() - self.start_time)
def generate_process(pid: int) -> Process:
return Process(
pid=pid,
command=f"Process {pid}",
cpu_percent=random.random() * 20,
memory=random.randint(10, 200) ** 3,
start_time=datetime.datetime.now()
- datetime.timedelta(seconds=random.randint(0, 500) ** 2),
thread_count=random.randint(1, 32),
state="running" if random.randint(0, 10) < 8 else "sleeping",
)
def create_process_table(height: int) -> Table:
processes = sorted(
[generate_process(pid) for pid in range(height)],
key=lambda p: p.cpu_percent,
reverse=True,
)
table = Table(
"PID", "Command", "CPU %", "Memory", "Time", "Thread #", "State", box=box.SIMPLE
)
for process in processes:
table.add_row(
str(process.pid),
process.command,
f"{process.cpu_percent:.1f}",
process.memory_str,
process.time_str,
str(process.thread_count),
process.state,
)
return table
console = Console()
with Live(console=console, transient=True, auto_refresh=False) as live:
while True:
live.update(create_process_table(console.size.height - 4), refresh=True)
time.sleep(1)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 729 KiB

After

Width:  |  Height:  |  Size: 718 KiB

Before After
Before After

BIN
imgs/status.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 KiB

376
poetry.lock generated
View file

@ -1,3 +1,11 @@
[[package]]
name = "appdirs"
version = "1.4.4"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "appnope" name = "appnope"
version = "0.1.0" version = "0.1.0"
@ -6,12 +14,20 @@ category = "main"
optional = true optional = true
python-versions = "*" python-versions = "*"
[[package]]
name = "atomicwrites"
version = "1.4.0"
description = "Atomic file writes."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]] [[package]]
name = "attrs" name = "attrs"
version = "19.3.0" version = "19.3.0"
description = "Classes Without Boilerplate" description = "Classes Without Boilerplate"
category = "main" category = "main"
optional = true optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.extras] [package.extras]
@ -28,6 +44,29 @@ category = "main"
optional = true optional = true
python-versions = "*" python-versions = "*"
[[package]]
name = "black"
version = "20.8b1"
description = "The uncompromising code formatter."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
appdirs = "*"
click = ">=7.1.2"
dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""}
mypy-extensions = ">=0.4.3"
pathspec = ">=0.6,<1"
regex = ">=2020.1.8"
toml = ">=0.10.1"
typed-ast = ">=1.4.0"
typing-extensions = ">=3.7.4"
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
[[package]] [[package]]
name = "bleach" name = "bleach"
version = "3.1.5" version = "3.1.5"
@ -41,6 +80,14 @@ packaging = "*"
six = ">=1.9.0" six = ">=1.9.0"
webencodings = "*" webencodings = "*"
[[package]]
name = "click"
version = "7.1.2"
description = "Composable command line interface toolkit"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.4.4" version = "0.4.4"
@ -60,6 +107,17 @@ python-versions = "*"
[package.extras] [package.extras]
test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"]
[[package]]
name = "coverage"
version = "5.3"
description = "Code coverage measurement for Python"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
[package.extras]
toml = ["toml"]
[[package]] [[package]]
name = "dataclasses" name = "dataclasses"
version = "0.8" version = "0.8"
@ -97,7 +155,7 @@ name = "importlib-metadata"
version = "1.7.0" version = "1.7.0"
description = "Read metadata from Python packages" description = "Read metadata from Python packages"
category = "main" category = "main"
optional = true optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[package.dependencies] [package.dependencies]
@ -107,6 +165,14 @@ zipp = ">=0.5"
docs = ["sphinx", "rst.linker"] docs = ["sphinx", "rst.linker"]
testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] testing = ["packaging", "pep517", "importlib-resources (>=1.3)"]
[[package]]
name = "iniconfig"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "ipykernel" name = "ipykernel"
version = "5.3.2" version = "5.3.2"
@ -275,6 +341,30 @@ category = "main"
optional = true optional = true
python-versions = "*" python-versions = "*"
[[package]]
name = "mypy"
version = "0.790"
description = "Optional static typing for Python"
category = "dev"
optional = false
python-versions = ">=3.5"
[package.dependencies]
mypy-extensions = ">=0.4.3,<0.5.0"
typed-ast = ">=1.4.0,<1.5.0"
typing-extensions = ">=3.7.4"
[package.extras]
dmypy = ["psutil (>=4.0)"]
[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "nbconvert" name = "nbconvert"
version = "5.6.1" version = "5.6.1"
@ -351,7 +441,7 @@ name = "packaging"
version = "20.4" version = "20.4"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
category = "main" category = "main"
optional = true optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies] [package.dependencies]
@ -377,6 +467,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.extras] [package.extras]
testing = ["docopt", "pytest (>=3.0.7)"] testing = ["docopt", "pytest (>=3.0.7)"]
[[package]]
name = "pathspec"
version = "0.8.1"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]] [[package]]
name = "pexpect" name = "pexpect"
version = "4.8.0" version = "4.8.0"
@ -396,6 +494,20 @@ category = "main"
optional = true optional = true
python-versions = "*" python-versions = "*"
[[package]]
name = "pluggy"
version = "0.13.1"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
[package.extras]
dev = ["pre-commit", "tox"]
[[package]] [[package]]
name = "prometheus-client" name = "prometheus-client"
version = "0.8.0" version = "0.8.0"
@ -426,9 +538,17 @@ category = "main"
optional = true optional = true
python-versions = "*" python-versions = "*"
[[package]]
name = "py"
version = "1.9.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]] [[package]]
name = "pygments" name = "pygments"
version = "2.7.2" version = "2.7.3"
description = "Pygments is a syntax highlighting package written in Python." description = "Pygments is a syntax highlighting package written in Python."
category = "main" category = "main"
optional = false optional = false
@ -439,7 +559,7 @@ name = "pyparsing"
version = "2.4.7" version = "2.4.7"
description = "Python parsing module" description = "Python parsing module"
category = "main" category = "main"
optional = true optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]] [[package]]
@ -453,6 +573,44 @@ python-versions = "*"
[package.dependencies] [package.dependencies]
six = "*" six = "*"
[[package]]
name = "pytest"
version = "6.1.2"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.5"
[package.dependencies]
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
attrs = ">=17.4.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<1.0"
py = ">=1.8.2"
toml = "*"
[package.extras]
checkqa_mypy = ["mypy (==0.780)"]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
[[package]]
name = "pytest-cov"
version = "2.10.1"
description = "Pytest plugin for measuring coverage."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.dependencies]
coverage = ">=4.4"
pytest = ">=4.6"
[package.extras]
testing = ["fields", "hunter", "process-tests (==2.0.2)", "six", "pytest-xdist", "virtualenv"]
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.8.1" version = "2.8.1"
@ -488,6 +646,14 @@ category = "main"
optional = true optional = true
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*" python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*"
[[package]]
name = "regex"
version = "2020.11.13"
description = "Alternative regular expression module, to replace re."
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "send2trash" name = "send2trash"
version = "1.5.0" version = "1.5.0"
@ -501,7 +667,7 @@ name = "six"
version = "1.15.0" version = "1.15.0"
description = "Python 2 and 3 compatibility utilities" description = "Python 2 and 3 compatibility utilities"
category = "main" category = "main"
optional = true optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]] [[package]]
@ -528,6 +694,14 @@ python-versions = "*"
[package.extras] [package.extras]
test = ["pathlib2"] test = ["pathlib2"]
[[package]]
name = "toml"
version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]] [[package]]
name = "tornado" name = "tornado"
version = "6.0.4" version = "6.0.4"
@ -552,6 +726,14 @@ six = "*"
[package.extras] [package.extras]
test = ["pytest", "mock"] test = ["pytest", "mock"]
[[package]]
name = "typed-ast"
version = "1.4.1"
description = "a fork of Python 2 and 3 ast modules with type comment support"
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "3.7.4.3" version = "3.7.4.3"
@ -592,7 +774,7 @@ name = "zipp"
version = "3.1.0" version = "3.1.0"
description = "Backport of pathlib-compatible object wrapper for zip files" description = "Backport of pathlib-compatible object wrapper for zip files"
category = "main" category = "main"
optional = true optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
[package.extras] [package.extras]
@ -605,13 +787,21 @@ jupyter = ["ipywidgets"]
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.6" python-versions = "^3.6"
content-hash = "2d2f8de1abd0be719c813e5c47d7b48620c01de464894ab995485e85fb804454" content-hash = "2aac9442e8c6265a9815532d55ada0835532f15a507a6c96445c5f8937f7d3f7"
[metadata.files] [metadata.files]
appdirs = [
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
]
appnope = [ appnope = [
{file = "appnope-0.1.0-py2.py3-none-any.whl", hash = "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0"}, {file = "appnope-0.1.0-py2.py3-none-any.whl", hash = "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0"},
{file = "appnope-0.1.0.tar.gz", hash = "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"}, {file = "appnope-0.1.0.tar.gz", hash = "sha256:8b995ffe925347a2138d7ac0fe77155e4311a0ea6d6da4f5128fe4b3cbe5ed71"},
] ]
atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
]
attrs = [ attrs = [
{file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"},
{file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"},
@ -620,10 +810,17 @@ backcall = [
{file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
{file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
] ]
black = [
{file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
]
bleach = [ bleach = [
{file = "bleach-3.1.5-py2.py3-none-any.whl", hash = "sha256:2bce3d8fab545a6528c8fa5d9f9ae8ebc85a56da365c7f85180bfe96a35ef22f"}, {file = "bleach-3.1.5-py2.py3-none-any.whl", hash = "sha256:2bce3d8fab545a6528c8fa5d9f9ae8ebc85a56da365c7f85180bfe96a35ef22f"},
{file = "bleach-3.1.5.tar.gz", hash = "sha256:3c4c520fdb9db59ef139915a5db79f8b51bc2a7257ea0389f30c846883430a4b"}, {file = "bleach-3.1.5.tar.gz", hash = "sha256:3c4c520fdb9db59ef139915a5db79f8b51bc2a7257ea0389f30c846883430a4b"},
] ]
click = [
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
]
colorama = [ colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
@ -632,6 +829,42 @@ commonmark = [
{file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
{file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
] ]
coverage = [
{file = "coverage-5.3-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270"},
{file = "coverage-5.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4"},
{file = "coverage-5.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9"},
{file = "coverage-5.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729"},
{file = "coverage-5.3-cp27-cp27m-win32.whl", hash = "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d"},
{file = "coverage-5.3-cp27-cp27m-win_amd64.whl", hash = "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418"},
{file = "coverage-5.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9"},
{file = "coverage-5.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5"},
{file = "coverage-5.3-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822"},
{file = "coverage-5.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097"},
{file = "coverage-5.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9"},
{file = "coverage-5.3-cp35-cp35m-win32.whl", hash = "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636"},
{file = "coverage-5.3-cp35-cp35m-win_amd64.whl", hash = "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f"},
{file = "coverage-5.3-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237"},
{file = "coverage-5.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54"},
{file = "coverage-5.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7"},
{file = "coverage-5.3-cp36-cp36m-win32.whl", hash = "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a"},
{file = "coverage-5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d"},
{file = "coverage-5.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8"},
{file = "coverage-5.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f"},
{file = "coverage-5.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c"},
{file = "coverage-5.3-cp37-cp37m-win32.whl", hash = "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751"},
{file = "coverage-5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709"},
{file = "coverage-5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516"},
{file = "coverage-5.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f"},
{file = "coverage-5.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259"},
{file = "coverage-5.3-cp38-cp38-win32.whl", hash = "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82"},
{file = "coverage-5.3-cp38-cp38-win_amd64.whl", hash = "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221"},
{file = "coverage-5.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978"},
{file = "coverage-5.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21"},
{file = "coverage-5.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24"},
{file = "coverage-5.3-cp39-cp39-win32.whl", hash = "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7"},
{file = "coverage-5.3-cp39-cp39-win_amd64.whl", hash = "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7"},
{file = "coverage-5.3.tar.gz", hash = "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0"},
]
dataclasses = [ dataclasses = [
{file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"},
{file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"},
@ -652,6 +885,10 @@ importlib-metadata = [
{file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"},
{file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"},
] ]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
ipykernel = [ ipykernel = [
{file = "ipykernel-5.3.2-py3-none-any.whl", hash = "sha256:0a5f1fc6f63241b9710b5960d314ffe44d8a18bf6674e3f28d2542b192fa318c"}, {file = "ipykernel-5.3.2-py3-none-any.whl", hash = "sha256:0a5f1fc6f63241b9710b5960d314ffe44d8a18bf6674e3f28d2542b192fa318c"},
{file = "ipykernel-5.3.2.tar.gz", hash = "sha256:89dc4bd19c7781f6d7eef0e666c59ce57beac56bb39b511544a71397b7b31cbb"}, {file = "ipykernel-5.3.2.tar.gz", hash = "sha256:89dc4bd19c7781f6d7eef0e666c59ce57beac56bb39b511544a71397b7b31cbb"},
@ -727,6 +964,26 @@ mistune = [
{file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"},
{file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"}, {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"},
] ]
mypy = [
{file = "mypy-0.790-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:bd03b3cf666bff8d710d633d1c56ab7facbdc204d567715cb3b9f85c6e94f669"},
{file = "mypy-0.790-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:2170492030f6faa537647d29945786d297e4862765f0b4ac5930ff62e300d802"},
{file = "mypy-0.790-cp35-cp35m-win_amd64.whl", hash = "sha256:e86bdace26c5fe9cf8cb735e7cedfe7850ad92b327ac5d797c656717d2ca66de"},
{file = "mypy-0.790-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e97e9c13d67fbe524be17e4d8025d51a7dca38f90de2e462243ab8ed8a9178d1"},
{file = "mypy-0.790-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0d34d6b122597d48a36d6c59e35341f410d4abfa771d96d04ae2c468dd201abc"},
{file = "mypy-0.790-cp36-cp36m-win_amd64.whl", hash = "sha256:72060bf64f290fb629bd4a67c707a66fd88ca26e413a91384b18db3876e57ed7"},
{file = "mypy-0.790-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:eea260feb1830a627fb526d22fbb426b750d9f5a47b624e8d5e7e004359b219c"},
{file = "mypy-0.790-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c614194e01c85bb2e551c421397e49afb2872c88b5830e3554f0519f9fb1c178"},
{file = "mypy-0.790-cp37-cp37m-win_amd64.whl", hash = "sha256:0a0d102247c16ce93c97066443d11e2d36e6cc2a32d8ccc1f705268970479324"},
{file = "mypy-0.790-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cf4e7bf7f1214826cf7333627cb2547c0db7e3078723227820d0a2490f117a01"},
{file = "mypy-0.790-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:af4e9ff1834e565f1baa74ccf7ae2564ae38c8df2a85b057af1dbbc958eb6666"},
{file = "mypy-0.790-cp38-cp38-win_amd64.whl", hash = "sha256:da56dedcd7cd502ccd3c5dddc656cb36113dd793ad466e894574125945653cea"},
{file = "mypy-0.790-py3-none-any.whl", hash = "sha256:2842d4fbd1b12ab422346376aad03ff5d0805b706102e475e962370f874a5122"},
{file = "mypy-0.790.tar.gz", hash = "sha256:2b21ba45ad9ef2e2eb88ce4aeadd0112d0f5026418324176fd494a6824b74975"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
nbconvert = [ nbconvert = [
{file = "nbconvert-5.6.1-py2.py3-none-any.whl", hash = "sha256:f0d6ec03875f96df45aa13e21fd9b8450c42d7e1830418cccc008c0df725fcee"}, {file = "nbconvert-5.6.1-py2.py3-none-any.whl", hash = "sha256:f0d6ec03875f96df45aa13e21fd9b8450c42d7e1830418cccc008c0df725fcee"},
{file = "nbconvert-5.6.1.tar.gz", hash = "sha256:21fb48e700b43e82ba0e3142421a659d7739b65568cc832a13976a77be16b523"}, {file = "nbconvert-5.6.1.tar.gz", hash = "sha256:21fb48e700b43e82ba0e3142421a659d7739b65568cc832a13976a77be16b523"},
@ -750,6 +1007,10 @@ parso = [
{file = "parso-0.7.0-py2.py3-none-any.whl", hash = "sha256:158c140fc04112dc45bca311633ae5033c2c2a7b732fa33d0955bad8152a8dd0"}, {file = "parso-0.7.0-py2.py3-none-any.whl", hash = "sha256:158c140fc04112dc45bca311633ae5033c2c2a7b732fa33d0955bad8152a8dd0"},
{file = "parso-0.7.0.tar.gz", hash = "sha256:908e9fae2144a076d72ae4e25539143d40b8e3eafbaeae03c1bfe226f4cdf12c"}, {file = "parso-0.7.0.tar.gz", hash = "sha256:908e9fae2144a076d72ae4e25539143d40b8e3eafbaeae03c1bfe226f4cdf12c"},
] ]
pathspec = [
{file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"},
{file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"},
]
pexpect = [ pexpect = [
{file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
{file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
@ -758,6 +1019,10 @@ pickleshare = [
{file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"},
{file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"},
] ]
pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
]
prometheus-client = [ prometheus-client = [
{file = "prometheus_client-0.8.0-py2.py3-none-any.whl", hash = "sha256:983c7ac4b47478720db338f1491ef67a100b474e3bc7dafcbaefb7d0b8f9b01c"}, {file = "prometheus_client-0.8.0-py2.py3-none-any.whl", hash = "sha256:983c7ac4b47478720db338f1491ef67a100b474e3bc7dafcbaefb7d0b8f9b01c"},
{file = "prometheus_client-0.8.0.tar.gz", hash = "sha256:c6e6b706833a6bd1fd51711299edee907857be10ece535126a158f911ee80915"}, {file = "prometheus_client-0.8.0.tar.gz", hash = "sha256:c6e6b706833a6bd1fd51711299edee907857be10ece535126a158f911ee80915"},
@ -770,9 +1035,13 @@ ptyprocess = [
{file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"}, {file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"},
{file = "ptyprocess-0.6.0.tar.gz", hash = "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0"}, {file = "ptyprocess-0.6.0.tar.gz", hash = "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0"},
] ]
py = [
{file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"},
{file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"},
]
pygments = [ pygments = [
{file = "Pygments-2.7.2-py3-none-any.whl", hash = "sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773"}, {file = "Pygments-2.7.3-py3-none-any.whl", hash = "sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08"},
{file = "Pygments-2.7.2.tar.gz", hash = "sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0"}, {file = "Pygments-2.7.3.tar.gz", hash = "sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716"},
] ]
pyparsing = [ pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
@ -781,6 +1050,14 @@ pyparsing = [
pyrsistent = [ pyrsistent = [
{file = "pyrsistent-0.16.0.tar.gz", hash = "sha256:28669905fe725965daa16184933676547c5bb40a5153055a8dee2a4bd7933ad3"}, {file = "pyrsistent-0.16.0.tar.gz", hash = "sha256:28669905fe725965daa16184933676547c5bb40a5153055a8dee2a4bd7933ad3"},
] ]
pytest = [
{file = "pytest-6.1.2-py3-none-any.whl", hash = "sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe"},
{file = "pytest-6.1.2.tar.gz", hash = "sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"},
]
pytest-cov = [
{file = "pytest-cov-2.10.1.tar.gz", hash = "sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e"},
{file = "pytest_cov-2.10.1-py2.py3-none-any.whl", hash = "sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191"},
]
python-dateutil = [ python-dateutil = [
{file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
{file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"},
@ -841,6 +1118,49 @@ pyzmq = [
{file = "pyzmq-19.0.1-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:aaa8b40b676576fd7806839a5de8e6d5d1b74981e6376d862af6c117af2a3c10"}, {file = "pyzmq-19.0.1-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:aaa8b40b676576fd7806839a5de8e6d5d1b74981e6376d862af6c117af2a3c10"},
{file = "pyzmq-19.0.1.tar.gz", hash = "sha256:13a5638ab24d628a6ade8f794195e1a1acd573496c3b85af2f1183603b7bf5e0"}, {file = "pyzmq-19.0.1.tar.gz", hash = "sha256:13a5638ab24d628a6ade8f794195e1a1acd573496c3b85af2f1183603b7bf5e0"},
] ]
regex = [
{file = "regex-2020.11.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa"},
{file = "regex-2020.11.13-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6"},
{file = "regex-2020.11.13-cp36-cp36m-win32.whl", hash = "sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e"},
{file = "regex-2020.11.13-cp36-cp36m-win_amd64.whl", hash = "sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884"},
{file = "regex-2020.11.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba"},
{file = "regex-2020.11.13-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538"},
{file = "regex-2020.11.13-cp37-cp37m-win32.whl", hash = "sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4"},
{file = "regex-2020.11.13-cp37-cp37m-win_amd64.whl", hash = "sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444"},
{file = "regex-2020.11.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f"},
{file = "regex-2020.11.13-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d"},
{file = "regex-2020.11.13-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5"},
{file = "regex-2020.11.13-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b"},
{file = "regex-2020.11.13-cp38-cp38-win32.whl", hash = "sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c"},
{file = "regex-2020.11.13-cp38-cp38-win_amd64.whl", hash = "sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683"},
{file = "regex-2020.11.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc"},
{file = "regex-2020.11.13-cp39-cp39-manylinux1_i686.whl", hash = "sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364"},
{file = "regex-2020.11.13-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9"},
{file = "regex-2020.11.13-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c"},
{file = "regex-2020.11.13-cp39-cp39-win32.whl", hash = "sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"},
{file = "regex-2020.11.13-cp39-cp39-win_amd64.whl", hash = "sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d"},
{file = "regex-2020.11.13.tar.gz", hash = "sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562"},
]
send2trash = [ send2trash = [
{file = "Send2Trash-1.5.0-py3-none-any.whl", hash = "sha256:f1691922577b6fa12821234aeb57599d887c4900b9ca537948d2dac34aea888b"}, {file = "Send2Trash-1.5.0-py3-none-any.whl", hash = "sha256:f1691922577b6fa12821234aeb57599d887c4900b9ca537948d2dac34aea888b"},
{file = "Send2Trash-1.5.0.tar.gz", hash = "sha256:60001cc07d707fe247c94f74ca6ac0d3255aabcb930529690897ca2a39db28b2"}, {file = "Send2Trash-1.5.0.tar.gz", hash = "sha256:60001cc07d707fe247c94f74ca6ac0d3255aabcb930529690897ca2a39db28b2"},
@ -857,6 +1177,10 @@ testpath = [
{file = "testpath-0.4.4-py2.py3-none-any.whl", hash = "sha256:bfcf9411ef4bf3db7579063e0546938b1edda3d69f4e1fb8756991f5951f85d4"}, {file = "testpath-0.4.4-py2.py3-none-any.whl", hash = "sha256:bfcf9411ef4bf3db7579063e0546938b1edda3d69f4e1fb8756991f5951f85d4"},
{file = "testpath-0.4.4.tar.gz", hash = "sha256:60e0a3261c149755f4399a1fff7d37523179a70fdc3abdf78de9fc2604aeec7e"}, {file = "testpath-0.4.4.tar.gz", hash = "sha256:60e0a3261c149755f4399a1fff7d37523179a70fdc3abdf78de9fc2604aeec7e"},
] ]
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
tornado = [ tornado = [
{file = "tornado-6.0.4-cp35-cp35m-win32.whl", hash = "sha256:5217e601700f24e966ddab689f90b7ea4bd91ff3357c3600fa1045e26d68e55d"}, {file = "tornado-6.0.4-cp35-cp35m-win32.whl", hash = "sha256:5217e601700f24e966ddab689f90b7ea4bd91ff3357c3600fa1045e26d68e55d"},
{file = "tornado-6.0.4-cp35-cp35m-win_amd64.whl", hash = "sha256:c98232a3ac391f5faea6821b53db8db461157baa788f5d6222a193e9456e1740"}, {file = "tornado-6.0.4-cp35-cp35m-win_amd64.whl", hash = "sha256:c98232a3ac391f5faea6821b53db8db461157baa788f5d6222a193e9456e1740"},
@ -872,6 +1196,38 @@ traitlets = [
{file = "traitlets-4.3.3-py2.py3-none-any.whl", hash = "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44"}, {file = "traitlets-4.3.3-py2.py3-none-any.whl", hash = "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44"},
{file = "traitlets-4.3.3.tar.gz", hash = "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"}, {file = "traitlets-4.3.3.tar.gz", hash = "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"},
] ]
typed-ast = [
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"},
{file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"},
{file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"},
{file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"},
{file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"},
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"},
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"},
{file = "typed_ast-1.4.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f"},
{file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"},
{file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"},
{file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"},
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"},
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"},
{file = "typed_ast-1.4.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298"},
{file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"},
{file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"},
{file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"},
{file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"},
{file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"},
{file = "typed_ast-1.4.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d"},
{file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"},
{file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"},
{file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"},
{file = "typed_ast-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c"},
{file = "typed_ast-1.4.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072"},
{file = "typed_ast-1.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91"},
{file = "typed_ast-1.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d"},
{file = "typed_ast-1.4.1-cp39-cp39-win32.whl", hash = "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395"},
{file = "typed_ast-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c"},
{file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
]
typing-extensions = [ typing-extensions = [
{file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
{file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},

View file

@ -36,6 +36,10 @@ ipywidgets = {version = "^7.5.1", optional = true}
jupyter = ["ipywidgets"] jupyter = ["ipywidgets"]
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pytest = "^6.1.2"
black = "^20.8b1"
mypy = "^0.790"
pytest-cov = "^2.10.1"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]

View file

@ -1,5 +0,0 @@
black==20.8b1
mypy==0.790
poetry==1.1.4
pytest==6.1.2
pytest-cov==2.10.1

View file

@ -26,10 +26,11 @@ class ColorBox:
for x in range(options.max_width): for x in range(options.max_width):
h = x / options.max_width h = x / options.max_width
l = 0.1 + ((y / 5) * 0.7) l = 0.1 + ((y / 5) * 0.7)
r, g, b = colorsys.hls_to_rgb(h, l, 1.0) r1, g1, b1 = colorsys.hls_to_rgb(h, l, 1.0)
yield Segment( r2, g2, b2 = colorsys.hls_to_rgb(h, l + 0.7 / 10, 1.0)
"", Style(color=Color.from_rgb(r * 255, g * 255, b * 255)) bgcolor = Color.from_rgb(r1 * 255, g1 * 255, b1 * 255)
) color = Color.from_rgb(r2 * 255, g2 * 255, b2 * 255)
yield Segment("", Style(color=color, bgcolor=bgcolor))
yield Segment.line() yield Segment.line()
def __rich_measure__(self, console: "Console", max_width: int) -> Measurement: def __rich_measure__(self, console: "Console", max_width: int) -> Measurement:
@ -96,7 +97,7 @@ def make_test_card() -> Table:
return table return table
table.add_row( table.add_row(
"Asian languages", "Asian\nlanguage\nsupport",
":flag_for_china: 该库支持中文,日文和韩文文本!\n:flag_for_japan: ライブラリは中国語、日本語、韓国語のテキストをサポートしています\n:flag_for_south_korea: 이 라이브러리는 중국어, 일본어 및 한국어 텍스트를 지원합니다", ":flag_for_china: 该库支持中文,日文和韩文文本!\n:flag_for_japan: ライブラリは中国語、日本語、韓国語のテキストをサポートしています\n:flag_for_south_korea: 이 라이브러리는 중국어, 일본어 및 한국어 텍스트를 지원합니다",
) )
@ -104,7 +105,7 @@ def make_test_card() -> Table:
"[bold magenta]Rich[/] supports a simple [i]bbcode[/i] like [b]markup[/b] for [yellow]color[/], [underline]style[/], and emoji! " "[bold magenta]Rich[/] supports a simple [i]bbcode[/i] like [b]markup[/b] for [yellow]color[/], [underline]style[/], and emoji! "
":+1: :apple: :ant: :bear: :baguette_bread: :bus: " ":+1: :apple: :ant: :bear: :baguette_bread: :bus: "
) )
table.add_row("Console markup", markup_example) table.add_row("Markup", markup_example)
example_table = Table( example_table = Table(
show_edge=False, show_edge=False,
@ -201,7 +202,7 @@ Supports much of the *markdown*, __syntax__!
) )
table.add_row( table.add_row(
"And more", "+more!",
"""Progress bars, columns, styled logging handler, tracebacks, etc...""", """Progress bars, columns, styled logging handler, tracebacks, etc...""",
) )
return table return table

View file

@ -72,3 +72,11 @@ class LogRender:
output.add_row(*row) output.add_row(*row)
return output return output
if __name__ == "__main__": # pragma: no cover
from rich.console import Console
c = Console()
c.print("[on blue]Hello", justify="right")
c.log("[on blue]hello", justify="right")

848
rich/_spinners.py Normal file
View file

@ -0,0 +1,848 @@
"""
Spinners are from:
* cli-spinners:
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
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.
"""
SPINNERS = {
"dots": {
"interval": 80,
"frames": ["", "", "", "", "", "", "", "", "", ""],
},
"dots2": {"interval": 80, "frames": ["", "", "", "", "", "", "", ""]},
"dots3": {
"interval": 80,
"frames": ["", "", "", "", "", "", "", "", "", ""],
},
"dots4": {
"interval": 80,
"frames": [
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
],
},
"dots5": {
"interval": 80,
"frames": [
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
],
},
"dots6": {
"interval": 80,
"frames": [
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
],
},
"dots7": {
"interval": 80,
"frames": [
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
],
},
"dots8": {
"interval": 80,
"frames": [
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
],
},
"dots9": {"interval": 80, "frames": ["", "", "", "", "", "", "", ""]},
"dots10": {"interval": 80, "frames": ["", "", "", "", "", "", ""]},
"dots11": {"interval": 100, "frames": ["", "", "", "", "", "", "", ""]},
"dots12": {
"interval": 80,
"frames": [
"⢀⠀",
"⡀⠀",
"⠄⠀",
"⢂⠀",
"⡂⠀",
"⠅⠀",
"⢃⠀",
"⡃⠀",
"⠍⠀",
"⢋⠀",
"⡋⠀",
"⠍⠁",
"⢋⠁",
"⡋⠁",
"⠍⠉",
"⠋⠉",
"⠋⠉",
"⠉⠙",
"⠉⠙",
"⠉⠩",
"⠈⢙",
"⠈⡙",
"⢈⠩",
"⡀⢙",
"⠄⡙",
"⢂⠩",
"⡂⢘",
"⠅⡘",
"⢃⠨",
"⡃⢐",
"⠍⡐",
"⢋⠠",
"⡋⢀",
"⠍⡁",
"⢋⠁",
"⡋⠁",
"⠍⠉",
"⠋⠉",
"⠋⠉",
"⠉⠙",
"⠉⠙",
"⠉⠩",
"⠈⢙",
"⠈⡙",
"⠈⠩",
"⠀⢙",
"⠀⡙",
"⠀⠩",
"⠀⢘",
"⠀⡘",
"⠀⠨",
"⠀⢐",
"⠀⡐",
"⠀⠠",
"⠀⢀",
"⠀⡀",
],
},
"dots8Bit": {
"interval": 80,
"frames": [
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
],
},
"line": {"interval": 130, "frames": ["-", "\\", "|", "/"]},
"line2": {"interval": 100, "frames": ["", "-", "", "", "", "-"]},
"pipe": {"interval": 100, "frames": ["", "", "", "", "", "", "", ""]},
"simpleDots": {"interval": 400, "frames": [". ", ".. ", "...", " "]},
"simpleDotsScrolling": {
"interval": 200,
"frames": [". ", ".. ", "...", " ..", " .", " "],
},
"star": {"interval": 70, "frames": ["", "", "", "", "", ""]},
"star2": {"interval": 80, "frames": ["+", "x", "*"]},
"flip": {
"interval": 70,
"frames": ["_", "_", "_", "-", "`", "`", "'", "´", "-", "_", "_", "_"],
},
"hamburger": {"interval": 100, "frames": ["", "", ""]},
"growVertical": {
"interval": 120,
"frames": ["", "", "", "", "", "", "", "", "", ""],
},
"growHorizontal": {
"interval": 120,
"frames": ["", "", "", "", "", "", "", "", "", "", "", ""],
},
"balloon": {"interval": 140, "frames": [" ", ".", "o", "O", "@", "*", " "]},
"balloon2": {"interval": 120, "frames": [".", "o", "O", "°", "O", "o", "."]},
"noise": {"interval": 100, "frames": ["", "", ""]},
"bounce": {"interval": 120, "frames": ["", "", "", ""]},
"boxBounce": {"interval": 120, "frames": ["", "", "", ""]},
"boxBounce2": {"interval": 100, "frames": ["", "", "", ""]},
"triangle": {"interval": 50, "frames": ["", "", "", ""]},
"arc": {"interval": 100, "frames": ["", "", "", "", "", ""]},
"circle": {"interval": 120, "frames": ["", "", ""]},
"squareCorners": {"interval": 180, "frames": ["", "", "", ""]},
"circleQuarters": {"interval": 120, "frames": ["", "", "", ""]},
"circleHalves": {"interval": 50, "frames": ["", "", "", ""]},
"squish": {"interval": 100, "frames": ["", ""]},
"toggle": {"interval": 250, "frames": ["", ""]},
"toggle2": {"interval": 80, "frames": ["", ""]},
"toggle3": {"interval": 120, "frames": ["", ""]},
"toggle4": {"interval": 100, "frames": ["", "", "", ""]},
"toggle5": {"interval": 100, "frames": ["", ""]},
"toggle6": {"interval": 300, "frames": ["", ""]},
"toggle7": {"interval": 80, "frames": ["", "⦿"]},
"toggle8": {"interval": 100, "frames": ["", ""]},
"toggle9": {"interval": 100, "frames": ["", ""]},
"toggle10": {"interval": 100, "frames": ["", "", ""]},
"toggle11": {"interval": 50, "frames": ["", ""]},
"toggle12": {"interval": 120, "frames": ["", ""]},
"toggle13": {"interval": 80, "frames": ["=", "*", "-"]},
"arrow": {"interval": 100, "frames": ["", "", "", "", "", "", "", ""]},
"arrow2": {
"interval": 80,
"frames": ["⬆️ ", "↗️ ", "➡️ ", "↘️ ", "⬇️ ", "↙️ ", "⬅️ ", "↖️ "],
},
"arrow3": {
"interval": 120,
"frames": ["▹▹▹▹▹", "▸▹▹▹▹", "▹▸▹▹▹", "▹▹▸▹▹", "▹▹▹▸▹", "▹▹▹▹▸"],
},
"bouncingBar": {
"interval": 80,
"frames": [
"[ ]",
"[= ]",
"[== ]",
"[=== ]",
"[ ===]",
"[ ==]",
"[ =]",
"[ ]",
"[ =]",
"[ ==]",
"[ ===]",
"[====]",
"[=== ]",
"[== ]",
"[= ]",
],
},
"bouncingBall": {
"interval": 80,
"frames": [
"( ● )",
"( ● )",
"( ● )",
"( ● )",
"( ●)",
"( ● )",
"( ● )",
"( ● )",
"( ● )",
"(● )",
],
},
"smiley": {"interval": 200, "frames": ["😄 ", "😝 "]},
"monkey": {"interval": 300, "frames": ["🙈 ", "🙈 ", "🙉 ", "🙊 "]},
"hearts": {"interval": 100, "frames": ["💛 ", "💙 ", "💜 ", "💚 ", "❤️ "]},
"clock": {
"interval": 100,
"frames": [
"🕛 ",
"🕐 ",
"🕑 ",
"🕒 ",
"🕓 ",
"🕔 ",
"🕕 ",
"🕖 ",
"🕗 ",
"🕘 ",
"🕙 ",
"🕚 ",
],
},
"earth": {"interval": 180, "frames": ["🌍 ", "🌎 ", "🌏 "]},
"material": {
"interval": 17,
"frames": [
"█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
"███▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
"████▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
"██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
"██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
"███████▁▁▁▁▁▁▁▁▁▁▁▁▁",
"████████▁▁▁▁▁▁▁▁▁▁▁▁",
"█████████▁▁▁▁▁▁▁▁▁▁▁",
"█████████▁▁▁▁▁▁▁▁▁▁▁",
"██████████▁▁▁▁▁▁▁▁▁▁",
"███████████▁▁▁▁▁▁▁▁▁",
"█████████████▁▁▁▁▁▁▁",
"██████████████▁▁▁▁▁▁",
"██████████████▁▁▁▁▁▁",
"▁██████████████▁▁▁▁▁",
"▁██████████████▁▁▁▁▁",
"▁██████████████▁▁▁▁▁",
"▁▁██████████████▁▁▁▁",
"▁▁▁██████████████▁▁▁",
"▁▁▁▁█████████████▁▁▁",
"▁▁▁▁██████████████▁▁",
"▁▁▁▁██████████████▁▁",
"▁▁▁▁▁██████████████▁",
"▁▁▁▁▁██████████████▁",
"▁▁▁▁▁██████████████▁",
"▁▁▁▁▁▁██████████████",
"▁▁▁▁▁▁██████████████",
"▁▁▁▁▁▁▁█████████████",
"▁▁▁▁▁▁▁█████████████",
"▁▁▁▁▁▁▁▁████████████",
"▁▁▁▁▁▁▁▁████████████",
"▁▁▁▁▁▁▁▁▁███████████",
"▁▁▁▁▁▁▁▁▁███████████",
"▁▁▁▁▁▁▁▁▁▁██████████",
"▁▁▁▁▁▁▁▁▁▁██████████",
"▁▁▁▁▁▁▁▁▁▁▁▁████████",
"▁▁▁▁▁▁▁▁▁▁▁▁▁███████",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁██████",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████",
"█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████",
"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
"███▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
"████▁▁▁▁▁▁▁▁▁▁▁▁▁▁██",
"█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
"█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
"██████▁▁▁▁▁▁▁▁▁▁▁▁▁█",
"████████▁▁▁▁▁▁▁▁▁▁▁▁",
"█████████▁▁▁▁▁▁▁▁▁▁▁",
"█████████▁▁▁▁▁▁▁▁▁▁▁",
"█████████▁▁▁▁▁▁▁▁▁▁▁",
"█████████▁▁▁▁▁▁▁▁▁▁▁",
"███████████▁▁▁▁▁▁▁▁▁",
"████████████▁▁▁▁▁▁▁▁",
"████████████▁▁▁▁▁▁▁▁",
"██████████████▁▁▁▁▁▁",
"██████████████▁▁▁▁▁▁",
"▁██████████████▁▁▁▁▁",
"▁██████████████▁▁▁▁▁",
"▁▁▁█████████████▁▁▁▁",
"▁▁▁▁▁████████████▁▁▁",
"▁▁▁▁▁████████████▁▁▁",
"▁▁▁▁▁▁███████████▁▁▁",
"▁▁▁▁▁▁▁▁█████████▁▁▁",
"▁▁▁▁▁▁▁▁█████████▁▁▁",
"▁▁▁▁▁▁▁▁▁█████████▁▁",
"▁▁▁▁▁▁▁▁▁█████████▁▁",
"▁▁▁▁▁▁▁▁▁▁█████████▁",
"▁▁▁▁▁▁▁▁▁▁▁████████▁",
"▁▁▁▁▁▁▁▁▁▁▁████████▁",
"▁▁▁▁▁▁▁▁▁▁▁▁███████▁",
"▁▁▁▁▁▁▁▁▁▁▁▁███████▁",
"▁▁▁▁▁▁▁▁▁▁▁▁▁███████",
"▁▁▁▁▁▁▁▁▁▁▁▁▁███████",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
],
},
"moon": {
"interval": 80,
"frames": ["🌑 ", "🌒 ", "🌓 ", "🌔 ", "🌕 ", "🌖 ", "🌗 ", "🌘 "],
},
"runner": {"interval": 140, "frames": ["🚶 ", "🏃 "]},
"pong": {
"interval": 80,
"frames": [
"▐⠂ ▌",
"▐⠈ ▌",
"▐ ⠂ ▌",
"▐ ⠠ ▌",
"▐ ⡀ ▌",
"▐ ⠠ ▌",
"▐ ⠂ ▌",
"▐ ⠈ ▌",
"▐ ⠂ ▌",
"▐ ⠠ ▌",
"▐ ⡀ ▌",
"▐ ⠠ ▌",
"▐ ⠂ ▌",
"▐ ⠈ ▌",
"▐ ⠂▌",
"▐ ⠠▌",
"▐ ⡀▌",
"▐ ⠠ ▌",
"▐ ⠂ ▌",
"▐ ⠈ ▌",
"▐ ⠂ ▌",
"▐ ⠠ ▌",
"▐ ⡀ ▌",
"▐ ⠠ ▌",
"▐ ⠂ ▌",
"▐ ⠈ ▌",
"▐ ⠂ ▌",
"▐ ⠠ ▌",
"▐ ⡀ ▌",
"▐⠠ ▌",
],
},
"shark": {
"interval": 120,
"frames": [
"▐|\\____________▌",
"▐_|\\___________▌",
"▐__|\\__________▌",
"▐___|\\_________▌",
"▐____|\\________▌",
"▐_____|\\_______▌",
"▐______|\\______▌",
"▐_______|\\_____▌",
"▐________|\\____▌",
"▐_________|\\___▌",
"▐__________|\\__▌",
"▐___________|\\_▌",
"▐____________|\\",
"▐____________/|▌",
"▐___________/|_▌",
"▐__________/|__▌",
"▐_________/|___▌",
"▐________/|____▌",
"▐_______/|_____▌",
"▐______/|______▌",
"▐_____/|_______▌",
"▐____/|________▌",
"▐___/|_________▌",
"▐__/|__________▌",
"▐_/|___________▌",
"▐/|____________▌",
],
},
"dqpb": {"interval": 100, "frames": ["d", "q", "p", "b"]},
"weather": {
"interval": 100,
"frames": [
"☀️ ",
"☀️ ",
"☀️ ",
"🌤 ",
"⛅️ ",
"🌥 ",
"☁️ ",
"🌧 ",
"🌨 ",
"🌧 ",
"🌨 ",
"🌧 ",
"🌨 ",
"",
"🌨 ",
"🌧 ",
"🌨 ",
"☁️ ",
"🌥 ",
"⛅️ ",
"🌤 ",
"☀️ ",
"☀️ ",
],
},
"christmas": {"interval": 400, "frames": ["🌲", "🎄"]},
"grenade": {
"interval": 80,
"frames": [
"، ",
" ",
" ´ ",
"",
"",
"",
" |",
" ",
"",
"",
" ",
" ",
" ",
" ",
],
},
"point": {"interval": 125, "frames": ["∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"]},
"layer": {"interval": 150, "frames": ["-", "=", ""]},
"betaWave": {
"interval": 80,
"frames": [
"ρββββββ",
"βρβββββ",
"ββρββββ",
"βββρβββ",
"ββββρββ",
"βββββρβ",
"ββββββρ",
],
},
"aesthetic": {
"interval": 80,
"frames": [
"▰▱▱▱▱▱▱",
"▰▰▱▱▱▱▱",
"▰▰▰▱▱▱▱",
"▰▰▰▰▱▱▱",
"▰▰▰▰▰▱▱",
"▰▰▰▰▰▰▱",
"▰▰▰▰▰▰▰",
"▰▱▱▱▱▱▱",
],
},
}

View file

@ -12,6 +12,7 @@ if TYPE_CHECKING:
from .console import Console, ConsoleOptions, RenderResult, RenderableType from .console import Console, ConsoleOptions, RenderResult, RenderableType
AlignValues = Literal["left", "center", "right"] AlignValues = Literal["left", "center", "right"]
AlignMethod = AlignValues # TODO: deprecate AlignValues
class Align(JupyterMixin): class Align(JupyterMixin):
@ -19,7 +20,7 @@ class Align(JupyterMixin):
Args: Args:
renderable (RenderableType): A console renderable. renderable (RenderableType): A console renderable.
align (AlignValues): One of "left", "center", or "right"" align (AlignMethod): One of "left", "center", or "right""
style (StyleType, optional): An optional style to apply to the renderable. style (StyleType, optional): An optional style to apply to the renderable.
pad (bool, optional): Pad the right with spaces. Defaults to True. pad (bool, optional): Pad the right with spaces. Defaults to True.
width (int, optional): Restrict contents to given width, or None to use default width. Defaults to None. width (int, optional): Restrict contents to given width, or None to use default width. Defaults to None.
@ -31,7 +32,7 @@ class Align(JupyterMixin):
def __init__( def __init__(
self, self,
renderable: "RenderableType", renderable: "RenderableType",
align: AlignValues, align: AlignMethod,
style: StyleType = None, style: StyleType = None,
*, *,
pad: bool = True, pad: bool = True,

View file

@ -1,4 +1,4 @@
from typing import Optional, Union from typing import Union
from .color import Color from .color import Color
from .console import Console, ConsoleOptions, RenderResult from .console import Console, ConsoleOptions, RenderResult

View file

@ -3,7 +3,7 @@ from itertools import chain
from operator import itemgetter from operator import itemgetter
from typing import Dict, Iterable, List, Optional, Tuple from typing import Dict, Iterable, List, Optional, Tuple
from .align import Align, AlignValues from .align import Align, AlignMethod
from .console import Console, ConsoleOptions, RenderableType, RenderResult from .console import Console, ConsoleOptions, RenderableType, RenderResult
from .constrain import Constrain from .constrain import Constrain
from .measure import Measurement from .measure import Measurement
@ -38,7 +38,7 @@ class Columns(JupyterMixin):
equal: bool = False, equal: bool = False,
column_first: bool = False, column_first: bool = False,
right_to_left: bool = False, right_to_left: bool = False,
align: AlignValues = None, align: AlignMethod = None,
title: TextType = None, title: TextType = None,
) -> None: ) -> None:
self.renderables = list(renderables or []) self.renderables = list(renderables or [])

View file

@ -31,7 +31,7 @@ from typing_extensions import Literal, Protocol, runtime_checkable
from . import errors, themes from . import errors, themes
from ._emoji_replace import _emoji_replace from ._emoji_replace import _emoji_replace
from ._log_render import LogRender from ._log_render import LogRender
from .align import Align, AlignValues from .align import Align, AlignMethod
from .color import ColorSystem from .color import ColorSystem
from .control import Control from .control import Control
from .highlighter import NullHighlighter, ReprHighlighter from .highlighter import NullHighlighter, ReprHighlighter
@ -42,11 +42,13 @@ from .pretty import Pretty
from .scope import render_scope from .scope import render_scope
from .segment import Segment from .segment import Segment
from .style import Style from .style import Style
from .styled import Styled
from .terminal_theme import DEFAULT_TERMINAL_THEME, TerminalTheme from .terminal_theme import DEFAULT_TERMINAL_THEME, TerminalTheme
from .text import Text, TextType from .text import Text, TextType
from .theme import Theme, ThemeStack from .theme import Theme, ThemeStack
if TYPE_CHECKING: if TYPE_CHECKING:
from .status import Status
from ._windows import WindowsConsoleFeatures from ._windows import WindowsConsoleFeatures
WINDOWS = platform.system() == "Windows" WINDOWS = platform.system() == "Windows"
@ -758,6 +760,39 @@ class Console:
""" """
self.control("\033[2J\033[H" if home else "\033[2J") self.control("\033[2J\033[H" if home else "\033[2J")
def status(
self,
status: RenderableType,
spinner: str = "dots",
spinner_style: str = "status.spinner",
speed: float = 1.0,
refresh_per_second: float = 12.5,
) -> "Status":
"""Display a status and spinner.
Args:
status (RenderableType): A status renderable (str or Text typically).
console (Console, optional): Console instance to use, or None for global console. Defaults to None.
spinner (str, optional): Name of spinner animation (see python -m rich.spinner). Defaults to "dots".
spinner_style (StyleType, optional): Style of spinner. Defaults to "status.spinner".
speed (float, optional): Speed factor for spinner animation. Defaults to 1.0.
refresh_per_second (float, optional): Number of refreshes per second. Defaults to 12.5.
Returns:
Status: A Status object that may be used as a context manager.
"""
from .status import Status
status_renderable = Status(
status,
console=self,
spinner=spinner,
spinner_style=spinner_style,
speed=speed,
refresh_per_second=refresh_per_second,
)
return status_renderable
def show_cursor(self, show: bool = True) -> None: def show_cursor(self, show: bool = True) -> None:
"""Show or hide the cursor. """Show or hide the cursor.
@ -946,7 +981,7 @@ class Console:
highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default. highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default.
Returns: Returns:
List[ConsoleRenderable]: A list of things to render. List[ConsoleRenderable]: A list oxf things to render.
""" """
renderables: List[ConsoleRenderable] = [] renderables: List[ConsoleRenderable] = []
_append = renderables.append _append = renderables.append
@ -957,7 +992,7 @@ class Console:
if justify in ("left", "center", "right"): if justify in ("left", "center", "right"):
def align_append(renderable: RenderableType) -> None: def align_append(renderable: RenderableType) -> None:
_append(Align(renderable, cast(AlignValues, justify))) _append(Align(renderable, cast(AlignMethod, justify)))
append = align_append append = align_append
@ -967,7 +1002,7 @@ class Console:
def check_text() -> None: def check_text() -> None:
if text: if text:
sep_text = Text(sep, end=end) sep_text = Text(sep, justify=justify, end=end)
append(sep_text.join(text)) append(sep_text.join(text))
del text[:] del text[:]
@ -1003,16 +1038,19 @@ class Console:
*, *,
characters: str = "", characters: str = "",
style: Union[str, Style] = "rule.line", style: Union[str, Style] = "rule.line",
align: AlignMethod = "center",
) -> None: ) -> None:
"""Draw a line with optional centered title. """Draw a line with optional centered title.
Args: Args:
title (str, optional): Text to render over the rule. Defaults to "". title (str, optional): Text to render over the rule. Defaults to "".
characters (str, optional): Character(s) to form the line. Defaults to "". characters (str, optional): Character(s) to form the line. Defaults to "".
style (str, optional): Style of line. Defaults to "rule.line".
align (str, optional): How to align the title, one of "left", "center", or "right". Defaults to "center".
""" """
from .rule import Rule from .rule import Rule
rule = Rule(title=title, characters=characters, style=style) rule = Rule(title=title, characters=characters, style=style, align=align)
self.print(rule) self.print(rule)
def control(self, control_codes: Union["Control", str]) -> None: def control(self, control_codes: Union["Control", str]) -> None:
@ -1172,6 +1210,7 @@ class Console:
*objects: Any, *objects: Any,
sep=" ", sep=" ",
end="\n", end="\n",
style: Union[str, Style] = None,
justify: JustifyMethod = None, justify: JustifyMethod = None,
emoji: bool = None, emoji: bool = None,
markup: bool = None, markup: bool = None,
@ -1185,6 +1224,7 @@ class Console:
objects (positional args): Objects to log to the terminal. objects (positional args): Objects to log to the terminal.
sep (str, optional): String to write between print data. Defaults to " ". sep (str, optional): String to write between print data. Defaults to " ".
end (str, optional): String to write at end of print data. Defaults to "\\n". end (str, optional): String to write at end of print data. Defaults to "\\n".
style (Union[str, Style], optional): A style to apply to output. Defaults to None.
justify (str, optional): One of "left", "right", "center", or "full". Defaults to ``None``. justify (str, optional): One of "left", "right", "center", or "full". Defaults to ``None``.
overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to None. overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to None.
emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to None. emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to None.
@ -1207,6 +1247,8 @@ class Console:
markup=markup, markup=markup,
highlight=highlight, highlight=highlight,
) )
if style is not None:
renderables = [Styled(renderable, style) for renderable in renderables]
caller = inspect.stack()[_stack_offset] caller = inspect.stack()[_stack_offset]
link_path = ( link_path = (

View file

@ -8,8 +8,6 @@ from typing import (
TYPE_CHECKING, TYPE_CHECKING,
) )
from .style import Style
if TYPE_CHECKING: if TYPE_CHECKING:
from .console import ( from .console import (
Console, Console,

View file

@ -44,6 +44,7 @@ DEFAULT_STYLES: Dict[str, Style] = {
"inspect.equals": Style(), "inspect.equals": Style(),
"inspect.help": Style(color="cyan"), "inspect.help": Style(color="cyan"),
"inspect.doc": Style(dim=True), "inspect.doc": Style(dim=True),
"live.ellipsis": Style(bold=True, color="red"),
"logging.keyword": Style(bold=True, color="yellow"), "logging.keyword": Style(bold=True, color="yellow"),
"logging.level.notset": Style(dim=True), "logging.level.notset": Style(dim=True),
"logging.level.debug": Style(color="green"), "logging.level.debug": Style(color="green"),
@ -115,6 +116,8 @@ DEFAULT_STYLES: Dict[str, Style] = {
"progress.percentage": Style(color="magenta"), "progress.percentage": Style(color="magenta"),
"progress.remaining": Style(color="cyan"), "progress.remaining": Style(color="cyan"),
"progress.data.speed": Style(color="red"), "progress.data.speed": Style(color="red"),
"progress.spinner": Style(color="green"),
"status.spinner": Style(color="green"),
} }
MARKDOWN_STYLES = { MARKDOWN_STYLES = {

47
rich/file_proxy.py Normal file
View file

@ -0,0 +1,47 @@
import io
from typing import List, Any, IO, TYPE_CHECKING
from .ansi import AnsiDecoder
from .text import Text
if TYPE_CHECKING:
from .console import Console
class FileProxy(io.TextIOBase):
"""Wraps a file (e.g. sys.stdout) and redirects writes to a console."""
def __init__(self, console: "Console", file: IO[str]) -> None:
self.__console = console
self.__file = file
self.__buffer: List[str] = []
self.__ansi_decoder = AnsiDecoder()
def __getattr__(self, name: str) -> Any:
return getattr(self.__file, name)
def write(self, text: str) -> int:
buffer = self.__buffer
lines: List[str] = []
while text:
line, new_line, text = text.partition("\n")
if new_line:
lines.append("".join(buffer) + line)
del buffer[:]
else:
buffer.append(line)
break
if lines:
console = self.__console
with console:
output = Text("\n").join(
self.__ansi_decoder.decode_line(line) for line in lines
)
console.print(output, markup=False, emoji=False, highlight=False)
return len(text)
def flush(self) -> None:
buffer = self.__buffer
if buffer:
self.__console.print("".join(buffer))
del buffer[:]

View file

@ -1,7 +1,6 @@
from typing import Iterable, List, TYPE_CHECKING from typing import Iterable, List, TYPE_CHECKING
# from .console import Console as BaseConsole from . import get_console
from .__init__ import get_console
from .segment import Segment from .segment import Segment
from .terminal_theme import DEFAULT_TERMINAL_THEME from .terminal_theme import DEFAULT_TERMINAL_THEME

376
rich/live.py Normal file
View file

@ -0,0 +1,376 @@
import sys
from threading import Event, RLock, Thread
from typing import IO, Any, List, Optional
from typing_extensions import Literal
from .__init__ import get_console
from ._loop import loop_last
from .console import (
Console,
ConsoleOptions,
ConsoleRenderable,
RenderableType,
RenderHook,
RenderResult,
)
from .control import Control
from .file_proxy import FileProxy
from .jupyter import JupyterMixin
from .live_render import LiveRender
from .segment import Segment
from .style import Style
from .text import Text
VerticalOverflowMethod = Literal["crop", "ellipsis", "visible"]
class _RefreshThread(Thread):
"""A thread that calls refresh() at regular intervals."""
def __init__(self, live: "Live", refresh_per_second: float) -> None:
self.live = live
self.refresh_per_second = refresh_per_second
self.done = Event()
super().__init__()
def stop(self) -> None:
self.done.set()
def run(self) -> None:
while not self.done.wait(1 / self.refresh_per_second):
with self.live._lock:
self.live.refresh()
class _LiveRender(LiveRender):
def __init__(self, live: "Live", renderable: RenderableType) -> None:
self._live = live
self.renderable = renderable
self._shape: Optional[Tuple[int, int]] = None
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
with self._live._lock:
lines = console.render_lines(self.renderable, options, pad=False)
shape = Segment.get_shape(lines)
_, height = shape
if height > console.size.height:
if self._live.vertical_overflow == "crop":
lines = lines[: console.size.height]
shape = Segment.get_shape(lines)
elif self._live.vertical_overflow == "ellipsis":
lines = lines[: (console.size.height - 1)]
lines.append(
list(
console.render(
Text(
"...",
overflow="crop",
justify="center",
end="",
style="live.ellipsis",
)
)
)
)
shape = Segment.get_shape(lines)
self._shape = shape
for last, line in loop_last(lines):
yield from line
if not last:
yield Segment.line()
class Live(JupyterMixin, RenderHook):
"""Renders an auto-updating live display of any given renderable.
Args:
renderable (RenderableType, optional): [The renderable to live display. Defaults to displaying nothing.
console (Console, optional): Optional Console instance. Default will an internal Console instance writing to stdout.
auto_refresh (bool, optional): Enable auto refresh. If disabled, you will need to call `refresh()` or `update()` with refresh flag. Defaults to True
refresh_per_second (float, optional): Number of times per second to refresh the live display. Defaults to 1.
transient (bool, optional): Clear the renderable on exit. Defaults to False.
redirect_stdout (bool, optional): Enable redirection of stdout, so ``print`` may be used. Defaults to True.
redirect_stderr (bool, optional): Enable redirection of stderr. Defaults to True.
vertical_overflow (VerticalOverflowMethod, optional): How to handle renderable when it is too tall for the console. Defaults to "ellipsis".
"""
def __init__(
self,
renderable: RenderableType = "",
*,
console: Console = None,
auto_refresh: bool = True,
refresh_per_second: float = 4,
transient: bool = False,
redirect_stdout: bool = True,
redirect_stderr: bool = True,
vertical_overflow: VerticalOverflowMethod = "ellipsis",
) -> None:
assert refresh_per_second > 0, "refresh_per_second must be > 0"
self.console = console if console is not None else get_console()
self._live_render = _LiveRender(self, renderable)
self._redirect_stdout = redirect_stdout
self._redirect_stderr = redirect_stderr
self._restore_stdout: Optional[IO[str]] = None
self._restore_stderr: Optional[IO[str]] = None
self._lock = RLock()
self.ipy_widget: Optional[Any] = None
self.auto_refresh = auto_refresh
self._started: bool = False
self.transient = transient
self._refresh_thread: Optional[_RefreshThread] = None
self.refresh_per_second = refresh_per_second
self.vertical_overflow = vertical_overflow
# cant store just clear_control as the live_render shape is lazily computed on render
def start(self) -> None:
"""Start live rendering display."""
with self._lock:
if self._started:
return
self.console.show_cursor(False)
self._enable_redirect_io()
self.console.push_render_hook(self)
self._started = True
if self.auto_refresh:
self._refresh_thread = _RefreshThread(self, self.refresh_per_second)
self._refresh_thread.start()
def stop(self) -> None:
"""Stop live rendering display."""
with self._lock:
if not self._started:
return
self._started = False
try:
if self.auto_refresh and self._refresh_thread is not None:
self._refresh_thread.stop()
# allow it to fully render on the last even if overflow
self.vertical_overflow = "visible"
self.refresh()
if self.console.is_terminal:
self.console.line()
finally:
self._disable_redirect_io()
self.console.pop_render_hook()
self.console.show_cursor(True)
if self.transient:
self.console.control(self._live_render.restore_cursor())
if self.ipy_widget is not None and self.transient: # pragma: no cover
self.ipy_widget.clear_output()
self.ipy_widget.close()
def __enter__(self) -> "Live":
self.start()
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
self.stop()
def _enable_redirect_io(self):
"""Enable redirecting of stdout / stderr."""
if self.console.is_terminal:
if self._redirect_stdout:
self._restore_stdout = sys.stdout
sys.stdout = FileProxy(self.console, sys.stdout)
if self._redirect_stderr:
self._restore_stderr = sys.stderr
sys.stderr = FileProxy(self.console, sys.stderr)
@property
def renderable(self) -> RenderableType:
"""Get the renderable that is being displayed
Returns:
RenderableType: Displayed renderable.
"""
with self._lock:
return self._live_render.renderable
def update(self, renderable: RenderableType, *, refresh: bool = False) -> None:
"""Update the renderable that is being displayed
Args:
renderable (RenderableType): New renderable to use.
refresh (bool, optional): Refresh the display. Defaults to False.
"""
with self._lock:
self._live_render.set_renderable(renderable)
if refresh:
self.refresh()
def refresh(self) -> None:
"""Update the display of the Live Render."""
if self.console.is_jupyter: # pragma: no cover
try:
from IPython.display import display
from ipywidgets import Output
except ImportError:
import warnings
warnings.warn('install "ipywidgets" for Jupyter support')
else:
with self._lock:
if self.ipy_widget is None:
self.ipy_widget = Output()
display(self.ipy_widget)
with self.ipy_widget:
self.ipy_widget.clear_output(wait=True)
self.console.print(self._live_render.renderable)
elif self.console.is_terminal and not self.console.is_dumb_terminal:
with self._lock, self.console:
self.console.print(Control(""))
elif (
not self._started and not self.transient
): # if it is finished allow files or dumb-terminals to see final result
with self.console:
self.console.print(Control(""))
def _disable_redirect_io(self):
"""Disable redirecting of stdout / stderr."""
if self._restore_stdout:
sys.stdout = self._restore_stdout
self._restore_stdout = None
if self._restore_stderr:
sys.stderr = self._restore_stderr
self._restore_stderr = None
def process_renderables(
self, renderables: List[ConsoleRenderable]
) -> List[ConsoleRenderable]:
"""Process renderables to restore cursor and display progress."""
if self.console.is_terminal:
# lock needs acquiring as user can modify live_render renerable at any time unlike in Progress.
with self._lock:
# determine the control command needed to clear previous rendering
renderables = [
self._live_render.position_cursor(),
*renderables,
self._live_render,
]
elif (
not self._started and not self.transient
): # if it is finished render the final output for files or dumb_terminals
renderables = [*renderables, self._live_render]
return renderables
if __name__ == "__main__": # pragma: no cover
import random
import time
from itertools import cycle
from typing import Dict, List, Tuple
from .console import Console
from .live import Live
from .panel import Panel
from .rule import Rule
from .syntax import Syntax
from .table import Table
console = Console()
syntax = Syntax(
'''def loop_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]:
"""Iterate and generate a tuple with a flag for last value."""
iter_values = iter(values)
try:
previous_value = next(iter_values)
except StopIteration:
return
for value in iter_values:
yield False, previous_value
previous_value = value
yield True, previous_value''',
"python",
line_numbers=True,
)
table = Table("foo", "bar", "baz")
table.add_row("1", "2", "3")
progress_renderables = [
"You can make the terminal shorter and taller to see the live table hide"
"Text may be printed while the progress bars are rendering.",
Panel("In fact, [i]any[/i] renderable will work"),
"Such as [magenta]tables[/]...",
table,
"Pretty printed structures...",
{"type": "example", "text": "Pretty printed"},
"Syntax...",
syntax,
Rule("Give it a try!"),
]
examples = cycle(progress_renderables)
exchanges = [
"SGD",
"MYR",
"EUR",
"USD",
"AUD",
"JPY",
"CNH",
"HKD",
"CAD",
"INR",
"DKK",
"GBP",
"RUB",
"NZD",
"MXN",
"IDR",
"TWD",
"THB",
"VND",
]
with Live(console=console) as live_table:
exchange_rate_dict: Dict[Tuple[str, str], float] = {}
for index in range(100):
select_exchange = exchanges[index % len(exchanges)]
for exchange in exchanges:
if exchange == select_exchange:
continue
time.sleep(0.4)
if random.randint(0, 10) < 1:
console.log(next(examples))
exchange_rate_dict[(select_exchange, exchange)] = 200 / (
(random.random() * 320) + 1
)
if len(exchange_rate_dict) > len(exchanges) - 1:
exchange_rate_dict.pop(list(exchange_rate_dict.keys())[0])
table = Table(title="Exchange Rates")
table.add_column("Source Currency")
table.add_column("Destination Currency")
table.add_column("Exchange Rate")
for ((soure, dest), exchange_rate) in exchange_rate_dict.items():
table.add_row(
soure,
dest,
Text(
f"{exchange_rate:.4f}",
style="red" if exchange_rate < 1.0 else "green",
),
)
live_table.update(table)

View file

@ -2,7 +2,7 @@ import logging
from datetime import datetime from datetime import datetime
from logging import Handler, LogRecord from logging import Handler, LogRecord
from pathlib import Path from pathlib import Path
from typing import ClassVar, List, Optional, Type from typing import ClassVar, List, Optional, Type, Union
from . import get_console from . import get_console
from ._log_render import LogRender from ._log_render import LogRender
@ -21,7 +21,7 @@ class RichHandler(Handler):
under your control. If a dependency writes messages containing square brackets, it may not produce the intended output. under your control. If a dependency writes messages containing square brackets, it may not produce the intended output.
Args: Args:
level (int, optional): Log level. Defaults to logging.NOTSET. level (Union[int, str], optional): Log level. Defaults to logging.NOTSET.
console (:class:`~rich.console.Console`, optional): Optional console instance to write logs. console (:class:`~rich.console.Console`, optional): Optional console instance to write logs.
Default will use a global console instance writing to stdout. Default will use a global console instance writing to stdout.
show_time (bool, optional): Show a column for the time. Defaults to True. show_time (bool, optional): Show a column for the time. Defaults to True.
@ -36,6 +36,9 @@ class RichHandler(Handler):
tracebacks_theme (str, optional): Override pygments theme used in traceback. tracebacks_theme (str, optional): Override pygments theme used in traceback.
tracebacks_word_wrap (bool, optional): Enable word wrapping of long tracebacks lines. Defaults to False. tracebacks_word_wrap (bool, optional): Enable word wrapping of long tracebacks lines. Defaults to False.
tracebacks_show_locals (bool, optional): Enable display of locals in tracebacks. Defaults to False. tracebacks_show_locals (bool, optional): Enable display of locals in tracebacks. Defaults to False.
locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
Defaults to 10.
locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.
""" """
KEYWORDS: ClassVar[Optional[List[str]]] = [ KEYWORDS: ClassVar[Optional[List[str]]] = [
@ -52,7 +55,7 @@ class RichHandler(Handler):
def __init__( def __init__(
self, self,
level: int = logging.NOTSET, level: Union[int, str] = logging.NOTSET,
console: Console = None, console: Console = None,
*, *,
show_time: bool = True, show_time: bool = True,
@ -67,6 +70,8 @@ class RichHandler(Handler):
tracebacks_theme: Optional[str] = None, tracebacks_theme: Optional[str] = None,
tracebacks_word_wrap: bool = True, tracebacks_word_wrap: bool = True,
tracebacks_show_locals: bool = False, tracebacks_show_locals: bool = False,
locals_max_length: int = 10,
locals_max_string: int = 80,
) -> None: ) -> None:
super().__init__(level=level) super().__init__(level=level)
self.console = console or get_console() self.console = console or get_console()
@ -85,6 +90,8 @@ class RichHandler(Handler):
self.tracebacks_theme = tracebacks_theme self.tracebacks_theme = tracebacks_theme
self.tracebacks_word_wrap = tracebacks_word_wrap self.tracebacks_word_wrap = tracebacks_word_wrap
self.tracebacks_show_locals = tracebacks_show_locals self.tracebacks_show_locals = tracebacks_show_locals
self.locals_max_length = locals_max_length
self.locals_max_string = locals_max_string
def get_level_text(self, record: LogRecord) -> Text: def get_level_text(self, record: LogRecord) -> Text:
"""Get the level name from the record. """Get the level name from the record.
@ -127,6 +134,8 @@ class RichHandler(Handler):
theme=self.tracebacks_theme, theme=self.tracebacks_theme,
word_wrap=self.tracebacks_word_wrap, word_wrap=self.tracebacks_word_wrap,
show_locals=self.tracebacks_show_locals, show_locals=self.tracebacks_show_locals,
locals_max_length=self.locals_max_length,
locals_max_string=self.locals_max_string,
) )
message = record.getMessage() message = record.getMessage()
@ -201,6 +210,8 @@ if __name__ == "__main__": # pragma: no cover
def divide(): def divide():
number = 1 number = 1
divisor = 0 divisor = 0
foos = ["foo"] * 100
log.debug("in divide")
try: try:
number / divisor number / divisor
except: except:

View file

@ -2,7 +2,7 @@ from typing import Optional, TYPE_CHECKING
from .box import Box, ROUNDED from .box import Box, ROUNDED
from .align import AlignValues from .align import AlignMethod
from .jupyter import JupyterMixin from .jupyter import JupyterMixin
from .measure import Measurement, measure_renderables from .measure import Measurement, measure_renderables
from .padding import Padding, PaddingDimensions from .padding import Padding, PaddingDimensions
@ -39,7 +39,7 @@ class Panel(JupyterMixin):
box: Box = ROUNDED, box: Box = ROUNDED,
*, *,
title: TextType = None, title: TextType = None,
title_align: AlignValues = "center", title_align: AlignMethod = "center",
safe_box: Optional[bool] = None, safe_box: Optional[bool] = None,
expand: bool = True, expand: bool = True,
style: StyleType = "none", style: StyleType = "none",
@ -65,7 +65,7 @@ class Panel(JupyterMixin):
box: Box = ROUNDED, box: Box = ROUNDED,
*, *,
title: TextType = None, title: TextType = None,
title_align: AlignValues = "center", title_align: AlignMethod = "center",
safe_box: Optional[bool] = None, safe_box: Optional[bool] = None,
style: StyleType = "none", style: StyleType = "none",
border_style: StyleType = "none", border_style: StyleType = "none",

View file

@ -20,7 +20,7 @@ from typing import (
from rich.highlighter import ReprHighlighter from rich.highlighter import ReprHighlighter
from .abc import RichRenderable from .abc import RichRenderable
from .__init__ import get_console from . import get_console
from ._pick import pick_bool from ._pick import pick_bool
from .cells import cell_len from .cells import cell_len
from .highlighter import ReprHighlighter from .highlighter import ReprHighlighter
@ -44,6 +44,7 @@ def install(
crop: bool = False, crop: bool = False,
indent_guides: bool = False, indent_guides: bool = False,
max_length: int = None, max_length: int = None,
max_string: int = None,
expand_all: bool = False, expand_all: bool = False,
) -> None: ) -> None:
"""Install automatic pretty printing in the Python REPL. """Install automatic pretty printing in the Python REPL.
@ -55,6 +56,7 @@ def install(
indent_guides (bool, optional): Enable indentation guides. Defaults to False. indent_guides (bool, optional): Enable indentation guides. Defaults to False.
max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation. max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
Defaults to None. Defaults to None.
max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to None.
expand_all (bool, optional): Expand all containers. Defaults to False expand_all (bool, optional): Expand all containers. Defaults to False
""" """
from rich import get_console from rich import get_console
@ -74,6 +76,7 @@ def install(
overflow=overflow, overflow=overflow,
indent_guides=indent_guides, indent_guides=indent_guides,
max_length=max_length, max_length=max_length,
max_string=max_string,
expand_all=expand_all, expand_all=expand_all,
), ),
crop=crop, crop=crop,
@ -152,7 +155,11 @@ class Pretty:
def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement": def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement":
pretty_str = pretty_repr( pretty_str = pretty_repr(
self._object, max_width=max_width, indent_size=self.indent_size self._object,
max_width=max_width,
indent_size=self.indent_size,
max_length=self.max_length,
max_string=self.max_string,
) )
text_width = max(cell_len(line) for line in pretty_str.splitlines()) text_width = max(cell_len(line) for line in pretty_str.splitlines())
return Measurement(text_width, text_width) return Measurement(text_width, text_width)

View file

@ -1,4 +1,3 @@
import io
import sys import sys
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections import deque from collections import deque
@ -7,10 +6,8 @@ from dataclasses import dataclass, field
from datetime import timedelta from datetime import timedelta
from math import ceil from math import ceil
from threading import Event, RLock, Thread from threading import Event, RLock, Thread
from time import monotonic
from typing import ( from typing import (
IO, IO,
TYPE_CHECKING,
Any, Any,
Callable, Callable,
Deque, Deque,
@ -27,7 +24,6 @@ from typing import (
) )
from . import filesize, get_console from . import filesize, get_console
from .ansi import AnsiDecoder
from .console import ( from .console import (
Console, Console,
ConsoleRenderable, ConsoleRenderable,
@ -37,13 +33,15 @@ from .console import (
RenderHook, RenderHook,
) )
from .control import Control from .control import Control
from .file_proxy import FileProxy
from .highlighter import Highlighter from .highlighter import Highlighter
from .jupyter import JupyterMixin from .jupyter import JupyterMixin
from .live_render import LiveRender from .live_render import LiveRender
from .progress_bar import ProgressBar from .progress_bar import ProgressBar
from .spinner import Spinner
from .style import StyleType from .style import StyleType
from .table import Table from .table import Table
from .text import Text from .text import Text, TextType
TaskID = NewType("TaskID", int) TaskID = NewType("TaskID", int)
@ -95,12 +93,13 @@ def track(
console: Optional[Console] = None, console: Optional[Console] = None,
transient: bool = False, transient: bool = False,
get_time: Callable[[], float] = None, get_time: Callable[[], float] = None,
refresh_per_second: int = None, refresh_per_second: float = None,
style: StyleType = "bar.back", style: StyleType = "bar.back",
complete_style: StyleType = "bar.complete", complete_style: StyleType = "bar.complete",
finished_style: StyleType = "bar.finished", finished_style: StyleType = "bar.finished",
pulse_style: StyleType = "bar.pulse", pulse_style: StyleType = "bar.pulse",
update_period: float = 0.1, update_period: float = 0.1,
disable: bool = False,
) -> Iterable[ProgressType]: ) -> Iterable[ProgressType]:
"""Track progress by iterating over a sequence. """Track progress by iterating over a sequence.
@ -111,12 +110,13 @@ def track(
auto_refresh (bool, optional): Automatic refresh, disable to force a refresh after each iteration. Default is True. auto_refresh (bool, optional): Automatic refresh, disable to force a refresh after each iteration. Default is True.
transient: (bool, optional): Clear the progress on exit. Defaults to False. transient: (bool, optional): Clear the progress on exit. Defaults to False.
console (Console, optional): Console to write to. Default creates internal Console instance. console (Console, optional): Console to write to. Default creates internal Console instance.
refresh_per_second (Optional[int], optional): Number of times per second to refresh the progress information, or None to use default. Defaults to None. refresh_per_second (Optional[float], optional): Number of times per second to refresh the progress information, or None to use default. Defaults to None.
style (StyleType, optional): Style for the bar background. Defaults to "bar.back". style (StyleType, optional): Style for the bar background. Defaults to "bar.back".
complete_style (StyleType, optional): Style for the completed bar. Defaults to "bar.complete". complete_style (StyleType, optional): Style for the completed bar. Defaults to "bar.complete".
finished_style (StyleType, optional): Style for a finished bar. Defaults to "bar.done". finished_style (StyleType, optional): Style for a finished bar. Defaults to "bar.done".
pulse_style (StyleType, optional): Style for pulsing bars. Defaults to "bar.pulse". pulse_style (StyleType, optional): Style for pulsing bars. Defaults to "bar.pulse".
update_period (float, optional): Minimum time (in seconds) between calls to update(). Defaults to 0.1. update_period (float, optional): Minimum time (in seconds) between calls to update(). Defaults to 0.1.
disable (bool, optional): Disable display of progress.
Returns: Returns:
Iterable[ProgressType]: An iterable of the values in the sequence. Iterable[ProgressType]: An iterable of the values in the sequence.
@ -144,6 +144,7 @@ def track(
transient=transient, transient=transient,
get_time=get_time, get_time=get_time,
refresh_per_second=refresh_per_second, refresh_per_second=refresh_per_second,
disable=disable,
) )
with progress: with progress:
@ -189,6 +190,68 @@ class ProgressColumn(ABC):
"""Should return a renderable object.""" """Should return a renderable object."""
class RenderableColumn(ProgressColumn):
"""A column to insert an arbitrary column.
Args:
renderable (RenderableType, optional): Any renderable. Defaults to empty string.
"""
def __init__(self, renderable: RenderableType = ""):
self.renderable = renderable
super().__init__()
def render(self, task: "Task") -> RenderableType:
return self.renderable
class SpinnerColumn(ProgressColumn):
"""A column with a 'spinner' animation.
Args:
spinner_name (str, optional): Name of spinner animation. Defaults to "dots".
style (StyleType, optional): Style of spinner. Defaults to "progress.spinner".
speed (float, optional): Speed factor of spinner. Defaults to 1.0.
finished_text (TextType, optional): Text used when task is finished. Defaults to " ".
"""
def __init__(
self,
spinner_name: str = "dots",
style: Optional[StyleType] = "progress.spinner",
speed: float = 1.0,
finished_text: TextType = " ",
):
self.spinner = Spinner(spinner_name, style=style, speed=speed)
self.finished_text = (
Text.from_markup(finished_text)
if isinstance(finished_text, str)
else finished_text
)
super().__init__()
def set_spinner(
self,
spinner_name: str,
spinner_style: Optional[StyleType] = "progress.spinner",
speed: float = 1.0,
):
"""Set a new spinner.
Args:
spinner_name (str): Spinner name, see python -m rich.spinner.
spinner_style (Optional[StyleType], optional): Spinner style. Defaults to "progress.spinner".
speed (float, optional): Speed factor of spinner. Defaults to 1.0.
"""
self.spinner = Spinner(spinner_name, style=spinner_style, speed=speed)
def render(self, task: "Task") -> Text:
if task.finished:
return self.finished_text
text = self.spinner.render(task.get_time())
return text
class TextColumn(ProgressColumn): class TextColumn(ProgressColumn):
"""A column containing text.""" """A column containing text."""
@ -460,7 +523,7 @@ class Task:
class _RefreshThread(Thread): class _RefreshThread(Thread):
"""A thread that calls refresh() on the Process object at regular intervals.""" """A thread that calls refresh() on the Process object at regular intervals."""
def __init__(self, progress: "Progress", refresh_per_second: int = 10) -> None: def __init__(self, progress: "Progress", refresh_per_second: float = 10) -> None:
self.progress = progress self.progress = progress
self.refresh_per_second = refresh_per_second self.refresh_per_second = refresh_per_second
self.done = Event() self.done = Event()
@ -474,57 +537,19 @@ class _RefreshThread(Thread):
self.progress.refresh() self.progress.refresh()
class _FileProxy(io.TextIOBase):
"""Wraps a file (e.g. sys.stdout) and redirects writes to a console."""
def __init__(self, console: Console, file: IO[str]) -> None:
self.__console = console
self.__file = file
self.__buffer: List[str] = []
self.__ansi_decoder = AnsiDecoder()
def __getattr__(self, name: str) -> Any:
return getattr(self.__file, name)
def write(self, text: str) -> int:
buffer = self.__buffer
lines: List[str] = []
while text:
line, new_line, text = text.partition("\n")
if new_line:
lines.append("".join(buffer) + line)
del buffer[:]
else:
buffer.append(line)
break
if lines:
console = self.__console
with console:
output = Text("\n").join(
self.__ansi_decoder.decode_line(line) for line in lines
)
console.print(output, markup=False, emoji=False, highlight=False)
return len(text)
def flush(self) -> None:
buffer = self.__buffer
if buffer:
self.__console.print("".join(buffer))
del buffer[:]
class Progress(JupyterMixin, RenderHook): class Progress(JupyterMixin, RenderHook):
"""Renders an auto-updating progress bar(s). """Renders an auto-updating progress bar(s).
Args: Args:
console (Console, optional): Optional Console instance. Default will an internal Console instance writing to stdout. console (Console, optional): Optional Console instance. Default will an internal Console instance writing to stdout.
auto_refresh (bool, optional): Enable auto refresh. If disabled, you will need to call `refresh()`. auto_refresh (bool, optional): Enable auto refresh. If disabled, you will need to call `refresh()`.
refresh_per_second (Optional[int], optional): Number of times per second to refresh the progress information or None to use default (10). Defaults to None. refresh_per_second (Optional[float], optional): Number of times per second to refresh the progress information or None to use default (10). Defaults to None.
speed_estimate_period: (float, optional): Period (in seconds) used to calculate the speed estimate. Defaults to 30. speed_estimate_period: (float, optional): Period (in seconds) used to calculate the speed estimate. Defaults to 30.
transient: (bool, optional): Clear the progress on exit. Defaults to False. transient: (bool, optional): Clear the progress on exit. Defaults to False.
redirect_stdout: (bool, optional): Enable redirection of stdout, so ``print`` may be used. Defaults to True. redirect_stdout: (bool, optional): Enable redirection of stdout, so ``print`` may be used. Defaults to True.
redirect_stderr: (bool, optional): Enable redirection of stderr. Defaults to True. redirect_stderr: (bool, optional): Enable redirection of stderr. Defaults to True.
get_time: (Callable, optional): A callable that gets the current time, or None to use Console.get_time. Defaults to None. get_time: (Callable, optional): A callable that gets the current time, or None to use Console.get_time. Defaults to None.
disable (bool, optional): Disable progress display. Defaults to False
""" """
def __init__( def __init__(
@ -532,12 +557,13 @@ class Progress(JupyterMixin, RenderHook):
*columns: Union[str, ProgressColumn], *columns: Union[str, ProgressColumn],
console: Console = None, console: Console = None,
auto_refresh: bool = True, auto_refresh: bool = True,
refresh_per_second: int = None, refresh_per_second: float = None,
speed_estimate_period: float = 30.0, speed_estimate_period: float = 30.0,
transient: bool = False, transient: bool = False,
redirect_stdout: bool = True, redirect_stdout: bool = True,
redirect_stderr: bool = True, redirect_stderr: bool = True,
get_time: GetTimeCallable = None, get_time: GetTimeCallable = None,
disable: bool = False,
) -> None: ) -> None:
assert ( assert (
refresh_per_second is None or refresh_per_second > 0 refresh_per_second is None or refresh_per_second > 0
@ -557,6 +583,7 @@ class Progress(JupyterMixin, RenderHook):
self._redirect_stdout = redirect_stdout self._redirect_stdout = redirect_stdout
self._redirect_stderr = redirect_stderr self._redirect_stderr = redirect_stderr
self.get_time = get_time or self.console.get_time self.get_time = get_time or self.console.get_time
self.disable = disable
self._tasks: Dict[TaskID, Task] = {} self._tasks: Dict[TaskID, Task] = {}
self._live_render = LiveRender(self.get_renderable()) self._live_render = LiveRender(self.get_renderable())
self._task_index: TaskID = TaskID(0) self._task_index: TaskID = TaskID(0)
@ -593,10 +620,10 @@ class Progress(JupyterMixin, RenderHook):
if self.console.is_terminal: if self.console.is_terminal:
if self._redirect_stdout: if self._redirect_stdout:
self._restore_stdout = sys.stdout self._restore_stdout = sys.stdout
sys.stdout = _FileProxy(self.console, sys.stdout) sys.stdout = FileProxy(self.console, sys.stdout)
if self._redirect_stderr: if self._redirect_stderr:
self._restore_stderr = sys.stderr self._restore_stderr = sys.stderr
sys.stderr = _FileProxy(self.console, sys.stderr) sys.stderr = FileProxy(self.console, sys.stderr)
def _disable_redirect_io(self): def _disable_redirect_io(self):
"""Disable redirecting of stdout / stderr.""" """Disable redirecting of stdout / stderr."""
@ -848,6 +875,7 @@ class Progress(JupyterMixin, RenderHook):
def refresh(self) -> None: def refresh(self) -> None:
"""Refresh (render) the progress information.""" """Refresh (render) the progress information."""
if not self.disable:
if self.console.is_jupyter: # pragma: no cover if self.console.is_jupyter: # pragma: no cover
try: try:
from IPython.display import display from IPython.display import display
@ -1023,7 +1051,15 @@ if __name__ == "__main__": # pragma: no coverage
console = Console(record=True) console = Console(record=True)
try: try:
with Progress(console=console, transient=True) as progress: with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
TimeRemainingColumn(),
console=console,
transient=True,
) as progress:
task1 = progress.add_task("[red]Downloading", total=1000) task1 = progress.add_task("[red]Downloading", total=1000)
task2 = progress.add_task("[green]Processing", total=1000) task2 = progress.add_task("[green]Processing", total=1000)

View file

@ -1,6 +1,6 @@
from typing import IO, Any, Generic, List, Optional, TextIO, TypeVar, Union, overload from typing import Any, Generic, List, Optional, TextIO, TypeVar, Union, overload
from .__init__ import get_console from . import get_console
from .console import Console from .console import Console
from .text import Text, TextType from .text import Text, TextType

View file

@ -1,5 +1,6 @@
from typing import Union from typing import Union
from .align import AlignMethod
from .cells import cell_len, set_cell_size from .cells import cell_len, set_cell_size
from .console import Console, ConsoleOptions, RenderResult from .console import Console, ConsoleOptions, RenderResult
from .jupyter import JupyterMixin from .jupyter import JupyterMixin
@ -15,6 +16,7 @@ class Rule(JupyterMixin):
characters (str, optional): Character(s) used to draw the line. Defaults to "". characters (str, optional): Character(s) used to draw the line. Defaults to "".
style (StyleType, optional): Style of Rule. Defaults to "rule.line". style (StyleType, optional): Style of Rule. Defaults to "rule.line".
end (str, optional): Character at end of Rule. defaults to "\\n" end (str, optional): Character at end of Rule. defaults to "\\n"
align (str, optional): How to align the title, one of "left", "center", or "right". Defaults to "center".
""" """
def __init__( def __init__(
@ -24,15 +26,21 @@ class Rule(JupyterMixin):
characters: str = "", characters: str = "",
style: Union[str, Style] = "rule.line", style: Union[str, Style] = "rule.line",
end: str = "\n", end: str = "\n",
align: AlignMethod = "center",
) -> None: ) -> None:
if cell_len(characters) < 1: if cell_len(characters) < 1:
raise ValueError( raise ValueError(
"'characters' argument must have a cell width of at least 1" "'characters' argument must have a cell width of at least 1"
) )
if align not in ("left", "center", "right"):
raise ValueError(
f'invalid value for align, expected "left", "center", "right" (not {align!r})'
)
self.title = title self.title = title
self.characters = characters self.characters = characters
self.style = style self.style = style
self.end = end self.end = end
self.align = align
def __repr__(self) -> str: def __repr__(self) -> str:
return f"Rule({self.title!r}, {self.characters!r})" return f"Rule({self.title!r}, {self.characters!r})"
@ -56,18 +64,21 @@ class Rule(JupyterMixin):
if not self.title: if not self.title:
rule_text = Text(characters * ((width // chars_len) + 1), self.style) rule_text = Text(characters * ((width // chars_len) + 1), self.style)
rule_text.truncate(width) rule_text.truncate(width)
else: rule_text.plain = set_cell_size(rule_text.plain, width)
yield rule_text
return
if isinstance(self.title, Text): if isinstance(self.title, Text):
title_text = self.title title_text = self.title
else: else:
title_text = console.render_str(self.title, style="rule.text") title_text = console.render_str(self.title, style="rule.text")
if cell_len(title_text.plain) > width - 4:
title_text.truncate(width - 4, overflow="ellipsis")
title_text.plain = title_text.plain.replace("\n", " ") title_text.plain = title_text.plain.replace("\n", " ")
title_text.expand_tabs() title_text.expand_tabs()
rule_text = Text(end=self.end) rule_text = Text(end=self.end)
if self.align == "center":
title_text.truncate(width - 4, overflow="ellipsis")
side_width = (width - cell_len(title_text.plain)) // 2 side_width = (width - cell_len(title_text.plain)) // 2
left = Text(characters * (side_width // chars_len + 1)) left = Text(characters * (side_width // chars_len + 1))
left.truncate(side_width - 1) left.truncate(side_width - 1)
@ -77,6 +88,17 @@ class Rule(JupyterMixin):
rule_text.append(left.plain + " ", self.style) rule_text.append(left.plain + " ", self.style)
rule_text.append(title_text) rule_text.append(title_text)
rule_text.append(" " + right.plain, self.style) rule_text.append(" " + right.plain, self.style)
elif self.align == "left":
title_text.truncate(width - 2, overflow="ellipsis")
rule_text.append(title_text)
rule_text.append(" ")
rule_text.append(characters * (width - rule_text.cell_len), self.style)
elif self.align == "right":
title_text.truncate(width - 2, overflow="ellipsis")
rule_text.append(characters * (width - title_text.cell_len - 1), self.style)
rule_text.append(" ")
rule_text.append(title_text)
rule_text.plain = set_cell_size(rule_text.plain, width) rule_text.plain = set_cell_size(rule_text.plain, width)
yield rule_text yield rule_text

View file

@ -16,7 +16,9 @@ def render_scope(
*, *,
title: TextType = None, title: TextType = None,
sort_keys: bool = True, sort_keys: bool = True,
indent_guides: bool = False indent_guides: bool = False,
max_length: int = None,
max_string: int = None,
) -> "ConsoleRenderable": ) -> "ConsoleRenderable":
"""Render python variables in a given scope. """Render python variables in a given scope.
@ -25,6 +27,9 @@ def render_scope(
title (str, optional): Optional title. Defaults to None. title (str, optional): Optional title. Defaults to None.
sort_keys (bool, optional): Enable sorting of items. Defaults to True. sort_keys (bool, optional): Enable sorting of items. Defaults to True.
indent_guides (bool, optional): Enable indentaton guides. Defaults to False. indent_guides (bool, optional): Enable indentaton guides. Defaults to False.
max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
Defaults to None.
max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to None.
Returns: Returns:
RenderableType: A renderable object. RenderableType: A renderable object.
@ -46,7 +51,13 @@ def render_scope(
) )
items_table.add_row( items_table.add_row(
key_text, key_text,
Pretty(value, highlighter=highlighter, indent_guides=indent_guides), Pretty(
value,
highlighter=highlighter,
indent_guides=indent_guides,
max_length=max_length,
max_string=max_string,
),
) )
return Panel.fit( return Panel.fit(
items_table, items_table,

88
rich/spinner.py Normal file
View file

@ -0,0 +1,88 @@
from typing import cast, List, Optional, TYPE_CHECKING
from ._spinners import SPINNERS
from .console import Console
from .measure import Measurement
from .style import StyleType
from .text import Text, TextType
if TYPE_CHECKING:
from .console import Console, ConsoleOptions, RenderResult
class Spinner:
def __init__(
self, name: str, text: TextType = "", *, style: StyleType = None, speed=1.0
) -> None:
"""A spinner animation.
Args:
name (str): Name of spinner (run python -m rich.spinner).
text (TextType, optional): Text to display at the right of the spinner. Defaults to "".
style (StyleType, optional): Style for sinner amimation. Defaults to None.
speed (float, optional): Speed factor for animation. Defaults to 1.0.
Raises:
KeyError: If name isn't one of the supported spinner animations.
"""
try:
spinner = SPINNERS[name]
except KeyError:
raise KeyError(f"no spinner called {name!r}")
self.text = text
self.frames = cast(List[str], spinner["frames"])[:]
self.interval = cast(float, spinner["interval"])
self.start_time: Optional[float] = None
self.style = style
self.speed = speed
self.time = 0.0
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
time = console.get_time()
if self.start_time is None:
self.start_time = time
text = self.render(time - self.start_time)
yield text
def __rich_measure__(self, console: "Console", max_width: int) -> Measurement:
text = self.render(0)
return Measurement.get(console, text, max_width)
def render(self, time: float) -> Text:
"""Render the spinner for a given time.
Args:
time (float): Time in seconds.
Returns:
Text: A Text instance containing animation frame.
"""
frame_no = int((time * self.speed) / (self.interval / 1000.0))
frame = Text(self.frames[frame_no % len(self.frames)], style=self.style or "")
return Text.assemble(frame, " ", self.text) if self.text else frame
if __name__ == "__main__": # pragma: no cover
from time import sleep
from .columns import Columns
from .panel import Panel
from .live import Live
all_spinners = Columns(
[
Spinner(spinner_name, text=Text(repr(spinner_name), style="green"))
for spinner_name in sorted(SPINNERS.keys())
],
column_first=True,
expand=True,
)
with Live(
Panel(all_spinners, title="Spinners", border_style="blue"),
refresh_per_second=20,
) as live:
while True:
sleep(0.1)

127
rich/status.py Normal file
View file

@ -0,0 +1,127 @@
from typing import Optional
from .console import Console, RenderableType
from .live import Live
from .spinner import Spinner
from .style import StyleType
from .table import Table
class Status:
"""Displays a status indicator with a 'spinner' animation.
Args:
status (RenderableType): A status renderable (str or Text typically).
console (Console, optional): Console instance to use, or None for global console. Defaults to None.
spinner (str, optional): Name of spinner animation (see python -m rich.spinner). Defaults to "dots".
spinner_style (StyleType, optional): Style of spinner. Defaults to "status.spinner".
speed (float, optional): Speed factor for spinner animation. Defaults to 1.0.
refresh_per_second (float, optional): Number of refreshes per second. Defaults to 12.5.
"""
def __init__(
self,
status: RenderableType,
*,
console: Console = None,
spinner: str = "dots",
spinner_style: StyleType = "status.spinner",
speed: float = 1.0,
refresh_per_second: float = 12.5,
):
self.status = status
self.spinner = spinner
self.spinner_style = spinner_style
self.speed = speed
self._spinner = Spinner(spinner, style=spinner_style, speed=speed)
self._live = Live(
self.renderable,
console=console,
refresh_per_second=refresh_per_second,
transient=True,
)
self.update(
status=status, spinner=spinner, spinner_style=spinner_style, speed=speed
)
@property
def renderable(self) -> Table:
"""Get the renderable for the status (a table with spinner and status)."""
table = Table.grid(padding=1)
table.add_row(self._spinner, self.status)
return table
@property
def console(self) -> "Console":
"""Get the Console used by the Status objects."""
return self._live.console
def update(
self,
*,
status: Optional[RenderableType] = None,
spinner: Optional[str] = None,
spinner_style: Optional[StyleType] = None,
speed: Optional[float] = None,
):
"""Update status.
Args:
status (Optional[RenderableType], optional): New status renderable or None for no change. Defaults to None.
spinner (Optional[str], optional): New spinner or None for no change. Defaults to None.
spinner_style (Optional[StyleType], optional): New spinner style or None for no change. Defaults to None.
speed (Optional[float], optional): Speed factor for spinner animation or None for no change. Defaults to None.
"""
if status is not None:
self.status = status
if spinner is not None:
self.spinner = spinner
if spinner_style is not None:
self.spinner_style = spinner_style
if speed is not None:
self.speed = speed
self._spinner = Spinner(
self.spinner, style=self.spinner_style, speed=self.speed
)
self._live.update(self.renderable, refresh=True)
def start(self) -> None:
"""Start the status animation."""
self._live.start()
def stop(self) -> None:
"""Stop the spinner animation."""
self._live.stop()
def __enter__(self) -> "Status":
self.start()
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
self.stop()
if __name__ == "__main__": # pragma: no cover
from time import sleep
from .console import Console
console = Console()
with console.status("[magenta]Covid detector booting up") as status:
sleep(3)
console.log("Importing advanced AI")
sleep(3)
console.log("Advanced Covid AI Ready")
sleep(3)
status.update(status="[bold blue] Scanning for Covid", spinner="earth")
sleep(3)
console.log("Found 10,000,000,000 copies of Covid32.exe")
sleep(3)
status.update(
status="[bold red]Moving Covid32.exe to Trash",
spinner="bouncingBall",
spinner_style="yellow",
)
sleep(5)
console.print("[bold green]Covid deleted successfully")

View file

@ -11,7 +11,6 @@ from .padding import Padding, PaddingDimensions
from .protocol import is_renderable from .protocol import is_renderable
from .segment import Segment from .segment import Segment
from .style import Style, StyleType from .style import Style, StyleType
from .styled import Styled
from .text import Text, TextType from .text import Text, TextType
if TYPE_CHECKING: if TYPE_CHECKING:
@ -81,6 +80,17 @@ class Column:
return self.ratio is not None return self.ratio is not None
@dataclass
class Row:
"""Information regarding a row."""
style: Optional[StyleType] = None
"""Style to apply to row."""
end_section: bool = False
"""Indicated end of section, which will force a line beneath the row."""
class _Cell(NamedTuple): class _Cell(NamedTuple):
"""A single cell in a table.""" """A single cell in a table."""
@ -122,6 +132,7 @@ class Table(JupyterMixin):
""" """
columns: List[Column] columns: List[Column]
rows: List[Row]
def __init__( def __init__(
self, self,
@ -153,6 +164,7 @@ class Table(JupyterMixin):
) -> None: ) -> None:
self.columns: List[Column] = [] self.columns: List[Column] = []
self.rows: List[Row] = []
append_column = self.columns.append append_column = self.columns.append
for index, header in enumerate(headers): for index, header in enumerate(headers):
if isinstance(header, str): if isinstance(header, str):
@ -184,7 +196,6 @@ class Table(JupyterMixin):
self.caption_style = caption_style self.caption_style = caption_style
self.title_justify = title_justify self.title_justify = title_justify
self.caption_justify = caption_justify self.caption_justify = caption_justify
self._row_count = 0
self.row_styles = list(row_styles or []) self.row_styles = list(row_styles or [])
@classmethod @classmethod
@ -240,13 +251,17 @@ class Table(JupyterMixin):
@property @property
def row_count(self) -> int: def row_count(self) -> int:
"""Get the current number of rows.""" """Get the current number of rows."""
return self._row_count return len(self.rows)
def get_row_style(self, index: int) -> StyleType: def get_row_style(self, console: "Console", index: int) -> StyleType:
"""Get the current row style.""" """Get the current row style."""
style = Style.null()
if self.row_styles: if self.row_styles:
return self.row_styles[index % len(self.row_styles)] style += console.get_style(self.row_styles[index % len(self.row_styles)])
return Style.null() row_style = self.rows[index].style
if row_style is not None:
style += console.get_style(row_style)
return style
def __rich_measure__(self, console: "Console", max_width: int) -> Measurement: def __rich_measure__(self, console: "Console", max_width: int) -> Measurement:
if self.width is not None: if self.width is not None:
@ -255,9 +270,7 @@ class Table(JupyterMixin):
return Measurement(0, 0) return Measurement(0, 0)
extra_width = self._extra_width extra_width = self._extra_width
max_width = sum(self._calculate_column_widths(console, max_width - extra_width)) max_width = sum(self._calculate_column_widths(console, max_width - extra_width))
_measure_column = self._measure_column _measure_column = self._measure_column
measurements = [ measurements = [
@ -342,7 +355,10 @@ class Table(JupyterMixin):
self.columns.append(column) self.columns.append(column)
def add_row( def add_row(
self, *renderables: Optional["RenderableType"], style: StyleType = None self,
*renderables: Optional["RenderableType"],
style: StyleType = None,
end_section: bool = False,
) -> None: ) -> None:
"""Add a row of renderables. """Add a row of renderables.
@ -350,15 +366,14 @@ class Table(JupyterMixin):
*renderables (None or renderable): Each cell in a row must be a renderable object (including str), *renderables (None or renderable): Each cell in a row must be a renderable object (including str),
or ``None`` for a blank cell. or ``None`` for a blank cell.
style (StyleType, optional): An optional style to apply to the entire row. Defaults to None. style (StyleType, optional): An optional style to apply to the entire row. Defaults to None.
end_section (bool, optional): End a section and draw a line. Defaults to False.
Raises: Raises:
errors.NotRenderableError: If you add something that can't be rendered. errors.NotRenderableError: If you add something that can't be rendered.
""" """
def add_cell(column: Column, renderable: "RenderableType") -> None: def add_cell(column: Column, renderable: "RenderableType") -> None:
column._cells.append( column._cells.append(renderable)
renderable if style is None else Styled(renderable, style)
)
cell_renderables: List[Optional["RenderableType"]] = list(renderables) cell_renderables: List[Optional["RenderableType"]] = list(renderables)
@ -371,7 +386,7 @@ class Table(JupyterMixin):
for index, renderable in enumerate(cell_renderables): for index, renderable in enumerate(cell_renderables):
if index == len(columns): if index == len(columns):
column = Column(_index=index) column = Column(_index=index)
for _ in range(self._row_count): for _ in self.rows:
add_cell(column, Text("")) add_cell(column, Text(""))
self.columns.append(column) self.columns.append(column)
else: else:
@ -384,7 +399,7 @@ class Table(JupyterMixin):
raise errors.NotRenderableError( raise errors.NotRenderableError(
f"unable to render {type(renderable).__name__}; a string or other renderable object is required" f"unable to render {type(renderable).__name__}; a string or other renderable object is required"
) )
self._row_count += 1 self.rows.append(Row(style=style, end_section=end_section))
def __rich_console__( def __rich_console__(
self, console: "Console", options: "ConsoleOptions" self, console: "Console", options: "ConsoleOptions"
@ -623,14 +638,11 @@ class Table(JupyterMixin):
table_style = console.get_style(self.style or "") table_style = console.get_style(self.style or "")
border_style = table_style + console.get_style(self.border_style or "") border_style = table_style + console.get_style(self.border_style or "")
rows: List[Tuple[_Cell, ...]] = list( _column_cells = (
zip(
*(
self._get_cells(column_index, column) self._get_cells(column_index, column)
for column_index, column in enumerate(self.columns) for column_index, column in enumerate(self.columns)
) )
) row_cells: List[Tuple[_Cell, ...]] = list(zip(*_column_cells))
)
_box = ( _box = (
self.box.substitute( self.box.substitute(
options, safe=pick_bool(self.safe_box, console.safe_box) options, safe=pick_bool(self.safe_box, console.safe_box)
@ -677,18 +689,23 @@ class Table(JupyterMixin):
get_row_style = self.get_row_style get_row_style = self.get_row_style
get_style = console.get_style get_style = console.get_style
for index, (first, last, row) in enumerate(loop_first_last(rows)): for index, (first, last, row_cell) in enumerate(loop_first_last(row_cells)):
header_row = first and show_header header_row = first and show_header
footer_row = last and show_footer footer_row = last and show_footer
row = (
self.rows[index - show_header]
if (not header_row and not footer_row)
else None
)
max_height = 1 max_height = 1
cells: List[List[List[Segment]]] = [] cells: List[List[List[Segment]]] = []
if header_row or footer_row: if header_row or footer_row:
row_style = Style.null() row_style = Style.null()
else: else:
row_style = get_style( row_style = get_style(
get_row_style(index - 1 if show_header else index) get_row_style(console, index - 1 if show_header else index)
) )
for width, cell, column in zip(widths, row, columns): for width, cell, column in zip(widths, row_cell, columns):
render_options = options.update( render_options = options.update(
width=width, width=width,
justify=column.justify, justify=column.justify,
@ -703,7 +720,9 @@ class Table(JupyterMixin):
cells.append(lines) cells.append(lines)
cells[:] = [ cells[:] = [
_Segment.set_shape(_cell, width, max_height, style=table_style) _Segment.set_shape(
_cell, width, max_height, style=table_style + row_style
)
for width, _cell in zip(widths, cells) for width, _cell in zip(widths, cells)
] ]
@ -743,16 +762,16 @@ class Table(JupyterMixin):
_box.get_row(widths, "head", edge=show_edge), border_style _box.get_row(widths, "head", edge=show_edge), border_style
) )
yield new_line yield new_line
if _box and (show_lines or leading): end_section = row and row.end_section
if _box and (show_lines or leading or end_section):
if ( if (
not last not last
and not (show_footer and index >= len(rows) - 2) and not (show_footer and index >= len(row_cells) - 2)
and not (show_header and header_row) and not (show_header and header_row)
): ):
if leading: if leading:
for _ in range(leading):
yield _Segment( yield _Segment(
_box.get_row(widths, "mid", edge=show_edge), _box.get_row(widths, "mid", edge=show_edge) * leading,
border_style, border_style,
) )
else: else:
@ -781,10 +800,24 @@ if __name__ == "__main__": # pragma: no cover
table.add_column("Title", style="magenta") table.add_column("Title", style="magenta")
table.add_column("Box Office", justify="right", style="green") table.add_column("Box Office", justify="right", style="green")
table.add_row("Dec 20, 2019", "Star Wars: The Rise of Skywalker", "$952,110,690") table.add_row(
"Dec 20, 2019",
"Star Wars: The Rise of Skywalker",
"$952,110,690",
)
table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347") table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347")
table.add_row("Dec 15, 2017", "Star Wars Ep. V111: The Last Jedi", "$1,332,539,889") table.add_row(
table.add_row("Dec 16, 2016", "Rogue One: A Star Wars Story", "$1,332,439,889") "Dec 15, 2017",
"Star Wars Ep. V111: The Last Jedi",
"$1,332,539,889",
style="on black",
end_section=True,
)
table.add_row(
"Dec 16, 2016",
"Rogue One: A Star Wars Story",
"$1,332,439,889",
)
def header(text: str) -> None: def header(text: str) -> None:
console.print() console.print()

View file

@ -1,4 +1,5 @@
from functools import partial, reduce from functools import partial, reduce
from io import UnsupportedOperation
from math import gcd from math import gcd
import re import re
from operator import itemgetter from operator import itemgetter
@ -19,7 +20,7 @@ from typing import (
from ._loop import loop_last from ._loop import loop_last
from ._pick import pick_bool from ._pick import pick_bool
from ._wrap import divide_line from ._wrap import divide_line
from .align import AlignValues from .align import AlignMethod
from .cells import cell_len, set_cell_size from .cells import cell_len, set_cell_size
from .containers import Lines from .containers import Lines
from .control import strip_control_codes from .control import strip_control_codes
@ -182,6 +183,32 @@ class Text(JupyterMixin):
return other.plain in self.plain return other.plain in self.plain
return False return False
def __getitem__(self, slice: Union[int, slice]) -> "Text":
def get_text_at(offset) -> "Text":
_Span = Span
text = Text(
self.plain[offset],
spans=[
_Span(0, 1, style)
for start, end, style in self._spans
if end > offset >= start
],
end="",
)
return text
if isinstance(slice, int):
return get_text_at(slice)
else:
start, stop, step = slice.indices(len(self.plain))
if step == 1:
lines = self.divide([start, stop])
return lines[1]
else:
# This would be a bit of work to implement efficiently
# For now, its not required
raise TypeError("slices with step!=1 are not supported")
@property @property
def cell_len(self) -> int: def cell_len(self) -> int:
"""Get the number of cells required to render this text.""" """Get the number of cells required to render this text."""
@ -521,10 +548,10 @@ class Text(JupyterMixin):
Iterable[Segment]: Result of render that may be written to the console. Iterable[Segment]: Result of render that may be written to the console.
""" """
_Segment = Segment
text = self.plain text = self.plain
null_style = Style.null()
enumerated_spans = list(enumerate(self._spans, 1)) enumerated_spans = list(enumerate(self._spans, 1))
get_style = partial(console.get_style, default=null_style) get_style = partial(console.get_style, default=Style.null())
style_map = {index: get_style(span.style) for index, span in enumerated_spans} style_map = {index: get_style(span.style) for index, span in enumerated_spans}
style_map[0] = get_style(self.style) style_map[0] = get_style(self.style)
@ -540,7 +567,6 @@ class Text(JupyterMixin):
stack_append = stack.append stack_append = stack.append
stack_pop = stack.remove stack_pop = stack.remove
_Segment = Segment
style_cache: Dict[Tuple[Style, ...], Style] = {} style_cache: Dict[Tuple[Style, ...], Style] = {}
style_cache_get = style_cache.get style_cache_get = style_cache.get
combine = Style.combine combine = Style.combine
@ -725,11 +751,11 @@ class Text(JupyterMixin):
if count: if count:
self.plain = f"{self.plain}{character * count}" self.plain = f"{self.plain}{character * count}"
def align(self, align: AlignValues, width: int, character: str = " ") -> None: def align(self, align: AlignMethod, width: int, character: str = " ") -> None:
"""Align text to a given width. """Align text to a given width.
Args: Args:
align (AlignValues): One of "left", "center", or "right". align (AlignMethod): One of "left", "center", or "right".
width (int): Desired width. width (int): Desired width.
character (str, optional): Character to pad with. Defaults to " ". character (str, optional): Character to pad with. Defaults to " ".
""" """
@ -930,6 +956,7 @@ class Text(JupyterMixin):
while True: while True:
span, new_span = span.split(line_end) span, new_span = span.split(line_end)
if span:
new_lines[line_index]._spans.append(span.move(-line_start)) new_lines[line_index]._spans.append(span.move(-line_start))
if new_span is None: if new_span is None:
break break

View file

@ -42,6 +42,9 @@ from .theme import Theme
WINDOWS = platform.system() == "Windows" WINDOWS = platform.system() == "Windows"
LOCALS_MAX_LENGTH = 10
LOCALS_MAX_STRING = 80
def install( def install(
*, *,
@ -146,6 +149,9 @@ class Traceback:
word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False. word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
show_locals (bool, optional): Enable display of local variables. Defaults to False. show_locals (bool, optional): Enable display of local variables. Defaults to False.
indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True. indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True.
locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
Defaults to 10.
locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.
""" """
def __init__( def __init__(
@ -157,6 +163,8 @@ class Traceback:
word_wrap: bool = False, word_wrap: bool = False,
show_locals: bool = False, show_locals: bool = False,
indent_guides: bool = True, indent_guides: bool = True,
locals_max_length: int = LOCALS_MAX_LENGTH,
locals_max_string: int = LOCALS_MAX_STRING,
): ):
if trace is None: if trace is None:
exc_type, exc_value, traceback = sys.exc_info() exc_type, exc_value, traceback = sys.exc_info()
@ -174,6 +182,8 @@ class Traceback:
self.word_wrap = word_wrap self.word_wrap = word_wrap
self.show_locals = show_locals self.show_locals = show_locals
self.indent_guides = indent_guides self.indent_guides = indent_guides
self.locals_max_length = locals_max_length
self.locals_max_string = locals_max_string
@classmethod @classmethod
def from_exception( def from_exception(
@ -187,6 +197,8 @@ class Traceback:
word_wrap: bool = False, word_wrap: bool = False,
show_locals: bool = False, show_locals: bool = False,
indent_guides: bool = True, indent_guides: bool = True,
locals_max_length: int = LOCALS_MAX_LENGTH,
locals_max_string: int = LOCALS_MAX_STRING,
) -> "Traceback": ) -> "Traceback":
"""Create a traceback from exception info """Create a traceback from exception info
@ -200,6 +212,9 @@ class Traceback:
word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False. word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
show_locals (bool, optional): Enable display of local variables. Defaults to False. show_locals (bool, optional): Enable display of local variables. Defaults to False.
indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True. indent_guides (bool, optional): Enable indent guides in code and locals. Defaults to True.
locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
Defaults to 10.
locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.
Returns: Returns:
Traceback: A Traceback instance that may be printed. Traceback: A Traceback instance that may be printed.
@ -215,6 +230,8 @@ class Traceback:
word_wrap=word_wrap, word_wrap=word_wrap,
show_locals=show_locals, show_locals=show_locals,
indent_guides=indent_guides, indent_guides=indent_guides,
locals_max_length=locals_max_length,
locals_max_string=locals_max_string,
) )
@classmethod @classmethod
@ -224,6 +241,8 @@ class Traceback:
exc_value: BaseException, exc_value: BaseException,
traceback: Optional[TracebackType], traceback: Optional[TracebackType],
show_locals: bool = False, show_locals: bool = False,
locals_max_length: int = LOCALS_MAX_LENGTH,
locals_max_string: int = LOCALS_MAX_STRING,
) -> Trace: ) -> Trace:
"""Extract traceback information. """Extract traceback information.
@ -232,6 +251,9 @@ class Traceback:
exc_value (BaseException): Exception value. exc_value (BaseException): Exception value.
traceback (TracebackType): Python Traceback object. traceback (TracebackType): Python Traceback object.
show_locals (bool, optional): Enable display of local variables. Defaults to False. show_locals (bool, optional): Enable display of local variables. Defaults to False.
locals_max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation.
Defaults to 10.
locals_max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to 80.
Returns: Returns:
Trace: A Trace instance which you can use to construct a `Traceback`. Trace: A Trace instance which you can use to construct a `Traceback`.
@ -266,7 +288,11 @@ class Traceback:
lineno=line_no, lineno=line_no,
name=frame_summary.f_code.co_name, name=frame_summary.f_code.co_name,
locals={ locals={
key: pretty.traverse(value) key: pretty.traverse(
value,
max_length=locals_max_length,
max_string=locals_max_string,
)
for key, value in frame_summary.f_locals.items() for key, value in frame_summary.f_locals.items()
} }
if show_locals if show_locals
@ -460,6 +486,8 @@ class Traceback:
frame.locals, frame.locals,
title="locals", title="locals",
indent_guides=self.indent_guides, indent_guides=self.indent_guides,
max_length=self.locals_max_length,
max_string=self.locals_max_string,
), ),
], ],
padding=1, padding=1,

File diff suppressed because one or more lines are too long

View file

@ -13,6 +13,7 @@ from rich.console import CaptureError, Console, ConsoleOptions, render_group
from rich.measure import measure_renderables from rich.measure import measure_renderables
from rich.pager import SystemPager from rich.pager import SystemPager
from rich.panel import Panel from rich.panel import Panel
from rich.status import Status
from rich.style import Style from rich.style import Style
@ -97,6 +98,21 @@ def test_print():
assert console.file.getvalue() == "foo\n" assert console.file.getvalue() == "foo\n"
def test_log():
console = Console(
file=io.StringIO(),
width=80,
color_system="truecolor",
log_time_format="TIME",
log_path=False,
)
console.log("foo", style="red")
expected = "\x1b[2;36mTIME\x1b[0m\x1b[2;36m \x1b[0m\x1b[31mfoo\x1b[0m\x1b[31m \x1b[0m\n"
result = console.file.getvalue()
print(repr(result))
assert result == expected
def test_print_empty(): def test_print_empty():
console = Console(file=io.StringIO(), color_system="truecolor") console = Console(file=io.StringIO(), color_system="truecolor")
console.print() console.print()
@ -218,6 +234,12 @@ def test_input_password(monkeypatch, capsys):
assert user_input == "bar" assert user_input == "bar"
def test_status():
console = Console(file=io.StringIO(), force_terminal=True, width=20)
status = console.status("foo")
assert isinstance(status, Status)
def test_justify_none(): def test_justify_none():
console = Console(file=io.StringIO(), force_terminal=True, width=20) console = Console(file=io.StringIO(), force_terminal=True, width=20)
console.print("FOO", justify=None) console.print("FOO", justify=None)

160
tests/test_live.py Normal file
View file

@ -0,0 +1,160 @@
# encoding=utf-8
import io
import time
from typing import Optional
# import pytest
from rich.console import Console
from rich.live import Live
def create_capture_console(
*, width: int = 60, height: int = 80, force_terminal: Optional[bool] = True
) -> Console:
return Console(
width=width,
height=height,
file=io.StringIO(),
force_terminal=force_terminal,
legacy_windows=False,
color_system=None, # use no color system to reduce complexity of output
)
def test_live_state() -> None:
with Live("") as live:
assert live._started
live.start()
assert live.renderable == ""
assert live._started
live.stop()
assert not live._started
assert not live._started
def test_growing_display() -> None:
console = create_capture_console()
console.begin_capture()
with Live(console=console, auto_refresh=False) as live:
display = ""
for step in range(10):
display += f"Step {step}\n"
live.update(display, refresh=True)
output = console.end_capture()
assert (
output
== "\x1b[?25lStep 0\n\r\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\nStep 9\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\nStep 9\n\n\x1b[?25h"
)
def test_growing_display_transient() -> None:
console = create_capture_console()
console.begin_capture()
with Live(console=console, auto_refresh=False, transient=True) as live:
display = ""
for step in range(10):
display += f"Step {step}\n"
live.update(display, refresh=True)
output = console.end_capture()
assert (
output
== "\x1b[?25lStep 0\n\r\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\nStep 9\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\nStep 9\n\n\x1b[?25h\r\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K"
)
def test_growing_display_overflow_ellipsis() -> None:
console = create_capture_console(height=5)
console.begin_capture()
with Live(
console=console, auto_refresh=False, vertical_overflow="ellipsis"
) as live:
display = ""
for step in range(10):
display += f"Step {step}\n"
live.update(display, refresh=True)
output = console.end_capture()
assert (
output
== "\x1b[?25lStep 0\n\r\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n ... \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n ... \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n ... \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n ... \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n ... \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n ... \r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\nStep 9\n\n\x1b[?25h"
)
def test_growing_display_overflow_crop() -> None:
console = create_capture_console(height=5)
console.begin_capture()
with Live(console=console, auto_refresh=False, vertical_overflow="crop") as live:
display = ""
for step in range(10):
display += f"Step {step}\n"
live.update(display, refresh=True)
output = console.end_capture()
assert (
output
== "\x1b[?25lStep 0\n\r\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\nStep 9\n\n\x1b[?25h"
)
def test_growing_display_overflow_visible() -> None:
console = create_capture_console(height=5)
console.begin_capture()
with Live(console=console, auto_refresh=False, vertical_overflow="visible") as live:
display = ""
for step in range(10):
display += f"Step {step}\n"
live.update(display, refresh=True)
output = console.end_capture()
assert (
output
== "\x1b[?25lStep 0\n\r\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\nStep 9\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\nStep 9\n\n\x1b[?25h"
)
def test_growing_display_autorefresh() -> None:
"""Test generating a table but using auto-refresh from threading"""
console = create_capture_console()
console = create_capture_console(height=5)
console.begin_capture()
with Live(console=console, auto_refresh=True, vertical_overflow="visible") as live:
display = ""
for step in range(10):
display += f"Step {step}\n"
live.update(display)
time.sleep(0.2)
# no way to truly test w/ multithreading, just make sure it doesn't crash
def test_growing_display_console_redirect() -> None:
console = create_capture_console()
console.begin_capture()
with Live(console=console, auto_refresh=False) as live:
display = ""
for step in range(10):
console.print(f"Running step {step}")
display += f"Step {step}\n"
live.update(display, refresh=True)
output = console.end_capture()
assert (
output
== "\x1b[?25lRunning step 0\n\r\x1b[2KStep 0\n\r\x1b[2K\x1b[1A\x1b[2KRunning step 1\nStep 0\n\r\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 2\nStep 0\nStep 1\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 3\nStep 0\nStep 1\nStep 2\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 4\nStep 0\nStep 1\nStep 2\nStep 3\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 5\nStep 0\nStep 1\nStep 2\nStep 3\nStep 4\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 6\nStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 7\nStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 8\nStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KRunning step 9\nStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\nStep 9\n\r\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2KStep 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\nStep 9\n\n\x1b[?25h"
)
def test_growing_display_file_console() -> None:
console = create_capture_console(force_terminal=False)
console.begin_capture()
with Live(console=console, auto_refresh=False) as live:
display = ""
for step in range(10):
display += f"Step {step}\n"
live.update(display, refresh=True)
output = console.end_capture()
assert (
output
== "Step 0\nStep 1\nStep 2\nStep 3\nStep 4\nStep 5\nStep 6\nStep 7\nStep 8\nStep 9\n"
)

View file

@ -45,6 +45,14 @@ def test_log():
assert rendered == expected assert rendered == expected
def test_justify():
console = Console(width=20, log_path=False, log_time=False, color_system=None)
console.begin_capture()
console.log("foo", justify="right")
result = console.end_capture()
assert result == " foo\n"
if __name__ == "__main__": if __name__ == "__main__":
render = render_log() render = render_log()
print(render) print(render)

View file

@ -1,7 +1,7 @@
# encoding=utf-8 # encoding=utf-8
import io import io
from time import time from time import sleep
import pytest import pytest
@ -143,8 +143,10 @@ def make_progress() -> Progress:
def render_progress() -> str: def render_progress() -> str:
progress = make_progress() progress = make_progress()
progress.start() # superfluous noop
with progress: with progress:
pass pass
progress.stop() # superfluous noop
progress_render = progress.console.file.getvalue() progress_render = progress.console.file.getvalue()
return progress_render return progress_render
@ -328,9 +330,20 @@ def test_progress_create() -> None:
def test_refresh_thread() -> None: def test_refresh_thread() -> None:
progress = Progress() class MockProgress:
thread = _RefreshThread(progress, 10) def __init__(self):
self.count = 0
def refresh(self):
self.count += 1
progress = MockProgress()
thread = _RefreshThread(progress, 100)
assert thread.progress == progress assert thread.progress == progress
thread.start()
sleep(0.2)
thread.stop()
assert progress.count >= 1
def test_track_thread() -> None: def test_track_thread() -> None:
@ -372,6 +385,42 @@ def test_reset() -> None:
assert not task._progress assert not task._progress
def test_progress_max_refresh() -> None:
"""Test max_refresh argment."""
time = 0.0
def get_time() -> float:
nonlocal time
try:
return time
finally:
time = time + 1.0
console = Console(
color_system=None, width=80, legacy_windows=False, force_terminal=True
)
column = TextColumn("{task.description}")
column.max_refresh = 3
progress = Progress(
column,
get_time=get_time,
auto_refresh=False,
console=console,
)
console.begin_capture()
with progress:
task_id = progress.add_task("start")
for tick in range(6):
progress.update(task_id, description=f"tick {tick}")
progress.refresh()
result = console.end_capture()
print(repr(result))
assert (
result
== "\x1b[?25l\r\x1b[2Kstart\r\x1b[2Kstart\r\x1b[2Ktick 1\r\x1b[2Ktick 1\r\x1b[2Ktick 3\r\x1b[2Ktick 3\r\x1b[2Ktick 5\r\x1b[2Ktick 5\n\x1b[?25h"
)
if __name__ == "__main__": if __name__ == "__main__":
_render = render_progress() _render = render_progress()
print(_render) print(_render)

View file

@ -24,6 +24,25 @@ def test_rule():
assert result == expected assert result == expected
def test_rule_error():
console = Console(width=16, file=io.StringIO(), legacy_windows=False)
with pytest.raises(ValueError):
console.rule("foo", align="foo")
def test_rule_align():
console = Console(width=16, file=io.StringIO(), legacy_windows=False)
console.rule("foo")
console.rule("foo", align="left")
console.rule("foo", align="center")
console.rule("foo", align="right")
console.rule()
result = console.file.getvalue()
print(repr(result))
expected = "───── foo ──────\nfoo ────────────\n───── foo ──────\n──────────── foo\n────────────────\n"
assert result == expected
def test_rule_cjk(): def test_rule_cjk():
console = Console( console = Console(
width=16, width=16,

42
tests/test_spinner.py Normal file
View file

@ -0,0 +1,42 @@
from time import time
import pytest
from rich.console import Console
from rich.measure import Measurement
from rich.spinner import Spinner
def test_spinner_create():
spinner = Spinner("dots")
assert spinner.time == 0.0
with pytest.raises(KeyError):
Spinner("foobar")
def test_spinner_render():
time = 0.0
def get_time():
nonlocal time
return time
console = Console(
width=80, color_system=None, force_terminal=True, get_time=get_time
)
console.begin_capture()
spinner = Spinner("dots", "Foo")
console.print(spinner)
time += 80 / 1000
console.print(spinner)
result = console.end_capture()
print(repr(result))
expected = "⠋ Foo\n⠙ Foo\n"
assert result == expected
def test_rich_measure():
console = Console(width=80, color_system=None, force_terminal=True)
spinner = Spinner("dots", "Foo")
min_width, max_width = Measurement.get(console, spinner, 80)
assert min_width == 3
assert max_width == 5

21
tests/test_status.py Normal file
View file

@ -0,0 +1,21 @@
from time import sleep
from rich.console import Console
from rich.status import Status
from rich.table import Table
def test_status():
console = Console(
color_system=None, width=80, legacy_windows=False, get_time=lambda: 0.0
)
status = Status("foo", console=console)
assert status.console == console
status.update(status="bar", spinner="dots2", spinner_style="red", speed=2.0)
assert isinstance(status.renderable, Table)
# TODO: Testing output is tricky with threads
with status:
sleep(0.2)

View file

@ -451,6 +451,14 @@ def test_render():
assert output == expected assert output == expected
def test_render_simple():
console = Console(width=80)
console.begin_capture()
console.print(Text("foo"))
result = console.end_capture()
assert result == "foo\n"
@pytest.mark.parametrize( @pytest.mark.parametrize(
"print_text,result", "print_text,result",
[ [
@ -636,3 +644,18 @@ foo = [
print(repr(result.plain)) print(repr(result.plain))
expected = "for a in range(10):\n│ print(a)\n\nfoo = [\n│ 1,\n{\n│ │ 2\n│ }\n]\n" expected = "for a in range(10):\n│ print(a)\n\nfoo = [\n│ 1,\n{\n│ │ 2\n│ }\n]\n"
assert result.plain == expected assert result.plain == expected
def test_slice():
text = Text.from_markup("[red]foo [bold]bar[/red] baz[/bold]")
assert text[0] == Text("f", spans=[Span(0, 1, "red")])
assert text[4] == Text("b", spans=[Span(0, 1, "red"), Span(0, 1, "bold")])
assert text[:3] == Text("foo", spans=[Span(0, 3, "red")])
assert text[:4] == Text("foo ", spans=[Span(0, 4, "red")])
assert text[:5] == Text("foo b", spans=[Span(0, 5, "red"), Span(4, 5, "bold")])
assert text[4:] == Text("bar baz", spans=[Span(0, 3, "red"), Span(0, 7, "bold")])
with pytest.raises(TypeError):
text[::-1]