diff --git a/README.md b/README.md index 866cba6f9..a8dd8f1be 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ class ColorChanger(App): self.background = f"on color({event.key})" -ColorChanger.run(log="textual.log") +ColorChanger.run(log_path="textual.log") ``` You'll notice that the `on_key` method above contains an additional `event` parameter which wasn't present on the beeper example. If the `event` argument is present, Textual will call the handler with an event object. Every event has an associated handler object, in this case it is a KeyEvent which contains additional information regarding which key was pressed. @@ -114,7 +114,7 @@ class SimpleApp(App): await self.view.dock(Placeholder(), Placeholder(), edge="top") -SimpleApp.run(log="textual.log") +SimpleApp.run(log_path="textual.log") ``` This app contains a single event handler `on_mount`. The mount event is sent when the app or widget is ready to start processing events, and is typically used for initialization. You may have noticed that `on_mount` is an `async` function. Since Textual is an asynchronous framework we will need this if we need to call most other methods. @@ -137,7 +137,7 @@ await self.view.dock(Placeholder(), Placeholder(), edge="top") You will notice that this time we are docking _two_ Placeholder objects onto the `"top"` edge. We haven't set an explicit size this time so Textual will divide the remaining size amongst the two new widgets. -The last line calls the `run` class method in the usual way, but with an argument we haven't seen before: `log="textual.log"` tells Textual to write log information to the given file. You can tail textual.log to see events being processed and other debug information. +The last line calls the `run` class method in the usual way, but with an argument we haven't seen before: `log_path="textual.log"` tells Textual to write log information to the given file. You can tail textual.log to see events being processed and other debug information. If you run the above example, you will see something like the following: @@ -183,7 +183,7 @@ class HoverApp(App): await self.view.dock(*hovers, edge="top") -HoverApp.run(log="textual.log") +HoverApp.run(log_path="textual.log") ``` The `Hover` class is a custom widget which displays a panel containing the classic text "Hello World". The first line in the Hover class may seem a little mysterious at this point: diff --git a/docs/examples/messages_and_events/color_changer.py b/docs/examples/messages_and_events/color_changer.py index 7d43a65e7..58e19c9db 100644 --- a/docs/examples/messages_and_events/color_changer.py +++ b/docs/examples/messages_and_events/color_changer.py @@ -7,4 +7,4 @@ class ColorChanger(App): self.background = f"on color({event.key})" -ColorChanger.run(log="textual.log") +ColorChanger.run(log_path="textual.log") diff --git a/docs/examples/widgets/custom.py b/docs/examples/widgets/custom.py index f47d585cf..8fda42589 100644 --- a/docs/examples/widgets/custom.py +++ b/docs/examples/widgets/custom.py @@ -29,4 +29,4 @@ class HoverApp(App): await self.screen.dock(*hovers, edge="top") -HoverApp.run(log="textual.log") +HoverApp.run(log_path="textual.log") diff --git a/docs/examples/widgets/placeholders.py b/docs/examples/widgets/placeholders.py index 5e6dea609..9f7a96069 100644 --- a/docs/examples/widgets/placeholders.py +++ b/docs/examples/widgets/placeholders.py @@ -12,4 +12,4 @@ class SimpleApp(App): await self.screen.dock(Placeholder(), Placeholder(), edge="top") -SimpleApp.run(log="textual.log") +SimpleApp.run(log_path="textual.log") diff --git a/e2e_tests/test_apps/basic.py b/e2e_tests/test_apps/basic.py index a4816fe1c..7a833d843 100644 --- a/e2e_tests/test_apps/basic.py +++ b/e2e_tests/test_apps/basic.py @@ -141,9 +141,12 @@ class BasicApp(App): self.panic(self.tree) -css_file = Path(__file__).parent / "basic.css" +sandbox_folder = Path(__file__).parent app = BasicApp( - css_file=str(css_file), watch_css=True, log="textual.log", log_verbosity=0 + css_path=sandbox_folder / "basic.css", + watch_css=True, + log_path=sandbox_folder / "basic.log", + log_verbosity=0, ) if __name__ == "__main__": diff --git a/examples/animation.py b/examples/animation.py index 7e9206b7d..0a6efaf19 100644 --- a/examples/animation.py +++ b/examples/animation.py @@ -35,4 +35,4 @@ class SmoothApp(App): # self.set_timer(10, lambda: self.action("quit")) -SmoothApp.run(log="textual.log", log_verbosity=2) +SmoothApp.run(log_path="textual.log", log_verbosity=2) diff --git a/examples/big_table.py b/examples/big_table.py index 23a66d47d..7bfc428d7 100644 --- a/examples/big_table.py +++ b/examples/big_table.py @@ -30,4 +30,4 @@ class MyApp(App): await self.call_later(add_content) -MyApp.run(title="Simple App", log="textual.log") +MyApp.run(title="Simple App", log_path="textual.log") diff --git a/examples/borders.py b/examples/borders.py index 694c82717..36e0bae47 100644 --- a/examples/borders.py +++ b/examples/borders.py @@ -55,4 +55,4 @@ class BordersApp(App): self.mount(borders=borders_view) -BordersApp.run(css_file="borders.css", log="textual.log") +BordersApp.run(css_path="borders.css", log_path="textual.log") diff --git a/examples/calculator.py b/examples/calculator.py index 0353c5b60..bbc28badf 100644 --- a/examples/calculator.py +++ b/examples/calculator.py @@ -212,4 +212,4 @@ class CalculatorApp(App): await self.screen.dock(Calculator()) -CalculatorApp.run(title="Calculator Test", log="textual.log") +CalculatorApp.run(title="Calculator Test", log_path="textual.log") diff --git a/examples/code_viewer.py b/examples/code_viewer.py index 4b9def743..6c9e4ce91 100644 --- a/examples/code_viewer.py +++ b/examples/code_viewer.py @@ -67,4 +67,4 @@ class MyApp(App): # Run our app class -MyApp.run(title="Code Viewer", log="textual.log") +MyApp.run(title="Code Viewer", log_path="textual.log") diff --git a/examples/easing.py b/examples/easing.py index 339d414de..447f69e5f 100644 --- a/examples/easing.py +++ b/examples/easing.py @@ -42,4 +42,4 @@ class EasingApp(App): self.side = not self.side -EasingApp().run(log="textual.log") +EasingApp().run(log_path="textual.log") diff --git a/examples/grid.py b/examples/grid.py index 7afe5690b..23873c406 100644 --- a/examples/grid.py +++ b/examples/grid.py @@ -31,4 +31,4 @@ class GridTest(App): ) -GridTest.run(title="Grid Test", log="textual.log") +GridTest.run(title="Grid Test", log_path="textual.log") diff --git a/examples/grid_auto.py b/examples/grid_auto.py index dc811f599..7a738670a 100644 --- a/examples/grid_auto.py +++ b/examples/grid_auto.py @@ -19,4 +19,4 @@ class GridTest(App): grid.place(*placeholders, center=Placeholder()) -GridTest.run(title="Grid Test", log="textual.log") +GridTest.run(title="Grid Test", log_path="textual.log") diff --git a/examples/simple.py b/examples/simple.py index 016328d2c..0d75c5190 100644 --- a/examples/simple.py +++ b/examples/simple.py @@ -48,4 +48,4 @@ class MyApp(App): await self.call_later(get_markdown, "richreadme.md") -MyApp.run(title="Simple App", log="textual.log") +MyApp.run(title="Simple App", log_path="textual.log") diff --git a/sandbox/align.py b/sandbox/align.py index 59984b57e..19cd4a933 100644 --- a/sandbox/align.py +++ b/sandbox/align.py @@ -22,4 +22,4 @@ class AlignApp(App): self.log(self.screen.tree) -AlignApp.run(css_file="align.css", log="textual.log", watch_css=True) +AlignApp.run(css_path="align.css", log_path="textual.log", watch_css=True) diff --git a/sandbox/basic.py b/sandbox/basic.py index 12cb26211..c56703c57 100644 --- a/sandbox/basic.py +++ b/sandbox/basic.py @@ -1,6 +1,3 @@ -from pathlib import Path - -from rich.align import Align from rich.console import RenderableType from rich.syntax import Syntax from rich.text import Text @@ -141,8 +138,11 @@ class BasicApp(App): self.panic(self.tree) -css_file = Path(__file__).parent / "basic.css" -app = BasicApp(css_file=str(css_file), watch_css=True, log="textual.log") +app = BasicApp( + css_path="basic.css", + watch_css=True, + log_path="textual.log", +) if __name__ == "__main__": app.run() diff --git a/sandbox/buttons.py b/sandbox/buttons.py index 149359573..e61a73baf 100644 --- a/sandbox/buttons.py +++ b/sandbox/buttons.py @@ -5,7 +5,6 @@ from textual import layout class ButtonsApp(App[str]): - def compose(self) -> ComposeResult: yield layout.Vertical( Button("foo", id="foo"), @@ -19,7 +18,7 @@ class ButtonsApp(App[str]): self.exit(event.button.id) -app = ButtonsApp(log="textual.log", log_verbosity=2) +app = ButtonsApp(log_path="textual.log", log_verbosity=2) if __name__ == "__main__": result = app.run() diff --git a/sandbox/dev_sandbox.py b/sandbox/dev_sandbox.py index ffbfcf8c2..0ead807d4 100644 --- a/sandbox/dev_sandbox.py +++ b/sandbox/dev_sandbox.py @@ -34,4 +34,4 @@ class BasicApp(App): self.panic(self.tree) -BasicApp.run(css_file="dev_sandbox.scss", watch_css=True, log="textual.log") +BasicApp.run(css_path="dev_sandbox.scss", watch_css=True, log_path="textual.log") diff --git a/sandbox/local_styles.py b/sandbox/local_styles.py index 17a25fcc3..63bfe9ac9 100644 --- a/sandbox/local_styles.py +++ b/sandbox/local_styles.py @@ -33,4 +33,4 @@ class BasicApp(App): self.log(header.styles) -BasicApp.run(css_file="local_styles.css", log="textual.log") +BasicApp.run(css_path="local_styles.css", log_path="textual.log") diff --git a/sandbox/tabs.py b/sandbox/tabs.py index 1ad1434ff..6bca6162e 100644 --- a/sandbox/tabs.py +++ b/sandbox/tabs.py @@ -144,4 +144,4 @@ class BasicApp(App): self.mount(example.widget) -BasicApp.run(css_file="tabs.scss", watch_css=True, log="textual.log") +BasicApp.run(css_path="tabs.scss", watch_css=True, log_path="textual.log") diff --git a/sandbox/uber.py b/sandbox/uber.py index 8363866cb..18edb4dbd 100644 --- a/sandbox/uber.py +++ b/sandbox/uber.py @@ -76,7 +76,7 @@ class BasicApp(App): self.focused.styles.border_top = ("solid", "invalid-color") -app = BasicApp(css_file="uber.css", log="textual.log", log_verbosity=1) +app = BasicApp(css_path="uber.css", log_path="textual.log", log_verbosity=1) if __name__ == "__main__": app.run() diff --git a/src/textual/app.py b/src/textual/app.py index 6192e458f..9005634c6 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -7,6 +7,7 @@ import platform import sys import warnings from contextlib import redirect_stdout +from pathlib import PurePath from time import perf_counter from typing import ( Any, @@ -105,20 +106,20 @@ class App(Generic[ReturnType], DOMNode): def __init__( self, driver_class: Type[Driver] | None = None, - log: str = "", + log_path: str | PurePath = "", log_verbosity: int = 1, title: str = "Textual Application", - css_file: str | None = None, + css_path: str | PurePath | None = None, watch_css: bool = True, ): """Textual application base class Args: 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_path (str | PurePath, 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_path (str | PurePath | None, optional): Path to CSS or ``None`` for no CSS file. Defaults to None. 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 @@ -150,8 +151,8 @@ class App(Generic[ReturnType], DOMNode): self._log_console: Console | None = None self._log_file: TextIO | None = None - if log: - self._log_file = open(log, "wt") + if log_path: + self._log_file = open(log_path, "wt") self._log_console = Console( file=self._log_file, markup=False, @@ -170,10 +171,10 @@ class App(Generic[ReturnType], DOMNode): self.stylesheet = Stylesheet(variables=self.get_css_variables()) self._require_styles_update = False - self.css_file = css_file + self.css_path = css_path self.css_monitor = ( - FileMonitor(css_file, self._on_css_change) - if (watch_css and css_file) + FileMonitor(css_path, self._on_css_change) + if (watch_css and css_path) else None ) @@ -449,13 +450,13 @@ class App(Generic[ReturnType], DOMNode): async def _on_css_change(self) -> None: """Called when the CSS changes (if watch_css is True).""" - if self.css_file is not None: + if self.css_path is not None: try: time = perf_counter() - self.stylesheet.read(self.css_file) + self.stylesheet.read(self.css_path) elapsed = (perf_counter() - time) * 1000 - self.log(f"loaded {self.css_file} in {elapsed:.0f}ms") + self.log(f"loaded {self.css_path} in {elapsed:.0f}ms") except Exception as error: # TODO: Catch specific exceptions self.console.bell() @@ -646,8 +647,8 @@ class App(Generic[ReturnType], DOMNode): except DevtoolsConnectionError: self.log(f"Couldn't connect to devtools ({self.devtools.url})") try: - if self.css_file is not None: - self.stylesheet.read(self.css_file) + if self.css_path is not None: + self.stylesheet.read(self.css_path) if self.CSS is not None: self.stylesheet.add_source( self.CSS, path=f"<{self.__class__.__name__}>" diff --git a/src/textual/css/match.py b/src/textual/css/match.py index 28cb7e5a7..40c0d47ac 100644 --- a/src/textual/css/match.py +++ b/src/textual/css/match.py @@ -36,8 +36,8 @@ def _check_selectors(selectors: list[Selector], node: DOMNode) -> bool: DESCENDENT = CombinatorType.DESCENDENT - css_path = node.css_path - path_count = len(css_path) + css_path_nodes = node.css_path_nodes + path_count = len(css_path_nodes) selector_count = len(selectors) stack: list[tuple[int, int]] = [(0, 0)] @@ -51,7 +51,7 @@ def _check_selectors(selectors: list[Selector], node: DOMNode) -> bool: if selector_index == selector_count or node_index == path_count: pop() else: - path_node = css_path[node_index] + path_node = css_path_nodes[node_index] selector = selectors[selector_index] if selector.combinator == DESCENDENT: # Find a matching descendent diff --git a/src/textual/css/parse.py b/src/textual/css/parse.py index 6cb47076a..f3ce8d3f7 100644 --- a/src/textual/css/parse.py +++ b/src/textual/css/parse.py @@ -1,6 +1,7 @@ from __future__ import annotations from functools import lru_cache +from pathlib import PurePath from typing import Iterator, Iterable from rich import print @@ -305,7 +306,7 @@ def substitute_references( def parse( - css: str, path: str, variables: dict[str, str] | None = None + css: str, path: str | PurePath, variables: dict[str, str] | None = None ) -> Iterable[RuleSet]: """Parse CSS by tokenizing it, performing variable substitution, and generating rule sets from it. diff --git a/src/textual/css/stylesheet.py b/src/textual/css/stylesheet.py index fc98fb901..d4831998b 100644 --- a/src/textual/css/stylesheet.py +++ b/src/textual/css/stylesheet.py @@ -3,7 +3,7 @@ from __future__ import annotations import os from collections import defaultdict from operator import itemgetter -from pathlib import Path +from pathlib import Path, PurePath from typing import cast, Iterable import rich.repr @@ -149,13 +149,13 @@ class Stylesheet: """ self.variables = variables - def _parse_rules(self, css: str, path: str) -> list[RuleSet]: + def _parse_rules(self, css: str, path: str | PurePath) -> list[RuleSet]: """Parse CSS and return rules. Args: css (str): String containing Textual CSS. - path (str): Path to CSS or unique identifier + path (str | PurePath): Path to CSS or unique identifier Raises: StylesheetError: If the CSS is invalid. @@ -171,11 +171,11 @@ class Stylesheet: raise StylesheetError(f"failed to parse css; {error}") return rules - def read(self, filename: str) -> None: + def read(self, filename: str | PurePath) -> None: """Read Textual CSS file. Args: - filename (str): filename of CSS. + filename (str | PurePath): filename of CSS. Raises: StylesheetError: If the CSS could not be read. @@ -188,15 +188,16 @@ class Stylesheet: path = os.path.abspath(filename) except Exception as error: raise StylesheetError(f"unable to read {filename!r}; {error}") - self.source[path] = css + self.source[str(path)] = css self._require_parse = True - def add_source(self, css: str, path: str | None = None) -> None: + def add_source(self, css: str, path: str | PurePath | None = None) -> None: """Parse CSS from a string. Args: css (str): String with CSS source. - path (str, optional): The path of the source if a file, or some other identifier. Defaults to "". + path (str | PurePath, optional): The path of the source if a file, or some other identifier. + Defaults to None. Raises: StylesheetError: If the CSS could not be read. @@ -205,6 +206,8 @@ class Stylesheet: if path is None: path = str(hash(css)) + elif isinstance(path, PurePath): + path = str(css) if path in self.source and self.source[path] == css: # Path already in source, and CSS is identical return diff --git a/src/textual/css/tokenize.py b/src/textual/css/tokenize.py index 7076d390a..dff909c4b 100644 --- a/src/textual/css/tokenize.py +++ b/src/textual/css/tokenize.py @@ -1,6 +1,7 @@ from __future__ import annotations import re +from pathlib import PurePath from typing import Iterable from textual.css.tokenizer import Expect, Tokenizer, Token @@ -136,7 +137,7 @@ class TokenizerState: "declaration_set_end": expect_root_scope, } - def __call__(self, code: str, path: str) -> Iterable[Token]: + def __call__(self, code: str, path: str | PurePath) -> Iterable[Token]: tokenizer = Tokenizer(code, path=path) expect = self.EXPECT get_token = tokenizer.get_token diff --git a/src/textual/css/tokenizer.py b/src/textual/css/tokenizer.py index c77d1d50e..958d1ca32 100644 --- a/src/textual/css/tokenizer.py +++ b/src/textual/css/tokenizer.py @@ -1,6 +1,7 @@ from __future__ import annotations import re +from pathlib import PurePath from typing import NamedTuple from rich.console import Group, RenderableType @@ -148,8 +149,8 @@ class Token(NamedTuple): class Tokenizer: - def __init__(self, text: str, path: str = "") -> None: - self.path = path + def __init__(self, text: str, path: str | PurePath = "") -> None: + self.path = str(path) self.code = text self.lines = text.splitlines(keepends=True) self.line_no = 0 diff --git a/src/textual/dom.py b/src/textual/dom.py index 144cca18d..86e0c3eb1 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -167,7 +167,7 @@ class DOMNode(MessagePump): return self.__class__.__name__.lower() @property - def css_path(self) -> list[DOMNode]: + def css_path_nodes(self) -> list[DOMNode]: """A list of nodes from the root to this node, forming a "path". Returns: diff --git a/src/textual/file_monitor.py b/src/textual/file_monitor.py index 58957a610..4b20e2981 100644 --- a/src/textual/file_monitor.py +++ b/src/textual/file_monitor.py @@ -1,4 +1,6 @@ +from __future__ import annotations import os +from pathlib import PurePath from typing import Callable import rich.repr @@ -8,7 +10,7 @@ from ._callback import invoke @rich.repr.auto class FileMonitor: - def __init__(self, path: str, callback: Callable) -> None: + def __init__(self, path: str | PurePath, callback: Callable) -> None: self.path = path self.callback = callback self._modified = self._get_modified() diff --git a/src/textual/widgets/_directory_tree.py b/src/textual/widgets/_directory_tree.py index 2754a5f7b..0cb60243a 100644 --- a/src/textual/widgets/_directory_tree.py +++ b/src/textual/widgets/_directory_tree.py @@ -132,4 +132,4 @@ if __name__ == "__main__": async def on_mount(self, event: events.Mount) -> None: await self.screen.dock(DirectoryTree("/Users/willmcgugan/projects")) - TreeApp.run(log="textual.log") + TreeApp(log_path="textual.log").run() diff --git a/src/textual/widgets/_tree_control.py b/src/textual/widgets/_tree_control.py index 276386546..a76030077 100644 --- a/src/textual/widgets/_tree_control.py +++ b/src/textual/widgets/_tree_control.py @@ -328,4 +328,4 @@ if __name__ == "__main__": else: await message.node.toggle() - TreeApp.run(log="textual.log") + TreeApp(log_path="textual.log").run()