Skip to content

Commit

Permalink
Rename rendering and formatting
Browse files Browse the repository at this point in the history
  • Loading branch information
jayqi committed Jan 17, 2025
1 parent b889228 commit 522563f
Show file tree
Hide file tree
Showing 11 changed files with 107 additions and 78 deletions.
7 changes: 0 additions & 7 deletions docs/docs/output-venues.md

This file was deleted.

29 changes: 29 additions & 0 deletions docs/docs/rendering-and-output-venues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Rendering and Output Venues

A rendered reprex will be code plus the computed outputs plus additional formatting markup appropriate for some particular output venue. For example, the `gh` venue (GitHub) will be in GitHub-flavored markdown and may look like this:

````
```python
2+2
#> 4
```
````

The venue can be set using the `--venue / -v` command-line flag or the `venue` configuration file option. The following section documents the available output venue options.

## Venue options

{{ create_venue_help_table() }}

## Formatter functions

{{ create_venue_help_examples() }}

## Under the hood and Python API

There are two steps to rendering a reprex:

1. The `Reprex.render()` method renders a reprex instance as just the code and outputs.
2. A formatter function from `reprexlite.formatting` (see [above](#formatter-functions)) formats the rendered reprex code and outputs for the specified venue.

The whole process is encapsulated in the `Reprex.render_and_format()` method.
14 changes: 7 additions & 7 deletions docs/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from typenames import typenames

from reprexlite.config import ReprexConfig
from reprexlite.rendering import renderer_registry
from reprexlite.formatting import formatter_registry


def define_env(env):
Expand Down Expand Up @@ -66,24 +66,24 @@ def create_venue_help_table():
data = [
{
"Venue Keyword": f"`{venue_key.value}`",
"Description": renderer_entry.label,
"Renderer": f"[`{renderer_entry.renderer.__name__}`](#{renderer_entry.renderer.__name__})",
"Description": formatter_entry.label,
"Formatter Function": f"[`{formatter_entry.fn.__name__}`](#{formatter_entry.fn.__name__})",
}
for venue_key, renderer_entry in renderer_registry.items()
for venue_key, formatter_entry in formatter_registry.items()
]
table = markdownTable(data)
return table.setParams(row_sep="markdown", quote=False).getMarkdown()

@env.macro
def create_venue_help_examples():
data = defaultdict(list)
for key, entry in renderer_registry.items():
data[entry.renderer].append(key)
for key, entry in formatter_registry.items():
data[entry.fn].append(key)

out = []
for fn, keys in data.items():
pass
fn = entry.renderer
fn = entry.fn

keys_list = ", ".join(f"`{key.value}`" for key in keys)
out.append(f"### `{fn.__name__}`")
Expand Down
4 changes: 2 additions & 2 deletions reprexlite/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,10 @@ def main(

if outfile:
with outfile.open("w") as fp:
fp.write(r.render(terminal=False))
fp.write(r.render_and_format(terminal=False))
print(f"Wrote rendered reprex to {outfile}")
else:
print(r.render(terminal=True), end="")
print(r.render_and_format(terminal=True), end="")

return r

Expand Down
59 changes: 30 additions & 29 deletions reprexlite/rendering.py → reprexlite/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,22 @@
from reprexlite.version import __version__


class Renderer(Protocol):
class Formatter(Protocol):
def __call__(self, reprex_str: str, config: Optional[ReprexConfig] = None) -> str: ...


@dataclass
class RendererRegistration:
renderer: Renderer
class FormatterRegistration:
fn: Formatter
label: str


class RendererRegistry:
class FormatterRegistry:
"""Registry of formatters keyed by venue keywords."""

_registry: Dict[str, RendererRegistration] = {}
_registry: Dict[str, FormatterRegistration] = {}

def __getitem__(self, key: Venue) -> Type[Renderer]:
def __getitem__(self, key: Venue) -> Type[Formatter]:
return self._registry[Venue(key)]

def __contains__(self, key: Venue) -> bool:
Expand All @@ -46,26 +46,26 @@ def register(self, venue: Venue, label: str):
label (str): Short human-readable label explaining the venue.
"""

def _register(fn: Renderer):
self._registry[Venue(venue)] = RendererRegistration(renderer=fn, label=label)
def _register(fn: Formatter):
self._registry[Venue(venue)] = FormatterRegistration(fn=fn, label=label)
return fn

return _register


renderer_registry = RendererRegistry()
formatter_registry = FormatterRegistry()


@renderer_registry.register(venue=Venue.DS, label=f"Discourse (alias for '{Venue.GH.value}')")
@renderer_registry.register(venue=Venue.SO, label=f"StackOverflow (alias for '{Venue.GH.value}')")
@renderer_registry.register(venue=Venue.GH, label="Github Flavored Markdown")
def render_to_markdown(
@formatter_registry.register(venue=Venue.DS, label=f"Discourse (alias for '{Venue.GH.value}')")
@formatter_registry.register(venue=Venue.SO, label=f"StackOverflow (alias for '{Venue.GH.value}')")
@formatter_registry.register(venue=Venue.GH, label="Github Flavored Markdown")
def format_as_markdown(
reprex_str: str,
config: Optional[ReprexConfig] = None,
) -> str:
"""
Render a reprex as a GitHub-Flavored Markdown code block. By default, includes a footer that
credits reprexlite.
Format a rendered reprex reprex as a GitHub-Flavored Markdown code block. By default, includes
a footer that credits reprexlite.
Args:
reprex_str (str): The reprex string to render.
Expand Down Expand Up @@ -98,11 +98,11 @@ def render_to_markdown(
return "\n".join(out) + "\n"


@renderer_registry.register(venue=Venue.HTML, label="HTML")
def render_to_html(reprex_str: str, config: Optional[ReprexConfig] = None) -> str:
"""Render a reprex as an HTML code block. If optional dependency Pygments is available, the
rendered HTML will have syntax highlighting for the Python code. By default, includes a footer
that credits reprexlite.
@formatter_registry.register(venue=Venue.HTML, label="HTML")
def format_as_html(reprex_str: str, config: Optional[ReprexConfig] = None) -> str:
"""Format a rendered reprex reprex as an HTML code block. If optional dependency Pygments is
available, the rendered HTML will have syntax highlighting for the Python code. By default,
includes a footer that credits reprexlite.
Args:
reprex_str (str): The reprex string to render.
Expand Down Expand Up @@ -139,9 +139,9 @@ def render_to_html(reprex_str: str, config: Optional[ReprexConfig] = None) -> st
return "\n".join(out) + "\n"


@renderer_registry.register(venue=Venue.PY, label="Python script")
def render_to_python_script(reprex_str: str, config: Optional[ReprexConfig] = None) -> str:
"""Render a reprex as a Python script.
@formatter_registry.register(venue=Venue.PY, label="Python script")
def format_as_python_script(reprex_str: str, config: Optional[ReprexConfig] = None) -> str:
"""Format a rendered reprex reprex as a Python script.
Args:
reprex_str (str): The reprex string to render.
Expand All @@ -167,9 +167,10 @@ def render_to_python_script(reprex_str: str, config: Optional[ReprexConfig] = No
return "\n".join(out) + "\n"


@renderer_registry.register(venue=Venue.RTF, label="Rich Text Format")
def render_to_rtf(reprex_str: str, config: Optional[ReprexConfig] = None) -> str:
"""Render a reprex as a Rich Text Format (RTF) document. Requires dependency Pygments."""
@formatter_registry.register(venue=Venue.RTF, label="Rich Text Format")
def format_as_rtf(reprex_str: str, config: Optional[ReprexConfig] = None) -> str:
"""Format a rendered reprex reprex as a Rich Text Format (RTF) document. Requires dependency
Pygments."""
if config is None:
config = ReprexConfig()
advertise = config.advertise if config.advertise is not None else False
Expand All @@ -191,9 +192,9 @@ def render_to_rtf(reprex_str: str, config: Optional[ReprexConfig] = None) -> str
return highlight(out, PythonLexer(), RtfFormatter()) + "\n"


@renderer_registry.register(venue=Venue.SLACK, label="Slack")
def render_for_slack(reprex_str: str, config: Optional[ReprexConfig] = None) -> str:
"""Render a reprex as Slack markup.
@formatter_registry.register(venue=Venue.SLACK, label="Slack")
def format_for_slack(reprex_str: str, config: Optional[ReprexConfig] = None) -> str:
"""Format a rendered reprex as Slack markup.
Args:
reprex_str (str): The reprex string to render.
Expand Down
2 changes: 1 addition & 1 deletion reprexlite/ipython.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def run_cell(self, raw_cell: str, *args, **kwargs):
if raw_cell != "exit":
try:
r = Reprex.from_input(raw_cell, config=self.reprex_config)
print(r.render(terminal=True), end="")
print(r.render_and_format(terminal=True), end="")
except Exception as e:
print("ERROR: reprexlite has encountered an error while evaluating your input.")
print(e, end="")
Expand Down
38 changes: 22 additions & 16 deletions reprexlite/reprexes.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@

from reprexlite.config import ParsingMethod, ReprexConfig
from reprexlite.exceptions import BlackNotFoundError, InputSyntaxError, UnexpectedError
from reprexlite.formatting import formatter_registry
from reprexlite.parsing import LineType, auto_parse, parse
from reprexlite.rendering import renderer_registry


@dataclasses.dataclass
Expand Down Expand Up @@ -393,14 +393,7 @@ def __eq__(self, other: Any) -> bool:
return NotImplemented

def __str__(self) -> str:
if self.config.keep_old_results:
lines = chain.from_iterable(zip(self.statements, self.old_results, self.results))
else:
lines = chain.from_iterable(zip(self.statements, self.results))
out = "\n".join(str(line) for line in lines if line)
if not out.endswith("\n"):
out += "\n"
return out
return self.render()

@property
def results_match(self) -> bool:
Expand All @@ -410,7 +403,15 @@ def results_match(self) -> bool:
)

def render(self, terminal: bool = False) -> str:
out = str(self)
"""Render the reprex as code."""
if self.config.keep_old_results:
lines = chain.from_iterable(zip(self.statements, self.old_results, self.results))
else:
lines = chain.from_iterable(zip(self.statements, self.results))
out = "\n".join(str(line) for line in lines if line)
if not out.endswith("\n"):
out += "\n"
# if terminal=True and Pygments is available, apply syntax highlighting
if terminal:
try:
from pygments import highlight
Expand All @@ -420,8 +421,13 @@ def render(self, terminal: bool = False) -> str:
out = highlight(out, PythonLexer(), Terminal256Formatter(style="friendly"))
except ModuleNotFoundError:
pass
renderer = renderer_registry[self.config.venue].renderer
return renderer(out.strip(), config=self.config)
return out

def render_and_format(self, terminal: bool = False) -> str:
"""Render the reprex as code and format it for the configured output venue."""
out = self.render(terminal=terminal)
formatter_fn = formatter_registry[self.config.venue].fn
return formatter_fn(out.strip(), config=self.config)

def __repr__(self) -> str:
return f"<Reprex ({len(self.statements)}) '{to_snippet(str(self), 10)}'>"
Expand All @@ -436,9 +442,9 @@ def _repr_html_(self) -> str:

formatter = HtmlFormatter(style="friendly", wrapcode=True)
out.append(f"<style>{formatter.get_style_defs('.highlight')}</style>")
out.append(highlight(self.render(), PythonLexer(), formatter))
out.append(highlight(self.render_and_format(), PythonLexer(), formatter))
except ModuleNotFoundError:
out.append(f"<pre><code>{self.render()}</code></pre>")
out.append(f"<pre><code>{self.render_and_format()}</code></pre>")
return "\n".join(out)


Expand Down Expand Up @@ -503,10 +509,10 @@ def reprex(
# Don't screw up output file or lexing for HTML and RTF with terminal syntax highlighting
terminal = False
r = Reprex.from_input(input, config=config)
output = r.render(terminal=terminal)
output = r.render_and_format(terminal=terminal)
if outfile is not None:
with Path(outfile).open("w") as fp:
fp.write(r.render(terminal=False))
fp.write(r.render_and_format(terminal=False))
if print_:
print(output)
return r
20 changes: 10 additions & 10 deletions tests/test_rendering.py → tests/test_formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from reprexlite.config import ReprexConfig, Venue
from reprexlite.exceptions import PygmentsNotFoundError
from reprexlite.rendering import renderer_registry
from reprexlite.formatting import formatter_registry
from reprexlite.reprexes import Reprex
from tests.expected_formatted import (
ASSETS_DIR,
Expand All @@ -21,23 +21,23 @@

@pytest.fixture
def patch_datetime(monkeypatch):
monkeypatch.setattr(sys.modules["reprexlite.rendering"], "datetime", MockDateTime)
monkeypatch.setattr(sys.modules["reprexlite.formatting"], "datetime", MockDateTime)


@pytest.fixture
def patch_version(monkeypatch):
monkeypatch.setattr(sys.modules["reprexlite.rendering"], "__version__", MOCK_VERSION)
monkeypatch.setattr(sys.modules["reprexlite.formatting"], "__version__", MOCK_VERSION)


@pytest.fixture
def patch_session_info(monkeypatch):
monkeypatch.setattr(sys.modules["reprexlite.rendering"], "SessionInfo", MockSessionInfo)
monkeypatch.setattr(sys.modules["reprexlite.formatting"], "SessionInfo", MockSessionInfo)


@pytest.mark.parametrize("ereprex", expected_reprexes, ids=[e.filename for e in expected_reprexes])
def test_reprex(ereprex, patch_datetime, patch_session_info, patch_version):
r = Reprex.from_input(INPUT, ReprexConfig(**ereprex.kwargs))
actual = r.render()
actual = r.render_and_format()
with (ASSETS_DIR / ereprex.filename).open("r") as fp:
assert str(actual) == fp.read()
assert str(actual).endswith("\n")
Expand All @@ -55,15 +55,15 @@ def mocked_import(name, *args):
monkeypatch.setattr(builtins, "__import__", mocked_import)


def test_all_venues_have_renderers():
def test_all_venues_have_formatters():
for venue in Venue:
print(venue)
assert venue in renderer_registry
assert venue in formatter_registry


def test_html_no_pygments(patch_datetime, patch_version, no_pygments):
r = Reprex.from_input(INPUT, ReprexConfig(venue="html"))
actual = r.render()
actual = r.render_and_format()
expected = dedent(
"""\
<pre><code>x = 2
Expand All @@ -79,7 +79,7 @@ def test_html_no_pygments(patch_datetime, patch_version, no_pygments):
def test_rtf_no_pygments(patch_datetime, patch_version, no_pygments):
with pytest.raises(PygmentsNotFoundError):
r = Reprex.from_input(INPUT, ReprexConfig(venue="rtf"))
r.render()
r.render_and_format()


@pytest.fixture
Expand All @@ -101,7 +101,7 @@ def test_rtf_pygments_bad_dependency(patch_datetime, patch_version, pygments_bad
"""Test that a bad import inside pygments does not trigger PygmentsNotFoundError"""
with pytest.raises(ModuleNotFoundError) as exc_info:
r = Reprex.from_input(INPUT, ReprexConfig(venue="rtf"))
r.render()
r.render_and_format()
assert not isinstance(exc_info.type, PygmentsNotFoundError)
assert exc_info.value.name != "pygments"
assert exc_info.value.name == pygments_bad_dependency
2 changes: 1 addition & 1 deletion tests/test_ipython_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def test_ipython_editor(reprexlite_ipython, capsys):
reprexlite_ipython.run_cell(input)
captured = capsys.readouterr()
r = Reprex.from_input(input)
expected = r.render()
expected = r.render_and_format()

print("\n---EXPECTED---\n")
print(expected)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_ipython_magics.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def test_cell_magic(ipython, capsys):
captured = capsys.readouterr()

r = Reprex.from_input(input, config=ReprexConfig(advertise=False, session_info=True))
expected = r.render(terminal=True)
expected = r.render_and_format(terminal=True)

print("\n---EXPECTED---\n")
print(expected)
Expand Down
Loading

0 comments on commit 522563f

Please sign in to comment.