Skip to content

Commit

Permalink
[inspect] Replace ASCII special characters with their "non-special" r…
Browse files Browse the repository at this point in the history
…eadable version

With this we allow the following:
- Such characters will be displayed in the data returned by "rich.inspect" when they are used in docstrings
- We fix the crash that can happen in some circumstances in the `Text.divide()` method when some docstrings have such special characters
  • Loading branch information
olivierphi committed May 27, 2022
1 parent b7aab32 commit 74d7807
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 27 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Expand Up @@ -5,6 +5,12 @@ 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).

## [Unreleased]

### Fixed

- Fix crashes that can happen with `inspect` when docstrings contain some special control codes https://github.com/Textualize/rich/pull/2294

## [12.4.4] - 2022-05-24

### Changed
Expand Down
17 changes: 2 additions & 15 deletions rich/_inspect.py
Expand Up @@ -5,26 +5,14 @@
from typing import Any, Iterable, Optional, Tuple

from .console import Group, RenderableType
from .control import make_control_codes_readable
from .highlighter import ReprHighlighter
from .jupyter import JupyterMixin
from .panel import Panel
from .pretty import Pretty
from .table import Table
from .text import Text, TextType

_SPECIAL_CHARACTERS_TRANSLATION_TABLE = str.maketrans(
# Some special characters (such as "\b", aka "Backspace") are stripped from the docstrings when
# we display them, and also mess up our processing
# --> let's replace them with a readable "non-special" version of them (i.e. "\b" -> "\\b"):
{
"\a": "\\a", # ASCII Bell
"\b": "\\b", # ASCII Backspace
"\f": "\\f", # ASCII Formfeed
"\r": "\\r", # ASCII Carriage Return
"\v": "\\v", # ASCII Vertical Tab
}
)


def _first_paragraph(doc: str) -> str:
"""Get the first paragraph from a docstring."""
Expand Down Expand Up @@ -230,5 +218,4 @@ def _get_formatted_doc(self, object_: Any) -> Optional[str]:
docs = cleandoc(docs).strip()
if not self.help:
docs = _first_paragraph(docs)
docs = docs.translate(_SPECIAL_CHARACTERS_TRANSLATION_TABLE)
return docs
return make_control_codes_readable(docs)
38 changes: 35 additions & 3 deletions rich/control.py
@@ -1,19 +1,35 @@
import sys
import time
from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Union

if sys.version_info >= (3, 8):
from typing import Final
else:
from typing_extensions import Final # pragma: no cover

from .segment import ControlCode, ControlType, Segment

if TYPE_CHECKING:
from .console import Console, ConsoleOptions, RenderResult

STRIP_CONTROL_CODES = [
STRIP_CONTROL_CODES: Final = [
7, # Bell
8, # Backspace
11, # Vertical tab
12, # Form feed
13, # Carriage return
]
_CONTROL_TRANSLATE = {_codepoint: None for _codepoint in STRIP_CONTROL_CODES}
_CONTROL_STRIP_TRANSLATE: Final = {
_codepoint: None for _codepoint in STRIP_CONTROL_CODES
}

_CONTROL_MAKE_READABLE_TRANSLATE: Final = {
7: "\\a",
8: "\\b",
11: "\\v",
12: "\\f",
13: "\\r",
}

CONTROL_CODES_FORMAT: Dict[int, Callable[..., str]] = {
ControlType.BELL: lambda: "\x07",
Expand Down Expand Up @@ -169,7 +185,7 @@ def __rich_console__(


def strip_control_codes(
text: str, _translate_table: Dict[int, None] = _CONTROL_TRANSLATE
text: str, _translate_table: Dict[int, None] = _CONTROL_STRIP_TRANSLATE
) -> str:
"""Remove control codes from text.
Expand All @@ -182,6 +198,22 @@ def strip_control_codes(
return text.translate(_translate_table)


def make_control_codes_readable(
text: str,
_translate_table: Dict[int, str] = _CONTROL_MAKE_READABLE_TRANSLATE,
) -> str:
"""Replace control codes with their "readable" equivalent in the given text.
(e.g. "\b" becomes "\\b")
Args:
text (str): A string possibly contain control codes.
Returns:
str: String with control codes replaced with a readable version.
"""
return text.translate(_translate_table)


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

Expand Down
17 changes: 9 additions & 8 deletions rich/text.py
Expand Up @@ -2,7 +2,6 @@
from functools import partial, reduce
from math import gcd
from operator import itemgetter
from rich.emoji import EmojiVariant
from typing import (
TYPE_CHECKING,
Any,
Expand Down Expand Up @@ -141,15 +140,16 @@ def __init__(
tab_size: Optional[int] = 8,
spans: Optional[List[Span]] = None,
) -> None:
self._text = [strip_control_codes(text)]
sanitized_text = strip_control_codes(text)
self._text = [sanitized_text]
self.style = style
self.justify: Optional["JustifyMethod"] = justify
self.overflow: Optional["OverflowMethod"] = overflow
self.no_wrap = no_wrap
self.end = end
self.tab_size = tab_size
self._spans: List[Span] = spans or []
self._length: int = len(text)
self._length: int = len(sanitized_text)

def __len__(self) -> int:
return self._length
Expand Down Expand Up @@ -394,9 +394,10 @@ def plain(self) -> str:
def plain(self, new_text: str) -> None:
"""Set the text to a new value."""
if new_text != self.plain:
self._text[:] = [new_text]
sanitized_text = strip_control_codes(new_text)
self._text[:] = [sanitized_text]
old_length = self._length
self._length = len(new_text)
self._length = len(sanitized_text)
if old_length > self._length:
self._trim_spans()

Expand Down Expand Up @@ -906,10 +907,10 @@ def append(

if len(text):
if isinstance(text, str):
text = strip_control_codes(text)
self._text.append(text)
sanitized_text = strip_control_codes(text)
self._text.append(sanitized_text)
offset = len(self)
text_length = len(text)
text_length = len(sanitized_text)
if style is not None:
self._spans.append(Span(offset, offset + text_length, style))
self._length += text_length
Expand Down
11 changes: 10 additions & 1 deletion tests/test_control.py
@@ -1,4 +1,4 @@
from rich.control import Control, strip_control_codes
from rich.control import Control, make_control_codes_readable, strip_control_codes
from rich.segment import ControlType, Segment


Expand All @@ -13,6 +13,15 @@ def test_strip_control_codes():
assert strip_control_codes("Fear is the mind killer") == "Fear is the mind killer"


def test_make_control_codes_readable():
assert make_control_codes_readable("") == ""
assert make_control_codes_readable("foo\rbar") == "foo\\rbar"
assert (
make_control_codes_readable("Fear is the mind killer")
== "Fear is the mind killer"
)


def test_control_move_to():
control = Control.move_to(5, 10)
print(control.segment)
Expand Down

0 comments on commit 74d7807

Please sign in to comment.