Skip to content

Commit 8d81166

Browse files
authored
[MNT] makefile for developers (#655)
* adds a makefile for developers * fixes some issues with the README CI
1 parent 6ec1288 commit 8d81166

File tree

5 files changed

+133
-16
lines changed

5 files changed

+133
-16
lines changed

.github/actions/setup-project/action.yml

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
# Action: Setup Project (composite action)
22
#
3-
# Purpose: Bootstrap a Python project in GitHub Actions by:
4-
# - Installing Task, uv, and uvx into a local ./bin directory
5-
# - Detecting presence of pyproject.toml and exposing it as an output
6-
# - Creating a virtual environment with uv and syncing dependencies
3+
# Purpose: Bootstrap a Python project within GitHub Actions by:
4+
# - Installing uv and uvx into a local ./bin directory and adding it to PATH
5+
# - Detecting the presence of pyproject.toml and exposing that as an output
6+
# - Creating a virtual environment with uv and (optionally) syncing dependencies
77
#
88
# Inputs:
9-
# - python-version: Python version used for the virtual environment (default: 3.12)
9+
# - python-version: Python version for the uv-managed virtual environment (default: 3.12)
1010
#
1111
# Outputs:
1212
# - pyproject_exists: "true" if pyproject.toml exists, otherwise "false"
1313
#
1414
# Notes:
1515
# - Safe to run in repositories without pyproject.toml; dependency sync will be skipped.
16-
# - Used by workflows such as CI, Book, Marimo, and Release.
16+
# - Purely a CI helper — it does not modify repository files.
1717

1818
name: 'Setup Project'
1919
description: 'Setup the project'
@@ -32,7 +32,7 @@ outputs:
3232
runs:
3333
using: 'composite'
3434
steps:
35-
- name: Set up task, uv, uvx and the venv
35+
- name: Set up uv, uvx and the venv
3636
shell: bash
3737
run: |
3838
mkdir -p bin
@@ -41,17 +41,9 @@ runs:
4141
echo "Adding ./bin to PATH"
4242
echo "$(pwd)/bin" >> $GITHUB_PATH
4343
44-
# Install Task
45-
curl -fsSL https://taskfile.dev/install.sh | sh -s -- -d -b ./bin
46-
4744
# Install uv and uvx
4845
curl -fsSL https://astral.sh/uv/install.sh | UV_INSTALL_DIR="./bin" sh
4946
50-
- name: Check version for task
51-
shell: bash
52-
run: |
53-
task --version
54-
5547
- name: Check version for uv
5648
shell: bash
5749
run: |

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,4 @@ dist
4848

4949
artifacts
5050

51+
bin

Makefile

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
## Makefile — PyPortfolioOpt developer conveniences
2+
#
3+
# This Makefile exposes common local development tasks and a friendly
4+
# `make help` index.
5+
# Conventions used by the help generator:
6+
# - Lines with `##` after a target are turned into help text.
7+
# - Lines starting with `##@` create section headers in the help output.
8+
# This file does not affect the library itself; it only streamlines dev workflows.
9+
10+
# Colors for pretty output in help messages
11+
BLUE := \033[36m
12+
BOLD := \033[1m
13+
GREEN := \033[32m
14+
RED := \033[31m
15+
RESET := \033[0m
16+
17+
# Default goal when running `make` with no target
18+
.DEFAULT_GOAL := help
19+
20+
# Declare phony targets (they don't produce files)
21+
.PHONY: install install-uv test fmt
22+
23+
UV_INSTALL_DIR := ./bin
24+
25+
##@ Bootstrap
26+
install-uv: ## ensure uv (and uvx) are installed locally
27+
@mkdir -p ${UV_INSTALL_DIR}
28+
@if [ -x "${UV_INSTALL_DIR}/uv" ] && [ -x "${UV_INSTALL_DIR}/uvx" ]; then \
29+
:; \
30+
else \
31+
printf "${BLUE}Installing uv${RESET}\n"; \
32+
curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR=${UV_INSTALL_DIR} sh 2>/dev/null || { printf "${RED}[ERROR] Failed to install uv ${RESET}\n"; exit 1; }; \
33+
fi
34+
35+
install: install-uv ## install
36+
@printf "${BLUE}[INFO] Creating virtual environment...${RESET}\n"
37+
# Create the virtual environment
38+
@./bin/uv venv --python 3.12 || { printf "${RED}[ERROR] Failed to create virtual environment${RESET}\n"; exit 1; }
39+
@printf "${BLUE}[INFO] Installing dependencies${RESET}\n"
40+
@./bin/uv sync --all-extras --frozen || { printf "${RED}[ERROR] Failed to install dependencies${RESET}\n"; exit 1; }
41+
42+
43+
##@ Development and Testing
44+
test: install ## run all tests
45+
@printf "${BLUE}[INFO] Running tests...${RESET}\n"
46+
@./bin/uv pip install pytest pytest-cov pytest-html
47+
@mkdir -p _tests/html-coverage _tests/html-report
48+
@./bin/uv run pytest tests --cov=pypfopt --cov-report=term --cov-report=html:_tests/html-coverage --html=_tests/html-report/report.html
49+
50+
fmt: install-uv ## check the pre-commit hooks and the linting
51+
@./bin/uvx pre-commit run --all-files
52+
@./bin/uvx deptry .
53+
54+
##@ Meta
55+
help: ## Display this help message
56+
+@printf "$(BOLD)Usage:$(RESET)\n"
57+
+@printf " make $(BLUE)<target>$(RESET)\n\n"
58+
+@printf "$(BOLD)Targets:$(RESET)\n"
59+
+@awk 'BEGIN {FS = ":.*##"; printf ""} /^[a-zA-Z_-]+:.*?##/ { printf " $(BLUE)%-15s$(RESET) %s\n", $$1, $$2 } /^##@/ { printf "\n$(BOLD)%s$(RESET)\n", substr($$0, 5) }' $(MAKEFILE_LIST)

tests/test_makefile.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from pathlib import Path
2+
import re
3+
import shutil
4+
import subprocess
5+
6+
import pytest
7+
8+
ROOT = Path(__file__).resolve().parents[1]
9+
10+
ANSI_ESCAPE_RE = re.compile(
11+
r"\x1B\[[0-?]*[ -/]*[@-~]" # basic CSI sequences
12+
)
13+
14+
15+
def _strip_ansi(s: str) -> str:
16+
return ANSI_ESCAPE_RE.sub("", s)
17+
18+
19+
@pytest.mark.skipif(shutil.which("make") is None, reason="make is not installed")
20+
def test_make_help_outputs_expected_sections_and_targets():
21+
# Ensure we run from repo root so that $(MAKEFILE_LIST) resolves correctly
22+
assert (ROOT / "Makefile").exists(), "Makefile not found at repository root"
23+
24+
proc = subprocess.run(
25+
["make", "help"],
26+
cwd=str(ROOT),
27+
capture_output=True,
28+
text=True,
29+
check=False,
30+
)
31+
32+
# Capture and normalize output
33+
out = _strip_ansi(proc.stdout)
34+
err = _strip_ansi(proc.stderr)
35+
36+
assert proc.returncode == 0, (
37+
f"`make help` exited with {proc.returncode}\nSTDOUT:\n{out}\nSTDERR:\n{err}"
38+
)
39+
40+
# Basic headings from help target
41+
assert "Usage:" in out
42+
assert "Targets:" in out
43+
44+
# Section headers defined in Makefile
45+
for section in [
46+
"Bootstrap",
47+
"Development and Testing",
48+
"Meta",
49+
]:
50+
assert section in out, (
51+
f"Section header '{section}' not found in help output.\nOutput was:\n{out}"
52+
)
53+
54+
# Targets declared in Makefile should appear in help
55+
for target in [
56+
"install-uv",
57+
"install",
58+
"test",
59+
"fmt",
60+
"help",
61+
]:
62+
assert re.search(rf"\b{re.escape(target)}\b", out) is not None, (
63+
f"Target '{target}' not found in help output.\nOutput was:\n{out}"
64+
)

tests/test_readme.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@ def test_readme_runs():
2525
result_blocks = RESULT.findall(readme_text)
2626

2727
# Optional: keep docs and expectations in sync.
28-
assert len(code_blocks) == len(result_blocks), (
28+
assert len(code_blocks) >= len(result_blocks), (
2929
"Mismatch between python and result blocks in README.md"
3030
)
31+
3132
code = "".join(code_blocks) # merged code
3233
expected = "".join(result_blocks)
3334

0 commit comments

Comments
 (0)