Skip to content

Commit 44a2444

Browse files
authored
Merge pull request #13
General Fixes
2 parents c1c51f4 + 6adeb38 commit 44a2444

File tree

10 files changed

+103
-74
lines changed

10 files changed

+103
-74
lines changed

.pytest.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[pytest]
2+
addopts = --show-capture=all

README.md

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,23 @@
33
A Python project template robust enough to follow up [cookiecutter-hypermodern-python]
44

55
# Caveat
6-
I really believe this idea has a lot of good ideas and best practices, however creating it is a ton of work.
6+
I really believe this idea has a lot of good ideas and best practices, however, creating it is a ton of work.
77

88
There is definitely a lot left to do before this project is truly a daily driver, but I think there are just a few more pieces missing from this being at least useful in many cases.
99

10-
If you have any interest in this project please don't hesitate to reach out!
11-
Any and all advice, support, PR's, etc are welcome and would be greatly appreciated.
10+
If you have any interest in this project, please don't hesitate to reach out!
11+
Any advice, support, PR's, etc. are welcome and would be greatly appreciated.
1212

1313

1414
# Why does this project exist?
1515
Unfortunately, the [Hypermodern Python Cookiecutter] is no longer maintained nor modern.
1616
While it will always have a place in my heart, there have been far too many improvements in Python tooling to keep using it as is.
1717

