├── .gitignore ├── {{cookiecutter.package_name}} ├── tests │ ├── __init__.py │ └── {{cookiecutter.module_name}} │ │ ├── __init__.py │ │ ├── conftest.py │ │ └── test_sample.py ├── .env ├── docs │ ├── .gitignore │ ├── license.md │ ├── usage.md │ ├── index.md │ └── conf.py ├── notebooks │ ├── .gitignore │ └── data │ │ └── .gitignore ├── poetry.toml ├── .gitignore ├── src │ └── {{cookiecutter.module_name}} │ │ ├── py.typed │ │ ├── app.py │ │ ├── __init__.py │ │ └── __main__.py ├── .vscode │ └── settings.json ├── .flake8 ├── .editorconfig ├── .gitlab-ci.yml ├── .github │ └── workflows │ │ ├── python-publish.yml │ │ └── python-tests.yaml ├── README.md ├── Makefile ├── .pre-commit-config.yaml ├── LICENSE └── pyproject.toml ├── poetry.toml ├── .coverage ├── .vscode └── settings.json ├── hooks ├── pre_gen_project.py └── post_gen_project.py ├── .editorconfig ├── cookiecutter.json ├── Makefile ├── .github └── workflows │ └── ci-tests.yml ├── LICENSE ├── pyproject.toml ├── README.md └── tests └── test_bake_project.py /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | .venv 3 | poetry.lock 4 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/.env: -------------------------------------------------------------------------------- 1 | PYTHONPATH=src 2 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/docs/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/notebooks/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/notebooks/data/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/tests/{{cookiecutter.module_name}}/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.coverage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timhughes/cookiecutter-poetry/HEAD/.coverage -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | build 3 | .venv 4 | .ipynb_checkpoints 5 | .coverage 6 | .pytest_cache 7 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/tests/{{cookiecutter.module_name}}/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vim:fenc=utf-8 3 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/docs/license.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | ```{literalinclude} ../LICENSE 4 | --- 5 | language: none 6 | --- 7 | ``` -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/src/{{cookiecutter.module_name}}/py.typed: -------------------------------------------------------------------------------- 1 | # See https://www.python.org/dev/peps/pep-0561/#packaging-type-information 2 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ```{eval-rst} 4 | .. click:: {{cookiecutter.module_name}}.__main__:cli 5 | :prog: {{cookiecutter.package_name}} 6 | :nested: full 7 | ``` 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": [ 3 | "tests" 4 | ], 5 | "python.testing.unittestEnabled": false, 6 | "python.testing.nosetestsEnabled": false, 7 | "python.testing.pytestEnabled": true 8 | } -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": ["tests"], 3 | "python.testing.unittestEnabled": false, 4 | "python.testing.pytestEnabled": true, 5 | "python.analysis.typeCheckingMode": "basic" 6 | } 7 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/docs/index.md: -------------------------------------------------------------------------------- 1 | ```{include} ../README.md 2 | 3 | ``` 4 | 5 | [license]: license 6 | [command-line reference]: usage 7 | 8 | ```{toctree} 9 | --- 10 | hidden: 11 | maxdepth: 2 12 | --- 13 | 14 | usage 15 | License 16 | Changelog 17 | ``` 18 | -------------------------------------------------------------------------------- /hooks/pre_gen_project.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import re 3 | import sys 4 | 5 | 6 | MODULE_REGEX = r"^[_a-zA-Z][_a-zA-Z0-9]+$" 7 | 8 | module_name = "{{ cookiecutter.module_name }}" 9 | 10 | if not re.match(MODULE_REGEX, module_name): 11 | print( 12 | "ERROR: The project slug (%s) is not a valid Python module name." % module_name 13 | ) 14 | 15 | # Exit to cancel project 16 | sys.exit(1) 17 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/docs/conf.py: -------------------------------------------------------------------------------- 1 | """Sphinx configuration.""" 2 | project = "{{cookiecutter.module_name}}" 3 | author = "{{cookiecutter.author}}" 4 | copyright = "{{cookiecutter.copyright_year}}, {{cookiecutter.author}}" 5 | extensions = [ 6 | "sphinx.ext.autodoc", 7 | 'sphinx.ext.coverage', 8 | "sphinx.ext.napoleon", 9 | "sphinx_click", 10 | "myst_parser", 11 | ] 12 | autodoc_typehints = "description" 13 | html_theme = "furo" 14 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/src/{{cookiecutter.module_name}}/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger("{{cookiecutter.module_name}}") 4 | 5 | 6 | class App: 7 | def __init__(self, settings) -> None: 8 | self.settings = settings 9 | 10 | def start(self) -> None: 11 | logger.info("{{cookiecutter.module_name}} Starting") 12 | 13 | def stop(self) -> None: 14 | logger.info("{{cookiecutter.module_name}} Stopping") 15 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/.flake8: -------------------------------------------------------------------------------- 1 | # Config for tools that do not use pyproject.toml yet 2 | [flake8] 3 | select = B,B9,C,D,DAR,E,F,N,RST,W 4 | extend-ignore = E203,E501,RST201,RST203,RST301,W503 5 | max-line-length = 88 6 | max-complexity = 10 7 | docstring-convention = google 8 | rst-roles = class,const,func,meth,mod,ref 9 | rst-directives = deprecated 10 | exclude = 11 | __pycache__, 12 | .git, 13 | .github, 14 | .mypy_cache, 15 | .pytest_cache, 16 | .venv, 17 | .vscode, 18 | build, 19 | dist 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | charset = utf-8 12 | 13 | [*.{py,toml,rst}] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [*.yml,yaml,json] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | # Tab indentation (no size specified) 22 | [Makefile] 23 | indent_style = tab 24 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | charset = utf-8 12 | 13 | [*.{py,toml,rst}] 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [*.yml,yaml,json] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | # Tab indentation (no size specified) 22 | [Makefile] 23 | indent_style = tab 24 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Tim Hughes", 3 | "email": "thughes@thegoldfish.org", 4 | "github_user": "timhughes", 5 | "package_name": "example-project", 6 | "module_name": "{{ cookiecutter.package_name|lower|replace('-', '_') }}", 7 | "short_description": "A simple application", 8 | "version": "0.0.1", 9 | "copyright_year": "{% now 'utc', '%Y' %}", 10 | "license": [ 11 | "MIT", 12 | "GPL-3.0-or-later", 13 | "Proprietary" 14 | ], 15 | "command_line_interface": [ 16 | "click", 17 | "no cli" 18 | ], 19 | "use_jupyterlab": [ 20 | "n", 21 | "y" 22 | ], 23 | "add_badges": [ 24 | "n", 25 | "y" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /hooks/post_gen_project.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # vim:fenc=utf-8 4 | 5 | import os 6 | import shutil 7 | 8 | PROJECT_DIRECTORY = os.path.realpath(os.path.curdir) 9 | 10 | 11 | def remove_file(filepath): 12 | os.remove(os.path.join(PROJECT_DIRECTORY, filepath)) 13 | 14 | 15 | def remove_dir(filepath): 16 | shutil.rmtree(os.path.join(PROJECT_DIRECTORY, filepath)) 17 | 18 | 19 | if __name__ == "__main__": 20 | if "{{ cookiecutter.use_jupyterlab }}" != "y": 21 | remove_dir("notebooks") 22 | 23 | if "no cli" in "{{ cookiecutter.command_line_interface|lower }}": 24 | cli_file = os.path.join("src", "{{ cookiecutter.module_name }}", "__main__.py") 25 | remove_file(cli_file) 26 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/src/{{cookiecutter.module_name}}/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | {{ cookiecutter.short_description }} 3 | """ 4 | from __future__ import annotations 5 | 6 | import sys 7 | from . import app 8 | 9 | # TODO: use try/except ImportError when 10 | # https://github.com/python/mypy/issues/1393 is fixed 11 | if sys.version_info < (3, 10): 12 | # compatibility for python <3.10 13 | import importlib_metadata as metadata 14 | else: 15 | from importlib import metadata 16 | 17 | 18 | module_metadata = metadata.metadata("{{ cookiecutter.package_name }}") 19 | 20 | __author__ = f"{module_metadata['Author']} <{module_metadata['Author-email']}>" 21 | __version__ = module_metadata["Version"] 22 | __version_info__ = tuple([int(num) for num in __version__.split(".")]) 23 | 24 | 25 | all = [app] 26 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: centos/python-38-centos7 2 | 3 | cache: 4 | paths: 5 | - .cache/pip 6 | - .venv 7 | 8 | before_script: 9 | - export PATH=${PATH}:${HOME}/.local/bin/ 10 | - python -V # Print out python version for debugging 11 | - dnf install -y make 12 | - python -m pip install --user poetry 13 | 14 | lint: 15 | tags: 16 | - docker 17 | script: 18 | - make install 19 | - make lint 20 | 21 | test: 22 | tags: 23 | - docker 24 | script: 25 | - make install 26 | - make test 27 | 28 | build: 29 | script: 30 | - make install 31 | - make build 32 | artifacts: 33 | paths: 34 | - dist 35 | only: 36 | - master 37 | 38 | pyinstaller: 39 | script: 40 | - make install 41 | - make pyinstaller 42 | artifacts: 43 | paths: 44 | - dist 45 | only: 46 | - master 47 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # vim:ft=make 3 | # Makefile 4 | # 5 | .DEFAULT_GOAL := help 6 | .PHONY: test help 7 | 8 | 9 | help: ## these help instructions 10 | @sed -rn 's/^([a-zA-Z_-]+):.*?## (.*)$$/"\1" "\2"/p' < $(MAKEFILE_LIST)|xargs printf "make %-20s# %s\n" 11 | 12 | hidden: # example undocumented, for internal usage only 13 | @true 14 | 15 | pydoc: ## Run a pydoc server and open the browser 16 | poetry run python -m pydoc -b 17 | 18 | install: ## Run `poetry install` 19 | poetry install 20 | 21 | showdeps: ## run poetry to show deps 22 | @echo "CURRENT:" 23 | poetry show --tree 24 | @echo 25 | @echo "LATEST:" 26 | poetry show --latest 27 | 28 | lint: ## Runs bandit and black in check mode 29 | poetry run black tests hooks --check 30 | @echo '-------------------------------' 31 | poetry run bandit -r hooks 32 | 33 | format: ## Formats you code with Black 34 | poetry run black tests hooks 35 | 36 | test: hidden ## run pytest with coverage 37 | poetry run pytest -v tests 38 | 39 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/tests/{{cookiecutter.module_name}}/test_sample.py: -------------------------------------------------------------------------------- 1 | # vim:fenc=utf-8 2 | {%- if cookiecutter.command_line_interface == "click" %} 3 | from click.testing import CliRunner 4 | {%- endif %} 5 | import {{cookiecutter.module_name}} 6 | {%- if cookiecutter.command_line_interface == "click" %} 7 | from {{cookiecutter.module_name}}.__main__ import cli 8 | {%- endif %} 9 | 10 | 11 | def test_true(): 12 | assert 1 == 1 13 | {%- if cookiecutter.command_line_interface == "click" %} 14 | 15 | 16 | def test_click_cli(): 17 | runner = CliRunner(mix_stderr=False) 18 | result = runner.invoke(cli, ['--help']) 19 | assert result.exit_code == 0 20 | assert 'Start {{cookiecutter.package_name}} in server mode' in result.output 21 | assert 'Start {{cookiecutter.package_name}} in server mode' in result.stdout 22 | assert '' == result.stderr 23 | 24 | result = runner.invoke(cli, ['--version']) 25 | assert result.exit_code == 0 26 | assert 'cli, version 0.0.1' in result.output 27 | 28 | {%- endif %} 29 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # To use test.pypi.org you you should swap they publish command for these two lines 2 | # 3 | # poetry config repositories.testpypi https://test.pypi.org/legacy/ 4 | # poetry publish --repository testpypi --username __token__ --password {% raw %}${{ secrets.PYPI_API_TOKEN }}{% endraw %} 5 | 6 | name: Publish to PyPI 7 | 8 | on: 9 | release: 10 | types: [created] 11 | 12 | jobs: 13 | deploy: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: '3.x' 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install poetry 27 | - name: Build and publish 28 | run: | 29 | poetry version $(git describe --tags --abbrev=0) 30 | poetry build 31 | poetry publish --username __token__ --password {% raw %}${{ secrets.PYPI_API_TOKEN }}{% endraw %} 32 | -------------------------------------------------------------------------------- /.github/workflows/ci-tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Unit Tests 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: ["3.11", "3.10"] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade poetry 29 | make install 30 | - name: Lint 31 | run: | 32 | # stop the build if there are Python syntax errors or undefined names 33 | make lint 34 | - name: Test 35 | run: | 36 | make test 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021, Tim Hughes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | 24 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/.github/workflows/python-tests.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python Tests 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | python-version: ["3.11", "3.10"] 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python {% raw %}${{ matrix.python-version }}{% endraw %} 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: {% raw %}${{ matrix.python-version }}{% endraw %} 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install poetry 33 | poetry install 34 | - name: Lint 35 | run: | 36 | make lint 37 | - name: Test with pytest 38 | run: | 39 | make test 40 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/README.md: -------------------------------------------------------------------------------- 1 | # {{ cookiecutter.package_name }} 2 | 3 | {% if cookiecutter.add_badges == 'y' %} 4 | [![PyPi](https://img.shields.io/pypi/v/{{ cookiecutter.package*name }}.svg)](https://pypi.python.org/pypi/{{ cookiecutter.package_name }}) 5 | [![Travis](https://img.shields.io/travis/{{ cookiecutter.github_user }}/{{ cookiecutter.package_name }}.svg)](https://travis-ci.com/{{ cookiecutter.github_user }}/{{ cookiecutter.package_name }}) 6 | [![Documentation](https://readthedocs.org/projects/{{ cookiecutter.package_name | replace("\*", "-") }}/badge/?version=latest)](https://{{ cookiecutter.package_name | replace("*", "-") }}.readthedocs.io/en/latest/?badge=latest) 7 | [![Updates](https://pyup.io/repos/github/{{ cookiecutter.github_user }}/{{ cookiecutter.package_name }}/shield.svg)](https://pyup.io/repos/github/{{ cookiecutter.github_user }}/{{ cookiecutter.package_name }}/) 8 | {% endif %} 9 | 10 | {{ cookiecutter.short_description }} 11 | 12 | ## Developing 13 | 14 | Run `make` for help 15 | 16 | make install # Run `poetry install` 17 | make showdeps # run poetry to show deps 18 | make lint # Runs bandit and black in check mode 19 | make format # Formats you code with Black 20 | make test # run pytest with coverage 21 | make build # run `poetry build` to build source distribution and wheel 22 | 23 | {%- if cookiecutter.command_line_interface != "no cli" %} 24 | make pyinstaller # Create a binary executable using pyinstaller 25 | {%- endif %} 26 | {%- if cookiecutter.use_jupyterlab == "y" %} 27 | make jupyter # run the jupyter-lab server 28 | {%- endif %} 29 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry_core"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "cookiecutter-poetry" 7 | version = "0.0.1" 8 | description = "Cookiecutter template for python with poetry and pytest" 9 | authors = [ 10 | "Tim Hughes ", 11 | ] 12 | # Use identifier from https://spdx.org/licenses/ 13 | license = "MIT" 14 | readme = "README.md" 15 | homepage = "https://github.com/timhughes/cookiecutter-poetry" 16 | repository = "https://github.com/timhughes/cookiecutter-poetry" 17 | documentation = "https://github.com/timhughes/cookiecutter-poetry/blob/master/README.md" 18 | classifiers = [ 19 | # https://pypi.org/classifiers/ 20 | "Environment :: Console", 21 | "Development Status :: 1 - Planning", 22 | ] 23 | 24 | [tool.poetry.urls] 25 | # If you publish you package on PyPI, these will appear in the Project Links section. 26 | "Bug Tracker" = "https://github.com/timhughes/cookiecutter-poetry/issues" 27 | 28 | [tool.poetry.dependencies] 29 | python = "^3.10.0" 30 | cookiecutter = "*" 31 | 32 | 33 | [tool.poetry.group.test.dependencies] 34 | bandit = "*" 35 | black = "*" 36 | bump2version = "*" 37 | flake8 = "*" 38 | isort = "*" 39 | jedi-language-server = "*" 40 | mypy = "*" 41 | pyinstaller = "*" 42 | pylint = "*" 43 | pytest = "*" 44 | pytest-cookies = "*" 45 | pytest-cov = "*" 46 | reorder-python-imports = "*" 47 | vulture = "*" 48 | 49 | [tool.isort] 50 | profile = "black" 51 | 52 | [tool.black] 53 | target-version = ['py311'] 54 | 55 | [tool.pytest.ini_options] 56 | # Example 57 | pythonpath = [ 58 | "src" 59 | ] 60 | filterwarnings = [ 61 | "ignore::DeprecationWarning:moto.*:", 62 | "ignore::DeprecationWarning:boto.*:", 63 | ] 64 | 65 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # vim:ft=make 3 | # Makefile 4 | # 5 | .DEFAULT_GOAL := help 6 | .PHONY: test help docs_build docs 7 | 8 | 9 | help: ## these help instructions 10 | @sed -rn 's/^([a-zA-Z_-]+):.*?## (.*)$$/"\1" "\2"/p' < $(MAKEFILE_LIST)|xargs printf "make %-20s# %s\n" 11 | 12 | hidden: # example undocumented, for internal usage only 13 | @true 14 | 15 | pydoc: ## Run a pydoc server and open the browser 16 | poetry run python -m pydoc -b 17 | 18 | docs_build: ## Build the documentation 19 | poetry run sphinx-apidoc --module-first -o docs/api src/example_project/ 20 | poetry run sphinx-build --color docs docs/_build 21 | 22 | docs: ## Build and serve the documentation with live reloading on file changes 23 | poetry run sphinx-apidoc --module-first -o docs/api src/example_project/ 24 | poetry run sphinx-autobuild --open-browser docs docs/_build 25 | 26 | install: ## Run `poetry install` 27 | poetry install 28 | 29 | showdeps: ## run poetry to show deps 30 | @echo "CURRENT:" 31 | poetry show --tree 32 | @echo 33 | @echo "LATEST:" 34 | poetry show --latest 35 | 36 | lint: ## Runs black, isort, bandit, flake8 in check mode 37 | poetry run black --check . 38 | poetry run isort --check . 39 | poetry run bandit -r src 40 | poetry run flake8 src tests 41 | poetry run ruff check . 42 | 43 | format: ## Formats you code with Black 44 | poetry run isort . 45 | poetry run black . 46 | 47 | test: hidden ## run pytest with coverage 48 | poetry run pytest -v --cov {{ cookiecutter.module_name }} 49 | 50 | build: install lint test ## run `poetry build` to build source distribution and wheel 51 | poetry build 52 | 53 | bumpversion: build ## bumpversion 54 | poetry run bump2version --tag --current-version $$(git describe --tags --abbrev=0) --tag-name '{new_version}' patch 55 | git push 56 | git push --tags 57 | 58 | {%- if cookiecutter.command_line_interface != "no cli" %} 59 | pyinstaller: install lint test ## Create a binary executable using pyinstaller 60 | poetry run pyinstaller src/{{ cookiecutter.module_name }}/cli.py --onefile --name {{ cookiecutter.package_name }} 61 | 62 | {%- endif %} 63 | {%- if cookiecutter.use_jupyterlab == "y" %} 64 | jupyter: ## run the jupyter-lab server 65 | poetry run jupyter-lab 66 | 67 | {%- endif %} 68 | run: ## run `poetry run {{ cookiecutter.package_name }}` 69 | poetry run {{ cookiecutter.package_name }} 70 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | # - id: ruff 5 | # name: ruff 6 | # description: "Run 'ruff' for extremely fast Python linting" 7 | # entry: ruff check --force-exclude 8 | # language: python 9 | # types_or: [python, pyi] 10 | # args: [--fix, --exit-non-zero-on-fix] 11 | # require_serial: true 12 | - id: bandit 13 | name: bandit 14 | entry: bandit 15 | language: system 16 | types: [python] 17 | require_serial: true 18 | args: ["-c", "pyproject.toml"] 19 | - id: black 20 | name: black 21 | entry: black 22 | language: system 23 | types: [python] 24 | require_serial: true 25 | - id: check-added-large-files 26 | name: Check for added large files 27 | entry: check-added-large-files 28 | language: system 29 | - id: check-toml 30 | name: Check Toml 31 | entry: check-toml 32 | language: system 33 | types: [toml] 34 | - id: check-yaml 35 | name: Check Yaml 36 | entry: check-yaml 37 | language: system 38 | types: [yaml] 39 | - id: darglint 40 | name: darglint 41 | entry: darglint 42 | language: system 43 | types: [python] 44 | stages: [manual] 45 | - id: end-of-file-fixer 46 | name: Fix End of Files 47 | entry: end-of-file-fixer 48 | language: system 49 | types: [text] 50 | stages: [commit, push, manual] 51 | - id: flake8 52 | name: flake8 53 | entry: flake8 54 | language: system 55 | types: [python] 56 | require_serial: true 57 | args: [--darglint-ignore-regex, .*] 58 | - id: isort 59 | name: isort 60 | entry: isort 61 | require_serial: true 62 | language: system 63 | types_or: [cython, pyi, python] 64 | args: ["--filter-files"] 65 | - id: pyupgrade 66 | name: pyupgrade 67 | description: Automatically upgrade syntax for newer versions. 68 | entry: pyupgrade 69 | language: system 70 | types: [python] 71 | args: [--py37-plus] 72 | - id: trailing-whitespace 73 | name: Trim Trailing Whitespace 74 | entry: trailing-whitespace-fixer 75 | language: system 76 | types: [text] 77 | stages: [commit, push, manual] 78 | - repo: https://github.com/pre-commit/mirrors-prettier 79 | rev: v2.6.0 80 | hooks: 81 | - id: prettier 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI Tests](https://github.com/timhughes/cookiecutter-poetry/actions/workflows/ci-tests.yml/badge.svg)](https://github.com/timhughes/cookiecutter-poetry/actions/workflows/ci-tests.yml) 2 | 3 | # cookiecutter-poetry 4 | 5 | Cookiecutter template configured with the following: 6 | 7 | - poetry 8 | - pytest 9 | - black 10 | - bandit 11 | - pyinstaller 12 | - jupyterlab 13 | - click 14 | 15 | A Makefile has been included so you don't have to remember commands. 16 | 17 | ## Usage: 18 | 19 | cookiecutter https://github.com/timhughes/cookiecutter-poetry.git 20 | 21 | eg: 22 | 23 | $ cookiecutter https://github.com/timhughes/cookiecutter-poetry.git 24 | You've downloaded /home/thughes/.cookiecutters/cookiecutter-poetry before. Is it okay to delete and re-download it? [yes]: 25 | author [Tim Hughes]: 26 | email [thughes@thegoldfish.org]: 27 | github_user [timhughes]: 28 | package_name [example-project]: 29 | module_name [example_project]: 30 | short_description [A simple application]: 31 | version [0.0.1]: 32 | Select license: 33 | 1 - MIT 34 | 2 - BSD-3-Clause 35 | 3 - GPL-3.0-or-later 36 | 4 - Proprietary 37 | Choose from 1, 2, 3, 4 [1]: 38 | Select command_line_interface: 39 | 1 - click 40 | 2 - no cli 41 | Choose from 1, 2 [1]: 42 | use_jupyterlab [n]: 43 | add_badges [y]: 44 | 45 | Access the poetry commands as usual: 46 | 47 | $ poetry add requests 48 | Using version ^2.25.1 for requests 49 | 50 | Updating dependencies 51 | Resolving dependencies... (0.3s) 52 | 53 | Writing lock file 54 | 55 | Package operations: 5 installs, 0 updates, 0 removals 56 | 57 | • Installing certifi (2020.12.5) 58 | • Installing chardet (4.0.0) 59 | • Installing idna (2.10) 60 | • Installing urllib3 (1.26.3) 61 | • Installing requests (2.25.1) 62 | 63 | You can then use the Makefile for other common commands 64 | 65 | $ make 66 | make help # these help instructions 67 | make pydoc # Run a pydoc server and open the browser 68 | make install # Run `poetry install` 69 | make showdeps # run poetry to show deps 70 | make lint # Runs bandit and black in check mode 71 | make format # Formats you code with Black 72 | make test # run pytest with coverage 73 | make build # run `poetry build` to build source distribution and wheel 74 | make pyinstaller # Create a binary executable using pyinstaller 75 | make run # run `poetry run example-project` 76 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/src/{{cookiecutter.module_name}}/__main__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import yaml 4 | import logging 5 | import click 6 | 7 | import {{cookiecutter.module_name}} 8 | from {{cookiecutter.module_name}}.app import App 9 | 10 | if sys.stdout.isatty(): 11 | # You're running in a real terminal 12 | LOG_FORMAT="%(asctime)s - %(name)s - %(levelname)s - %(message)s" 13 | else: 14 | LOG_FORMAT="%(name)s - %(levelname)s - %(message)s" 15 | 16 | logging.basicConfig( 17 | level=getattr(logging, os.getenv("LOGLEVEL", "INFO").upper()), 18 | format=LOG_FORMAT, 19 | datefmt="%Y-%m-%dT%H:%M:%S" 20 | ) 21 | 22 | logger = logging.getLogger("{{cookiecutter.module_name}}") 23 | 24 | # set levels for other modules 25 | logging.getLogger("urllib3").setLevel(logging.WARNING) 26 | 27 | @click.group() 28 | @click.version_option(package_name="{{cookiecutter.package_name}}") 29 | def cli(): 30 | pass 31 | 32 | @cli.command() 33 | @click.option("-v", "--verbose", count=True) 34 | def version(verbose): 35 | """Displays the version""" 36 | click.echo("Version: %s" % {{cookiecutter.module_name}}.__version__) 37 | if verbose > 0: 38 | click.echo("Author: %s" % {{cookiecutter.module_name}}.__author__) 39 | 40 | 41 | @cli.command() 42 | @click.option( 43 | "-c", 44 | "--conf", 45 | "--config", 46 | "config_file", 47 | type=click.Path(exists=True), 48 | ) 49 | def serve(config_file): 50 | """Start {{cookiecutter.package_name}} in server mode""" 51 | 52 | settings = {} 53 | if config_file: 54 | try: 55 | with open(config_file, "r") as stream: 56 | try: 57 | settings = yaml.safe_load(stream) 58 | except yaml.YAMLError as exc: 59 | click.echo(exc) 60 | sys.exit(1) 61 | except IOError as exc: 62 | logger.fatal("%s: %s", exc.strerror, exc.filename) 63 | sys.exit(1) 64 | except Exception as exc: 65 | logger.fatal( 66 | "Cannot load conf file '%s'. Error message is: %s", config_file, exc 67 | ) 68 | sys.exit(1) 69 | 70 | # TODO: Create your application object 71 | app = App(settings) 72 | try: 73 | logger.info("Starting") 74 | # TODO: Start your application 75 | app.start() 76 | except KeyboardInterrupt: 77 | pass 78 | except Exception as exc: # pylint: disable=broad-except 79 | logger.exception("Unexpected exception: %s", exc) 80 | finally: 81 | logger.info("Shutting down") 82 | # TODO: Cleanup code 83 | app.stop() 84 | 85 | logger.info("All done") 86 | 87 | 88 | if __name__ == "__main__": 89 | cli(prog_name="{{cookiecutter.package_name}}") 90 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/LICENSE: -------------------------------------------------------------------------------- 1 | {% if cookiecutter.license == 'MIT' -%} 2 | MIT License 3 | 4 | Copyright (c) {% now 'local', '%Y' %}, {{ cookiecutter.author }} 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | {% elif cookiecutter.license == 'GPL-3.0-or-later' -%} 24 | GNU GENERAL PUBLIC LICENSE 25 | Version 3, 29 June 2007 26 | 27 | {{ cookiecutter.short_description }} 28 | Copyright (C) {% now 'local', '%Y' %} {{ cookiecutter.author }} 29 | 30 | This program is free software: you can redistribute it and/or modify 31 | it under the terms of the GNU General Public License as published by 32 | the Free Software Foundation, either version 3 of the License, or 33 | (at your option) any later version. 34 | 35 | This program is distributed in the hope that it will be useful, 36 | but WITHOUT ANY WARRANTY; without even the implied warranty of 37 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 38 | GNU General Public License for more details. 39 | 40 | You should have received a copy of the GNU General Public License 41 | along with this program. If not, see . 42 | 43 | Also add information on how to contact you by electronic and paper mail. 44 | 45 | You should also get your employer (if you work as a programmer) or school, 46 | if any, to sign a "copyright disclaimer" for the program, if necessary. 47 | For more information on this, and how to apply and follow the GNU GPL, see 48 | . 49 | 50 | The GNU General Public License does not permit incorporating your program 51 | into proprietary programs. If your program is a subroutine library, you 52 | may consider it more useful to permit linking proprietary applications with 53 | the library. If this is what you want to do, use the GNU Lesser General 54 | Public License instead of this License. But first, please read 55 | . 56 | {% elif cookiecutter.license == 'Proprietary' -%} 57 | Proprietary and confidential 58 | Copyright (c) {% now 'local', '%Y' %}, {{ cookiecutter.author }} 59 | All rights reserved. 60 | 61 | Unauthorized copying of this project, via any medium is strictly prohibited 62 | {% endif %} 63 | 64 | -------------------------------------------------------------------------------- /{{cookiecutter.package_name}}/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry_core"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "{{ cookiecutter.package_name }}" 7 | version = "{{ cookiecutter.version }}" 8 | description = "{{ cookiecutter.short_description }}" 9 | authors = ["{{ cookiecutter.author }} <{{ cookiecutter.email }}>"] 10 | # Use identifier from https://spdx.org/licenses/ 11 | license = "{{ cookiecutter.license }}" 12 | readme = "README.md" 13 | homepage = "https://github.com/{{ cookiecutter.github_user }}/{{ cookiecutter.package_name }}" 14 | repository = "https://github.com/{{ cookiecutter.github_user }}/{{ cookiecutter.package_name }}" 15 | documentation = "README.md" 16 | classifiers = [ 17 | # https://pypi.org/classifiers/ 18 | {%- if cookiecutter.command_line_interface != "no cli" %} 19 | "Environment :: Console", 20 | {%- endif %} 21 | "Development Status :: 1 - Planning", 22 | ] 23 | {%- if cookiecutter.package_name|lower|replace('-', '_') != cookiecutter.module_name %} 24 | packages = [ 25 | { include = "{{ cookiecutter.module_name }}", from = "src" } 26 | ] 27 | {%- endif %} 28 | 29 | [tool.poetry.urls] 30 | # If you publish you package on PyPI, these will appear in the Project Links section. 31 | "Bug Tracker" = "https://github.com/{{ cookiecutter.github_user }}/{{ cookiecutter.package_name }}/issues" 32 | 33 | [tool.poetry.scripts] 34 | {%- if cookiecutter.command_line_interface != "no cli" %} 35 | {{ cookiecutter.package_name }} = "{{ cookiecutter.module_name }}.__main__:cli" 36 | {%- endif %} 37 | 38 | [tool.poetry.dependencies] 39 | python = "^3.10.0" 40 | {%- if cookiecutter.command_line_interface == "click" %} 41 | click = "*" 42 | {%- endif %} 43 | {%- if cookiecutter.use_jupyterlab == "y" %} 44 | jupyterlab = "*" 45 | pandas = "*" 46 | jupyterlab_git = "*" 47 | jupyterlab_widgets = "*" 48 | {%- endif %} 49 | 50 | [tool.poetry.group.test.dependencies] 51 | bandit = "*" 52 | black = "*" 53 | bump2version = "*" 54 | flake8 = "*" 55 | isort = "*" 56 | jedi-language-server = "*" 57 | mypy = "*" 58 | pylint = "*" 59 | pytest = "*" 60 | pytest-cov = "*" 61 | reorder-python-imports = "*" 62 | vulture = "*" 63 | typeguard = "*" 64 | pre-commit = "*" 65 | pre-commit-hooks = "*" 66 | pyupgrade = "*" 67 | darglint = "*" 68 | {%- if cookiecutter.command_line_interface != "no cli" %} 69 | pyinstaller = "*" 70 | {%- endif %} 71 | 72 | [tool.poetry.group.docs.dependencies] 73 | furo = "*" 74 | myst_parser = "*" 75 | sphinx = "*" 76 | sphinx-autobuild = "*" 77 | sphinx-click = "*" 78 | 79 | [tool.isort] 80 | profile = "black" 81 | 82 | [tool.black] 83 | target-version = ['py311'] 84 | strict = true 85 | warn_unreachable = true 86 | pretty = true 87 | show_column_numbers = true 88 | show_error_context = true 89 | 90 | [tool.ruff] 91 | select = ["ALL"] 92 | lines-after-imports = 2 93 | [tool.ruff.per-file-ignores] 94 | "tests/**/*.py" = [ 95 | # at least this three should be fine in tests: 96 | "S101", # asserts allowed in tests... 97 | "ARG", # Unused function args -> fixtures nevertheless are functionally relevant... 98 | "FBT", # Don't care about booleans as positional arguments in tests, e.g. via @pytest.mark.parametrize() 99 | # The below are debateable 100 | "PLR2004", # Magic value used in comparison, ... 101 | "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes 102 | ] 103 | 104 | [tool.bandit] 105 | [tool.bandit.assert_used] 106 | skips = ['*_test.py', '*/test_*.py'] 107 | 108 | [tool.pytest.ini_options] 109 | pythonpath = [ 110 | "src" 111 | ] 112 | # Example 113 | filterwarnings = [ 114 | "ignore::DeprecationWarning:moto.*:", 115 | "ignore::DeprecationWarning:boto.*:", 116 | ] 117 | -------------------------------------------------------------------------------- /tests/test_bake_project.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | import shlex 3 | import os 4 | import sys 5 | import subprocess 6 | import yaml 7 | import datetime 8 | from cookiecutter.utils import rmtree 9 | 10 | from click.testing import CliRunner 11 | 12 | import importlib 13 | 14 | 15 | @contextmanager 16 | def inside_dir(dirpath): 17 | """ 18 | Execute code from inside the given directory 19 | :param dirpath: String, path of the directory the command is being run. 20 | """ 21 | old_path = os.getcwd() 22 | try: 23 | os.chdir(dirpath) 24 | yield 25 | finally: 26 | os.chdir(old_path) 27 | 28 | 29 | @contextmanager 30 | def bake_in_temp_dir(cookies, *args, **kwargs): 31 | """ 32 | Delete the temporal directory that is created when executing the tests 33 | :param cookies: pytest_cookies.Cookies, 34 | cookie to be baked and its temporal files will be removed 35 | """ 36 | result = cookies.bake(*args, **kwargs) 37 | try: 38 | yield result 39 | finally: 40 | rmtree(str(result.project)) 41 | 42 | 43 | def run_inside_dir(command, dirpath): 44 | """ 45 | Run a command from inside a given directory, returning the exit status 46 | :param command: Command that will be executed 47 | :param dirpath: String, path of the directory the command is being run. 48 | """ 49 | with inside_dir(dirpath): 50 | return subprocess.check_call(shlex.split(command)) 51 | 52 | 53 | def check_output_inside_dir(command, dirpath): 54 | "Run a command from inside a given directory, returning the command output" 55 | with inside_dir(dirpath): 56 | return subprocess.check_output(shlex.split(command)) 57 | 58 | 59 | def project_info(result): 60 | """Get toplevel dir, package_name, and project dir from baked cookies""" 61 | project_path = str(result.project) 62 | package_name = os.path.split(project_path)[-1] 63 | project_dir = os.path.join(project_path, "src", package_name.replace("-", "_")) 64 | return project_path, package_name, project_dir 65 | 66 | 67 | def test_bake_with_defaults(cookies): 68 | with bake_in_temp_dir(cookies) as result: 69 | assert result.project.isdir() 70 | assert result.exit_code == 0 71 | assert result.exception is None 72 | 73 | found_toplevel_files = [f.basename for f in result.project.listdir()] 74 | assert "src" in found_toplevel_files 75 | assert "tests" in found_toplevel_files 76 | assert "pyproject.toml" in found_toplevel_files 77 | assert "README.md" in found_toplevel_files 78 | 79 | assert "notebooks" not in found_toplevel_files 80 | 81 | 82 | def test_year_compute_in_license_file(cookies): 83 | with bake_in_temp_dir(cookies) as result: 84 | license_file_path = result.project.join("LICENSE") 85 | now = datetime.datetime.now() 86 | assert str(now.year) in license_file_path.read() 87 | 88 | 89 | def test_bake_and_run_tests(cookies): 90 | context = {"command_line_interface": "click"} 91 | with bake_in_temp_dir(cookies, extra_context=context) as result: 92 | assert result.project.isdir() 93 | run_inside_dir("make install", str(result.project)) == 0 94 | run_inside_dir("make test", str(result.project)) == 0 95 | print("test_bake_and_run_tests path", str(result.project)) 96 | 97 | 98 | def test_make_help(cookies): 99 | with bake_in_temp_dir(cookies) as result: 100 | # The supplied Makefile does not support win32 101 | if sys.platform != "win32": 102 | output = check_output_inside_dir("make help", str(result.project)) 103 | assert b"make help # these help instructions" in output 104 | 105 | 106 | def test_bake_selecting_license(cookies): 107 | license_strings = { 108 | "MIT": "MIT License", 109 | "GPL-3.0-or-later": "GNU GENERAL PUBLIC LICENSE", 110 | "Proprietary": "Proprietary and confidential", 111 | } 112 | for license, target_string in license_strings.items(): 113 | with bake_in_temp_dir(cookies, extra_context={"license": license}) as result: 114 | assert target_string in result.project.join("LICENSE").read() 115 | assert license in result.project.join("pyproject.toml").read() 116 | 117 | 118 | def test_bake_with_no_console_script(cookies): 119 | context = {"command_line_interface": "no cli"} 120 | result = cookies.bake(extra_context=context) 121 | project_path, _, project_dir = project_info(result) 122 | print(project_dir) 123 | found_project_files = os.listdir(project_dir) 124 | assert "cli.py" not in found_project_files 125 | 126 | pyproject_path = os.path.join(project_path, "pyproject.toml") 127 | with open(pyproject_path, "r") as pyproject_file: 128 | assert "entry_points" not in pyproject_file.read() 129 | 130 | 131 | def test_bake_with_jupyterlab(cookies): 132 | context = {"use_jupyterlab": "y"} 133 | result = cookies.bake(extra_context=context) 134 | found_toplevel_files = [f.basename for f in result.project.listdir()] 135 | assert "notebooks" in found_toplevel_files 136 | 137 | pyproject_path = os.path.join(result.project, "pyproject.toml") 138 | 139 | with open(pyproject_path, "r") as pyproject_file: 140 | assert "jupyterlab" in pyproject_file.read() 141 | --------------------------------------------------------------------------------