This commit is contained in:
Will McGugan 2020-01-11 16:13:39 +00:00
parent c9ce00abe0
commit e778af6aa2
31 changed files with 576 additions and 204 deletions

View file

@ -4,5 +4,6 @@ Appendix
.. toctree::
:maxdepth: 3
appendix/box.rst
appendix/colors.rst

View file

@ -0,0 +1,121 @@
.. _appendix-box:
Box
===
.. raw:: html
<pre style="font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span style="color: #800080"> box.ASCII </span>
+-------------------------+
|<span style="color: #7f7f7f; font-weight: bold"> </span>|<span style="color: #7f7f7f; font-weight: bold"> </span>|
|------------+------------|
|<span style="color: #7f7f7f"> </span>|<span style="color: #7f7f7f"> </span>|
|------------+------------|
|<span style="color: #7f7f7f; font-weight: bold"> </span>|<span style="color: #7f7f7f; font-weight: bold"> </span>|
+-------------------------+
<span style="color: #800080"> box.SQUARE </span>
┌────────────┬────────────┐
│<span style="color: #7f7f7f; font-weight: bold"> </span>│<span style="color: #7f7f7f; font-weight: bold"> </span>│
├────────────┼────────────┤
│<span style="color: #7f7f7f"> </span>│<span style="color: #7f7f7f"> </span>│
├────────────┼────────────┤
│<span style="color: #7f7f7f; font-weight: bold"> </span>│<span style="color: #7f7f7f; font-weight: bold"> </span>│
└────────────┴────────────┘
<span style="color: #800080"> box.MINIMAL </span>
<span style="color: #7f7f7f; font-weight: bold"> </span>│<span style="color: #7f7f7f; font-weight: bold"> </span>
────────────┼────────────
<span style="color: #7f7f7f"> </span>│<span style="color: #7f7f7f"> </span>
────────────┼────────────
<span style="color: #7f7f7f; font-weight: bold"> </span>│<span style="color: #7f7f7f; font-weight: bold"> </span>
<span style="color: #800080"> box.MINIMAL_HEAVY_HEAD </span>
<span style="color: #7f7f7f; font-weight: bold"> </span>│<span style="color: #7f7f7f; font-weight: bold"> </span>
━━━━━━━━━━━━┿━━━━━━━━━━━━
<span style="color: #7f7f7f"> </span>│<span style="color: #7f7f7f"> </span>
────────────┼────────────
<span style="color: #7f7f7f; font-weight: bold"> </span>│<span style="color: #7f7f7f; font-weight: bold"> </span>
<span style="color: #800080"> box.MINIMAL_DOUBLE_HEAD </span>
<span style="color: #7f7f7f; font-weight: bold"> </span>│<span style="color: #7f7f7f; font-weight: bold"> </span>
════════════╪════════════
<span style="color: #7f7f7f"> </span>│<span style="color: #7f7f7f"> </span>
────────────┼────────────
<span style="color: #7f7f7f; font-weight: bold"> </span>│<span style="color: #7f7f7f; font-weight: bold"> </span>
<span style="color: #800080"> box.SIMPLE </span>
<span style="color: #7f7f7f; font-weight: bold"> </span> <span style="color: #7f7f7f; font-weight: bold"> </span>
───────────────────────────
<span style="color: #7f7f7f"> </span> <span style="color: #7f7f7f"> </span>
───────────────────────────
<span style="color: #7f7f7f; font-weight: bold"> </span> <span style="color: #7f7f7f; font-weight: bold"> </span>
<span style="color: #800080"> box.SIMPLE_HEAVY </span>
<span style="color: #7f7f7f; font-weight: bold"> </span> <span style="color: #7f7f7f; font-weight: bold"> </span>
╺━━━━━━━━━━━━━━━━━━━━━━━━━╸
<span style="color: #7f7f7f"> </span> <span style="color: #7f7f7f"> </span>
╺━━━━━━━━━━━━━━━━━━━━━━━━━╸
<span style="color: #7f7f7f; font-weight: bold"> </span> <span style="color: #7f7f7f; font-weight: bold"> </span>
<span style="color: #800080"> box.HORIZONTALS </span>
───────────────────────────
<span style="color: #7f7f7f; font-weight: bold"> </span> <span style="color: #7f7f7f; font-weight: bold"> </span>
───────────────────────────
<span style="color: #7f7f7f"> </span> <span style="color: #7f7f7f"> </span>
───────────────────────────
<span style="color: #7f7f7f; font-weight: bold"> </span> <span style="color: #7f7f7f; font-weight: bold"> </span>
───────────────────────────
<span style="color: #800080"> box.ROUNDED </span>
╭────────────┬────────────╮
│<span style="color: #7f7f7f; font-weight: bold"> </span>│<span style="color: #7f7f7f; font-weight: bold"> </span>│
├────────────┼────────────┤
│<span style="color: #7f7f7f"> </span>│<span style="color: #7f7f7f"> </span>│
├────────────┼────────────┤
│<span style="color: #7f7f7f; font-weight: bold"> </span>│<span style="color: #7f7f7f; font-weight: bold"> </span>│
╰────────────┴────────────╯
<span style="color: #800080"> box.HEAVY </span>
┏━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃<span style="color: #7f7f7f; font-weight: bold"> </span>┃<span style="color: #7f7f7f; font-weight: bold"> </span>┃
┣━━━━━━━━━━━━╋━━━━━━━━━━━━┫
┃<span style="color: #7f7f7f"> </span>┃<span style="color: #7f7f7f"> </span>┃
┣━━━━━━━━━━━━╋━━━━━━━━━━━━┫
┃<span style="color: #7f7f7f; font-weight: bold"> </span>┃<span style="color: #7f7f7f; font-weight: bold"> </span>┃
┗━━━━━━━━━━━━┻━━━━━━━━━━━━┛
<span style="color: #800080"> box.HEAVY_EDGE </span>
┏━━━━━━━━━━━━┯━━━━━━━━━━━━┓
┃<span style="color: #7f7f7f; font-weight: bold"> </span>│<span style="color: #7f7f7f; font-weight: bold"> </span>┃
┠────────────┼────────────┨
┃<span style="color: #7f7f7f"> </span>│<span style="color: #7f7f7f"> </span>┃
┠────────────┼────────────┨
┃<span style="color: #7f7f7f; font-weight: bold"> </span>│<span style="color: #7f7f7f; font-weight: bold"> </span>┃
┗━━━━━━━━━━━━┷━━━━━━━━━━━━┛
<span style="color: #800080"> box.HEAVY_HEAD </span>
┏━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃<span style="color: #7f7f7f; font-weight: bold"> </span>┃<span style="color: #7f7f7f; font-weight: bold"> </span>┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│<span style="color: #7f7f7f"> </span>│<span style="color: #7f7f7f"> </span>│
├────────────┼────────────┤
│<span style="color: #7f7f7f; font-weight: bold"> </span>│<span style="color: #7f7f7f; font-weight: bold"> </span>│
└────────────┴────────────┘
<span style="color: #800080"> box.DOUBLE </span>
╔════════════╦════════════╗
║<span style="color: #7f7f7f; font-weight: bold"> </span>║<span style="color: #7f7f7f; font-weight: bold"> </span>║
╠════════════╬════════════╣
║<span style="color: #7f7f7f"> </span>║<span style="color: #7f7f7f"> </span>║
╠════════════╬════════════╣
║<span style="color: #7f7f7f; font-weight: bold"> </span>║<span style="color: #7f7f7f; font-weight: bold"> </span>║
╚════════════╩════════════╝
<span style="color: #800080"> box.DOUBLE_EDGE </span>
╔════════════╤════════════╗
║<span style="color: #7f7f7f; font-weight: bold"> </span>│<span style="color: #7f7f7f; font-weight: bold"> </span>║
╟────────────┼────────────╢
║<span style="color: #7f7f7f"> </span>│<span style="color: #7f7f7f"> </span>║
╟────────────┼────────────╢
║<span style="color: #7f7f7f; font-weight: bold"> </span>│<span style="color: #7f7f7f; font-weight: bold"> </span>║
╚════════════╧════════════╝
</pre>

