mirror of https://github.com/JoshKarpel/spiel
Add nbterm support and options (#22)
parent
862f523397
commit
b37497490d
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,57 @@
|
||||
{
|
||||
"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
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
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)
|
@ -0,0 +1,69 @@
|
||||
from dataclasses import asdict, dataclass, fields
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
import toml
|
||||
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
|
||||
|
||||
|
||||
@dataclass
|
||||
class Options:
|
||||
profiling: bool = False
|
||||
repl: str = "ipython"
|
||||
notebook: str = "nbterm"
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: Mapping[str, Any]) -> "Options":
|
||||
fields_by_name = {field.name: field for field in fields(cls)}
|
||||
only_valid = {k: fields_by_name[k].type(v) for k, v in d.items() if k in fields_by_name}
|
||||
return cls(**only_valid)
|
||||
|
||||
def as_toml(self) -> str:
|
||||
return toml.dumps({PACKAGE_NAME: self.as_dict()})
|
||||
|
||||
@classmethod
|
||||
def from_toml(cls, t: str) -> "Options":
|
||||
return cls.from_dict(toml.loads(t).get(PACKAGE_NAME, {}))
|
||||
|
||||
def save(self, path: Path) -> Path:
|
||||
path.write_text(self.as_toml())
|
||||
return path
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: Path) -> "Options":
|
||||
return cls.from_toml(path.read_text())
|
||||
|
||||
def __rich__(self) -> ConsoleRenderable:
|
||||
table = Table(
|
||||
Column("Option"),
|
||||
Column("Type"),
|
||||
Column("Value"),
|
||||
)
|
||||
|
||||
fields_by_name = {field.name: field for field in fields(self)}
|
||||
|
||||
for key, value in self.as_dict().items():
|
||||
table.add_row(key, fields_by_name[key].type.__name__, str(value))
|
||||
|
||||
return Padding(
|
||||
Align.center(table),
|
||||
pad=(0, 1),
|
||||
)
|
@ -0,0 +1,30 @@
|
||||
import code
|
||||
from typing import Callable, MutableMapping
|
||||
|
||||
import IPython
|
||||
from traitlets.config import Config
|
||||
|
||||
REPLExecutor = Callable[[], None]
|
||||
|
||||
REPLS: MutableMapping[str, REPLExecutor] = {}
|
||||
|
||||
|
||||
def repl(name: str) -> Callable[[REPLExecutor], REPLExecutor]:
|
||||
def registrar(executor: REPLExecutor) -> REPLExecutor:
|
||||
REPLS[name] = executor
|
||||
return executor
|
||||
|
||||
return registrar
|
||||
|
||||
|
||||
@repl("builtin")
|
||||
def builtin() -> None:
|
||||
code.InteractiveConsole().interact()
|
||||
|
||||
|
||||
@repl("ipython")
|
||||
def ipython() -> None:
|
||||
c = Config()
|
||||
c.InteractiveShellEmbed.colors = "Neutral"
|
||||
|
||||
IPython.embed(config=c)
|
@ -0,0 +1,56 @@
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from _pytest.tmpdir import TempPathFactory
|
||||
from hypothesis import given, infer
|
||||
from hypothesis import strategies as st
|
||||
from hypothesis.strategies import SearchStrategy
|
||||
from rich.console import Console
|
||||
|
||||
from spiel import Options
|
||||
from spiel.exceptions import InvalidOptionValue
|
||||
from spiel.notebooks import NOTEBOOKS
|
||||
from spiel.repls import REPLS
|
||||
|
||||
|
||||
def valid_options() -> SearchStrategy[Options]:
|
||||
return st.builds(
|
||||
Options,
|
||||
profiling=infer,
|
||||
repl=st.sampled_from(list(REPLS.keys())),
|
||||
notebook=st.sampled_from(list(NOTEBOOKS.keys())),
|
||||
)
|
||||
|
||||
|
||||
@given(o=valid_options())
|
||||
def test_round_trip_to_dict(o: Options) -> None:
|
||||
assert o == Options.from_dict(o.as_dict())
|
||||
|
||||
|
||||
@given(o=valid_options())
|
||||
def test_round_trip_to_toml(o: Options) -> None:
|
||||
assert o == Options.from_toml(o.as_toml())
|
||||
|
||||
|
||||
@given(o=valid_options())
|
||||
def test_round_trip_to_file(o: Options, tmp_path_factory: TempPathFactory) -> None:
|
||||
dir = tmp_path_factory.mktemp(basename="options-roundtrip")
|
||||
path = dir / "options.toml"
|
||||
|
||||
assert o == Options.load(o.save(path))
|
||||
|
||||
|
||||
def test_can_render_options(console: Console, three_slide_options: Options) -> None:
|
||||
console.print(three_slide_options)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"key, value",
|
||||
[
|
||||
("repl", "foobar"),
|
||||
("notebook", "foobar"),
|
||||
],
|
||||
)
|
||||
def test_reject_invalid_option_values(key: str, value: Any) -> None:
|
||||
with pytest.raises(InvalidOptionValue):
|
||||
Options(**{key: value})
|
Loading…
Reference in New Issue