1from operator import itemgetter 2from typing import Iterable, NamedTuple, TYPE_CHECKING 3 4from . import errors 5from .protocol import is_renderable 6 7if TYPE_CHECKING: 8 from .console import Console, ConsoleOptions, RenderableType 9 10 11class Measurement(NamedTuple): 12 """Stores the minimum and maximum widths (in characters) required to render an object.""" 13 14 minimum: int 15 """Minimum number of cells required to render.""" 16 maximum: int 17 """Maximum number of cells required to render.""" 18 19 @property 20 def span(self) -> int: 21 """Get difference between maximum and minimum.""" 22 return self.maximum - self.minimum 23 24 def normalize(self) -> "Measurement": 25 """Get measurement that ensures that minimum <= maximum and minimum >= 0 26 27 Returns: 28 Measurement: A normalized measurement. 29 """ 30 minimum, maximum = self 31 minimum = min(max(0, minimum), maximum) 32 return Measurement(max(0, minimum), max(0, max(minimum, maximum))) 33 34 def with_maximum(self, width: int) -> "Measurement": 35 """Get a RenderableWith where the widths are <= width. 36 37 Args: 38 width (int): Maximum desired width. 39 40 Returns: 41 Measurement: New Measurement object. 42 """ 43 minimum, maximum = self 44 return Measurement(min(minimum, width), min(maximum, width)) 45 46 def with_minimum(self, width: int) -> "Measurement": 47 """Get a RenderableWith where the widths are >= width. 48 49 Args: 50 width (int): Minimum desired width. 51 52 Returns: 53 Measurement: New Measurement object. 54 """ 55 minimum, maximum = self 56 width = max(0, width) 57 return Measurement(max(minimum, width), max(maximum, width)) 58 59 def clamp(self, min_width: int = None, max_width: int = None) -> "Measurement": 60 """Clamp a measurement within the specified range. 61 62 Args: 63 min_width (int): Minimum desired width, or ``None`` for no minimum. Defaults to None. 64 max_width (int): Maximum desired width, or ``None`` for no maximum. Defaults to None. 65 66 Returns: 67 Measurement: New Measurement object. 68 """ 69 measurement = self 70 if min_width is not None: 71 measurement = measurement.with_minimum(min_width) 72 if max_width is not None: 73 measurement = measurement.with_maximum(max_width) 74 return measurement 75 76 @classmethod 77 def get( 78 cls, console: "Console", options: "ConsoleOptions", renderable: "RenderableType" 79 ) -> "Measurement": 80 """Get a measurement for a renderable. 81 82 Args: 83 console (~rich.console.Console): Console instance. 84 options (~rich.console.ConsoleOptions): Console options. 85 renderable (RenderableType): An object that may be rendered with Rich. 86 87 Raises: 88 errors.NotRenderableError: If the object is not renderable. 89 90 Returns: 91 Measurement: Measurement object containing range of character widths required to render the object. 92 """ 93 _max_width = options.max_width 94 if _max_width < 1: 95 return Measurement(0, 0) 96 if isinstance(renderable, str): 97 renderable = console.render_str(renderable, markup=options.markup) 98 if hasattr(renderable, "__rich__"): 99 renderable = renderable.__rich__() # type: ignore 100 if is_renderable(renderable): 101 get_console_width = getattr(renderable, "__rich_measure__", None) 102 if get_console_width is not None: 103 render_width = ( 104 get_console_width(console, options) 105 .normalize() 106 .with_maximum(_max_width) 107 ) 108 if render_width.maximum < 1: 109 return Measurement(0, 0) 110 return render_width.normalize() 111 else: 112 return Measurement(0, _max_width) 113 else: 114 raise errors.NotRenderableError( 115 f"Unable to get render width for {renderable!r}; " 116 "a str, Segment, or object with __rich_console__ method is required" 117 ) 118 119 120def measure_renderables( 121 console: "Console", 122 options: "ConsoleOptions", 123 renderables: Iterable["RenderableType"], 124) -> "Measurement": 125 """Get a measurement that would fit a number of renderables. 126 127 Args: 128 console (~rich.console.Console): Console instance. 129 renderables (Iterable[RenderableType]): One or more renderable objects. 130 max_width (int): The maximum width available. 131 132 Returns: 133 Measurement: Measurement object containing range of character widths required to 134 contain all given renderables. 135 """ 136 if not renderables: 137 return Measurement(0, 0) 138 get_measurement = Measurement.get 139 measurements = [ 140 get_measurement(console, options, renderable) for renderable in renderables 141 ] 142 measured_width = Measurement( 143 max(measurements, key=itemgetter(0)).minimum, 144 max(measurements, key=itemgetter(1)).maximum, 145 ) 146 return measured_width 147