Big cleanup (#89)

pull/90/head
Josh Karpel 2 years ago committed by GitHub
parent 240684f5ed
commit 9346e7212b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7,12 +7,12 @@ on:
pull_request:
jobs:
test:
test-code:
strategy:
fail-fast: false
matrix:
platform: [ubuntu-latest, macos-latest]
python-version: [3.9]
python-version: ["3.9", "3.10"]
defaults:
run:
shell: bash
@ -51,3 +51,19 @@ jobs:
with:
env_vars: PLATFORM,PYTHON_VERSION
fail_ci_if_error: true
test-docker:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v2.4.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1.6.0
with:
install: true
- name: Build Image
uses: docker/build-push-action@v2.7.0
with:
file: ./docker/Dockerfile
pull: true
push: false

@ -31,6 +31,11 @@ repos:
rev: 21.12b0
hooks:
- id: black
- repo: https://github.com/hadialqattan/pycln
rev: v1.1.0
hooks:
- id: pycln
args: [ --config=pyproject.toml ]
- repo: https://github.com/PyCQA/isort
rev: 5.10.1
hooks:

@ -7,15 +7,15 @@ RUN : \
&& rm -rf /var/lib/apt/lists/* \
&& :
ENV COLORTERM=truecolor \
EDITOR=/usr/bin/vim
WORKDIR /app
COPY . /app/spiel
RUN : \
&& pip install --no-cache-dir /app/spiel ipython \
&& pip install --no-cache-dir /app/spiel \
&& spiel version \
&& :
ENV COLORTERM=truecolor \
EDITOR=/usr/bin/vim
CMD ["spiel", "demo", "present"]

@ -1,28 +0,0 @@
#!/usr/bin/env python
import os
from rich.console import Console
from spiel.main import DEMO_SOURCE
from spiel.present import render_slide
from spiel.state import State
CYCLES_PER_SLIDE = 100
def render_image_repeatedly() -> None:
with open(os.devnull, "w") as f:
state = State.from_file(DEMO_SOURCE, console=Console(file=f))
state.trigger()
state.trigger()
for _ in range(CYCLES_PER_SLIDE):
slide = [slide for slide in state.deck.slides if "Plot" in slide.title][0]
rendered = render_slide(state, slide)
state.console.print(rendered)
state.trigger()
if __name__ == "__main__":
render_image_repeatedly()

1166
poetry.lock generated

File diff suppressed because it is too large Load Diff

@ -13,7 +13,7 @@ line_length = 100
[tool.poetry]
name = "spiel"
version = "0.2.1"
version = "0.3.0"
description = "A framework for building and presenting richly-styled presentations in your terminal using Python."
readme="README.md"
homepage="https://github.com/JoshKarpel/spiel"
@ -33,33 +33,30 @@ license = "MIT"
include = ["py.typed", "demo/*"]
[tool.poetry.dependencies]
python = "^3.9"
rich = "^10.2.2"
typer = "^0.3.2"
watchdog = "^2.0.2"
pendulum = "^2.1.2"
Pillow = "^8.2.0"
ipython = "^7.23.0"
ipykernel = "^5.5.3"
nbterm = ">=0.0.7,<0.0.11"
toml = "^0.10.2"
uniplot = "^0.4.4"
numpy = "^1.20.3"
python = ">=3.9,<4"
rich = ">=10"
typer = ">=0.3"
watchdog = ">=2"
pendulum = ">=2"
Pillow = ">=8"
tomli = ">=2"
tomli-w = ">=1"
ipython = ">= 7"
ipykernel = ">= 5"
[tool.poetry.dev-dependencies]
pytest = "^6.2.2"
pytest-watch = "^4.2.0"
pytest-cov = "^2.11.1"
pytest-xdist = "^2.2.1"
mypy = "^0.900"
pytest-mypy = "^0.8.1"
pytest-mock = "^3.5.1"
hypothesis = "^6.13.5"
Sphinx = "^4.0.2"
sphinx-rtd-theme = "^0.5.2"
sphinx-autobuild = "^2021.3.14"
line-profiler = "^3.2.6"
types-toml = "^0.1.3"
pytest = ">=6"
pytest-watch = ">=4"
pytest-cov = ">=3"
pytest-xdist = ">=2"
mypy = ">=0.900"
pytest-mypy = ">=0.8"
pytest-mock = ">=3"
hypothesis = ">=6"
Sphinx = ">=4"
sphinx-rtd-theme = ">=0.5"
sphinx-autobuild = ">=2021.3.14"
line-profiler = ">=3"
[tool.poetry.scripts]
spiel = 'spiel.main:app'
@ -80,4 +77,12 @@ check_untyped_defs = true
disallow_incomplete_defs = true
disallow_untyped_defs = true
no_implicit_optional = true
disallow_any_generics = true
warn_unused_configs = true
warn_unused_ignores = true
warn_no_return = true
warn_unreachable = true
warn_redundant_casts = true
ignore_missing_imports = true

@ -1,9 +1,8 @@
from .constants import __version__
from .deck import Deck
from .example import Example, example_panels
from .image import Image
from .notebooks import notebook
from .options import Options
from .repls import repl
from .slide import Slide
from .triggers import Triggers
from spiel.constants import __version__
from spiel.deck import Deck
from spiel.example import Example, example_panels
from spiel.image import Image
from spiel.options import Options
from spiel.repls import repl
from spiel.slide import Slide
from spiel.triggers import Triggers

@ -1,4 +1,4 @@
from .constants import PACKAGE_NAME
from .main import app
from spiel.constants import PACKAGE_NAME
from spiel.main import app
app(prog_name=PACKAGE_NAME)

@ -5,17 +5,16 @@ import inspect
import sys
from collections.abc import Collection
from dataclasses import dataclass, field
from pathlib import Path
from textwrap import dedent
from typing import Callable, Iterator, List, Optional, Sequence
from typing import Callable, Iterator, List, Sequence
from .example import Example
from .presentable import Presentable
from .slide import MakeRenderable, Slide
from spiel.example import Example
from spiel.presentable import Presentable
from spiel.slide import MakeRenderable, Slide
@dataclass
class Deck(Collection):
class Deck(Collection[Presentable]):
name: str
slides: List[Presentable] = field(default_factory=list)
@ -38,12 +37,10 @@ class Deck(Collection):
def slide(
self,
title: str = "",
notebook: Optional[Path] = None,
) -> Callable[[MakeRenderable], Slide]:
def slideify(content: MakeRenderable) -> Slide:
slide = Slide(
title=title,
notebook=notebook,
content=content,
)
self.add_slides(slide)
@ -57,12 +54,10 @@ class Deck(Collection):
command: Sequence[str] = (sys.executable,),
name: str = "example.py",
language: str = "python",
notebook: Optional[Path] = None,
) -> Callable[[Callable], Example]:
def exampleify(example: Callable) -> Example:
) -> Callable[[Callable[[], None]], Example]:
def exampleify(example: Callable[[], None]) -> Example:
ex = Example(
title=title,
notebook=notebook,
source=get_function_body(example),
command=command,
name=name,
@ -74,7 +69,7 @@ class Deck(Collection):
return exampleify
def get_function_body(function: Callable) -> str:
def get_function_body(function: Callable[[], None]) -> str:
lines, line_of_def_start = inspect.getsourcelines(function)
line_of_first_instruction = list(dis.Bytecode(function))[0].starts_line or line_of_def_start
offset = line_of_first_instruction - line_of_def_start

@ -7,7 +7,6 @@ from math import cos, floor, pi
from pathlib import Path
from textwrap import dedent
import numpy as np
from rich.align import Align
from rich.box import SQUARE
from rich.color import Color, blend_rgb
@ -21,15 +20,12 @@ from rich.syntax import Syntax
from rich.text import Text
from spiel import Deck, Image, Options, Slide, __version__, example_panels
from spiel.plot import Plot
deck = Deck(name=f"Spiel Demo Deck (v{__version__})")
options = Options()
SPIEL = "[Spiel](https://github.com/JoshKarpel/spiel)"
RICH = "[Rich](https://rich.readthedocs.io/)"
NBTERM = "[nbterm](https://github.com/davidbrochart/nbterm)"
UNIPLOT = "[Uniplot](https://github.com/olavolav/uniplot)"
IPYTHON = "[IPython](https://ipython.readthedocs.io)"
WSL = "[Windows Subsystem for Linux](https://docs.microsoft.com/en-us/windows/wsl/)"
@ -164,46 +160,43 @@ def dynamic():
home = Path.home()
width = shutil.get_terminal_size().columns
width_limit = 80
home_dir_contents = list(home.iterdir())
return RenderGroup(
Align(
Align.center(
Text(
f"Slides can have dynamic content!",
style=Style(color="bright_magenta", bold=True, italic=True),
justify="center",
),
align="center",
),
Align(
Align.center(
Panel(
Text(
f"Your terminal is {width} cells wide."
f"Your terminal is {width} cells wide"
if width > width_limit
else f"Your terminal is only {width} cells wide! Get a bigger monitor!",
style=Style(color="green1" if width > width_limit else "red"),
justify="center",
)
),
align="center",
),
Align(
Align.center(
Panel(
Text(
f"The time on this computer, {socket.gethostname()}, is {datetime.now()}",
style=Style(color="bright_cyan", bold=True, italic=True),
Text.from_markup(
f"The time on this computer ([bold]{socket.gethostname()}[/bold]) is {datetime.now()}",
style="bright_cyan",
justify="center",
)
),
align="center",
),
Align(
Align.center(
Panel(
Text(
f"There are {len([f for f in home.iterdir() if f.is_file()])} files in {home} right now.",
f"There are {len([f for f in home_dir_contents if f.is_file()])} files and {len([f for f in home_dir_contents if f.is_dir()])} directories in {home}",
style=Style(color="yellow"),
justify="center",
)
),
align="center",
),
)
@ -345,48 +338,7 @@ def image():
root = Layout()
root.split_row(
Layout(Padding(Markdown(markup, justify="center"), pad=(0, 2))),
Layout(Image.from_file(THIS_DIR / "img.jpg")),
)
return root
@deck.slide(title="Plots")
def plots(triggers):
markup = dedent(
f"""\
## Plots
{SPIEL} can display plots... sort of!
Spiel includes a `Plot` widget that uses {UNIPLOT} to render plots.
You can even make animated plots by using triggers! Try triggering this slide.
"""
)
x = np.linspace(-3, 3, 1000)
coefficients = np.array([0, 2, -1, 3, -1, 1])
dither = 0.1 * np.sin(triggers.time_since_last_trigger) if triggers.triggered else 0
hermite = 0.9 * np.polynomial.hermite.hermval(x, c=coefficients + dither)
upper_plot = Plot(xs=x, ys=hermite, title="A Hermite Polynomial", y_min=-100, y_max=100)
theta = np.linspace(-3 * np.pi, 3 * np.pi, 1000)
phase = (1 if triggers.triggered else 0) * triggers.time_since_last_trigger
cos = np.cos(theta + phase)
sin = 1.3 * np.sin(theta + phase)
lower_plot = Plot(xs=[theta, theta], ys=[cos, sin], title="[cos(x), 1.3 * sin(x)]")
lower = Layout()
lower.split_column(
Layout(upper_plot),
Layout(lower_plot),
)
root = Layout()
root.split_row(
Layout(Padding(Markdown(markup, justify="center"), pad=(0, 2))),
lower,
Layout(Image.from_file(THIS_DIR / "tree.jpg")),
)
return root
@ -463,7 +415,7 @@ def repl():
f"""\
## Live Coding: REPL
Sometimes an static example,
Sometimes a static example,
or even an example that you're editing and running multiple times,
just isn't interactive enough.
@ -473,7 +425,7 @@ def repl():
There are two REPLs available by default: the [builtin Python REPL](https://docs.python.org/3/tutorial/interpreter.html#interactive-mode) and {IPYTHON}.
You can change which REPL to use via `Options`, which will be discussed later.
When you exit the REPL (by pressing `ctrl-d` or executing `exit`),
When you exit the REPL (by pressing `ctrl-d` or running `exit()`),
you'll be back at the same point in your presentation.
The state of the REPL is not persistent between invocations
@ -483,34 +435,6 @@ def repl():
return Markdown(markup, justify="center")
@deck.slide(title="Live Coding with Notebooks", notebook=THIS_DIR / "notebook.ipynb")
def notebooks():
markup = dedent(
f"""\
## Live Coding: Notebooks
For a more persistent live-coding experience than a REPL,
you can open a Jupyter Notebook via {NBTERM}
by pressing `n`.
Each slide has a notebook attached to it.
The notebook kernel will be restarted between invocations,
but changes made to the notebook contents
will persist throughout your presentation.
By default, the notebook will initially be blank,
but you can also provide a path to a notebook on disk to initialize from.
This slide has a small example notebook attached to it - try it out!
(In theory, {SPIEL} will be able to support different kinds of terminal notebook viewers in the future,
but right now, only {NBTERM} is packaged by default, and the internal API is not stable or very generic.
Stay tuned!)
"""
)
return Markdown(markup, justify="center")
@deck.slide(title="Options")
def options_():
markup = dedent(

@ -1,57 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"source": [
"Welcome to the demo notebook!\n",
"\n",
"Not much to see here...\n",
"\n",
"This is powered by nbterm (https://github.com/davidbrochart/nbterm).\n",
"See their `README` for usage."
],
"metadata": {
"collapsed": false,
"pycharm": {
"name": "#%% md\n"
}
}
},
{
"cell_type": "code",
"execution_count": null,
"outputs": [],
"source": [
"print('foobar')\n",
"2 ** 10"
],
"metadata": {
"collapsed": false,
"pycharm": {
"name": "#%%\n"
}
}
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 0
}

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

@ -68,7 +68,7 @@ class Example(Presentable):
_cache: Optional[CachedExample] = None
def layout(self, function: ExampleLayout) -> ExampleLayout:
self._layout = function # type: ignore
self._layout = function
return function
@property
@ -87,7 +87,7 @@ class Example(Presentable):
def input(self) -> Syntax:
input = (self._cache.input or "") if self._cache is not None else ""
return Syntax(
input,
input.strip(),
lexer_name=self.language,
code_width=max(len(line) for line in input.splitlines()),
)
@ -109,4 +109,6 @@ class Example(Presentable):
elif self._cache.trigger_number != len(triggers):
self._cache = CachedExample(len(triggers), self.source, self.execute())
return self._layout(self, **self.get_render_kwargs(function=self._layout, triggers=triggers)) # type: ignore
return self._layout(
self, **self.get_render_kwargs(function=self._layout, triggers=triggers)
)

@ -6,10 +6,10 @@ from rich.style import Style
from rich.table import Column, Table
from rich.text import Text
from .modes import Mode
from .rps import RPSCounter
from .state import State
from .utils import drop_nones, joinify
from spiel.modes import Mode
from spiel.rps import RPSCounter
from spiel.state import State
from spiel.utils import drop_nones, filter_join
@dataclass
@ -54,7 +54,7 @@ class Footer:
grid.add_row(
*drop_nones(
Text(
joinify(
filter_join(
" | ",
[
self.state.deck.name,

@ -1,6 +1,6 @@
from dataclasses import dataclass
from click._termui_impl import Editor # type: ignore
from click._termui_impl import Editor
from rich.align import Align
from rich.console import Console, ConsoleRenderable, RenderGroup
from rich.padding import Padding
@ -8,10 +8,10 @@ from rich.style import Style
from rich.table import Column, Table
from rich.text import Text
from .constants import PACKAGE_NAME, __python_version__, __rich_version__, __version__
from .input import INPUT_HANDLER_HELP, SpecialCharacters
from .modes import Mode
from .state import State
from spiel.constants import PACKAGE_NAME, __python_version__, __rich_version__, __version__
from spiel.input import INPUT_HANDLER_HELP, SpecialCharacters
from spiel.modes import Mode
from spiel.state import State
@dataclass

@ -12,7 +12,7 @@ from rich.console import Console, ConsoleOptions
from rich.segment import Segment
from rich.style import Style
from .utils import chunks
from spiel.utils import chunks
class ImageSize(NamedTuple):

@ -30,17 +30,16 @@ from typing import (
import typer
from rich.control import Control
from rich.text import Text
from toml import TomlDecodeError
from tomli import TOMLDecodeError
from typer import Exit
from .constants import EDITOR, PACKAGE_NAME
from .example import Example
from .exceptions import DuplicateInputHandler, InvalidOptionValue
from .modes import Mode
from .notebooks import NOTEBOOKS
from .options import Options
from .repls import REPLS
from .state import State
from spiel.constants import EDITOR, PACKAGE_NAME
from spiel.example import Example
from spiel.exceptions import DuplicateInputHandler, InvalidOptionValue
from spiel.modes import Mode
from spiel.options import Options
from spiel.repls import REPLS
from spiel.state import State
LFLAG = 3
CC = 6
@ -268,11 +267,13 @@ def edit_options(state: State) -> None:
with suspend_live(state):
new_toml = state.options.as_toml()
while True:
new_toml = _clean_toml(typer.edit(text=new_toml, extension=".toml", require_save=False))
new_toml = _clean_toml(
typer.edit(text=new_toml, extension=".toml", require_save=False) or ""
)
try:
state.options = Options.from_toml(new_toml)
return
except TomlDecodeError as e:
except TOMLDecodeError as e:
new_toml = f"{new_toml}\n\n# Parse Error: {e}\n"
except InvalidOptionValue as e:
new_toml = f"{new_toml}\n\n# Invalid Option Value: {e}\n"
@ -404,8 +405,11 @@ def edit_example(state: State) -> None:
example = state.current_slide
if isinstance(example, Example):
with suspend_live(state):
example.source = typer.edit(
text=example.source, extension=Path(example.name).suffix, require_save=False
example.source = (
typer.edit(
text=example.source, extension=Path(example.name).suffix, require_save=False
)
or ""
)
example.clear_cache()
@ -428,24 +432,6 @@ def open_repl(state: State) -> None:
start_no_echo(sys.stdin)
@input_handler(
"n",
name="Open Notebook",
modes=NOT_HELP,
help=f"Open a Jupyter Notebook in your terminal using [link=https://github.com/davidbrochart/nbterm]nbterm[/link].",
)
def open_notebook(state: State) -> None:
with suspend_live(state):
reset_tty(sys.stdin)
state.console.print(Control.clear())
state.console.print(Control.move_to(0, 0))
try:
NOTEBOOKS[state.options.notebook](state)
finally:
start_no_echo(sys.stdin)
@input_handler(
SpecialCharacters.CtrlK,
SpecialCharacters.CtrlC,

@ -11,10 +11,10 @@ from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
from watchdog.observers.polling import PollingObserver
from .constants import DECK, OPTIONS
from .deck import Deck
from .exceptions import NoDeckFound
from .options import Options
from spiel.constants import DECK, OPTIONS
from spiel.deck import Deck
from spiel.exceptions import NoDeckFound
from spiel.options import Options
def load_deck_and_options(path: Path) -> Tuple[Deck, Options]:
@ -49,7 +49,7 @@ def load_deck_and_options(path: Path) -> Tuple[Deck, Options]:
@dataclass
class DeckWatcher(ContextManager):
class DeckWatcher(ContextManager["DeckWatcher"]):
event_handler: FileSystemEventHandler
path: Path
poll: bool = False

@ -10,13 +10,13 @@ from rich.syntax import Syntax
from rich.text import Text
from typer import Argument, Exit, Option, Typer
from .constants import PACKAGE_NAME, __version__
from .help import version_details
from .load import DeckWatcher
from .modes import Mode
from .present import present_deck
from .reloader import DeckReloader
from .state import State
from spiel.constants import PACKAGE_NAME, __version__
from spiel.help import version_details
from spiel.load import DeckWatcher
from spiel.modes import Mode
from spiel.present import present_deck
from spiel.reloader import DeckReloader
from spiel.state import State
THIS_DIR = Path(__file__).resolve().parent
@ -31,7 +31,8 @@ app = Typer(
A {PACKAGE_NAME.capitalize()} presentation (a "deck [of slides]") is defined programmatically using a Python script.
"""
)
),
no_args_is_help=True,
)

@ -1,30 +0,0 @@
from typing import TYPE_CHECKING, Callable, MutableMapping
if TYPE_CHECKING:
from .state import State
from nbterm import Notebook
NotebookExecutor = Callable[["State"], None]
NOTEBOOKS: MutableMapping[str, NotebookExecutor] = {}
def notebook(name: str) -> Callable[[NotebookExecutor], NotebookExecutor]:
def registrar(executor: NotebookExecutor) -> NotebookExecutor:
NOTEBOOKS[name] = executor
return executor
return registrar
@notebook("nbterm")
def nbterm(state: "State") -> None:
save_path = state.tmp_dir / f"{id(state.current_slide)}.ipynb"
nb = Notebook(state.current_slide.notebook or save_path)
state.current_slide.notebook = save_path
nb.show()
nb.save(save_path)

@ -2,22 +2,21 @@ from dataclasses import asdict, dataclass, fields
from pathlib import Path
from typing import Any, Mapping
import toml
import tomli
import tomli_w
from rich.align import Align
from rich.console import ConsoleRenderable
from rich.padding import Padding
from rich.table import Column, Table
from .constants import PACKAGE_NAME
from .exceptions import InvalidOptionValue
from .notebooks import NOTEBOOKS
from .repls import REPLS
from spiel.constants import PACKAGE_NAME
from spiel.exceptions import InvalidOptionValue
from spiel.repls import REPLS
@dataclass
class Options:
repl: str = "ipython"
notebook: str = "nbterm"
footer_time_format: str = "YYYY-MM-DD hh:mm A"
profiling: bool = False
@ -25,9 +24,6 @@ class Options:
if self.repl not in REPLS:
raise InvalidOptionValue(f"repl must be one of: {set(REPLS.keys())}")
if self.notebook not in NOTEBOOKS:
raise InvalidOptionValue(f"notebook must be one of: {set(NOTEBOOKS.keys())}")
def as_dict(self) -> Mapping[str, Any]:
return asdict(self)
@ -38,11 +34,11 @@ class Options:
return cls(**only_valid)
def as_toml(self) -> str:
return toml.dumps({PACKAGE_NAME: self.as_dict()})
return tomli_w.dumps({PACKAGE_NAME: self.as_dict()})
@classmethod
def from_toml(cls, t: str) -> "Options":
return cls.from_dict(toml.loads(t).get(PACKAGE_NAME, {}))
return cls.from_dict(tomli.loads(t).get(PACKAGE_NAME, {}))
def save(self, path: Path) -> Path:
path.write_text(self.as_toml())

@ -1,89 +0,0 @@
import pickle
import re
from functools import lru_cache
from typing import Any, Iterable, List, Sequence, Union
import numpy as np
import uniplot
from colorama import Fore
from colorama import Style as CStyle
from rich.console import Console, ConsoleOptions
from rich.segment import Segment
from rich.style import Style
Plottable = Union[
np.ndarray,
Sequence[np.ndarray],
]
RE_ANSI_ESCAPE = re.compile(r"(\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]))")
ANSI_COLOR_TO_STYLE = {
CStyle.RESET_ALL: Style.null(),
Fore.RED: Style(color="red"),
Fore.GREEN: Style(color="green"),
Fore.BLUE: Style(color="blue"),
Fore.CYAN: Style(color="cyan"),
Fore.YELLOW: Style(color="yellow"),
Fore.MAGENTA: Style(color="magenta"),
Fore.BLACK: Style(color="black"),
Fore.WHITE: Style(color="white"),
}
@lru_cache(maxsize=2 ** 8)
def _ansi_to_text(s: str) -> List[Segment]:
segments = []
tmp = ""
null_style = Style.null()
style = null_style
for char in RE_ANSI_ESCAPE.split(s):
if char in ANSI_COLOR_TO_STYLE:
segments.append(Segment(tmp, style=style))
style = ANSI_COLOR_TO_STYLE[char]
tmp = ""
else:
tmp += char
# catch leftovers
segments.append(Segment(tmp, style=style))
return list(Segment.simplify(segments))
@lru_cache(maxsize=2 ** 8)
def _make_plot(pickled_plot_args: bytes) -> List[str]:
# This is kind of ugly, but we pickle the args before passing them as an easy
# way to make them hashable. This helps a lot for performance on static plots,
# and doesn't have toooooo much impact on dynamic plots.
return uniplot.plot_to_string(**pickle.loads(pickled_plot_args))
class Plot:
def __init__(
self,
**plot_args: Any,
) -> None:
self.plot_args = plot_args
def __rich_console__(self, console: Console, options: ConsoleOptions) -> Iterable[Segment]:
if self.plot_args.get("height") is None and options.height is None:
height = None
else:
# 5 = title + top bar + bottom bar + bottom axis labels + 1
height = max(
(options.height - 5) if options.height else 1, self.plot_args.get("height", 1)
)
plot_args = {
**self.plot_args,
**dict(
height=height,
width=max(options.max_width - 10, self.plot_args.get("width", 1)),
),
}
plot = "\n".join(_make_plot(pickled_plot_args=pickle.dumps(plot_args)))
# plot = "\n".join(uniplot.plot_to_string(plot_args))
yield from _ansi_to_text(plot)

@ -10,17 +10,17 @@ from rich.padding import Padding
from rich.panel import Panel
from rich.style import Style
from .constants import TARGET_RPS
from .exceptions import UnknownModeError
from .footer import Footer
from .help import Help
from .input import handle_input, no_echo
from .modes import Mode
from .presentable import Presentable
from .rps import RPSCounter
from .state import State
from .triggers import Triggers
from .utils import clamp, joinify
from spiel.constants import TARGET_RPS
from spiel.exceptions import UnknownModeError
from spiel.footer import Footer
from spiel.help import Help
from spiel.input import handle_input, no_echo
from spiel.modes import Mode
from spiel.presentable import Presentable
from spiel.rps import RPSCounter
from spiel.state import State
from spiel.triggers import Triggers
from spiel.utils import clamp, filter_join
def render_slide(state: State, slide: Presentable) -> ConsoleRenderable:
@ -56,7 +56,7 @@ def split_layout_into_deck_grid(root: Layout, state: State) -> Layout:
layout.update(
Panel(
slide.render(triggers=Triggers(times=(monotonic(),))),
title=joinify(" | ", [slide_number, slide.title]),
title=filter_join(" | ", [slide_number, slide.title]),
border_style=Style(
color="bright_cyan" if is_active_slide else None,
dim=not is_active_slide,

@ -1,22 +1,22 @@
import inspect
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Dict, Mapping, Optional
from typing import Any, Callable, Dict, Mapping
from rich.console import ConsoleRenderable
from .triggers import Triggers
from spiel.triggers import Triggers
@dataclass
class Presentable: # Why not an ABC? https://github.com/python/mypy/issues/5374
title: str = ""
notebook: Optional[Path] = None
def render(self, triggers: Triggers) -> ConsoleRenderable:
raise NotImplementedError
def get_render_kwargs(self, function: Callable, triggers: Triggers) -> Mapping[str, Any]:
def get_render_kwargs(
self, function: Callable[..., ConsoleRenderable], triggers: Triggers
) -> Mapping[str, Any]:
signature = inspect.signature(function)
kwargs: Dict[str, Any] = {}

@ -8,8 +8,8 @@ from rich.style import Style
from rich.text import Text
from watchdog.events import FileSystemEvent, FileSystemEventHandler
from .load import load_deck_and_options
from .state import State
from spiel.load import load_deck_and_options
from spiel.state import State
@dataclass

@ -10,11 +10,11 @@ REPLS: MutableMapping[str, REPLExecutor] = {}
def repl(name: str) -> Callable[[REPLExecutor], REPLExecutor]:
def registrar(executor: REPLExecutor) -> REPLExecutor:
def register(executor: REPLExecutor) -> REPLExecutor:
REPLS[name] = executor
return executor
return registrar
return register
@repl("builtin")

@ -2,7 +2,7 @@ from collections import deque
from time import monotonic
from typing import Deque, Optional
from .constants import TARGET_RPS
from spiel.constants import TARGET_RPS
class RPSCounter:

@ -6,8 +6,8 @@ from typing import Callable, Union
from rich.console import ConsoleRenderable
from rich.text import Text
from .presentable import Presentable
from .triggers import Triggers
from spiel.presentable import Presentable
from spiel.triggers import Triggers
MakeRenderable = Callable[..., ConsoleRenderable]
RenderableLike = Union[MakeRenderable, ConsoleRenderable]

@ -6,24 +6,24 @@ from pathlib import Path
from tempfile import TemporaryDirectory
from time import monotonic
from types import TracebackType
from typing import Callable, List, Optional, Type, Union
from typing import Callable, ContextManager, List, Optional, Type, Union
from rich.console import Console
from rich.style import Style
from rich.text import Text
from .constants import PACKAGE_NAME
from .deck import Deck
from .load import load_deck_and_options
from .modes import Mode
from .options import Options
from .presentable import Presentable
from spiel.constants import PACKAGE_NAME
from spiel.deck import Deck
from spiel.load import load_deck_and_options
from spiel.modes import Mode
from spiel.options import Options
from spiel.presentable import Presentable
TextLike = Union[Text, Callable[[], Text]]
@dataclass
class State:
class State(ContextManager["State"]):
console: Console
deck: Deck
options: Options
@ -103,7 +103,7 @@ class State:
self.trigger()
@cached_property
def _tmp_dir(self) -> TemporaryDirectory:
def _tmp_dir(self) -> TemporaryDirectory[str]:
return TemporaryDirectory(prefix=f"{PACKAGE_NAME}-")
@cached_property

@ -4,8 +4,8 @@ from typing import Any, Iterable, Iterator, Optional, TypeVar
T = TypeVar("T")
def joinify(joiner: str, items: Iterable[Optional[Any]]) -> str:
return joiner.join(map(str, filter(None, items)))
def filter_join(separator: str, items: Iterable[Optional[Any]]) -> str:
return separator.join(map(str, filter(None, items)))
def drop_nones(*items: Optional[T]) -> Iterator[T]:

@ -1,17 +1,20 @@
from typing import Any, List
from typing import List, Optional, TypeVar
import pytest
from spiel.utils import chunks
T = TypeVar("T")
@pytest.mark.parametrize(
"items, n, fill, expected",
[
("abcdef", 3, None, [["a", "b", "c"], ["d", "e", "f"]]),
("abcde", 3, None, [["a", "b", "c"], ["d", "e", None]]),
("abcde", 3, "fill", [["a", "b", "c"], ["d", "e", "fill"]]),
("", 2, None, []),
],
)
def test_chunks(items: List[Any], n: int, fill: Any, expected: List[List[Any]]) -> None:
assert [list(chunk) for chunk in chunks(items, n)] == expected
def test_chunks(items: List[T], n: int, fill: Optional[T], expected: List[List[T]]) -> None:
assert [list(chunk) for chunk in chunks(items, n, fill_value=fill)] == expected

@ -33,4 +33,4 @@ def test_get_function_body() -> None:
def test_get_function_body_raises_on_function_with_no_source() -> None:
with pytest.raises(TypeError):
get_function_body(str)
get_function_body(str) # type: ignore

@ -36,6 +36,6 @@ def test_render_image(image: Image, console: Console) -> None:
def test_render_image_from_file(console: Console) -> None:
image = Image.from_file(DEMO_DIR / "img.jpg")
image = Image.from_file(DEMO_DIR / "tree.jpg")
console.print(image)

@ -20,7 +20,6 @@ from spiel.input import (
exit,
jump_to_slide,
next_slide,
open_notebook,
open_repl,
previous_slide,
slide_mode,
@ -66,7 +65,6 @@ TESTABLE_INPUT_HANDLERS = list(
exit,
jump_to_slide,
open_repl,
open_notebook,
edit_options,
edit_example,
}

@ -2,7 +2,7 @@ from typing import Any, Iterable, Optional
import pytest
from spiel.utils import joinify
from spiel.utils import filter_join
@pytest.mark.parametrize(
@ -15,5 +15,5 @@ from spiel.utils import joinify
(".", iter(["a", None, "b"]), "a.b"),
],
)
def test_joinify(joiner: str, items: Iterable[Optional[Any]], expected: str) -> None:
assert joinify(joiner, items) == expected
def test_filter_join(joiner: str, items: Iterable[Optional[Any]], expected: str) -> None:
assert filter_join(joiner, items) == expected

@ -9,7 +9,6 @@ from rich.console import Console
from spiel import Options
from spiel.exceptions import InvalidOptionValue
from spiel.notebooks import NOTEBOOKS
from spiel.repls import REPLS
@ -18,7 +17,6 @@ def valid_options() -> SearchStrategy[Options]:
Options,
profiling=infer,
repl=st.sampled_from(list(REPLS.keys())),
notebook=st.sampled_from(list(NOTEBOOKS.keys())),
)
@ -48,7 +46,6 @@ def test_can_render_options(console: Console, three_slide_options: Options) -> N
"key, value",
[
("repl", "foobar"),
("notebook", "foobar"),
],
)
def test_reject_invalid_option_values(key: str, value: Any) -> None:

Loading…
Cancel
Save