Skip to content

Commit

Permalink
Merge branch 'main' into fix-text-area-update-code-editor-constructor
Browse files Browse the repository at this point in the history
  • Loading branch information
darrenburns authored Feb 20, 2024
2 parents 4cb6244 + 741a42e commit bda1c90
Show file tree
Hide file tree
Showing 37 changed files with 1,684 additions and 72 deletions.
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,29 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

- Fixed `TextArea.code_editor` missing recently added attributes https://github.com/Textualize/textual/pull/4172

## [0.52.1] - 2024-02-20

### Fixed

- Fixed the check for animation level in `LoadingIndicator` https://github.com/Textualize/textual/issues/4188

## [0.52.0] - 2024-02-19

### Changed

- Textual now writes to stderr rather than stdout https://github.com/Textualize/textual/pull/4177

### Added

- Added an `asyncio` lock attribute `Widget.lock` to be used to synchronize widget state https://github.com/Textualize/textual/issues/4134
- 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
- Added support for a `TEXTUAL_SCREENSHOT_LOCATION` environment variable to specify the location of an automated screenshot https://github.com/Textualize/textual/pull/4181/
- Added support for a `TEXTUAL_SCREENSHOT_FILENAME` environment variable to specify the filename of an automated screenshot https://github.com/Textualize/textual/pull/4181/
- Added an `asyncio` lock attribute `Widget.lock` to be used to synchronize widget state https://github.com/Textualize/textual/issues/4134
- `Widget.remove_children` now accepts a CSS selector to specify which children to remove https://github.com/Textualize/textual/pull/4183
- `Widget.batch` combines widget locking and app update batching https://github.com/Textualize/textual/pull/4183

## [0.51.0] - 2024-02-15

### Added
Expand Down Expand Up @@ -127,6 +150,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added `App.suspend` https://github.com/Textualize/textual/pull/4064
- Added `App.action_suspend_process` https://github.com/Textualize/textual/pull/4064


### Fixed

- Parameter `animate` from `DataTable.move_cursor` was being ignored https://github.com/Textualize/textual/issues/3840
Expand Down
1 change: 1 addition & 0 deletions docs/api/constants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
::: textual.constants
44 changes: 44 additions & 0 deletions docs/blog/posts/remote-memray.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
draft: false
date: 2024-02-20
categories:
- DevLog
authors:
- willmcgugan
---

# Remote memory profiling with Memray

