Skip to content

Commit 6c5b101

Browse files
committed
build: attempt to consolidate more logic into the scripts folder and get the retrocookie of linted demos to work
1 parent 4dcbf78 commit 6c5b101

File tree

7 files changed

+155
-128
lines changed

7 files changed

+155
-128
lines changed

noxfile.py

Lines changed: 7 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -41,72 +41,16 @@
4141
*("--demo-name", DEFAULT_DEMO_NAME),
4242
)
4343

44-
SYNC_UV_WITH_DEMO_SCRIPT: Path = SCRIPTS_FOLDER / "sync-uv-with-demo.py"
45-
SYNC_UV_WITH_DEMO_OPTIONS: tuple[str, ...] = (
46-
*("--template-folder", TEMPLATE_FOLDER),
47-
*("--demos-cache-folder", PROJECT_DEMOS_FOLDER),
48-
*("--demo-name", DEFAULT_DEMO_NAME),
49-
)
44+
45+
MATCH_GENERATED_PRECOMMIT_SCRIPT: Path = SCRIPTS_FOLDER / "match-generated-precommit.py"
46+
MATCH_GENERATED_PRECOMMIT_OPTIONS: tuple[str, ...] = GENERATE_DEMO_PROJECT_OPTIONS
5047

5148

5249
@nox.session(name="generate-demo-project", python=DEFAULT_TEMPLATE_PYTHON_VERSION)
5350
def generate_demo_project(session: Session) -> None:
5451
"""Generates a project demo using the cookiecutter-robust-python template."""
5552
session.install("cookiecutter", "platformdirs", "loguru", "typer")
56-
session.run(
57-
"python",
58-
GENERATE_DEMO_PROJECT_SCRIPT,
59-
*GENERATE_DEMO_PROJECT_OPTIONS,
60-
*session.posargs
61-
)
62-
63-
64-
@nox.session(name="sync-uv-with-demo", python=DEFAULT_TEMPLATE_PYTHON_VERSION)
65-
def sync_uv_with_demo(session: Session) -> None:
66-
"""Syncs the uv environment with the current demo project."""
67-
session.install("cookiecutter", "platformdirs", "loguru", "typer")
68-
session.run(
69-
"python",
70-
SYNC_UV_WITH_DEMO_SCRIPT,
71-
*SYNC_UV_WITH_DEMO_OPTIONS,
72-
)
73-
74-
75-
@nox.session(name="uv-in-demo", python=DEFAULT_TEMPLATE_PYTHON_VERSION)
76-
def uv_in_demo(session: Session) -> None:
77-
"""Runs a uv command in a new project demo project then syncs with it."""
78-
session.install("cookiecutter", "platformdirs", "loguru", "typer")
79-
session.run(
80-
"python",
81-
GENERATE_DEMO_PROJECT_SCRIPT,
82-
*GENERATE_DEMO_PROJECT_OPTIONS,
83-
)
84-
original_dir: Path = Path.cwd()
85-
session.cd(DEMO_ROOT_FOLDER)
86-
session.run("uv", *session.posargs)
87-
session.cd(original_dir)
88-
session.run(
89-
SYNC_UV_WITH_DEMO_SCRIPT,
90-
*SYNC_UV_WITH_DEMO_OPTIONS,
91-
external=True,
92-
)
93-
94-
95-
@nox.session(name="in-demo", python=DEFAULT_TEMPLATE_PYTHON_VERSION)
96-
def in_demo(session: Session) -> None:
97-
"""Generates a project demo and run a uv command in it."""
98-
session.install("cookiecutter", "platformdirs", "loguru", "typer")
99-
session.run(
100-
"python",
101-
GENERATE_DEMO_PROJECT_SCRIPT,
102-
*GENERATE_DEMO_PROJECT_OPTIONS,
103-
)
104-
original_dir: Path = Path.cwd()
105-
session.cd(DEMO_ROOT_FOLDER)
106-
session.run("python", DEMO_ROOT_FOLDER / "scripts" / "setup-git.py", DEMO_ROOT_FOLDER)
107-
session.run("git", "checkout", ".")
108-
session.run(*session.posargs)
109-
session.cd(original_dir)
53+
session.run("python", GENERATE_DEMO_PROJECT_SCRIPT, *GENERATE_DEMO_PROJECT_OPTIONS, *session.posargs)
11054

