Skip to content

Commit

Permalink
Merge branch 'main' into duplicate-screen-css-errors
Browse files Browse the repository at this point in the history
  • Loading branch information
rodrigogiraoserrao authored Nov 1, 2023
2 parents 37aac01 + ad83e91 commit 8818f12
Show file tree
Hide file tree
Showing 44 changed files with 714 additions and 415 deletions.
21 changes: 17 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
args: [ '--unsafe' ]
- id: check-ast # simply checks whether the files parse as valid python
- id: check-builtin-literals # requires literal syntax when initializing empty or zero python builtin types
- id: check-case-conflict # checks for files that would conflict in case-insensitive filesystems
- id: check-merge-conflict # checks for files that contain merge conflict strings
- id: check-json # checks json files for parseable syntax
- id: check-toml # checks toml files for parseable syntax
- id: check-yaml # checks yaml files for parseable syntax
args: [ '--unsafe' ] # Instead of loading the files, simply parse them for syntax.
- id: check-shebang-scripts-are-executable # ensures that (non-binary) files with a shebang are executable
- id: check-vcs-permalinks # ensures that links to vcs websites are permalinks
- id: end-of-file-fixer # ensures that a file is either empty, or ends with one newline
- id: mixed-line-ending # replaces or checks mixed line ending
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
Expand All @@ -19,4 +27,9 @@ repos:
rev: 23.1.0
hooks:
- id: black
- repo: https://github.com/hadialqattan/pycln # removes unused imports
rev: v2.3.0
hooks:
- id: pycln
args: [--all]
exclude: ^tests/snapshot_tests
29 changes: 18 additions & 11 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

### Fixed

- Duplicate CSS errors when parsing CSS from a screen https://github.com/Textualize/textual/issues/3581

### Changed

- CSS error reporting will no longer provide links to the files in question https://github.com/Textualize/textual/pull/3582
- inline CSS error reporting will report widget/class variable where the CSS was read from https://github.com/Textualize/textual/pull/3582

## [0.41.0] - 2023-10-31

### Fixed

- Fixed `Input.cursor_blink` reactive not changing blink state after `Input` was mounted https://github.com/Textualize/textual/pull/3498
- Fixed `Tabs.active` attribute value not being re-assigned after removing a tab or clearing https://github.com/Textualize/textual/pull/3498
- Fixed `DirectoryTree` race-condition crash when changing path https://github.com/Textualize/textual/pull/3498
Expand All @@ -21,12 +32,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Fixed `OptionList` event leakage from `CommandPalette` to `App`.
- Fixed crash in `LoadingIndicator` https://github.com/Textualize/textual/pull/3498
- Fixed crash when `Tabs` appeared as a descendant of `TabbedContent` in the DOM https://github.com/Textualize/textual/pull/3602
- Duplicate CSS errors when parsing CSS from a screen https://github.com/Textualize/textual/issues/3581
- Fixed the command palette cancelling other workers https://github.com/Textualize/textual/issues/3615

### Added

- Add Document `get_index_from_location` / `get_location_from_index` https://github.com/Textualize/textual/pull/3410
- Add setter for `TextArea.text` https://github.com/Textualize/textual/discussions/3525
- Added `key` argument to the `DataTable.sort()` method, allowing the table to be sorted using a custom function (or other callable) https://github.com/Textualize/textual/pull/3090
- Added `initial` to all css rules, which restores default (i.e. value from DEFAULT_CSS) https://github.com/Textualize/textual/pull/3566
- Added HorizontalPad to pad.py https://github.com/Textualize/textual/pull/3571
- Added `AwaitComplete` class, to be used for optionally awaitable return values https://github.com/Textualize/textual/pull/3498


### Changed

Expand All @@ -49,15 +65,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Improved startup time by caching CSS parsing https://github.com/Textualize/textual/pull/3575
- Workers are now created/run in a thread-safe way https://github.com/Textualize/textual/pull/3586

### Added

- Added `initial` to all css rules, which restores default (i.e. value from DEFAULT_CSS) https://github.com/Textualize/textual/pull/3566
- Added HorizontalPad to pad.py https://github.com/Textualize/textual/pull/3571

### Added

- Added `AwaitComplete` class, to be used for optionally awaitable return values https://github.com/Textualize/textual/pull/3498

## [0.40.0] - 2023-10-11

### Added
Expand Down Expand Up @@ -251,7 +258,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

- DescendantBlur and DescendantFocus can now be used with @on decorator


## [0.32.0] - 2023-08-03

