CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
text.py1362 linesDownload Raw Back to rich
1import re2from functools import partial, reduce3from math import gcd4from operator import itemgetter5from typing import (6    TYPE_CHECKING,7    Any,8    Callable,9    Dict,10    Iterable,11    List,12    NamedTuple,13    Optional,14    Pattern,15    Tuple,16    Union,17)18 19from ._loop import loop_last20from ._pick import pick_bool21from ._wrap import divide_line22from .align import AlignMethod23from .cells import cell_len, set_cell_size24from .containers import Lines25from .control import strip_control_codes26from .emoji import EmojiVariant27from .jupyter import JupyterMixin28from .measure import Measurement29from .segment import Segment30from .style import Style, StyleType31 32if TYPE_CHECKING:  # pragma: no cover33    from .console import Console, ConsoleOptions, JustifyMethod, OverflowMethod34 35DEFAULT_JUSTIFY: "JustifyMethod" = "default"36DEFAULT_OVERFLOW: "OverflowMethod" = "fold"37 38 39_re_whitespace = re.compile(r"\s+$")40 41TextType = Union[str, "Text"]42"""A plain string or a :class:`Text` instance."""43 44GetStyleCallable = Callable[[str], Optional[StyleType]]45 46 47class Span(NamedTuple):48    """A marked up region in some text."""49 50    start: int51    """Span start index."""52    end: int53    """Span end index."""54    style: Union[str, Style]55    """Style associated with the span."""56 57    def __repr__(self) -> str:58        return f"Span({self.start}, {self.end}, {self.style!r})"59 60    def __bool__(self) -> bool:61        return self.end > self.start62 63    def split(self, offset: int) -> Tuple["Span", Optional["Span"]]:64        """Split a span in to 2 from a given offset."""65 66        if offset < self.start:67            return self, None68        if offset >= self.end:69            return self, None70 71        start, end, style = self72        span1 = Span(start, min(end, offset), style)73        span2 = Span(span1.end, end, style)74        return span1, span275 76    def move(self, offset: int) -> "Span":77        """Move start and end by a given offset.78 79        Args:80            offset (int): Number of characters to add to start and end.81 82        Returns:83            TextSpan: A new TextSpan with adjusted position.84        """85        start, end, style = self86        return Span(start + offset, end + offset, style)87 88    def right_crop(self, offset: int) -> "Span":89        """Crop the span at the given offset.90 91        Args:92            offset (int): A value between start and end.93 94        Returns:95            Span: A new (possibly smaller) span.96        """97        start, end, style = self98        if offset >= end:99            return self100        return Span(start, min(offset, end), style)101 102    def extend(self, cells: int) -> "Span":103        """Extend the span by the given number of cells.104 105        Args:106            cells (int): Additional space to add to end of span.107 108        Returns:109            Span: A span.110        """111        if cells:112            start, end, style = self113            return Span(start, end + cells, style)114        else:115            return self116 117 118class Text(JupyterMixin):119    """Text with color / style.120 121    Args:122        text (str, optional): Default unstyled text. Defaults to "".123        style (Union[str, Style], optional): Base style for text. Defaults to "".124        justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.125        overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.126        no_wrap (bool, optional): Disable text wrapping, or None for default. Defaults to None.127        end (str, optional): Character to end text with. Defaults to "\\\\n".128        tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to None.129        spans (List[Span], optional). A list of predefined style spans. Defaults to None.130    """131 132    __slots__ = [133        "_text",134        "style",135        "justify",136        "overflow",137        "no_wrap",138        "end",139        "tab_size",140        "_spans",141        "_length",142    ]143 144    def __init__(145        self,146        text: str = "",147        style: Union[str, Style] = "",148        *,149        justify: Optional["JustifyMethod"] = None,150        overflow: Optional["OverflowMethod"] = None,151        no_wrap: Optional[bool] = None,152        end: str = "\n",153        tab_size: Optional[int] = None,154        spans: Optional[List[Span]] = None,155    ) -> None:156        sanitized_text = strip_control_codes(text)157        self._text = [sanitized_text]158        self.style = style159        self.justify: Optional["JustifyMethod"] = justify160        self.overflow: Optional["OverflowMethod"] = overflow161        self.no_wrap = no_wrap162        self.end = end163        self.tab_size = tab_size164        self._spans: List[Span] = spans or []165        self._length: int = len(sanitized_text)166 167    def __len__(self) -> int:168        return self._length169 170    def __bool__(self) -> bool:171        return bool(self._length)172 173    def __str__(self) -> str:174        return self.plain175 176    def __repr__(self) -> str:177        return f"<text {self.plain!r} {self._spans!r} {self.style!r}>"178 179    def __add__(self, other: Any) -> "Text":180        if isinstance(other, (str, Text)):181            result = self.copy()182            result.append(other)183            return result184        return NotImplemented185 186    def __eq__(self, other: object) -> bool:187        if not isinstance(other, Text):188            return NotImplemented189        return self.plain == other.plain and self._spans == other._spans190 191    def __contains__(self, other: object) -> bool:192        if isinstance(other, str):193            return other in self.plain194        elif isinstance(other, Text):195            return other.plain in self.plain196        return False197 198    def __getitem__(self, slice: Union[int, slice]) -> "Text":199        def get_text_at(offset: int) -> "Text":200            _Span = Span201            text = Text(202                self.plain[offset],203                spans=[204                    _Span(0, 1, style)205                    for start, end, style in self._spans206                    if end > offset >= start207                ],208                end="",209            )210            return text211 212        if isinstance(slice, int):213            return get_text_at(slice)214        else:215            start, stop, step = slice.indices(len(self.plain))216            if step == 1:217                lines = self.divide([start, stop])218                return lines[1]219            else:220                # This would be a bit of work to implement efficiently221                # For now, its not required222                raise TypeError("slices with step!=1 are not supported")223 224    @property225    def cell_len(self) -> int:226        """Get the number of cells required to render this text."""227        return cell_len(self.plain)228 229    @property230    def markup(self) -> str:231        """Get console markup to render this Text.232 233        Returns:234            str: A string potentially creating markup tags.235        """236        from .markup import escape237 238        output: List[str] = []239 240        plain = self.plain241        markup_spans = [242            (0, False, self.style),243            *((span.start, False, span.style) for span in self._spans),244            *((span.end, True, span.style) for span in self._spans),245            (len(plain), True, self.style),246        ]247        markup_spans.sort(key=itemgetter(0, 1))248        position = 0249        append = output.append250        for offset, closing, style in markup_spans:251            if offset > position:252                append(escape(plain[position:offset]))253                position = offset254            if style:255                append(f"[/{style}]" if closing else f"[{style}]")256        markup = "".join(output)257        return markup258 259    @classmethod260    def from_markup(261        cls,262        text: str,263        *,264        style: Union[str, Style] = "",265        emoji: bool = True,266        emoji_variant: Optional[EmojiVariant] = None,267        justify: Optional["JustifyMethod"] = None,268        overflow: Optional["OverflowMethod"] = None,269        end: str = "\n",270    ) -> "Text":271        """Create Text instance from markup.272 273        Args:274            text (str): A string containing console markup.275            style (Union[str, Style], optional): Base style for text. Defaults to "".276            emoji (bool, optional): Also render emoji code. Defaults to True.277            emoji_variant (str, optional): Optional emoji variant, either "text" or "emoji". Defaults to None.278            justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.279            overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.280            end (str, optional): Character to end text with. Defaults to "\\\\n".281 282        Returns:283            Text: A Text instance with markup rendered.284        """285        from .markup import render286 287        rendered_text = render(text, style, emoji=emoji, emoji_variant=emoji_variant)288        rendered_text.justify = justify289        rendered_text.overflow = overflow290        rendered_text.end = end291        return rendered_text292 293    @classmethod294    def from_ansi(295        cls,296        text: str,297        *,298        style: Union[str, Style] = "",299        justify: Optional["JustifyMethod"] = None,300        overflow: Optional["OverflowMethod"] = None,301        no_wrap: Optional[bool] = None,302        end: str = "\n",303        tab_size: Optional[int] = 8,304    ) -> "Text":305        """Create a Text object from a string containing ANSI escape codes.306 307        Args:308            text (str): A string containing escape codes.309            style (Union[str, Style], optional): Base style for text. Defaults to "".310            justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.311            overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.312            no_wrap (bool, optional): Disable text wrapping, or None for default. Defaults to None.313            end (str, optional): Character to end text with. Defaults to "\\\\n".314            tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to None.315        """316        from .ansi import AnsiDecoder317 318        joiner = Text(319            "\n",320            justify=justify,321            overflow=overflow,322            no_wrap=no_wrap,323            end=end,324            tab_size=tab_size,325            style=style,326        )327        decoder = AnsiDecoder()328        result = joiner.join(line for line in decoder.decode(text))329        return result330 331    @classmethod332    def styled(333        cls,334        text: str,335        style: StyleType = "",336        *,337        justify: Optional["JustifyMethod"] = None,338        overflow: Optional["OverflowMethod"] = None,339    ) -> "Text":340        """Construct a Text instance with a pre-applied styled. A style applied in this way won't be used341        to pad the text when it is justified.342 343        Args:344            text (str): A string containing console markup.345            style (Union[str, Style]): Style to apply to the text. Defaults to "".346            justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.347            overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.348 349        Returns:350            Text: A text instance with a style applied to the entire string.351        """352        styled_text = cls(text, justify=justify, overflow=overflow)353        styled_text.stylize(style)354        return styled_text355 356    @classmethod357    def assemble(358        cls,359        *parts: Union[str, "Text", Tuple[str, StyleType]],360        style: Union[str, Style] = "",361        justify: Optional["JustifyMethod"] = None,362        overflow: Optional["OverflowMethod"] = None,363        no_wrap: Optional[bool] = None,364        end: str = "\n",365        tab_size: int = 8,366        meta: Optional[Dict[str, Any]] = None,367    ) -> "Text":368        """Construct a text instance by combining a sequence of strings with optional styles.369        The positional arguments should be either strings, or a tuple of string + style.370 371        Args:372            style (Union[str, Style], optional): Base style for text. Defaults to "".373            justify (str, optional): Justify method: "left", "center", "full", "right". Defaults to None.374            overflow (str, optional): Overflow method: "crop", "fold", "ellipsis". Defaults to None.375            no_wrap (bool, optional): Disable text wrapping, or None for default. Defaults to None.376            end (str, optional): Character to end text with. Defaults to "\\\\n".377            tab_size (int): Number of spaces per tab, or ``None`` to use ``console.tab_size``. Defaults to None.378            meta (Dict[str, Any], optional). Meta data to apply to text, or None for no meta data. Default to None379 380        Returns:381            Text: A new text instance.382        """383        text = cls(384            style=style,385            justify=justify,386            overflow=overflow,387            no_wrap=no_wrap,388            end=end,389            tab_size=tab_size,390        )391        append = text.append392        _Text = Text393        for part in parts:394            if isinstance(part, (_Text, str)):395                append(part)396            else:397                append(*part)398        if meta:399            text.apply_meta(meta)400        return text401 402    @property403    def plain(self) -> str:404        """Get the text as a single string."""405        if len(self._text) != 1:406            self._text[:] = ["".join(self._text)]407        return self._text[0]408 409    @plain.setter410    def plain(self, new_text: str) -> None:411        """Set the text to a new value."""412        if new_text != self.plain:413            sanitized_text = strip_control_codes(new_text)414            self._text[:] = [sanitized_text]415            old_length = self._length416            self._length = len(sanitized_text)417            if old_length > self._length:418                self._trim_spans()419 420    @property421    def spans(self) -> List[Span]:422        """Get a reference to the internal list of spans."""423        return self._spans424 425    @spans.setter426    def spans(self, spans: List[Span]) -> None:427        """Set spans."""428        self._spans = spans[:]429 430    def blank_copy(self, plain: str = "") -> "Text":431        """Return a new Text instance with copied metadata (but not the string or spans)."""432        copy_self = Text(433            plain,434            style=self.style,435            justify=self.justify,436            overflow=self.overflow,437            no_wrap=self.no_wrap,438            end=self.end,439            tab_size=self.tab_size,440        )441        return copy_self442 443    def copy(self) -> "Text":444        """Return a copy of this instance."""445        copy_self = Text(446            self.plain,447            style=self.style,448            justify=self.justify,449            overflow=self.overflow,450            no_wrap=self.no_wrap,451            end=self.end,452            tab_size=self.tab_size,453        )454        copy_self._spans[:] = self._spans455        return copy_self456 457    def stylize(458        self,459        style: Union[str, Style],460        start: int = 0,461        end: Optional[int] = None,462    ) -> None:463        """Apply a style to the text, or a portion of the text.464 465        Args:466            style (Union[str, Style]): Style instance or style definition to apply.467            start (int): Start offset (negative indexing is supported). Defaults to 0.468            end (Optional[int], optional): End offset (negative indexing is supported), or None for end of text. Defaults to None.469        """470        if style:471            length = len(self)472            if start < 0:473                start = length + start474            if end is None:475                end = length476            if end < 0:477                end = length + end478            if start >= length or end <= start:479                # Span not in text or not valid480                return481            self._spans.append(Span(start, min(length, end), style))482 483    def stylize_before(484        self,485        style: Union[str, Style],486        start: int = 0,487        end: Optional[int] = None,488    ) -> None:489        """Apply a style to the text, or a portion of the text. Styles will be applied before other styles already present.490 491        Args:492            style (Union[str, Style]): Style instance or style definition to apply.493            start (int): Start offset (negative indexing is supported). Defaults to 0.494            end (Optional[int], optional): End offset (negative indexing is supported), or None for end of text. Defaults to None.495        """496        if style:497            length = len(self)498            if start < 0:499                start = length + start500            if end is None:501                end = length502            if end < 0:503                end = length + end504            if start >= length or end <= start:505                # Span not in text or not valid506                return507            self._spans.insert(0, Span(start, min(length, end), style))508 509    def apply_meta(510        self, meta: Dict[str, Any], start: int = 0, end: Optional[int] = None511    ) -> None:512        """Apply metadata to the text, or a portion of the text.513 514        Args:515            meta (Dict[str, Any]): A dict of meta information.516            start (int): Start offset (negative indexing is supported). Defaults to 0.517            end (Optional[int], optional): End offset (negative indexing is supported), or None for end of text. Defaults to None.518 519        """520        style = Style.from_meta(meta)521        self.stylize(style, start=start, end=end)522 523    def on(self, meta: Optional[Dict[str, Any]] = None, **handlers: Any) -> "Text":524        """Apply event handlers (used by Textual project).525 526        Example:527            >>> from rich.text import Text528            >>> text = Text("hello world")529            >>> text.on(click="view.toggle('world')")530 531        Args:532            meta (Dict[str, Any]): Mapping of meta information.533            **handlers: Keyword args are prefixed with "@" to defined handlers.534 535        Returns:536            Text: Self is returned to method may be chained.537        """538        meta = {} if meta is None else meta539        meta.update({f"@{key}": value for key, value in handlers.items()})540        self.stylize(Style.from_meta(meta))541        return self542 543    def remove_suffix(self, suffix: str) -> None:544        """Remove a suffix if it exists.545 546        Args:547            suffix (str): Suffix to remove.548        """549        if self.plain.endswith(suffix):550            self.right_crop(len(suffix))551 552    def get_style_at_offset(self, console: "Console", offset: int) -> Style:553        """Get the style of a character at give offset.554 555        Args:556            console (~Console): Console where text will be rendered.557            offset (int): Offset in to text (negative indexing supported)558 559        Returns:560            Style: A Style instance.561        """562        # TODO: This is a little inefficient, it is only used by full justify563        if offset < 0:564            offset = len(self) + offset565        get_style = console.get_style566        style = get_style(self.style).copy()567        for start, end, span_style in self._spans:568            if end > offset >= start:569                style += get_style(span_style, default="")570        return style571 572    def extend_style(self, spaces: int) -> None:573        """Extend the Text given number of spaces where the spaces have the same style as the last character.574 575        Args:576            spaces (int): Number of spaces to add to the Text.577        """578        if spaces <= 0:579            return580        spans = self.spans581        new_spaces = " " * spaces582        if spans:583            end_offset = len(self)584            self._spans[:] = [585                span.extend(spaces) if span.end >= end_offset else span586                for span in spans587            ]588            self._text.append(new_spaces)589            self._length += spaces590        else:591            self.plain += new_spaces592 593    def highlight_regex(594        self,595        re_highlight: Union[Pattern[str], str],596        style: Optional[Union[GetStyleCallable, StyleType]] = None,597        *,598        style_prefix: str = "",599    ) -> int:600        """Highlight text with a regular expression, where group names are601        translated to styles.602 603        Args:604            re_highlight (Union[re.Pattern, str]): A regular expression object or string.605            style (Union[GetStyleCallable, StyleType]): Optional style to apply to whole match, or a callable606                which accepts the matched text and returns a style. Defaults to None.607            style_prefix (str, optional): Optional prefix to add to style group names.608 609        Returns:610            int: Number of regex matches611        """612        count = 0613        append_span = self._spans.append614        _Span = Span615        plain = self.plain616        if isinstance(re_highlight, str):617            re_highlight = re.compile(re_highlight)618        for match in re_highlight.finditer(plain):619            get_span = match.span620            if style:621                start, end = get_span()622                match_style = style(plain[start:end]) if callable(style) else style623                if match_style is not None and end > start:624                    append_span(_Span(start, end, match_style))625 626            count += 1627            for name in match.groupdict().keys():628                start, end = get_span(name)629                if start != -1 and end > start:630                    append_span(_Span(start, end, f"{style_prefix}{name}"))631        return count632 633    def highlight_words(634        self,635        words: Iterable[str],636        style: Union[str, Style],637        *,638        case_sensitive: bool = True,639    ) -> int:640        """Highlight words with a style.641 642        Args:643            words (Iterable[str]): Words to highlight.644            style (Union[str, Style]): Style to apply.645            case_sensitive (bool, optional): Enable case sensitive matching. Defaults to True.646 647        Returns:648            int: Number of words highlighted.649        """650        re_words = "|".join(re.escape(word) for word in words)651        add_span = self._spans.append652        count = 0653        _Span = Span654        for match in re.finditer(655            re_words, self.plain, flags=0 if case_sensitive else re.IGNORECASE656        ):657            start, end = match.span(0)658            add_span(_Span(start, end, style))659            count += 1660        return count661 662    def rstrip(self) -> None:663        """Strip whitespace from end of text."""664        self.plain = self.plain.rstrip()665 666    def rstrip_end(self, size: int) -> None:667        """Remove whitespace beyond a certain width at the end of the text.668 669        Args:670            size (int): The desired size of the text.671        """672        text_length = len(self)673        if text_length > size:674            excess = text_length - size675            whitespace_match = _re_whitespace.search(self.plain)676            if whitespace_match is not None:677                whitespace_count = len(whitespace_match.group(0))678                self.right_crop(min(whitespace_count, excess))679 680    def set_length(self, new_length: int) -> None:681        """Set new length of the text, clipping or padding is required."""682        length = len(self)683        if length != new_length:684            if length < new_length:685                self.pad_right(new_length - length)686            else:687                self.right_crop(length - new_length)688 689    def __rich_console__(690        self, console: "Console", options: "ConsoleOptions"691    ) -> Iterable[Segment]:692        tab_size: int = console.tab_size if self.tab_size is None else self.tab_size693        justify = self.justify or options.justify or DEFAULT_JUSTIFY694 695        overflow = self.overflow or options.overflow or DEFAULT_OVERFLOW696 697        lines = self.wrap(698            console,699            options.max_width,700            justify=justify,701            overflow=overflow,702            tab_size=tab_size or 8,703            no_wrap=pick_bool(self.no_wrap, options.no_wrap, False),704        )705        all_lines = Text("\n").join(lines)706        yield from all_lines.render(console, end=self.end)707 708    def __rich_measure__(709        self, console: "Console", options: "ConsoleOptions"710    ) -> Measurement:711        text = self.plain712        lines = text.splitlines()713        max_text_width = max(cell_len(line) for line in lines) if lines else 0714        words = text.split()715        min_text_width = (716            max(cell_len(word) for word in words) if words else max_text_width717        )718        return Measurement(min_text_width, max_text_width)719 720    def render(self, console: "Console", end: str = "") -> Iterable["Segment"]:721        """Render the text as Segments.722 723        Args:724            console (Console): Console instance.725            end (Optional[str], optional): Optional end character.726 727        Returns:728            Iterable[Segment]: Result of render that may be written to the console.729        """730        _Segment = Segment731        text = self.plain732        if not self._spans:733            yield Segment(text)734            if end:735                yield _Segment(end)736            return737        get_style = partial(console.get_style, default=Style.null())738 739        enumerated_spans = list(enumerate(self._spans, 1))740        style_map = {index: get_style(span.style) for index, span in enumerated_spans}741        style_map[0] = get_style(self.style)742 743        spans = [744            (0, False, 0),745            *((span.start, False, index) for index, span in enumerated_spans),746            *((span.end, True, index) for index, span in enumerated_spans),747            (len(text), True, 0),748        ]749        spans.sort(key=itemgetter(0, 1))750 751        stack: List[int] = []752        stack_append = stack.append753        stack_pop = stack.remove754 755        style_cache: Dict[Tuple[Style, ...], Style] = {}756        style_cache_get = style_cache.get757        combine = Style.combine758 759        def get_current_style() -> Style:760            """Construct current style from stack."""761            styles = tuple(style_map[_style_id] for _style_id in sorted(stack))762            cached_style = style_cache_get(styles)763            if cached_style is not None:764                return cached_style765            current_style = combine(styles)766            style_cache[styles] = current_style767            return current_style768 769        for (offset, leaving, style_id), (next_offset, _, _) in zip(spans, spans[1:]):770            if leaving:771                stack_pop(style_id)772            else:773                stack_append(style_id)774            if next_offset > offset:775                yield _Segment(text[offset:next_offset], get_current_style())776        if end:777            yield _Segment(end)778 779    def join(self, lines: Iterable["Text"]) -> "Text":780        """Join text together with this instance as the separator.781 782        Args:783            lines (Iterable[Text]): An iterable of Text instances to join.784 785        Returns:786            Text: A new text instance containing join text.787        """788 789        new_text = self.blank_copy()790 791        def iter_text() -> Iterable["Text"]:792            if self.plain:793                for last, line in loop_last(lines):794                    yield line795                    if not last:796                        yield self797            else:798                yield from lines799 800        extend_text = new_text._text.extend801        append_span = new_text._spans.append802        extend_spans = new_text._spans.extend803        offset = 0804        _Span = Span805 806        for text in iter_text():807            extend_text(text._text)808            if text.style:809                append_span(_Span(offset, offset + len(text), text.style))810            extend_spans(811                _Span(offset + start, offset + end, style)812                for start, end, style in text._spans813            )814            offset += len(text)815        new_text._length = offset816        return new_text817 818    def expand_tabs(self, tab_size: Optional[int] = None) -> None:819        """Converts tabs to spaces.820 821        Args:822            tab_size (int, optional): Size of tabs. Defaults to 8.823 824        """825        if "\t" not in self.plain:826            return827        if tab_size is None:828            tab_size = self.tab_size829        if tab_size is None:830            tab_size = 8831 832        new_text: List[Text] = []833        append = new_text.append834 835        for line in self.split("\n", include_separator=True):836            if "\t" not in line.plain:837                append(line)838            else:839                cell_position = 0840                parts = line.split("\t", include_separator=True)841                for part in parts:842                    if part.plain.endswith("\t"):843                        part._text[-1] = part._text[-1][:-1] + " "844                        cell_position += part.cell_len845                        tab_remainder = cell_position % tab_size846                        if tab_remainder:847                            spaces = tab_size - tab_remainder848                            part.extend_style(spaces)849                            cell_position += spaces850                    else:851                        cell_position += part.cell_len852                    append(part)853 854        result = Text("").join(new_text)855 856        self._text = [result.plain]857        self._length = len(self.plain)858        self._spans[:] = result._spans859 860    def truncate(861        self,862        max_width: int,863        *,864        overflow: Optional["OverflowMethod"] = None,865        pad: bool = False,866    ) -> None:867        """Truncate text if it is longer that a given width.868 869        Args:870            max_width (int): Maximum number of characters in text.871            overflow (str, optional): Overflow method: "crop", "fold", or "ellipsis". Defaults to None, to use self.overflow.872            pad (bool, optional): Pad with spaces if the length is less than max_width. Defaults to False.873        """874        _overflow = overflow or self.overflow or DEFAULT_OVERFLOW875        if _overflow != "ignore":876            length = cell_len(self.plain)877            if length > max_width:878                if _overflow == "ellipsis":879                    self.plain = set_cell_size(self.plain, max_width - 1) + "…"880                else:881                    self.plain = set_cell_size(self.plain, max_width)882            if pad and length < max_width:883                spaces = max_width - length884                self._text = [f"{self.plain}{' ' * spaces}"]885                self._length = len(self.plain)886 887    def _trim_spans(self) -> None:888        """Remove or modify any spans that are over the end of the text."""889        max_offset = len(self.plain)890        _Span = Span891        self._spans[:] = [892            (893                span894                if span.end < max_offset895                else _Span(span.start, min(max_offset, span.end), span.style)896            )897            for span in self._spans898            if span.start < max_offset899        ]900 901    def pad(self, count: int, character: str = " ") -> None:902        """Pad left and right with a given number of characters.903 904        Args:905            count (int): Width of padding.906            character (str): The character to pad with. Must be a string of length 1.907        """908        assert len(character) == 1, "Character must be a string of length 1"909        if count:910            pad_characters = character * count911            self.plain = f"{pad_characters}{self.plain}{pad_characters}"912            _Span = Span913            self._spans[:] = [914                _Span(start + count, end + count, style)915                for start, end, style in self._spans916            ]917 918    def pad_left(self, count: int, character: str = " ") -> None:919        """Pad the left with a given character.920 921        Args:922            count (int): Number of characters to pad.923            character (str, optional): Character to pad with. Defaults to " ".924        """925        assert len(character) == 1, "Character must be a string of length 1"926        if count:927            self.plain = f"{character * count}{self.plain}"928            _Span = Span929            self._spans[:] = [930                _Span(start + count, end + count, style)931                for start, end, style in self._spans932            ]933 934    def pad_right(self, count: int, character: str = " ") -> None:935        """Pad the right with a given character.936 937        Args:938            count (int): Number of characters to pad.939            character (str, optional): Character to pad with. Defaults to " ".940        """941        assert len(character) == 1, "Character must be a string of length 1"942        if count:943            self.plain = f"{self.plain}{character * count}"944 945    def align(self, align: AlignMethod, width: int, character: str = " ") -> None:946        """Align text to a given width.947 948        Args:949            align (AlignMethod): One of "left", "center", or "right".950            width (int): Desired width.951            character (str, optional): Character to pad with. Defaults to " ".952        """953        self.truncate(width)954        excess_space = width - cell_len(self.plain)955        if excess_space:956            if align == "left":957                self.pad_right(excess_space, character)958            elif align == "center":959                left = excess_space // 2960                self.pad_left(left, character)961                self.pad_right(excess_space - left, character)962            else:963                self.pad_left(excess_space, character)964 965    def append(966        self, text: Union["Text", str], style: Optional[Union[str, "Style"]] = None967    ) -> "Text":968        """Add text with an optional style.969 970        Args:971            text (Union[Text, str]): A str or Text to append.972            style (str, optional): A style name. Defaults to None.973 974        Returns:975            Text: Returns self for chaining.976        """977 978        if not isinstance(text, (str, Text)):979            raise TypeError("Only str or Text can be appended to Text")980 981        if len(text):982            if isinstance(text, str):983                sanitized_text = strip_control_codes(text)984                self._text.append(sanitized_text)985                offset = len(self)986                text_length = len(sanitized_text)987                if style:988                    self._spans.append(Span(offset, offset + text_length, style))989                self._length += text_length990            elif isinstance(text, Text):991                _Span = Span992                if style is not None:993                    raise ValueError(994                        "style must not be set when appending Text instance"995                    )996                text_length = self._length997                if text.style:998                    self._spans.append(999                        _Span(text_length, text_length + len(text), text.style)1000                    )1001                self._text.append(text.plain)1002                self._spans.extend(1003                    _Span(start + text_length, end + text_length, style)1004                    for start, end, style in text._spans.copy()1005                )1006                self._length += len(text)1007        return self1008 1009    def append_text(self, text: "Text") -> "Text":1010        """Append another Text instance. This method is more performant that Text.append, but1011        only works for Text.1012 1013        Args:1014            text (Text): The Text instance to append to this instance.1015 1016        Returns:1017            Text: Returns self for chaining.1018        """1019        _Span = Span1020        text_length = self._length1021        if text.style:1022            self._spans.append(_Span(text_length, text_length + len(text), text.style))1023        self._text.append(text.plain)1024        self._spans.extend(1025            _Span(start + text_length, end + text_length, style)1026            for start, end, style in text._spans.copy()1027        )1028        self._length += len(text)1029        return self1030 1031    def append_tokens(1032        self, tokens: Iterable[Tuple[str, Optional[StyleType]]]1033    ) -> "Text":1034        """Append iterable of str and style. Style may be a Style instance or a str style definition.1035 1036        Args:1037            tokens (Iterable[Tuple[str, Optional[StyleType]]]): An iterable of tuples containing str content and style.1038 1039        Returns:1040            Text: Returns self for chaining.1041        """1042        append_text = self._text.append1043        append_span = self._spans.append1044        _Span = Span1045        offset = len(self)1046        for content, style in tokens:1047            content = strip_control_codes(content)1048            append_text(content)1049            if style:1050                append_span(_Span(offset, offset + len(content), style))1051            offset += len(content)1052        self._length = offset1053        return self1054 1055    def copy_styles(self, text: "Text") -> None:1056        """Copy styles from another Text instance.1057 1058        Args:1059            text (Text): A Text instance to copy styles from, must be the same length.1060        """1061        self._spans.extend(text._spans)1062 1063    def split(1064        self,1065        separator: str = "\n",1066        *,1067        include_separator: bool = False,1068        allow_blank: bool = False,1069    ) -> Lines:1070        """Split rich text in to lines, preserving styles.1071 1072        Args:1073            separator (str, optional): String to split on. Defaults to "\\\\n".1074            include_separator (bool, optional): Include the separator in the lines. Defaults to False.1075            allow_blank (bool, optional): Return a blank line if the text ends with a separator. Defaults to False.1076 1077        Returns:1078            List[RichText]: A list of rich text, one per line of the original.1079        """1080        assert separator, "separator must not be empty"1081 1082        text = self.plain1083        if separator not in text:1084            return Lines([self.copy()])1085 1086        if include_separator:1087            lines = self.divide(1088                match.end() for match in re.finditer(re.escape(separator), text)1089            )1090        else:1091 1092            def flatten_spans() -> Iterable[int]:1093                for match in re.finditer(re.escape(separator), text):1094                    start, end = match.span()1095                    yield start1096                    yield end1097 1098            lines = Lines(1099                line for line in self.divide(flatten_spans()) if line.plain != separator1100            )1101 1102        if not allow_blank and text.endswith(separator):1103            lines.pop()1104 1105        return lines1106 1107    def divide(self, offsets: Iterable[int]) -> Lines:1108        """Divide text in to a number of lines at given offsets.1109 1110        Args:1111            offsets (Iterable[int]): Offsets used to divide text.1112 1113        Returns:1114            Lines: New RichText instances between offsets.1115        """1116        _offsets = list(offsets)1117 1118        if not _offsets:1119            return Lines([self.copy()])1120 1121        text = self.plain1122        text_length = len(text)1123        divide_offsets = [0, *_offsets, text_length]1124        line_ranges = list(zip(divide_offsets, divide_offsets[1:]))1125 1126        style = self.style1127        justify = self.justify1128        overflow = self.overflow1129        _Text = Text1130        new_lines = Lines(1131            _Text(1132                text[start:end],1133                style=style,1134                justify=justify,1135                overflow=overflow,1136            )1137            for start, end in line_ranges1138        )1139        if not self._spans:1140            return new_lines1141 1142        _line_appends = [line._spans.append for line in new_lines._lines]1143        line_count = len(line_ranges)1144        _Span = Span1145 1146        for span_start, span_end, style in self._spans:1147            lower_bound = 01148            upper_bound = line_count1149            start_line_no = (lower_bound + upper_bound) // 21150 1151            while True:1152                line_start, line_end = line_ranges[start_line_no]1153                if span_start < line_start:1154                    upper_bound = start_line_no - 11155                elif span_start > line_end:1156                    lower_bound = start_line_no + 11157                else:1158                    break1159                start_line_no = (lower_bound + upper_bound) // 21160 1161            if span_end < line_end:1162                end_line_no = start_line_no1163            else:1164                end_line_no = lower_bound = start_line_no1165                upper_bound = line_count1166 1167                while True:1168                    line_start, line_end = line_ranges[end_line_no]1169                    if span_end < line_start:1170                        upper_bound = end_line_no - 11171                    elif span_end > line_end:1172                        lower_bound = end_line_no + 11173                    else:1174                        break1175                    end_line_no = (lower_bound + upper_bound) // 21176 1177            for line_no in range(start_line_no, end_line_no + 1):1178                line_start, line_end = line_ranges[line_no]1179                new_start = max(0, span_start - line_start)1180                new_end = min(span_end - line_start, line_end - line_start)1181                if new_end > new_start:1182                    _line_appends[line_no](_Span(new_start, new_end, style))1183 1184        return new_lines1185 1186    def right_crop(self, amount: int = 1) -> None:1187        """Remove a number of characters from the end of the text."""1188        max_offset = len(self.plain) - amount1189        _Span = Span1190        self._spans[:] = [1191            (1192                span1193                if span.end < max_offset1194                else _Span(span.start, min(max_offset, span.end), span.style)1195            )1196            for span in self._spans1197            if span.start < max_offset1198        ]1199        self._text = [self.plain[:-amount]]1200        self._length -= amount

Showing the first 1,200 of 1362 lines. Download the file for the rest.

Aluode/PerceptionLabPortable · CoolFace