Загрузить файлы в «venv/Lib/site-packages/pip/_vendor/rich»

This commit is contained in:
2026-07-02 19:33:52 +00:00
parent cdab66761f
commit 22f7072c19
5 changed files with 1002 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
"""Functions for reporting filesizes. Borrowed from https://github.com/PyFilesystem/pyfilesystem2
The functions declared in this module should cover the different
use cases needed to generate a string representation of a file size
using several different units. Since there are many standards regarding
file size units, three different functions have been implemented.
See Also:
* `Wikipedia: Binary prefix <https://en.wikipedia.org/wiki/Binary_prefix>`_
"""
__all__ = ["decimal"]
from typing import Iterable, List, Optional, Tuple
def _to_str(
size: int,
suffixes: Iterable[str],
base: int,
*,
precision: Optional[int] = 1,
separator: Optional[str] = " ",
) -> str:
if size == 1:
return "1 byte"
elif size < base:
return f"{size:,} bytes"
for i, suffix in enumerate(suffixes, 2): # noqa: B007
unit = base**i
if size < unit:
break
return "{:,.{precision}f}{separator}{}".format(
(base * size / unit),
suffix,
precision=precision,
separator=separator,
)
def pick_unit_and_suffix(size: int, suffixes: List[str], base: int) -> Tuple[int, str]:
"""Pick a suffix and base for the given size."""
for i, suffix in enumerate(suffixes):
unit = base**i
if size < unit * base:
break
return unit, suffix
def decimal(
size: int,
*,
precision: Optional[int] = 1,
separator: Optional[str] = " ",
) -> str:
"""Convert a filesize in to a string (powers of 1000, SI prefixes).
In this convention, ``1000 B = 1 kB``.
This is typically the format used to advertise the storage
capacity of USB flash drives and the like (*256 MB* meaning
actually a storage capacity of more than *256 000 000 B*),
or used by **Mac OS X** since v10.6 to report file sizes.
Arguments:
int (size): A file size.
int (precision): The number of decimal places to include (default = 1).
str (separator): The string to separate the value from the units (default = " ").
Returns:
`str`: A string containing a abbreviated file size and units.
Example:
>>> filesize.decimal(30000)
'30.0 kB'
>>> filesize.decimal(30000, precision=2, separator="")
'30.00kB'
"""
return _to_str(
size,
("kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"),
1000,
precision=precision,
separator=separator,
)

View File

