mirror of
https://github.com/Textualize/rich.git
synced 2025-08-19 01:40:37 +00:00
Merge branch 'master' into master
This commit is contained in:
commit
02741b795d
57 changed files with 3047 additions and 282 deletions
22
.github/workflows/pythonpackage.yml
vendored
22
.github/workflows/pythonpackage.yml
vendored
|
@ -9,7 +9,9 @@ jobs:
|
|||
matrix:
|
||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||
python-version: [3.6, 3.7, 3.8, 3.9]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
|
@ -17,22 +19,22 @@ jobs:
|
|||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
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
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements-dev.txt
|
||||
poetry install
|
||||
run: poetry install
|
||||
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
|
||||
- name: Format check with black
|
||||
run: |
|
||||
make format-check
|
||||
run: make format-check
|
||||
- name: Typecheck with mypy
|
||||
run: |
|
||||
make typecheck
|
||||
run: make typecheck
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pip install .
|
||||
python -m pytest tests -v --cov=./rich --cov-report=xml:./coverage.xml --cov-report term-missing
|
||||
|
||||
- name: Upload code coverage
|
||||
uses: codecov/codecov-action@v1.0.10
|
||||
with:
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -6,6 +6,7 @@ mypy_report
|
|||
docs/build
|
||||
docs/source/_build
|
||||
tools/*.txt
|
||||
playground/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
|
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -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/),
|
||||
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
|
||||
|
||||
|
@ -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 rich.abc.RichRenderable
|
||||
- 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
|
||||
|
||||
|
@ -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 broken expanded tuple of one https://github.com/willmcgugan/rich/issues/445
|
||||
- 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
|
||||
|
||||
|
|
|
@ -1,13 +1,24 @@
|
|||
# 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
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
|
@ -29,7 +40,8 @@ New code should ideally have tests and not break existing tests.
|
|||
|
||||
### 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
|
||||
|
|
|
@ -7,3 +7,4 @@ The following people have contributed to the development of Rich:
|
|||
- [Oleksis Fraga](https://github.com/oleksis)
|
||||
- [Hedy Li](https://github.com/hedythedev)
|
||||
- [Will McGugan](https://github.com/willmcgugan)
|
||||
- [Nathan Page](https://github.com/nathanrpage97)
|
||||
|
|
|
@ -205,6 +205,7 @@ Rich 可以将内容通过排列整齐的,具有相等或最佳的宽度的[
|
|||
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
|
||||
from rich import print
|
||||
from rich.columns import Columns
|
||||
|
|
|
@ -219,6 +219,7 @@ Rich puede representar contenido en [columnas](https://rich.readthedocs.io/en/la
|
|||
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
|
||||
from rich import print
|
||||
from rich.columns import Columns
|
||||
|
|
29
README.md
29
README.md
|
@ -31,6 +31,12 @@ Install with `pip` or your favorite PyPi package manager.
|
|||
pip install rich
|
||||
```
|
||||
|
||||
Run the following to test Rich output on your terminal:
|
||||
|
||||
```
|
||||
python -m rich
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
@ -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.
|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
import os
|
||||
import sys
|
||||
|
||||
from rich import print
|
||||
from rich.columns import Columns
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
||||
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
|
||||
----------------
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ Welcome to Rich's documentation!
|
|||
panel.rst
|
||||
group.rst
|
||||
columns.rst
|
||||
live.rst
|
||||
progress.rst
|
||||
markdown.rst
|
||||
syntax.rst
|
||||
|
|
172
docs/source/live.rst
Normal file
172
docs/source/live.rst
Normal 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.
|
|
@ -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.DownloadColumn` Displays download progress (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.
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ Reference
|
|||
reference/emoji.rst
|
||||
reference/highlighter.rst
|
||||
reference/init.rst
|
||||
reference/live.rst
|
||||
reference/logging.rst
|
||||
reference/markdown.rst
|
||||
reference/markup.rst
|
||||
|
@ -25,6 +26,8 @@ Reference
|
|||
reference/protocol.rst
|
||||
reference/rule.rst
|
||||
reference/segment.rst
|
||||
reference/spinner.rst
|
||||
reference/status.rst
|
||||
reference/style.rst
|
||||
reference/styled.rst
|
||||
reference/syntax.rst
|
||||
|
|
5
docs/source/reference/live.rst
Normal file
5
docs/source/reference/live.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
rich.live
|
||||
=========
|
||||
|
||||
.. automodule:: rich.live
|
||||
:members:
|
5
docs/source/reference/spinner.rst
Normal file
5
docs/source/reference/spinner.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
rich.spinner
|
||||
============
|
||||
|
||||
.. automodule:: rich.spinner
|
||||
:members:
|
5
docs/source/reference/status.rst
Normal file
5
docs/source/reference/status.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
rich.status
|
||||
============
|
||||
|
||||
.. automodule:: rich.status
|
||||
:members:
|
13
examples/status.py
Normal file
13
examples/status.py
Normal 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")
|
|
@ -1,12 +1,16 @@
|
|||
"""Same as the table_movie.py but uses Live to update"""
|
||||
from contextlib import contextmanager
|
||||
import time
|
||||
|
||||
from rich.console import Console
|
||||
from rich.columns import Columns
|
||||
from rich.table import Table
|
||||
from rich.measure import Measurement
|
||||
from rich import box
|
||||
from rich.text import Text
|
||||
|
||||
from rich.live import Live
|
||||
|
||||
TABLE_DATA = [
|
||||
[
|
||||
"May 25, 1977",
|
||||
|
@ -60,136 +64,110 @@ BEAT_TIME = 0.04
|
|||
@contextmanager
|
||||
def beat(length: int = 1) -> None:
|
||||
with console:
|
||||
console.clear()
|
||||
yield
|
||||
time.sleep(length * BEAT_TIME)
|
||||
|
||||
|
||||
table = Table(show_footer=False)
|
||||
table_centered = Columns((table,), align="center", expand=True)
|
||||
|
||||
|
||||
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)
|
||||
with beat(10):
|
||||
console.print(table, justify="center")
|
||||
|
||||
with beat(10):
|
||||
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)
|
||||
with beat(10):
|
||||
console.print(table, justify="center")
|
||||
|
||||
with beat(10):
|
||||
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)
|
||||
with beat(10):
|
||||
console.print(table, justify="center")
|
||||
|
||||
with beat(10):
|
||||
table.title = "Star Wars Box Office"
|
||||
with beat(10):
|
||||
console.print(table, justify="center")
|
||||
|
||||
with beat(10):
|
||||
table.title = (
|
||||
"[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"
|
||||
with beat(10):
|
||||
console.print(table, justify="center")
|
||||
|
||||
with beat(10):
|
||||
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):
|
||||
console.print(table, justify="center")
|
||||
table.caption = "Made with [b magenta not dim]Rich[/]"
|
||||
|
||||
for row in TABLE_DATA:
|
||||
with beat(10):
|
||||
table.add_row(*row)
|
||||
with beat(10):
|
||||
console.print(table, justify="center")
|
||||
|
||||
table.show_footer = True
|
||||
with beat(10):
|
||||
console.print(table, justify="center")
|
||||
table.show_footer = True
|
||||
|
||||
table_width = Measurement.get(console, table, console.width).maximum
|
||||
|
||||
with beat(10):
|
||||
table.columns[2].justify = "right"
|
||||
with beat(10):
|
||||
console.print(table, justify="center")
|
||||
|
||||
with beat(10):
|
||||
table.columns[3].justify = "right"
|
||||
with beat(10):
|
||||
console.print(table, justify="center")
|
||||
|
||||
with beat(10):
|
||||
table.columns[4].justify = "right"
|
||||
with beat(10):
|
||||
console.print(table, justify="center")
|
||||
|
||||
with beat(10):
|
||||
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"
|
||||
with beat(10):
|
||||
console.print(table, justify="center")
|
||||
|
||||
with beat(10):
|
||||
table.columns[4].header_style = "bold blue"
|
||||
with beat(10):
|
||||
console.print(table, justify="center")
|
||||
|
||||
with beat(10):
|
||||
table.columns[2].style = "red"
|
||||
with beat(10):
|
||||
console.print(table, justify="center")
|
||||
|
||||
with beat(10):
|
||||
table.columns[3].style = "green"
|
||||
with beat(10):
|
||||
console.print(table, justify="center")
|
||||
|
||||
with beat(10):
|
||||
table.columns[4].style = "blue"
|
||||
with beat(10):
|
||||
console.print(table, justify="center")
|
||||
|
||||
with beat(10):
|
||||
table.columns[0].style = "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].header_style = "bold magenta"
|
||||
with beat(10):
|
||||
console.print(table, justify="center")
|
||||
|
||||
with beat(10):
|
||||
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"
|
||||
with beat(10):
|
||||
console.print(table, justify="center")
|
||||
|
||||
with beat(10):
|
||||
table.columns[4].footer_style = "bright_blue"
|
||||
with beat(10):
|
||||
console.print(table, justify="center")
|
||||
|
||||
with beat(10):
|
||||
table.row_styles = ["none", "dim"]
|
||||
with beat(10):
|
||||
console.print(table, justify="center")
|
||||
|
||||
table.border_style = "bright_yellow"
|
||||
with beat(10):
|
||||
console.print(table, justify="center")
|
||||
table.border_style = "bright_yellow"
|
||||
|
||||
for box in [
|
||||
box.SQUARE,
|
||||
|
@ -197,39 +175,29 @@ try:
|
|||
box.SIMPLE,
|
||||
box.SIMPLE_HEAD,
|
||||
]:
|
||||
with beat(10):
|
||||
table.box = box
|
||||
with beat(10):
|
||||
console.print(table, justify="center")
|
||||
|
||||
table.pad_edge = False
|
||||
with beat(10):
|
||||
console.print(table, justify="center")
|
||||
table.pad_edge = False
|
||||
|
||||
original_width = Measurement.get(console, table).maximum
|
||||
|
||||
for width in range(original_width, console.width, 2):
|
||||
table.width = width
|
||||
with beat(2):
|
||||
console.print(table, justify="center")
|
||||
table.width = width
|
||||
|
||||
for width in range(console.width, original_width, -2):
|
||||
table.width = width
|
||||
with beat(2):
|
||||
console.print(table, justify="center")
|
||||
table.width = width
|
||||
|
||||
for width in range(original_width, 90, -2):
|
||||
table.width = width
|
||||
with beat(2):
|
||||
console.print(table, justify="center")
|
||||
table.width = width
|
||||
|
||||
for width in range(90, original_width + 1, 2):
|
||||
with beat(2):
|
||||
table.width = width
|
||||
with beat(2):
|
||||
console.print(table, justify="center")
|
||||
|
||||
with beat(2):
|
||||
table.width = None
|
||||
with beat(2):
|
||||
console.print(table, justify="center")
|
||||
|
||||
finally:
|
||||
console.show_cursor(True)
|
||||
|
|
81
examples/top_lite_simulator.py
Normal file
81
examples/top_lite_simulator.py
Normal 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 |
BIN
imgs/status.gif
Normal file
BIN
imgs/status.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 588 KiB |
376
poetry.lock
generated
376
poetry.lock
generated
|
@ -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]]
|
||||
name = "appnope"
|
||||
version = "0.1.0"
|
||||
|
@ -6,12 +14,20 @@ category = "main"
|
|||
optional = true
|
||||
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]]
|
||||
name = "attrs"
|
||||
version = "19.3.0"
|
||||
description = "Classes Without Boilerplate"
|
||||
category = "main"
|
||||
optional = true
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[package.extras]
|
||||
|
@ -28,6 +44,29 @@ category = "main"
|
|||
optional = true
|
||||
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]]
|
||||
name = "bleach"
|
||||
version = "3.1.5"
|
||||
|
@ -41,6 +80,14 @@ packaging = "*"
|
|||
six = ">=1.9.0"
|
||||
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]]
|
||||
name = "colorama"
|
||||
version = "0.4.4"
|
||||
|
@ -60,6 +107,17 @@ python-versions = "*"
|
|||
[package.extras]
|
||||
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]]
|
||||
name = "dataclasses"
|
||||
version = "0.8"
|
||||
|
@ -97,7 +155,7 @@ name = "importlib-metadata"
|
|||
version = "1.7.0"
|
||||
description = "Read metadata from Python packages"
|
||||
category = "main"
|
||||
optional = true
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -107,6 +165,14 @@ zipp = ">=0.5"
|
|||
docs = ["sphinx", "rst.linker"]
|
||||
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]]
|
||||
name = "ipykernel"
|
||||
version = "5.3.2"
|
||||
|
@ -275,6 +341,30 @@ category = "main"
|
|||
optional = true
|
||||
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]]
|
||||
name = "nbconvert"
|
||||
version = "5.6.1"
|
||||
|
@ -351,7 +441,7 @@ name = "packaging"
|
|||
version = "20.4"
|
||||
description = "Core utilities for Python packages"
|
||||
category = "main"
|
||||
optional = true
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -377,6 +467,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
|||
[package.extras]
|
||||
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]]
|
||||
name = "pexpect"
|
||||
version = "4.8.0"
|
||||
|
@ -396,6 +494,20 @@ category = "main"
|
|||
optional = true
|
||||
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]]
|
||||
name = "prometheus-client"
|
||||
version = "0.8.0"
|
||||
|
@ -426,9 +538,17 @@ category = "main"
|
|||
optional = true
|
||||
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]]
|
||||
name = "pygments"
|
||||
version = "2.7.2"
|
||||
version = "2.7.3"
|
||||
description = "Pygments is a syntax highlighting package written in Python."
|
||||
category = "main"
|
||||
optional = false
|
||||
|
@ -439,7 +559,7 @@ name = "pyparsing"
|
|||
version = "2.4.7"
|
||||
description = "Python parsing module"
|
||||
category = "main"
|
||||
optional = true
|
||||
optional = false
|
||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
|
@ -453,6 +573,44 @@ python-versions = "*"
|
|||
[package.dependencies]
|
||||
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]]
|
||||
name = "python-dateutil"
|
||||
version = "2.8.1"
|
||||
|
@ -488,6 +646,14 @@ category = "main"
|
|||
optional = true
|
||||
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]]
|
||||
name = "send2trash"
|
||||
version = "1.5.0"
|
||||
|
@ -501,7 +667,7 @@ name = "six"
|
|||
version = "1.15.0"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
category = "main"
|
||||
optional = true
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
|
@ -528,6 +694,14 @@ python-versions = "*"
|
|||
[package.extras]
|
||||
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]]
|
||||
name = "tornado"
|
||||
version = "6.0.4"
|
||||
|
@ -552,6 +726,14 @@ six = "*"
|
|||
[package.extras]
|
||||
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]]
|
||||
name = "typing-extensions"
|
||||
version = "3.7.4.3"
|
||||
|
@ -592,7 +774,7 @@ name = "zipp"
|
|||
version = "3.1.0"
|
||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||
category = "main"
|
||||
optional = true
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.extras]
|
||||
|
@ -605,13 +787,21 @@ jupyter = ["ipywidgets"]
|
|||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.6"
|
||||
content-hash = "2d2f8de1abd0be719c813e5c47d7b48620c01de464894ab995485e85fb804454"
|
||||
content-hash = "2aac9442e8c6265a9815532d55ada0835532f15a507a6c96445c5f8937f7d3f7"
|
||||
|
||||
[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 = [
|
||||
{file = "appnope-0.1.0-py2.py3-none-any.whl", hash = "sha256:5b26757dc6f79a3b7dc9fab95359328d5747fcb2409d331ea66d0272b90ab2a0"},
|
||||
{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 = [
|
||||
{file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"},
|
||||
{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.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
|
||||
]
|
||||
black = [
|
||||
{file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
|
||||
]
|
||||
bleach = [
|
||||
{file = "bleach-3.1.5-py2.py3-none-any.whl", hash = "sha256:2bce3d8fab545a6528c8fa5d9f9ae8ebc85a56da365c7f85180bfe96a35ef22f"},
|
||||
{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 = [
|
||||
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
|
||||
{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.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 = [
|
||||
{file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"},
|
||||
{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.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 = [
|
||||
{file = "ipykernel-5.3.2-py3-none-any.whl", hash = "sha256:0a5f1fc6f63241b9710b5960d314ffe44d8a18bf6674e3f28d2542b192fa318c"},
|
||||
{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.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 = [
|
||||
{file = "nbconvert-5.6.1-py2.py3-none-any.whl", hash = "sha256:f0d6ec03875f96df45aa13e21fd9b8450c42d7e1830418cccc008c0df725fcee"},
|
||||
{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.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 = [
|
||||
{file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
|
||||
{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.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 = [
|
||||
{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"},
|
||||
|
@ -770,9 +1035,13 @@ ptyprocess = [
|
|||
{file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"},
|
||||
{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 = [
|
||||
{file = "Pygments-2.7.2-py3-none-any.whl", hash = "sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773"},
|
||||
{file = "Pygments-2.7.2.tar.gz", hash = "sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0"},
|
||||
{file = "Pygments-2.7.3-py3-none-any.whl", hash = "sha256:f275b6c0909e5dafd2d6269a656aa90fa58ebf4a74f8fcf9053195d226b24a08"},
|
||||
{file = "Pygments-2.7.3.tar.gz", hash = "sha256:ccf3acacf3782cbed4a989426012f1c535c9a90d3a7fc3f16d231b9372d2b716"},
|
||||
]
|
||||
pyparsing = [
|
||||
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
|
||||
|
@ -781,6 +1050,14 @@ pyparsing = [
|
|||
pyrsistent = [
|
||||
{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 = [
|
||||
{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"},
|
||||
|
@ -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.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 = [
|
||||
{file = "Send2Trash-1.5.0-py3-none-any.whl", hash = "sha256:f1691922577b6fa12821234aeb57599d887c4900b9ca537948d2dac34aea888b"},
|
||||
{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.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 = [
|
||||
{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"},
|
||||
|
@ -872,6 +1196,38 @@ traitlets = [
|
|||
{file = "traitlets-4.3.3-py2.py3-none-any.whl", hash = "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44"},
|
||||
{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 = [
|
||||
{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"},
|
||||
|
|
|
@ -36,6 +36,10 @@ ipywidgets = {version = "^7.5.1", optional = true}
|
|||
jupyter = ["ipywidgets"]
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^6.1.2"
|
||||
black = "^20.8b1"
|
||||
mypy = "^0.790"
|
||||
pytest-cov = "^2.10.1"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
black==20.8b1
|
||||
mypy==0.790
|
||||
poetry==1.1.4
|
||||
pytest==6.1.2
|
||||
pytest-cov==2.10.1
|
|
@ -26,10 +26,11 @@ class ColorBox:
|
|||
for x in range(options.max_width):
|
||||
h = x / options.max_width
|
||||
l = 0.1 + ((y / 5) * 0.7)
|
||||
r, g, b = colorsys.hls_to_rgb(h, l, 1.0)
|
||||
yield Segment(
|
||||
"█", Style(color=Color.from_rgb(r * 255, g * 255, b * 255))
|
||||
)
|
||||
r1, g1, b1 = colorsys.hls_to_rgb(h, l, 1.0)
|
||||
r2, g2, b2 = colorsys.hls_to_rgb(h, l + 0.7 / 10, 1.0)
|
||||
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()
|
||||
|
||||
def __rich_measure__(self, console: "Console", max_width: int) -> Measurement:
|
||||
|
@ -96,7 +97,7 @@ def make_test_card() -> Table:
|
|||
return table
|
||||
|
||||
table.add_row(
|
||||
"Asian languages",
|
||||
"Asian\nlanguage\nsupport",
|
||||
":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! "
|
||||
":+1: :apple: :ant: :bear: :baguette_bread: :bus: "
|
||||
)
|
||||
table.add_row("Console markup", markup_example)
|
||||
table.add_row("Markup", markup_example)
|
||||
|
||||
example_table = Table(
|
||||
show_edge=False,
|
||||
|
@ -201,7 +202,7 @@ Supports much of the *markdown*, __syntax__!
|
|||
)
|
||||
|
||||
table.add_row(
|
||||
"And more",
|
||||
"+more!",
|
||||
"""Progress bars, columns, styled logging handler, tracebacks, etc...""",
|
||||
)
|
||||
return table
|
||||
|
|
|
@ -72,3 +72,11 @@ class LogRender:
|
|||
|
||||
output.add_row(*row)
|
||||
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
848
rich/_spinners.py
Normal 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": [
|
||||
"▰▱▱▱▱▱▱",
|
||||
"▰▰▱▱▱▱▱",
|
||||
"▰▰▰▱▱▱▱",
|
||||
"▰▰▰▰▱▱▱",
|
||||
"▰▰▰▰▰▱▱",
|
||||
"▰▰▰▰▰▰▱",
|
||||
"▰▰▰▰▰▰▰",
|
||||
"▰▱▱▱▱▱▱",
|
||||
],
|
||||
},
|
||||
}
|
|
@ -12,6 +12,7 @@ if TYPE_CHECKING:
|
|||
from .console import Console, ConsoleOptions, RenderResult, RenderableType
|
||||
|
||||
AlignValues = Literal["left", "center", "right"]
|
||||
AlignMethod = AlignValues # TODO: deprecate AlignValues
|
||||
|
||||
|
||||
class Align(JupyterMixin):
|
||||
|
@ -19,7 +20,7 @@ class Align(JupyterMixin):
|
|||
|
||||
Args:
|
||||
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.
|
||||
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.
|
||||
|
@ -31,7 +32,7 @@ class Align(JupyterMixin):
|
|||
def __init__(
|
||||
self,
|
||||
renderable: "RenderableType",
|
||||
align: AlignValues,
|
||||
align: AlignMethod,
|
||||
style: StyleType = None,
|
||||
*,
|
||||
pad: bool = True,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Optional, Union
|
||||
from typing import Union
|
||||
|
||||
from .color import Color
|
||||
from .console import Console, ConsoleOptions, RenderResult
|
||||
|
|
|
@ -3,7 +3,7 @@ from itertools import chain
|
|||
from operator import itemgetter
|
||||
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 .constrain import Constrain
|
||||
from .measure import Measurement
|
||||
|
@ -38,7 +38,7 @@ class Columns(JupyterMixin):
|
|||
equal: bool = False,
|
||||
column_first: bool = False,
|
||||
right_to_left: bool = False,
|
||||
align: AlignValues = None,
|
||||
align: AlignMethod = None,
|
||||
title: TextType = None,
|
||||
) -> None:
|
||||
self.renderables = list(renderables or [])
|
||||
|
|
|
@ -31,7 +31,7 @@ from typing_extensions import Literal, Protocol, runtime_checkable
|
|||
from . import errors, themes
|
||||
from ._emoji_replace import _emoji_replace
|
||||
from ._log_render import LogRender
|
||||
from .align import Align, AlignValues
|
||||
from .align import Align, AlignMethod
|
||||
from .color import ColorSystem
|
||||
from .control import Control
|
||||
from .highlighter import NullHighlighter, ReprHighlighter
|
||||
|
@ -42,11 +42,13 @@ from .pretty import Pretty
|
|||
from .scope import render_scope
|
||||
from .segment import Segment
|
||||
from .style import Style
|
||||
from .styled import Styled
|
||||
from .terminal_theme import DEFAULT_TERMINAL_THEME, TerminalTheme
|
||||
from .text import Text, TextType
|
||||
from .theme import Theme, ThemeStack
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .status import Status
|
||||
from ._windows import WindowsConsoleFeatures
|
||||
|
||||
WINDOWS = platform.system() == "Windows"
|
||||
|
@ -758,6 +760,39 @@ class Console:
|
|||
"""
|
||||
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:
|
||||
"""Show or hide the cursor.
|
||||
|
||||
|
@ -946,7 +981,7 @@ class Console:
|
|||
highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default.
|
||||
|
||||
Returns:
|
||||
List[ConsoleRenderable]: A list of things to render.
|
||||
List[ConsoleRenderable]: A list oxf things to render.
|
||||
"""
|
||||
renderables: List[ConsoleRenderable] = []
|
||||
_append = renderables.append
|
||||
|
@ -957,7 +992,7 @@ class Console:
|
|||
if justify in ("left", "center", "right"):
|
||||
|
||||
def align_append(renderable: RenderableType) -> None:
|
||||
_append(Align(renderable, cast(AlignValues, justify)))
|
||||
_append(Align(renderable, cast(AlignMethod, justify)))
|
||||
|
||||
append = align_append
|
||||
|
||||
|
@ -967,7 +1002,7 @@ class Console:
|
|||
|
||||
def check_text() -> None:
|
||||
if text:
|
||||
sep_text = Text(sep, end=end)
|
||||
sep_text = Text(sep, justify=justify, end=end)
|
||||
append(sep_text.join(text))
|
||||
del text[:]
|
||||
|
||||
|
@ -1003,16 +1038,19 @@ class Console:
|
|||
*,
|
||||
characters: str = "─",
|
||||
style: Union[str, Style] = "rule.line",
|
||||
align: AlignMethod = "center",
|
||||
) -> None:
|
||||
"""Draw a line with optional centered title.
|
||||
|
||||
Args:
|
||||
title (str, optional): Text to render over the rule. 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
|
||||
|
||||
rule = Rule(title=title, characters=characters, style=style)
|
||||
rule = Rule(title=title, characters=characters, style=style, align=align)
|
||||
self.print(rule)
|
||||
|
||||
def control(self, control_codes: Union["Control", str]) -> None:
|
||||
|
@ -1172,6 +1210,7 @@ class Console:
|
|||
*objects: Any,
|
||||
sep=" ",
|
||||
end="\n",
|
||||
style: Union[str, Style] = None,
|
||||
justify: JustifyMethod = None,
|
||||
emoji: bool = None,
|
||||
markup: bool = None,
|
||||
|
@ -1185,6 +1224,7 @@ class Console:
|
|||
objects (positional args): Objects to log to the terminal.
|
||||
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".
|
||||
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``.
|
||||
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.
|
||||
|
@ -1207,6 +1247,8 @@ class Console:
|
|||
markup=markup,
|
||||
highlight=highlight,
|
||||
)
|
||||
if style is not None:
|
||||
renderables = [Styled(renderable, style) for renderable in renderables]
|
||||
|
||||
caller = inspect.stack()[_stack_offset]
|
||||
link_path = (
|
||||
|
|
|
@ -8,8 +8,6 @@ from typing import (
|
|||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
from .style import Style
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .console import (
|
||||
Console,
|
||||
|
|
|
@ -44,6 +44,7 @@ DEFAULT_STYLES: Dict[str, Style] = {
|
|||
"inspect.equals": Style(),
|
||||
"inspect.help": Style(color="cyan"),
|
||||
"inspect.doc": Style(dim=True),
|
||||
"live.ellipsis": Style(bold=True, color="red"),
|
||||
"logging.keyword": Style(bold=True, color="yellow"),
|
||||
"logging.level.notset": Style(dim=True),
|
||||
"logging.level.debug": Style(color="green"),
|
||||
|
@ -115,6 +116,8 @@ DEFAULT_STYLES: Dict[str, Style] = {
|
|||
"progress.percentage": Style(color="magenta"),
|
||||
"progress.remaining": Style(color="cyan"),
|
||||
"progress.data.speed": Style(color="red"),
|
||||
"progress.spinner": Style(color="green"),
|
||||
"status.spinner": Style(color="green"),
|
||||
}
|
||||
|
||||
MARKDOWN_STYLES = {
|
||||
|
|
47
rich/file_proxy.py
Normal file
47
rich/file_proxy.py
Normal 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[:]
|
|
@ -1,7 +1,6 @@
|
|||
from typing import Iterable, List, TYPE_CHECKING
|
||||
|
||||
# from .console import Console as BaseConsole
|
||||
from .__init__ import get_console
|
||||
from . import get_console
|
||||
from .segment import Segment
|
||||
from .terminal_theme import DEFAULT_TERMINAL_THEME
|
||||
|
||||
|
|
376
rich/live.py
Normal file
376
rich/live.py
Normal 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)
|
|
@ -2,7 +2,7 @@ import logging
|
|||
from datetime import datetime
|
||||
from logging import Handler, LogRecord
|
||||
from pathlib import Path
|
||||
from typing import ClassVar, List, Optional, Type
|
||||
from typing import ClassVar, List, Optional, Type, Union
|
||||
|
||||
from . import get_console
|
||||
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.
|
||||
|
||||
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.
|
||||
Default will use a global console instance writing to stdout.
|
||||
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_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.
|
||||
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]]] = [
|
||||
|
@ -52,7 +55,7 @@ class RichHandler(Handler):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
level: int = logging.NOTSET,
|
||||
level: Union[int, str] = logging.NOTSET,
|
||||
console: Console = None,
|
||||
*,
|
||||
show_time: bool = True,
|
||||
|
@ -67,6 +70,8 @@ class RichHandler(Handler):
|
|||
tracebacks_theme: Optional[str] = None,
|
||||
tracebacks_word_wrap: bool = True,
|
||||
tracebacks_show_locals: bool = False,
|
||||
locals_max_length: int = 10,
|
||||
locals_max_string: int = 80,
|
||||
) -> None:
|
||||
super().__init__(level=level)
|
||||
self.console = console or get_console()
|
||||
|
@ -85,6 +90,8 @@ class RichHandler(Handler):
|
|||
self.tracebacks_theme = tracebacks_theme
|
||||
self.tracebacks_word_wrap = tracebacks_word_wrap
|
||||
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:
|
||||
"""Get the level name from the record.
|
||||
|
@ -127,6 +134,8 @@ class RichHandler(Handler):
|
|||
theme=self.tracebacks_theme,
|
||||
word_wrap=self.tracebacks_word_wrap,
|
||||
show_locals=self.tracebacks_show_locals,
|
||||
locals_max_length=self.locals_max_length,
|
||||
locals_max_string=self.locals_max_string,
|
||||
)
|
||||
message = record.getMessage()
|
||||
|
||||
|
@ -201,6 +210,8 @@ if __name__ == "__main__": # pragma: no cover
|
|||
def divide():
|
||||
number = 1
|
||||
divisor = 0
|
||||
foos = ["foo"] * 100
|
||||
log.debug("in divide")
|
||||
try:
|
||||
number / divisor
|
||||
except:
|
||||
|
|
|
@ -2,7 +2,7 @@ from typing import Optional, TYPE_CHECKING
|
|||
|
||||
from .box import Box, ROUNDED
|
||||
|
||||
from .align import AlignValues
|
||||
from .align import AlignMethod
|
||||
from .jupyter import JupyterMixin
|
||||
from .measure import Measurement, measure_renderables
|
||||
from .padding import Padding, PaddingDimensions
|
||||
|
@ -39,7 +39,7 @@ class Panel(JupyterMixin):
|
|||
box: Box = ROUNDED,
|
||||
*,
|
||||
title: TextType = None,
|
||||
title_align: AlignValues = "center",
|
||||
title_align: AlignMethod = "center",
|
||||
safe_box: Optional[bool] = None,
|
||||
expand: bool = True,
|
||||
style: StyleType = "none",
|
||||
|
@ -65,7 +65,7 @@ class Panel(JupyterMixin):
|
|||
box: Box = ROUNDED,
|
||||
*,
|
||||
title: TextType = None,
|
||||
title_align: AlignValues = "center",
|
||||
title_align: AlignMethod = "center",
|
||||
safe_box: Optional[bool] = None,
|
||||
style: StyleType = "none",
|
||||
border_style: StyleType = "none",
|
||||
|
|
|
@ -20,7 +20,7 @@ from typing import (
|
|||
from rich.highlighter import ReprHighlighter
|
||||
|
||||
from .abc import RichRenderable
|
||||
from .__init__ import get_console
|
||||
from . import get_console
|
||||
from ._pick import pick_bool
|
||||
from .cells import cell_len
|
||||
from .highlighter import ReprHighlighter
|
||||
|
@ -44,6 +44,7 @@ def install(
|
|||
crop: bool = False,
|
||||
indent_guides: bool = False,
|
||||
max_length: int = None,
|
||||
max_string: int = None,
|
||||
expand_all: bool = False,
|
||||
) -> None:
|
||||
"""Install automatic pretty printing in the Python REPL.
|
||||
|
@ -55,6 +56,7 @@ def install(
|
|||
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.
|
||||
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
|
||||
"""
|
||||
from rich import get_console
|
||||
|
@ -74,6 +76,7 @@ def install(
|
|||
overflow=overflow,
|
||||
indent_guides=indent_guides,
|
||||
max_length=max_length,
|
||||
max_string=max_string,
|
||||
expand_all=expand_all,
|
||||
),
|
||||
crop=crop,
|
||||
|
@ -152,7 +155,11 @@ class Pretty:
|
|||
|
||||
def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement":
|
||||
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())
|
||||
return Measurement(text_width, text_width)
|
||||
|
|
140
rich/progress.py
140
rich/progress.py
|
@ -1,4 +1,3 @@
|
|||
import io
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import deque
|
||||
|
@ -7,10 +6,8 @@ from dataclasses import dataclass, field
|
|||
from datetime import timedelta
|
||||
from math import ceil
|
||||
from threading import Event, RLock, Thread
|
||||
from time import monotonic
|
||||
from typing import (
|
||||
IO,
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Deque,
|
||||
|
@ -27,7 +24,6 @@ from typing import (
|
|||
)
|
||||
|
||||
from . import filesize, get_console
|
||||
from .ansi import AnsiDecoder
|
||||
from .console import (
|
||||
Console,
|
||||
ConsoleRenderable,
|
||||
|
@ -37,13 +33,15 @@ from .console import (
|
|||
RenderHook,
|
||||
)
|
||||
from .control import Control
|
||||
from .file_proxy import FileProxy
|
||||
from .highlighter import Highlighter
|
||||
from .jupyter import JupyterMixin
|
||||
from .live_render import LiveRender
|
||||
from .progress_bar import ProgressBar
|
||||
from .spinner import Spinner
|
||||
from .style import StyleType
|
||||
from .table import Table
|
||||
from .text import Text
|
||||
from .text import Text, TextType
|
||||
|
||||
TaskID = NewType("TaskID", int)
|
||||
|
||||
|
@ -95,12 +93,13 @@ def track(
|
|||
console: Optional[Console] = None,
|
||||
transient: bool = False,
|
||||
get_time: Callable[[], float] = None,
|
||||
refresh_per_second: int = None,
|
||||
refresh_per_second: float = None,
|
||||
style: StyleType = "bar.back",
|
||||
complete_style: StyleType = "bar.complete",
|
||||
finished_style: StyleType = "bar.finished",
|
||||
pulse_style: StyleType = "bar.pulse",
|
||||
update_period: float = 0.1,
|
||||
disable: bool = False,
|
||||
) -> Iterable[ProgressType]:
|
||||
"""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.
|
||||
transient: (bool, optional): Clear the progress on exit. Defaults to False.
|
||||
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".
|
||||
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".
|
||||
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.
|
||||
disable (bool, optional): Disable display of progress.
|
||||
Returns:
|
||||
Iterable[ProgressType]: An iterable of the values in the sequence.
|
||||
|
||||
|
@ -144,6 +144,7 @@ def track(
|
|||
transient=transient,
|
||||
get_time=get_time,
|
||||
refresh_per_second=refresh_per_second,
|
||||
disable=disable,
|
||||
)
|
||||
|
||||
with progress:
|
||||
|
@ -189,6 +190,68 @@ class ProgressColumn(ABC):
|
|||
"""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):
|
||||
"""A column containing text."""
|
||||
|
||||
|
@ -460,7 +523,7 @@ class Task:
|
|||
class _RefreshThread(Thread):
|
||||
"""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.refresh_per_second = refresh_per_second
|
||||
self.done = Event()
|
||||
|
@ -474,57 +537,19 @@ class _RefreshThread(Thread):
|
|||
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):
|
||||
"""Renders an auto-updating progress bar(s).
|
||||
|
||||
Args:
|
||||
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()`.
|
||||
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.
|
||||
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_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.
|
||||
disable (bool, optional): Disable progress display. Defaults to False
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
@ -532,12 +557,13 @@ class Progress(JupyterMixin, RenderHook):
|
|||
*columns: Union[str, ProgressColumn],
|
||||
console: Console = None,
|
||||
auto_refresh: bool = True,
|
||||
refresh_per_second: int = None,
|
||||
refresh_per_second: float = None,
|
||||
speed_estimate_period: float = 30.0,
|
||||
transient: bool = False,
|
||||
redirect_stdout: bool = True,
|
||||
redirect_stderr: bool = True,
|
||||
get_time: GetTimeCallable = None,
|
||||
disable: bool = False,
|
||||
) -> None:
|
||||
assert (
|
||||
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_stderr = redirect_stderr
|
||||
self.get_time = get_time or self.console.get_time
|
||||
self.disable = disable
|
||||
self._tasks: Dict[TaskID, Task] = {}
|
||||
self._live_render = LiveRender(self.get_renderable())
|
||||
self._task_index: TaskID = TaskID(0)
|
||||
|
@ -593,10 +620,10 @@ class Progress(JupyterMixin, RenderHook):
|
|||
if self.console.is_terminal:
|
||||
if self._redirect_stdout:
|
||||
self._restore_stdout = sys.stdout
|
||||
sys.stdout = _FileProxy(self.console, 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)
|
||||
sys.stderr = FileProxy(self.console, sys.stderr)
|
||||
|
||||
def _disable_redirect_io(self):
|
||||
"""Disable redirecting of stdout / stderr."""
|
||||
|
@ -848,6 +875,7 @@ class Progress(JupyterMixin, RenderHook):
|
|||
|
||||
def refresh(self) -> None:
|
||||
"""Refresh (render) the progress information."""
|
||||
if not self.disable:
|
||||
if self.console.is_jupyter: # pragma: no cover
|
||||
try:
|
||||
from IPython.display import display
|
||||
|
@ -1023,7 +1051,15 @@ if __name__ == "__main__": # pragma: no coverage
|
|||
|
||||
console = Console(record=True)
|
||||
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)
|
||||
task2 = progress.add_task("[green]Processing", total=1000)
|
||||
|
|
|
@ -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 .text import Text, TextType
|
||||
|
||||
|
|
30
rich/rule.py
30
rich/rule.py
|
@ -1,5 +1,6 @@
|
|||
from typing import Union
|
||||
|
||||
from .align import AlignMethod
|
||||
from .cells import cell_len, set_cell_size
|
||||
from .console import Console, ConsoleOptions, RenderResult
|
||||
from .jupyter import JupyterMixin
|
||||
|
@ -15,6 +16,7 @@ class Rule(JupyterMixin):
|
|||
characters (str, optional): Character(s) used to draw the line. Defaults to "─".
|
||||
style (StyleType, optional): Style of Rule. Defaults to "rule.line".
|
||||
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__(
|
||||
|
@ -24,15 +26,21 @@ class Rule(JupyterMixin):
|
|||
characters: str = "─",
|
||||
style: Union[str, Style] = "rule.line",
|
||||
end: str = "\n",
|
||||
align: AlignMethod = "center",
|
||||
) -> None:
|
||||
if cell_len(characters) < 1:
|
||||
raise ValueError(
|
||||
"'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.characters = characters
|
||||
self.style = style
|
||||
self.end = end
|
||||
self.align = align
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Rule({self.title!r}, {self.characters!r})"
|
||||
|
@ -56,18 +64,21 @@ class Rule(JupyterMixin):
|
|||
if not self.title:
|
||||
rule_text = Text(characters * ((width // chars_len) + 1), self.style)
|
||||
rule_text.truncate(width)
|
||||
else:
|
||||
rule_text.plain = set_cell_size(rule_text.plain, width)
|
||||
yield rule_text
|
||||
return
|
||||
|
||||
if isinstance(self.title, Text):
|
||||
title_text = self.title
|
||||
else:
|
||||
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.expand_tabs()
|
||||
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
|
||||
left = Text(characters * (side_width // chars_len + 1))
|
||||
left.truncate(side_width - 1)
|
||||
|
@ -77,6 +88,17 @@ class Rule(JupyterMixin):
|
|||
rule_text.append(left.plain + " ", self.style)
|
||||
rule_text.append(title_text)
|
||||
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)
|
||||
yield rule_text
|
||||
|
||||
|
|
|
@ -16,7 +16,9 @@ def render_scope(
|
|||
*,
|
||||
title: TextType = None,
|
||||
sort_keys: bool = True,
|
||||
indent_guides: bool = False
|
||||
indent_guides: bool = False,
|
||||
max_length: int = None,
|
||||
max_string: int = None,
|
||||
) -> "ConsoleRenderable":
|
||||
"""Render python variables in a given scope.
|
||||
|
||||
|
@ -25,6 +27,9 @@ def render_scope(
|
|||
title (str, optional): Optional title. Defaults to None.
|
||||
sort_keys (bool, optional): Enable sorting of items. Defaults to True.
|
||||
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:
|
||||
RenderableType: A renderable object.
|
||||
|
@ -46,7 +51,13 @@ def render_scope(
|
|||
)
|
||||
items_table.add_row(
|
||||
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(
|
||||
items_table,
|
||||
|
|
88
rich/spinner.py
Normal file
88
rich/spinner.py
Normal 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
127
rich/status.py
Normal 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")
|
|
@ -11,7 +11,6 @@ from .padding import Padding, PaddingDimensions
|
|||
from .protocol import is_renderable
|
||||
from .segment import Segment
|
||||
from .style import Style, StyleType
|
||||
from .styled import Styled
|
||||
from .text import Text, TextType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -81,6 +80,17 @@ class Column:
|
|||
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):
|
||||
"""A single cell in a table."""
|
||||
|
||||
|
@ -122,6 +132,7 @@ class Table(JupyterMixin):
|
|||
"""
|
||||
|
||||
columns: List[Column]
|
||||
rows: List[Row]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -153,6 +164,7 @@ class Table(JupyterMixin):
|
|||
) -> None:
|
||||
|
||||
self.columns: List[Column] = []
|
||||
self.rows: List[Row] = []
|
||||
append_column = self.columns.append
|
||||
for index, header in enumerate(headers):
|
||||
if isinstance(header, str):
|
||||
|
@ -184,7 +196,6 @@ class Table(JupyterMixin):
|
|||
self.caption_style = caption_style
|
||||
self.title_justify = title_justify
|
||||
self.caption_justify = caption_justify
|
||||
self._row_count = 0
|
||||
self.row_styles = list(row_styles or [])
|
||||
|
||||
@classmethod
|
||||
|
@ -240,13 +251,17 @@ class Table(JupyterMixin):
|
|||
@property
|
||||
def row_count(self) -> int:
|
||||
"""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."""
|
||||
style = Style.null()
|
||||
if self.row_styles:
|
||||
return self.row_styles[index % len(self.row_styles)]
|
||||
return Style.null()
|
||||
style += console.get_style(self.row_styles[index % len(self.row_styles)])
|
||||
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:
|
||||
if self.width is not None:
|
||||
|
@ -255,9 +270,7 @@ class Table(JupyterMixin):
|
|||
return Measurement(0, 0)
|
||||
|
||||
extra_width = self._extra_width
|
||||
|
||||
max_width = sum(self._calculate_column_widths(console, max_width - extra_width))
|
||||
|
||||
_measure_column = self._measure_column
|
||||
|
||||
measurements = [
|
||||
|
@ -342,7 +355,10 @@ class Table(JupyterMixin):
|
|||
self.columns.append(column)
|
||||
|
||||
def add_row(
|
||||
self, *renderables: Optional["RenderableType"], style: StyleType = None
|
||||
self,
|
||||
*renderables: Optional["RenderableType"],
|
||||
style: StyleType = None,
|
||||
end_section: bool = False,
|
||||
) -> None:
|
||||
"""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),
|
||||
or ``None`` for a blank cell.
|
||||
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:
|
||||
errors.NotRenderableError: If you add something that can't be rendered.
|
||||
"""
|
||||
|
||||
def add_cell(column: Column, renderable: "RenderableType") -> None:
|
||||
column._cells.append(
|
||||
renderable if style is None else Styled(renderable, style)
|
||||
)
|
||||
column._cells.append(renderable)
|
||||
|
||||
cell_renderables: List[Optional["RenderableType"]] = list(renderables)
|
||||
|
||||
|
@ -371,7 +386,7 @@ class Table(JupyterMixin):
|
|||
for index, renderable in enumerate(cell_renderables):
|
||||
if index == len(columns):
|
||||
column = Column(_index=index)
|
||||
for _ in range(self._row_count):
|
||||
for _ in self.rows:
|
||||
add_cell(column, Text(""))
|
||||
self.columns.append(column)
|
||||
else:
|
||||
|
@ -384,7 +399,7 @@ class Table(JupyterMixin):
|
|||
raise errors.NotRenderableError(
|
||||
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__(
|
||||
self, console: "Console", options: "ConsoleOptions"
|
||||
|
@ -623,14 +638,11 @@ class Table(JupyterMixin):
|
|||
table_style = console.get_style(self.style or "")
|
||||
|
||||
border_style = table_style + console.get_style(self.border_style or "")
|
||||
rows: List[Tuple[_Cell, ...]] = list(
|
||||
zip(
|
||||
*(
|
||||
_column_cells = (
|
||||
self._get_cells(column_index, column)
|
||||
for column_index, column in enumerate(self.columns)
|
||||
)
|
||||
)
|
||||
)
|
||||
row_cells: List[Tuple[_Cell, ...]] = list(zip(*_column_cells))
|
||||
_box = (
|
||||
self.box.substitute(
|
||||
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_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
|
||||
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
|
||||
cells: List[List[List[Segment]]] = []
|
||||
if header_row or footer_row:
|
||||
row_style = Style.null()
|
||||
else:
|
||||
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(
|
||||
width=width,
|
||||
justify=column.justify,
|
||||
|
@ -703,7 +720,9 @@ class Table(JupyterMixin):
|
|||
cells.append(lines)
|
||||
|
||||
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)
|
||||
]
|
||||
|
||||
|
@ -743,16 +762,16 @@ class Table(JupyterMixin):
|
|||
_box.get_row(widths, "head", edge=show_edge), border_style
|
||||
)
|
||||
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 (
|
||||
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)
|
||||
):
|
||||
if leading:
|
||||
for _ in range(leading):
|
||||
yield _Segment(
|
||||
_box.get_row(widths, "mid", edge=show_edge),
|
||||
_box.get_row(widths, "mid", edge=show_edge) * leading,
|
||||
border_style,
|
||||
)
|
||||
else:
|
||||
|
@ -781,10 +800,24 @@ if __name__ == "__main__": # pragma: no cover
|
|||
table.add_column("Title", style="magenta")
|
||||
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("Dec 15, 2017", "Star Wars Ep. V111: The Last Jedi", "$1,332,539,889")
|
||||
table.add_row("Dec 16, 2016", "Rogue One: A Star Wars Story", "$1,332,439,889")
|
||||
table.add_row(
|
||||
"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:
|
||||
console.print()
|
||||
|
|
39
rich/text.py
39
rich/text.py
|
@ -1,4 +1,5 @@
|
|||
from functools import partial, reduce
|
||||
from io import UnsupportedOperation
|
||||
from math import gcd
|
||||
import re
|
||||
from operator import itemgetter
|
||||
|
@ -19,7 +20,7 @@ from typing import (
|
|||
from ._loop import loop_last
|
||||
from ._pick import pick_bool
|
||||
from ._wrap import divide_line
|
||||
from .align import AlignValues
|
||||
from .align import AlignMethod
|
||||
from .cells import cell_len, set_cell_size
|
||||
from .containers import Lines
|
||||
from .control import strip_control_codes
|
||||
|
@ -182,6 +183,32 @@ class Text(JupyterMixin):
|
|||
return other.plain in self.plain
|
||||
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
|
||||
def cell_len(self) -> int:
|
||||
"""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.
|
||||
"""
|
||||
|
||||
_Segment = Segment
|
||||
text = self.plain
|
||||
null_style = Style.null()
|
||||
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[0] = get_style(self.style)
|
||||
|
||||
|
@ -540,7 +567,6 @@ class Text(JupyterMixin):
|
|||
stack_append = stack.append
|
||||
stack_pop = stack.remove
|
||||
|
||||
_Segment = Segment
|
||||
style_cache: Dict[Tuple[Style, ...], Style] = {}
|
||||
style_cache_get = style_cache.get
|
||||
combine = Style.combine
|
||||
|
@ -725,11 +751,11 @@ class Text(JupyterMixin):
|
|||
if 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.
|
||||
|
||||
Args:
|
||||
align (AlignValues): One of "left", "center", or "right".
|
||||
align (AlignMethod): One of "left", "center", or "right".
|
||||
width (int): Desired width.
|
||||
character (str, optional): Character to pad with. Defaults to " ".
|
||||
"""
|
||||
|
@ -930,6 +956,7 @@ class Text(JupyterMixin):
|
|||
|
||||
while True:
|
||||
span, new_span = span.split(line_end)
|
||||
if span:
|
||||
new_lines[line_index]._spans.append(span.move(-line_start))
|
||||
if new_span is None:
|
||||
break
|
||||
|
|
|
@ -42,6 +42,9 @@ from .theme import Theme
|
|||
|
||||
WINDOWS = platform.system() == "Windows"
|
||||
|
||||
LOCALS_MAX_LENGTH = 10
|
||||
LOCALS_MAX_STRING = 80
|
||||
|
||||
|
||||
def install(
|
||||
*,
|
||||
|
@ -146,6 +149,9 @@ class Traceback:
|
|||
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.
|
||||
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__(
|
||||
|
@ -157,6 +163,8 @@ class Traceback:
|
|||
word_wrap: bool = False,
|
||||
show_locals: bool = False,
|
||||
indent_guides: bool = True,
|
||||
locals_max_length: int = LOCALS_MAX_LENGTH,
|
||||
locals_max_string: int = LOCALS_MAX_STRING,
|
||||
):
|
||||
if trace is None:
|
||||
exc_type, exc_value, traceback = sys.exc_info()
|
||||
|
@ -174,6 +182,8 @@ class Traceback:
|
|||
self.word_wrap = word_wrap
|
||||
self.show_locals = show_locals
|
||||
self.indent_guides = indent_guides
|
||||
self.locals_max_length = locals_max_length
|
||||
self.locals_max_string = locals_max_string
|
||||
|
||||
@classmethod
|
||||
def from_exception(
|
||||
|
@ -187,6 +197,8 @@ class Traceback:
|
|||
word_wrap: bool = False,
|
||||
show_locals: bool = False,
|
||||
indent_guides: bool = True,
|
||||
locals_max_length: int = LOCALS_MAX_LENGTH,
|
||||
locals_max_string: int = LOCALS_MAX_STRING,
|
||||
) -> "Traceback":
|
||||
"""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.
|
||||
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.
|
||||
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:
|
||||
Traceback: A Traceback instance that may be printed.
|
||||
|
@ -215,6 +230,8 @@ class Traceback:
|
|||
word_wrap=word_wrap,
|
||||
show_locals=show_locals,
|
||||
indent_guides=indent_guides,
|
||||
locals_max_length=locals_max_length,
|
||||
locals_max_string=locals_max_string,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -224,6 +241,8 @@ class Traceback:
|
|||
exc_value: BaseException,
|
||||
traceback: Optional[TracebackType],
|
||||
show_locals: bool = False,
|
||||
locals_max_length: int = LOCALS_MAX_LENGTH,
|
||||
locals_max_string: int = LOCALS_MAX_STRING,
|
||||
) -> Trace:
|
||||
"""Extract traceback information.
|
||||
|
||||
|
@ -232,6 +251,9 @@ class Traceback:
|
|||
exc_value (BaseException): Exception value.
|
||||
traceback (TracebackType): Python Traceback object.
|
||||
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:
|
||||
Trace: A Trace instance which you can use to construct a `Traceback`.
|
||||
|
@ -266,7 +288,11 @@ class Traceback:
|
|||
lineno=line_no,
|
||||
name=frame_summary.f_code.co_name,
|
||||
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()
|
||||
}
|
||||
if show_locals
|
||||
|
@ -460,6 +486,8 @@ class Traceback:
|
|||
frame.locals,
|
||||
title="locals",
|
||||
indent_guides=self.indent_guides,
|
||||
max_length=self.locals_max_length,
|
||||
max_string=self.locals_max_string,
|
||||
),
|
||||
],
|
||||
padding=1,
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -13,6 +13,7 @@ from rich.console import CaptureError, Console, ConsoleOptions, render_group
|
|||
from rich.measure import measure_renderables
|
||||
from rich.pager import SystemPager
|
||||
from rich.panel import Panel
|
||||
from rich.status import Status
|
||||
from rich.style import Style
|
||||
|
||||
|
||||
|
@ -97,6 +98,21 @@ def test_print():
|
|||
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():
|
||||
console = Console(file=io.StringIO(), color_system="truecolor")
|
||||
console.print()
|
||||
|
@ -218,6 +234,12 @@ def test_input_password(monkeypatch, capsys):
|
|||
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():
|
||||
console = Console(file=io.StringIO(), force_terminal=True, width=20)
|
||||
console.print("FOO", justify=None)
|
||||
|
|
160
tests/test_live.py
Normal file
160
tests/test_live.py
Normal 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"
|
||||
)
|
|
@ -45,6 +45,14 @@ def test_log():
|
|||
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__":
|
||||
render = render_log()
|
||||
print(render)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# encoding=utf-8
|
||||
|
||||
import io
|
||||
from time import time
|
||||
from time import sleep
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -143,8 +143,10 @@ def make_progress() -> Progress:
|
|||
|
||||
def render_progress() -> str:
|
||||
progress = make_progress()
|
||||
progress.start() # superfluous noop
|
||||
with progress:
|
||||
pass
|
||||
progress.stop() # superfluous noop
|
||||
progress_render = progress.console.file.getvalue()
|
||||
return progress_render
|
||||
|
||||
|
@ -328,9 +330,20 @@ def test_progress_create() -> None:
|
|||
|
||||
|
||||
def test_refresh_thread() -> None:
|
||||
progress = Progress()
|
||||
thread = _RefreshThread(progress, 10)
|
||||
class MockProgress:
|
||||
def __init__(self):
|
||||
self.count = 0
|
||||
|
||||
def refresh(self):
|
||||
self.count += 1
|
||||
|
||||
progress = MockProgress()
|
||||
thread = _RefreshThread(progress, 100)
|
||||
assert thread.progress == progress
|
||||
thread.start()
|
||||
sleep(0.2)
|
||||
thread.stop()
|
||||
assert progress.count >= 1
|
||||
|
||||
|
||||
def test_track_thread() -> None:
|
||||
|
@ -372,6 +385,42 @@ def test_reset() -> None:
|
|||
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__":
|
||||
_render = render_progress()
|
||||
print(_render)
|
||||
|
|
|
@ -24,6 +24,25 @@ def test_rule():
|
|||
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():
|
||||
console = Console(
|
||||
width=16,
|
||||
|
|
42
tests/test_spinner.py
Normal file
42
tests/test_spinner.py
Normal 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
21
tests/test_status.py
Normal 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)
|
|
@ -451,6 +451,14 @@ def test_render():
|
|||
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(
|
||||
"print_text,result",
|
||||
[
|
||||
|
@ -636,3 +644,18 @@ foo = [
|
|||
print(repr(result.plain))
|
||||
expected = "for a in range(10):\n│ print(a)\n\nfoo = [\n│ 1,\n│ {\n│ │ 2\n│ }\n]\n"
|
||||
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]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue