diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index f58b551..10114bd 100644 --- a/.github/workflows/cicd.yaml +++ b/.github/workflows/cicd.yaml @@ -15,7 +15,10 @@ jobs: - "3.9" - "3.10" - "3.11" + - "3.12" + - "3.13" - "pypy-3.9" + - "pypy-3.10" steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} diff --git a/.gitignore b/.gitignore index f7a5122..1c0dcf1 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,4 @@ notebooks/ # project artefacts test_project +.python-version diff --git a/tests/test.sh b/tests/test.sh index 7f73ab4..2444804 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -4,6 +4,7 @@ # * The documentation builds # * The test_reminder test fails as expected # * All other tests pass +# * The main.py script executes successfully set -xe @@ -25,9 +26,15 @@ echo "✅ Cookiecutter executes successfully." cd test_project/my_project +poetry install + git add . poetry run pre-commit run --all-files -echo "✓ Commit hooks installed and working." +echo "✅ Commit hooks installed and working." + +# check that we can run the project +poetry run python -m my_project.main +echo "✅ main.py script executes successfully." # check that the documentation builds without warnings make docs diff --git a/{{ cookiecutter.project_slug }}/.github/workflows/cicd.yaml b/{{ cookiecutter.project_slug }}/.github/workflows/cicd.yaml index a650276..f208bf9 100644 --- a/{{ cookiecutter.project_slug }}/.github/workflows/cicd.yaml +++ b/{{ cookiecutter.project_slug }}/.github/workflows/cicd.yaml @@ -16,6 +16,8 @@ jobs: - "3.9" - "3.10" - "3.11" + - "3.12" + - "3.13" platform: [ubuntu-latest] runs-on: ${{ matrix.platform }} steps: @@ -37,7 +39,10 @@ jobs: - "3.9" - "3.10" - "3.11" + - "3.12" + - "3.13" - "pypy-3.9" + - "pypy-3.10" steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} @@ -80,7 +85,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.10" + python-version: "3.13" - name: Install poetry run: | curl -sSL https://install.python-poetry.org | python3 - diff --git a/{{ cookiecutter.project_slug }}/.pre-commit-config.yaml b/{{ cookiecutter.project_slug }}/.pre-commit-config.yaml index a651077..68892a2 100644 --- a/{{ cookiecutter.project_slug }}/.pre-commit-config.yaml +++ b/{{ cookiecutter.project_slug }}/.pre-commit-config.yaml @@ -1,13 +1,12 @@ minimum_pre_commit_version: "3.0" repos: -- repo: https://github.com/charliermarsh/ruff-pre-commit +- repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: 'v0.0.256' + rev: 'v0.8.2' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - files: ^src/.*\.py$ - exclude: ^tests/ + - id: ruff-format - repo: https://github.com/python-jsonschema/check-jsonschema rev: 0.22.0 hooks: @@ -19,12 +18,8 @@ repos: - id: yamllint args: [-d relaxed] verbose: true -- repo: https://github.com/psf/black - rev: 23.1.0 - hooks: - - id: black - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.0.1 + rev: v1.13.0 hooks: - id: mypy # for args see https://mypy.readthedocs.io/en/stable/command_line.html @@ -33,3 +28,4 @@ repos: --ignore-missing-imports, --allow-untyped-globals ] + files: ^(src|tests)/.*\.py$ diff --git a/{{ cookiecutter.project_slug }}/__init__.py b/{{ cookiecutter.project_slug }}/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/{{ cookiecutter.project_slug }}/docs/source/conf.py b/{{ cookiecutter.project_slug }}/docs/source/conf.py index 3e3b63f..fc334b6 100644 --- a/{{ cookiecutter.project_slug }}/docs/source/conf.py +++ b/{{ cookiecutter.project_slug }}/docs/source/conf.py @@ -14,7 +14,7 @@ import sys sys.path.insert(0, os.path.abspath("..")) -import {{ cookiecutter.project_slug }} # noqa: E402 +import {{ cookiecutter.project_slug }} # -- Project information ----------------------------------------------------- @@ -23,8 +23,8 @@ author = "{{ cookiecutter.author_name }}" # The full version, including alpha/beta/rc tags -release = {{ cookiecutter.project_slug }}.__version__ -version = {{ cookiecutter.project_slug }}.__version__ +release = {{cookiecutter.project_slug}}.__version__ +version = {{cookiecutter.project_slug}}.__version__ # -- General configuration --------------------------------------------------- diff --git a/{{ cookiecutter.project_slug }}/pyproject.toml b/{{ cookiecutter.project_slug }}/pyproject.toml index 90f8273..554d87f 100644 --- a/{{ cookiecutter.project_slug }}/pyproject.toml +++ b/{{ cookiecutter.project_slug }}/pyproject.toml @@ -13,12 +13,16 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: PyPy" ] # Only packages in this group will be installed in the release image [tool.poetry.dependencies] python = "^3.9" +pydantic = "^2.0.0" +pydantic-settings = "^2.0.0" # documentation [tool.poetry.group.docs.dependencies] @@ -42,7 +46,7 @@ tox = "^4.0.0" # deployment twine = "^4.0.2" -[tool.ruff] +[tool.ruff.lint] select = [ "ANN", # flake8-annotations "E", # flake8 @@ -57,7 +61,6 @@ ignore = [ "E501", "E712", - "ANN101", # Missing type annotation for `self` in method "ANN202", # Missing return type annotation for private function "ANN204", # Missing return type annotation for special function "ANN401", # Dynamically typed expressions (typing.Any) are disallowed @@ -68,11 +71,11 @@ ignore = [ "D106", # Missing docstring in public nested class ] -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] "tests/**" = ["S", "ANN"] -[tool.ruff.pydocstyle] +[tool.ruff.lint.pydocstyle] convention = "numpy" diff --git a/{{ cookiecutter.project_slug }}/src/__init__.py b/{{ cookiecutter.project_slug }}/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/main.py b/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/main.py index 0c6e87c..50a1b76 100644 --- a/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/main.py +++ b/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/main.py @@ -1,8 +1,13 @@ -from {{cookiecutter.project_slug}}.utils.logger import logger +import logging + +from {{cookiecutter.project_slug}}.utils import setup_logging + +logger = logging.getLogger(__name__) def main() -> None: """Run the project pipeline.""" + setup_logging() logger.info("Running the project") diff --git a/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/settings.py b/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/settings.py new file mode 100644 index 0000000..37650eb --- /dev/null +++ b/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/settings.py @@ -0,0 +1,43 @@ +import logging +from typing import Any + +from pydantic import field_validator +from pydantic_settings import BaseSettings + + +class LogSettings(BaseSettings): + """Define settings for the logger.""" + + loglevel: int = logging.DEBUG # loglevel for "our" packages + loglevel_3rdparty: int = logging.WARNING # loglevel for 3rdparty packages + our_packages: list[str] = [ + # list of "our" packages + "__main__", + "the_next_iteration", + ] + basicConfig: dict[str, Any] = { + # "basicConfig" of the logging module. + # Do not include the level parameter here since it's being controlled + # by the loglevel... parameters above. + "format": "%(asctime)s:%(levelname)s:%(name)s:%(message)s", + } + + @field_validator("loglevel", "loglevel_3rdparty") + def string_to_loglevel(cls, v: str) -> int: + """Convert a string to a loglevel.""" + try: + return int(v) + except TypeError: + v = v.lower() + if v == "debug": + return logging.DEBUG + elif v == "info": + return logging.INFO + elif v == "warning": + return logging.WARNING + elif v == "error": + return logging.ERROR + elif v == "critical": + return logging.CRITICAL + else: + raise ValueError(f"invalid loglevel {v}") diff --git a/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/utils.py b/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/utils.py new file mode 100644 index 0000000..a3402b8 --- /dev/null +++ b/{{ cookiecutter.project_slug }}/src/{{ cookiecutter.project_slug }}/utils.py @@ -0,0 +1,13 @@ +import logging + +from {{cookiecutter.project_slug}}.settings import LogSettings + + +def setup_logging() -> None: + """Set up the logging system.""" + log_config = LogSettings() + logging.basicConfig(level=log_config.loglevel_3rdparty, **log_config.basicConfig) + + our_loglevel = log_config.loglevel + for package in log_config.our_packages: + logging.getLogger(package).setLevel(our_loglevel) diff --git a/{{ cookiecutter.project_slug }}/tests/test_{{ cookiecutter.project_slug }}.py b/{{ cookiecutter.project_slug }}/tests/test_{{ cookiecutter.project_slug }}.py index d151d51..bc1cec3 100644 --- a/{{ cookiecutter.project_slug }}/tests/test_{{ cookiecutter.project_slug }}.py +++ b/{{ cookiecutter.project_slug }}/tests/test_{{ cookiecutter.project_slug }}.py @@ -7,9 +7,7 @@ def test_reminder(): """Reminder to write tests.""" - assert ( - False - ), "If this test runs it probably means that you have not written your own tests yet 😱 Do it now!" + assert False, "If this test runs it probably means that you have not written your own tests yet 😱 Do it now!" def test_version_is_semver_string(): diff --git a/{{ cookiecutter.project_slug }}/tox.ini b/{{ cookiecutter.project_slug }}/tox.ini index 636ae29..4596982 100644 --- a/{{ cookiecutter.project_slug }}/tox.ini +++ b/{{ cookiecutter.project_slug }}/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py39,py310,py311,pypy39 +envlist = py39,py310,py311,py312,py313,pypy39,pypy310 isolated_build = True parallel = True @@ -16,4 +16,7 @@ python = 3.9: py39 3.10: py310 3.11: py311 + 3.12: py312 + 3.13: py313 pypy-3.9: pypy39 + pypy-3.10: pypy310