From 2083455c72b838455a75b8bc27a9f168b90a5b01 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Mon, 27 Feb 2023 18:59:47 -0500 Subject: [PATCH 01/31] WIP --- CONTRIBUTING.md | 43 ++++++++++++++++++++++ pyproject.toml | 71 +++++++++++++++++++++++++++++++++++- reprexlite/reprexes.py | 2 +- tests/conftest.py | 36 ++++++++++++++++++ tests/expected_formatted.py | 15 ++++++++ tests/test_cli.py | 18 ++------- tests/test_formatting.py | 20 +++------- tests/test_ipython_editor.py | 14 ++++--- tests/test_ipython_magics.py | 13 ++++--- tests/test_reprexes.py | 38 ++++++------------- tests/utils.py | 20 ++++++++++ 11 files changed, 223 insertions(+), 67 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 tests/conftest.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..87e4870 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,43 @@ +# Contributing to reprexlite + +[![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch) +[![linting - Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v0.json)](https://github.com/charliermarsh/ruff) +[![code style - black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![types - mypy](https://img.shields.io/badge/types-mypy-blue.svg)](https://github.com/python/mypy) + +## Report a bug or request a feature + +Please file an issue in the [issue tracker](https://github.com/jayqi/reprexlite/issues). + +## Developers guide + +This project uses [Hatch](https://github.com/pypa/hatch) as its project management tool. + +### Tests + +To run tests in your current environment, you should install from source with the `tests` extra to additionally install test dependencies (pytest). Then, use pytest to run the tests. + +```bash +# Install with test dependencies +pip install .[tests] +# Run tests +pytest tests.py +``` + +To run tests on the full test matrix, you should use Hatch: + +```bash +hatch run tests:run +``` + +### Type annotation inspection notebooks + +The directory [`inspect_types/`](./inspect_types/) contains Jupyter notebooks for each supported Python version that inspects attributes and behavior of various type annotations. These are intended as a development aide for understanding behavior of different annotations in different versions of Python. + +To regenerate these notebooks, run: + +```bash +hatch run inspect-types:generate-notebook +``` + +This command will run `nbconvert` on the configured Python version matrix in isolated environments. diff --git a/pyproject.toml b/pyproject.toml index 1cdb8f9..fb7564c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dependencies = [ black = ["black"] pygments = ["Pygments"] ipython = ["ipython"] +tests = ["pytest"] [project.scripts] reprex = "reprexlite.cli:app" @@ -43,6 +44,75 @@ reprex = "reprexlite.cli:app" "Bug Tracker" = "https://github.com/jayqi/reprexlite/issues" "Changelog" = "https://jayqi.github.io/reprexlite/stable/changelog/" +[tool.hatch.build] +exclude = ["docs/"] + +## DEFAULT ENVIRONMENT ## + +[tool.hatch.envs.default] +dependencies = ["black", "ipython", "mypy", "ruff"] +python = "3.10" +path = ".venv" + +[tool.hatch.envs.default.scripts] +lint = ["black --check reprexlite tests", "ruff check reprexlite tests"] +typecheck = ["mypy reprexlite --install-types --non-interactive"] + +## TESTS ENVIRONMENT ## + +[tool.hatch.envs.tests] +features = ["tests"] +dependencies = ["coverage", "pytest-cov"] +template = "tests" + +[[tool.hatch.envs.tests.matrix]] +python = ["3.7", "3.8", "3.9", "3.10", "3.11"] + +[[tool.hatch.envs.tests.matrix]] +python = ["3.10"] +extras = ["black", "ipython", "pygments"] + +[tool.hatch.envs.tests.overrides] +matrix.extras.features = [ + { value = "black", if = [ + "black", + ] }, + { value = "ipython", if = [ + "ipython", + ] }, + { value = "pygments", if = [ + "pygments", + ] }, +] + +[tool.hatch.envs.tests.scripts] +run = "pytest tests" +run-cov = "run --cov=reprexlite --cov-report=term --cov-report=html --cov-report=xml" +run-debug = "run --pdb" + + +## DOCS ENVIRONMENT ## + +[tool.hatch.envs.docs] +dependencies = [ + "mkdocs", + "mkdocs-material", + "mike", + "mkdocstrings[python]", + "mdx-truly-sane-lists", +] +template = "docs" + +[tool.hatch.envs.docs.scripts] +build = [ + "rm -rf site/", + "cp README.md docs/index.md", + "cp CHANGELOG.md docs/changelog.md", + "mkdocs build", +] + + +## TOOLS ## [tool.black] line-length = 99 @@ -81,7 +151,6 @@ ignore_missing_imports = true [tool.pytest.ini_options] minversion = "6.0" -addopts = "--cov=reprexlite --cov-report=term --cov-report=html --cov-report=xml" testpaths = ["tests"] [tool.coverage.run] diff --git a/reprexlite/reprexes.py b/reprexlite/reprexes.py index 7817aeb..a91f954 100644 --- a/reprexlite/reprexes.py +++ b/reprexlite/reprexes.py @@ -137,7 +137,7 @@ def evaluate(self, scope: dict) -> RawResult: return RawResult(config=self.config, raw=None, stdout=None) if "__name__" not in scope: - scope["__name__"] = "__reprex__" + scope["__name__"] = "__main__" stdout_io = StringIO() try: with redirect_stdout(stdout_io): diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..d30c60c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,36 @@ +import pytest + + +def pytest_configure(config): + try: + import IPython + + print(IPython.__version__) + pytest.IPYTHON_IS_AVAILABLE = True + except ModuleNotFoundError as e: + if e.name == "IPython": + pytest.IPYTHON_IS_AVAILABLE = False + else: + raise + + try: + import black + + print(black.__version__) + pytest.BLACK_IS_AVAILABLE = True + except ModuleNotFoundError as e: + if e.name == "black": + pytest.BLACK_IS_AVAILABLE = False + else: + raise + + try: + import pygments + + print(pygments.__version__) + pytest.PYGMENTS_IS_AVAILABLE = True + except ModuleNotFoundError as e: + if e.name == "pygments": + pytest.PYGMENTS_IS_AVAILABLE = False + else: + raise diff --git a/tests/expected_formatted.py b/tests/expected_formatted.py index 67bbbd3..2054bab 100644 --- a/tests/expected_formatted.py +++ b/tests/expected_formatted.py @@ -4,6 +4,7 @@ python -m tests.expected_formatted """ +import builtins from contextlib import contextmanager from dataclasses import dataclass from pathlib import Path @@ -133,6 +134,20 @@ def patch_session_info(): sys.modules["reprexlite.formatting"].SessionInfo = SessionInfo +@contextmanager +def no_pygments(): + import_orig = builtins.__import__ + + def mocked_import(name, *args): + if name.startswith("pygments"): + raise ModuleNotFoundError(name="pygments") + return import_orig(name, *args) + + builtins.__import__ = mocked_import + yield + builtins.__import__ = import_orig + + if __name__ == "__main__": shutil.rmtree(ASSETS_DIR, ignore_errors=True) with patch_datetime(), patch_version(), patch_session_info(): diff --git a/tests/test_cli.py b/tests/test_cli.py index 2adb8e6..0e1b72e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -9,7 +9,7 @@ from reprexlite.cli import app from reprexlite.exceptions import IPythonNotFoundError from reprexlite.version import __version__ -from tests.utils import remove_ansi_escape +from tests.utils import remove_ansi_escape, requires_ipython, requires_no_ipython runner = CliRunner() @@ -42,18 +42,6 @@ def mock_edit(self, *args, **kwargs): return patch -@pytest.fixture -def no_ipython(monkeypatch): - import_orig = builtins.__import__ - - def mocked_import(name, *args): - if name.startswith("reprexlite.ipython"): - raise IPythonNotFoundError - return import_orig(name, *args) - - monkeypatch.setattr(builtins, "__import__", mocked_import) - - def test_reprex(patch_edit): result = runner.invoke(app) print(result.stdout) @@ -103,6 +91,7 @@ def test_old_results(patch_edit): assert "#> [2, 3, 4, 5, 6]" in result.stdout +@requires_ipython def test_ipython_editor(): """Test that IPython interactive editor opens as expected. Not testing a reprex. Not sure how to inject input into the IPython shell.""" @@ -111,7 +100,8 @@ def test_ipython_editor(): assert "Interactive reprex editor via IPython" in result.stdout # text from banner -def test_ipython_editor_not_installed(no_ipython): +@requires_no_ipython +def test_ipython_editor_not_installed(): """Test for expected error when opening the IPython interactive editor without IPython installed""" result = runner.invoke(app, ["-e", "ipython"]) diff --git a/tests/test_formatting.py b/tests/test_formatting.py index 8bd02b1..74d57e0 100644 --- a/tests/test_formatting.py +++ b/tests/test_formatting.py @@ -16,7 +16,7 @@ MockSessionInfo, expected_reprexes, ) -from tests.utils import assert_str_equals +from tests.utils import assert_str_equals, requires_no_pygments, requires_pygments @pytest.fixture @@ -43,19 +43,8 @@ def test_reprex(ereprex, patch_datetime, patch_session_info, patch_version): assert str(actual).endswith("\n") -@pytest.fixture -def no_pygments(monkeypatch): - import_orig = builtins.__import__ - - def mocked_import(name, *args): - if name.startswith("pygments"): - raise ModuleNotFoundError(name="pygments") - return import_orig(name, *args) - - monkeypatch.setattr(builtins, "__import__", mocked_import) - - -def test_html_no_pygments(patch_datetime, patch_version, no_pygments): +@requires_no_pygments +def test_html_no_pygments(patch_datetime, patch_version): r = Reprex.from_input(INPUT, ReprexConfig(venue="html")) actual = r.format() expected = dedent( @@ -70,7 +59,8 @@ def test_html_no_pygments(patch_datetime, patch_version, no_pygments): assert str(actual).endswith("\n") -def test_rtf_no_pygments(patch_datetime, patch_version, no_pygments): +@requires_no_pygments +def test_rtf_no_pygments(patch_datetime, patch_version): with pytest.raises(PygmentsNotFoundError): r = Reprex.from_input(INPUT, ReprexConfig(venue="rtf")) r.format() diff --git a/tests/test_ipython_editor.py b/tests/test_ipython_editor.py index 7048e01..3b1da48 100644 --- a/tests/test_ipython_editor.py +++ b/tests/test_ipython_editor.py @@ -3,17 +3,19 @@ import sys from textwrap import dedent -from IPython.testing import globalipapp import pytest from reprexlite.exceptions import IPythonNotFoundError -from reprexlite.ipython import ReprexTerminalInteractiveShell from reprexlite.reprexes import Reprex -from tests.utils import remove_ansi_escape +from tests.utils import remove_ansi_escape, requires_ipython, requires_no_ipython @pytest.fixture() def reprexlite_ipython(monkeypatch): + from IPython.testing import globalipapp + + from reprexlite.ipython import ReprexTerminalInteractiveShell + monkeypatch.setattr(globalipapp, "TerminalInteractiveShell", ReprexTerminalInteractiveShell) monkeypatch.setattr(ReprexTerminalInteractiveShell, "_instance", None) ipython = globalipapp.start_ipython() @@ -48,6 +50,7 @@ def mocked_import(name, *args): yield module_name +@requires_ipython def test_ipython_editor(reprexlite_ipython, capsys): input = dedent( """\ @@ -68,12 +71,13 @@ def test_ipython_editor(reprexlite_ipython, capsys): assert remove_ansi_escape(captured.out) == expected -def test_no_ipython_error(no_ipython, monkeypatch): - monkeypatch.delitem(sys.modules, "reprexlite.ipython") +@requires_no_ipython +def test_no_ipython_error(monkeypatch): with pytest.raises(IPythonNotFoundError): importlib.import_module("reprexlite.ipython") +@requires_ipython def test_bad_ipython_dependency(ipython_bad_dependency, monkeypatch): """Test that a bad import inside IPython does not trigger IPythonNotFoundError""" monkeypatch.delitem(sys.modules, "reprexlite.ipython") diff --git a/tests/test_ipython_magics.py b/tests/test_ipython_magics.py index e1cfc19..e07d7b4 100644 --- a/tests/test_ipython_magics.py +++ b/tests/test_ipython_magics.py @@ -3,16 +3,18 @@ import sys from textwrap import dedent -from IPython.terminal.interactiveshell import TerminalInteractiveShell -from IPython.testing import globalipapp import pytest from reprexlite.config import ReprexConfig from reprexlite.reprexes import Reprex +from tests.utils import requires_ipython, requires_no_ipython @pytest.fixture() def ipython(monkeypatch): + from IPython.terminal.interactiveshell import TerminalInteractiveShell + from IPython.testing import globalipapp + monkeypatch.setattr(TerminalInteractiveShell, "_instance", None) ipython = globalipapp.start_ipython() ipython.run_line_magic("load_ext", "reprexlite") @@ -33,6 +35,7 @@ def mocked_import(name, *args): monkeypatch.setattr(builtins, "__import__", mocked_import) +@requires_ipython def test_line_magic(ipython, capsys): ipython.run_line_magic("reprex", line="") captured = capsys.readouterr() @@ -40,6 +43,7 @@ def test_line_magic(ipython, capsys): assert r"Cell Magic Usage: %%reprex" in captured.out +@requires_ipython def test_cell_magic(ipython, capsys): input = dedent( """\ @@ -62,8 +66,7 @@ def test_cell_magic(ipython, capsys): assert captured.out == expected -def test_no_ipython(no_ipython, monkeypatch): +@requires_no_ipython +def test_no_ipython(monkeypatch): """Tests that not having ipython installed should not cause any import errors.""" - monkeypatch.delitem(sys.modules, "reprexlite") - monkeypatch.delitem(sys.modules, "reprexlite.ipython") importlib.import_module("reprexlite") diff --git a/tests/test_reprexes.py b/tests/test_reprexes.py index 59ea3a2..0040566 100644 --- a/tests/test_reprexes.py +++ b/tests/test_reprexes.py @@ -7,7 +7,13 @@ from reprexlite.config import ReprexConfig from reprexlite.exceptions import BlackNotFoundError, UnexpectedError from reprexlite.reprexes import ParsedResult, RawResult, Reprex, reprex -from tests.utils import assert_equals, assert_not_equals, assert_str_equals +from tests.utils import ( + assert_equals, + assert_not_equals, + assert_str_equals, + requires_no_black, + requires_no_pygments, +) Case = namedtuple("Case", ["id", "input", "expected"]) @@ -663,6 +669,7 @@ def test_raw_result_to_parsed_result_comparisons(): ) +@requires_black def test_style_with_black(): input = dedent( """\ @@ -682,18 +689,7 @@ def test_style_with_black(): reprex.statements[0].raw_code == expected.strip() -@pytest.fixture -def no_black(monkeypatch): - import_orig = builtins.__import__ - - def mocked_import(name, *args): - if name.startswith("black"): - raise ModuleNotFoundError(name="black") - return import_orig(name, *args) - - monkeypatch.setattr(builtins, "__import__", mocked_import) - - +@requires_no_black def test_no_black(no_black): with pytest.raises(BlackNotFoundError): reprex = Reprex.from_input("2+2", config=ReprexConfig(style=True)) @@ -724,19 +720,8 @@ def test_black_bad_dependency(black_bad_dependency, monkeypatch): assert exc_info.value.name == black_bad_dependency -@pytest.fixture -def no_pygments(monkeypatch): - import_orig = builtins.__import__ - - def mocked_import(name, *args): - if name.startswith("pygments"): - raise ModuleNotFoundError(name="pygments") - return import_orig(name, *args) - - monkeypatch.setattr(builtins, "__import__", mocked_import) - - -def test_no_pygments_terminal(no_pygments): +@requires_no_pygments +def test_no_pygments_terminal(): """Test that format for terminal works even if pygments is not installed.""" r = Reprex.from_input("2+2") assert_str_equals(r.format(terminal=False), r.format(terminal=True)) @@ -748,6 +733,7 @@ def test_repr_html(): r._repr_html_() +@requires_no_pygments def test_repr_html_no_pygments(no_pygments): """Test that rich HTML display for Jupyter Notebooks runs without error even if pygments is not installed.""" diff --git a/tests/utils.py b/tests/utils.py index 8512599..dd03fcd 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,6 +1,26 @@ import re from typing import Any +import pytest + +## SKIP DECORATORS + +requires_ipython = pytest.mark.skipif( + not pytest.IPYTHON_IS_AVAILABLE, reason="ipython is not available" +) +requires_no_ipython = pytest.mark.skipif( + pytest.IPYTHON_IS_AVAILABLE, reason="ipython is available" +) +requires_black = pytest.mark.skipif(not pytest.BLACK_IS_AVAILABLE, reason="black is not available") +requires_no_black = pytest.mark.skipif(pytest.BLACK_IS_AVAILABLE, reason="black is available") +requires_pygments = pytest.mark.skipif( + not pytest.PYGMENTS_IS_AVAILABLE, reason="pygments is not available" +) +requires_no_pygments = pytest.mark.skipif( + pytest.PYGMENTS_IS_AVAILABLE, reason="pygments is available" +) + + # https://stackoverflow.com/a/14693789/5957621 ANSI_ESCAPE_REGEX = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") From 250b931954b41d4132183bc28dbfbaa3ab475c80 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Sat, 25 Mar 2023 00:18:02 -0400 Subject: [PATCH 02/31] Swap to formatter callables --- pyproject.toml | 2 +- reprexlite/config.py | 4 + reprexlite/formatting.py | 279 ++++++++++++++++----------------------- reprexlite/reprexes.py | 4 +- 4 files changed, 118 insertions(+), 171 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fb7564c..3eaf0cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ exclude = ["docs/"] ## DEFAULT ENVIRONMENT ## [tool.hatch.envs.default] -dependencies = ["black", "ipython", "mypy", "ruff"] +dependencies = ["black", "ipython", "mypy", "ruff", "tqdm"] python = "3.10" path = ".venv" diff --git a/reprexlite/config.py b/reprexlite/config.py index 5a37d82..9c37d54 100644 --- a/reprexlite/config.py +++ b/reprexlite/config.py @@ -28,6 +28,10 @@ class ReprexConfig: formatting. """ + # Editor + editor: Optional[str] = field(default=None, metadata={"help": "..."}) + no_color: Optional[str] = 0 + # Formatting venue: str = field( default="gh", diff --git a/reprexlite/formatting.py b/reprexlite/formatting.py index 1209de1..0ba98c6 100644 --- a/reprexlite/formatting.py +++ b/reprexlite/formatting.py @@ -1,8 +1,23 @@ -from abc import ABC, abstractmethod import dataclasses from datetime import datetime -from textwrap import dedent -from typing import ClassVar, Dict, Optional, Type +from typing import Dict, Optional + +try: + from typing import Protocol, runtime_checkable +except ImportError: + from typing_extensions import Protocol, runtime_checkable + +try: + from pygments import highlight + import pygments.formatters + from pygments.lexers import PythonLexer + + PYGMENTS_AVAILABLE = True +except ModuleNotFoundError as e: + if e.name == "pygments": + PYGMENTS_AVAILABLE = False + else: + raise from reprexlite.exceptions import NotAFormatterError, PygmentsNotFoundError from reprexlite.session_info import SessionInfo @@ -15,24 +30,10 @@ class FormatterMetadata: venues: Dict[str, str] = dataclasses.field(default_factory=lambda: dict()) -class Formatter(ABC): - """Abstract base class for a reprex formatter. Concrete subclasses should implement the - formatting logic appropriate to a specific venue for sharing. Call `str(...)` on an instance - to return the formatted reprex. - - Attributes: - default_advertise (bool): Whether to render reprexlite advertisement by default - meta (FormatterMeta): Contains metadata for the formatter, such as label text and an - example - """ - - default_advertise: ClassVar[bool] = True - meta: ClassVar[FormatterMetadata] - - @classmethod - @abstractmethod - def format( - cls, reprex_str: str, advertise: Optional[bool] = None, session_info: bool = False +@runtime_checkable +class Formatter(Protocol): + def __call__( + self, reprex_str: str, advertise: Optional[bool] = None, session_info: bool = False ) -> str: """Format a reprex string for a specific sharing venue. @@ -48,7 +49,7 @@ def format( """ -formatter_registry: Dict[str, Type[Formatter]] = {} +formatter_registry: Dict[str, Formatter] = {} """Registry of formatters keyed by venue keywords.""" @@ -60,13 +61,12 @@ def register_formatter(venue: str, label: str): label (str): Short human-readable label explaining the venue. """ - def registrar(cls): + def registrar(fn): global formatter_registry - if not isinstance(cls, type) or not issubclass(cls, Formatter): + if not isinstance(fn, Formatter): raise NotAFormatterError("Only subclasses of Formatter can be registered.") - formatter_registry[venue] = cls - cls.meta.venues[venue] = label - return cls + formatter_registry[venue] = {"formatter": fn, "label": label} + return fn return registrar @@ -74,76 +74,60 @@ def registrar(cls): @register_formatter(venue="ds", label="Discourse (alias for 'gh')") @register_formatter(venue="so", label="StackOverflow (alias for 'gh')") @register_formatter(venue="gh", label="Github Flavored Markdown") -class GitHubFormatter(Formatter): - """Formatter for rendering reprexes in GitHub Flavored Markdown.""" - - default_advertise = True - meta = FormatterMetadata( - example=dedent( - """\ - ```python - 2+2 - #> 4 - ``` - """ - ) - ) +def format_github_flavored_markdown( + reprex_str: str, advertise: Optional[bool] = None, session_info: bool = False +) -> str: + """Formatter for rendering reprexes in GitHub Flavored Markdown. - @classmethod - def format( - cls, reprex_str: str, advertise: Optional[bool] = None, session_info: bool = False - ) -> str: - if advertise is None: - advertise = cls.default_advertise - out = [] - out.append("```python") - out.append(reprex_str) + Args: + reprex_str (str): String containing rendered reprex output. + advertise (Optional[bool], optional): Whether to include the advertisement for + reprexlite. Defaults to None, which uses a per-formatter default. + session_info (bool, optional): Whether to include detailed session information. + Defaults to False. + + Returns: + str: String containing formatted reprex code. Ends with newline. + """ + if advertise is None: + advertise = True + out = [] + out.append("```python") + out.append(reprex_str) + out.append("```") + if advertise: + out.append("\n" + Advertisement().markdown()) + if session_info: + out.append("\n
Session Info") + out.append("```text") + out.append(str(SessionInfo())) out.append("```") - if advertise: - out.append("\n" + Advertisement().markdown()) - if session_info: - out.append("\n
Session Info") - out.append("```text") - out.append(str(SessionInfo())) - out.append("```") - out.append("
") - return "\n".join(out) + "\n" + out.append("
") + return "\n".join(out) + "\n" -@register_formatter(venue="html", label="HTML") -class HtmlFormatter(Formatter): +@dataclasses.dataclass +class HtmlFormatter: """Formatter for rendering reprexes in HTML. If optional dependency Pygments is available, the rendered HTML will have syntax highlighting for the Python code.""" - default_advertise = True - meta = FormatterMetadata( - example=dedent( - """\ -
2+2
-            #> 4
- """ - ) - ) + no_color: bool - @classmethod - def format( - cls, reprex_str: str, advertise: Optional[bool] = None, session_info: bool = False + def __call__( + self, reprex_str: str, advertise: Optional[bool] = None, session_info: bool = False ) -> str: if advertise is None: - advertise = cls.default_advertise + advertise = True out = [] - try: - from pygments import highlight - from pygments.formatters import HtmlFormatter - from pygments.lexers import PythonLexer - formatter = HtmlFormatter( - style="friendly", lineanchors=True, linenos=True, wrapcode=True + if self.no_color or not PYGMENTS_AVAILABLE: + out.append(f'
{reprex_str}
') + else: + formatter = pygments.formatters.HtmlFormatter( + lineanchors=True, linenos=True, wrapcode=True ) out.append(f"") out.append(highlight(str(reprex_str), PythonLexer(), formatter)) - except ImportError: - out.append(f"
{reprex_str}
") if advertise: out.append(Advertisement().html().strip()) @@ -154,102 +138,61 @@ def format( return "\n".join(out) + "\n" -@register_formatter(venue="py", label="Python script") -class PyScriptFormatter(Formatter): - """Formatter for rendering reprexes as a Python script.""" - - default_advertise = False - meta = FormatterMetadata( - example=dedent( - """\ - 2+2 - #> 4 - """ - ) - ) +register_formatter(venue="html", label="HTML")(HtmlFormatter(no_color=False)) +register_formatter(venue="htmlnocolor", label="HTML (No Color)")(HtmlFormatter(no_color=True)) - @classmethod - def format( - cls, reprex_str: str, advertise: Optional[bool] = None, session_info: bool = False - ) -> str: - if advertise is None: - advertise = cls.default_advertise - out = [str(reprex_str)] - if advertise: - out.append("\n" + Advertisement().code_comment()) - if session_info: - out.append("") - sess_lines = str(SessionInfo()).split("\n") - out.extend("# " + line for line in sess_lines) - return "\n".join(out) + "\n" + +@register_formatter(venue="py", label="Python script") +def format_python( + reprex_str: str, advertise: Optional[bool] = None, session_info: bool = False +) -> str: + if advertise is None: + advertise = False + out = [str(reprex_str)] + if advertise: + out.append("\n" + Advertisement().code_comment()) + if session_info: + out.append("") + sess_lines = str(SessionInfo()).split("\n") + out.extend("# " + line for line in sess_lines) + return "\n".join(out) + "\n" @register_formatter(venue="rtf", label="Rich Text Format") -class RtfFormatter(Formatter): - """Formatter for rendering reprexes in Rich Text Format.""" +def format_rtf( + reprex_str: str, advertise: Optional[bool] = None, session_info: bool = False +) -> str: + if not PYGMENTS_AVAILABLE: + raise PygmentsNotFoundError("Pygments is required for RTF output.", name="pygments") - default_advertise = False - meta = FormatterMetadata(example=None) + if advertise is None: + advertise = False - @classmethod - def format( - cls, reprex_str: str, advertise: Optional[bool] = None, session_info: bool = False - ) -> str: - if advertise is None: - advertise = cls.default_advertise - try: - from pygments import highlight - from pygments.formatters import RtfFormatter - from pygments.lexers import PythonLexer - except ModuleNotFoundError as e: - if e.name == "pygments": - raise PygmentsNotFoundError( - "Pygments is required for RTF output.", name="pygments" - ) - else: - raise - - out = str(reprex_str) - if advertise: - out += "\n\n" + Advertisement().text() - if session_info: - out += "\n\n" + str(SessionInfo()) - return highlight(out, PythonLexer(), RtfFormatter()) + "\n" + out = str(reprex_str) + if advertise: + out += "\n\n" + Advertisement().text() + if session_info: + out += "\n\n" + str(SessionInfo()) + return highlight(out, PythonLexer(), pygments.formatters.RtfFormatter()) + "\n" @register_formatter(venue="slack", label="Slack") -class SlackFormatter(Formatter): - """Formatter for rendering reprexes as Slack markup.""" - - default_advertise = False - meta = FormatterMetadata( - example=dedent( - """\ - ``` - 2+2 - #> 4 - ``` - """ - ) - ) - - @classmethod - def format( - cls, reprex_str: str, advertise: Optional[bool] = None, session_info: bool = False - ) -> str: - if advertise is None: - advertise = cls.default_advertise - out = [] - out.append("```") - out.append(str(reprex_str)) +def format_slack( + reprex_str: str, advertise: Optional[bool] = None, session_info: bool = False +) -> str: + if advertise is None: + advertise = False + out = [] + out.append("```") + out.append(str(reprex_str)) + out.append("```") + if advertise: + out.append("\n" + Advertisement().text()) + if session_info: + out.append("\n```") + out.append(str(SessionInfo())) out.append("```") - if advertise: - out.append("\n" + Advertisement().text()) - if session_info: - out.append("\n```") - out.append(str(SessionInfo())) - out.append("```") - return "\n".join(out) + "\n" + return "\n".join(out) + "\n" class Advertisement: diff --git a/reprexlite/reprexes.py b/reprexlite/reprexes.py index a91f954..f2aaf0b 100644 --- a/reprexlite/reprexes.py +++ b/reprexlite/reprexes.py @@ -420,8 +420,8 @@ def format(self, terminal: bool = False) -> str: out = highlight(out, PythonLexer(), Terminal256Formatter(style="friendly")) except ModuleNotFoundError: pass - formatter = formatter_registry[self.config.venue] - return formatter.format( + formatter = formatter_registry[self.config.venue]["formatter"] + return formatter( out.strip(), advertise=self.config.advertise, session_info=self.config.session_info ) From 27a840b012351c07d6948d87843b0ea2c0e02f61 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Sat, 25 Mar 2023 00:18:14 -0400 Subject: [PATCH 03/31] Update tests --- .github/workflows/tests.yml | 29 +++-- tests/assets/ad/html.html | 122 ++++++++++----------- tests/assets/ad/htmlnocolor.html | 4 + tests/assets/html.html | 122 ++++++++++----------- tests/assets/htmlnocolor.html | 4 + tests/assets/no_ad/html.html | 122 ++++++++++----------- tests/assets/no_ad/htmlnocolor.html | 3 + tests/assets/session_info/html.html | 122 ++++++++++----------- tests/assets/session_info/htmlnocolor.html | 13 +++ tests/expected_formatted.py | 58 ++++++---- tests/test_formatting.py | 82 +++++++------- tests/test_reprexes.py | 9 +- 12 files changed, 367 insertions(+), 323 deletions(-) create mode 100644 tests/assets/ad/htmlnocolor.html create mode 100644 tests/assets/htmlnocolor.html create mode 100644 tests/assets/no_ad/htmlnocolor.html create mode 100644 tests/assets/session_info/htmlnocolor.html diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c4b26da..c38b13c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,17 +23,14 @@ jobs: cache: "pip" cache-dependency-path: | pyproject.toml - requirements-dev.txt - - name: Install dependencies + - name: Install hatch run: | - python -m pip install --upgrade pip - pip install -r requirements-dev.txt + pip install hatch - name: Lint package run: | - make lint - make typecheck + hatch run lint tests: name: "Tests (${{ matrix.os }}, Python ${{ matrix.python-version }})" @@ -43,6 +40,16 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] + include: + - os: ubuntu-latest + python-version: "3.11" + extra: "-black" + - os: ubuntu-latest + python-version: "3.11" + extra: "-ipython" + - os: ubuntu-latest + python-version: "3.11" + extra: "-pygments" steps: - uses: actions/checkout@v3 @@ -54,16 +61,14 @@ jobs: cache: "pip" cache-dependency-path: | pyproject.toml - requirements-dev.txt - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements-dev.txt + - name: Install hatch + run: | + pip install hatch - name: Run tests run: | - make test + hatch run tests.py${{ matrix.python-version }}${{ matrix.extra }}:run - name: Upload coverage to codecov uses: codecov/codecov-action@v3 diff --git a/tests/assets/ad/html.html b/tests/assets/ad/html.html index 56bac09..e980610 100644 --- a/tests/assets/ad/html.html +++ b/tests/assets/ad/html.html @@ -1,77 +1,77 @@ +.highlight .mb { color: #666666 } /* Literal.Number.Bin */ +.highlight .mf { color: #666666 } /* Literal.Number.Float */ +.highlight .mh { color: #666666 } /* Literal.Number.Hex */ +.highlight .mi { color: #666666 } /* Literal.Number.Integer */ +.highlight .mo { color: #666666 } /* Literal.Number.Oct */ +.highlight .sa { color: #BA2121 } /* Literal.String.Affix */ +.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ +.highlight .sc { color: #BA2121 } /* Literal.String.Char */ +.highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */ +.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #BA2121 } /* Literal.String.Double */ +.highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ +.highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ +.highlight .sx { color: #008000 } /* Literal.String.Other */ +.highlight .sr { color: #A45A77 } /* Literal.String.Regex */ +.highlight .s1 { color: #BA2121 } /* Literal.String.Single */ +.highlight .ss { color: #19177C } /* Literal.String.Symbol */ +.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #0000FF } /* Name.Function.Magic */ +.highlight .vc { color: #19177C } /* Name.Variable.Class */ +.highlight .vg { color: #19177C } /* Name.Variable.Global */ +.highlight .vi { color: #19177C } /* Name.Variable.Instance */ +.highlight .vm { color: #19177C } /* Name.Variable.Magic */ +.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */
1
 2
 3
