mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge pull request #449 from Textualize/add-e2e-smoke-test-to-CI
[CI] Check that the "basic.py" sandbox script can be run for a few seconds without crashing
This commit is contained in:
4
.github/workflows/pythonpackage.yml
vendored
4
.github/workflows/pythonpackage.yml
vendored
@@ -39,6 +39,10 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
source $VENV
|
source $VENV
|
||||||
pytest tests -v --cov=./src/textual --cov-report=xml:./coverage.xml --cov-report term-missing
|
pytest tests -v --cov=./src/textual --cov-report=xml:./coverage.xml --cov-report term-missing
|
||||||
|
- name: Quick e2e smoke test
|
||||||
|
run: |
|
||||||
|
source $VENV
|
||||||
|
python e2e_tests/sandbox_basic_test.py basic 2.0
|
||||||
- name: Upload code coverage
|
- name: Upload code coverage
|
||||||
uses: codecov/codecov-action@v1.0.10
|
uses: codecov/codecov-action@v1.0.10
|
||||||
with:
|
with:
|
||||||
|
|||||||
55
e2e_tests/sandbox_basic_test.py
Normal file
55
e2e_tests/sandbox_basic_test.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
target_script_name = "basic"
|
||||||
|
script_time_to_live = 2.0 # in seconds
|
||||||
|
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
target_script_name = sys.argv[1]
|
||||||
|
if len(sys.argv) > 2:
|
||||||
|
script_time_to_live = float(sys.argv[2])
|
||||||
|
|
||||||
|
e2e_root = Path(__file__).parent
|
||||||
|
|
||||||
|
completed_process = None
|
||||||
|
|
||||||
|
|
||||||
|
def launch_sandbox_script(python_file_name: str) -> None:
|
||||||
|
global completed_process
|
||||||
|
|
||||||
|
command = f"{sys.executable} ./test_apps/{shlex.quote(python_file_name)}.py"
|
||||||
|
print(f"Launching command '{command}'...")
|
||||||
|
try:
|
||||||
|
completed_process = subprocess.run(
|
||||||
|
command, shell=True, check=True, capture_output=True, cwd=str(e2e_root)
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as err:
|
||||||
|
print(f"Process error: {err.stderr}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=launch_sandbox_script, args=(target_script_name,), daemon=True
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Launching Python script in a sub-thread; we'll wait for it for {script_time_to_live} seconds..."
|
||||||
|
)
|
||||||
|
thread.join(timeout=script_time_to_live)
|
||||||
|
print("The wait is over.")
|
||||||
|
|
||||||
|
process_still_running = completed_process is None
|
||||||
|
process_was_able_to_run_without_errors = process_still_running
|
||||||
|
|
||||||
|
if process_was_able_to_run_without_errors:
|
||||||
|
print("Python script is still running :-)")
|
||||||
|
else:
|
||||||
|
print("Python script is no longer running :-/")
|
||||||
|
|
||||||
|
sys.exit(0 if process_was_able_to_run_without_errors else 1)
|
||||||
227
e2e_tests/test_apps/basic.css
Normal file
227
e2e_tests/test_apps/basic.css
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
/* CSS file for basic.py */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
* {
|
||||||
|
transition: color 300ms linear, background 300ms linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
* {
|
||||||
|
scrollbar-background: $panel-darken-2;
|
||||||
|
scrollbar-background-hover: $panel-darken-3;
|
||||||
|
scrollbar-color: $system;
|
||||||
|
scrollbar-color-active: $accent-darken-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
App > Screen {
|
||||||
|
layout: dock;
|
||||||
|
docks: side=left/1;
|
||||||
|
background: $surface;
|
||||||
|
color: $text-surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#sidebar {
|
||||||
|
color: $text-primary;
|
||||||
|
background: $primary;
|
||||||
|
dock: side;
|
||||||
|
width: 30;
|
||||||
|
offset-x: -100%;
|
||||||
|
layout: dock;
|
||||||
|
transition: offset 500ms in_out_cubic;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar.-active {
|
||||||
|
offset-x: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar .title {
|
||||||
|
height: 3;
|
||||||
|
background: $primary-darken-2;
|
||||||
|
color: $text-primary-darken-2 ;
|
||||||
|
border-right: outer $primary-darken-3;
|
||||||
|
content-align: center middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar .user {
|
||||||
|
height: 8;
|
||||||
|
background: $primary-darken-1;
|
||||||
|
color: $text-primary-darken-1;
|
||||||
|
border-right: outer $primary-darken-3;
|
||||||
|
content-align: center middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar .content {
|
||||||
|
background: $primary;
|
||||||
|
color: $text-primary;
|
||||||
|
border-right: outer $primary-darken-3;
|
||||||
|
content-align: center middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header {
|
||||||
|
color: $text-primary-darken-1;
|
||||||
|
background: $primary-darken-1;
|
||||||
|
height: 3;
|
||||||
|
content-align: center middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
color: $text-background;
|
||||||
|
background: $background;
|
||||||
|
layout: vertical;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Tweet {
|
||||||
|
height: 12;
|
||||||
|
width: 80;
|
||||||
|
|
||||||
|
margin: 1 3;
|
||||||
|
background: $panel;
|
||||||
|
color: $text-panel;
|
||||||
|
layout: vertical;
|
||||||
|
/* border: outer $primary; */
|
||||||
|
padding: 1;
|
||||||
|
border: wide $panel-darken-2;
|
||||||
|
overflow-y: scroll;
|
||||||
|
align-horizontal: center;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable {
|
||||||
|
width: 80;
|
||||||
|
overflow-y: scroll;
|
||||||
|
max-width:80;
|
||||||
|
height: 20;
|
||||||
|
align-horizontal: center;
|
||||||
|
layout: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
|
||||||
|
height: 34;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TweetHeader {
|
||||||
|
height:1;
|
||||||
|
background: $accent;
|
||||||
|
color: $text-accent
|
||||||
|
}
|
||||||
|
|
||||||
|
TweetBody {
|
||||||
|
width: 100%;
|
||||||
|
background: $panel;
|
||||||
|
color: $text-panel;
|
||||||
|
height:20;
|
||||||
|
padding: 0 1 0 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background: $accent;
|
||||||
|
color: $text-accent;
|
||||||
|
width:20;
|
||||||
|
height: 3;
|
||||||
|
/* border-top: hidden $accent-darken-3; */
|
||||||
|
border: tall $accent-darken-2;
|
||||||
|
/* border-left: tall $accent-darken-1; */
|
||||||
|
|
||||||
|
|
||||||
|
/* padding: 1 0 0 0 ; */
|
||||||
|
|
||||||
|
transition: background 200ms in_out_cubic, color 300ms in_out_cubic;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background: $accent-lighten-1;
|
||||||
|
color: $text-accent-lighten-1;
|
||||||
|
width: 20;
|
||||||
|
height: 3;
|
||||||
|
border: tall $accent-darken-1;
|
||||||
|
/* border-left: tall $accent-darken-3; */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer {
|
||||||
|
color: $text-accent;
|
||||||
|
background: $accent;
|
||||||
|
height: 1;
|
||||||
|
border-top: hkey $accent-darken-2;
|
||||||
|
content-align: center middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#sidebar .content {
|
||||||
|
layout: vertical
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionItem {
|
||||||
|
height: 3;
|
||||||
|
background: $primary;
|
||||||
|
transition: background 100ms linear;
|
||||||
|
border-right: outer $primary-darken-2;
|
||||||
|
border-left: hidden;
|
||||||
|
content-align: center middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
OptionItem:hover {
|
||||||
|
height: 3;
|
||||||
|
color: $accent;
|
||||||
|
background: $primary-darken-1;
|
||||||
|
/* border-top: hkey $accent2-darken-3;
|
||||||
|
border-bottom: hkey $accent2-darken-3; */
|
||||||
|
text-style: bold;
|
||||||
|
border-left: outer $accent-darken-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
Error {
|
||||||
|
width: 80;
|
||||||
|
height:3;
|
||||||
|
background: $error;
|
||||||
|
color: $text-error;
|
||||||
|
border-top: hkey $error-darken-2;
|
||||||
|
border-bottom: hkey $error-darken-2;
|
||||||
|
margin: 1 3;
|
||||||
|
|
||||||
|
text-style: bold;
|
||||||
|
align-horizontal: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
Warning {
|
||||||
|
width: 80;
|
||||||
|
height:3;
|
||||||
|
background: $warning;
|
||||||
|
color: $text-warning-fade-1;
|
||||||
|
border-top: hkey $warning-darken-2;
|
||||||
|
border-bottom: hkey $warning-darken-2;
|
||||||
|
margin: 1 2;
|
||||||
|
text-style: bold;
|
||||||
|
align-horizontal: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
Success {
|
||||||
|
width: 80;
|
||||||
|
height:3;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: $success-lighten-3;
|
||||||
|
color: $text-success-lighten-3-fade-1;
|
||||||
|
border-top: hkey $success;
|
||||||
|
border-bottom: hkey $success;
|
||||||
|
margin: 1 2;
|
||||||
|
text-style: bold;
|
||||||
|
align-horizontal: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.horizontal {
|
||||||
|
layout: horizontal
|
||||||
|
}
|
||||||
150
e2e_tests/test_apps/basic.py
Normal file
150
e2e_tests/test_apps/basic.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from rich.align import Align
|
||||||
|
from rich.console import RenderableType
|
||||||
|
from rich.syntax import Syntax
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
|
from textual.app import App
|
||||||
|
from textual.widget import Widget
|
||||||
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
CODE = '''
|
||||||
|
class Offset(NamedTuple):
|
||||||
|
"""A point defined by x and y coordinates."""
|
||||||
|
|
||||||
|
x: int = 0
|
||||||
|
y: int = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_origin(self) -> bool:
|
||||||
|
"""Check if the point is at the origin (0, 0)"""
|
||||||
|
return self == (0, 0)
|
||||||
|
|
||||||
|
def __bool__(self) -> bool:
|
||||||
|
return self != (0, 0)
|
||||||
|
|
||||||
|
def __add__(self, other: object) -> Offset:
|
||||||
|
if isinstance(other, tuple):
|
||||||
|
_x, _y = self
|
||||||
|
x, y = other
|
||||||
|
return Offset(_x + x, _y + y)
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __sub__(self, other: object) -> Offset:
|
||||||
|
if isinstance(other, tuple):
|
||||||
|
_x, _y = self
|
||||||
|
x, y = other
|
||||||
|
return Offset(_x - x, _y - y)
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __mul__(self, other: object) -> Offset:
|
||||||
|
if isinstance(other, (float, int)):
|
||||||
|
x, y = self
|
||||||
|
return Offset(int(x * other), int(y * other))
|
||||||
|
return NotImplemented
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
lorem = Text.from_markup(
|
||||||
|
"""Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit libero, volutpat nec hendrerit at, faucibus in odio. Aliquam hendrerit nibh sed quam volutpat maximus. Nullam suscipit convallis lorem quis sodales. In tristique lobortis ante et dictum. Ut at finibus ipsum. In urna dolor, placerat et mi facilisis, congue sollicitudin massa. Phasellus felis turpis, cursus eu lectus et, porttitor malesuada augue. Sed feugiat volutpat velit, sollicitudin fringilla velit bibendum faucibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit libero, volutpat nec hendrerit at, faucibus in odio. Aliquam hendrerit nibh sed quam volutpat maximus. Nullam suscipit convallis lorem quis sodales. In tristique lobortis ante et dictum. Ut at finibus ipsum. In urna dolor, placerat et mi facilisis, congue sollicitudin massa. Phasellus felis turpis, cursus eu lectus et, porttitor malesuada augue. Sed feugiat volutpat velit, sollicitudin fringilla velit bibendum faucibus. """
|
||||||
|
"""Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit libero, volutpat nec hendrerit at, faucibus in odio. Aliquam hendrerit nibh sed quam volutpat maximus. Nullam suscipit convallis lorem quis sodales. In tristique lobortis ante et dictum. Ut at finibus ipsum. In urna dolor, placerat et mi facilisis, congue sollicitudin massa. Phasellus felis turpis, cursus eu lectus et, porttitor malesuada augue. Sed feugiat volutpat velit, sollicitudin fringilla velit bibendum faucibus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. In velit libero, volutpat nec hendrerit at, faucibus in odio. Aliquam hendrerit nibh sed quam volutpat maximus. Nullam suscipit convallis lorem quis sodales. In tristique lobortis ante et dictum. Ut at finibus ipsum. In urna dolor, placerat et mi facilisis, congue sollicitudin massa. Phasellus felis turpis, cursus eu lectus et, porttitor malesuada augue. Sed feugiat volutpat velit, sollicitudin fringilla velit bibendum faucibus. """
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TweetHeader(Widget):
|
||||||
|
def render(self) -> RenderableType:
|
||||||
|
return Text("Lorem Impsum", justify="center")
|
||||||
|
|
||||||
|
|
||||||
|
class TweetBody(Widget):
|
||||||
|
def render(self) -> Text:
|
||||||
|
return lorem
|
||||||
|
|
||||||
|
|
||||||
|
class Tweet(Widget):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class OptionItem(Widget):
|
||||||
|
def render(self) -> Text:
|
||||||
|
return Text("Option")
|
||||||
|
|
||||||
|
|
||||||
|
class Error(Widget):
|
||||||
|
def render(self) -> Text:
|
||||||
|
return Text("This is an error message", justify="center")
|
||||||
|
|
||||||
|
|
||||||
|
class Warning(Widget):
|
||||||
|
def render(self) -> Text:
|
||||||
|
return Text("This is a warning message", justify="center")
|
||||||
|
|
||||||
|
|
||||||
|
class Success(Widget):
|
||||||
|
def render(self) -> Text:
|
||||||
|
return Text("This is a success message", justify="center")
|
||||||
|
|
||||||
|
|
||||||
|
class BasicApp(App):
|
||||||
|
"""A basic app demonstrating CSS"""
|
||||||
|
|
||||||
|
def on_load(self):
|
||||||
|
"""Bind keys here."""
|
||||||
|
self.bind("tab", "toggle_class('#sidebar', '-active')")
|
||||||
|
|
||||||
|
def on_mount(self):
|
||||||
|
"""Build layout here."""
|
||||||
|
self.mount(
|
||||||
|
header=Static(
|
||||||
|
Text.from_markup(
|
||||||
|
"[b]This is a [u]Textual[/u] app, running in the terminal"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
content=Widget(
|
||||||
|
Tweet(
|
||||||
|
TweetBody(),
|
||||||
|
# Widget(
|
||||||
|
# Widget(classes={"button"}),
|
||||||
|
# Widget(classes={"button"}),
|
||||||
|
# classes={"horizontal"},
|
||||||
|
# ),
|
||||||
|
),
|
||||||
|
Widget(
|
||||||
|
Static(Syntax(CODE, "python"), classes="code"),
|
||||||
|
classes="scrollable",
|
||||||
|
),
|
||||||
|
Error(),
|
||||||
|
Tweet(TweetBody()),
|
||||||
|
Warning(),
|
||||||
|
Tweet(TweetBody()),
|
||||||
|
Success(),
|
||||||
|
),
|
||||||
|
footer=Widget(),
|
||||||
|
sidebar=Widget(
|
||||||
|
Widget(classes="title"),
|
||||||
|
Widget(classes="user"),
|
||||||
|
OptionItem(),
|
||||||
|
OptionItem(),
|
||||||
|
OptionItem(),
|
||||||
|
Widget(classes="content"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_key(self, event) -> None:
|
||||||
|
await self.dispatch_key(event)
|
||||||
|
|
||||||
|
def key_d(self):
|
||||||
|
self.dark = not self.dark
|
||||||
|
|
||||||
|
def key_x(self):
|
||||||
|
self.panic(self.tree)
|
||||||
|
|
||||||
|
|
||||||
|
css_file = Path(__file__).parent / "basic.css"
|
||||||
|
app = BasicApp(
|
||||||
|
css_file=str(css_file), watch_css=True, log="textual.log", log_verbosity=0
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run()
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from rich.align import Align
|
from rich.align import Align
|
||||||
from rich.console import RenderableType
|
from rich.console import RenderableType
|
||||||
from rich.syntax import Syntax
|
from rich.syntax import Syntax
|
||||||
@@ -139,7 +141,8 @@ class BasicApp(App):
|
|||||||
self.panic(self.tree)
|
self.panic(self.tree)
|
||||||
|
|
||||||
|
|
||||||
app = BasicApp(css_file="basic.css", watch_css=True, log="textual.log")
|
css_file = Path(__file__).parent / "basic.css"
|
||||||
|
app = BasicApp(css_file=str(css_file), watch_css=True, log="textual.log")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run()
|
app.run()
|
||||||
|
|||||||
@@ -448,7 +448,7 @@ class Compositor:
|
|||||||
segment_lines: list[list[Segment]] = [
|
segment_lines: list[list[Segment]] = [
|
||||||
sum(
|
sum(
|
||||||
[line for line in bucket.values() if line is not None],
|
[line for line in bucket.values() if line is not None],
|
||||||
start=[],
|
[],
|
||||||
)
|
)
|
||||||
for bucket in chops
|
for bucket in chops
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import weakref
|
import weakref
|
||||||
from asyncio import (
|
from asyncio import (
|
||||||
get_event_loop,
|
|
||||||
CancelledError,
|
CancelledError,
|
||||||
Event,
|
Event,
|
||||||
sleep,
|
sleep,
|
||||||
@@ -84,7 +84,7 @@ class Timer:
|
|||||||
Returns:
|
Returns:
|
||||||
Task: A Task instance for the timer.
|
Task: A Task instance for the timer.
|
||||||
"""
|
"""
|
||||||
self._task = get_event_loop().create_task(self._run())
|
self._task = asyncio.create_task(self._run())
|
||||||
return self._task
|
return self._task
|
||||||
|
|
||||||
async def stop(self) -> None:
|
async def stop(self) -> None:
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import os
|
|||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
from asyncio import AbstractEventLoop
|
|
||||||
from contextlib import redirect_stdout
|
from contextlib import redirect_stdout
|
||||||
from time import perf_counter
|
from time import perf_counter
|
||||||
from typing import Any, Generic, Iterable, TextIO, Type, TypeVar, TYPE_CHECKING
|
from typing import Any, Generic, Iterable, TextIO, Type, TypeVar, TYPE_CHECKING
|
||||||
@@ -28,9 +27,8 @@ from ._animator import Animator
|
|||||||
from ._callback import invoke
|
from ._callback import invoke
|
||||||
from ._context import active_app
|
from ._context import active_app
|
||||||
from ._event_broker import extract_handler_actions, NoHandler
|
from ._event_broker import extract_handler_actions, NoHandler
|
||||||
from ._profile import timer
|
|
||||||
from .binding import Bindings, NoBinding
|
from .binding import Bindings, NoBinding
|
||||||
from .css.stylesheet import Stylesheet, StylesheetError
|
from .css.stylesheet import Stylesheet
|
||||||
from .design import ColorSystem
|
from .design import ColorSystem
|
||||||
from .devtools.client import DevtoolsClient, DevtoolsConnectionError, DevtoolsLog
|
from .devtools.client import DevtoolsClient, DevtoolsConnectionError, DevtoolsLog
|
||||||
from .devtools.redirect_output import StdoutRedirector
|
from .devtools.redirect_output import StdoutRedirector
|
||||||
@@ -54,6 +52,9 @@ WINDOWS = PLATFORM == "Windows"
|
|||||||
# asyncio will warn against resources not being cleared
|
# asyncio will warn against resources not being cleared
|
||||||
warnings.simplefilter("always", ResourceWarning)
|
warnings.simplefilter("always", ResourceWarning)
|
||||||
|
|
||||||
|
# `asyncio.get_event_loop()` is deprecated since Python 3.10:
|
||||||
|
_ASYNCIO_GET_EVENT_LOOP_IS_DEPRECATED = sys.version_info >= (3, 10, 0)
|
||||||
|
|
||||||
LayoutDefinition = "dict[str, Any]"
|
LayoutDefinition = "dict[str, Any]"
|
||||||
|
|
||||||
|
|
||||||
@@ -109,6 +110,11 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
css (str | None, optional): CSS code to parse, or ``None`` for no literal CSS. Defaults to None.
|
css (str | None, optional): CSS code to parse, or ``None`` for no literal CSS. Defaults to None.
|
||||||
watch_css (bool, optional): Watch CSS for changes. Defaults to True.
|
watch_css (bool, optional): Watch CSS for changes. Defaults to True.
|
||||||
"""
|
"""
|
||||||
|
# N.B. This must be done *before* we call the parent constructor, because MessagePump's
|
||||||
|
# constructor instantiates a `asyncio.PriorityQueue` and in Python versions older than 3.10
|
||||||
|
# this will create some first references to an asyncio loop.
|
||||||
|
_init_uvloop()
|
||||||
|
|
||||||
self.console = Console(
|
self.console = Console(
|
||||||
file=sys.__stdout__, markup=False, highlight=False, emoji=False
|
file=sys.__stdout__, markup=False, highlight=False, emoji=False
|
||||||
)
|
)
|
||||||
@@ -319,25 +325,19 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
keys, action, description, show=show, key_display=key_display
|
keys, action, description, show=show, key_display=key_display
|
||||||
)
|
)
|
||||||
|
|
||||||
def run(self, loop: AbstractEventLoop | None = None) -> ReturnType | None:
|
def run(self) -> ReturnType | None:
|
||||||
|
"""The entry point to run a Textual app."""
|
||||||
|
|
||||||
async def run_app() -> None:
|
async def run_app() -> None:
|
||||||
await self.process_messages()
|
await self.process_messages()
|
||||||
|
|
||||||
if loop:
|
if _ASYNCIO_GET_EVENT_LOOP_IS_DEPRECATED:
|
||||||
asyncio.set_event_loop(loop)
|
# N.B. This doesn't work with Python<3.10, as we end up with 2 event loops:
|
||||||
else:
|
|
||||||
try:
|
|
||||||
import uvloop
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
|
||||||
|
|
||||||
event_loop = asyncio.get_event_loop()
|
|
||||||
try:
|
|
||||||
asyncio.run(run_app())
|
asyncio.run(run_app())
|
||||||
finally:
|
else:
|
||||||
event_loop.close()
|
# However, this works with Python<3.10:
|
||||||
|
event_loop = asyncio.get_event_loop()
|
||||||
|
event_loop.run_until_complete(run_app())
|
||||||
|
|
||||||
return self._return_value
|
return self._return_value
|
||||||
|
|
||||||
@@ -508,6 +508,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
active_app.set(self)
|
active_app.set(self)
|
||||||
log("---")
|
log("---")
|
||||||
log(f"driver={self.driver_class}")
|
log(f"driver={self.driver_class}")
|
||||||
|
log(f"asyncio running loop={asyncio.get_running_loop()!r}")
|
||||||
|
|
||||||
if self.devtools_enabled:
|
if self.devtools_enabled:
|
||||||
try:
|
try:
|
||||||
@@ -877,3 +878,26 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
|
|
||||||
async def handle_styles_updated(self, message: messages.StylesUpdated) -> None:
|
async def handle_styles_updated(self, message: messages.StylesUpdated) -> None:
|
||||||
self.stylesheet.update(self, animate=True)
|
self.stylesheet.update(self, animate=True)
|
||||||
|
|
||||||
|
|
||||||
|
_uvloop_init_done: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
def _init_uvloop() -> None:
|
||||||
|
"""
|
||||||
|
Import and install the `uvloop` asyncio policy, if available.
|
||||||
|
This is done only once, even if the function is called multiple times.
|
||||||
|
"""
|
||||||
|
global _uvloop_init_done
|
||||||
|
|
||||||
|
if _uvloop_init_done:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
import uvloop
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
uvloop.install()
|
||||||
|
|
||||||
|
_uvloop_init_done = True
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class Driver(ABC):
|
|||||||
self.console = console
|
self.console = console
|
||||||
self._target = target
|
self._target = target
|
||||||
self._debug = debug
|
self._debug = debug
|
||||||
self._loop = asyncio.get_event_loop()
|
self._loop = asyncio.get_running_loop()
|
||||||
self._mouse_down_time = time()
|
self._mouse_down_time = time()
|
||||||
|
|
||||||
def send_event(self, event: events.Event) -> None:
|
def send_event(self, event: events.Event) -> None:
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class LinuxDriver(Driver):
|
|||||||
|
|
||||||
def start_application_mode(self):
|
def start_application_mode(self):
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
def send_size_event():
|
def send_size_event():
|
||||||
terminal_size = self._get_terminal_size()
|
terminal_size = self._get_terminal_size()
|
||||||
@@ -121,7 +121,7 @@ class LinuxDriver(Driver):
|
|||||||
self.console.file.write("\033[?1003h\n")
|
self.console.file.write("\033[?1003h\n")
|
||||||
self.console.file.flush()
|
self.console.file.flush()
|
||||||
self._key_thread = Thread(
|
self._key_thread = Thread(
|
||||||
target=self.run_input_thread, args=(asyncio.get_event_loop(),)
|
target=self.run_input_thread, args=(asyncio.get_running_loop(),)
|
||||||
)
|
)
|
||||||
send_size_event()
|
send_size_event()
|
||||||
self._key_thread.start()
|
self._key_thread.start()
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class WindowsDriver(Driver):
|
|||||||
|
|
||||||
def start_application_mode(self) -> None:
|
def start_application_mode(self) -> None:
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
self._restore_console = win32.enable_application_mode()
|
self._restore_console = win32.enable_application_mode()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user