-
Notifications
You must be signed in to change notification settings - Fork 17
/
Copy pathconftest.py
101 lines (79 loc) · 3.08 KB
/
conftest.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
"""
Custom PyTest configuration that runs all the docstrings in the README as separate
doctests. This is a bit of a hack, but it works. The README is parsed for Python code
blocks, which are then written to a temporary file in the 'tests/' directory. PyTest
then collects these files and runs them as doctests.
"""
from __future__ import annotations
import pytest
import re
import tempfile
from pathlib import Path
import datetime
# Global list to track temporary files
TEMP_FILES: list[Path] = []
MODULE_IMPORTS = """
from timefhuman import timefhuman, tfhConfig
import datetime
import pytz
import pytest
"""
TEST_TEMPLATE = """
def test_readme_example_{i}():
\"\"\"
{example}
\"\"\"
pass
"""
@pytest.fixture
def now():
return datetime.datetime(year=2018, month=8, day=4, hour=14)
@pytest.fixture(autouse=True)
def conditional_setup_teardown(now, request):
"""Specifically for the doctests only, which currently only exist in the README,
set the 'now' attribute to a fixed date for consistent results.
This fixture is applied to all tests, but only has an effect on doctests. Weirdly,
doing this the "normal" way -- simply defining a fixture, then specifying it
explicitly in the test function arguments -- doesn't work for doctests.
"""
if isinstance(request.node, pytest.DoctestItem):
from timefhuman import DEFAULT_CONFIG
old_now = DEFAULT_CONFIG.now
DEFAULT_CONFIG.now = now
yield
DEFAULT_CONFIG.now = old_now
else:
yield
class ReadmeDoctestModule(pytest.Module):
@staticmethod
def create_doctest_file(readme_path: Path) -> Path | None:
with readme_path.open("r", encoding="utf-8") as f:
content = f.read()
# Extract Python code blocks from README.md
matches = re.findall(r"```python\n(.*?)```", content, re.DOTALL)
if not matches:
return None
# Assemble the Python code into a module
module_code = MODULE_IMPORTS
for i, match in enumerate(matches):
module_code += TEST_TEMPLATE.format(i=i, example=match)
# Create a temporary file in the 'tests/' directory
temp_fd, temp_path = tempfile.mkstemp(dir="tests/", suffix=".py", prefix="test_readme_")
with open(temp_path, "w", encoding="utf-8") as f:
f.write(module_code)
return Path(temp_path)
def collect(self):
# Return a module based on the temporary file's path
return [pytest.Module.from_parent(parent=self.parent, path=self.path)]
@pytest.hookimpl
def pytest_collect_file(file_path: Path, parent):
if file_path.name == "README.md":
doctest_file = ReadmeDoctestModule.create_doctest_file(file_path)
if doctest_file:
TEMP_FILES.append(doctest_file) # Track the temporary file for cleanup later
return ReadmeDoctestModule.from_parent(parent=parent, path=doctest_file)
return None
def pytest_sessionfinish(session, exitstatus):
# Cleanup all temporary files after the session ends
for temp_file in TEMP_FILES:
temp_file.unlink(missing_ok=True)