Merge branch 'main' into package-docs

This commit is contained in:
Dave Pearson 2023-02-08 10:40:20 +00:00
commit 86e1997b30
No known key found for this signature in database
GPG key ID: B413E0EF113D4ABF
74 changed files with 969 additions and 297 deletions

1
docs/api/scroll_view.md Normal file
View file

@ -0,0 +1 @@
::: textual.scroll_view.ScrollView

1
docs/api/strip.md Normal file
View file

@ -0,0 +1 @@
::: textual.strip.Strip

View file

@ -0,0 +1,43 @@
from rich.segment import Segment
from rich.style import Style
from textual.app import App, ComposeResult
from textual.strip import Strip
from textual.widget import Widget
class CheckerBoard(Widget):
"""Render an 8x8 checkerboard."""
def render_line(self, y: int) -> Strip:
"""Render a line of the widget. y is relative to the top of the widget."""
row_index = y // 4 # A checkerboard square consists of 4 rows
if row_index >= 8: # Generate blank lines when we reach the end
return Strip.blank(self.size.width)
is_odd = row_index % 2 # Used to alternate the starting square on each row
white = Style.parse("on white") # Get a style object for a white background
black = Style.parse("on black") # Get a style object for a black background
# Generate a list of segments with alternating black and white space characters
segments = [
Segment(" " * 8, black if (column + is_odd) % 2 else white)
for column in range(8)
]
strip = Strip(segments, 8 * 8)
return strip
class BoardApp(App):
"""A simple app to show our widget."""
def compose(self) -> ComposeResult:
yield CheckerBoard()
if __name__ == "__main__":
app = BoardApp()
app.run()

View file

@ -0,0 +1,55 @@
from rich.segment import Segment
from textual.app import App, ComposeResult
from textual.strip import Strip
from textual.widget import Widget
class CheckerBoard(Widget):
"""Render an 8x8 checkerboard."""
COMPONENT_CLASSES = {
"checkerboard--white-square",
"checkerboard--black-square",
}
DEFAULT_CSS = """
CheckerBoard .checkerboard--white-square {
background: #A5BAC9;
}
CheckerBoard .checkerboard--black-square {
background: #004578;
}
"""
def render_line(self, y: int) -> Strip:
"""Render a line of the widget. y is relative to the top of the widget."""
row_index = y // 4 # four lines per row
if row_index >= 8:
return Strip.blank(self.size.width)
is_odd = row_index % 2
white = self.get_component_rich_style("checkerboard--white-square")
black = self.get_component_rich_style("checkerboard--black-square")
segments = [
Segment(" " * 8, black if (column + is_odd) % 2 else white)
for column in range(8)
]
strip = Strip(segments, 8 * 8)
return strip
class BoardApp(App):
"""A simple app to show our widget."""
def compose(self) -> ComposeResult:
yield CheckerBoard()
if __name__ == "__main__":
app = BoardApp()
app.run()

View file

@ -0,0 +1,64 @@
from __future__ import annotations
from textual.app import App, ComposeResult
from textual.geometry import Size
from textual.strip import Strip
from textual.scroll_view import ScrollView
from rich.segment import Segment
class CheckerBoard(ScrollView):
COMPONENT_CLASSES = {
"checkerboard--white-square",
"checkerboard--black-square",
}
DEFAULT_CSS = """
CheckerBoard .checkerboard--white-square {
background: #A5BAC9;
}
CheckerBoard .checkerboard--black-square {
background: #004578;
}
"""
def __init__(self, board_size: int) -> None:
super().__init__()
self.board_size = board_size
# Each square is 4 rows and 8 columns
self.virtual_size = Size(board_size * 8, board_size * 4)
def render_line(self, y: int) -> Strip:
"""Render a line of the widget. y is relative to the top of the widget."""
scroll_x, scroll_y = self.scroll_offset # The current scroll position
y += scroll_y # The line at the top of the widget is now `scroll_y`, not zero!
row_index = y // 4 # four lines per row
white = self.get_component_rich_style("checkerboard--white-square")
black = self.get_component_rich_style("checkerboard--black-square")
if row_index >= self.board_size:
return Strip.blank(self.size.width)
is_odd = row_index % 2
segments = [
Segment(" " * 8, black if (column + is_odd) % 2 else white)
for column in range(self.board_size)
]
strip = Strip(segments, self.board_size * 8)
# Crop the strip so that is covers the visible area
strip = strip.crop(scroll_x, scroll_x + self.size.width)
return strip
class BoardApp(App):
def compose(self) -> ComposeResult:
yield CheckerBoard(100)
if __name__ == "__main__":
app = BoardApp()
app.run()

View file

