Skip to content

Commit

Permalink
Merge branch 'main' into textual-animations
Browse files Browse the repository at this point in the history
  • Loading branch information
rodrigogiraoserrao committed Feb 19, 2024
2 parents 6b1e166 + 2b3c71c commit 5cb2471
Show file tree
Hide file tree
Showing 85 changed files with 6,084 additions and 1,974 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ on:
- "**.lock"
- "Makefile"

env:
PYTEST_ADDOPTS: "--color=yes"

jobs:
build:
runs-on: ${{ matrix.os }}
Expand Down
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,20 @@ repos:
- 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
rev: '5.12.0'
hooks:
- id: isort
name: isort (python)
language_version: '3.11'
args: ["--profile", "black", "--filter-files"]
args: ['--profile', 'black', '--filter-files']
- repo: https://github.com/psf/black
rev: 24.1.1
rev: '24.1.1'
hooks:
- id: black
- repo: https://github.com/hadialqattan/pycln # removes unused imports
rev: v2.3.0
hooks:
- id: pycln
language_version: "3.11"
language_version: '3.11'
args: [--all]
exclude: ^tests/snapshot_tests
106 changes: 102 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,99 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [0.48.0] - 2023-02-01
## Unreleased

### Added

- Added support for environment variable `TEXTUAL_ANIMATIONS` to control what animations Textual displays https://github.com/Textualize/textual/pull/4062
- Add attribute `App.animation_level` to control whether animations on that app run or not https://github.com/Textualize/textual/pull/4062

## [0.51.0] - 2024-02-15

### Added

- TextArea now has `read_only` mode https://github.com/Textualize/textual/pull/4151
- Add some syntax highlighting to TextArea default theme https://github.com/Textualize/textual/pull/4149
- Add undo and redo to TextArea https://github.com/Textualize/textual/pull/4124
- Added support for command palette command discoverability https://github.com/Textualize/textual/pull/4154

### Fixed

- Fixed out-of-view `Tab` not being scrolled into view when `Tabs.active` is assigned https://github.com/Textualize/textual/issues/4150
- Fixed `TabbedContent.TabActivate` not being posted when `TabbedContent.active` is assigned https://github.com/Textualize/textual/issues/4150

### Changed

- Breaking change: Renamed `TextArea.tab_behaviour` to `TextArea.tab_behavior` https://github.com/Textualize/textual/pull/4124
- `TextArea.theme` now defaults to `"css"` instead of None, and is no longer optional https://github.com/Textualize/textual/pull/4157

### Fixed

- Improve support for selector lists in nested TCSS https://github.com/Textualize/textual/issues/3969
- Improve support for rule declarations after nested TCSS rule sets https://github.com/Textualize/textual/issues/3999

## [0.50.1] - 2024-02-09

### Fixed

- Fixed tint applied to ANSI colors https://github.com/Textualize/textual/pull/4142

## [0.50.0] - 2024-02-08

### Fixed

- Fixed issue with ANSI colors not being converted to truecolor https://github.com/Textualize/textual/pull/4138
- Fixed duplicate watch methods being attached to DOM nodes https://github.com/Textualize/textual/pull/4030
- Fixed using `watch` to create additional watchers would trigger other watch methods https://github.com/Textualize/textual/issues/3878

### Added

- Added support for configuring dark and light themes for code in `Markdown` https://github.com/Textualize/textual/issues/3997

## [0.49.0] - 2024-02-07

### Fixed

- Fixed scrolling in long `OptionList` by adding max height of 100% https://github.com/Textualize/textual/issues/4021
- Fixed `DirectoryTree.clear_node` not clearing the node specified https://github.com/Textualize/textual/issues/4122

### Changed

- `DirectoryTree.reload` and `DirectoryTree.reload_node` now preserve state when reloading https://github.com/Textualize/textual/issues/4056
- Fixed a crash in the TextArea when performing a backward replace https://github.com/Textualize/textual/pull/4126
- Fixed selection not updating correctly when pasting while there's a non-zero selection https://github.com/Textualize/textual/pull/4126
- Breaking change: `TextArea` will not use `Escape` to shift focus if the `tab_behaviour` is the default https://github.com/Textualize/textual/issues/4110
- `TextArea` cursor will now be invisible before first focus https://github.com/Textualize/textual/pull/4128
- Fix toggling `TextArea.cursor_blink` reactive when widget does not have focus https://github.com/Textualize/textual/pull/4128

### Added

- Added DOMQuery.set https://github.com/Textualize/textual/pull/4075
- Added DOMNode.set_reactive https://github.com/Textualize/textual/pull/4075
- Added DOMNode.data_bind https://github.com/Textualize/textual/pull/4075
- Added DOMNode.action_toggle https://github.com/Textualize/textual/pull/4075
- Added Worker.cancelled_event https://github.com/Textualize/textual/pull/4075
- `Tree` (and `DirectoryTree`) grew an attribute `lock` that can be used for synchronization across coroutines https://github.com/Textualize/textual/issues/4056


## [0.48.2] - 2024-02-02

### Fixed

