Merge pull request #448 from Textualize/features-env

Add TEXTUAL features env var
This commit is contained in:
Will McGugan
2022-04-29 15:18:17 +01:00
committed by GitHub
8 changed files with 103 additions and 20 deletions

View File

@@ -7,5 +7,5 @@
.list-item {
height: 8;
background: dark_blue;
background: darkblue;
}

View File

@@ -19,15 +19,15 @@ class XTermParser(Parser[events.Event]):
_re_sgr_mouse = re.compile(r"\x1b\[<(\d+);(\d+);(\d+)([Mm])")
def __init__(self, sender: MessageTarget, more_data: Callable[[], bool]) -> None:
def __init__(
self, sender: MessageTarget, more_data: Callable[[], bool], debug: bool = False
) -> None:
self.sender = sender
self.more_data = more_data
self.last_x = 0
self.last_y = 0
self._debug_log_file = (
open("keys.log", "wt") if "TEXTUAL_DEBUG" in os.environ else None
)
self._debug_log_file = open("keys.log", "wt") if debug else None
super().__init__()

View File

@@ -36,6 +36,7 @@ from .devtools.client import DevtoolsClient, DevtoolsConnectionError, DevtoolsLo
from .devtools.redirect_output import StdoutRedirector
from .dom import DOMNode
from .driver import Driver
from .features import parse_features, FeatureFlag
from .file_monitor import FileMonitor
from .geometry import Offset, Region, Size
from .layouts.dock import Dock
@@ -89,7 +90,6 @@ class App(Generic[ReturnType], DOMNode):
def __init__(
self,
screen: bool = True,
driver_class: Type[Driver] | None = None,
log: str = "",
log_verbosity: int = 1,
@@ -98,19 +98,21 @@ class App(Generic[ReturnType], DOMNode):
css: str | None = None,
watch_css: bool = True,
):
"""The Textual Application base class
"""Textual application base class
Args:
console (Console, optional): A Rich Console. Defaults to None.
screen (bool, optional): Enable full-screen application mode. Defaults to True.
driver_class (Type[Driver], optional): Driver class, or None to use default. Defaults to None.
title (str, optional): Title of the application. Defaults to "Textual Application".
driver_class (Type[Driver] | None, optional): Driver class or ``None`` to auto-detect. Defaults to None.
log (str, optional): Path to log file, or "" to disable. Defaults to "".
log_verbosity (int, optional): Log verbosity from 0-3. Defaults to 1.
title (str, optional): Default title of the application. Defaults to "Textual Application".
css_file (str | None, optional): Path to CSS or ``None`` for no CSS file. 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.
"""
self.console = Console(
file=sys.__stdout__, markup=False, highlight=False, emoji=False
)
self.error_console = Console(markup=False, stderr=True)
self._screen = screen
self.driver_class = driver_class or self.get_driver_class()
self._title = title
self._screen_stack: list[Screen] = []
@@ -160,6 +162,8 @@ class App(Generic[ReturnType], DOMNode):
if css is not None:
self.css = css
self.features: frozenset[FeatureFlag] = parse_features(os.getenv("TEXTUAL", ""))
self.registry: set[MessagePump] = set()
self.devtools = DevtoolsClient()
@@ -173,6 +177,16 @@ class App(Generic[ReturnType], DOMNode):
background: Reactive[str] = Reactive("black")
dark = Reactive(False)
@property
def devtools_enabled(self) -> bool:
"""Check if devtools are enabled."""
return "devtools" in self.features
@property
def debug(self) -> bool:
"""Check if debug mode is enabled."""
return "debug" in self.features
def exit(self, result: ReturnType | None = None) -> None:
"""Exit the app, and return the supplied result.
@@ -495,7 +509,7 @@ class App(Generic[ReturnType], DOMNode):
log("---")
log(f"driver={self.driver_class}")
if os.getenv("TEXTUAL_DEVTOOLS") == "1":
if self.devtools_enabled:
try:
await self.devtools.connect()
self.log(f"Connected to devtools ({self.devtools.url})")
@@ -504,7 +518,6 @@ class App(Generic[ReturnType], DOMNode):
try:
if self.css_file is not None:
self.stylesheet.read(self.css_file)
self.stylesheet.parse()
if self.css is not None:
self.stylesheet.add_source(
self.css, path=f"<{self.__class__.__name__}>"

View File

@@ -15,9 +15,12 @@ if TYPE_CHECKING:
class Driver(ABC):
def __init__(self, console: "Console", target: "MessageTarget") -> None:
def __init__(
self, console: "Console", target: "MessageTarget", debug: bool = False
) -> None:
self.console = console
self._target = target
self._debug = debug
self._loop = asyncio.get_event_loop()
self._mouse_down_time = time()

View File

@@ -27,8 +27,10 @@ from .._profile import timer
class LinuxDriver(Driver):
"""Powers display and input for Linux / MacOS"""
def __init__(self, console: "Console", target: "MessageTarget") -> None:
super().__init__(console, target)
def __init__(
self, console: "Console", target: "MessageTarget", debug: bool = False
) -> None:
super().__init__(console, target, debug)
self.fileno = sys.stdin.fileno()
self.attrs_before: list[Any] | None = None
self.exit_event = Event()
@@ -189,7 +191,7 @@ class LinuxDriver(Driver):
return True
return False
parser = XTermParser(self._target, more_data)
parser = XTermParser(self._target, more_data, self._debug)
feed = parser.feed
utf8_decoder = getincrementaldecoder("utf-8")().decode

View File

@@ -17,8 +17,10 @@ if TYPE_CHECKING:
class WindowsDriver(Driver):
"""Powers display and input for Windows."""
def __init__(self, console: "Console", target: "MessageTarget") -> None:
super().__init__(console, target)
def __init__(
self, console: "Console", target: "MessageTarget", debug: bool = False
) -> None:
super().__init__(console, target, debug)
self.in_fileno = sys.stdin.fileno()
self.out_fileno = sys.stdout.fileno()

34
src/textual/features.py Normal file
View File

@@ -0,0 +1,34 @@
from __future__ import annotations
import sys
from typing import cast
if sys.version_info >= (3, 8):
from typing import Final, Literal
else:
from typing_extensions import Final, Literal
FEATURES: Final = {"devtools", "debug"}
FeatureFlag = Literal["devtools", "debug"]
def parse_features(features: str) -> frozenset[FeatureFlag]:
"""Parse features env var
Args:
features (str): Comma seprated feature flags
Returns:
frozenset[FeatureFlag]: A frozen set of known features.
"""
features_set = frozenset(
set(
feature.strip().lower()
for feature in features.split(",")
if feature.strip()
).intersection(FEATURES)
)
return cast("frozenset[FeatureFlag]", features_set)

29
tests/test_features.py Normal file
View File

@@ -0,0 +1,29 @@
from __future__ import annotations
from textual.app import App
def test_textual_env_var(monkeypatch):
monkeypatch.setenv("TEXTUAL", "")
app = App()
assert app.features == set()
assert app.devtools_enabled is False
assert app.debug is False
monkeypatch.setenv("TEXTUAL", "devtools")
app = App()
assert app.features == {"devtools"}
assert app.devtools_enabled is True
assert app.debug is False
monkeypatch.setenv("TEXTUAL", "devtools,debug")
app = App()
assert app.features == {"devtools", "debug"}
assert app.devtools_enabled is True
assert app.debug is True
monkeypatch.setenv("TEXTUAL", "devtools, debug")
app = App()
assert app.features == {"devtools", "debug"}
assert app.devtools_enabled is True
assert app.debug is True