@@ -0,0 +1,232 @@
import re
from abc import ABC, abstractmethod
from typing import List, Union
from .text import Span, Text
def _combine_regex(*regexes: str) -> str:
"""Combine a number of regexes in to a single regex.
Returns:
str: New regex with all regexes ORed together.
"""
return "|".join(regexes)
class Highlighter(ABC):
"""Abstract base class for highlighters."""
def __call__(self, text: Union[str, Text]) -> Text:
"""Highlight a str or Text instance.
Args:
text (Union[str, ~Text]): Text to highlight.
Raises:
TypeError: If not called with text or str.
Returns:
Text: A test instance with highlighting applied.
"""
if isinstance(text, str):
highlight_text = Text(text)
elif isinstance(text, Text):
highlight_text = text.copy()
else:
raise TypeError(f"str or Text instance required, not {text!r}")
self.highlight(highlight_text)
return highlight_text
@abstractmethod
def highlight(self, text: Text) -> None:
"""Apply highlighting in place to text.
Args:
text (~Text): A text object highlight.
"""
class NullHighlighter(Highlighter):
"""A highlighter object that doesn't highlight.
May be used to disable highlighting entirely.
"""
def highlight(self, text: Text) -> None:
"""Nothing to do"""
class RegexHighlighter(Highlighter):
"""Applies highlighting from a list of regular expressions."""
highlights: List[str] = []
base_style: str = ""
def highlight(self, text: Text) -> None:
"""Highlight :class:`rich.text.Text` using regular expressions.
Args:
text (~Text): Text to highlighted.
"""
highlight_regex = text.highlight_regex
for re_highlight in self.highlights:
highlight_regex(re_highlight, style_prefix=self.base_style)
class ReprHighlighter(RegexHighlighter):
"""Highlights the text typically produced from ``__repr__`` methods."""
base_style = "repr."
highlights = [
r"(?P<tag_start><)(?P<tag_name>[-\w.:|]*)(?P<tag_contents>[\w\W]*)(?P<tag_end>>)",
r'(?P<attrib_name>[\w_]{1,50})=(?P<attrib_value>"?[\w_]+"?)?',
r"(?P<brace>[][{}()])",
_combine_regex(
r"(?P<ipv4>[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})",
r"(?P<ipv6>([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4})",
r"(?P<eui64>(?:[0-9A-Fa-f]{1,2}-){7}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{1,2}:){7}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{4}\.){3}[0-9A-Fa-f]{4})",
r"(?P<eui48>(?:[0-9A-Fa-f]{1,2}-){5}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{1,2}:){5}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{4}\.){2}[0-9A-Fa-f]{4})",
r"(?P<uuid>[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})",
r"(?P<call>[\w.]*?)\(",
r"\b(?P<bool_true>True)\b|\b(?P<bool_false>False)\b|\b(?P<none>None)\b",
r"(?P<ellipsis>\.\.\.)",
r"(?P<number_complex>(?<!\w)(?:\-?[0-9]+\.?[0-9]*(?:e[-+]?\d+?)?)(?:[-+](?:[0-9]+\.?[0-9]*(?:e[-+]?\d+)?))?j)",
r"(?P<number>(?<!\w)\-?[0-9]+\.?[0-9]*(e[-+]?\d+?)?\b|0x[0-9a-fA-F]*)",
r"(?P<path>\B(/[-\w._+]+)*\/)(?P<filename>[-\w._+]*)?",
r"(?<![\\\w])(?P<str>b?'''.*?(?<!\\)'''|b?'.*?(?<!\\)'|b?\"\"\".*?(?<!\\)\"\"\"|b?\".*?(?<!\\)\")",
r"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~@]*)",
),
]
class JSONHighlighter(RegexHighlighter):
"""Highlights JSON"""
# Captures the start and end of JSON strings, handling escaped quotes
JSON_STR = r"(?<![\\\w])(?P<str>b?\".*?(?<!\\)\")"
JSON_WHITESPACE = {" ", "\n", "\r", "\t"}
base_style = "json."
highlights = [
_combine_regex(
r"(?P<brace>[\{\[\(\)\]\}])",
r"\b(?P<bool_true>true)\b|\b(?P<bool_false>false)\b|\b(?P<null>null)\b",
r"(?P<number>(?<!\w)\-?[0-9]+\.?[0-9]*(e[\-\+]?\d+?)?\b|0x[0-9a-fA-F]*)",
JSON_STR,
),
]
def highlight(self, text: Text) -> None:
super().highlight(text)
# Additional work to handle highlighting JSON keys
plain = text.plain
append = text.spans.append
whitespace = self.JSON_WHITESPACE
for match in re.finditer(self.JSON_STR, plain):
start, end = match.span()
cursor = end
while cursor < len(plain):
char = plain[cursor]
cursor += 1
if char == ":":
append(Span(start, end, "json.key"))
elif char in whitespace:
continue
break
class ISO8601Highlighter(RegexHighlighter):
"""Highlights the ISO8601 date time strings.
Regex reference: https://www.oreilly.com/library/view/regular-expressions-cookbook/9781449327453/ch04s07.html
"""
base_style = "iso8601."
highlights = [
#
# Dates
#
# Calendar month (e.g. 2008-08). The hyphen is required
r"^(?P<year>[0-9]{4})-(?P<month>1[0-2]|0[1-9])$",
# Calendar date w/o hyphens (e.g. 20080830)
r"^(?P<date>(?P<year>[0-9]{4})(?P<month>1[0-2]|0[1-9])(?P<day>3[01]|0[1-9]|[12][0-9]))$",
# Ordinal date (e.g. 2008-243). The hyphen is optional
r"^(?P<date>(?P<year>[0-9]{4})-?(?P<day>36[0-6]|3[0-5][0-9]|[12][0-9]{2}|0[1-9][0-9]|00[1-9]))$",
#
# Weeks
#
# Week of the year (e.g., 2008-W35). The hyphen is optional
r"^(?P<date>(?P<year>[0-9]{4})-?W(?P<week>5[0-3]|[1-4][0-9]|0[1-9]))$",
# Week date (e.g., 2008-W35-6). The hyphens are optional
r"^(?P<date>(?P<year>[0-9]{4})-?W(?P<week>5[0-3]|[1-4][0-9]|0[1-9])-?(?P<day>[1-7]))$",
#
# Times
#
# Hours and minutes (e.g., 17:21). The colon is optional
r"^(?P<time>(?P<hour>2[0-3]|[01][0-9]):?(?P<minute>[0-5][0-9]))$",
# Hours, minutes, and seconds w/o colons (e.g., 172159)
r"^(?P<time>(?P<hour>2[0-3]|[01][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-5][0-9]))$",
# Time zone designator (e.g., Z, +07 or +07:00). The colons and the minutes are optional
r"^(?P<timezone>(Z|[+-](?:2[0-3]|[01][0-9])(?::?(?:[0-5][0-9]))?))$",
# Hours, minutes, and seconds with time zone designator (e.g., 17:21:59+07:00).
# All the colons are optional. The minutes in the time zone designator are also optional
r"^(?P<time>(?P<hour>2[0-3]|[01][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-5][0-9]))(?P<timezone>Z|[+-](?:2[0-3]|[01][0-9])(?::?(?:[0-5][0-9]))?)$",
#
# Date and Time
#
# Calendar date with hours, minutes, and seconds (e.g., 2008-08-30 17:21:59 or 20080830 172159).
# A space is required between the date and the time. The hyphens and colons are optional.
# This regex matches dates and times that specify some hyphens or colons but omit others.
# This does not follow ISO 8601
r"^(?P<date>(?P<year>[0-9]{4})(?P<hyphen>-)?(?P<month>1[0-2]|0[1-9])(?(hyphen)-)(?P<day>3[01]|0[1-9]|[12][0-9])) (?P<time>(?P<hour>2[0-3]|[01][0-9])(?(hyphen):)(?P<minute>[0-5][0-9])(?(hyphen):)(?P<second>[0-5][0-9]))$",
#
# XML Schema dates and times
#
# Date, with optional time zone (e.g., 2008-08-30 or 2008-08-30+07:00).
# Hyphens are required. This is the XML Schema 'date' type
r"^(?P<date>(?P<year>-?(?:[1-9][0-9]*)?[0-9]{4})-(?P<month>1[0-2]|0[1-9])-(?P<day>3[01]|0[1-9]|[12][0-9]))(?P<timezone>Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$",
# Time, with optional fractional seconds and time zone (e.g., 01:45:36 or 01:45:36.123+07:00).
# There is no limit on the number of digits for the fractional seconds. This is the XML Schema 'time' type
r"^(?P<time>(?P<hour>2[0-3]|[01][0-9]):(?P<minute>[0-5][0-9]):(?P<second>[0-5][0-9])(?P<frac>\.[0-9]+)?)(?P<timezone>Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$",
# Date and time, with optional fractional seconds and time zone (e.g., 2008-08-30T01:45:36 or 2008-08-30T01:45:36.123Z).
# This is the XML Schema 'dateTime' type
r"^(?P<date>(?P<year>-?(?:[1-9][0-9]*)?[0-9]{4})-(?P<month>1[0-2]|0[1-9])-(?P<day>3[01]|0[1-9]|[12][0-9]))T(?P<time>(?P<hour>2[0-3]|[01][0-9]):(?P<minute>[0-5][0-9]):(?P<second>[0-5][0-9])(?P<ms>\.[0-9]+)?)(?P<timezone>Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$",
]
if __name__ == "__main__": # pragma: no cover
from .console import Console
console = Console()
console.print("[bold green]hello world![/bold green]")
console.print("'[bold green]hello world![/bold green]'")
console.print(" /foo")
console.print("/foo/")
console.print("/foo/bar")
console.print("foo/bar/baz")
console.print("/foo/bar/baz?foo=bar+egg&egg=baz")
console.print("/foo/bar/baz/")
console.print("/foo/bar/baz/egg")
console.print("/foo/bar/baz/egg.py")
console.print("/foo/bar/baz/egg.py word")
console.print(" /foo/bar/baz/egg.py word")
console.print("foo /foo/bar/baz/egg.py word")
console.print("foo /foo/bar/ba._++z/egg+.py word")
console.print("https://example.org?foo=bar#header")
console.print(1234567.34)
console.print(1 / 2)
console.print(-1 / 123123123123)
console.print(
"127.0.1.1 bar 192.168.1.4 2001:0db8:85a3:0000:0000:8a2e:0370:7334 foo"
)
import json
console.print_json(json.dumps(obj={"name": "apple", "count": 1}), indent=None)