18-
For a while I maintained [a personal fork](https://github.com/56kyle/cookiecutter-hypermodern-python) that I would update, however when it came time to switch
19-
to new tooling such as [ruff], [uv], [maturin], etc, I found the process of updating the existing tooling to be extremly painful.
18+
For a while I maintained [a personal fork](https://github.com/56kyle/cookiecutter-hypermodern-python) that I would update, however, when it came time to switch
19+
to new tooling such as [ruff], [uv], [maturin], etc., I found the process of updating the existing tooling to be extremely painful.
2020

21-
The [Hypermodern Python Cookiecutter] remains as a fantastic sendoff point for devs interested in building a 2021 style Python Package, but there were
22-
a handfull of issues with it that prevented it from being able to adapt to new Python developments over the years.
21+
The [Hypermodern Python Cookiecutter] remains as a fantastic sendoff point for devs interested in building a 2021-style Python Package. However, there were
22+
a handful of issues with it that prevented it from being able to adapt to new Python developments over the years.
2323

2424
# Okay, so what's different this time?
2525
The [Robust Python Cookiecutter] exists to solve a few main concerns
@@ -30,7 +30,7 @@ The [Robust Python Cookiecutter] exists to solve a few main concerns
3030
- [Project Neglect](#project-neglect)
3131

3232

33-
## Template Update Propogation
33+
## Template Update Propagation
3434
One of the main issues I encountered with [my personal fork] of the [Hypermodern Python Cookiecutter] was that any change
3535
I made to my repos would mean a later conflict if I tried to rerun [cookiecutter] to sync a change from a different project.
3636

@@ -42,9 +42,9 @@ in the pyproject.toml.
4242

4343

4444
## Project Domain Expansion
45-
Now, I'm not one to advocate for mixing languages together in a project. However, there is a really unique case that has arisen with the creation of [maturin].
45+
Now, I'm not one to advocate for mixing languages in a project. However, there is a unique case that has arisen with the creation of [maturin].
4646

47-
There are a plethora of great projects such as [ruff], [uv], [polars], [just], etc all making use of [maturin] to get the performance improvements of [rust] while
47+
There are a plethora of great projects such as [ruff], [uv], [polars], [just], etc. all making use of [maturin] to get the performance improvements of [rust] while
4848
submitting their package to both pypi and crates.io
4949

5050
Now, this definitely is not required by any means to make a good Python package, however this pattern only seems to be picking up momentum and has honestly been a massive boon
@@ -60,9 +60,6 @@ Additionally, the [Robust Python Cookiecutter] is designed with both normal and
6060
a quick [rust] module for performance or you are trying to publish a series of crates and packages, either case will be handled using a setup inspired by [polars].
6161

6262

63-
64-
65-
6663
## Documenting Tooling Decisions
6764
One of the really stand out features of the [Hypermodern Python Cookiecutter] was its incredibly detailed documentation.
6865
It did a pretty great job of describing the tooling to use, but there was a distinct lack of ***why*** these decisions were made.
@@ -72,11 +69,11 @@ It may seem like a small detail, but detailing why a decision was made has an in
7269
Rather than having to go through a mini crusade to determine whether we use [poetry] or [uv], we can just point to the
7370
[existing reasoning](https://cookiecutter-robust-python.readthedocs.io/en/latest/topics/02_dependency_management.md#option-2--term--poetry) to see if it still is true or not.
7471

75-
Overall it's rather rare that people debate over tooling for no reason. Most things have merit in some cases, and a large goal of this template is identifying the tools that have the most merit in almost all cases.
72+
Overall, it's rather rare that people debate over tooling for no reason. Most things have merit in some cases, and a large goal of this template is identifying the tools that have the most merit in almost all cases.
7673

7774
## CI/CD Vendor Lock
7875
Now don't get me wrong, I love [github-actions] and do pretty much everything in my power to avoid [bitbucket-pipelines].
79-
However, not all jobs have the luxury of github, and I would love to be able to just use the same template for both my personal and professional projects.
76+
However, not all jobs have the luxury of GitHub, and I would love to be able to just use the same template for both my personal and professional projects.
8077

8178
The [Robust Python Cookiecutter] focuses on being as modular as possible for areas that connect to the CI/CD pipeline. Additionally, there will always be either alternative
8279
CI/CD options or at a minimum basic examples of what the translated CI/CD pipeline would look like.
@@ -94,7 +91,7 @@ implementing best practices in their python packages.
9491

9592
However, Open Source work is draining, and is especially so for a project template including metacode.
9693

97-
I can guarantee that if the [Robust Python Cookiecutter] ever sees any amount of users I will immediately transfer it to an organization to enable at least a handful
94+
I can guarantee that if the [Robust Python Cookiecutter] ever sees any number of users, I will immediately transfer it to an organization to enable at least a handful
9895
of trusted individuals to ensure the project is taken care of.
9996

10097
[bitbucket-pipelines]: https://support.atlassian.com/bitbucket-cloud/docs/write-a-pipe-for-bitbucket-pipelines/

tests/conftest.py

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import subprocess
44
from pathlib import Path
5+
from typing import Any
56

67
import pytest
8+
from _pytest.fixtures import FixtureRequest
79
from _pytest.tmpdir import TempPathFactory
810
from cookiecutter.main import cookiecutter
911

@@ -20,31 +22,53 @@ def demos_folder(tmp_path_factory: TempPathFactory) -> Path:
2022

2123

2224
@pytest.fixture(scope="session")
23-
def robust_python_demo_path(demos_folder: Path) -> Path:
24-
"""Creates a temporary example python project for testing against and returns its Path."""
25+
def robust_demo(
26+
request: FixtureRequest,
27+
demos_folder: Path,
28+
robust_demo__path: Path,
29+
robust_demo__extra_context: dict[str, Any],
30+
robust_demo__is_setup: bool
31+
) -> Path:
2532
cookiecutter(
2633
str(REPO_FOLDER),
2734
no_input=True,
2835
overwrite_if_exists=True,
2936
output_dir=demos_folder,
30-
extra_context={"project_name": "robust-python-demo", "add_rust_extension": False},
37+
extra_context=robust_demo__extra_context,
3138
)
32-
path: Path = demos_folder / "robust-python-demo"
33-
subprocess.run(["nox", "-s", "setup-git"], cwd=path, capture_output=True)
34-
subprocess.run(["nox", "-s", "setup-venv"], cwd=path, capture_output=True)
35-
return path
39+
if robust_demo__is_setup:
40+
subprocess.run(["nox", "-s", "setup-git"], cwd=robust_demo__path, capture_output=True)
41+
subprocess.run(["nox", "-s", "setup-venv"], cwd=robust_demo__path, capture_output=True)
42+
return getattr(request, "param", robust_demo__path)
3643

3744

3845
@pytest.fixture(scope="session")
39-
def robust_maturin_demo_path(demos_folder: Path) -> Path:
40-
"""Creates a temporary example maturin project for testing against and returns its Path."""
41-
cookiecutter(
42-
str(REPO_FOLDER),
43-
no_input=True,
44-
overwrite_if_exists=True,
45-
output_dir=demos_folder,
46-
extra_context={"project_name": "robust-maturin-demo", "add_rust_extension": True}
47-
)
48-
path: Path = demos_folder / "robust-maturin-demo"
49-
subprocess.run(["nox", "-s", "setup-repo"], cwd=path, capture_output=True)
50-
return path
46+
def robust_demo__path(request: FixtureRequest, demos_folder: Path, robust_demo__name: str) -> Path:
47+
return getattr(request, "param", demos_folder / robust_demo__name)
48+
49+
50+
@pytest.fixture(scope="session")
51+
def robust_demo__name(request: FixtureRequest) -> str:
52+
return getattr(request, "param", "robust-python-demo-with-setup")
53+
54+
55+
@pytest.fixture(scope="session")
56+
def robust_demo__extra_context(
57+
request: FixtureRequest,
58+
robust_demo__name: str,
59+
robust_demo__add_rust_extension: bool
60+
) -> dict[str, Any]:
61+
return getattr(request, "param", {
62+
"project_name": robust_demo__name,
63+
"add_rust_extension": robust_demo__add_rust_extension
64+
})
65+
66+
67+
@pytest.fixture(scope="session")
68+
def robust_demo__add_rust_extension(request: FixtureRequest) -> bool:
69+
return getattr(request, "param", False)
70+
71+
72+
@pytest.fixture(scope="session")
73+
def robust_demo__is_setup(request: FixtureRequest) -> bool:
74+
return getattr(request, "param", True)

tests/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
DEFAULT_PYTHON_VERSION: str = PYTHON_VERSIONS[1]
2121

2222
TYPE_CHECK_NOX_SESSIONS: list[str] = [f"typecheck-{python_version}" for python_version in PYTHON_VERSIONS]
23-
TESTS_NOX_SESSIONS: list[str] = [f"tests-{python_version}" for python_version in PYTHON_VERSIONS]
23+
TESTS_NOX_SESSIONS: list[str] = [f"tests-python-{python_version}" for python_version in PYTHON_VERSIONS]
2424

2525
GLOBAL_NOX_SESSIONS: list[str] = [
2626
"pre-commit",

tests/integration_tests/test_robust_python_demo.py

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,43 +8,50 @@
88
from tests.constants import GLOBAL_NOX_SESSIONS
99

1010

11-
def test_demo_project_generation(robust_python_demo_path: Path) -> None:
12-
assert robust_python_demo_path.exists()
11+
def test_demo_project_generation(robust_python_demo_with_setup: Path) -> None:
12+
assert robust_python_demo_with_setup.exists()
1313

1414

1515
@pytest.mark.parametrize("session", GLOBAL_NOX_SESSIONS)
16-
def test_demo_project_nox_session(robust_python_demo_path: Path, session: str) -> None:
16+
def test_demo_project_nox_session(robust_demo: Path, session: str) -> None:
1717
command: list[str] = ["nox", "-s", session]
18-
result: subprocess.CompletedProcess = subprocess.run(
19-
command,
20-
cwd=robust_python_demo_path,
21-
capture_output=True,
22-
)
23-
print(result.stdout)
24-
print(result.stderr)
25-
result.check_returncode()
26-
27-
28-
def test_demo_project_nox_pre_commit(robust_python_demo_path: Path) -> None:
18+
try:
19+
subprocess.run(
20+
command,
21+
cwd=robust_demo,
22+
stdout=subprocess.PIPE,
23+
stderr=subprocess.STDOUT,
24+
text=True,
25+
check=True
26+
)
27+
except subprocess.CalledProcessError as e:
28+
pytest.fail(
29+
f"nox session '{session}' failed with exit code {e.returncode}\n"
30+
f"{'-'*20} STDOUT {'-'*20}\n{e.stdout}\n"
31+
f"{'-'*20} STDERR {'-'*20}\n{e.stderr}"
32+
)
33+
34+
35+
def test_demo_project_nox_pre_commit(robust_demo: Path) -> None:
2936
command: list[str] = ["nox", "-s", "pre-commit"]
3037
result: subprocess.CompletedProcess = subprocess.run(
3138
command,
32-
cwd=robust_python_demo_path,
39+
cwd=robust_demo,
3340
capture_output=True,
3441
text=True,
3542
timeout=20.0
3643
)
3744
assert result.returncode == 0
3845

3946

40-
def test_demo_project_nox_pre_commit_with_install(robust_python_demo_path: Path) -> None:
47+
def test_demo_project_nox_pre_commit_with_install(robust_demo: Path) -> None:
4148
command: list[str] = ["nox", "-s", "pre-commit", "--", "install"]
42-
pre_commit_hook_path: Path = robust_python_demo_path / ".git" / "hooks" / "pre-commit"
49+
pre_commit_hook_path: Path = robust_demo / ".git" / "hooks" / "pre-commit"
4350
assert not pre_commit_hook_path.exists()
4451

4552
result: subprocess.CompletedProcess = subprocess.run(
4653
command,
47-
cwd=robust_python_demo_path,
54+
cwd=robust_demo,
4855
capture_output=True,
4956
text=True,
5057
timeout=20.0

{{cookiecutter.project_name}}/noxfile.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,14 @@ def tests_python(session: Session) -> None:
157157
test_results_dir.mkdir(parents=True, exist_ok=True)
158158
junitxml_file = test_results_dir / f"test-results-py{session.python}.xml"
159159

160-
session.run("pytest", "--cov={}".format(PACKAGE_NAME), "--cov-report=xml", f"--junitxml={junitxml_file}", "tests/")
160+
session.run(
161+
"pytest",
162+
"--cov={}".format(PACKAGE_NAME),
163+
"--cov-report=term",
164+
"--cov-report=xml",
165+
f"--junitxml={junitxml_file}",
166+
"tests/"
167+
)
161168

162169

163170
{% if cookiecutter.add_rust_extension == 'y' -%}
@@ -190,10 +197,6 @@ def docs_build(session: Session) -> None:
190197
@nox.session(python=DEFAULT_PYTHON_VERSION, name="build-python", tags=[BUILD, PYTHON])
191198
def build_python(session: Session) -> None:
192199
"""Build sdist and wheel packages (uv build)."""
193-
session.log("Installing build dependencies...")
194-
# Sync core & dev deps are needed for accessing project source code.
195-
session.install("-e", ".", "--group", "dev")
196-
197200
session.log(f"Building sdist and wheel packages with py{session.python}.")
198201
{% if cookiecutter.add_rust_extension == "y" -%}
199202
session.run("maturin", "develop", "--uv")
@@ -252,7 +255,7 @@ def publish_python(session: Session) -> None:
252255
Requires packages to be built first (`nox -s build-python` or `nox -s build`).
253256
Requires TWINE_USERNAME/TWINE_PASSWORD or TWINE_API_KEY environment variables set (usually in CI).
254257
"""
255-
session.install("-e", ".", "--group", "dev")
258+
session.install("twine")
256259

257260
session.log("Checking built packages with Twine.")
258261
session.run("twine", "check", "dist/*")
@@ -280,8 +283,6 @@ def release(session: Session) -> None:
280283
Optionally accepts increment (major, minor, patch) after '--'.
281284
"""
282285
session.log("Running release process using Commitizen...")
283-
session.install("-e", ".", "--group", "dev")
284-
285286
try:
286287
session.run("git", "version", success_codes=[0], external=True, silent=True)
287288
except CommandFailed:

{{cookiecutter.project_name}}/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,13 @@ Repository = "https://github.com/{{ cookiecutter.github_user | lower | replace('
4949

5050
{% if cookiecutter.add_rust_extension == 'y' -%}
5151
[build-system]
52-
requires = "maturin>=1.3.0,<2.0"
52+
requires = ["maturin>=1.3.0,<2.0"]
5353
build-backend = "maturin"
5454

5555
[tool.maturin]
5656
rust-src = "rust"
5757
{% else -%}
5858
[build-system]
59-
requires = "setuptools>=61.0"
59+
requires = ["setuptools>=61.0"]
6060
build-backend = "setuptools.build_meta"
6161
{% endif -%}

{{cookiecutter.project_name}}/scripts/setup-venv.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Script responsible for first time setup of the project's venv.
22
3-
Since this a first time setup script, we intentionally only use builtin Python dependencies.
3+
Since this is a first time setup script, we intentionally only use builtin Python dependencies.
44
"""
55

66
import argparse

{{cookiecutter.project_name}}/scripts/util.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,12 @@ class MissingDependencyError(Exception):
1313

1414
def __init__(self, project: Path, dependency: str):
1515
"""Initializes MisssingDependencyError."""
16-
super().__init__(
17-
"\n".join(
18-
[
19-
f"Unable to find {dependency=}.",
20-
f"Please ensure that {dependency} is installed before setting up the repo at {project.absolute()}",
21-
]
22-
)
23-
)
16+
message_lines: list[str] = [
17+
f"Unable to find {dependency=}.",
18+
f"Please ensure that {dependency} is installed before setting up the repo at {project.absolute()}",
19+
]
20+
message: str = "\n".join(message_lines)
21+
super().__init__(message)
2422

2523

2624
def check_dependencies(path: Path, dependencies: list[str]) -> None:

{{cookiecutter.project_name}}/src/{{cookiecutter.package_name}}/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ def main() -> None:
1212

1313

1414
if __name__ == "__main__":
15-
app()
15+
app() # pragma: no cover

0 commit comments

Comments
 (0)