diff --git a/poetry.lock b/poetry.lock index c526bc6f..94809e84 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -90,20 +90,6 @@ files = [ {file = "bids_validator-1.13.1-py2.py3-none-any.whl", hash = "sha256:da6edf5e76ef86c8a63b3fcee1dbfb039a16a9ef63cb0d2d05312c200d4607f7"}, ] -[[package]] -name = "binaryornot" -version = "0.4.4" -description = "Ultra-lightweight pure Python package to check if a file is binary or text." -optional = false -python-versions = "*" -files = [ - {file = "binaryornot-0.4.4-py2.py3-none-any.whl", hash = "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4"}, - {file = "binaryornot-0.4.4.tar.gz", hash = "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061"}, -] - -[package.dependencies] -chardet = ">=3.0.2" - [[package]] name = "black" version = "23.9.1" @@ -205,17 +191,6 @@ files = [ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] -[[package]] -name = "chardet" -version = "5.2.0" -description = "Universal encoding detector for Python 3" -optional = false -python-versions = ">=3.7" -files = [ - {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, - {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, -] - [[package]] name = "charset-normalizer" version = "3.2.0" @@ -350,27 +325,6 @@ files = [ {file = "connection_pool-0.0.3.tar.gz", hash = "sha256:bf429e7aef65921c69b4ed48f3d48d3eac1383b05d2df91884705842d974d0dc"}, ] -[[package]] -name = "cookiecutter" -version = "2.3.0" -description = "A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project template." -optional = false -python-versions = ">=3.7" -files = [ - {file = "cookiecutter-2.3.0-py3-none-any.whl", hash = "sha256:7e87944757c6e9f8729cf89a4139b6a35ab4d6dcbc6ae3e7d6360d44ad3ad383"}, - {file = "cookiecutter-2.3.0.tar.gz", hash = "sha256:942a794981747f6d7f439d6e49d39dc91a9a641283614160c93c474c72c29621"}, -] - -[package.dependencies] -arrow = "*" -binaryornot = ">=0.4.4" -click = ">=7.0,<9.0.0" -Jinja2 = ">=2.7,<4.0.0" -python-slugify = ">=4.0.0" -pyyaml = ">=5.3.1" -requests = ">=2.23.0" -rich = "*" - [[package]] name = "copier" version = "8.3.0" @@ -489,13 +443,13 @@ files = [ [[package]] name = "dunamai" -version = "1.18.0" +version = "1.18.1" description = "Dynamic version generation" optional = false python-versions = ">=3.5,<4.0" files = [ - {file = "dunamai-1.18.0-py3-none-any.whl", hash = "sha256:f9284a9f4048f0b809d11539896e78bde94c05b091b966a04a44ab4c48df03ce"}, - {file = "dunamai-1.18.0.tar.gz", hash = "sha256:5200598561ea5ba956a6174c36e402e92206c6a6aa4a93a6c5cb8003ee1e0997"}, + {file = "dunamai-1.18.1-py3-none-any.whl", hash = "sha256:ee7b042f7a687fa04fc383258eb93bd819c7bd8aec62e0974f3c69747e5958f2"}, + {file = "dunamai-1.18.1.tar.gz", hash = "sha256:5e9a91e43d16bb56fa8fcddcf92fa31b2e1126e060c3dcc8d094d9b508061f9d"}, ] [package.dependencies] @@ -611,20 +565,20 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.36" +version = "3.1.37" description = "GitPython is a Python library used to interact with Git repositories" optional = false python-versions = ">=3.7" files = [ - {file = "GitPython-3.1.36-py3-none-any.whl", hash = "sha256:8d22b5cfefd17c79914226982bb7851d6ade47545b1735a9d010a2a4c26d8388"}, - {file = "GitPython-3.1.36.tar.gz", hash = "sha256:4bb0c2a6995e85064140d31a33289aa5dce80133a23d36fcd372d716c54d3ebf"}, + {file = "GitPython-3.1.37-py3-none-any.whl", hash = "sha256:5f4c4187de49616d710a77e98ddf17b4782060a1788df441846bddefbb89ab33"}, + {file = "GitPython-3.1.37.tar.gz", hash = "sha256:f9b9ddc0761c125d5780eab2d64be4873fc6817c2899cbcb34b02344bdc7bc54"}, ] [package.dependencies] gitdb = ">=4.0.1,<5" [package.extras] -test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-sugar", "virtualenv"] +test = ["black", "coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mypy", "pre-commit", "pytest", "pytest-cov", "pytest-sugar"] [[package]] name = "graphlib-backport" @@ -1115,30 +1069,6 @@ traitlets = ">=5.3" docs = ["myst-parser", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "traitlets"] test = ["ipykernel", "pre-commit", "pytest", "pytest-cov", "pytest-timeout"] -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - [[package]] name = "markupsafe" version = "2.1.3" @@ -1198,17 +1128,6 @@ files = [ {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, ] -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - [[package]] name = "mkinit" version = "1.0.0" @@ -1494,9 +1413,9 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, {version = ">=1.20.3", markers = "python_version < \"3.10\""}, - {version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""}, + {version = ">=1.21.0", markers = "python_version >= \"3.10\""}, + {version = ">=1.23.2", markers = "python_version >= \"3.11\""}, ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" @@ -2117,23 +2036,6 @@ files = [ [package.dependencies] six = ">=1.5" -[[package]] -name = "python-slugify" -version = "8.0.1" -description = "A Python slugify application that also handles Unicode" -optional = false -python-versions = ">=3.7" -files = [ - {file = "python-slugify-8.0.1.tar.gz", hash = "sha256:ce0d46ddb668b3be82f4ed5e503dbc33dd815d83e2eb6824211310d3fb172a27"}, - {file = "python_slugify-8.0.1-py2.py3-none-any.whl", hash = "sha256:70ca6ea68fe63ecc8fa4fcf00ae651fc8a5d02d93dcd12ae6d4fc7ca46c4d395"}, -] - -[package.dependencies] -text-unidecode = ">=1.3" - -[package.extras] -unidecode = ["Unidecode (>=1.1.1)"] - [[package]] name = "pytz" version = "2023.3.post1" @@ -2284,6 +2186,25 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "requests-mock" +version = "1.11.0" +description = "Mock out responses from the requests package" +optional = false +python-versions = "*" +files = [ + {file = "requests-mock-1.11.0.tar.gz", hash = "sha256:ef10b572b489a5f28e09b708697208c4a3b2b89ef80a9f01584340ea357ec3c4"}, + {file = "requests_mock-1.11.0-py2.py3-none-any.whl", hash = "sha256:f7fae383f228633f6bececebdab236c478ace2284d6292c6e7e2867b9ab74d15"}, +] + +[package.dependencies] +requests = ">=2.3,<3" +six = "*" + +[package.extras] +fixture = ["fixtures"] +test = ["fixtures", "mock", "purl", "pytest", "requests-futures", "sphinx", "testtools"] + [[package]] name = "reretry" version = "0.11.8" @@ -2295,25 +2216,6 @@ files = [ {file = "reretry-0.11.8.tar.gz", hash = "sha256:f2791fcebe512ea2f1d153a2874778523a8064860b591cd90afc21a8bed432e3"}, ] -[[package]] -name = "rich" -version = "13.5.3" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "rich-13.5.3-py3-none-any.whl", hash = "sha256:9257b468badc3d347e146a4faa268ff229039d4c2d176ab0cffb4c4fbc73d5d9"}, - {file = "rich-13.5.3.tar.gz", hash = "sha256:87b43e0543149efa1253f485cd845bb7ee54df16c9617b8a893650ab84b4acb6"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - [[package]] name = "rpds-py" version = "0.10.3" @@ -2783,7 +2685,7 @@ files = [ ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} +greenlet = {version = "!=0.4.17", markers = "platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\""} typing-extensions = ">=4.2.0" [package.extras] @@ -2862,17 +2764,6 @@ files = [ [package.extras] tests = ["pytest", "pytest-cov"] -[[package]] -name = "text-unidecode" -version = "1.3" -description = "The most basic Text::Unidecode port" -optional = false -python-versions = "*" -files = [ - {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, - {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, -] - [[package]] name = "throttler" version = "1.2.2" @@ -3137,4 +3028,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.8,<3.12" -content-hash = "d04df6284308cf4e568991e65706416b207feabff2cd2c300278006d2f75cdd3" +content-hash = "ee44d16e76daecd66db38c73509de996fbfb41ebe50cf1066daee05cd936b854" diff --git a/pyproject.toml b/pyproject.toml index 08cb340b..e20bd7db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,9 +12,7 @@ authors = [ "Jason Kai ", ] license = "MIT" -packages = [ - { include = "snakebids" } -] +packages = [{ include = "snakebids" }] exclude = ["snakebids/tests/**"] [tool.poetry-dynamic-versioning] @@ -26,12 +24,11 @@ bump = true [tool.poetry-dynamic-versioning.substitution] files = [ - 'snakebids/project_template/cookiecutter.json', - 'snakebids/__init__.py' + 'snakebids/__init__.py', ] patterns = [ "(^\\s+\"snakebids_version\":\\s*\")[^'\"]*(\")", - "(^__version__\\s*(?::.*?)?=\\s*['\"])[^'\"]*(['\"])" + "(^__version__\\s*(?::.*?)?=\\s*['\"])[^'\"]*(['\"])", ] [tool.poetry.dependencies] @@ -42,7 +39,6 @@ snakemake = [ { version = ">=7.18.2", python = ">=3.11" }, ] PyYAML = "^6" -cookiecutter = "^2.1.1" typing-extensions = ">=3.10.0" attrs = ">=21.2.0,<24" boutiques = "^0.5.25" @@ -57,15 +53,18 @@ importlib-resources = ">=5.12.0,<7" # on the python version being used. numpy = [ { version = "<=1.24.4", python = "<3.9" }, - { version = ">=1.23.2", python = ">=3.11" } + { version = ">=1.23.2", python = ">=3.11" }, ] # Use minimum of 1.10.0 because of security vulnerability scipy = [ { version = ">=1.10.0,<=1.10.1", python = "<3.9" }, - { version = ">=1.10.0", python = ">=3.9" } + { version = ">=1.10.0", python = ">=3.9" }, ] +# minimum 8.2.0 to use post-copy mesages copier = ">=8.2.0" jinja2-time = ">=0.2.0" +# minimum 2.31.0 because of security vulnerability +requests = ">=2.31.0" [tool.poetry.group.dev.dependencies] black = "^23.1.0" @@ -88,6 +87,7 @@ ruff = "^0.0.285" pytest-xdist = "^3.3.1" pytest-split = "^0.8.1" tomli = "^2.0.1" +requests-mock = "^1.11.0" [tool.poetry.scripts] snakebids = "snakebids.admin:main" @@ -133,11 +133,11 @@ reportImportCycles = false [tool.ruff] select = [ - "E", # pycodestyle error - "F", # pyflakes - "PL", # pylint - "RUF", # ruff - "UP", #pyupgrade + "E", # pycodestyle error + "F", # pyflakes + "PL", # pylint + "RUF", # ruff + "UP", #pyupgrade "T100", # debugger ] ignore = ["PLR0913"] diff --git a/snakebids/jinja2_ext/snakebids_version.py b/snakebids/jinja2_ext/snakebids_version.py new file mode 100644 index 00000000..04c9eb7d --- /dev/null +++ b/snakebids/jinja2_ext/snakebids_version.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +import importlib.metadata as impm +import re + +import jinja2.parser +import requests +from jinja2.ext import Extension + + +class SnakebidsVersionExtension(Extension): + def __init__(self, env: jinja2.Environment): + env.globals["snakebids_version"] = self._lookup_version() # type: ignore + super().__init__(env) + + def _lookup_version(self): + request = requests.get("https://pypi.org/pypi/snakebids/json") + version_regex = re.compile(r"\d+\.\d+\.\d+") + try: + request.raise_for_status() + version = request.json()["info"]["version"] + if not re.fullmatch(version_regex, version): + raise TypeError() + return version + except (requests.HTTPError, KeyError, TypeError): + version = impm.version("snakebids") + if version == "0.0.0" or not re.fullmatch(version_regex, version): + return None + return version diff --git a/snakebids/project_template/copier.yaml b/snakebids/project_template/copier.yaml index ab8aeefe..3bb226ef 100644 --- a/snakebids/project_template/copier.yaml +++ b/snakebids/project_template/copier.yaml @@ -64,8 +64,12 @@ bids_version: default: "1.8.0" when: false +python_version: + default: ">=3.8,<3.12" + when: false + snakebids_version: - default: "0.9.2" + default: "{{ snakebids_version or '0.9.3' }}" when: false snakemake_version: @@ -137,3 +141,4 @@ _jinja_extensions: - snakebids.jinja2_ext.vcs.GitConfigExtension - snakebids.jinja2_ext.colorama.ColoramaExtension - snakebids.jinja2_ext.toml_encode.TomlEncodeExtension + - snakebids.jinja2_ext.snakebids_version.SnakebidsVersionExtension diff --git a/snakebids/project_template/{% if build_system != 'poetry' %}pyproject.toml{% endif %}.jinja b/snakebids/project_template/{% if build_system != 'poetry' %}pyproject.toml{% endif %}.jinja index a0f18145..4969772d 100644 --- a/snakebids/project_template/{% if build_system != 'poetry' %}pyproject.toml{% endif %}.jinja +++ b/snakebids/project_template/{% if build_system != 'poetry' %}pyproject.toml{% endif %}.jinja @@ -26,7 +26,7 @@ classifiers = [ "Operating System :: OS Independent", ] -requires-python = ">=3.8,<3.12" +requires-python = "{{ python_version }}" dependencies = [ "snakemake >= {{ snakemake_version }}", "snakebids >= {{ snakebids_version }}", diff --git a/snakebids/project_template/{% if build_system == 'poetry' %}pyproject.toml{% endif %}.jinja b/snakebids/project_template/{% if build_system == 'poetry' %}pyproject.toml{% endif %}.jinja index 729511d3..566c916a 100644 --- a/snakebids/project_template/{% if build_system == 'poetry' %}pyproject.toml{% endif %}.jinja +++ b/snakebids/project_template/{% if build_system == 'poetry' %}pyproject.toml{% endif %}.jinja @@ -21,7 +21,7 @@ classifiers = [ ] [tool.poetry.dependencies] -python = ">=3.8,<3.12" +python = "{{ python_version }}" snakemake = ">={{ snakemake_version }}" snakebids = ">={{ snakebids_version }}" diff --git a/snakebids/tests/test_template.py b/snakebids/tests/test_template.py index 4ca2e846..427b8ff2 100644 --- a/snakebids/tests/test_template.py +++ b/snakebids/tests/test_template.py @@ -6,15 +6,17 @@ import sys import tempfile from pathlib import Path -from typing import Literal, TypedDict +from typing import Any, Literal, TypedDict import copier import more_itertools as itx import pathvalidate import prompt_toolkit.validation import pytest +import requests_mock from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st +from pytest_mock.plugin import MockerFixture from typing_extensions import Unpack if sys.version_info < (3, 11): @@ -25,7 +27,7 @@ import snakebids from snakebids.tests.helpers import allow_function_scoped, needs_docker -BuildSystems = Literal["poetry", "hatch", "flit", "setuptools"] +BuildSystem = Literal["poetry", "hatch", "flit", "setuptools"] class DataFields(TypedDict): @@ -34,13 +36,13 @@ class DataFields(TypedDict): app_full_name: str github: str app_description: str - build_system: BuildSystems + build_system: BuildSystem app_version: str create_doc_template: bool license: str -def get_empty_data(app_name: str, build: BuildSystems) -> DataFields: +def get_empty_data(app_name: str, build: BuildSystem) -> DataFields: data: DataFields = { "full_name": "", "email": "", @@ -101,6 +103,59 @@ def test_invalid_email_raises_error(email: str, tmp_path: Path): ) +@pytest.mark.parametrize("build", ["poetry", "hatch", "flit", "setuptools"]) +@pytest.mark.parametrize( + ["server", "server_status", "metadata", "expected"], + [ + # Server returns valid version + ("1.1.1", 200, "2.2.2", "1.1.1"), + # Server raises 400 error + ("1.1.1", 400, "2.2.2", "2.2.2"), + # server returns invalid and importlib.metadata gives valid + ("1.1.1.dev1", 200, "2.2.2", "2.2.2"), + # server invalid and importlib.metadata gives 0.0.0 + ("1.1.1.dev1", 200, "0.0.0", "0.9.3"), + # server and importlib.metadata invalid + ("1.1.1.dev1", 200, "2.2.2.dev1", "0.9.3"), + # server gives non-string type + (True, 200, "2.2.2", "2.2.2"), + ], +) +def test_gets_correct_snakebids_version( + build: BuildSystem, + server: Any, + server_status: str, + metadata: str, + expected: str, + mocker: MockerFixture, + tmp_path: Path, +): + tmpdir = Path(tempfile.mkdtemp(dir=tmp_path)) + with requests_mock.Mocker() as m: + m.get( + "https://pypi.org/pypi/snakebids/json", + json={"info": {"version": server}}, + status_code=server_status, + ) + metadata_patch = mocker.patch("importlib.metadata.version") + metadata_patch.return_value = metadata + data = get_empty_data("testapp", build) + copier.run_copy( + str(Path(itx.first(snakebids.__path__), "project_template")), + tmpdir / data["app_full_name"], + data=data, + unsafe=True, + ) + with open(tmpdir / data["app_full_name"] / "pyproject.toml", "rb") as f: + pyproject = tomllib.load(f) + if build == "poetry": + assert ( + pyproject["tool"]["poetry"]["dependencies"]["snakebids"] == f">={expected}" + ) + else: + assert f"snakebids >= {expected}" in pyproject["project"]["dependencies"] + + @given( name=st.text() .filter(lambda s: not re.match(r"^[a-zA-Z_][a-zA-Z_0-9]*$", s)) @@ -128,7 +183,7 @@ def test_invalid_app_name_raises_error(name: str, tmp_path: Path): ], ) def test_correct_build_system_used( - tmp_path: Path, build: BuildSystems, build_backend: str + tmp_path: Path, build: BuildSystem, build_backend: str ): tmpdir = Path(tempfile.mkdtemp(dir=tmp_path)) data = get_empty_data("testapp", build) @@ -154,11 +209,9 @@ def test_correct_build_system_used( license=st.text(), ) @settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=1000) -@pytest.mark.parametrize( - ["build"], [("poetry",), ("hatch",), ("flit",), ("setuptools",)] -) +@pytest.mark.parametrize("build", ["poetry", "hatch", "flit", "setuptools"]) def test_pyproject_correctly_formatted( - tmp_path: Path, build: BuildSystems, **kwargs: Unpack[DataFields] + tmp_path: Path, build: BuildSystem, **kwargs: Unpack[DataFields] ): tmpdir = Path(tempfile.mkdtemp(dir=tmp_path)) kwargs["build_system"] = build @@ -218,7 +271,7 @@ def test_pyproject_correctly_formatted( ("flit", "pdm"), ], ) -def test_template_dry_runs_successfully(tmp_path: Path, build: BuildSystems, venv: str): +def test_template_dry_runs_successfully(tmp_path: Path, build: BuildSystem, venv: str): app_name = "snakebids_app" data = get_empty_data(app_name, build)