Add `Triggers.slice` (#205)

pull/206/head v0.4.5
Josh Karpel 1 year ago committed by GitHub
parent 72755aa09c
commit 02b683d48c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 124 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

@ -1,8 +1,16 @@
# Changelog
## `0.5.0` | *Unreleased*
## `0.5.0`
## `0.4.5` | *Unreleased*
*Unreleased*
## `0.4.5`
Released `2023-01-16`
### Added
- [#205](https://github.com/JoshKarpel/spiel/pull/205) Add `Triggers.take` to make gradually revealing content on a slide more straightforward.
### Fixed
@ -12,7 +20,9 @@
- [#203](https://github.com/JoshKarpel/spiel/pull/203) The `Image` example in the demo deck is now centered inside its `Panel`.
## `0.4.4` | 2023-01-13
## `0.4.4`
Released `2023-01-13`
### Added
@ -24,7 +34,9 @@
- [#194](https://github.com/JoshKarpel/spiel/pull/194) The `Deck.slide` decorator now returns the decorated function, not the `Slide` it was attached to.
- [#199](https://github.com/JoshKarpel/spiel/pull/199) The CLI command `spiel present`'s `--watch` option now defaults to the parent directory of the deck file instead of the current working directory.
## `0.4.3` | 2023-01-02
## `0.4.3`
Released `2023-01-02`
### Added
@ -36,19 +48,25 @@
- [#168](https://github.com/JoshKarpel/spiel/pull/168) The correct type for the `suspend` optional argument to slide-level keybinding functions is now available as `spiel.SuspendType`.
- [#168](https://github.com/JoshKarpel/spiel/pull/168) The [Spiel container image](https://github.com/JoshKarpel/spiel/pkgs/container/spiel) no longer has a leftover copy of the `spiel` package directory inside the image under `/app`.
## `0.4.2` | 2022-12-10
## `0.4.2`
Released `2022-12-10`
### Added
- [#163](https://github.com/JoshKarpel/spiel/pull/163) Added a public `spiel.present()` function that presents the deck at the given file.
## `0.4.1` | 2022-11-25
## `0.4.1`
Released `2022-11-25`
### Fixed
- [#157](https://github.com/JoshKarpel/spiel/pull/157) Pinned to Textual v0.4.0 to work around [Textual#1274](https://github.com/Textualize/textual/issues/1274).
## `0.4.0` | 2022-11-25
## `0.4.0`
Released `2022-11-25`
### Changed

26
poetry.lock generated

@ -504,14 +504,14 @@ async = ["aiofiles (>=0.7,<1.0)"]
[[package]]
name = "hypothesis"
version = "6.62.0"
version = "6.62.1"
description = "A library for property-based testing"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "hypothesis-6.62.0-py3-none-any.whl", hash = "sha256:e250da77878460f74b53039493a7a18d6fc137b0b77791b382b6a0f4ada9144e"},
{file = "hypothesis-6.62.0.tar.gz", hash = "sha256:76f1141e8237f6dd0780a171bec5d6aec873208ccc27b5f9753d4cccd8904272"},
{file = "hypothesis-6.62.1-py3-none-any.whl", hash = "sha256:d00a4a9c54b0b8b4570fe1abe42395807a973b4a507e6718309800e6f84e160d"},
{file = "hypothesis-6.62.1.tar.gz", hash = "sha256:7d1e2f9871e6509662da317adf9b4aabd6b38280fb6c7930aa4f574d2ed25150"},
]
[package.dependencies]
@ -735,14 +735,14 @@ mkdocs = ">=1.1"
[[package]]
name = "mkdocs-material"
version = "9.0.4"
version = "9.0.5"
description = "Documentation that simply works"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "mkdocs_material-9.0.4-py3-none-any.whl", hash = "sha256:f5f94f5daa0e07deb2f192453f9812c66c4bc0cd48078c60bdde32af137f7357"},
{file = "mkdocs_material-9.0.4.tar.gz", hash = "sha256:4f429b4e50242544020f0fc21b9e10062418d3cd1e591c040c4c155506250a66"},
{file = "mkdocs_material-9.0.5-py3-none-any.whl", hash = "sha256:53194bf8ae7dfb527fef2892a6ee291d3efc7b57d010b04dbb818b4ee88074a5"},
{file = "mkdocs_material-9.0.5.tar.gz", hash = "sha256:bbfed71788223b4c548a6e637cb7a9ee5b6ad6593c6d5b04e57c9c4d2c39d76b"},
]
[package.dependencies]
@ -1230,14 +1230,14 @@ markdown = ">=3.2"
[[package]]
name = "pytest"
version = "7.2.0"
version = "7.2.1"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
{file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
{file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"},
{file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"},
]
[package.dependencies]
@ -1571,14 +1571,14 @@ jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
[[package]]
name = "setuptools"
version = "65.7.0"
version = "66.0.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "setuptools-65.7.0-py3-none-any.whl", hash = "sha256:8ab4f1dbf2b4a65f7eec5ad0c620e84c34111a68d3349833494b9088212214dd"},
{file = "setuptools-65.7.0.tar.gz", hash = "sha256:4d3c92fac8f1118bb77a22181355e29c239cabfe2b9effdaa665c66b711136d7"},
{file = "setuptools-66.0.0-py3-none-any.whl", hash = "sha256:a78d01d1e2c175c474884671dde039962c9d74c7223db7369771fcf6e29ceeab"},
{file = "setuptools-66.0.0.tar.gz", hash = "sha256:bd6eb2d6722568de6d14b87c44a96fac54b2a45ff5e940e639979a3d1792adb6"},
]
[package.extras]
@ -1907,4 +1907,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<4"
content-hash = "1ea15a591ae6a2e7f0ca7b4395d5b249f8591085f8918f6643b6ef265b3db5d2"
content-hash = "32aa9da5bad39b65c0fed5734eeb75bc169becc660517755faa80bc6c88f4d9a"

@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "spiel"
version = "0.4.3"
version = "0.4.5"
description = "A framework for building and presenting richly-styled presentations in your terminal using Python."
readme="README.md"
homepage="https://github.com/JoshKarpel/spiel"

@ -3,10 +3,11 @@
import inspect
import shutil
import socket
from collections.abc import Callable, Iterator
from datetime import datetime
from math import cos, floor, pi
from pathlib import Path
from textwrap import dedent, indent
from textwrap import dedent
from click import edit
from rich.align import Align
@ -32,7 +33,8 @@ RICH = "[Rich](https://rich.readthedocs.io/)"
IPYTHON = "[IPython](https://ipython.readthedocs.io)"
WSL = "[Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/)"
THIS_DIR = Path(__file__).resolve().parent
THIS_FILE = Path(__file__).resolve()
THIS_DIR = THIS_FILE.parent
def pad_markdown(markup: str) -> RenderableType:
@ -118,6 +120,29 @@ def what() -> RenderableType:
return root
def make_code_panel_from_object(obj: type | Callable[..., object]) -> RenderableType:
lines, line_number = inspect.getsourcelines(obj)
return make_code_panel(line_number, lines)
def make_code_panel(line_number: int, lines: list[str], title: str | None = None) -> RenderableType:
return Align.center(
Panel(
Syntax(
"".join(lines),
lexer="python",
line_numbers=True,
start_line=line_number,
),
title=title,
box=SQUARE,
border_style=Style(dim=True),
height=len(lines) + 2,
expand=False,
)
)
@deck.slide(title="Decks and Slides")
def code() -> RenderableType:
markup = f"""\
@ -134,23 +159,9 @@ def code() -> RenderableType:
lower = Layout()
root.split_column(upper, lower)
def make_code_panel(obj: type) -> RenderableType:
lines, line_number = inspect.getsourcelines(obj)
return Panel(
Syntax(
"".join(lines),
lexer="python",
line_numbers=True,
start_line=line_number,
),
box=SQUARE,
border_style=Style(dim=True),
height=len(lines) + 2,
)
lower.split_row(
Layout(make_code_panel(Deck)),
Layout(make_code_panel(Slide)),
Layout(make_code_panel_from_object(Deck)),
Layout(make_code_panel_from_object(Slide)),
)
return root
@ -230,25 +241,6 @@ def triggers(triggers: Triggers) -> RenderableType:
"""
)
bounce_period = 10
width = 50
half_width = width // 2
bounce_time = triggers.time_since_first_trigger % bounce_period
bounce_character = "" if bounce_time < (1 / 2) * bounce_period else ""
bounce_position = floor(half_width * cos(2 * pi * bounce_time / bounce_period))
before = half_width + bounce_position
ball = Align.center(
Panel(
Padding(
bounce_character,
pad=(0, before, 0, (half_width - bounce_position - 1)),
),
title="Bouncing Bullet",
padding=0,
)
)
white = Color.parse("bright_white")
black = Color.parse("black")
red = Color.parse("bright_red")
@ -293,7 +285,94 @@ def triggers(triggers: Triggers) -> RenderableType:
pad=(1, 0),
)
return Group(info, fun, ball if len(triggers) > 2 else Text(""))
return Group(info, fun)
def make_reveals(triggers: Triggers) -> Iterator[RenderableType]:
return triggers.take(
Align.center(r)
for r in [
Text("Reveal 1", style=Style(color="black", bgcolor="#E40303")),
Text("Reveal 2", style=Style(color="black", bgcolor="#FF8C00")),
Text("Reveal 3", style=Style(color="black", bgcolor="#FFED00")),
Text("Reveal 4", style=Style(color="black", bgcolor="#008026")),
Text("Reveal 5", style=Style(color="black", bgcolor="#24408E")),
Text("Reveal 6", style=Style(color="black", bgcolor="#732982")),
]
)
@deck.slide(title="Triggers: Reveals")
def bullets(triggers: Triggers) -> RenderableType:
info_upper = pad_markdown(
f"""\
## Triggers: Reveals
Triggers can be useful even without considering their tracking of relative time.
We can track the number of times the slide has been triggered to gradually
reveal content.
`{Triggers.take.__qualname__}` makes this straightforward:
"""
)
info_lower = pad_markdown(
f"""\
Trigger this slide (press `t`) a few times to reveal some content.
Press `r` to hide the content again (by resetting the trigger state).
"""
)
return Group(
info_upper,
Padding(make_code_panel_from_object(make_reveals), pad=(0, 0, 1, 0)),
info_lower,
*make_reveals(triggers),
)
def make_bullet(triggers: Triggers) -> RenderableType:
bounce_period = 10
width = 50
half_width = width // 2
bounce_time = triggers.time_since_first_trigger % bounce_period
bounce_character = "" if bounce_time < (1 / 2) * bounce_period else ""
bounce_position = floor(half_width * cos(2 * pi * bounce_time / bounce_period))
before = half_width + bounce_position
bullet = Padding(
bounce_character,
pad=(0, before, 0, (half_width - bounce_position - 1)),
)
return Align.center(
Panel(bullet, title="Bouncing Bullet", padding=0),
vertical="middle",
)
@deck.slide(title="Triggers: Animations")
def bouncing_bullet(triggers: Triggers) -> RenderableType:
info = pad_markdown(
f"""\
## Triggers: Animations
Here's an example of how triggers can be used to build
more complex animations.
The position and facing direction of the bullet are calculated deterministically
based on the time since the first trigger time (the automatic one
from when the slide starts being presented).
There is no state stored in the slide function itself.
The apparent motion is based on {SPIEL} evaluating the
slide content function with different `Trigger` values.
"""
)
return Group(info, make_bullet(triggers), make_code_panel_from_object(make_bullet))
@deck.slide(title="Views")
@ -374,7 +453,7 @@ def edit_this_file(suspend: SuspendType) -> None:
},
)
def bindings() -> RenderableType:
edit_this_file_source_lines, _ = inspect.getsourcelines(edit_this_file)
edit_this_file_source_lines, line_number = inspect.getsourcelines(edit_this_file)
this_slide_source_lines, _ = inspect.getsourcelines(bindings)
source_lines = [
*edit_this_file_source_lines,
@ -382,11 +461,9 @@ def bindings() -> RenderableType:
*this_slide_source_lines[:7], # up through the slide's def
" ...", # replace the slide body with a "..."
]
code = make_code_panel(line_number, source_lines, title=THIS_FILE.name)
# get the indentation right for the triple-quoted string below
source_code = indent("".join(source_lines), " " * 8).lstrip()
return pad_markdown(
info_upper = pad_markdown(
f"""\
## Custom Per-Slide Key Bindings
@ -399,18 +476,21 @@ def bindings() -> RenderableType:
suspends {SPIEL} while inside the `with` block.
A binding has been registered on this slide that suspends {SPIEL}
and opens your `$EDITOR` on the demo deck's Python file.
Try pressing `e`!
and opens your `$EDITOR` on the demo deck's Python file:
"""
)
```python
{source_code}
```
info_lower = pad_markdown(
f"""\
Try pressing `e`!
Due to reloading, any changes you make will be reflected in the
presentation you're seeing right now.
"""
)
return Group(info_upper, code, info_lower)
class DemoRenderFailure(Exception):
pass

@ -1,10 +1,13 @@
from __future__ import annotations
from collections.abc import Sequence
from collections.abc import Iterable, Sequence
from dataclasses import dataclass
from functools import cached_property
from itertools import islice
from time import monotonic
from typing import Iterator, overload
from typing import Iterator, TypeVar, overload
T = TypeVar("T")
@dataclass(frozen=True)
@ -79,3 +82,20 @@ class Triggers(Sequence[float]):
(i.e., this ignores the initial trigger from when the slide starts being displayed).
"""
return len(self) > 1
def take(self, iter: Iterable[T], offset: int = 1) -> Iterator[T]:
"""
Takes elements from the iterable `iter`
equal to the number of times in the `Triggers` minus the offset.
Args:
iter: The iterable to take elements from.
offset: This `offset` will be subtracted from the number of triggers,
reducing the number of elements that will be returned.
It defaults to `1` to ignore the automatic trigger from when the
slide starts being shown.
Returns:
An iterator over the first `len(self) - offset` elements of `iter`.
"""
return islice(iter, len(self) - offset)

@ -1,3 +1,5 @@
from itertools import count
import pytest
from hypothesis import given
from hypothesis.strategies import slices
@ -106,3 +108,17 @@ def test_triggered(triggers: Triggers, expected: bool) -> None:
def test_invalid_triggers(times: tuple[float], now: float) -> None:
with pytest.raises(ValueError):
Triggers(_times=times, now=now)
@pytest.mark.parametrize(
"triggers, offset, expected",
[
(Triggers(_times=(0,), now=1000), 1, []),
(Triggers(_times=(0,), now=1000), 0, [0]),
(Triggers(_times=(0, 0, 0), now=1000), 1, [0, 1]),
(Triggers(_times=(0, 0, 0), now=1000), 0, [0, 1, 2]),
(Triggers(_times=(0, 0, 0), now=1000), 2, [0]),
],
)
def test_slice(triggers: Triggers, offset: int, expected: float) -> None:
assert list(triggers.take(count(0), offset=offset)) == expected

Loading…
Cancel
Save