diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2a973c4c..55bbd0fb 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -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}}" diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 21aafe27..c22a4fa6 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a113c352..37db4643 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 38deb7ac..9eff3e39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 5d048990..44171b77 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -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 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 9e3fdcac..0ce5307a 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -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) diff --git a/FAQ.md b/FAQ.md index 01935f67..72ce2b31 100644 --- a/FAQ.md +++ b/FAQ.md @@ -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.) - - -## 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, - - -## 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. - - -## 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) ## 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. - + +## 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. + + +## 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`. + + +## 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. + + +## 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()`. + + ## 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. + +## 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. + + +## 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, +
Generated by [FAQtory](https://github.com/willmcgugan/faqtory) diff --git a/README.md b/README.md index f76def1b..3078de83 100644 --- a/README.md +++ b/README.md @@ -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). - - diff --git a/README.ru.md b/README.ru.md index 14cb6709..2498cfb7 100644 --- a/README.ru.md +++ b/README.ru.md @@ -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 (нажмите чтобы узнать больше):
Лог -В классе 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. + +Метод может быть использован для вывода данных в терминал в длительно работающих программ, таких как сервера, но он также может помочь в отладке.
@@ -185,25 +186,25 @@ test_log()
Эмоджи -Чтобы вставить эмоджи в вывод консоли поместите название между двумя двоеточиями. Вот пример: +Чтобы вставить эмоджи в вывод консоли, поместите его название между двумя двоеточиями. Вот пример: ```python >>> console.print(":smiley: :vampire: :pile_of_poo: :thumbs_up: :raccoon:") 😃 🧛 💩 👍 🦝 ``` -Пожалуйста, используйте это мудро. +Пожалуйста, используйте эту функцию с умом.
Таблицы -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-адресов одновременно, отображая прогресс.
Статус -Для ситуаций где сложно высчитать прогресс, вы можете использовать метод [статус](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
Дерево -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`.
-Столбцы +Колонки -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))
Markdown -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)
-Подсвечивание Синтаксиса +Подсветка синтаксиса -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)
Ошибки -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
-Все визуализации 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) diff --git a/docs/requirements.txt b/docs/requirements.txt index 4e029845..4b7b608e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -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' diff --git a/docs/source/conf.py b/docs/source/conf.py index d1078d61..451a1345 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -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 --------------------------------------------------- diff --git a/docs/source/progress.rst b/docs/source/progress.rst index 272687d9..7a771c8d 100644 --- a/docs/source/progress.rst +++ b/docs/source/progress.rst @@ -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 ~~~~~~~~~~~~~~ diff --git a/docs/source/prompt.rst b/docs/source/prompt.rst index 088aa8e7..b998cdfa 100644 --- a/docs/source/prompt.rst +++ b/docs/source/prompt.rst @@ -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 \ No newline at end of file + python -m rich.prompt diff --git a/pyproject.toml b/pyproject.toml index 0e9a3b74..e589ba9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/questions/ansi_escapes.question.md b/questions/ansi_escapes.question.md new file mode 100644 index 00000000..cb60ed93 --- /dev/null +++ b/questions/ansi_escapes.question.md @@ -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`. diff --git a/questions/logging_color.md b/questions/logging_color.question.md similarity index 100% rename from questions/logging_color.md rename to questions/logging_color.question.md diff --git a/questions/rich_spinner.question.md b/questions/rich_spinner.question.md index 77174635..09327f5d 100644 --- a/questions/rich_spinner.question.md +++ b/questions/rich_spinner.question.md @@ -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). diff --git a/questions/tracebacks_installed.question.md b/questions/tracebacks_installed.question.md new file mode 100644 index 00000000..17f0da24 --- /dev/null +++ b/questions/tracebacks_installed.question.md @@ -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()`. diff --git a/rich/_inspect.py b/rich/_inspect.py index 30446ceb..e87698d1 100644 --- a/rich/_inspect.py +++ b/rich/_inspect.py @@ -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 diff --git a/rich/align.py b/rich/align.py index e230a66b..fac50b1e 100644 --- a/rich/align.py +++ b/rich/align.py @@ -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__( diff --git a/rich/ansi.py b/rich/ansi.py index 66365e65..7de86ce5 100644 --- a/rich/ansi.py +++ b/rich/ansi.py @@ -9,6 +9,7 @@ from .text import Text re_ansi = re.compile( r""" +(?:\x1b[0-?])| (?:\x1b\](.*?)\x1b\\)| (?:\x1b([(@-Z\\-_]|\[[0-?]*[ -/]*[@-~])) """, diff --git a/rich/color.py b/rich/color.py index 4270a278..e2c23a6a 100644 --- a/rich/color.py +++ b/rich/color.py @@ -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): diff --git a/rich/console.py b/rich/console.py index 1232cd5c..23cc44d5 100644 --- a/rich/console.py +++ b/rich/console.py @@ -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) diff --git a/rich/default_styles.py b/rich/default_styles.py index ad72fff5..031b94a1 100644 --- a/rich/default_styles.py +++ b/rich/default_styles.py @@ -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(), diff --git a/rich/filesize.py b/rich/filesize.py index 99f118e2..83bc9118 100644 --- a/rich/filesize.py +++ b/rich/filesize.py @@ -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 diff --git a/rich/logging.py b/rich/logging.py index 96859934..b2624cd5 100644 --- a/rich/logging.py +++ b/rich/logging.py @@ -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: diff --git a/rich/markdown.py b/rich/markdown.py index 9b5ceacd..a58a04fc 100644 --- a/rich/markdown.py +++ b/rich/markdown.py @@ -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( diff --git a/rich/panel.py b/rich/panel.py index 95f4c84c..3b78fca4 100644 --- a/rich/panel.py +++ b/rich/panel.py @@ -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) diff --git a/rich/pretty.py b/rich/pretty.py index 5c48cfd9..fa340212 100644 --- a/rich/pretty.py +++ b/rich/pretty.py @@ -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 diff --git a/rich/progress.py b/rich/progress.py index 8810aeac..effcab40 100644 --- a/rich/progress.py +++ b/rich/progress.py @@ -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 diff --git a/rich/progress_bar.py b/rich/progress_bar.py index a2bf3261..41794f76 100644 --- a/rich/progress_bar.py +++ b/rich/progress_bar.py @@ -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 diff --git a/rich/prompt.py b/rich/prompt.py index 972082b7..c7cf25ba 100644 --- a/rich/prompt.py +++ b/rich/prompt.py @@ -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:") diff --git a/rich/style.py b/rich/style.py index 313c8894..262fd6ec 100644 --- a/rich/style.py +++ b/rich/style.py @@ -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 diff --git a/rich/syntax.py b/rich/syntax.py index dd7de6a6..4da6c3b7 100644 --- a/rich/syntax.py +++ b/rich/syntax.py @@ -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 diff --git a/rich/table.py b/rich/table.py index fe4054cf..10af2e6f 100644 --- a/rich/table.py +++ b/rich/table.py @@ -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, diff --git a/rich/theme.py b/rich/theme.py index 471dfb2f..227f1d86 100644 --- a/rich/theme.py +++ b/rich/theme.py @@ -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) diff --git a/rich/traceback.py b/rich/traceback.py index 821c7501..fcefeb23 100644 --- a/rich/traceback.py +++ b/rich/traceback.py @@ -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, ) diff --git a/rich/tree.py b/rich/tree.py index 8c5e7181..03f835c3 100644 --- a/rich/tree.py +++ b/rich/tree.py @@ -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__( diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..52662964 --- /dev/null +++ b/tests/conftest.py @@ -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) diff --git a/tests/test_ansi.py b/tests/test_ansi.py index ae87cde1..d81f6459 100644 --- a/tests/test_ansi.py +++ b/tests/test_ansi.py @@ -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 diff --git a/tests/test_box.py b/tests/test_box.py index 21ba4fd0..6d02f7b7 100644 --- a/tests/test_box.py +++ b/tests/test_box.py @@ -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 diff --git a/tests/test_inspect.py b/tests/test_inspect.py index a9d55393..4f249443 100644 --- a/tests/test_inspect.py +++ b/tests/test_inspect.py @@ -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 = ( "╭──────────────── ─────────────────╮\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 = ( "╭──────────────── ─────────────────╮\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 diff --git a/tests/test_pretty.py b/tests/test_pretty.py index e505ed4d..929de1cf 100644 --- a/tests/test_pretty.py +++ b/tests/test_pretty.py @@ -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(, {'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. """ diff --git a/tests/test_progress.py b/tests/test_progress.py index 72497a0c..0be683c3 100644 --- a/tests/test_progress.py +++ b/tests/test_progress.py @@ -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 == "" diff --git a/tests/test_prompt.py b/tests/test_prompt.py index 9a41cc39..11bffa71 100644 --- a/tests/test_prompt.py +++ b/tests/test_prompt.py @@ -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()) diff --git a/tests/test_style.py b/tests/test_style.py index b5c01729..84a65a67 100644 --- a/tests/test_style.py +++ b/tests/test_style.py @@ -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 diff --git a/tests/test_syntax.py b/tests/test_syntax.py index 69736916..bbd4c7a5 100644 --- a/tests/test_syntax.py +++ b/tests/test_syntax.py @@ -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 = '''\ diff --git a/tests/test_table.py b/tests/test_table.py index 2eea1897..79033f21 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -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)