Skip to content

Commit

Permalink
Merge pull request #82 from nschloe/new-markers
Browse files Browse the repository at this point in the history
New markers
  • Loading branch information
nschloe authored Jun 7, 2022
2 parents 2b58b11 + c8902c1 commit 1b92190
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 163 deletions.
5 changes: 5 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ repos:
rev: 4.0.1
hooks:
- id: flake8

- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.6.2
hooks:
- id: prettier
39 changes: 17 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,15 @@ README.md .......................
pytest-codeblocks will only pick up code blocks with `python` and `sh`/`bash`/`zsh`
syntax highlighting.

#### Skipping code blocks
#### Marking code blocks

Prefix your code block with a `pytest-codeblocks:skip` comment to skip
It is possible to use `pytest.mark` for marking code blocks. For example,
to skip a code block use `pytest.mark.skip` or `pytest.mark.skipif`:

````markdown
Lorem ipsum

<!--pytest-codeblocks:skip-->
<!--pytest.mark.skip-->

```python
foo + bar # not working
Expand All @@ -63,10 +64,8 @@ foo + bar # not working
dolor sit amet.
````

Conditionally skipping code blocks works with `skipif`, e.g.,

```markdown
<!--pytest-codeblocks:skipif(sys.version_info <= (3, 7))-->
<!--pytest.mark.skipif(sys.version_info <= (3, 7), reason="Need at least Python 3.8")-->
```

You can skip code blocks on import errors with
Expand All @@ -83,6 +82,18 @@ Skip the entire file by putting

in the first line.

For expected errors, use `pytest.mark.xfail`:

````markdown
The following gives an error:

<!--pytest.mark.xfail-->

```python
1 / 0
```
````

#### Merging code blocks

Broken-up code blocks can be merged into one with the `pytest-codeblocks:cont` prefix
Expand Down Expand Up @@ -148,19 +159,3 @@ gives

(Conditionally) Skipping the output verfication works by prepending the first
block with `skip`/`skipif` (see [above](#skipping-code-blocks)).

#### Expected errors

Some code blocks are expected to give errors. You can verify this with

````markdown
The following gives an error:

<!--pytest-codeblocks:expect-error-->

```python
1 / 0
```
````

The keyword `expect-exception` is also possible.
10 changes: 7 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[build-system]
requires = ["flit_core >=3.2,<4"]
build-backend = "flit_core.buildapi"
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"

[tool.isort]
profile = "black"
Expand All @@ -12,7 +12,7 @@ description = "Test code blocks in your READMEs"
readme = "README.md"
license = {file = "LICENSE.txt"}
classifiers = [
"Development Status :: 4 - Beta",
"Development Status :: 5 - Production/Stable",
"Framework :: Pytest",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
Expand All @@ -30,7 +30,11 @@ dependencies = [
"pytest >= 7.0.0"
]

[tool.setuptools.dynamic]
version = {attr = "pytest_codeblocks.__about__.__version__"}

[project.urls]
Homepage = "https://github.com/nschloe/pytest-codeblocks"
Code = "https://github.com/nschloe/pytest-codeblocks"
Issues = "https://github.com/nschloe/pytest-codeblocks/issues"
Funding = "https://github.com/sponsors/nschloe"
Expand Down
2 changes: 1 addition & 1 deletion src/pytest_codeblocks/__about__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.15.0"
__version__ = "0.16.0"
152 changes: 88 additions & 64 deletions src/pytest_codeblocks/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
import contextlib
import re
import sys
import warnings

# namedtuple with default arguments
# <https://stackoverflow.com/a/18348004/353337>
from dataclasses import dataclass
from dataclasses import dataclass, field
from io import StringIO
from pathlib import Path

Expand All @@ -17,10 +18,8 @@ class CodeBlock:
lineno: int
syntax: str | None = None
expected_output: str | None = None
expect_exception: bool = False
skip: bool = False
skipif: str | None = None
importorskip: str | None = None
marks: list[str] = field(default_factory=lambda: [])


def extract_from_file(
Expand All @@ -32,7 +31,10 @@ def extract_from_file(

def extract_from_buffer(f, max_num_lines: int = 10000) -> list[CodeBlock]:
out = []
previous_nonempty_line = None
marks = []
continued_block = None
expected_output_block = None
importorskip = None
k = 1

while True:
Expand All @@ -49,44 +51,12 @@ def extract_from_buffer(f, max_num_lines: int = 10000) -> list[CodeBlock]:
if line.strip() == "":
continue

if line.lstrip()[:3] == "```":
syntax = line.strip()[3:]
num_leading_spaces = len(line) - len(line.lstrip())
lineno = k - 1
# read the block
code_block = []
while True:
line = f.readline()
k += 1
if not line:
raise RuntimeError("Hit end-of-file prematurely. Syntax error?")
if k > max_num_lines:
raise RuntimeError(
f"File too large (> {max_num_lines} lines). Set max_num_lines."
)
# check if end of block
if line.lstrip()[:3] == "```":
break
# Cut (at most) num_leading_spaces leading spaces
nls = min(num_leading_spaces, len(line) - len(line.lstrip()))
line = line[nls:]
code_block.append(line)

