Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.venv
**/__pycache__/
**/*.egg-info/
**/*.pyc
7 changes: 5 additions & 2 deletions .github/workflows/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ permissions:

jobs:
test-python:
uses: NERC-CEH/dri-cicd/.github/workflows/test-python.yml@main
uses: NERC-CEH/dri-cicd/.github/workflows/test-python.yml@uv-action
with:
use_uv: true

build-test-deploy-docker:
needs: [test-python]
uses: NERC-CEH/dri-cicd/.github/workflows/build-test-deploy-docker.yml@main
uses: NERC-CEH/dri-cicd/.github/workflows/build-test-deploy-docker.yml@uv-action
with:
use_uv: true
package_name: mypackage
secrets:
AWS_REGION: ${{ secrets.AWS_REGION }}
Expand Down
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12
58 changes: 35 additions & 23 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,25 +1,37 @@
# Build virtualenv
FROM python:3.12-slim as build
# N.B. The Python versions in the builder and prod images must match.
# Make sure to update *both* FROM lines when making changes!

FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder

# Disable Python downloads, because we want to use the system interpreter
# across both images.
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=0

RUN apt update && apt install -y --no-install-recommends git

WORKDIR /app
COPY pyproject.toml README.md /app/

RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
--mount=type=bind,source=src/mypackage/__init__.py,target=src/mypackage/__init__.py \
uv sync --locked --no-install-project --no-dev

COPY pyproject.toml uv.lock /app/
# COPY .git /app/.git
COPY src /app/src
COPY .git /app/.git
RUN pip install --upgrade pip pdm
# Installs the codebase in editable mode into .venv
RUN pdm install

# Build production containerdocker
# Only the ./.venv ./src ./tests are present in the production image
FROM python:3.12-slim as prod
WORKDIR /app
RUN groupadd -g 999 python && \
useradd -m -r -u 999 -g python python
RUN chown python:python /app
COPY --chown=python:python --from=build /app/.venv /app/.venv
COPY --chown=python:python --from=build /app/src /app/src
COPY --chown=python:python tests/ /app/tests

USER python
ENV PATH="/app/.venv/bin:$PATH"
ENV VIRTUAL_ENV="/app/.venv"
CMD ["python", "-m", "mypackage"]
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev

FROM python:3.12-slim-bookworm AS prod

# Copy the application from the builder
COPY --from=builder --chown=app:app /app /app

# Place executables in the environment at the front of the path
ENV PATH="/app/.venv/bin:$PATH" VIRTUAL_ENV="/app/.venv"

# Unset entrypoint from parent image
ENTRYPOINT []

CMD ["python", "-m", "mypackage"]
54 changes: 14 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,45 +24,26 @@ This repository is a template for a basic Python project. Included here is:

From the root directory of the repo, run:

```
```console
git config --local core.hooksPath .githooks/
```

This will set this repo up to use the git hooks in the `.githooks/` directory. The hook runs `ruff format --check` and `ruff check` to prevent commits that are not formatted correctly or have errors. The hook intentionally does not alter the files, but informs the user which command to run.

### Installing the package

This package is configured to use optional dependencies based on what you are doing with the code.

As a user, you would install the code with only the dependencies needed to run it:

```
pip install .
```

To work on the docs:

```
pip install -e .[docs]
```

To work on tests:

```
pip install -e .[tests]
```

To run the linter and githook:
You can install everything needed to run the project (even including
Python) with [uv](https://docs.astral.sh/uv).

```
pip install -e .[lint]
```console
uv sync
```

The docs, tests, and linter packages can be installed together with:

```
pip install -e .[dev]
```
It will set up a Python virtualenv in `.venv`. Activate it as normal
with `. .venv/bin/activate` or prefix commands with `uv run`. In
fact, `uv run` will automatically set things up with no need for `uv
sync`. You can add packages with `uv add` and remove them with `uv
remove`.

### Making it Your Own

Expand All @@ -75,15 +56,15 @@ This repo has a single package in the `./src/...` path called `mypackage` (creat

To make thing move a bit faster, use the script `./rename-package.sh` to rename all references of `mypackage` to whatever you like. For example:

```
```console
./rename-package.sh "acoolnewname"
```

Will rename the package and all references to "acoolnewname"

After doing this it is recommended to also run:

```
```console
cd docs
make apidoc
```
Expand All @@ -100,10 +81,7 @@ The documentation is driven by [Sphinx](https://www.sphinx-doc.org/) an industry

To run `sphinx-apidoc` run:

```
# Install your package with optional dependencies for docs
pip install -e .[docs]

```console
cd docs
make apidoc
```
Expand All @@ -116,10 +94,7 @@ Documentation can then be built locally by running `make html`, or found on the

To run the tests run:

```
#Install package with optional dependencies for testing
pip install -e .[test]

```console
pytest
```

Expand All @@ -142,4 +117,3 @@ The python code is packaged into a docker image and pushed to the AWS ECR. For t
* AWS_REGION: \<our-region\>
* AWS_ROLE_ARN: \<the-IAM-role-used-to-deploy\>
* Add a repository to the ECR with the same name as the GitHub repo

15 changes: 7 additions & 8 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@

import mypackage

project = 'My Project'
copyright = '2024, UKCEH'
author = 'UKCEH'
project = "My Project"
project_copyright = "2024, UKCEH"
author = "UKCEH"

release = mypackage.__version__
version = release
Expand All @@ -37,13 +37,12 @@
"python": ("https://docs.python.org/3", None),
}

templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']

templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]


# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output

html_theme = 'sphinx_rtd_theme'
html_static_path = ['_static']
html_theme = "sphinx_rtd_theme"
html_static_path = ["_static"]
28 changes: 13 additions & 15 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
[build-system]
requires = ["setuptools >= 61.0", "autosemver"]
# build-backend = "setuptools.build_meta"

[project]
requires-python = ">=3.12"
dependencies = [
"autosemver"
"autosemver",
# autosemver uses some deprecated setuptools APIs
# https://github.com/david-caro/python-autosemver/issues/56
"setuptools < 81",
]
name = "mypackage"
dynamic = ["version"]
authors = [{ name = "John Doe", email = "johdoe@ceh.ac.uk" }]
description = "A minimal setup for a template package."

[project.optional-dependencies]
test = ["pytest", "pytest-cov", "parameterized"]
[dependency-groups]
test = ["pytest", "pytest-cov"]
docs = ["sphinx", "sphinx-copybutton", "sphinx-rtd-theme"]
lint = ["ruff"]
dev = ["mypackage[test,docs,lint]"]
dev = [
{ include-group = "test"},
{ include-group = "docs"},
{ include-group = "lint"},
]

[tool.setuptools.dynamic]
version = { attr = "mypackage.__version__" }


[tool.setuptools.packages.find]
where = ["src"]
include = ["mypackage*"]

[tool.pytest.ini_options]

addopts = "--cov=mypackage"
markers = [
"slow: Marks slow tests",
Expand All @@ -39,15 +39,13 @@ filterwarnings = [
]

[tool.coverage.run]
omit = ["*__init__.py"]
omit = ["*__init__.py", "src/mypackage/__main__.py"]

[tool.ruff]
src = ["src", "tests"]
include = ["src/**.py"]
line-length = 120

[tool.ruff.lint]
select = ["F", "E", "W", "A", "PLC", "PLE", "PLW", "I", "N816", "ANN001", "ANN201", "ANN202", "ANN205", "ANN206"]

[tool.ruff.lint.flake8-type-checking]
strict = true
strict = true
10 changes: 6 additions & 4 deletions rename-package.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#!/bin/sh

package_rgx='[_a-zA-Z0-9\-]+'
oldname=$(grep -E "^ *name *= *\"${package_rgx}\"" pyproject.toml | grep -Eo "\"${package_rgx}\"" | sed s/\"//g)

Expand All @@ -7,11 +9,11 @@ if [ -z "$oldname" ]; then
fi

# Rename package to new name
find tests src docs .github/workflows pyproject.toml README.md Dockerfile -type f -exec sed -i s/$oldname/${1}/g {} +
mv src/$oldname src/$1
find tests src docs .github/workflows pyproject.toml README.md Dockerfile -type f -exec sed -i "s/$oldname/$1/g" {} +
mv "src/$oldname" "src/$1"

# Change URLs to new repo path
sed -i s/python-template/$(basename `git rev-parse --show-toplevel`)/g README.md
sed -i s/python-template/"$(basename "$(git rev-parse --show-toplevel)")"/g README.md

# Remove this file
rm -- "$0"
rm -- "$0"
3 changes: 3 additions & 0 deletions src/mypackage/__main__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""This file is run when `python -m mypackage` is called. Put your client code here if you have any."""

from mypackage.module import add_int

print("Hello World!")
print(f"1 + 1 = {add_int(1, 1)}")
32 changes: 10 additions & 22 deletions tests/test_module.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,15 @@
import unittest
from mypackage.module import add_int
from parameterized import parameterized

class TestModuleMethods(unittest.TestCase):
import pytest

def test_errors(self):

with self.assertRaises(TypeError, msg="Expected integer argument for y."):
add_int(1, "3")

with self.assertRaises(TypeError, msg="Expected integer argument for x."):
add_int({1,2}, 5)
from mypackage.module import add_int


@parameterized.expand([
[1, 2, 3],
[-4, 10, 6],
[1000, 100, 1100]
])
def test_result(self, x, y, expected):

self.assertEqual(add_int(x, y), expected)
def test_errors() -> None:
with pytest.raises(TypeError):
add_int(1, "3")
with pytest.raises(TypeError):
add_int({1, 2}, 5)


if __name__ == "__main__":
unittest.main()
@pytest.mark.parametrize("x,y,expected", [[1, 2, 3], [-4, 10, 6], [1000, 100, 1100]])
def test_result(x: int, y: int, expected: int) -> None:
assert add_int(x, y) == expected
Loading