Merge branch 'main' into move-child-no-op

This commit is contained in:
Rodrigo Girão Serrão
2023-05-15 10:31:43 +01:00
committed by GitHub
13 changed files with 2853 additions and 2699 deletions

View File

@@ -6,12 +6,21 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased
## Unrealeased
### Changed
- App `title` and `sub_title` attributes can be set to any type https://github.com/Textualize/textual/issues/2521
- Using `Widget.move_child` where the target and the child being moved are the same is now a no-op https://github.com/Textualize/textual/issues/1743
### Fixed
- Fixed `ZeroDivisionError` in `resolve_fraction_unit` https://github.com/Textualize/textual/issues/2502
- Fixed `TreeNode.expand` and `TreeNode.expand_all` not posting a `Tree.NodeExpanded` message https://github.com/Textualize/textual/issues/2535
- Fixed `TreeNode.collapse` and `TreeNode.collapse_all` not posting a `Tree.NodeCollapsed` message https://github.com/Textualize/textual/issues/2535
- Fixed `TreeNode.toggle` and `TreeNode.toggle_all` not posting a `Tree.NodeExpanded` or `Tree.NodeCollapsed` message https://github.com/Textualize/textual/issues/2535
- `footer--description` component class was being ignored https://github.com/Textualize/textual/issues/2544
## [0.24.1] - 2023-05-08
### Fixed

BIN
reference/spacing.monopic Normal file

Binary file not shown.

View File

