├── .coveragerc ├── .faq ├── FAQ.md └── suggest.md ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── codeql.yml │ ├── codespell.yml │ ├── comment.yml │ ├── newissue.yml │ ├── pythonpackage.yml │ └── readmechanged.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── FAQ.md ├── LICENSE ├── Makefile ├── README.cn.md ├── README.de-ch.md ├── README.de.md ├── README.es.md ├── README.fa.md ├── README.fr.md ├── README.hi.md ├── README.id.md ├── README.it.md ├── README.ja.md ├── README.kr.md ├── README.md ├── README.pl.md ├── README.pt-br.md ├── README.ru.md ├── README.sv.md ├── README.tr.md ├── README.zh-tw.md ├── SECURITY.md ├── assets ├── logo.ai ├── logo.svg └── logo.txt ├── asv.conf.json ├── asvhashfile ├── benchmarks ├── README.md ├── __init__.py ├── benchmarks.py ├── results │ ├── benchmarks.json │ └── darrenburns-2022-mbp │ │ ├── 008854c4-virtualenv-py3.10.json │ │ ├── 03392a1b-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 038e22eb-virtualenv-py3.10.json │ │ ├── 03a52134-virtualenv-py3.10.json │ │ ├── 06922006-virtualenv-py3.10.json │ │ ├── 06aa1271-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 07d51ffc-virtualenv-py3.10.json │ │ ├── 0a3fcb9c-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 0ac4e308-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 0d2aeb75-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 0d69004c-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 0db0cbd0-virtualenv-py3.10.json │ │ ├── 0e8df8cd-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 0fd6bc56-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 11c00224-virtualenv-py3.10.json │ │ ├── 11c305e1-virtualenv-py3.10.json │ │ ├── 1442dd77-virtualenv-py3.10.json │ │ ├── 15623c5a-virtualenv-py3.10.json │ │ ├── 177958c5-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 189a2a3f-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 19e26c94-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 1cdcd1ae-virtualenv-py3.10.json │ │ ├── 1daa1771-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 1f3f7f1e-virtualenv-py3.10.json │ │ ├── 1ffbd443-virtualenv-py3.10.json │ │ ├── 20024635-virtualenv-py3.10.json │ │ ├── 21432b4c-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 2356d7c0-virtualenv-py3.10.json │ │ ├── 23aa7177-virtualenv-py3.10.json │ │ ├── 24743154-virtualenv-py3.10.json │ │ ├── 25a1bf06-virtualenv-py3.10.json │ │ ├── 26fe4667-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 27ab1732-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 2aea8526-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 2ba277ac-virtualenv-py3.10.json │ │ ├── 2c93dce9-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 2d3152a2-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 2d3ec69f-virtualenv-py3.10.json │ │ ├── 2ea7e586-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 2ea7e586-virtualenv-py3.10.json │ │ ├── 30498f59-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 3473658d-virtualenv-py3.10.json │ │ ├── 36efcb5a-virtualenv-py3.10.json │ │ ├── 3827b4ae-virtualenv-py3.10.json │ │ ├── 3be88c08-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 3db6396a-virtualenv-py3.10.json │ │ ├── 3f7d3e4e-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 3f7d3e4e-virtualenv-py3.10.json │ │ ├── 4020d5a9-virtualenv-py3.10.json │ │ ├── 41279bca-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 416033ff-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 43a26c0a-virtualenv-py3.10.json │ │ ├── 43d4c4e5-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 43d4c4e5-virtualenv-py3.10.json │ │ ├── 44f54dd8-virtualenv-py3.10.json │ │ ├── 464e4e33-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 489fafc6-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 48da2791-virtualenv-py3.10.json │ │ ├── 4b123ddf-virtualenv-py3.10.json │ │ ├── 4b3b6531-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 4bf3f19c-virtualenv-py3.10.json │ │ ├── 4d6a6d88-virtualenv-py3.10.json │ │ ├── 4dc1d4cb-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 4f8908a6-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 52d159aa-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 52d159aa-virtualenv-py3.10.json │ │ ├── 53cda574-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 53d9eeaf-virtualenv-py3.10.json │ │ ├── 550d3911-virtualenv-py3.10.json │ │ ├── 55e11902-virtualenv-py3.10.json │ │ ├── 573125e9-virtualenv-py3.10.json │ │ ├── 579a29c8-virtualenv-py3.10.json │ │ ├── 588f0331-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 58bfa48f-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 5f021978-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 5f021978-virtualenv-py3.10.json │ │ ├── 5f03e3ba-virtualenv-py3.10.json │ │ ├── 5f55063b-virtualenv-py3.10.json │ │ ├── 5f82274a-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 5fafb92f-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 60dadaf2-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 64471afc-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 646d933d-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 64755d41-virtualenv-py3.10.json │ │ ├── 656b7a18-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 666d0cf2-virtualenv-py3.10.json │ │ ├── 690507d4-virtualenv-py3.10.json │ │ ├── 6d7ba589-virtualenv-py3.10.json │ │ ├── 71135d19-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 7441bf27-virtualenv-py3.10.json │ │ ├── 76620730-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 79ea1c1d-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 7bad81d5-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 7d00fa83-virtualenv-py3.10.json │ │ ├── 7d02d29b-virtualenv-py3.10.json │ │ ├── 7e4a2db4-virtualenv-py3.10.json │ │ ├── 7edd619f-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 7ef7ffee-virtualenv-py3.10.json │ │ ├── 83756d62-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 837b6d7e-virtualenv-py3.10.json │ │ ├── 877c53d9-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 88b07b3e-virtualenv-py3.10.json │ │ ├── 8a7f5d82-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 8a7f5d82-virtualenv-py3.10.json │ │ ├── 8b185610-virtualenv-py3.10.json │ │ ├── 8b47f338-virtualenv-py3.10.json │ │ ├── 8c3e6be4-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 8e649fea-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 911d305f-virtualenv-py3.10.json │ │ ├── 932e26b6-virtualenv-py3.10.json │ │ ├── 949e1f72-virtualenv-py3.10.json │ │ ├── 95d8bf98-virtualenv-py3.10.json │ │ ├── 96ea5fed-virtualenv-py3.10.json │ │ ├── 972dedff-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 972dedff-virtualenv-py3.10.json │ │ ├── 99831099-virtualenv-py3.10.json │ │ ├── 9a4fbf83-virtualenv-py3.10.json │ │ ├── 9abc0292-virtualenv-py3.10.json │ │ ├── 9bfb6190-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── 9f2a426e-virtualenv-py3.10.json │ │ ├── a27a3ee2-virtualenv-py3.10.json │ │ ├── a2f6688e-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── a6d1d784-virtualenv-py3.10.json │ │ ├── a6ea9890-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── a81230bc-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── a81230bc-virtualenv-py3.10.json │ │ ├── a8d2bb20-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── aa4546ac-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── aa7926c1-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── aaea99f7-virtualenv-py3.10.json │ │ ├── ac1a33da-virtualenv-py3.10.json │ │ ├── aca0b60b-virtualenv-py3.10.json │ │ ├── ad6e3dea-virtualenv-py3.10.json │ │ ├── ae5865eb-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── b15bc18c-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── b391635e-virtualenv-py3.10.json │ │ ├── b9e0014a-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── b9e0014a-virtualenv-py3.10.json │ │ ├── ba5d0c2c-virtualenv-py3.10.json │ │ ├── bd34e0a1-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── bd34e0a1-virtualenv-py3.10.json │ │ ├── bf728dbc-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── c24ab497-virtualenv-py3.10.json │ │ ├── c3d0e358-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── c3d0e358-virtualenv-py3.10.json │ │ ├── c3ee3b05-virtualenv-py3.10.json │ │ ├── c57e1f50-virtualenv-py3.10.json │ │ ├── c9afafdd-virtualenv-py3.10.json │ │ ├── cefafdc1-virtualenv-py3.10.json │ │ ├── cf606f0a-virtualenv-py3.10.json │ │ ├── d06540a2-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── d1ea01d0-virtualenv-py3.10.json │ │ ├── d6e6a762-virtualenv-py3.10.json │ │ ├── d9d59c6e-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── d9d59c6e-virtualenv-py3.10.json │ │ ├── dc3f0623-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── e0a1fd30-virtualenv-py3.10.json │ │ ├── e21ac11a-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── e338ab14-virtualenv-py3.10.json │ │ ├── e34eadb3-virtualenv-py3.10.json │ │ ├── e5246436-virtualenv-py3.10.json │ │ ├── e7849495-virtualenv-py3.10.json │ │ ├── e78acae6-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── e7de32a0-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── e9e72000-virtualenv-py3.10.json │ │ ├── ea049ffc-virtualenv-py3.10.json │ │ ├── ea2ed337-virtualenv-py3.10.json │ │ ├── eab3fe8e-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── ecf3d7f1-virtualenv-py3.10.json │ │ ├── edcb6f9e-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── ef80460f-virtualenv-py3.10.json │ │ ├── f2845e12-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── f2af8c9d-virtualenv-py3.10.json │ │ ├── f5ed5bde-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── f82a4ccf-virtualenv-py3.10-setuptools59.2.0.json │ │ ├── f84d5dee-virtualenv-py3.10.json │ │ └── machine.json └── snippets.py ├── docs ├── Makefile ├── images │ ├── box.svg │ └── svg_export.svg ├── make.bat ├── requirements.txt └── source │ ├── appendix.rst │ ├── appendix │ ├── box.rst │ └── colors.rst │ ├── columns.rst │ ├── conf.py │ ├── console.rst │ ├── group.rst │ ├── highlighting.rst │ ├── index.rst │ ├── introduction.rst │ ├── layout.rst │ ├── live.rst │ ├── logging.rst │ ├── markdown.rst │ ├── markup.rst │ ├── padding.rst │ ├── panel.rst │ ├── pretty.rst │ ├── progress.rst │ ├── prompt.rst │ ├── protocol.rst │ ├── reference.rst │ ├── reference │ ├── abc.rst │ ├── align.rst │ ├── bar.rst │ ├── color.rst │ ├── columns.rst │ ├── console.rst │ ├── emoji.rst │ ├── highlighter.rst │ ├── init.rst │ ├── json.rst │ ├── layout.rst │ ├── live.rst │ ├── logging.rst │ ├── markdown.rst │ ├── markup.rst │ ├── measure.rst │ ├── padding.rst │ ├── panel.rst │ ├── pretty.rst │ ├── progress.rst │ ├── progress_bar.rst │ ├── prompt.rst │ ├── protocol.rst │ ├── rule.rst │ ├── segment.rst │ ├── spinner.rst │ ├── status.rst │ ├── style.rst │ ├── styled.rst │ ├── syntax.rst │ ├── table.rst │ ├── text.rst │ ├── theme.rst │ ├── traceback.rst │ └── tree.rst │ ├── style.rst │ ├── syntax.rst │ ├── tables.rst │ ├── text.rst │ ├── traceback.rst │ └── tree.rst ├── examples ├── README.md ├── attrs.py ├── bars.py ├── columns.py ├── cp_progress.py ├── downloader.py ├── dynamic_progress.py ├── exception.py ├── export.py ├── file_progress.py ├── fullscreen.py ├── group.py ├── group2.py ├── highlighter.py ├── jobs.py ├── justify.py ├── justify2.py ├── layout.py ├── link.py ├── listdir.py ├── live_progress.py ├── log.py ├── overflow.py ├── padding.py ├── print_calendar.py ├── rainbow.py ├── recursive_error.py ├── repr.py ├── save_table_svg.py ├── screen.py ├── spinners.py ├── status.py ├── suppress.py ├── table.py ├── table_movie.py ├── top_lite_simulator.py └── tree.py ├── faq.yml ├── imgs ├── columns.png ├── downloader.gif ├── features.png ├── hello_world.png ├── inspect.png ├── log.png ├── logging.png ├── logo.svg ├── markdown.png ├── print.png ├── progress.gif ├── progress.png ├── repl.png ├── spinners.gif ├── status.gif ├── syntax.png ├── table.png ├── table2.png ├── table_movie.gif ├── traceback.png ├── tree.png └── where_there_is_a_will.png ├── make.bat ├── poetry.lock ├── pyproject.toml ├── questions ├── README.md ├── ansi_escapes.question.md ├── emoji_broken.question.md ├── highlighting_unexpected.question.md ├── log_renderables.question.md ├── logging_color.question.md ├── rich_spinner.question.md ├── square_brackets.question.md └── tracebacks_installed.question.md ├── rich ├── __init__.py ├── __main__.py ├── _cell_widths.py ├── _emoji_codes.py ├── _emoji_replace.py ├── _export_format.py ├── _extension.py ├── _fileno.py ├── _inspect.py ├── _log_render.py ├── _loop.py ├── _null_file.py ├── _palettes.py ├── _pick.py ├── _ratio.py ├── _spinners.py ├── _stack.py ├── _timer.py ├── _win32_console.py ├── _windows.py ├── _windows_renderer.py ├── _wrap.py ├── abc.py ├── align.py ├── ansi.py ├── bar.py ├── box.py ├── cells.py ├── color.py ├── color_triplet.py ├── columns.py ├── console.py ├── constrain.py ├── containers.py ├── control.py ├── default_styles.py ├── diagnose.py ├── emoji.py ├── errors.py ├── file_proxy.py ├── filesize.py ├── highlighter.py ├── json.py ├── jupyter.py ├── layout.py ├── live.py ├── live_render.py ├── logging.py ├── markdown.py ├── markup.py ├── measure.py ├── padding.py ├── pager.py ├── palette.py ├── panel.py ├── pretty.py ├── progress.py ├── progress_bar.py ├── prompt.py ├── protocol.py ├── py.typed ├── region.py ├── repr.py ├── rule.py ├── scope.py ├── screen.py ├── segment.py ├── spinner.py ├── status.py ├── style.py ├── styled.py ├── syntax.py ├── table.py ├── terminal_theme.py ├── text.py ├── theme.py ├── themes.py ├── traceback.py └── tree.py ├── setup.py ├── tests ├── __init__.py ├── _card_render.py ├── conftest.py ├── pytest.ini ├── render.py ├── test_align.py ├── test_ansi.py ├── test_bar.py ├── test_block_bar.py ├── test_box.py ├── test_card.py ├── test_cells.py ├── test_color.py ├── test_color_triplet.py ├── test_columns.py ├── test_columns_align.py ├── test_console.py ├── test_constrain.py ├── test_containers.py ├── test_control.py ├── test_emoji.py ├── test_file_proxy.py ├── test_filesize.py ├── test_getfileno.py ├── test_highlighter.py ├── test_inspect.py ├── test_json.py ├── test_jupyter.py ├── test_layout.py ├── test_live.py ├── test_live_render.py ├── test_log.py ├── test_logging.py ├── test_markdown.py ├── test_markdown_no_hyperlinks.py ├── test_markup.py ├── test_measure.py ├── test_null_file.py ├── test_padding.py ├── test_palette.py ├── test_panel.py ├── test_pick.py ├── test_pretty.py ├── test_progress.py ├── test_prompt.py ├── test_protocol.py ├── test_ratio.py ├── test_repr.py ├── test_rich_print.py ├── test_rule.py ├── test_rule_in_table.py ├── test_screen.py ├── test_segment.py ├── test_spinner.py ├── test_stack.py ├── test_status.py ├── test_style.py ├── test_styled.py ├── test_syntax.py ├── test_table.py ├── test_text.py ├── test_theme.py ├── test_tools.py ├── test_traceback.py ├── test_tree.py ├── test_win32_console.py └── test_windows_renderer.py ├── tools ├── README.md ├── cats.json ├── make_emoji.py ├── make_terminal_widths.py ├── movies.md ├── profile_divide.py ├── profile_pretty.py └── stress_test_pretty.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = rich/jupyter.py 3 | rich/_windows.py 4 | rich/_timer.py 5 | rich/diagnose.py 6 | 7 | [report] 8 | exclude_lines = 9 | pragma: no cover 10 | if TYPE_CHECKING: 11 | if __name__ == "__main__": 12 | @overload 13 | -------------------------------------------------------------------------------- /.faq/FAQ.md: -------------------------------------------------------------------------------- 1 | 2 | # Frequently Asked Questions 3 | 4 | {%- for question in questions %} 5 | - [{{ question.title }}](#{{ question.slug }}) 6 | {%- endfor %} 7 | 8 | 9 | {%- for question in questions %} 10 | 11 | 12 | ## {{ question.title }} 13 | 14 | {{ question.body }} 15 | 16 | {%- endfor %} 17 | 18 |
19 | 20 | Generated by [FAQtory](https://github.com/willmcgugan/faqtory) 21 | -------------------------------------------------------------------------------- /.faq/suggest.md: -------------------------------------------------------------------------------- 1 | {%- if questions -%} 2 | {% if questions|length == 1 %} 3 | We found the following entry in the [FAQ]({{ faq_url }}) which you may find helpful: 4 | {%- else %} 5 | We found the following entries in the [FAQ]({{ faq_url }}) which you may find helpful: 6 | {%- endif %} 7 | 8 | {% for question in questions %} 9 | - [{{ question.title }}]({{ faq_url }}#{{ question.slug }}) 10 | {%- endfor %} 11 | 12 | Feel free to close this issue if you found an answer in the FAQ. Otherwise, please give us a little time to review. 13 | 14 | {%- else -%} 15 | Thank you for your issue. Give us a little time to review it. 16 | 17 | PS. You might want to check the [FAQ]({{ faq_url }}) if you haven't done so already. 18 | {%- endif %} 19 | 20 | This is an automated reply, generated by [FAQtory](https://github.com/willmcgugan/faqtory) 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: Needs triage 6 | assignees: "" 7 | --- 8 | 9 | - [ ] I've checked [docs](https://rich.readthedocs.io/en/latest/introduction.html) and [closed issues](https://github.com/Textualize/rich/issues?q=is%3Aissue+is%3Aclosed) for possible solutions. 10 | - [ ] I can't find my issue in the [FAQ](https://github.com/Textualize/rich/blob/master/FAQ.md). 11 | 12 | **Describe the bug** 13 | 14 | Edit this with a clear and concise description of what the bug. 15 | 16 | Provide a minimal code example that demonstrates the issue if you can. If the issue is visual in nature, consider posting a screenshot. 17 | 18 | **Platform** 19 |
20 | Click to expand 21 | 22 | What platform (Win/Linux/Mac) are you running on? What terminal software are you using? 23 | 24 | I may ask you to copy and paste the output of the following commands. It may save some time if you do it now. 25 | 26 | If you're using Rich in a terminal: 27 | 28 | ``` 29 | python -m rich.diagnose 30 | pip freeze | grep rich 31 | ``` 32 | 33 | If you're using Rich in a Jupyter Notebook, run the following snippet in a cell 34 | and paste the output in your bug report. 35 | 36 | ```python 37 | from rich.diagnose import report 38 | report() 39 | ``` 40 | 41 |
42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[REQUEST]" 5 | labels: Needs triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | NOTE: If a feature could be implemented in a third-party library, then it is unlikely to be accepted as part of the core library. 11 | 12 | Consider posting in https://github.com/textualize/rich/discussions for feedback before raising a feature request. 13 | 14 | Have you checked the issues for a similar suggestions? 15 | 16 | **How would you improve Rich?** 17 | 18 | Give as much detail as you can. Example code of how you would like it to work would help. 19 | 20 | **What problem does it solve for you?** 21 | 22 | What problem do you have that this feature would solve? I may be able to suggest an existing way of solving it. 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Type of changes 2 | 3 | - [ ] Bug fix 4 | - [ ] New feature 5 | - [ ] Documentation / docstrings 6 | - [ ] Tests 7 | - [ ] Other 8 | 9 | ## Checklist 10 | 11 | - [ ] I've run the latest [black](https://github.com/psf/black) with default args on new code. 12 | - [ ] I've updated CHANGELOG.md and CONTRIBUTORS.md where appropriate. 13 | - [ ] I've added tests for new code. 14 | - [ ] I accept that @willmcgugan may be pedantic in the code review. 15 | 16 | ## Description 17 | 18 | Please describe your changes here. If this fixes a bug, please link to the issue, if possible. 19 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ 'master' ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ 'master' ] 9 | schedule: 10 | - cron: '38 19 * * 5' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: [ 'python' ] 25 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 26 | # Use only 'java' to analyze code written in Java, Kotlin or both 27 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 28 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 29 | 30 | steps: 31 | - name: Checkout repository 32 | uses: actions/checkout@v4 33 | 34 | # Initializes the CodeQL tools for scanning. 35 | - name: Initialize CodeQL 36 | uses: github/codeql-action/init@v3 37 | with: 38 | languages: ${{ matrix.language }} 39 | # If you wish to specify custom queries, you can do so here or in a config file. 40 | # By default, queries listed here will override any specified in a config file. 41 | # Prefix the list here with "+" to use these queries and those in the config file. 42 | 43 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 44 | # queries: security-extended,security-and-quality 45 | 46 | 47 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). 48 | # If this step fails, then you should remove it and run the build manually (see below) 49 | - name: Autobuild 50 | uses: github/codeql-action/autobuild@v3 51 | 52 | # ℹ️ Command-line programs to run using the OS shell. 53 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 54 | 55 | # If the Autobuild fails above, remove it and uncomment the following three lines. 56 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 57 | 58 | # - run: | 59 | # echo "Run, Build Application using script" 60 | # ./location_of_script_within_repo/buildscript.sh 61 | 62 | - name: Perform CodeQL Analysis 63 | uses: github/codeql-action/analyze@v3 64 | with: 65 | category: "/language:${{matrix.language}}" 66 | -------------------------------------------------------------------------------- /.github/workflows/codespell.yml: -------------------------------------------------------------------------------- 1 | name: codespell 2 | on: [pull_request, push] 3 | jobs: 4 | codespell: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | - run: python3 -m pip install codespell 9 | - run: codespell --ignore-words-list="ba,fo,hel,revered,womens" 10 | --skip="./README.*.md,*.svg,*.ai,./benchmarks/snippets.py,./tests,./tools,*.lock" 11 | -------------------------------------------------------------------------------- /.github/workflows/comment.yml: -------------------------------------------------------------------------------- 1 | name: issues 2 | on: 3 | issues: 4 | types: [closed] 5 | jobs: 6 | add-comment: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | issues: write 10 | steps: 11 | - name: Did I solve your problem? 12 | uses: peter-evans/create-or-update-comment@a35cf36e5301d70b76f316e867e7788a55a31dae 13 | with: 14 | issue-number: ${{ github.event.issue.number }} 15 | body: | 16 | I hope we solved your problem. 17 | 18 | If you like using Rich, you might also enjoy [Textual](https://textual.textualize.io) 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/newissue.yml: -------------------------------------------------------------------------------- 1 | name: issues 2 | on: 3 | issues: 4 | types: [opened] 5 | jobs: 6 | add-comment: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | issues: write 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | ref: master 14 | - name: Install FAQtory 15 | run: pip install FAQtory 16 | - name: Run Suggest 17 | env: 18 | TITLE: ${{ github.event.issue.title }} 19 | run: faqtory suggest "$TITLE" > suggest.md 20 | - name: Read suggest.md 21 | id: suggest 22 | uses: juliangruber/read-file-action@v1 23 | with: 24 | path: ./suggest.md 25 | - name: Suggest FAQ 26 | uses: peter-evans/create-or-update-comment@a35cf36e5301d70b76f316e867e7788a55a31dae 27 | with: 28 | issue-number: ${{ github.event.issue.number }} 29 | body: ${{ steps.suggest.outputs.content }} 30 | -------------------------------------------------------------------------------- /.github/workflows/pythonpackage.yml: -------------------------------------------------------------------------------- 1 | name: Test Rich module 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | os: [windows-latest, ubuntu-latest, macos-latest] 12 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 13 | exclude: 14 | - { os: windows-latest, python-version: "3.13" } 15 | defaults: 16 | run: 17 | shell: bash 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | allow-prereleases: true 25 | - name: Install and configure Poetry 26 | # TODO: workaround for https://github.com/snok/install-poetry/issues/94 27 | uses: snok/install-poetry@v1.3.4 28 | with: 29 | version: 1.3.1 30 | virtualenvs-in-project: true 31 | - name: Install dependencies 32 | run: poetry install 33 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 34 | - name: Format check with black 35 | run: | 36 | source $VENV 37 | make format-check 38 | - name: Typecheck with mypy 39 | run: | 40 | source $VENV 41 | make typecheck 42 | - name: Test with pytest (with coverage) 43 | run: | 44 | source $VENV 45 | pytest tests -v --cov=./rich --cov-report=xml:./coverage.xml --cov-report term-missing 46 | - name: Upload code coverage 47 | uses: codecov/codecov-action@v4 48 | with: 49 | token: ${{ secrets.CODECOV_TOKEN }} 50 | file: ./coverage.xml 51 | name: rich 52 | flags: unittests 53 | env_vars: OS,PYTHON 54 | -------------------------------------------------------------------------------- /.github/workflows/readmechanged.yml: -------------------------------------------------------------------------------- 1 | name: README.md Changed 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'README.md' 9 | 10 | jobs: 11 | send_notification: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Send notification to README Authors 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GHP_README_WORKFLOW }} 18 | GIT_SHA: ${{ github.sha }} 19 | run: | 20 | COMMIT=$(git rev-parse --short "$GIT_SHA") 21 | AUTHORS='@willmcgugan @oleksis @Adilius' 22 | BODY="🤓 $AUTHORS README.md changed 📝. Check the [commit $COMMIT](https://github.com/willmcgugan/rich/commit/$GIT_SHA) 👀" 23 | DISCUSSIONID='MDEwOkRpc2N1c3Npb24zMzI2NzM0' 24 | gh api graphql -H 'GraphQL-Features: discussions_api' -f body="$BODY" -F discussionId="$DISCUSSIONID" -f query='mutation($body: String!, $discussionId: ID!){addDiscussionComment(input:{body: $body , discussionId: $discussionId}){comment{id}}}' 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.ipynb 2 | .pytype 3 | .DS_Store 4 | .vscode 5 | .idea/ 6 | mypy_report 7 | docs/build 8 | docs/source/_build 9 | tools/*.txt 10 | playground/ 11 | 12 | # Byte-compiled / optimized / DLL files 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | MANIFEST 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | .hypothesis/ 59 | .pytest_cache/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # celery beat schedule file 90 | celerybeat-schedule 91 | 92 | # SageMath parsed files 93 | *.sage.py 94 | 95 | # Environments 96 | .env 97 | .venv 98 | env/ 99 | venv/ 100 | ENV/ 101 | env.bak/ 102 | venv.bak/ 103 | 104 | # Spyder project settings 105 | .spyderproject 106 | .spyproject 107 | 108 | # Rope project settings 109 | .ropeproject 110 | 111 | # mkdocs documentation 112 | /site 113 | 114 | # mypy 115 | .mypy_cache/ 116 | 117 | # airspeed velocity 118 | benchmarks/env/ 119 | benchmarks/html/ 120 | 121 | sandbox/ 122 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | exclude: benchmarks/ 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v4.4.0 7 | hooks: 8 | - id: check-ast 9 | - id: check-builtin-literals 10 | - id: check-case-conflict 11 | - id: check-merge-conflict 12 | - id: check-json 13 | - id: check-toml 14 | - id: check-yaml 15 | - id: end-of-file-fixer 16 | - id: mixed-line-ending 17 | - id: check-vcs-permalinks 18 | - id: check-shebang-scripts-are-executable 19 | - id: trailing-whitespace 20 | - repo: https://github.com/pre-commit/pygrep-hooks 21 | rev: v1.10.0 22 | hooks: 23 | - id: python-no-log-warn 24 | - id: python-use-type-annotations 25 | - id: rst-directive-colons 26 | - id: rst-inline-touching-normal 27 | - repo: https://github.com/hadialqattan/pycln 28 | rev: v2.2.2 29 | hooks: 30 | - id: pycln 31 | args: [--all] 32 | - repo: https://github.com/psf/black-pre-commit-mirror 33 | rev: 23.11.0 34 | hooks: 35 | - id: black 36 | exclude: ^benchmarks/ 37 | - repo: https://github.com/PyCQA/isort 38 | rev: 5.12.0 39 | hooks: 40 | - id: isort 41 | name: isort (python) 42 | language_version: "3.11" 43 | args: ["--profile", "black"] 44 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Build documentation in the docs/ directory with Sphinx 8 | sphinx: 9 | configuration: docs/source/conf.py 10 | 11 | # Optionally build your docs in additional formats such as PDF and ePub 12 | formats: all 13 | 14 | build: 15 | os: "ubuntu-24.04" 16 | tools: 17 | python: "3.12" 18 | 19 | python: 20 | install: 21 | - requirements: docs/requirements.txt 22 | - method: pip 23 | path: . 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Will McGugan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | TERM=unknown pytest --cov-report term-missing --cov=rich tests/ -vv 3 | test-no-cov: 4 | TERM=unknown pytest tests/ -vv 5 | format-check: 6 | black --check . 7 | format: 8 | black . 9 | typecheck: 10 | mypy -p rich --no-incremental 11 | typecheck-report: 12 | mypy -p rich --html-report mypy_report 13 | .PHONY: docs 14 | docs: 15 | cd docs; make html 16 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security contact information 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /assets/logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/assets/logo.ai -------------------------------------------------------------------------------- /assets/logo.txt: -------------------------------------------------------------------------------- 1 | .-----------. 2 | /___/__|__\___\ 3 | \ oo / 4 | \_o8o888___ / 5 | /888888(. .)8. 6 | |"""""[H]\ /""| 7 | |-- `. - |^| -| 8 | |__ _)\__|_| _| 9 | _(________)_ 10 | (____________) -------------------------------------------------------------------------------- /asv.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "project": "rich", 4 | "project_url": "https://github.com/Textualize/rich", 5 | "repo": ".", 6 | "repo_subdir": "", 7 | "install_command": [ 8 | "in-dir={env_dir} python -mpip install {wheel_file}" 9 | ], 10 | "uninstall_command": [ 11 | "return-code=any python -mpip uninstall -y {project}" 12 | ], 13 | "build_command": [ 14 | "pip install poetry", 15 | "python setup.py build", 16 | "PIP_NO_BUILD_ISOLATION=false python -mpip wheel --no-deps --no-index -w {build_cache_dir} {build_dir}" 17 | ], 18 | "branches": [ 19 | "master" 20 | ], 21 | "html_dir": "./benchmarks/html", 22 | "results_dir": "./benchmarks/results", 23 | "env_dir": "./benchmarks/env", 24 | "dvcs": "git", 25 | "environment_type": "virtualenv", 26 | "install_timeout": 180, 27 | "show_commit_url": "http://github.com/Textualize/rich/commit/", 28 | "pythons": [ 29 | "3.10" 30 | ], 31 | "matrix": { 32 | "setuptools": ["59.2.0"] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /asvhashfile: -------------------------------------------------------------------------------- 1 | v10.0.0 2 | v10.2.2 3 | v10.6.0 4 | v10.7.0 5 | v10.8.0 6 | v10.9.0 7 | v11.0.0 8 | v11.1.0 9 | v11.2.0 10 | v12.0.0 11 | v12.0.1 12 | v12.1.0 13 | v12.2.0 14 | v12.3.0 15 | v12.4.0 16 | v12.4.1 17 | v12.4.2 18 | v12.4.3 19 | v12.4.4 20 | v12.5.0 21 | v8.0.0 22 | v9.13.0 23 | v9.5.1 24 | -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # Benchmarking Rich 2 | 3 | This directory contains benchmarks, for monitoring the performance of Rich over time. 4 | 5 | View the benchmark dashboard [here](https://textualize.github.io/rich-benchmarks/). 6 | 7 | The benchmarks use a tool called [Airspeed Velocity](https://asv.readthedocs.io/en/stable) (`asv`), 8 | and we've configured it in [asv.conf.json](../asv.conf.json). 9 | 10 | ## Running Benchmarks 11 | 12 | We strongly recommend running `asv run --help` for a full list of options, but 13 | here are some common actions: 14 | 15 | * You can run the benchmarks against the `master` branch with `asv run`. 16 | * To test the most recent commit on your branch `asv run HEAD^!`. 17 | * To generate a static website for browsing the results, run `asv publish`. The resulting HTML can be found in `benchmarks/html`. 18 | 19 | The asv docs have some more examples [here](https://asv.readthedocs.io/en/stable/using.html#benchmarking). 20 | 21 | ## Updating the Benchmark Website 22 | 23 | 1. Ensure any tags you wish to benchmark are included in the file `asvhashfile` at the root of the repo. 24 | 2. Run the benchmarks for those tags by running `asv run HASHFILE:asvhashfile`. This will take several minutes. 25 | 3. Create the HTML locally for those benchmarks by running `asv publish`. 26 | 4. Run `asv preview` to launch a local webserver that will let you preview the benchmarks dashboard. Navigate to the URL this command gives you and check everything looks fine. 27 | 5. Checkout the `rich-benchmarks` repo from [here](https://github.com/Textualize/rich-benchmarks) and `cd` into it. 28 | 6. Copy the HTML you generated earlier into the root of this repo, e.g. `cp -r ../rich/benchmarks/html/* .` (assuming you checked out `rich-benchmarks` alongside `rich` in your filesystem) 29 | 7. When the HTML is merged into `main`, the [benchmark dashboard](https://textualize.github.io/rich-benchmarks/) will be updated automatically via a GitHub Action. 30 | -------------------------------------------------------------------------------- /benchmarks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/benchmarks/__init__.py -------------------------------------------------------------------------------- /benchmarks/results/darrenburns-2022-mbp/machine.json: -------------------------------------------------------------------------------- 1 | { 2 | "arch": "arm64", 3 | "cpu": "Apple M1 Pro", 4 | "machine": "darrenburns-2022-mbp", 5 | "num_cpu": "10", 6 | "os": "Darwin 21.2.0", 7 | "ram": "17179869184", 8 | "version": 1 9 | } -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | alabaster==1.0.0 2 | Sphinx==8.2.3 3 | sphinx-rtd-theme==3.0.2 4 | sphinx-copybutton==0.5.2 5 | -------------------------------------------------------------------------------- /docs/source/appendix.rst: -------------------------------------------------------------------------------- 1 | Appendix 2 | ========= 3 | 4 | .. toctree:: 5 | :maxdepth: 3 6 | 7 | appendix/box.rst 8 | appendix/colors.rst 9 | -------------------------------------------------------------------------------- /docs/source/appendix/box.rst: -------------------------------------------------------------------------------- 1 | .. _appendix_box: 2 | 3 | Box 4 | === 5 | 6 | Rich has a number of constants that set the box characters used to draw tables and panels. To select a box style import one of the constants below from ``rich.box``. For example:: 7 | 8 | from rich import box 9 | table = Table(box=box.SQUARE) 10 | 11 | 12 | .. note:: 13 | Some of the box drawing characters will not display correctly on Windows legacy terminal (cmd.exe) with *raster* fonts, and are disabled by default. If you want the full range of box options on Windows legacy terminal, use a *truetype* font and set the ``safe_box`` parameter on the Table class to ``False``. 14 | 15 | 16 | The following table is generated with this command:: 17 | 18 | python -m rich.box 19 | 20 | .. image:: ../../images/box.svg 21 | -------------------------------------------------------------------------------- /docs/source/columns.rst: -------------------------------------------------------------------------------- 1 | Columns 2 | ======= 3 | 4 | Rich can render text or other Rich renderables in neat columns with the :class:`~rich.columns.Columns` class. To use, construct a Columns instance with an iterable of renderables and print it to the Console. 5 | 6 | The following example is a very basic clone of the ``ls`` command in OSX / Linux to list directory contents:: 7 | 8 | import os 9 | import sys 10 | 11 | from rich import print 12 | from rich.columns import Columns 13 | 14 | if len(sys.argv) < 2: 15 | print("Usage: python columns.py DIRECTORY") 16 | else: 17 | directory = os.listdir(sys.argv[1]) 18 | columns = Columns(directory, equal=True, expand=True) 19 | print(columns) 20 | 21 | 22 | See `columns.py `_ for an example which outputs columns containing more than just text. 23 | 24 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | import sys 21 | 22 | import sphinx_rtd_theme 23 | 24 | if sys.version_info >= (3, 8): 25 | from importlib.metadata import Distribution 26 | else: 27 | from importlib_metadata import Distribution 28 | 29 | html_theme = "sphinx_rtd_theme" 30 | 31 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 32 | 33 | project = "Rich" 34 | copyright = "Will McGugan" 35 | author = "Will McGugan" 36 | 37 | # The full version, including alpha/beta/rc tags 38 | release = Distribution.from_name("rich").version 39 | 40 | 41 | # -- General configuration --------------------------------------------------- 42 | 43 | # Add any Sphinx extension module names here, as strings. They can be 44 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 45 | # ones. 46 | extensions = [ 47 | "sphinx.ext.autodoc", 48 | "sphinx.ext.viewcode", 49 | "sphinx.ext.napoleon", 50 | "sphinx.ext.intersphinx", 51 | "sphinx.ext.autosectionlabel", 52 | "sphinx_copybutton", 53 | "sphinx_rtd_theme", 54 | ] 55 | 56 | # Add any paths that contain templates here, relative to this directory. 57 | templates_path = ["_templates"] 58 | 59 | # List of patterns, relative to source directory, that match files and 60 | # directories to ignore when looking for source files. 61 | # This pattern also affects html_static_path and html_extra_path. 62 | exclude_patterns = [] 63 | 64 | 65 | # -- Options for HTML output ------------------------------------------------- 66 | 67 | # The theme to use for HTML and HTML Help pages. See the documentation for 68 | # a list of builtin themes. 69 | # 70 | # html_theme = "alabaster" 71 | 72 | # Add any paths that contain custom static files (such as style sheets) here, 73 | # relative to this directory. They are copied after the builtin static files, 74 | # so a file named "default.css" will overwrite the builtin "default.css". 75 | html_static_path = ["_static"] 76 | 77 | intersphinx_mapping = {"python": ("http://docs.python.org/3", None)} 78 | 79 | autodoc_typehints = "description" 80 | 81 | html_css_files = [ 82 | "https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/fira_code.min.css" 83 | ] 84 | -------------------------------------------------------------------------------- /docs/source/group.rst: -------------------------------------------------------------------------------- 1 | Render Groups 2 | ============= 3 | 4 | The :class:`~rich.console.Group` class allows you to group several renderables together so they may be rendered in a context where only a single renderable may be supplied. For instance, you might want to display several renderables within a :class:`~rich.panel.Panel`. 5 | 6 | To render two panels within a third panel, you would construct a Group with the *child* renderables as positional arguments then wrap the result in another Panel:: 7 | 8 | from rich import print 9 | from rich.console import Group 10 | from rich.panel import Panel 11 | 12 | panel_group = Group( 13 | Panel("Hello", style="on blue"), 14 | Panel("World", style="on red"), 15 | ) 16 | print(Panel(panel_group)) 17 | 18 | 19 | This pattern is nice when you know in advance what renderables will be in a group, but can get awkward if you have a larger number of renderables, especially if they are dynamic. Rich provides a :func:`~rich.console.group` decorator to help with these situations. The decorator builds a group from an iterator of renderables. The following is the equivalent of the previous example using the decorator:: 20 | 21 | from rich import print 22 | from rich.console import group 23 | from rich.panel import Panel 24 | 25 | @group() 26 | def get_panels(): 27 | yield Panel("Hello", style="on blue") 28 | yield Panel("World", style="on red") 29 | 30 | print(Panel(get_panels())) 31 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Rich documentation master file, created by 2 | sphinx-quickstart on Thu Dec 26 17:03:20 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Rich's documentation! 7 | ================================ 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | introduction.rst 14 | console.rst 15 | style.rst 16 | markup.rst 17 | text.rst 18 | highlighting.rst 19 | pretty.rst 20 | logging.rst 21 | traceback.rst 22 | prompt.rst 23 | 24 | columns.rst 25 | group.rst 26 | markdown.rst 27 | padding.rst 28 | panel.rst 29 | progress.rst 30 | syntax.rst 31 | tables.rst 32 | tree.rst 33 | live.rst 34 | layout.rst 35 | 36 | protocol.rst 37 | 38 | reference.rst 39 | appendix.rst 40 | 41 | Indices and tables 42 | ================== 43 | 44 | * :ref:`genindex` 45 | * :ref:`modindex` 46 | * :ref:`search` 47 | -------------------------------------------------------------------------------- /docs/source/logging.rst: -------------------------------------------------------------------------------- 1 | Logging Handler 2 | =============== 3 | 4 | Rich supplies a :ref:`logging handler` which will format and colorize text written by Python's logging module. 5 | 6 | Here's an example of how to set up a rich logger:: 7 | 8 | import logging 9 | from rich.logging import RichHandler 10 | 11 | FORMAT = "%(message)s" 12 | logging.basicConfig( 13 | level="NOTSET", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()] 14 | ) 15 | 16 | log = logging.getLogger("rich") 17 | log.info("Hello, World!") 18 | 19 | Rich logs won't render :ref:`console_markup` in logging by default as most libraries won't be aware of the need to escape literal square brackets, but you can enable it by setting ``markup=True`` on the handler. Alternatively you can enable it per log message by supplying the ``extra`` argument as follows:: 20 | 21 | log.error("[bold red blink]Server is shutting down![/]", extra={"markup": True}) 22 | 23 | Similarly, the highlighter may be overridden per log message:: 24 | 25 | log.error("123 will not be highlighted", extra={"highlighter": None}) 26 | 27 | 28 | Handle exceptions 29 | ------------------- 30 | 31 | The :class:`~rich.logging.RichHandler` class may be configured to use Rich's :class:`~rich.traceback.Traceback` class to format exceptions, which provides more context than a built-in exception. To get beautiful exceptions in your logs set ``rich_tracebacks=True`` on the handler constructor:: 32 | 33 | 34 | import logging 35 | from rich.logging import RichHandler 36 | 37 | logging.basicConfig( 38 | level="NOTSET", 39 | format="%(message)s", 40 | datefmt="[%X]", 41 | handlers=[RichHandler(rich_tracebacks=True)] 42 | ) 43 | 44 | log = logging.getLogger("rich") 45 | try: 46 | print(1 / 0) 47 | except Exception: 48 | log.exception("unable print!") 49 | 50 | 51 | There are a number of other options you can use to configure logging output, see the :class:`~rich.logging.RichHandler` reference for details. 52 | 53 | Suppressing Frames 54 | ------------------ 55 | 56 | If you are working with a framework (click, django etc), you may only be interested in seeing the code from your own application within the traceback. You can exclude framework code by setting the `suppress` argument on `Traceback`, `install`, and `Console.print_exception`, which should be a list of modules or str paths. 57 | 58 | Here's how you would exclude `click `_ from Rich exceptions:: 59 | 60 | import click 61 | import logging 62 | from rich.logging import RichHandler 63 | 64 | logging.basicConfig( 65 | level="NOTSET", 66 | format="%(message)s", 67 | datefmt="[%X]", 68 | handlers=[RichHandler(rich_tracebacks=True, tracebacks_suppress=[click])] 69 | ) 70 | 71 | Suppressed frames will show the line and file only, without any code. -------------------------------------------------------------------------------- /docs/source/markdown.rst: -------------------------------------------------------------------------------- 1 | Markdown 2 | ======== 3 | 4 | Rich can render Markdown to the console. To render markdown, construct a :class:`~rich.markdown.Markdown` object then print it to the console. Markdown is a great way of adding rich content to your command line applications. Here's an example of use:: 5 | 6 | MARKDOWN = """ 7 | # This is an h1 8 | 9 | Rich can do a pretty *decent* job of rendering markdown. 10 | 11 | 1. This is a list item 12 | 2. This is another list item 13 | """ 14 | from rich.console import Console 15 | from rich.markdown import Markdown 16 | 17 | console = Console() 18 | md = Markdown(MARKDOWN) 19 | console.print(md) 20 | 21 | Note that code blocks are rendered with full syntax highlighting! 22 | 23 | You can also use the Markdown class from the command line. The following example displays a readme in the terminal:: 24 | 25 | python -m rich.markdown README.md 26 | 27 | Run the following to see the full list of arguments for the markdown command:: 28 | 29 | python -m rich.markdown -h -------------------------------------------------------------------------------- /docs/source/padding.rst: -------------------------------------------------------------------------------- 1 | Padding 2 | ======= 3 | 4 | The :class:`~rich.padding.Padding` class may be used to add whitespace around text or other renderable. The following example will print the word "Hello" with a padding of 1 character, so there will be a blank line above and below, and a space on the left and right edges:: 5 | 6 | from rich import print 7 | from rich.padding import Padding 8 | test = Padding("Hello", 1) 9 | print(test) 10 | 11 | You can specify the padding on a more granular level by using a tuple of values rather than a single value. A tuple of 2 values sets the top/bottom and left/right padding, whereas a tuple of 4 values sets the padding for top, right, bottom, and left sides. You may recognize this scheme if you are familiar with CSS. 12 | 13 | For example, the following displays 2 blank lines above and below the text, and a padding of 4 spaces on the left and right sides:: 14 | 15 | from rich import print 16 | from rich.padding import Padding 17 | test = Padding("Hello", (2, 4)) 18 | print(test) 19 | 20 | The Padding class can also accept a ``style`` argument which applies a style to the padding and contents, and an ``expand`` switch which can be set to False to prevent the padding from extending to the full width of the terminal. Here's an example which demonstrates both these arguments:: 21 | 22 | from rich import print 23 | from rich.padding import Padding 24 | test = Padding("Hello", (2, 4), style="on blue", expand=False) 25 | print(test) 26 | 27 | Note that, as with all Rich renderables, you can use Padding in any context. For instance, if you want to emphasize an item in a :class:`~rich.table.Table` you could add a Padding object to a row with a padding of 1 and a style of "on red". 28 | -------------------------------------------------------------------------------- /docs/source/panel.rst: -------------------------------------------------------------------------------- 1 | Panel 2 | ===== 3 | 4 | To draw a border around text or other renderable, construct a :class:`~rich.panel.Panel` with the renderable as the first positional argument. Here's an example:: 5 | 6 | from rich import print 7 | from rich.panel import Panel 8 | print(Panel("Hello, [red]World!")) 9 | 10 | You can change the style of the panel by setting the ``box`` argument to the Panel constructor. See :ref:`appendix_box` for a list of available box styles. 11 | 12 | Panels will extend to the full width of the terminal. You can make panel *fit* the content by setting ``expand=False`` on the constructor, or by creating the Panel with :meth:`~rich.panel.Panel.fit`. For example:: 13 | 14 | from rich import print 15 | from rich.panel import Panel 16 | print(Panel.fit("Hello, [red]World!")) 17 | 18 | The Panel constructor accepts a ``title`` argument which will draw a title on the top of the panel, as well as a ``subtitle`` argument which will draw a subtitle on the bottom of the panel:: 19 | 20 | from rich import print 21 | from rich.panel import Panel 22 | print(Panel("Hello, [red]World!", title="Welcome", subtitle="Thank you")) 23 | 24 | See :class:`~rich.panel.Panel` for details how to customize Panels. 25 | -------------------------------------------------------------------------------- /docs/source/prompt.rst: -------------------------------------------------------------------------------- 1 | Prompt 2 | ====== 3 | 4 | Rich has a number of :class:`~rich.prompt.Prompt` classes which ask a user for input and loop until a valid response is received (they all use the :ref:`Console API` internally). Here's a simple example:: 5 | 6 | >>> from rich.prompt import Prompt 7 | >>> name = Prompt.ask("Enter your name") 8 | 9 | The prompt may be given as a string (which may contain :ref:`console_markup` and emoji code) or as a :class:`~rich.text.Text` instance. 10 | 11 | You can set a default value which will be returned if the user presses return without entering any text:: 12 | 13 | >>> from rich.prompt import Prompt 14 | >>> name = Prompt.ask("Enter your name", default="Paul Atreides") 15 | 16 | If you supply a list of choices, the prompt will loop until the user enters one of the choices:: 17 | 18 | >>> from rich.prompt import Prompt 19 | >>> name = Prompt.ask("Enter your name", choices=["Paul", "Jessica", "Duncan"], default="Paul") 20 | 21 | By default this is case sensitive, but you can set `case_sensitive=False` to make it case insensitive:: 22 | 23 | >>> from rich.prompt import Prompt 24 | >>> name = Prompt.ask("Enter your name", choices=["Paul", "Jessica", "Duncan"], default="Paul", case_sensitive=False) 25 | 26 | Now, it would accept "paul" or "Paul" as valid responses. 27 | 28 | 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. 29 | 30 | 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:: 31 | 32 | >>> from rich.prompt import Confirm 33 | >>> is_rich_great = Confirm.ask("Do you like rich?") 34 | >>> assert is_rich_great 35 | 36 | The Prompt class was designed to be customizable via inheritance. See `prompt.py `_ for examples. 37 | 38 | To see some of the prompts in action, run the following command from the command line:: 39 | 40 | python -m rich.prompt 41 | -------------------------------------------------------------------------------- /docs/source/reference.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ========= 3 | 4 | .. toctree:: 5 | :maxdepth: 3 6 | 7 | reference/align.rst 8 | reference/bar.rst 9 | reference/color.rst 10 | reference/columns.rst 11 | reference/console.rst 12 | reference/emoji.rst 13 | reference/highlighter.rst 14 | reference/init.rst 15 | reference/json.rst 16 | reference/layout.rst 17 | reference/live.rst 18 | reference/logging.rst 19 | reference/markdown.rst 20 | reference/markup.rst 21 | reference/measure.rst 22 | reference/padding.rst 23 | reference/panel.rst 24 | reference/pretty.rst 25 | reference/progress_bar.rst 26 | reference/progress.rst 27 | reference/prompt.rst 28 | reference/protocol.rst 29 | reference/rule.rst 30 | reference/segment.rst 31 | reference/spinner.rst 32 | reference/status.rst 33 | reference/style.rst 34 | reference/styled.rst 35 | reference/syntax.rst 36 | reference/table.rst 37 | reference/text.rst 38 | reference/theme.rst 39 | reference/traceback.rst 40 | reference/tree.rst 41 | reference/abc.rst 42 | -------------------------------------------------------------------------------- /docs/source/reference/abc.rst: -------------------------------------------------------------------------------- 1 | rich.abc 2 | ======== 3 | 4 | .. automodule:: rich.abc 5 | :members: 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/source/reference/align.rst: -------------------------------------------------------------------------------- 1 | rich.align 2 | ========== 3 | 4 | .. automodule:: rich.align 5 | :members: 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/source/reference/bar.rst: -------------------------------------------------------------------------------- 1 | rich.bar 2 | ======== 3 | 4 | .. automodule:: rich.bar 5 | :members: 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/source/reference/color.rst: -------------------------------------------------------------------------------- 1 | rich.color 2 | ========== 3 | 4 | .. automodule:: rich.color 5 | :members: 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/source/reference/columns.rst: -------------------------------------------------------------------------------- 1 | rich.columns 2 | ============ 3 | 4 | .. automodule:: rich.columns 5 | :members: 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/source/reference/console.rst: -------------------------------------------------------------------------------- 1 | rich.console 2 | ============ 3 | 4 | .. automodule:: rich.console 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/source/reference/emoji.rst: -------------------------------------------------------------------------------- 1 | rich.emoji 2 | ========== 3 | 4 | .. automodule:: rich.emoji 5 | :members: Emoji 6 | 7 | -------------------------------------------------------------------------------- /docs/source/reference/highlighter.rst: -------------------------------------------------------------------------------- 1 | rich.highlighter 2 | ================ 3 | 4 | .. automodule:: rich.highlighter 5 | :members: 6 | :special-members: __call__ 7 | 8 | -------------------------------------------------------------------------------- /docs/source/reference/init.rst: -------------------------------------------------------------------------------- 1 | rich 2 | ==== 3 | 4 | .. automodule:: rich 5 | :members: 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/source/reference/json.rst: -------------------------------------------------------------------------------- 1 | rich.json 2 | ========= 3 | 4 | .. automodule:: rich.json 5 | :members: 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/source/reference/layout.rst: -------------------------------------------------------------------------------- 1 | rich.layout 2 | =========== 3 | 4 | .. automodule:: rich.layout 5 | :members: 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/source/reference/live.rst: -------------------------------------------------------------------------------- 1 | rich.live 2 | ========= 3 | 4 | .. automodule:: rich.live 5 | :members: -------------------------------------------------------------------------------- /docs/source/reference/logging.rst: -------------------------------------------------------------------------------- 1 | .. _logging: 2 | 3 | rich.logging 4 | ============ 5 | 6 | .. automodule:: rich.logging 7 | :members: RichHandler 8 | 9 | -------------------------------------------------------------------------------- /docs/source/reference/markdown.rst: -------------------------------------------------------------------------------- 1 | rich.markdown 2 | ============= 3 | 4 | .. automodule:: rich.markdown 5 | :members: 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/source/reference/markup.rst: -------------------------------------------------------------------------------- 1 | rich.markup 2 | =========== 3 | 4 | .. automodule:: rich.markup 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/source/reference/measure.rst: -------------------------------------------------------------------------------- 1 | rich.measure 2 | ============ 3 | 4 | .. automodule:: rich.measure 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/source/reference/padding.rst: -------------------------------------------------------------------------------- 1 | rich.padding 2 | ============ 3 | 4 | .. automodule:: rich.padding 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/source/reference/panel.rst: -------------------------------------------------------------------------------- 1 | rich.panel 2 | ========== 3 | 4 | .. automodule:: rich.panel 5 | :members: Panel 6 | 7 | -------------------------------------------------------------------------------- /docs/source/reference/pretty.rst: -------------------------------------------------------------------------------- 1 | rich.pretty 2 | =========== 3 | 4 | .. automodule:: rich.pretty 5 | :members: 6 | 7 | -------------------------------------------------------------------------------- /docs/source/reference/progress.rst: -------------------------------------------------------------------------------- 1 | rich.progress 2 | ============= 3 | 4 | .. automodule:: rich.progress 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/source/reference/progress_bar.rst: -------------------------------------------------------------------------------- 1 | rich.progress_bar 2 | ================= 3 | 4 | .. automodule:: rich.progress_bar 5 | :members: 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/source/reference/prompt.rst: -------------------------------------------------------------------------------- 1 | rich.prompt 2 | =========== 3 | 4 | .. automodule:: rich.prompt 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/source/reference/protocol.rst: -------------------------------------------------------------------------------- 1 | rich.protocol 2 | ============= 3 | 4 | .. automodule:: rich.protocol 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/source/reference/rule.rst: -------------------------------------------------------------------------------- 1 | rich.rule 2 | ========= 3 | 4 | .. automodule:: rich.rule 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/source/reference/segment.rst: -------------------------------------------------------------------------------- 1 | rich.segment 2 | ============ 3 | 4 | .. automodule:: rich.segment 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/source/reference/spinner.rst: -------------------------------------------------------------------------------- 1 | rich.spinner 2 | ============ 3 | 4 | .. automodule:: rich.spinner 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/source/reference/status.rst: -------------------------------------------------------------------------------- 1 | rich.status 2 | ============ 3 | 4 | .. automodule:: rich.status 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/source/reference/style.rst: -------------------------------------------------------------------------------- 1 | rich.style 2 | ========== 3 | 4 | .. automodule:: rich.style 5 | :members: 6 | :special-members: __call__ 7 | 8 | -------------------------------------------------------------------------------- /docs/source/reference/styled.rst: -------------------------------------------------------------------------------- 1 | rich.styled 2 | =========== 3 | 4 | .. automodule:: rich.styled 5 | :members: 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/source/reference/syntax.rst: -------------------------------------------------------------------------------- 1 | rich.syntax 2 | =========== 3 | 4 | .. automodule:: rich.syntax 5 | :members: Syntax 6 | -------------------------------------------------------------------------------- /docs/source/reference/table.rst: -------------------------------------------------------------------------------- 1 | rich.table 2 | ========== 3 | 4 | .. automodule:: rich.table 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/source/reference/text.rst: -------------------------------------------------------------------------------- 1 | rich.text 2 | ========= 3 | 4 | .. automodule:: rich.text 5 | :members: Text, TextType 6 | 7 | -------------------------------------------------------------------------------- /docs/source/reference/theme.rst: -------------------------------------------------------------------------------- 1 | rich.theme 2 | ========== 3 | 4 | .. automodule:: rich.theme 5 | :members: Theme 6 | 7 | -------------------------------------------------------------------------------- /docs/source/reference/traceback.rst: -------------------------------------------------------------------------------- 1 | rich.traceback 2 | ============== 3 | 4 | .. automodule:: rich.traceback 5 | :members: Traceback, install 6 | 7 | -------------------------------------------------------------------------------- /docs/source/reference/tree.rst: -------------------------------------------------------------------------------- 1 | rich.tree 2 | ========= 3 | 4 | .. automodule:: rich.tree 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/source/syntax.rst: -------------------------------------------------------------------------------- 1 | Syntax 2 | ====== 3 | 4 | Rich can syntax highlight various programming languages with line numbers. 5 | 6 | To syntax highlight code, construct a :class:`~rich.syntax.Syntax` object and print it to the console. Here's an example:: 7 | 8 | from rich.console import Console 9 | from rich.syntax import Syntax 10 | 11 | console = Console() 12 | with open("syntax.py", "rt") as code_file: 13 | syntax = Syntax(code_file.read(), "python") 14 | console.print(syntax) 15 | 16 | You may also use the :meth:`~rich.syntax.Syntax.from_path` alternative constructor which will load the code from disk and auto-detect the file type. The example above could be re-written as follows:: 17 | 18 | 19 | from rich.console import Console 20 | from rich.syntax import Syntax 21 | 22 | console = Console() 23 | syntax = Syntax.from_path("syntax.py") 24 | console.print(syntax) 25 | 26 | 27 | Line numbers 28 | ------------ 29 | 30 | If you set ``line_numbers=True``, Rich will render a column for line numbers:: 31 | 32 | syntax = Syntax.from_path("syntax.py", line_numbers=True) 33 | 34 | 35 | Theme 36 | ----- 37 | 38 | The Syntax constructor (and :meth:`~rich.syntax.Syntax.from_path`) accept a ``theme`` attribute which should be the name of a `Pygments theme `_. It may also be one of the special case theme names "ansi_dark" or "ansi_light" which will use the color theme configured by the terminal. 39 | 40 | 41 | Background color 42 | ---------------- 43 | 44 | You can override the background color from the theme by supplying a ``background_color`` argument to the constructor. This should be a string in the same format a style definition accepts, e.g. "red", "#ff0000", "rgb(255,0,0)" etc. You may also set the special value "default" which will use the default background color set in the terminal. 45 | 46 | 47 | Syntax CLI 48 | ---------- 49 | 50 | You can use this class from the command line. Here's how you would syntax highlight a file called "syntax.py":: 51 | 52 | python -m rich.syntax syntax.py 53 | 54 | For the full list of arguments, run the following:: 55 | 56 | python -m rich.syntax -h 57 | 58 | -------------------------------------------------------------------------------- /docs/source/text.rst: -------------------------------------------------------------------------------- 1 | .. _rich_text: 2 | 3 | Rich Text 4 | ========= 5 | 6 | Rich has a :class:`~rich.text.Text` class you can use to mark up strings with color and style attributes. You can use a Text instance anywhere a string is accepted, which gives you a lot of control over presentation. 7 | 8 | You can consider this class to be like a string with marked up regions of text. Unlike a built-in ``str``, a Text instance is mutable, and most methods operate in-place rather than returning a new instance. 9 | 10 | One way to add a style to Text is the :meth:`~rich.text.Text.stylize` method which applies a style to a start and end offset. Here is an example:: 11 | 12 | from rich.console import Console 13 | from rich.text import Text 14 | 15 | console = Console() 16 | text = Text("Hello, World!") 17 | text.stylize("bold magenta", 0, 6) 18 | console.print(text) 19 | 20 | This will print "Hello, World!" to the terminal, with the first word in bold magenta. 21 | 22 | Alternatively, you can construct styled text by calling :meth:`~rich.text.Text.append` to add a string and style to the end of the Text. Here's an example:: 23 | 24 | text = Text() 25 | text.append("Hello", style="bold magenta") 26 | text.append(" World!") 27 | console.print(text) 28 | 29 | If you would like to use text that is already formatted with ANSI codes, call :meth:`~rich.text.Text.from_ansi` to convert it to a ``Text`` object:: 30 | 31 | text = Text.from_ansi("\033[1;35mHello\033[0m, World!") 32 | console.print(text.spans) 33 | 34 | Since building Text instances from parts is a common requirement, Rich offers :meth:`~rich.text.Text.assemble` which will combine strings or pairs of string and Style, and return a Text instance. The following example is equivalent to the ANSI example above:: 35 | 36 | text = Text.assemble(("Hello", "bold magenta"), ", World!") 37 | console.print(text) 38 | 39 | You can apply a style to given words in the text with :meth:`~rich.text.Text.highlight_words` or for ultimate control call :meth:`~rich.text.Text.highlight_regex` to highlight text matching a *regular expression*. 40 | 41 | 42 | Text attributes 43 | ~~~~~~~~~~~~~~~ 44 | 45 | The Text class has a number of parameters you can set on the constructor to modify how the text is displayed. 46 | 47 | - ``justify`` should be "left", "center", "right", or "full", and will override default justify behavior. 48 | - ``overflow`` should be "fold", "crop", or "ellipsis", and will override default overflow. 49 | - ``no_wrap`` prevents wrapping if the text is longer then the available width. 50 | - ``tab_size`` Sets the number of characters in a tab. 51 | 52 | A Text instance may be used in place of a plain string virtually everywhere in the Rich API, which gives you a lot of control in how text renders within other Rich renderables. For instance, the following example right aligns text within a :class:`~rich.panel.Panel`:: 53 | 54 | from rich import print 55 | from rich.panel import Panel 56 | from rich.text import Text 57 | panel = Panel(Text("Hello", justify="right")) 58 | print(panel) 59 | 60 | 61 | -------------------------------------------------------------------------------- /docs/source/tree.rst: -------------------------------------------------------------------------------- 1 | Tree 2 | ==== 3 | 4 | Rich has a :class:`~rich.tree.Tree` class which can generate a tree view in the terminal. A tree view is a great way of presenting the contents of a filesystem or any other hierarchical data. Each branch of the tree can have a label which may be text or any other Rich renderable. 5 | 6 | Run the following command to see a demonstration of a Rich tree:: 7 | 8 | python -m rich.tree 9 | 10 | The following code creates and prints a tree with a simple text label:: 11 | 12 | from rich.tree import Tree 13 | from rich import print 14 | 15 | tree = Tree("Rich Tree") 16 | print(tree) 17 | 18 | With only a single ``Tree`` instance this will output nothing more than the text "Rich Tree". Things get more interesting when we call :meth:`~rich.tree.Tree.add` to add more branches to the Tree. The following code adds two more branches:: 19 | 20 | tree.add("foo") 21 | tree.add("bar") 22 | print(tree) 23 | 24 | The tree will now have two branches connected to the original tree with guide lines. 25 | 26 | When you call :meth:`~rich.tree.Tree.add` a new Tree instance is returned. You can use this instance to add more branches to, and build up a more complex tree. Let's add a few more levels to the tree:: 27 | 28 | baz_tree = tree.add("baz") 29 | baz_tree.add("[red]Red").add("[green]Green").add("[blue]Blue") 30 | print(tree) 31 | 32 | 33 | Tree Styles 34 | ~~~~~~~~~~~ 35 | 36 | The Tree constructor and :meth:`~rich.tree.Tree.add` method allows you to specify a ``style`` argument which sets a style for the entire branch, and ``guide_style`` which sets the style for the guide lines. These styles are inherited by the branches and will apply to any sub-trees as well. 37 | 38 | If you set ``guide_style`` to bold, Rich will select the thicker variations of unicode line characters. Similarly, if you select the "underline2" style you will get double line style of unicode characters. 39 | 40 | 41 | Examples 42 | ~~~~~~~~ 43 | 44 | For a more practical demonstration, see `tree.py `_ which can generate a tree view of a directory in your hard drive. 45 | 46 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This directory contains various demonstrations various Rich features. To run them, make sure Rich is installed, then run the example you want with `python example.py` on the command line. For example, `python justify.py`. 4 | 5 | Be sure to check the source! 6 | -------------------------------------------------------------------------------- /examples/attrs.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | try: 4 | import attr 5 | except ImportError: 6 | print("This example requires attrs library") 7 | print("pip install attrs") 8 | raise SystemExit() 9 | 10 | 11 | @attr.define 12 | class Point3D: 13 | x: float 14 | y: float 15 | z: float = 0 16 | 17 | 18 | @attr.define 19 | class Triangle: 20 | point1: Point3D 21 | point2: Point3D 22 | point3: Point3D 23 | 24 | 25 | @attr.define 26 | class Model: 27 | name: str 28 | triangles: List[Triangle] = attr.Factory(list) 29 | 30 | 31 | if __name__ == "__main__": 32 | model = Model( 33 | name="Alien#1", 34 | triangles=[ 35 | Triangle( 36 | Point3D(x=20, y=50), 37 | Point3D(x=50, y=15, z=-45.34), 38 | Point3D(3.1426, 83.2323, -16), 39 | ) 40 | ], 41 | ) 42 | 43 | from rich.console import Console 44 | from rich.pretty import Pretty 45 | from rich.table import Column, Table 46 | from rich.text import Text 47 | 48 | console = Console() 49 | 50 | table = Table("attrs *with* Rich", Column(Text.from_markup("attrs *without* Rich"))) 51 | 52 | table.add_row(Pretty(model), repr(model)) 53 | console.print(table) 54 | -------------------------------------------------------------------------------- /examples/bars.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Use Bar to renderer a sort-of circle. 4 | 5 | """ 6 | import math 7 | 8 | from rich.align import Align 9 | from rich.bar import Bar 10 | from rich.color import Color 11 | from rich import print 12 | 13 | 14 | SIZE = 40 15 | 16 | for row in range(SIZE): 17 | y = (row / (SIZE - 1)) * 2 - 1 18 | x = math.sqrt(1 - y * y) 19 | color = Color.from_rgb((1 + y) * 127.5, 0, 0) 20 | bar = Bar(2, width=SIZE * 2, begin=1 - x, end=1 + x, color=color) 21 | print(Align.center(bar)) 22 | -------------------------------------------------------------------------------- /examples/columns.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to display content in columns. 3 | 4 | The data is pulled from https://randomuser.me 5 | """ 6 | 7 | import json 8 | from urllib.request import urlopen 9 | 10 | from rich.console import Console 11 | from rich.columns import Columns 12 | from rich.panel import Panel 13 | 14 | 15 | def get_content(user): 16 | """Extract text from user dict.""" 17 | country = user["location"]["country"] 18 | name = f"{user['name']['first']} {user['name']['last']}" 19 | return f"[b]{name}[/b]\n[yellow]{country}" 20 | 21 | 22 | console = Console() 23 | 24 | 25 | users = json.loads(urlopen("https://randomuser.me/api/?results=30").read())["results"] 26 | console.print(users, overflow="ignore", crop=False) 27 | user_renderables = [Panel(get_content(user), expand=True) for user in users] 28 | console.print(Columns(user_renderables)) 29 | -------------------------------------------------------------------------------- /examples/cp_progress.py: -------------------------------------------------------------------------------- 1 | """ 2 | A very minimal `cp` clone that displays a progress bar. 3 | """ 4 | import os 5 | import shutil 6 | import sys 7 | 8 | from rich.progress import Progress 9 | 10 | if __name__ == "__main__": 11 | if len(sys.argv) == 3: 12 | with Progress() as progress: 13 | desc = os.path.basename(sys.argv[1]) 14 | with progress.open(sys.argv[1], "rb", description=desc) as src: 15 | with open(sys.argv[2], "wb") as dst: 16 | shutil.copyfileobj(src, dst) 17 | else: 18 | print("Copy a file with a progress bar.") 19 | print("Usage:\n\tpython cp_progress.py SRC DST") 20 | -------------------------------------------------------------------------------- /examples/downloader.py: -------------------------------------------------------------------------------- 1 | """ 2 | A rudimentary URL downloader (like wget or curl) to demonstrate Rich progress bars. 3 | """ 4 | 5 | import os.path 6 | import sys 7 | from concurrent.futures import ThreadPoolExecutor 8 | import signal 9 | from functools import partial 10 | from threading import Event 11 | from typing import Iterable 12 | from urllib.request import urlopen 13 | 14 | from rich.progress import ( 15 | BarColumn, 16 | DownloadColumn, 17 | Progress, 18 | TaskID, 19 | TextColumn, 20 | TimeRemainingColumn, 21 | TransferSpeedColumn, 22 | ) 23 | 24 | progress = Progress( 25 | TextColumn("[bold blue]{task.fields[filename]}", justify="right"), 26 | BarColumn(bar_width=None), 27 | "[progress.percentage]{task.percentage:>3.1f}%", 28 | "•", 29 | DownloadColumn(), 30 | "•", 31 | TransferSpeedColumn(), 32 | "•", 33 | TimeRemainingColumn(), 34 | ) 35 | 36 | 37 | done_event = Event() 38 | 39 | 40 | def handle_sigint(signum, frame): 41 | done_event.set() 42 | 43 | 44 | signal.signal(signal.SIGINT, handle_sigint) 45 | 46 | 47 | def copy_url(task_id: TaskID, url: str, path: str) -> None: 48 | """Copy data from a url to a local file.""" 49 | progress.console.log(f"Requesting {url}") 50 | response = urlopen(url) 51 | # This will break if the response doesn't contain content length 52 | progress.update(task_id, total=int(response.info()["Content-length"])) 53 | with open(path, "wb") as dest_file: 54 | progress.start_task(task_id) 55 | for data in iter(partial(response.read, 32768), b""): 56 | dest_file.write(data) 57 | progress.update(task_id, advance=len(data)) 58 | if done_event.is_set(): 59 | return 60 | progress.console.log(f"Downloaded {path}") 61 | 62 | 63 | def download(urls: Iterable[str], dest_dir: str): 64 | """Download multiple files to the given directory.""" 65 | 66 | with progress: 67 | with ThreadPoolExecutor(max_workers=4) as pool: 68 | for url in urls: 69 | filename = url.split("/")[-1] 70 | dest_path = os.path.join(dest_dir, filename) 71 | task_id = progress.add_task("download", filename=filename, start=False) 72 | pool.submit(copy_url, task_id, url, dest_path) 73 | 74 | 75 | if __name__ == "__main__": 76 | # Try with https://releases.ubuntu.com/noble/ubuntu-24.04-desktop-amd64.iso 77 | # and https://releases.ubuntu.com/noble/ubuntu-24.04-live-server-amd64.iso 78 | if sys.argv[1:]: 79 | download(sys.argv[1:], "./") 80 | else: 81 | print("Usage:\n\tpython downloader.py URL1 URL2 URL3 (etc)") 82 | -------------------------------------------------------------------------------- /examples/exception.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic example to show how to print an traceback of an exception 3 | """ 4 | from typing import List, Tuple 5 | 6 | from rich.console import Console 7 | 8 | console = Console() 9 | 10 | 11 | def divide_by(number: float, divisor: float) -> float: 12 | """Divide any number by zero.""" 13 | # Will throw a ZeroDivisionError if divisor is 0 14 | result = number / divisor 15 | return result 16 | 17 | 18 | def divide_all(divides: List[Tuple[float, float]]) -> None: 19 | """Do something impossible every day.""" 20 | 21 | for number, divisor in divides: 22 | console.print(f"dividing {number} by {divisor}") 23 | try: 24 | result = divide_by(number, divisor) 25 | except Exception: 26 | console.print_exception(extra_lines=8, show_locals=True) 27 | else: 28 | console.print(f" = {result}") 29 | 30 | 31 | DIVIDES = [ 32 | (1000, 200), 33 | (10000, 500), 34 | (1, 0), 35 | (0, 1000000), 36 | (3.1427, 2), 37 | (888, 0), 38 | (2**32, 2**16), 39 | ] 40 | 41 | divide_all(DIVIDES) 42 | -------------------------------------------------------------------------------- /examples/export.py: -------------------------------------------------------------------------------- 1 | """ 2 | Demonstrates export console output 3 | """ 4 | 5 | from rich.console import Console 6 | from rich.table import Table 7 | 8 | console = Console(record=True) 9 | 10 | 11 | def print_table(): 12 | table = Table(title="Star Wars Movies") 13 | 14 | table.add_column("Released", style="cyan", no_wrap=True) 15 | table.add_column("Title", style="magenta") 16 | table.add_column("Box Office", justify="right", style="green") 17 | 18 | table.add_row("Dec 20, 2019", "Star Wars: The Rise of Skywalker", "$952,110,690") 19 | table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347") 20 | table.add_row("Dec 15, 2017", "Star Wars Ep. V111: The Last Jedi", "$1,332,539,889") 21 | table.add_row("Dec 16, 2016", "Rogue One: A Star Wars Story", "$1,332,439,889") 22 | 23 | console.print(table) 24 | 25 | 26 | # Prints table 27 | print_table() 28 | 29 | # Get console output as text 30 | file1 = "table_export_plaintext.txt" 31 | text = console.export_text() 32 | with open(file1, "w") as file: 33 | file.write(text) 34 | print(f"Exported console output as plain text to {file1}") 35 | 36 | # Calling print_table again because console output buffer 37 | # is flushed once export function is called 38 | print_table() 39 | 40 | # Get console output as html 41 | # use clear=False so output is not flushed after export 42 | file2 = "table_export_html.html" 43 | html = console.export_html(clear=False) 44 | with open(file2, "w") as file: 45 | file.write(html) 46 | print(f"Exported console output as html to {file2}") 47 | 48 | # Export text output to table_export.txt 49 | file3 = "table_export_plaintext2.txt" 50 | console.save_text(file3, clear=False) 51 | print(f"Exported console output as plain text to {file3}") 52 | 53 | # Export html output to table_export.html 54 | file4 = "table_export_html2.html" 55 | console.save_html(file4) 56 | print(f"Exported console output as html to {file4}") 57 | -------------------------------------------------------------------------------- /examples/file_progress.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from urllib.request import urlopen 3 | 4 | from rich.progress import wrap_file 5 | 6 | # Read a URL with urlopen 7 | response = urlopen("https://www.textualize.io") 8 | # Get the size from the headers 9 | size = int(response.headers["Content-Length"]) 10 | 11 | # Wrap the response so that it update progress 12 | 13 | with wrap_file(response, size) as file: 14 | for line in file: 15 | print(line.decode("utf-8"), end="") 16 | sleep(0.1) 17 | -------------------------------------------------------------------------------- /examples/group.py: -------------------------------------------------------------------------------- 1 | from rich import print 2 | from rich.console import Group 3 | from rich.panel import Panel 4 | 5 | panel_group = Group( 6 | Panel("Hello", style="on blue"), 7 | Panel("World", style="on red"), 8 | ) 9 | print(Panel(panel_group)) 10 | -------------------------------------------------------------------------------- /examples/group2.py: -------------------------------------------------------------------------------- 1 | from rich import print 2 | from rich.console import group 3 | from rich.panel import Panel 4 | 5 | 6 | @group() 7 | def get_panels(): 8 | yield Panel("Hello", style="on blue") 9 | yield Panel("World", style="on red") 10 | 11 | 12 | print(Panel(get_panels())) 13 | -------------------------------------------------------------------------------- /examples/highlighter.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example demonstrates a simple text highlighter. 3 | """ 4 | 5 | from rich.console import Console 6 | from rich.highlighter import RegexHighlighter 7 | from rich.theme import Theme 8 | 9 | 10 | class EmailHighlighter(RegexHighlighter): 11 | """Apply style to anything that looks like an email.""" 12 | 13 | base_style = "example." 14 | highlights = [r"(?P[\w-]+@([\w-]+\.)+[\w-]+)"] 15 | 16 | 17 | theme = Theme({"example.email": "bold magenta"}) 18 | console = Console(highlighter=EmailHighlighter(), theme=theme) 19 | 20 | console.print("Send funds to money@example.org") 21 | -------------------------------------------------------------------------------- /examples/jobs.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from rich.panel import Panel 3 | from rich.progress import Progress 4 | 5 | 6 | JOBS = [100, 150, 25, 70, 110, 90] 7 | 8 | progress = Progress(auto_refresh=False) 9 | master_task = progress.add_task("overall", total=sum(JOBS)) 10 | jobs_task = progress.add_task("jobs") 11 | 12 | progress.console.print( 13 | Panel( 14 | "[bold blue]A demonstration of progress with a current task and overall progress.", 15 | padding=1, 16 | ) 17 | ) 18 | 19 | with progress: 20 | for job_no, job in enumerate(JOBS): 21 | progress.log(f"Starting job #{job_no}") 22 | sleep(0.2) 23 | progress.reset(jobs_task, total=job, description=f"job [bold yellow]#{job_no}") 24 | progress.start_task(jobs_task) 25 | for wait in progress.track(range(job), task_id=jobs_task): 26 | sleep(0.01) 27 | progress.advance(master_task, job) 28 | progress.log(f"Job #{job_no} is complete") 29 | progress.log( 30 | Panel(":sparkle: All done! :sparkle:", border_style="green", padding=1) 31 | ) 32 | -------------------------------------------------------------------------------- /examples/justify.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example demonstrates the justify argument to print. 3 | """ 4 | 5 | from rich.console import Console 6 | 7 | console = Console(width=20) 8 | 9 | style = "bold white on blue" 10 | console.print("Rich", style=style) 11 | console.print("Rich", style=style, justify="left") 12 | console.print("Rich", style=style, justify="center") 13 | console.print("Rich", style=style, justify="right") 14 | -------------------------------------------------------------------------------- /examples/justify2.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example demonstrates the justify argument to print. 3 | """ 4 | 5 | from rich.console import Console 6 | from rich.panel import Panel 7 | 8 | console = Console(width=20) 9 | 10 | style = "bold white on blue" 11 | panel = Panel("Rich", style="on red", expand=False) 12 | console.print(panel, style=style) 13 | console.print(panel, style=style, justify="left") 14 | console.print(panel, style=style, justify="center") 15 | console.print(panel, style=style, justify="right") 16 | -------------------------------------------------------------------------------- /examples/layout.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Demonstrates a dynamic Layout 4 | 5 | """ 6 | 7 | from datetime import datetime 8 | 9 | from time import sleep 10 | 11 | from rich.align import Align 12 | from rich.console import Console 13 | from rich.layout import Layout 14 | from rich.live import Live 15 | from rich.text import Text 16 | 17 | console = Console() 18 | layout = Layout() 19 | 20 | layout.split( 21 | Layout(name="header", size=1), 22 | Layout(ratio=1, name="main"), 23 | Layout(size=10, name="footer"), 24 | ) 25 | 26 | layout["main"].split_row(Layout(name="side"), Layout(name="body", ratio=2)) 27 | 28 | layout["side"].split(Layout(), Layout()) 29 | 30 | layout["body"].update( 31 | Align.center( 32 | Text( 33 | """This is a demonstration of rich.Layout\n\nHit Ctrl+C to exit""", 34 | justify="center", 35 | ), 36 | vertical="middle", 37 | ) 38 | ) 39 | 40 | 41 | class Clock: 42 | """Renders the time in the center of the screen.""" 43 | 44 | def __rich__(self) -> Text: 45 | return Text(datetime.now().ctime(), style="bold magenta", justify="center") 46 | 47 | 48 | layout["header"].update(Clock()) 49 | 50 | with Live(layout, screen=True, redirect_stderr=False) as live: 51 | try: 52 | while True: 53 | sleep(1) 54 | except KeyboardInterrupt: 55 | pass 56 | -------------------------------------------------------------------------------- /examples/link.py: -------------------------------------------------------------------------------- 1 | from rich import print 2 | 3 | print("If your terminal supports links, the following text should be clickable:") 4 | print("[link=https://www.willmcgugan.com][i]Visit [red]my[/red][/i] [yellow]Blog[/]") 5 | -------------------------------------------------------------------------------- /examples/listdir.py: -------------------------------------------------------------------------------- 1 | """ 2 | A very simple `ls` clone. 3 | 4 | If your terminal supports hyperlinks you should be able to launch files by clicking the filename 5 | (usually with cmd / ctrl). 6 | 7 | """ 8 | 9 | import os 10 | import sys 11 | 12 | from rich import print 13 | from rich.columns import Columns 14 | from rich.text import Text 15 | 16 | try: 17 | root_path = sys.argv[1] 18 | except IndexError: 19 | print("Usage: python listdir.py DIRECTORY") 20 | else: 21 | 22 | def make_filename_text(filename): 23 | path = os.path.abspath(os.path.join(root_path, filename)) 24 | text = Text(filename, style="bold blue" if os.path.isdir(path) else "default") 25 | text.stylize(f"link file://{path}") 26 | text.highlight_regex(r"\..*?$", "bold") 27 | return text 28 | 29 | filenames = [ 30 | filename for filename in os.listdir(root_path) if not filename.startswith(".") 31 | ] 32 | filenames.sort(key=lambda filename: filename.lower()) 33 | filename_text = [make_filename_text(filename) for filename in filenames] 34 | columns = Columns(filename_text, equal=True, column_first=True) 35 | print(columns) 36 | -------------------------------------------------------------------------------- /examples/live_progress.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Demonstrates the use of multiple Progress instances in a single Live display. 4 | 5 | """ 6 | 7 | from time import sleep 8 | 9 | from rich.live import Live 10 | from rich.panel import Panel 11 | from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn 12 | from rich.table import Table 13 | 14 | 15 | job_progress = Progress( 16 | "{task.description}", 17 | SpinnerColumn(), 18 | BarColumn(), 19 | TextColumn("[progress.percentage]{task.percentage:>3.0f}%"), 20 | ) 21 | job1 = job_progress.add_task("[green]Cooking") 22 | job2 = job_progress.add_task("[magenta]Baking", total=200) 23 | job3 = job_progress.add_task("[cyan]Mixing", total=400) 24 | 25 | total = sum(task.total for task in job_progress.tasks) 26 | overall_progress = Progress() 27 | overall_task = overall_progress.add_task("All Jobs", total=int(total)) 28 | 29 | progress_table = Table.grid() 30 | progress_table.add_row( 31 | Panel.fit( 32 | overall_progress, title="Overall Progress", border_style="green", padding=(2, 2) 33 | ), 34 | Panel.fit(job_progress, title="[b]Jobs", border_style="red", padding=(1, 2)), 35 | ) 36 | 37 | with Live(progress_table, refresh_per_second=10): 38 | while not overall_progress.finished: 39 | sleep(0.1) 40 | for job in job_progress.tasks: 41 | if not job.finished: 42 | job_progress.advance(job.id) 43 | 44 | completed = sum(task.completed for task in job_progress.tasks) 45 | overall_progress.update(overall_task, completed=completed) 46 | -------------------------------------------------------------------------------- /examples/log.py: -------------------------------------------------------------------------------- 1 | """ 2 | A simulation of Rich console logging. 3 | """ 4 | 5 | import time 6 | from rich.console import Console 7 | from rich.style import Style 8 | from rich.theme import Theme 9 | from rich.highlighter import RegexHighlighter 10 | 11 | 12 | class RequestHighlighter(RegexHighlighter): 13 | base_style = "req." 14 | highlights = [ 15 | r"^(?P\w+) (?P\w+) (?P\S+) (?P\w+) (?P\[.+\])$", 16 | r"\/(?P\w+\..{3,4})", 17 | ] 18 | 19 | 20 | theme = Theme( 21 | { 22 | "req.protocol": Style.parse("dim bold green"), 23 | "req.method": Style.parse("bold cyan"), 24 | "req.path": Style.parse("magenta"), 25 | "req.filename": Style.parse("bright_magenta"), 26 | "req.result": Style.parse("yellow"), 27 | "req.stats": Style.parse("dim"), 28 | } 29 | ) 30 | console = Console(theme=theme) 31 | 32 | console.log("Server starting...") 33 | console.log("Serving on http://127.0.0.1:8000") 34 | 35 | time.sleep(1) 36 | 37 | request_highlighter = RequestHighlighter() 38 | 39 | console.log( 40 | request_highlighter("HTTP GET /foo/bar/baz/egg.html 200 [0.57, 127.0.0.1:59076]"), 41 | ) 42 | 43 | console.log( 44 | request_highlighter( 45 | "HTTP GET /foo/bar/baz/background.jpg 200 [0.57, 127.0.0.1:59076]" 46 | ), 47 | ) 48 | 49 | 50 | time.sleep(1) 51 | 52 | 53 | def test_locals(): 54 | foo = (1, 2, 3) 55 | movies = ["Deadpool", "Rise of the Skywalker"] 56 | console = Console() 57 | 58 | console.log( 59 | "[b]JSON[/b] RPC [i]batch[/i]", 60 | [ 61 | {"jsonrpc": "2.0", "method": "sum", "params": [1, 2, 4], "id": "1"}, 62 | {"jsonrpc": "2.0", "method": "notify_hello", "params": [7]}, 63 | {"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": "2"}, 64 | {"foo": "boo"}, 65 | { 66 | "jsonrpc": "2.0", 67 | "method": "foo.get", 68 | "params": {"name": "myself", "enable": False, "grommits": None}, 69 | "id": "5", 70 | }, 71 | {"jsonrpc": "2.0", "method": "get_data", "id": "9"}, 72 | ], 73 | log_locals=True, 74 | ) 75 | 76 | 77 | test_locals() 78 | -------------------------------------------------------------------------------- /examples/overflow.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from rich.console import Console, OverflowMethod 3 | 4 | console = Console(width=14) 5 | supercali = "supercalifragilisticexpialidocious" 6 | 7 | overflow_methods: List[OverflowMethod] = ["fold", "crop", "ellipsis"] 8 | for overflow in overflow_methods: 9 | console.rule(overflow) 10 | console.print(supercali, overflow=overflow, style="bold blue") 11 | console.print() 12 | -------------------------------------------------------------------------------- /examples/padding.py: -------------------------------------------------------------------------------- 1 | from rich import print 2 | from rich.padding import Padding 3 | 4 | test = Padding("Hello", (2, 4), style="on blue", expand=False) 5 | print(test) 6 | -------------------------------------------------------------------------------- /examples/print_calendar.py: -------------------------------------------------------------------------------- 1 | """ 2 | Builds calendar layout using Columns and Tables. 3 | Usage: 4 | python print_calendar.py [YEAR] 5 | Example: 6 | python print_calendar.py 2021 7 | """ 8 | import argparse 9 | import calendar 10 | from datetime import datetime 11 | 12 | from rich.align import Align 13 | from rich import box 14 | from rich.columns import Columns 15 | from rich.console import Console 16 | from rich.table import Table 17 | from rich.text import Text 18 | 19 | 20 | def print_calendar(year): 21 | """Print a calendar for a given year.""" 22 | 23 | today = datetime.today() 24 | year = int(year) 25 | cal = calendar.Calendar() 26 | today_tuple = today.day, today.month, today.year 27 | 28 | tables = [] 29 | 30 | for month in range(1, 13): 31 | table = Table( 32 | title=f"{calendar.month_name[month]} {year}", 33 | style="green", 34 | box=box.SIMPLE_HEAVY, 35 | padding=0, 36 | ) 37 | 38 | for week_day in cal.iterweekdays(): 39 | table.add_column( 40 | "{:.3}".format(calendar.day_name[week_day]), justify="right" 41 | ) 42 | 43 | month_days = cal.monthdayscalendar(year, month) 44 | for weekdays in month_days: 45 | days = [] 46 | for index, day in enumerate(weekdays): 47 | day_label = Text(str(day or ""), style="magenta") 48 | if index in (5, 6): 49 | day_label.stylize("blue") 50 | if day and (day, month, year) == today_tuple: 51 | day_label.stylize("white on dark_red") 52 | days.append(day_label) 53 | table.add_row(*days) 54 | 55 | tables.append(Align.center(table)) 56 | 57 | console = Console() 58 | columns = Columns(tables, padding=1, expand=True) 59 | console.rule(str(year)) 60 | console.print() 61 | console.print(columns) 62 | console.rule(str(year)) 63 | 64 | 65 | if __name__ == "__main__": 66 | parser = argparse.ArgumentParser(description="Rich calendar") 67 | parser.add_argument("year", metavar="year", type=int) 68 | args = parser.parse_args() 69 | 70 | print_calendar(args.year) 71 | -------------------------------------------------------------------------------- /examples/rainbow.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This example demonstrates how to write a custom highlighter. 4 | 5 | """ 6 | 7 | from random import randint 8 | 9 | from rich import print 10 | from rich.highlighter import Highlighter 11 | 12 | 13 | class RainbowHighlighter(Highlighter): 14 | def highlight(self, text): 15 | for index in range(len(text)): 16 | text.stylize(f"color({randint(16, 255)})", index, index + 1) 17 | 18 | 19 | rainbow = RainbowHighlighter() 20 | print(rainbow("I must not fear. Fear is the mind-killer.")) 21 | -------------------------------------------------------------------------------- /examples/recursive_error.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Demonstrates Rich tracebacks for recursion errors. 4 | 5 | Rich can exclude frames in the middle to avoid huge tracebacks. 6 | 7 | """ 8 | 9 | from rich.console import Console 10 | 11 | 12 | def foo(n): 13 | return bar(n) 14 | 15 | 16 | def bar(n): 17 | return foo(n) 18 | 19 | 20 | console = Console() 21 | 22 | try: 23 | foo(1) 24 | except Exception: 25 | console.print_exception(max_frames=20) 26 | -------------------------------------------------------------------------------- /examples/repr.py: -------------------------------------------------------------------------------- 1 | import rich.repr 2 | 3 | 4 | @rich.repr.auto 5 | class Bird: 6 | def __init__(self, name, eats=None, fly=True, extinct=False): 7 | self.name = name 8 | self.eats = list(eats) if eats else [] 9 | self.fly = fly 10 | self.extinct = extinct 11 | 12 | 13 | # Note that the repr is still generated without Rich 14 | # Try commenting out the following line 15 | 16 | from rich import print 17 | 18 | BIRDS = { 19 | "gull": Bird("gull", eats=["fish", "chips", "ice cream", "sausage rolls"]), 20 | "penguin": Bird("penguin", eats=["fish"], fly=False), 21 | "dodo": Bird("dodo", eats=["fruit"], fly=False, extinct=True), 22 | } 23 | print(BIRDS) 24 | -------------------------------------------------------------------------------- /examples/save_table_svg.py: -------------------------------------------------------------------------------- 1 | """ 2 | Demonstrates how to export a SVG 3 | """ 4 | 5 | from rich.console import Console 6 | from rich.table import Table 7 | 8 | table = Table(title="Star Wars Movies") 9 | 10 | table.add_column("Released", style="cyan", no_wrap=True) 11 | table.add_column("Title", style="magenta") 12 | table.add_column("Box Office", justify="right", style="green") 13 | 14 | table.add_row("Dec 20, 2019", "Star Wars: The Rise of Skywalker", "$952,110,690") 15 | table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347") 16 | table.add_row("Dec 15, 2017", "Star Wars Ep. V111: The Last Jedi", "$1,332,539,889") 17 | table.add_row("Dec 16, 2016", "Rogue One: A Star Wars Story", "$1,332,439,889") 18 | 19 | console = Console(record=True) 20 | console.print(table, justify="center") 21 | console.save_svg("table.svg", title="save_table_svg.py") 22 | 23 | import os 24 | import webbrowser 25 | 26 | webbrowser.open(f"file://{os.path.abspath('table.svg')}") 27 | -------------------------------------------------------------------------------- /examples/screen.py: -------------------------------------------------------------------------------- 1 | """ 2 | Demonstration of Console.screen() 3 | """ 4 | 5 | from time import sleep 6 | 7 | from rich.align import Align 8 | from rich.console import Console 9 | from rich.panel import Panel 10 | 11 | console = Console() 12 | 13 | with console.screen(style="bold white on red") as screen: 14 | text = Align.center("[blink]Don't Panic![/blink]", vertical="middle") 15 | screen.update(Panel(text)) 16 | sleep(5) 17 | -------------------------------------------------------------------------------- /examples/spinners.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | from rich.columns import Columns 4 | from rich.panel import Panel 5 | from rich.live import Live 6 | from rich.text import Text 7 | from rich.spinner import Spinner, SPINNERS 8 | 9 | all_spinners = Columns( 10 | [ 11 | Spinner(spinner_name, text=Text(repr(spinner_name), style="green")) 12 | for spinner_name in sorted(SPINNERS) 13 | ], 14 | column_first=True, 15 | expand=True, 16 | ) 17 | 18 | with Live( 19 | Panel(all_spinners, title="Spinners", border_style="blue"), 20 | refresh_per_second=20, 21 | ) as live: 22 | while True: 23 | sleep(0.1) 24 | -------------------------------------------------------------------------------- /examples/status.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | from rich.console import Console 3 | 4 | console = Console() 5 | console.print() 6 | 7 | tasks = [f"task {n}" for n in range(1, 11)] 8 | 9 | with console.status("[bold green]Working on tasks...") as status: 10 | while tasks: 11 | task = tasks.pop(0) 12 | sleep(1) 13 | console.log(f"{task} complete") 14 | -------------------------------------------------------------------------------- /examples/suppress.py: -------------------------------------------------------------------------------- 1 | try: 2 | import click 3 | except ImportError: 4 | print("Please install click for this example") 5 | print(" pip install click") 6 | exit() 7 | 8 | from rich.traceback import install 9 | 10 | install(suppress=[click]) 11 | 12 | 13 | @click.command() 14 | @click.option("--count", default=1, help="Number of greetings.") 15 | def hello(count): 16 | """Simple program that greets NAME for a total of COUNT times.""" 17 | 1 / 0 18 | for x in range(count): 19 | click.echo(f"Hello {name}!") 20 | 21 | 22 | if __name__ == "__main__": 23 | hello() 24 | -------------------------------------------------------------------------------- /examples/table.py: -------------------------------------------------------------------------------- 1 | """ 2 | Demonstrates how to render a table. 3 | """ 4 | 5 | from rich.console import Console 6 | from rich.table import Table 7 | 8 | table = Table(title="Star Wars Movies") 9 | 10 | table.add_column("Released", style="cyan", no_wrap=True) 11 | table.add_column("Title", style="magenta") 12 | table.add_column("Box Office", justify="right", style="green") 13 | 14 | table.add_row("Dec 20, 2019", "Star Wars: The Rise of Skywalker", "$952,110,690") 15 | table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347") 16 | table.add_row("Dec 15, 2017", "Star Wars Ep. V111: The Last Jedi", "$1,332,539,889") 17 | table.add_row("Dec 16, 2016", "Rogue One: A Star Wars Story", "$1,332,439,889") 18 | 19 | console = Console() 20 | console.print(table, justify="center") 21 | -------------------------------------------------------------------------------- /examples/top_lite_simulator.py: -------------------------------------------------------------------------------- 1 | """Lite simulation of the top linux command.""" 2 | import datetime 3 | import random 4 | import sys 5 | import time 6 | from dataclasses import dataclass 7 | 8 | from rich import box 9 | from rich.console import Console 10 | from rich.live import Live 11 | from rich.table import Table 12 | 13 | if sys.version_info >= (3, 8): 14 | from typing import Literal 15 | else: 16 | from typing_extensions import Literal 17 | 18 | 19 | @dataclass 20 | class Process: 21 | pid: int 22 | command: str 23 | cpu_percent: float 24 | memory: int 25 | start_time: datetime.datetime 26 | thread_count: int 27 | state: Literal["running", "sleeping"] 28 | 29 | @property 30 | def memory_str(self) -> str: 31 | if self.memory > 1e6: 32 | return f"{int(self.memory/1e6)}M" 33 | if self.memory > 1e3: 34 | return f"{int(self.memory/1e3)}K" 35 | return str(self.memory) 36 | 37 | @property 38 | def time_str(self) -> str: 39 | return str(datetime.datetime.now() - self.start_time) 40 | 41 | 42 | def generate_process(pid: int) -> Process: 43 | return Process( 44 | pid=pid, 45 | command=f"Process {pid}", 46 | cpu_percent=random.random() * 20, 47 | memory=random.randint(10, 200) ** 3, 48 | start_time=datetime.datetime.now() 49 | - datetime.timedelta(seconds=random.randint(0, 500) ** 2), 50 | thread_count=random.randint(1, 32), 51 | state="running" if random.randint(0, 10) < 8 else "sleeping", 52 | ) 53 | 54 | 55 | def create_process_table(height: int) -> Table: 56 | processes = sorted( 57 | [generate_process(pid) for pid in range(height)], 58 | key=lambda p: p.cpu_percent, 59 | reverse=True, 60 | ) 61 | table = Table( 62 | "PID", "Command", "CPU %", "Memory", "Time", "Thread #", "State", box=box.SIMPLE 63 | ) 64 | 65 | for process in processes: 66 | table.add_row( 67 | str(process.pid), 68 | process.command, 69 | f"{process.cpu_percent:.1f}", 70 | process.memory_str, 71 | process.time_str, 72 | str(process.thread_count), 73 | process.state, 74 | ) 75 | 76 | return table 77 | 78 | 79 | console = Console() 80 | 81 | with Live(console=console, screen=True, auto_refresh=False) as live: 82 | while True: 83 | live.update(create_process_table(console.size.height - 4), refresh=True) 84 | time.sleep(1) 85 | -------------------------------------------------------------------------------- /examples/tree.py: -------------------------------------------------------------------------------- 1 | """ 2 | Demonstrates how to display a tree of files / directories with the Tree renderable. 3 | """ 4 | 5 | import os 6 | import pathlib 7 | import sys 8 | 9 | from rich import print 10 | from rich.filesize import decimal 11 | from rich.markup import escape 12 | from rich.text import Text 13 | from rich.tree import Tree 14 | 15 | 16 | def walk_directory(directory: pathlib.Path, tree: Tree) -> None: 17 | """Recursively build a Tree with directory contents.""" 18 | # Sort dirs first then by filename 19 | paths = sorted( 20 | pathlib.Path(directory).iterdir(), 21 | key=lambda path: (path.is_file(), path.name.lower()), 22 | ) 23 | for path in paths: 24 | # Remove hidden files 25 | if path.name.startswith("."): 26 | continue 27 | if path.is_dir(): 28 | style = "dim" if path.name.startswith("__") else "" 29 | branch = tree.add( 30 | f"[bold magenta]:open_file_folder: [link file://{path}]{escape(path.name)}", 31 | style=style, 32 | guide_style=style, 33 | ) 34 | walk_directory(path, branch) 35 | else: 36 | text_filename = Text(path.name, "green") 37 | text_filename.highlight_regex(r"\..*$", "bold red") 38 | text_filename.stylize(f"link file://{path}") 39 | file_size = path.stat().st_size 40 | text_filename.append(f" ({decimal(file_size)})", "blue") 41 | icon = "🐍 " if path.suffix == ".py" else "📄 " 42 | tree.add(Text(icon) + text_filename) 43 | 44 | 45 | try: 46 | directory = os.path.abspath(sys.argv[1]) 47 | except IndexError: 48 | print("[b]Usage:[/] python tree.py ") 49 | else: 50 | tree = Tree( 51 | f":open_file_folder: [link file://{directory}]{directory}", 52 | guide_style="bold bright_blue", 53 | ) 54 | walk_directory(pathlib.Path(directory), tree) 55 | print(tree) 56 | -------------------------------------------------------------------------------- /faq.yml: -------------------------------------------------------------------------------- 1 | # FAQtory settings 2 | 3 | faq_url: "https://github.com/textualize/rich/blob/master/FAQ.md" # Replace this with the URL to your FAQ.md! 4 | 5 | questions_path: "./questions" # Where questions should be stored 6 | output_path: "./FAQ.md" # Where FAQ.md should be generated 7 | templates_path: ".faq" # Path to templates 8 | -------------------------------------------------------------------------------- /imgs/columns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/imgs/columns.png -------------------------------------------------------------------------------- /imgs/downloader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/imgs/downloader.gif -------------------------------------------------------------------------------- /imgs/features.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/imgs/features.png -------------------------------------------------------------------------------- /imgs/hello_world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/imgs/hello_world.png -------------------------------------------------------------------------------- /imgs/inspect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/imgs/inspect.png -------------------------------------------------------------------------------- /imgs/log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/imgs/log.png -------------------------------------------------------------------------------- /imgs/logging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/imgs/logging.png -------------------------------------------------------------------------------- /imgs/markdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/imgs/markdown.png -------------------------------------------------------------------------------- /imgs/print.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/imgs/print.png -------------------------------------------------------------------------------- /imgs/progress.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/imgs/progress.gif -------------------------------------------------------------------------------- /imgs/progress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/imgs/progress.png -------------------------------------------------------------------------------- /imgs/repl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/imgs/repl.png -------------------------------------------------------------------------------- /imgs/spinners.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/imgs/spinners.gif -------------------------------------------------------------------------------- /imgs/status.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/imgs/status.gif -------------------------------------------------------------------------------- /imgs/syntax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/imgs/syntax.png -------------------------------------------------------------------------------- /imgs/table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/imgs/table.png -------------------------------------------------------------------------------- /imgs/table2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/imgs/table2.png -------------------------------------------------------------------------------- /imgs/table_movie.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/imgs/table_movie.gif -------------------------------------------------------------------------------- /imgs/traceback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/imgs/traceback.png -------------------------------------------------------------------------------- /imgs/tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/imgs/tree.png -------------------------------------------------------------------------------- /imgs/where_there_is_a_will.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/imgs/where_there_is_a_will.png -------------------------------------------------------------------------------- /make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "rich" 3 | homepage = "https://github.com/Textualize/rich" 4 | documentation = "https://rich.readthedocs.io/en/latest/" 5 | version = "14.0.0" 6 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 7 | authors = ["Will McGugan "] 8 | license = "MIT" 9 | readme = "README.md" 10 | classifiers = [ 11 | "Development Status :: 5 - Production/Stable", 12 | "Environment :: Console", 13 | "Framework :: IPython", 14 | "Intended Audience :: Developers", 15 | "Operating System :: Microsoft :: Windows", 16 | "Operating System :: MacOS", 17 | "Operating System :: POSIX :: Linux", 18 | "Programming Language :: Python :: 3.8", 19 | "Programming Language :: Python :: 3.9", 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: 3.11", 22 | "Programming Language :: Python :: 3.12", 23 | "Programming Language :: Python :: 3.13", 24 | "Typing :: Typed", 25 | ] 26 | include = ["rich/py.typed"] 27 | 28 | 29 | [tool.poetry.dependencies] 30 | python = ">=3.8.0" 31 | typing-extensions = { version = ">=4.0.0, <5.0", python = "<3.11" } 32 | pygments = "^2.13.0" 33 | ipywidgets = { version = ">=7.5.1,<9", optional = true } 34 | markdown-it-py = ">=2.2.0" 35 | 36 | [tool.poetry.extras] 37 | jupyter = ["ipywidgets"] 38 | 39 | [tool.poetry.dev-dependencies] 40 | pytest = "^7.0.0" 41 | black = "^22.6" 42 | mypy = "^1.11" 43 | pytest-cov = "^3.0.0" 44 | attrs = "^21.4.0" 45 | pre-commit = "^2.17.0" 46 | asv = "^0.5.1" 47 | 48 | [build-system] 49 | requires = ["poetry-core>=1.0.0"] 50 | build-backend = "poetry.core.masonry.api" 51 | 52 | 53 | [tool.mypy] 54 | files = ["rich"] 55 | show_error_codes = true 56 | strict = true 57 | enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] 58 | 59 | 60 | [[tool.mypy.overrides]] 61 | module = ["pygments.*", "IPython.*", "ipywidgets.*"] 62 | ignore_missing_imports = true 63 | 64 | 65 | [tool.pytest.ini_options] 66 | testpaths = ["tests"] 67 | 68 | [tool.isort] 69 | profile = "black" 70 | -------------------------------------------------------------------------------- /questions/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Questions 3 | 4 | Your questions should go in this directory. 5 | 6 | Question files should be named with the extension ".question.md". 7 | -------------------------------------------------------------------------------- /questions/ansi_escapes.question.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Natively inserted ANSI escape sequence characters break alignment of Panel." 3 | alt_titles: 4 | - "Escape codes break alignment." 5 | --- 6 | 7 | If you print ansi escape sequences for color and style you may find the output breaks your output. 8 | You may find that border characters in Panel and Table are in the wrong place, for example. 9 | 10 | As a general rule, you should allow Rich to generate all ansi escape sequences, so it can correctly account for these invisible characters. 11 | If you can't avoid a string with escape codes, you can convert it to an equivalent `Text` instance with `Text.from_ansi`. 12 | -------------------------------------------------------------------------------- /questions/emoji_broken.question.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Why does emoji break alignment in a Table or Panel?" 3 | --- 4 | 5 | Certain emoji take up double space within the terminal. Unfortunately, terminals don't always agree how wide a given character should be. 6 | 7 | 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. 8 | 9 | There are also *multiple codepoints* characters, such as country flags, and emoji modifiers, which produce wildly different results across terminal emulators. 10 | 11 | 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, 12 | -------------------------------------------------------------------------------- /questions/highlighting_unexpected.question.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Strange colors in console output." 3 | alt_titles: 4 | - "Why are numbers in cyan?" 5 | - "Rich print seems to make decimals cyan" 6 | - "Numbers get printed differently" 7 | --- 8 | 9 | Rich will highlight certain patterns in your output such as numbers, strings, and other objects like IP addresses. 10 | 11 | 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. 12 | -------------------------------------------------------------------------------- /questions/log_renderables.question.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How do I log a renderable?" 3 | alt_titles: 4 | - "Cannot log Tree() output to file" 5 | - "Log a Panel or Table to a RichHandler" 6 | --- 7 | 8 | Python's logging module is designed to work with strings. Consequently you won't be able to log Rich renderables (Table, Tree, etc) by calling `logger.debug` or other similar method. 9 | 10 | You could use the [capture](https://rich.readthedocs.io/en/latest/console.html#capturing-output) API to convert the renderable to a string and log that. However I would advise against it. 11 | 12 | Logging supports configurable back-ends, which means that a log message could go somewhere other than the terminal -- which may not correctly render the formatting and style produced by Rich. 13 | 14 | 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. 15 | -------------------------------------------------------------------------------- /questions/logging_color.question.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How do I render console markup in RichHandler?" 3 | alt_titles: 4 | - "Color is not applied during logging." 5 | - "Style tags don't work with logging handler" 6 | 7 | --- 8 | 9 | Console markup won't work anywhere else, other than `RichHandler` -- which is why they are disabled by default. 10 | 11 | See the docs if you want to [enable console markup](https://rich.readthedocs.io/en/latest/logging.html#logging-handler) in the logging handler. 12 | -------------------------------------------------------------------------------- /questions/rich_spinner.question.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "python -m rich.spinner shows extra lines." 3 | --- 4 | 5 | The spinner example is know to break on some terminals (Windows in particular). 6 | 7 | Some terminals don't display emoji with the correct width, which means Rich can't always align them accurately inside a panel. 8 | -------------------------------------------------------------------------------- /questions/square_brackets.question.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Why does content in square brackets disappear?" 3 | alt_titles: 4 | - "Can not print a [string]" 5 | --- 6 | 7 | Rich will treat text within square brackets as *markup tags*, for instance `"[bold]This is bold[/bold]"`. 8 | 9 | If you are printing strings with literally square brackets you can either disable markup, or escape your strings. 10 | See the docs on [console markup](https://rich.readthedocs.io/en/latest/markup.html) for how to do this. 11 | -------------------------------------------------------------------------------- /questions/tracebacks_installed.question.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Rich is automatically installing traceback handler." 3 | alt_titles: 4 | - "Can you stop overriding traceback message formatting by default?" 5 | --- 6 | 7 | Rich will never install the traceback handler automatically. 8 | 9 | If you are getting Rich tracebacks and you don't want them, then some other piece of software is calling `rich.traceback.install()`. 10 | -------------------------------------------------------------------------------- /rich/_emoji_replace.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Match, Optional 2 | import re 3 | 4 | from ._emoji_codes import EMOJI 5 | 6 | 7 | _ReStringMatch = Match[str] # regex match object 8 | _ReSubCallable = Callable[[_ReStringMatch], str] # Callable invoked by re.sub 9 | _EmojiSubMethod = Callable[[_ReSubCallable, str], str] # Sub method of a compiled re 10 | 11 | 12 | def _emoji_replace( 13 | text: str, 14 | default_variant: Optional[str] = None, 15 | _emoji_sub: _EmojiSubMethod = re.compile(r"(:(\S*?)(?:(?:\-)(emoji|text))?:)").sub, 16 | ) -> str: 17 | """Replace emoji code in text.""" 18 | get_emoji = EMOJI.__getitem__ 19 | variants = {"text": "\uFE0E", "emoji": "\uFE0F"} 20 | get_variant = variants.get 21 | default_variant_code = variants.get(default_variant, "") if default_variant else "" 22 | 23 | def do_replace(match: Match[str]) -> str: 24 | emoji_code, emoji_name, variant = match.groups() 25 | try: 26 | return get_emoji(emoji_name.lower()) + get_variant( 27 | variant, default_variant_code 28 | ) 29 | except KeyError: 30 | return emoji_code 31 | 32 | return _emoji_sub(do_replace, text) 33 | -------------------------------------------------------------------------------- /rich/_export_format.py: -------------------------------------------------------------------------------- 1 | CONSOLE_HTML_FORMAT = """\ 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 |
{code}
16 | 17 | 18 | """ 19 | 20 | CONSOLE_SVG_FORMAT = """\ 21 | 22 | 23 | 57 | 58 | 59 | 60 | 61 | 62 | {lines} 63 | 64 | 65 | {chrome} 66 | 67 | {backgrounds} 68 | 69 | {matrix} 70 | 71 | 72 | 73 | """ 74 | 75 | _SVG_FONT_FAMILY = "Rich Fira Code" 76 | _SVG_CLASSES_PREFIX = "rich-svg" 77 | -------------------------------------------------------------------------------- /rich/_extension.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | 4 | def load_ipython_extension(ip: Any) -> None: # pragma: no cover 5 | # prevent circular import 6 | from rich.pretty import install 7 | from rich.traceback import install as tr_install 8 | 9 | install() 10 | tr_install() 11 | -------------------------------------------------------------------------------- /rich/_fileno.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import IO, Callable 4 | 5 | 6 | def get_fileno(file_like: IO[str]) -> int | None: 7 | """Get fileno() from a file, accounting for poorly implemented file-like objects. 8 | 9 | Args: 10 | file_like (IO): A file-like object. 11 | 12 | Returns: 13 | int | None: The result of fileno if available, or None if operation failed. 14 | """ 15 | fileno: Callable[[], int] | None = getattr(file_like, "fileno", None) 16 | if fileno is not None: 17 | try: 18 | return fileno() 19 | except Exception: 20 | # `fileno` is documented as potentially raising a OSError 21 | # Alas, from the issues, there are so many poorly implemented file-like objects, 22 | # that `fileno()` can raise just about anything. 23 | return None 24 | return None 25 | -------------------------------------------------------------------------------- /rich/_log_render.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Iterable, List, Optional, TYPE_CHECKING, Union, Callable 3 | 4 | 5 | from .text import Text, TextType 6 | 7 | if TYPE_CHECKING: 8 | from .console import Console, ConsoleRenderable, RenderableType 9 | from .table import Table 10 | 11 | FormatTimeCallable = Callable[[datetime], Text] 12 | 13 | 14 | class LogRender: 15 | def __init__( 16 | self, 17 | show_time: bool = True, 18 | show_level: bool = False, 19 | show_path: bool = True, 20 | time_format: Union[str, FormatTimeCallable] = "[%x %X]", 21 | omit_repeated_times: bool = True, 22 | level_width: Optional[int] = 8, 23 | ) -> None: 24 | self.show_time = show_time 25 | self.show_level = show_level 26 | self.show_path = show_path 27 | self.time_format = time_format 28 | self.omit_repeated_times = omit_repeated_times 29 | self.level_width = level_width 30 | self._last_time: Optional[Text] = None 31 | 32 | def __call__( 33 | self, 34 | console: "Console", 35 | renderables: Iterable["ConsoleRenderable"], 36 | log_time: Optional[datetime] = None, 37 | time_format: Optional[Union[str, FormatTimeCallable]] = None, 38 | level: TextType = "", 39 | path: Optional[str] = None, 40 | line_no: Optional[int] = None, 41 | link_path: Optional[str] = None, 42 | ) -> "Table": 43 | from .containers import Renderables 44 | from .table import Table 45 | 46 | output = Table.grid(padding=(0, 1)) 47 | output.expand = True 48 | if self.show_time: 49 | output.add_column(style="log.time") 50 | if self.show_level: 51 | output.add_column(style="log.level", width=self.level_width) 52 | output.add_column(ratio=1, style="log.message", overflow="fold") 53 | if self.show_path and path: 54 | output.add_column(style="log.path") 55 | row: List["RenderableType"] = [] 56 | if self.show_time: 57 | log_time = log_time or console.get_datetime() 58 | time_format = time_format or self.time_format 59 | if callable(time_format): 60 | log_time_display = time_format(log_time) 61 | else: 62 | log_time_display = Text(log_time.strftime(time_format)) 63 | if log_time_display == self._last_time and self.omit_repeated_times: 64 | row.append(Text(" " * len(log_time_display))) 65 | else: 66 | row.append(log_time_display) 67 | self._last_time = log_time_display 68 | if self.show_level: 69 | row.append(level) 70 | 71 | row.append(Renderables(renderables)) 72 | if self.show_path and path: 73 | path_text = Text() 74 | path_text.append( 75 | path, style=f"link file://{link_path}" if link_path else "" 76 | ) 77 | if line_no: 78 | path_text.append(":") 79 | path_text.append( 80 | f"{line_no}", 81 | style=f"link file://{link_path}#{line_no}" if link_path else "", 82 | ) 83 | row.append(path_text) 84 | 85 | output.add_row(*row) 86 | return output 87 | 88 | 89 | if __name__ == "__main__": # pragma: no cover 90 | from rich.console import Console 91 | 92 | c = Console() 93 | c.print("[on blue]Hello", justify="right") 94 | c.log("[on blue]hello", justify="right") 95 | -------------------------------------------------------------------------------- /rich/_loop.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Tuple, TypeVar 2 | 3 | T = TypeVar("T") 4 | 5 | 6 | def loop_first(values: Iterable[T]) -> Iterable[Tuple[bool, T]]: 7 | """Iterate and generate a tuple with a flag for first value.""" 8 | iter_values = iter(values) 9 | try: 10 | value = next(iter_values) 11 | except StopIteration: 12 | return 13 | yield True, value 14 | for value in iter_values: 15 | yield False, value 16 | 17 | 18 | def loop_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]: 19 | """Iterate and generate a tuple with a flag for last value.""" 20 | iter_values = iter(values) 21 | try: 22 | previous_value = next(iter_values) 23 | except StopIteration: 24 | return 25 | for value in iter_values: 26 | yield False, previous_value 27 | previous_value = value 28 | yield True, previous_value 29 | 30 | 31 | def loop_first_last(values: Iterable[T]) -> Iterable[Tuple[bool, bool, T]]: 32 | """Iterate and generate a tuple with a flag for first and last value.""" 33 | iter_values = iter(values) 34 | try: 35 | previous_value = next(iter_values) 36 | except StopIteration: 37 | return 38 | first = True 39 | for value in iter_values: 40 | yield first, False, previous_value 41 | first = False 42 | previous_value = value 43 | yield first, True, previous_value 44 | -------------------------------------------------------------------------------- /rich/_null_file.py: -------------------------------------------------------------------------------- 1 | from types import TracebackType 2 | from typing import IO, Iterable, Iterator, List, Optional, Type 3 | 4 | 5 | class NullFile(IO[str]): 6 | def close(self) -> None: 7 | pass 8 | 9 | def isatty(self) -> bool: 10 | return False 11 | 12 | def read(self, __n: int = 1) -> str: 13 | return "" 14 | 15 | def readable(self) -> bool: 16 | return False 17 | 18 | def readline(self, __limit: int = 1) -> str: 19 | return "" 20 | 21 | def readlines(self, __hint: int = 1) -> List[str]: 22 | return [] 23 | 24 | def seek(self, __offset: int, __whence: int = 1) -> int: 25 | return 0 26 | 27 | def seekable(self) -> bool: 28 | return False 29 | 30 | def tell(self) -> int: 31 | return 0 32 | 33 | def truncate(self, __size: Optional[int] = 1) -> int: 34 | return 0 35 | 36 | def writable(self) -> bool: 37 | return False 38 | 39 | def writelines(self, __lines: Iterable[str]) -> None: 40 | pass 41 | 42 | def __next__(self) -> str: 43 | return "" 44 | 45 | def __iter__(self) -> Iterator[str]: 46 | return iter([""]) 47 | 48 | def __enter__(self) -> IO[str]: 49 | return self 50 | 51 | def __exit__( 52 | self, 53 | __t: Optional[Type[BaseException]], 54 | __value: Optional[BaseException], 55 | __traceback: Optional[TracebackType], 56 | ) -> None: 57 | pass 58 | 59 | def write(self, text: str) -> int: 60 | return 0 61 | 62 | def flush(self) -> None: 63 | pass 64 | 65 | def fileno(self) -> int: 66 | return -1 67 | 68 | 69 | NULL_FILE = NullFile() 70 | -------------------------------------------------------------------------------- /rich/_pick.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | 4 | def pick_bool(*values: Optional[bool]) -> bool: 5 | """Pick the first non-none bool or return the last value. 6 | 7 | Args: 8 | *values (bool): Any number of boolean or None values. 9 | 10 | Returns: 11 | bool: First non-none boolean. 12 | """ 13 | assert values, "1 or more values required" 14 | for value in values: 15 | if value is not None: 16 | return value 17 | return bool(value) 18 | -------------------------------------------------------------------------------- /rich/_stack.py: -------------------------------------------------------------------------------- 1 | from typing import List, TypeVar 2 | 3 | T = TypeVar("T") 4 | 5 | 6 | class Stack(List[T]): 7 | """A small shim over builtin list.""" 8 | 9 | @property 10 | def top(self) -> T: 11 | """Get top of stack.""" 12 | return self[-1] 13 | 14 | def push(self, item: T) -> None: 15 | """Push an item on to the stack (append in stack nomenclature).""" 16 | self.append(item) 17 | -------------------------------------------------------------------------------- /rich/_timer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Timer context manager, only used in debug. 3 | 4 | """ 5 | 6 | from time import time 7 | 8 | import contextlib 9 | from typing import Generator 10 | 11 | 12 | @contextlib.contextmanager 13 | def timer(subject: str = "time") -> Generator[None, None, None]: 14 | """print the elapsed time. (only used in debugging)""" 15 | start = time() 16 | yield 17 | elapsed = time() - start 18 | elapsed_ms = elapsed * 1000 19 | print(f"{subject} elapsed {elapsed_ms:.1f}ms") 20 | -------------------------------------------------------------------------------- /rich/_windows.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from dataclasses import dataclass 3 | 4 | 5 | @dataclass 6 | class WindowsConsoleFeatures: 7 | """Windows features available.""" 8 | 9 | vt: bool = False 10 | """The console supports VT codes.""" 11 | truecolor: bool = False 12 | """The console supports truecolor.""" 13 | 14 | 15 | try: 16 | import ctypes 17 | from ctypes import LibraryLoader 18 | 19 | if sys.platform == "win32": 20 | windll = LibraryLoader(ctypes.WinDLL) 21 | else: 22 | windll = None 23 | raise ImportError("Not windows") 24 | 25 | from rich._win32_console import ( 26 | ENABLE_VIRTUAL_TERMINAL_PROCESSING, 27 | GetConsoleMode, 28 | GetStdHandle, 29 | LegacyWindowsError, 30 | ) 31 | 32 | except (AttributeError, ImportError, ValueError): 33 | # Fallback if we can't load the Windows DLL 34 | def get_windows_console_features() -> WindowsConsoleFeatures: 35 | features = WindowsConsoleFeatures() 36 | return features 37 | 38 | else: 39 | 40 | def get_windows_console_features() -> WindowsConsoleFeatures: 41 | """Get windows console features. 42 | 43 | Returns: 44 | WindowsConsoleFeatures: An instance of WindowsConsoleFeatures. 45 | """ 46 | handle = GetStdHandle() 47 | try: 48 | console_mode = GetConsoleMode(handle) 49 | success = True 50 | except LegacyWindowsError: 51 | console_mode = 0 52 | success = False 53 | vt = bool(success and console_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) 54 | truecolor = False 55 | if vt: 56 | win_version = sys.getwindowsversion() 57 | truecolor = win_version.major > 10 or ( 58 | win_version.major == 10 and win_version.build >= 15063 59 | ) 60 | features = WindowsConsoleFeatures(vt=vt, truecolor=truecolor) 61 | return features 62 | 63 | 64 | if __name__ == "__main__": 65 | import platform 66 | 67 | features = get_windows_console_features() 68 | from rich import print 69 | 70 | print(f'platform="{platform.system()}"') 71 | print(repr(features)) 72 | -------------------------------------------------------------------------------- /rich/_windows_renderer.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Sequence, Tuple, cast 2 | 3 | from rich._win32_console import LegacyWindowsTerm, WindowsCoordinates 4 | from rich.segment import ControlCode, ControlType, Segment 5 | 6 | 7 | def legacy_windows_render(buffer: Iterable[Segment], term: LegacyWindowsTerm) -> None: 8 | """Makes appropriate Windows Console API calls based on the segments in the buffer. 9 | 10 | Args: 11 | buffer (Iterable[Segment]): Iterable of Segments to convert to Win32 API calls. 12 | term (LegacyWindowsTerm): Used to call the Windows Console API. 13 | """ 14 | for text, style, control in buffer: 15 | if not control: 16 | if style: 17 | term.write_styled(text, style) 18 | else: 19 | term.write_text(text) 20 | else: 21 | control_codes: Sequence[ControlCode] = control 22 | for control_code in control_codes: 23 | control_type = control_code[0] 24 | if control_type == ControlType.CURSOR_MOVE_TO: 25 | _, x, y = cast(Tuple[ControlType, int, int], control_code) 26 | term.move_cursor_to(WindowsCoordinates(row=y - 1, col=x - 1)) 27 | elif control_type == ControlType.CARRIAGE_RETURN: 28 | term.write_text("\r") 29 | elif control_type == ControlType.HOME: 30 | term.move_cursor_to(WindowsCoordinates(0, 0)) 31 | elif control_type == ControlType.CURSOR_UP: 32 | term.move_cursor_up() 33 | elif control_type == ControlType.CURSOR_DOWN: 34 | term.move_cursor_down() 35 | elif control_type == ControlType.CURSOR_FORWARD: 36 | term.move_cursor_forward() 37 | elif control_type == ControlType.CURSOR_BACKWARD: 38 | term.move_cursor_backward() 39 | elif control_type == ControlType.CURSOR_MOVE_TO_COLUMN: 40 | _, column = cast(Tuple[ControlType, int], control_code) 41 | term.move_cursor_to_column(column - 1) 42 | elif control_type == ControlType.HIDE_CURSOR: 43 | term.hide_cursor() 44 | elif control_type == ControlType.SHOW_CURSOR: 45 | term.show_cursor() 46 | elif control_type == ControlType.ERASE_IN_LINE: 47 | _, mode = cast(Tuple[ControlType, int], control_code) 48 | if mode == 0: 49 | term.erase_end_of_line() 50 | elif mode == 1: 51 | term.erase_start_of_line() 52 | elif mode == 2: 53 | term.erase_line() 54 | elif control_type == ControlType.SET_WINDOW_TITLE: 55 | _, title = cast(Tuple[ControlType, str], control_code) 56 | term.set_title(title) 57 | -------------------------------------------------------------------------------- /rich/abc.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | 4 | class RichRenderable(ABC): 5 | """An abstract base class for Rich renderables. 6 | 7 | Note that there is no need to extend this class, the intended use is to check if an 8 | object supports the Rich renderable protocol. For example:: 9 | 10 | if isinstance(my_object, RichRenderable): 11 | console.print(my_object) 12 | 13 | """ 14 | 15 | @classmethod 16 | def __subclasshook__(cls, other: type) -> bool: 17 | """Check if this class supports the rich render protocol.""" 18 | return hasattr(other, "__rich_console__") or hasattr(other, "__rich__") 19 | 20 | 21 | if __name__ == "__main__": # pragma: no cover 22 | from rich.text import Text 23 | 24 | t = Text() 25 | print(isinstance(Text, RichRenderable)) 26 | print(isinstance(t, RichRenderable)) 27 | 28 | class Foo: 29 | pass 30 | 31 | f = Foo() 32 | print(isinstance(f, RichRenderable)) 33 | print(isinstance("", RichRenderable)) 34 | -------------------------------------------------------------------------------- /rich/bar.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union 2 | 3 | from .color import Color 4 | from .console import Console, ConsoleOptions, RenderResult 5 | from .jupyter import JupyterMixin 6 | from .measure import Measurement 7 | from .segment import Segment 8 | from .style import Style 9 | 10 | # There are left-aligned characters for 1/8 to 7/8, but 11 | # the right-aligned characters exist only for 1/8 and 4/8. 12 | BEGIN_BLOCK_ELEMENTS = ["█", "█", "█", "▐", "▐", "▐", "▕", "▕"] 13 | END_BLOCK_ELEMENTS = [" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉"] 14 | FULL_BLOCK = "█" 15 | 16 | 17 | class Bar(JupyterMixin): 18 | """Renders a solid block bar. 19 | 20 | Args: 21 | size (float): Value for the end of the bar. 22 | begin (float): Begin point (between 0 and size, inclusive). 23 | end (float): End point (between 0 and size, inclusive). 24 | width (int, optional): Width of the bar, or ``None`` for maximum width. Defaults to None. 25 | color (Union[Color, str], optional): Color of the bar. Defaults to "default". 26 | bgcolor (Union[Color, str], optional): Color of bar background. Defaults to "default". 27 | """ 28 | 29 | def __init__( 30 | self, 31 | size: float, 32 | begin: float, 33 | end: float, 34 | *, 35 | width: Optional[int] = None, 36 | color: Union[Color, str] = "default", 37 | bgcolor: Union[Color, str] = "default", 38 | ): 39 | self.size = size 40 | self.begin = max(begin, 0) 41 | self.end = min(end, size) 42 | self.width = width 43 | self.style = Style(color=color, bgcolor=bgcolor) 44 | 45 | def __repr__(self) -> str: 46 | return f"Bar({self.size}, {self.begin}, {self.end})" 47 | 48 | def __rich_console__( 49 | self, console: Console, options: ConsoleOptions 50 | ) -> RenderResult: 51 | width = min( 52 | self.width if self.width is not None else options.max_width, 53 | options.max_width, 54 | ) 55 | 56 | if self.begin >= self.end: 57 | yield Segment(" " * width, self.style) 58 | yield Segment.line() 59 | return 60 | 61 | prefix_complete_eights = int(width * 8 * self.begin / self.size) 62 | prefix_bar_count = prefix_complete_eights // 8 63 | prefix_eights_count = prefix_complete_eights % 8 64 | 65 | body_complete_eights = int(width * 8 * self.end / self.size) 66 | body_bar_count = body_complete_eights // 8 67 | body_eights_count = body_complete_eights % 8 68 | 69 | # When start and end fall into the same cell, we ideally should render 70 | # a symbol that's "center-aligned", but there is no good symbol in Unicode. 71 | # In this case, we fall back to right-aligned block symbol for simplicity. 72 | 73 | prefix = " " * prefix_bar_count 74 | if prefix_eights_count: 75 | prefix += BEGIN_BLOCK_ELEMENTS[prefix_eights_count] 76 | 77 | body = FULL_BLOCK * body_bar_count 78 | if body_eights_count: 79 | body += END_BLOCK_ELEMENTS[body_eights_count] 80 | 81 | suffix = " " * (width - len(body)) 82 | 83 | yield Segment(prefix + body[len(prefix) :] + suffix, self.style) 84 | yield Segment.line() 85 | 86 | def __rich_measure__( 87 | self, console: Console, options: ConsoleOptions 88 | ) -> Measurement: 89 | return ( 90 | Measurement(self.width, self.width) 91 | if self.width is not None 92 | else Measurement(4, options.max_width) 93 | ) 94 | -------------------------------------------------------------------------------- /rich/color_triplet.py: -------------------------------------------------------------------------------- 1 | from typing import NamedTuple, Tuple 2 | 3 | 4 | class ColorTriplet(NamedTuple): 5 | """The red, green, and blue components of a color.""" 6 | 7 | red: int 8 | """Red component in 0 to 255 range.""" 9 | green: int 10 | """Green component in 0 to 255 range.""" 11 | blue: int 12 | """Blue component in 0 to 255 range.""" 13 | 14 | @property 15 | def hex(self) -> str: 16 | """get the color triplet in CSS style.""" 17 | red, green, blue = self 18 | return f"#{red:02x}{green:02x}{blue:02x}" 19 | 20 | @property 21 | def rgb(self) -> str: 22 | """The color in RGB format. 23 | 24 | Returns: 25 | str: An rgb color, e.g. ``"rgb(100,23,255)"``. 26 | """ 27 | red, green, blue = self 28 | return f"rgb({red},{green},{blue})" 29 | 30 | @property 31 | def normalized(self) -> Tuple[float, float, float]: 32 | """Convert components into floats between 0 and 1. 33 | 34 | Returns: 35 | Tuple[float, float, float]: A tuple of three normalized colour components. 36 | """ 37 | red, green, blue = self 38 | return red / 255.0, green / 255.0, blue / 255.0 39 | -------------------------------------------------------------------------------- /rich/constrain.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, TYPE_CHECKING 2 | 3 | from .jupyter import JupyterMixin 4 | from .measure import Measurement 5 | 6 | if TYPE_CHECKING: 7 | from .console import Console, ConsoleOptions, RenderableType, RenderResult 8 | 9 | 10 | class Constrain(JupyterMixin): 11 | """Constrain the width of a renderable to a given number of characters. 12 | 13 | Args: 14 | renderable (RenderableType): A renderable object. 15 | width (int, optional): The maximum width (in characters) to render. Defaults to 80. 16 | """ 17 | 18 | def __init__(self, renderable: "RenderableType", width: Optional[int] = 80) -> None: 19 | self.renderable = renderable 20 | self.width = width 21 | 22 | def __rich_console__( 23 | self, console: "Console", options: "ConsoleOptions" 24 | ) -> "RenderResult": 25 | if self.width is None: 26 | yield self.renderable 27 | else: 28 | child_options = options.update_width(min(self.width, options.max_width)) 29 | yield from console.render(self.renderable, child_options) 30 | 31 | def __rich_measure__( 32 | self, console: "Console", options: "ConsoleOptions" 33 | ) -> "Measurement": 34 | if self.width is not None: 35 | options = options.update_width(self.width) 36 | measurement = Measurement.get(console, options, self.renderable) 37 | return measurement 38 | -------------------------------------------------------------------------------- /rich/diagnose.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | 4 | from rich import inspect 5 | from rich.console import Console, get_windows_console_features 6 | from rich.panel import Panel 7 | from rich.pretty import Pretty 8 | 9 | 10 | def report() -> None: # pragma: no cover 11 | """Print a report to the terminal with debugging information""" 12 | console = Console() 13 | inspect(console) 14 | features = get_windows_console_features() 15 | inspect(features) 16 | 17 | env_names = ( 18 | "CLICOLOR", 19 | "COLORTERM", 20 | "COLUMNS", 21 | "JPY_PARENT_PID", 22 | "JUPYTER_COLUMNS", 23 | "JUPYTER_LINES", 24 | "LINES", 25 | "NO_COLOR", 26 | "TERM_PROGRAM", 27 | "TERM", 28 | "TTY_COMPATIBLE", 29 | "VSCODE_VERBOSE_LOGGING", 30 | ) 31 | env = {name: os.getenv(name) for name in env_names} 32 | console.print(Panel.fit((Pretty(env)), title="[b]Environment Variables")) 33 | 34 | console.print(f'platform="{platform.system()}"') 35 | 36 | 37 | if __name__ == "__main__": # pragma: no cover 38 | report() 39 | -------------------------------------------------------------------------------- /rich/emoji.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import TYPE_CHECKING, Optional, Union 3 | 4 | from .jupyter import JupyterMixin 5 | from .segment import Segment 6 | from .style import Style 7 | from ._emoji_codes import EMOJI 8 | from ._emoji_replace import _emoji_replace 9 | 10 | if sys.version_info >= (3, 8): 11 | from typing import Literal 12 | else: 13 | from typing_extensions import Literal # pragma: no cover 14 | 15 | 16 | if TYPE_CHECKING: 17 | from .console import Console, ConsoleOptions, RenderResult 18 | 19 | 20 | EmojiVariant = Literal["emoji", "text"] 21 | 22 | 23 | class NoEmoji(Exception): 24 | """No emoji by that name.""" 25 | 26 | 27 | class Emoji(JupyterMixin): 28 | __slots__ = ["name", "style", "_char", "variant"] 29 | 30 | VARIANTS = {"text": "\uFE0E", "emoji": "\uFE0F"} 31 | 32 | def __init__( 33 | self, 34 | name: str, 35 | style: Union[str, Style] = "none", 36 | variant: Optional[EmojiVariant] = None, 37 | ) -> None: 38 | """A single emoji character. 39 | 40 | Args: 41 | name (str): Name of emoji. 42 | style (Union[str, Style], optional): Optional style. Defaults to None. 43 | 44 | Raises: 45 | NoEmoji: If the emoji doesn't exist. 46 | """ 47 | self.name = name 48 | self.style = style 49 | self.variant = variant 50 | try: 51 | self._char = EMOJI[name] 52 | except KeyError: 53 | raise NoEmoji(f"No emoji called {name!r}") 54 | if variant is not None: 55 | self._char += self.VARIANTS.get(variant, "") 56 | 57 | @classmethod 58 | def replace(cls, text: str) -> str: 59 | """Replace emoji markup with corresponding unicode characters. 60 | 61 | Args: 62 | text (str): A string with emojis codes, e.g. "Hello :smiley:!" 63 | 64 | Returns: 65 | str: A string with emoji codes replaces with actual emoji. 66 | """ 67 | return _emoji_replace(text) 68 | 69 | def __repr__(self) -> str: 70 | return f"" 71 | 72 | def __str__(self) -> str: 73 | return self._char 74 | 75 | def __rich_console__( 76 | self, console: "Console", options: "ConsoleOptions" 77 | ) -> "RenderResult": 78 | yield Segment(self._char, console.get_style(self.style)) 79 | 80 | 81 | if __name__ == "__main__": # pragma: no cover 82 | import sys 83 | 84 | from rich.columns import Columns 85 | from rich.console import Console 86 | 87 | console = Console(record=True) 88 | 89 | columns = Columns( 90 | (f":{name}: {name}" for name in sorted(EMOJI.keys()) if "\u200D" not in name), 91 | column_first=True, 92 | ) 93 | 94 | console.print(columns) 95 | if len(sys.argv) > 1: 96 | console.save_html(sys.argv[1]) 97 | -------------------------------------------------------------------------------- /rich/errors.py: -------------------------------------------------------------------------------- 1 | class ConsoleError(Exception): 2 | """An error in console operation.""" 3 | 4 | 5 | class StyleError(Exception): 6 | """An error in styles.""" 7 | 8 | 9 | class StyleSyntaxError(ConsoleError): 10 | """Style was badly formatted.""" 11 | 12 | 13 | class MissingStyle(StyleError): 14 | """No such style.""" 15 | 16 | 17 | class StyleStackError(ConsoleError): 18 | """Style stack is invalid.""" 19 | 20 | 21 | class NotRenderableError(ConsoleError): 22 | """Object is not renderable.""" 23 | 24 | 25 | class MarkupError(ConsoleError): 26 | """Markup was badly formatted.""" 27 | 28 | 29 | class LiveError(ConsoleError): 30 | """Error related to Live display.""" 31 | 32 | 33 | class NoAltScreen(ConsoleError): 34 | """Alt screen mode was required.""" 35 | -------------------------------------------------------------------------------- /rich/file_proxy.py: -------------------------------------------------------------------------------- 1 | import io 2 | from typing import IO, TYPE_CHECKING, Any, List 3 | 4 | from .ansi import AnsiDecoder 5 | from .text import Text 6 | 7 | if TYPE_CHECKING: 8 | from .console import Console 9 | 10 | 11 | class FileProxy(io.TextIOBase): 12 | """Wraps a file (e.g. sys.stdout) and redirects writes to a console.""" 13 | 14 | def __init__(self, console: "Console", file: IO[str]) -> None: 15 | self.__console = console 16 | self.__file = file 17 | self.__buffer: List[str] = [] 18 | self.__ansi_decoder = AnsiDecoder() 19 | 20 | @property 21 | def rich_proxied_file(self) -> IO[str]: 22 | """Get proxied file.""" 23 | return self.__file 24 | 25 | def __getattr__(self, name: str) -> Any: 26 | return getattr(self.__file, name) 27 | 28 | def write(self, text: str) -> int: 29 | if not isinstance(text, str): 30 | raise TypeError(f"write() argument must be str, not {type(text).__name__}") 31 | buffer = self.__buffer 32 | lines: List[str] = [] 33 | while text: 34 | line, new_line, text = text.partition("\n") 35 | if new_line: 36 | lines.append("".join(buffer) + line) 37 | buffer.clear() 38 | else: 39 | buffer.append(line) 40 | break 41 | if lines: 42 | console = self.__console 43 | with console: 44 | output = Text("\n").join( 45 | self.__ansi_decoder.decode_line(line) for line in lines 46 | ) 47 | console.print(output) 48 | return len(text) 49 | 50 | def flush(self) -> None: 51 | output = "".join(self.__buffer) 52 | if output: 53 | self.__console.print(output) 54 | del self.__buffer[:] 55 | 56 | def fileno(self) -> int: 57 | return self.__file.fileno() 58 | -------------------------------------------------------------------------------- /rich/filesize.py: -------------------------------------------------------------------------------- 1 | """Functions for reporting filesizes. Borrowed from https://github.com/PyFilesystem/pyfilesystem2 2 | 3 | The functions declared in this module should cover the different 4 | use cases needed to generate a string representation of a file size 5 | using several different units. Since there are many standards regarding 6 | file size units, three different functions have been implemented. 7 | 8 | See Also: 9 | * `Wikipedia: Binary prefix `_ 10 | 11 | """ 12 | 13 | __all__ = ["decimal"] 14 | 15 | from typing import Iterable, List, Optional, Tuple 16 | 17 | 18 | def _to_str( 19 | size: int, 20 | suffixes: Iterable[str], 21 | base: int, 22 | *, 23 | precision: Optional[int] = 1, 24 | separator: Optional[str] = " ", 25 | ) -> str: 26 | if size == 1: 27 | return "1 byte" 28 | elif size < base: 29 | return f"{size:,} bytes" 30 | 31 | for i, suffix in enumerate(suffixes, 2): # noqa: B007 32 | unit = base**i 33 | if size < unit: 34 | break 35 | return "{:,.{precision}f}{separator}{}".format( 36 | (base * size / unit), 37 | suffix, 38 | precision=precision, 39 | separator=separator, 40 | ) 41 | 42 | 43 | def pick_unit_and_suffix(size: int, suffixes: List[str], base: int) -> Tuple[int, str]: 44 | """Pick a suffix and base for the given size.""" 45 | for i, suffix in enumerate(suffixes): 46 | unit = base**i 47 | if size < unit * base: 48 | break 49 | return unit, suffix 50 | 51 | 52 | def decimal( 53 | size: int, 54 | *, 55 | precision: Optional[int] = 1, 56 | separator: Optional[str] = " ", 57 | ) -> str: 58 | """Convert a filesize in to a string (powers of 1000, SI prefixes). 59 | 60 | In this convention, ``1000 B = 1 kB``. 61 | 62 | This is typically the format used to advertise the storage 63 | capacity of USB flash drives and the like (*256 MB* meaning 64 | actually a storage capacity of more than *256 000 000 B*), 65 | or used by **Mac OS X** since v10.6 to report file sizes. 66 | 67 | Arguments: 68 | int (size): A file size. 69 | int (precision): The number of decimal places to include (default = 1). 70 | str (separator): The string to separate the value from the units (default = " "). 71 | 72 | Returns: 73 | `str`: A string containing a abbreviated file size and units. 74 | 75 | Example: 76 | >>> filesize.decimal(30000) 77 | '30.0 kB' 78 | >>> filesize.decimal(30000, precision=2, separator="") 79 | '30.00kB' 80 | 81 | """ 82 | return _to_str( 83 | size, 84 | ("kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"), 85 | 1000, 86 | precision=precision, 87 | separator=separator, 88 | ) 89 | -------------------------------------------------------------------------------- /rich/pager.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any 3 | 4 | 5 | class Pager(ABC): 6 | """Base class for a pager.""" 7 | 8 | @abstractmethod 9 | def show(self, content: str) -> None: 10 | """Show content in pager. 11 | 12 | Args: 13 | content (str): Content to be displayed. 14 | """ 15 | 16 | 17 | class SystemPager(Pager): 18 | """Uses the pager installed on the system.""" 19 | 20 | def _pager(self, content: str) -> Any: #  pragma: no cover 21 | return __import__("pydoc").pager(content) 22 | 23 | def show(self, content: str) -> None: 24 | """Use the same pager used by pydoc.""" 25 | self._pager(content) 26 | 27 | 28 | if __name__ == "__main__": # pragma: no cover 29 | from .__main__ import make_test_card 30 | from .console import Console 31 | 32 | console = Console() 33 | with console.pager(styles=True): 34 | console.print(make_test_card()) 35 | -------------------------------------------------------------------------------- /rich/protocol.py: -------------------------------------------------------------------------------- 1 | from typing import Any, cast, Set, TYPE_CHECKING 2 | from inspect import isclass 3 | 4 | if TYPE_CHECKING: 5 | from rich.console import RenderableType 6 | 7 | _GIBBERISH = """aihwerij235234ljsdnp34ksodfipwoe234234jlskjdf""" 8 | 9 | 10 | def is_renderable(check_object: Any) -> bool: 11 | """Check if an object may be rendered by Rich.""" 12 | return ( 13 | isinstance(check_object, str) 14 | or hasattr(check_object, "__rich__") 15 | or hasattr(check_object, "__rich_console__") 16 | ) 17 | 18 | 19 | def rich_cast(renderable: object) -> "RenderableType": 20 | """Cast an object to a renderable by calling __rich__ if present. 21 | 22 | Args: 23 | renderable (object): A potentially renderable object 24 | 25 | Returns: 26 | object: The result of recursively calling __rich__. 27 | """ 28 | from rich.console import RenderableType 29 | 30 | rich_visited_set: Set[type] = set() # Prevent potential infinite loop 31 | while hasattr(renderable, "__rich__") and not isclass(renderable): 32 | # Detect object which claim to have all the attributes 33 | if hasattr(renderable, _GIBBERISH): 34 | return repr(renderable) 35 | cast_method = getattr(renderable, "__rich__") 36 | renderable = cast_method() 37 | renderable_type = type(renderable) 38 | if renderable_type in rich_visited_set: 39 | break 40 | rich_visited_set.add(renderable_type) 41 | 42 | return cast(RenderableType, renderable) 43 | -------------------------------------------------------------------------------- /rich/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/rich/py.typed -------------------------------------------------------------------------------- /rich/region.py: -------------------------------------------------------------------------------- 1 | from typing import NamedTuple 2 | 3 | 4 | class Region(NamedTuple): 5 | """Defines a rectangular region of the screen.""" 6 | 7 | x: int 8 | y: int 9 | width: int 10 | height: int 11 | -------------------------------------------------------------------------------- /rich/scope.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Mapping 2 | from typing import TYPE_CHECKING, Any, Optional, Tuple 3 | 4 | from .highlighter import ReprHighlighter 5 | from .panel import Panel 6 | from .pretty import Pretty 7 | from .table import Table 8 | from .text import Text, TextType 9 | 10 | if TYPE_CHECKING: 11 | from .console import ConsoleRenderable 12 | 13 | 14 | def render_scope( 15 | scope: "Mapping[str, Any]", 16 | *, 17 | title: Optional[TextType] = None, 18 | sort_keys: bool = True, 19 | indent_guides: bool = False, 20 | max_length: Optional[int] = None, 21 | max_string: Optional[int] = None, 22 | ) -> "ConsoleRenderable": 23 | """Render python variables in a given scope. 24 | 25 | Args: 26 | scope (Mapping): A mapping containing variable names and values. 27 | title (str, optional): Optional title. Defaults to None. 28 | sort_keys (bool, optional): Enable sorting of items. Defaults to True. 29 | indent_guides (bool, optional): Enable indentation guides. Defaults to False. 30 | max_length (int, optional): Maximum length of containers before abbreviating, or None for no abbreviation. 31 | Defaults to None. 32 | max_string (int, optional): Maximum length of string before truncating, or None to disable. Defaults to None. 33 | 34 | Returns: 35 | ConsoleRenderable: A renderable object. 36 | """ 37 | highlighter = ReprHighlighter() 38 | items_table = Table.grid(padding=(0, 1), expand=False) 39 | items_table.add_column(justify="right") 40 | 41 | def sort_items(item: Tuple[str, Any]) -> Tuple[bool, str]: 42 | """Sort special variables first, then alphabetically.""" 43 | key, _ = item 44 | return (not key.startswith("__"), key.lower()) 45 | 46 | items = sorted(scope.items(), key=sort_items) if sort_keys else scope.items() 47 | for key, value in items: 48 | key_text = Text.assemble( 49 | (key, "scope.key.special" if key.startswith("__") else "scope.key"), 50 | (" =", "scope.equals"), 51 | ) 52 | items_table.add_row( 53 | key_text, 54 | Pretty( 55 | value, 56 | highlighter=highlighter, 57 | indent_guides=indent_guides, 58 | max_length=max_length, 59 | max_string=max_string, 60 | ), 61 | ) 62 | return Panel.fit( 63 | items_table, 64 | title=title, 65 | border_style="scope.border", 66 | padding=(0, 1), 67 | ) 68 | 69 | 70 | if __name__ == "__main__": # pragma: no cover 71 | from rich import print 72 | 73 | print() 74 | 75 | def test(foo: float, bar: float) -> None: 76 | list_of_things = [1, 2, 3, None, 4, True, False, "Hello World"] 77 | dict_of_things = { 78 | "version": "1.1", 79 | "method": "confirmFruitPurchase", 80 | "params": [["apple", "orange", "mangoes", "pomelo"], 1.123], 81 | "id": "194521489", 82 | } 83 | print(render_scope(locals(), title="[i]locals", sort_keys=False)) 84 | 85 | test(20.3423, 3.1427) 86 | print() 87 | -------------------------------------------------------------------------------- /rich/screen.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, TYPE_CHECKING 2 | 3 | from .segment import Segment 4 | from .style import StyleType 5 | from ._loop import loop_last 6 | 7 | 8 | if TYPE_CHECKING: 9 | from .console import ( 10 | Console, 11 | ConsoleOptions, 12 | RenderResult, 13 | RenderableType, 14 | Group, 15 | ) 16 | 17 | 18 | class Screen: 19 | """A renderable that fills the terminal screen and crops excess. 20 | 21 | Args: 22 | renderable (RenderableType): Child renderable. 23 | style (StyleType, optional): Optional background style. Defaults to None. 24 | """ 25 | 26 | renderable: "RenderableType" 27 | 28 | def __init__( 29 | self, 30 | *renderables: "RenderableType", 31 | style: Optional[StyleType] = None, 32 | application_mode: bool = False, 33 | ) -> None: 34 | from rich.console import Group 35 | 36 | self.renderable = Group(*renderables) 37 | self.style = style 38 | self.application_mode = application_mode 39 | 40 | def __rich_console__( 41 | self, console: "Console", options: "ConsoleOptions" 42 | ) -> "RenderResult": 43 | width, height = options.size 44 | style = console.get_style(self.style) if self.style else None 45 | render_options = options.update(width=width, height=height) 46 | lines = console.render_lines( 47 | self.renderable or "", render_options, style=style, pad=True 48 | ) 49 | lines = Segment.set_shape(lines, width, height, style=style) 50 | new_line = Segment("\n\r") if self.application_mode else Segment.line() 51 | for last, line in loop_last(lines): 52 | yield from line 53 | if not last: 54 | yield new_line 55 | -------------------------------------------------------------------------------- /rich/styled.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from .measure import Measurement 4 | from .segment import Segment 5 | from .style import StyleType 6 | 7 | if TYPE_CHECKING: 8 | from .console import Console, ConsoleOptions, RenderResult, RenderableType 9 | 10 | 11 | class Styled: 12 | """Apply a style to a renderable. 13 | 14 | Args: 15 | renderable (RenderableType): Any renderable. 16 | style (StyleType): A style to apply across the entire renderable. 17 | """ 18 | 19 | def __init__(self, renderable: "RenderableType", style: "StyleType") -> None: 20 | self.renderable = renderable 21 | self.style = style 22 | 23 | def __rich_console__( 24 | self, console: "Console", options: "ConsoleOptions" 25 | ) -> "RenderResult": 26 | style = console.get_style(self.style) 27 | rendered_segments = console.render(self.renderable, options) 28 | segments = Segment.apply_style(rendered_segments, style) 29 | return segments 30 | 31 | def __rich_measure__( 32 | self, console: "Console", options: "ConsoleOptions" 33 | ) -> Measurement: 34 | return Measurement.get(console, options, self.renderable) 35 | 36 | 37 | if __name__ == "__main__": # pragma: no cover 38 | from rich import print 39 | from rich.panel import Panel 40 | 41 | panel = Styled(Panel("hello"), "on blue") 42 | print(panel) 43 | -------------------------------------------------------------------------------- /rich/themes.py: -------------------------------------------------------------------------------- 1 | from .default_styles import DEFAULT_STYLES 2 | from .theme import Theme 3 | 4 | 5 | DEFAULT = Theme(DEFAULT_STYLES) 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # This is a shim to hopefully allow Github to detect the package, build is done with poetry 4 | 5 | import setuptools 6 | 7 | if __name__ == "__main__": 8 | setuptools.setup(name="rich") 9 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Textualize/rich/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(autouse=True) 5 | def reset_color_envvars(monkeypatch): 6 | """Remove color-related envvars to fix test output""" 7 | monkeypatch.delenv("FORCE_COLOR", raising=False) 8 | monkeypatch.delenv("NO_COLOR", raising=False) 9 | -------------------------------------------------------------------------------- /tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | junit_family=legacy 3 | -------------------------------------------------------------------------------- /tests/render.py: -------------------------------------------------------------------------------- 1 | import io 2 | import re 3 | 4 | from rich.console import Console, RenderableType 5 | 6 | 7 | re_link_ids = re.compile(r"id=[\d.\-]*?;.*?\x1b") 8 | 9 | 10 | def replace_link_ids(render: str) -> str: 11 | """Link IDs have a random ID and system path which is a problem for 12 | reproducible tests. 13 | 14 | """ 15 | return re_link_ids.sub("id=0;foo\x1b", render) 16 | 17 | 18 | def render(renderable: RenderableType, no_wrap: bool = False) -> str: 19 | console = Console( 20 | width=100, file=io.StringIO(), color_system="truecolor", legacy_windows=False 21 | ) 22 | console.print(renderable, no_wrap=no_wrap) 23 | output = replace_link_ids(console.file.getvalue()) 24 | return output 25 | -------------------------------------------------------------------------------- /tests/test_ansi.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from rich.ansi import AnsiDecoder 4 | from rich.console import Console 5 | from rich.style import Style 6 | from rich.text import Span, Text 7 | 8 | 9 | def test_decode(): 10 | console = Console( 11 | force_terminal=True, legacy_windows=False, color_system="truecolor" 12 | ) 13 | console.begin_capture() 14 | console.print("Hello") 15 | console.print("[b]foo[/b]") 16 | console.print("[link http://example.org]bar") 17 | console.print("[#ff0000 on color(200)]red") 18 | console.print("[color(200) on #ff0000]red") 19 | terminal_codes = console.end_capture() 20 | 21 | decoder = AnsiDecoder() 22 | lines = list(decoder.decode(terminal_codes)) 23 | 24 | expected = [ 25 | Text("Hello"), 26 | Text("foo", spans=[Span(0, 3, Style.parse("bold"))]), 27 | Text("bar", spans=[Span(0, 3, Style.parse("link http://example.org"))]), 28 | Text("red", spans=[Span(0, 3, Style.parse("#ff0000 on color(200)"))]), 29 | Text("red", spans=[Span(0, 3, Style.parse("color(200) on #ff0000"))]), 30 | ] 31 | 32 | assert lines == expected 33 | 34 | 35 | def test_decode_example(): 36 | ansi_bytes = b"\x1b[01m\x1b[KC:\\Users\\stefa\\AppData\\Local\\Temp\\tmp3ydingba:\x1b[m\x1b[K In function '\x1b[01m\x1b[Kmain\x1b[m\x1b[K':\n\x1b[01m\x1b[KC:\\Users\\stefa\\AppData\\Local\\Temp\\tmp3ydingba:3:5:\x1b[m\x1b[K \x1b[01;35m\x1b[Kwarning: \x1b[m\x1b[Kunused variable '\x1b[01m\x1b[Ka\x1b[m\x1b[K' [\x1b[01;35m\x1b[K-Wunused-variable\x1b[m\x1b[K]\n 3 | int \x1b[01;35m\x1b[Ka\x1b[m\x1b[K=1;\n | \x1b[01;35m\x1b[K^\x1b[m\x1b[K\n" 37 | ansi_text = ansi_bytes.decode("utf-8") 38 | 39 | text = Text.from_ansi(ansi_text) 40 | 41 | console = Console( 42 | force_terminal=True, legacy_windows=False, color_system="truecolor" 43 | ) 44 | with console.capture() as capture: 45 | console.print(text) 46 | result = capture.get() 47 | print(repr(result)) 48 | expected = "\x1b[1mC:\\Users\\stefa\\AppData\\Local\\Temp\\tmp3ydingba:\x1b[0m In function '\x1b[1mmain\x1b[0m':\n\x1b[1mC:\\Users\\stefa\\AppData\\Local\\Temp\\tmp3ydingba:3:5:\x1b[0m \x1b[1;35mwarning: \x1b[0munused variable '\x1b[1ma\x1b[0m' \n[\x1b[1;35m-Wunused-variable\x1b[0m]\n 3 | int \x1b[1;35ma\x1b[0m=1;\n | \x1b[1;35m^\x1b[0m\n" 49 | assert result == expected 50 | 51 | 52 | @pytest.mark.parametrize( 53 | "ansi_bytes, expected_text", 54 | [ 55 | # https://github.com/Textualize/rich/issues/2688 56 | ( 57 | b"\x1b[31mFound 4 errors in 2 files (checked 18 source files)\x1b(B\x1b[m\n", 58 | "Found 4 errors in 2 files (checked 18 source files)", 59 | ), 60 | # https://mail.python.org/pipermail/python-list/2007-December/424756.html 61 | (b"Hallo", "Hallo"), 62 | (b"\x1b(BHallo", "Hallo"), 63 | (b"\x1b(JHallo", "Hallo"), 64 | (b"\x1b(BHal\x1b(Jlo", "Hallo"), 65 | ], 66 | ) 67 | def test_decode_issue_2688(ansi_bytes, expected_text): 68 | text = Text.from_ansi(ansi_bytes.decode()) 69 | 70 | assert str(text) == expected_text 71 | 72 | 73 | @pytest.mark.parametrize("code", [*"0123456789:;<=>?"]) 74 | def test_strip_private_escape_sequences(code): 75 | text = Text.from_ansi(f"\x1b{code}x") 76 | 77 | console = Console(force_terminal=True) 78 | 79 | with console.capture() as capture: 80 | console.print(text) 81 | 82 | expected = "x\n" 83 | 84 | assert capture.get() == expected 85 | -------------------------------------------------------------------------------- /tests/test_block_bar.py: -------------------------------------------------------------------------------- 1 | from rich.bar import Bar 2 | from rich.console import Console 3 | 4 | from .render import render 5 | 6 | 7 | expected = [ 8 | "\x1b[39;49m ▐█████████████████████████ \x1b[0m\n", 9 | "\x1b[39;49m ██████████████████████▌ \x1b[0m\n", 10 | "\x1b[39;49m \x1b[0m\n", 11 | ] 12 | 13 | 14 | def test_repr(): 15 | bar = Bar(size=100, begin=11, end=62, width=50) 16 | assert repr(bar) == "Bar(100, 11, 62)" 17 | 18 | 19 | def test_render(): 20 | bar = Bar(size=100, begin=11, end=62, width=50) 21 | bar_render = render(bar) 22 | assert bar_render == expected[0] 23 | bar = Bar(size=100, begin=12, end=57, width=50) 24 | bar_render = render(bar) 25 | assert bar_render == expected[1] 26 | # begin after end 27 | bar = Bar(size=100, begin=60, end=40, width=50) 28 | bar_render = render(bar) 29 | assert bar_render == expected[2] 30 | 31 | 32 | def test_measure(): 33 | console = Console(width=120) 34 | bar = Bar(size=100, begin=11, end=62) 35 | measurement = bar.__rich_measure__(console, console.options) 36 | assert measurement.minimum == 4 37 | assert measurement.maximum == 120 38 | 39 | 40 | def test_zero_total(): 41 | # Shouldn't throw zero division error 42 | bar = Bar(size=0, begin=0, end=0) 43 | render(bar) 44 | 45 | 46 | if __name__ == "__main__": 47 | bar = Bar(size=100, begin=11, end=62, width=50) 48 | bar_render = render(bar) 49 | print(repr(bar_render)) 50 | bar = Bar(size=100, begin=12, end=57, width=50) 51 | bar_render = render(bar) 52 | print(repr(bar_render)) 53 | bar = Bar(size=100, begin=60, end=40, width=50) 54 | bar_render = render(bar) 55 | print(repr(bar_render)) 56 | -------------------------------------------------------------------------------- /tests/test_box.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from rich.console import ConsoleOptions, ConsoleDimensions 4 | from rich.box import ( 5 | ASCII, 6 | DOUBLE, 7 | ROUNDED, 8 | HEAVY, 9 | SQUARE, 10 | MINIMAL_HEAVY_HEAD, 11 | MINIMAL, 12 | SIMPLE_HEAVY, 13 | SIMPLE, 14 | HEAVY_EDGE, 15 | HEAVY_HEAD, 16 | ) 17 | 18 | 19 | def test_str(): 20 | assert str(ASCII) == "+--+\n| ||\n|-+|\n| ||\n|-+|\n|-+|\n| ||\n+--+\n" 21 | 22 | 23 | def test_repr(): 24 | assert repr(ASCII) == "Box(...)" 25 | 26 | 27 | def test_get_top(): 28 | top = HEAVY.get_top(widths=[1, 2]) 29 | assert top == "┏━┳━━┓" 30 | 31 | 32 | def test_get_row(): 33 | head_row = DOUBLE.get_row(widths=[3, 2, 1], level="head") 34 | assert head_row == "╠═══╬══╬═╣" 35 | 36 | row = ASCII.get_row(widths=[1, 2, 3], level="row") 37 | assert row == "|-+--+---|" 38 | 39 | foot_row = ROUNDED.get_row(widths=[2, 1, 3], level="foot") 40 | assert foot_row == "├──┼─┼───┤" 41 | 42 | with pytest.raises(ValueError): 43 | ROUNDED.get_row(widths=[1, 2, 3], level="FOO") 44 | 45 | 46 | def test_get_bottom(): 47 | bottom = HEAVY.get_bottom(widths=[1, 2, 3]) 48 | assert bottom == "┗━┻━━┻━━━┛" 49 | 50 | 51 | def test_box_substitute_for_same_box(): 52 | options = ConsoleOptions( 53 | ConsoleDimensions(80, 25), 54 | legacy_windows=False, 55 | min_width=1, 56 | max_width=100, 57 | is_terminal=True, 58 | encoding="utf-8", 59 | max_height=25, 60 | ) 61 | 62 | assert ROUNDED.substitute(options) == ROUNDED 63 | assert MINIMAL_HEAVY_HEAD.substitute(options) == MINIMAL_HEAVY_HEAD 64 | assert SIMPLE_HEAVY.substitute(options) == SIMPLE_HEAVY 65 | assert HEAVY.substitute(options) == HEAVY 66 | assert HEAVY_EDGE.substitute(options) == HEAVY_EDGE 67 | assert HEAVY_HEAD.substitute(options) == HEAVY_HEAD 68 | 69 | 70 | def test_box_substitute_for_different_box_legacy_windows(): 71 | options = ConsoleOptions( 72 | ConsoleDimensions(80, 25), 73 | legacy_windows=True, 74 | min_width=1, 75 | max_width=100, 76 | is_terminal=True, 77 | encoding="utf-8", 78 | max_height=25, 79 | ) 80 | 81 | assert ROUNDED.substitute(options) == SQUARE 82 | assert MINIMAL_HEAVY_HEAD.substitute(options) == MINIMAL 83 | assert SIMPLE_HEAVY.substitute(options) == SIMPLE 84 | assert HEAVY.substitute(options) == SQUARE 85 | assert HEAVY_EDGE.substitute(options) == SQUARE 86 | assert HEAVY_HEAD.substitute(options) == SQUARE 87 | 88 | 89 | def test_box_substitute_for_different_box_ascii_encoding(): 90 | options = ConsoleOptions( 91 | ConsoleDimensions(80, 25), 92 | legacy_windows=True, 93 | min_width=1, 94 | max_width=100, 95 | is_terminal=True, 96 | encoding="ascii", 97 | max_height=25, 98 | ) 99 | 100 | assert ROUNDED.substitute(options) == ASCII 101 | assert MINIMAL_HEAVY_HEAD.substitute(options) == ASCII 102 | assert SIMPLE_HEAVY.substitute(options) == ASCII 103 | assert HEAVY.substitute(options) == ASCII 104 | assert HEAVY_EDGE.substitute(options) == ASCII 105 | assert HEAVY_HEAD.substitute(options) == ASCII 106 | -------------------------------------------------------------------------------- /tests/test_card.py: -------------------------------------------------------------------------------- 1 | import io 2 | import re 3 | 4 | from rich.__main__ import make_test_card 5 | from rich.console import Console, RenderableType 6 | 7 | from ._card_render import expected 8 | 9 | re_link_ids = re.compile(r"id=[\d\.\-]*?;.*?\x1b") 10 | 11 | 12 | def replace_link_ids(render: str) -> str: 13 | """Link IDs have a random ID and system path which is a problem for 14 | reproducible tests. 15 | 16 | """ 17 | return re_link_ids.sub("id=0;foo\x1b", render) 18 | 19 | 20 | def render(renderable: RenderableType) -> str: 21 | console = Console( 22 | width=100, file=io.StringIO(), color_system="truecolor", legacy_windows=False 23 | ) 24 | console.print(renderable) 25 | output = replace_link_ids(console.file.getvalue()) 26 | return output 27 | 28 | 29 | def test_card_render(): 30 | card = make_test_card() 31 | result = render(card) 32 | print(repr(result)) 33 | assert result == expected 34 | 35 | 36 | if __name__ == "__main__": 37 | card = make_test_card() 38 | with open("_card_render.py", "wt") as fh: 39 | card_render = render(card) 40 | print(card_render) 41 | fh.write(f"expected={card_render!r}") 42 | -------------------------------------------------------------------------------- /tests/test_cells.py: -------------------------------------------------------------------------------- 1 | import string 2 | 3 | from rich import cells 4 | from rich.cells import _is_single_cell_widths, chop_cells 5 | 6 | 7 | def test_cell_len_long_string(): 8 | # Long strings don't use cached cell length implementation 9 | assert cells.cell_len("abc" * 200) == 3 * 200 10 | # Boundary case 11 | assert cells.cell_len("a" * 512) == 512 12 | 13 | 14 | def test_cell_len_short_string(): 15 | # Short strings use cached cell length implementation 16 | assert cells.cell_len("abc" * 100) == 3 * 100 17 | # Boundary case 18 | assert cells.cell_len("a" * 511) == 511 19 | 20 | 21 | def test_set_cell_size(): 22 | assert cells.set_cell_size("foo", 0) == "" 23 | assert cells.set_cell_size("f", 0) == "" 24 | assert cells.set_cell_size("", 0) == "" 25 | assert cells.set_cell_size("😽😽", 0) == "" 26 | assert cells.set_cell_size("foo", 2) == "fo" 27 | assert cells.set_cell_size("foo", 3) == "foo" 28 | assert cells.set_cell_size("foo", 4) == "foo " 29 | assert cells.set_cell_size("😽😽", 4) == "😽😽" 30 | assert cells.set_cell_size("😽😽", 3) == "😽 " 31 | assert cells.set_cell_size("😽😽", 2) == "😽" 32 | assert cells.set_cell_size("😽😽", 1) == " " 33 | assert cells.set_cell_size("😽😽", 5) == "😽😽 " 34 | 35 | 36 | def test_set_cell_size_infinite(): 37 | for size in range(38): 38 | assert ( 39 | cells.cell_len( 40 | cells.set_cell_size( 41 | "เป็นเกมที่ต้องมีความอดทนมากที่สุดตั้งเเต่เคยเล่นมา", size 42 | ) 43 | ) 44 | == size 45 | ) 46 | 47 | 48 | def test_chop_cells(): 49 | """Simple example of splitting cells into lines of width 3.""" 50 | text = "abcdefghijk" 51 | assert chop_cells(text, 3) == ["abc", "def", "ghi", "jk"] 52 | 53 | 54 | def test_chop_cells_double_width_boundary(): 55 | """The available width lies within a double-width character.""" 56 | text = "ありがとう" 57 | assert chop_cells(text, 3) == ["あ", "り", "が", "と", "う"] 58 | 59 | 60 | def test_chop_cells_mixed_width(): 61 | """Mixed single and double-width characters.""" 62 | text = "あ1り234が5と6う78" 63 | assert chop_cells(text, 3) == ["あ1", "り2", "34", "が5", "と6", "う7", "8"] 64 | 65 | 66 | def test_is_single_cell_widths() -> None: 67 | # Check _is_single_cell_widths reports correctly 68 | for character in string.printable: 69 | if ord(character) >= 32: 70 | assert _is_single_cell_widths(character) 71 | 72 | BOX = "┌─┬┐│ ││├─┼┤│ ││├─┼┤├─┼┤│ ││└─┴┘" 73 | 74 | for character in BOX: 75 | assert _is_single_cell_widths(character) 76 | 77 | for character in "💩😽": 78 | assert not _is_single_cell_widths(character) 79 | 80 | for character in "わさび": 81 | assert not _is_single_cell_widths(character) 82 | -------------------------------------------------------------------------------- /tests/test_color_triplet.py: -------------------------------------------------------------------------------- 1 | from rich.color_triplet import ColorTriplet 2 | 3 | 4 | def test_hex(): 5 | assert ColorTriplet(255, 255, 255).hex == "#ffffff" 6 | assert ColorTriplet(0, 255, 0).hex == "#00ff00" 7 | 8 | 9 | def test_rgb(): 10 | assert ColorTriplet(255, 255, 255).rgb == "rgb(255,255,255)" 11 | assert ColorTriplet(0, 255, 0).rgb == "rgb(0,255,0)" 12 | 13 | 14 | def test_normalized(): 15 | assert ColorTriplet(255, 255, 255).normalized == (1.0, 1.0, 1.0) 16 | assert ColorTriplet(0, 255, 0).normalized == (0.0, 1.0, 0.0) 17 | -------------------------------------------------------------------------------- /tests/test_columns_align.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | 3 | import io 4 | 5 | from rich import box 6 | from rich.columns import Columns 7 | from rich.console import Console 8 | from rich.panel import Panel 9 | 10 | 11 | def render(): 12 | console = Console(file=io.StringIO(), width=100, legacy_windows=False) 13 | panel = Panel.fit("foo", box=box.SQUARE, padding=0) 14 | columns = Columns([panel] * 4) 15 | columns.expand = True 16 | console.rule("no align") 17 | console.print(columns) 18 | 19 | columns.align = "left" 20 | console.rule("left align") 21 | console.print(columns) 22 | 23 | columns.align = "center" 24 | console.rule("center align") 25 | console.print(columns) 26 | 27 | columns.align = "right" 28 | console.rule("right align") 29 | console.print(columns) 30 | 31 | return console.file.getvalue() 32 | 33 | 34 | def test_align(): 35 | result = render() 36 | expected = "───────────────────────────────────────────── no align ─────────────────────────────────────────────\n┌───┐ ┌───┐ ┌───┐ ┌───┐ \n│foo│ │foo│ │foo│ │foo│ \n└───┘ └───┘ └───┘ └───┘ \n──────────────────────────────────────────── left align ────────────────────────────────────────────\n┌───┐ ┌───┐ ┌───┐ ┌───┐ \n│foo│ │foo│ │foo│ │foo│ \n└───┘ └───┘ └───┘ └───┘ \n─────────────────────────────────────────── center align ───────────────────────────────────────────\n ┌───┐ ┌───┐ ┌───┐ ┌───┐ \n │foo│ │foo│ │foo│ │foo│ \n └───┘ └───┘ └───┘ └───┘ \n─────────────────────────────────────────── right align ────────────────────────────────────────────\n ┌───┐ ┌───┐ ┌───┐ ┌───┐\n │foo│ │foo│ │foo│ │foo│\n └───┘ └───┘ └───┘ └───┘\n" 37 | assert result == expected 38 | 39 | 40 | if __name__ == "__main__": 41 | rendered = render() 42 | print(rendered) 43 | print(repr(rendered)) 44 | -------------------------------------------------------------------------------- /tests/test_constrain.py: -------------------------------------------------------------------------------- 1 | from rich.console import Console 2 | from rich.constrain import Constrain 3 | from rich.text import Text 4 | 5 | 6 | def test_width_of_none(): 7 | console = Console() 8 | constrain = Constrain(Text("foo"), width=None) 9 | min_width, max_width = constrain.__rich_measure__( 10 | console, console.options.update_width(80) 11 | ) 12 | assert min_width == 3 13 | assert max_width == 3 14 | -------------------------------------------------------------------------------- /tests/test_containers.py: -------------------------------------------------------------------------------- 1 | from rich.console import Console 2 | from rich.containers import Lines, Renderables 3 | from rich.text import Span, Text 4 | from rich.style import Style 5 | 6 | 7 | def test_renderables_measure(): 8 | console = Console() 9 | text = Text("foo") 10 | renderables = Renderables([text]) 11 | 12 | result = renderables.__rich_measure__(console, console.options) 13 | _min, _max = result 14 | assert _min == 3 15 | assert _max == 3 16 | 17 | assert list(renderables) == [text] 18 | 19 | 20 | def test_renderables_empty(): 21 | console = Console() 22 | renderables = Renderables() 23 | 24 | result = renderables.__rich_measure__(console, console.options) 25 | _min, _max = result 26 | assert _min == 1 27 | assert _max == 1 28 | 29 | 30 | def test_lines_rich_console(): 31 | console = Console() 32 | lines = Lines([Text("foo")]) 33 | 34 | result = list(lines.__rich_console__(console, console.options)) 35 | assert result == [Text("foo")] 36 | 37 | 38 | def test_lines_justify(): 39 | console = Console() 40 | lines1 = Lines([Text("foo", style="b"), Text("test", style="b")]) 41 | lines1.justify(console, 10, justify="left") 42 | assert lines1._lines == [Text("foo "), Text("test ")] 43 | lines1.justify(console, 10, justify="center") 44 | assert lines1._lines == [Text(" foo "), Text(" test ")] 45 | lines1.justify(console, 10, justify="right") 46 | assert lines1._lines == [Text(" foo"), Text(" test")] 47 | 48 | lines2 = Lines([Text("foo bar", style="b"), Text("test", style="b")]) 49 | lines2.justify(console, 7, justify="full") 50 | print(repr(lines2._lines[0].spans)) 51 | assert lines2._lines == [ 52 | Text( 53 | "foo bar", 54 | spans=[Span(0, 3, "b"), Span(3, 4, Style.parse("bold")), Span(4, 7, "b")], 55 | ), 56 | Text("test"), 57 | ] 58 | -------------------------------------------------------------------------------- /tests/test_control.py: -------------------------------------------------------------------------------- 1 | from rich.control import Control, escape_control_codes, strip_control_codes 2 | from rich.segment import ControlType, Segment 3 | 4 | 5 | def test_control(): 6 | control = Control(ControlType.BELL) 7 | assert str(control) == "\x07" 8 | 9 | 10 | def test_strip_control_codes(): 11 | assert strip_control_codes("") == "" 12 | assert strip_control_codes("foo\rbar") == "foobar" 13 | assert strip_control_codes("Fear is the mind killer") == "Fear is the mind killer" 14 | 15 | 16 | def test_escape_control_codes(): 17 | assert escape_control_codes("") == "" 18 | assert escape_control_codes("foo\rbar") == "foo\\rbar" 19 | assert escape_control_codes("Fear is the mind killer") == "Fear is the mind killer" 20 | 21 | 22 | def test_control_move_to(): 23 | control = Control.move_to(5, 10) 24 | print(control.segment) 25 | assert control.segment == Segment( 26 | "\x1b[11;6H", None, [(ControlType.CURSOR_MOVE_TO, 5, 10)] 27 | ) 28 | 29 | 30 | def test_control_move(): 31 | assert Control.move(0, 0).segment == Segment("", None, []) 32 | control = Control.move(3, 4) 33 | print(repr(control.segment)) 34 | assert control.segment == Segment( 35 | "\x1b[3C\x1b[4B", 36 | None, 37 | [(ControlType.CURSOR_FORWARD, 3), (ControlType.CURSOR_DOWN, 4)], 38 | ) 39 | 40 | 41 | def test_move_to_column(): 42 | print(repr(Control.move_to_column(10, 20).segment)) 43 | assert Control.move_to_column(10, 20).segment == Segment( 44 | "\x1b[11G\x1b[20B", 45 | None, 46 | [(ControlType.CURSOR_MOVE_TO_COLUMN, 10), (ControlType.CURSOR_DOWN, 20)], 47 | ) 48 | 49 | assert Control.move_to_column(10, -20).segment == Segment( 50 | "\x1b[11G\x1b[20A", 51 | None, 52 | [(ControlType.CURSOR_MOVE_TO_COLUMN, 10), (ControlType.CURSOR_UP, 20)], 53 | ) 54 | 55 | 56 | def test_title(): 57 | control_segment = Control.title("hello").segment 58 | assert control_segment == Segment( 59 | "\x1b]0;hello\x07", 60 | None, 61 | [(ControlType.SET_WINDOW_TITLE, "hello")], 62 | ) 63 | -------------------------------------------------------------------------------- /tests/test_emoji.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from rich.emoji import Emoji, NoEmoji 4 | 5 | from .render import render 6 | 7 | 8 | def test_no_emoji(): 9 | with pytest.raises(NoEmoji): 10 | Emoji("ambivalent_bunny") 11 | 12 | 13 | def test_str_repr(): 14 | assert str(Emoji("pile_of_poo")) == "💩" 15 | assert repr(Emoji("pile_of_poo")) == "" 16 | 17 | 18 | def test_replace(): 19 | assert Emoji.replace("my code is :pile_of_poo:") == "my code is 💩" 20 | 21 | 22 | def test_render(): 23 | render_result = render(Emoji("pile_of_poo")) 24 | assert render_result == "💩" 25 | 26 | 27 | def test_variant(): 28 | print(repr(Emoji.replace(":warning:"))) 29 | assert Emoji.replace(":warning:") == "⚠" 30 | assert Emoji.replace(":warning-text:") == "⚠" + "\uFE0E" 31 | assert Emoji.replace(":warning-emoji:") == "⚠" + "\uFE0F" 32 | assert Emoji.replace(":warning-foo:") == ":warning-foo:" 33 | 34 | 35 | def test_variant_non_default(): 36 | render_result = render(Emoji("warning", variant="emoji")) 37 | assert render_result == "⚠" + "\uFE0F" 38 | -------------------------------------------------------------------------------- /tests/test_file_proxy.py: -------------------------------------------------------------------------------- 1 | import io 2 | import sys 3 | 4 | import pytest 5 | 6 | from rich.console import Console 7 | from rich.file_proxy import FileProxy 8 | 9 | 10 | def test_empty_bytes(): 11 | console = Console() 12 | file_proxy = FileProxy(console, sys.stdout) 13 | # File should raise TypeError when writing bytes 14 | with pytest.raises(TypeError): 15 | file_proxy.write(b"") # type: ignore 16 | with pytest.raises(TypeError): 17 | file_proxy.write(b"foo") # type: ignore 18 | 19 | 20 | def test_flush(): 21 | file = io.StringIO() 22 | console = Console(file=file) 23 | file_proxy = FileProxy(console, file) 24 | file_proxy.write("foo") 25 | assert file.getvalue() == "" 26 | file_proxy.flush() 27 | assert file.getvalue() == "foo\n" 28 | 29 | 30 | def test_new_lines(): 31 | file = io.StringIO() 32 | console = Console(file=file) 33 | file_proxy = FileProxy(console, file) 34 | file_proxy.write("-\n-") 35 | assert file.getvalue() == "-\n" 36 | file_proxy.flush() 37 | assert file.getvalue() == "-\n-\n" 38 | -------------------------------------------------------------------------------- /tests/test_filesize.py: -------------------------------------------------------------------------------- 1 | from rich import filesize 2 | 3 | 4 | def test_traditional(): 5 | assert filesize.decimal(0) == "0 bytes" 6 | assert filesize.decimal(1) == "1 byte" 7 | assert filesize.decimal(2) == "2 bytes" 8 | assert filesize.decimal(1000) == "1.0 kB" 9 | assert filesize.decimal(1.5 * 1000 * 1000) == "1.5 MB" 10 | assert filesize.decimal(0, precision=2) == "0 bytes" 11 | assert filesize.decimal(1111, precision=0) == "1 kB" 12 | assert filesize.decimal(1111, precision=1) == "1.1 kB" 13 | assert filesize.decimal(1111, precision=2) == "1.11 kB" 14 | assert filesize.decimal(1111, separator="") == "1.1kB" 15 | 16 | 17 | def test_pick_unit_and_suffix(): 18 | units = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] 19 | assert filesize.pick_unit_and_suffix(50, units, 1024) == (1, "bytes") 20 | assert filesize.pick_unit_and_suffix(2048, units, 1024) == (1024, "KB") 21 | -------------------------------------------------------------------------------- /tests/test_getfileno.py: -------------------------------------------------------------------------------- 1 | from rich._fileno import get_fileno 2 | 3 | 4 | def test_get_fileno(): 5 | class FileLike: 6 | def fileno(self) -> int: 7 | return 123 8 | 9 | assert get_fileno(FileLike()) == 123 10 | 11 | 12 | def test_get_fileno_missing(): 13 | class FileLike: 14 | pass 15 | 16 | assert get_fileno(FileLike()) is None 17 | 18 | 19 | def test_get_fileno_broken(): 20 | class FileLike: 21 | def fileno(self) -> int: 22 | 1 / 0 23 | return 123 24 | 25 | assert get_fileno(FileLike()) is None 26 | -------------------------------------------------------------------------------- /tests/test_json.py: -------------------------------------------------------------------------------- 1 | from rich.json import JSON 2 | import datetime 3 | 4 | 5 | def test_print_json_data_with_default(): 6 | date = datetime.date(2021, 1, 1) 7 | json = JSON.from_data({"date": date}, default=lambda d: d.isoformat()) 8 | assert str(json.text) == '{\n "date": "2021-01-01"\n}' 9 | -------------------------------------------------------------------------------- /tests/test_jupyter.py: -------------------------------------------------------------------------------- 1 | from rich.console import Console 2 | 3 | 4 | def test_jupyter(): 5 | console = Console(force_jupyter=True) 6 | assert console.width == 115 7 | assert console.height == 100 8 | assert console.color_system == "truecolor" 9 | 10 | 11 | def test_jupyter_columns_env(): 12 | console = Console(_environ={"JUPYTER_COLUMNS": "314"}, force_jupyter=True) 13 | assert console.width == 314 14 | # width take precedence 15 | console = Console(width=40, _environ={"JUPYTER_COLUMNS": "314"}, force_jupyter=True) 16 | assert console.width == 40 17 | # Should not fail 18 | console = Console( 19 | width=40, _environ={"JUPYTER_COLUMNS": "broken"}, force_jupyter=True 20 | ) 21 | 22 | 23 | def test_jupyter_lines_env(): 24 | console = Console(_environ={"JUPYTER_LINES": "220"}, force_jupyter=True) 25 | assert console.height == 220 26 | # height take precedence 27 | console = Console(height=40, _environ={"JUPYTER_LINES": "220"}, force_jupyter=True) 28 | assert console.height == 40 29 | # Should not fail 30 | console = Console( 31 | width=40, _environ={"JUPYTER_LINES": "broken"}, force_jupyter=True 32 | ) 33 | -------------------------------------------------------------------------------- /tests/test_live_render.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from rich.live_render import LiveRender 3 | from rich.console import Console, ConsoleDimensions, ConsoleOptions 4 | from rich.style import Style 5 | from rich.segment import Segment 6 | 7 | 8 | @pytest.fixture 9 | def live_render(): 10 | return LiveRender(renderable="my string") 11 | 12 | 13 | def test_renderable(live_render): 14 | assert live_render.renderable == "my string" 15 | live_render.set_renderable("another string") 16 | assert live_render.renderable == "another string" 17 | 18 | 19 | def test_position_cursor(live_render): 20 | assert str(live_render.position_cursor()) == "" 21 | live_render._shape = (80, 2) 22 | assert str(live_render.position_cursor()) == "\r\x1b[2K\x1b[1A\x1b[2K" 23 | 24 | 25 | def test_restore_cursor(live_render): 26 | assert str(live_render.restore_cursor()) == "" 27 | live_render._shape = (80, 2) 28 | assert str(live_render.restore_cursor()) == "\r\x1b[1A\x1b[2K\x1b[1A\x1b[2K" 29 | 30 | 31 | def test_rich_console(live_render): 32 | options = ConsoleOptions( 33 | ConsoleDimensions(80, 25), 34 | max_height=25, 35 | legacy_windows=False, 36 | min_width=10, 37 | max_width=20, 38 | is_terminal=False, 39 | encoding="utf-8", 40 | ) 41 | rich_console = live_render.__rich_console__(Console(), options) 42 | assert [Segment("my string", None)] == list(rich_console) 43 | live_render.style = "red" 44 | rich_console = live_render.__rich_console__(Console(), options) 45 | assert [Segment("my string", Style.parse("red"))] == list(rich_console) 46 | -------------------------------------------------------------------------------- /tests/test_log.py: -------------------------------------------------------------------------------- 1 | # encoding=utf-8 2 | 3 | 4 | import io 5 | import re 6 | 7 | from rich.console import Console 8 | 9 | re_link_ids = re.compile(r"id=[\d\.\-]*?;.*?\x1b") 10 | 11 | 12 | def replace_link_ids(render: str) -> str: 13 | """Link IDs have a random ID and system path which is a problem for 14 | reproducible tests. 15 | 16 | """ 17 | return re_link_ids.sub("id=0;foo\x1b", render) 18 | 19 | 20 | test_data = [1, 2, 3] 21 | 22 | 23 | def render_log(): 24 | console = Console( 25 | file=io.StringIO(), 26 | width=80, 27 | force_terminal=True, 28 | log_time_format="[TIME]", 29 | color_system="truecolor", 30 | legacy_windows=False, 31 | ) 32 | console.log() 33 | console.log("Hello from", console, "!") 34 | console.log(test_data, log_locals=True) 35 | return replace_link_ids(console.file.getvalue()).replace("test_log.py", "source.py") 36 | 37 | 38 | def test_log(): 39 | expected = replace_link_ids( 40 | "\x1b[2;36m[TIME]\x1b[0m\x1b[2;36m \x1b[0m \x1b]8;id=0;foo\x1b\\\x1b[2msource.py\x1b[0m\x1b]8;;\x1b\\\x1b[2m:\x1b[0m\x1b]8;id=0;foo\x1b\\\x1b[2m32\x1b[0m\x1b]8;;\x1b\\\n\x1b[2;36m \x1b[0m\x1b[2;36m \x1b[0mHello from \x1b[1m<\x1b[0m\x1b[1;95mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;36m80\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m ! \x1b]8;id=0;foo\x1b\\\x1b[2msource.py\x1b[0m\x1b]8;;\x1b\\\x1b[2m:\x1b[0m\x1b]8;id=0;foo\x1b\\\x1b[2m33\x1b[0m\x1b]8;;\x1b\\\n\x1b[2;36m \x1b[0m\x1b[2;36m \x1b[0m\x1b[1m[\x1b[0m\x1b[1;36m1\x1b[0m, \x1b[1;36m2\x1b[0m, \x1b[1;36m3\x1b[0m\x1b[1m]\x1b[0m \x1b]8;id=0;foo\x1b\\\x1b[2msource.py\x1b[0m\x1b]8;;\x1b\\\x1b[2m:\x1b[0m\x1b]8;id=0;foo\x1b\\\x1b[2m34\x1b[0m\x1b]8;;\x1b\\\n\x1b[2;36m \x1b[0m\x1b[34m╭─\x1b[0m\x1b[34m─────────────────────\x1b[0m\x1b[34m \x1b[0m\x1b[3;34mlocals\x1b[0m\x1b[34m \x1b[0m\x1b[34m─────────────────────\x1b[0m\x1b[34m─╮\x1b[0m \x1b[2m \x1b[0m\n\x1b[2;36m \x1b[0m\x1b[34m│\x1b[0m \x1b[3;33mconsole\x1b[0m\x1b[31m =\x1b[0m \x1b[1m<\x1b[0m\x1b[1;95mconsole\x1b[0m\x1b[39m \x1b[0m\x1b[33mwidth\x1b[0m\x1b[39m=\x1b[0m\x1b[1;36m80\x1b[0m\x1b[39m ColorSystem.TRUECOLOR\x1b[0m\x1b[1m>\x1b[0m \x1b[34m│\x1b[0m \x1b[2m \x1b[0m\n\x1b[2;36m \x1b[0m\x1b[34m╰────────────────────────────────────────────────────╯\x1b[0m \x1b[2m \x1b[0m\n" 41 | ) 42 | rendered = render_log() 43 | print(repr(rendered)) 44 | assert rendered == expected 45 | 46 | 47 | def test_log_caller_frame_info(): 48 | for i in range(2): 49 | assert Console._caller_frame_info(i) == Console._caller_frame_info( 50 | i, lambda: None 51 | ) 52 | 53 | 54 | def test_justify(): 55 | console = Console(width=20, log_path=False, log_time=False, color_system=None) 56 | console.begin_capture() 57 | console.log("foo", justify="right") 58 | result = console.end_capture() 59 | assert result == " foo\n" 60 | 61 | 62 | if __name__ == "__main__": 63 | render = render_log() 64 | print(render) 65 | print(repr(render)) 66 | -------------------------------------------------------------------------------- /tests/test_measure.py: -------------------------------------------------------------------------------- 1 | from rich.text import Text 2 | import pytest 3 | 4 | from rich.errors import NotRenderableError 5 | from rich.console import Console 6 | from rich.measure import Measurement, measure_renderables 7 | 8 | 9 | def test_span(): 10 | measurement = Measurement(10, 100) 11 | assert measurement.span == 90 12 | 13 | 14 | def test_no_renderable(): 15 | console = Console() 16 | text = Text() 17 | 18 | with pytest.raises(NotRenderableError): 19 | Measurement.get(console, console.options, None) 20 | 21 | 22 | def test_measure_renderables(): 23 | console = Console() 24 | assert measure_renderables(console, console.options, "") == Measurement(0, 0) 25 | assert measure_renderables( 26 | console, console.options.update_width(0), "hello" 27 | ) == Measurement(0, 0) 28 | 29 | 30 | def test_clamp(): 31 | measurement = Measurement(20, 100) 32 | assert measurement.clamp(10, 50) == Measurement(20, 50) 33 | assert measurement.clamp(30, 50) == Measurement(30, 50) 34 | assert measurement.clamp(None, 50) == Measurement(20, 50) 35 | assert measurement.clamp(30, None) == Measurement(30, 100) 36 | assert measurement.clamp(None, None) == Measurement(20, 100) 37 | -------------------------------------------------------------------------------- /tests/test_null_file.py: -------------------------------------------------------------------------------- 1 | from rich._null_file import NullFile 2 | 3 | 4 | def test_null_file(): 5 | file = NullFile() 6 | with file: 7 | assert file.write("abc") == 0 8 | assert file.close() is None 9 | assert not file.isatty() 10 | assert file.read() == "" 11 | assert not file.readable() 12 | assert file.readline() == "" 13 | assert file.readlines() == [] 14 | assert file.seek(0, 0) == 0 15 | assert not file.seekable() 16 | assert file.tell() == 0 17 | assert file.truncate() == 0 18 | assert file.writable() == False 19 | assert file.writelines([""]) is None 20 | assert next(file) == "" 21 | assert next(iter(file)) == "" 22 | assert file.fileno() == -1 23 | assert file.flush() is None 24 | -------------------------------------------------------------------------------- /tests/test_padding.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from rich.padding import Padding 4 | from rich.console import Console, ConsoleDimensions, ConsoleOptions 5 | from rich.style import Style 6 | from rich.segment import Segment 7 | 8 | 9 | def test_repr(): 10 | padding = Padding("foo", (1, 2)) 11 | assert isinstance(repr(padding), str) 12 | 13 | 14 | def test_indent(): 15 | indent_result = Padding.indent("test", 4) 16 | assert indent_result.top == 0 17 | assert indent_result.right == 0 18 | assert indent_result.bottom == 0 19 | assert indent_result.left == 4 20 | 21 | 22 | def test_unpack(): 23 | assert Padding.unpack(3) == (3, 3, 3, 3) 24 | assert Padding.unpack((3,)) == (3, 3, 3, 3) 25 | assert Padding.unpack((3, 4)) == (3, 4, 3, 4) 26 | assert Padding.unpack((3, 4, 5, 6)) == (3, 4, 5, 6) 27 | with pytest.raises(ValueError): 28 | Padding.unpack((1, 2, 3)) 29 | 30 | 31 | def test_expand_false(): 32 | console = Console(width=100, color_system=None) 33 | console.begin_capture() 34 | console.print(Padding("foo", 1, expand=False)) 35 | assert console.end_capture() == " \n foo \n \n" 36 | 37 | 38 | def test_rich_console(): 39 | renderable = "test renderable" 40 | style = Style(color="red") 41 | options = ConsoleOptions( 42 | ConsoleDimensions(80, 25), 43 | max_height=25, 44 | legacy_windows=False, 45 | min_width=10, 46 | max_width=20, 47 | is_terminal=False, 48 | encoding="utf-8", 49 | ) 50 | 51 | expected_outputs = [ 52 | Segment(renderable, style=style), 53 | Segment(" " * (20 - len(renderable)), style=style), 54 | Segment("\n", style=None), 55 | ] 56 | padding_generator = Padding(renderable, style=style).__rich_console__( 57 | Console(), options 58 | ) 59 | for output, expected in zip(padding_generator, expected_outputs): 60 | assert output == expected 61 | -------------------------------------------------------------------------------- /tests/test_palette.py: -------------------------------------------------------------------------------- 1 | from rich._palettes import STANDARD_PALETTE 2 | from rich.table import Table 3 | 4 | 5 | def test_rich_cast(): 6 | table = STANDARD_PALETTE.__rich__() 7 | assert isinstance(table, Table) 8 | assert table.row_count == 16 9 | -------------------------------------------------------------------------------- /tests/test_pick.py: -------------------------------------------------------------------------------- 1 | from rich._pick import pick_bool 2 | 3 | 4 | def test_pick_bool(): 5 | assert pick_bool(False) == False 6 | assert pick_bool(True) == True 7 | assert pick_bool(None) == False 8 | assert pick_bool(False, True) == False 9 | assert pick_bool(None, True) == True 10 | assert pick_bool(True, None) == True 11 | assert pick_bool(False, None) == False 12 | assert pick_bool(None, None) == False 13 | assert pick_bool(None, None, False, True) == False 14 | assert pick_bool(None, None, True, False) == True 15 | -------------------------------------------------------------------------------- /tests/test_protocol.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | from rich.abc import RichRenderable 4 | from rich.console import Console 5 | from rich.panel import Panel 6 | from rich.text import Text 7 | 8 | 9 | class Foo: 10 | def __rich__(self) -> Text: 11 | return Text("Foo") 12 | 13 | 14 | def test_rich_cast(): 15 | foo = Foo() 16 | console = Console(file=io.StringIO()) 17 | console.print(foo) 18 | assert console.file.getvalue() == "Foo\n" 19 | 20 | 21 | class Fake: 22 | def __getattr__(self, name): 23 | return 12 24 | 25 | def __repr__(self) -> str: 26 | return "Fake()" 27 | 28 | 29 | def test_rich_cast_fake(): 30 | fake = Fake() 31 | console = Console(file=io.StringIO()) 32 | console.print(fake) 33 | assert console.file.getvalue() == "Fake()\n" 34 | 35 | 36 | def test_rich_cast_container(): 37 | foo = Foo() 38 | console = Console(file=io.StringIO(), legacy_windows=False) 39 | console.print(Panel.fit(foo, padding=0)) 40 | assert console.file.getvalue() == "╭───╮\n│Foo│\n╰───╯\n" 41 | 42 | 43 | def test_abc(): 44 | foo = Foo() 45 | assert isinstance(foo, RichRenderable) 46 | assert isinstance(Text("hello"), RichRenderable) 47 | assert isinstance(Panel("hello"), RichRenderable) 48 | assert not isinstance(foo, str) 49 | assert not isinstance("foo", RichRenderable) 50 | assert not isinstance([], RichRenderable) 51 | 52 | 53 | def test_cast_deep(): 54 | class B: 55 | def __rich__(self) -> Foo: 56 | return Foo() 57 | 58 | class A: 59 | def __rich__(self) -> B: 60 | return B() 61 | 62 | console = Console(file=io.StringIO()) 63 | console.print(A()) 64 | assert console.file.getvalue() == "Foo\n" 65 | 66 | 67 | def test_cast_recursive(): 68 | class B: 69 | def __rich__(self) -> "A": 70 | return A() 71 | 72 | def __repr__(self) -> str: 73 | return "" 74 | 75 | class A: 76 | def __rich__(self) -> B: 77 | return B() 78 | 79 | def __repr__(self) -> str: 80 | return "" 81 | 82 | console = Console(file=io.StringIO()) 83 | console.print(A()) 84 | assert console.file.getvalue() == "\n" 85 | -------------------------------------------------------------------------------- /tests/test_ratio.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from typing import NamedTuple, Optional 3 | 4 | from rich._ratio import ratio_reduce, ratio_resolve 5 | 6 | 7 | class Edge(NamedTuple): 8 | size: Optional[int] = None 9 | ratio: int = 1 10 | minimum_size: int = 1 11 | 12 | 13 | @pytest.mark.parametrize( 14 | "total,ratios,maximums,values,result", 15 | [ 16 | (20, [2, 4], [20, 20], [5, 5], [-2, -8]), 17 | (20, [2, 4], [1, 1], [5, 5], [4, 4]), 18 | (20, [2, 4], [1, 1], [2, 2], [1, 1]), 19 | (3, [2, 4], [3, 3], [2, 2], [1, 0]), 20 | (3, [2, 4], [3, 3], [0, 0], [-1, -2]), 21 | (3, [0, 0], [3, 3], [4, 4], [4, 4]), 22 | ], 23 | ) 24 | def test_ratio_reduce(total, ratios, maximums, values, result): 25 | assert ratio_reduce(total, ratios, maximums, values) == result 26 | 27 | 28 | def test_ratio_resolve(): 29 | assert ratio_resolve(100, []) == [] 30 | assert ratio_resolve(100, [Edge(size=100), Edge(ratio=1)]) == [100, 1] 31 | assert ratio_resolve(100, [Edge(ratio=1)]) == [100] 32 | assert ratio_resolve(100, [Edge(ratio=1), Edge(ratio=1)]) == [50, 50] 33 | assert ratio_resolve(100, [Edge(size=20), Edge(ratio=1), Edge(ratio=1)]) == [ 34 | 20, 35 | 40, 36 | 40, 37 | ] 38 | assert ratio_resolve(100, [Edge(size=40), Edge(ratio=2), Edge(ratio=1)]) == [ 39 | 40, 40 | 40, 41 | 20, 42 | ] 43 | assert ratio_resolve( 44 | 100, [Edge(size=40), Edge(ratio=2), Edge(ratio=1, minimum_size=25)] 45 | ) == [40, 35, 25] 46 | assert ratio_resolve(100, [Edge(ratio=1), Edge(ratio=1), Edge(ratio=1)]) == [ 47 | 33, 48 | 33, 49 | 34, 50 | ] 51 | assert ratio_resolve( 52 | 50, [Edge(size=30), Edge(ratio=1, minimum_size=10), Edge(size=30)] 53 | ) == [30, 10, 30] 54 | assert ratio_resolve(110, [Edge(ratio=1), Edge(ratio=1), Edge(ratio=1)]) == [ 55 | 36, 56 | 37, 57 | 37, 58 | ] 59 | -------------------------------------------------------------------------------- /tests/test_rich_print.py: -------------------------------------------------------------------------------- 1 | import io 2 | import json 3 | 4 | import rich 5 | from rich.console import Console 6 | 7 | 8 | def test_get_console(): 9 | console = rich.get_console() 10 | assert isinstance(console, Console) 11 | 12 | 13 | def test_reconfigure_console(): 14 | rich.reconfigure(width=100) 15 | assert rich.get_console().width == 100 16 | 17 | 18 | def test_rich_print(): 19 | console = rich.get_console() 20 | output = io.StringIO() 21 | backup_file = console.file 22 | try: 23 | console.file = output 24 | rich.print("foo", "bar") 25 | rich.print("foo\n") 26 | rich.print("foo\n\n") 27 | assert output.getvalue() == "foo bar\nfoo\n\nfoo\n\n\n" 28 | finally: 29 | console.file = backup_file 30 | 31 | 32 | def test_rich_print_json(): 33 | console = rich.get_console() 34 | with console.capture() as capture: 35 | rich.print_json('[false, true, null, "foo"]', indent=4) 36 | result = capture.get() 37 | print(repr(result)) 38 | expected = '[\n false,\n true,\n null,\n "foo"\n]\n' 39 | assert result == expected 40 | 41 | 42 | def test_rich_print_json_round_trip(): 43 | data = ["x" * 100, 2e128] 44 | console = rich.get_console() 45 | with console.capture() as capture: 46 | rich.print_json(data=data, indent=4) 47 | result = capture.get() 48 | print(repr(result)) 49 | result_data = json.loads(result) 50 | assert result_data == data 51 | 52 | 53 | def test_rich_print_json_no_truncation(): 54 | console = rich.get_console() 55 | with console.capture() as capture: 56 | rich.print_json(f'["{"x" * 100}", {int(2e128)}]', indent=4) 57 | result = capture.get() 58 | print(repr(result)) 59 | assert ("x" * 100) in result 60 | assert str(int(2e128)) in result 61 | 62 | 63 | def test_rich_print_X(): 64 | console = rich.get_console() 65 | output = io.StringIO() 66 | backup_file = console.file 67 | try: 68 | console.file = output 69 | rich.print("foo") 70 | rich.print("fooX") 71 | rich.print("fooXX") 72 | assert output.getvalue() == "foo\nfooX\nfooXX\n" 73 | finally: 74 | console.file = backup_file 75 | -------------------------------------------------------------------------------- /tests/test_rule_in_table.py: -------------------------------------------------------------------------------- 1 | import io 2 | from textwrap import dedent 3 | 4 | import pytest 5 | 6 | from rich import box 7 | from rich.console import Console 8 | from rich.rule import Rule 9 | from rich.table import Table 10 | 11 | 12 | @pytest.mark.parametrize("expand_kwarg", ({}, {"expand": False})) 13 | def test_rule_in_unexpanded_table(expand_kwarg): 14 | console = Console(width=32, file=io.StringIO(), legacy_windows=False, _environ={}) 15 | table = Table(box=box.ASCII, show_header=False, **expand_kwarg) 16 | table.add_column() 17 | table.add_column() 18 | table.add_row("COL1", "COL2") 19 | table.add_row("COL1", Rule()) 20 | table.add_row("COL1", "COL2") 21 | console.print(table) 22 | expected = dedent( 23 | """\ 24 | +-------------+ 25 | | COL1 | COL2 | 26 | | COL1 | ──── | 27 | | COL1 | COL2 | 28 | +-------------+ 29 | """ 30 | ) 31 | result = console.file.getvalue() 32 | assert result == expected 33 | 34 | 35 | def test_rule_in_expanded_table(): 36 | console = Console(width=32, file=io.StringIO(), legacy_windows=False, _environ={}) 37 | table = Table(box=box.ASCII, expand=True, show_header=False) 38 | table.add_column() 39 | table.add_column() 40 | table.add_row("COL1", "COL2") 41 | table.add_row("COL1", Rule(style=None)) 42 | table.add_row("COL1", "COL2") 43 | console.print(table) 44 | expected = dedent( 45 | """\ 46 | +------------------------------+ 47 | | COL1 | COL2 | 48 | | COL1 | ──────────── | 49 | | COL1 | COL2 | 50 | +------------------------------+ 51 | """ 52 | ) 53 | result = console.file.getvalue() 54 | assert result == expected 55 | 56 | 57 | def test_rule_in_ratio_table(): 58 | console = Console(width=32, file=io.StringIO(), legacy_windows=False, _environ={}) 59 | table = Table(box=box.ASCII, expand=True, show_header=False) 60 | table.add_column(ratio=1) 61 | table.add_column() 62 | table.add_row("COL1", "COL2") 63 | table.add_row("COL1", Rule(style=None)) 64 | table.add_row("COL1", "COL2") 65 | console.print(table) 66 | expected = dedent( 67 | """\ 68 | +------------------------------+ 69 | | COL1 | COL2 | 70 | | COL1 | ──── | 71 | | COL1 | COL2 | 72 | +------------------------------+ 73 | """ 74 | ) 75 | result = console.file.getvalue() 76 | assert result == expected 77 | -------------------------------------------------------------------------------- /tests/test_screen.py: -------------------------------------------------------------------------------- 1 | from rich.console import Console 2 | from rich.screen import Screen 3 | 4 | 5 | def test_screen(): 6 | console = Console(color_system=None, width=20, height=5, legacy_windows=False) 7 | with console.capture() as capture: 8 | console.print(Screen("foo\nbar\nbaz\nfoo\nbar\nbaz\foo")) 9 | result = capture.get() 10 | print(repr(result)) 11 | expected = "foo \nbar \nbaz \nfoo \nbar " 12 | assert result == expected 13 | -------------------------------------------------------------------------------- /tests/test_spinner.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from rich.console import Console 4 | from rich.measure import Measurement 5 | from rich.rule import Rule 6 | from rich.spinner import Spinner 7 | from rich.text import Text 8 | 9 | 10 | def test_spinner_create(): 11 | Spinner("dots") 12 | with pytest.raises(KeyError): 13 | Spinner("foobar") 14 | 15 | 16 | def test_spinner_render(): 17 | time = 0.0 18 | 19 | def get_time(): 20 | nonlocal time 21 | return time 22 | 23 | console = Console( 24 | width=80, color_system=None, force_terminal=True, get_time=get_time 25 | ) 26 | console.begin_capture() 27 | spinner = Spinner("dots", "Foo") 28 | console.print(spinner) 29 | time += 80 / 1000 30 | console.print(spinner) 31 | result = console.end_capture() 32 | print(repr(result)) 33 | expected = "⠋ Foo\n⠙ Foo\n" 34 | assert result == expected 35 | 36 | 37 | def test_spinner_update(): 38 | time = 0.0 39 | 40 | def get_time(): 41 | nonlocal time 42 | return time 43 | 44 | console = Console(width=20, force_terminal=True, get_time=get_time, _environ={}) 45 | console.begin_capture() 46 | spinner = Spinner("dots") 47 | console.print(spinner) 48 | 49 | rule = Rule("Bar") 50 | 51 | spinner.update(text=rule) 52 | time += 80 / 1000 53 | console.print(spinner) 54 | 55 | result = console.end_capture() 56 | print(repr(result)) 57 | expected = "⠋\n⠙ \x1b[92m─\x1b[0m\n" 58 | assert result == expected 59 | 60 | 61 | def test_rich_measure(): 62 | console = Console(width=80, color_system=None, force_terminal=True) 63 | spinner = Spinner("dots", "Foo") 64 | min_width, max_width = Measurement.get(console, console.options, spinner) 65 | assert min_width == 3 66 | assert max_width == 5 67 | 68 | 69 | def test_spinner_markup(): 70 | spinner = Spinner("dots", "[bold]spinning[/bold]") 71 | assert isinstance(spinner.text, Text) 72 | assert str(spinner.text) == "spinning" 73 | -------------------------------------------------------------------------------- /tests/test_stack.py: -------------------------------------------------------------------------------- 1 | from rich._stack import Stack 2 | 3 | 4 | def test_stack(): 5 | stack = Stack() 6 | stack.push("foo") 7 | stack.push("bar") 8 | assert stack.top == "bar" 9 | assert stack.pop() == "bar" 10 | assert stack.top == "foo" 11 | -------------------------------------------------------------------------------- /tests/test_status.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | from rich.console import Console 4 | from rich.spinner import Spinner 5 | from rich.status import Status 6 | 7 | 8 | def test_status(): 9 | console = Console( 10 | color_system=None, width=80, legacy_windows=False, get_time=lambda: 0.0 11 | ) 12 | status = Status("foo", console=console) 13 | assert status.console == console 14 | previous_status_renderable = status.renderable 15 | status.update(status="bar", spinner_style="red", speed=2.0) 16 | 17 | assert previous_status_renderable == status.renderable 18 | assert isinstance(status.renderable, Spinner) 19 | status.update(spinner="dots2") 20 | assert previous_status_renderable != status.renderable 21 | 22 | # TODO: Testing output is tricky with threads 23 | with status: 24 | sleep(0.2) 25 | 26 | 27 | def test_renderable(): 28 | console = Console( 29 | color_system=None, width=80, legacy_windows=False, get_time=lambda: 0.0 30 | ) 31 | status = Status("foo", console=console) 32 | console.begin_capture() 33 | console.print(status) 34 | assert console.end_capture() == "⠋ foo\n" 35 | -------------------------------------------------------------------------------- /tests/test_styled.py: -------------------------------------------------------------------------------- 1 | import io 2 | 3 | from rich.console import Console 4 | from rich.measure import Measurement 5 | from rich.styled import Styled 6 | 7 | 8 | def test_styled(): 9 | styled_foo = Styled("foo", "on red") 10 | console = Console(file=io.StringIO(), force_terminal=True, _environ={}) 11 | assert Measurement.get(console, console.options, styled_foo) == Measurement(3, 3) 12 | console.print(styled_foo) 13 | result = console.file.getvalue() 14 | expected = "\x1b[41mfoo\x1b[0m\n" 15 | assert result == expected 16 | -------------------------------------------------------------------------------- /tests/test_theme.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import tempfile 4 | 5 | import pytest 6 | 7 | from rich.style import Style 8 | from rich.theme import Theme, ThemeStack, ThemeStackError 9 | 10 | 11 | def test_inherit(): 12 | theme = Theme({"warning": "red"}) 13 | assert theme.styles["warning"] == Style(color="red") 14 | assert theme.styles["dim"] == Style(dim=True) 15 | 16 | 17 | def test_config(): 18 | theme = Theme({"warning": "red"}) 19 | config = theme.config 20 | assert "warning = red\n" in config 21 | 22 | 23 | def test_from_file(): 24 | theme = Theme({"warning": "red"}) 25 | text_file = io.StringIO() 26 | text_file.write(theme.config) 27 | text_file.seek(0) 28 | 29 | load_theme = Theme.from_file(text_file) 30 | assert theme.styles == load_theme.styles 31 | 32 | 33 | def test_read(): 34 | theme = Theme({"warning": "red"}) 35 | with tempfile.TemporaryDirectory("richtheme") as name: 36 | filename = os.path.join(name, "theme.cfg") 37 | with open(filename, "wt") as write_theme: 38 | write_theme.write(theme.config) 39 | load_theme = Theme.read(filename) 40 | assert theme.styles == load_theme.styles 41 | 42 | 43 | def test_theme_stack(): 44 | theme = Theme({"warning": "red"}) 45 | stack = ThemeStack(theme) 46 | assert stack.get("warning") == Style.parse("red") 47 | new_theme = Theme({"warning": "bold yellow"}) 48 | stack.push_theme(new_theme) 49 | assert stack.get("warning") == Style.parse("bold yellow") 50 | stack.pop_theme() 51 | assert stack.get("warning") == Style.parse("red") 52 | with pytest.raises(ThemeStackError): 53 | stack.pop_theme() 54 | -------------------------------------------------------------------------------- /tests/test_tools.py: -------------------------------------------------------------------------------- 1 | from rich._loop import loop_first, loop_last, loop_first_last 2 | from rich._ratio import ratio_distribute 3 | 4 | 5 | def test_loop_first(): 6 | assert list(loop_first([])) == [] 7 | iterable = loop_first(["apples", "oranges", "pears", "lemons"]) 8 | assert next(iterable) == (True, "apples") 9 | assert next(iterable) == (False, "oranges") 10 | assert next(iterable) == (False, "pears") 11 | assert next(iterable) == (False, "lemons") 12 | 13 | 14 | def test_loop_last(): 15 | assert list(loop_last([])) == [] 16 | iterable = loop_last(["apples", "oranges", "pears", "lemons"]) 17 | assert next(iterable) == (False, "apples") 18 | assert next(iterable) == (False, "oranges") 19 | assert next(iterable) == (False, "pears") 20 | assert next(iterable) == (True, "lemons") 21 | 22 | 23 | def test_loop_first_last(): 24 | assert list(loop_first_last([])) == [] 25 | iterable = loop_first_last(["apples", "oranges", "pears", "lemons"]) 26 | assert next(iterable) == (True, False, "apples") 27 | assert next(iterable) == (False, False, "oranges") 28 | assert next(iterable) == (False, False, "pears") 29 | assert next(iterable) == (False, True, "lemons") 30 | 31 | 32 | def test_ratio_distribute(): 33 | assert ratio_distribute(10, [1]) == [10] 34 | assert ratio_distribute(10, [1, 1]) == [5, 5] 35 | assert ratio_distribute(12, [1, 3]) == [3, 9] 36 | assert ratio_distribute(0, [1, 3]) == [0, 0] 37 | assert ratio_distribute(0, [1, 3], [1, 1]) == [1, 1] 38 | assert ratio_distribute(10, [1, 0]) == [10, 0] 39 | -------------------------------------------------------------------------------- /tools/README.md: -------------------------------------------------------------------------------- 1 | # Tools 2 | 3 | These are scripts used in the development of Rich, and aren't for general use. But feel free to look around. 4 | 5 | Some ~~strikethrough~~ text. 6 | -------------------------------------------------------------------------------- /tools/make_emoji.py: -------------------------------------------------------------------------------- 1 | try: 2 | import emoji 3 | except ImportError: 4 | print("pip install emoji") 5 | raise 6 | 7 | from emoji.unicode_codes import EMOJI_ALIAS_UNICODE 8 | 9 | emoji = {k.lower().strip(":"): v for k, v in EMOJI_ALIAS_UNICODE.items()} 10 | 11 | with open("_emoji_codes.py", "wt") as f: 12 | f.write("EMOJI=" + str(emoji)) 13 | -------------------------------------------------------------------------------- /tools/make_terminal_widths.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from typing import List, Tuple 3 | import sys 4 | 5 | from rich.progress import Progress 6 | 7 | from wcwidth import wcwidth 8 | 9 | 10 | progress = Progress() 11 | 12 | 13 | def make_widths_table() -> List[Tuple[int, int, int]]: 14 | table: List[Tuple[int, int, int]] = [] 15 | append = table.append 16 | 17 | make_table_task = progress.add_task("Calculating table...") 18 | 19 | widths = ( 20 | (codepoint, wcwidth(chr(codepoint))) 21 | for codepoint in range(0, sys.maxunicode + 1) 22 | ) 23 | 24 | _widths = [(codepoint, width) for codepoint, width in widths if width != 1] 25 | iter_widths = iter(_widths) 26 | 27 | endpoint, group_cell_size = next(iter_widths) 28 | start_codepoint = end_codepoint = endpoint 29 | for codepoint, cell_size in progress.track( 30 | iter_widths, task_id=make_table_task, total=len(_widths) - 1 31 | ): 32 | if cell_size != group_cell_size or codepoint != end_codepoint + 1: 33 | append((start_codepoint, end_codepoint, group_cell_size)) 34 | start_codepoint = end_codepoint = codepoint 35 | group_cell_size = cell_size 36 | else: 37 | end_codepoint = codepoint 38 | append((start_codepoint, end_codepoint, group_cell_size)) 39 | return table 40 | 41 | 42 | def get_cell_size(table: List[Tuple[int, int, int]], character: str) -> int: 43 | codepoint = ord(character) 44 | lower_bound = 0 45 | upper_bound = len(table) - 1 46 | index = (lower_bound + upper_bound) // 2 47 | while True: 48 | start, end, width = table[index] 49 | if codepoint < start: 50 | upper_bound = index - 1 51 | elif codepoint > end: 52 | lower_bound = index + 1 53 | else: 54 | return width 55 | if upper_bound < lower_bound: 56 | break 57 | index = (lower_bound + upper_bound) // 2 58 | return 1 59 | 60 | 61 | def test(widths_table): 62 | for codepoint in progress.track( 63 | range(0, sys.maxunicode + 1), description="Testing..." 64 | ): 65 | character = chr(codepoint) 66 | width1 = get_cell_size(widths_table, character) 67 | width2 = wcwidth(character) 68 | if width1 != width2: 69 | print(f"{width1} != {width2}") 70 | break 71 | 72 | 73 | def run(): 74 | with progress: 75 | widths_table = make_widths_table() 76 | test(widths_table) 77 | table_file = f"""# Auto generated by make_terminal_widths.py 78 | 79 | CELL_WIDTHS = {widths_table!r} 80 | 81 | """ 82 | with open("../rich/_cell_widths.py", "wt") as fh: 83 | fh.write(table_file) 84 | 85 | subprocess.run("black ../rich/_cell_widths.py", shell=True) 86 | 87 | 88 | if __name__ == "__main__": 89 | run() 90 | -------------------------------------------------------------------------------- /tools/movies.md: -------------------------------------------------------------------------------- 1 | # Top 80's Movies 2 | 3 | The 1980's was an era that produced some of the most iconic movies of all time. This page provides a brief overview of some of the most popular movies from this decade. 4 | 5 | ## Box Office Hits 6 | 7 | Here are the top 5 highest-grossing films of the 1980s: 8 | 9 | | Year | Title | Director | Box Office (USD) | 10 | |------|:----------------------------------------------:|:------------------|------------------:| 11 | | 1982 | *E.T. the Extra-Terrestrial* | Steven Spielberg | $792.9 million | 12 | | 1980 | Star Wars: Episode V – The Empire Strikes Back | Irvin Kershner | $538.4 million | 13 | | 1983 | Star Wars: Episode VI – Return of the Jedi | Richard Marquand | $475.1 million | 14 | | 1981 | Raiders of the Lost Ark | Steven Spielberg | $389.9 million | 15 | | 1984 | Indiana Jones and the Temple of Doom | Steven Spielberg | $333.1 million | 16 | 17 | ## Oscar Winners 18 | 19 | The following table displays the films from the '80s that won the Oscar for Best Picture: 20 | 21 | | Year | Movie | Director | 22 | |------|---------------------|-----------------------| 23 | | 1980 | Ordinary People | Robert Redford | 24 | | 1981 | Chariots of Fire | Hugh Hudson | 25 | | 1982 | Gandhi | Richard Attenborough | 26 | | 1983 | Terms of Endearment | James L. Brooks | 27 | | 1984 | Amadeus | Milos Forman | 28 | | 1985 | Out of Africa | Sydney Pollack | 29 | | 1986 | Platoon | Oliver Stone | 30 | | 1987 | The Last Emperor | Bernardo Bertolucci | 31 | | 1988 | Rain Man | Barry Levinson | 32 | | 1989 | Driving Miss Daisy | Bruce Beresford | 33 | 34 | ## Cult Classics 35 | 36 | The 80s also produced a number of cult classics. Here are a few notable ones: 37 | 38 | | Year | Movie | Director | 39 | |------|--------------------|-----------------| 40 | | 1985 | The Goonies | Richard Donner | 41 | | 1988 | Beetlejuice | Tim Burton | 42 | | 1984 | Ghostbusters | Ivan Reitman | 43 | | 1985 | Back to the Future | Robert Zemeckis | 44 | | 1987 | The Princess Bride | Rob Reiner | 45 | -------------------------------------------------------------------------------- /tools/profile_divide.py: -------------------------------------------------------------------------------- 1 | from rich.segment import Segment 2 | 3 | text = """Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.""" 4 | 5 | 6 | segments = [Segment(text[n : n + 7]) for n in range(0, len(text), 7)] 7 | 8 | 9 | from time import perf_counter 10 | 11 | start = perf_counter() 12 | for _ in range(10000): 13 | list(Segment.divide(segments, [0, 1, 20, 24, 65, len(text)])) 14 | print(perf_counter() - start) 15 | -------------------------------------------------------------------------------- /tools/profile_pretty.py: -------------------------------------------------------------------------------- 1 | import json 2 | import io 3 | from time import time 4 | from rich.console import Console 5 | from rich.pretty import Pretty 6 | 7 | 8 | console = Console(file=io.StringIO(), color_system="truecolor", width=100) 9 | 10 | with open("cats.json") as fh: 11 | cats = json.load(fh) 12 | 13 | 14 | console.begin_capture() 15 | start = time() 16 | pretty = Pretty(cats) 17 | console.print(pretty, overflow="ignore", crop=False) 18 | result = console.end_capture() 19 | taken = (time() - start) * 1000 20 | print(result) 21 | 22 | print(console.file.getvalue()) 23 | print(f"{taken:.1f}") 24 | -------------------------------------------------------------------------------- /tools/stress_test_pretty.py: -------------------------------------------------------------------------------- 1 | from rich.console import Console 2 | from rich.panel import Panel 3 | from rich.pretty import Pretty 4 | from rich._timer import timer 5 | 6 | DATA = { 7 | "foo": [1, 2, 3, (), {}, (1, 2, 3), {4, 5, 6, (7, 8, 9)}, "Hello, World"], 8 | "bar": [None, (False, True)] * 2, 9 | "Dune": { 10 | "names": { 11 | "Paul Atreides", 12 | "Vladimir Harkonnen", 13 | "Thufir Hawat", 14 | "Duncan Idaho", 15 | } 16 | }, 17 | } 18 | console = Console() 19 | with timer("Stress test"): 20 | for w in range(130): 21 | console.print(Panel(Pretty(DATA, indent_guides=True), width=w)) 22 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 4.0.0 3 | envlist = 4 | lint 5 | docs 6 | py{38,39,310,311,312,313} 7 | isolated_build = True 8 | 9 | [testenv] 10 | description = Run unit-testing 11 | deps = 12 | poetry 13 | # do not put * in passenv as it may break builds due to reduced isolation 14 | passenv = 15 | CI 16 | GITHUB_* 17 | HOME 18 | PYTEST_* 19 | SSH_AUTH_SOCK 20 | TERM 21 | setenv = 22 | PYTHONDONTWRITEBYTECODE=1 23 | PYTHONUNBUFFERED=1 24 | commands = 25 | poetry install 26 | pytest --cov-report term-missing --cov=rich tests/ {posargs} 27 | 28 | [testenv:lint] 29 | description = Runs all linting tasks 30 | commands_pre = 31 | poetry install -vv --with lint 32 | commands = 33 | ; poetry install --only dev 34 | # as long GHA pipelines are not configured to use tox, we should call 35 | # `make` in order to make testing similar and prevent divergence. 36 | make format-check 37 | make typecheck 38 | deps = 39 | poetry 40 | skip_install = true 41 | allowlist_externals = 42 | make 43 | poetry 44 | 45 | [testenv:docs] 46 | description = Builds documentation 47 | changedir = docs 48 | deps = 49 | -r docs/requirements.txt 50 | commands = 51 | sphinx-build -M html source build 52 | --------------------------------------------------------------------------------