Merge branch 'master' into fix-syntax-include-padding-with-background-color-override

This commit is contained in:
Will McGugan 2024-07-01 21:00:06 +01:00 committed by GitHub
commit 9b4e3f1783
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 740 additions and 287 deletions

View File

@ -33,7 +33,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -47,7 +47,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@ -60,6 +60,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

View File

@ -8,20 +8,23 @@ jobs:
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11.0", "3.12.0-rc.3"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
include:
- { os: ubuntu-latest, python-version: "3.7" }
- { os: windows-latest, python-version: "3.7" }
- { os: macos-12, python-version: "3.7" }
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
architecture: x64
- name: Install and configure Poetry
# TODO: workaround for https://github.com/snok/install-poetry/issues/94
uses: snok/install-poetry@v1.3.3
uses: snok/install-poetry@v1.3.4
with:
version: 1.3.1
virtualenvs-in-project: true
@ -41,7 +44,7 @@ jobs:
source $VENV
pytest tests -v --cov=./rich --cov-report=xml:./coverage.xml --cov-report term-missing
- name: Upload code coverage
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.xml

View File

@ -30,8 +30,8 @@ repos:
hooks:
- id: pycln
args: [--all]
- repo: https://github.com/psf/black
rev: 23.7.0
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.11.0
hooks:
- id: black
exclude: ^benchmarks/

View File

@ -9,9 +9,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fixed `Table` rendering of box elements so "footer" elements truly appear at bottom of table, "mid" elements in main table body.
- Fixed styles in Panel when Text objects are used for title https://github.com/Textualize/rich/pull/3401
- Fix pretty repr for `collections.deque` https://github.com/Textualize/rich/pull/2864
- Thread used in progress.track will exit if an exception occurs in a generator https://github.com/Textualize/rich/pull/3402
- Progress track thread is now a daemon thread https://github.com/Textualize/rich/pull/3402
- Fixed cached hash preservation upon clearing meta and links https://github.com/Textualize/rich/issues/2942
- Fixed overriding the `background_color` of `Syntax` not including padding https://github.com/Textualize/rich/issues/3295
## [13.7.1] - 2023-02-28
### Changed
- `RichHandler` errors and warnings will now use different colors (red and yellow) https://github.com/Textualize/rich/issues/2825
- Removed the empty line printed in jupyter while using `Progress` https://github.com/Textualize/rich/pull/2616
- Running tests in environment with `FORCE_COLOR` or `NO_COLOR` environment variables
- ansi decoder will now strip problematic private escape sequences (like `\x1b7`) https://github.com/Textualize/rich/pull/3278/
### Added
- Adds a `case_sensitive` parameter to `prompt.Prompt`. This determines if the
response is treated as case-sensitive. Defaults to `True`.
## [13.7.1] - 2024-02-28
### Fixed
@ -75,7 +94,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Text.tab_size now defaults to `None` to indicate that Console.tab_size should be used.
## [13.4.2] - 2023-06-12
### Changed
@ -130,6 +148,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added Polish README
### Changed
- `rich.progress.track()` will now show the elapsed time after finishing the task https://github.com/Textualize/rich/pull/2659

View File

@ -6,7 +6,7 @@ In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
level of experience, education, socioeconomic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards

View File

