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 (