- Fixed a hang in the Linux driver when connected to a pipe https://github.com/Textualize/textual/issues/4104
- Fixed broken `OptionList` `Option.id` mappings https://github.com/Textualize/textual/issues/4101

### Changed

- Breaking change: keyboard navigation in `RadioSet`, `ListView`, `OptionList`, and `SelectionList`, no longer allows highlighting disabled items https://github.com/Textualize/textual/issues/3881

## [0.48.1] - 2024-02-01

### Fixed

- `TextArea` uses CSS theme by default instead of `monokai` https://github.com/Textualize/textual/pull/4091

## [0.48.0] - 2024-02-01

### Changed

Expand Down Expand Up @@ -35,8 +127,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added `TabbedContent.active_pane` https://github.com/Textualize/textual/pull/4012
- Added `App.suspend` https://github.com/Textualize/textual/pull/4064
- Added `App.action_suspend_process` https://github.com/Textualize/textual/pull/4064
- Added support for environment variable `TEXTUAL_ANIMATIONS` to control what animations Textual displays https://github.com/Textualize/textual/pull/4062
- Add attribute `App.animation_level` to control whether animations on that app run or not https://github.com/Textualize/textual/pull/4062


### Fixed

Expand All @@ -55,7 +146,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Fixed display of keys when used in conjunction with other keys https://github.com/Textualize/textual/pull/3050
- Fixed double detection of <kbd>Escape</kbd> on Windows https://github.com/Textualize/textual/issues/4038

## [0.47.1] - 2023-01-05
## [0.47.1] - 2024-01-05

### Fixed

Expand Down Expand Up @@ -1623,6 +1714,13 @@ 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.51.0]: https://github.com/Textualize/textual/compare/v0.50.1...v0.51.0
[0.50.1]: https://github.com/Textualize/textual/compare/v0.50.0...v0.50.1
[0.50.0]: https://github.com/Textualize/textual/compare/v0.49.0...v0.50.0
[0.49.1]: https://github.com/Textualize/textual/compare/v0.49.0...v0.49.1
[0.49.0]: https://github.com/Textualize/textual/compare/v0.48.2...v0.49.0
[0.48.2]: https://github.com/Textualize/textual/compare/v0.48.1...v0.48.2
[0.48.1]: https://github.com/Textualize/textual/compare/v0.48.0...v0.48.1
[0.48.0]: https://github.com/Textualize/textual/compare/v0.47.1...v0.48.0
[0.47.1]: https://github.com/Textualize/textual/compare/v0.47.0...v0.47.1
[0.47.0]: https://github.com/Textualize/textual/compare/v0.46.0...v0.47.0
Expand Down
143 changes: 143 additions & 0 deletions docs/blog/posts/toolong-retrospective.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
---
draft: false
date: 2024-02-11
categories:
- DevLog
authors:
- willmcgugan
---

# File magic with the Python standard library