if previous_nonempty_line is None:
out.append(CodeBlock("".join(code_block), lineno, syntax))
continue

# check for keywords
m = re.match(
r"<!--[-\s]*pytest-codeblocks:(.*)-->",
previous_nonempty_line.strip(),
)
if m is None:
out.append(CodeBlock("".join(code_block), lineno, syntax))
continue

m = re.match(
r"<!--[-\s]*pytest-codeblocks:(.*)-->",
line.strip(),
)
if m is not None:
keyword = m.group(1).strip("- ")

# handle special tags
if keyword == "expected-output":
if len(out) == 0:
Expand All @@ -99,57 +69,111 @@ def extract_from_buffer(f, max_num_lines: int = 10000) -> list[CodeBlock]:
"Found <!--pytest-codeblocks-expected-output--> "
+ "but block already has expected_output."
)
out[-1].expected_output = "".join(code_block)
expected_output_block = out[-1]

elif keyword == "cont":
if len(out) == 0:
raise RuntimeError(
"Found <!--pytest-codeblocks-cont--> but no previous code block."
)
out[-1] = CodeBlock(
out[-1].code + "".join(code_block),
out[-1].lineno,
out[-1].syntax,
out[-1].expected_output,
out[-1].expect_exception,
)
continued_block = out[-1]

elif keyword == "skip":
out.append(CodeBlock("".join(code_block), lineno, syntax, skip=True))
warnings.warn(
"pytest-codeblocks:skip is deprecated. Use pytest.mark.skip",
DeprecationWarning,
)
marks.append("pytest.mark.skip")

elif keyword.startswith("skipif"):
warnings.warn(
"pytest-codeblocks:skipif is deprecated. Use pytest.mark.skipif",
DeprecationWarning,
)
m = re.match(r"skipif\((.*)\)", keyword)
if m is None:
raise RuntimeError(
"pytest-codeblocks: Expected skipif(some-condition)"
)
out.append(
CodeBlock("".join(code_block), lineno, syntax, skipif=m.group(1))
)
marks.append(f"pytest.mark.skipif({m.group(1)}, reason='')")

elif keyword.startswith("importorskip"):
m = re.match(r"importorskip\((.*)\)", keyword)
if m is None:
raise RuntimeError(
"pytest-codeblocks: Expected importorskip(some-module)"
)
out.append(
CodeBlock(
"".join(code_block), lineno, syntax, importorskip=m.group(1)
)
)
importorskip = m.group(1)

elif keyword in ["expect-exception", "expect-error"]:
out.append(
CodeBlock(
"".join(code_block), lineno, syntax, expect_exception=True
)
warnings.warn(
f"pytest-codeblocks:{keyword} is deprecated. Use pytest.mark.xfail",
DeprecationWarning,
)
marks.append("pytest.mark.xfail")

else:
raise RuntimeError(f'Unknown pytest-codeblocks keyword "{keyword}."')

previous_nonempty_line = line
continue

m = re.match(
r"<!--[-\s]*(pytest\.mark\..*)-->",
line.strip(),
)
if m is not None:
marks.append(m.group(1))
continue

lsline = line.lstrip()
if lsline.startswith("```"):
# normally 3, but can be more:
num_leading_backticks = len(lsline) - len(lsline.lstrip("`"))
syntax = line.strip()[num_leading_backticks:]
num_leading_spaces = len(line) - len(lsline)
lineno = k - 1
# read the block
code_block = []
while True:
line = f.readline()
lsline = line.lstrip()
k += 1
if not line:
raise RuntimeError("Hit end-of-file prematurely. Syntax error?")
if k > max_num_lines:
raise RuntimeError(
f"File too large (> {max_num_lines} lines). Set max_num_lines."
)
# check if end of block
if lsline[:num_leading_backticks] == "`" * num_leading_backticks:
break
# Cut (at most) num_leading_spaces leading spaces
nls = min(num_leading_spaces, len(line) - len(lsline))
line = line[nls:]
code_block.append(line)

code = "".join(code_block)

if continued_block:
continued_block.code += code
continued_block = None

elif expected_output_block:
expected_output_block.expected_output = code
expected_output_block = None

else:
out.append(
CodeBlock(
code,
lineno,
syntax,
marks=marks,
importorskip=importorskip,
)
)
marks = []
importorskip = None

return out

Expand Down
Loading

0 comments on commit 1b92190

Please sign in to comment.