### Added
Expand Down Expand Up @@ -1399,6 +1405,7 @@ https://textual.textualize.io/blog/2022/11/08/version-040/#version-040
- New handler system for messages that doesn't require inheritance
- Improved traceback handling

[0.41.0]: https://github.com/Textualize/textual/compare/v0.40.0...v0.41.0
[0.40.0]: https://github.com/Textualize/textual/compare/v0.39.0...v0.40.0
[0.39.0]: https://github.com/Textualize/textual/compare/v0.38.1...v0.39.0
[0.38.1]: https://github.com/Textualize/textual/compare/v0.38.0...v0.38.1
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import asyncio
import time
from random import randint

from textual.app import App, ComposeResult
Expand Down
1 change: 0 additions & 1 deletion docs/examples/events/on_decorator01.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from textual import on
from textual.app import App, ComposeResult
from textual.widgets import Button

Expand Down
1 change: 0 additions & 1 deletion docs/examples/events/prevent.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from textual.app import App, ComposeResult
from textual.containers import Horizontal
from textual.widgets import Button, Input


Expand Down
2 changes: 1 addition & 1 deletion docs/examples/guide/reactivity/validate01.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def on_button_pressed(self, event: Button.Pressed) -> None:
self.count += 1
else:
self.count -= 1
self.query_one(RichLog).write(f"{self.count=}")
self.query_one(RichLog).write(f"count = {self.count}")


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/styles/padding_all.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from textual.app import App
from textual.containers import Container, Grid
from textual.containers import Grid
from textual.widgets import Placeholder


Expand Down
2 changes: 0 additions & 2 deletions docs/examples/widgets/content_switcher.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from rich.align import VerticalCenter

from textual.app import App, ComposeResult
from textual.containers import Horizontal, VerticalScroll
from textual.widgets import Button, ContentSwitcher, DataTable, Markdown
Expand Down
92 changes: 92 additions & 0 deletions docs/examples/widgets/data_table_sort.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from rich.text import Text

from textual.app import App, ComposeResult
from textual.widgets import DataTable, Footer

ROWS = [
("lane", "swimmer", "country", "time 1", "time 2"),
(4, "Joseph Schooling", Text("Singapore", style="italic"), 50.39, 51.84),
(2, "Michael Phelps", Text("United States", style="italic"), 50.39, 51.84),
(5, "Chad le Clos", Text("South Africa", style="italic"), 51.14, 51.73),
(6, "László Cseh", Text("Hungary", style="italic"), 51.14, 51.58),
(3, "Li Zhuhao", Text("China", style="italic"), 51.26, 51.26),
(8, "Mehdy Metella", Text("France", style="italic"), 51.58, 52.15),
(7, "Tom Shields", Text("United States", style="italic"), 51.73, 51.12),
(1, "Aleksandr Sadovnikov", Text("Russia", style="italic"), 51.84, 50.85),
(10, "Darren Burns", Text("Scotland", style="italic"), 51.84, 51.55),
]


class TableApp(App):
BINDINGS = [
("a", "sort_by_average_time", "Sort By Average Time"),
("n", "sort_by_last_name", "Sort By Last Name"),
("c", "sort_by_country", "Sort By Country"),
("d", "sort_by_columns", "Sort By Columns (Only)"),
]

current_sorts: set = set()

def compose(self) -> ComposeResult:
yield DataTable()
yield Footer()

def on_mount(self) -> None:
table = self.query_one(DataTable)
for col in ROWS[0]:
table.add_column(col, key=col)
table.add_rows(ROWS[1:])

def sort_reverse(self, sort_type: str):
"""Determine if `sort_type` is ascending or descending."""
reverse = sort_type in self.current_sorts
if reverse:
self.current_sorts.remove(sort_type)
else:
self.current_sorts.add(sort_type)
return reverse

def action_sort_by_average_time(self) -> None:
"""Sort DataTable by average of times (via a function) and
passing of column data through positional arguments."""

def sort_by_average_time_then_last_name(row_data):
name, *scores = row_data
return (sum(scores) / len(scores), name.split()[-1])

table = self.query_one(DataTable)
table.sort(
"swimmer",
"time 1",
"time 2",
key=sort_by_average_time_then_last_name,
reverse=self.sort_reverse("time"),
)

def action_sort_by_last_name(self) -> None:
"""Sort DataTable by last name of swimmer (via a lambda)."""
table = self.query_one(DataTable)
table.sort(
"swimmer",
key=lambda swimmer: swimmer.split()[-1],
reverse=self.sort_reverse("swimmer"),
)

def action_sort_by_country(self) -> None:
"""Sort DataTable by country which is a `Rich.Text` object."""
table = self.query_one(DataTable)
table.sort(
"country",
key=lambda country: country.plain,
reverse=self.sort_reverse("country"),
)

