From 522563f2d5743ae58b880ccd4f5ee9c2f5898f41 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Thu, 16 Jan 2025 23:33:38 -0500 Subject: [PATCH] Rename rendering and formatting --- docs/docs/output-venues.md | 7 --- docs/docs/rendering-and-output-venues.md | 29 +++++++++ docs/main.py | 14 ++--- reprexlite/cli.py | 4 +- reprexlite/{rendering.py => formatting.py} | 59 ++++++++++--------- reprexlite/ipython.py | 2 +- reprexlite/reprexes.py | 38 +++++++----- .../{test_rendering.py => test_formatting.py} | 20 +++---- tests/test_ipython_editor.py | 2 +- tests/test_ipython_magics.py | 2 +- tests/test_reprexes.py | 8 +-- 11 files changed, 107 insertions(+), 78 deletions(-) delete mode 100644 docs/docs/output-venues.md create mode 100644 docs/docs/rendering-and-output-venues.md rename reprexlite/{rendering.py => formatting.py} (78%) rename tests/{test_rendering.py => test_formatting.py} (84%) diff --git a/docs/docs/output-venues.md b/docs/docs/output-venues.md deleted file mode 100644 index d7ad3ee..0000000 --- a/docs/docs/output-venues.md +++ /dev/null @@ -1,7 +0,0 @@ -# Output Venues - -## Venue options -{{ create_venue_help_table() }} - -## Renderer functions -{{ create_venue_help_examples() }} diff --git a/docs/docs/rendering-and-output-venues.md b/docs/docs/rendering-and-output-venues.md new file mode 100644 index 0000000..5b53176 --- /dev/null +++ b/docs/docs/rendering-and-output-venues.md @@ -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. diff --git a/docs/main.py b/docs/main.py index b58348b..85b3ddc 100644 --- a/docs/main.py +++ b/docs/main.py @@ -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): @@ -66,10 +66,10 @@ 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() @@ -77,13 +77,13 @@ def create_venue_help_table(): @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__}`") diff --git a/reprexlite/cli.py b/reprexlite/cli.py index 11f9d18..cc96998 100644 --- a/reprexlite/cli.py +++ b/reprexlite/cli.py @@ -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 diff --git a/reprexlite/rendering.py b/reprexlite/formatting.py similarity index 78% rename from reprexlite/rendering.py rename to reprexlite/formatting.py index d09a699..6a32902 100644 --- a/reprexlite/rendering.py +++ b/reprexlite/formatting.py @@ -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: @@ -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. @@ -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. @@ -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. @@ -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 @@ -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. diff --git a/reprexlite/ipython.py b/reprexlite/ipython.py index 33512e2..31078db 100644 --- a/reprexlite/ipython.py +++ b/reprexlite/ipython.py @@ -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="") diff --git a/reprexlite/reprexes.py b/reprexlite/reprexes.py index 59af76c..41b9113 100644 --- a/reprexlite/reprexes.py +++ b/reprexlite/reprexes.py @@ -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 @@ -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: @@ -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 @@ -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"" @@ -436,9 +442,9 @@ def _repr_html_(self) -> str: formatter = HtmlFormatter(style="friendly", wrapcode=True) out.append(f"") - out.append(highlight(self.render(), PythonLexer(), formatter)) + out.append(highlight(self.render_and_format(), PythonLexer(), formatter)) except ModuleNotFoundError: - out.append(f"
{self.render()}
") + out.append(f"
{self.render_and_format()}
") return "\n".join(out) @@ -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 diff --git a/tests/test_rendering.py b/tests/test_formatting.py similarity index 84% rename from tests/test_rendering.py rename to tests/test_formatting.py index 8060c29..3ceb46b 100644 --- a/tests/test_rendering.py +++ b/tests/test_formatting.py @@ -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, @@ -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") @@ -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( """\
x = 2
@@ -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
@@ -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
diff --git a/tests/test_ipython_editor.py b/tests/test_ipython_editor.py
index db5fcbf..aafc50c 100644
--- a/tests/test_ipython_editor.py
+++ b/tests/test_ipython_editor.py
@@ -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)
diff --git a/tests/test_ipython_magics.py b/tests/test_ipython_magics.py
index f3de7dd..df56856 100644
--- a/tests/test_ipython_magics.py
+++ b/tests/test_ipython_magics.py
@@ -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)
diff --git a/tests/test_reprexes.py b/tests/test_reprexes.py
index b24b60c..4ae192b 100644
--- a/tests/test_reprexes.py
+++ b/tests/test_reprexes.py
@@ -697,7 +697,7 @@ def mocked_import(name, *args):
 def test_no_black(no_black):
     with pytest.raises(BlackNotFoundError):
         reprex = Reprex.from_input("2+2", config=ReprexConfig(style=True))
-        reprex.render()
+        reprex.render_and_format()
 
 
 @pytest.fixture
@@ -718,7 +718,7 @@ def mocked_import(name, *args):
 def test_black_bad_dependency(black_bad_dependency, monkeypatch):
     with pytest.raises(ModuleNotFoundError) as exc_info:
         reprex = Reprex.from_input("2+2", config=ReprexConfig(style=True))
-        reprex.render()
+        reprex.render_and_format()
     assert not isinstance(exc_info.type, BlackNotFoundError)
     assert exc_info.value.name != "black"
     assert exc_info.value.name == black_bad_dependency
@@ -739,7 +739,7 @@ def mocked_import(name, *args):
 def test_no_pygments_terminal(no_pygments):
     """Test that format for terminal works even if pygments is not installed."""
     r = Reprex.from_input("2+2")
-    assert_str_equals(r.render(terminal=False), r.render(terminal=True))
+    assert_str_equals(r.render_and_format(terminal=False), r.render_and_format(terminal=True))
 
 
 def test_repr_html():
@@ -776,4 +776,4 @@ def test_reprex_function(tmp_path):
 
     # Test writing to file
     with (tmp_path / "rendered.txt").open("r") as fp:
-        assert expected.render() == fp.read()
+        assert expected.render_and_format() == fp.read()