diff --git a/README.rst b/README.rst
index 219377e..1d08ba6 100644
--- a/README.rst
+++ b/README.rst
@@ -1,3 +1,4 @@
+###########################
Cookiecutter Python Project
###########################
@@ -64,28 +65,121 @@ It is assumed that the new Python package will eventually be:
The generated docs have some references and links to those sites.
+===============
Getting Started
===============
+--------------------
One Time Setup Steps
--------------------
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Install cookiecutter via pip
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
The process for using Cookiecutter to create a new Python package project
starts with installing Cookiecutter. This is best done by creating a new
virtual environment specifically for cookiecutter and then installing
cookiecutter using ``pip``. The example below shows how to do this.
-.. code-block:: console
+.. code-block:: shell-session
$ python -m venv --prompt cc ccvenv
+ $
$ source ccvenv/bin/activate
+ $ # or for cmd.exe:
+ $ # ccvenv\Scripts\activate.bat
+ $ # or for PowerShell:
+ $ # ccvenv\Scripts\Activate.ps1
+ $
(cc) $ pip install -U pip # update pip to avoid any warnings
(cc) $ pip install cookiecutter
-You are now ready to create a new Python project from the Cookiecutter
-template provided by this project.
+^^^^^^^^^^^^^
+Install hatch
+^^^^^^^^^^^^^
+
+If you do not yet have Hatch installed, now would be a good time to do
+so. Refer to the installation instructions for your operating system
+`here `_.
+
+
+^^^^^^^^^^^^^^^^^^^^^^
+Install git (optional)
+^^^^^^^^^^^^^^^^^^^^^^
+
+It may also be a good idea to ensure you have ``git`` installed (and
+it may be required for cookiecutter to function if using it to clone
+this template). Under Windows, you can use `winget
+`_.
+
+.. code-block:: shell-session
+
+ (cc) $ winget install --id Git.Git --exact --source winget
+
+Under macOS you can use `brew `_.
+
+.. code-block:: shell-session
+
+ (cc) $ brew install git
+
+Users of other operating systems likely already have it installed or
+will be able to install it via their operating system's package
+manager.
+
+
+^^^^^^^^^^^^^^^^^^^^^^^
+Install make (optional)
+^^^^^^^^^^^^^^^^^^^^^^^
+
+If you wish to use the fancy Makefile included in this project, you
+may wish to install the ``make`` command. Under Windows, again using
+winget:
+
+.. code-block:: shell-session
+
+ (cc) $ winget install --id GnuWin32.Make --exact --source winget
+
+Unlike with git, you will need to `manually add
+`_ the directory
+containing ``make.exe`` to your PATH, which is typically something like:
+``C:\Program Files(x86)\GnuWin32\bin\``.
+
+Under macOS you can again use brew.
+
+.. code-block:: shell-session
+
+ (cc) $ brew install make
+
+Users of other operating systems should again have no trouble finding
+it in their operating system's package manager.
+
+.. code-block:: console
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+An important note for Windows users running make
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+If you are using the Makefile system, be aware that two of the targets
+(``help`` and ``dist-test``) make use of PowerShell scripts to achieve
+Windows compatibility. These may not run unless you adjust an
+execution policy to permit them. This can be done by opening a Windows
+PowerShell as an administrator (just right-click the launcher and
+select ``Run as Administrator``) and issuing the following command:
+
+.. code-block:: shell-session
+
+ PS > Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
+
+You can read more about this `here
+`_.
+
+Finally, you are ready to create a new Python project from the
+Cookiecutter template provided by this project.
+
+
+--------------------
Create a new project
--------------------
@@ -94,17 +188,18 @@ simply navigate to a directory where you want to create the new project, then
run the ``cookiecutter`` command with a command line argument referencing this
template.
-The easiest method is to reference this template via its GitHub URL (where 'gh'
-is a shortened form for GitHub):
+The easiest method (which will fail if ``git`` is not installed) is to
+reference this template via its GitHub URL (where 'gh' is a shortened
+form for GitHub):
-.. code-block:: console
+.. code-block:: shell-session
(cc) $ cookiecutter gh:boltronics/cookiecutter-python-project
-Alternatively, if you have cloned a local copy of this template you can
-reference it directly:
+Alternatively, if you have cloned or downloaded a local copy of this
+template, you can reference it directly:
-.. code-block:: console
+.. code-block:: shell-session
(cc) $ cookiecutter path/to/cookiecutter-python-project
@@ -115,12 +210,13 @@ shown in order.
Once you have generated your new Python package project you can exit the
cookiecutter virtual environment as it is no longer required.
-.. code-block:: console
+.. code-block:: shell-session
(cc) $ deactivate
$
+--------------------
Manual Modifications
--------------------
@@ -141,6 +237,7 @@ using the new project.
`_.
+=======
Example
=======
@@ -161,7 +258,7 @@ and hyphens in it. The package display name is first converted to lowercase
text and then any spaces or hyphens are converted to underscores to produce a
Python package name.
-.. code-block:: console
+.. code-block:: shell-session
(cc) $ cookiecutter gh:boltronics/cookiecutter-python-project
[1/10] package_display_name (Package-Name): abc 123
@@ -187,18 +284,18 @@ Python package name.
The project has been created in the ``abc_123`` directory.
-.. code-block:: console
+.. code-block:: shell-session
$ cd abc_123
If you are planning to use git, it might be a good idea to create a
new repository at this point.
-.. code-block:: console
+.. code-block:: shell-session
$ git init
$ git add .
- $ git commit -m 'Initial cookiecutter-python-project setup'
+ $ git commit -m "Initial cookiecutter-python-project setup"
With that out of the way, it will be easy to use git to undo any
potential mistakes made while experimenting.
@@ -210,7 +307,7 @@ First, let's enter a project-specific virtual environment. Hatch
will install any of the project's dependencies (if added to pyproject.toml) as well as
the project itself as an editable package.
-.. code-block:: console
+.. code-block:: shell-session
$ hatch shell
(abc_123) $
@@ -224,7 +321,7 @@ There are a number of other virtual environments available to you, and
most of these have their own packages and scripts to ease
development. You can bring up a summary like so:
-.. code-block:: console
+.. code-block:: shell-session
$ hatch env show
Standalone
@@ -261,7 +358,7 @@ development. You can bring up a summary like so:
You can enter use these virtual environments like so:
-.. code-block:: console
+.. code-block:: shell-session
$ hatch shell types
(types) $ pip freeze
@@ -294,7 +391,7 @@ If you have make installed, the included Makefile provides handy
shortcuts for various Hatch commands and the configured scripts. You
can print a summary of options via the `make help` command, like so:
-.. code-block:: console
+.. code-block:: shell-session
$ make help
@@ -323,10 +420,12 @@ can print a summary of options via the `make help` command, like so:
dist-test - test a wheel distribution package
dist-upload - upload a wheel distribution package
+ $
+
Here is an example of one in action:
-.. code-block:: console
+.. code-block:: shell-session
$ make test-verbose
────────────────────────────── hatch-test.py3.13 ───────────────────────────────
@@ -384,6 +483,7 @@ Here is an example of one in action:
$
+=====================================
Suggestions? Contributions? Problems?
=====================================
diff --git a/{{cookiecutter.package_name}}/Makefile b/{{cookiecutter.package_name}}/Makefile
index 781aece..6f26179 100644
--- a/{{cookiecutter.package_name}}/Makefile
+++ b/{{cookiecutter.package_name}}/Makefile
@@ -1,9 +1,22 @@
# This makefile has been created to help developers perform common actions.
-# Most actions assume it is operating in a virtual environment where the
-# python command links to the appropriate virtual environment Python.
MAKEFLAGS += --no-print-directory
-GIT := $(shell command -v git)
+ifeq ($(OS),Windows_NT)
+ UNAME_S := Windows
+else
+ UNAME_S := $(shell uname -s)
+endif
+
+# Set GIT variable based on the operating system
+ifeq ($(UNAME_S), Linux)
+ GIT := $(shell command -v git)
+endif
+ifeq ($(UNAME_S), Darwin)
+ GIT := $(shell command -v git)
+endif
+ifeq ($(UNAME_S), Windows)
+ GIT := $(shell where git)
+endif
define CHECK_GIT
@@ -21,11 +34,16 @@ endef
# help: help - display makefile help information
.PHONY: help
+ifeq ($(UNAME_S), Windows)
+help:
+ @powershell -File scripts/generate_help.ps1 -MakefilePath Makefile
+else
help:
@grep "^#\shelp:" Makefile | \
grep -v grep | \
sed 's/\# help\: //' | \
sed 's/\# help\://'
+endif
# help: venv - enter a dev virtual environment
@@ -143,10 +161,18 @@ dist:
# help: dist-test - test a wheel distribution package
.PHONY: dist-test
+ifeq ($(UNAME_S), Windows)
+dist-test: dist
+ @cd dist && \
+ @powershell \
+ -File ../tests/test-dist.ps1 \
+ ./{{cookiecutter.package_name}}-*-py3-none-any.whl
+else
dist-test: dist
@cd dist && \
../tests/test-dist.bash \
./{{cookiecutter.package_name}}-*-py3-none-any.whl
+endif
# help: dist-upload - upload a wheel distribution package
diff --git a/{{cookiecutter.package_name}}/pyproject.toml b/{{cookiecutter.package_name}}/pyproject.toml
index 8b9ac9f..14a4dcc 100644
--- a/{{cookiecutter.package_name}}/pyproject.toml
+++ b/{{cookiecutter.package_name}}/pyproject.toml
@@ -111,12 +111,7 @@ extra-dependencies = [
[tool.hatch.envs.docs.scripts]
build = [
- """
- rm -rf \
- docs/source/api/sitemap_generator*.rst \
- docs/source/api/modules.rst \
- docs/build/*
- """,
+ "python scripts/cleanup_docs.py",
"sphinx-build -M html docs/source docs/build",
]
build-dummy = [
diff --git a/{{cookiecutter.package_name}}/scripts/cleanup_docs.py b/{{cookiecutter.package_name}}/scripts/cleanup_docs.py
new file mode 100644
index 0000000..b22ef02
--- /dev/null
+++ b/{{cookiecutter.package_name}}/scripts/cleanup_docs.py
@@ -0,0 +1,18 @@
+"""Remove docs build files and generated API docs"""
+
+import shutil
+from pathlib import Path
+
+paths = [
+ "docs/build",
+ "docs/source/api/modules.rst",
+ "docs/source/api/{{cookiecutter.package_name}}*.rst",
+]
+
+for path in paths:
+ p = Path(path)
+ if p.exists():
+ if p.is_dir():
+ shutil.rmtree(p)
+ else:
+ p.unlink()
diff --git a/{{cookiecutter.package_name}}/scripts/generate_help.ps1 b/{{cookiecutter.package_name}}/scripts/generate_help.ps1
new file mode 100644
index 0000000..6d69fec
--- /dev/null
+++ b/{{cookiecutter.package_name}}/scripts/generate_help.ps1
@@ -0,0 +1,11 @@
+param (
+ [string]$MakefilePath = "Makefile"
+)
+
+if (Test-Path $MakefilePath) {
+ Get-Content $MakefilePath | Select-String '^#\shelp:' | ForEach-Object {
+ $_.Line -replace '# help: ', '' -replace '# help:', ''
+ }
+} else {
+ Write-Error "Makefile not found at path: $MakefilePath"
+}
diff --git a/{{cookiecutter.package_name}}/tests/test-dist.ps1 b/{{cookiecutter.package_name}}/tests/test-dist.ps1
new file mode 100644
index 0000000..660bc1f
--- /dev/null
+++ b/{{cookiecutter.package_name}}/tests/test-dist.ps1
@@ -0,0 +1,51 @@
+param (
+ [string]$ReleaseArchivePattern = "./package_name-*-py3-none-any.whl"
+)
+
+if (-not $ReleaseArchivePattern) {
+ Write-Host "usage: .\test-dist.ps1 package_name-YY.MM.MICRO-py3-none-any.whl"
+ exit 1
+}
+
+# Set ErrorActionPreference to Stop to treat all errors as terminating
+$ErrorActionPreference = "Stop"
+
+try {
+ $ReleaseArchive = Get-ChildItem -Path $ReleaseArchivePattern | Select-Object -First 1
+
+ if (-not $ReleaseArchive) {
+ Write-Host "No matching release archive found for pattern: $ReleaseArchivePattern"
+ exit 1
+ }
+
+ Write-Host "Release archive: $ReleaseArchive"
+
+ Write-Host "Removing any old artefacts"
+ if (Test-Path -Path test_venv) {
+ Remove-Item -Recurse -Force test_venv
+ }
+
+ Write-Host "Creating test virtual environment"
+ python -m venv test_venv
+
+ Write-Host "Entering test virtual environment"
+ & .\test_venv\Scripts\Activate.ps1
+
+ Write-Host "Upgrading pip"
+ pip install --upgrade pip
+
+ Write-Host "Installing $ReleaseArchive"
+ pip install $ReleaseArchive.FullName
+
+ Write-Host "Running tests"
+ python -m unittest discover -s ..
+
+ Write-Host "Exiting test virtual environment"
+ deactivate
+
+ Write-Host "Removing test virtual environment"
+ Remove-Item -Recurse -Force test_venv
+} catch {
+ Write-Host "An error occurred: $_"
+ exit 1
+}
diff --git a/{{cookiecutter.package_name}}/tests/test_examples.py b/{{cookiecutter.package_name}}/tests/test_examples.py
index 549166d..fa28ab8 100644
--- a/{{cookiecutter.package_name}}/tests/test_examples.py
+++ b/{{cookiecutter.package_name}}/tests/test_examples.py
@@ -3,6 +3,7 @@
suite.
"""
+import logging
import os
import shlex
import subprocess
@@ -39,15 +40,22 @@ def run_in_venv(
original_cwd = os.getcwd()
script_dir = os.path.join(REPO_DIR, os.path.dirname(filepath))
filename = os.path.basename(filepath)
- args = shlex.split(
- (
- f'/bin/bash -c "source {VENV_DIR}/bin/activate '
- f'&& python {filename}"'
+
+ if os.name == "nt":
+ # Windows
+ activate_cmd = f"{VENV_DIR}\\Scripts\\activate"
+ args = shlex.split(
+ f'cmd.exe /c "{activate_cmd} && python {filename}"'
+ )
+ else:
+ # Unix-like systems
+ activate_cmd = f". {VENV_DIR}/bin/activate"
+ args = shlex.split(
+ f'sh -c "{activate_cmd} && python {filename}"'
)
- )
- env = {}
- if os.environ["PATH"]:
+ env: dict[str, str] = {}
+ if os.environ.get("PATH"):
env["PATH"] = os.environ["PATH"]
if "LD_LIBRARY_PATH" in os.environ:
env["LD_LIBRARY_PATH"] = os.environ["LD_LIBRARY_PATH"]
@@ -55,26 +63,28 @@ def run_in_venv(
if popen_kwargs is None:
popen_kwargs = {}
- popen_default_kwargs = {
+ popen_default_kwargs: dict[str, Any] = {
"env": env,
+ "cwd": script_dir,
+ "timeout": timeout,
"stdout": subprocess.PIPE,
"stderr": subprocess.PIPE,
- "shell": False,
}
+ popen_default_kwargs.update(popen_kwargs)
try:
- os.chdir(script_dir)
- with subprocess.Popen(
- args=args,
- **{**popen_default_kwargs, **popen_kwargs},
- ) as proc:
- _out, _err = proc.communicate(timeout=timeout)
- returncode = proc.returncode
+ subprocess.run(args, **popen_default_kwargs, check=True)
+ return True
+ except subprocess.CalledProcessError as error:
+ logging.error("Error: %s", error)
+ if error.stdout:
+ logging.error("stdout: %s", error.stdout.decode())
+ if error.stderr:
+ logging.error("stderr: %s", error.stderr.decode())
+ return False
finally:
os.chdir(original_cwd)
- return returncode == 0
-
def test_quickstart_example(self) -> None:
"""check quickstart example"""
assert (