Skip to content

Commit

Permalink
fix for line wrapping, added switch for word wrap
Browse files Browse the repository at this point in the history
  • Loading branch information
willmcgugan committed Mar 31, 2020
1 parent d273049 commit 3509614
Show file tree
Hide file tree
Showing 12 changed files with 204 additions and 40 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.8.7] - 2020-03-31

### Fixed

- Broken wrapping of long lines
- Fixed wrapping in Syntax

### Changed

- Added word_wrap option to Syntax, which defaults to False.
- Added word_wrap option to Traceback.

## [0.8.6] - 2020-03-29

### Added

- Experimental Jupyter notebook support: from rich.jupyter import print

## [0.8.5] - 2020-03-29

### Changed
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "rich"
homepage = "https://github.com/willmcgugan/rich"
documentation = "https://rich.readthedocs.io/en/latest/"
version = "0.8.5"
version = "0.8.7"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
authors = ["Will McGugan <[email protected]>"]
license = "MIT"
Expand All @@ -18,6 +18,7 @@ classifiers = [
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
]
include = ["rich/py.typed"]


[tool.poetry.dependencies]
Expand Down
2 changes: 1 addition & 1 deletion rich/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def comparison(renderable1, renderable2) -> Table:
return table

table.add_row(
"Chinese Japanese & Korean support",
"CJK support",
Panel("该库支持中文,日文和韩文文本!", expand=False, style="red", box=box.DOUBLE_EDGE,),
)

Expand Down
23 changes: 17 additions & 6 deletions rich/_wrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,28 @@ def divide_line(text: str, width: int) -> List[int]:
append = divides.append
line_position = 0
for start, _end, word in words(text):
if line_position + cell_len(word.rstrip()) > width:
if line_position and start:
append(start)
line_position = cell_len(word)
else:
for last, line in loop_last(chop_cells(text, width)):
word_length = cell_len(word.rstrip())
if line_position + word_length > width:
if word_length > width:
for last, line in loop_last(
chop_cells(word, width, position=line_position)
):
if last:
line_position = cell_len(line)
else:
start += len(line)
append(start)
elif line_position and start:
append(start)
line_position = cell_len(word)
else:
line_position += cell_len(word)
return divides


if __name__ == "__main__": # pragma: no cover
from .console import Console

console = Console(width=10)
console.print("12345 abcdefghijklmnopqrstuvwyxzABCDEFGHIJKLMNOPQRSTUVWXYZ 12345")
print(chop_cells("abcdefghijklmnopqrstuvwxyz", 10, position=2))
18 changes: 15 additions & 3 deletions rich/cells.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ def cell_len(text: str, _cache: LRUCache[str, int] = LRUCache(1024 * 4)) -> int:
return total_size


@lru_cache(maxsize=5000)
def get_character_cell_size(character: str) -> int:
"""Get the cell size of a character.
Expand All @@ -40,6 +39,19 @@ def get_character_cell_size(character: str) -> int:
if 127 > codepoint > 31:
# Shortcut for ascii
return 1
return _get_codepoint_cell_size(codepoint)


@lru_cache(maxsize=5000)
def _get_codepoint_cell_size(codepoint: int) -> int:
"""Get the cell size of a character.
Args:
character (str): A single character.
Returns:
int: Number of cells (0, 1 or 2) occupied by that character.
"""

_table = CELL_WIDTHS
lower_bound = 0
Expand Down Expand Up @@ -79,13 +91,13 @@ def set_cell_size(text: str, total: int) -> str:
return text


def chop_cells(text: str, max_size: int) -> List[str]:
def chop_cells(text: str, max_size: int, position: int = 0) -> List[str]:
"""Break text in to equal (cell) length strings."""
_get_character_cell_size = get_character_cell_size
characters = [
(character, _get_character_cell_size(character)) for character in text
][::-1]
total_size = 0
total_size = position
lines: List[List[str]] = [[]]
append = lines[-1].append

Expand Down
16 changes: 5 additions & 11 deletions rich/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,16 +139,6 @@ def __console__(
_null_highlighter = NullHighlighter()


class RichRenderable:
def __init__(self, rich_cast: Callable[[], ConsoleRenderable]) -> None:
self.rich_cast = rich_cast

def __console__(
self, console: "Console", options: "ConsoleOptions"
) -> RenderResult:
yield self.rich_cast()


class RenderGroup:
def __init__(self, *renderables: RenderableType, fit: bool = True) -> None:
"""Takes a group of renderables and returns a renderable object,
Expand Down Expand Up @@ -781,17 +771,21 @@ def print_exception(
width: Optional[int] = 88,
extra_lines: int = 3,
theme: Optional[str] = None,
word_wrap: bool = False,
) -> None:
"""Prints a rich render of the last exception and traceback.
Args:
code_width (Optional[int], optional): Number of characters used to render code. Defaults to 88.
extra_lines (int, optional): Additional lines of code to render. Defaults to 3.
theme (str, optional): Override pygments theme used in traceback
word_wrap (bool, optional): Enable word wrapping of long lines. Defaults to False.
"""
from .traceback import Traceback

traceback = Traceback(width=width, extra_lines=extra_lines, theme=theme)
traceback = Traceback(
width=width, extra_lines=extra_lines, theme=theme, word_wrap=word_wrap
)
self.print(traceback)

