Skip to content

Commit c1c51f4

Browse files
authored
Merge pull request #12 from 56kyle/develop
Many small changes and general workflow tweaks
2 parents aa00508 + e1fab3c commit c1c51f4

File tree

17 files changed

+327
-261
lines changed

17 files changed

+327
-261
lines changed

noxfile.py

Lines changed: 7 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -41,70 +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(*session.posargs)
107-
session.cd(original_dir)
53+
session.run("python", GENERATE_DEMO_PROJECT_SCRIPT, *GENERATE_DEMO_PROJECT_OPTIONS, *session.posargs)
10854

10955

11056
@nox.session(name="clear-cache", python=DEFAULT_TEMPLATE_PYTHON_VERSION)
@@ -131,15 +77,12 @@ def lint(session: Session):
13177
session.run("ruff", "check", "--verbose", "--fix")
13278

13379

134-
@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION, name="lint-generated-project", tags=[])
135-
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):
13682
"""Lint the generated project's Python files and configurations."""
13783
session.log("Installing linting dependencies for the generated project...")
13884
session.install("-e", ".", "--group", "dev", "--group", "lint")
139-
session._runner.posargs = ["nox", "-s", "pre-commit"]
140-
in_demo(session)
141-
session._runner.posargs = [""]
142-
session.run("retrocookie")
85+
session.run("python", MATCH_GENERATED_PRECOMMIT_SCRIPT, *MATCH_GENERATED_PRECOMMIT_OPTIONS, *session.posargs)
14386

14487

14588
@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION)

scripts/generate-demo-project.py

Lines changed: 2 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,13 @@
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
14-
from typer.models import OptionInfo
158

16-
17-
FolderOption: partial[OptionInfo] = partial(
18-
typer.Option, dir_okay=True, file_okay=False, resolve_path=True, path_type=Path
19-
)
20-
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)
9+
from util import FolderOption
10+
from util import generate_demo_project
5811

5912

6013
cli: typer.Typer = typer.Typer()
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import sys
2+
from pathlib import Path
3+
from typing import Annotated
4+
5+
import pre_commit.main
6+
import typer
7+
from retrocookie.core import retrocookie
8+
9+
from util import git
10+
from util import FolderOption
11+
from util import in_new_demo
12+
13+
14+
cli: typer.Typer = typer.Typer()
15+
16+
17+
@cli.callback(invoke_without_command=True)
18+
def match_generated_precommit(
19+
repo_folder: Annotated[Path, FolderOption("--repo-folder", "-r")],
20+
demos_cache_folder: Annotated[Path, FolderOption("--demos-cache-folder", "-c")],
21+
demo_name: Annotated[str, typer.Option("--demo-name", "-d")],
22+
no_cache: Annotated[bool, typer.Option("--no-cache", "-n")] = False,
23+
) -> None:
24+
"""Runs precommit in a generated project and matches the template to the results."""
25+
try:
26+
with in_new_demo(
27+
repo_folder=repo_folder,
28+
demos_cache_folder=demos_cache_folder,
29+
demo_name=demo_name,
30+
no_cache=no_cache
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+
git("checkout", "HEAD", "--", "{{cookiecutter.project_name}}/pyproject.toml")
35+
except Exception as error:
36+
typer.secho(f"error: {error}", fg="red")
37+
sys.exit(1)
38+
39+
40+
if __name__ == '__main__':
41+
cli()

scripts/sync-uv-with-demo.py

Lines changed: 0 additions & 60 deletions
This file was deleted.

scripts/util.py

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

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-

0 commit comments

Comments
 (0)