Merge branch 'main' into outline-top-bottom-colour-glitch

This commit is contained in:
Dave Pearson
2023-05-02 11:05:52 +01:00
committed by GitHub
10 changed files with 148 additions and 51 deletions

View File

@@ -22,7 +22,7 @@ jobs:
- name: Install and configure Poetry
uses: snok/install-poetry@v1.3.3
with:
version: 1.2.2
version: 1.4.2
virtualenvs-in-project: true
- name: Install dependencies
run: poetry install --extras "dev"

View File

@@ -11,6 +11,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Fixed `outline` top and bottom not handling alpha - https://github.com/Textualize/textual/issues/2371
### Changed
- Setting attributes with a `compute_` method will now raise an `AttributeError` https://github.com/Textualize/textual/issues/2383
- Unknown psuedo-selectors will now raise a tokenizer error (previously they were silently ignored) https://github.com/Textualize/textual/pull/2445
### Added
- Watch methods can now optionally be private https://github.com/Textualize/textual/issues/2382
## [0.22.3] - 2023-04-29
### Fixed

View File

@@ -130,30 +130,29 @@ textual run --dev --port 7342 my_app.py
## Textual log
In addition to simple strings, Textual console supports [Rich](https://rich.readthedocs.io/en/latest/) formatting. To write rich logs, import `log` as follows:
Use the `log` function to pretty-print data structures and anything that [Rich](https://rich.readthedocs.io/en/latest/) can display.
You can import the log function as follows:
```python
from textual import log
```
This method will pretty print data structures (like lists and dicts) as well as [Rich renderables](https://rich.readthedocs.io/en/stable/protocol.html). Here are some examples:
Here's a few examples of writing to the console, with `log`:
```python
log("Hello, World") # simple string
log(locals()) # Log local variables
log(children=self.children, pi=3.141592) # key/values
log(self.tree) # Rich renderables
```
Textual log messages may contain [console Markup](https://rich.readthedocs.io/en/stable/markup.html):
```python
log("[bold red]DANGER![/] We're having too much fun")
def on_mount(self) -> None:
log("Hello, World") # simple string
log(locals()) # Log local variables
log(children=self.children, pi=3.141592) # key/values
log(self.tree) # Rich renderables
```
### Log method
There's a convenient shortcut to `log` available on the `App` and `Widget` objects. This is useful in event handlers. Here's an example:
There's a convenient shortcut to `log` on the `App` and `Widget` objects. This is useful in event handlers. Here's an example:
```python
from textual.app import App
@@ -170,7 +169,7 @@ if __name__ == "__main__":
LogApp().run()
```
### Logging handler
## Logging handler
Textual has a [logging handler][textual.logging.TextualHandler] which will write anything logged via the builtin logging library to the devtools.
This may be useful if you have a third-party library that uses the logging module, and you want to see those logs with Textual logs.

View File

@@ -61,6 +61,14 @@ VALID_STYLE_FLAGS: Final = {
"underline",
"uu",
}
VALID_PSEUDO_CLASSES: Final = {
"blur",
"disabled",
"enabled",
"focus-within",
"focus",
"hover",
}
NULL_SPACING: Final = Spacing.all(0)

View File

@@ -12,7 +12,9 @@ from rich.panel import Panel
from rich.syntax import Syntax
from rich.text import Text
from ..suggestions import get_suggestion
from ._error_tools import friendly_list
from .constants import VALID_PSEUDO_CLASSES
class TokenError(Exception):
@@ -56,7 +58,7 @@ class TokenError(Exception):
line_numbers=True,
indent_guides=True,
line_range=(max(0, line_no - 2), line_no + 2),
highlight_lines={line_no},
highlight_lines={line_no + 1},
)
syntax.stylize_range("reverse bold", self.start, self.end)
return Panel(syntax, border_style="red")
@@ -227,6 +229,29 @@ class Tokenizer:
(line_no, col_no),
referenced_by=None,
)
if (
token.name == "pseudo_class"
and token.value.strip(":") not in VALID_PSEUDO_CLASSES
):
pseudo_class = token.value.strip(":")
suggestion = get_suggestion(pseudo_class, list(VALID_PSEUDO_CLASSES))
all_valid = f"must be one of {friendly_list(VALID_PSEUDO_CLASSES)}"
if suggestion:
raise TokenError(
self.path,
self.code,
(line_no, col_no),
f"unknown pseudo-class {pseudo_class!r}; did you mean {suggestion!r}?; {all_valid}",
)
else:
raise TokenError(
self.path,
self.code,
(line_no, col_no),
f"unknown pseudo-class {pseudo_class!r}; {all_valid}",
)
col_no += len(value)
if col_no >= len(line):
line_no += 1

View File

@@ -174,6 +174,12 @@ class Reactive(Generic[ReactiveType]):
_rich_traceback_omit = True
self._initialize_reactive(obj, self.name)
if hasattr(obj, self.compute_name):
raise AttributeError(
f"Can't set {obj}.{self.name!r}; reactive attributes with a compute method are read-only"
)
name = self.name
current_value = getattr(obj, name)
# Check for validate function
@@ -241,9 +247,13 @@ class Reactive(Generic[ReactiveType]):
events.Callback(callback=partial(await_watcher, watch_result))
)
watch_function = getattr(obj, f"watch_{name}", None)
if callable(watch_function):
invoke_watcher(watch_function, old_value, value)
private_watch_function = getattr(obj, f"_watch_{name}", None)
if callable(private_watch_function):
invoke_watcher(private_watch_function, old_value, value)
public_watch_function = getattr(obj, f"watch_{name}", None)
if callable(public_watch_function):
invoke_watcher(public_watch_function, old_value, value)
# Process "global" watchers
watchers: list[tuple[Reactable, Callable]]
@@ -272,7 +282,9 @@ class Reactive(Generic[ReactiveType]):
compute_method = getattr(obj, f"compute_{compute}")
except AttributeError:
continue
current_value = getattr(obj, f"_reactive_{compute}")
current_value = getattr(
obj, f"_reactive_{compute}", getattr(obj, f"_default_{compute}", None)
)
value = compute_method()
setattr(obj, f"_reactive_{compute}", value)
if value != current_value:

View File

@@ -336,7 +336,6 @@ class ProgressBar(Widget, can_focus=False):
self.show_percentage = show_percentage
self.show_eta = show_eta
self.percentage = None
self.total = total
def compose(self) -> ComposeResult:

View File

@@ -140,52 +140,52 @@ A1
/**********************************************************************/
A:foo {}
A:foo:bar {}
A:focus {}
A:focus:hover {}
A
:foo {}
:focus {}
A
:foo:bar {}
:focus:hover {}
A
:foo
:bar {}
A:foo-bar {}
:focus
:hover {}
A:enabled {}
A
:foo-bar {}
:enabled {}
A :foo {}
A :foo :bar {}
A :foo-bar {}
A :focus {}
A :focus :hover {}
A :enabled {}
.A:foo {}
.A:foo:bar {}
.A:focus {}
.A:focus:hover {}
.A
:foo {}
:focus {}
.A
:foo:bar {}
:focus:hover {}
.A
:foo
:bar {}
.A:foo-bar {}
:focus
:hover {}
.A:enabled {}
.A
:foo-bar {}
:enabled {}
#A:foo {}
#A:foo:bar {}
#A:focus {}
#A:focus:hover {}
#A
:foo {}
:focus {}
#A
:foo:bar {}
:focus:hover {}
#A
:foo
:bar {}
#A:foo-bar {}
:focus
:hover {}
#A:enabled {}
#A
:foo-bar {}
:enabled {}
A1.A1.A1:foo {}
A1.A1#A1:foo {}
A1:foo.A1:foo#A1:foo {}
A1.A1.A1:focus {}
A1.A1#A1:focus {}
A1:focus.A1:focus#A1:focus {}
/**********************************************************************/

View File

@@ -1226,3 +1226,21 @@ class TestTypeNames:
stylesheet.add_source(f"StartType {separator} 1TestType {{}}")
with pytest.raises(TokenError):
stylesheet.parse()
def test_parse_bad_psuedo_selector():
"""Check unknown selector raises a token error."""
bad_selector = """\
Widget:foo{
border: red;
}
"""
stylesheet = Stylesheet()
stylesheet.add_source(bad_selector, "foo")
with pytest.raises(TokenError) as error:
stylesheet.parse()
assert error.value.start == (0, 6)

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
import asyncio
import pytest
@@ -354,6 +356,9 @@ async def test_compute():
app.start = 10
assert app.count_double == 14
with pytest.raises(AttributeError):
app.count_double = 100
async def test_watch_compute():
"""Check that watching a computed attribute works."""
@@ -386,3 +391,25 @@ async def test_watch_compute():
assert app.show_ac is False
assert watch_called == [True, True, False, False, True, True, False, False]
async def test_public_and_private_watch() -> None:
"""If a reactive/var has public and private watches both should get called."""
calls: dict[str, bool] = {"private": False, "public": False}
class PrivateWatchTest(App):
counter = var(0, init=False)
def watch_counter(self) -> None:
calls["public"] = True
def _watch_counter(self) -> None:
calls["private"] = True
async with PrivateWatchTest().run_test() as pilot:
assert calls["private"] is False
assert calls["public"] is False
pilot.app.counter += 1
assert calls["private"] is True
assert calls["public"] is True