Skip to content

Commit

Permalink
Merge pull request #40 from nschloe/pytest-plugin
Browse files Browse the repository at this point in the history
Pytest plugin
  • Loading branch information
nschloe authored May 7, 2021
2 parents 1a4c4f9 + 9e36bbf commit 6f97bce
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 22 deletions.
41 changes: 23 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# pytest-codeblocks
<p align="center">
<a href="https://github.com/nschloe/meshio"><img alt="meshio" src="https://nschloe.github.io/pytest-codeblocks/logo.svg" width="60%"></a>
<p align="center">Test code blocks in your READMEs.</p>
</p>

[![PyPi Version](https://img.shields.io/pypi/v/pytest-codeblocks.svg?style=flat-square)](https://pypi.org/project/pytest-codeblocks/)
[![Anaconda Cloud](https://anaconda.org/conda-forge/pytest-codeblocks/badges/version.svg?=style=flat-square)](https://anaconda.org/conda-forge/pytest-codeblocks/)
Expand All @@ -11,32 +14,36 @@
[![LGTM](https://img.shields.io/lgtm/grade/python/github/nschloe/pytest-codeblocks.svg?style=flat-square)](https://lgtm.com/projects/g/nschloe/pytest-codeblocks)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg?style=flat-square)](https://github.com/psf/black)

This is pytest-codeblocks, a tool for extracting code blocks from Markdown files and to create
tests from them.
This is pytest-codeblocks, a [pytest](https://pytest.org/) plugin for testing code
blocks from README files.

Install with
```
pip install pytest-codeblocks
```
and create tests for [pytest](https://docs.pytest.org/en/stable/) with
```python
import pytest_codeblocks

test_readme = pytest_codeblocks.pytests_from_file("README.md")
and run pytest with
```
The `test_readme` variable is really a decorated function that pytest will pick up and
turn into tests.
pytest --codeblocks
```
tests/test_readme.py ............. [100%]
```
================================= test session starts =================================
platform linux -- Python 3.9.4, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /path/to/directory
plugins: codeblocks-0.10.0
collected 56 items
README.md ....................... [ 50%]
example.md ....................... [100%]
#### Skipping code blocks
If you don't want all code blocks to be extracted, you can **filter by syntax**
```python
pytest - codeblocks.pytests_from_file("README.md", syntax_filter="python")
================================= 56 passed in 0.08s ==================================
```
or prefix your code block in the Markdown file with a `pytest-codeblocks:skip` comment
By default, pytest-codeblocks will only pick up code blocks with `python` syntax
highlighting.


#### Skipping code blocks

Prefix your code block with a `pytest-codeblocks:skip` comment to skip
````markdown
Lorem ipsum
<!--pytest-codeblocks:skip-->
Expand All @@ -46,7 +53,6 @@ foo + bar # not working
dolor sit amet.
````


#### Merging code blocks
Broken-up code blocks can be merged into one with the `pytest-codeblocks:cont` prefix
````markdown
Expand All @@ -62,7 +68,6 @@ a + 1
```
````


#### Expected output
You can also define the expected output of a code block:
````markdown
Expand Down
8 changes: 7 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = pytest-codeblocks
version = 0.9.0
version = 0.10.0
author = Nico Schlömer
author_email = nico.schloemer@gmail.com
description = Extract code blocks from markdown
Expand All @@ -14,6 +14,7 @@ long_description_content_type = text/markdown
license = MIT
classifiers =
Development Status :: 4 - Beta
Framework :: Pytest
Intended Audience :: Developers
License :: OSI Approved :: MIT License
Operating System :: OS Independent
Expand All @@ -30,11 +31,16 @@ package_dir =
packages = find:
install_requires =
importlib_metadata;python_version<"3.8"
pytest >=6
python_requires = >=3.6

[options.packages.find]
where=src

[options.entry_points]
pytest11 =
codeblocks = pytest_codeblocks.plugin

[options.extras_require]
all =
pytest
2 changes: 2 additions & 0 deletions src/pytest_codeblocks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from . import plugin
from .__about__ import __version__
from .main import (
CodeBlock,
Expand All @@ -18,4 +19,5 @@
"pytests",
"pytests_from_buffer",
"pytests_from_file",
"plugin",
]
6 changes: 3 additions & 3 deletions src/pytest_codeblocks/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def pytests_from_file(
return pytests_from_buffer(handle, *args, **kwargs)


def pytests_from_buffer(buf, syntax_filter: Optional[str] = None):
def pytests_from_buffer(buf, syntax_filter: Optional[str] = "python"):
code_blocks = extract_from_buffer(buf)

if syntax_filter is not None:
Expand All @@ -149,7 +149,7 @@ def exec_raise(code_block):
with pytest.raises(Exception):
exec(code_block.code, {"__MODULE__": "__main__"})
else:
with stdoutIO() as s:
with stdout_io() as s:
try:
# https://stackoverflow.com/a/62851176/353337
exec(code_block.code, {"__MODULE__": "__main__"})
Expand All @@ -175,7 +175,7 @@ def exec_raise(code_block):

# https://stackoverflow.com/a/3906390/353337
@contextlib.contextmanager
def stdoutIO(stdout=None):
def stdout_io(stdout=None):
old = sys.stdout
if stdout is None:
stdout = StringIO()
Expand Down
77 changes: 77 additions & 0 deletions src/pytest_codeblocks/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#
# Take a look at the example
# https://docs.pytest.org/en/stable/example/nonpython.html
#
import pytest

from .main import extract_from_file, stdout_io


def pytest_addoption(parser):
group = parser.getgroup("general")
group.addoption(
"--codeblocks", action="store_true", help="enable testing of codeblocks"
)


def pytest_collect_file(path, parent):
config = parent.config
if config.option.codeblocks and path.ext == ".md":
return MarkdownFile.from_parent(parent, fspath=path)


class MarkdownFile(pytest.File):
def __init__(self, fspath, parent):
super().__init__(fspath, parent)

def collect(self):
for block in extract_from_file(self.fspath):
if block.syntax != "python":
continue
# https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent
out = Codeblock.from_parent(parent=self, name=self.name)
out.obj = block
yield out


class Codeblock(pytest.Item):
def __init__(self, name, parent, obj=None):
super().__init__(name, parent=parent)
self.obj = obj

def runtest(self):
if self.obj.expect_exception:
with pytest.raises(Exception):
exec(self.obj.code, {"__MODULE__": "__main__"})
else:
with stdout_io() as s:
try:
# https://stackoverflow.com/a/62851176/353337
exec(self.obj.code, {"__MODULE__": "__main__"})
except Exception as e:
raise RuntimeError(
f"{self.name}, line {self.obj.lineno}:\n```\n"
+ self.obj.code
+ "```\n\n"
+ f"{e}"
)

output = s.getvalue()
if self.obj.expected_output is not None:
if self.obj.expected_output != output:
raise RuntimeError(
f"{self.name}, line {self.obj.lineno}:\n```\n"
+ f"Expected output\n```\n{self.obj.expected_output}```\n"
+ f"but got\n```\n{output}```"
)

def repr_failure(self, excinfo):
"""Called when self.runtest() raises an exception."""
# if isinstance(excinfo.value, CodeblockException):
return excinfo.value.args[0]
# if excinfo.errisinstance(RuntimeError):
# return excinfo.value.args[0].stdout
# return super().repr_failure(excinfo)

def reportinfo(self):
return (self.fspath, -1, "code block check")
2 changes: 2 additions & 0 deletions tests/test_general.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,6 @@ def test_reference():
lst = pytest_codeblocks.extract_from_file(this_dir / "example.md")
print(lst)
for r, obj in zip(ref, lst):
print("r ", r)
print("obj", obj)
assert r == obj

0 comments on commit 6f97bce

Please sign in to comment.