def action_sort_by_columns(self) -> None:
"""Sort DataTable without a key."""
table = self.query_one(DataTable)
table.sort("swimmer", "lane", reverse=self.sort_reverse("columns"))


app = TableApp()
if __name__ == "__main__":
app.run()
21 changes: 16 additions & 5 deletions docs/widgets/data_table.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,22 @@ visible as you scroll through the data table.

### Sorting

The `DataTable` can be sorted using the [sort][textual.widgets.DataTable.sort] method.
In order to sort your data by a column, you must have supplied a `key` to the `add_column` method
when you added it.
You can then pass this key to the `sort` method to sort by that column.
Additionally, you can sort by multiple columns by passing multiple keys to `sort`.
The `DataTable` can be sorted using the [sort][textual.widgets.DataTable.sort] method. In order to sort your data by a column, you can provide the `key` you supplied to the `add_column` method or a `ColumnKey`. You can then pass one more column keys to the `sort` method to sort by one or more columns.

Additionally, you can sort your `DataTable` with a custom function (or other callable) via the `key` argument. Similar to the `key` parameter of the built-in [sorted()](https://docs.python.org/3/library/functions.html#sorted) function, your function (or other callable) should take a single argument (row) and return a key to use for sorting purposes.

Providing both `columns` and `key` will limit the row information sent to your `key` function (or other callable) to only the columns specified.

=== "Output"

```{.textual path="docs/examples/widgets/data_table_sort.py"}
```

=== "data_table_sort.py"

```python
--8<-- "docs/examples/widgets/data_table_sort.py"
```

### Labelled rows

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "textual"
version = "0.40.0"
version = "0.41.0"
homepage = "https://github.com/Textualize/textual"
repository = "https://github.com/Textualize/textual"
documentation = "https://textual.textualize.io/"
Expand Down
2 changes: 0 additions & 2 deletions src/textual/_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,6 @@ def parse(

test_parser = TestParser()

import time

for n in range(0, len(data), 5):
for token in test_parser.feed(data[n : n + 5]):
print(token)
Expand Down
8 changes: 1 addition & 7 deletions src/textual/_types.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Union

from typing_extensions import (
Literal,
Protocol,
SupportsIndex,
get_args,
runtime_checkable,
)
from typing_extensions import Protocol

if TYPE_CHECKING:
from rich.segment import Segment
Expand Down
27 changes: 13 additions & 14 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1771,20 +1771,20 @@ def _load_screen_css(self, screen: Screen):

update = False
for path in screen.css_path:
if not self.stylesheet.has_source(path):
if not self.stylesheet.has_source(str(path), ""):
self.stylesheet.read(path)
update = True
if screen.CSS:
try:
screen_css_path = (
f"{inspect.getfile(screen.__class__)}:{screen.__class__.__name__}"
)
screen_path = inspect.getfile(screen.__class__)
except (TypeError, OSError):
screen_css_path = f"{screen.__class__.__name__}"
if not self.stylesheet.has_source(screen_css_path):
screen_path = ""
screen_class_var = f"{screen.__class__.__name__}.CSS"
read_from = (screen_path, screen_class_var)
if not self.stylesheet.has_source(screen_path, screen_class_var):
self.stylesheet.add_source(
screen.CSS,
path=screen_css_path,
read_from=read_from,
is_default_css=False,
scope=screen._css_type_name if screen.SCOPED_CSS else "",
)
Expand Down Expand Up @@ -2145,23 +2145,22 @@ async def _process_messages(
try:
if self.css_path:
self.stylesheet.read_all(self.css_path)
for path, css, tie_breaker, scope in self._get_default_css():
for read_from, css, tie_breaker, scope in self._get_default_css():
self.stylesheet.add_source(
css,
path=path,
read_from=read_from,
is_default_css=True,
tie_breaker=tie_breaker,
scope=scope,
)
if self.CSS:
try:
app_css_path = (
f"{inspect.getfile(self.__class__)}:{self.__class__.__name__}"
)
app_path = inspect.getfile(self.__class__)
except (TypeError, OSError):
app_css_path = f"{self.__class__.__name__}"
app_path = ""
read_from = (app_path, f"{self.__class__.__name__}.CSS")
self.stylesheet.add_source(
self.CSS, path=app_css_path, is_default_css=False
self.CSS, read_from=read_from, is_default_css=False
)
except Exception as error:
self._handle_exception(error)
Expand Down
Loading

0 comments on commit 8818f12

Please sign in to comment.