def log(
Expand Down
89 changes: 89 additions & 0 deletions rich/jupyter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import io
from typing import Any, IO, Union

from .console import Console as BaseConsole
from .style import Style

JUPYTER_HTML_FORMAT = """\
<pre style="white-space:pre;overflow-x:auto;line-height:1em;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace">{code}</pre>
"""


class JupyterRenderable:
"""A shim to write html to Jupyter notebook."""

def __init__(self, text: str, html: str) -> None:
self.text = text
self.html = html

def __str__(self) -> str:
return self.text

def _repr_html_(self) -> str:
return self.html


class Console(BaseConsole):
def __init__(self, **kwargs) -> None:
kwargs["file"] = io.StringIO()
kwargs["record"] = True
if "width" not in kwargs:
kwargs["width"] = 100
super().__init__(**kwargs)

def is_terminal(self) -> bool:
return True

def print( # type: ignore
self,
*objects: Any,
sep=" ",
end="\n",
file: IO[str] = None,
style: Union[str, Style] = None,
emoji: bool = None,
markup: bool = None,
highlight: bool = None,
flush: bool = False
) -> JupyterRenderable:
r"""Print to the console.
Args:
objects (positional args): Objects to log to the terminal.
sep (str, optional): String to write between print data. Defaults to " ".
end (str, optional): String to write at end of print data. Defaults to "\n".
style (Union[str, Style], optional): A style to apply to output. Defaults to None.
emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to None.
markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. Defaults to None
highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default. Defaults to None.
"""
if file is None:
super().print(
*objects,
sep=sep,
end=end,
style=style,
emoji=emoji,
markup=markup,
highlight=highlight,
)
else:
Console(file=file).print(
*objects,
sep=sep,
end=end,
style=style,
emoji=emoji,
markup=markup,
highlight=highlight,
)

html = self.export_html(code_format=JUPYTER_HTML_FORMAT, inline_styles=True)
text = self.file.getvalue() # type: ignore
self.file = io.StringIO()
jupyter_renderable = JupyterRenderable(text, html)
return jupyter_renderable


console = Console()
print = console.print
Empty file added rich/py.typed
Empty file.
27 changes: 20 additions & 7 deletions rich/syntax.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class Syntax:
highlight_lines (Set[int]): A set of line numbers to highlight.
code_width: Width of code to render (not including line numbers), or ``None`` to use all available width.
tab_size (int, optional): Size of tabs. Defaults to 4.
word_wrap (bool, optional): Enable word wrapping.
"""

def __init__(
Expand All @@ -48,6 +49,7 @@ def __init__(
highlight_lines: Set[int] = None,
code_width: Optional[int] = None,
tab_size: int = 4,
word_wrap: bool = False
) -> None:
self.code = code
self.lexer_name = lexer_name
Expand All @@ -58,6 +60,7 @@ def __init__(
self.highlight_lines = highlight_lines or set()
self.code_width = code_width
self.tab_size = tab_size
self.word_wrap = word_wrap

self._style_cache: Dict[Any, Style] = {}
if not isinstance(theme, str) and issubclass(theme, PygmentsStyle):
Expand All @@ -82,6 +85,7 @@ def from_path(
highlight_lines: Set[int] = None,
code_width: Optional[int] = None,
tab_size: int = 4,
word_wrap: bool = False,
) -> "Syntax":
"""Construct a Syntax object from a file.
Expand All @@ -97,6 +101,7 @@ def from_path(
highlight_lines (Set[int]): A set of line numbers to highlight.
code_width: Width of code to render (not including line numbers), or ``None`` to use all available width.
tab_size (int, optional): Size of tabs. Defaults to 4.
word_wrap (bool, optional): Enable word wrapping of code.
Returns:
[Syntax]: A Syntax object that may be printed to the console
Expand All @@ -118,6 +123,7 @@ def from_path(
start_line=start_line,
highlight_lines=highlight_lines,
code_width=code_width,
word_wrap=word_wrap,
)

def _get_theme_style(self, token_type) -> Style:
Expand Down Expand Up @@ -208,7 +214,11 @@ def __measure__(self, console: "Console", max_width: int) -> "Measurement":
return Measurement(max_width, max_width)

def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
code_width = options.max_width if self.code_width is None else self.code_width
code_width = (
(options.max_width - self._numbers_column_width - 1)
if self.code_width is None
else self.code_width
)
code = self.code
if self.dedent:
code = textwrap.dedent(code)
Expand All @@ -231,7 +241,7 @@ def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult
lines = lines[line_offset:end_line]

numbers_column_width = self._numbers_column_width
render_options = options.update(width=code_width + numbers_column_width)
render_options = options.update(width=code_width)

(
background_style,
Expand All @@ -241,15 +251,18 @@ def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult

highlight_line = self.highlight_lines.__contains__
_Segment = Segment
padding = _Segment(" " * numbers_column_width, background_style)
padding = _Segment(" " * numbers_column_width + " ", background_style)
new_line = _Segment("\n")

line_pointer = "❱ "

for line_no, line in enumerate(lines, self.start_line + line_offset):
wrapped_lines = console.render_lines(
line, render_options, style=background_style
)
if self.word_wrap:
wrapped_lines = console.render_lines(
line, render_options, style=background_style
)
else:
wrapped_lines = [list(line.render(console, render_options, end=""))]
for first, wrapped_line in loop_first(wrapped_lines):
if first:
line_column = str(line_no).rjust(numbers_column_width - 2) + " "
Expand All @@ -276,5 +289,5 @@ def __console__(self, console: Console, options: ConsoleOptions) -> RenderResult

console = Console()

syntax = Syntax.from_path(sys.argv[1], line_numbers=False)
syntax = Syntax.from_path(sys.argv[1], line_numbers=True, word_wrap=True)
console.print(syntax)

0 comments on commit 3509614

Please sign in to comment.