[Memray](https://github.com/bloomberg/memray) is a memory profiler for Python, built by some very smart devs at Bloomberg.
It is a fantastic tool to identify memory leaks in your code or other libraries (down to the C level)!

They recently added a [Textual](https://github.com/textualize/textual/) interface which looks amazing, and lets you monitor your process right from the terminal:

![Memray](https://raw.githubusercontent.com/bloomberg/memray/main/docs/_static/images/live_animated.webp)

<!-- more -->

You would typically run this locally, or over a ssh session, but it is also possible to serve the interface over the web with the help of [textual-web](https://github.com/Textualize/textual-web).
I'm not sure if even the Memray devs themselves are aware of this, but here's how.

First install Textual web (ideally with pipx) alongside Memray:

```bash
pipx install textual-web
```

Now you can serve Memray with the following command (replace the text in quotes with your Memray options):

```bash
textual-web -r "memray run --live -m http.server"
```

This will return a URL, where you can access the Memray app from anywhere.
Here's a quick video of that in action:

<iframe style="aspect-ratio: 16 /10" width="100%" src="https://www.youtube.com/embed/7lpoUBdxzus" title="Serving Memray with Textual web" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

## Found this interesting?


Join our [Discord server](https://discord.gg/Enf6Z3qhVr) if you want to discuss this post with the Textual devs or community.
12 changes: 12 additions & 0 deletions docs/guide/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ In the following example we have three buttons, each of which does something dif

1. The message handler is called when any button is pressed

=== "on_decorator.tcss"

```python title="on_decorator.tcss"
--8<-- "docs/examples/events/on_decorator.tcss"
```

=== "Output"

```{.textual path="docs/examples/events/on_decorator01.py"}
Expand All @@ -233,6 +239,12 @@ The following example uses the decorator approach to write individual message ha
2. Matches the button with class names "toggle" *and* "dark"
3. Matches the button with an id of "quit"

=== "on_decorator.tcss"

```python title="on_decorator.tcss"
--8<-- "docs/examples/events/on_decorator.tcss"
```

=== "Output"

```{.textual path="docs/examples/events/on_decorator02.py"}
Expand Down
232 changes: 232 additions & 0 deletions docs/images/screenshots/frogmouth.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions docs/images/screenshots/harlequin.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
200 changes: 200 additions & 0 deletions docs/images/screenshots/memray.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
199 changes: 199 additions & 0 deletions docs/images/screenshots/toolong.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 43 additions & 8 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,20 +77,55 @@ Build sophisticated user interfaces with a simple Python API. Run your apps in t

</div>

---

<div>
<a href="https://github.com/Textualize/toolong">
--8<-- "docs/images/screenshots/toolong.svg"
</a>
</div>

```{.textual path="examples/calculator.py" columns=100 lines=41 press="3,.,1,4,5,9,2,wait:400"}
```
---

```{.textual path="examples/pride.py"}
```
<div>
<a href="https://github.com/textualize/frogmouth">
--8<-- "docs/images/screenshots/frogmouth.svg"
</a>
</div>

---

<div>
<a href="https://github.com/bloomberg/memray">
--8<-- "docs/images/screenshots/memray.svg"
</a>
</div>

---

```{.textual path="docs/examples/tutorial/stopwatch.py" columns="100" lines="30" press="d,tab,enter"}
```

<a href="https://github.com/charles-001/dolphie">

```{.textual path="docs/examples/guide/layout/combining_layouts.py" columns="100", lines="30"}
![Dolphie](https://www.textualize.io/static/img/dolphie.png)

</a>


---

<div>
<a href="https://harlequin.sh">
--8<-- "docs/images/screenshots/harlequin.svg"
</a>
</div>


---

```{.textual path="examples/calculator.py" columns=100 lines=41 press="3,.,1,4,5,9,2,wait:400"}
```

```{.textual path="docs/examples/app/widgets01.py"}
---

```{.textual path="examples/pride.py"}
```
1 change: 1 addition & 0 deletions mkdocs-nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ nav:
- "api/cache.md"
- "api/color.md"
- "api/command.md"
- "api/constants.md"
- "api/containers.md"
- "api/content_switcher.md"
- "api/coordinate.md"
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.51.0"
version = "0.52.1"
homepage = "https://github.com/Textualize/textual"
repository = "https://github.com/Textualize/textual"
documentation = "https://textual.textualize.io/"
Expand Down
20 changes: 14 additions & 6 deletions src/textual/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,21 @@
LogCallable: TypeAlias = "Callable"


def __getattr__(name: str) -> str:
"""Lazily get the version."""
if name == "__version__":
from importlib.metadata import version
if TYPE_CHECKING:

from importlib.metadata import version

__version__ = version("textual")

else:

def __getattr__(name: str) -> str:
"""Lazily get the version."""
if name == "__version__":
from importlib.metadata import version

return version("textual")
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
return version("textual")
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


class LoggerError(Exception):
Expand Down
36 changes: 30 additions & 6 deletions src/textual/_animator.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from . import _time
from ._callback import invoke
from ._easing import DEFAULT_EASING, EASING
from ._types import CallbackType
from ._types import AnimationLevel, CallbackType
from .timer import Timer

if TYPE_CHECKING:
Expand Down Expand Up @@ -53,7 +53,11 @@ class Animation(ABC):
"""Callback to run after animation completes"""

@abstractmethod
def __call__(self, time: float) -> bool: # pragma: no cover
def __call__(
self,
time: float,
app_animation_level: AnimationLevel = "full",
) -> bool: # pragma: no cover
"""Call the animation, return a boolean indicating whether animation is in-progress or complete.
Args:
Expand Down Expand Up @@ -93,9 +97,18 @@ class SimpleAnimation(Animation):
final_value: object
easing: EasingFunction
on_complete: CallbackType | None = None
level: AnimationLevel = "full"
"""Minimum level required for the animation to take place (inclusive)."""

def __call__(self, time: float) -> bool:
if self.duration == 0:
def __call__(
self, time: float, app_animation_level: AnimationLevel = "full"
) -> bool:
if (
self.duration == 0
or app_animation_level == "none"
or app_animation_level == "basic"
and self.level == "full"
):
setattr(self.obj, self.attribute, self.final_value)
return True

Expand Down Expand Up @@ -170,6 +183,7 @@ def __call__(
delay: float = 0.0,
easing: EasingFunction | str = DEFAULT_EASING,
on_complete: CallbackType | None = None,
level: AnimationLevel = "full",
) -> None:
"""Animate an attribute.
Expand All @@ -182,6 +196,7 @@ def __call__(
delay: A delay (in seconds) before the animation starts.
easing: An easing method.
on_complete: A callable to invoke when the animation is finished.
level: Minimum level required for the animation to take place (inclusive).
"""
start_value = getattr(self._obj, attribute)
if isinstance(value, str) and hasattr(start_value, "parse"):
Expand All @@ -200,6 +215,7 @@ def __call__(
delay=delay,
easing=easing_function,
on_complete=on_complete,
level=level,
)


Expand Down Expand Up @@ -284,6 +300,7 @@ def animate(
easing: EasingFunction | str = DEFAULT_EASING,
delay: float = 0.0,
on_complete: CallbackType | None = None,
level: AnimationLevel = "full",
) -> None:
"""Animate an attribute to a new value.
Expand All @@ -297,6 +314,7 @@ def animate(
easing: An easing function.
delay: Number of seconds to delay the start of the animation by.
on_complete: Callback to run after the animation completes.
level: Minimum level required for the animation to take place (inclusive).
"""
animate_callback = partial(
self._animate,
Expand All @@ -308,6 +326,7 @@ def animate(
speed=speed,
easing=easing,
on_complete=on_complete,
level=level,
)
if delay:
self._complete_event.clear()
Expand All @@ -328,7 +347,8 @@ def _animate(
speed: float | None = None,
easing: EasingFunction | str = DEFAULT_EASING,
on_complete: CallbackType | None = None,
):
level: AnimationLevel = "full",
) -> None:
"""Animate an attribute to a new value.
Args:
Expand All @@ -340,6 +360,7 @@ def _animate(
speed: The speed of the animation.
easing: An easing function.
on_complete: Callback to run after the animation completes.
level: Minimum level required for the animation to take place (inclusive).
"""
if not hasattr(obj, attribute):
raise AttributeError(
Expand Down Expand Up @@ -373,6 +394,7 @@ def _animate(
speed=speed,
easing=easing_function,
on_complete=on_complete,
level=level,
)

if animation is None:
Expand Down Expand Up @@ -414,6 +436,7 @@ def _animate(
if on_complete is not None
else None
),
level=level,
)
assert animation is not None, "animation expected to be non-None"

Expand Down Expand Up @@ -521,11 +544,12 @@ def __call__(self) -> None:
if not self._scheduled:
self._complete_event.set()
else:
app_animation_level = self.app.animation_level
animation_time = self._get_time()
animation_keys = list(self._animations.keys())
for animation_key in animation_keys:
animation = self._animations[animation_key]
animation_complete = animation(animation_time)
animation_complete = animation(animation_time, app_animation_level)
if animation_complete:
del self._animations[animation_key]
if animation.on_complete is not None:
Expand Down
6 changes: 4 additions & 2 deletions src/textual/_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ class NoActiveAppError(RuntimeError):
"""Runtime error raised if we try to retrieve the active app when there is none."""


active_app: ContextVar["App"] = ContextVar("active_app")
active_app: ContextVar["App[object]"] = ContextVar("active_app")
active_message_pump: ContextVar["MessagePump"] = ContextVar("active_message_pump")
prevent_message_types_stack: ContextVar[list[set[type[Message]]]] = ContextVar(
"prevent_message_types_stack"
)
visible_screen_stack: ContextVar[list[Screen]] = ContextVar("visible_screen_stack")
visible_screen_stack: ContextVar[list[Screen[object]]] = ContextVar(
"visible_screen_stack"
)
"""A stack of visible screens (with background alpha < 1), used in the screen render process."""
message_hook: ContextVar[Callable[[Message], None]] = ContextVar("message_hook")
"""A callable that accepts a message. Used by App.run_test."""
5 changes: 4 additions & 1 deletion src/textual/_types.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Union
from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Literal, Union

from typing_extensions import Protocol

Expand Down Expand Up @@ -52,3 +52,6 @@ class UnusedParameter:
WatchCallbackNoArgsType,
]
"""Type used for callbacks passed to the `watch` method of widgets."""

AnimationLevel = Literal["none", "basic", "full"]
"""The levels that the [`TEXTUAL_ANIMATIONS`][textual.constants.TEXTUAL_ANIMATIONS] env var can be set to."""
Loading

0 comments on commit bda1c90

Please sign in to comment.