@ -0,0 +1,106 @@
from __future__ import annotations
from textual import events
from textual.app import App, ComposeResult
from textual.geometry import Offset, Region, Size
from textual.reactive import var
from textual.strip import Strip
from textual.scroll_view import ScrollView
from rich.segment import Segment
from rich.style import Style
class CheckerBoard(ScrollView):
COMPONENT_CLASSES = {
"checkerboard--white-square",
"checkerboard--black-square",
"checkerboard--cursor-square",
}
DEFAULT_CSS = """
CheckerBoard > .checkerboard--white-square {
background: #A5BAC9;
}
CheckerBoard > .checkerboard--black-square {
background: #004578;
}
CheckerBoard > .checkerboard--cursor-square {
background: darkred;
}
"""
cursor_square = var(Offset(0, 0))
def __init__(self, board_size: int) -> None:
super().__init__()
self.board_size = board_size
# Each square is 4 rows and 8 columns
self.virtual_size = Size(board_size * 8, board_size * 4)
def on_mouse_move(self, event: events.MouseMove) -> None:
"""Called when the user moves the mouse over the widget."""
mouse_position = event.offset + self.scroll_offset
self.cursor_square = Offset(mouse_position.x // 8, mouse_position.y // 4)
def watch_cursor_square(
self, previous_square: Offset, cursor_square: Offset
) -> None:
"""Called when the cursor square changes."""
def get_square_region(square_offset: Offset) -> Region:
"""Get region relative to widget from square coordinate."""
x, y = square_offset
region = Region(x * 8, y * 4, 8, 4)
# Move the region in to the widgets frame of reference
region = region.translate(-self.scroll_offset)
return region
# Refresh the previous cursor square
self.refresh(get_square_region(previous_square))
# Refresh the new cursor square
self.refresh(get_square_region(cursor_square))
def render_line(self, y: int) -> Strip:
"""Render a line of the widget. y is relative to the top of the widget."""
scroll_x, scroll_y = self.scroll_offset # The current scroll position
y += scroll_y # The line at the top of the widget is now `scroll_y`, not zero!
row_index = y // 4 # four lines per row
white = self.get_component_rich_style("checkerboard--white-square")
black = self.get_component_rich_style("checkerboard--black-square")
cursor = self.get_component_rich_style("checkerboard--cursor-square")
if row_index >= self.board_size:
return Strip.blank(self.size.width)
is_odd = row_index % 2
def get_square_style(column: int, row: int) -> Style:
"""Get the cursor style at the given position on the checkerboard."""
if self.cursor_square == Offset(column, row):
square_style = cursor
else:
square_style = black if (column + is_odd) % 2 else white
return square_style
segments = [
Segment(" " * 8, get_square_style(column, row_index))
for column in range(self.board_size)
]
strip = Strip(segments, self.board_size * 8)
# Crop the strip so that is covers the visible area
strip = strip.crop(scroll_x, scroll_x + self.size.width)
return strip
class BoardApp(App):
def compose(self) -> ComposeResult:
yield CheckerBoard(100)
if __name__ == "__main__":
app = BoardApp()
app.run()

View file

@ -200,4 +200,191 @@ TODO: Explanation of compound widgets
## Line API
TODO: Explanation of line API
A downside of widgets that return Rich renderables is that Textual will redraw the entire widget when its state is updated or it changes size.
If a widget is large enough to require scrolling, or updates frequently, then this redrawing can make your app feel less responsive.
Textual offers an alternative API which reduces the amount of work required to refresh a widget, and makes it possible to update portions of a widget (as small as a single character) without a full redraw. This is known as the *line API*.
!!! note
The Line API requires a little more work that typical Rich renderables, but can produce powerful widgets such as the builtin [DataTable](./../widgets/data_table.md) which can handle thousands or even millions of rows.
### Render Line method
To build a widget with the line API, implement a `render_line` method rather than a `render` method. The `render_line` method takes a single integer argument `y` which is an offset from the top of the widget, and should return a [Strip][textual.strip.Strip] object containing that line's content.
Textual will call this method as required to get content for every row of characters in the widget.
<div class="excalidraw">
--8<-- "docs/images/render_line.excalidraw.svg"
</div>
Let's look at an example before we go in to the details. The following Textual app implements a widget with the line API that renders a checkerboard pattern. This might form the basis of a chess / checkers game. Here's the code:
=== "checker01.py"
```python title="checker01.py" hl_lines="12-31"
--8<-- "docs/examples/guide/widgets/checker01.py"
```
=== "Output"
```{.textual path="docs/examples/guide/widgets/checker01.py"}
```
The `render_line` method above calculates a `Strip` for every row of characters in the widget. Each strip contains alternating black and white space characters which form the squares in the checkerboard.
You may have noticed that the checkerboard widget makes use of some objects we haven't covered before. Let's explore those.
#### Segment and Style
A [Segment](https://rich.readthedocs.io/en/latest/protocol.html#low-level-render) is a class borrowed from the [Rich](https://github.com/Textualize/rich) project. It is small object (actually a named tuple) which bundles a string to be displayed and a [Style](https://rich.readthedocs.io/en/latest/style.html) which tells Textual how the text should look (color, bold, italic etc).
Let's look at a simple segment which would produce the text "Hello, World!" in bold.
```python
greeting = Segment("Hello, World!", Style(bold=True))
```
This would create the following object:
<div class="excalidraw">
--8<-- "docs/images/segment.excalidraw.svg"
</div>
Both Rich and Textual work with segments to generate content. A Textual app is the result of combining hundreds, or perhaps thousands, of segments,
#### Strips
A [Strip][textual.strip.Strip] is a container for a number of segments covering a single *line* (or row) in the Widget. A Strip will contain at least one segment, but often many more.
A `Strip` is constructed from a list of `Segment` objects. Here's now you might construct a strip that displays the text "Hello, World!", but with the second word in bold:
```python
segments = [
Segment("Hello, "),
Segment("World", Style(bold=True)),
Segment("!")
]
strip = Strip(segments)
```
The first and third `Segment` omit a style, which results in the widget's default style being used. The second segment has a style object which applies bold to the text "World". If this were part of a widget it would produce the text: <code>Hello, **World**!</code>
The `Strip` constructor has an optional second parameter, which should be the *cell length* of the strip. The strip above has a length of 13, so we could have constructed it like this:
```python
strip = Strip(segments, 13)
```
Note that the cell length parameter is _not_ the total number of characters in the string. It is the number of terminal "cells". Some characters (such as Asian language characters and certain emoji) take up the space of two Western alphabet characters. If you don't know in advance the number of cells your segments will occupy, it is best to omit the length parameter so that Textual calculates it automatically.
### Component classes
When applying styles to widgets we can use CSS to select the child widgets. Widgets rendered with the line API don't have children per-se, but we can still use CSS to apply styles to parts of our widget by defining *component classes*. Component classes are associated with a widget by defining a `COMPONENT_CLASSES` class variable which should be a `set` of strings containing CSS class names.
In the checkerboard example above we hard-coded the color of the squares to "white" and "black". But what if we want to create a checkerboard with different colors? We can do this by defining two component classes, one for the "white" squares and one for the "dark" squares. This will allow us to change the colors with CSS.
The following example replaces our hard-coded colors with component classes.
=== "checker02.py"
```python title="checker02.py" hl_lines="11-13 16-23 35-36"
--8<-- "docs/examples/guide/widgets/checker02.py"
```
=== "Output"
```{.textual path="docs/examples/guide/widgets/checker02.py"}
```
The `COMPONENT_CLASSES` class variable above adds two class names: `checkerboard--white-square` and `checkerboard--black-square`. These are set in the `DEFAULT_CSS` but can modified in the app's `CSS` class variable or external CSS.
!!! tip
Component classes typically begin with the name of the widget followed by *two* hyphens. This is a convention to avoid potential name clashes.
The `render_line` method calls [get_component_rich_style][textual.widget.Widget.get_component_rich_style] to get `Style` objects from the CSS, which we apply to the segments to create a more colorful looking checkerboard.
### Scrolling
A Line API widget can be made to scroll by extending the [ScrollView][textual.scroll_view.ScrollView] class (rather than `Widget`).
The `ScrollView` class will do most of the work, but we will need to manage the following details:
1. The `ScrollView` class requires a *virtual size*, which is the size of the scrollable content and should be set via the `virtual_size` property. If this is larger than the widget then Textual will add scrollbars.
2. We need to update the `render_line` method to generate strips for the visible area of the widget, taking into account the current position of the scrollbars.
Let's add scrolling to our checkerboard example. A standard 8 x 8 board isn't sufficient to demonstrate scrolling so we will make the size of the board configurable and set it to 100 x 100, for a total of 10,000 squares.
=== "checker03.py"
```python title="checker03.py" hl_lines="4 26-30 35-36 52-53"
--8<-- "docs/examples/guide/widgets/checker03.py"
```
=== "Output"
```{.textual path="docs/examples/guide/widgets/checker03.py"}
```
The virtual size is set in the constructor to match the total size of the board, which will enable scrollbars (unless you have your terminal zoomed out very far). You can update the `virtual_size` attribute dynamically as required, but our checkerboard isn't going to change size so we only need to set it once.
The `render_line` method gets the *scroll offset* which is an [Offset][textual.geometry.Offset] containing the current position of the scrollbars. We add `scroll_offset.y` to the `y` argument because `y` is relative to the top of the widget, and we need a Y coordinate relative to the scrollable content.
We also need to compensate for the position of the horizontal scrollbar. This is done in the call to `strip.crop` which *crops* the strip to the visible area between `scroll_x` and `scroll_x + self.size.width`.
!!! tip
[Strip][textual.strip.Strip] objects are immutable, so methods will return a new Strip rather than modifying the original.
<div class="excalidraw">
--8<-- "docs/images/scroll_view.excalidraw.svg"
</div>
### Region updates
The Line API makes it possible to refresh parts of a widget, as small as a single character.
Refreshing smaller regions makes updates more efficient, and keeps your widget feeling responsive.
To demonstrate this we will update the checkerboard to highlight the square under the mouse pointer.
Here's the code:
=== "checker04.py"
```python title="checker04.py" hl_lines="18 28-30 33 41-44 46-63 74 81-92"
--8<-- "docs/examples/guide/widgets/checker04.py"
```
=== "Output"
```{.textual path="docs/examples/guide/widgets/checker04.py"}
```
We've added a style to the checkerboard which is the color of the highlighted square, with a default of "darkred".
We will need this when we come to render the highlighted square.
We've also added a [reactive variable](./reactivity.md) called `cursor_square` which will hold the coordinate of the square underneath the mouse. Note that we have used [var][textual.reactive.var] which gives us reactive superpowers but won't automatically refresh the whole widget, because we want to update only the squares under the cursor.
The `on_mouse_move` handler takes the mouse coordinates from the [MouseMove][textual.events.MouseMove] object and calculates the coordinate of the square underneath the mouse. There's a little math here, so let's break it down.
- The event contains the coordinates of the mouse relative to the top left of the widget, but we need the coordinate relative to the top left of board which depends on the position of the scrollbars.
We can perform this conversion by adding `self.scroll_offset` to `event.offset`.
- Once we have the board coordinate underneath the mouse we divide the x coordinate by 8 and divide the y coordinate by 4 to give us the coordinate of a square.
If the cursor square coordinate calculated in `on_mouse_move` changes, Textual will call `watch_cursor_square` with the previous coordinate and new coordinate of the square. This method works out the regions of the widget to update and essentially does the reverse of the steps we took to go from mouse coordinates to square coordinates.
The `get_square_region` function calculates a [Region][textual.geometry.Region] object for each square and uses them as a positional argument in a call to [refresh][textual.widget.Widget.refresh]. Passing Region objects to `refresh` tells Textual to update only the cells underneath those regions, and not the entire widget.
!!! note
Textual is smart about performing updates. If you refresh multiple regions, Textual will combine them into as few non-overlapping regions as possible.
The final step is to update the `render_line` method to use the cursor style when rendering the square underneath the mouse.
You should find that if you move the mouse over the widget now, it will highlight the square underneath the mouse pointer in red.
### Line API examples
The following builtin widgets use the Line API. If you are building advanced widgets, it may be worth looking through the code for inspiration!
- [DataTable](https://github.com/Textualize/textual/blob/main/src/textual/widgets/_data_table.py)
- [TextLog](https://github.com/Textualize/textual/blob/main/src/textual/widgets/_text_log.py)
- [Tree](https://github.com/Textualize/textual/blob/main/src/textual/widgets/_tree.py)

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 28 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 155 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -1,25 +1,24 @@
from __future__ import annotations
"""Simple version of 5x5, developed for/with Textual."""
from pathlib import Path
from typing import cast
import sys
if sys.version_info >= (3, 8):
from typing import Final
else:
from typing_extensions import Final
from textual.containers import Horizontal
from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.widget import Widget
from textual.widgets import Footer, Button, Label
from textual.css.query import DOMQuery
from textual.reactive import reactive
from textual.binding import Binding
from typing import TYPE_CHECKING, cast
from rich.markdown import Markdown
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Horizontal
from textual.css.query import DOMQuery
from textual.reactive import reactive
from textual.screen import Screen
from textual.widget import Widget
from textual.widgets import Button, Footer, Label
if TYPE_CHECKING:
from typing_extensions import Final
class Help(Screen):
"""The help screen for the application."""

View file

@ -179,7 +179,9 @@ nav:
- "api/query.md"
- "api/reactive.md"
- "api/screen.md"
- "api/scroll_view.md"
- "api/static.md"
- "api/strip.md"
- "api/text_log.md"
- "api/timer.md"
- "api/tree.md"

127
poetry.lock generated
View file

@ -196,43 +196,46 @@ tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy
[[package]]
name = "black"
version = "22.8.0"
version = "23.1.0"
description = "The uncompromising code formatter."
category = "dev"
optional = false
python-versions = ">=3.6.2"
python-versions = ">=3.7"
files = [
{file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"},
{file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"},
{file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"},
{file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"},
{file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"},
{file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"},
{file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"},
{file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"},
{file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"},
{file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"},
{file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"},
{file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"},
{file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"},
{file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"},
{file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"},
{file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"},
{file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"},
{file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"},
{file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"},
{file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"},
{file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"},
{file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"},
{file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"},
{file = "black-23.1.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221"},
{file = "black-23.1.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26"},
{file = "black-23.1.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b"},
{file = "black-23.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104"},
{file = "black-23.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074"},
{file = "black-23.1.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27"},
{file = "black-23.1.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648"},
{file = "black-23.1.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958"},
{file = "black-23.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a"},
{file = "black-23.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481"},
{file = "black-23.1.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad"},
{file = "black-23.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8"},
{file = "black-23.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24"},
{file = "black-23.1.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6"},
{file = "black-23.1.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd"},
{file = "black-23.1.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580"},
{file = "black-23.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468"},
{file = "black-23.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753"},
{file = "black-23.1.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651"},
{file = "black-23.1.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06"},
{file = "black-23.1.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739"},
{file = "black-23.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9"},
{file = "black-23.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555"},
{file = "black-23.1.0-py3-none-any.whl", hash = "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32"},
{file = "black-23.1.0.tar.gz", hash = "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac"},
]
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""}
typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
@ -1079,6 +1082,18 @@ files = [
{file = "msgpack-1.0.4.tar.gz", hash = "sha256:f5d869c18f030202eb412f08b28d2afeea553d6613aee89e200d7aca7ef01f5f"},
]
[[package]]
name = "msgpack-types"
version = "0.2.0"
description = "Type stubs for msgpack"
category = "dev"
optional = false
python-versions = ">=3.7,<4.0"
files = [
{file = "msgpack-types-0.2.0.tar.gz", hash = "sha256:b6b7ce9f52599f9dc3497006be8cf6bed7bd2c83fa48c4df43ac6958b97b0720"},
{file = "msgpack_types-0.2.0-py3-none-any.whl", hash = "sha256:7e5bce9e3bba9fe08ed14005ad107aa44ea8d4b779ec28b8db880826d4c67303"},
]
[[package]]
name = "multidict"
version = "6.0.4"
@ -1165,42 +1180,38 @@ files = [
[[package]]
name = "mypy"
version = "0.990"
version = "1.0.0"
description = "Optional static typing for Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "mypy-0.990-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aaf1be63e0207d7d17be942dcf9a6b641745581fe6c64df9a38deb562a7dbafa"},
{file = "mypy-0.990-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d555aa7f44cecb7ea3c0ac69d58b1a5afb92caa017285a8e9c4efbf0518b61b4"},
{file = "mypy-0.990-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f694d6d09a460b117dccb6857dda269188e3437c880d7b60fa0014fa872d1e9"},
{file = "mypy-0.990-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:269f0dfb6463b8780333310ff4b5134425157ef0d2b1d614015adaf6d6a7eabd"},
{file = "mypy-0.990-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8798c8ed83aa809f053abff08664bdca056038f5a02af3660de00b7290b64c47"},
{file = "mypy-0.990-cp310-cp310-win_amd64.whl", hash = "sha256:47a9955214615108c3480a500cfda8513a0b1cd3c09a1ed42764ca0dd7b931dd"},
{file = "mypy-0.990-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4a8a6c10f4c63fbf6ad6c03eba22c9331b3946a4cec97f008e9ffb4d3b31e8e2"},
{file = "mypy-0.990-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cd2dd3730ba894ec2a2082cc703fbf3e95a08479f7be84912e3131fc68809d46"},
{file = "mypy-0.990-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7da0005e47975287a92b43276e460ac1831af3d23032c34e67d003388a0ce8d0"},
{file = "mypy-0.990-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:262c543ef24deb10470a3c1c254bb986714e2b6b1a67d66daf836a548a9f316c"},
{file = "mypy-0.990-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3ff201a0c6d3ea029d73b1648943387d75aa052491365b101f6edd5570d018ea"},
{file = "mypy-0.990-cp311-cp311-win_amd64.whl", hash = "sha256:1767830da2d1afa4e62b684647af0ff79b401f004d7fa08bc5b0ce2d45bcd5ec"},
{file = "mypy-0.990-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6826d9c4d85bbf6d68cb279b561de6a4d8d778ca8e9ab2d00ee768ab501a9852"},
{file = "mypy-0.990-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46897755f944176fbc504178422a5a2875bbf3f7436727374724842c0987b5af"},
{file = "mypy-0.990-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0680389c34284287fe00e82fc8bccdea9aff318f7e7d55b90d967a13a9606013"},
{file = "mypy-0.990-cp37-cp37m-win_amd64.whl", hash = "sha256:b08541a06eed35b543ae1a6b301590eb61826a1eb099417676ddc5a42aa151c5"},
{file = "mypy-0.990-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:be88d665e76b452c26fb2bdc3d54555c01226fba062b004ede780b190a50f9db"},
{file = "mypy-0.990-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b8f4a8213b1fd4b751e26b59ae0e0c12896568d7e805861035c7a15ed6dc9eb"},
{file = "mypy-0.990-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2b6f85c2ad378e3224e017904a051b26660087b3b76490d533b7344f1546d3ff"},
{file = "mypy-0.990-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ee5f99817ee70254e7eb5cf97c1b11dda29c6893d846c8b07bce449184e9466"},
{file = "mypy-0.990-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49082382f571c3186ce9ea0bd627cb1345d4da8d44a8377870f4442401f0a706"},
{file = "mypy-0.990-cp38-cp38-win_amd64.whl", hash = "sha256:aba38e3dd66bdbafbbfe9c6e79637841928ea4c79b32e334099463c17b0d90ef"},
{file = "mypy-0.990-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9d851c09b981a65d9d283a8ccb5b1d0b698e580493416a10942ef1a04b19fd37"},
{file = "mypy-0.990-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d847dd23540e2912d9667602271e5ebf25e5788e7da46da5ffd98e7872616e8e"},
{file = "mypy-0.990-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cc6019808580565040cd2a561b593d7c3c646badd7e580e07d875eb1bf35c695"},
{file = "mypy-0.990-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a3150d409609a775c8cb65dbe305c4edd7fe576c22ea79d77d1454acd9aeda8"},
{file = "mypy-0.990-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3227f14fe943524f5794679156488f18bf8d34bfecd4623cf76bc55958d229c5"},
{file = "mypy-0.990-cp39-cp39-win_amd64.whl", hash = "sha256:c76c769c46a1e6062a84837badcb2a7b0cdb153d68601a61f60739c37d41cc74"},
{file = "mypy-0.990-py3-none-any.whl", hash = "sha256:8f1940325a8ed460ba03d19ab83742260fa9534804c317224e5d4e5aa588e2d6"},
{file = "mypy-0.990.tar.gz", hash = "sha256:72382cb609142dba3f04140d016c94b4092bc7b4d98ca718740dc989e5271b8d"},
{file = "mypy-1.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0626db16705ab9f7fa6c249c017c887baf20738ce7f9129da162bb3075fc1af"},
{file = "mypy-1.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1ace23f6bb4aec4604b86c4843276e8fa548d667dbbd0cb83a3ae14b18b2db6c"},
{file = "mypy-1.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87edfaf344c9401942883fad030909116aa77b0fa7e6e8e1c5407e14549afe9a"},
{file = "mypy-1.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0ab090d9240d6b4e99e1fa998c2d0aa5b29fc0fb06bd30e7ad6183c95fa07593"},
{file = "mypy-1.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:7cc2c01dfc5a3cbddfa6c13f530ef3b95292f926329929001d45e124342cd6b7"},
{file = "mypy-1.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14d776869a3e6c89c17eb943100f7868f677703c8a4e00b3803918f86aafbc52"},
{file = "mypy-1.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bb2782a036d9eb6b5a6efcdda0986774bf798beef86a62da86cb73e2a10b423d"},
{file = "mypy-1.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cfca124f0ac6707747544c127880893ad72a656e136adc935c8600740b21ff5"},
{file = "mypy-1.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8845125d0b7c57838a10fd8925b0f5f709d0e08568ce587cc862aacce453e3dd"},
{file = "mypy-1.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b1b9e1ed40544ef486fa8ac022232ccc57109f379611633ede8e71630d07d2"},
{file = "mypy-1.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c7cf862aef988b5fbaa17764ad1d21b4831436701c7d2b653156a9497d92c83c"},
{file = "mypy-1.0.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cd187d92b6939617f1168a4fe68f68add749902c010e66fe574c165c742ed88"},
{file = "mypy-1.0.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4e5175026618c178dfba6188228b845b64131034ab3ba52acaffa8f6c361f805"},
{file = "mypy-1.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2f6ac8c87e046dc18c7d1d7f6653a66787a4555085b056fe2d599f1f1a2a2d21"},
{file = "mypy-1.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7306edca1c6f1b5fa0bc9aa645e6ac8393014fa82d0fa180d0ebc990ebe15964"},
{file = "mypy-1.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3cfad08f16a9c6611e6143485a93de0e1e13f48cfb90bcad7d5fde1c0cec3d36"},
{file = "mypy-1.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67cced7f15654710386e5c10b96608f1ee3d5c94ca1da5a2aad5889793a824c1"},
{file = "mypy-1.0.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a86b794e8a56ada65c573183756eac8ac5b8d3d59daf9d5ebd72ecdbb7867a43"},
{file = "mypy-1.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:50979d5efff8d4135d9db293c6cb2c42260e70fb010cbc697b1311a4d7a39ddb"},
{file = "mypy-1.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ae4c7a99e5153496243146a3baf33b9beff714464ca386b5f62daad601d87af"},
{file = "mypy-1.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e398652d005a198a7f3c132426b33c6b85d98aa7dc852137a2a3be8890c4072"},
{file = "mypy-1.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be78077064d016bc1b639c2cbcc5be945b47b4261a4f4b7d8923f6c69c5c9457"},
{file = "mypy-1.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92024447a339400ea00ac228369cd242e988dd775640755fa4ac0c126e49bb74"},
{file = "mypy-1.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:fe523fcbd52c05040c7bee370d66fee8373c5972171e4fbc323153433198592d"},
{file = "mypy-1.0.0-py3-none-any.whl", hash = "sha256:2efa963bdddb27cb4a0d42545cd137a8d2b883bd181bbc4525b568ef6eca258f"},
{file = "mypy-1.0.0.tar.gz", hash = "sha256:f34495079c8d9da05b183f9f7daec2878280c2ad7cc81da686ef0b484cea2ecf"},
]
[package.dependencies]
@ -2109,4 +2120,4 @@ dev = ["aiohttp", "click", "msgpack"]
[metadata]
lock-version = "2.0"
python-versions = "^3.7"
content-hash = "8b7c3330ea6ad356b3d0c2e0ee7a2bea7674b482982dc57fe36e88638ca0b4ce"
content-hash = "7c62b5cfada89bed9920c540db9d70dc276a3868b15e69ecec458de0df0f698e"

View file

@ -41,7 +41,7 @@ python = "^3.7"
rich = ">12.6.0"
#rich = {path="../rich", develop=true}
importlib-metadata = "^4.11.3"
typing-extensions = { version = "^4.0.0", python = "<3.10" }
typing-extensions = "^4.0.0"
# Dependencies below are required for devtools only
aiohttp = { version = ">=3.8.1", optional = true }
@ -55,8 +55,8 @@ dev = ["aiohttp", "click", "msgpack"]
[tool.poetry.group.dev.dependencies]
pytest = "^7.1.3"
black = "^22.3.0,<22.10.0" # macos wheel issue on 22.10
mypy = "^0.990"
black = "^23.1.0"
mypy = "^1.0.0"
pytest-cov = "^2.12.1"
mkdocs = "^1.3.0"
mkdocstrings = {extras = ["python"], version = "^0.20.0"}
@ -68,6 +68,7 @@ Jinja2 = "<3.1.0"
syrupy = "^3.0.0"
mkdocs-rss-plugin = "^1.5.0"
httpx = "^0.23.1"
msgpack-types = "^0.2.0"
[tool.black]
includes = "src"

View file

@ -4,10 +4,14 @@ import inspect
import rich.repr
from rich.console import RenderableType
from typing import Callable, TYPE_CHECKING
from ._context import active_app
from ._log import LogGroup, LogVerbosity
from ._typing import TypeAlias
if TYPE_CHECKING:
from typing_extensions import TypeAlias
__all__ = ["log", "panic", "__version__"] # type: ignore

View file

@ -1,23 +1,19 @@
from __future__ import annotations
import asyncio
import sys
from abc import ABC, abstractmethod
from dataclasses import dataclass
from functools import partial
from typing import TYPE_CHECKING, Any, Callable, TypeVar
from typing_extensions import Protocol, runtime_checkable
from . import _clock
from ._callback import invoke
from ._easing import DEFAULT_EASING, EASING
from ._types import CallbackType
from .timer import Timer
if sys.version_info >= (3, 8):
from typing import Protocol, runtime_checkable
else: # pragma: no cover
from typing_extensions import Protocol, runtime_checkable
if TYPE_CHECKING:
from textual.app import App
@ -322,7 +318,6 @@ class Animator:
)
if animation is None:
if not isinstance(value, (int, float)) and not isinstance(
value, Animatable
):

View file

@ -2,38 +2,105 @@ from __future__ import annotations
from functools import lru_cache
from typing import cast, Tuple, Union
from typing import TYPE_CHECKING
from rich.segment import Segment
from rich.style import Style
from .color import Color
from .css.types import EdgeStyle, EdgeType
from ._typing import TypeAlias
if TYPE_CHECKING:
from typing_extensions import TypeAlias
INNER = 1
OUTER = 2
BORDER_CHARS: dict[EdgeType, tuple[str, str, str]] = {
# Each string of the tuple represents a sub-tuple itself:
# - 1st string represents (top1, top2, top3)
# - 2nd string represents (mid1, mid2, mid3)
# - 3rd string represents (bottom1, bottom2, bottom3)
"": (" ", " ", " "),
"ascii": ("+-+", "| |", "+-+"),
"none": (" ", " ", " "),
"hidden": (" ", " ", " "),
"blank": (" ", " ", " "),
"round": ("╭─╮", "│ │", "╰─╯"),
"solid": ("┌─┐", "│ │", "└─┘"),
"double": ("╔═╗", "║ ║", "╚═╝"),
"dashed": ("┏╍┓", "╏ ╏", "┗╍┛"),
"heavy": ("┏━┓", "┃ ┃", "┗━┛"),
"inner": ("▗▄▖", "▐ ▌", "▝▀▘"),
"outer": ("▛▀▜", "▌ ▐", "▙▄▟"),
"hkey": ("▔▔▔", " ", "▁▁▁"),
"vkey": ("▏ ▕", "▏ ▕", "▏ ▕"),
"tall": ("▊▔▎", "▊ ▎", "▊▁▎"),
"wide": ("▁▁▁", "▎ ▋", "▔▔▔"),
BORDER_CHARS: dict[
EdgeType, tuple[tuple[str, str, str], tuple[str, str, str], tuple[str, str, str]]
] = {
# Three tuples for the top, middle, and bottom rows.
# The sub-tuples are the characters for the left, center, and right borders.
"": (
(" ", " ", " "),
(" ", " ", " "),
(" ", " ", " "),
),
"ascii": (
("+", "-", "+"),
("|", " ", "|"),
("+", "-", "+"),
),
"none": (
(" ", " ", " "),
(" ", " ", " "),
(" ", " ", " "),
),
"hidden": (
(" ", " ", " "),
(" ", " ", " "),
(" ", " ", " "),
),
"blank": (
(" ", " ", " "),
(" ", " ", " "),
(" ", " ", " "),
),
"round": (
("", "", ""),
("", " ", ""),
("", "", ""),
),
"solid": (
("", "", ""),
("", " ", ""),
("", "", ""),
),
"double": (
("", "", ""),
("", " ", ""),
("", "", ""),
),
"dashed": (
("", "", ""),
("", " ", ""),
("", "", ""),
),
"heavy": (
("", "", ""),
("", " ", ""),
("", "", ""),
),
"inner": (
("", "", ""),
("", " ", ""),
("", "", ""),
),
"outer": (
("", "", ""),
("", " ", ""),
("", "", ""),
),
"hkey": (
("", "", ""),
(" ", " ", " "),
("", "", ""),
),
"vkey": (
("", " ", ""),
("", " ", ""),
("", " ", ""),
),
"tall": (
("", "", ""),
("", " ", ""),
("", "", ""),
),
"wide": (
("", "", ""),
("", " ", ""),
("", "", ""),
),
}
# Some of the borders are on the widget background and some are on the background of the parent
@ -41,22 +108,86 @@ BORDER_CHARS: dict[EdgeType, tuple[str, str, str]] = {
BORDER_LOCATIONS: dict[
EdgeType, tuple[tuple[int, int, int], tuple[int, int, int], tuple[int, int, int]]
] = {
"": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
"ascii": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
"none": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
"hidden": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
"blank": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
"round": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
"solid": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
"double": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
"dashed": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
"heavy": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
"inner": ((1, 1, 1), (1, 1, 1), (1, 1, 1)),
"outer": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
"hkey": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
"vkey": ((0, 0, 0), (0, 0, 0), (0, 0, 0)),
"tall": ((2, 0, 1), (2, 0, 1), (2, 0, 1)),
"wide": ((1, 1, 1), (0, 1, 3), (1, 1, 1)),
"": (
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
),
"ascii": (
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
),
"none": (
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
),
"hidden": (
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
),
"blank": (
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
),
"round": (
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
),
"solid": (
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
),
"double": (
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
),
"dashed": (
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
),
"heavy": (
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
),
"inner": (
(1, 1, 1),
(1, 1, 1),
(1, 1, 1),
),
"outer": (
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
),
"hkey": (
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
),
"vkey": (
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
),
"tall": (
(2, 0, 1),
(2, 0, 1),
(2, 0, 1),
),
"wide": (
(1, 1, 1),
(0, 1, 3),
(1, 1, 1),
),
}
INVISIBLE_EDGE_TYPES = cast("frozenset[EdgeType]", frozenset(("", "none", "hidden")))

View file

@ -1,5 +1,9 @@
from typing import Callable
__all__ = ["cell_len"]
cell_len: Callable[[str], int]
try:
from rich.cells import cached_cell_len as cell_len
except ImportError:

View file

@ -1,6 +1,6 @@
from __future__ import annotations
COLOR_NAME_TO_RGB: dict[str, tuple[int, int, int] | tuple[int, int, int, float]] = {
COLOR_NAME_TO_RGB: dict[str, tuple[int, int, int] | tuple[int, int, int, int]] = {
# Let's start with a specific pseudo-color::
"transparent": (0, 0, 0, 0),
# Then, the 16 common ANSI colors:

View file

@ -26,12 +26,12 @@ from . import errors
from ._cells import cell_len
from ._loop import loop_last
from .strip import Strip
from ._typing import TypeAlias
from .geometry import NULL_OFFSET, Offset, Region, Size
if TYPE_CHECKING:
from .widget import Widget
from typing_extensions import TypeAlias
class ReflowResult(NamedTuple):

View file

@ -4,13 +4,13 @@ from abc import ABC, abstractmethod
from typing import ClassVar, NamedTuple, TYPE_CHECKING
from .geometry import Region, Size, Spacing
from ._typing import TypeAlias
if TYPE_CHECKING:
from typing_extensions import TypeAlias
from .widget import Widget
ArrangeResult: TypeAlias = "tuple[list[WidgetPlacement], set[Widget]]"
DockArrangeResult: TypeAlias = "tuple[list[WidgetPlacement], set[Widget], Spacing]]"
DockArrangeResult: TypeAlias = "tuple[list[WidgetPlacement], set[Widget], Spacing]"
class WidgetPlacement(NamedTuple):

View file

@ -1,15 +1,10 @@
from __future__ import annotations
from dataclasses import dataclass
import sys
from fractions import Fraction
from typing import cast, Sequence
from typing import Sequence, cast
if sys.version_info >= (3, 8):
from typing import Protocol
else:
from typing_extensions import Protocol # pragma: no cover
from typing_extensions import Protocol
class EdgeProtocol(Protocol):

View file

@ -79,7 +79,6 @@ class Parser(Generic[T]):
self._awaiting = next(self._gen)
def feed(self, data: str) -> Iterable[T]:
if self._eof:
raise ParseError("end of file reached") from None
if not data:
@ -104,7 +103,6 @@ class Parser(Generic[T]):
yield popleft()
while pos < data_size or isinstance(self._awaiting, _PeekBuffer):
_awaiting = self._awaiting
if isinstance(_awaiting, _Read1):
self._awaiting = self._gen.send(data[pos : pos + 1])

View file

@ -1,9 +1,10 @@
from __future__ import annotations
import sys
from fractions import Fraction
from itertools import accumulate
from typing import cast, Sequence, TYPE_CHECKING
from typing import Sequence, cast, TYPE_CHECKING
from typing_extensions import Literal
from .box_model import BoxModel
from .css.scalar import Scalar
@ -13,12 +14,6 @@ if TYPE_CHECKING:
from .widget import Widget
if sys.version_info >= (3, 8):
from typing import Literal
else:
from typing_extensions import Literal
def resolve(
dimensions: Sequence[Scalar],
total: int,

View file

@ -2,7 +2,7 @@ from __future__ import annotations
from time import sleep, perf_counter
from asyncio import get_running_loop
from asyncio import get_running_loop, Future
from threading import Thread, Event
@ -13,7 +13,7 @@ class Sleeper(Thread):
self._exit = False
self._sleep_time = 0.0
self._event = Event()
self.future = None
self.future: Future | None = None
self._loop = get_running_loop()
super().__init__(daemon=True)
@ -25,6 +25,7 @@ class Sleeper(Thread):
sleep(self._sleep_time)
self._event.clear()
# self.future.set_result(None)
assert self.future is not None
self._loop.call_soon_threadsafe(self.future.set_result, None)
async def sleep(self, sleep_time: float) -> None:
@ -33,12 +34,8 @@ class Sleeper(Thread):
self._event.set()
await future
# await self._async_event.wait()
# self._async_event.clear()
async def check_sleeps() -> None:
sleeper = Sleeper()
sleeper.start()
@ -47,7 +44,6 @@ async def check_sleeps() -> None:
while perf_counter() - start < sleep_for:
sleep(0)
# await sleeper.sleep(sleep_for)
elapsed = perf_counter() - start
return elapsed

View file

@ -2,7 +2,7 @@ from __future__ import annotations
from functools import lru_cache
from sys import intern
from typing import TYPE_CHECKING, Callable, Iterable, List
from typing import TYPE_CHECKING, Callable, Iterable
from rich.segment import Segment
from rich.style import Style
@ -10,8 +10,7 @@ from rich.style import Style
from ._border import get_box, render_row
from ._filter import LineFilter
from ._opacity import _apply_opacity
from ._segment_tools import line_crop, line_pad, line_trim
from ._typing import TypeAlias
from ._segment_tools import line_pad, line_trim
from .color import Color
from .geometry import Region, Size, Spacing
from .renderables.text_opacity import TextOpacity
@ -19,10 +18,12 @@ from .renderables.tint import Tint
from .strip import Strip
if TYPE_CHECKING:
from typing import TypeAlias
from .css.styles import StylesBase
from .widget import Widget
RenderLineCallback: TypeAlias = Callable[[int], List[Segment]]
RenderLineCallback: TypeAlias = Callable[[int], Strip]
@lru_cache(1024 * 8)
@ -212,7 +213,7 @@ class StylesCache:
padding: Spacing,
base_background: Color,
background: Color,
render_content_line: RenderLineCallback,
render_content_line: Callable[[int], Strip],
) -> Strip:
"""Render a styled line.
@ -313,6 +314,7 @@ class StylesCache:
content_y = y - gutter.top
if content_y < content_height:
line = render_content_line(y - gutter.top)
line = line.adjust_cell_length(content_width)
else:
line = [make_blank(content_width, inner)]
if inner:

View file

@ -1,13 +1,10 @@
from typing import Awaitable, Callable, List, TYPE_CHECKING, Union
from typing import TYPE_CHECKING, Awaitable, Callable, List, Union
from rich.segment import Segment
from ._typing import Protocol
from typing_extensions import Protocol
if TYPE_CHECKING:
from .message import Message
from .strip import Strip
class MessageTarget(Protocol):

View file

@ -1,13 +0,0 @@
import sys
if sys.version_info >= (3, 10):
from typing import TypeAlias
else: # pragma: no cover
from typing_extensions import TypeAlias
if sys.version_info >= (3, 8):
from typing import Final, Literal, Protocol, TypedDict
else:
from typing_extensions import Final, Literal, Protocol, TypedDict
__all__ = ["TypeAlias", "Final", "Literal", "Protocol", "TypedDict"]

View file

@ -20,7 +20,7 @@ try:
import ctypes
from ctypes.wintypes import LARGE_INTEGER
kernel32 = ctypes.windll.kernel32
kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
except Exception:
sleep = time_sleep
else:

View file

@ -92,7 +92,6 @@ class XTermParser(Parser[events.Event]):
return None
def parse(self, on_token: TokenCallback) -> Generator[Awaitable, str, None]:
ESC = "\x1b"
read1 = self.read1
sequence_to_key_events = self._sequence_to_key_events
@ -161,7 +160,6 @@ class XTermParser(Parser[events.Event]):
# Look ahead through the suspected escape sequence for a match
while True:
# If we run into another ESC at this point, then we've failed
# to find a match, and should issue everything we've seen within
# the suspected sequence as Key events instead.

View file

@ -46,11 +46,12 @@ from ._animator import DEFAULT_EASING, Animatable, Animator, EasingFunction
from ._ansi_sequences import SYNC_END, SYNC_START
from ._asyncio import create_task
from ._callback import invoke
from ._context import active_app, active_message_pump
from ._context import active_app
from ._event_broker import NoHandler, extract_handler_actions
from ._filter import LineFilter, Monochrome
from ._path import _make_path_object_relative
from ._typing import Final, TypeAlias
from ._wait import wait_for_idle
from .actions import SkipAction
from .await_remove import AwaitRemove
from .binding import Binding, Bindings
@ -68,10 +69,11 @@ from .messages import CallbackType
from .reactive import Reactive
from .renderables.blank import Blank
from .screen import Screen
from ._wait import wait_for_idle
from .widget import AwaitMount, Widget
if TYPE_CHECKING:
from typing_extensions import Coroutine, Final, TypeAlias
from .devtools.client import DevtoolsClient
from .pilot import Pilot

View file

@ -1,12 +1,14 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterable, MutableMapping
from typing import TYPE_CHECKING, Iterable, MutableMapping
import rich.repr
from textual.keys import _character_to_key
from textual._typing import TypeAlias
from .keys import _character_to_key
if TYPE_CHECKING:
from typing_extensions import TypeAlias
BindingType: TypeAlias = "Binding | tuple[str, str, str]"

View file

@ -30,7 +30,6 @@ class Content(Vertical):
class ColorsView(Vertical):
def compose(self) -> ComposeResult:
LEVELS = [
"darken-3",
"darken-2",
@ -42,7 +41,6 @@ class ColorsView(Vertical):
]
for color_name in ColorSystem.COLOR_NAMES:
items: list[Widget] = [Label(f'"{color_name}"')]
for level in LEVELS:
color = f"{color_name}-{level}" if level else color_name

View file

@ -431,7 +431,9 @@ class Color(NamedTuple):
suggested_color = None
if not color_text.startswith(("#", "rgb", "hsl")):
# Seems like we tried to use a color name: let's try to find one that is close enough:
suggested_color = get_suggestion(color_text, COLOR_NAME_TO_RGB.keys())
suggested_color = get_suggestion(
color_text, list(COLOR_NAME_TO_RGB.keys())
)
if suggested_color:
error_message += f"; did you mean '{suggested_color}'?"
raise ColorParseError(error_message, suggested_color)
@ -447,10 +449,10 @@ class Color(NamedTuple):
) = color_match.groups()
if rgb_hex_triple is not None:
r, g, b = rgb_hex_triple
r, g, b = rgb_hex_triple # type: ignore[misc]
color = cls(int(f"{r}{r}", 16), int(f"{g}{g}", 16), int(f"{b}{b}", 16))
elif rgb_hex_quad is not None:
r, g, b, a = rgb_hex_quad
r, g, b, a = rgb_hex_quad # type: ignore[misc]
color = cls(
int(f"{r}{r}", 16),
int(f"{g}{g}", 16),

View file

@ -61,7 +61,8 @@ class Bullet:
self, console: Console, options: ConsoleOptions
) -> RenderResult:
yield _markup_and_highlight(self.markup)
yield from self.examples
if self.examples is not None:
yield from self.examples
@rich.repr.auto
@ -76,7 +77,9 @@ class HelpText:
context around the issue. These are rendered below the summary. Defaults to None.
"""
def __init__(self, summary: str, *, bullets: Iterable[Bullet] = None) -> None:
def __init__(
self, summary: str, *, bullets: Iterable[Bullet] | None = None
) -> None:
self.summary: str = summary
self.bullets: Iterable[Bullet] | None = bullets or []
@ -87,6 +90,7 @@ class HelpText:
self, console: Console, options: ConsoleOptions
) -> RenderResult:
tree = Tree(_markup_and_highlight(f"[b blue]{self.summary}"), guide_style="dim")
for bullet in self.bullets:
tree.add(bullet)
if self.bullets is not None:
for bullet in self.bullets:
tree.add(bullet)
yield tree

View file

@ -3,10 +3,11 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import Iterable, Sequence
from textual._typing import Literal
from textual.color import ColorParseError
from textual.css._help_renderables import Example, Bullet, HelpText
from textual.css.constants import (
from typing_extensions import Literal
from ..color import ColorParseError
from ._help_renderables import Example, Bullet, HelpText
from .constants import (
VALID_BORDER,
VALID_LAYOUT,
VALID_ALIGN_HORIZONTAL,
@ -125,7 +126,10 @@ def _spacing_examples(property_name: str) -> ContextSpecificBullets:
def property_invalid_value_help_text(
property_name: str, context: StylingContext, *, suggested_property_name: str = None
property_name: str,
context: StylingContext,
*,
suggested_property_name: str | None = None,
) -> HelpText:
"""Help text to show when the user supplies an invalid value for CSS property
property.
@ -300,7 +304,7 @@ def color_property_help_text(
property_name: str,
context: StylingContext,
*,
error: Exception = None,
error: Exception | None = None,
) -> HelpText:
"""Help text to show when the user supplies an invalid value for a color
property. For example, an unparseable color string.

View file

@ -10,7 +10,14 @@ when setting and getting.
from __future__ import annotations
from operator import attrgetter
from typing import TYPE_CHECKING, Generic, Iterable, NamedTuple, TypeVar, cast
from typing import (
TYPE_CHECKING,
Generic,
Iterable,
NamedTuple,
TypeVar,
cast,
)
import rich.errors
import rich.repr

View file

@ -434,7 +434,6 @@ class StylesBuilder:
process_padding_left = _process_space_partial
def _parse_border(self, name: str, tokens: list[Token]) -> BorderValue:
border_type: EdgeType = "solid"
border_color = Color(0, 255, 0)

View file

@ -2,14 +2,13 @@ from __future__ import annotations
import typing
from ..geometry import Spacing
from .._typing import Final
if typing.TYPE_CHECKING:
from .types import EdgeType
from typing_extensions import Final
VALID_VISIBILITY: Final = {"visible", "hidden"}
VALID_DISPLAY: Final = {"block", "none"}
VALID_BORDER: Final[set[EdgeType]] = {
VALID_BORDER: Final = {
"none",
"hidden",
"ascii",
@ -33,7 +32,14 @@ VALID_BOX_SIZING: Final = {"border-box", "content-box"}
VALID_OVERFLOW: Final = {"scroll", "hidden", "auto"}
VALID_ALIGN_HORIZONTAL: Final = {"left", "center", "right"}
VALID_ALIGN_VERTICAL: Final = {"top", "middle", "bottom"}
VALID_TEXT_ALIGN: Final = {"start", "end", "left", "right", "center", "justify"}
VALID_TEXT_ALIGN: Final = {
"start",
"end",
"left",
"right",
"center",
"justify",
}
VALID_SCROLLBAR_GUTTER: Final = {"auto", "stable"}
VALID_STYLE_FLAGS: Final = {
"b",
@ -53,4 +59,5 @@ VALID_STYLE_FLAGS: Final = {
"uu",
}
NULL_SPACING: Final = Spacing.all(0)

View file

@ -4,8 +4,6 @@ from functools import lru_cache
from pathlib import PurePath
from typing import Iterator, Iterable, NoReturn
from rich import print
from .errors import UnresolvedVariableError
from .types import Specificity3
from ._styles_builder import StylesBuilder, DeclarationError
@ -36,7 +34,6 @@ SELECTOR_MAP: dict[str, tuple[SelectorType, Specificity3]] = {
@lru_cache(maxsize=1024)
def parse_selectors(css_selectors: str) -> tuple[SelectorSet, ...]:
if not css_selectors.strip():
return ()

View file

@ -72,7 +72,6 @@ class DOMQuery(Generic[QueryType]):
exclude: str | None = None,
parent: DOMQuery | None = None,
) -> None:
self._node = node
self._nodes: list[QueryType] | None = None
self._filters: list[tuple[SelectorSet, ...]] = (

View file

@ -4,13 +4,15 @@ from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from functools import lru_cache
from operator import attrgetter
from typing import TYPE_CHECKING, Any, Iterable, NamedTuple, cast
from typing import Iterable, cast
from typing import TYPE_CHECKING, Any, NamedTuple
import rich.repr
from rich.style import Style
from typing_extensions import TypedDict
from .._animator import DEFAULT_EASING, Animatable, BoundAnimator, EasingFunction
from .._types import CallbackType
from .._animator import BoundAnimator, DEFAULT_EASING, Animatable, EasingFunction
from ..color import Color
from ..geometry import Offset, Spacing
from ._style_properties import (
@ -41,8 +43,8 @@ from .constants import (
VALID_DISPLAY,
VALID_OVERFLOW,
VALID_SCROLLBAR_GUTTER,
VALID_VISIBILITY,
VALID_TEXT_ALIGN,
VALID_VISIBILITY,
)
from .scalar import Scalar, ScalarOffset, Unit
from .scalar_animation import ScalarAnimation
@ -57,10 +59,9 @@ from .types import (
ScrollbarGutter,
Specificity3,
Specificity6,
Visibility,
TextAlign,
Visibility,
)
from .._typing import TypedDict
if TYPE_CHECKING:
from .._layout import Layout
@ -327,7 +328,6 @@ class StylesBase(ABC):
# Check we are animating a Scalar or Scalar offset
if isinstance(start_value, (Scalar, ScalarOffset)):
# If destination is a number, we can convert that to a scalar
if isinstance(value, (int, float)):
value = Scalar(value, Unit.CELLS, Unit.CELLS)

View file

@ -1,9 +1,9 @@
from __future__ import annotations
from typing import Tuple
from typing_extensions import Literal
from ..color import Color
from .._typing import Literal
Edge = Literal["top", "right", "bottom", "left"]
DockEdge = Literal["top", "right", "bottom", "left", ""]

View file

@ -136,6 +136,7 @@ class DevtoolsClient:
change, it will update its own Console to ensure it renders at
the correct width for server-side display.
"""
assert self.websocket is not None
async for message in self.websocket:
if message.type == aiohttp.WSMsgType.TEXT:
message_json = json.loads(message.data)

View file

@ -5,7 +5,6 @@ from pathlib import Path
from typing import Iterable
from importlib_metadata import version
from rich.align import Align
from rich.console import Console, ConsoleOptions, RenderResult
from rich.markup import escape
@ -15,8 +14,9 @@ from rich.style import Style
from rich.styled import Styled
from rich.table import Table
from rich.text import Text
from typing_extensions import Literal
from textual._log import LogGroup
from textual._typing import Literal
DevConsoleMessageLevel = Literal["info", "warning", "error"]

View file

@ -2,11 +2,10 @@
from __future__ import annotations
import asyncio
import base64
import json
import pickle
from json import JSONDecodeError
from typing import cast
from typing import Any, cast
from aiohttp import WSMessage, WSMsgType
from aiohttp.abc import Request
@ -232,18 +231,16 @@ class ClientHandler:
)
try:
await self.service.send_server_info(client_handler=self)
async for message in self.websocket:
message = cast(WSMessage, message)
if message.type in (WSMsgType.TEXT, WSMsgType.BINARY):
async for websocket_message in self.websocket:
if websocket_message.type in (WSMsgType.TEXT, WSMsgType.BINARY):
message: dict[str, Any]
try:
if isinstance(message.data, bytes):
message = msgpack.unpackb(message.data)
if isinstance(websocket_message.data, bytes):
message = msgpack.unpackb(websocket_message.data)
else:
message = json.loads(message.data)
message = json.loads(websocket_message.data)
except JSONDecodeError:
self.service.console.print(escape(str(message.data)))
self.service.console.print(escape(str(websocket_message.data)))
continue
type = message.get("type")
@ -254,7 +251,7 @@ class ClientHandler:
and not self.service.shutdown_event.is_set()
):
await self.incoming_queue.put(message)
elif message.type == WSMsgType.ERROR:
elif websocket_message.type == WSMsgType.ERROR:
self.service.console.print(
DevConsoleNotice("Websocket error occurred", level="error")
)

View file

@ -40,8 +40,9 @@ if TYPE_CHECKING:
from .css.query import DOMQuery
from .screen import Screen
from .widget import Widget
from typing_extensions import TypeAlias
from textual._typing import Literal, TypeAlias
from typing_extensions import Literal
_re_identifier = re.compile(IDENTIFIER)

View file

@ -92,7 +92,6 @@ class LinuxDriver(Driver):
self.console.file.flush()
def start_application_mode(self):
loop = asyncio.get_running_loop()
def send_size_event():
@ -123,7 +122,6 @@ class LinuxDriver(Driver):
except termios.error:
pass
else:
newattr[tty.LFLAG] = self._patch_lflag(newattr[tty.LFLAG])
newattr[tty.IFLAG] = self._patch_iflag(newattr[tty.IFLAG])
@ -208,7 +206,6 @@ class LinuxDriver(Driver):
pass # TODO: log
def _run_input_thread(self, loop) -> None:
selector = selectors.DefaultSelector()
selector.register(self.fileno, selectors.EVENT_READ)

View file

@ -58,7 +58,6 @@ class WindowsDriver(Driver):
self.console.file.write("\x1b[?2004l")
def start_application_mode(self) -> None:
loop = asyncio.get_running_loop()
self._restore_console = win32.enable_application_mode()

View file

@ -1,7 +1,11 @@
from __future__ import annotations
from typing import cast
from textual._typing import Final, Literal
from typing import TYPE_CHECKING, cast
from typing_extensions import Literal
if TYPE_CHECKING:
from typing_extensions import Final
FEATURES: Final = {"devtools", "debug", "headless"}

View file

@ -8,9 +8,20 @@ from __future__ import annotations
from functools import lru_cache
from operator import attrgetter, itemgetter
from typing import Any, Collection, NamedTuple, Tuple, TypeVar, Union, cast
from typing import (
Any,
Collection,
NamedTuple,
Tuple,
TypeVar,
Union,
cast,
TYPE_CHECKING,
)
if TYPE_CHECKING:
from typing_extensions import TypeAlias
from textual._typing import TypeAlias
SpacingDimensions: TypeAlias = Union[
int, Tuple[int], Tuple[int, int], Tuple[int, int, int, int]

View file

@ -18,7 +18,6 @@ class HorizontalLayout(Layout):
def arrange(
self, parent: Widget, children: list[Widget], size: Size
) -> ArrangeResult:
placements: list[WidgetPlacement] = []
add_placement = placements.append
x = max_height = Fraction(0)

View file

@ -19,7 +19,6 @@ class VerticalLayout(Layout):
def arrange(
self, parent: Widget, children: list[Widget], size: Size
) -> ArrangeResult:
placements: list[WidgetPlacement] = []
add_placement = placements.append
parent_size = parent.outer_size

View file

@ -398,7 +398,6 @@ class MessagePump(metaclass=MessagePumpMeta):
self.app._handle_exception(error)
break
finally:
self._message_queue.task_done()
current_time = time()

View file

@ -170,7 +170,6 @@ class Reactive(Generic[ReactiveType]):
getattr(obj, "__computes", []).clear()
def __set_name__(self, owner: Type[MessageTarget], name: str) -> None:
# Check for compute method
if hasattr(owner, f"compute_{name}"):
# Compute methods are stored in a list called `__computes`

View file

@ -15,6 +15,8 @@ def blend_colors(color1: Color, color2: Color, ratio: float) -> Color:
Returns:
A Color representing the blending of the two supplied colors.
"""
assert color1.triplet is not None
assert color2.triplet is not None
r1, g1, b1 = color1.triplet
r2, g2, b2 = color2.triplet

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from typing import Iterable, Iterator
from typing import Iterable, Iterator, TYPE_CHECKING
import rich.repr
from rich.console import RenderableType
@ -15,11 +15,12 @@ from .dom import DOMNode
from .timer import Timer
from ._types import CallbackType
from .geometry import Offset, Region, Size
from ._typing import Final
from .reactive import Reactive
from .renderables.blank import Blank
from .widget import Widget
if TYPE_CHECKING:
from typing_extensions import Final
# Screen updates will be batched so that they don't happen more often than 120 times per second:
UPDATE_PERIOD: Final[float] = 1 / 120

View file

@ -92,7 +92,6 @@ class ScrollBarRender:
back_color: Color = Color.parse("#555555"),
bar_color: Color = Color.parse("bright_magenta"),
) -> Segments:
if vertical:
bars = ["", "", "", "", "", "", "", " "]
else:
@ -190,7 +189,6 @@ class ScrollBarRender:
@rich.repr.auto
class ScrollBar(Widget):
renderer: ClassVar[Type[ScrollBarRender]] = ScrollBarRender
"""The class used for rendering scrollbars.
This can be overriden and set to a ScrollBarRender-derived class

View file

@ -6,7 +6,7 @@ from typing import Iterable, Iterator
import rich.repr
from rich.cells import cell_len, set_cell_size
from rich.segment import Segment
from rich.style import Style
from rich.style import Style, StyleType
from ._cache import FIFOCache
from ._filter import LineFilter
@ -49,7 +49,7 @@ class Strip:
return "".join(segment.text for segment in self._segments)
@classmethod
def blank(cls, cell_length: int, style: Style | None) -> Strip:
def blank(cls, cell_length: int, style: StyleType | None = None) -> Strip:
"""Create a blank strip.
Args:
@ -59,7 +59,8 @@ class Strip:
Returns:
New strip.
"""
return cls([Segment(" " * cell_length, style)], cell_length)
segment_style = Style.parse(style) if isinstance(style, str) else style
return cls([Segment(" " * cell_length, segment_style)], cell_length)
@classmethod
def from_lines(
@ -135,6 +136,23 @@ class Strip:
self._segments == strip._segments and self.cell_length == strip.cell_length
)
def extend_cell_length(self, cell_length: int, style: Style | None = None) -> Strip:
"""Extend the cell length if it is less than the given value.
Args:
cell_length: Required minimum cell length.
style: Style for padding if the cell length is extended.
Returns:
A new Strip.
"""
if self.cell_length < cell_length:
missing_space = cell_length - self.cell_length
segments = self._segments + [Segment(" " * missing_space, style)]
return Strip(segments, cell_length)
else:
return self
def adjust_cell_length(self, cell_length: int, style: Style | None = None) -> Strip:
"""Adjust the cell length, possibly truncating or extending.

View file

@ -2,6 +2,7 @@ from __future__ import annotations
from functools import partial
from typing import cast
from typing_extensions import Literal
import rich.repr
from rich.console import RenderableType
@ -12,7 +13,7 @@ from ..css._error_tools import friendly_list
from ..message import Message
from ..reactive import Reactive
from ..widgets import Static
from .._typing import Literal
ButtonVariant = Literal["default", "primary", "success", "warning", "error"]
_VALID_BUTTON_VARIANTS = {"default", "primary", "success", "warning", "error"}

View file

@ -2,7 +2,8 @@ from __future__ import annotations
from dataclasses import dataclass, field
from itertools import chain, zip_longest
from typing import ClassVar, Generic, Iterable, TypeVar, cast
from typing import Generic, Iterable, cast
from typing_extensions import ClassVar, TypeVar, Literal
import rich.repr
from rich.console import RenderableType
@ -16,7 +17,6 @@ from .. import events, messages
from .._cache import LRUCache
from .._segment_tools import line_crop
from .._types import SegmentLines
from .._typing import Literal
from ..binding import Binding, BindingType
from ..coordinate import Coordinate
from ..geometry import Region, Size, Spacing, clamp

View file

@ -88,7 +88,6 @@ class DirectoryTree(Tree[DirEntry]):
id: str | None = None,
classes: str | None = None,
) -> None:
self.path = path
super().__init__(
path,

View file

@ -28,7 +28,6 @@ class _InputRenderable:
def __rich_console__(
self, console: "Console", options: "ConsoleOptions"
) -> "RenderResult":
input = self.input
result = input._value
if input._cursor_at_end:

View file

@ -1,7 +1,6 @@
from __future__ import annotations
from typing import ClassVar
from textual import events
from textual.await_remove import AwaitRemove
from textual.binding import Binding, BindingType
from textual.containers import Vertical

View file

@ -2,11 +2,13 @@ from __future__ import annotations
from itertools import cycle
from typing_extensions import Literal
from .. import events
from ..css._error_tools import friendly_list
from ..reactive import Reactive, reactive
from ..widget import Widget, RenderResult
from .._typing import Literal
PlaceholderVariant = Literal["default", "size", "text"]
_VALID_PLACEHOLDER_VARIANTS_ORDERED: list[PlaceholderVariant] = [

View file

@ -57,7 +57,6 @@ class Static(Widget, inherit_bindings=False):
id: str | None = None,
classes: str | None = None,
) -> None:
super().__init__(name=name, id=id, classes=classes)
self.expand = expand
self.shrink = shrink

View file

@ -10,11 +10,10 @@ from rich.protocol import is_renderable
from rich.segment import Segment
from rich.text import Text
from ..reactive import var
from ..geometry import Size, Region
from ..scroll_view import ScrollView
from .._cache import LRUCache
from .._segment_tools import line_crop
from ..geometry import Region, Size
from ..reactive import var
from ..scroll_view import ScrollView
from ..strip import Strip
@ -160,7 +159,6 @@ class TextLog(ScrollView, can_focus=True):
return lines
def _render_line(self, y: int, scroll_x: int, width: int) -> Strip:
if y >= len(self.lines):
return Strip.blank(width, self.rich_style)

View file

@ -1,7 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import ClassVar, Generic, NewType, TypeVar
from typing import TYPE_CHECKING, ClassVar, Generic, NewType, TypeVar
import rich.repr
from rich.style import NULL_STYLE, Style
@ -9,11 +9,10 @@ from rich.text import Text, TextType
from .. import events
from .._cache import LRUCache
from .._immutable_sequence_view import ImmutableSequenceView
from .._loop import loop_last
from .._segment_tools import line_pad
from .._types import MessageTarget
from .._typing import TypeAlias
from .._immutable_sequence_view import ImmutableSequenceView
from ..binding import Binding, BindingType
from ..geometry import Region, Size, clamp
from ..message import Message
@ -21,6 +20,9 @@ from ..reactive import reactive, var
from ..scroll_view import ScrollView
from ..strip import Strip
if TYPE_CHECKING:
from typing_extensions import TypeAlias
NodeID = NewType("NodeID", int)
TreeDataType = TypeVar("TreeDataType")
EventTreeDataType = TypeVar("EventTreeDataType")
@ -281,7 +283,6 @@ class TreeNode(Generic[TreeDataType]):
class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
BINDINGS: ClassVar[list[BindingType]] = [
Binding("enter", "select_cursor", "Select", show=False),
Binding("space", "toggle_node", "Toggle", show=False),

View file

@ -24,11 +24,10 @@ Where the fear has gone there will be nothing. Only I will remain."
class Welcome(Static):
DEFAULT_CSS = """
Welcome {
width: 100%;
height: 100%;
height: 100%;
background: $surface;
}
@ -44,7 +43,7 @@ class Welcome(Static):
Welcome #close {
dock: bottom;
width: 100%;
width: 100%;
}
"""

View file

@ -4,8 +4,7 @@ from typing import Any
import pytest
from textual.color import Color
from textual.css._help_renderables import HelpText
from textual.css.stylesheet import Stylesheet, StylesheetParseError, CssSource
from textual.css.stylesheet import CssSource, Stylesheet, StylesheetParseError
from textual.css.tokenizer import TokenError
from textual.dom import DOMNode
from textual.geometry import Spacing

View file

@ -118,7 +118,6 @@ def pytest_sessionfinish(
diffs: List[SvgSnapshotDiff] = []
num_snapshots_passing = 0
for item in session.items:
# Grab the data our fixture attached to the pytest node
num_snapshots_passing += int(item.stash.get(TEXTUAL_SNAPSHOT_PASS, False))
snapshot_svg = item.stash.get(TEXTUAL_SNAPSHOT_SVG_KEY, None)

View file

@ -3,6 +3,7 @@ import pytest
from typing import Sequence
from textual._immutable_sequence_view import ImmutableSequenceView
def wrap(source: Sequence[int]) -> ImmutableSequenceView[int]:
"""Wrap a sequence of integers inside an immutable sequence view."""
return ImmutableSequenceView[int](source)

View file

@ -83,6 +83,14 @@ def test_adjust_cell_length():
)
def test_extend_cell_length():
strip = Strip([Segment("foo"), Segment("bar")])
assert strip.extend_cell_length(3).text == "foobar"
assert strip.extend_cell_length(6).text == "foobar"
assert strip.extend_cell_length(7).text == "foobar "
assert strip.extend_cell_length(9).text == "foobar "
def test_simplify():
assert Strip([Segment("foo"), Segment("bar")]).simplify() == Strip(
[Segment("foobar")]

View file

@ -10,7 +10,7 @@ from textual.geometry import Region, Size
from textual.strip import Strip
def _extract_content(lines: list[list[Segment]]):
def _extract_content(lines: list[Strip]) -> list[str]:
"""Extract the text content from lines."""
content = ["".join(segment.text for segment in line) for line in lines]
return content
@ -28,9 +28,9 @@ def test_set_dirty():
def test_no_styles():
"""Test that empty style returns the content un-altered"""
content = [
[Segment("foo")],
[Segment("bar")],
[Segment("baz")],
Strip([Segment("foo")]),
Strip([Segment("bar")]),
Strip([Segment("baz")]),
]
styles = Styles()
cache = StylesCache()
@ -54,9 +54,9 @@ def test_no_styles():
def test_border():
content = [
[Segment("foo")],
[Segment("bar")],
[Segment("baz")],
Strip([Segment("foo")]),
Strip([Segment("bar")]),
Strip([Segment("baz")]),
]
styles = Styles()
styles.border = ("heavy", "white")
@ -85,9 +85,9 @@ def test_border():
def test_padding():
content = [
[Segment("foo")],
[Segment("bar")],
[Segment("baz")],
Strip([Segment("foo")]),
Strip([Segment("bar")]),
Strip([Segment("baz")]),
]
styles = Styles()
styles.padding = 1
@ -116,9 +116,9 @@ def test_padding():
def test_padding_border():
content = [
[Segment("foo")],
[Segment("bar")],
[Segment("baz")],
Strip([Segment("foo")]),
Strip([Segment("bar")]),
Strip([Segment("baz")]),
]
styles = Styles()
styles.padding = 1
@ -150,9 +150,9 @@ def test_padding_border():
def test_outline():
content = [
[Segment("foo")],
[Segment("bar")],
[Segment("baz")],
Strip([Segment("foo")]),
Strip([Segment("bar")]),
Strip([Segment("baz")]),
]
styles = Styles()
styles.outline = ("heavy", "white")
@ -177,9 +177,9 @@ def test_outline():
def test_crop():
content = [
[Segment("foo")],
[Segment("bar")],
[Segment("baz")],
Strip([Segment("foo")]),
Strip([Segment("bar")]),
Strip([Segment("baz")]),
]
styles = Styles()
styles.padding = 1
@ -203,17 +203,17 @@ def test_crop():
assert text_content == expected_text
def test_dirty_cache():
def test_dirty_cache() -> None:
"""Check that we only render content once or if it has been marked as dirty."""
content = [
[Segment("foo")],
[Segment("bar")],
[Segment("baz")],
Strip([Segment("foo")]),
Strip([Segment("bar")]),
Strip([Segment("baz")]),
]
rendered_lines: list[int] = []
def get_content_line(y: int) -> list[Segment]:
def get_content_line(y: int) -> Strip:
rendered_lines.append(y)
return content[y]
@ -227,11 +227,13 @@ def test_dirty_cache():
Color.parse("blue"),
Color.parse("green"),
get_content_line,
Size(3, 3),
)
assert rendered_lines == [0, 1, 2]
del rendered_lines[:]
text_content = _extract_content(lines)
expected_text = [
"┏━━━━━┓",
"┃ ┃",