Expand demo deck (#3)

* upgrade demo deck, and make some incidental improvements along the way

* bump rich version
pull/5/head
Josh Karpel 3 years ago committed by GitHub
parent 52de31de1c
commit 3b6cb78cfe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,89 +1,189 @@
import inspect
import os
import shutil
import socket
import tempfile
from datetime import datetime
from textwrap import dedent
from rich.align import Align
from rich.console import ConsoleRenderable
from rich.console import RenderGroup
from rich.layout import Layout
from rich.markdown import Markdown
from rich.panel import Panel
from rich.style import Style
from rich.syntax import Syntax
from rich.text import Text
from spiel import Deck, Slide
from spiel import Deck, Slide, __version__
DECK = Deck(name="Spiel Demo Deck")
SPIEL = "[Spiel](https://github.com/JoshKarpel/spiel)"
RICH = "[Rich](https://rich.readthedocs.io/)"
left_markup = """\
## What is Spiel?
[Spiel](https://github.com/JoshKarpel/spiel) is a framework for building slide decks in Python.
def what():
left_markup = dedent(
f"""\
## What is Spiel?
Spiel uses [Rich](https://rich.readthedocs.io/) to render slide content.
"""
{SPIEL} is a framework for building slide decks in Python.
right_markup = """\
## Why Spiel?
Spiel uses {RICH} to render slide content.
It's fun!
Anything you can display with Rich, you can display with Spiel!
"""
)
It's weird!
"""
right_markup = dedent(
"""\
## Why use Spiel?
layout = Layout()
left = Layout(
Markdown(
left_markup,
justify="center",
),
ratio=2,
)
buffer = Layout(" ")
right = Layout(
Markdown(
right_markup,
justify="center",
),
ratio=2,
)
layout.split_row(left, buffer, right)
It's fun!
DECK.add_slide(
Slide(
content=layout,
It's weird!
"""
)
)
class Now:
def __rich__(self) -> ConsoleRenderable:
return Align(
Text(
f"Right now, at {datetime.now()}!",
style=Style(color="bright_cyan", bold=True, italic=True),
r = 3
root = Layout()
root.split_row(
Layout(
Markdown(
left_markup,
justify="center",
),
ratio=r,
),
Layout(" "),
Layout(
Markdown(
right_markup,
justify="center",
),
align="center",
)
ratio=r,
),
)
return Slide(root, title="What is Spiel?")
DECK.add_slide(
Slide(
content=Now(),
def code():
markup = dedent(
f"""\
## Decks are made of Slides
Here's the code for `Deck` and `Slide`!
The source code is pulled directly from the definitions via [`inspect.getsource`](https://docs.python.org/3/library/inspect.html#inspect.getsource).
(Because {RICH} supports syntax highlighting, so does {SPIEL}!)
"""
)
root = Layout()
upper = Layout(Markdown(markup, justify="center"), size=len(markup.split("\n")) + 1)
lower = Layout()
root.split_column(upper, lower)
lower.split_row(
Layout(
Syntax(
inspect.getsource(Deck),
lexer_name="python",
),
),
Layout(
Syntax(
inspect.getsource(Slide),
lexer_name="python",
),
),
)
)
return Slide(root, title="Decks and Slides")
class Where:
def __rich__(self) -> ConsoleRenderable:
return Align(
Text(
f"Right here, at {socket.gethostname()}!",
style=Style(color="bright_cyan", bold=True, italic=True),
def dynamic():
tmp_dir = tempfile.gettempdir()
width = shutil.get_terminal_size().columns
return Slide(
RenderGroup(
Align(
Text(
f"Your slides can have very dynamic content, like this!",
style=Style(color="bright_magenta", bold=True, italic=True),
),
align="center",
),
Align(
Panel(
Text(
f"The time on this computer, {socket.gethostname()}, is {datetime.now()}",
style=Style(color="bright_cyan", bold=True, italic=True),
justify="center",
)
),
align="center",
),
Align(
Panel(
Text(
f"Your terminal is {width} characters wide."
if width > 80
else f"Your terminal is only {width} characters wide! Get a bigger monitor!",
style=Style(color="green1" if width > 80 else "red"),
justify="center",
)
),
align="center",
),
Align(
Panel(
Text(
f"There are {len(os.listdir(tmp_dir))} entries under {tmp_dir} right now.",
style=Style(color="yellow"),
justify="center",
)
),
align="center",
),
align="right",
)
),
title="Dynamic Content",
)
def grid():
markup = dedent(
"""\
## Multiple Views
DECK.add_slide(
Slide(
content=Where(),
Try pressing 'd' to go into "deck" view.
Press 's' to go back to "slide" view.
"""
)
return Slide(Markdown(markup, justify="center"), title="Views")
def watch():
markup = dedent(
f"""\
## Developing a Deck
{SPIEL} can reload your deck as you edit it if you add the `--watch` option to `display`:
`$ spiel display examples/demo.py --watch`
If you're on a system without inotify support (e.g., Windows Subsystem for Linux), you may need to use the `--poll` option instead.
When you're ready to present your deck for real, just drop the `--watch` option.
"""
)
return Slide(Markdown(markup, justify="center"), title="Watch Mode")
DECK = Deck(name=f"Spiel Demo Deck (v{__version__})").add_slides(
what(),
code(),
dynamic,
grid(),
watch(),
)

@ -5,10 +5,10 @@ from rich.text import Text
from spiel import Deck, Slide
DECK = Deck(
name="Many Slides",
slides=[
DECK = Deck(name="Many Slides")
DECK.add_slides(
*(
Slide(Text(f"This is slide {n + 1}"), title="".join(sample(string.ascii_letters, 30)))
for n in range(30)
],
)
)

82
poetry.lock generated

@ -139,6 +139,18 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
pyparsing = ">=2.0.2"
[[package]]
name = "pendulum"
version = "2.1.2"
description = "Python datetimes made easy"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.dependencies]
python-dateutil = ">=2.6,<3.0"
pytzdata = ">=2020.1"
[[package]]
name = "pluggy"
version = "0.13.1"
@ -281,9 +293,28 @@ pytest-forked = "*"
psutil = ["psutil (>=3.0)"]
testing = ["filelock"]
[[package]]
name = "python-dateutil"
version = "2.8.1"
description = "Extensions to the standard Python datetime module"
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
[package.dependencies]
six = ">=1.5"
[[package]]
name = "pytzdata"
version = "2020.1"
description = "The Olson timezone database for Python."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "rich"
version = "10.0.0"
version = "10.0.1"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
category = "main"
optional = false
@ -298,6 +329,14 @@ typing-extensions = ">=3.7.4,<4.0.0"
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
[[package]]
name = "six"
version = "1.15.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "toml"
version = "0.10.2"
@ -353,7 +392,7 @@ watchmedo = ["PyYAML (>=3.10)", "argh (>=0.24.1)"]
[metadata]
lock-version = "1.1"
python-versions = "^3.9"
content-hash = "1c49eed51b797e85e879ea31f75d419230eb0838ecf4e7e1ba6984700768c74f"
content-hash = "980d52ae5399ae29b8d4e26e95998d1b68be728ab6a3d3590ec614584702432a"
[metadata.files]
apipkg = [
@ -481,6 +520,29 @@ packaging = [
{file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
{file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
]
pendulum = [
{file = "pendulum-2.1.2-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:b6c352f4bd32dff1ea7066bd31ad0f71f8d8100b9ff709fb343f3b86cee43efe"},
{file = "pendulum-2.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:318f72f62e8e23cd6660dbafe1e346950281a9aed144b5c596b2ddabc1d19739"},
{file = "pendulum-2.1.2-cp35-cp35m-macosx_10_15_x86_64.whl", hash = "sha256:0731f0c661a3cb779d398803655494893c9f581f6488048b3fb629c2342b5394"},
{file = "pendulum-2.1.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:3481fad1dc3f6f6738bd575a951d3c15d4b4ce7c82dce37cf8ac1483fde6e8b0"},
{file = "pendulum-2.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9702069c694306297ed362ce7e3c1ef8404ac8ede39f9b28b7c1a7ad8c3959e3"},
{file = "pendulum-2.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:fb53ffa0085002ddd43b6ca61a7b34f2d4d7c3ed66f931fe599e1a531b42af9b"},
{file = "pendulum-2.1.2-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:c501749fdd3d6f9e726086bf0cd4437281ed47e7bca132ddb522f86a1645d360"},
{file = "pendulum-2.1.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c807a578a532eeb226150d5006f156632df2cc8c5693d778324b43ff8c515dd0"},
{file = "pendulum-2.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2d1619a721df661e506eff8db8614016f0720ac171fe80dda1333ee44e684087"},
{file = "pendulum-2.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f888f2d2909a414680a29ae74d0592758f2b9fcdee3549887779cd4055e975db"},
{file = "pendulum-2.1.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:e95d329384717c7bf627bf27e204bc3b15c8238fa8d9d9781d93712776c14002"},
{file = "pendulum-2.1.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:4c9c689747f39d0d02a9f94fcee737b34a5773803a64a5fdb046ee9cac7442c5"},
{file = "pendulum-2.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1245cd0075a3c6d889f581f6325dd8404aca5884dea7223a5566c38aab94642b"},
{file = "pendulum-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:db0a40d8bcd27b4fb46676e8eb3c732c67a5a5e6bfab8927028224fbced0b40b"},
{file = "pendulum-2.1.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:f5e236e7730cab1644e1b87aca3d2ff3e375a608542e90fe25685dae46310116"},
{file = "pendulum-2.1.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:de42ea3e2943171a9e95141f2eecf972480636e8e484ccffaf1e833929e9e052"},
{file = "pendulum-2.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7c5ec650cb4bec4c63a89a0242cc8c3cebcec92fcfe937c417ba18277d8560be"},
{file = "pendulum-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:33fb61601083f3eb1d15edeb45274f73c63b3c44a8524703dc143f4212bf3269"},
{file = "pendulum-2.1.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:29c40a6f2942376185728c9a0347d7c0f07905638c83007e1d262781f1e6953a"},
{file = "pendulum-2.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:94b1fc947bfe38579b28e1cccb36f7e28a15e841f30384b5ad6c5e31055c85d7"},
{file = "pendulum-2.1.2.tar.gz", hash = "sha256:b06a0ca1bfe41c990bbf0c029f0b6501a7f2ec4e38bfec730712015e8860f207"},
]
pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
@ -524,9 +586,21 @@ pytest-xdist = [
{file = "pytest-xdist-2.2.1.tar.gz", hash = "sha256:718887296892f92683f6a51f25a3ae584993b06f7076ce1e1fd482e59a8220a2"},
{file = "pytest_xdist-2.2.1-py3-none-any.whl", hash = "sha256:2447a1592ab41745955fb870ac7023026f20a5f0bfccf1b52a879bd193d46450"},
]
python-dateutil = [
{file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
{file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"},
]
pytzdata = [
{file = "pytzdata-2020.1-py2.py3-none-any.whl", hash = "sha256:e1e14750bcf95016381e4d472bad004eef710f2d6417240904070b3d6654485f"},
{file = "pytzdata-2020.1.tar.gz", hash = "sha256:3efa13b335a00a8de1d345ae41ec78dd11c9f8807f522d39850f2dd828681540"},
]
rich = [
{file = "rich-10.0.0-py3-none-any.whl", hash = "sha256:01b3fcc305ae71b9ade4a645b6e371d395c6cd9ba52dcf180bfba69ef05c13b5"},
{file = "rich-10.0.0.tar.gz", hash = "sha256:4674bd3056a72bb282ad581e3f8092dc110cdcc456b5ba76e34965cb85a69724"},
{file = "rich-10.0.1-py3-none-any.whl", hash = "sha256:bd13f71c32692d08489f1dd90993308f6ab10d24b1cd28a71db8af771d8ee9f9"},
{file = "rich-10.0.1.tar.gz", hash = "sha256:d5f620a067e5d5f9c6e8039d9b09d8bb7b730bca5c696ca7e32788b37c0b9fa5"},
]
six = [
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
]
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},

@ -23,6 +23,7 @@ python = "^3.9"
rich = "^10.0.0"
typer = "^0.3.2"
watchdog = "^2.0.2"
pendulum = "^2.1.2"
[tool.poetry.dev-dependencies]
pytest = "^6.2.2"
@ -38,4 +39,4 @@ spiel = 'spiel.main:app'
[tool.pytest.ini_options]
addopts = ['--strict-markers', '--mypy']
testpaths = ["tests", "spiel", "examples"]
testpaths = ["tests", "spiel"]

@ -23,6 +23,10 @@ class Footer(Stateful):
style=Style(dim=True),
justify="left",
),
Column(
style=Style(bold=True),
justify="center",
),
Column(
style=Style(dim=True),
justify="center",
@ -41,6 +45,7 @@ class Footer(Stateful):
self.state.current_slide.title if self.state.mode is Mode.SLIDE else None,
],
),
self.state.message,
date.today().isoformat(),
f"[{self.state.current_slide_idx + 1:>0{self.longest_slide_number_length}d} / {len(self.state.deck)}]",
)

@ -2,11 +2,15 @@ from __future__ import annotations
import importlib.util
import sys
from dataclasses import dataclass
from dataclasses import dataclass, field
from pathlib import Path
from types import TracebackType
from typing import ContextManager, Optional, Type
from pendulum import DateTime, now
from rich.control import Control
from rich.style import Style
from rich.text import Text
from watchdog.events import FileSystemEvent, FileSystemEventHandler
from watchdog.observers import Observer
from watchdog.observers.polling import PollingObserver
@ -38,9 +42,30 @@ def load_deck(deck_path: Path) -> Deck:
class DeckReloader(FileSystemEventHandler):
state: State
deck_path: Path
last_reload: DateTime = field(default_factory=now)
def on_modified(self, event: FileSystemEvent) -> None:
self.state.deck = load_deck(self.deck_path)
try:
self.state.deck = load_deck(self.deck_path)
self.state.set_message(
lambda: Text(
f"Reloaded deck from {self.deck_path} {self.last_reload.diff_for_humans(None, False)}",
style=Style(color="bright_green"),
)
)
except Exception:
try:
exc_type, exc_obj, exc_tb = sys.exc_info()
self.state.set_message(
lambda: Text(
f"Error: {self.last_reload.diff_for_humans(None, False)}: {exc_obj!r}{Control.bell()}",
style=Style(color="bright_red"),
)
)
except Exception:
# If something goes wrong generating the reloader's message, don't let the reloader die!
pass
self.last_reload = now()
def __hash__(self) -> int:
return hash((type(self), id(self)))

@ -4,6 +4,7 @@ from itertools import islice
from rich.console import Console
from rich.layout import Layout
from rich.live import Live
from rich.padding import Padding
from rich.panel import Panel
from rich.style import Style
@ -23,7 +24,7 @@ def present_deck(console: Console, state: State) -> None:
body = Layout(name="body", ratio=1)
if state.mode is Mode.SLIDE:
body.update(current_slide.content)
body.update(Padding(current_slide.render(), pad=1))
elif state.mode is Mode.DECK:
n = console.size.width // 30
row_of_current_slide = state.current_slide_idx // n
@ -39,15 +40,15 @@ def present_deck(console: Console, state: State) -> None:
body.split_column(*rows)
for row, layouts in zip(rows, cols):
for layout in layouts:
slide_idx, slide = next(slides, (None, None))
slide_number, slide = next(slides, (None, None))
if slide is None:
layout.update("")
else:
is_active_slide = slide is state.current_slide
layout.update(
Panel(
slide.content,
title=joinify(" | ", [slide_idx, slide.title]),
slide.render(),
title=joinify(" | ", [slide_number, slide.title]),
border_style=Style(
color="bright_cyan" if is_active_slide else None,
dim=not is_active_slide,

@ -1,30 +1,49 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import List, Union
from typing import Callable, List, Union
from rich.console import ConsoleRenderable, RichCast
from rich.text import Text
Renderable = Union[RichCast, ConsoleRenderable]
Contentlike = Union[Renderable, Callable[[], Renderable]]
@dataclass
class Slide:
content: Union[RichCast, ConsoleRenderable] = field(default_factory=Text)
content: Contentlike = field(default_factory=Text)
title: str = ""
def render(self) -> Renderable:
if callable(self.content):
return self.content()
else:
return self.content
def __call__(self) -> Slide:
return self
Slidelike = Union[Slide, Callable[[], Slide]]
@dataclass
class Deck:
name: str
slides: List[Slide] = field(default_factory=list)
_slides: List[Slidelike] = field(default_factory=list)
@property
def slides(self) -> List[Slide]:
return [slide() for slide in self._slides]
def __getitem__(self, idx: int) -> Slide:
return self.slides[idx]
return self.slides[idx]()
def __len__(self) -> int:
return len(self.slides)
def add_slide(self, slide: Slide) -> Deck:
self.slides.append(slide)
def add_slides(self, *slides: Slidelike) -> Deck:
self._slides.extend(slides)
return self

@ -1,14 +1,20 @@
from dataclasses import dataclass
from typing import Callable, Union
from rich.text import Text
from .modes import Mode
from .slides import Deck, Slide
Textlike = Union[Text, Callable[[], Text]]
@dataclass
class State:
deck: Deck
_current_slide_idx: int = 0
mode: Mode = Mode.SLIDE
_message: Textlike = Text("")
@property
def current_slide_idx(self) -> int:
@ -31,6 +37,16 @@ class State:
def current_slide(self) -> Slide:
return self.deck[self.current_slide_idx]
@property
def message(self) -> Text:
if callable(self._message):
return self._message()
else:
return self._message
def set_message(self, message: Textlike) -> None:
self._message = message
@dataclass
class Stateful:

@ -43,7 +43,9 @@ def cli(runner: CliRunner) -> CLI:
@pytest.fixture
def three_slide_deck() -> Deck:
return Deck(name="three-slides", slides=[Slide(), Slide(), Slide()])
deck = Deck(name="three-slides")
deck.add_slides(Slide(), Slide(), Slide())
return deck
@pytest.fixture

@ -5,7 +5,7 @@ def test_can_add_slide_to_deck(three_slide_deck: Deck) -> None:
initial_len = len(three_slide_deck)
new_slide = Slide()
three_slide_deck.add_slide(new_slide)
three_slide_deck.add_slides(new_slide)
assert len(three_slide_deck) == initial_len + 1
assert three_slide_deck[-1] is new_slide

Loading…
Cancel
Save