View file

@ -1,11 +1,13 @@
Console API
===========
For more finely grained control over terminal formatting, Rich offers a :class:`~rich.console.Console` class. Most applications will require a single Console instance, so you may want to create one at the module level or as an attribute of your top-level object.
For complete control over terminal formatting, Rich offers a :class:`~rich.console.Console` class. Most applications will require a single Console instance, so you may want to create one at the module level or as an attribute of your top-level object. For example::
from rich.console import Console
console = Console()
The console object handles the mechanics of generating ANSI escape sequences for color and style. It will auto-detect the capabilities of the terminal and convert colors if necessary.
Attributes
----------
@ -15,9 +17,44 @@ The console will auto-detect a number of properties required when rendering.
* :obj:`~rich.console.Console.size` is the current dimensions of the terminal (which may change if you resize the window).
* :obj:`~rich.console.Console.encoding` is the default encoding (typically "utf-8").
* :obj:`~rich.console.Console.is_terminal` is a boolean that indicates if the Console instance is writing to a terminal or not.
* :obj:`~rich.console.Console.color_system` is a string containing "standard", "256" or "truecolor", or `None` if not writing to a terminal.
* :obj:`~rich.console.Console.color_system` is a string containing "standard", "256" or "truecolor", or ``None`` if not writing to a terminal.
Printing
--------
To write rich content to the terminal use the :meth:`~rich.console.Console.print` method. Rich will convert any object to a string via its (``__str__``) method and perform some simple syntax highlighting. It will also do pretty printing of any containers, such as dicts and lists. If you print a string it will render :ref:`console_markup`. Here are some examples::
console.print([1, 2, 3])
console.print("[blue underline]Looks like a link")
console.print(locals())
You can also use :meth:`~rich.console.Console.print` to render objects that support the :ref:`protocol`, which includes Rich's built in objects such as :class:`~rich.text.Text`, :class:`~rich.table.Table`, and :class:`~rich.syntax.Syntax` -- or other custom objects.
Logging
-------
The :meth:`~rich.console.Console.log` methods offers the same capabilities as print, but adds some features useful for debugging a running application. Logging writes the current time in a column to the left, and the file and line where the method was called to a column on the right. Here's an example::
>>> console.log("Hello, World!")
.. raw:: html
<pre style="font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span style="color: #7fbfbf">[16:32:08] </span>Hello, World! <span style="color: #7f7f7f">&lt;stdin&gt;:1</span>
</pre>
To help with debugging, the log() method has a ``log_locals`` parameter. If you set this to ``True`` Rich will display a table of local variables where the method was called.
Exporting
---------
The Console class can export anything written to it as either text or html. To enable exporting, first set ``record=True`` on the constructor. This tells Rich to save a copy of any data you ``print()`` or ``log()``. Here's an example::
from rich.console import Console
console = Console(record=True)
After you have written content, you can call :meth:`~rich.console.Console.export_text` or :meth:`~rich.console.Console.export_html` to get the console output as a string. You can also call :meth:`~rich.console.Console.save_text` or :meth:`~rich.console.Console.save_html` to write the contents directly to disk.
For examples of the html output generated by Rich Console, see :ref:`appendix-colors`.

View file

@ -14,6 +14,8 @@ Welcome to Rich's documentation!
console.rst
markup.rst
style.rst
tables.rst
protocol.rst
reference.rst
appendix.rst

View file