I recently published [Toolong](https://github.com/textualize/toolong), an app for viewing log files.
There were some interesting technical challenges in building Toolong that I'd like to cover in this post.

<!-- more -->

!!! note "Python is awesome"

This isn't specifically [Textual](https://github.com/textualize/textual/) related. These techniques could be employed in any Python project.

These techniques aren't difficult, and shouldn't be beyond anyone with an intermediate understanding of Python.
They are the kind of "if you know it you know it" knowledge that you may not need often, but can make a massive difference when you do!

## Opening large files

If you were to open a very large text file (multiple gigabyte in size) in an editor, you will almost certainly find that it takes a while. You may also find that it doesn't load at all because you don't have enough memory, or it disables features like syntax highlighting.

This is because most app will do something analogous to this:

```python
with open("access.log", "rb") as log_file:
log_data = log_file.read()
```

All the data is read in to memory, where it can be easily processed.
This is fine for most files of a reasonable size, but when you get in to the gigabyte territory the read and any additional processing will start to use a significant amount of time and memory.

Yet Toolong can open a file of *any* size in a second or so, with syntax highlighting.
It can do this because it doesn't need to read the entire log file in to memory.
Toolong opens a file and reads only the portion of it required to display whatever is on screen at that moment.
When you scroll around the log file, Toolong reads the data off disk as required -- fast enough that you may never even notice it.

### Scanning lines

There is an additional bit of work that Toolong has to do up front in order to show the file.
If you open a large file you may see a progress bar and a message about "scanning".

Toolong needs to know where every line starts and ends in a log file, so it can display a scrollbar bar and allow the user to navigate lines in the file.
In other words it needs to know the offset of every new line (`\n`) character within the file.

This isn't a hard problem in itself.
You might have imagined a loop that reads a chunk at a time and searches for new lines characters.
And that would likely have worked just fine, but there is a bit of magic in the Python standard library that can speed that up.

The [mmap](https://docs.python.org/3/library/mmap.html) module is a real gem for this kind of thing.
A *memory mapped file* is an OS-level construct that *appears* to load a file instantaneously.
In Python you get an object which behaves like a `bytearray`, but loads data from disk when it is accessed.
The beauty of this module is that you can work with files in much the same way as if you had read the entire file in to memory, while leaving the actual reading of the file to the OS.

Here's the method that Toolong uses to scan for line breaks.
Forgive the micro-optimizations, I was going for raw execution speed here.

```python
def scan_line_breaks(
self, batch_time: float = 0.25
) -> Iterable[tuple[int, list[int]]]:
"""Scan the file for line breaks.
Args:
batch_time: Time to group the batches.
Returns:
An iterable of tuples, containing the scan position and a list of offsets of new lines.
"""
fileno = self.fileno
size = self.size
if not size:
return
log_mmap = mmap.mmap(fileno, size, prot=mmap.PROT_READ)
rfind = log_mmap.rfind
position = size
batch: list[int] = []
append = batch.append
get_length = batch.__len__
monotonic = time.monotonic
break_time = monotonic()

while (position := rfind(b"\n", 0, position)) != -1:
append(position)
if get_length() % 1000 == 0 and monotonic() - break_time > batch_time:
break_time = monotonic()
yield (position, batch)
batch = []
append = batch.append
yield (0, batch)
log_mmap.close()
```

This code runs in a thread (actually a [worker](https://textual.textualize.io/guide/workers/)), and will generate line breaks in batches. Without batching, it risks slowing down the UI with millions of rapid events.

It's fast because most of the work is done in `rfind`, which runs at C speed, while the OS reads from the disk.

## Watching a file for changes

Toolong can tail files in realtime.
When something appends to the file, it will be read and displayed virtually instantly.
How is this done?

You can easily *poll* a file for changes, by periodically querying the size or timestamp of a file until it changes.
The downside of this is that you don't get notified immediately if a file changes between polls.
You could poll at a very fast rate, but if you were to do that you would end up burning a lot of CPU for no good reason.

There is a very good solution for this in the standard library.
The [selectors](https://docs.python.org/3/library/selectors.html) module is typically used for working with sockets (network data), but can also work with files (at least on macOS and Linux).

!!! info "Software developers are an unimaginative bunch when it comes to naming things"

Not to be confused with CSS [selectors](https://textual.textualize.io/guide/CSS/#selectors)!

The selectors module can tell you precisely when a file can be read.
It can do this very efficiently, because it relies on the OS to tell us when a file can be read, and doesn't need to poll.

You register a file with a `Selector` object, then call `select()` which returns as soon as there is new data available for reading.

See [watcher.py](https://github.com/Textualize/toolong/blob/main/src/toolong/watcher.py) in Toolong, which runs a thread to monitors files for changes with a selector.

## Textual learnings

This project was a chance for me to "dogfood" Textual.
Other Textual devs have build some cool projects ([Trogon](https://github.com/Textualize/trogon) and [Frogmouth](https://github.com/Textualize/frogmouth)), but before Toolong I had only ever written example apps for docs.

I paid particular attention to Textual error messages when working on Toolong, and improved many of them in Textual.
Much of what I improved were general programming errors, and not Textual errors per se.
For instance, if you forget to call `super()` on a widget constructor, Textual used to give a fairly cryptic error.
It's a fairly common gotcha, even for experience devs, but now Textual will detect that and tell you how to fix it.

There's a lot of other improvements which I thought about when working on this app.
Mostly quality of life features that will make implementing some features more intuitive.
Keep an eye out for those in the next few weeks.

## Found this interesting?

If you would like to talk about this post or anything Textual related, join us on the [Discord server](https://discord.gg/Enf6Z3qhVr).
67 changes: 67 additions & 0 deletions docs/examples/guide/reactivity/set_reactive01.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from textual.app import App, ComposeResult
from textual.containers import Horizontal
from textual.reactive import reactive, var
from textual.widgets import Label

GREETINGS = [
"Bonjour",
"Hola",
"こんにちは",
"你好",
"안녕하세요",
"Hello",
]


class Greeter(Horizontal):
"""Display a greeting and a name."""

DEFAULT_CSS = """
Greeter {
width: auto;
height: 1;
& Label {
margin: 0 1;
}
}
"""
greeting: reactive[str] = reactive("")
who: reactive[str] = reactive("")

def __init__(self, greeting: str = "Hello", who: str = "World!") -> None:
super().__init__()
self.greeting = greeting # (1)!
self.who = who

def compose(self) -> ComposeResult:
yield Label(self.greeting, id="greeting")
yield Label(self.who, id="name")

def watch_greeting(self, greeting: str) -> None:
self.query_one("#greeting", Label).update(greeting) # (2)!

def watch_who(self, who: str) -> None:
self.query_one("#who", Label).update(who)


class NameApp(App):

CSS = """
Screen {
align: center middle;
}
"""
greeting_no: var[int] = var(0)
BINDINGS = [("space", "greeting")]

def compose(self) -> ComposeResult:
yield Greeter(who="Textual")

def action_greeting(self) -> None:
self.greeting_no = (self.greeting_no + 1) % len(GREETINGS)
self.query_one(Greeter).greeting = GREETINGS[self.greeting_no]


if __name__ == "__main__":
app = NameApp()
app.run()
Loading

0 comments on commit 5cb2471

Please sign in to comment.