11155

11256
@nox.session(name="clear-cache", python=DEFAULT_TEMPLATE_PYTHON_VERSION)
@@ -133,19 +77,12 @@ def lint(session: Session):
13377
session.run("ruff", "check", "--verbose", "--fix")
13478

13579

136-
@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION, name="lint-generated-project", tags=[])
137-
def lint_generated_project(session: Session):
80+
@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION, name="match-generated-precommit", tags=[])
81+
def match_generated_precommit(session: Session):
13882
"""Lint the generated project's Python files and configurations."""
13983
session.log("Installing linting dependencies for the generated project...")
14084
session.install("-e", ".", "--group", "dev", "--group", "lint")
141-
session._runner.posargs = ["nox", "-t", "lint", "format"]
142-
in_demo(session)
143-
original_dir: Path = Path.cwd()
144-
session.chdir(DEMO_ROOT_FOLDER)
145-
session.run("git", "commit", "-a", "-m", "meta: lint-generated-project", "--no-verify")
146-
session.chdir(original_dir)
147-
148-
session.run("retrocookie", DEMO_ROOT_FOLDER, "HEAD")
85+
session.run("python", MATCH_GENERATED_PRECOMMIT_SCRIPT, *MATCH_GENERATED_PRECOMMIT_OPTIONS, *session.posargs)
14986

15087

15188
@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION)

scripts/generate-demo-project.py

Lines changed: 2 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,19 @@
11
"""Python script for generating a demo project."""
2-
import os
3-
import shutil
4-
import stat
52
import sys
63
from functools import partial
74
from pathlib import Path
85
from typing import Annotated
9-
from typing import Any
10-
from typing import Callable
116

127
import typer
13-
from cookiecutter.main import cookiecutter
148
from typer.models import OptionInfo
159

10+
from util import generate_demo_project
11+
1612

1713
FolderOption: partial[OptionInfo] = partial(
1814
typer.Option, dir_okay=True, file_okay=False, resolve_path=True, path_type=Path
1915
)
2016

21-
22-
def generate_demo_project(repo_folder: Path, demos_cache_folder: Path, demo_name: str, no_cache: bool) -> Path:
23-
"""Generates a demo project and returns its root path."""
24-
demos_cache_folder.mkdir(exist_ok=True)
25-
if no_cache:
26-
_remove_existing_demo(demo_path=demos_cache_folder / demo_name)
27-
cookiecutter(
28-
template=str(repo_folder),
29-
no_input=True,
30-
extra_context={"project_name": demo_name},
31-
overwrite_if_exists=True,
32-
output_dir=str(demos_cache_folder),
33-
)
34-
return demos_cache_folder / demo_name
35-
36-
37-
def _remove_existing_demo(demo_path: Path) -> None:
38-
"""Removes the existing demo if present."""
39-
if demo_path.exists() and demo_path.is_dir():
40-
previous_demo_pyproject: Path = Path(demo_path, "pyproject.toml")
41-
if not previous_demo_pyproject.exists():
42-
typer.secho(f"No pyproject.toml found at {previous_demo_pyproject=}.", fg="red")
43-
typer.confirm(
44-
"This folder may not be a demo, are you sure you would like to continue?",
45-
default=False,
46-
abort=True,
47-
show_default=True
48-
)
49-
50-
typer.secho(f"Removing existing demo project at {demo_path=}.", fg="yellow")
51-
shutil.rmtree(demo_path, onerror=remove_readonly)
52-
53-
54-
def remove_readonly(func: Callable[[str], Any], path: str, _: Any) -> None:
55-
"""Clears the readonly bit and attempts to call the provided function."""
56-
os.chmod(path, stat.S_IWRITE)
57-
func(path)
58-
59-
6017
cli: typer.Typer = typer.Typer()
6118