View File

@@ -0,0 +1,139 @@
from pathlib import Path
from json import loads, dumps
from typing import Any, Callable, Optional, Union
from .text import Text
from .highlighter import JSONHighlighter, NullHighlighter
class JSON:
"""A renderable which pretty prints JSON.
Args:
json (str): JSON encoded data.
indent (Union[None, int, str], optional): Number of characters to indent by. Defaults to 2.
highlight (bool, optional): Enable highlighting. Defaults to True.
skip_keys (bool, optional): Skip keys not of a basic type. Defaults to False.
ensure_ascii (bool, optional): Escape all non-ascii characters. Defaults to False.
check_circular (bool, optional): Check for circular references. Defaults to True.
allow_nan (bool, optional): Allow NaN and Infinity values. Defaults to True.
default (Callable, optional): A callable that converts values that can not be encoded
in to something that can be JSON encoded. Defaults to None.
sort_keys (bool, optional): Sort dictionary keys. Defaults to False.
"""
def __init__(
self,
json: str,
indent: Union[None, int, str] = 2,
highlight: bool = True,
skip_keys: bool = False,
ensure_ascii: bool = False,
check_circular: bool = True,
allow_nan: bool = True,
default: Optional[Callable[[Any], Any]] = None,
sort_keys: bool = False,
) -> None:
data = loads(json)
json = dumps(
data,
indent=indent,
skipkeys=skip_keys,
ensure_ascii=ensure_ascii,
check_circular=check_circular,
allow_nan=allow_nan,
default=default,
sort_keys=sort_keys,
)
highlighter = JSONHighlighter() if highlight else NullHighlighter()
self.text = highlighter(json)
self.text.no_wrap = True
self.text.overflow = None
@classmethod
def from_data(
cls,
data: Any,
indent: Union[None, int, str] = 2,
highlight: bool = True,
skip_keys: bool = False,
ensure_ascii: bool = False,
check_circular: bool = True,
allow_nan: bool = True,
default: Optional[Callable[[Any], Any]] = None,
sort_keys: bool = False,
) -> "JSON":
"""Encodes a JSON object from arbitrary data.
Args:
data (Any): An object that may be encoded in to JSON
indent (Union[None, int, str], optional): Number of characters to indent by. Defaults to 2.
highlight (bool, optional): Enable highlighting. Defaults to True.
default (Callable, optional): Optional callable which will be called for objects that cannot be serialized. Defaults to None.
skip_keys (bool, optional): Skip keys not of a basic type. Defaults to False.
ensure_ascii (bool, optional): Escape all non-ascii characters. Defaults to False.
check_circular (bool, optional): Check for circular references. Defaults to True.
allow_nan (bool, optional): Allow NaN and Infinity values. Defaults to True.
default (Callable, optional): A callable that converts values that can not be encoded
in to something that can be JSON encoded. Defaults to None.
sort_keys (bool, optional): Sort dictionary keys. Defaults to False.
Returns:
JSON: New JSON object from the given data.
"""
json_instance: "JSON" = cls.__new__(cls)
json = dumps(
data,
indent=indent,
skipkeys=skip_keys,
ensure_ascii=ensure_ascii,
check_circular=check_circular,
allow_nan=allow_nan,
default=default,
sort_keys=sort_keys,
)
highlighter = JSONHighlighter() if highlight else NullHighlighter()
json_instance.text = highlighter(json)
json_instance.text.no_wrap = True
json_instance.text.overflow = None
return json_instance
def __rich__(self) -> Text:
return self.text
if __name__ == "__main__":
import argparse
import sys
parser = argparse.ArgumentParser(description="Pretty print json")
parser.add_argument(
"path",
metavar="PATH",
help="path to file, or - for stdin",
)
parser.add_argument(
"-i",
"--indent",
metavar="SPACES",
type=int,
help="Number of spaces in an indent",
default=2,
)
args = parser.parse_args()
from pip._vendor.rich.console import Console
console = Console()
error_console = Console(stderr=True)
try:
if args.path == "-":
json_data = sys.stdin.read()
else:
json_data = Path(args.path).read_text()
except Exception as error:
error_console.print(f"Unable to read {args.path!r}; {error}")
sys.exit(-1)
console.print(JSON(json_data, indent=args.indent), soft_wrap=True)