@ -11,10 +11,12 @@ The following people have contributed to the development of Rich:
- [Robin Bowes](https://github.com/yo61)
- [Dennis Brakhane](https://github.com/brakhane)
- [Darren Burns](https://github.com/darrenburns)
- [Ceyda Cinarel](https://github.com/cceyda)
- [Jim Crist-Harif](https://github.com/jcrist)
- [Ed Davis](https://github.com/davised)
- [Pete Davison](https://github.com/pd93)
- [James Estevez](https://github.com/jstvz)
- [Jonathan Eunice](https://github.com/jonathan-3play)
- [Aryaz Eghbali](https://github.com/AryazE)
- [Oleksis Fraga](https://github.com/oleksis)
- [Andy Gimblett](https://github.com/gimbo)
@ -26,11 +28,14 @@ The following people have contributed to the development of Rich:
- [Kenneth Hoste](https://github.com/boegel)
- [Lanqing Huang](https://github.com/lqhuang)
- [Finn Hughes](https://github.com/finnhughes)
- [Logan Hunt](https://github.com/dosisod)
- [JP Hutchins](https://github.com/JPhutchins)
- [Ionite](https://github.com/ionite34)
- [Josh Karpel](https://github.com/JoshKarpel)
- [Jan Katins](https://github.com/jankatins)
- [Hugo van Kemenade](https://github.com/hugovk)
- [Andrew Kettmann](https://github.com/akettmann)
- [Alexander Krasnikov](https://github.com/askras)
- [Martin Larralde](https://github.com/althonos)
- [Hedy Li](https://github.com/hedythedev)
- [Henry Mai](https://github.com/tanducmai)
@ -50,6 +55,7 @@ The following people have contributed to the development of Rich:
- [Kylian Point](https://github.com/p0lux)
- [Kyle Pollina](https://github.com/kylepollina)
- [Sebastián Ramírez](https://github.com/tiangolo)
- [Grant Ramsay](https://github.com/seapagan)
- [Felipe Guedes](https://github.com/guedesfelipe)
- [Min RK](https://github.com/minrk)
- [Clément Robert](https://github.com/neutrinoceros)
@ -61,6 +67,7 @@ The following people have contributed to the development of Rich:
- [Anthony Shaw](https://github.com/tonybaloney)
- [Nicolas Simonds](https://github.com/0xDEC0DE)
- [Aaron Stephens](https://github.com/aaronst)
- [Karolina Surma](https://github.com/befeleme)
- [Gabriele N. Tornetta](https://github.com/p403n1x87)
- [Nils Vu](https://github.com/nilsvu)
- [Arian Mollik Wasi](https://github.com/wasi-master)

88
FAQ.md
View File

@ -1,36 +1,13 @@
# Frequently Asked Questions
- [Why does emoji break alignment in a Table or Panel?](#why-does-emoji-break-alignment-in-a-table-or-panel)
- [Why does content in square brackets disappear?](#why-does-content-in-square-brackets-disappear)
- [python -m rich.spinner shows extra lines](#python--m-rich.spinner-shows-extra-lines)
- [How do I log a renderable?](#how-do-i-log-a-renderable)
- [Strange colors in console output.](#strange-colors-in-console-output.)
<a name="why-does-emoji-break-alignment-in-a-table-or-panel"></a>
## Why does emoji break alignment in a Table or Panel?
Certain emoji take up double space within the terminal. Unfortunately, terminals don't always agree how wide a given character should be.
Rich has no way of knowing how wide a character will be on any given terminal. This can break alignment in containers like Table and Panel, where Rich needs to know the width of the content.
There are also *multiple codepoints* characters, such as country flags, and emoji modifiers, which produce wildly different results across terminal emulators.
Fortunately, most characters will work just fine. But you may have to avoid using the emojis that break alignment. You will get good results if you stick to emoji released on or before version 9 of the Unicode database,
<a name="why-does-content-in-square-brackets-disappear"></a>
## Why does content in square brackets disappear?
Rich will treat text within square brackets as *markup tags*, for instance `"[bold]This is bold[/bold]"`.
If you are printing strings with literally square brackets you can either disable markup, or escape your strings.
See the docs on [console markup](https://rich.readthedocs.io/en/latest/markup.html) for how to do this.
<a name="python--m-rich.spinner-shows-extra-lines"></a>
## python -m rich.spinner shows extra lines
The spinner example is know to break on some terminals (Windows in particular).
Some terminals don't display emoji with the correct width, which means Rich can't always align them accurately inside a panel.
- [How do I render console markup in RichHandler?](#how-do-i-render-console-markup-in-richhandler)
- [Natively inserted ANSI escape sequence characters break alignment of Panel.](#natively-inserted-ansi-escape-sequence-characters-break-alignment-of-panel)
- [python -m rich.spinner shows extra lines.](#python--m-richspinner-shows-extra-lines)
- [Rich is automatically installing traceback handler.](#rich-is-automatically-installing-traceback-handler)
- [Strange colors in console output.](#strange-colors-in-console-output)
- [Why does content in square brackets disappear?](#why-does-content-in-square-brackets-disappear)
- [Why does emoji break alignment in a Table or Panel?](#why-does-emoji-break-alignment-in-a-table-or-panel)
<a name="how-do-i-log-a-renderable"></a>
## How do I log a renderable?
@ -43,13 +20,62 @@ Logging supports configurable back-ends, which means that a log message could go
If you are only logging with a file-handler to stdout, then you probably don't need to use the logging module at all. Consider using [Console.log](https://rich.readthedocs.io/en/latest/reference/console.html#rich.console.Console.log) which will render anything that you can print with Rich, with a timestamp.
<a name="strange-colors-in-console-output."></a>
<a name="how-do-i-render-console-markup-in-richhandler"></a>
## How do I render console markup in RichHandler?
Console markup won't work anywhere else, other than `RichHandler` -- which is why they are disabled by default.
See the docs if you want to [enable console markup](https://rich.readthedocs.io/en/latest/logging.html#logging-handler) in the logging handler.
<a name="natively-inserted-ansi-escape-sequence-characters-break-alignment-of-panel"></a>
## Natively inserted ANSI escape sequence characters break alignment of Panel.
If you print ansi escape sequences for color and style you may find the output breaks your output.
You may find that border characters in Panel and Table are in the wrong place, for example.
As a general rule, you should allow Rich to generate all ansi escape sequences, so it can correctly account for these invisible characters.
If you can't avoid a string with escape codes, you can convert it to an equivalent `Text` instance with `Text.from_ansi`.
<a name="python--m-richspinner-shows-extra-lines"></a>
## python -m rich.spinner shows extra lines.
The spinner example is know to break on some terminals (Windows in particular).
Some terminals don't display emoji with the correct width, which means Rich can't always align them accurately inside a panel.
<a name="rich-is-automatically-installing-traceback-handler"></a>
## Rich is automatically installing traceback handler.
Rich will never install the traceback handler automatically.
If you are getting Rich tracebacks and you don't want them, then some other piece of software is calling `rich.traceback.install()`.
<a name="strange-colors-in-console-output"></a>
## Strange colors in console output.
Rich will highlight certain patterns in your output such as numbers, strings, and other objects like IP addresses.
Occasionally this may also highlight parts of your output you didn't intend. See the [docs on highlighting](https://rich.readthedocs.io/en/latest/highlighting.html) for how to disable highlighting.
<a name="why-does-content-in-square-brackets-disappear"></a>
## Why does content in square brackets disappear?
Rich will treat text within square brackets as *markup tags*, for instance `"[bold]This is bold[/bold]"`.
If you are printing strings with literally square brackets you can either disable markup, or escape your strings.
See the docs on [console markup](https://rich.readthedocs.io/en/latest/markup.html) for how to do this.
<a name="why-does-emoji-break-alignment-in-a-table-or-panel"></a>
## Why does emoji break alignment in a Table or Panel?
Certain emoji take up double space within the terminal. Unfortunately, terminals don't always agree how wide a given character should be.
Rich has no way of knowing how wide a character will be on any given terminal. This can break alignment in containers like Table and Panel, where Rich needs to know the width of the content.
There are also *multiple codepoints* characters, such as country flags, and emoji modifiers, which produce wildly different results across terminal emulators.
Fortunately, most characters will work just fine. But you may have to avoid using the emojis that break alignment. You will get good results if you stick to emoji released on or before version 9 of the Unicode database,
<hr>
Generated by [FAQtory](https://github.com/willmcgugan/faqtory)

View File

@ -437,11 +437,3 @@ See also [Rich CLI](https://github.com/textualize/rich-cli) for a command line a
See also Rich's sister project, [Textual](https://github.com/Textualize/textual), which you can use to build sophisticated User Interfaces in the terminal.
![Textual screenshot](https://raw.githubusercontent.com/Textualize/textual/main/imgs/textual.png)
# Projects using Rich
For some examples of projects using Rich, see the [Rich Gallery](https://www.textualize.io/rich/gallery) on [Textualize.io](https://www.textualize.io).
Would you like to add your own project to the gallery? You can! Follow [these instructions](https://www.textualize.io/gallery-instructions).
<!-- This is a test, no need to translate -->

View File

@ -27,23 +27,23 @@
Rich это Python библиотека, позволяющая отображать расивый_ текст и форматировать терминал.
[Rich API](https://rich.readthedocs.io/en/latest/) упрощает добавление цветов и стилей к выводу терминала. Rich также позволяет отображать красивые таблицы, прогресс бары, markdown, код с отображением синтаксиса, ошибки, и т.д. — прямо после установки.
[Rich API](https://rich.readthedocs.io/en/latest/) упрощает добавление цветов и стилей к выводу терминала. Rich также позволяет отображать красивые таблицы, прогресс бары, markdown, код с подсветкой синтаксиса, ошибки, и т.д. — прямо после установки.
![Features](https://github.com/textualize/rich/raw/master/imgs/features.png)
Для видеоинструкции смотрите [calmcode.io](https://calmcode.io/rich/introduction.html) от [@fishnets88](https://twitter.com/fishnets88).
Смотрите видеоинструкцию [calmcode.io](https://calmcode.io/rich/introduction.html) от [@fishnets88](https://twitter.com/fishnets88).
Посмотрите [что люди думают о Rich](https://www.willmcgugan.com/blog/pages/post/rich-tweets/).
## Cовместимость
Rich работает с Linux, OSX, и Windows. True color / эмоджи работают с новым терминалом Windows, классический терминал лимитирован 16 цветами. Rich требует Python 3.6.3 или более новый.
Rich работает с Linux, OSX и Windows. True color / эмоджи работают с новым терминалом Windows, классический терминал лимитирован 16 цветами. Rich требует Python 3.6.3 или более новый.
Rich работает с [Jupyter notebooks](https://jupyter.org/) без дополнительной конфигурации.
## Установка
Установите с `pip` или вашим любимым PyPI менеджером пакетов.
Установите с помощью `pip` или вашего любимого PyPI менеджера пакетов.
```sh
python -m pip install rich
@ -57,7 +57,7 @@ python -m rich
## Rich Print
Простейший способ получить красивый вывод это импортировать метод [rich print](https://rich.readthedocs.io/en/latest/introduction.html#quick-start), он принимает такие же аргументы что и стандартный метод print. Попробуйте:
Простейший способ получить красивый вывод это импортировать метод [rich print](https://rich.readthedocs.io/en/latest/introduction.html#quick-start), он принимает такие же аргументы что и стандартный метод `print`. Попробуйте:
```python
from rich import print
@ -88,13 +88,13 @@ from rich.console import Console
console = Console()
```
У класса console есть метод `print` который имеет идентичный функционал к встроеной функции `print`. Вот пример использования:
У класса Сonsole есть метод `print` который имеет идентичный встроенной функции функционал `print`. Вот пример использования:
```python
console.print("Hello", "World!")
```
Как вы могли подумать, этот выведет `"Hello World!"` в терминал. Запомните что, в отличии от встроеной функции `print`, Rich увеличит ваш текст так, чтобы он распространялся на всю ширину терминала.
Как вы могли догадаться, это выведет `Hello World!` в терминал. Запомните что, в отличии от встроенной функции `print`, Rich настроит переносы слов так, чтобы ваш текст соответствовал ширине терминала.
Есть несколько способов добавить цвет и стиль к вашему выводу. Вы можете выбрать стиль для всего вывода добавив аргумент `style`. Вот пример:
@ -102,11 +102,11 @@ console.print("Hello", "World!")
console.print("Hello", "World!", style="bold red")
```
Вывод будет выглядить примерно так:
Вывод будет выглядеть примерно так:
![Hello World](https://github.com/textualize/rich/raw/master/imgs/hello_world.png)
Этого достаточно чтобы стилизовать 1 строку. Для более детального стилизования, Rich использует специальную разметку похожую по синтаксису на [bbcode](https://en.wikipedia.org/wiki/BBCode). Вот пример:
Этого достаточно чтобы стилизовать 1 строку. Для более детальной стилизации, Rich использует специальную разметку похожую по синтаксису на [bbcode](https://en.wikipedia.org/wiki/BBCode). Вот пример:
```python
console.print("Where there is a [bold cyan]Will[/bold cyan] there [u]is[/u] a [i]way[/i].")
@ -114,11 +114,11 @@ console.print("Where there is a [bold cyan]Will[/bold cyan] there [u]is[/u] a [i
![Console Markup](https://github.com/textualize/rich/raw/master/imgs/where_there_is_a_will.png)
Вы можете использовать класс Console чтобы генерировать утонченный вывод с минимальными усилиями. Смотрите [документацию Console API](https://rich.readthedocs.io/en/latest/console.html) для детального объяснения.
Вы можете использовать класс Console чтобы генерировать красивый вывод с минимальными усилиями. Для получения детальной информации смотрите [документацию Console API](https://rich.readthedocs.io/en/latest/console.html).
## Rich Inspect
В Rich есть функция [inspect](https://rich.readthedocs.io/en/latest/reference/init.html?highlight=inspect#rich.inspect) которая может украсить любой Python объект, например класс, переменная, или функция.
В Rich имеется функция [inspect](https://rich.readthedocs.io/en/latest/reference/init.html?highlight=inspect#rich.inspect) которая может украсить любой Python объект, например класс, переменную, или функцию.
```python
>>> my_list = ["foo", "bar"]
@ -128,18 +128,18 @@ console.print("Where there is a [bold cyan]Will[/bold cyan] there [u]is[/u] a [i
![Log](https://github.com/textualize/rich/raw/master/imgs/inspect.png)
Смотрите [документацию inspect](https://rich.readthedocs.io/en/latest/reference/init.html#rich.inspect) для детального объяснения.
Для получения детальной информации смотрите [документацию inspect](https://rich.readthedocs.io/en/latest/reference/init.html#rich.inspect).
# Библиотека Rich
Rich содержит несколько встроенных _визуализаций_ которые вы можете использовать чтобы сделать элегантный вывод в важем CLI или помочь в дебаггинге кода.
Rich содержит несколько встроенных _визуализаций_ которые вы можете использовать чтобы сделать красивый вывод в вашем CLI, а также они помогают в отладке кода.
Вот несколько вещей которые может делать Rich (нажмите чтобы узнать больше):
<details>
<summary>Лог</summary>
В классе console есть метод `log()` который похож на `print()`, но также изображает столбец для текущего времени, файла и линии кода которая вызвала метод. По умолчанию Rich будет подсвечивать синтаксис для структур Python и для строк repr. Если вы передадите в метод коллекцию (т.е. dict или list) Rich выведет её так, чтобы она помещалась в доступном месте. Вот пример использования этого метода.
В классе Сonsole есть метод `log()` который имеет интерфейс, аналогичный `print()`, но также отображает колонку текущим временем, именем файла и номером строки кода в которой был вызван метод. По умолчанию Rich будет подсвечивать синтаксис для структур Python и для строк repr. Если вы передадите в метод коллекцию (т.е. dict или list) Rich выведет её так, чтобы она разместилась в доступном пространстве. Вот пример использования этого метода.
```python
from rich.console import Console
@ -164,13 +164,14 @@ def test_log():
test_log()
```
Код выше выведет это:
Приведенный выше код выведет это:
![Log](https://github.com/textualize/rich/raw/master/imgs/log.png)
Запомните аргумент `log_locals`, он выводит таблицу имеющую локальные переменные функции в которой метод был вызван.
Метод может быть использован для вывода данных в терминал в длинно-работающих программ, таких как сервера, но он также может помочь в дебаггинге.
Обратите внимание на аргумент `log_locals`, который выводит таблицу, содержащую локальные переменные функции, в которой был вызван метод log.
Метод может быть использован для вывода данных в терминал в длительно работающих программ, таких как сервера, но он также может помочь в отладке.
</details>
<details>
@ -185,25 +186,25 @@ test_log()
<details>
<summary>Эмоджи</summary>
Чтобы вставить эмоджи в вывод консоли поместите название между двумя двоеточиями. Вот пример:
Чтобы вставить эмоджи в вывод консоли, поместите его название между двумя двоеточиями. Вот пример:
```python
>>> console.print(":smiley: :vampire: :pile_of_poo: :thumbs_up: :raccoon:")
😃 🧛 💩 👍 🦝
```
Пожалуйста, используйте это мудро.
Пожалуйста, используйте эту функцию с умом.
</details>
<details>
<summary>Таблицы</summary>
Rich может отображать гибкие [таблицы](https://rich.readthedocs.io/en/latest/tables.html) с символами unicode. Есть большое количество форматов границ, стилей, выравниваний ячеек и т.п.
Rich может отображать гибкие настраиваемые [таблицы](https://rich.readthedocs.io/en/latest/tables.html) с помощью символов unicode. Есть большое количество вариантов границ, стилей, выравниваний ячеек и т.п.
![table movie](https://github.com/textualize/rich/raw/master/imgs/table_movie.gif)
Эта анимация была сгенерирована с помощью [table_movie.py](https://github.com/textualize/rich/blob/master/examples/table_movie.py) в директории примеров.
Эта анимация была сгенерирована с помощью [table_movie.py](https://github.com/textualize/rich/blob/master/examples/table_movie.py) в папке примеров.
Вот пример более простой таблицы:
@ -241,7 +242,7 @@ console.print(table)
![table](https://github.com/textualize/rich/raw/master/imgs/table.png)
Запомните что разметка консоли отображается таким же способом что и `print()` и `log()`. На самом деле, всё, что может отобразить Rich может быть в заголовках или рядах (даже другие таблицы).
Обратите внимание, что разметка осуществляется таким же способом, что и `print()` и `log()`. На самом деле, все, что может быть отображено Rich, может быть включено в заголовки / строки (даже в другие таблицы).
Класс `Table` достаточно умный чтобы менять размер столбцов, так, чтобы они заполняли доступную ширину терминала, обёртывая текст как нужно. Вот тот же самый пример с терминалом меньше таблицы:
@ -254,7 +255,7 @@ console.print(table)
Rich может отображать несколько плавных [прогресс](https://rich.readthedocs.io/en/latest/progress.html) баров чтобы отслеживать долго-идущие задания.
Для базового использования, оберните любую последовательность в функции `track` и переберите результат. Вот пример:
Для базового использования, оберните любую последовательность в функцию `track` и выполните итерации по результату. Вот пример:
```python
from rich.progress import track
@ -263,22 +264,22 @@ for step in track(range(100)):
do_step(step)
```
Отслеживать больше чем 1 задание не сложнее. Вот пример взятый из документации:
Добавить несколько индикаторов выполнения не намного сложнее. Вот пример взятый из документации:
![progress](https://github.com/textualize/rich/raw/master/imgs/progress.gif)
Столбцы могут быть настроены чтобы показывать любые детали. Стандартные столбцы содержат проценты исполнения, размер файлы, скорость файла, и оставшееся время. Вот ещё пример показывающий загрузку в прогрессе:
Столбцы могут быть сконфигурированы таким образом, чтобы отображать любые сведения, которые вы хотите. Стандартные столбцы содержат проценты выполнения, размер файлы, скорость файла и оставшееся время. Вот ещё пример показывающий прогресс загрузки:
![progress](https://github.com/textualize/rich/raw/master/imgs/downloader.gif)
Чтобы попробовать самому, скачайте [examples/downloader.py](https://github.com/textualize/rich/blob/master/examples/downloader.py) который может скачать несколько URL одновременно пока отображая прогресс.
Чтобы попробовать самому, скачайте [examples/downloader.py](https://github.com/textualize/rich/blob/master/examples/downloader.py), который может загружать несколько URL-адресов одновременно, отображая прогресс.
</details>
<details>
<summary>Статус</summary>
Для ситуаций где сложно высчитать прогресс, вы можете использовать метод [статус](https://rich.readthedocs.io/en/latest/reference/console.html#rich.console.Console.status) который будет отображать крутящуюся анимацию и сообщение. Анимация не перекроет вам доступ к консоли. Вот пример:
Для ситуаций где сложно вычислить прогресс, вы можете использовать метод [статус](https://rich.readthedocs.io/en/latest/reference/console.html#rich.console.Console.status) который будет отображать крутящуюся анимацию и сообщение. Анимация не помешает вам использовать консоль в обычном режиме. Вот пример:
```python
from time import sleep
@ -294,17 +295,17 @@ with console.status("[bold green]Working on tasks...") as status:
console.log(f"{task} complete")
```
Это генерирует вот такой вывод в консоль.
Это сгенерирует вот такой вывод в консоль.
![status](https://github.com/textualize/rich/raw/master/imgs/status.gif)
Крутящиеся анимации были взяты из [cli-spinners](https://www.npmjs.com/package/cli-spinners). Вы можете выбрать одну из них указав параметр `spinner`. Запустите следующую команду чтобы узнать доступные анимации:
Крутящиеся анимации были взяты из [cli-spinners](https://www.npmjs.com/package/cli-spinners). Вы можете выбрать одну из них указав параметр `spinner`. Введите следующую команду чтобы посмотреть доступные анимации:
```
python -m rich.spinner
```
Эта команда выдаёт вот такой вывод в терминал:
Приведенная выше команда сгенерирует следующий вывод в терминале:
![spinners](https://github.com/textualize/rich/raw/master/imgs/spinners.gif)
@ -313,9 +314,9 @@ python -m rich.spinner
<details>
<summary>Дерево</summary>
Rich может отобразить [дерево](https://rich.readthedocs.io/en/latest/tree.html) с указаниями. Дерево идеально подходит для отображения структуры файлов или любых других иерархических данных.
Rich может отобразить [дерево](https://rich.readthedocs.io/en/latest/tree.html) с направляющими уровнями. Дерево идеально подходит для отображения структуры файлов или любых других иерархических данных.
Ярлыки дерева могут быть простым текстом или любой другой вещью Rich может отобразить. Запустите следующую команду для демонстрации:
Метки дерева могут быть содержать простой текст или чем-либо еще, что может отобразить Rich. Запустите следующую команду для демонстрации:
```
python -m rich.tree
@ -325,14 +326,14 @@ python -m rich.tree
![markdown](https://github.com/textualize/rich/raw/master/imgs/tree.png)
Смотрите пример [tree.py](https://github.com/textualize/rich/blob/master/examples/tree.py) для скрипта который отображает дерево любой директории, похоже на команду linux `tree`.
Смотрите пример [tree.py](https://github.com/textualize/rich/blob/master/examples/tree.py) скрипта,который отображает древовидное представление любого каталога, аналогично команде linux `tree`.
</details>
<details>
<summary>Столбцы</summary>
<summary>Колонки</summary>
Rich может отображать контент в [столбцах](https://rich.readthedocs.io/en/latest/columns.html) с равной или оптимальной шириной. Вот очень простой пример клона команды `ls` (MacOS / Linux) который отображает a файлы директории в столбцах:
Rich может отображать контент в аккуратных [колонках](https://rich.readthedocs.io/en/latest/columns.html) с равной или оптимальной шириной. Вот очень простой пример клона команды `ls` (MacOS / Linux) который отображает список файлов из папки в виде колонок:
```python
import os
@ -345,7 +346,7 @@ directory = os.listdir(sys.argv[1])
print(Columns(directory))
```
Следующий скриншот это вывод из [примера столбцов](https://github.com/textualize/rich/blob/master/examples/columns.py) который изображает данные взятые из API в столбцах:
Следующий снимок экрана является [примером колонок](https://github.com/textualize/rich/blob/master/examples/columns.py) который изображает данные взятые из API в столбцах:
![columns](https://github.com/textualize/rich/raw/master/imgs/columns.png)
@ -354,9 +355,9 @@ print(Columns(directory))
<details>
<summary>Markdown</summary>
Rich может отображать [markdown](https://rich.readthedocs.io/en/latest/markdown.html) и делает неплохую работу в форматировании под терминал.
Rich может отображать [markdown](https://rich.readthedocs.io/en/latest/markdown.html), проделывая неплохую работу в форматировании под терминал.
Чтобы отобразить markdown импортируйте класс `Markdown` и инициализируйте его с помощью строки содержащей код markdown. После чего выведите его в консоль. Вот пример:
Чтобы отобразить markdown импортируйте класс `Markdown` и инициализируйте его с помощью строки содержащей код markdown. Затем распечатайте его в консоли. Вот пример:
```python
from rich.console import Console
@ -375,9 +376,9 @@ console.print(markdown)
</details>
<details>
<summary>Подсвечивание Синтаксиса</summary>
<summary>Подсветка синтаксиса</summary>
Rich использует библиотеку [pygments](https://pygments.org/) чтобы имплементировать [подсвечивание синтаксиса](https://rich.readthedocs.io/en/latest/syntax.html). Использование похоже на отображение markdown; инициализируйте класс `Syntax` и выводите его в консоль. Вот пример:
Rich использует библиотеку [pygments](https://pygments.org/) чтобы выполнить [подсветку синтаксиса](https://rich.readthedocs.io/en/latest/syntax.html). Использование аналогично рендерингу markdown; создайте объект `Syntax` и выведите его на консоль. Вот пример:
```python
from rich.console import Console
@ -412,7 +413,7 @@ console.print(syntax)
<details>
<summary>Ошибки</summary>
Rich может отображать [красивые ошибки](https://rich.readthedocs.io/en/latest/traceback.html) которые проще читать и показывают больше кода чем стандартные ошибки Python. Вы можете установить Rich как стандартный обработчик ошибок чтобы все непойманные ошибки отображал Rich.
Rich может отображать [красивый стек ошибок](https://rich.readthedocs.io/en/latest/traceback.html), который проще читать, и показывает больше информации чем стандартные стек ошибок Python. Вы можете установить Rich как стандартный обработчик ошибок чтобы все не перехваченные исключения отображались Rich.
Вот как это выглядит на OSX (похоже на Linux):
@ -420,17 +421,28 @@ Rich может отображать [красивые ошибки](https://ric
</details>
Все визуализации Rich используют [протокол Console](https://rich.readthedocs.io/en/latest/protocol.html), который также позволяет вам добавлять свой Rich контент.
Все визуализации Rich используют [протокол Console](https://rich.readthedocs.io/en/latest/protocol.html), который позволяет вам добавлять свой собственный Rich контент.
# Rich CLI
Смотрите также [Rich CLI](https://github.com/textualize/rich-cli) для получения информации о приложении командной строки, работающего на базе Rich. Подсветка синтаксиса кода, рендеринг markdown, отображение CSV-файлов в таблицах и многое другое доступно непосредственно из командной строки.
![Rich CLI](https://raw.githubusercontent.com/Textualize/rich-cli/main/imgs/rich-cli-splash.jpg)
# Textual
Смотрите также дочерний проект Rich, [Textual](https://github.com/Textualize/textual), который вы можете использовать для создания сложных пользовательских интерфейсов в терминале.
# Rich для предприятий
Rich доступен как часть подписки Tidelift.
Поддержатели проекта Rich и тысячи других работают над подпиской Tidelift чтобы предоставить коммерческую поддержку и поддержание для проектов с открытым кодом вы используете чтобы построить своё приложение. Сохраните время, избавьтесь от риска, и улучшите состояние кода, пока вы платите поддержателям проектов вы используете. [Узнайте больше.](https://tidelift.com/subscription/pkg/pypi-rich?utm_source=pypi-rich&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)
Ментейнеры проекта Rich, как и тысячи других разработчиков работают с подпиской Tidelift чтобы предоставить коммерческую поддержку и поддержку для проектов с открытым кодом, которые вы используете для создания своих приложений. Экономьте время, устраняйте риски и улучшайте состояние вашего кода, одновременно платя спонсорам проектов, которые вы используете. [Узнайте больше.](https://tidelift.com/subscription/pkg/pypi-rich?utm_source=pypi-rich&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)
# Проекты использующие Rich
Вот пару проектов использующих Rich:
Вот несколько проектов использующих Rich:
- [BrancoLab/BrainRender](https://github.com/BrancoLab/BrainRender)
библиотека Python для визуализации нейроанатомических данных в 3 измерениях
@ -441,7 +453,7 @@ Rich доступен как часть подписки Tidelift.
- [hedythedev/StarCli](https://github.com/hedythedev/starcli)
Просматривайте трендовые проекты GitHub прямо из вашего терминала
- [intel/cve-bin-tool](https://github.com/intel/cve-bin-tool)
Эта утилита сканирует известные уязвимости (openssl, libpng, libxml2, expat and a few others) чтобы уведомить вас если ваша система использует библиотеки с известными уязвимостями.
Эта утилита сканирует известные уязвимости (openssl, libpng, libxml2, expat and a few others) чтобы уведомить вас, если ваша система использует библиотеки с известными уязвимостями.
- [nf-core/tools](https://github.com/nf-core/tools)
Библиотека Python с полезными инструментами для сообщества nf-core.
- [cansarigol/pdbr](https://github.com/cansarigol/pdbr)

View File

@ -2,3 +2,4 @@ alabaster==0.7.12
Sphinx==5.1.1
sphinx-rtd-theme==1.0.0
sphinx-copybutton==0.5.1
importlib-metadata; python_version < '3.8'

View File

@ -17,10 +17,15 @@
# -- Project information -----------------------------------------------------
import sys
import pkg_resources
import sphinx_rtd_theme
if sys.version_info >= (3, 8):
from importlib.metadata import Distribution
else:
from importlib_metadata import Distribution
html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
@ -30,7 +35,7 @@ copyright = "Will McGugan"
author = "Will McGugan"
# The full version, including alpha/beta/rc tags
release = pkg_resources.get_distribution("rich").version
release = Distribution.from_name("rich").version
# -- General configuration ---------------------------------------------------

View File

@ -55,6 +55,34 @@ Here's a simple example::
The ``total`` value associated with a task is the number of steps that must be completed for the progress to reach 100%. A *step* in this context is whatever makes sense for your application; it could be number of bytes of a file read, or number of images processed, etc.
Starting and stopping
~~~~~~~~~~~~~~~~~~~~~
The context manager is recommended if you can use it. If you don't use the context manager, be sure to call :meth:`~rich.progress.Progress.start` to start the progress display, and :meth:`~rich.progress.Progress.stop` to stop it.
Here's an example that doesn't use the context manager::
import time
from rich.progress import Progress
progress = Progress()
progress.start()
try:
task1 = progress.add_task("[red]Downloading...", total=1000)
task2 = progress.add_task("[green]Processing...", total=1000)
task3 = progress.add_task("[cyan]Cooking...", total=1000)
while not progress.finished:
progress.update(task1, advance=0.5)
progress.update(task2, advance=0.3)
progress.update(task3, advance=0.9)
time.sleep(0.02)
finally:
progress.stop()
Note the use of the try / finally, to ensure that ``stop()`` is called.
Updating tasks
~~~~~~~~~~~~~~

View File

@ -18,6 +18,13 @@ If you supply a list of choices, the prompt will loop until the user enters one
>>> from rich.prompt import Prompt
>>> name = Prompt.ask("Enter your name", choices=["Paul", "Jessica", "Duncan"], default="Paul")
By default this is case sensitive, but you can set `case_sensitive=False` to make it case insensitive::
>>> from rich.prompt import Prompt
>>> name = Prompt.ask("Enter your name", choices=["Paul", "Jessica", "Duncan"], default="Paul", case_sensitive=False)
Now, it would accept "paul" or "Paul" as valid responses.
In addition to :class:`~rich.prompt.Prompt` which returns strings, you can also use :class:`~rich.prompt.IntPrompt` which asks the user for an integer, and :class:`~rich.prompt.FloatPrompt` for floats.
The :class:`~rich.prompt.Confirm` class is a specialized prompt which may be used to ask the user a simple yes / no question. Here's an example::
@ -30,4 +37,4 @@ The Prompt class was designed to be customizable via inheritance. See `prompt.py
To see some of the prompts in action, run the following command from the command line::
python -m rich.prompt
python -m rich.prompt

View File

@ -44,6 +44,7 @@ pytest-cov = "^3.0.0"
attrs = "^21.4.0"
pre-commit = "^2.17.0"
asv = "^0.5.1"
importlib-metadata = { version = "*", python = "<3.8" }
[build-system]
requires = ["poetry-core>=1.0.0"]

View File

@ -0,0 +1,11 @@
---
title: "Natively inserted ANSI escape sequence characters break alignment of Panel."
alt_titles:
- "Escape codes break alignment."
---
If you print ansi escape sequences for color and style you may find the output breaks your output.
You may find that border characters in Panel and Table are in the wrong place, for example.
As a general rule, you should allow Rich to generate all ansi escape sequences, so it can correctly account for these invisible characters.
If you can't avoid a string with escape codes, you can convert it to an equivalent `Text` instance with `Text.from_ansi`.

View File

@ -1,5 +1,5 @@
---
title: "python -m rich.spinner shows extra lines"
title: "python -m rich.spinner shows extra lines."
---
The spinner example is know to break on some terminals (Windows in particular).

View File

@ -0,0 +1,9 @@
---
title: "Rich is automatically installing traceback handler."
alt_titles:
- "Can you stop overriding traceback message formatting by default?"
---
Rich will never install the traceback handler automatically.
If you are getting Rich tracebacks and you don't want them, then some other piece of software is calling `rich.traceback.install()`.

View File

@ -1,5 +1,3 @@
from __future__ import absolute_import
import inspect
from inspect import cleandoc, getdoc, getfile, isclass, ismodule, signature
from typing import Any, Collection, Iterable, Optional, Tuple, Type, Union

View File

@ -240,6 +240,7 @@ class VerticalCenter(JupyterMixin):
Args:
renderable (RenderableType): A renderable object.
style (StyleType, optional): An optional style to apply to the background. Defaults to None.
"""
def __init__(

View File

@ -9,6 +9,7 @@ from .text import Text
re_ansi = re.compile(
r"""
(?:\x1b[0-?])|
(?:\x1b\](.*?)\x1b\\)|
(?:\x1b([(@-Z\\-_]|\[[0-?]*[ -/]*[@-~]))
""",

View File

@ -1,5 +1,5 @@
import platform
import re
import sys
from colorsys import rgb_to_hls
from enum import IntEnum
from functools import lru_cache
@ -15,7 +15,7 @@ if TYPE_CHECKING: # pragma: no cover
from .text import Text
WINDOWS = platform.system() == "Windows"
WINDOWS = sys.platform == "win32"
class ColorSystem(IntEnum):

View File

@ -1,6 +1,5 @@
import inspect
import os
import platform
import sys
import threading
import zlib
@ -76,7 +75,7 @@ if TYPE_CHECKING:
JUPYTER_DEFAULT_COLUMNS = 115
JUPYTER_DEFAULT_LINES = 100
WINDOWS = platform.system() == "Windows"
WINDOWS = sys.platform == "win32"
HighlighterType = Callable[[Union[str, "Text"]], "Text"]
JustifyMethod = Literal["default", "left", "center", "right", "full"]
@ -2166,7 +2165,7 @@ class Console:
"""
text = self.export_text(clear=clear, styles=styles)
with open(path, "wt", encoding="utf-8") as write_file:
with open(path, "w", encoding="utf-8") as write_file:
write_file.write(text)
def export_html(
@ -2272,7 +2271,7 @@ class Console:
code_format=code_format,
inline_styles=inline_styles,
)
with open(path, "wt", encoding="utf-8") as write_file:
with open(path, "w", encoding="utf-8") as write_file:
write_file.write(html)
def export_svg(
@ -2561,7 +2560,7 @@ class Console:
font_aspect_ratio=font_aspect_ratio,
unique_id=unique_id,
)
with open(path, "wt", encoding="utf-8") as write_file:
with open(path, "w", encoding="utf-8") as write_file:
write_file.write(svg)

View File

@ -54,7 +54,7 @@ DEFAULT_STYLES: Dict[str, Style] = {
"logging.level.notset": Style(dim=True),
"logging.level.debug": Style(color="green"),
"logging.level.info": Style(color="blue"),
"logging.level.warning": Style(color="red"),
"logging.level.warning": Style(color="yellow"),
"logging.level.error": Style(color="red", bold=True),
"logging.level.critical": Style(color="red", bold=True, reverse=True),
"log.level": Style.null(),

View File

@ -1,4 +1,3 @@
# coding: utf-8
"""Functions for reporting filesizes. Borrowed from https://github.com/PyFilesystem/pyfilesystem2
The functions declared in this module should cover the different
@ -27,7 +26,7 @@ def _to_str(
if size == 1:
return "1 byte"
elif size < base:
return "{:,} bytes".format(size)
return f"{size:,} bytes"
for i, suffix in enumerate(suffixes, 2): # noqa: B007
unit = base**i

View File

@ -36,11 +36,13 @@ class RichHandler(Handler):
markup (bool, optional): Enable console markup in log messages. Defaults to False.
rich_tracebacks (bool, optional): Enable rich tracebacks with syntax highlighting and formatting. Defaults to False.
tracebacks_width (Optional[int], optional): Number of characters used to render tracebacks, or None for full width. Defaults to None.
tracebacks_code_width (int, optional): Number of code characters used to render tracebacks, or None for full width. Defaults to 88.
tracebacks_extra_lines (int, optional): Additional lines of code to render tracebacks, or None for full width. Defaults to None.
tracebacks_theme (str, optional): Override pygments theme used in traceback.
tracebacks_word_wrap (bool, optional): Enable word wrapping of long tracebacks lines. Defaults to True.
tracebacks_show_locals (bool, optional): Enable display of locals in tracebacks. Defaults to False.
tracebacks_suppress (Sequence[Union[str, ModuleType]]): Optional sequence of modules or paths to exclude from traceback.
tracebacks_max_frames (int, optional): Optional maximum number of frames returned by traceback.
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.
@ -74,11 +76,13 @@ class RichHandler(Handler):
markup: bool = False,
rich_tracebacks: bool = False,
tracebacks_width: Optional[int] = None,
tracebacks_code_width: int = 88,
tracebacks_extra_lines: int = 3,
tracebacks_theme: Optional[str] = None,
tracebacks_word_wrap: bool = True,
tracebacks_show_locals: bool = False,
tracebacks_suppress: Iterable[Union[str, ModuleType]] = (),
tracebacks_max_frames: int = 100,
locals_max_length: int = 10,
locals_max_string: int = 80,
log_time_format: Union[str, FormatTimeCallable] = "[%x %X]",
@ -104,6 +108,8 @@ class RichHandler(Handler):
self.tracebacks_word_wrap = tracebacks_word_wrap
self.tracebacks_show_locals = tracebacks_show_locals
self.tracebacks_suppress = tracebacks_suppress
self.tracebacks_max_frames = tracebacks_max_frames
self.tracebacks_code_width = tracebacks_code_width
self.locals_max_length = locals_max_length
self.locals_max_string = locals_max_string
self.keywords = keywords
@ -140,6 +146,7 @@ class RichHandler(Handler):
exc_value,
exc_traceback,
width=self.tracebacks_width,
code_width=self.tracebacks_code_width,
extra_lines=self.tracebacks_extra_lines,
theme=self.tracebacks_theme,
word_wrap=self.tracebacks_word_wrap,
@ -147,6 +154,7 @@ class RichHandler(Handler):
locals_max_length=self.locals_max_length,
locals_max_string=self.locals_max_string,
suppress=self.tracebacks_suppress,
max_frames=self.tracebacks_max_frames,
)
message = record.getMessage()
if self.formatter:

View File

@ -1,7 +1,7 @@
from __future__ import annotations
import sys
from typing import ClassVar, Dict, Iterable, List, Optional, Type, Union
from typing import ClassVar, Iterable
from markdown_it import MarkdownIt
from markdown_it.token import Token
@ -31,7 +31,7 @@ class MarkdownElement:
new_line: ClassVar[bool] = True
@classmethod
def create(cls, markdown: "Markdown", token: Token) -> "MarkdownElement":
def create(cls, markdown: Markdown, token: Token) -> MarkdownElement:
"""Factory to create markdown element,
Args:
@ -43,30 +43,28 @@ class MarkdownElement:
"""
return cls()
def on_enter(self, context: "MarkdownContext") -> None:
def on_enter(self, context: MarkdownContext) -> None:
"""Called when the node is entered.
Args:
context (MarkdownContext): The markdown context.
"""
def on_text(self, context: "MarkdownContext", text: TextType) -> None:
def on_text(self, context: MarkdownContext, text: TextType) -> None:
"""Called when text is parsed.
Args:
context (MarkdownContext): The markdown context.
"""
def on_leave(self, context: "MarkdownContext") -> None:
def on_leave(self, context: MarkdownContext) -> None:
"""Called when the parser leaves the element.
Args:
context (MarkdownContext): [description]
"""
def on_child_close(
self, context: "MarkdownContext", child: "MarkdownElement"
) -> bool:
def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool:
"""Called when a child element is closed.
This method allows a parent element to take over rendering of its children.
@ -81,8 +79,8 @@ class MarkdownElement:
return True
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
self, console: Console, options: ConsoleOptions
) -> RenderResult:
return ()
@ -100,14 +98,14 @@ class TextElement(MarkdownElement):
style_name = "none"
def on_enter(self, context: "MarkdownContext") -> None:
def on_enter(self, context: MarkdownContext) -> None:
self.style = context.enter_style(self.style_name)
self.text = Text(justify="left")
def on_text(self, context: "MarkdownContext", text: TextType) -> None:
def on_text(self, context: MarkdownContext, text: TextType) -> None:
self.text.append(text, context.current_style if isinstance(text, str) else None)
def on_leave(self, context: "MarkdownContext") -> None:
def on_leave(self, context: MarkdownContext) -> None:
context.leave_style()
@ -118,7 +116,7 @@ class Paragraph(TextElement):
justify: JustifyMethod
@classmethod
def create(cls, markdown: "Markdown", token: Token) -> "Paragraph":
def create(cls, markdown: Markdown, token: Token) -> Paragraph:
return cls(justify=markdown.justify or "left")
def __init__(self, justify: JustifyMethod) -> None:
@ -135,10 +133,10 @@ class Heading(TextElement):
"""A heading."""
@classmethod
def create(cls, markdown: "Markdown", token: Token) -> "Heading":
def create(cls, markdown: Markdown, token: Token) -> Heading:
return cls(token.tag)
def on_enter(self, context: "MarkdownContext") -> None:
def on_enter(self, context: MarkdownContext) -> None:
self.text = Text()
context.enter_style(self.style_name)
@ -172,7 +170,7 @@ class CodeBlock(TextElement):
style_name = "markdown.code_block"
@classmethod
def create(cls, markdown: "Markdown", token: Token) -> "CodeBlock":
def create(cls, markdown: Markdown, token: Token) -> CodeBlock:
node_info = token.info or ""
lexer_name = node_info.partition(" ")[0]
return cls(lexer_name or "text", markdown.code_theme)
@ -199,9 +197,7 @@ class BlockQuote(TextElement):
def __init__(self) -> None:
self.elements: Renderables = Renderables()
def on_child_close(
self, context: "MarkdownContext", child: "MarkdownElement"
) -> bool:
def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool:
self.elements.append(child)
return False
@ -238,9 +234,7 @@ class TableElement(MarkdownElement):
self.header: TableHeaderElement | None = None
self.body: TableBodyElement | None = None
def on_child_close(
self, context: "MarkdownContext", child: "MarkdownElement"
) -> bool:
def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool:
if isinstance(child, TableHeaderElement):
self.header = child
elif isinstance(child, TableBodyElement):
@ -272,9 +266,7 @@ class TableHeaderElement(MarkdownElement):
def __init__(self) -> None:
self.row: TableRowElement | None = None
def on_child_close(
self, context: "MarkdownContext", child: "MarkdownElement"
) -> bool:
def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool:
assert isinstance(child, TableRowElement)
self.row = child
return False
@ -286,9 +278,7 @@ class TableBodyElement(MarkdownElement):
def __init__(self) -> None:
self.rows: list[TableRowElement] = []
def on_child_close(
self, context: "MarkdownContext", child: "MarkdownElement"
) -> bool:
def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool:
assert isinstance(child, TableRowElement)
self.rows.append(child)
return False
@ -298,11 +288,9 @@ class TableRowElement(MarkdownElement):
"""MarkdownElement corresponding to `tr_open` and `tr_close`."""
def __init__(self) -> None:
self.cells: List[TableDataElement] = []
self.cells: list[TableDataElement] = []
def on_child_close(
self, context: "MarkdownContext", child: "MarkdownElement"
) -> bool:
def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool:
assert isinstance(child, TableDataElement)
self.cells.append(child)
return False
@ -313,7 +301,7 @@ class TableDataElement(MarkdownElement):
and `th_open` and `th_close`."""
@classmethod
def create(cls, markdown: "Markdown", token: Token) -> "MarkdownElement":
def create(cls, markdown: Markdown, token: Token) -> MarkdownElement:
style = str(token.attrs.get("style")) or ""
justify: JustifyMethod
@ -333,7 +321,7 @@ class TableDataElement(MarkdownElement):
self.content: Text = Text("", justify=justify)
self.justify = justify
def on_text(self, context: "MarkdownContext", text: TextType) -> None:
def on_text(self, context: MarkdownContext, text: TextType) -> None:
text = Text(text) if isinstance(text, str) else text
text.stylize(context.current_style)
self.content.append_text(text)
@ -343,17 +331,15 @@ class ListElement(MarkdownElement):
"""A list element."""
@classmethod
def create(cls, markdown: "Markdown", token: Token) -> "ListElement":
def create(cls, markdown: Markdown, token: Token) -> ListElement:
return cls(token.type, int(token.attrs.get("start", 1)))
def __init__(self, list_type: str, list_start: int | None) -> None:
self.items: List[ListItem] = []
self.items: list[ListItem] = []
self.list_type = list_type
self.list_start = list_start
def on_child_close(
self, context: "MarkdownContext", child: "MarkdownElement"
) -> bool:
def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool:
assert isinstance(child, ListItem)
self.items.append(child)
return False
@ -381,9 +367,7 @@ class ListItem(TextElement):
def __init__(self) -> None:
self.elements: Renderables = Renderables()
def on_child_close(
self, context: "MarkdownContext", child: "MarkdownElement"
) -> bool:
def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bool:
self.elements.append(child)
return False
@ -419,7 +403,7 @@ class ListItem(TextElement):
class Link(TextElement):
@classmethod
def create(cls, markdown: "Markdown", token: Token) -> "MarkdownElement":
def create(cls, markdown: Markdown, token: Token) -> MarkdownElement:
url = token.attrs.get("href", "#")
return cls(token.content, str(url))
@ -434,7 +418,7 @@ class ImageItem(TextElement):
new_line = False
@classmethod
def create(cls, markdown: "Markdown", token: Token) -> "MarkdownElement":
def create(cls, markdown: Markdown, token: Token) -> MarkdownElement:
"""Factory to create markdown element,
Args:
@ -449,10 +433,10 @@ class ImageItem(TextElement):
def __init__(self, destination: str, hyperlinks: bool) -> None:
self.destination = destination
self.hyperlinks = hyperlinks
self.link: Optional[str] = None
self.link: str | None = None
super().__init__()
def on_enter(self, context: "MarkdownContext") -> None:
def on_enter(self, context: MarkdownContext) -> None:
self.link = context.current_style.link
self.text = Text(justify="left")
super().on_enter(context)
@ -476,7 +460,7 @@ class MarkdownContext:
console: Console,
options: ConsoleOptions,
style: Style,
inline_code_lexer: Optional[str] = None,
inline_code_lexer: str | None = None,
inline_code_theme: str = "monokai",
) -> None:
self.console = console
@ -484,7 +468,7 @@ class MarkdownContext:
self.style_stack: StyleStack = StyleStack(style)
self.stack: Stack[MarkdownElement] = Stack()
self._syntax: Optional[Syntax] = None
self._syntax: Syntax | None = None
if inline_code_lexer is not None:
self._syntax = Syntax("", inline_code_lexer, theme=inline_code_theme)
@ -504,7 +488,7 @@ class MarkdownContext:
else:
self.stack.top.on_text(self, text)
def enter_style(self, style_name: Union[str, Style]) -> Style:
def enter_style(self, style_name: str | Style) -> Style:
"""Enter a style context."""
style = self.console.get_style(style_name, default="none")
self.style_stack.push(style)
@ -521,7 +505,7 @@ class Markdown(JupyterMixin):
Args:
markup (str): A string containing markdown.
code_theme (str, optional): Pygments theme for code blocks. Defaults to "monokai".
code_theme (str, optional): Pygments theme for code blocks. Defaults to "monokai". See https://pygments.org/styles/ for code themes.
justify (JustifyMethod, optional): Justify value for paragraphs. Defaults to None.
style (Union[str, Style], optional): Optional style to apply to markdown.
hyperlinks (bool, optional): Enable hyperlinks. Defaults to ``True``.
@ -531,7 +515,7 @@ class Markdown(JupyterMixin):
highlighting, or None for no highlighting. Defaults to None.
"""
elements: ClassVar[Dict[str, Type[MarkdownElement]]] = {
elements: ClassVar[dict[str, type[MarkdownElement]]] = {
"paragraph_open": Paragraph,
"heading_open": Heading,
"fence": CodeBlock,
@ -556,17 +540,17 @@ class Markdown(JupyterMixin):
self,
markup: str,
code_theme: str = "monokai",
justify: Optional[JustifyMethod] = None,
style: Union[str, Style] = "none",
justify: JustifyMethod | None = None,
style: str | Style = "none",
hyperlinks: bool = True,
inline_code_lexer: Optional[str] = None,
inline_code_theme: Optional[str] = None,
inline_code_lexer: str | None = None,
inline_code_theme: str | None = None,
) -> None:
parser = MarkdownIt().enable("strikethrough").enable("table")
self.markup = markup
self.parsed = parser.parse(markup)
self.code_theme = code_theme
self.justify: Optional[JustifyMethod] = justify
self.justify: JustifyMethod | None = justify
self.style = style
self.hyperlinks = hyperlinks
self.inline_code_lexer = inline_code_lexer
@ -772,7 +756,7 @@ if __name__ == "__main__": # pragma: no cover
if args.path == "-":
markdown_body = sys.stdin.read()
else:
with open(args.path, "rt", encoding="utf-8") as markdown_file:
with open(args.path, encoding="utf-8") as markdown_file:
markdown_body = markdown_file.read()
markdown = Markdown(

View File

@ -22,11 +22,13 @@ class Panel(JupyterMixin):
Args:
renderable (RenderableType): A console renderable object.
box (Box, optional): A Box instance that defines the look of the border (see :ref:`appendix_box`.
Defaults to box.ROUNDED.
box (Box, optional): A Box instance that defines the look of the border (see :ref:`appendix_box`. Defaults to box.ROUNDED.
title (Optional[TextType], optional): Optional title displayed in panel header. Defaults to None.
title_align (AlignMethod, optional): Alignment of title. Defaults to "center".
subtitle (Optional[TextType], optional): Optional subtitle displayed in panel footer. Defaults to None.
subtitle_align (AlignMethod, optional): Alignment of subtitle. Defaults to "center".
safe_box (bool, optional): Disable box characters that don't display on windows legacy terminal with *raster* fonts. Defaults to True.
expand (bool, optional): If True the panel will stretch to fill the console
width, otherwise it will be sized to fit the contents. Defaults to True.
expand (bool, optional): If True the panel will stretch to fill the console width, otherwise it will be sized to fit the contents. Defaults to True.
style (str, optional): The style of the panel (border and contents). Defaults to "none".
border_style (str, optional): The style of the border. Defaults to "none".
width (Optional[int], optional): Optional width of panel. Defaults to None to auto-detect.
@ -144,7 +146,8 @@ class Panel(JupyterMixin):
Padding(self.renderable, _padding) if any(_padding) else self.renderable
)
style = console.get_style(self.style)
border_style = style + console.get_style(self.border_style)
partial_border_style = console.get_style(self.border_style)
border_style = style + partial_border_style
width = (
options.max_width
if self.width is None
@ -200,7 +203,7 @@ class Panel(JupyterMixin):
title_text = self._title
if title_text is not None:
title_text.stylize_before(border_style)
title_text.stylize_before(partial_border_style)
child_width = (
width - 2
@ -249,7 +252,7 @@ class Panel(JupyterMixin):
subtitle_text = self._subtitle
if subtitle_text is not None:
subtitle_text.stylize_before(border_style)
subtitle_text.stylize_before(partial_border_style)
if subtitle_text is None or width <= 4:
yield Segment(box.get_bottom([width - 2]), border_style)

View File

@ -15,6 +15,7 @@ from typing import (
Any,
Callable,
DefaultDict,
Deque,
Dict,
Iterable,
List,
@ -130,17 +131,19 @@ def _ipy_display_hook(
if _safe_isinstance(value, ConsoleRenderable):
console.line()
console.print(
value
if _safe_isinstance(value, RichRenderable)
else Pretty(
value,
overflow=overflow,
indent_guides=indent_guides,
max_length=max_length,
max_string=max_string,
max_depth=max_depth,
expand_all=expand_all,
margin=12,
(
value
if _safe_isinstance(value, RichRenderable)
else Pretty(
value,
overflow=overflow,
indent_guides=indent_guides,
max_length=max_length,
max_string=max_string,
max_depth=max_depth,
expand_all=expand_all,
margin=12,
)
),
crop=crop,
new_line_start=True,
@ -196,16 +199,18 @@ def install(
assert console is not None
builtins._ = None # type: ignore[attr-defined]
console.print(
value
if _safe_isinstance(value, RichRenderable)
else Pretty(
value,
overflow=overflow,
indent_guides=indent_guides,
max_length=max_length,
max_string=max_string,
max_depth=max_depth,
expand_all=expand_all,
(
value
if _safe_isinstance(value, RichRenderable)
else Pretty(
value,
overflow=overflow,
indent_guides=indent_guides,
max_length=max_length,
max_string=max_string,
max_depth=max_depth,
expand_all=expand_all,
)
),
crop=crop,
)
@ -353,6 +358,16 @@ def _get_braces_for_defaultdict(_object: DefaultDict[Any, Any]) -> Tuple[str, st
)
def _get_braces_for_deque(_object: Deque[Any]) -> Tuple[str, str, str]:
if _object.maxlen is None:
return ("deque([", "])", "deque()")
return (
"deque([",
f"], maxlen={_object.maxlen})",
f"deque(maxlen={_object.maxlen})",
)
def _get_braces_for_array(_object: "array[Any]") -> Tuple[str, str, str]:
return (f"array({_object.typecode!r}, [", "])", f"array({_object.typecode!r})")
@ -362,7 +377,7 @@ _BRACES: Dict[type, Callable[[Any], Tuple[str, str, str]]] = {
array: _get_braces_for_array,
defaultdict: _get_braces_for_defaultdict,
Counter: lambda _object: ("Counter({", "})", "Counter()"),
deque: lambda _object: ("deque([", "])", "deque()"),
deque: _get_braces_for_deque,
dict: lambda _object: ("{", "}", "{}"),
UserDict: lambda _object: ("{", "}", "{}"),
frozenset: lambda _object: ("frozenset({", "})", "frozenset()"),
@ -846,7 +861,7 @@ def traverse(
pop_visited(obj_id)
else:
node = Node(value_repr=to_repr(obj), last=root)
node.is_tuple = _safe_isinstance(obj, tuple)
node.is_tuple = type(obj) == tuple
node.is_namedtuple = _is_namedtuple(obj)
return node

View File

@ -70,7 +70,7 @@ class _TrackThread(Thread):
self.done = Event()
self.completed = 0
super().__init__()
super().__init__(daemon=True)
def run(self) -> None:
task_id = self.task_id
@ -78,7 +78,7 @@ class _TrackThread(Thread):
update_period = self.update_period
last_completed = 0
wait = self.done.wait
while not wait(update_period):
while not wait(update_period) and self.progress.live.is_started:
completed = self.completed
if last_completed != completed:
advance(task_id, completed - last_completed)
@ -104,6 +104,7 @@ def track(
sequence: Union[Sequence[ProgressType], Iterable[ProgressType]],
description: str = "Working...",
total: Optional[float] = None,
completed: int = 0,
auto_refresh: bool = True,
console: Optional[Console] = None,
transient: bool = False,
@ -123,6 +124,7 @@ def track(
sequence (Iterable[ProgressType]): A sequence (must support "len") you wish to iterate over.
description (str, optional): Description of task show next to progress bar. Defaults to "Working".
total: (float, optional): Total number of steps. Default is len(sequence).
completed (int, optional): Number of steps completed so far. Defaults to 0.
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.
@ -166,7 +168,11 @@ def track(
with progress:
yield from progress.track(
sequence, total=total, description=description, update_period=update_period
sequence,
total=total,
completed=completed,
description=description,
update_period=update_period,
)
@ -1161,7 +1167,7 @@ class Progress(JupyterMixin):
def stop(self) -> None:
"""Stop the progress display."""
self.live.stop()
if not self.console.is_interactive:
if not self.console.is_interactive and not self.console.is_jupyter:
self.console.print()
def __enter__(self) -> "Progress":
@ -1180,6 +1186,7 @@ class Progress(JupyterMixin):
self,
sequence: Union[Iterable[ProgressType], Sequence[ProgressType]],
total: Optional[float] = None,
completed: int = 0,
task_id: Optional[TaskID] = None,
description: str = "Working...",
update_period: float = 0.1,
@ -1189,6 +1196,7 @@ class Progress(JupyterMixin):
Args:
sequence (Sequence[ProgressType]): A sequence of values you want to iterate over and track progress.
total: (float, optional): Total number of steps. Default is len(sequence).
completed (int, optional): Number of steps completed so far. Defaults to 0.
task_id: (TaskID): Task to track. Default is new task.
description: (str, optional): Description of task, if new task is created.
update_period (float, optional): Minimum time (in seconds) between calls to update(). Defaults to 0.1.
@ -1200,9 +1208,9 @@ class Progress(JupyterMixin):
total = float(length_hint(sequence)) or None
if task_id is None:
task_id = self.add_task(description, total=total)
task_id = self.add_task(description, total=total, completed=completed)
else:
self.update(task_id, total=total)
self.update(task_id, total=total, completed=completed)
if self.live.auto_refresh:
with _TrackThread(self, task_id, update_period) as track_thread:
@ -1326,7 +1334,7 @@ class Progress(JupyterMixin):
# normalize the mode (always rb, rt)
_mode = "".join(sorted(mode, reverse=False))
if _mode not in ("br", "rt", "r"):
raise ValueError("invalid mode {!r}".format(mode))
raise ValueError(f"invalid mode {mode!r}")
# patch buffering to provide the same behaviour as the builtin `open`
line_buffering = buffering == 1

View File

@ -108,7 +108,7 @@ class ProgressBar(JupyterMixin):
for index in range(PULSE_SIZE):
position = index / PULSE_SIZE
fade = 0.5 + cos((position * pi * 2)) / 2.0
fade = 0.5 + cos(position * pi * 2) / 2.0
color = blend_rgb(fore_color, back_color, cross_fade=fade)
append(_Segment(bar, _Style(color=from_triplet(color))))
return segments

View File

@ -36,6 +36,7 @@ class PromptBase(Generic[PromptType]):
console (Console, optional): A Console instance or None to use global console. Defaults to None.
password (bool, optional): Enable password input. Defaults to False.
choices (List[str], optional): A list of valid choices. Defaults to None.
case_sensitive (bool, optional): Matching of choices should be case-sensitive. Defaults to True.
show_default (bool, optional): Show default in prompt. Defaults to True.
show_choices (bool, optional): Show choices in prompt. Defaults to True.
"""
@ -57,6 +58,7 @@ class PromptBase(Generic[PromptType]):
console: Optional[Console] = None,
password: bool = False,
choices: Optional[List[str]] = None,
case_sensitive: bool = True,
show_default: bool = True,
show_choices: bool = True,
) -> None:
@ -69,6 +71,7 @@ class PromptBase(Generic[PromptType]):
self.password = password
if choices is not None:
self.choices = choices
self.case_sensitive = case_sensitive
self.show_default = show_default
self.show_choices = show_choices
@ -81,6 +84,7 @@ class PromptBase(Generic[PromptType]):
console: Optional[Console] = None,
password: bool = False,
choices: Optional[List[str]] = None,
case_sensitive: bool = True,
show_default: bool = True,
show_choices: bool = True,
default: DefaultType,
@ -97,6 +101,7 @@ class PromptBase(Generic[PromptType]):
console: Optional[Console] = None,
password: bool = False,
choices: Optional[List[str]] = None,
case_sensitive: bool = True,
show_default: bool = True,
show_choices: bool = True,
stream: Optional[TextIO] = None,
@ -111,6 +116,7 @@ class PromptBase(Generic[PromptType]):
console: Optional[Console] = None,
password: bool = False,
choices: Optional[List[str]] = None,
case_sensitive: bool = True,
show_default: bool = True,
show_choices: bool = True,
default: Any = ...,
@ -126,6 +132,7 @@ class PromptBase(Generic[PromptType]):
console (Console, optional): A Console instance or None to use global console. Defaults to None.
password (bool, optional): Enable password input. Defaults to False.
choices (List[str], optional): A list of valid choices. Defaults to None.
case_sensitive (bool, optional): Matching of choices should be case-sensitive. Defaults to True.
show_default (bool, optional): Show default in prompt. Defaults to True.
show_choices (bool, optional): Show choices in prompt. Defaults to True.
stream (TextIO, optional): Optional text file open for reading to get input. Defaults to None.
@ -135,6 +142,7 @@ class PromptBase(Generic[PromptType]):
console=console,
password=password,
choices=choices,
case_sensitive=case_sensitive,
show_default=show_default,
show_choices=show_choices,
)
@ -212,7 +220,9 @@ class PromptBase(Generic[PromptType]):
bool: True if choice was valid, otherwise False.
"""
assert self.choices is not None
return value.strip() in self.choices
if self.case_sensitive:
return value.strip() in self.choices
return value.strip().lower() in [choice.lower() for choice in self.choices]
def process_response(self, value: str) -> PromptType:
"""Process response from user, convert to prompt type.
@ -232,9 +242,17 @@ class PromptBase(Generic[PromptType]):
except ValueError:
raise InvalidResponse(self.validate_error_message)
if self.choices is not None and not self.check_choice(value):
raise InvalidResponse(self.illegal_choice_message)
if self.choices is not None:
if not self.check_choice(value):
raise InvalidResponse(self.illegal_choice_message)
if not self.case_sensitive:
# return the original choice, not the lower case version
return_value = self.response_type(
self.choices[
[choice.lower() for choice in self.choices].index(value.lower())
]
)
return return_value
def on_validate_error(self, value: str, error: InvalidResponse) -> None:
@ -371,5 +389,12 @@ if __name__ == "__main__": # pragma: no cover
fruit = Prompt.ask("Enter a fruit", choices=["apple", "orange", "pear"])
print(f"fruit={fruit!r}")
doggie = Prompt.ask(
"What's the best Dog? (Case INSENSITIVE)",
choices=["Border Terrier", "Collie", "Labradoodle"],
case_sensitive=False,
)
print(f"doggie={doggie!r}")
else:
print("[b]OK :loudly_crying_face:")

View File

@ -663,7 +663,7 @@ class Style:
style._set_attributes = self._set_attributes
style._link = None
style._link_id = ""
style._hash = self._hash
style._hash = None
style._null = False
style._meta = None
return style

View File

@ -1,5 +1,4 @@
import os.path
import platform
import re
import sys
import textwrap
@ -52,7 +51,7 @@ from .text import Text
TokenType = Tuple[str, ...]
WINDOWS = platform.system() == "Windows"
WINDOWS = sys.platform == "win32"
DEFAULT_THEME = "monokai"
# The following styles are based on https://github.com/pygments/pygments/blob/master/pygments/formatters/terminal.py

View File

@ -106,6 +106,9 @@ class Column:
no_wrap: bool = False
"""bool: Prevent wrapping of text within the column. Defaults to ``False``."""
highlight: bool = False
"""bool: Apply highlighter to column. Defaults to ``False``."""
_index: int = 0
"""Index of column."""
@ -365,6 +368,7 @@ class Table(JupyterMixin):
footer: "RenderableType" = "",
*,
header_style: Optional[StyleType] = None,
highlight: Optional[bool] = None,
footer_style: Optional[StyleType] = None,
style: Optional[StyleType] = None,
justify: "JustifyMethod" = "left",
@ -384,6 +388,7 @@ class Table(JupyterMixin):
footer (RenderableType, optional): Text or renderable for the footer.
Defaults to "".
header_style (Union[str, Style], optional): Style for the header, or None for default. Defaults to None.
highlight (bool, optional): Whether to highlight the text. The default of None uses the value of the table (self) object.
footer_style (Union[str, Style], optional): Style for the footer, or None for default. Defaults to None.
style (Union[str, Style], optional): Style for the column cells, or None for default. Defaults to None.
justify (JustifyMethod, optional): Alignment for cells. Defaults to "left".
@ -401,6 +406,7 @@ class Table(JupyterMixin):
header=header,
footer=footer,
header_style=header_style or "",
highlight=highlight if highlight is not None else self.highlight,
footer_style=footer_style or "",
style=style or "",
justify=justify,
@ -775,16 +781,16 @@ class Table(JupyterMixin):
_Segment(_box.head_right, border_style),
_Segment(_box.head_vertical, border_style),
),
(
_Segment(_box.foot_left, border_style),
_Segment(_box.foot_right, border_style),
_Segment(_box.foot_vertical, border_style),
),
(
_Segment(_box.mid_left, border_style),
_Segment(_box.mid_right, border_style),
_Segment(_box.mid_vertical, border_style),
),
(
_Segment(_box.foot_left, border_style),
_Segment(_box.foot_right, border_style),
_Segment(_box.foot_vertical, border_style),
),
]
if show_edge:
yield _Segment(_box.get_top(widths), border_style)
@ -818,6 +824,7 @@ class Table(JupyterMixin):
no_wrap=column.no_wrap,
overflow=column.overflow,
height=None,
highlight=column.highlight,
)
lines = console.render_lines(
cell.renderable,

View File

@ -1,5 +1,5 @@
import configparser
from typing import Dict, List, IO, Mapping, Optional
from typing import IO, Dict, List, Mapping, Optional
from .default_styles import DEFAULT_STYLES
from .style import Style, StyleType
@ -69,7 +69,7 @@ class Theme:
Returns:
Theme: A new theme instance.
"""
with open(path, "rt", encoding=encoding) as config_file:
with open(path, encoding=encoding) as config_file:
return cls.from_file(config_file, source=path, inherit=inherit)

View File

@ -1,8 +1,5 @@
from __future__ import absolute_import
import linecache
import os
import platform
import sys
from dataclasses import dataclass, field
from traceback import walk_tb
@ -39,7 +36,7 @@ from .syntax import Syntax
from .text import Text
from .theme import Theme
WINDOWS = platform.system() == "Windows"
WINDOWS = sys.platform == "win32"
LOCALS_MAX_LENGTH = 10
LOCALS_MAX_STRING = 80
@ -49,6 +46,7 @@ def install(
*,
console: Optional[Console] = None,
width: Optional[int] = 100,
code_width: Optional[int] = 88,
extra_lines: int = 3,
theme: Optional[str] = None,
word_wrap: bool = False,
@ -69,6 +67,7 @@ def install(
Args:
console (Optional[Console], optional): Console to write exception to. Default uses internal Console instance.
width (Optional[int], optional): Width (in characters) of traceback. Defaults to 100.
code_width (Optional[int], optional): Code width (in characters) of traceback. Defaults to 88.
extra_lines (int, optional): Extra lines of code. Defaults to 3.
theme (Optional[str], optional): Pygments theme to use in traceback. Defaults to ``None`` which will pick
a theme appropriate for the platform.
@ -105,6 +104,7 @@ def install(
value,
traceback,
width=width,
code_width=code_width,
extra_lines=extra_lines,
theme=theme,
word_wrap=word_wrap,
@ -215,6 +215,7 @@ class Traceback:
trace (Trace, optional): A `Trace` object produced from `extract`. Defaults to None, which uses
the last exception.
width (Optional[int], optional): Number of characters used to traceback. Defaults to 100.
code_width (Optional[int], optional): Number of code characters used to traceback. Defaults to 88.
extra_lines (int, optional): Additional lines of code to render. Defaults to 3.
theme (str, optional): Override pygments theme used in traceback.
word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
@ -243,6 +244,7 @@ class Traceback:
trace: Optional[Trace] = None,
*,
width: Optional[int] = 100,
code_width: Optional[int] = 88,
extra_lines: int = 3,
theme: Optional[str] = None,
word_wrap: bool = False,
@ -266,6 +268,7 @@ class Traceback:
)
self.trace = trace
self.width = width
self.code_width = code_width
self.extra_lines = extra_lines
self.theme = Syntax.get_theme(theme or "ansi_dark")
self.word_wrap = word_wrap
@ -297,6 +300,7 @@ class Traceback:
traceback: Optional[TracebackType],
*,
width: Optional[int] = 100,
code_width: Optional[int] = 88,
extra_lines: int = 3,
theme: Optional[str] = None,
word_wrap: bool = False,
@ -316,6 +320,7 @@ class Traceback:
exc_value (BaseException): Exception value.
traceback (TracebackType): Python Traceback object.
width (Optional[int], optional): Number of characters used to traceback. Defaults to 100.
code_width (Optional[int], optional): Number of code characters used to traceback. Defaults to 88.
extra_lines (int, optional): Additional lines of code to render. Defaults to 3.
theme (str, optional): Override pygments theme used in traceback.
word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
@ -346,6 +351,7 @@ class Traceback:
return cls(
rich_traceback,
width=width,
code_width=code_width,
extra_lines=extra_lines,
theme=theme,
word_wrap=word_wrap,
@ -695,7 +701,7 @@ class Traceback:
),
highlight_lines={frame.lineno},
word_wrap=self.word_wrap,
code_width=88,
code_width=self.code_width,
indent_guides=self.indent_guides,
dedent=False,
)

View File

@ -18,6 +18,7 @@ class Tree(JupyterMixin):
guide_style (StyleType, optional): Style of the guide lines. Defaults to "tree.line".
expanded (bool, optional): Also display children. Defaults to True.
highlight (bool, optional): Highlight renderable (if str). Defaults to False.
hide_root (bool, optional): Hide the root node. Defaults to False.
"""
def __init__(

8
tests/conftest.py Normal file
View File

@ -0,0 +1,8 @@
import pytest
@pytest.fixture(autouse=True)
def reset_color_envvars(monkeypatch):
"""Remove color-related envvars to fix test output"""
monkeypatch.delenv("FORCE_COLOR", raising=False)
monkeypatch.delenv("NO_COLOR", raising=False)

View File

@ -68,3 +68,17 @@ def test_decode_issue_2688(ansi_bytes, expected_text):
text = Text.from_ansi(ansi_bytes.decode())
assert str(text) == expected_text
@pytest.mark.parametrize("code", [*"0123456789:;<=>?"])
def test_strip_private_escape_sequences(code):
text = Text.from_ansi(f"\x1b{code}x")
console = Console(force_terminal=True)
with console.capture() as capture:
console.print(text)
expected = "x\n"
assert capture.get() == expected

View File

@ -1,7 +1,19 @@
import pytest
from rich.console import ConsoleOptions, ConsoleDimensions
from rich.box import ASCII, DOUBLE, ROUNDED, HEAVY, SQUARE
from rich.box import (
ASCII,
DOUBLE,
ROUNDED,
HEAVY,
SQUARE,
MINIMAL_HEAVY_HEAD,
MINIMAL,
SIMPLE_HEAVY,
SIMPLE,
HEAVY_EDGE,
HEAVY_HEAD,
)
def test_str():
@ -36,7 +48,26 @@ def test_get_bottom():
assert bottom == "┗━┻━━┻━━━┛"
def test_box_substitute():
def test_box_substitute_for_same_box():
options = ConsoleOptions(
ConsoleDimensions(80, 25),
legacy_windows=False,
min_width=1,
max_width=100,
is_terminal=True,
encoding="utf-8",
max_height=25,
)
assert ROUNDED.substitute(options) == ROUNDED
assert MINIMAL_HEAVY_HEAD.substitute(options) == MINIMAL_HEAVY_HEAD
assert SIMPLE_HEAVY.substitute(options) == SIMPLE_HEAVY
assert HEAVY.substitute(options) == HEAVY
assert HEAVY_EDGE.substitute(options) == HEAVY_EDGE
assert HEAVY_HEAD.substitute(options) == HEAVY_HEAD
def test_box_substitute_for_different_box_legacy_windows():
options = ConsoleOptions(
ConsoleDimensions(80, 25),
legacy_windows=True,
@ -46,10 +77,29 @@ def test_box_substitute():
encoding="utf-8",
max_height=25,
)
assert ROUNDED.substitute(options) == SQUARE
assert MINIMAL_HEAVY_HEAD.substitute(options) == MINIMAL
assert SIMPLE_HEAVY.substitute(options) == SIMPLE
assert HEAVY.substitute(options) == SQUARE
assert HEAVY_EDGE.substitute(options) == SQUARE
assert HEAVY_HEAD.substitute(options) == SQUARE
options.legacy_windows = False
assert HEAVY.substitute(options) == HEAVY
options.encoding = "ascii"
def test_box_substitute_for_different_box_ascii_encoding():
options = ConsoleOptions(
ConsoleDimensions(80, 25),
legacy_windows=True,
min_width=1,
max_width=100,
is_terminal=True,
encoding="ascii",
max_height=25,
)
assert ROUNDED.substitute(options) == ASCII
assert MINIMAL_HEAVY_HEAD.substitute(options) == ASCII
assert SIMPLE_HEAVY.substitute(options) == ASCII
assert HEAVY.substitute(options) == ASCII
assert HEAVY_EDGE.substitute(options) == ASCII
assert HEAVY_HEAD.substitute(options) == ASCII

View File

@ -43,6 +43,11 @@ skip_py312 = pytest.mark.skipif(
reason="rendered differently on py3.12",
)
skip_py313 = pytest.mark.skipif(
sys.version_info.minor == 13 and sys.version_info.major == 3,
reason="rendered differently on py3.13",
)
skip_pypy3 = pytest.mark.skipif(
hasattr(sys, "pypy_version_info"),
reason="rendered differently on pypy3",
@ -140,6 +145,7 @@ def test_inspect_empty_dict():
assert render({}).startswith(expected)
@skip_py313
@skip_py312
@skip_py311
@skip_pypy3
@ -219,6 +225,7 @@ def test_inspect_integer_with_value():
@skip_py310
@skip_py311
@skip_py312
@skip_py313
def test_inspect_integer_with_methods_python38_and_python39():
expected = (
"╭──────────────── <class 'int'> ─────────────────╮\n"
@ -257,6 +264,7 @@ def test_inspect_integer_with_methods_python38_and_python39():
@skip_py39
@skip_py311
@skip_py312
@skip_py313
def test_inspect_integer_with_methods_python310only():
expected = (
"╭──────────────── <class 'int'> ─────────────────╮\n"
@ -299,6 +307,7 @@ def test_inspect_integer_with_methods_python310only():
@skip_py39
@skip_py310
@skip_py312
@skip_py313
def test_inspect_integer_with_methods_python311():
# to_bytes and from_bytes methods on int had minor signature change -
# they now, as of 3.11, have default values for all of their parameters

View File

@ -2,7 +2,7 @@ import collections
import io
import sys
from array import array
from collections import UserDict, defaultdict
from collections import UserDict, defaultdict, deque
from dataclasses import dataclass, field
from typing import Any, List, NamedTuple
@ -38,9 +38,13 @@ skip_py312 = pytest.mark.skipif(
sys.version_info.minor == 12 and sys.version_info.major == 3,
reason="rendered differently on py3.12",
)
skip_py313 = pytest.mark.skipif(
sys.version_info.minor == 13 and sys.version_info.major == 3,
reason="rendered differently on py3.13",
)
def test_install():
def test_install() -> None:
console = Console(file=io.StringIO())
dh = sys.displayhook
install(console)
@ -49,7 +53,7 @@ def test_install():
assert sys.displayhook is not dh
def test_install_max_depth():
def test_install_max_depth() -> None:
console = Console(file=io.StringIO())
dh = sys.displayhook
install(console, max_depth=1)
@ -58,7 +62,7 @@ def test_install_max_depth():
assert sys.displayhook is not dh
def test_ipy_display_hook__repr_html():
def test_ipy_display_hook__repr_html() -> None:
console = Console(file=io.StringIO(), force_jupyter=True)
class Thing:
@ -72,7 +76,7 @@ def test_ipy_display_hook__repr_html():
assert console.end_capture() == ""
def test_ipy_display_hook__multiple_special_reprs():
def test_ipy_display_hook__multiple_special_reprs() -> None:
"""
The case where there are multiple IPython special _repr_*_
methods on the object, and one of them returns None but another
@ -94,7 +98,7 @@ def test_ipy_display_hook__multiple_special_reprs():
assert result == "A Thing"
def test_ipy_display_hook__no_special_repr_methods():
def test_ipy_display_hook__no_special_repr_methods() -> None:
console = Console(file=io.StringIO(), force_jupyter=True)
class Thing:
@ -106,7 +110,7 @@ def test_ipy_display_hook__no_special_repr_methods():
assert result == "hello"
def test_ipy_display_hook__special_repr_raises_exception():
def test_ipy_display_hook__special_repr_raises_exception() -> None:
"""
When an IPython special repr method raises an exception,
we treat it as if it doesn't exist and look for the next.
@ -130,14 +134,14 @@ def test_ipy_display_hook__special_repr_raises_exception():
assert result == "therepr"
def test_ipy_display_hook__console_renderables_on_newline():
def test_ipy_display_hook__console_renderables_on_newline() -> None:
console = Console(file=io.StringIO(), force_jupyter=True)
console.begin_capture()
result = _ipy_display_hook(Text("hello"), console=console)
assert result == "\nhello"
def test_pretty():
def test_pretty() -> None:
test = {
"foo": [1, 2, 3, (4, 5, {6}, 7, 8, {9}), {}],
"bar": {"egg": "baz", "words": ["Hello World"] * 10},
@ -167,7 +171,7 @@ class Empty:
pass
def test_pretty_dataclass():
def test_pretty_dataclass() -> None:
dc = ExampleDataclass(1000, "Hello, World", 999, ["foo", "bar", "baz"])
result = pretty_repr(dc, max_width=80)
print(repr(result))
@ -187,7 +191,7 @@ def test_pretty_dataclass():
assert result == "ExampleDataclass(foo=1000, bar=..., baz=['foo', 'bar', 'baz'])"
def test_empty_dataclass():
def test_empty_dataclass() -> None:
assert pretty_repr(Empty()) == "Empty()"
assert pretty_repr([Empty()]) == "[Empty()]"
@ -200,7 +204,7 @@ class StockKeepingUnit(NamedTuple):
reviews: List[str]
def test_pretty_namedtuple():
def test_pretty_namedtuple() -> None:
console = Console(color_system=None)
console.begin_capture()
@ -227,17 +231,17 @@ def test_pretty_namedtuple():
)
def test_pretty_namedtuple_length_one_no_trailing_comma():
def test_pretty_namedtuple_length_one_no_trailing_comma() -> None:
instance = collections.namedtuple("Thing", ["name"])(name="Bob")
assert pretty_repr(instance) == "Thing(name='Bob')"
def test_pretty_namedtuple_empty():
def test_pretty_namedtuple_empty() -> None:
instance = collections.namedtuple("Thing", [])()
assert pretty_repr(instance) == "Thing()"
def test_pretty_namedtuple_custom_repr():
def test_pretty_namedtuple_custom_repr() -> None:
class Thing(NamedTuple):
def __repr__(self):
return "XX"
@ -245,7 +249,7 @@ def test_pretty_namedtuple_custom_repr():
assert pretty_repr(Thing()) == "XX"
def test_pretty_namedtuple_fields_invalid_type():
def test_pretty_namedtuple_fields_invalid_type() -> None:
class LooksLikeANamedTupleButIsnt(tuple):
_fields = "blah"
@ -254,20 +258,20 @@ def test_pretty_namedtuple_fields_invalid_type():
assert result == "()" # Treated as tuple
def test_pretty_namedtuple_max_depth():
def test_pretty_namedtuple_max_depth() -> None:
instance = {"unit": StockKeepingUnit("a", "b", 1.0, "c", ["d", "e"])}
result = pretty_repr(instance, max_depth=1)
assert result == "{'unit': StockKeepingUnit(...)}"
def test_small_width():
def test_small_width() -> None:
test = ["Hello world! 12345"]
result = pretty_repr(test, max_width=10)
expected = "[\n 'Hello world! 12345'\n]"
assert result == expected
def test_ansi_in_pretty_repr():
def test_ansi_in_pretty_repr() -> None:
class Hello:
def __repr__(self):
return "Hello \x1b[38;5;239mWorld!"
@ -281,7 +285,7 @@ def test_ansi_in_pretty_repr():
assert result == "Hello World!\n"
def test_broken_repr():
def test_broken_repr() -> None:
class BrokenRepr:
def __repr__(self):
1 / 0
@ -292,7 +296,7 @@ def test_broken_repr():
assert result == expected
def test_broken_getattr():
def test_broken_getattr() -> None:
class BrokenAttr:
def __getattr__(self, name):
1 / 0
@ -305,7 +309,7 @@ def test_broken_getattr():
assert result == "BrokenAttr()"
def test_reference_cycle_container():
def test_reference_cycle_container() -> None:
test = []
test.append(test)
res = pretty_repr(test)
@ -323,7 +327,7 @@ def test_reference_cycle_container():
assert res == "[1, [[2], [2]]]"
def test_reference_cycle_namedtuple():
def test_reference_cycle_namedtuple() -> None:
class Example(NamedTuple):
x: int
y: Any
@ -340,7 +344,7 @@ def test_reference_cycle_namedtuple():
assert res == "Example(x=1, y=[Example(x=2, y=None), Example(x=2, y=None)])"
def test_reference_cycle_dataclass():
def test_reference_cycle_dataclass() -> None:
@dataclass
class Example:
x: int
@ -363,7 +367,7 @@ def test_reference_cycle_dataclass():
assert res == "Example(x=1, y=[Example(x=2, y=None), Example(x=2, y=None)])"
def test_reference_cycle_attrs():
def test_reference_cycle_attrs() -> None:
@attr.define
class Example:
x: int
@ -386,7 +390,7 @@ def test_reference_cycle_attrs():
assert res == "Example(x=1, y=[Example(x=2, y=None), Example(x=2, y=None)])"
def test_reference_cycle_custom_repr():
def test_reference_cycle_custom_repr() -> None:
class Example:
def __init__(self, x, y):
self.x = x
@ -413,7 +417,7 @@ def test_reference_cycle_custom_repr():
assert res == "Example(x=1, y=[Example(x=2, y=None), Example(x=2, y=None)])"
def test_max_depth():
def test_max_depth() -> None:
d = {}
d["foo"] = {"fob": {"a": [1, 2, 3], "b": {"z": "x", "y": ["a", "b", "c"]}}}
@ -435,7 +439,7 @@ def test_max_depth():
)
def test_max_depth_rich_repr():
def test_max_depth_rich_repr() -> None:
class Foo:
def __init__(self, foo):
self.foo = foo
@ -456,7 +460,7 @@ def test_max_depth_rich_repr():
)
def test_max_depth_attrs():
def test_max_depth_attrs() -> None:
@attr.define
class Foo:
foo = attr.field()
@ -471,7 +475,7 @@ def test_max_depth_attrs():
)
def test_max_depth_dataclass():
def test_max_depth_dataclass() -> None:
@dataclass
class Foo:
foo: object
@ -486,28 +490,55 @@ def test_max_depth_dataclass():
)
def test_defaultdict():
def test_defaultdict() -> None:
test_dict = defaultdict(int, {"foo": 2})
result = pretty_repr(test_dict)
assert result == "defaultdict(<class 'int'>, {'foo': 2})"
def test_array():
def test_deque() -> None:
test_deque = deque([1, 2, 3])
result = pretty_repr(test_deque)
assert result == "deque([1, 2, 3])"
test_deque = deque([1, 2, 3], maxlen=None)
result = pretty_repr(test_deque)
assert result == "deque([1, 2, 3])"
test_deque = deque([1, 2, 3], maxlen=5)
result = pretty_repr(test_deque)
assert result == "deque([1, 2, 3], maxlen=5)"
test_deque = deque([1, 2, 3], maxlen=0)
result = pretty_repr(test_deque)
assert result == "deque(maxlen=0)"
test_deque = deque([])
result = pretty_repr(test_deque)
assert result == "deque()"
test_deque = deque([], maxlen=None)
result = pretty_repr(test_deque)
assert result == "deque()"
test_deque = deque([], maxlen=5)
result = pretty_repr(test_deque)
assert result == "deque(maxlen=5)"
test_deque = deque([], maxlen=0)
result = pretty_repr(test_deque)
assert result == "deque(maxlen=0)"
def test_array() -> None:
test_array = array("I", [1, 2, 3])
result = pretty_repr(test_array)
assert result == "array('I', [1, 2, 3])"
def test_tuple_of_one():
def test_tuple_of_one() -> None:
assert pretty_repr((1,)) == "(1,)"
def test_node():
def test_node() -> None:
node = Node("abc")
assert pretty_repr(node) == "abc: "
def test_indent_lines():
def test_indent_lines() -> None:
console = Console(width=100, color_system=None)
console.begin_capture()
console.print(Pretty([100, 200], indent_guides=True), width=8)
@ -523,35 +554,35 @@ def test_indent_lines():
assert result == expected
def test_pprint():
def test_pprint() -> None:
console = Console(color_system=None)
console.begin_capture()
pprint(1, console=console)
assert console.end_capture() == "1\n"
def test_pprint_max_values():
def test_pprint_max_values() -> None:
console = Console(color_system=None)
console.begin_capture()
pprint([1, 2, 3, 4, 5, 6, 7, 8, 9, 0], console=console, max_length=2)
assert console.end_capture() == "[1, 2, ... +8]\n"
def test_pprint_max_items():
def test_pprint_max_items() -> None:
console = Console(color_system=None)
console.begin_capture()
pprint({"foo": 1, "bar": 2, "egg": 3}, console=console, max_length=2)
assert console.end_capture() == """{'foo': 1, 'bar': 2, ... +1}\n"""
def test_pprint_max_string():
def test_pprint_max_string() -> None:
console = Console(color_system=None)
console.begin_capture()
pprint(["Hello" * 20], console=console, max_string=8)
assert console.end_capture() == """['HelloHel'+92]\n"""
def test_tuples():
def test_tuples() -> None:
console = Console(color_system=None)
console.begin_capture()
pprint((1,), console=console)
@ -566,7 +597,7 @@ def test_tuples():
assert result == expected
def test_newline():
def test_newline() -> None:
console = Console(color_system=None)
console.begin_capture()
console.print(Pretty((1,), insert_line=True, expand_all=True))
@ -575,7 +606,7 @@ def test_newline():
assert result == expected
def test_empty_repr():
def test_empty_repr() -> None:
class Foo:
def __repr__(self):
return ""
@ -583,7 +614,7 @@ def test_empty_repr():
assert pretty_repr(Foo()) == ""
def test_attrs():
def test_attrs() -> None:
@attr.define
class Point:
x: int
@ -597,7 +628,7 @@ def test_attrs():
assert result == expected
def test_attrs_empty():
def test_attrs_empty() -> None:
@attr.define
class Nada:
pass
@ -611,7 +642,8 @@ def test_attrs_empty():
@skip_py310
@skip_py311
@skip_py312
def test_attrs_broken():
@skip_py313
def test_attrs_broken() -> None:
@attr.define
class Foo:
bar: int
@ -627,7 +659,7 @@ def test_attrs_broken():
@skip_py37
@skip_py38
@skip_py39
def test_attrs_broken_310():
def test_attrs_broken_310() -> None:
@attr.define
class Foo:
bar: int
@ -640,7 +672,7 @@ def test_attrs_broken_310():
assert result == expected
def test_user_dict():
def test_user_dict() -> None:
class D1(UserDict):
pass
@ -658,7 +690,7 @@ def test_user_dict():
assert result == "FOO"
def test_lying_attribute():
def test_lying_attribute() -> None:
"""Test getattr doesn't break rich repr protocol"""
class Foo:
@ -670,7 +702,7 @@ def test_lying_attribute():
assert "Foo" in result
def test_measure_pretty():
def test_measure_pretty() -> None:
"""Test measure respects expand_all"""
# https://github.com/Textualize/rich/issues/1998
console = Console()
@ -680,7 +712,7 @@ def test_measure_pretty():
assert measurement == Measurement(12, 12)
def test_tuple_rich_repr():
def test_tuple_rich_repr() -> None:
"""
Test that can use None as key to have tuple positional values.
"""
@ -692,7 +724,7 @@ def test_tuple_rich_repr():
assert pretty_repr(Foo()) == "Foo((1,))"
def test_tuple_rich_repr_default():
def test_tuple_rich_repr_default() -> None:
"""
Test that can use None as key to have tuple positional values and with a default.
"""

View File

@ -646,7 +646,7 @@ def test_wrap_file_task_total() -> None:
os.remove(filename)
def test_task_progress_column_speed():
def test_task_progress_column_speed() -> None:
speed_text = TaskProgressColumn.render_speed(None)
assert speed_text.plain == ""

View File

@ -1,7 +1,7 @@
import io
from rich.console import Console
from rich.prompt import Prompt, IntPrompt, Confirm
from rich.prompt import Confirm, IntPrompt, Prompt
def test_prompt_str():
@ -21,6 +21,24 @@ def test_prompt_str():
assert output == expected
def test_prompt_str_case_insensitive():
INPUT = "egg\nFoO"
console = Console(file=io.StringIO())
name = Prompt.ask(
"what is your name",
console=console,
choices=["foo", "bar"],
default="baz",
case_sensitive=False,
stream=io.StringIO(INPUT),
)
assert name == "foo"
expected = "what is your name [foo/bar] (baz): Please select one of the available options\nwhat is your name [foo/bar] (baz): "
output = console.file.getvalue()
print(repr(output))
assert output == expected
def test_prompt_str_default():
INPUT = ""
console = Console(file=io.StringIO())

View File

@ -251,3 +251,17 @@ def test_clear_meta_and_links():
assert clear_style.bgcolor == Color.parse("black")
assert clear_style.bold
assert not clear_style.italic
def test_clear_meta_and_links_clears_hash():
"""Regression test for https://github.com/Textualize/rich/issues/2942."""
style = Style.parse("bold red on black link https://example.org") + Style.on(
click="CLICK"
)
hash(style) # Force hash caching.
assert style._hash is not None
clear_style = style.clear_meta_and_links()
assert clear_style._hash is None

View File

@ -3,7 +3,6 @@ import os
import sys
import tempfile
import pkg_resources
import pytest
from pygments.lexers import PythonLexer
@ -21,7 +20,12 @@ from rich.syntax import (
from .render import render
PYGMENTS_VERSION = pkg_resources.get_distribution("pygments").version
if sys.version_info >= (3, 8):
from importlib.metadata import Distribution
else:
from importlib_metadata import Distribution
PYGMENTS_VERSION = Distribution.from_name("pygments").version
OLD_PYGMENTS = PYGMENTS_VERSION == "2.13.0"
CODE = '''\

View File

@ -1,6 +1,7 @@
# encoding=utf-8
import io
from textwrap import dedent
import pytest
@ -234,6 +235,134 @@ def test_section():
assert output == expected
@pytest.mark.parametrize(
"show_header,show_footer,expected",
[
(
False,
False,
dedent(
"""
abbbbbcbbbbbbbbbcbbbbcbbbbbd
1Dec 2Skywalker2275M2375M 3
4May 5Solo 5275M5393M 6
ijjjjjkjjjjjjjjjkjjjjkjjjjjl
7Dec 8Last Jedi8262M81333M9
qrrrrrsrrrrrrrrrsrrrrsrrrrrt
"""
).lstrip(),
),
(
True,
False,
dedent(
"""
abbbbbcbbbbbbbbbcbbbbcbbbbbd
1Month2Nickname 2Cost2Gross3
efffffgfffffffffgffffgfffffh
4Dec 5Skywalker5275M5375M 6
4May 5Solo 5275M5393M 6
ijjjjjkjjjjjjjjjkjjjjkjjjjjl
7Dec 8Last Jedi8262M81333M9
qrrrrrsrrrrrrrrrsrrrrsrrrrrt
"""
).lstrip(),
),
(
False,
True,
dedent(
"""
abbbbbcbbbbbbbbbcbbbbcbbbbbd
1Dec 2Skywalker2275M2375M 3
4May 5Solo 5275M5393M 6
ijjjjjkjjjjjjjjjkjjjjkjjjjjl
4Dec 5Last Jedi5262M51333M6
mnnnnnonnnnnnnnnonnnnonnnnnp
7MONTH8NICKNAME 8COST8GROSS9
qrrrrrsrrrrrrrrrsrrrrsrrrrrt
"""
).lstrip(),
),
(
True,
True,
dedent(
"""
abbbbbcbbbbbbbbbcbbbbcbbbbbd
1Month2Nickname 2Cost2Gross3
efffffgfffffffffgffffgfffffh
4Dec 5Skywalker5275M5375M 6
4May 5Solo 5275M5393M 6
ijjjjjkjjjjjjjjjkjjjjkjjjjjl
4Dec 5Last Jedi5262M51333M6
mnnnnnonnnnnnnnnonnnnonnnnnp
7MONTH8NICKNAME 8COST8GROSS9
qrrrrrsrrrrrrrrrsrrrrsrrrrrt
"""
).lstrip(),
),
],
)
def test_placement_table_box_elements(show_header, show_footer, expected):
"""Ensure box drawing characters correctly positioned."""
table = Table(
box=box.ASCII, show_header=show_header, show_footer=show_footer, padding=0
)
# content rows indicated by numerals, pure dividers by letters
table.box.__dict__.update(
top_left="a",
top="b",
top_divider="c",
top_right="d",
head_left="1",
head_vertical="2",
head_right="3",
head_row_left="e",
head_row_horizontal="f",
head_row_cross="g",
head_row_right="h",
mid_left="4",
mid_vertical="5",
mid_right="6",
row_left="i",
row_horizontal="j",
row_cross="k",
row_right="l",
foot_left="7",
foot_vertical="8",
foot_right="9",
foot_row_left="m",
foot_row_horizontal="n",
foot_row_cross="o",
foot_row_right="p",
bottom_left="q",
bottom="r",
bottom_divider="s",
bottom_right="t",
)
# add content - note headers title case, footers upper case
table.add_column("Month", "MONTH", width=5)
table.add_column("Nickname", "NICKNAME", width=9)
table.add_column("Cost", "COST", width=4)
table.add_column("Gross", "GROSS", width=5)
table.add_row("Dec", "Skywalker", "275M", "375M")
table.add_row("May", "Solo", "275M", "393M")
table.add_section()
table.add_row("Dec", "Last Jedi", "262M", "1333M")
console = Console(record=True, width=28)
console.print(table)
output = console.export_text()
print(repr(output))
assert output == expected
if __name__ == "__main__":
render = render_tables()
print(render)