6219

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import sys
2+
from pathlib import Path
3+
from typing import Any
4+
5+
import pre_commit.main
6+
import typer
7+
from retrocookie.core import retrocookie
8+
9+
from scripts.util import in_new_demo
10+
11+
12+
cli: typer.Typer = typer.Typer()
13+
14+
15+
@cli.callback(invoke_without_command=True)
16+
def match_generated_precommit(
17+
repo_folder: Path,
18+
demos_cache_folder: Path,
19+
demo_name: str,
20+
no_cache: bool,
21+
**kwargs: Any
22+
) -> None:
23+
"""Runs precommit in a generated project and matches the template to the results."""
24+
try:
25+
with in_new_demo(
26+
repo_folder=repo_folder,
27+
demos_cache_folder=demos_cache_folder,
28+
demo_name=demo_name,
29+
no_cache=no_cache,
30+
**kwargs
31+
) as demo_path:
32+
pre_commit.main.main(["run", "--all-files", "--hook-stage=manual", "--show-diff-on-failure"])
33+
retrocookie(instance_path=demo_path, commits=["HEAD"])
34+
except Exception as error:
35+
typer.secho(f"error: {error}", fg="red")
36+
sys.exit(1)
37+
38+
39+
if __name__ == '__main__':
40+
cli()