View File

@@ -0,0 +1,101 @@
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Sequence
if TYPE_CHECKING:
from pip._vendor.rich.console import ConsoleRenderable
from . import get_console
from .segment import Segment
from .terminal_theme import DEFAULT_TERMINAL_THEME
if TYPE_CHECKING:
from pip._vendor.rich.console import ConsoleRenderable
JUPYTER_HTML_FORMAT = """\
<pre style="white-space:pre;overflow-x:auto;line-height:normal;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, html: str, text: str) -> None:
self.html = html
self.text = text
def _repr_mimebundle_(
self, include: Sequence[str], exclude: Sequence[str], **kwargs: Any
) -> Dict[str, str]:
data = {"text/plain": self.text, "text/html": self.html}
if include:
data = {k: v for (k, v) in data.items() if k in include}
if exclude:
data = {k: v for (k, v) in data.items() if k not in exclude}
return data
class JupyterMixin:
"""Add to an Rich renderable to make it render in Jupyter notebook."""
__slots__ = ()
def _repr_mimebundle_(
self: "ConsoleRenderable",
include: Sequence[str],
exclude: Sequence[str],
**kwargs: Any,
) -> Dict[str, str]:
console = get_console()
segments = list(console.render(self, console.options))
html = _render_segments(segments)
text = console._render_buffer(segments)
data = {"text/plain": text, "text/html": html}
if include:
data = {k: v for (k, v) in data.items() if k in include}
if exclude:
data = {k: v for (k, v) in data.items() if k not in exclude}
return data
def _render_segments(segments: Iterable[Segment]) -> str:
def escape(text: str) -> str:
"""Escape html."""
return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
fragments: List[str] = []
append_fragment = fragments.append
theme = DEFAULT_TERMINAL_THEME
for text, style, control in Segment.simplify(segments):
if control:
continue
text = escape(text)
if style:
rule = style.get_html_style(theme)
text = f'<span style="{rule}">{text}</span>' if rule else text
if style.link:
text = f'<a href="{style.link}" target="_blank">{text}</a>'
append_fragment(text)
code = "".join(fragments)
html = JUPYTER_HTML_FORMAT.format(code=code)
return html
def display(segments: Iterable[Segment], text: str) -> None:
"""Render segments to Jupyter."""
html = _render_segments(segments)
jupyter_renderable = JupyterRenderable(html, text)
try:
from IPython.display import display as ipython_display
ipython_display(jupyter_renderable)
except ModuleNotFoundError:
# Handle the case where the Console has force_jupyter=True,
# but IPython is not installed.
pass
def print(*args: Any, **kwargs: Any) -> None:
"""Proxy for Console print."""
console = get_console()
return console.print(*args, **kwargs)

View File

@@ -0,0 +1,442 @@
from abc import ABC, abstractmethod
from itertools import islice
from operator import itemgetter
from threading import RLock
from typing import (
TYPE_CHECKING,
Dict,
Iterable,
List,
NamedTuple,
Optional,
Sequence,
Tuple,
Union,
)
from ._ratio import ratio_resolve
from .align import Align
from .console import Console, ConsoleOptions, RenderableType, RenderResult
from .highlighter import ReprHighlighter
from .panel import Panel
from .pretty import Pretty
from .region import Region
from .repr import Result, rich_repr
from .segment import Segment
from .style import StyleType
if TYPE_CHECKING:
from pip._vendor.rich.tree import Tree
class LayoutRender(NamedTuple):
"""An individual layout render."""
region: Region
render: List[List[Segment]]
RegionMap = Dict["Layout", Region]
RenderMap = Dict["Layout", LayoutRender]
class LayoutError(Exception):
"""Layout related error."""
class NoSplitter(LayoutError):
"""Requested splitter does not exist."""
class _Placeholder:
"""An internal renderable used as a Layout placeholder."""
highlighter = ReprHighlighter()
def __init__(self, layout: "Layout", style: StyleType = "") -> None:
self.layout = layout
self.style = style
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
width = options.max_width
height = options.height or options.size.height
layout = self.layout
title = (
f"{layout.name!r} ({width} x {height})"
if layout.name
else f"({width} x {height})"
)
yield Panel(
Align.center(Pretty(layout), vertical="middle"),
style=self.style,
title=self.highlighter(title),
border_style="blue",
height=height,
)
class Splitter(ABC):
"""Base class for a splitter."""
name: str = ""
@abstractmethod
def get_tree_icon(self) -> str:
"""Get the icon (emoji) used in layout.tree"""
@abstractmethod
def divide(
self, children: Sequence["Layout"], region: Region
) -> Iterable[Tuple["Layout", Region]]:
"""Divide a region amongst several child layouts.
Args:
children (Sequence(Layout)): A number of child layouts.
region (Region): A rectangular region to divide.
"""
class RowSplitter(Splitter):
"""Split a layout region in to rows."""
name = "row"
def get_tree_icon(self) -> str:
return "[layout.tree.row]⬌"
def divide(
self, children: Sequence["Layout"], region: Region
) -> Iterable[Tuple["Layout", Region]]:
x, y, width, height = region
render_widths = ratio_resolve(width, children)
offset = 0
_Region = Region
for child, child_width in zip(children, render_widths):
yield child, _Region(x + offset, y, child_width, height)
offset += child_width
class ColumnSplitter(Splitter):
"""Split a layout region in to columns."""
name = "column"
def get_tree_icon(self) -> str:
return "[layout.tree.column]⬍"
def divide(
self, children: Sequence["Layout"], region: Region
) -> Iterable[Tuple["Layout", Region]]:
x, y, width, height = region
render_heights = ratio_resolve(height, children)
offset = 0
_Region = Region
for child, child_height in zip(children, render_heights):
yield child, _Region(x, y + offset, width, child_height)
offset += child_height
@rich_repr
class Layout:
"""A renderable to divide a fixed height in to rows or columns.
Args:
renderable (RenderableType, optional): Renderable content, or None for placeholder. Defaults to None.
name (str, optional): Optional identifier for Layout. Defaults to None.
size (int, optional): Optional fixed size of layout. Defaults to None.
minimum_size (int, optional): Minimum size of layout. Defaults to 1.
ratio (int, optional): Optional ratio for flexible layout. Defaults to 1.
visible (bool, optional): Visibility of layout. Defaults to True.
"""
splitters = {"row": RowSplitter, "column": ColumnSplitter}
def __init__(
self,
renderable: Optional[RenderableType] = None,
*,
name: Optional[str] = None,
size: Optional[int] = None,
minimum_size: int = 1,
ratio: int = 1,
visible: bool = True,
) -> None:
self._renderable = renderable or _Placeholder(self)
self.size = size
self.minimum_size = minimum_size
self.ratio = ratio
self.name = name
self.visible = visible
self.splitter: Splitter = self.splitters["column"]()
self._children: List[Layout] = []
self._render_map: RenderMap = {}
self._lock = RLock()
def __rich_repr__(self) -> Result:
yield "name", self.name, None
yield "size", self.size, None
yield "minimum_size", self.minimum_size, 1
yield "ratio", self.ratio, 1
@property
def renderable(self) -> RenderableType:
"""Layout renderable."""
return self if self._children else self._renderable
@property
def children(self) -> List["Layout"]:
"""Gets (visible) layout children."""
return [child for child in self._children if child.visible]
@property
def map(self) -> RenderMap:
"""Get a map of the last render."""
return self._render_map
def get(self, name: str) -> Optional["Layout"]:
"""Get a named layout, or None if it doesn't exist.
Args:
name (str): Name of layout.
Returns:
Optional[Layout]: Layout instance or None if no layout was found.
"""
if self.name == name:
return self
else:
for child in self._children:
named_layout = child.get(name)
if named_layout is not None:
return named_layout
return None
def __getitem__(self, name: str) -> "Layout":
layout = self.get(name)
if layout is None:
raise KeyError(f"No layout with name {name!r}")
return layout
@property
def tree(self) -> "Tree":
"""Get a tree renderable to show layout structure."""
from pip._vendor.rich.styled import Styled
from pip._vendor.rich.table import Table
from pip._vendor.rich.tree import Tree
def summary(layout: "Layout") -> Table:
icon = layout.splitter.get_tree_icon()
table = Table.grid(padding=(0, 1, 0, 0))
text: RenderableType = (
Pretty(layout) if layout.visible else Styled(Pretty(layout), "dim")
)
table.add_row(icon, text)
_summary = table
return _summary
layout = self
tree = Tree(
summary(layout),
guide_style=f"layout.tree.{layout.splitter.name}",
highlight=True,
)
def recurse(tree: "Tree", layout: "Layout") -> None:
for child in layout._children:
recurse(
tree.add(
summary(child),
guide_style=f"layout.tree.{child.splitter.name}",
),
child,
)
recurse(tree, self)
return tree
def split(
self,
*layouts: Union["Layout", RenderableType],
splitter: Union[Splitter, str] = "column",
) -> None:
"""Split the layout in to multiple sub-layouts.
Args:
*layouts (Layout): Positional arguments should be (sub) Layout instances.
splitter (Union[Splitter, str]): Splitter instance or name of splitter.
"""
_layouts = [
layout if isinstance(layout, Layout) else Layout(layout)
for layout in layouts
]
try:
self.splitter = (
splitter
if isinstance(splitter, Splitter)
else self.splitters[splitter]()
)
except KeyError:
raise NoSplitter(f"No splitter called {splitter!r}")
self._children[:] = _layouts
def add_split(self, *layouts: Union["Layout", RenderableType]) -> None:
"""Add a new layout(s) to existing split.
Args:
*layouts (Union[Layout, RenderableType]): Positional arguments should be renderables or (sub) Layout instances.
"""
_layouts = (
layout if isinstance(layout, Layout) else Layout(layout)
for layout in layouts
)
self._children.extend(_layouts)
def split_row(self, *layouts: Union["Layout", RenderableType]) -> None:
"""Split the layout in to a row (layouts side by side).
Args:
*layouts (Layout): Positional arguments should be (sub) Layout instances.
"""
self.split(*layouts, splitter="row")
def split_column(self, *layouts: Union["Layout", RenderableType]) -> None:
"""Split the layout in to a column (layouts stacked on top of each other).
Args:
*layouts (Layout): Positional arguments should be (sub) Layout instances.
"""
self.split(*layouts, splitter="column")
def unsplit(self) -> None:
"""Reset splits to initial state."""
del self._children[:]
def update(self, renderable: RenderableType) -> None:
"""Update renderable.
Args:
renderable (RenderableType): New renderable object.
"""
with self._lock:
self._renderable = renderable
def refresh_screen(self, console: "Console", layout_name: str) -> None:
"""Refresh a sub-layout.
Args:
console (Console): Console instance where Layout is to be rendered.
layout_name (str): Name of layout.
"""
with self._lock:
layout = self[layout_name]
region, _lines = self._render_map[layout]
(x, y, width, height) = region
lines = console.render_lines(
layout, console.options.update_dimensions(width, height)
)
self._render_map[layout] = LayoutRender(region, lines)
console.update_screen_lines(lines, x, y)
def _make_region_map(self, width: int, height: int) -> RegionMap:
"""Create a dict that maps layout on to Region."""
stack: List[Tuple[Layout, Region]] = [(self, Region(0, 0, width, height))]
push = stack.append
pop = stack.pop
layout_regions: List[Tuple[Layout, Region]] = []
append_layout_region = layout_regions.append
while stack:
append_layout_region(pop())
layout, region = layout_regions[-1]
children = layout.children
if children:
for child_and_region in layout.splitter.divide(children, region):
push(child_and_region)
region_map = {
layout: region
for layout, region in sorted(layout_regions, key=itemgetter(1))
}
return region_map
def render(self, console: Console, options: ConsoleOptions) -> RenderMap:
"""Render the sub_layouts.
Args:
console (Console): Console instance.
options (ConsoleOptions): Console options.
Returns:
RenderMap: A dict that maps Layout on to a tuple of Region, lines
"""
render_width = options.max_width
render_height = options.height or console.height
region_map = self._make_region_map(render_width, render_height)
layout_regions = [
(layout, region)
for layout, region in region_map.items()
if not layout.children
]
render_map: Dict["Layout", "LayoutRender"] = {}
render_lines = console.render_lines
update_dimensions = options.update_dimensions
for layout, region in layout_regions:
lines = render_lines(
layout.renderable, update_dimensions(region.width, region.height)
)
render_map[layout] = LayoutRender(region, lines)
return render_map
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
with self._lock:
width = options.max_width or console.width
height = options.height or console.height
render_map = self.render(console, options.update_dimensions(width, height))
self._render_map = render_map
layout_lines: List[List[Segment]] = [[] for _ in range(height)]
_islice = islice
for region, lines in render_map.values():
_x, y, _layout_width, layout_height = region
for row, line in zip(
_islice(layout_lines, y, y + layout_height), lines
):
row.extend(line)
new_line = Segment.line()
for layout_row in layout_lines:
yield from layout_row
yield new_line
if __name__ == "__main__":
from pip._vendor.rich.console import Console
console = Console()
layout = Layout()
layout.split_column(
Layout(name="header", size=3),
Layout(ratio=1, name="main"),
Layout(size=10, name="footer"),
)
layout["main"].split_row(Layout(name="side"), Layout(name="body", ratio=2))
layout["body"].split_row(Layout(name="content", ratio=2), Layout(name="s2"))
layout["s2"].split_column(
Layout(name="top"), Layout(name="middle"), Layout(name="bottom")
)
layout["side"].split_column(Layout(layout.tree, name="left1"), Layout(name="left2"))
layout["content"].update("foo")
console.print(layout)