diff --git a/README.md b/README.md index 32eeb1a..eef2c6a 100644 --- a/README.md +++ b/README.md @@ -154,3 +154,35 @@ The following gives an error: ```` The keyword `expect-exception` is also possible. + +#### Doctest-style code blocks + +Code blocks starting with `">>>"` will be treated as interactive Python shell code, and tested by [doctest](https://docs.python.org/3/library/doctest.html). + +````markdown +```python +>>> print("Hello") +Hello +>>> 2+3 +5 +``` +```` +You can concatenate multiple doctest-style code blocks with ``, but cannot mix doctest-style ones with normal python code blocks, or vice versa. + +There are two ways to indicate expected exceptions in doctest-style code blocks. One is to include the expected exception into you code block, and let doctest handle it. + +````markdown +```python +>>> raise Exception("This should fail") +Traceback (most recent call last): + File "", line 1, in +Exception: This should fail +``` +```` +The other way is to use the ``, and let pytest handle it. +````markdown + +```python +>>> raise Exception("This should fail") +``` +```` \ No newline at end of file diff --git a/src/pytest_codeblocks/main.py b/src/pytest_codeblocks/main.py index c7debae..8a26920 100644 --- a/src/pytest_codeblocks/main.py +++ b/src/pytest_codeblocks/main.py @@ -7,9 +7,13 @@ # namedtuple with default arguments # from dataclasses import dataclass +from doctest import DebugRunner, DocTestFinder from io import StringIO from pathlib import Path +finder = DocTestFinder() +runner = DebugRunner() + @dataclass class CodeBlock: @@ -21,6 +25,13 @@ class CodeBlock: skip: bool = False skipif: str | None = None + def exec(self, globs: dict) -> None: + if self.code.startswith(">>>"): + for test in finder.find(self.code, name=f"line {self.lineno}", globs=globs): + runner.run(test) + else: + exec(self.code, globs) + def extract_from_file( f: str | bytes | Path, encoding: str | None = "utf-8", *args, **kwargs diff --git a/src/pytest_codeblocks/plugin.py b/src/pytest_codeblocks/plugin.py index a140dcb..fd67b88 100644 --- a/src/pytest_codeblocks/plugin.py +++ b/src/pytest_codeblocks/plugin.py @@ -64,12 +64,12 @@ def runtest(self): if self.obj.syntax == "python": if self.obj.expect_exception: with pytest.raises(Exception): - exec(self.obj.code, {"__MODULE__": "__main__"}) + self.obj.exec({"__MODULE__": "__main__"}) else: with stdout_io() as s: try: # https://stackoverflow.com/a/62851176/353337 - exec(self.obj.code, {"__MODULE__": "__main__"}) + self.obj.exec({"__MODULE__": "__main__"}) except Exception as e: raise RuntimeError( f"{self.name}, line {self.obj.lineno}:\n```\n" diff --git a/tests/test_repl.py b/tests/test_repl.py new file mode 100644 index 0000000..09bc05f --- /dev/null +++ b/tests/test_repl.py @@ -0,0 +1,56 @@ +import pathlib + +from pytest import skip + +import pytest_codeblocks + + +def test_repr(testdir): + string1 = """ + ```python + >>> print("Hello World!") + Hello World! + ``` + """ + testdir.makefile(".md", string1) + result = testdir.runpytest("--codeblocks") + result.assert_outcomes(passed=1) + + +def test_cont(testdir): + string1 = """ + ```python + >>> def add(a, b): + ... return a + b + ``` + + + ```python + >>> add(2,7) + 9 + ``` + """ + testdir.makefile(".md", string1) + result = testdir.runpytest("--codeblocks") + result.assert_outcomes(passed=1) + + +def test_exception(testdir): + string1 = """ + Handle the exception by doctest: + ```python + >>> raise Exception("This should fail") + Traceback (most recent call last): + File "", line 1, in + Exception: This should fail + ``` + + Handle the exception by pytest: + + ```python + >>> raise Exception("This should fail") + ``` + """ + testdir.makefile(".md", string1) + result = testdir.runpytest("--codeblocks") + result.assert_outcomes(passed=2)