scripts/util.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""Module containing utility functions used throughout cookiecutter_robust_python scripts."""
2+
import os
3+
import shutil
4+
import stat
5+
import subprocess
6+
import sys
7+
from contextlib import contextmanager
8+
from functools import partial
9+
from pathlib import Path
10+
from typing import Any
11+
from typing import Callable
12+
from typing import Generator
13+
14+
import typer
15+
from cookiecutter.main import cookiecutter
16+
from cookiecutter.utils import rmtree
17+
from cookiecutter.utils import work_in
18+
from pygments.lexers import q
19+
20+
21+
def remove_readonly(func: Callable[[str], Any], path: str, _: Any) -> None:
22+
"""Clears the readonly bit and attempts to call the provided function.
23+
24+
Meant for use as the onerror callback in shutil.rmtree.
25+
"""
26+
os.chmod(path, stat.S_IWRITE)
27+
func(path)
28+
29+
30+
def run_command(command: str, *args: str) -> subprocess.CompletedProcess:
31+
"""Runs the provided command in a subprocess."""
32+
try:
33+
process = subprocess.run([command, *args], check=True, capture_output=True, text=True)
34+
return process
35+
except subprocess.CalledProcessError as error:
36+
print(error.stdout, end="")
37+
print(error.stderr, end="", file=sys.stderr)
38+
raise
39+
40+
41+
git: partial[subprocess.CompletedProcess] = partial(run_command, "git")
42+
uv: partial[subprocess.CompletedProcess] = partial(run_command, "uv")
43+
44+
45+
@contextmanager
46+
def in_new_demo(
47+
repo_folder: Path,
48+
demos_cache_folder: Path,
49+
demo_name: str,
50+
no_cache: bool,
51+
**kwargs: Any
52+
) -> Generator[Path]:
53+
"""Returns a context manager for working within a new demo."""
54+
demo_path: Path = generate_demo_project(
55+
repo_folder=repo_folder,
56+
demos_cache_folder=demos_cache_folder,
57+
demo_name=demo_name,
58+
no_cache=no_cache,
59+
**kwargs
60+
)
61+
with work_in(demo_path):
62+
yield demo_path
63+
64+
65+
def generate_demo_project(
66+
repo_folder: Path,
67+
demos_cache_folder: Path,
68+
demo_name: str,
69+
no_cache: bool,
70+
**kwargs: Any
71+
) -> Path:
72+
"""Generates a demo project and returns its root path."""
73+
demos_cache_folder.mkdir(exist_ok=True)
74+
if no_cache:
75+
_remove_existing_demo(demo_path=demos_cache_folder / demo_name)
76+
cookiecutter(
77+
template=str(repo_folder),
78+
no_input=True,
79+
extra_context={"project_name": demo_name, **kwargs},
80+
overwrite_if_exists=True,
81+
output_dir=str(demos_cache_folder),
82+
)
83+
return demos_cache_folder / demo_name
84+
85+
86+
def _remove_existing_demo(demo_path: Path) -> None:
87+
"""Removes the existing demo if present."""
88+
if demo_path.exists() and demo_path.is_dir():
89+
previous_demo_pyproject: Path = Path(demo_path, "pyproject.toml")
90+
if not previous_demo_pyproject.exists():
91+
typer.secho(f"No pyproject.toml found at {previous_demo_pyproject=}.", fg="red")
92+
typer.confirm(
93+
"This folder may not be a demo, are you sure you would like to continue?",
94+
default=False,
95+
abort=True,
96+
show_default=True
97+
)
98+
99+
typer.secho(f"Removing existing demo project at {demo_path=}.", fg="yellow")
100+
shutil.rmtree(demo_path, onerror=remove_readonly)
101+
102+
103+
104+

tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ def robust_python_demo_path(demos_folder: Path) -> Path:
3030
extra_context={"project_name": "robust-python-demo", "add_rust_extension": False},
3131
)
3232
path: Path = demos_folder / "robust-python-demo"
33-
subprocess.run(["nox", "-s", "setup-repo"], cwd=path, capture_output=True)
33+
subprocess.run(["nox", "-s", "setup-git"], cwd=path, capture_output=True)
34+
subprocess.run(["nox", "-s", "setup-venv"], cwd=path, capture_output=True)
3435
return path
3536

3637

@@ -47,4 +48,3 @@ def robust_maturin_demo_path(demos_folder: Path) -> Path:
4748
path: Path = demos_folder / "robust-maturin-demo"
4849
subprocess.run(["nox", "-s", "setup-repo"], cwd=path, capture_output=True)
4950
return path
50-

tests/constants.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,8 @@
1919
PYTHON_VERSIONS: list[str] = [f"3.{VERSION_SLUG}" for VERSION_SLUG in range(MIN_PYTHON_SLUG, MAX_PYTHON_SLUG + 1)]
2020
DEFAULT_PYTHON_VERSION: str = PYTHON_VERSIONS[1]
2121

22-
FIRST_TIME_SETUP_NOX_SESSIONS: list[str] = ["setup-git", "setup-venv", "setup-repo"]
2322
TYPE_CHECK_NOX_SESSIONS: list[str] = [f"typecheck-{python_version}" for python_version in PYTHON_VERSIONS]
2423
TESTS_NOX_SESSIONS: list[str] = [f"tests-{python_version}" for python_version in PYTHON_VERSIONS]
25-
CHECK_NOX_SESSIONS: list[str] = [f"check-{python_version}" for python_version in PYTHON_VERSIONS]
26-
FULL_CHECK_NOX_SESSIONS: list[str] = [f"full-check-{python_version}" for python_version in PYTHON_VERSIONS]
27-
28-
2924

3025
GLOBAL_NOX_SESSIONS: list[str] = [
3126
"pre-commit",
@@ -37,9 +32,5 @@
3732
"publish-python",
3833
"release",
3934
"tox",
40-
*CHECK_NOX_SESSIONS,
41-
*FULL_CHECK_NOX_SESSIONS,
4235
"coverage",
4336
]
44-
45-
RUST_NOX_SESSIONS: list[str] = ["tests-rust", "publish-rust"]

tests/integration_tests/test_robust_python_demo.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ def test_demo_project_nox_session(robust_python_demo_path: Path, session: str) -
1919
command,
2020
cwd=robust_python_demo_path,
2121
capture_output=True,
22-
text=True,
23-
timeout=20.0
2422
)
2523
print(result.stdout)
2624
print(result.stderr)

0 commit comments

Comments
 (0)