tests for geometry

This commit is contained in:
Will McGugan
2022-02-23 10:56:06 +00:00
parent cc945b2ca7
commit 3ee5eb8a2b
4 changed files with 103 additions and 68 deletions

View File

@@ -485,7 +485,7 @@ class Spacing(NamedTuple):
left: int = 0 left: int = 0
def __bool__(self) -> bool: def __bool__(self) -> bool:
return self == (0, 0, 0, 0) return self != (0, 0, 0, 0)
@property @property
def width(self) -> int: def width(self) -> int:
@@ -507,16 +507,6 @@ class Spacing(NamedTuple):
"""Bottom right space.""" """Bottom right space."""
return (self.right, self.bottom) return (self.right, self.bottom)
@property
def packed(self) -> str:
top, right, bottom, left = self
if top == right == bottom == left:
return f"{top}"
if (top, right) == (bottom, left):
return f"{top}, {right}"
else:
return f"{top}, {right}, {bottom}, {left}"
@property @property
def css(self) -> str: def css(self) -> str:
top, right, bottom, left = self top, right, bottom, left = self
@@ -532,16 +522,17 @@ class Spacing(NamedTuple):
"""Unpack padding specified in CSS style.""" """Unpack padding specified in CSS style."""
if isinstance(pad, int): if isinstance(pad, int):
return cls(pad, pad, pad, pad) return cls(pad, pad, pad, pad)
if len(pad) == 1: pad_len = len(pad)
if pad_len == 1:
_pad = pad[0] _pad = pad[0]
return cls(_pad, _pad, _pad, _pad) return cls(_pad, _pad, _pad, _pad)
if len(pad) == 2: if pad_len == 2:
pad_top, pad_right = cast(Tuple[int, int], pad) pad_top, pad_right = cast(Tuple[int, int], pad)
return cls(pad_top, pad_right, pad_top, pad_right) return cls(pad_top, pad_right, pad_top, pad_right)
if len(pad) == 4: if pad_len == 4:
top, right, bottom, left = cast(Tuple[int, int, int, int], pad) top, right, bottom, left = cast(Tuple[int, int, int, int], pad)
return cls(top, right, bottom, left) return cls(top, right, bottom, left)
raise ValueError(f"1, 2 or 4 integers required for spacing; {len(pad)} given") raise ValueError(f"1, 2 or 4 integers required for spacing; {pad_len} given")
def __add__(self, other: object) -> Spacing: def __add__(self, other: object) -> Spacing:
if isinstance(other, tuple): if isinstance(other, tuple):

View File

