Skip to content

Commit 63e8268

Browse files
[ENH] Add test to validate execution of README Python examples (#713)
Adds a pytest that parses `README.md` and executes all python code blocks to ensure examples remain valid. Closes #669 - Uses regex to extract Python blocks - Executes them sequentially in a shared namespace (since examples are stateful) - Runs from repo root so relative paths like `tests/resources/...` resolve correctly - Adds repo root to `sys.path` so `pypfopt` can be imported without installation This verifies that README examples run as documented and helps prevent regressions in imports or usage.
1 parent b63f293 commit 63e8268

3 files changed

Lines changed: 42 additions & 3 deletions

File tree

tests/test_discrete_allocation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
from cvxpy.error import SolverError
12
import numpy as np
23
import pandas as pd
34
import pytest
4-
from cvxpy.error import SolverError
55

66
from pypfopt.discrete_allocation import DiscreteAllocation, get_latest_prices
77
from tests.utilities_for_tests import get_data, setup_efficient_frontier

tests/test_efficient_frontier.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,7 @@ def test_min_vol_pair_constraint():
608608
ef.min_volatility()
609609
old_sum = ef.weights[:2].sum()
610610
ef = setup_efficient_frontier()
611-
ef.add_constraint(lambda w: (w[1] + w[0] <= old_sum / 2))
611+
ef.add_constraint(lambda w: w[1] + w[0] <= old_sum / 2)
612612
ef.min_volatility()
613613
new_sum = ef.weights[:2].sum()
614614
assert new_sum <= old_sum / 2 + 1e-4
@@ -620,7 +620,7 @@ def test_max_sharpe_pair_constraint():
620620
old_sum = ef.weights[:2].sum()
621621

622622
ef = setup_efficient_frontier()
623-
ef.add_constraint(lambda w: (w[1] + w[0] <= old_sum / 2))
623+
ef.add_constraint(lambda w: w[1] + w[0] <= old_sum / 2)
624624
ef.max_sharpe(risk_free_rate=0.02)
625625
new_sum = ef.weights[:2].sum()
626626
assert new_sum <= old_sum / 2 + 1e-4

tests/test_readme_examples.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import os
2+
from pathlib import Path
3+
import re
4+
import sys
5+
6+
README_PATH = Path(__file__).resolve().parent.parent / "README.md"
7+
8+
9+
def _extract_python_blocks(markdown_text: str) -> list[str]:
10+
pattern = re.compile(r"```python[^\n]*\n(.*?)```", re.DOTALL)
11+
return [block.strip() for block in pattern.findall(markdown_text)]
12+
13+
14+
def test_readme_python_examples_run():
15+
readme_text = README_PATH.read_text(encoding="utf-8")
16+
python_blocks = _extract_python_blocks(readme_text)
17+
18+
assert python_blocks, "No python code blocks found in README.md"
19+
20+
repo_root = README_PATH.parent
21+
22+
# Make package importable
23+
sys.path.insert(0, str(repo_root))
24+
25+
# Run from repo root so relative paths work
26+
old_cwd = os.getcwd()
27+
os.chdir(repo_root)
28+
29+
globals_dict = {"__name__": "__main__"}
30+
31+
try:
32+
for idx, block in enumerate(python_blocks, start=1):
33+
exec(block, globals_dict)
34+
35+
except Exception as e:
36+
raise AssertionError(f"README python block #{idx} failed:\n{block}") from e
37+
38+
finally:
39+
os.chdir(old_cwd)

0 commit comments

Comments
 (0)