x = 2
diff --git a/tests/assets/ad/htmlnocolor.html b/tests/assets/ad/htmlnocolor.html
new file mode 100644
index 0000000..c549309
--- /dev/null
+++ b/tests/assets/ad/htmlnocolor.html
@@ -0,0 +1,4 @@
+
x = 2
+x + 2
+#> 4
+

Created at DATETIME by reprexlite vVERSION

diff --git a/tests/assets/html.html b/tests/assets/html.html index 56bac09..e980610 100644 --- a/tests/assets/html.html +++ b/tests/assets/html.html @@ -1,77 +1,77 @@ +.highlight .mb { color: #666666 } /* Literal.Number.Bin */ +.highlight .mf { color: #666666 } /* Literal.Number.Float */ +.highlight .mh { color: #666666 } /* Literal.Number.Hex */ +.highlight .mi { color: #666666 } /* Literal.Number.Integer */ +.highlight .mo { color: #666666 } /* Literal.Number.Oct */ +.highlight .sa { color: #BA2121 } /* Literal.String.Affix */ +.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ +.highlight .sc { color: #BA2121 } /* Literal.String.Char */ +.highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */ +.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #BA2121 } /* Literal.String.Double */ +.highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ +.highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ +.highlight .sx { color: #008000 } /* Literal.String.Other */ +.highlight .sr { color: #A45A77 } /* Literal.String.Regex */ +.highlight .s1 { color: #BA2121 } /* Literal.String.Single */ +.highlight .ss { color: #19177C } /* Literal.String.Symbol */ +.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #0000FF } /* Name.Function.Magic */ +.highlight .vc { color: #19177C } /* Name.Variable.Class */ +.highlight .vg { color: #19177C } /* Name.Variable.Global */ +.highlight .vi { color: #19177C } /* Name.Variable.Instance */ +.highlight .vm { color: #19177C } /* Name.Variable.Magic */ +.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */
1
 2
 3
x = 2
diff --git a/tests/assets/htmlnocolor.html b/tests/assets/htmlnocolor.html
new file mode 100644
index 0000000..c549309
--- /dev/null
+++ b/tests/assets/htmlnocolor.html
@@ -0,0 +1,4 @@
+
x = 2
+x + 2
+#> 4
+

Created at DATETIME by reprexlite vVERSION

diff --git a/tests/assets/no_ad/html.html b/tests/assets/no_ad/html.html index e9e10b8..a3b8e7c 100644 --- a/tests/assets/no_ad/html.html +++ b/tests/assets/no_ad/html.html @@ -1,77 +1,77 @@ +.highlight .mb { color: #666666 } /* Literal.Number.Bin */ +.highlight .mf { color: #666666 } /* Literal.Number.Float */ +.highlight .mh { color: #666666 } /* Literal.Number.Hex */ +.highlight .mi { color: #666666 } /* Literal.Number.Integer */ +.highlight .mo { color: #666666 } /* Literal.Number.Oct */ +.highlight .sa { color: #BA2121 } /* Literal.String.Affix */ +.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ +.highlight .sc { color: #BA2121 } /* Literal.String.Char */ +.highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */ +.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #BA2121 } /* Literal.String.Double */ +.highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ +.highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ +.highlight .sx { color: #008000 } /* Literal.String.Other */ +.highlight .sr { color: #A45A77 } /* Literal.String.Regex */ +.highlight .s1 { color: #BA2121 } /* Literal.String.Single */ +.highlight .ss { color: #19177C } /* Literal.String.Symbol */ +.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #0000FF } /* Name.Function.Magic */ +.highlight .vc { color: #19177C } /* Name.Variable.Class */ +.highlight .vg { color: #19177C } /* Name.Variable.Global */ +.highlight .vi { color: #19177C } /* Name.Variable.Instance */ +.highlight .vm { color: #19177C } /* Name.Variable.Magic */ +.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */
1
 2
 3
x = 2
diff --git a/tests/assets/no_ad/htmlnocolor.html b/tests/assets/no_ad/htmlnocolor.html
new file mode 100644
index 0000000..0da0ee0
--- /dev/null
+++ b/tests/assets/no_ad/htmlnocolor.html
@@ -0,0 +1,3 @@
+
x = 2
+x + 2
+#> 4
diff --git a/tests/assets/session_info/html.html b/tests/assets/session_info/html.html index 45ad741..56cbe78 100644 --- a/tests/assets/session_info/html.html +++ b/tests/assets/session_info/html.html @@ -1,77 +1,77 @@ +.highlight .mb { color: #666666 } /* Literal.Number.Bin */ +.highlight .mf { color: #666666 } /* Literal.Number.Float */ +.highlight .mh { color: #666666 } /* Literal.Number.Hex */ +.highlight .mi { color: #666666 } /* Literal.Number.Integer */ +.highlight .mo { color: #666666 } /* Literal.Number.Oct */ +.highlight .sa { color: #BA2121 } /* Literal.String.Affix */ +.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ +.highlight .sc { color: #BA2121 } /* Literal.String.Char */ +.highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */ +.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #BA2121 } /* Literal.String.Double */ +.highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ +.highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ +.highlight .sx { color: #008000 } /* Literal.String.Other */ +.highlight .sr { color: #A45A77 } /* Literal.String.Regex */ +.highlight .s1 { color: #BA2121 } /* Literal.String.Single */ +.highlight .ss { color: #19177C } /* Literal.String.Symbol */ +.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #0000FF } /* Name.Function.Magic */ +.highlight .vc { color: #19177C } /* Name.Variable.Class */ +.highlight .vg { color: #19177C } /* Name.Variable.Global */ +.highlight .vi { color: #19177C } /* Name.Variable.Instance */ +.highlight .vm { color: #19177C } /* Name.Variable.Magic */ +.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */
1
 2
 3
x = 2
diff --git a/tests/assets/session_info/htmlnocolor.html b/tests/assets/session_info/htmlnocolor.html
new file mode 100644
index 0000000..5c34abb
--- /dev/null
+++ b/tests/assets/session_info/htmlnocolor.html
@@ -0,0 +1,13 @@
+
x = 2
+x + 2
+#> 4
+

Created at DATETIME by reprexlite vVERSION

+
Session Info +
-- Session Info --------------------------------------------------------------
+version Python 3.x.y (Jan 01 2020 03:33:33)
+os      GLaDOS
+-- Packages ------------------------------------------------------------------
+datatable 1.0
+ggplot2   2.0
+pkgnet    3.0
+
diff --git a/tests/expected_formatted.py b/tests/expected_formatted.py index 2054bab..4d02b0f 100644 --- a/tests/expected_formatted.py +++ b/tests/expected_formatted.py @@ -4,17 +4,15 @@ python -m tests.expected_formatted """ -import builtins from contextlib import contextmanager from dataclasses import dataclass +from itertools import chain from pathlib import Path import shutil import sys from textwrap import dedent from typing import Any, Dict -from tqdm import tqdm - from reprexlite import reprex from reprexlite.session_info import Package, SessionInfo @@ -40,36 +38,50 @@ class ExpectedReprex: ExpectedReprex("gh.md", {"venue": "gh"}), ExpectedReprex("so.md", {"venue": "so"}), ExpectedReprex("ds.md", {"venue": "ds"}), - ExpectedReprex("html.html", {"venue": "html"}), + ExpectedReprex("htmlnocolor.html", {"venue": "htmlnocolor"}), ExpectedReprex("py.py", {"venue": "py"}), - ExpectedReprex("rtf.rtf", {"venue": "rtf"}), ExpectedReprex("slack.txt", {"venue": "slack"}), # With ad ExpectedReprex("ad/gh.md", {"venue": "gh", "advertise": True}), ExpectedReprex("ad/so.md", {"venue": "so", "advertise": True}), ExpectedReprex("ad/ds.md", {"venue": "ds", "advertise": True}), - ExpectedReprex("ad/html.html", {"venue": "html", "advertise": True}), + ExpectedReprex("ad/htmlnocolor.html", {"venue": "htmlnocolor", "advertise": True}), ExpectedReprex("ad/py.py", {"venue": "py", "advertise": True}), - ExpectedReprex("ad/rtf.rtf", {"venue": "rtf", "advertise": True}), ExpectedReprex("ad/slack.txt", {"venue": "slack", "advertise": True}), # No ad ExpectedReprex("no_ad/gh.md", {"venue": "gh", "advertise": False}), ExpectedReprex("no_ad/so.md", {"venue": "so", "advertise": False}), ExpectedReprex("no_ad/ds.md", {"venue": "ds", "advertise": False}), - ExpectedReprex("no_ad/html.html", {"venue": "html", "advertise": False}), + ExpectedReprex("no_ad/htmlnocolor.html", {"venue": "htmlnocolor", "advertise": False}), ExpectedReprex("no_ad/py.py", {"venue": "py", "advertise": False}), - ExpectedReprex("no_ad/rtf.rtf", {"venue": "rtf", "advertise": False}), ExpectedReprex("no_ad/slack.txt", {"venue": "slack", "advertise": False}), # With session info ExpectedReprex("session_info/gh.md", {"venue": "gh", "session_info": True}), ExpectedReprex("session_info/so.md", {"venue": "so", "session_info": True}), ExpectedReprex("session_info/ds.md", {"venue": "ds", "session_info": True}), - ExpectedReprex("session_info/html.html", {"venue": "html", "session_info": True}), + ExpectedReprex( + "session_info/htmlnocolor.html", + {"venue": "htmlnocolor", "session_info": True}, + ), ExpectedReprex("session_info/py.py", {"venue": "py", "session_info": True}), - ExpectedReprex("session_info/rtf.rtf", {"venue": "rtf", "session_info": True}), ExpectedReprex("session_info/slack.txt", {"venue": "slack", "session_info": True}), ] +expected_reprexes_requires_pygments = [ + ExpectedReprex("html.html", {"venue": "html"}), + ExpectedReprex("rtf.rtf", {"venue": "rtf"}), + # With ad + ExpectedReprex("ad/html.html", {"venue": "html", "advertise": True}), + ExpectedReprex("ad/rtf.rtf", {"venue": "rtf", "advertise": True}), + # No ad + ExpectedReprex("no_ad/html.html", {"venue": "html", "advertise": False}), + ExpectedReprex("no_ad/rtf.rtf", {"venue": "rtf", "advertise": False}), + # With session info + ExpectedReprex("session_info/html.html", {"venue": "html", "session_info": True}), + ExpectedReprex("session_info/rtf.rtf", {"venue": "rtf", "session_info": True}), +] + + MOCK_VERSION = "VERSION" @@ -134,24 +146,26 @@ def patch_session_info(): sys.modules["reprexlite.formatting"].SessionInfo = SessionInfo -@contextmanager -def no_pygments(): - import_orig = builtins.__import__ +# @contextmanager +# def no_pygments(): +# import_orig = builtins.__import__ - def mocked_import(name, *args): - if name.startswith("pygments"): - raise ModuleNotFoundError(name="pygments") - return import_orig(name, *args) +# def mocked_import(name, *args): +# if name.startswith("pygments"): +# raise ModuleNotFoundError(name="pygments") +# return import_orig(name, *args) - builtins.__import__ = mocked_import - yield - builtins.__import__ = import_orig +# builtins.__import__ = mocked_import +# yield +# builtins.__import__ = import_orig if __name__ == "__main__": + import tqdm + shutil.rmtree(ASSETS_DIR, ignore_errors=True) with patch_datetime(), patch_version(), patch_session_info(): - for ereprex in tqdm(expected_reprexes): + for ereprex in tqdm(chain(expected_reprexes, expected_reprexes_requires_pygments)): outfile = ASSETS_DIR / ereprex.filename outfile.parent.mkdir(exist_ok=True) reprex(INPUT, outfile=outfile, **ereprex.kwargs, print_=False) diff --git a/tests/test_formatting.py b/tests/test_formatting.py index 74d57e0..8470a4b 100644 --- a/tests/test_formatting.py +++ b/tests/test_formatting.py @@ -15,6 +15,7 @@ MockDateTime, MockSessionInfo, expected_reprexes, + expected_reprexes_requires_pygments, ) from tests.utils import assert_str_equals, requires_no_pygments, requires_pygments @@ -35,7 +36,25 @@ def patch_session_info(monkeypatch): @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): +def test_reprex_formatting(ereprex, patch_datetime, patch_session_info, patch_version): + """Test that venue formatting works in basic cases.""" + r = Reprex.from_input(INPUT, ReprexConfig(**ereprex.kwargs)) + actual = r.format() + with (ASSETS_DIR / ereprex.filename).open("r") as fp: + assert str(actual) == fp.read() + assert str(actual).endswith("\n") + + +@requires_pygments +@pytest.mark.parametrize( + "ereprex", + expected_reprexes_requires_pygments, + ids=[e.filename for e in expected_reprexes_requires_pygments], +) +def test_reprex_formatting_requires_pygments( + ereprex, patch_datetime, patch_session_info, patch_version +): + """Test that venue formatting works in basic cases.""" r = Reprex.from_input(INPUT, ReprexConfig(**ereprex.kwargs)) actual = r.format() with (ASSETS_DIR / ereprex.filename).open("r") as fp: @@ -45,18 +64,14 @@ def test_reprex(ereprex, patch_datetime, patch_session_info, patch_version): @requires_no_pygments def test_html_no_pygments(patch_datetime, patch_version): - r = Reprex.from_input(INPUT, ReprexConfig(venue="html")) - actual = r.format() - expected = dedent( - """\ -
x = 2
-        x + 2
-        #> 4
-

Created at DATETIME by reprexlite vVERSION

- """ # noqa: E501 - ) - assert_str_equals(expected, str(actual)) - assert str(actual).endswith("\n") + """Test that html produces the same thing as htmlnocolor when pygments is not installed.""" + r_html = Reprex.from_input(INPUT, ReprexConfig(venue="html")) + actual_html = r_html.format() + + r_htmlnocolor = Reprex.from_input(INPUT, ReprexConfig(venue="htmlnocolor")) + actual_htmlnocolor = r_htmlnocolor.format() + + assert_str_equals(str(actual_htmlnocolor), str(actual_html)) @requires_no_pygments @@ -66,34 +81,19 @@ def test_rtf_no_pygments(patch_datetime, patch_version): r.format() -@pytest.fixture -def pygments_bad_dependency(monkeypatch): - """ModuleNotFoundError inside pygments""" - module_name = "dependency_of_pygments" - import_orig = builtins.__import__ - - def mocked_import(name, *args): - if name.startswith("pygments"): - raise ModuleNotFoundError(name=module_name) - return import_orig(name, *args) - - monkeypatch.setattr(builtins, "__import__", mocked_import) - yield module_name - - -def test_rtf_pygments_bad_dependency(patch_datetime, patch_version, pygments_bad_dependency): - """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.format() - assert not isinstance(exc_info.type, PygmentsNotFoundError) - assert exc_info.value.name != "pygments" - assert exc_info.value.name == pygments_bad_dependency +# def test_rtf_pygments_bad_dependency(patch_datetime, patch_version, pygments_bad_dependency): +# """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.format() +# assert not isinstance(exc_info.type, PygmentsNotFoundError) +# assert exc_info.value.name != "pygments" +# assert exc_info.value.name == pygments_bad_dependency -def test_not_a_formatter_error(): - with pytest.raises(NotAFormatterError): +# def test_not_a_formatter_error(): +# with pytest.raises(NotAFormatterError): - @register_formatter("l33t", label="l33t") - class F0rm4tt3r: - pass +# @register_formatter("l33t", label="l33t") +# class F0rm4tt3r: +# pass diff --git a/tests/test_reprexes.py b/tests/test_reprexes.py index 0040566..ee975ec 100644 --- a/tests/test_reprexes.py +++ b/tests/test_reprexes.py @@ -11,6 +11,7 @@ assert_equals, assert_not_equals, assert_str_equals, + requires_black, requires_no_black, requires_no_pygments, ) @@ -205,11 +206,11 @@ class MyClass: ... """, expected="""\ __name__ - #> '__reprex__' + #> '__main__' class MyClass: ... MyClass.__module__ - #> '__reprex__' + #> '__main__' """, ), ] @@ -690,7 +691,7 @@ def test_style_with_black(): @requires_no_black -def test_no_black(no_black): +def test_no_black(): with pytest.raises(BlackNotFoundError): reprex = Reprex.from_input("2+2", config=ReprexConfig(style=True)) reprex.format() @@ -734,7 +735,7 @@ def test_repr_html(): @requires_no_pygments -def test_repr_html_no_pygments(no_pygments): +def test_repr_html_no_pygments(): """Test that rich HTML display for Jupyter Notebooks runs without error even if pygments is not installed.""" r = Reprex.from_input("2+2") From c109a3db81a76ba62ca504b0b95900c584000585 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Sat, 25 Mar 2023 00:34:17 -0400 Subject: [PATCH 04/31] Fix lint --- reprexlite/exceptions.py | 4 ---- tests/test_cli.py | 2 -- tests/test_formatting.py | 5 +---- tests/test_ipython_magics.py | 1 - 4 files changed, 1 insertion(+), 11 deletions(-) diff --git a/reprexlite/exceptions.py b/reprexlite/exceptions.py index d13dcd2..cb961c2 100644 --- a/reprexlite/exceptions.py +++ b/reprexlite/exceptions.py @@ -34,10 +34,6 @@ class NoPrefixMatchError(ValueError, ReprexliteException): pass -class NotAFormatterError(TypeError, ReprexliteException): - """Raised when registering a formatter that is not a subclass of the Formatter base class.""" - - class PromptLengthMismatchError(ReprexliteException): pass diff --git a/tests/test_cli.py b/tests/test_cli.py index 0e1b72e..a650c07 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,4 +1,3 @@ -import builtins import subprocess from textwrap import dedent @@ -7,7 +6,6 @@ from typer.testing import CliRunner from reprexlite.cli import app -from reprexlite.exceptions import IPythonNotFoundError from reprexlite.version import __version__ from tests.utils import remove_ansi_escape, requires_ipython, requires_no_ipython diff --git a/tests/test_formatting.py b/tests/test_formatting.py index 8470a4b..a826d82 100644 --- a/tests/test_formatting.py +++ b/tests/test_formatting.py @@ -1,12 +1,9 @@ -import builtins import sys -from textwrap import dedent import pytest from reprexlite.config import ReprexConfig -from reprexlite.exceptions import NotAFormatterError, PygmentsNotFoundError -from reprexlite.formatting import register_formatter +from reprexlite.exceptions import PygmentsNotFoundError from reprexlite.reprexes import Reprex from tests.expected_formatted import ( ASSETS_DIR, diff --git a/tests/test_ipython_magics.py b/tests/test_ipython_magics.py index e07d7b4..316ae95 100644 --- a/tests/test_ipython_magics.py +++ b/tests/test_ipython_magics.py @@ -1,6 +1,5 @@ import builtins import importlib -import sys from textwrap import dedent import pytest From 0877e6b0f6b834b5d96cf64853267a9268e3b0cf Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Sat, 25 Mar 2023 01:08:09 -0400 Subject: [PATCH 05/31] Fix indentation --- .github/workflows/tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c38b13c..7cbab2f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -62,9 +62,9 @@ jobs: cache-dependency-path: | pyproject.toml - - name: Install hatch - run: | - pip install hatch + - name: Install hatch + run: | + pip install hatch - name: Run tests run: | From 05f50a51419bba9040244eac85446eb145abb1a3 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Sat, 25 Mar 2023 01:10:12 -0400 Subject: [PATCH 06/31] Fix version --- .github/workflows/tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7cbab2f..ffcc8b7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -42,13 +42,13 @@ jobs: python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] include: - os: ubuntu-latest - python-version: "3.11" + python-version: "3.10" extra: "-black" - os: ubuntu-latest - python-version: "3.11" + python-version: "3.10" extra: "-ipython" - os: ubuntu-latest - python-version: "3.11" + python-version: "3.10" extra: "-pygments" steps: From d9fbec6b0aad9c1f55bbd58d93bb43c7b9fa0233 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Sat, 25 Mar 2023 01:17:37 -0400 Subject: [PATCH 07/31] Change formatter validation --- reprexlite/exceptions.py | 4 ++++ reprexlite/formatting.py | 11 +++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/reprexlite/exceptions.py b/reprexlite/exceptions.py index cb961c2..d8d7b35 100644 --- a/reprexlite/exceptions.py +++ b/reprexlite/exceptions.py @@ -10,6 +10,10 @@ class InputSyntaxError(SyntaxError, ReprexliteException): """Raised when encountering a syntax error when parsing input.""" +class InvalidFormatterError(TypeError, ReprexliteException): + """Raised when registering a formatter that is not a callable.""" + + class InvalidInputPrefixesError(ValueError, ReprexliteException): pass diff --git a/reprexlite/formatting.py b/reprexlite/formatting.py index 0ba98c6..4395bda 100644 --- a/reprexlite/formatting.py +++ b/reprexlite/formatting.py @@ -3,9 +3,9 @@ from typing import Dict, Optional try: - from typing import Protocol, runtime_checkable + from typing import Protocol except ImportError: - from typing_extensions import Protocol, runtime_checkable + from typing_extensions import Protocol try: from pygments import highlight @@ -19,7 +19,7 @@ else: raise -from reprexlite.exceptions import NotAFormatterError, PygmentsNotFoundError +from reprexlite.exceptions import InvalidFormatterError, PygmentsNotFoundError from reprexlite.session_info import SessionInfo from reprexlite.version import __version__ @@ -30,7 +30,6 @@ class FormatterMetadata: venues: Dict[str, str] = dataclasses.field(default_factory=lambda: dict()) -@runtime_checkable class Formatter(Protocol): def __call__( self, reprex_str: str, advertise: Optional[bool] = None, session_info: bool = False @@ -62,9 +61,9 @@ def register_formatter(venue: str, label: str): """ def registrar(fn): + if not callable(fn): + raise InvalidFormatterError("Formatter must be a callable.") global formatter_registry - if not isinstance(fn, Formatter): - raise NotAFormatterError("Only subclasses of Formatter can be registered.") formatter_registry[venue] = {"formatter": fn, "label": label} return fn From 70979fcd9a1eed7b8a4548ed0198c1e1643f9ac1 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Sat, 25 Mar 2023 01:39:56 -0400 Subject: [PATCH 08/31] Run coverage --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ffcc8b7..c974f19 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -68,7 +68,7 @@ jobs: - name: Run tests run: | - hatch run tests.py${{ matrix.python-version }}${{ matrix.extra }}:run + hatch run tests.py${{ matrix.python-version }}${{ matrix.extra }}:run-cov - name: Upload coverage to codecov uses: codecov/codecov-action@v3 From 098d7d9dfdbdb557457c755ea11d3bba16660325 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Sat, 25 Mar 2023 01:43:15 -0400 Subject: [PATCH 09/31] Add codecov token --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c974f19..ae81869 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -73,6 +73,7 @@ jobs: - name: Upload coverage to codecov uses: codecov/codecov-action@v3 with: + token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage.xml fail_ci_if_error: true if: ${{ matrix.os == 'ubuntu-latest' }} From 3270e600867ce7cb05be3a0fee2ce0beafa6e6e0 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Sat, 25 Mar 2023 01:46:39 -0400 Subject: [PATCH 10/31] Build with hatch --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ae81869..d624689 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -81,7 +81,7 @@ jobs: - name: Build distribution and test installation shell: bash run: | - make dist + hatch build if [[ ${{ matrix.os }} == "windows-latest" ]]; then PYTHON_BIN=Scripts/python else From 112fa3a5c20b0635f5be03655847171fbe500112 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Sat, 25 Mar 2023 01:57:50 -0400 Subject: [PATCH 11/31] Remove InvalidFormatterError. Not that useful --- reprexlite/exceptions.py | 4 ---- reprexlite/formatting.py | 4 +--- tests/test_formatting.py | 18 ------------------ 3 files changed, 1 insertion(+), 25 deletions(-) diff --git a/reprexlite/exceptions.py b/reprexlite/exceptions.py index d8d7b35..cb961c2 100644 --- a/reprexlite/exceptions.py +++ b/reprexlite/exceptions.py @@ -10,10 +10,6 @@ class InputSyntaxError(SyntaxError, ReprexliteException): """Raised when encountering a syntax error when parsing input.""" -class InvalidFormatterError(TypeError, ReprexliteException): - """Raised when registering a formatter that is not a callable.""" - - class InvalidInputPrefixesError(ValueError, ReprexliteException): pass diff --git a/reprexlite/formatting.py b/reprexlite/formatting.py index 4395bda..d58b702 100644 --- a/reprexlite/formatting.py +++ b/reprexlite/formatting.py @@ -19,7 +19,7 @@ else: raise -from reprexlite.exceptions import InvalidFormatterError, PygmentsNotFoundError +from reprexlite.exceptions import PygmentsNotFoundError from reprexlite.session_info import SessionInfo from reprexlite.version import __version__ @@ -61,8 +61,6 @@ def register_formatter(venue: str, label: str): """ def registrar(fn): - if not callable(fn): - raise InvalidFormatterError("Formatter must be a callable.") global formatter_registry formatter_registry[venue] = {"formatter": fn, "label": label} return fn diff --git a/tests/test_formatting.py b/tests/test_formatting.py index a826d82..790c6f3 100644 --- a/tests/test_formatting.py +++ b/tests/test_formatting.py @@ -76,21 +76,3 @@ def test_rtf_no_pygments(patch_datetime, patch_version): with pytest.raises(PygmentsNotFoundError): r = Reprex.from_input(INPUT, ReprexConfig(venue="rtf")) r.format() - - -# def test_rtf_pygments_bad_dependency(patch_datetime, patch_version, pygments_bad_dependency): -# """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.format() -# assert not isinstance(exc_info.type, PygmentsNotFoundError) -# assert exc_info.value.name != "pygments" -# assert exc_info.value.name == pygments_bad_dependency - - -# def test_not_a_formatter_error(): -# with pytest.raises(NotAFormatterError): - -# @register_formatter("l33t", label="l33t") -# class F0rm4tt3r: -# pass From 922937d34bd720107d7c6669a8402f88d52bd631 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Sat, 25 Mar 2023 01:58:38 -0400 Subject: [PATCH 12/31] Use hatchling --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3eaf0cb..70edf12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] -requires = ["flit_core >=3.2,<4"] -build-backend = "flit_core.buildapi" +requires = ["hatchling"] +build-backend = "hatchling.build" [project] name = "reprexlite" From 3d3252127db062dbf83ae477b7985f8afb96c991 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Sat, 25 Mar 2023 02:04:23 -0400 Subject: [PATCH 13/31] Use hatch docs command --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d624689..e497a73 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -100,7 +100,7 @@ jobs: - name: Test building documentation run: | - make docs + hatch run docs:build if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.10' - name: Deploy site preview to Netlify From e607b2499cd4e854785416e43193d4700403f322 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Sat, 25 Mar 2023 02:13:55 -0400 Subject: [PATCH 14/31] Fix docs build --- pyproject.toml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 70edf12..f5c14f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,19 +96,23 @@ run-debug = "run --pdb" [tool.hatch.envs.docs] dependencies = [ "mkdocs", + "mkdocs-jupyter", + "mkdocs-macros-plugin", "mkdocs-material", "mike", "mkdocstrings[python]", "mdx-truly-sane-lists", + "py-markdown-table==0.3.3", + "typenames", ] template = "docs" [tool.hatch.envs.docs.scripts] build = [ - "rm -rf site/", - "cp README.md docs/index.md", - "cp CHANGELOG.md docs/changelog.md", - "mkdocs build", + "rm -rf docs/site/", + "cp README.md docs/docs/index.md", + "cp CHANGELOG.md docs/docs/changelog.md", + "cd docs && mkdocs build", ] From 012afae73b8fe680794a232dded6eb06a72462f3 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Sat, 25 Mar 2023 12:47:25 -0400 Subject: [PATCH 15/31] Use sys.executable instead of python --- tests/test_cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index a650c07..0b43a9a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,4 +1,5 @@ import subprocess +import sys from textwrap import dedent import pytest @@ -124,7 +125,7 @@ def test_version(): def test_python_m_version(): """Test the CLI with python -m and --version flag.""" result = subprocess.run( - ["python", "-m", "reprexlite", "--version"], + [sys.executable, "-m", "reprexlite", "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, From 01df74b02f8fa5e8209c14ad95d610b2d78954cc Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Sat, 25 Mar 2023 13:09:37 -0400 Subject: [PATCH 16/31] Improve envs --- CONTRIBUTING.md | 29 ++++++++++++++--------------- pyproject.toml | 17 +++++++++++++---- requirements-dev.txt | 20 -------------------- 3 files changed, 27 insertions(+), 39 deletions(-) delete mode 100644 requirements-dev.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 87e4870..abe29ea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,13 +9,9 @@ Please file an issue in the [issue tracker](https://github.com/jayqi/reprexlite/issues). -## Developers guide - -This project uses [Hatch](https://github.com/pypa/hatch) as its project management tool. - -### Tests +## Standalone tests -To run tests in your current environment, you should install from source with the `tests` extra to additionally install test dependencies (pytest). Then, use pytest to run the tests. +To run tests in your current environment, you should install from source with the `tests` extra to additionally install test dependencies. Then, use pytest to run the tests. ```bash # Install with test dependencies @@ -24,20 +20,23 @@ pip install .[tests] pytest tests.py ``` -To run tests on the full test matrix, you should use Hatch: +## Developers guide + +This project uses [Hatch](https://github.com/pypa/hatch) as its project management tool. The default environment includes dependencies for linting as well as all extra dependencies. + +### Linting + +To run linting or typechecking from the default environment: ```bash -hatch run tests:run +hatch run lint +hatch run typecheck ``` -### Type annotation inspection notebooks - -The directory [`inspect_types/`](./inspect_types/) contains Jupyter notebooks for each supported Python version that inspects attributes and behavior of various type annotations. These are intended as a development aide for understanding behavior of different annotations in different versions of Python. +### Tests -To regenerate these notebooks, run: +To run tests on the full test matrix, use the Hatch command: ```bash -hatch run inspect-types:generate-notebook +hatch run tests:run ``` - -This command will run `nbconvert` on the configured Python version matrix in isolated environments. diff --git a/pyproject.toml b/pyproject.toml index f5c14f4..7406729 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,15 +50,24 @@ exclude = ["docs/"] ## DEFAULT ENVIRONMENT ## [tool.hatch.envs.default] -dependencies = ["black", "ipython", "mypy", "ruff", "tqdm"] -python = "3.10" +features = ["black", "pygments", "ipython", "tests"] +dependencies = [ + "tqdm", # For script to regenerate test assets +] path = ".venv" +template = "lint" + +## LINTING ENVIRONMENT ## +[tool.hatch.envs.lint] +dependencies = ["black", "mypy", "ruff"] +python = "3.10" +detached = true -[tool.hatch.envs.default.scripts] +[tool.hatch.envs.lint.scripts] lint = ["black --check reprexlite tests", "ruff check reprexlite tests"] typecheck = ["mypy reprexlite --install-types --non-interactive"] -## TESTS ENVIRONMENT ## +## TESTS ENVIRONMENTS ## [tool.hatch.envs.tests] features = ["tests"] diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index bab6875..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,20 +0,0 @@ --e .[black,pygments,ipython] - -black -build -mdx_truly_sane_lists==1.3 -mike -mkdocs>=1.2.2 -mkdocs-jupyter -mkdocs-macros-plugin -mkdocs-material>=7.2.6 -mkdocstrings[python-legacy]>=0.15.2 -mypy -pip>=21.3 -py-markdown-table==0.3.3 -pytest -pytest-cov -ruff -tqdm -typenames -wheel From a20c52647c97a423ea7b0bdbdab91ee4c764bb06 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Sat, 25 Mar 2023 13:25:35 -0400 Subject: [PATCH 17/31] Update workflows --- .github/workflows/docs-main.yml | 10 ++++------ .github/workflows/release.yml | 11 +++++------ .github/workflows/tests.yml | 2 +- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/.github/workflows/docs-main.yml b/.github/workflows/docs-main.yml index c7f611a..d20acdd 100644 --- a/.github/workflows/docs-main.yml +++ b/.github/workflows/docs-main.yml @@ -19,16 +19,14 @@ jobs: cache: "pip" cache-dependency-path: | pyproject.toml - requirements-dev.txt - - name: Install dependencies + - name: Install hatch run: | - python -m pip install --upgrade pip - pip install -r requirements-dev.txt + pip install hatch - name: Test building documentation run: | - make docs + hatch run docs:build - name: Deploy docs to gh-pages working-directory: docs @@ -36,4 +34,4 @@ jobs: git fetch origin gh-pages --depth=1 git config user.name github-actions[bot] git config user.email 41898282+github-actions[bot]@users.noreply.github.com - mike deploy --push ~latest --title=latest + hatch run docs:mike deploy --push ~latest --title=latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 934d4ff..f5e673f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,21 +20,19 @@ jobs: cache: "pip" cache-dependency-path: | pyproject.toml - requirements-dev.txt - - name: Install dependencies + - name: Install hatch run: | - python -m pip install --upgrade pip - pip install -r requirements-dev.txt + pip install hatch - name: Check that versions match id: version run: | echo "Release tag: [${{ github.event.release.tag_name }}]" - PACKAGE_VERSION=$(python -c "import reprexlite; print(reprexlite.__version__)") + PACKAGE_VERSION=$(hatch project metadata | jq -r .version) echo "Package version: [$PACKAGE_VERSION]" [ ${{ github.event.release.tag_name }} == "v$PACKAGE_VERSION" ] || { exit 1; } - echo "::set-output name=major_minor_version::v${PACKAGE_VERSION%.*}" + echo "major_minor_version=v${PACKAGE_VERSION%.*}" >> $GITHUB_OUTPUT - name: Build package run: | @@ -61,6 +59,7 @@ jobs: git fetch origin gh-pages --depth=1 git config user.name github-actions[bot] git config user.email 41898282+github-actions[bot]@users.noreply.github.com + hatch shell docs # Rename old stable version mike list -j | jq OLD_STABLE=$(mike list -j | jq -r '.[] | select(.aliases | index("stable")) | .title' | awk '{print $1;}') diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e497a73..d64b24d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,7 +30,7 @@ jobs: - name: Lint package run: | - hatch run lint + hatch run lint:lint tests: name: "Tests (${{ matrix.os }}, Python ${{ matrix.python-version }})" From c7b7db6f4045cbac4683fee124490e31a465eb12 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Sat, 25 Mar 2023 13:29:06 -0400 Subject: [PATCH 18/31] Parameterize pygments style --- reprexlite/formatting.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/reprexlite/formatting.py b/reprexlite/formatting.py index d58b702..1bb0019 100644 --- a/reprexlite/formatting.py +++ b/reprexlite/formatting.py @@ -60,7 +60,7 @@ def register_formatter(venue: str, label: str): label (str): Short human-readable label explaining the venue. """ - def registrar(fn): + def registrar(fn: Formatter): global formatter_registry formatter_registry[venue] = {"formatter": fn, "label": label} return fn @@ -109,6 +109,7 @@ class HtmlFormatter: available, the rendered HTML will have syntax highlighting for the Python code.""" no_color: bool + pygments_style: str = "default" def __call__( self, reprex_str: str, advertise: Optional[bool] = None, session_info: bool = False @@ -121,7 +122,7 @@ def __call__( out.append(f'
{reprex_str}
') else: formatter = pygments.formatters.HtmlFormatter( - lineanchors=True, linenos=True, wrapcode=True + lineanchors=True, linenos=True, wrapcode=True, style=self.pygments_style ) out.append(f"") out.append(highlight(str(reprex_str), PythonLexer(), formatter)) From 3588e027124f35d6b46b36d42807a324d6c1579e Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Sat, 25 Mar 2023 13:29:20 -0400 Subject: [PATCH 19/31] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5e646a..8c859ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,7 +45,7 @@ This release involves major changes to reprexlite. There is a significant refact #### Changed - Changed formatting abstractions in `reprexlite.formatting` module. - - Rather than `*Reprex` classes that encapsulate reprex data, we now have `*Formatter` classes and take a rendered reprex output string as input to a `format` class method that appropriately prepares the reprex output for a venue, such as adding venue-specific markup. + - Rather than `*Reprex` classes that encapsulate reprex data, we now have simple formatter callables that take a rendered reprex output string as input and appropriately prepares the reprex output for a venue, such as adding venue-specific markup. - The `venues_dispatcher` dictionary in `reprexlite.formatting` is now a `formatter_registry` dictionary. - Formatters are added to the registry using a `register_formatter` decorator instead of being hard-coded. From 4eb5159cab70ad27d3f8fc01dab1e47a19066a8e Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Sat, 25 Mar 2023 14:06:10 -0400 Subject: [PATCH 20/31] Fix typecheck --- reprexlite/config.py | 2 +- reprexlite/formatting.py | 20 ++++++++++++++------ reprexlite/reprexes.py | 2 +- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/reprexlite/config.py b/reprexlite/config.py index 9c37d54..f3074d8 100644 --- a/reprexlite/config.py +++ b/reprexlite/config.py @@ -30,7 +30,7 @@ class ReprexConfig: # Editor editor: Optional[str] = field(default=None, metadata={"help": "..."}) - no_color: Optional[str] = 0 + no_color: Optional[bool] = None # Formatting venue: str = field( diff --git a/reprexlite/formatting.py b/reprexlite/formatting.py index 1bb0019..e0175ba 100644 --- a/reprexlite/formatting.py +++ b/reprexlite/formatting.py @@ -1,11 +1,11 @@ import dataclasses from datetime import datetime -from typing import Dict, Optional +from typing import Dict, NamedTuple, Optional try: from typing import Protocol except ImportError: - from typing_extensions import Protocol + from typing_extensions import Protocol # type: ignore[assignment] try: from pygments import highlight @@ -48,12 +48,20 @@ def __call__( """ -formatter_registry: Dict[str, Formatter] = {} -"""Registry of formatters keyed by venue keywords.""" +class FormatterRegistration(NamedTuple): + """Named tuple that contains a reference to a venue formatter callable and a human-readable + label.""" + + formatter: Formatter + label: str + + +formatter_registry: Dict[str, FormatterRegistration] = {} +"""Registry of venue formatters keyed by venue keywords.""" def register_formatter(venue: str, label: str): - """Decorator that registers a formatter implementation. + """Decorator that registers a venue formatter implementation to a venue keyword. Args: venue (str): Venue keyword that formatter will be registered to. @@ -62,7 +70,7 @@ def register_formatter(venue: str, label: str): def registrar(fn: Formatter): global formatter_registry - formatter_registry[venue] = {"formatter": fn, "label": label} + formatter_registry[venue] = FormatterRegistration(formatter=fn, label=label) return fn return registrar diff --git a/reprexlite/reprexes.py b/reprexlite/reprexes.py index f2aaf0b..2267a74 100644 --- a/reprexlite/reprexes.py +++ b/reprexlite/reprexes.py @@ -420,7 +420,7 @@ def format(self, terminal: bool = False) -> str: out = highlight(out, PythonLexer(), Terminal256Formatter(style="friendly")) except ModuleNotFoundError: pass - formatter = formatter_registry[self.config.venue]["formatter"] + formatter = formatter_registry[self.config.venue].formatter return formatter( out.strip(), advertise=self.config.advertise, session_info=self.config.session_info ) From 9ee9fbfd601f2238a15436babccb8e8a6599923f Mon Sep 17 00:00:00 2001 From: Jay Qi <2721979+jayqi@users.noreply.github.com> Date: Sun, 26 Mar 2023 22:00:30 -0400 Subject: [PATCH 21/31] Use rich for printing (#76) Co-authored-by: Jay Qi --- pyproject.toml | 7 ++-- reprexlite/cli.py | 9 +++-- reprexlite/exceptions.py | 8 ++--- reprexlite/formatting.py | 15 +++----- reprexlite/ipython.py | 2 +- reprexlite/printing.py | 67 ++++++++++++++++++++++++++++++++++++ reprexlite/reprexes.py | 36 +++++++++---------- tests/test_ipython_magics.py | 2 +- tests/test_reprexes.py | 7 ---- 9 files changed, 105 insertions(+), 48 deletions(-) create mode 100644 reprexlite/printing.py diff --git a/pyproject.toml b/pyproject.toml index 7406729..4e8f783 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,8 +31,9 @@ dependencies = [ [project.optional-dependencies] black = ["black"] -pygments = ["Pygments"] ipython = ["ipython"] +pygments = ["Pygments"] +rich = ["rich"] tests = ["pytest"] [project.scripts] @@ -50,7 +51,7 @@ exclude = ["docs/"] ## DEFAULT ENVIRONMENT ## [tool.hatch.envs.default] -features = ["black", "pygments", "ipython", "tests"] +features = ["black", "pygments", "ipython", "rich", "tests"] dependencies = [ "tqdm", # For script to regenerate test assets ] @@ -79,7 +80,7 @@ python = ["3.7", "3.8", "3.9", "3.10", "3.11"] [[tool.hatch.envs.tests.matrix]] python = ["3.10"] -extras = ["black", "ipython", "pygments"] +extras = ["black", "ipython", "pygments", "rich"] [tool.hatch.envs.tests.overrides] matrix.extras.features = [ diff --git a/reprexlite/cli.py b/reprexlite/cli.py index afc323f..f5ec501 100644 --- a/reprexlite/cli.py +++ b/reprexlite/cli.py @@ -4,6 +4,11 @@ import typer +try: + from rich import print +except ModuleNotFoundError: + pass + from reprexlite.config import ParsingMethod, ReprexConfig from reprexlite.exceptions import InputSyntaxError, IPythonNotFoundError from reprexlite.formatting import formatter_registry @@ -172,10 +177,10 @@ def main( if outfile: with outfile.open("w") as fp: - fp.write(r.format(terminal=False)) + fp.write(r.format()) print(f"Wrote rendered reprex to {outfile}") else: - print(r.format(terminal=True), end="") + r.print(end="") return r diff --git a/reprexlite/exceptions.py b/reprexlite/exceptions.py index cb961c2..6e5b696 100644 --- a/reprexlite/exceptions.py +++ b/reprexlite/exceptions.py @@ -26,10 +26,6 @@ class IPythonNotFoundError(ModuleNotFoundError, ReprexliteException): """Raised when ipython cannot be found when using an IPython-dependent feature.""" -class MissingDependencyError(ImportError, ReprexliteException): - pass - - class NoPrefixMatchError(ValueError, ReprexliteException): pass @@ -42,6 +38,10 @@ class PygmentsNotFoundError(ModuleNotFoundError, ReprexliteException): """Raised when pygments cannot be found when using a pygments-dependent feature.""" +class RichNotFoundError(ModuleNotFoundError, ReprexliteException): + """Raised when rich cannot be found when using a rich-dependent feature.""" + + class UnexpectedError(ReprexliteException): """Raised when an unexpected case happens.""" diff --git a/reprexlite/formatting.py b/reprexlite/formatting.py index e0175ba..b1210e4 100644 --- a/reprexlite/formatting.py +++ b/reprexlite/formatting.py @@ -12,24 +12,19 @@ import pygments.formatters from pygments.lexers import PythonLexer - PYGMENTS_AVAILABLE = True + PYGMENTS_IS_AVAILABLE = True except ModuleNotFoundError as e: if e.name == "pygments": - PYGMENTS_AVAILABLE = False + PYGMENTS_IS_AVAILABLE = False else: raise + from reprexlite.exceptions import PygmentsNotFoundError from reprexlite.session_info import SessionInfo from reprexlite.version import __version__ -@dataclasses.dataclass -class FormatterMetadata: - example: Optional[str] - venues: Dict[str, str] = dataclasses.field(default_factory=lambda: dict()) - - class Formatter(Protocol): def __call__( self, reprex_str: str, advertise: Optional[bool] = None, session_info: bool = False @@ -126,7 +121,7 @@ def __call__( advertise = True out = [] - if self.no_color or not PYGMENTS_AVAILABLE: + if self.no_color or not PYGMENTS_IS_AVAILABLE: out.append(f'
{reprex_str}
') else: formatter = pygments.formatters.HtmlFormatter( @@ -168,7 +163,7 @@ def format_python( def format_rtf( reprex_str: str, advertise: Optional[bool] = None, session_info: bool = False ) -> str: - if not PYGMENTS_AVAILABLE: + if not PYGMENTS_IS_AVAILABLE: raise PygmentsNotFoundError("Pygments is required for RTF output.", name="pygments") if advertise is None: diff --git a/reprexlite/ipython.py b/reprexlite/ipython.py index 6a053c8..8a491e4 100644 --- a/reprexlite/ipython.py +++ b/reprexlite/ipython.py @@ -86,7 +86,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.format(terminal=True), end="") + r.print(end="") except Exception as e: print("ERROR: reprexlite has encountered an error while evaluating your input.") print(e, end="") diff --git a/reprexlite/printing.py b/reprexlite/printing.py new file mode 100644 index 0000000..b3f6f4f --- /dev/null +++ b/reprexlite/printing.py @@ -0,0 +1,67 @@ +from typing import Dict + +try: + from typing import Protocol +except ImportError: + from typing_extensions import Protocol # type: ignore[assignment] + + +try: + from rich.console import Console + from rich.syntax import Syntax + + RICH_IS_AVAILABLE = True + console = Console(soft_wrap=True) +except ModuleNotFoundError as e: + if e.name == "rich": + RICH_IS_AVAILABLE = False + else: + raise + + +from reprexlite.exceptions import RichNotFoundError + + +class Printer(Protocol): + def __call__(self, formatted_reprex: str, **kwargs): + pass + + +printer_registry: Dict[str, Printer] = {} +"""Registry of venue formatters keyed by venue keywords.""" + + +def register_printer(venue: str): + def registrar(fn: Printer): + global formatter_registry + printer_registry[venue] = fn + return fn + + return registrar + + +@register_printer("ds") +@register_printer("so") +@register_printer("gh") +def print_markdown(formatted_reprex: str, **kwargs): + if RICH_IS_AVAILABLE: + console.print(Syntax(formatted_reprex, "markdown", theme="ansi_dark"), **kwargs) + else: + raise RichNotFoundError + + +@register_printer("htmlnocolor") +@register_printer("html") +def print_html(formatted_reprex: str, **kwargs): + if RICH_IS_AVAILABLE: + console.print(Syntax(formatted_reprex, "html", theme="ansi_dark"), **kwargs) + else: + raise RichNotFoundError + + +@register_printer("py") +def print_python_code(formatted_reprex: str, **kwargs): + if RICH_IS_AVAILABLE: + console.print(Syntax(formatted_reprex, "python", theme="ansi_dark"), **kwargs) + else: + raise RichNotFoundError diff --git a/reprexlite/reprexes.py b/reprexlite/reprexes.py index 2267a74..7d136ba 100644 --- a/reprexlite/reprexes.py +++ b/reprexlite/reprexes.py @@ -16,9 +16,15 @@ import libcst as cst from reprexlite.config import ParsingMethod, ReprexConfig -from reprexlite.exceptions import BlackNotFoundError, InputSyntaxError, UnexpectedError +from reprexlite.exceptions import ( + BlackNotFoundError, + InputSyntaxError, + RichNotFoundError, + UnexpectedError, +) from reprexlite.formatting import formatter_registry from reprexlite.parsing import LineType, auto_parse, parse +from reprexlite.printing import printer_registry @dataclasses.dataclass @@ -409,22 +415,19 @@ def results_match(self) -> bool: result == old_result for result, old_result in zip(self.results, self.old_results) ) - def format(self, terminal: bool = False) -> str: + def format(self) -> str: out = str(self) - if terminal: - try: - from pygments import highlight - from pygments.formatters import Terminal256Formatter - from pygments.lexers import PythonLexer - - out = highlight(out, PythonLexer(), Terminal256Formatter(style="friendly")) - except ModuleNotFoundError: - pass formatter = formatter_registry[self.config.venue].formatter return formatter( out.strip(), advertise=self.config.advertise, session_info=self.config.session_info ) + def print(self, **kwargs) -> None: + try: + printer_registry[self.config.venue](self.format(), **kwargs) + except (KeyError, RichNotFoundError): + print(self.format(), **kwargs) + def __repr__(self) -> str: return f"" @@ -455,7 +458,6 @@ def reprex( input: str, outfile: Optional[Union[str, os.PathLike]] = None, print_: bool = True, - terminal: bool = False, config: Optional[ReprexConfig] = None, **kwargs, ) -> Reprex: @@ -484,8 +486,6 @@ def reprex( outfile (Optional[str | os.PathLike]): If provided, path to write formatted reprex output to. Defaults to None, which does not write to any file. print_ (bool): Whether to print formatted reprex output to console. - terminal (bool): Whether currently in a terminal. If true, will automatically apply code - highlighting if pygments is installed. config (Optional[ReprexConfig]): Instance of the configuration dataclass. Default of none will instantiate one with default values. **kwargs: Configuration options from [ReprexConfig][reprexlite.config.ReprexConfig]. Any @@ -501,14 +501,10 @@ def reprex( config = dataclasses.replace(config, **kwargs) config = ReprexConfig(**kwargs) - if config.venue in ["html", "rtf"]: - # 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.format(terminal=terminal) if outfile is not None: with Path(outfile).open("w") as fp: - fp.write(r.format(terminal=False)) + fp.write(r.format()) if print_: - print(output) + r.print() return r diff --git a/tests/test_ipython_magics.py b/tests/test_ipython_magics.py index 316ae95..fd8237e 100644 --- a/tests/test_ipython_magics.py +++ b/tests/test_ipython_magics.py @@ -54,7 +54,7 @@ def test_cell_magic(ipython, capsys): captured = capsys.readouterr() r = Reprex.from_input(input, config=ReprexConfig(advertise=False, session_info=True)) - expected = r.format(terminal=True) + expected = r.format() print("\n---EXPECTED---\n") print(expected) diff --git a/tests/test_reprexes.py b/tests/test_reprexes.py index ee975ec..f3db478 100644 --- a/tests/test_reprexes.py +++ b/tests/test_reprexes.py @@ -721,13 +721,6 @@ def test_black_bad_dependency(black_bad_dependency, monkeypatch): assert exc_info.value.name == black_bad_dependency -@requires_no_pygments -def test_no_pygments_terminal(): - """Test that format for terminal works even if pygments is not installed.""" - r = Reprex.from_input("2+2") - assert_str_equals(r.format(terminal=False), r.format(terminal=True)) - - def test_repr_html(): """Test rich HTML display for Jupyter Notebooks runs without error.""" r = Reprex.from_input("2+2") From 42dc9eb83d190d07183a85ec1cb61f9f47f3224d Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Sun, 26 Mar 2023 22:32:29 -0400 Subject: [PATCH 22/31] Fix test matrix --- .github/workflows/tests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d64b24d..ce9a91a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,6 +40,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: [3.7, 3.8, 3.9, "3.10", "3.11"] + extra: [""] include: - os: ubuntu-latest python-version: "3.10" @@ -50,6 +51,9 @@ jobs: - os: ubuntu-latest python-version: "3.10" extra: "-pygments" + - os: ubuntu-latest + python-version: "3.10" + extra: "-rich" steps: - uses: actions/checkout@v3 From 74e3f22ebbfec11b31adba165caadf00106db549 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Sun, 26 Mar 2023 22:38:40 -0400 Subject: [PATCH 23/31] Include extra in build name --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ce9a91a..da26a7a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,7 +33,7 @@ jobs: hatch run lint:lint tests: - name: "Tests (${{ matrix.os }}, Python ${{ matrix.python-version }})" + name: "Tests (${{ matrix.os }}, Python ${{ matrix.python-version }}, Extra ${{ extra }})" needs: code-quality runs-on: ${{ matrix.os }} strategy: From 173379e54666ccbf0b9535f4c81117654327db96 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Sun, 26 Mar 2023 23:11:43 -0400 Subject: [PATCH 24/31] Add tests --- reprexlite/cli.py | 15 +++++++++++---- reprexlite/printing.py | 9 +++++++-- tests/test_cli.py | 12 ++++++++++++ tests/test_ipython_editor.py | 17 +++++++++++++++++ tests/test_reprexes.py | 7 ++++++- 5 files changed, 53 insertions(+), 7 deletions(-) diff --git a/reprexlite/cli.py b/reprexlite/cli.py index f5ec501..c45e33e 100644 --- a/reprexlite/cli.py +++ b/reprexlite/cli.py @@ -5,9 +5,16 @@ import typer try: - from rich import print -except ModuleNotFoundError: - pass + from rich.console import Console + + console = Console(soft_wrap=True) + print = console.print +except ModuleNotFoundError as e: + if e.name == "rich": + pass + else: + raise + from reprexlite.config import ParsingMethod, ReprexConfig from reprexlite.exceptions import InputSyntaxError, IPythonNotFoundError @@ -178,7 +185,7 @@ def main( if outfile: with outfile.open("w") as fp: fp.write(r.format()) - print(f"Wrote rendered reprex to {outfile}") + print(f"Wrote rendered reprex to {outfile}", no_wrap=True) else: r.print(end="") diff --git a/reprexlite/printing.py b/reprexlite/printing.py index b3f6f4f..3bc3489 100644 --- a/reprexlite/printing.py +++ b/reprexlite/printing.py @@ -24,11 +24,16 @@ class Printer(Protocol): def __call__(self, formatted_reprex: str, **kwargs): - pass + """Print a formatted reprex, possibly with colors. + + Args: + formatted_reprex (str): Formatted reprex. + **kwargs: Arguments passed to print(). + """ printer_registry: Dict[str, Printer] = {} -"""Registry of venue formatters keyed by venue keywords.""" +"""Registry of venue printers keyed by venue keywords.""" def register_printer(venue: str): diff --git a/tests/test_cli.py b/tests/test_cli.py index 0b43a9a..be41478 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -108,6 +108,18 @@ def test_ipython_editor_not_installed(): assert "ipython is required" in result.stdout +def test_syntax_error(patch_edit): + patch_edit.input = dedent( + """\ + 2+ + """ + ) + result = runner.invoke(app) + assert result.exit_code == 1 + assert "Syntax Error" in result.stdout + assert "Incomplete input." in result.stdout + + def test_help(): """Test the CLI with --help flag.""" result = runner.invoke(app, ["--help"]) diff --git a/tests/test_ipython_editor.py b/tests/test_ipython_editor.py index 3b1da48..cfe5cd3 100644 --- a/tests/test_ipython_editor.py +++ b/tests/test_ipython_editor.py @@ -71,6 +71,23 @@ def test_ipython_editor(reprexlite_ipython, capsys): assert remove_ansi_escape(captured.out) == expected +@requires_ipython +def test_ipython_syntax_error(reprexlite_ipython, capsys): + input = dedent( + """\ + 2+ + """ + ) + reprexlite_ipython.run_cell(input) + captured = capsys.readouterr() + + print("\n---ACTUAL-----\n") + print(captured.out) + print("\n--------------\n") + assert "Syntax Error" in remove_ansi_escape(captured.out) + assert "Incomplete input." in remove_ansi_escape(captured.out) + + @requires_no_ipython def test_no_ipython_error(monkeypatch): with pytest.raises(IPythonNotFoundError): diff --git a/tests/test_reprexes.py b/tests/test_reprexes.py index f3db478..416a8b1 100644 --- a/tests/test_reprexes.py +++ b/tests/test_reprexes.py @@ -5,7 +5,7 @@ import pytest from reprexlite.config import ReprexConfig -from reprexlite.exceptions import BlackNotFoundError, UnexpectedError +from reprexlite.exceptions import BlackNotFoundError, InputSyntaxError, UnexpectedError from reprexlite.reprexes import ParsedResult, RawResult, Reprex, reprex from tests.utils import ( assert_equals, @@ -735,6 +735,11 @@ def test_repr_html_no_pygments(): r._repr_html_() +def test_syntax_error(): + with pytest.raises(InputSyntaxError): + Reprex.from_input("2+") + + def test_reprex_init_bad_lengths_error(): r = Reprex.from_input("2+2") with pytest.raises(UnexpectedError): From c10498fae07519859fbc304f106b3e279b1c9ea5 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Mon, 27 Mar 2023 00:08:42 -0400 Subject: [PATCH 25/31] Fix workflow --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index da26a7a..66fe3d1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,7 +33,7 @@ jobs: hatch run lint:lint tests: - name: "Tests (${{ matrix.os }}, Python ${{ matrix.python-version }}, Extra ${{ extra }})" + name: "Tests (${{ matrix.os }}, Python ${{ matrix.python-version }}, Extra ${{ matrix.extra }})" needs: code-quality runs-on: ${{ matrix.os }} strategy: From ea550460bf9e8effb7eb4d7a99dfc5f7c21cca93 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Mon, 27 Mar 2023 00:14:38 -0400 Subject: [PATCH 26/31] Don't use no_wrap --- reprexlite/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reprexlite/cli.py b/reprexlite/cli.py index c45e33e..7b07f03 100644 --- a/reprexlite/cli.py +++ b/reprexlite/cli.py @@ -185,7 +185,7 @@ def main( if outfile: with outfile.open("w") as fp: fp.write(r.format()) - print(f"Wrote rendered reprex to {outfile}", no_wrap=True) + print(f"Wrote rendered reprex to {outfile}") else: r.print(end="") From faebb308f46374bc32b5af738e18e368a52f60cb Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Mon, 27 Mar 2023 13:48:44 -0400 Subject: [PATCH 27/31] Printing tests --- pyproject.toml | 3 + reprexlite/config.py | 7 +- reprexlite/reprexes.py | 9 +- tests/assets/{ => formatted}/ad/ds.md | 0 tests/assets/{ => formatted}/ad/gh.md | 0 tests/assets/{ => formatted}/ad/html.html | 0 .../{ => formatted}/ad/htmlnocolor.html | 0 tests/assets/{ => formatted}/ad/py.py | 0 tests/assets/{ => formatted}/ad/rtf.rtf | 0 tests/assets/{ => formatted}/ad/slack.txt | 0 tests/assets/{ => formatted}/ad/so.md | 0 tests/assets/{ => formatted}/ds.md | 0 tests/assets/{ => formatted}/gh.md | 0 tests/assets/{ => formatted}/html.html | 0 tests/assets/{ => formatted}/htmlnocolor.html | 0 tests/assets/{ => formatted}/no_ad/ds.md | 0 tests/assets/{ => formatted}/no_ad/gh.md | 0 tests/assets/{ => formatted}/no_ad/html.html | 0 .../{ => formatted}/no_ad/htmlnocolor.html | 0 tests/assets/{ => formatted}/no_ad/py.py | 0 tests/assets/{ => formatted}/no_ad/rtf.rtf | 0 tests/assets/{ => formatted}/no_ad/slack.txt | 0 tests/assets/{ => formatted}/no_ad/so.md | 0 tests/assets/{ => formatted}/py.py | 0 tests/assets/{ => formatted}/rtf.rtf | 0 .../assets/{ => formatted}/session_info/ds.md | 0 .../assets/{ => formatted}/session_info/gh.md | 0 .../{ => formatted}/session_info/html.html | 0 .../session_info/htmlnocolor.html | 0 .../assets/{ => formatted}/session_info/py.py | 0 .../{ => formatted}/session_info/rtf.rtf | 0 .../{ => formatted}/session_info/slack.txt | 0 .../assets/{ => formatted}/session_info/so.md | 0 tests/assets/{ => formatted}/slack.txt | 0 tests/assets/{ => formatted}/so.md | 0 tests/assets/printed/ds.md | 8 ++ tests/assets/printed/gh.md | 8 ++ tests/assets/printed/htmlnocolor.html | 5 + tests/assets/printed/no_color/gh.md | 8 ++ .../assets/printed/no_color/htmlnocolor.html | 5 + tests/assets/printed/no_color/py.py | 4 + tests/assets/printed/no_color/slack.txt | 6 ++ tests/assets/printed/py.py | 4 + tests/assets/printed/slack.txt | 6 ++ tests/assets/printed/so.md | 8 ++ tests/conftest.py | 10 ++ tests/expected_formatted.py | 86 +---------------- tests/expected_printed.py | 63 +++++++++++++ tests/pytest_utils.py | 22 +++++ tests/test_cli.py | 3 +- tests/test_formatting.py | 9 +- tests/test_ipython_editor.py | 3 +- tests/test_ipython_magics.py | 2 +- tests/test_printing.py | 93 +++++++++++++++++++ tests/test_reprexes.py | 10 +- tests/utils.py | 83 +++++++++++++---- 56 files changed, 348 insertions(+), 117 deletions(-) rename tests/assets/{ => formatted}/ad/ds.md (100%) rename tests/assets/{ => formatted}/ad/gh.md (100%) rename tests/assets/{ => formatted}/ad/html.html (100%) rename tests/assets/{ => formatted}/ad/htmlnocolor.html (100%) rename tests/assets/{ => formatted}/ad/py.py (100%) rename tests/assets/{ => formatted}/ad/rtf.rtf (100%) rename tests/assets/{ => formatted}/ad/slack.txt (100%) rename tests/assets/{ => formatted}/ad/so.md (100%) rename tests/assets/{ => formatted}/ds.md (100%) rename tests/assets/{ => formatted}/gh.md (100%) rename tests/assets/{ => formatted}/html.html (100%) rename tests/assets/{ => formatted}/htmlnocolor.html (100%) rename tests/assets/{ => formatted}/no_ad/ds.md (100%) rename tests/assets/{ => formatted}/no_ad/gh.md (100%) rename tests/assets/{ => formatted}/no_ad/html.html (100%) rename tests/assets/{ => formatted}/no_ad/htmlnocolor.html (100%) rename tests/assets/{ => formatted}/no_ad/py.py (100%) rename tests/assets/{ => formatted}/no_ad/rtf.rtf (100%) rename tests/assets/{ => formatted}/no_ad/slack.txt (100%) rename tests/assets/{ => formatted}/no_ad/so.md (100%) rename tests/assets/{ => formatted}/py.py (100%) rename tests/assets/{ => formatted}/rtf.rtf (100%) rename tests/assets/{ => formatted}/session_info/ds.md (100%) rename tests/assets/{ => formatted}/session_info/gh.md (100%) rename tests/assets/{ => formatted}/session_info/html.html (100%) rename tests/assets/{ => formatted}/session_info/htmlnocolor.html (100%) rename tests/assets/{ => formatted}/session_info/py.py (100%) rename tests/assets/{ => formatted}/session_info/rtf.rtf (100%) rename tests/assets/{ => formatted}/session_info/slack.txt (100%) rename tests/assets/{ => formatted}/session_info/so.md (100%) rename tests/assets/{ => formatted}/slack.txt (100%) rename tests/assets/{ => formatted}/so.md (100%) create mode 100644 tests/assets/printed/ds.md create mode 100644 tests/assets/printed/gh.md create mode 100644 tests/assets/printed/htmlnocolor.html create mode 100644 tests/assets/printed/no_color/gh.md create mode 100644 tests/assets/printed/no_color/htmlnocolor.html create mode 100644 tests/assets/printed/no_color/py.py create mode 100644 tests/assets/printed/no_color/slack.txt create mode 100644 tests/assets/printed/py.py create mode 100644 tests/assets/printed/slack.txt create mode 100644 tests/assets/printed/so.md create mode 100644 tests/expected_printed.py create mode 100644 tests/pytest_utils.py create mode 100644 tests/test_printing.py diff --git a/pyproject.toml b/pyproject.toml index 4e8f783..dd48702 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,6 +93,9 @@ matrix.extras.features = [ { value = "pygments", if = [ "pygments", ] }, + { value = "rich", if = [ + "rich", + ] }, ] [tool.hatch.envs.tests.scripts] diff --git a/reprexlite/config.py b/reprexlite/config.py index f3074d8..7e8a395 100644 --- a/reprexlite/config.py +++ b/reprexlite/config.py @@ -30,7 +30,12 @@ class ReprexConfig: # Editor editor: Optional[str] = field(default=None, metadata={"help": "..."}) - no_color: Optional[bool] = None + no_color: bool = field( + default=False, + metadata={ + "help": ("If True, will disable the colored text output when printing to stdout.") + }, + ) # Formatting venue: str = field( diff --git a/reprexlite/reprexes.py b/reprexlite/reprexes.py index 7d136ba..133f0cd 100644 --- a/reprexlite/reprexes.py +++ b/reprexlite/reprexes.py @@ -423,10 +423,13 @@ def format(self) -> str: ) def print(self, **kwargs) -> None: - try: - printer_registry[self.config.venue](self.format(), **kwargs) - except (KeyError, RichNotFoundError): + if self.config.no_color: print(self.format(), **kwargs) + else: + try: + printer_registry[self.config.venue](self.format(), **kwargs) + except (KeyError, RichNotFoundError): + print(self.format(), **kwargs) def __repr__(self) -> str: return f"" diff --git a/tests/assets/ad/ds.md b/tests/assets/formatted/ad/ds.md similarity index 100% rename from tests/assets/ad/ds.md rename to tests/assets/formatted/ad/ds.md diff --git a/tests/assets/ad/gh.md b/tests/assets/formatted/ad/gh.md similarity index 100% rename from tests/assets/ad/gh.md rename to tests/assets/formatted/ad/gh.md diff --git a/tests/assets/ad/html.html b/tests/assets/formatted/ad/html.html similarity index 100% rename from tests/assets/ad/html.html rename to tests/assets/formatted/ad/html.html diff --git a/tests/assets/ad/htmlnocolor.html b/tests/assets/formatted/ad/htmlnocolor.html similarity index 100% rename from tests/assets/ad/htmlnocolor.html rename to tests/assets/formatted/ad/htmlnocolor.html diff --git a/tests/assets/ad/py.py b/tests/assets/formatted/ad/py.py similarity index 100% rename from tests/assets/ad/py.py rename to tests/assets/formatted/ad/py.py diff --git a/tests/assets/ad/rtf.rtf b/tests/assets/formatted/ad/rtf.rtf similarity index 100% rename from tests/assets/ad/rtf.rtf rename to tests/assets/formatted/ad/rtf.rtf diff --git a/tests/assets/ad/slack.txt b/tests/assets/formatted/ad/slack.txt similarity index 100% rename from tests/assets/ad/slack.txt rename to tests/assets/formatted/ad/slack.txt diff --git a/tests/assets/ad/so.md b/tests/assets/formatted/ad/so.md similarity index 100% rename from tests/assets/ad/so.md rename to tests/assets/formatted/ad/so.md diff --git a/tests/assets/ds.md b/tests/assets/formatted/ds.md similarity index 100% rename from tests/assets/ds.md rename to tests/assets/formatted/ds.md diff --git a/tests/assets/gh.md b/tests/assets/formatted/gh.md similarity index 100% rename from tests/assets/gh.md rename to tests/assets/formatted/gh.md diff --git a/tests/assets/html.html b/tests/assets/formatted/html.html similarity index 100% rename from tests/assets/html.html rename to tests/assets/formatted/html.html diff --git a/tests/assets/htmlnocolor.html b/tests/assets/formatted/htmlnocolor.html similarity index 100% rename from tests/assets/htmlnocolor.html rename to tests/assets/formatted/htmlnocolor.html diff --git a/tests/assets/no_ad/ds.md b/tests/assets/formatted/no_ad/ds.md similarity index 100% rename from tests/assets/no_ad/ds.md rename to tests/assets/formatted/no_ad/ds.md diff --git a/tests/assets/no_ad/gh.md b/tests/assets/formatted/no_ad/gh.md similarity index 100% rename from tests/assets/no_ad/gh.md rename to tests/assets/formatted/no_ad/gh.md diff --git a/tests/assets/no_ad/html.html b/tests/assets/formatted/no_ad/html.html similarity index 100% rename from tests/assets/no_ad/html.html rename to tests/assets/formatted/no_ad/html.html diff --git a/tests/assets/no_ad/htmlnocolor.html b/tests/assets/formatted/no_ad/htmlnocolor.html similarity index 100% rename from tests/assets/no_ad/htmlnocolor.html rename to tests/assets/formatted/no_ad/htmlnocolor.html diff --git a/tests/assets/no_ad/py.py b/tests/assets/formatted/no_ad/py.py similarity index 100% rename from tests/assets/no_ad/py.py rename to tests/assets/formatted/no_ad/py.py diff --git a/tests/assets/no_ad/rtf.rtf b/tests/assets/formatted/no_ad/rtf.rtf similarity index 100% rename from tests/assets/no_ad/rtf.rtf rename to tests/assets/formatted/no_ad/rtf.rtf diff --git a/tests/assets/no_ad/slack.txt b/tests/assets/formatted/no_ad/slack.txt similarity index 100% rename from tests/assets/no_ad/slack.txt rename to tests/assets/formatted/no_ad/slack.txt diff --git a/tests/assets/no_ad/so.md b/tests/assets/formatted/no_ad/so.md similarity index 100% rename from tests/assets/no_ad/so.md rename to tests/assets/formatted/no_ad/so.md diff --git a/tests/assets/py.py b/tests/assets/formatted/py.py similarity index 100% rename from tests/assets/py.py rename to tests/assets/formatted/py.py diff --git a/tests/assets/rtf.rtf b/tests/assets/formatted/rtf.rtf similarity index 100% rename from tests/assets/rtf.rtf rename to tests/assets/formatted/rtf.rtf diff --git a/tests/assets/session_info/ds.md b/tests/assets/formatted/session_info/ds.md similarity index 100% rename from tests/assets/session_info/ds.md rename to tests/assets/formatted/session_info/ds.md diff --git a/tests/assets/session_info/gh.md b/tests/assets/formatted/session_info/gh.md similarity index 100% rename from tests/assets/session_info/gh.md rename to tests/assets/formatted/session_info/gh.md diff --git a/tests/assets/session_info/html.html b/tests/assets/formatted/session_info/html.html similarity index 100% rename from tests/assets/session_info/html.html rename to tests/assets/formatted/session_info/html.html diff --git a/tests/assets/session_info/htmlnocolor.html b/tests/assets/formatted/session_info/htmlnocolor.html similarity index 100% rename from tests/assets/session_info/htmlnocolor.html rename to tests/assets/formatted/session_info/htmlnocolor.html diff --git a/tests/assets/session_info/py.py b/tests/assets/formatted/session_info/py.py similarity index 100% rename from tests/assets/session_info/py.py rename to tests/assets/formatted/session_info/py.py diff --git a/tests/assets/session_info/rtf.rtf b/tests/assets/formatted/session_info/rtf.rtf similarity index 100% rename from tests/assets/session_info/rtf.rtf rename to tests/assets/formatted/session_info/rtf.rtf diff --git a/tests/assets/session_info/slack.txt b/tests/assets/formatted/session_info/slack.txt similarity index 100% rename from tests/assets/session_info/slack.txt rename to tests/assets/formatted/session_info/slack.txt diff --git a/tests/assets/session_info/so.md b/tests/assets/formatted/session_info/so.md similarity index 100% rename from tests/assets/session_info/so.md rename to tests/assets/formatted/session_info/so.md diff --git a/tests/assets/slack.txt b/tests/assets/formatted/slack.txt similarity index 100% rename from tests/assets/slack.txt rename to tests/assets/formatted/slack.txt diff --git a/tests/assets/so.md b/tests/assets/formatted/so.md similarity index 100% rename from tests/assets/so.md rename to tests/assets/formatted/so.md diff --git a/tests/assets/printed/ds.md b/tests/assets/printed/ds.md new file mode 100644 index 0000000..9159c97 --- /dev/null +++ b/tests/assets/printed/ds.md @@ -0,0 +1,8 @@ +```python +x = 2 +x + 2 +#> 4 +``` + +Created at DATETIME by [reprexlite](https://github.com/jayqi/reprexlite) vVERSION + diff --git a/tests/assets/printed/gh.md b/tests/assets/printed/gh.md new file mode 100644 index 0000000..9159c97 --- /dev/null +++ b/tests/assets/printed/gh.md @@ -0,0 +1,8 @@ +```python +x = 2 +x + 2 +#> 4 +``` + +Created at DATETIME by [reprexlite](https://github.com/jayqi/reprexlite) vVERSION + diff --git a/tests/assets/printed/htmlnocolor.html b/tests/assets/printed/htmlnocolor.html new file mode 100644 index 0000000..79300d9 --- /dev/null +++ b/tests/assets/printed/htmlnocolor.html @@ -0,0 +1,5 @@ +<pre><code class="language-python">x = 2 +x + 2 +#> 4 +<p><sup>Created at DATETIME by <a href="https://github.com/jayqi/reprexlite">reprexlite vVERSION + diff --git a/tests/assets/printed/no_color/gh.md b/tests/assets/printed/no_color/gh.md new file mode 100644 index 0000000..fcfe0a4 --- /dev/null +++ b/tests/assets/printed/no_color/gh.md @@ -0,0 +1,8 @@ +```python +x = 2 +x + 2 +#> 4 +``` + +Created at DATETIME by [reprexlite](https://github.com/jayqi/reprexlite) vVERSION + diff --git a/tests/assets/printed/no_color/htmlnocolor.html b/tests/assets/printed/no_color/htmlnocolor.html new file mode 100644 index 0000000..aafad2d --- /dev/null +++ b/tests/assets/printed/no_color/htmlnocolor.html @@ -0,0 +1,5 @@ +
x = 2
+x + 2
+#> 4
+

Created at DATETIME by reprexlite vVERSION

+ diff --git a/tests/assets/printed/no_color/py.py b/tests/assets/printed/no_color/py.py new file mode 100644 index 0000000..62f0e60 --- /dev/null +++ b/tests/assets/printed/no_color/py.py @@ -0,0 +1,4 @@ +x = 2 +x + 2 +#> 4 + diff --git a/tests/assets/printed/no_color/slack.txt b/tests/assets/printed/no_color/slack.txt new file mode 100644 index 0000000..67c1f39 --- /dev/null +++ b/tests/assets/printed/no_color/slack.txt @@ -0,0 +1,6 @@ +``` +x = 2 +x + 2 +#> 4 +``` + diff --git a/tests/assets/printed/py.py b/tests/assets/printed/py.py new file mode 100644 index 0000000..a3a0efb --- /dev/null +++ b/tests/assets/printed/py.py @@ -0,0 +1,4 @@ +x = 2 +x + 2 +#> 4 + diff --git a/tests/assets/printed/slack.txt b/tests/assets/printed/slack.txt new file mode 100644 index 0000000..67c1f39 --- /dev/null +++ b/tests/assets/printed/slack.txt @@ -0,0 +1,6 @@ +``` +x = 2 +x + 2 +#> 4 +``` + diff --git a/tests/assets/printed/so.md b/tests/assets/printed/so.md new file mode 100644 index 0000000..9159c97 --- /dev/null +++ b/tests/assets/printed/so.md @@ -0,0 +1,8 @@ +```python +x = 2 +x + 2 +#> 4 +``` + +Created at DATETIME by [reprexlite](https://github.com/jayqi/reprexlite) vVERSION + diff --git a/tests/conftest.py b/tests/conftest.py index d30c60c..0a9726a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -34,3 +34,13 @@ def pytest_configure(config): pytest.PYGMENTS_IS_AVAILABLE = False else: raise + + try: + import rich + + pytest.RICH_IS_AVAILABLE = True + except ModuleNotFoundError as e: + if e.name == "rich": + pytest.RICH_IS_AVAILABLE = False + else: + raise diff --git a/tests/expected_formatted.py b/tests/expected_formatted.py index 4d02b0f..130ac35 100644 --- a/tests/expected_formatted.py +++ b/tests/expected_formatted.py @@ -4,19 +4,17 @@ python -m tests.expected_formatted """ -from contextlib import contextmanager from dataclasses import dataclass from itertools import chain from pathlib import Path import shutil -import sys from textwrap import dedent from typing import Any, Dict from reprexlite import reprex -from reprexlite.session_info import Package, SessionInfo +from tests.utils import patch_datetime, patch_session_info, patch_version -ASSETS_DIR = (Path(__file__).parent / "assets").resolve() +ASSETS_DIR = (Path(__file__).parent / "assets" / "formatted").resolve() INPUT = dedent( @@ -82,86 +80,8 @@ class ExpectedReprex: ] -MOCK_VERSION = "VERSION" - - -@contextmanager -def patch_version(): - version = sys.modules["reprexlite.formatting"].__version__ - sys.modules["reprexlite.formatting"].__version__ = MOCK_VERSION - yield - sys.modules["reprexlite.formatting"].__version__ = version - - -class MockDateTime: - @classmethod - def now(cls): - return cls() - - def astimezone(self): - return self - - def strftime(self, format): - return "DATETIME" - - -@contextmanager -def patch_datetime(): - datetime = sys.modules["reprexlite.formatting"].datetime - sys.modules["reprexlite.formatting"].datetime = MockDateTime - yield - sys.modules["reprexlite.formatting"].datetime = datetime - - -class MockPackage(Package): - def __init__(self, name: str, version: str): - self._name = name - self._version = version - - @property - def name(self): - return self._name - - @property - def version(self): - return self._version - - -class MockSessionInfo(SessionInfo): - def __init__(self, *args, **kwargs): - self.python_version = "3.x.y" - self.python_build_date = "Jan 01 2020 03:33:33" - self.os = "GLaDOS" - self.packages = [ - MockPackage("datatable", "1.0"), - MockPackage("ggplot2", "2.0"), - MockPackage("pkgnet", "3.0"), - ] - - -@contextmanager -def patch_session_info(): - sys.modules["reprexlite.formatting"].SessionInfo = MockSessionInfo - yield - sys.modules["reprexlite.formatting"].SessionInfo = SessionInfo - - -# @contextmanager -# def no_pygments(): -# import_orig = builtins.__import__ - -# def mocked_import(name, *args): -# if name.startswith("pygments"): -# raise ModuleNotFoundError(name="pygments") -# return import_orig(name, *args) - -# builtins.__import__ = mocked_import -# yield -# builtins.__import__ = import_orig - - if __name__ == "__main__": - import tqdm + from tqdm import tqdm shutil.rmtree(ASSETS_DIR, ignore_errors=True) with patch_datetime(), patch_version(), patch_session_info(): diff --git a/tests/expected_printed.py b/tests/expected_printed.py new file mode 100644 index 0000000..1951c3d --- /dev/null +++ b/tests/expected_printed.py @@ -0,0 +1,63 @@ +"""This module holds metadata about formatted test cases. It also can be run as a script to +generate expected formatted test assets. + + python -m tests.expected_formatted +""" + +from contextlib import redirect_stdout +from dataclasses import dataclass +from pathlib import Path +import shutil +from textwrap import dedent +from typing import Any, Dict + +from reprexlite import reprex +from tests.utils import patch_datetime, patch_version + +ASSETS_DIR = (Path(__file__).parent / "assets" / "printed").resolve() + + +INPUT = dedent( + """\ + x = 2 + x + 2 + """ +) + + +@dataclass +class ExpectedReprex: + filename: str + kwargs: Dict[str, Any] + + +expected_reprexes = [ + # Defaults + ExpectedReprex("gh.md", {"venue": "gh"}), + ExpectedReprex("so.md", {"venue": "so"}), + ExpectedReprex("ds.md", {"venue": "ds"}), + ExpectedReprex("htmlnocolor.html", {"venue": "htmlnocolor"}), + ExpectedReprex("py.py", {"venue": "py"}), + ExpectedReprex("slack.txt", {"venue": "slack"}), +] +expected_reprexes_no_color = [ + # No Color + ExpectedReprex("no_color/gh.md", {"venue": "gh", "no_color": True}), + ExpectedReprex("no_color/htmlnocolor.html", {"venue": "htmlnocolor", "no_color": True}), + ExpectedReprex("no_color/py.py", {"venue": "py", "no_color": True}), + ExpectedReprex("no_color/slack.txt", {"venue": "slack", "no_color": True}), +] + +if __name__ == "__main__": + import rich + from tqdm import tqdm + + shutil.rmtree(ASSETS_DIR, ignore_errors=True) + with patch_datetime(), patch_version(): + for ereprex in tqdm(expected_reprexes): + outfile = ASSETS_DIR / ereprex.filename + outfile.parent.mkdir(exist_ok=True) + r = reprex(INPUT, **ereprex.kwargs, print_=False) + with outfile.open("w") as fp: + with redirect_stdout(fp): + r.print() diff --git a/tests/pytest_utils.py b/tests/pytest_utils.py new file mode 100644 index 0000000..3b37aae --- /dev/null +++ b/tests/pytest_utils.py @@ -0,0 +1,22 @@ +import pytest + +# Requires invoking with pytest + +## SKIP DECORATORS + +requires_ipython = pytest.mark.skipif( + not pytest.IPYTHON_IS_AVAILABLE, reason="ipython is not available" +) +requires_no_ipython = pytest.mark.skipif( + pytest.IPYTHON_IS_AVAILABLE, reason="ipython is available" +) +requires_black = pytest.mark.skipif(not pytest.BLACK_IS_AVAILABLE, reason="black is not available") +requires_no_black = pytest.mark.skipif(pytest.BLACK_IS_AVAILABLE, reason="black is available") +requires_pygments = pytest.mark.skipif( + not pytest.PYGMENTS_IS_AVAILABLE, reason="pygments is not available" +) +requires_no_pygments = pytest.mark.skipif( + pytest.PYGMENTS_IS_AVAILABLE, reason="pygments is available" +) +requires_rich = pytest.mark.skipif(not pytest.RICH_IS_AVAILABLE, reason="rich is not available") +requires_no_rich = pytest.mark.skipif(pytest.RICH_IS_AVAILABLE, reason="rich is available") diff --git a/tests/test_cli.py b/tests/test_cli.py index be41478..b76f893 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -8,7 +8,8 @@ from reprexlite.cli import app from reprexlite.version import __version__ -from tests.utils import remove_ansi_escape, requires_ipython, requires_no_ipython +from tests.pytest_utils import requires_ipython, requires_no_ipython +from tests.utils import remove_ansi_escape runner = CliRunner() diff --git a/tests/test_formatting.py b/tests/test_formatting.py index 790c6f3..4425446 100644 --- a/tests/test_formatting.py +++ b/tests/test_formatting.py @@ -8,13 +8,16 @@ from tests.expected_formatted import ( ASSETS_DIR, INPUT, + expected_reprexes, + expected_reprexes_requires_pygments, +) +from tests.pytest_utils import requires_no_pygments, requires_pygments +from tests.utils import ( MOCK_VERSION, MockDateTime, MockSessionInfo, - expected_reprexes, - expected_reprexes_requires_pygments, + assert_str_equals, ) -from tests.utils import assert_str_equals, requires_no_pygments, requires_pygments @pytest.fixture diff --git a/tests/test_ipython_editor.py b/tests/test_ipython_editor.py index cfe5cd3..b2faca2 100644 --- a/tests/test_ipython_editor.py +++ b/tests/test_ipython_editor.py @@ -7,7 +7,8 @@ from reprexlite.exceptions import IPythonNotFoundError from reprexlite.reprexes import Reprex -from tests.utils import remove_ansi_escape, requires_ipython, requires_no_ipython +from tests.pytest_utils import requires_ipython, requires_no_ipython +from tests.utils import remove_ansi_escape @pytest.fixture() diff --git a/tests/test_ipython_magics.py b/tests/test_ipython_magics.py index fd8237e..5a6e024 100644 --- a/tests/test_ipython_magics.py +++ b/tests/test_ipython_magics.py @@ -6,7 +6,7 @@ from reprexlite.config import ReprexConfig from reprexlite.reprexes import Reprex -from tests.utils import requires_ipython, requires_no_ipython +from tests.pytest_utils import requires_ipython, requires_no_ipython @pytest.fixture() diff --git a/tests/test_printing.py b/tests/test_printing.py new file mode 100644 index 0000000..1007613 --- /dev/null +++ b/tests/test_printing.py @@ -0,0 +1,93 @@ +from contextlib import redirect_stdout +import io +from itertools import chain +import sys + +import pytest + +from reprexlite.config import ReprexConfig +import reprexlite.printing +from reprexlite.reprexes import Reprex +from tests.expected_printed import ASSETS_DIR, INPUT, expected_reprexes, expected_reprexes_no_color +from tests.pytest_utils import requires_no_rich, requires_rich +from tests.utils import ( + MOCK_VERSION, + MockDateTime, + assert_str_equals, +) + +all_expected_reprexes = list(chain(expected_reprexes, expected_reprexes_no_color)) + + +@pytest.fixture +def patch_datetime(monkeypatch): + monkeypatch.setattr(sys.modules["reprexlite.formatting"], "datetime", MockDateTime) + + +@pytest.fixture +def patch_version(monkeypatch): + monkeypatch.setattr(sys.modules["reprexlite.formatting"], "__version__", MOCK_VERSION) + + +@pytest.fixture +def patch_rich_force_terminal(monkeypatch): + from rich.console import Console + + force_terminal_console = Console(soft_wrap=True, force_terminal=True) + monkeypatch.setattr(reprexlite.printing, "console", force_terminal_console) + + +@requires_rich +@pytest.mark.parametrize( + "ereprex", all_expected_reprexes, ids=[e.filename for e in all_expected_reprexes] +) +def test_reprex_printing(ereprex, patch_datetime, patch_version, patch_rich_force_terminal): + """Test that printing works.""" + r = Reprex.from_input(INPUT, ReprexConfig(**ereprex.kwargs)) + + with io.StringIO() as buffer: + with redirect_stdout(buffer): + r.print() + buffer.seek(0) + actual = buffer.read() + + with (ASSETS_DIR / ereprex.filename).open("r") as fp: + assert_str_equals(expected=fp.read(), actual=actual) + assert str(actual).endswith("\n") + + +@requires_no_rich +@pytest.mark.parametrize( + "ereprex", expected_reprexes_no_color, ids=[e.filename for e in expected_reprexes_no_color] +) +def test_reprex_printing_no_rich(ereprex, patch_datetime, patch_version): + """Test that printing works when rich is not available and produces the same output as + no_color=True.""" + kwargs = ereprex.kwargs.copy() + kwargs.pop("no_color") + + ## default + r = Reprex.from_input(INPUT, ReprexConfig(**kwargs)) + + with io.StringIO() as buffer: + with redirect_stdout(buffer): + r.print() + buffer.seek(0) + actual = buffer.read() + + with (ASSETS_DIR / ereprex.filename).open("r") as fp: + assert_str_equals(expected=fp.read(), actual=actual) + assert str(actual).endswith("\n") + + # no_color=True + r = Reprex.from_input(INPUT, ReprexConfig(**kwargs, no_color=True)) + + with io.StringIO() as buffer: + with redirect_stdout(buffer): + r.print() + buffer.seek(0) + actual = buffer.read() + + with (ASSETS_DIR / ereprex.filename).open("r") as fp: + assert_str_equals(expected=fp.read(), actual=actual) + assert str(actual).endswith("\n") diff --git a/tests/test_reprexes.py b/tests/test_reprexes.py index 416a8b1..ecc2a6b 100644 --- a/tests/test_reprexes.py +++ b/tests/test_reprexes.py @@ -7,14 +7,8 @@ from reprexlite.config import ReprexConfig from reprexlite.exceptions import BlackNotFoundError, InputSyntaxError, UnexpectedError from reprexlite.reprexes import ParsedResult, RawResult, Reprex, reprex -from tests.utils import ( - assert_equals, - assert_not_equals, - assert_str_equals, - requires_black, - requires_no_black, - requires_no_pygments, -) +from tests.pytest_utils import requires_black, requires_no_black, requires_no_pygments +from tests.utils import assert_equals, assert_not_equals, assert_str_equals Case = namedtuple("Case", ["id", "input", "expected"]) diff --git a/tests/utils.py b/tests/utils.py index dd03fcd..af055f2 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,24 +1,11 @@ +from contextlib import contextmanager import re +import sys from typing import Any import pytest -## SKIP DECORATORS - -requires_ipython = pytest.mark.skipif( - not pytest.IPYTHON_IS_AVAILABLE, reason="ipython is not available" -) -requires_no_ipython = pytest.mark.skipif( - pytest.IPYTHON_IS_AVAILABLE, reason="ipython is available" -) -requires_black = pytest.mark.skipif(not pytest.BLACK_IS_AVAILABLE, reason="black is not available") -requires_no_black = pytest.mark.skipif(pytest.BLACK_IS_AVAILABLE, reason="black is available") -requires_pygments = pytest.mark.skipif( - not pytest.PYGMENTS_IS_AVAILABLE, reason="pygments is not available" -) -requires_no_pygments = pytest.mark.skipif( - pytest.PYGMENTS_IS_AVAILABLE, reason="pygments is available" -) +from reprexlite.session_info import Package, SessionInfo # https://stackoverflow.com/a/14693789/5957621 @@ -54,3 +41,67 @@ def assert_not_equals(left: Any, right: Any): """Tests not equals in both directions""" assert left != right assert right != left + + +MOCK_VERSION = "VERSION" + + +@contextmanager +def patch_version(): + version = sys.modules["reprexlite.formatting"].__version__ + sys.modules["reprexlite.formatting"].__version__ = MOCK_VERSION + yield + sys.modules["reprexlite.formatting"].__version__ = version + + +class MockDateTime: + @classmethod + def now(cls): + return cls() + + def astimezone(self): + return self + + def strftime(self, format): + return "DATETIME" + + +@contextmanager +def patch_datetime(): + datetime = sys.modules["reprexlite.formatting"].datetime + sys.modules["reprexlite.formatting"].datetime = MockDateTime + yield + sys.modules["reprexlite.formatting"].datetime = datetime + + +class MockPackage(Package): + def __init__(self, name: str, version: str): + self._name = name + self._version = version + + @property + def name(self): + return self._name + + @property + def version(self): + return self._version + + +class MockSessionInfo(SessionInfo): + def __init__(self, *args, **kwargs): + self.python_version = "3.x.y" + self.python_build_date = "Jan 01 2020 03:33:33" + self.os = "GLaDOS" + self.packages = [ + MockPackage("datatable", "1.0"), + MockPackage("ggplot2", "2.0"), + MockPackage("pkgnet", "3.0"), + ] + + +@contextmanager +def patch_session_info(): + sys.modules["reprexlite.formatting"].SessionInfo = MockSessionInfo + yield + sys.modules["reprexlite.formatting"].SessionInfo = SessionInfo From b99c92b9f37267b9a33a4fcb4f670453bfa4d49f Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Tue, 28 Mar 2023 00:26:26 -0400 Subject: [PATCH 28/31] Add more cases --- tests/assets/printed/html.html | 83 +++++++++++++++++++++++++ tests/assets/printed/no_color/ds.md | 8 +++ tests/assets/printed/no_color/html.html | 83 +++++++++++++++++++++++++ tests/assets/printed/no_color/so.md | 8 +++ tests/expected_printed.py | 14 ++++- 5 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 tests/assets/printed/html.html create mode 100644 tests/assets/printed/no_color/ds.md create mode 100644 tests/assets/printed/no_color/html.html create mode 100644 tests/assets/printed/no_color/so.md diff --git a/tests/assets/printed/html.html b/tests/assets/printed/html.html new file mode 100644 index 0000000..6b2659f --- /dev/null +++ b/tests/assets/printed/html.html @@ -0,0 +1,83 @@ +<style>pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #f8f8f8; } +.highlight .c { color: #3D7B7B; font-style: italic } /* Comment */ +.highlight .err { border: 1px solid #FF0000 } /* Error */ +.highlight .k { color: #008000; font-weight: bold } /* Keyword */ +.highlight .o { color: #666666 } /* Operator */ +.highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #9C6500 } /* Comment.Preproc */ +.highlight .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #A00000 } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .gr { color: #E40000 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #008400 } /* Generic.Inserted */ +.highlight .go { color: #717171 } /* Generic.Output */ +.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #0044DD } /* Generic.Traceback */ +.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #008000 } /* Keyword.Pseudo */ +.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #B00040 } /* Keyword.Type */ +.highlight .m { color: #666666 } /* Literal.Number */ +.highlight .s { color: #BA2121 } /* Literal.String */ +.highlight .na { color: #687822 } /* Name.Attribute */ +.highlight .nb { color: #008000 } /* Name.Builtin */ +.highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */ +.highlight .no { color: #880000 } /* Name.Constant */ +.highlight .nd { color: #AA22FF } /* Name.Decorator */ +.highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */ +.highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #0000FF } /* Name.Function */ +.highlight .nl { color: #767600 } /* Name.Label */ +.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #19177C } /* Name.Variable */ +.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +.highlight .w { color: #bbbbbb } /* Text.Whitespace */ +.highlight .mb { color: #666666 } /* Literal.Number.Bin */ +.highlight .mf { color: #666666 } /* Literal.Number.Float */ +.highlight .mh { color: #666666 } /* Literal.Number.Hex */ +.highlight .mi { color: #666666 } /* Literal.Number.Integer */ +.highlight .mo { color: #666666 } /* Literal.Number.Oct */ +.highlight .sa { color: #BA2121 } /* Literal.String.Affix */ +.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ +.highlight .sc { color: #BA2121 } /* Literal.String.Char */ +.highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */ +.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #BA2121 } /* Literal.String.Double */ +.highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ +.highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ +.highlight .sx { color: #008000 } /* Literal.String.Other */ +.highlight .sr { color: #A45A77 } /* Literal.String.Regex */ +.highlight .s1 { color: #BA2121 } /* Literal.String.Single */ +.highlight .ss { color: #19177C } /* Literal.String.Symbol */ +.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #0000FF } /* Name.Function.Magic */ +.highlight .vc { color: #19177C } /* Name.Variable.Class */ +.highlight .vg { color: #19177C } /* Name.Variable.Global */ +.highlight .vi { color: #19177C } /* Name.Variable.Instance */ +.highlight .vm { color: #19177C } /* Name.Variable.Magic */ +.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */ +<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span class="normal">1 +<span class="normal">2 +<span class="normal">3<td class="code"><div><pre><span><code><a id="True-1" name="True-1"><span class="n">x <span class="o">= <span class="mi">2 +<a id="True-2" name="True-2"><span class="n">x <span class="o">+ <span class="mi">2 +<a id="True-3" name="True-3"><span class="c1">#> 4 + + +<p><sup>Created at DATETIME by <a href="https://github.com/jayqi/reprexlite">reprexlite vVERSION + diff --git a/tests/assets/printed/no_color/ds.md b/tests/assets/printed/no_color/ds.md new file mode 100644 index 0000000..fcfe0a4 --- /dev/null +++ b/tests/assets/printed/no_color/ds.md @@ -0,0 +1,8 @@ +```python +x = 2 +x + 2 +#> 4 +``` + +Created at DATETIME by [reprexlite](https://github.com/jayqi/reprexlite) vVERSION + diff --git a/tests/assets/printed/no_color/html.html b/tests/assets/printed/no_color/html.html new file mode 100644 index 0000000..97338ae --- /dev/null +++ b/tests/assets/printed/no_color/html.html @@ -0,0 +1,83 @@ + +
1
+2
+3
x = 2
+x + 2
+#> 4
+
+ +

Created at DATETIME by reprexlite vVERSION

+ diff --git a/tests/assets/printed/no_color/so.md b/tests/assets/printed/no_color/so.md new file mode 100644 index 0000000..fcfe0a4 --- /dev/null +++ b/tests/assets/printed/no_color/so.md @@ -0,0 +1,8 @@ +```python +x = 2 +x + 2 +#> 4 +``` + +Created at DATETIME by [reprexlite](https://github.com/jayqi/reprexlite) vVERSION + diff --git a/tests/expected_printed.py b/tests/expected_printed.py index 1951c3d..c91b9cd 100644 --- a/tests/expected_printed.py +++ b/tests/expected_printed.py @@ -6,6 +6,7 @@ from contextlib import redirect_stdout from dataclasses import dataclass +from itertools import chain from pathlib import Path import shutil from textwrap import dedent @@ -36,6 +37,7 @@ class ExpectedReprex: ExpectedReprex("gh.md", {"venue": "gh"}), ExpectedReprex("so.md", {"venue": "so"}), ExpectedReprex("ds.md", {"venue": "ds"}), + ExpectedReprex("html.html", {"venue": "html"}), ExpectedReprex("htmlnocolor.html", {"venue": "htmlnocolor"}), ExpectedReprex("py.py", {"venue": "py"}), ExpectedReprex("slack.txt", {"venue": "slack"}), @@ -43,6 +45,9 @@ class ExpectedReprex: expected_reprexes_no_color = [ # No Color ExpectedReprex("no_color/gh.md", {"venue": "gh", "no_color": True}), + ExpectedReprex("no_color/so.md", {"venue": "so", "no_color": True}), + ExpectedReprex("no_color/ds.md", {"venue": "ds", "no_color": True}), + ExpectedReprex("no_color/html.html", {"venue": "html", "no_color": True}), ExpectedReprex("no_color/htmlnocolor.html", {"venue": "htmlnocolor", "no_color": True}), ExpectedReprex("no_color/py.py", {"venue": "py", "no_color": True}), ExpectedReprex("no_color/slack.txt", {"venue": "slack", "no_color": True}), @@ -54,7 +59,14 @@ class ExpectedReprex: shutil.rmtree(ASSETS_DIR, ignore_errors=True) with patch_datetime(), patch_version(): - for ereprex in tqdm(expected_reprexes): + for ereprex in tqdm( + list( + chain( + expected_reprexes, + expected_reprexes_no_color, + ) + ) + ): outfile = ASSETS_DIR / ereprex.filename outfile.parent.mkdir(exist_ok=True) r = reprex(INPUT, **ereprex.kwargs, print_=False) From 47d8654479b908cb8da336333cfd8a6ec7338c30 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Tue, 28 Mar 2023 01:07:06 -0400 Subject: [PATCH 29/31] Fix linting; docs --- reprexlite/formatting.py | 30 ++++++++++++++++++++++++++---- reprexlite/printing.py | 23 ++++++++++++++++++++++- reprexlite/reprexes.py | 3 +++ tests/conftest.py | 8 ++++++++ tests/expected_printed.py | 2 +- tests/utils.py | 3 --- 6 files changed, 60 insertions(+), 9 deletions(-) diff --git a/reprexlite/formatting.py b/reprexlite/formatting.py index b1210e4..8767595 100644 --- a/reprexlite/formatting.py +++ b/reprexlite/formatting.py @@ -26,10 +26,14 @@ class Formatter(Protocol): + """Callback protocol that defines the venue formatter callable type. A formatter callable + should take a reprex string (code with results as comments) and format it for rendering + in a particular venue.""" + def __call__( self, reprex_str: str, advertise: Optional[bool] = None, session_info: bool = False ) -> str: - """Format a reprex string for a specific sharing venue. + """Format a stringified reprex for rendering in some venue. Args: reprex_str (str): String containing rendered reprex output. @@ -77,7 +81,7 @@ def registrar(fn: Formatter): def format_github_flavored_markdown( reprex_str: str, advertise: Optional[bool] = None, session_info: bool = False ) -> str: - """Formatter for rendering reprexes in GitHub Flavored Markdown. + """Format a reprex in GitHub Flavored Markdown. Args: reprex_str (str): String containing rendered reprex output. @@ -108,8 +112,14 @@ def format_github_flavored_markdown( @dataclasses.dataclass class HtmlFormatter: - """Formatter for rendering reprexes in HTML. If optional dependency Pygments is - available, the rendered HTML will have syntax highlighting for the Python code.""" + """Format a reprex in HTML. Can use Pygments to add syntax highlighting to the rendered Python + code block. + + Attributes: + no_color (bool): Whether to disable syntax highlighting, regardless of whether Pygments is + available. + pygments_style (str): A valid Pygments style name. + """ no_color: bool pygments_style: str = "default" @@ -117,6 +127,18 @@ class HtmlFormatter: def __call__( self, reprex_str: str, advertise: Optional[bool] = None, session_info: bool = False ) -> str: + """Format a reprex in HTML. + + Args: + reprex_str (str): String containing rendered reprex output. + advertise (Optional[bool], optional): Whether to include the advertisement for + reprexlite. Defaults to None, which uses a per-formatter default. + session_info (bool, optional): Whether to include detailed session information. + Defaults to False. + + Returns: + str: String containing formatted reprex code. Ends with newline. + """ if advertise is None: advertise = True out = [] diff --git a/reprexlite/printing.py b/reprexlite/printing.py index 3bc3489..e55f83f 100644 --- a/reprexlite/printing.py +++ b/reprexlite/printing.py @@ -23,8 +23,11 @@ class Printer(Protocol): + """Callback protocol for a printer callable type. A printer callable should print a given + venue-formatted reprex to stdout, potentially with terminal coloring.""" + def __call__(self, formatted_reprex: str, **kwargs): - """Print a formatted reprex, possibly with colors. + """Print given venue-formatted reprex to stdout. Args: formatted_reprex (str): Formatted reprex. @@ -49,6 +52,12 @@ def registrar(fn: Printer): @register_printer("so") @register_printer("gh") def print_markdown(formatted_reprex: str, **kwargs): + """Print a formatted markdown reprex using rich. + + Args: + formatted_reprex (str): Formatted reprex. + **kwargs: Arguments passed to rich's Console.print. + """ if RICH_IS_AVAILABLE: console.print(Syntax(formatted_reprex, "markdown", theme="ansi_dark"), **kwargs) else: @@ -58,6 +67,12 @@ def print_markdown(formatted_reprex: str, **kwargs): @register_printer("htmlnocolor") @register_printer("html") def print_html(formatted_reprex: str, **kwargs): + """Print a formatted HTML reprex using rich. + + Args: + formatted_reprex (str): Formatted reprex. + **kwargs: Arguments passed to rich's Console.print. + """ if RICH_IS_AVAILABLE: console.print(Syntax(formatted_reprex, "html", theme="ansi_dark"), **kwargs) else: @@ -66,6 +81,12 @@ def print_html(formatted_reprex: str, **kwargs): @register_printer("py") def print_python_code(formatted_reprex: str, **kwargs): + """Print a formatted Python code reprex using rich. + + Args: + formatted_reprex (str): Formatted reprex. + **kwargs: Arguments passed to rich's Console.print. + """ if RICH_IS_AVAILABLE: console.print(Syntax(formatted_reprex, "python", theme="ansi_dark"), **kwargs) else: diff --git a/reprexlite/reprexes.py b/reprexlite/reprexes.py index 133f0cd..ed9210f 100644 --- a/reprexlite/reprexes.py +++ b/reprexlite/reprexes.py @@ -416,6 +416,7 @@ def results_match(self) -> bool: ) def format(self) -> str: + """Returns the reprex formatted for the configured venue.""" out = str(self) formatter = formatter_registry[self.config.venue].formatter return formatter( @@ -423,6 +424,7 @@ def format(self) -> str: ) def print(self, **kwargs) -> None: + """Prints to stdout the reprex formatted for the configured venue.""" if self.config.no_color: print(self.format(), **kwargs) else: @@ -451,6 +453,7 @@ def _repr_html_(self) -> str: def to_snippet(s: str, n: int) -> str: + """Utility function that truncates a string to n characters.""" if len(s) <= n: return rf"{s}" else: diff --git a/tests/conftest.py b/tests/conftest.py index 0a9726a..34db975 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,10 @@ +import sys + +if sys.version_info[:2] >= (3, 8): + import importlib.metadata as importlib_metadata +else: + import importlib_metadata + import pytest @@ -38,6 +45,7 @@ def pytest_configure(config): try: import rich + print(importlib_metadata.version(rich.__name__)) pytest.RICH_IS_AVAILABLE = True except ModuleNotFoundError as e: if e.name == "rich": diff --git a/tests/expected_printed.py b/tests/expected_printed.py index c91b9cd..2f65591 100644 --- a/tests/expected_printed.py +++ b/tests/expected_printed.py @@ -54,7 +54,7 @@ class ExpectedReprex: ] if __name__ == "__main__": - import rich + import rich # noqa: F401 from tqdm import tqdm shutil.rmtree(ASSETS_DIR, ignore_errors=True) diff --git a/tests/utils.py b/tests/utils.py index af055f2..1c961a1 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -3,11 +3,8 @@ import sys from typing import Any -import pytest - from reprexlite.session_info import Package, SessionInfo - # https://stackoverflow.com/a/14693789/5957621 ANSI_ESCAPE_REGEX = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") From 5953db96c0461d15ed481ffe5da2630f9c73d0c0 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Tue, 28 Mar 2023 01:09:15 -0400 Subject: [PATCH 30/31] Fix tests --- tests/assets/printed/html.html | 83 ------------------------- tests/assets/printed/no_color/html.html | 83 ------------------------- tests/conftest.py | 8 +-- tests/expected_printed.py | 2 - 4 files changed, 4 insertions(+), 172 deletions(-) delete mode 100644 tests/assets/printed/html.html delete mode 100644 tests/assets/printed/no_color/html.html diff --git a/tests/assets/printed/html.html b/tests/assets/printed/html.html deleted file mode 100644 index 6b2659f..0000000 --- a/tests/assets/printed/html.html +++ /dev/null @@ -1,83 +0,0 @@ -<style>pre { line-height: 125%; } -td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } -span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } -td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } -span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } -.highlight .hll { background-color: #ffffcc } -.highlight { background: #f8f8f8; } -.highlight .c { color: #3D7B7B; font-style: italic } /* Comment */ -.highlight .err { border: 1px solid #FF0000 } /* Error */ -.highlight .k { color: #008000; font-weight: bold } /* Keyword */ -.highlight .o { color: #666666 } /* Operator */ -.highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #9C6500 } /* Comment.Preproc */ -.highlight .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */ -.highlight .gd { color: #A00000 } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gr { color: #E40000 } /* Generic.Error */ -.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #008400 } /* Generic.Inserted */ -.highlight .go { color: #717171 } /* Generic.Output */ -.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #0044DD } /* Generic.Traceback */ -.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #008000 } /* Keyword.Pseudo */ -.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #B00040 } /* Keyword.Type */ -.highlight .m { color: #666666 } /* Literal.Number */ -.highlight .s { color: #BA2121 } /* Literal.String */ -.highlight .na { color: #687822 } /* Name.Attribute */ -.highlight .nb { color: #008000 } /* Name.Builtin */ -.highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */ -.highlight .no { color: #880000 } /* Name.Constant */ -.highlight .nd { color: #AA22FF } /* Name.Decorator */ -.highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */ -.highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */ -.highlight .nf { color: #0000FF } /* Name.Function */ -.highlight .nl { color: #767600 } /* Name.Label */ -.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ -.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ -.highlight .nv { color: #19177C } /* Name.Variable */ -.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ -.highlight .w { color: #bbbbbb } /* Text.Whitespace */ -.highlight .mb { color: #666666 } /* Literal.Number.Bin */ -.highlight .mf { color: #666666 } /* Literal.Number.Float */ -.highlight .mh { color: #666666 } /* Literal.Number.Hex */ -.highlight .mi { color: #666666 } /* Literal.Number.Integer */ -.highlight .mo { color: #666666 } /* Literal.Number.Oct */ -.highlight .sa { color: #BA2121 } /* Literal.String.Affix */ -.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ -.highlight .sc { color: #BA2121 } /* Literal.String.Char */ -.highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */ -.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ -.highlight .s2 { color: #BA2121 } /* Literal.String.Double */ -.highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ -.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ -.highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ -.highlight .sx { color: #008000 } /* Literal.String.Other */ -.highlight .sr { color: #A45A77 } /* Literal.String.Regex */ -.highlight .s1 { color: #BA2121 } /* Literal.String.Single */ -.highlight .ss { color: #19177C } /* Literal.String.Symbol */ -.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #0000FF } /* Name.Function.Magic */ -.highlight .vc { color: #19177C } /* Name.Variable.Class */ -.highlight .vg { color: #19177C } /* Name.Variable.Global */ -.highlight .vi { color: #19177C } /* Name.Variable.Instance */ -.highlight .vm { color: #19177C } /* Name.Variable.Magic */ -.highlight .il { color: #666666 } /* Literal.Number.Integer.Long */ -<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span class="normal">1 -<span class="normal">2 -<span class="normal">3<td class="code"><div><pre><span><code><a id="True-1" name="True-1"><span class="n">x <span class="o">= <span class="mi">2 -<a id="True-2" name="True-2"><span class="n">x <span class="o">+ <span class="mi">2 -<a id="True-3" name="True-3"><span class="c1">#> 4 - - -<p><sup>Created at DATETIME by <a href="https://github.com/jayqi/reprexlite">reprexlite vVERSION - diff --git a/tests/assets/printed/no_color/html.html b/tests/assets/printed/no_color/html.html deleted file mode 100644 index 97338ae..0000000 --- a/tests/assets/printed/no_color/html.html +++ /dev/null @@ -1,83 +0,0 @@ - -
1
-2
-3
x = 2
-x + 2
-#> 4
-
- -

Created at DATETIME by reprexlite vVERSION

- diff --git a/tests/conftest.py b/tests/conftest.py index 34db975..6acd499 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,7 @@ def pytest_configure(config): try: import IPython - print(IPython.__version__) + print("ipython", IPython.__version__) pytest.IPYTHON_IS_AVAILABLE = True except ModuleNotFoundError as e: if e.name == "IPython": @@ -23,7 +23,7 @@ def pytest_configure(config): try: import black - print(black.__version__) + print("black", black.__version__) pytest.BLACK_IS_AVAILABLE = True except ModuleNotFoundError as e: if e.name == "black": @@ -34,7 +34,7 @@ def pytest_configure(config): try: import pygments - print(pygments.__version__) + print("pygments", pygments.__version__) pytest.PYGMENTS_IS_AVAILABLE = True except ModuleNotFoundError as e: if e.name == "pygments": @@ -45,7 +45,7 @@ def pytest_configure(config): try: import rich - print(importlib_metadata.version(rich.__name__)) + print("rich", importlib_metadata.version(rich.__name__)) pytest.RICH_IS_AVAILABLE = True except ModuleNotFoundError as e: if e.name == "rich": diff --git a/tests/expected_printed.py b/tests/expected_printed.py index 2f65591..8493cde 100644 --- a/tests/expected_printed.py +++ b/tests/expected_printed.py @@ -37,7 +37,6 @@ class ExpectedReprex: ExpectedReprex("gh.md", {"venue": "gh"}), ExpectedReprex("so.md", {"venue": "so"}), ExpectedReprex("ds.md", {"venue": "ds"}), - ExpectedReprex("html.html", {"venue": "html"}), ExpectedReprex("htmlnocolor.html", {"venue": "htmlnocolor"}), ExpectedReprex("py.py", {"venue": "py"}), ExpectedReprex("slack.txt", {"venue": "slack"}), @@ -47,7 +46,6 @@ class ExpectedReprex: ExpectedReprex("no_color/gh.md", {"venue": "gh", "no_color": True}), ExpectedReprex("no_color/so.md", {"venue": "so", "no_color": True}), ExpectedReprex("no_color/ds.md", {"venue": "ds", "no_color": True}), - ExpectedReprex("no_color/html.html", {"venue": "html", "no_color": True}), ExpectedReprex("no_color/htmlnocolor.html", {"venue": "htmlnocolor", "no_color": True}), ExpectedReprex("no_color/py.py", {"venue": "py", "no_color": True}), ExpectedReprex("no_color/slack.txt", {"venue": "slack", "no_color": True}), From ea1b8425d0d6e64d72a92058e2096536a270dfb5 Mon Sep 17 00:00:00 2001 From: Jay Qi Date: Wed, 29 Mar 2023 01:52:52 -0400 Subject: [PATCH 31/31] Coverage --- reprexlite/cli.py | 2 +- reprexlite/formatting.py | 2 +- reprexlite/ipython.py | 2 +- reprexlite/printing.py | 2 +- tests/test_cli.py | 7 +++++++ tests/test_reprexes.py | 6 ++++++ 6 files changed, 17 insertions(+), 4 deletions(-) diff --git a/reprexlite/cli.py b/reprexlite/cli.py index 7b07f03..424518d 100644 --- a/reprexlite/cli.py +++ b/reprexlite/cli.py @@ -13,7 +13,7 @@ if e.name == "rich": pass else: - raise + raise # pragma: no cover from reprexlite.config import ParsingMethod, ReprexConfig diff --git a/reprexlite/formatting.py b/reprexlite/formatting.py index 8767595..b3d04a0 100644 --- a/reprexlite/formatting.py +++ b/reprexlite/formatting.py @@ -17,7 +17,7 @@ if e.name == "pygments": PYGMENTS_IS_AVAILABLE = False else: - raise + raise # pragma: no cover from reprexlite.exceptions import PygmentsNotFoundError diff --git a/reprexlite/ipython.py b/reprexlite/ipython.py index 8a491e4..b47cf0f 100644 --- a/reprexlite/ipython.py +++ b/reprexlite/ipython.py @@ -21,7 +21,7 @@ if e.name == "IPython": raise IPythonNotFoundError(*e.args, name="IPython") else: - raise + raise # pragma: no cover runner = CliRunner() diff --git a/reprexlite/printing.py b/reprexlite/printing.py index e55f83f..736344e 100644 --- a/reprexlite/printing.py +++ b/reprexlite/printing.py @@ -16,7 +16,7 @@ if e.name == "rich": RICH_IS_AVAILABLE = False else: - raise + raise # pragma: no cover from reprexlite.exceptions import RichNotFoundError diff --git a/tests/test_cli.py b/tests/test_cli.py index b76f893..e03ae64 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -121,6 +121,13 @@ def test_syntax_error(patch_edit): assert "Incomplete input." in result.stdout +def test_verbose(patch_edit): + result = runner.invoke(app, ["--verbose"]) + print(result.stdout) + assert result.exit_code == 0 + assert "ReprexConfig" in remove_ansi_escape(result.stdout) + + def test_help(): """Test the CLI with --help flag.""" result = runner.invoke(app, ["--help"]) diff --git a/tests/test_reprexes.py b/tests/test_reprexes.py index ecc2a6b..771e0bf 100644 --- a/tests/test_reprexes.py +++ b/tests/test_reprexes.py @@ -715,6 +715,12 @@ def test_black_bad_dependency(black_bad_dependency, monkeypatch): assert exc_info.value.name == black_bad_dependency +def test_repr(): + """Test rich HTML display for Jupyter Notebooks runs without error.""" + r = Reprex.from_input("2+2") + assert repr(r).startswith("