@ -1,9 +1,12 @@
.. _protocol:
Console Protocol
================
Rich supports a simple protocol to add rich formatting capabilities to custom objects, so you can :meth:`~rich.console.Console.print` your object with color, styles and formatting.
You can use this for presentation or to display additional debugging information that might be hard to parse from a ``__repr__`` output.
You can use this for presentation or to display additional debugging information that might be hard to parse from a typical ``__repr__`` string.
Console Str
@ -21,17 +24,45 @@ If you were to print or log an instance of ``MyObject`` it would render as ``MyO
Console Render
--------------
The ``__console_str__`` method is limited to styled text. For more advanced rendering, Rich supports a ``__console__`` method which you can use to render custom output with other renderable objects. For instance, a complex data type might be best represented as a :class:`~rich.table.Table`.
The ``__console_str__`` method is limited to styled text. For more advanced rendering, Rich supports a ``__console__`` method which you can use to generate custom output with other renderable objects. For instance, a complex data type might be best represented as a :class:`~rich.table.Table`.
The ``__console__`` method should accept a :class:`~rich.console.Console` and a :class:`~rich.console.ConsoleOptions` instance. It should return an iterable of other renderable objects. Although that means it *could* return a container such as a list, it is customary to ``yield`` output (making the method a generator)
Here's an example of a ``__console__`` method::
class MyObject:
@dataclass
class Student:
name: str
age: int
def __console__(self, console: Console, options: ConsoleOptions) -> Iterable[Table]:
my_table = Table("Key, "Value")
my_table.add_row("foo", "bar")
my_table.add_row("egg", "baz")
my_table = Table("Attribute, "Value")
my_table.add_row("name", self.name)
my_table.add_row("age", str(self.age))
yield my_table
If you were to print a ``MyObject`` instance, it would render a simple table to the terminal.
If you were to print a ``Student`` instance, it would render a simple table to the terminal.
Low Level Render
~~~~~~~~~~~~~~~~
For complete control over how a custom object is rendered to the terminal, you can yield :class:`~rich.segment.Segment` objects. A Segment consists of a piece of text and an optional Style. The following example, writes multi-colored text when rendering a ``MyObject`` instance::
class MyObject:
def __console__(self, console: Console, options: ConsoleOptions) -> Iterable[Table]:
yield Segment("My", "magenta")
yield Segment("Object", green")
yield Segment("()", "cyan")
Console Width
~~~~~~~~~~~~~
Sometimes Rich needs to know how many characters an object will take up when rendering. The :class:`~rich.table.Table` class for instance, will use this information to calculate the optimal dimensions for the columns. If you aren't using one of the standard classes, you will need to supply a ``__console_width__`` method which accepts the maximum width as an integer and returns a :class:`~rich.render_width.RenderWidth` object. The RenderWidth object should contain the *minimum* and *maximum* number of characters required to render.
For example, if we are rendering a chess board, it would require a minimum of 8 characters to render. The maximum can be left as the maximum available width (assuming a centered board)::
class ChessBoard:
def __console_width__(self, max_width: int) -> RenderWidth:
return RenderWidth(8, max_width)

View file

@ -8,5 +8,9 @@ Reference
reference/console.rst
reference/emoji.rst
reference/highlighter.rst
reference/render_width.rst
reference/panel.rst
reference/segment.rst
reference/style.rst
reference/table.rst
reference/text.rst

View file

@ -0,0 +1,6 @@
rich.panel
==========
.. automodule:: rich.panel
:members: Panel

View file

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

View file

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

View file

@ -0,0 +1,6 @@
rich.table
==========
.. automodule:: rich.table
:members: Table

45
docs/source/tables.rst Normal file
View file

@ -0,0 +1,45 @@
Tables
======
Rich's :class:`~rich.table.Table` class offers a variety ways of rendering tabular data to the terminal.
To render a table, construct a :class:`~rich.table.Table` object, add data, then call :meth:`~rich.console.Console.print` or :meth:`~rich.console.Console.log` to write it to the console.
Here's an example::
from rich.console import Console
from rich.table import Table
table = Table(title="Star Wars Movies")
table.add_column("Released", justify="right", style="cyan", no_wrap=True)
table.add_column("Title", style="magenta")
table.add_column("Box Office", justify="right", style="green")
table.add_row("Dec 20, 2019", "Star Wars: The Rise of Skywalker", "$952,110,690")
table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347")
table.add_row("Dec 15, 2017", "Star Wars Ep. V111: The Last Jedi", "$1,332,539,889")
table.add_row("Dec 16, 2016", "Rouge One: A Star Wars Story", "$1,332,439,889")
console = Console()
console.print(table)
This produces the following output:
.. raw:: html
<pre style="font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span style="font-style: italic"> Star Wars Movies </span>
┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
┃<span style="font-weight: bold"> Released </span>┃<span style="font-weight: bold"> Title </span>┃<span style="font-weight: bold"> Box Office </span>┃
┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━┩
│<span style="color: #008080"> Dec 20, 2019 </span>│<span style="color: #800080"> Star Wars: The Rise of Skywalker </span>│<span style="color: #008000"> $952,110,690 </span>│
│<span style="color: #008080"> May 25, 2018 </span>│<span style="color: #800080"> Solo: A Star Wars Story </span>│<span style="color: #008000"> $393,151,347 </span>│
│<span style="color: #008080"> Dec 15, 2017 </span>│<span style="color: #800080"> Star Wars Ep. V111: The Last Jedi </span>│<span style="color: #008000"> $1,332,539,889 </span>│
│<span style="color: #008080"> Dec 16, 2016 </span>│<span style="color: #800080"> Rouge One: A Star Wars Story </span>│<span style="color: #008000"> $1,332,439,889 </span>│
└──────────────┴───────────────────────────────────┴────────────────┘
</pre>
Rich is quite smart about rendering the table. It will adjust the columns widths to fit the contents and will wrap text if it doesn't fit. You can also add anything that Rich knows how to render as a title or row cell (even another table)!
The :class:`~rich.table.Table` class offers a number of configuration options to set the look and feel of the table, including how the borders are rendered, and the style and alignment of the columns.

View file

@ -25,18 +25,18 @@ console.push_styles(
)
console.log("Server starting...")
console.log("Serving on http://127.0.0.1:8000", highlight=None)
console.log("Serving on http://127.0.0.1:8000")
time.sleep(1)
console.log(
"HTTP GET /foo/bar/baz/egg.html 200 [0.57, 127.0.0.1:59076]",
highlight=RequestHighlighter(),
highlighter=RequestHighlighter(),
)
console.log(
"HTTP GET /foo/bar/baz/background.jpg 200 [0.57, 127.0.0.1:59076]",
highlight=RequestHighlighter(),
highlighter=RequestHighlighter(),
)

26
examples/table.html Normal file
View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<head>
<style>
body {
color: #000000;
background-color: #ffffff;
}
</style>
</head>
<html>
<body>
<code>
<pre style="font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><span style="font-style: italic"> Star Wars Movies </span>
┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┓
<span style="font-weight: bold"> Released </span><span style="font-weight: bold"> Title </span><span style="font-weight: bold"> Box Office </span>
┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━┩
<span style="color: #008080"> Dec 20, 2019 </span><span style="color: #800080"> Star Wars: The Rise of Skywalker </span><span style="color: #008000"> $952,110,690 </span>
<span style="color: #008080"> May 25, 2018 </span><span style="color: #800080"> Solo: A Star Wars Story </span><span style="color: #008000"> $393,151,347 </span>
<span style="color: #008080"> Dec 15, 2017 </span><span style="color: #800080"> Star Wars Ep. V111: The Last Jedi </span><span style="color: #008000"> $1,332,539,889 </span>
<span style="color: #008080"> Dec 16, 2016 </span><span style="color: #800080"> Rouge One: A Star Wars Story </span><span style="color: #008000"> $1,332,439,889 </span>
└──────────────┴───────────────────────────────────┴────────────────┘
</pre>
</code>
</body>
</html>

17
examples/table.py Normal file
View file

@ -0,0 +1,17 @@
from rich.console import Console
from rich.table import Table
table = Table(title="Star Wars Movies")
table.add_column("Released", justify="right", style="cyan", no_wrap=True)
table.add_column("Title", style="magenta")
table.add_column("Box Office", justify="right", style="green")
table.add_row("Dec 20, 2019", "Star Wars: The Rise of Skywalker", "$952,110,690")
table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347")
table.add_row("Dec 15, 2017", "Star Wars Ep. V111: The Last Jedi", "$1,332,539,889")
table.add_row("Dec 16, 2016", "Rouge One: A Star Wars Story", "$1,332,439,889")
console = Console(record=True)
console.print(table)
console.save_html("table.html", inline_styles=True)

View file

@ -75,7 +75,6 @@ class Box:
if not last:
append(self.top_divider)
append(self.top_right)
append("\n")
return "".join(parts)
def get_row(
@ -120,7 +119,6 @@ class Box:
append(cross)
if edge:
append(right)
append("\n")
return "".join(parts)
def get_bottom(self, widths: Iterable[int]) -> str:
@ -141,7 +139,6 @@ class Box:
if not last:
append(self.bottom_divider)
append(self.bottom_right)
append("\n")
return "".join(parts)
@ -334,29 +331,43 @@ DOUBLE_EDGE = Box(
if __name__ == "__main__": # pragma: no cover
print("ASCII")
print(ASCII)
print("SQUARE")
print(SQUARE)
from .console import Console
from .panel import Panel
from .table import Table
from .text import Text
from . import box
print("HORIZONTALS")
print(HORIZONTALS)
import sys
print("ROUNDED")
print(ROUNDED)
console = Console(record=True)
print("HEAVY")
print(HEAVY)
table = Table(width=80, show_footer=True, style="dim", border_style="not dim")
spaces = " " * 10
table.add_column(spaces, spaces)
table.add_column(spaces, spaces)
table.add_row(spaces, spaces)
print("HEAVY_EDGE")
print(HEAVY_EDGE)
BOXES = [
"ASCII",
"SQUARE",
"MINIMAL",
"MINIMAL_HEAVY_HEAD",
"MINIMAL_DOUBLE_HEAD",
"SIMPLE",
"SIMPLE_HEAVY",
"HORIZONTALS",
"ROUNDED",
"HEAVY",
"HEAVY_EDGE",
"HEAVY_HEAD",
"DOUBLE",
"DOUBLE_EDGE",
]
print("HEAVY_HEAD")
print(HEAVY_HEAD)
for box_name in BOXES:
table.box = getattr(box, box_name)
table.title = Text(f"box.{box_name}", style="magenta")
console.print(table)
print("DOUBLE")
print(DOUBLE)
print("DOUBLE_EDGE")
print(DOUBLE_EDGE)
console.save_html("box.html", inline_styles=True)

View file

@ -30,12 +30,12 @@ from typing_extensions import Protocol, runtime_checkable, Literal
from ._emoji_replace import _emoji_replace
from . import markup
from ._render_width import RenderWidth
from .render_width import RenderWidth
from ._log_render import LogRender
from .default_styles import DEFAULT_STYLES
from . import errors
from .color import ColorSystem
from .highlighter import ReprHighlighter
from .highlighter import NullHighlighter, ReprHighlighter
from .pretty import Pretty
from .style import Style
from .tabulate import tabulate_mapping
@ -170,6 +170,7 @@ class Console:
log_time (bool, optional): Boolean to enable logging of time by :meth:`log` methods. Defaults to True.
log_path (bool, optional): Boolean to enable the logging of the caller by :meth:`log`. Defaults to True.
log_time_format (str, optional): Log time format if ``log_time`` is enabled. Defaults to "[%X] ".
highlighter(HighlighterType, optional): Default highlighter.
"""
def __init__(
@ -186,21 +187,8 @@ class Console:
log_time: bool = True,
log_path: bool = True,
log_time_format: str = "[%X] ",
highlighter: "HighlighterType" = ReprHighlighter(),
):
"""[summary]
Args:
color_system (Optional[Literal[, optional): [description]. Defaults to "auto".
styles (Dict[str, Style], optional): [description]. Defaults to None.
file (IO, optional): [description]. Defaults to None.
width (int, optional): [description]. Defaults to None.
height (int, optional): [description]. Defaults to None.
record (bool, optional): [description]. Defaults to False.
markup (bool, optional): [description]. Defaults to True.
log_time (bool, optional): [description]. Defaults to True.
log_path (bool, optional): [description]. Defaults to True.
log_time_format (str, optional): [description]. Defaults to "[%X] ".
"""
self._styles = ChainMap(DEFAULT_STYLES if styles is None else styles)
self.file = file or sys.stdout
@ -227,6 +215,7 @@ class Console:
self._log_render = LogRender(
show_time=log_time, show_path=log_path, time_format=log_time_format
)
self.highlighter: HighlighterType = highlighter
def __repr__(self) -> str:
return f"<console width={self.width} {str(self._color_system)}>"
@ -569,7 +558,7 @@ class Console:
sep: str,
end: str,
emoji=True,
highlight: "HighlighterType" = None,
highlighter: "HighlighterType" = None,
) -> List[ConsoleRenderable]:
"""Combined a number of renderables and text in to one renderable.
@ -589,6 +578,8 @@ class Console:
text: List[Text] = []
append_text = text.append
_highlighter = highlighter or self.highlighter
def check_text() -> None:
if text:
if end:
@ -610,24 +601,49 @@ class Console:
if emoji:
render_str = _emoji_replace(render_str)
render_text = self.render_str(render_str)
append_text(highlight(render_text) if highlight else render_text)
append_text(_highlighter(render_text))
elif isinstance(renderable, Text):
append_text(renderable)
elif isinstance(renderable, (int, float, bool, bytes, type(None))):
append_text(
highlight(repr(renderable)) if highlight else Text(repr(renderable))
)
append_text(_highlighter(repr(renderable)))
elif isinstance(renderable, (Mapping, Sequence)):
check_text()
append(Pretty(renderable))
append(Pretty(renderable, highlighter=_highlighter))
else:
append_text(
highlight(repr(renderable)) if highlight else Text(repr(renderable))
)
append_text(_highlighter(repr(renderable)))
check_text()
return renderables
def rule(self, title: str = "", character: str = "") -> None:
"""Draw a line with optional centered title.
Args:
title (str, optional): Text to render over the rule. Defaults to "".
character (str, optional): Character to form the line. Defaults to "".
"""
from .text import Text
width = self.width
if not title:
self.print(Text(character * width, "rule.line"))
else:
title_text = Text.from_markup(title, "rule.text")
if len(title_text) > width - 4:
title_text.set_length(width - 4)
rule_text = Text()
center = (width - len(title_text)) // 2
rule_text.append(character * (center - 1) + " ", "rule.line")
rule_text.append(title_text)
rule_text.append(
" " + character * (width - len(rule_text) - 1), "rule.line"
)
self.print(rule_text)
def print(
self,
*objects: Any,
@ -635,7 +651,7 @@ class Console:
end="\n",
style: Union[str, Style] = None,
emoji=True,
highlight: "HighlighterType" = None,
highlighter: "HighlighterType" = None,
) -> None:
r"""Print to the console.
@ -646,7 +662,7 @@ class Console:
end (str, optional): String to write at end of print data. Defaults to "\n".
style (Union[str, Style], optional): A style to apply to output. Defaults to None.
emoji (bool): If True, emoji codes will be replaced, otherwise emoji codes will be left in.
highlight ([type], optional): A callable that accepts a :class:`~rich.text.Text` instance
highlighter ([type], optional): A callable that accepts a :class:`~rich.text.Text` instance
and returns a Text instance, used to add highlighting. Defaults to None.
"""
if not objects:
@ -654,7 +670,7 @@ class Console:
return
renderables = self._collect_renderables(
objects, sep=sep, end=end, emoji=emoji, highlight=highlight
objects, sep=sep, end=end, emoji=emoji, highlighter=highlighter,
)
render_options = self.options
@ -669,7 +685,7 @@ class Console:
*objects: Any,
sep=" ",
end="\n",
highlight: "HighlighterType" = None,
highlighter: "HighlighterType" = None,
log_locals: bool = False,
_stack_offset=1,
) -> None:
@ -679,7 +695,7 @@ class Console:
objects (positional args): Objects to log to the terminal.
sep (str, optional): String to write between print data. Defaults to " ".
end (str, optional): String to write at end of print data. Defaults to "\n".
highlight ([type], optional): A callable that accepts a :class:`~rich.text.Text` instance
highlighter (HighlighterType, optional): A callable that accepts a :class:`~rich.text.Text` instance
and returns a Text instance, used to add highlighting. Defaults to None.
log_locals (bool, optional): Boolean to enable logging of locals where ``log()``
was called. Defaults to False.
@ -688,9 +704,8 @@ class Console:
if not objects:
self.line()
return
highlighter = highlight or ReprHighlighter()
renderables = self._collect_renderables(
objects, sep=sep, end=end, highlight=highlighter
objects, sep=sep, end=end, highlighter=highlighter
)
caller = inspect.stack()[_stack_offset]

View file

@ -14,7 +14,7 @@ if TYPE_CHECKING:
)
from .text import Text
from ._render_width import RenderWidth
from .render_width import RenderWidth
T = TypeVar("T")

View file

@ -43,24 +43,27 @@ DEFAULT_STYLES: Dict[str, Style] = {
"log.time": Style(color="cyan", dim=True),
"log.message": Style(),
"log.path": Style(dim=True),
"repr.str": Style(color="green"),
"repr.str": Style(color="green", italic=False),
"repr.brace": Style(bold=True),
"repr.tag_start": Style(bold=True),
"repr.tag_name": Style(color="bright_magenta"),
"repr.tag_contents": Style(color="default"),
"repr.tag_contents": Style(color="default", italic=True),
"repr.tag_end": Style(bold=True),
"repr.attrib_name": Style(color="yellow", italic=True),
"repr.attrib_equal": Style(bold=True),
"repr.attrib_value": Style(color="magenta"),
"repr.number": Style(color="blue", bold=True),
"repr.attrib_value": Style(color="magenta", italic=False),
"repr.number": Style(color="blue", bold=True, italic=False),
"repr.bool_true": Style(color="bright_green", italic=True),
"repr.bool_false": Style(color="bright_red", italic=True),
"repr.none": Style(color="magenta", italic=True),
"repr.url": Style(underline=True, color="default"),
"rule.line": Style(color="green"),
"rule.text": Style(),
"table.header": Style(bold=True),
"table.footer": Style(bold=True),
"table.cell": Style(),
"table.title": Style(italic=True),
"table.caption": Style(italic=True, dim=True),
}
MARKDOWN_STYLES = {

View file

@ -37,6 +37,11 @@ class Highlighter(ABC):
"""
class NullHighlighter(Highlighter):
def highlight(self, text: Text) -> None:
pass
class RegexHighlighter(Highlighter):
"""Applies highlighting from a list of regular expressions."""

View file

@ -428,71 +428,62 @@ class Markdown:
new_line = element.new_line
markup = """
An h1 header
============
Paragraphs are separated by a blank line.
2nd paragraph. *Italic*, **bold**, and `monospace`. Itemized lists look like:
* this one
* that one
* the other one
Note that --- not considering the asterisk --- the actual text content starts at 4-columns in.
> Block quotes are
> written like so.
>
> They can span multiple paragraphs,
> if you like.
Use 3 dashes for an em-dash. Use 2 dashes for ranges (ex., "it's all in chapters 12--14"). Three dots ... will be converted to an ellipsis. Unicode is supported.
An h2 header
------------
```python
@classmethod
def adjust_line_length(
cls, line: List[Segment], length: int, style: Style = None
) -> List[Segment]:
line_length = sum(len(text) for text, _style in line)
if line_length < length:
return line[:] + [Segment(" " * (length - line_length), style)]
elif line_length > length:
line_length = 0
new_line: List[Segment] = []
append = new_line.append
for segment in line:
segment_length = len(segment.text)
if line_length + segment_length < length:
append(segment)
line_length += segment_length
else:
text, style = segment
append(Segment(text[: length - line_length], style))
break
return new_line
return line
```
"""
# markup = """\
# # Heading
# This is `code`!
# Hello, *World*!
# **Bold**
# """
if __name__ == "__main__": # pragma: no cover
markup = """
An h1 header
============
Paragraphs are separated by a blank line.
2nd paragraph. *Italic*, **bold**, and `monospace`. Itemized lists look like:
* this one
* that one
* the other one
Note that --- not considering the asterisk --- the actual text content starts at 4-columns in.
> Block quotes are
> written like so.
>
> They can span multiple paragraphs,
> if you like.
Use 3 dashes for an em-dash. Use 2 dashes for ranges (ex., "it's all in chapters 12--14"). Three dots ... will be converted to an ellipsis. Unicode is supported.
An h2 header
------------
```python
@classmethod
def adjust_line_length(
cls, line: List[Segment], length: int, style: Style = None
) -> List[Segment]:
line_length = sum(len(text) for text, _style in line)
if line_length < length:
return line[:] + [Segment(" " * (length - line_length), style)]
elif line_length > length:
line_length = 0
new_line: List[Segment] = []
append = new_line.append
for segment in line:
segment_length = len(segment.text)
if line_length + segment_length < length:
append(segment)
line_length += segment_length
else:
text, style = segment
append(Segment(text[: length - line_length], style))
break
return new_line
return line
```
"""
from .console import Console
console = Console(record=True, width=90)

View file

@ -7,7 +7,7 @@ if TYPE_CHECKING:
RenderableType,
RenderResult,
)
from ._render_width import RenderWidth
from .render_width import RenderWidth
from .style import Style
from .text import Text
from .segment import Segment

View file

@ -15,6 +15,20 @@ from .segment import Segment
class Panel:
"""A console renderable that draws a border around its contents.
Example::
>>> console.print(Panel("Hello, World!))
Args:
renderable (ConsoleRenderable): A console renderable objects.
box (Box, optional): A Box instance that defines the look of the border.
Defaults to box.SQUARE.
expand (bool, optional): If True the panel will stretch to fill the console
width, otherwise it will be sized to fit the contents. Defaults to False.
style (str, optional): The style of the border. Defaults to "none".
"""
def __init__(
self,
renderable: Union[str, ConsoleRenderable],
@ -22,16 +36,7 @@ class Panel:
expand: bool = True,
style: Union[str, Style] = "none",
) -> None:
"""A console renderable that draws a border around its contents.
Args:
renderable (ConsoleRenderable): A console renderable objects.
box (Box, optional): A Box instance that defines the look of the border.
Defaults to box.SQUARE.
expand (bool, optional): If True the panel will stretch to fill the console
width, otherwise it will be sized to fit the contents. Defaults to False.
style (str, optional): The style of the border. Defaults to "none".
"""
self.renderable = (
Text.from_markup(renderable) if isinstance(renderable, str) else renderable
)
@ -61,11 +66,13 @@ class Panel:
line_start = Segment(box.mid_left, style)
line_end = Segment(f"{box.mid_right}\n", style)
yield Segment(box.get_top([width - 2]), style)
yield Segment.line()
for line in lines:
yield line_start
yield from line
yield line_end
yield Segment(box.get_bottom([width - 2]), style)
yield Segment.line()
def __console_width__(self, max_width: int) -> RenderWidth:
if self.expand:
@ -74,7 +81,7 @@ class Panel:
return RenderWidth(width, width)
if __name__ == "__main__":
if __name__ == "__main__": # pragma: no cover
from .console import Console
c = Console(width=22)

View file

@ -2,24 +2,24 @@ from typing import Any, TYPE_CHECKING
from pprintpp import pformat
from ._render_width import RenderWidth
from .render_width import RenderWidth
from .highlighter import ReprHighlighter
from .text import Text
if TYPE_CHECKING: # pragma: no cover
from .console import Console, ConsoleOptions, RenderResult
from .console import Console, ConsoleOptions, HighlighterType, RenderResult
class Pretty:
def __init__(self, _object: Any) -> None:
def __init__(self, _object: Any, highlighter: "HighlighterType" = None) -> None:
self._object = _object
self.highlighter = highlighter or Text
def __console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
highlighter = ReprHighlighter()
pretty_str = pformat(self._object, width=options.max_width)
pretty_text = highlighter(pretty_str)
pretty_text = self.highlighter(pretty_str)
yield pretty_text
def __console_width__(self, max_width: int) -> "RenderWidth":

View file

@ -26,11 +26,11 @@ class Segment(NamedTuple):
cls, segments: Iterable["Segment"], style: Style = None
) -> Iterable["Segment"]:
"""Apply a style to an iterable of segments.
Args:
segments (Iterable[Segment]): Segments to process.
style (Style, optional): A style to apply. Defaults to None.
Returns:
Iterable[Segments]: A new iterable of segments (possibly the same iterable).
"""
@ -44,12 +44,12 @@ class Segment(NamedTuple):
cls, segments: Iterable["Segment"], length: int, style: Style = None
) -> Iterable[List["Segment"]]:
"""Split segments in to lines, and crop lines greater than a given length.
Args:
segments (Iterable[Segment]): An iterable of segments, probably
segments (Iterable[Segment]): An iterable of segments, probably
generated from console.render.
length (Optional[int]): Desired line length.
length (Optional[int]): Desired line length.
Returns:
Iterable[List[Segment]]: An iterable of lines of segments.
"""
@ -77,12 +77,12 @@ class Segment(NamedTuple):
cls, line: List["Segment"], length: int, style: Style = None
) -> List["Segment"]:
"""Adjust a line to a given width (cropping or padding as required.
Args:
segments (Iterable[Segment]): A list of segments in a single line.
length (int): The desired width of the line.
style (Style, optional): The style of padding if used (space on the end). Defaults to None.
Returns:
List[Segment]: A line of segments with the desired length.
"""
@ -108,10 +108,10 @@ class Segment(NamedTuple):
@classmethod
def get_line_length(cls, line: List["Segment"]) -> int:
r"""Get the length of list of segments.
Args:
line (List[Segment]): A line encoded as a list of Segments (assumes no '\n' characters),
Returns:
int: The length of the line.
"""
@ -119,13 +119,13 @@ class Segment(NamedTuple):
@classmethod
def get_shape(cls, lines: List[List["Segment"]]) -> Tuple[int, int]:
"""Get the shape (enclosing rectangle) of a list of lines
"""Get the shape (enclosing rectangle) of a list of lines.
Args:
lines (List[List[Segment]]): A list of lines (no '\n' characters)
lines (List[List[Segment]]): A list of lines (no '\n' characters).
Returns:
Tuple[int, int]: Width and height in characters
Tuple[int, int]: Width and height in characters.
"""
get_line_length = cls.get_line_length
max_width = max(get_line_length(line) for line in lines)
@ -140,13 +140,13 @@ class Segment(NamedTuple):
style: Style = None,
) -> List[List["Segment"]]:
"""Set the shape of a list of lines (enclosing rectangle)
Args:
lines (List[List[Segment]]): A list of lines.
width (int): Desired width.
height (int, optional): Desired height or None for no change..
style (Style, optional): Style of any padding added. Defaults to None.
Returns:
[type]: New list of lines that fits width x height.
"""
@ -166,10 +166,10 @@ class Segment(NamedTuple):
@classmethod
def simplify(cls, segments: Iterable["Segment"]) -> Iterable["Segment"]:
"""Simplify an iterable of segments by combining contiguous segments with the same style.
Args:
segments (Iterable[Segment]): An iterable segments.
Returns:
Iterable[Segment]: A possibly smaller iterable of segments that will render the same way.
"""

View file

@ -1,6 +1,6 @@
from functools import lru_cache
import sys
from typing import Any, Dict, Iterable, List, Mapping, Optional, Type
from typing import Any, Dict, Iterable, List, Mapping, Optional, Type, Union
from . import errors
from .color import blend_rgb, Color, ColorParseError, ColorSystem
@ -124,6 +124,14 @@ class Style:
except errors.StyleSyntaxError:
return style.strip().lower()
@classmethod
def pick_first(cls, *values: Optional[Union["Style", str]]) -> Union["Style", str]:
"""Pick first non-None style."""
for value in values:
if value is not None:
return value
raise ValueError("expected at least one non-None style")
def __repr__(self) -> str:
"""Render a named style differently from an anonymous style."""
return f'<style "{self}">'

View file

@ -29,7 +29,7 @@ from .padding import Padding, PaddingDimensions
from .segment import Segment
from .style import Style
from .text import Text
from ._render_width import RenderWidth
from .render_width import RenderWidth
from ._tools import iter_first_last, iter_first, iter_last, ratio_divide
@ -45,6 +45,7 @@ class Column:
justify: "JustifyValues" = "left"
width: Optional[int] = None
ratio: Optional[int] = None
no_wrap: bool = False
_cells: List["ConsoleRenderable"] = field(default_factory=list)
@property
@ -86,10 +87,10 @@ class Table:
def __init__(
self,
*headers: Union[Column, str],
title: str = None,
footer: str = None,
title: Union[str, Text] = None,
caption: Union[str, Text] = None,
width: int = None,
box: Optional[box.Box] = box.DOUBLE_EDGE,
box: Optional[box.Box] = box.HEAVY_HEAD,
padding: PaddingDimensions = (0, 1),
pad_edge: bool = True,
expand: bool = False,
@ -101,12 +102,14 @@ class Table:
footer_style: Union[str, Style] = None,
border_style: Union[str, Style] = None,
title_style: Union[str, Style] = None,
caption_style: Union[str, Style] = None,
) -> None:
self.columns = [
(Column(header) if isinstance(header, str) else header)
for header in headers
]
self.title = title
self.caption = caption
self.width = width
self.box = box
self._padding = Padding.unpack(padding)
@ -120,6 +123,7 @@ class Table:
self.footer_style = footer_style
self.border_style = border_style
self.title_style = title_style
self.caption_style = title_style
self._row_count = 0
@property
@ -141,6 +145,7 @@ class Table:
justify: "JustifyValues" = "left",
width: int = None,
ratio: int = None,
no_wrap: bool = False,
):
"""Add a column to the table.
@ -155,30 +160,23 @@ class Table:
justify (JustifyValues, optional): Alignment for cells. Defaults to "left".
width (int, optional): A minimum width in characters. Defaults to None.
ratio (int, optional): Flexible ratio for the column. Defaults to None.
no_wrap (bool, optional): Set to ``True`` to disable wrapping of this column.
"""
def _pick_first_style(
*values: Optional[Union[Style, str]]
) -> Union[Style, str]:
"""Pick first non-None style."""
for value in values:
if value is not None:
return value
raise ValueError("expected at least one non-None style")
column = Column(
header=header,
footer=footer,
header_style=_pick_first_style(
header_style=Style.pick_first(
header_style, self.header_style, "table.header"
),
footer_style=_pick_first_style(
footer_style=Style.pick_first(
footer_style, self.footer_style, "table.footer"
),
style=_pick_first_style(style, self.style, "table.cell"),
style=Style.pick_first(style, self.style, "table.cell"),
justify=justify,
width=width,
ratio=ratio,
no_wrap=no_wrap,
)
self.columns.append(column)
@ -232,16 +230,30 @@ class Table:
max_width -= 2
widths = self._calculate_column_widths(max_width)
table_width = sum(widths) + len(self.columns) + (2 if self.box else 0)
if self.title:
title_text = Text.from_markup(
self.title,
style=self.title_style
if self.title_style is not None
else "table.title",
)
wrapped_title = title_text.wrap(table_width, "center")
yield wrapped_title
yield from self.render_title(table_width)
yield from self._render(console, options, widths)
yield from self.render_caption(table_width)
def render_title(self, table_width: int) -> "RenderResult":
if self.title:
if isinstance(self.title, str):
title_text = Text.from_markup(
self.title, style=Style.pick_first(self.title_style, "table.title")
)
else:
title_text = self.title
yield title_text.wrap(table_width, "center")
def render_caption(self, table_width: int) -> "RenderResult":
if self.caption:
if isinstance(self.caption, str):
caption_text = Text.from_markup(
self.caption,
style=Style.pick_first(self.caption_style, "table.caption"),
)
else:
caption_text = self.caption
yield caption_text.wrap(table_width, "center")
def _calculate_column_widths(self, max_width: int) -> List[int]:
"""Calculate the widths of each column."""
@ -339,10 +351,14 @@ class Table:
_min, _max = get_render_width(cell.renderable, max_width)
append_min(_min)
append_max(_max)
return RenderWidth(
max(min_widths) if min_widths else 1,
max(max_widths) if max_widths else max_width,
)
if column.no_wrap:
_width = max(max_widths) if max_widths else max_width
return RenderWidth(_width, _width)
else:
return RenderWidth(
max(min_widths) if min_widths else 1,
max(max_widths) if max_widths else max_width,
)
def _render(
self, console: "Console", options: "ConsoleOptions", widths: List[int]
@ -366,6 +382,7 @@ class Table:
if box and show_edge:
yield Segment(box.get_top(widths), border_style)
yield new_line
for first, last, row in iter_first_last(rows):
max_height = 1
@ -389,6 +406,7 @@ class Table:
yield Segment(
box.get_row(widths, "foot", edge=show_edge), border_style
)
yield new_line
if first:
left = Segment(box.head_left, border_style)
right = Segment(box.head_right, border_style)
@ -419,9 +437,11 @@ class Table:
yield new_line
if box and first and show_header:
yield Segment(box.get_row(widths, "head", edge=show_edge), border_style)
yield new_line
if box and show_edge:
yield Segment(box.get_bottom(widths), border_style)
yield new_line
if __name__ == "__main__": # pragma: no cover

View file

@ -2,6 +2,7 @@ from collections.abc import Mapping
from . import box
from .highlighter import ReprHighlighter
from .pretty import Pretty
from .table import Table
from .text import Text
@ -20,6 +21,9 @@ def tabulate_mapping(mapping: Mapping, title: str = None) -> Table:
table = Table(
show_header=False, title=title, box=box.ROUNDED, border_style="blue", padding=0
)
highlighter = ReprHighlighter()
for key, value in mapping.items():
table.add_row(Pretty(key), Pretty(value))
table.add_row(
Pretty(key, highlighter=highlighter), Pretty(value, highlighter=highlighter)
)
return table

View file

@ -24,7 +24,7 @@ if TYPE_CHECKING: # pragma: no cover
from .containers import Lines
from .style import Style
from .segment import Segment
from ._render_width import RenderWidth
from .render_width import RenderWidth
from ._tools import iter_last, iter_first_last
from ._wrap import divide_line

2
tests/pytest.ini Normal file
View file

@ -0,0 +1,2 @@
[pytest]
junit_family=legacy

View file

@ -2,7 +2,7 @@ import pytest
from rich.console import Console
from rich.text import Span, Text
from rich._render_width import RenderWidth
from rich.render_width import RenderWidth
def test_span():