@@ -27,12 +27,7 @@ if TYPE_CHECKING:
class NoWidget(Exception): class NoWidget(Exception):
pass """Raised when there is no widget at the requested coordinate."""
class OrderedRegion(NamedTuple):
region: Region
order: tuple[int, int]
class ReflowResult(NamedTuple): class ReflowResult(NamedTuple):
@@ -44,9 +39,10 @@ class ReflowResult(NamedTuple):
class WidgetPlacement(NamedTuple): class WidgetPlacement(NamedTuple):
"""The position, size, and relative order of a widget within its parent."""
region: Region region: Region
widget: Widget | None = None widget: Widget | None = None # A widget of None means empty space
order: int = 0 order: int = 0
def apply_margin(self) -> "WidgetPlacement": def apply_margin(self) -> "WidgetPlacement":
@@ -61,7 +57,7 @@ class WidgetPlacement(NamedTuple):
region, widget, order = self region, widget, order = self
if widget is not None: if widget is not None:
styles = widget.styles styles = widget.styles
if any(styles.margin): if styles.margin:
return WidgetPlacement( return WidgetPlacement(
region=region.shrink(styles.margin), region=region.shrink(styles.margin),
widget=widget, widget=widget,
@@ -72,6 +68,8 @@ class WidgetPlacement(NamedTuple):
@rich.repr.auto @rich.repr.auto
class LayoutUpdate: class LayoutUpdate:
"""A renderable containing the result of a render for a given region."""
def __init__(self, lines: Lines, region: Region) -> None: def __init__(self, lines: Lines, region: Region) -> None:
self.lines = lines self.lines = lines
self.region = region self.region = region
@@ -79,12 +77,12 @@ class LayoutUpdate:
def __rich_console__( def __rich_console__(
self, console: Console, options: ConsoleOptions self, console: Console, options: ConsoleOptions
) -> RenderResult: ) -> RenderResult:
yield Control.home().segment yield Control.home()
x = self.region.x x = self.region.x
new_line = Segment.line() new_line = Segment.line()
move_to = Control.move_to move_to = Control.move_to
for last, (y, line) in loop_last(enumerate(self.lines, self.region.y)): for last, (y, line) in loop_last(enumerate(self.lines, self.region.y)):
yield move_to(x, y).segment yield move_to(x, y)
yield from line yield from line
if not last: if not last:
yield new_line yield new_line
@@ -187,7 +185,8 @@ class Layout(ABC):
view.mount(*widgets) view.mount(*widgets)
@property @property
def map(self) -> LayoutMap | None: def map(self) -> LayoutMap:
assert self._layout_map is not None
return self._layout_map return self._layout_map
def __iter__(self) -> Iterator[tuple[Widget, Region, Region]]: def __iter__(self) -> Iterator[tuple[Widget, Region, Region]]:

View File

@@ -99,44 +99,6 @@ class Widget(DOMNode):
yield "hover" yield "hover"
# TODO: focus # TODO: focus
def get_child_by_id(self, id: str) -> Widget:
"""Get a child with a given id.
Args:
id (str): A Widget id.
Raises:
errors.MissingWidget: If the widget was not found.
Returns:
Widget: A child widget.
"""
for widget in self.children:
if widget.id == id:
return cast(Widget, widget)
raise errors.MissingWidget(f"Widget with id=={id!r} was not found in {self}")
def get_child_by_name(self, name: str) -> Widget:
"""Get a child widget with a given name.
Args:
name (str): A name. Defaults to None.
Raises:
errors.MissingWidget: If no Widget is found.
Returns:
Widget: A Widget with the given name.
"""
for widget in self.children:
if widget.name == name:
return cast(Widget, widget)
raise errors.MissingWidget(
f"Widget with name=={name!r} was not found in {self}"
)
def watch(self, attribute_name, callback: Callable[[Any], Awaitable[None]]) -> None: def watch(self, attribute_name, callback: Callable[[Any], Awaitable[None]]) -> None:
watch(self, attribute_name, callback) watch(self, attribute_name, callback)

View File

@@ -63,31 +63,52 @@ def test_clamp():
assert clamp(5, 10, 0) == 5 assert clamp(5, 10, 0) == 5
def test_point_is_origin(): def test_offset_bool():
assert Offset(1, 0)
assert Offset(0, 1)
assert Offset(0, -1)
assert not Offset(0, 0)
def test_offset_is_origin():
assert Offset(0, 0).is_origin assert Offset(0, 0).is_origin
assert not Offset(1, 0).is_origin assert not Offset(1, 0).is_origin
def test_point_add(): def test_offset_add():
assert Offset(1, 1) + Offset(2, 2) == Offset(3, 3) assert Offset(1, 1) + Offset(2, 2) == Offset(3, 3)
assert Offset(1, 2) + Offset(3, 4) == Offset(4, 6) assert Offset(1, 2) + Offset(3, 4) == Offset(4, 6)
with pytest.raises(TypeError): with pytest.raises(TypeError):
Offset(1, 1) + "foo" Offset(1, 1) + "foo"
def test_point_sub(): def test_offset_sub():
assert Offset(1, 1) - Offset(2, 2) == Offset(-1, -1) assert Offset(1, 1) - Offset(2, 2) == Offset(-1, -1)
assert Offset(3, 4) - Offset(2, 1) == Offset(1, 3) assert Offset(3, 4) - Offset(2, 1) == Offset(1, 3)
with pytest.raises(TypeError): with pytest.raises(TypeError):
Offset(1, 1) - "foo" Offset(1, 1) - "foo"
def test_point_blend(): def test_offset_mul():
assert Offset(2, 1) * 2 == Offset(4, 2)
assert Offset(2, 1) * -2 == Offset(-4, -2)
assert Offset(2, 1) * 0 == Offset(0, 0)
with pytest.raises(TypeError):
Offset(10, 20) * "foo"
def test_offset_blend():
assert Offset(1, 2).blend(Offset(3, 4), 0) == Offset(1, 2) assert Offset(1, 2).blend(Offset(3, 4), 0) == Offset(1, 2)
assert Offset(1, 2).blend(Offset(3, 4), 1) == Offset(3, 4) assert Offset(1, 2).blend(Offset(3, 4), 1) == Offset(3, 4)
assert Offset(1, 2).blend(Offset(3, 4), 0.5) == Offset(2, 3) assert Offset(1, 2).blend(Offset(3, 4), 0.5) == Offset(2, 3)
def test_offset_get_distance_to():
assert Offset(20, 30).get_distance_to(Offset(20, 30)) == 0
assert Offset(0, 0).get_distance_to(Offset(1, 0)) == 1.0
assert Offset(2, 1).get_distance_to(Offset(5, 5)) == 5.0
def test_region_null(): def test_region_null():
assert Region() == Region(0, 0, 0, 0) assert Region() == Region(0, 0, 0, 0)
assert not Region() assert not Region()
@@ -196,10 +217,14 @@ def test_region_union():
def test_size_add(): def test_size_add():
assert Size(5, 10) + Size(2, 3) == Size(7, 13) assert Size(5, 10) + Size(2, 3) == Size(7, 13)
with pytest.raises(TypeError):
Size(1, 2) + "foo"
def test_size_sub(): def test_size_sub():
assert Size(5, 10) - Size(2, 3) == Size(3, 7) assert Size(5, 10) - Size(2, 3) == Size(3, 7)
with pytest.raises(TypeError):
Size(1, 2) - "foo"
def test_region_x_extents(): def test_region_x_extents():
@@ -224,3 +249,61 @@ def test_region_x_range():
def test_region_y_range(): def test_region_y_range():
assert Region(5, 10, 20, 30).y_range == range(10, 40) assert Region(5, 10, 20, 30).y_range == range(10, 40)
def test_region_expand():
assert Region(50, 10, 10, 5).expand((2, 3)) == Region(48, 7, 14, 11)
def test_spacing_bool():
assert Spacing(1, 0, 0, 0)
assert Spacing(0, 1, 0, 0)
assert Spacing(0, 1, 0, 0)
assert Spacing(0, 0, 1, 0)
assert Spacing(0, 0, 0, 1)
assert not Spacing(0, 0, 0, 0)
def test_spacing_width():
assert Spacing(2, 3, 4, 5).width == 8
def test_spacing_height():
assert Spacing(2, 3, 4, 5).height == 6
def test_spacing_top_left():
assert Spacing(2, 3, 4, 5).top_left == (5, 2)
def test_spacing_bottom_right():
assert Spacing(2, 3, 4, 5).bottom_right == (3, 4)
def test_spacing_css():
assert Spacing(1, 1, 1, 1).css == "1"
assert Spacing(1, 2, 1, 2).css == "1 2"
assert Spacing(1, 2, 3, 4).css == "1 2 3 4"
def test_spacing_unpack():
assert Spacing.unpack(1) == Spacing(1, 1, 1, 1)
assert Spacing.unpack((1,)) == Spacing(1, 1, 1, 1)
assert Spacing.unpack((1, 2)) == Spacing(1, 2, 1, 2)
assert Spacing.unpack((1, 2, 3, 4)) == Spacing(1, 2, 3, 4)
with pytest.raises(ValueError):
assert Spacing.unpack(()) == Spacing(1, 2, 1, 2)
with pytest.raises(ValueError):
assert Spacing.unpack((1, 2, 3)) == Spacing(1, 2, 1, 2)
with pytest.raises(ValueError):
assert Spacing.unpack((1, 2, 3, 4, 5)) == Spacing(1, 2, 1, 2)
def test_spacing_add():
assert Spacing(1, 2, 3, 4) + Spacing(5, 6, 7, 8) == Spacing(6, 8, 10, 12)
with pytest.raises(TypeError):
Spacing(1, 2, 3, 4) + "foo"