@@ -143,7 +143,7 @@ def resolve_fraction_unit(
resolved: list[Fraction | None] = [None] * len(resolve)
remaining_fraction = Fraction(sum(scalar.value for scalar, _, _ in resolve))
while True:
while remaining_fraction > 0:
remaining_space_changed = False
resolve_fraction = Fraction(remaining_space, remaining_fraction)
for index, (scalar, min_value, max_value) in enumerate(resolve):
@@ -166,7 +166,7 @@ def resolve_fraction_unit(
return (
Fraction(remaining_space, remaining_fraction)
if remaining_space
if remaining_space > 0
else Fraction(1)
)

View File

@@ -316,6 +316,7 @@ class App(Generic[ReturnType], DOMNode):
the name of the app if it doesn't.
Assign a new value to this attribute to change the title.
The new value is always converted to string.
"""
self.sub_title = self.SUB_TITLE if self.SUB_TITLE is not None else ""
@@ -328,6 +329,7 @@ class App(Generic[ReturnType], DOMNode):
the file being worker on.
Assign a new value to this attribute to change the sub-title.
The new value is always converted to string.
"""
self._logger = Logger(self._log)
@@ -406,6 +408,14 @@ class App(Generic[ReturnType], DOMNode):
self.set_class(self.dark, "-dark-mode")
self.set_class(not self.dark, "-light-mode")
def validate_title(self, title: Any) -> str:
"""Make sure the title is set to a string."""
return str(title)
def validate_sub_title(self, sub_title: Any) -> str:
"""Make sure the sub-title is set to a string."""
return str(sub_title)
@property
def workers(self) -> WorkerManager:
"""The [worker](guide/workers/) manager.

View File

@@ -887,7 +887,7 @@ class DOMNode(MessagePump):
Example:
Here's how you could detect when the app changes from dark to light mode (and visa versa).
Here's how you could detect when the app changes from dark to light mode (and vice versa).
```python
def on_dark_change(old_value:bool, new_value:bool):

View File

@@ -839,7 +839,7 @@ class Region(NamedTuple):
@lru_cache(maxsize=1024)
def split_horizontal(self, cut: int) -> tuple[Region, Region]:
"""Split a region in to two, from a given x offset.
"""Split a region in to two, from a given y offset.
```
┌─────────┐
@@ -852,8 +852,8 @@ class Region(NamedTuple):
```
Args:
cut: An offset from self.x where the cut should be made. May be negative,
for the offset to start from the right edge.
cut: An offset from self.y where the cut should be made. May be negative,
for the offset to start from the lower edge.
Returns:
Two regions, which add up to the original (self).
@@ -909,7 +909,19 @@ class Region(NamedTuple):
class Spacing(NamedTuple):
"""The spacing around a renderable, such as padding and border
Spacing is defined by four integers for the space at the top, right, bottom, and left of a region,
Spacing is defined by four integers for the space at the top, right, bottom, and left of a region.
```
┌ ─ ─ ─ ─ ─ ─ ─▲─ ─ ─ ─ ─ ─ ─ ─ ┐
│ top
│ ┏━━━━━▼━━━━━━┓ │
◀──────▶┃ ┃◀───────▶
│ left ┃ ┃ right │
┃ ┃
│ ┗━━━━━▲━━━━━━┛ │
│ bottom
└ ─ ─ ─ ─ ─ ─ ─▼─ ─ ─ ─ ─ ─ ─ ─ ┘
```
Example:
```python

View File

@@ -379,16 +379,22 @@ class Strip:
"""
pos = 0
cell_length = self.cell_length
cuts = [cut for cut in cuts if cut <= cell_length]
cache_key = tuple(cuts)
cached = self._divide_cache.get(cache_key)
if cached is not None:
return cached
strips: list[Strip] = []
add_strip = strips.append
for segments, cut in zip(Segment.divide(self._segments, cuts), cuts):
add_strip(Strip(segments, cut - pos))
pos = cut
strips: list[Strip]
if cuts == [cell_length]:
strips = [self]
else:
strips = []
add_strip = strips.append
for segments, cut in zip(Segment.divide(self._segments, cuts), cuts):
add_strip(Strip(segments, cut - pos))
pos = cut
self._divide_cache[cache_key] = strips
return strips

View File

@@ -98,6 +98,7 @@ class Footer(Widget):
highlight_style = self.get_component_rich_style("footer--highlight")
highlight_key_style = self.get_component_rich_style("footer--highlight-key")
key_style = self.get_component_rich_style("footer--key")
description_style = self.get_component_rich_style("footer--description")
bindings = [
binding
@@ -122,7 +123,7 @@ class Footer(Widget):
(f" {key_display} ", highlight_key_style if hovered else key_style),
(
f" {binding.description} ",
highlight_style if hovered else base_style,
highlight_style if hovered else base_style + description_style,
),
meta={
"@click": f"app.check_bindings('{binding.key}')",

View File

@@ -207,6 +207,7 @@ class TreeNode(Generic[TreeDataType]):
"""
self._expanded = True
self._updates += 1
self._tree.post_message(Tree.NodeExpanded(self._tree, self))
if expand_all:
for child in self.children:
child._expand(expand_all)
@@ -239,6 +240,7 @@ class TreeNode(Generic[TreeDataType]):
"""
self._expanded = False
self._updates += 1
self._tree.post_message(Tree.NodeCollapsed(self._tree, self))
if collapse_all:
for child in self.children:
child._collapse(collapse_all)
@@ -1157,10 +1159,8 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
return
if node.is_expanded:
node.collapse()
self.post_message(self.NodeCollapsed(self, node))
else:
node.expand()
self.post_message(self.NodeExpanded(self, node))
async def _on_click(self, event: events.Click) -> None:
meta = event.style.meta

File diff suppressed because one or more lines are too long

View File

@@ -36,3 +36,33 @@ async def test_hover_update_styles():
# We've hovered, so ensure the pseudoclass is present and background changed
assert button.pseudo_classes == {"enabled", "hover"}
assert button.styles.background != initial_background
def test_setting_title():
app = MyApp()
app.title = None
assert app.title == "None"
app.title = ""
assert app.title == ""
app.title = 0.125
assert app.title == "0.125"
app.title = [True, False, 2]
assert app.title == "[True, False, 2]"
def test_setting_sub_title():
app = MyApp()
app.sub_title = None
assert app.sub_title == "None"
app.sub_title = ""
assert app.sub_title == ""
app.sub_title = 0.125
assert app.sub_title == "0.125"
app.sub_title = [True, False, 2]
assert app.sub_title == "[True, False, 2]"

View File

@@ -1,4 +1,5 @@
from fractions import Fraction
from itertools import chain
import pytest
@@ -122,3 +123,22 @@ def test_resolve_fraction_unit():
Fraction(32),
resolve_dimension="width",
) == Fraction(2)
def test_resolve_issue_2502():
"""Test https://github.com/Textualize/textual/issues/2502"""
widget = Widget()
widget.styles.width = "1fr"
widget.styles.min_width = 11
assert isinstance(
resolve_fraction_unit(
(widget.styles,),
Size(80, 24),
Size(80, 24),
Fraction(10),
resolve_dimension="width",
),
Fraction,
)

View File

@@ -78,6 +78,20 @@ async def test_tree_node_expanded_message() -> None:
assert pilot.app.messages == [("NodeExpanded", "test-tree")]
async def tree_node_expanded_by_code_message() -> None:
"""Expanding a node via the API should result in an expanded message being posted."""
async with TreeApp().run_test() as pilot:
pilot.app.query_one(Tree).root.children[0].expand()
assert pilot.app.messages == [("NodeExpanded", "test-tree")]
async def tree_node_all_expanded_by_code_message() -> None:
"""Expanding all nodes via the API should result in expanded messages being posted."""
async with TreeApp().run_test() as pilot:
pilot.app.query_one(Tree).root.children[0].expand_all()
assert pilot.app.messages == [("NodeExpanded", "test-tree")]
async def test_tree_node_collapsed_message() -> None:
"""Collapsing a node should result in a collapsed message being emitted."""
async with TreeApp().run_test() as pilot:
@@ -89,6 +103,46 @@ async def test_tree_node_collapsed_message() -> None:
]
async def tree_node_collapsed_by_code_message() -> None:
"""Collapsing a node via the API should result in a collapsed message being posted."""
async with TreeApp().run_test() as pilot:
pilot.app.query_one(Tree).root.children[0].expand().collapse()
assert pilot.app.messages == [
("NodeExpanded", "test-tree"),
("NodeCollapsed", "test-tree"),
]
async def tree_node_all_collapsed_by_code_message() -> None:
"""Collapsing all nodes via the API should result in collapsed messages being posted."""
async with TreeApp().run_test() as pilot:
pilot.app.query_one(Tree).root.children[0].expand_all().collapse_all()
assert pilot.app.messages == [
("NodeExpanded", "test-tree"),
("NodeCollapsed", "test-tree"),
]
async def tree_node_toggled_by_code_message() -> None:
"""Toggling a node twice via the API should result in expanded and collapsed messages."""
async with TreeApp().run_test() as pilot:
pilot.app.query_one(Tree).root.children[0].toggle().toggle()
assert pilot.app.messages == [
("NodeExpanded", "test-tree"),
("NodeCollapsed", "test-tree"),
]
async def tree_node_all_toggled_by_code_message() -> None:
"""Toggling all nodes twice via the API should result in expanded and collapsed messages."""
async with TreeApp().run_test() as pilot:
pilot.app.query_one(Tree).root.children[0].toggle_all().toggle_all()
assert pilot.app.messages == [
("NodeExpanded", "test-tree"),
("NodeCollapsed", "test-tree"),
]
async def test_tree_node_highlighted_message() -> None:
"""Highlighting a node should result in a highlighted message being emitted."""
async with TreeApp().run_test() as pilot: