Merge pull request #6121 from TomJGooding/build-update-supported-python-versions

build!: update supported Python versions
This commit is contained in:
Will McGugan
2025-10-11 12:16:14 +01:00
committed by GitHub
29 changed files with 1796 additions and 1395 deletions

View File

@@ -20,7 +20,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
defaults: defaults:
run: run:
shell: bash shell: bash
@@ -35,18 +35,18 @@ jobs:
cache: "poetry" cache: "poetry"
- name: Install dependencies - name: Install dependencies
run: poetry install --no-interaction --extras syntax run: poetry install --no-interaction --extras syntax
if: ${{ matrix.python-version != '3.8' && matrix.python-version != '3.9' }} if: ${{ matrix.python-version != '3.9' }}
- name: Install dependencies for 3.8 and 3.9 - name: Install dependencies for 3.9
run: poetry install --no-interaction run: poetry install --no-interaction
if: ${{ matrix.python-version == '3.8' || matrix.python-version == '3.9' }} if: ${{ matrix.python-version == '3.9' }}
- name: Test with pytest (Py310+ - with syntax highlighting) - name: Test with pytest (Py310+ - with syntax highlighting)
run: | run: |
poetry run pytest tests -v --cov=./src/textual --cov-report=xml:./coverage.xml --cov-report term-missing poetry run pytest tests -v --cov=./src/textual --cov-report=xml:./coverage.xml --cov-report term-missing
if: ${{ matrix.python-version != '3.8' && matrix.python-version != '3.9' }} if: ${{ matrix.python-version != '3.9' }}
- name: Test with pytest (Py39 - without syntax highlighting) - name: Test with pytest (Py39 - without syntax highlighting)
run: | run: |
poetry run pytest tests -v --cov=./src/textual --cov-report=xml:./coverage.xml --cov-report term-missing -m 'not syntax' poetry run pytest tests -v --cov=./src/textual --cov-report=xml:./coverage.xml --cov-report term-missing -m 'not syntax'
if: ${{ matrix.python-version == '3.8' || matrix.python-version == '3.9' }} if: ${{ matrix.python-version == '3.9' }}
- name: Upload snapshot report - name: Upload snapshot report
if: always() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4

View File

@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/) The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/). and this project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased ## [6.3.0] - 2025-10-11
### Added ### Added
@@ -15,6 +15,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Fixed highlight not auto-detecting lexer https://github.com/Textualize/textual/pull/6167 - Fixed highlight not auto-detecting lexer https://github.com/Textualize/textual/pull/6167
### Changed
- Dropped support for Python3.8 https://github.com/Textualize/textual/pull/6121/
- Added support for Python3.14 https://github.com/Textualize/textual/pull/6121/
## [6.2.1] - 2025-10-01 ## [6.2.1] - 2025-10-01
- Fix inability to copy text outside of an input/textarea when it was focused https://github.com/Textualize/textual/pull/6148 - Fix inability to copy text outside of an input/textarea when it was focused https://github.com/Textualize/textual/pull/6148
@@ -3144,6 +3149,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040
- New handler system for messages that doesn't require inheritance - New handler system for messages that doesn't require inheritance
- Improved traceback handling - Improved traceback handling
[6.3.0]: https://github.com/Textualize/textual/compare/v6.2.1...v6.3.0
[6.2.1]: https://github.com/Textualize/textual/compare/v6.2.0...v6.2.1 [6.2.1]: https://github.com/Textualize/textual/compare/v6.2.0...v6.2.1
[6.2.0]: https://github.com/Textualize/textual/compare/v6.1.0...v6.2.0 [6.2.0]: https://github.com/Textualize/textual/compare/v6.1.0...v6.2.0
[6.1.0]: https://github.com/Textualize/textual/compare/v6.0.0...v6.1.0 [6.1.0]: https://github.com/Textualize/textual/compare/v6.0.0...v6.1.0

View File

@@ -1,7 +1,7 @@
[![Discord](https://img.shields.io/discord/1026214085173461072)](https://discord.gg/Enf6Z3qhVr) [![Discord](https://img.shields.io/discord/1026214085173461072)](https://discord.gg/Enf6Z3qhVr)
[![Supported Python Versions](https://img.shields.io/pypi/pyversions/textual/1.0.0)](https://pypi.org/project/textual/) [![Supported Python Versions](https://img.shields.io/pypi/pyversions/textual)](https://pypi.org/project/textual/)
[![PyPI version](https://badge.fury.io/py/textual.svg?)](https://badge.fury.io/py/textual) [![PyPI version](https://badge.fury.io/py/textual.svg?)](https://badge.fury.io/py/textual)
![OS support](https://img.shields.io/badge/OS-macOS%20Linux%20Windows-red) ![OS support](https://img.shields.io/badge/OS-macOS%20Linux%20Windows-red)

View File

@@ -3,7 +3,7 @@ All you need to get started building Textual apps.
## Requirements ## Requirements
Textual requires Python 3.8 or later (if you have a choice, pick the most recent Python). Textual runs on Linux, macOS, Windows and probably any OS where Python also runs. Textual requires Python 3.9 or later (if you have a choice, pick the most recent Python). Textual runs on Linux, macOS, Windows and probably any OS where Python also runs.
!!! info "Your platform" !!! info "Your platform"

2863
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "textual" name = "textual"
version = "6.2.1" version = "6.3.0"
homepage = "https://github.com/Textualize/textual" homepage = "https://github.com/Textualize/textual"
repository = "https://github.com/Textualize/textual" repository = "https://github.com/Textualize/textual"
documentation = "https://textual.textualize.io/" documentation = "https://textual.textualize.io/"
@@ -17,12 +17,12 @@ classifiers = [
"Operating System :: Microsoft :: Windows :: Windows 11", "Operating System :: Microsoft :: Windows :: Windows 11",
"Operating System :: MacOS", "Operating System :: MacOS",
"Operating System :: POSIX :: Linux", "Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Typing :: Typed", "Typing :: Typed",
] ]
include = [ include = [
@@ -41,12 +41,12 @@ include = [
"Bug Tracker" = "https://github.com/Textualize/textual/issues" "Bug Tracker" = "https://github.com/Textualize/textual/issues"
[tool.ruff] [tool.ruff]
target-version = "py38" target-version = "py39"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.8.1" python = "^3.9"
markdown-it-py = { extras = ["plugins", "linkify"], version = ">=2.1.0" } markdown-it-py = { extras = ["plugins", "linkify"], version = ">=2.1.0" }
rich = ">=13.3.3" rich = ">=14.2.0"
#rich = {path="../rich", develop=true} #rich = {path="../rich", develop=true}
typing-extensions = "^4.4.0" typing-extensions = "^4.4.0"
platformdirs = ">=3.6.0,<5" platformdirs = ">=3.6.0,<5"

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -7,7 +7,7 @@ from textual.layout import WidgetPlacement
from textual.widget import Widget from textual.widget import Widget
def test_arrange_empty(): async def test_arrange_empty():
container = Widget(id="container") container = Widget(id="container")
result = arrange(container, [], Size(80, 24), Size(80, 24)) result = arrange(container, [], Size(80, 24), Size(80, 24))
@@ -15,7 +15,7 @@ def test_arrange_empty():
assert result.widgets == set() assert result.widgets == set()
def test_arrange_dock_top(): async def test_arrange_dock_top():
container = Widget(id="container") container = Widget(id="container")
container._parent = App() container._parent = App()
child = Widget(id="child") child = Widget(id="child")
@@ -36,7 +36,7 @@ def test_arrange_dock_top():
assert result.widgets == {child, header} assert result.widgets == {child, header}
def test_arrange_dock_left(): async def test_arrange_dock_left():
container = Widget(id="container") container = Widget(id="container")
container._parent = App() container._parent = App()
child = Widget(id="child") child = Widget(id="child")
@@ -61,7 +61,7 @@ def test_arrange_dock_left():
assert result.widgets == {child, header} assert result.widgets == {child, header}
def test_arrange_dock_right(): async def test_arrange_dock_right():
container = Widget(id="container") container = Widget(id="container")
container._parent = App() container._parent = App()
child = Widget(id="child") child = Widget(id="child")
@@ -86,7 +86,7 @@ def test_arrange_dock_right():
assert result.widgets == {child, header} assert result.widgets == {child, header}
def test_arrange_dock_bottom(): async def test_arrange_dock_bottom():
container = Widget(id="container") container = Widget(id="container")
container._parent = App() container._parent = App()
child = Widget(id="child") child = Widget(id="child")
@@ -111,7 +111,7 @@ def test_arrange_dock_bottom():
assert result.widgets == {child, header} assert result.widgets == {child, header}
def test_arrange_dock_badly(): async def test_arrange_dock_badly():
child = Widget(id="child") child = Widget(id="child")
child.styles.dock = "nowhere" child.styles.dock = "nowhere"
with pytest.raises(AssertionError): with pytest.raises(AssertionError):

View File

@@ -31,7 +31,7 @@ def test_border_render_row():
] ]
def test_border_title_single_line(): async def test_border_title_single_line():
"""The border_title gets set to a single line even when multiple lines are provided.""" """The border_title gets set to a single line even when multiple lines are provided."""
widget = Widget() widget = Widget()
@@ -59,7 +59,7 @@ def test_border_title_single_line():
assert widget.border_title == "[bold]Hello World[/bold]" assert widget.border_title == "[bold]Hello World[/bold]"
def test_border_subtitle_single_line(): async def test_border_subtitle_single_line():
"""The border_subtitle gets set to a single line even when multiple lines are provided.""" """The border_subtitle gets set to a single line even when multiple lines are provided."""
widget = Widget() widget = Widget()

View File

@@ -7,7 +7,7 @@ from textual.geometry import Size, Spacing
from textual.widget import Widget from textual.widget import Widget
def test_content_box(): async def test_content_box():
one = Fraction(1) one = Fraction(1)
class TestWidget(Widget): class TestWidget(Widget):
@@ -44,7 +44,7 @@ def test_content_box():
assert box_model == BoxModel(Fraction(14), Fraction(12), Spacing(0, 0, 0, 0)) assert box_model == BoxModel(Fraction(14), Fraction(12), Spacing(0, 0, 0, 0))
def test_width(): async def test_width():
"""Test width settings.""" """Test width settings."""
one = Fraction(1) one = Fraction(1)
@@ -93,7 +93,7 @@ def test_width():
assert box_model == BoxModel(Fraction(27), Fraction(16), Spacing(1, 2, 3, 4)) assert box_model == BoxModel(Fraction(27), Fraction(16), Spacing(1, 2, 3, 4))
def test_height(): async def test_height():
"""Test height settings.""" """Test height settings."""
one = Fraction(1) one = Fraction(1)
@@ -143,7 +143,7 @@ def test_height():
assert box_model == BoxModel(Fraction(54), Fraction(8), Spacing(1, 2, 3, 4)) assert box_model == BoxModel(Fraction(54), Fraction(8), Spacing(1, 2, 3, 4))
def test_max(): async def test_max():
"""Check that max_width and max_height are respected.""" """Check that max_width and max_height are respected."""
one = Fraction(1) one = Fraction(1)
@@ -166,7 +166,7 @@ def test_max():
assert box_model == BoxModel(Fraction(40), Fraction(30), Spacing(0, 0, 0, 0)) assert box_model == BoxModel(Fraction(40), Fraction(30), Spacing(0, 0, 0, 0))
def test_min(): async def test_min():
"""Check that min_width and min_height are respected.""" """Check that min_width and min_height are respected."""
one = Fraction(1) one = Fraction(1)

View File

@@ -286,7 +286,7 @@ def test_first_line():
assert first_line.spans == [Span(0, 3, "red")] assert first_line.spans == [Span(0, 3, "red")]
def test_split_and_tabs(): async def test_split_and_tabs():
spans = [ spans = [
Span(0, 49, style="$text"), Span(0, 49, style="$text"),
] ]

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from textual.app import App from textual.app import App
def test_textual_env_var(monkeypatch): async def test_textual_env_var(monkeypatch):
monkeypatch.setenv("TEXTUAL", "") monkeypatch.setenv("TEXTUAL", "")
app = App() app = App()
assert app.features == set() assert app.features == set()

View File

@@ -2,7 +2,7 @@ from textual.app import App, ComposeResult
from textual.widgets import Log from textual.widgets import Log
def test_process_line(): async def test_process_line():
log = Log() log = Log()
assert log._process_line("foo") == "foo" assert log._process_line("foo") == "foo"
assert log._process_line("foo\t") == "foo " assert log._process_line("foo\t") == "foo "

View File

@@ -9,14 +9,14 @@ def test_empty_list():
assert len(NodeList()) == 0 assert len(NodeList()) == 0
def test_add_one(): async def test_add_one():
"""Does adding a node to the node list report as having one item?""" """Does adding a node to the node list report as having one item?"""
nodes = NodeList() nodes = NodeList()
nodes._append(Widget()) nodes._append(Widget())
assert len(nodes) == 1 assert len(nodes) == 1
def test_length_hint(): async def test_length_hint():
"""Check length hint dunder method.""" """Check length hint dunder method."""
nodes = NodeList() nodes = NodeList()
assert nodes.__length_hint__() == 0 assert nodes.__length_hint__() == 0
@@ -26,7 +26,7 @@ def test_length_hint():
assert nodes.__length_hint__() == 3 assert nodes.__length_hint__() == 3
def test_repeat_add_one(): async def test_repeat_add_one():
"""Does adding the same item to the node list ignore the additional adds?""" """Does adding the same item to the node list ignore the additional adds?"""
nodes = NodeList() nodes = NodeList()
widget = Widget() widget = Widget()
@@ -35,7 +35,7 @@ def test_repeat_add_one():
assert len(nodes) == 1 assert len(nodes) == 1
def test_insert(): async def test_insert():
nodes = NodeList() nodes = NodeList()
widget1 = Widget() widget1 = Widget()
widget2 = Widget() widget2 = Widget()
@@ -46,7 +46,7 @@ def test_insert():
assert list(nodes) == [widget1, widget2, widget3] assert list(nodes) == [widget1, widget2, widget3]
def test_truthy(): async def test_truthy():
"""Does a node list act as a truthy object?""" """Does a node list act as a truthy object?"""
nodes = NodeList() nodes = NodeList()
assert not bool(nodes) assert not bool(nodes)
@@ -54,7 +54,7 @@ def test_truthy():
assert bool(nodes) assert bool(nodes)
def test_contains(): async def test_contains():
"""Can we check if a widget is (not) within the list?""" """Can we check if a widget is (not) within the list?"""
widget = Widget() widget = Widget()
nodes = NodeList() nodes = NodeList()
@@ -64,7 +64,7 @@ def test_contains():
assert Widget() not in nodes assert Widget() not in nodes
def test_index(): async def test_index():
"""Can we get the index of a widget in the list?""" """Can we get the index of a widget in the list?"""
widget = Widget() widget = Widget()
nodes = NodeList() nodes = NodeList()
@@ -74,7 +74,7 @@ def test_index():
assert nodes.index(widget) == 0 assert nodes.index(widget) == 0
def test_remove(): async def test_remove():
"""Can we remove a widget we've added?""" """Can we remove a widget we've added?"""
widget = Widget() widget = Widget()
nodes = NodeList() nodes = NodeList()
@@ -84,7 +84,7 @@ def test_remove():
assert widget not in nodes assert widget not in nodes
def test_clear(): async def test_clear():
"""Can we clear the list?""" """Can we clear the list?"""
nodes = NodeList() nodes = NodeList()
assert len(nodes) == 0 assert len(nodes) == 0
@@ -100,7 +100,7 @@ def test_clear():
assert widget not in nodes assert widget not in nodes
def test_listy(): async def test_listy():
nodes = NodeList() nodes = NodeList()
widget1 = Widget() widget1 = Widget()
widget2 = Widget() widget2 = Widget()

View File

@@ -4,12 +4,12 @@ from textual.widgets import Placeholder
from textual.widgets._placeholder import InvalidPlaceholderVariant from textual.widgets._placeholder import InvalidPlaceholderVariant
def test_invalid_placeholder_variant(): async def test_invalid_placeholder_variant():
with pytest.raises(InvalidPlaceholderVariant): with pytest.raises(InvalidPlaceholderVariant):
Placeholder(variant="this is clearly not a valid variant!") Placeholder(variant="this is clearly not a valid variant!")
def test_invalid_reactive_variant_change(): async def test_invalid_reactive_variant_change():
p = Placeholder() p = Placeholder()
with pytest.raises(InvalidPlaceholderVariant): with pytest.raises(InvalidPlaceholderVariant):
p.variant = "this is clearly not a valid variant!" p.variant = "this is clearly not a valid variant!"

View File

@@ -11,7 +11,7 @@ from textual.widget import Widget
from textual.widgets import ProgressBar from textual.widgets import ProgressBar
def test_initial_status(): async def test_initial_status():
pb = ProgressBar() pb = ProgressBar()
assert pb.total is None assert pb.total is None
assert pb.progress == 0 assert pb.progress == 0
@@ -23,7 +23,7 @@ def test_initial_status():
assert pb.percentage == 0 assert pb.percentage == 0
def test_advance(): async def test_advance():
pb = ProgressBar(total=100) pb = ProgressBar(total=100)
pb.advance(10) pb.advance(10)
@@ -39,7 +39,7 @@ def test_advance():
assert pb.percentage == approx(0.520625) assert pb.percentage == approx(0.520625)
def test_advance_backwards(): async def test_advance_backwards():
pb = ProgressBar(total=100) pb = ProgressBar(total=100)
pb.progress = 50 pb.progress = 50
@@ -48,7 +48,7 @@ def test_advance_backwards():
assert pb.progress == 40 assert pb.progress == 40
def test_progress_overflow(): async def test_progress_overflow():
pb = ProgressBar(total=100) pb = ProgressBar(total=100)
pb.advance(999_999) pb.advance(999_999)
@@ -58,19 +58,19 @@ def test_progress_overflow():
assert pb.percentage == 1 assert pb.percentage == 1
def test_progress_underflow(): async def test_progress_underflow():
pb = ProgressBar(total=100) pb = ProgressBar(total=100)
pb.advance(-999_999) pb.advance(-999_999)
assert pb.percentage == 0 assert pb.percentage == 0
def test_non_negative_total(): async def test_non_negative_total():
pb = ProgressBar(total=-100) pb = ProgressBar(total=-100)
assert pb.total == 0 assert pb.total == 0
def test_update_total(): async def test_update_total():
pb = ProgressBar() pb = ProgressBar()
pb.update(total=100) pb.update(total=100)
@@ -86,7 +86,7 @@ def test_update_total():
assert pb.total == 100 assert pb.total == 100
def test_update_progress(): async def test_update_progress():
pb = ProgressBar(total=100) pb = ProgressBar(total=100)
pb.update(progress=10) pb.update(progress=10)
@@ -99,7 +99,7 @@ def test_update_progress():
assert pb.progress == 40 assert pb.progress == 40
def test_update_advance(): async def test_update_advance():
pb = ProgressBar(total=100) pb = ProgressBar(total=100)
pb.update(advance=10) pb.update(advance=10)
@@ -112,7 +112,7 @@ def test_update_advance():
assert pb.progress == 30 assert pb.progress == 30
def test_update(): async def test_update():
pb = ProgressBar() pb = ProgressBar()
pb.update(total=100, progress=30, advance=20) pb.update(total=100, progress=30, advance=20)
@@ -120,7 +120,7 @@ def test_update():
assert pb.progress == 50 assert pb.progress == 50
def test_go_back_to_indeterminate(): async def test_go_back_to_indeterminate():
pb = ProgressBar() pb = ProgressBar()
pb.total = 100 pb.total = 100

View File

@@ -30,7 +30,7 @@ async def test_query_errors():
app.query_one("1") app.query_one("1")
def test_query(): async def test_query():
class View(Widget): class View(Widget):
pass pass
@@ -159,7 +159,7 @@ def test_query():
_ = app.query(".float").last(View) _ = app.query(".float").last(View)
def test_query_classes(): async def test_query_classes():
class App(Widget): class App(Widget):
pass pass
@@ -225,7 +225,7 @@ def test_query_classes():
assert len(app.query(".test")) == 0 assert len(app.query(".test")) == 0
def test_invalid_query(): async def test_invalid_query():
class App(Widget): class App(Widget):
pass pass

View File

@@ -61,7 +61,7 @@ def test_resolve(scalars, total, gutter, result):
) )
def test_resolve_fraction_unit(): async def test_resolve_fraction_unit():
"""Test resolving fraction units in combination with minimum widths.""" """Test resolving fraction units in combination with minimum widths."""
widget1 = Widget() widget1 = Widget()
widget2 = Widget() widget2 = Widget()
@@ -124,7 +124,7 @@ def test_resolve_fraction_unit():
) == Fraction(2) ) == Fraction(2)
def test_resolve_fraction_unit_stress_test(): async def test_resolve_fraction_unit_stress_test():
"""Check for zero division errors.""" """Check for zero division errors."""
# https://github.com/Textualize/textual/issues/2673 # https://github.com/Textualize/textual/issues/2673
widget = Widget() widget = Widget()
@@ -149,7 +149,7 @@ def test_resolve_fraction_unit_stress_test():
assert resolved_unit <= remaining_space assert resolved_unit <= remaining_space
def test_resolve_issue_2502(): async def test_resolve_issue_2502():
"""Test https://github.com/Textualize/textual/issues/2502""" """Test https://github.com/Textualize/textual/issues/2502"""
widget = Widget() widget = Widget()

View File

@@ -4,23 +4,23 @@ from textual.widgets import Rule
from textual.widgets.rule import InvalidLineStyle, InvalidRuleOrientation from textual.widgets.rule import InvalidLineStyle, InvalidRuleOrientation
def test_invalid_rule_orientation(): async def test_invalid_rule_orientation():
with pytest.raises(InvalidRuleOrientation): with pytest.raises(InvalidRuleOrientation):
Rule(orientation="invalid orientation!") Rule(orientation="invalid orientation!")
def test_invalid_rule_line_style(): async def test_invalid_rule_line_style():
with pytest.raises(InvalidLineStyle): with pytest.raises(InvalidLineStyle):
Rule(line_style="invalid line style!") Rule(line_style="invalid line style!")
def test_invalid_reactive_rule_orientation_change(): async def test_invalid_reactive_rule_orientation_change():
rule = Rule() rule = Rule()
with pytest.raises(InvalidRuleOrientation): with pytest.raises(InvalidRuleOrientation):
rule.orientation = "invalid orientation!" rule.orientation = "invalid orientation!"
def test_invalid_reactive_rule_line_style_change(): async def test_invalid_reactive_rule_line_style_change():
rule = Rule() rule = Rule()
with pytest.raises(InvalidLineStyle): with pytest.raises(InvalidLineStyle):
rule.line_style = "invalid line style!" rule.line_style = "invalid line style!"

View File

@@ -58,7 +58,7 @@ async def test_signal():
assert called == 3 assert called == 3
def test_signal_errors(): async def test_signal_errors():
"""Check exceptions raised by Signal class.""" """Check exceptions raised by Signal class."""
app = App() app = App()
test_signal = Signal(app, "test") test_signal = Signal(app, "test")

View File

@@ -2,7 +2,7 @@ from textual.content import Content
from textual.widgets import Static from textual.widgets import Static
def test_content_property(): async def test_content_property():
static = Static() static = Static()
assert static.content == "" assert static.content == ""
static.content = "Foo" static.content = "Foo"

View File

@@ -3,7 +3,7 @@ from rich.text import Text
from textual.widgets import RichLog from textual.widgets import RichLog
def test_make_renderable_expand_tabs(): async def test_make_renderable_expand_tabs():
# Regression test for https://github.com/Textualize/textual/issues/3007 # Regression test for https://github.com/Textualize/textual/issues/3007
text_log = RichLog() text_log = RichLog()
renderable = text_log._make_renderable("\tfoo") renderable = text_log._make_renderable("\tfoo")

View File

@@ -63,7 +63,7 @@ async def test_widget_construct():
["visible", True, "visible"], ["visible", True, "visible"],
], ],
) )
def test_widget_set_visible_true(set_val, get_val, style_str): async def test_widget_set_visible_true(set_val, get_val, style_str):
widget = Widget() widget = Widget()
widget.visible = set_val widget.visible = set_val
@@ -71,7 +71,7 @@ def test_widget_set_visible_true(set_val, get_val, style_str):
assert widget.styles.visibility == style_str assert widget.styles.visibility == style_str
def test_widget_set_visible_invalid_string(): async def test_widget_set_visible_invalid_string():
widget = Widget() widget = Widget()
with pytest.raises(StyleValueError): with pytest.raises(StyleValueError):
@@ -80,7 +80,7 @@ def test_widget_set_visible_invalid_string():
assert widget.visible assert widget.visible
def test_widget_content_width(): async def test_widget_content_width():
class TextWidget(Widget): class TextWidget(Widget):
def __init__(self, text: str, id: str) -> None: def __init__(self, text: str, id: str) -> None:
self.text = text self.text = text
@@ -238,19 +238,19 @@ async def test_widget_mount_ids_must_be_unique_mounting_multiple_calls(hierarchy
parent.mount(widget2) parent.mount(widget2)
def test_get_pseudo_class_state(): async def test_get_pseudo_class_state():
widget = Widget() widget = Widget()
pseudo_classes = widget.get_pseudo_class_state() pseudo_classes = widget.get_pseudo_class_state()
assert pseudo_classes == PseudoClasses(enabled=True, focus=False, hover=False) assert pseudo_classes == PseudoClasses(enabled=True, focus=False, hover=False)
def test_get_pseudo_class_state_disabled(): async def test_get_pseudo_class_state_disabled():
widget = Widget(disabled=True) widget = Widget(disabled=True)
pseudo_classes = widget.get_pseudo_class_state() pseudo_classes = widget.get_pseudo_class_state()
assert pseudo_classes == PseudoClasses(enabled=False, focus=False, hover=False) assert pseudo_classes == PseudoClasses(enabled=False, focus=False, hover=False)
def test_get_pseudo_class_state_parent_disabled(): async def test_get_pseudo_class_state_parent_disabled():
child = Widget() child = Widget()
_parent = Widget(disabled=True) _parent = Widget(disabled=True)
child._attach(_parent) child._attach(_parent)
@@ -258,14 +258,14 @@ def test_get_pseudo_class_state_parent_disabled():
assert pseudo_classes == PseudoClasses(enabled=False, focus=False, hover=False) assert pseudo_classes == PseudoClasses(enabled=False, focus=False, hover=False)
def test_get_pseudo_class_state_hover(): async def test_get_pseudo_class_state_hover():
widget = Widget() widget = Widget()
widget.mouse_hover = True widget.mouse_hover = True
pseudo_classes = widget.get_pseudo_class_state() pseudo_classes = widget.get_pseudo_class_state()
assert pseudo_classes == PseudoClasses(enabled=True, focus=False, hover=True) assert pseudo_classes == PseudoClasses(enabled=True, focus=False, hover=True)
def test_get_pseudo_class_state_focus(): async def test_get_pseudo_class_state_focus():
widget = Widget() widget = Widget()
widget.has_focus = True widget.has_focus = True
pseudo_classes = widget.get_pseudo_class_state() pseudo_classes = widget.get_pseudo_class_state()
@@ -314,7 +314,7 @@ async def test_remove_unmounted():
assert mounted assert mounted
def test_render_str() -> None: async def test_render_str() -> None:
widget = Label() widget = Label()
assert widget.render_str("foo") == Content("foo") assert widget.render_str("foo") == Content("foo")
assert widget.render_str("[b]foo") == Content.from_markup("[b]foo") assert widget.render_str("[b]foo") == Content.from_markup("[b]foo")
@@ -371,11 +371,11 @@ def test_children_must_be_widgets():
Widget(1, 2, 3) Widget(1, 2, 3)
def test_orphan_widget_has_no_siblings(): async def test_orphan_widget_has_no_siblings():
assert Widget().siblings == [] assert Widget().siblings == []
def test__allow_scroll_default(): async def test__allow_scroll_default():
assert not Widget()._allow_scroll assert not Widget()._allow_scroll
@@ -407,7 +407,7 @@ async def test_offset_getter_setter():
assert label.offset == Offset(7, 3) assert label.offset == Offset(7, 3)
def test_get_set_tooltip(): async def test_get_set_tooltip():
widget = Widget() widget = Widget()
assert widget.tooltip is None assert widget.tooltip is None
widget.tooltip = "This is a tooltip." widget.tooltip = "This is a tooltip."

View File

@@ -11,7 +11,7 @@ class Body(Widget):
pass pass
def test_find_dom_spot(): async def test_find_dom_spot():
# Build up a "fake" DOM for an application. # Build up a "fake" DOM for an application.
screen = Widget(name="Screen") screen = Widget(name="Screen")
header = Widget(name="Header", id="header") header = Widget(name="Header", id="header")

View File

@@ -6,7 +6,7 @@ from textual.widgets import Tree
from textual.widgets.tree import NodeID, UnknownNodeID from textual.widgets.tree import NodeID, UnknownNodeID
def test_get_tree_node_by_id() -> None: async def test_get_tree_node_by_id() -> None:
"""It should be possible to get a TreeNode by its ID.""" """It should be possible to get a TreeNode by its ID."""
tree = Tree[None]("Anakin") tree = Tree[None]("Anakin")
child = tree.root.add("Leia") child = tree.root.add("Leia")

View File

@@ -4,13 +4,13 @@ from textual.widgets import Tree
from textual.widgets.tree import AddNodeError from textual.widgets.tree import AddNodeError
def test_tree_node_add_before_and_after_raises_exception(): async def test_tree_node_add_before_and_after_raises_exception():
tree = Tree[None]("root") tree = Tree[None]("root")
with pytest.raises(AddNodeError): with pytest.raises(AddNodeError):
tree.root.add("error", before=99, after=0) tree.root.add("error", before=99, after=0)
def test_tree_node_add_before_or_after_with_invalid_type_raises_exception(): async def test_tree_node_add_before_or_after_with_invalid_type_raises_exception():
tree = Tree[None]("root") tree = Tree[None]("root")
tree.root.add("node") tree.root.add("node")
with pytest.raises(TypeError): with pytest.raises(TypeError):
@@ -19,7 +19,7 @@ def test_tree_node_add_before_or_after_with_invalid_type_raises_exception():
tree.root.add("after node", after="node") tree.root.add("after node", after="node")
def test_tree_node_add_before_index(): async def test_tree_node_add_before_index():
tree = Tree[None]("root") tree = Tree[None]("root")
tree.root.add("node") tree.root.add("node")
tree.root.add("before node", before=0) tree.root.add("before node", before=0)
@@ -38,7 +38,7 @@ def test_tree_node_add_before_index():
assert str(tree.root.children[6].label) == "last" assert str(tree.root.children[6].label) == "last"
def test_tree_node_add_after_index(): async def test_tree_node_add_after_index():
tree = Tree[None]("root") tree = Tree[None]("root")
tree.root.add("node") tree.root.add("node")
tree.root.add("after node", after=0) tree.root.add("after node", after=0)
@@ -57,7 +57,7 @@ def test_tree_node_add_after_index():
assert str(tree.root.children[6].label) == "last" assert str(tree.root.children[6].label) == "last"
def test_tree_node_add_relative_to_unknown_node_raises_exception(): async def test_tree_node_add_relative_to_unknown_node_raises_exception():
tree = Tree[None]("root") tree = Tree[None]("root")
removed_node = tree.root.add("removed node") removed_node = tree.root.add("removed node")
removed_node.remove() removed_node.remove()
@@ -67,7 +67,7 @@ def test_tree_node_add_relative_to_unknown_node_raises_exception():
tree.root.add("node", after=removed_node) tree.root.add("node", after=removed_node)
def test_tree_node_add_before_node(): async def test_tree_node_add_before_node():
tree = Tree[None]("root") tree = Tree[None]("root")
node = tree.root.add("node") node = tree.root.add("node")
before_node = tree.root.add("before node", before=node) before_node = tree.root.add("before node", before=node)
@@ -86,7 +86,7 @@ def test_tree_node_add_before_node():
assert str(tree.root.children[6].label) == "last" assert str(tree.root.children[6].label) == "last"
def test_tree_node_add_after_node(): async def test_tree_node_add_after_node():
tree = Tree[None]("root") tree = Tree[None]("root")
node = tree.root.add("node") node = tree.root.add("node")
after_node = tree.root.add("after node", after=node) after_node = tree.root.add("after node", after=node)
@@ -105,7 +105,7 @@ def test_tree_node_add_after_node():
assert str(tree.root.children[6].label) == "last" assert str(tree.root.children[6].label) == "last"
def test_tree_node_add_leaf_before_or_after(): async def test_tree_node_add_leaf_before_or_after():
tree = Tree[None]("root") tree = Tree[None]("root")
leaf = tree.root.add_leaf("leaf") leaf = tree.root.add_leaf("leaf")
tree.root.add_leaf("before leaf", before=leaf) tree.root.add_leaf("before leaf", before=leaf)

View File

@@ -9,7 +9,7 @@ def label_of(node: TreeNode[None]):
return str(node.label) return str(node.label)
def test_tree_node_children() -> None: async def test_tree_node_children() -> None:
"""A node's children property should act like an immutable list.""" """A node's children property should act like an immutable list."""
CHILDREN = 23 CHILDREN = 23
tree = Tree[None]("Root") tree = Tree[None]("Root")

View File

@@ -4,7 +4,7 @@ from textual.widgets import Tree
from textual.widgets.tree import TreeNode from textual.widgets.tree import TreeNode
def test_tree_node_label() -> None: async def test_tree_node_label() -> None:
"""It should be possible to modify a TreeNode's label.""" """It should be possible to modify a TreeNode's label."""
node = TreeNode(Tree[None]("Xenomorph Lifecycle"), None, 0, "Facehugger") node = TreeNode(Tree[None]("Xenomorph Lifecycle"), None, 0, "Facehugger")
assert node.label == Text("Facehugger") assert node.label == Text("Facehugger")
@@ -12,7 +12,7 @@ def test_tree_node_label() -> None:
assert node.label == Text("Chestbuster") assert node.label == Text("Chestbuster")
def test_tree_node_label_via_tree() -> None: async def test_tree_node_label_via_tree() -> None:
"""It should be possible to modify a TreeNode's label when created via a Tree.""" """It should be possible to modify a TreeNode's label when created via a Tree."""
tree = Tree[None]("Xenomorph Lifecycle") tree = Tree[None]("Xenomorph Lifecycle")
node = tree.root.add("Facehugger") node = tree.root.add("Facehugger")

View File

@@ -1,7 +1,7 @@
from textual.widgets import Tree from textual.widgets import Tree
def test_tree_node_parent() -> None: async def test_tree_node_parent() -> None:
"""It should be possible to access a TreeNode's parent.""" """It should be possible to access a TreeNode's parent."""
tree = Tree[None]("Anakin") tree = Tree[None]("Anakin")
child = tree.root.add("Leia") child = tree.root.add("Leia")