pax_global_header00006660000000000000000000000064145654333350014525gustar00rootroot0000000000000052 comment=da0df9d3a09205749307c403f06a1b4ca3af4cb8 cookiecutter-2.6.0/000077500000000000000000000000001456543333500142325ustar00rootroot00000000000000cookiecutter-2.6.0/.bandit000066400000000000000000000000571456543333500154760ustar00rootroot00000000000000[bandit] exclude=tests/* targets=cookiecutter/ cookiecutter-2.6.0/.gitattributes000066400000000000000000000002571456543333500171310ustar00rootroot00000000000000# By default, all text files should use LF newlines. * text=auto eol=lf # Use CRLF newlines for text files containing "crlf" in their names. *crlf* text=auto eol=crlf cookiecutter-2.6.0/.github/000077500000000000000000000000001456543333500155725ustar00rootroot00000000000000cookiecutter-2.6.0/.github/ISSUE_TEMPLATE.md000066400000000000000000000005251456543333500203010ustar00rootroot00000000000000* Cookiecutter version: * Template project url: * Python version: * Operating System: ### Description: // REPLACE ME: What are you trying to get done, what has happened, what went wrong, and what did you expect? ### What I've run: ``` // REPLACE ME: Paste a log of command(s) you ran and cookiecutter's output, tracebacks, etc, here ``` cookiecutter-2.6.0/.github/dependabot.yml000066400000000000000000000001671456543333500204260ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" cookiecutter-2.6.0/.github/release-drafter.yml000066400000000000000000000025631456543333500213700ustar00rootroot00000000000000commitish: main name-template: '$RESOLVED_VERSION' tag-template: '$RESOLVED_VERSION' category-template: '### $TITLE' version-resolver: major: labels: - 'breaking-change' - 'major' minor: labels: - 'enhancement' - 'feature' patch: labels: - 'bug' - 'CI/CD' - 'code style' - 'documentation' - 'tests' - 'patch' default: patch autolabeler: - label: 'CI/CD' files: - '.github/*' - '.pre-commit-config.yaml' - '*.cfg' - '*.ini' - 'setup.*' - 'docs/conf.py' - 'Makefile' - 'make.bat' - '*requirements*.txt' - label: 'documentation' files: - '*.md' - '*.rst' categories: - title: 'Breaking Changes' labels: - 'breaking-change' - title: 'Non-Breaking Changes' labels: - 'major' - title: 'Minor Changes' labels: - 'feature' - 'enhancement' - title: 'CI/CD and QA changes' labels: - 'CI/CD' - 'tests' - 'code style' - title: 'Documentation updates' labels: - 'documentation' - title: 'Bugfixes' labels: - 'bug' - title: 'Deprecations' labels: - 'deprecated' sort-by: title sort-direction: ascending exclude-labels: - 'skip-changelog' template: | ## Changes $CHANGES ### This release is made by wonderful contributors: $CONTRIBUTORS cookiecutter-2.6.0/.github/workflows/000077500000000000000000000000001456543333500176275ustar00rootroot00000000000000cookiecutter-2.6.0/.github/workflows/drafter.yml000066400000000000000000000006301456543333500220000ustar00rootroot00000000000000name: Release Drafter on: push: branches: - master # autolabeler pull_request: types: - opened - reopened - synchronize jobs: update_release_draft: permissions: contents: write pull-requests: write runs-on: ubuntu-latest steps: - uses: release-drafter/release-drafter@v5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} cookiecutter-2.6.0/.github/workflows/pip-publish.yml000066400000000000000000000014511456543333500226070ustar00rootroot00000000000000name: Upload to PyPI on: release: types: [published] jobs: upload: runs-on: ubuntu-latest environment: name: pypi.org url: https://pypi.org/project/cookiecutter/ steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install pypa/build run: >- python -m pip install build --user - name: Build a binary wheel and a source tarball run: >- python -m build --sdist --wheel --outdir dist/ . - name: Publish a Python distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} cookiecutter-2.6.0/.github/workflows/tests.yml000066400000000000000000000036751456543333500215270ustar00rootroot00000000000000name: CI/CD Tests on: push: branches: - main tags: - "*" pull_request: branches: - "*" jobs: documentation_build_test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: 'recursive' - uses: actions/setup-python@v5 with: python-version: "3.10" - name: Install dependencies run: | python -m pip install --upgrade pip pip install tox - name: Build docs run: tox -e docs - uses: actions/upload-artifact@v4 with: name: DocumentationHTML path: docs/_build/html/ tests_run: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: - ubuntu-latest - macos-latest - windows-latest python: - "3.7" - "3.8" - "3.9" - "3.10" - "3.11" - "3.12" steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install tox - name: Project internals test build run: "tox -e py${{ matrix.python }}" - name: Project security test run: "tox -e safety" - name: Send coverage report to codeclimate continue-on-error: true uses: paambaati/codeclimate-action@v5.0.0 with: coverageCommand: echo "Ignore rerun" coverageLocations: ${{github.workspace}}/coverage.xml:coverage.py env: CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}} - name: Send coverage report to codecov uses: codecov/codecov-action@v3 with: env_vars: OS=${{ matrix.os }},PYTHON=${{ matrix.python }} file: ./coverage.xml cookiecutter-2.6.0/.gitignore000066400000000000000000000063531456543333500162310ustar00rootroot00000000000000# Adapted from https://github.com/github/gitignore/blob/main/Python.gitignore tests/tmp/ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/#use-with-ide .pdm.toml # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ # VSCode settings (e.g. .vscode/settings.json containing personal preferred path to venv) .vscode/ # macOS auto-generated file .DS_Store cookiecutter-2.6.0/.pre-commit-config.yaml000066400000000000000000000043511456543333500205160ustar00rootroot00000000000000--- repos: - repo: meta hooks: - id: check-hooks-apply - id: check-useless-excludes - repo: https://github.com/PyCQA/doc8 rev: v1.1.1 hooks: - id: doc8 name: doc8 description: This hook runs doc8 for linting docs. entry: python -m doc8 language: python files: \.rst$ require_serial: true - repo: https://github.com/psf/black-pre-commit-mirror rev: 24.2.0 hooks: - id: black language_version: python3 exclude: ^(tests\/hooks-abort-render\/hooks|docs\/HelloCookieCutter1) - repo: https://github.com/pycqa/isort rev: 5.13.2 hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: trailing-whitespace - id: mixed-line-ending name: "Enforce LF newlines on most files" args: - "--fix=lf" # Exclude files with "crlf" in their names. exclude: "crlf" - id: mixed-line-ending name: "Enforce CRLF newlines on files named '*crlf*'" args: - "--fix=crlf" files: "crlf" - id: end-of-file-fixer - id: fix-byte-order-marker - id: check-executables-have-shebangs - id: check-shebang-scripts-are-executable - id: check-merge-conflict - id: check-symlinks - id: check-case-conflict - id: check-docstring-first - id: pretty-format-json args: - "--autofix" - "--indent=2" - "--no-sort-keys" - "--no-ensure-ascii" exclude: "invalid-syntax.json|tests/fake-repo-bad-json/cookiecutter.json|tests/fake-repo/cookiecutter.json" - id: check-toml - id: check-xml - id: check-yaml exclude: "not_rendered.yml|invalid-config.yaml|invalid-config-w-multiple-docs.yaml" - repo: https://github.com/PyCQA/flake8 rev: 7.0.0 hooks: - id: flake8 additional_dependencies: - flake8-absolute-import - flake8-black - flake8-docstrings - repo: https://github.com/PyCQA/bandit rev: 1.7.7 hooks: - id: bandit args: [--ini, .bandit] - repo: https://github.com/mgedmin/check-manifest rev: "0.49" hooks: - id: check-manifest cookiecutter-2.6.0/.readthedocs.yaml000066400000000000000000000005541456543333500174650ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 build: os: ubuntu-22.04 tools: python: "3.10" sphinx: configuration: docs/conf.py formats: - htmlzip - pdf - epub python: install: - requirements: docs/requirements.txt - method: pip path: . cookiecutter-2.6.0/AUTHORS.md000066400000000000000000000270241456543333500157060ustar00rootroot00000000000000# Credits ## Development Leads - Audrey Roy Greenfeld ([@audreyfeldroy](https://github.com/audreyfeldroy)) - Daniel Roy Greenfeld ([@pydanny](https://github.com/pydanny)) - Raphael Pierzina ([@hackebrot](https://github.com/hackebrot)) ## Core Committers - Michael Joseph ([@michaeljoseph](https://github.com/michaeljoseph)) - Paul Moore ([@pfmoore](https://github.com/pfmoore)) - Andrey Shpak ([@insspb](https://github.com/insspb)) - Sorin Sbarnea ([@ssbarnea](https://github.com/ssbarnea)) - Fábio C. Barrionuevo da Luz ([@luzfcb](https://github.com/luzfcb)) - Simone Basso ([@simobasso](https://github.com/simobasso)) - Jens Klein ([@jensens](https://github.com/jensens)) - Érico Andrei ([@ericof](https://github.com/ericof)) ## Contributors - Steven Loria ([@sloria](https://github.com/sloria)) - Goran Peretin ([@gperetin](https://github.com/gperetin)) - Hamish Downer ([@foobacca](https://github.com/foobacca)) - Thomas Orozco ([@krallin](https://github.com/krallin)) - Jindrich Smitka ([@s-m-i-t-a](https://github.com/s-m-i-t-a)) - Benjamin Schwarze ([@benjixx](https://github.com/benjixx)) - Raphi ([@raphigaziano](https://github.com/raphigaziano)) - Thomas Chiroux ([@ThomasChiroux](https://github.com/ThomasChiroux)) - Sergi Almacellas Abellana ([@pokoli](https://github.com/pokoli)) - Alex Gaynor ([@alex](https://github.com/alex)) - Rolo ([@rolo](https://github.com/rolo)) - Pablo ([@oubiga](https://github.com/oubiga)) - Bruno Rocha ([@rochacbruno](https://github.com/rochacbruno)) - Alexander Artemenko ([@svetlyak40wt](https://github.com/svetlyak40wt)) - Mahmoud Abdelkader ([@mahmoudimus](https://github.com/mahmoudimus)) - Leonardo Borges Avelino ([@lborgav](https://github.com/lborgav)) - Chris Trotman ([@solarnz](https://github.com/solarnz)) - Rolf ([@relekang](https://github.com/relekang)) - Noah Kantrowitz ([@coderanger](https://github.com/coderanger)) - Vincent Bernat ([@vincentbernat](https://github.com/vincentbernat)) - Germán Moya ([@pbacterio](https://github.com/pbacterio)) - Ned Batchelder ([@nedbat](https://github.com/nedbat)) - Dave Dash ([@davedash](https://github.com/davedash)) - Johan Charpentier ([@cyberj](https://github.com/cyberj)) - Éric Araujo ([@merwok](https://github.com/merwok)) - saxix ([@saxix](https://github.com/saxix)) - Tzu-ping Chung ([@uranusjr](https://github.com/uranusjr)) - Caleb Hattingh ([@cjrh](https://github.com/cjrh)) - Flavio Curella ([@fcurella](https://github.com/fcurella)) - Adam Venturella ([@aventurella](https://github.com/aventurella)) - Monty Taylor ([@emonty](https://github.com/emonty)) - schacki ([@schacki](https://github.com/schacki)) - Ryan Olson ([@ryanolson](https://github.com/ryanolson)) - Trey Hunner ([@treyhunner](https://github.com/treyhunner)) - Russell Keith-Magee ([@freakboy3742](https://github.com/freakboy3742)) - Mishbah Razzaque ([@mishbahr](https://github.com/mishbahr)) - Robin Andeer ([@robinandeer](https://github.com/robinandeer)) - Rachel Sanders ([@trustrachel](https://github.com/trustrachel)) - Rémy Hubscher ([@Natim](https://github.com/Natim)) - Dino Petron3 ([@dinopetrone](https://github.com/dinopetrone)) - Peter Inglesby ([@inglesp](https://github.com/inglesp)) - Ramiro Batista da Luz ([@ramiroluz](https://github.com/ramiroluz)) - Omer Katz ([@thedrow](https://github.com/thedrow)) - lord63 ([@lord63](https://github.com/lord63)) - Randy Syring ([@rsyring](https://github.com/rsyring)) - Mark Jones ([@mark0978](https://github.com/mark0978)) - Marc Abramowitz ([@msabramo](https://github.com/msabramo)) - Lucian Ursu ([@LucianU](https://github.com/LucianU)) - Osvaldo Santana Neto ([@osantana](https://github.com/osantana)) - Matthias84 ([@Matthias84](https://github.com/Matthias84)) - Simeon Visser ([@svisser](https://github.com/svisser)) - Guruprasad ([@lgp171188](https://github.com/lgp171188)) - Charles-Axel Dein ([@charlax](https://github.com/charlax)) - Diego Garcia ([@drgarcia1986](https://github.com/drgarcia1986)) - maiksensi ([@maiksensi](https://github.com/maiksensi)) - Andrew Conti ([@agconti](https://github.com/agconti)) - Valentin Lab ([@vaab](https://github.com/vaab)) - Ilja Bauer ([@iljabauer](https://github.com/iljabauer)) - Elias Dorneles ([@eliasdorneles](https://github.com/eliasdorneles)) - Matias Saguir ([@mativs](https://github.com/mativs)) - Johannes ([@johtso](https://github.com/johtso)) - macrotim ([@macrotim](https://github.com/macrotim)) - Will McGinnis ([@wdm0006](https://github.com/wdm0006)) - Cédric Krier ([@cedk](https://github.com/cedk)) - Tim Osborn ([@ptim](https://github.com/ptim)) - Aaron Gallagher ([@habnabit](https://github.com/habnabit)) - mozillazg ([@mozillazg](https://github.com/mozillazg)) - Joachim Jablon ([@ewjoachim](https://github.com/ewjoachim)) - Andrew Ittner ([@tephyr](https://github.com/tephyr)) - Diane DeMers Chen ([@purplediane](https://github.com/purplediane)) - zzzirk ([@zzzirk](https://github.com/zzzirk)) - Carol Willing ([@willingc](https://github.com/willingc)) - phoebebauer ([@phoebebauer](https://github.com/phoebebauer)) - Adam Chainz ([@adamchainz](https://github.com/adamchainz)) - Sulé ([@suledev](https://github.com/suledev)) - Evan Palmer ([@palmerev](https://github.com/palmerev)) - Bruce Eckel ([@BruceEckel](https://github.com/BruceEckel)) - Robert Lyon ([@ivanlyon](https://github.com/ivanlyon)) - Terry Bates ([@terryjbates](https://github.com/terryjbates)) - Brett Cannon ([@brettcannon](https://github.com/brettcannon)) - Michael Warkentin ([@mwarkentin](https://github.com/mwarkentin)) - Bartłomiej Kurzeja ([@B3QL](https://github.com/B3QL)) - Thomas O'Donnell ([@andytom](https://github.com/andytom)) - Jeremy Carbaugh ([@jcarbaugh](https://github.com/jcarbaugh)) - Nathan Cheung ([@cheungnj](https://github.com/cheungnj)) - Abdó Roig-Maranges ([@aroig](https://github.com/aroig)) - Steve Piercy ([@stevepiercy](https://github.com/stevepiercy)) - Corey ([@coreysnyder04](https://github.com/coreysnyder04)) - Dmitry Evstratov ([@devstrat](https://github.com/devstrat)) - Eyal Levin ([@eyalev](https://github.com/eyalev)) - mathagician ([@mathagician](https://github.com/mathagician)) - Guillaume Gelin ([@ramnes](https://github.com/ramnes)) - @delirious-lettuce ([@delirious-lettuce](https://github.com/delirious-lettuce)) - Gasper Vozel ([@karantan](https://github.com/karantan)) - Joshua Carp ([@jmcarp](https://github.com/jmcarp)) - @meahow ([@meahow](https://github.com/meahow)) - Andrea Grandi ([@andreagrandi](https://github.com/andreagrandi)) - Issa Jubril ([@jubrilissa](https://github.com/jubrilissa)) - Nytiennzo Madooray ([@Nythiennzo](https://github.com/Nythiennzo)) - Erik Bachorski ([@dornheimer](https://github.com/dornheimer)) - cclauss ([@cclauss](https://github.com/cclauss)) - Andy Craze ([@accraze](https://github.com/accraze)) - Anthony Sottile ([@asottile](https://github.com/asottile)) - Jonathan Sick ([@jonathansick](https://github.com/jonathansick)) - Hugo ([@hugovk](https://github.com/hugovk)) - Min ho Kim ([@minho42](https://github.com/minho42)) - Ryan Ly ([@rly](https://github.com/rly)) - Akintola Rahmat ([@mihrab34](https://github.com/mihrab34)) - Jai Ram Rideout ([@jairideout](https://github.com/jairideout)) - Diego Carrasco Gubernatis ([@dacog](https://github.com/dacog)) - Wagner Negrão ([@wagnernegrao](https://github.com/wagnernegrao)) - Josh Barnes ([@jcb91](https://github.com/jcb91)) - Nikita Sobolev ([@sobolevn](https://github.com/sobolevn)) - Matt Stibbs ([@mattstibbs](https://github.com/mattstibbs)) - MinchinWeb ([@MinchinWeb](https://github.com/MinchinWeb)) - kishan ([@kishan](https://github.com/kishan3)) - tonytheleg ([@tonytheleg](https://github.com/tonytheleg)) - Roman Hartmann ([@RomHartmann](https://github.com/RomHartmann)) - DSEnvel ([@DSEnvel](https://github.com/DSEnvel)) - kishan ([@kishan](https://github.com/kishan3)) - Bruno Alla ([@browniebroke](https://github.com/browniebroke)) - nicain ([@nicain](https://github.com/nicain)) - Carsten Rösnick-Neugebauer ([@croesnick](https://github.com/croesnick)) - igorbasko01 ([@igorbasko01](https://github.com/igorbasko01)) - Dan Booth Dev ([@DanBoothDev](https://github.com/DanBoothDev)) - Pablo Panero ([@ppanero](https://github.com/ppanero)) - Chuan-Heng Hsiao ([@chhsiao1981](https://github.com/chhsiao1981)) - Mohammad Hossein Sekhavat ([@mhsekhavat](https://github.com/mhsekhavat)) - Amey Joshi ([@amey589](https://github.com/amey589)) - Paul Harrison ([@smoothml](https://github.com/smoothml)) - Fabio Todaro ([@SharpEdgeMarshall](https://github.com/SharpEdgeMarshall)) - Nicholas Bollweg ([@bollwyvl](https://github.com/bollwyvl)) - Jace Browning ([@jacebrowning](https://github.com/jacebrowning)) - Ionel Cristian Mărieș ([@ionelmc](https://github.com/ionelmc)) - Kishan Mehta ([@kishan3](https://github.com/kishan3)) - Wieland Hoffmann ([@mineo](https://github.com/mineo)) - Antony Lee ([@anntzer](https://github.com/anntzer)) - Aurélien Gâteau ([@agateau](https://github.com/agateau)) - Axel H. ([@noirbizarre](https://github.com/noirbizarre)) - Chris ([@chrisbrake](https://github.com/chrisbrake)) - Chris Streeter ([@streeter](https://github.com/streeter)) - Gábor Lipták ([@gliptak](https://github.com/gliptak)) - Javier Sánchez Portero ([@javiersanp](https://github.com/javiersanp)) - Nimrod Milo ([@milonimrod](https://github.com/milonimrod)) - Philipp Kats ([@Casyfill](https://github.com/Casyfill)) - Reinout van Rees ([@reinout](https://github.com/reinout)) - Rémy Greinhofer ([@rgreinho](https://github.com/rgreinho)) - Sebastian ([@sebix](https://github.com/sebix)) - Stuart Mumford ([@Cadair](https://github.com/Cadair)) - Tom Forbes ([@orf](https://github.com/orf)) - Xie Yanbo ([@xyb](https://github.com/xyb)) - Maxim Ivanov ([@ivanovmg](https://github.com/ivanovmg)) ## Backers We would like to thank the following people for supporting us in our efforts to maintain and improve Cookiecutter: - Alex DeBrie - Alexandre Y. Harano - Bruno Alla - Carol Willing - Russell Keith-Magee ## Sprint Contributors ### PyCon 2016 Sprint The following people made contributions to the cookiecutter project at the PyCon sprints in Portland, OR from June 2-5 2016. Contributions include user testing, debugging, improving documentation, reviewing issues, writing tutorials, creating and updating project templates, and teaching each other. - Adam Chainz ([@adamchainz](https://github.com/adamchainz)) - Andrew Ittner ([@tephyr](https://github.com/tephyr)) - Audrey Roy Greenfeld ([@audreyfeldroy](https://github.com/audreyfeldroy)) - Carol Willing ([@willingc](https://github.com/willingc)) - Christopher Clarke ([@chrisdev](https://github.com/chrisdev)) - Citlalli Murillo ([@citmusa](https://github.com/citmusa)) - Daniel Roy Greenfeld ([@pydanny](https://github.com/pydanny)) - Diane DeMers Chen ([@purplediane](https://github.com/purplediane)) - Elaine Wong ([@elainewong](https://github.com/elainewong)) - Elias Dorneles ([@eliasdorneles](https://github.com/eliasdorneles)) - Emily Cain ([@emcain](https://github.com/emcain)) - John Roa ([@jhonjairoroa87](https://github.com/jhonjairoroa87)) - Jonan Scheffler ([@1337807](https://github.com/1337807)) - Phoebe Bauer ([@phoebebauer](https://github.com/phoebebauer)) - Kartik Sundararajan ([@skarbot](https://github.com/skarbot)) - Katia Lira ([@katialira](https://github.com/katialira)) - Leonardo Jimenez ([@xpostudio4](https://github.com/xpostudio4)) - Lindsay Slazakowski ([@lslaz1](https://github.com/lslaz1)) - Meghan Heintz ([@dot2dotseurat](https://github.com/dot2dotseurat)) - Raphael Pierzina ([@hackebrot](https://github.com/hackebrot)) - Umair Ashraf ([@umrashrf](https://github.com/umrashrf)) - Valdir Stumm Junior ([@stummjr](https://github.com/stummjr)) - Vivian Guillen ([@viviangb](https://github.com/viviangb)) - Zaro ([@zaro0508](https://github.com/zaro0508)) cookiecutter-2.6.0/CODE_OF_CONDUCT.md000066400000000000000000000005061456543333500170320ustar00rootroot00000000000000# Code of Conduct Everyone interacting in the Cookiecutter project's codebases and documentation is expected to follow the [PyPA Code of Conduct](https://www.pypa.io/en/latest/code-of-conduct/). This includes, but is not limited to, issue trackers, chat rooms, mailing lists, and other virtual or in real life communication. cookiecutter-2.6.0/CONTRIBUTING.md000066400000000000000000000305721456543333500164720ustar00rootroot00000000000000# Contributing Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. - [Types of Contributions](#types-of-contributions) - [Contributor Setup](#setting-up-the-code-for-local-development) - [Contributor Guidelines](#contributor-guidelines) - [Contributor Testing](#testing-with-tox) - [Core Committer Guide](#core-committer-guide) ## Types of Contributions You can contribute in many ways: ### Report Bugs Report bugs at [https://github.com/cookiecutter/cookiecutter/issues](https://github.com/cookiecutter/cookiecutter/issues). If you are reporting a bug, please include: - Your operating system name and version. - Any details about your local setup that might be helpful in troubleshooting. - If you can, provide detailed steps to reproduce the bug. - If you don't have steps to reproduce the bug, just note your observations in as much detail as you can. Questions to start a discussion about the issue are welcome. ### Fix Bugs Look through the GitHub issues for bugs. Anything tagged with "bug" is open to whoever wants to implement it. ### Implement Features Look through the GitHub issues for features. Anything tagged with "enhancement" and "please-help" is open to whoever wants to implement it. Please do not combine multiple feature enhancements into a single pull request. Note: this project is very conservative, so new features that aren't tagged with "please-help" might not get into core. We're trying to keep the code base small, extensible, and streamlined. Whenever possible, it's best to try and implement feature ideas as separate projects outside of the core codebase. ### Write Documentation Cookiecutter could always use more documentation, whether as part of the official Cookiecutter docs, in docstrings, or even on the web in blog posts, articles, and such. If you want to review your changes on the documentation locally, you can do: ```bash pip install -r docs/requirements.txt make servedocs ``` This will compile the documentation, open it in your browser and start watching the files for changes, recompiling as you save. ### Submit Feedback The best way to send feedback is to file an issue at [https://github.com/cookiecutter/cookiecutter/issues](https://github.com/cookiecutter/cookiecutter/issues). If you are proposing a feature: - Explain in detail how it would work. - Keep the scope as narrow as possible, to make it easier to implement. - Remember that this is a volunteer-driven project, and that contributions are welcome :) ## Setting Up the Code for Local Development Here's how to set up `cookiecutter` for local development. 1. Fork the `cookiecutter` repo on GitHub. 2. Clone your fork locally: ```bash git clone git@github.com:your_name_here/cookiecutter.git ``` 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development: ```bash cd cookiecutter/ pip install -e . ``` 4. Create a branch for local development: ```bash git checkout -b name-of-your-bugfix-or-feature ``` Now you can make your changes locally. 5. When you're done making changes, check that your changes pass the tests and lint check: ```bash pip install tox tox ``` 6. Ensure that your feature or commit is fully covered by tests. Check report after regular `tox` run. You can also run coverage only report and get html report with statement by statement highlighting: ```bash make coverage ``` You report will be placed to `htmlcov` directory. Please do not include this directory to your commits. By default this directory in our `.gitignore` file. 7. Commit your changes and push your branch to GitHub: ```bash git add . git commit -m "Your detailed description of your changes." git push origin name-of-your-bugfix-or-feature ``` 8. Submit a pull request through the GitHub website. ## Contributor Guidelines ### Pull Request Guidelines Before you submit a pull request, check that it meets these guidelines: 1. The pull request should include tests. 2. The pull request should be contained: if it's too big consider splitting it into smaller pull requests. 3. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.md. 4. The pull request must pass all CI/CD jobs before being ready for review. 5. If one CI/CD job is failing for unrelated reasons you may want to create another PR to fix that first. ### Coding Standards - PEP8 - Functions over classes except in tests - Quotes via [http://stackoverflow.com/a/56190/5549](http://stackoverflow.com/a/56190/5549) - Use double quotes around strings that are used for interpolation or that are natural language messages - Use single quotes for small symbol-like strings (but break the rules if the strings contain quotes) - Use triple double quotes for docstrings and raw string literals for regular expressions even if they aren't needed. - Example: ```python LIGHT_MESSAGES = { 'English': "There are %(number_of_lights)s lights.", 'Pirate': "Arr! Thar be %(number_of_lights)s lights." } def lights_message(language, number_of_lights): """Return a language-appropriate string reporting the light count.""" return LIGHT_MESSAGES[language] % locals() def is_pirate(message): """Return True if the given message sounds piratical.""" return re.search(r"(?i)(arr|avast|yohoho)!", message) is not None ``` ## Testing with tox `tox` uses `pytest` under the hood, hence it supports the same syntax for selecting tests. For further information please consult the [pytest usage docs](http://pytest.org/en/latest/example/index.html). To run a particular test class with `tox`: ```bash tox -e py310 -- '-k TestFindHooks' ``` To run some tests with names matching a string expression: ```bash tox -e py310 -- '-k generate' ``` Will run all tests matching "generate", test_generate_files for example. To run just one method: ```bash tox -e py310 -- '-k "TestFindHooks and test_find_hook"' ``` To run all tests using various versions of Python, just run `tox`: ```bash tox ``` This configuration file setup the pytest-cov plugin and it is an additional dependency. It generate a coverage report after the tests. It is possible to test with specific versions of Python. To do this, the command is: ```bash tox -e py37,py38 ``` This will run `py.test` with the `python3.7` and `python3.8` interpreters. ## Core Committer Guide ### Vision and Scope Core committers, use this section to: - Guide your instinct and decisions as a core committer - Limit the codebase from growing infinitely #### Command-Line Accessible - Provides a command-line utility that creates projects from cookiecutters - Extremely easy to use without having to think too hard - Flexible for more complex use via optional arguments #### API Accessible - Entirely function-based and stateless (Class-free by intentional design) - Usable in pieces for developers of template generation tools #### Being Jinja2-specific - Sets a standard baseline for project template creators, facilitating reuse - Minimizes the learning curve for those who already use Flask or Django - Minimizes scope of Cookiecutter codebase #### Extensible Being extendable by people with different ideas for Jinja2-based project template tools. - Entirely function-based - Aim for statelessness - Lets anyone write more opinionated tools Freedom for Cookiecutter users to build and extend. - No officially-maintained cookiecutter templates, only ones by individuals - Commercial project-friendly licensing, allowing for private cookiecutters and private Cookiecutter-based tools #### Fast and Focused Cookiecutter is designed to do one thing, and do that one thing very well. - Cover the use cases that the core committers need, and as little as possible beyond that :) - Generates project templates from the command-line or API, nothing more - Minimize internal line of code (LOC) count - Ultra-fast project generation for high performance downstream tools #### Inclusive - Cross-platform and cross-version support are more important than features/functionality - Fixing Windows bugs even if it's a pain, to allow for use by more beginner coders #### Stable - Aim for 100% test coverage and covering corner cases - No pull requests will be accepted that drop test coverage on any platform, including Windows - Conservative decisions patterned after CPython's conservative decisions with stability in mind - Stable APIs that tool builders can rely on - New features require a +1 from 3 core committers #### VCS-Hosted Templates Cookiecutter project templates are intentionally hosted VCS repos as-is. - They are easily forkable - It's easy for users to browse forks and files - They are searchable via standard Github/Bitbucket/other search interface - Minimizes the need for packaging-related cruft files - Easy to create a public project template and host it for free - Easy to collaborate ### Process: Pull Requests If a pull request is untriaged: - Look at the roadmap - Set it for the milestone where it makes the most sense - Add it to the roadmap How to prioritize pull requests, from most to least important: - Fixes for broken tests. Broken means broken on any supported platform or Python version. - Extra tests to cover corner cases. - Minor edits to docs. - Bug fixes. - Major edits to docs. - Features. #### Pull Requests Review Guidelines - Think carefully about the long-term implications of the change. How will it affect existing projects that are dependent on this? If this is complicated, do we really want to maintain it forever? - Take the time to get things right, PRs almost always require additional improvements to meet the bar for quality. **Be very strict about quality.** - When you merge a pull request take care of closing/updating every related issue explaining how they were affected by those changes. Also, remember to add the author to `AUTHORS.md`. ### Process: Issues If an issue is a bug that needs an urgent fix, mark it for the next patch release. Then either fix it or mark as please-help. For other issues: encourage friendly discussion, moderate debate, offer your thoughts. New features require a +1 from 2 other core committers (besides yourself). ### Process: Roadmap The roadmap located [here](https://github.com/cookiecutter/cookiecutter/milestones?direction=desc&sort=due_date&state=open) Due dates are flexible. Core committers can change them as needed. Note that GitHub sort on them is buggy. How to number milestones: - Follow semantic versioning. Look at: [http://semver.org](http://semver.org) Milestone size: - If a milestone contains too much, move some to the next milestone. - Err on the side of more frequent patch releases. ### Process: Your own code changes All code changes, regardless of who does them, need to be reviewed and merged by someone else. This rule applies to all the core committers. Exceptions: - Minor corrections and fixes to pull requests submitted by others. - While making a formal release, the release manager can make necessary, appropriate changes. - Small documentation changes that reinforce existing subject matter. Most commonly being, but not limited to spelling and grammar corrections. ### Responsibilities - Ensure cross-platform compatibility for every change that's accepted. Windows, macOS and Linux. - Create issues for any major changes and enhancements that you wish to make. Discuss things transparently and get community feedback. - Don't add any classes to the codebase unless absolutely needed. Err on the side of using functions. - Keep feature versions as small as possible, preferably one new feature per version. - Be welcoming to newcomers and encourage diverse new contributors from all backgrounds. Look at [Code of Conduct](CODE_OF_CONDUCT.md). ### Becoming a Core Committer Contributors may be given core commit privileges. Preference will be given to those with: 1. Past contributions to Cookiecutter and other open-source projects. Contributions to Cookiecutter include both code (both accepted and pending) and friendly participation in the issue tracker. Quantity and quality are considered. 2. A coding style that the other core committers find simple, minimal, and clean. 3. Access to resources for cross-platform development and testing. 4. Time to devote to the project regularly. cookiecutter-2.6.0/HISTORY.md000066400000000000000000002274251456543333500157310ustar00rootroot00000000000000# History History is important, but our current roadmap can be found [here](https://github.com/cookiecutter/cookiecutter/projects) ## 2.6.0 (2024-02-21) ### Minor Changes * Support Python 3.12 (#1989) @ericof * Modifying Jinja2 start and end variable strings (#1997) @sacha-c ### CI/CD and QA changes * Add isort as a pre-commit hook (#1988) @kurtmckee * Bump actions/setup-python from 4 to 5 (#2000) @dependabot * Bump actions/upload-artifact from 3 to 4 (#1999) @dependabot * Quick resolution of #2003 (#2004) @jensens * Support Python 3.12 (#1989) @ericof * [pre-commit.ci] pre-commit autoupdate (#1996) @pre-commit-ci * Quick resolution of #2003 (#2004) @jensens ### Documentation updates * Support Python 3.12 (#1989) @ericof ### Bugfixes * Fix regression #2009: Adding value to nested dicts broken (#2010) @jensens * Fixed errors caused by invalid config files. (#1995) @alanverresen ### This release is made by wonderful contributors: @alanverresen, @dependabot, @dependabot[bot], @ericof, @jensens, @kurtmckee, @pre-commit-ci, @pre-commit-ci[bot] and @sacha-c ## 2.5.0 (2023-11-21) ### Minor Changes * Default values can be passed as a dict (#1924) @matveyvarg * Implement new style for nested templates config (#1981) @ericof ### CI/CD and QA changes * Bump actions/checkout from 3 to 4 (#1953) @dependabot * [pre-commit.ci] pre-commit autoupdate (#1977) @pre-commit-ci * [pre-commit.ci] pre-commit autoupdate (#1957) @pre-commit-ci ### Documentation updates * Add argument run to pipx command in README.md (#1964) @staeff * Fix tutorial2 generated HTML (#1971) @aantoin * Update README.md (#1967) @HarshRanaOC * Update README.md to fix broken link (#1952) @david-abn * Update README.md to include installation instructions (#1949) @david-abn * Update cookiecutter-plone-starter link in readme (#1965) @zahidkizmaz ### Bugfixes * Fix FileExistsError when using a relative template path (#1968) @pkrueger-cariad * Fix recursive context overwrites (#1961) @padraic-padraic ### This release is made by wonderful contributors: @HarshRanaOC, @aantoin, @david-abn, @dependabot, @dependabot[bot], @ericof, @matveyvarg, @padraic-padraic, @pkrueger-cariad, @pre-commit-ci, @pre-commit-ci[bot], @staeff and @zahidkizmaz ## 2.4.0 (2023-09-29) ### Minor Changes * Gracefully handle files with mixed lined endings (#1942) @EricHripko * Implement a pre_prompt hook that will run before prompts (#1950) @ericof ### Documentation updates * Implement a pre_prompt hook that will run before prompts (#1950) @ericof * update main docstrings to include overwrite_if_exists and skip_if_file_exists (#1947) @david-abn ### This release is made by wonderful contributors: @EricHripko, @david-abn and @ericof ## 2.3.1 (2023-09-21) ### Minor Changes * add checkout details to the context (fixes #1759) (#1923) @JonZeolla ### CI/CD and QA changes * Update the black pre-commit hook URL and version (#1934) @kurtmckee * Use UTF-8 for file reading/writing (#1937) @rmartin16 ### Documentation updates * Add missing "parent dir" symbol in tutorial 2 (#1932) @tvoirand * Remove colons from exemplary prompt messages (#1912) @paduszyk * docs: add install instruction for Void Linux (#1917) @tranzystorek-io ### Bugfixes * Fix nested templates in Git repository (#1922) @BTatlock * Fix prompt counter. (#1940) @ericof * Fix variables with null default not being required (#1919) (#1920) @limtis0 ### This release is made by wonderful contributors: @BTatlock, @JonZeolla, @ericof, @kurtmckee, @limtis0, @paduszyk, @rmartin16, @tranzystorek-io and @tvoirand ## 2.3.0 (2023-08-03) ### Minor Changes * Improve style of prompts using `rich` (#1901) @vemonet ### CI/CD and QA changes * Bump paambaati/codeclimate-action from 4.0.0 to 5.0.0 (#1908) @dependabot * [pre-commit.ci] pre-commit autoupdate (#1907) @pre-commit-ci ### Bugfixes * Fix replay (#1904) @vemonet * Support multichoice overwrite (#1903) @Meepit ### This release is made by wonderful contributors: @Meepit, @dependabot, @dependabot[bot], @ericof, @pre-commit-ci, @pre-commit-ci[bot] and @vemonet ## 2.2.3 (2023-07-11) ### Changes ### Minor Changes * Add support for adding human-readable labels for choices when defining multiple choices questions (#1898) @vemonet * Prompt with replay file (#1758) @w1ndblow ### CI/CD and QA changes * Set cookiecutter/VERSION.txt as source of truth for version number (#1896) @ericof * [pre-commit.ci] pre-commit autoupdate (#1897) @pre-commit-ci ### Bugfixes * Fix issue where the prompts dict was not passed for yes_no questions (#1895) @vemonet * Set cookiecutter/VERSION.txt as source of truth for version number (#1896) @ericof ### This release is made by wonderful contributors: @ericof, @pre-commit-ci, @pre-commit-ci[bot], @vemonet and @w1ndblow ## 2.2.2 (2023-07-10) ### CI/CD and QA changes * Improve gitignore (#1889) @audreyfeldroy * Add warning for jinja2_time (#1890) @henryiii ### This release is made by wonderful contributors: @audreyfeldroy, @ericof and @henryiii ## 2.2.0 (2023-07-06) ### Changes * Added timeout on request.get() for ensuring that if a recipient serve… (#1772) @openrefactory * Fixing Carriage Return Line Feed (CRLF) order in docs #1792 (#1793) @Lahiry * Reduce I/O (#1877) @kurtmckee * Remove a pre-commit hook special case (#1875) @kurtmckee * Remove universal bdist_wheel option; use "python -m build" (#1739) @mwtoews * Remove unused import from post-generate hook script example (#1795) @KAZYPinkSaurus * Standardize newlines for all platforms (#1870) @kurtmckee * feat: Add resolved template repository path as _repo_dir to the context (#1771) @tmeckel ### Minor Changes * Added support for providing human-readable prompts to the different variables (#1881) @vemonet * Added: Boolean variable support in JSON (#1626) @liortct * Added: CLI option to keep project files on failure. (#1669) @MaciejPatro * Added: Support partially overwrite keys in nested dict (#1692) @cksac * Added: Templates inheritance (#1485) @simobasso * Code quality: Tests upgrade: Use pathlib for files read/write (#1718) @insspb * Inline jinja2-time extension code (#1779) @tranzystorek-io * Support Python 3.11 (#1850) @kurtmckee * Support nested config files (#1770) @dariocurr * preserves original options in `_cookiecutter` (#1874) @kjaymiller ### CI/CD and QA changes * Add a Dependabot config to autoupdate GitHub workflow actions (#1851) @kurtmckee * Added: Readthedocs build config (#1707) @insspb * Bump actions/setup-python from 3 to 4 (#1854) @dependabot * Bump paambaati/codeclimate-action from 3.0.0 to 4.0.0 (#1853) @dependabot * CI/CD: Tox -> Nox: Added nox configuration (#1706) @insspb * CI/CD: Tox -> Nox: Github actions definition minimized + Sync nox and github actions (#1714) @insspb * CI/CD: Tox -> Nox: Makefile update: Removed watchmedo and sed dependency, tox replaced with nox (#1713) @insspb * CI/CD: Updated .pre-commit-config.yaml to use latest hooks versions (#1712) @insspb * Code quality: Core files: Added exception reason reraise when exception class changed (PEP 3134) (#1719) @insspb * Code quality: Tests upgrade: Use pathlib for files read/write (#1718) @insspb * Code quality: core files: Format replaced with f-strings (#1716) @insspb * Code quality: find.py refactored and type annotated (#1721) @insspb * Code quality: tests files: Simplify statements fixes (#1717) @insspb * Code quality: utils.make_sure_path_exists refactored and type annotated (#1722) @insspb * Fixed: recommonmark replaced with myst, as recommonmark is deprecated (#1709) @insspb * Pretty-format JSON files (#1864) @kurtmckee * Rename `master` to `main` so CI runs correctly on merge (#1852) @kurtmckee * Standardize EOF newlines (#1876) @kurtmckee * Update `.gitignore` and cite where it was copied from (#1879) @kurtmckee * Update base docs, remove tox (#1858) @ericof * Update pre-commit hook versions (#1849) @kurtmckee * Updated: Release drafter configuration (#1704) @insspb * Use tox (#1866) @kurtmckee * Verify an expected warning is raised (#1869) @kurtmckee * fixed failing lint ci action by updating repo of flake8 (#1838) @Tamronimus ### Documentation updates * Add jinja env docs (#1872) @pamelafox * Documentation extension: Create a Cookiecutter From Scratch tutorial (#1592) @miro-jelaska * Easy PR! Fix typos and add minor doc updates (#1741) @Alex0Blackwell * Expand cli documentation relating to the no-input flag (#1543) (#1587) @jeremyswerdlow * Fix @audreyr to @audreyfeldroy github account rename (#1604) @ri0t * Fixed broken links to jinja docs (#1691) @insspb * Fixed minor typos in docs (#1753) @segunb * Fixed: Python code block in the replay documentation (#1715) @juhannc * Fixed: recommonmark replaced with myst, as recommonmark is deprecated (#1709) @insspb * Improve Docs Readability (#1690) @ryanrussell * Update base docs, remove tox (#1858) @ericof * Updated: Boolean Variables documentation and docstrings (#1705) @italomaia * docs: fix simple typo, shat -> that (#1749) @timgates42 * fixing badge display problem (#1798) @Paulokim1 ### Bugfixes * Fixed the override not working with copy only dir #1650 (#1651) @zhongdai * Fixed: Removed mention of packages versions, to exclude dependabot warnings alerts (#1711) @insspb * cleanup files if panics during hooks - bugfix (#1760) @liortct ### This release is made by wonderful contributors: @Alex0Blackwell, @KAZYPinkSaurus, @Lahiry, @MaciejPatro, @Paulokim1, @Tamronimus, @cksac, @cookies-xor-cream, @dariocurr, @dependabot, @dependabot[bot], @ericof, @insspb, @italomaia, @jeremyswerdlow, @juhannc, @kjaymiller, @kurtmckee, @liortct, @miro-jelaska, @mwtoews, @openrefactory, @pamelafox, @ri0t, @ryanrussell, @segunb, @simobasso, @timgates42, @tmeckel, @tranzystorek-io, @vemonet and @zhongdai ## 2.1.1 (2022-06-01) ### Documentation updates * Fix local extensions documentation (#1686) @alkatar21 ### Bugfixes * Sanitize Mercurial branch information before checkout. (#1689) @ericof ### This release is made by wonderfull contributors: @alkatar21, @ericof and @jensens ## 2.1.0 (2022-05-30) ### Changes * Move contributors and backers to credits section (#1599) @doobrie * test_generate_file_verbose_template_syntax_error fixed (#1671) @MaciejPatro * Removed changes related to setuptools_scm (#1629) @ozer550 * Feature/local extensions (#1240) @mwesterhof ### CI/CD and QA changes * Check manifest: pre-commit, fixes, cleaning (#1683) @jensens * Follow PyPA guide to release package using GitHub Actions. (#1682) @ericof ### Documentation updates * Fix typo in dict_variables.rst (#1680) @ericof * Documentation overhaul (#1677) @jensens * Fixed incorrect link on docs. (#1649) @luzfcb ### Bugfixes * Restore accidentally deleted support for click 8.x (#1643) @jaklan ### This release was made possible by our wonderful contributors: @doobrie, @jensens, @ericof, @luzfcb ## 2.0.2 (2021-12-27) *Remark: This release never made it to official PyPI* * Fix Python version number in cookiecutter --version and test on Python 3.10 (#1621) @ozer550 * Removed changes related to setuptools_scm (#1629) @audreyfeldroy @ozer550 ## 2.0.1 (2021-12-11) *Remark: This release never made it to official PyPI* ### Breaking Changes * Release preparation for 2.0.1rc1 (#1608) @audreyfeldroy * Replace poyo with pyyaml. (#1489) @dHannasch * Added: Path templates will be rendered when copy_without_render used (#839) @noirbizarre * Added: End of line detection and configuration. (#1407) @insspb * Remove support for python2.7 (#1386) @ssbarnea ### Minor Changes * Adopt setuptools-scm packaging (#1577) @ssbarnea * Log the error message when git clone fails, not just the return code (#1505) @logworthy * allow jinja 3.0.0 (#1548) @wouterdb * Added uuid extension to be able to generate uuids (#1493) @jonaswre * Alert user if choice is invalid (#1496) @dHannasch * Replace poyo with pyyaml. (#1489) @dHannasch * update AUTHOR lead (#1532) @HosamAlmoghraby * Add Python 3.9 (#1478) @gliptak * Added: --list-installed cli option, listing already downloaded cookiecutter packages (#1096) @chrisbrake * Added: Jinja2 Environment extension on files generation stage (#1419) @insspb * Added: --replay-file cli option, for replay file distributing (#906) @Cadair * Added: _output_dir to cookiecutter context (#1034) @Casyfill * Added: CLI option to ignore hooks (#992) @rgreinho * Changed: Generated projects can use multiple type hooks at same time. (sh + py) (#974) @milonimrod * Added: Path templates will be rendered when copy_without_render used (#839) @noirbizarre * Added: End of line detection and configuration. (#1407) @insspb * Making code python 3 only: Remove python2 u' sign, fix some strings (#1402) @insspb * py3: remove futures, six and encoding (#1401) @insspb * Render variables starting with an underscore. (#1339) @smoothml * Tests refactoring: test_utils write issues fixed #1405 (#1406) @insspb ### CI/CD and QA changes * enable branch coverage (#1542) @simobasso * Make release-drafter diff only between master releases (#1568) @SharpEdgeMarshall * ensure filesystem isolation during tests execution (#1564) @simobasso * add safety ci step (#1560) @simobasso * pre-commit: add bandit hook (#1559) @simobasso * Replace tmpdir in favour of tmp_path (#1545) @SharpEdgeMarshall * Fix linting in CI (#1546) @SharpEdgeMarshall * Coverage 100% (#1526) @SharpEdgeMarshall * Run coverage with matrix (#1521) @SharpEdgeMarshall * Lint rst files (#1443) @ssbarnea * Python3: Changed io.open to build-in open (PEP3116) (#1408) @insspb * Making code python 3 only: Remove python2 u' sign, fix some strings (#1402) @insspb * py3: remove futures, six and encoding (#1401) @insspb * Removed: Bumpversion, setup.py arguments. (#1404) @insspb * Tests refactoring: test_utils write issues fixed #1405 (#1406) @insspb * Added: Automatic PyPI deploy on tag creation (#1400) @insspb * Changed: Restored coverage reporter (#1399) @insspb ### Documentation updates * Fix pull requests checklist reference (#1537) @glumia * Fix author name (#1544) @HosamAlmoghraby * Add missing contributors (#1535) @glumia * Update CONTRIBUTING.md (#1529) @glumia * Update LICENSE (#1519) @simobasso * docs: rewrite the conditional files / directories example description. (#1437) @lyz-code * Fix incorrect years in release history (#1473) @graue70 * Add slugify in the default extensions list (#1470) @oncleben31 * Renamed cookiecutter.package to API (#1442) @grrlic * Fixed wording detail (#1427) @steltenpower * Changed: CLI Commands documentation engine (#1418) @insspb * Added: Example for conditional files / directories in hooks (#1397) @xyb * Changed: README.md PyPI URLs changed to the modern PyPI last version (#1391) @brettcannon * Fixed: Comma in README.md (#1390) @Cy-dev-tex * Fixed: Replaced no longer maintained pipsi by pipx (#1395) @ndclt ### Bugfixes * Add support for click 8.x (#1569) @cjolowicz * Force click<8.0.0 (#1562) @SharpEdgeMarshall * Remove direct dependency on markupsafe (#1549) @ssbarnea * fixes prompting private rendered dicts (#1504) @juhuebner * User's JSON parse error causes ugly Python exception #809 (#1468) @noone234 * config: set default on missing default_context key (#1516) @simobasso * Fixed: Values encoding on Windows (#1414) @agateau * Fixed: Fail with gitolite repositories (#1144) @javiersanp * MANIFEST: Fix file name extensions (#1387) @sebix ### Deprecations * Removed: Bumpversion, setup.py arguments. (#1404) @insspb * Removed support for Python 3.6 and PyPy (#1608) @audreyfeldroy ### This release was made possible by our wonderful contributors: @Cadair, @Casyfill, @Cy-dev-tex, @HosamAlmoghraby, @SharpEdgeMarshall, @agateau, @audreyfeldroy, @brettcannon, @chrisbrake, @cjolowicz, @dHannasch, @gliptak, @glumia, @graue70, @grrlic, @insspb, @javiersanp, @jonaswre, @jsoref, @Jthevos, @juhuebner, @logworthy, @lyz-code, @milonimrod, @ndclt, @noirbizarre, @noone234, @oncleben31, @ozer550, @rgreinho, @sebix, @Sahil-101, @simobasso, @smoothml, @ssbarnea, @steltenpower, @wouterdb, @xyb, Christopher Wolfe and Hosam Almoghraby ( RIAG Digital ) ## 1.7.2 (2020-04-21) * Fixed: Jinja2&Six version limits causing build errors with ansible project [@insspb](https://github.com/insspb) (#1385) ## 1.7.1 (2020-04-21) This release was focused on internal code and CI/CD changes. During this release all code was verified to match pep8, pep257 and other code-styling guides. Project CI/CD was significantly changed, Windows platform checks based on Appveyor engine was replaced by GitHub actions tests. Appveyor was removed. Also our CI/CD was extended with Mac builds, to verify project builds on Apple devices. Important Changes: * Added: Added debug messages for get_user_config [@ssbarnea](https://github.com/ssbarnea) (#1357) * Multiple templates per one repository feature added. [@RomHartmann](https://github.com/RomHartmann) (#1224, #1063) * Update replay.py json.dump indent for easy viewing [@nicain](https://github.com/nicain) (#1293) * 'future' library replaced with 'six' as a more lightweight python porting library [@asottile](https://github.com/asottile) (#941) * Added extension: Slugify template filter [@ppanero](https://github.com/ppanero) (#1336) * Added command line option: `--skip-if-file-exists`, allow to skip the existing files when doing `overwrite_if_exists`. [@chhsiao1981](https://github.com/chhsiao1981) (#1076) * Some packages versions limited to be compatible with python2.7 and python 3.5 [@insspb](https://github.com/insspb) (#1349) Internal CI/CD and tests changes: * Coverage comment in future merge requests disabled [@ssbarnea](https://github.com/ssbarnea) (#1279) * Fixed Python 3.8 travis tests and setup.py message [@insspb](https://github.com/insspb) (#1295, #1297) * Travis builds extended with Windows setup for all supported python versions [@insspb](https://github.com/insspb) (#1300, #1301) * Update .travis.yml to be compatible with latest travis cfg specs [@luzfcb](https://github.com/luzfcb) (#1346) * Added new test to improve tests coverage [@amey589](https://github.com/amey589) (#1023) * Added missed coverage lines highlight to pytest-coverage report [@insspb](https://github.com/insspb) (#1352) * pytest-catchlog package removed from test_requirements, as now it is included in pytest [@insspb](https://github.com/insspb) (#1347) * Fixed `cov-report` tox invocation environment [@insspb](https://github.com/insspb) (#1350) * Added: Release drafter support and configuration to exclude changelog update work and focus on development [@ssbarnea](https://github.com/ssbarnea) [@insspb](https://github.com/insspb) (#1356, #1362) * Added: CI/CD steps for Github actions to speedup CI/CD [@insspb](https://github.com/insspb) (#1360) * Removed: Appveyor CI/CD completely removed [@insspb](https://github.com/insspb) [@ssbarnea](https://github.com/ssbarnea) [@insspb](https://github.com/insspb) (#1363, #1367) Code style and docs changes: * Added black formatting verification on lint stage + project files reformatting [@ssbarnea](https://github.com/ssbarnea) [@insspb](https://github.com/insspb) (#1368) * Added pep257 docstring for tests/* files [@insspb](https://github.com/insspb) (#1369, #1370, #1371, #1372, #1373, #1374, #1375, #1376, #1377, #1378, #1380, #1381) * Added pep257 docstring for tests/conftests.py [@kishan](https://github.com/kishan3) (#1272, #1263) * Added pep257 docstring for tests/replay/conftest.py [@kishan](https://github.com/kishan3) (#1270, #1268) * Added pep257 docstring for docs/__init__.py [@kishan](https://github.com/kishan3) (#1273, #1265) * Added missing docstring headers to all files [@croesnick](https://github.com/croesnick) (#1269, #1283) * Gitter links replaced by Slack in README [@browniebroke](https://github.com/browniebroke) (#1282) * flake8-docstrings tests added to CI/CD [@ssbarnea](https://github.com/ssbarnea) (#1284) * Activated pydocstyle rule: D401 - First line should be in imperative mood [@ssbarnea](https://github.com/ssbarnea) (#1285) * Activated pydocstyle rule: D200 - One-line docstring should fit on one line with quotes [@ssbarnea](https://github.com/ssbarnea) (#1288) * Activated pydocstyle rule: D202 - No blank lines allowed after function docstring [@ssbarnea](https://github.com/ssbarnea) (#1288) * Activated pydocstyle rule: D205 - 1 blank line required between summary line and description [@ssbarnea](https://github.com/ssbarnea) (#1286, #1287) * Activated pydocstyle rule: ABS101 [@ssbarnea](https://github.com/ssbarnea) (#1288) * Replaced click documentation links to point to version 7 [@igorbasko01](https://github.com/igorbasko01) (#1303) * Updated submodule link to latest version with documentation links fix [@DanBoothDev](https://github.com/DanBoothDev) (#1388) * Fixed links in main README file. [@insspb](https://github.com/insspb) (#1342) * Fix indentation of .cookiecutterrc in README.md [@mhsekhavat](https://github.com/mhsekhavat) (#1322) * Changed format of loggers invocation [@insspb](https://github.com/insspb) (#1307) ## 1.7.0 (2019-12-22) Old friend Important Changes: * Drop support for EOL Python 3.4, thanks to [@jamescurtin](https://github.com/jamescurtin) and [@insspb](https://github.com/insspb) (#1024) * Drop support for EOL Python 3.3, thanks to [@hugovk](https://github.com/hugovk) (#1024) * Increase the minimum click version to 7.0, thanks to [@rly](https://github.com/rly) and [@luzfcb](https://github.com/luzfcb) (#1168) Other Changes: * PEP257 fixing docstrings in exceptions.py. Thanks to [@MinchinWeb](https://github.com/MinchinWeb) (#1237) * PEP257 fixing docstrings in replay.py. Thanks to [@kishan](https://github.com/kishan3) (#1234) * PEP257 fixing docstrings in test_unzip.py. Thanks to [@tonytheleg](https://github.com/tonytheleg) and [@insspb](https://github.com/insspb) (#1236, #1262) * Fixed tests sequence for appveyor, to exclude file not found bug. Thanks to [@insspb](https://github.com/insspb) (#1257) * Updates REAMDE.md with svg badge for appveyor. Thanks to [@sobolevn](https://github.com/sobolevn) (#1254) * Add missing {% endif %} to Choice Variables example. Thanks to [@mattstibbs](https://github.com/mattstibbs) (#1249) * Core documentation converted to Markdown format thanks to [@wagnernegrao](https://github.com/wagnernegrao), [@insspb](https://github.com/insspb) (#1216) * Tests update: use sys.executable when invoking python in python 3 only environment thanks to [@vincentbernat](https://github.com/vincentbernat) (#1221) * Prevent `click` API v7.0 from showing choices when already shown, thanks to [@rly](https://github.com/rly) and [@luzfcb](https://github.com/luzfcb) (#1168) * Test the codebase with python3.8 beta on tox and travis-ci (#1206), thanks to [@mihrab34](https://github.com/mihrab34) * Add a [CODE\_OF\_CONDUCT.md](https://github.com/audreyfeldroy/cookiecutter/blob/master/CODE_OF_CONDUCT.md) file to the project, thanks to [@andreagrandi](https://github.com/andreagrandi) (#1009) * Update docstrings in `cookiecutter/main.py`, `cookiecutter/__init__.py`, and `cookiecutter/log.py` to follow the PEP 257 style guide, thanks to [@meahow](https://github.com/meahow) (#998, #999, #1000) * Update docstrings in `cookiecutter/utils.py` to follow the PEP 257 style guide, thanks to [@dornheimer](https://github.com/dornheimer)(#1026) * Fix grammar in *Choice Variables* documentation, thanks to [@jubrilissa](https://github.com/jubrilissa) (#1011) * Update installation docs with links to the Windows Subsystem and GNU utilities, thanks to [@Nythiennzo](https://github.com/Nythiennzo) for the PR and [@BruceEckel](https://github.com/BruceEckel) for the review (#1016) * Upgrade flake8 to version 3.5.0, thanks to [@cclauss](https://github.com/cclauss) (#1038) * Update tutorial with explanation for how cookiecutter finds the template file, thanks to [@accraze](https://github.com/accraze)(#1025) * Update CI config files to use `TOXENV` environment variable, thanks to [@asottile](https://github.com/asottile) (#1019) * Improve user documentation for writing hooks, thanks to [@jonathansick](https://github.com/jonathansick) (#1057) * Make sure to preserve the order of items in the generated cookiecutter context, thanks to [@hackebrot](https://github.com/hackebrot) (#1074) * Fixed DeprecationWarning for a regular expression on python 3.6, thanks to [@reinout](https://github.com/reinout) (#1124) * Document use of cookiecutter-template topic on GitHub, thanks to [@ssbarnea](https://github.com/ssbarnea) (#1189) * Update README badge links, thanks to [@luzfcb](https://github.com/luzfcb) (#1207) * Update prompt.py to match pep257 guidelines, thanks to [@jairideout](https://github.com/jairideout) (#1105) * Update link to Jinja2 extensions documentation, thanks to [@dacog](https://github.com/dacog) (#1193) * Require pip 9.0.0 or newer for tox environments, thanks to [@hackebrot](https://github.com/hackebrot) (#1215) * Use io.open contextmanager when reading hook files, thanks to [@jcb91](https://github.com/jcb91) (#1147) * Add more cookiecutter templates to the mix: * [cookiecutter-python-cli](https://github.com/xuanluong/cookiecutter-python-cli) by [@xuanluong](https://github.com/xuanluong) (#1003) * [cookiecutter-docker-science](https://github.com/docker-science/cookiecutter-docker-science) by [@takahi-i](https://github.com/takahi-i) (#1040) * [cookiecutter-flask-skeleton](https://github.com/realpython/cookiecutter-flask-skeleton) by [@mjhea0](https://github.com/mjhea0) (#1052) * [cookiecutter-awesome](https://github.com/Pawamoy/cookiecutter-awesome) by [@Pawamoy](https://github.com/Pawamoy) (#1051) * [cookiecutter-flask-ask](https://github.com/chrisvoncsefalvay/cookiecutter-flask-ask) by [@machinekoder](https://github.com/machinekoder) (#1056) * [cookiecutter-data-driven-journalism](https://github.com/jastark/cookiecutter-data-driven-journalism) by [@JAStark](https://github.com/JAStark) (#1020) * [cookiecutter-tox-plugin](https://github.com/tox-dev/cookiecutter-tox-plugin) by [@obestwalter](https://github.com/obestwalter) (#1103) * [cookiecutter-django-dokku](https://github.com/mashrikt/cookiecutter-django-dokku) by [@mashrikt](https://github.com/mashrikt) (#1093) ## 1.6.0 (2017-10-15) Tim Tam New Features: * Include template path or template URL in cookiecutter context under `_template`, thanks to [@aroig](https://github.com/aroig) (#774) * Add a URL abbreviation for GitLab template projects, thanks to [@hackebrot](https://github.com/hackebrot) (#963) * Add option to use templates from Zip files or Zip URLs, thanks to [@freakboy3742](https://github.com/freakboy3742) (#961) Bug Fixes: * Fix an issue with missing default template abbreviations for when a user defined custom abbreviations, thanks to [@noirbizarre](https://github.com/noirbizarre) for the issue report and [@hackebrot](https://github.com/hackebrot) for the fix (#966, #967) * Preserve existing output directory on project generation failure, thanks to [@ionelmc](https://github.com/ionelmc) for the report and [@michaeljoseph](https://github.com/michaeljoseph) for the fix (#629, #964) * Fix Python 3.x error handling for `git` operation failures, thanks to [@jmcarp](https://github.com/jmcarp) (#905) Other Changes: * Fix broken link to *Copy without Render* docs, thanks to [@coreysnyder04](https://github.com/coreysnyder04) (#912) * Improve debug log message for when a hook is not found, thanks to [@raphigaziano](https://github.com/raphigaziano/) (#160) * Fix module summary and `expand_abbreviations()` doc string as per pep257, thanks to [@terryjbates](https://github.com/terryjbates) (#772) * Update doc strings in `cookiecutter/cli.py` and `cookiecutter/config.py` according to pep257, thanks to [@terryjbates](https://github.com/terryjbates) (#922, #931) * Update doc string for `is_copy_only_path()` according to pep257, thanks to [@mathagician](https://github.com/mathagician) and [@terryjbates](https://github.com/terryjbates) (#935, #949) * Update doc strings in `cookiecutter/extensions.py` according to pep257, thanks to [@meahow](https://github.com/meahow) (#996) * Fix miscellaneous issues with building docs, thanks to [@stevepiercy](https://github.com/stevepiercy) (#889) * Re-implement Makefile and update several make rules, thanks to [@hackebrot](https://github.com/hackebrot) (#930) * Fix broken link to pytest docs, thanks to [@eyalev](https://github.com/eyalev) for the issue report and [@devstrat](https://github.com/devstrat) for the fix (#939, #940) * Add `test_requirements.txt` file for easier testing outside of tox, thanks to [@ramnes](https://github.com/ramnes) (#945) * Improve wording in *copy without render* docs, thanks to [@eyalev](https://github.com/eyalev) (#938) * Fix a number of typos, thanks to [@delirious-lettuce](https://github.com/delirious-lettuce) (#968) * Improved *extra context* docs by noting that extra context keys must be present in the template\'s `cookiecutter.json`, thanks to [@karantan](https://github.com/karantan) for the report and fix (#863, #864) * Added more cookiecutter templates to the mix: * [cookiecutter-kata-cpputest](https://github.com/13coders/cookiecutter-kata-cpputest) by [@13coders](https://github.com/13coders) (#901) * [cookiecutter-kata-gtest](https://github.com/13coders/cookiecutter-kata-gtest) by [@13coders](https://github.com/13coders) (#901) * [cookiecutter-pyramid-talk-python-starter](https://github.com/mikeckennedy/cookiecutter-pyramid-talk-python-starter) by [@mikeckennedy](https://github.com/mikeckennedy) (#915) * [cookiecutter-android](https://github.com/alexfu/cookiecutter-android) by [@alexfu](https://github.com/alexfu) (#890) * [cookiecutter-lux-python](https://github.com/alexkey/cookiecutter-lux-python) by [@alexkey](https://github.com/alexkey) (#895) * [cookiecutter-git](https://github.com/webevllc/cookiecutter-git) by [@tuxredux](https://github.com/tuxredux) (#921) * [cookiecutter-ansible-role-ci](https://github.com/ferrarimarco/cookiecutter-ansible-role) by [@ferrarimarco](https://github.com/ferrarimarco) (#903) * [cookiecutter\_dotfile](https://github.com/bdcaf/cookiecutter_dotfile) by [@bdcaf](https://github.com/bdcaf) (#925) * [painless-continuous-delivery](https://github.com/painless-software/painless-continuous-delivery) by [@painless-software](https://github.com/painless-software) (#927) * [cookiecutter-molecule](https://github.com/retr0h/cookiecutter-molecule) by [@retr0h](https://github.com/retr0h) (#954) * [sublime-snippet-package-template](https://github.com/agenoria/sublime-snippet-package-template) by [@agenoria](https://github.com/agenoria) (#956) * [cookiecutter-conda-python](https://github.com/conda/cookiecutter-conda-python) by [@conda](https://github.com/conda) (#969) * [cookiecutter-flask-minimal](https://github.com/candidtim/cookiecutter-flask-minimal) by [@candidtim](https://github.com/candidtim) (#977) * [cookiecutter-pypackage-rust-cross-platform-publish](https://github.com/mckaymatt/cookiecutter-pypackage-rust-cross-platform-publish) by [@mckaymatt](https://github.com/mckaymatt) (#957) * [cookie-cookie](https://github.com/tuxredux/cookie-cookie) by [@tuxredux](https://github.com/tuxredux) (#951) * [cookiecutter-telegram-bot](https://github.com/Ars2014/cookiecutter-telegram-bot) by [@Ars2014](https://github.com/Ars2014) (#984) * [python-project-template](https://github.com/Kwpolska/python-project-template) by [@Kwpolska](https://github.com/Kwpolska) (#986) * [wemake-django-template](https://github.com/wemake-services/wemake-django-template) by [@wemake-services](https://github.com/wemake-services) (#990) * [cookiecutter-raml](https://github.com/genzj/cookiecutter-raml) by [@genzj](https://github.com/genzj) (#994) * [cookiecutter-anyblok-project](https://github.com/AnyBlok/cookiecutter-anyblok-project) by [@AnyBlok](https://github.com/AnyBlok) (#988) * [cookiecutter-devenv](https://bitbucket.org/greenguavalabs/cookiecutter-devenv.git) by [@greenguavalabs](https://bitbucket.org/greenguavalabs) (#991) ## 1.5.1 (2017-02-04) Alfajor New Features: * Major update to installation documentation, thanks to [@stevepiercy](https://github.com/stevepiercy) (#880) Bug Fixes: * Resolve an issue around default values for dict variables, thanks to [@e-kolpakov](https://github.com/e-kolpakov) for raising the issue and [@hackebrot](https://github.com/hackebrot) for the PR (#882, #884) Other Changes: * Contributor documentation reST fixes, thanks to [@stevepiercy](https://github.com/stevepiercy) (#878) * Added more cookiecutter templates to the mix: * [widget-cookiecutter](https://github.com/jupyter/widget-cookiecutter) by [@willingc](https://github.com/willingc) (#781) * [cookiecutter-django-foundation](https://github.com/Parbhat/cookiecutter-django-foundation) by [@Parbhat](https://github.com/Parbhat) (#804) * [cookiecutter-tornado](https://github.com/hkage/cookiecutter-tornado) by [@hkage](https://github.com/hkage) (#807) * [cookiecutter-django-ansible](https://github.com/HackSoftware/cookiecutter-django-ansible) by [@Ivaylo-Bachvarov](https://github.com/Ivaylo-Bachvarov)(#816) * [CICADA](https://github.com/TAMU-CPT/CICADA) by [@elenimijalis](https://github.com/elenimijalis) (#840) * [cookiecutter-tf-module](https://github.com/DualSpark/cookiecutter-tf-module) by [@VDuda](https://github.com/VDuda) (#843) * [cookiecutter-pyqt4](https://github.com/aeroaks/cookiecutter-pyqt4) by [@aeroaks](https://github.com/aeroaks) (#847) * [cookiecutter-golang](https://github.com/lacion/cookiecutter-golang) by [@mjhea0](https://github.com/mjhea0) and [@lacion](https://github.com/lacion) (#872, #873) * [cookiecutter-elm](https://github.com/m-x-k/cookiecutter-elm.git), [cookiecutter-java](https://github.com/m-x-k/cookiecutter-java.git) and [cookiecutter-spring-boot](https://github.com/m-x-k/cookiecutter-spring-boot.git) by [@m-x-k](https://github.com/m-x-k) (#879) ## 1.5.0 (2016-12-18) Alfajor The primary goal of this release was to add command-line support for passing extra context, address minor bugs and make a number of improvements. New Features: * Inject extra context with command-line arguments, thanks to [@msabramo](https://github.com/msabramo) and [@michaeljoseph](https://github.com/michaeljoseph) (#666). * Updated conda installation instructions to work with the new conda-forge distribution of Cookiecutter, thanks to [@pydanny](https://github.com/pydanny) and especially [@bollwyvl](https://github.com/bollwyvl) (#232, #705). * Refactor code responsible for interaction with version control systems and raise better error messages, thanks to [@michaeljoseph](https://github.com/michaeljoseph) (#778). * Add support for executing cookiecutter using `python -m cookiecutter` or from a checkout/zip file, thanks to [@brettcannon](https://github.com/brettcannon) (#788). * New CLI option `--debug-file PATH` to store a log file on disk. By default no log file is written. Entries for `DEBUG` level and higher. Thanks to [@hackebrot](https://github.com/hackebrot)(#792). * Existing templates in a user\'s `cookiecutters_dir` (default is `~/.cookiecutters/`) can now be referenced by directory name, thanks to [@michaeljoseph](https://github.com/michaeljoseph) (#825). * Add support for dict values in `cookiecutter.json`, thanks to [@freakboy3742](https://github.com/freakboy3742) and [@hackebrot](https://github.com/hackebrot) (#815, #858). * Add a `jsonify` filter to default jinja2 extensions that json.dumps a Python object into a string, thanks to [@aroig](https://github.com/aroig) (#791). Bug Fixes: * Fix typo in the error logging text for when a hook did not exit successfully, thanks to [@luzfcb](https://github.com/luzfcb) (#656) * Fix an issue around **replay** file names when **cookiecutter** is used with a relative path to a template, thanks to [@eliasdorneles](https://github.com/eliasdorneles) for raising the issue and [@hackebrot](https://github.com/hackebrot) for the PR (#752, #753) * Ignore hook files with tilde-suffixes, thanks to [@hackebrot](https://github.com/hackebrot) (#768) * Fix a minor issue with the code that generates a name for a template, thanks to [@hackebrot](https://github.com/hackebrot)(#798) * Handle empty hook file or other OS errors, thanks to [@christianmlong](https://github.com/christianmlong) for raising this bug and [@jcarbaugh](https://github.com/jcarbaugh) and [@hackebrot](https://github.com/hackebrot) for the fix (#632, #729, #862) * Resolve an issue with custom extensions not being loaded for `pre_gen_project` and `post_gen_project` hooks, thanks to [@cheungnj](https://github.com/cheungnj) (#860) Other Changes: * Remove external dependencies from tests, so that tests can be run w/o network connection, thanks to [@hackebrot](https://github.com/hackebrot) (#603) * Remove execute permissions on Python files, thanks to [@mozillazg](https://github.com/mozillazg) (#650) * Report code coverage info from AppVeyor build to codecov, thanks to [@ewjoachim](https://github.com/ewjoachim) (#670) * Documented functions and methods lacking documentation, thanks to [@pydanny](https://github.com/pydanny) (#673) * Documented `__init__` methods for Environment objects, thanks to [@pydanny](https://github.com/pydanny) (#677) * Updated whichcraft to 0.4.0, thanks to [@pydanny](https://github.com/pydanny). * Updated documentation link to Read the Docs, thanks to [@natim](https://github.com/Natim) (#687) * Moved cookiecutter templates and added category links, thanks to [@willingc](https://github.com/willingc) (#674) * Added Github Issue Template, thanks to [@luzfcb](https://github.com/luzfcb) (#700) * Added `ssh` repository examples, thanks to [@pokoli](https://github.com/pokoli/) (#702) * Fix links to the cookiecutter-data-science template and its documentation, thanks to [@tephyr](https://github.com/tephyr) for the PR and [@willingc](https://github.com/willingc) for the review (#711, #714) * Update link to docs for Django\'s `--template` command line option, thanks to [@purplediane](https://github.com/purplediane) (#754) * Create *hook backup files* during the tests as opposed to having them as static files in the repository, thanks to [@hackebrot](https://github.com/hackebrot) (#789) * Applied PEP 257 docstring conventions to: * `environment.py`, thanks to [@terryjbates](https://github.com/terryjbates) (#759) * `find.py`, thanks to [@terryjbates](https://github.com/terryjbates) (#761) * `generate.py`, thanks to [@terryjbates](https://github.com/terryjbates) (#764) * `hooks.py`, thanks to [@terryjbates](https://github.com/terryjbates) (#766) * `repository.py`, thanks to [@terryjbates](https://github.com/terryjbates) (#833) * `vcs.py`, thanks to [@terryjbates](https://github.com/terryjbates) (#831) * Fix link to the Tryton cookiecutter, thanks to [@cedk](https://github.com/cedk) and [@nicoe](https://github.com/nicoe) (#697, #698) * Added PyCon US 2016 sponsorship to README, thanks to [@purplediane](https://github.com/purplediane) (#720) * Added a sprint contributor doc, thanks to [@phoebebauer](https://github.com/phoebebauer) (#727) * Converted readthedocs links (.org -\> .io), thanks to [@adamchainz](https://github.com/adamchainz) (#718) * Added Python 3.6 support, thanks to [@suledev](https://github.com/suledev) (#728) * Update occurrences of `repo_name` in documentation, thanks to [@palmerev](https://github.com/palmerev) (#734) * Added case studies document, thanks to [@pydanny](https://github.com/pydanny) (#735) * Added first steps cookiecutter creation tutorial, thanks to [@BruceEckel](https://github.com/BruceEckel) (#736) * Reorganised tutorials and setup git submodule to external tutorial, thanks to [@dot2dotseurat](https://github.com/dot2dotseurat) (#740) * Debian installation instructions, thanks to [@ivanlyon](https://github.com/ivanlyon) (#738) * Usage documentation typo fix., thanks to [@terryjbates](https://github.com/terryjbates) (#739) * Updated documentation copyright date, thanks to [@zzzirk](https://github.com/zzzirk) (#747) * Add a make rule to update git submodules, thanks to [@hackebrot](https://github.com/hackebrot) (#746) * Split up advanced usage docs, thanks to [@zzzirk](https://github.com/zzzirk) (#749) * Documentation for the `no_input` option, thanks to [@pokoli](https://github.com/pokoli/) (#701) * Remove unnecessary shebangs from python files, thanks to [@michaeljoseph](https://github.com/michaeljoseph) (#763) * Refactor cookiecutter template identification, thanks to [@michaeljoseph](https://github.com/michaeljoseph) (#777) * Add a `cli_runner` test fixture to simplify CLI tests, thanks to [@hackebrot](https://github.com/hackebrot) (#790) * Add a check to ensure cookiecutter repositories have JSON context, thanks to [@michaeljoseph](https://github.com/michaeljoseph)(#782) * Rename the internal function that determines whether a file should be rendered, thanks to [@audreyfeldroy](https://github.com/audreyfeldroy) for raising the issue and [@hackebrot](https://github.com/hackebrot)for the PR (#741, #802) * Fix typo in docs, thanks to [@mwarkentin](https://github.com/mwarkentin) (#828) * Fix broken link to *Invoke* docs, thanks to [@B3QL](https://github.com/B3QL) (#820) * Add documentation to `render_variable` function in `prompt.py`, thanks to [@pydanny](https://github.com/pydanny) (#678) * Fix python3.6 travis-ci and tox configuration, thanks to [@luzfcb](https://github.com/luzfcb) (#844) * Add missing encoding declarations to python files, thanks to [@andytom](https://github.com/andytom) (#852) * Disable poyo logging for tests, thanks to [@hackebrot](https://github.com/hackebrot) (#855) * Remove pycache directories in make clean-pyc, thanks to [@hackebrot](https://github.com/hackebrot) (#849) * Refactor hook system to only find the requested hook, thanks to [@michaeljoseph](https://github.com/michaeljoseph) (#834) * Add tests for custom extensions in `pre_gen_project` and `post_gen_project` hooks, thanks to [@hackebrot](https://github.com/hackebrot) (#856) * Make the build reproducible by avoiding nondeterministic keyword arguments, thanks to [@lamby](https://github.com/lamby) and [@hackebrot](https://github.com/hackebrot) (#800, #861) * Extend CLI help message and point users to the github project to engage with the community, thanks to [@hackebrot](https://github.com/hackebrot) (#859) * Added more cookiecutter templates to the mix: * [cookiecutter-funkload-friendly](https://github.com/tokibito/cookiecutter-funkload-friendly) by [@tokibito](https://github.com/tokibito) (#657) * [cookiecutter-reveal.js](https://github.com/keimlink/cookiecutter-reveal.js) by [@keimlink](https://github.com/keimlink) (#660) * [cookiecutter-python-app](https://github.com/mdklatt/cookiecutter-python-app) by [@mdklatt](https://github.com/mdklatt) (#659) * [morepath-cookiecutter](https://github.com/morepath/morepath-cookiecutter) by [@href](https://github.com/href) (#672) * [hovercraft-slides](https://github.com/Springerle/hovercraft-slides) by [@jhermann](https://github.com/jhermann) (#665) * [cookiecutter-es6-package](https://github.com/ratson/cookiecutter-es6-package) by [@ratson](https://github.com/ratson) (#667) * [cookiecutter-webpack](https://github.com/hzdg/cookiecutter-webpack) by [@hzdg](https://github.com/hzdg) (#668) * [cookiecutter-django-herokuapp](https://github.com/dulaccc/cookiecutter-django-herokuapp) by [@dulaccc](https://github.com/dulaccc) (#374) * [cookiecutter-django-aws-eb](https://github.com/dolphinkiss/cookiecutter-django-aws-eb) by [@peterlauri](https://github.com/peterlauri) (#626) * [wagtail-starter-kit](https://github.com/tkjone/wagtail-starter-kit) by [@tkjone](https://github.com/tkjone) (#658) * [cookiecutter-dpf-effect](https://github.com/SpotlightKid/cookiecutter-dpf-effect) by [@SpotlightKid](https://github.com/SpotlightKid) (#663) * [cookiecutter-dpf-audiotk](https://github.com/SpotlightKid/cookiecutter-dpf-audiotk) by [@SpotlightKid](https://github.com/SpotlightKid) (#663) * [cookiecutter-template](https://github.com/eviweb/cookiecutter-template) by [@eviweb](https://github.com/eviweb) (#664) * [cookiecutter-angular2](https://github.com/matheuspoleza/cookiecutter-angular2) by [@matheuspoleza](https://github.com/matheuspoleza) (#675) * [cookiecutter-data-science](http://drivendata.github.io/cookiecutter-data-science/) by [@pjbull](https://github.com/pjbull) (#680) * [cc\_django\_ember\_app](https://bitbucket.org/levit_scs/cc_django_ember_app) by [@nanuxbe](https://github.com/nanuxbe) (#686) * [cc\_project\_app\_drf](https://bitbucket.org/levit_scs/cc_project_app_drf) by [@nanuxbe](https://github.com/nanuxbe) (#686) * [cc\_project\_app\_full\_with\_hooks](https://bitbucket.org/levit_scs/cc_project_app_full_with_hooks) by [@nanuxbe](https://github.com/nanuxbe) (#686) * [beat-generator](https://github.com/elastic/beat-generator) by [@ruflin](https://github.com/ruflin) (#695) * [cookiecutter-scala](https://github.com/Plippe/cookiecutter-scala) by [@Plippe](https://github.com/Plippe) (#751) * [cookiecutter-snakemake-analysis-pipeline](https://github.com/xguse/cookiecutter-snakemake-analysis-pipeline) by [@xguse](https://github.com/xguse) (#692) * [cookiecutter-py3tkinter](https://github.com/ivanlyon/cookiecutter-py3tkinter) by [@ivanlyon](https://github.com/ivanlyon) (#730) * [pyramid-cookiecutter-alchemy](https://github.com/Pylons/pyramid-cookiecutter-alchemy) by [@stevepiercy](https://github.com/stevepiercy) (#745) * [pyramid-cookiecutter-starter](https://github.com/Pylons/pyramid-cookiecutter-starter) by [@stevepiercy](https://github.com/stevepiercy) (#745) * [pyramid-cookiecutter-zodb](https://github.com/Pylons/pyramid-cookiecutter-zodb) by [@stevepiercy](https://github.com/stevepiercy) (#745) * [substanced-cookiecutter](https://github.com/Pylons/substanced-cookiecutter) by [@stevepiercy](https://github.com/stevepiercy) (#745) * [cookiecutter-simple-django-cn](https://github.com/shenyushun/cookiecutter-simple-django-cn) by [@shenyushun](https://github.com/shenyushun) (#765) * [cookiecutter-pyqt5](https://github.com/mandeepbhutani/cookiecutter-pyqt5) by [@mandeepbhutani](https://github.com/mandeepbhutani) (#797) * [cookiecutter-xontrib](https://github.com/laerus/cookiecutter-xontrib) by [@laerus](https://github.com/laerus) (#817) * [cookiecutter-reproducible-science](https://github.com/mkrapp/cookiecutter-reproducible-science) by [@mkrapp](https://github.com/mkrapp) (#826) * [cc-automated-drf-template](https://github.com/TAMU-CPT/cc-automated-drf-template) by [@elenimijalis](https://github.com/elenimijalis) (#832) ## 1.4.0 (2016-03-20) Shortbread The goal of this release is changing to a strict Jinja2 environment, paving the way to more awesome in the future, as well as adding support for Jinja2 extensions. New Features: * Added support for Jinja2 extension support, thanks to [@hackebrot](https://github.com/hackebrot) (#617). * Now raises an error if Cookiecutter tries to render a template that contains an undefined variable. Makes generation more robust and secure (#586). Work done by [@hackebrot](https://github.com/hackebrot) (#111, #586, #592) * Uses strict Jinja2 env in prompt, thanks to [@hackebrot](https://github.com/hackebrot) (#598, #613) * Switched from pyyaml/ruamel.yaml libraries that were problematic across platforms to the pure Python [poyo](https://pypi.python.org/pypi/poyo) library, thanks to [@hackebrot](https://github.com/hackebrot) (#557, #569, #621) * User config values for `cookiecutters_dir` and `replay_dir` now support environment variable and user home expansion, thanks to [@nfarrar](https://github.com/nfarrar) for the suggestion and [@hackebrot](https://github.com/hackebrot) for the PR (#640,#642) * Add [jinja2-time](https://pypi.python.org/pypi/jinja2-time) as default extension for dates and times in templates via `{% now 'utc' %}`,thanks to [@hackebrot](https://github.com/hackebrot) (#653) Bug Fixes: * Provided way to define options that have no defaults, thanks to [@johtso](https://github.com/johtso) (#587, #588) * Make sure that `replay.dump()` and `replay.load()` use the correct user config, thanks to [@hackebrot](https://github.com/hackebrot) (#590, #594) * Added correct CA bundle for Git on Appveyor, thanks to [@maiksensi](https://github.com/maiksensi) (#599, #602) * Open `HISTORY.rst` with `utf-8` encoding when reading the changelog, thanks to [@0-wiz-0](https://github.com/0-wiz-0) for submitting the issue and [@hackebrot](https://github.com/hackebrot) for the fix (#638, #639) * Fix repository indicators for [privaterepository](http://cookiecutter.readthedocs.io/en/latest/usage.html#works-with-private-repos) urls, thanks to [@habnabit](https://github.com/habnabit) for the fix (#595) and [@hackebrot](https://github.com/hackebrot) for the tests (#655) Other Changes: * Set path before running tox, thanks to [@maiksensi](https://github.com/maiksensi) (#615, #620) * Removed xfail in test\_cookiecutters, thanks to [@hackebrot](https://github.com/hackebrot) (#618) * Removed django-cms-plugin on account of 404 error, thanks to [@mativs](https://github.com/mativs) and [@pydanny](https://github.com/pydanny) (#593) * Fixed docs/usage.rst, thanks to [@macrotim](https://github.com/macrotim) (#604) * Update .gitignore to latest Python.gitignore and ignore PyCharm files, thanks to [@audreyfeldroy](https://github.com/audreyfeldroy) * Use open context manager to read context\_file in generate() function, thanks to [@hackebrot](https://github.com/hackebrot) (#607, #608) * Added documentation for choice variables, thanks to [@maiksensi](https://github.com/maiksensi) (#611) * Set up Scrutinizer to check code quality, thanks to [@audreyfeldroy](https://github.com/audreyfeldroy) * Drop distutils support in setup.py, thanks to [@hackebrot](https://github.com/hackebrot) (#606, #609) * Change cookiecutter-pypackage-minimal link, thanks to [@kragniz](https://github.com/kragniz) (#614) * Fix typo in one of the template\'s description, thanks to [@ryanfreckleton](https://github.com/ryanfreckleton) (#643) * Fix broken link to [\_copy\_without\_render](http://cookiecutter.readthedocs.io/en/latest/advanced_usage.html#copy-without-render) in *troubleshooting.rst*, thanks to [@ptim](https://github.com/ptim) (#647) * Added more cookiecutter templates to the mix: * [cookiecutter-pipproject](https://github.com/wdm0006/cookiecutter-pipproject) by [@wdm0006](https://github.com/wdm0006) (#624) * [cookiecutter-flask-2](https://github.com/wdm0006/cookiecutter-flask) by [@wdm0006](https://github.com/wdm0006) (#624) * [cookiecutter-kotlin-gradle](https://github.com/thomaslee/cookiecutter-kotlin-gradle) by [@thomaslee](https://github.com/thomaslee) (#622) * [cookiecutter-tryton-fulfilio](https://github.com/fulfilio/cookiecutter-tryton) by [@cedk](https://github.com/cedk) (#631) * [django-starter](https://github.com/tkjone/django-starter) by [@tkjone](https://github.com/tkjone) (#635) * [django-docker-bootstrap](https://github.com/legios89/django-docker-bootstrap) by [@legios89](https://github.com/legios89) (#636) * [cookiecutter-mediawiki-extension](https://github.com/JonasGroeger/cookiecutter-mediawiki-extension) by [@JonasGroeger](https://github.com/JonasGroeger) (#645) * [cookiecutter-django-gulp](https://github.com/valerymelou/cookiecutter-django-gulp) by [@valerymelou](https://github.com/valerymelou) (#648) ## 1.3.0 (2015-11-10) Pumpkin Spice The goal of this release is to extend the user config feature and to make hook execution more robust. New Features: * Abort project generation if `pre_gen_project` or `post_gen_project` hook scripts fail, thanks to [@eliasdorneles](https://github.com/eliasdorneles) (#464, #549) * Extend user config capabilities with additional cli options `--config-file` and `--default-config` and environment variable `COOKIECUTTER_CONFIG`, thanks to [@jhermann](https://github.com/jhermann), [@pfmoore](https://github.com/pfmoore), and [@hackebrot](https://github.com/hackebrot) (#258, #424, #565) Bug Fixes: * Fixed conditional dependencies for wheels in setup.py, thanks to [@hackebrot](https://github.com/hackebrot) (#557, #568) * Reverted skipif markers to use correct reasons (bug fixed in pytest), thanks to [@hackebrot](https://github.com/hackebrot) (#574) Other Changes: * Improved path and documentation for rendering the Sphinx documentation, thanks to [@eliasdorneles](https://github.com/eliasdorneles) and [@hackebrot](https://github.com/hackebrot) (#562, #583) * Added additional help entrypoints, thanks to [@michaeljoseph](https://github.com/michaeljoseph) (#563, #492) * Added Two Scoops Academy to the README, thanks to [@hackebrot](https://github.com/hackebrot) (#576) * Now handling trailing slash on URL, thanks to [@ramiroluz](https://github.com/ramiroluz) (#573, #546) * Support for testing x86 and x86-64 architectures on appveyor, thanks to [@maiksensi](https://github.com/maiksensi) (#567) * Made tests work without installing Cookiecutter, thanks to [@vincentbernat](https://github.com/vincentbernat) (#550) * Encoded the result of the hook template to utf8, thanks to [@ionelmc](https://github.com/ionelmc) (#577. #578) * Added test for \_run\_hook\_from\_repo\_dir, thanks to [@hackebrot](https://github.com/hackebrot) (#579, #580) * Implemented bumpversion, thanks to [@hackebrot](https://github.com/hackebrot) (#582) * Added more cookiecutter templates to the mix: * [cookiecutter-octoprint-plugin](https://github.com/OctoPrint/cookiecutter-octoprint-plugin) by [@foosel](https://github.com/foosel) (#560) * [wagtail-cookiecutter-foundation](https://github.com/chrisdev/wagtail-cookiecutter-foundation) by [@chrisdev](https://github.com/chrisdev), et al. (#566) ## 1.2.1 (2015-10-18) Zimtsterne *Zimtsterne are cinnamon star cookies.* New Feature: * Returns rendered project dir, thanks to [@hackebrot](https://github.com/hackebrot) (#553) Bug Fixes: * Factor in *choice* variables (as introduced in 1.1.0) when using a user config or extra context, thanks to [@ionelmc](https://github.com/ionelmc) and [@hackebrot](https://github.com/hackebrot) (#536, #542). Other Changes: * Enable py35 support on Travis by using Python 3.5 as base Python ([@maiksensi](https://github.com/maiksensi) / #540) * If a filename is empty, do not generate. Log instead ([@iljabauer](https://github.com/iljabauer) / #444) * Fix tests as per last changes in [cookiecutter-pypackage](https://github.com/audreyfeldroy/cookiecutter-pypackage), thanks to [@eliasdorneles](https://github.com/eliasdorneles)(#555). * Removed deprecated cookiecutter-pylibrary-minimal from the list, thanks to [@ionelmc](https://github.com/ionelmc) (#556) * Moved to using rualmel.yaml instead of PyYAML, except for Windows users on Python 2.7, thanks to [@pydanny](https://github.com/pydanny) (#557) *Why 1.2.1 instead of 1.2.0? There was a problem in the distribution that we pushed to PyPI. Since you can\'t replace previous files uploaded to PyPI, we deleted the files on PyPI and released 1.2.1.* ## 1.1.0 (2015-09-26) Snickerdoodle The goals of this release were ```copy without render``` and a few additional command-line options such as ```--overwrite-if-exists```, ```---replay```, and ```output-dir```. Features: * Added [copy without render](http://cookiecutter.readthedocs.io/en/latest/advanced_usage.html#copy-without-render) feature, making it much easier for developers of Ansible, Salt Stack, and other recipe-based tools to work with Cookiecutter. Thanks to [@osantana](https://github.com/osantana) and [@LucianU](https://github.com/LucianU) for their innovation, as well as [@hackebrot](https://github.com/hackebrot) for fixing the Windows problems (#132, #184, #425). * Added specify output directory, thanks to [@tony](https://github.com/tony) and [@hackebrot](https://github.com/hackebrot) (#531, #452). * Abort template rendering if the project output directory already exists, thanks to [@lgp171188](https://github.com/lgp171188) (#470, #471). * Add a flag to overwrite existing output directory, thanks to [@lgp171188](https://github.com/lgp171188) for the implementation (#495) and [@schacki](https://github.com/schacki), [@ionelmc](https://github.com/ionelmc), [@pydanny](https://github.com/pydanny) and [@hackebrot](https://github.com/hackebrot) for submitting issues and code reviews (#475, #493). * Remove test command in favor of tox, thanks to [@hackebrot](https://github.com/hackebrot) (#480). * Allow cookiecutter invocation, even without installing it, via `python -m cookiecutter.cli`, thanks to [@vincentbernat](https://github.com/vincentbernat) and [@hackebrot](https://github.com/hackebrot) (#449, #487). * Improve the type detection handler for online and offline repositories, thanks to [@charlax](https://github.com/charlax) (#490). * Add replay feature, thanks to [@hackebrot](https://github.com/hackebrot) (#501). * Be more precise when raising an error for an invalid user config file, thanks to [@vaab](https://github.com/vaab) and [@hackebrot](https://github.com/hackebrot) (#378, #528). * Added official Python 3.5 support, thanks to [@pydanny](https://github.com/pydanny) and [@hackebrot](https://github.com/hackebrot) (#522). * Added support for *choice* variables and switch to click style prompts, thanks to [@hackebrot](https://github.com/hackebrot) (#441, #455). Other Changes: * Updated click requirement to \< 6.0, thanks to [@pydanny](https://github.com/pydanny) (#473). * Added landscape.io flair, thanks to [@michaeljoseph](https://github.com/michaeljoseph) (#439). * Descriptions of PEP8 specifications and milestone management, thanks to [@michaeljoseph](https://github.com/michaeljoseph) (#440). * Added alternate installation options in the documentation, thanks to [@pydanny](https://github.com/pydanny) (#117, #315). * The test of the which() function now tests against the date command, thanks to [@vincentbernat](https://github.com/vincentbernat) (#446) * Ensure file handles in setup.py are closed using with statement, thanks to [@svisser](https://github.com/svisser) (#280). * Removed deprecated and fully extraneous compat.is\_exe() function, thanks to [@hackebrot](https://github.com/hackebrot) (#485). * Disabled sudo in .travis, thanks to [@hackebrot](https://github.com/hackebrot) (#482). * Switched to shields.io for problematic badges, thanks to [@pydanny](https://github.com/pydanny) (#491). * Added whichcraft and removed `compat.which()`, thanks to [@pydanny](https://github.com/pydanny) (#511). * Changed to export tox environment variables to codecov, thanks to [@maiksensi](https://github.com/maiksensi). (#508). * Moved to using click version command, thanks to [@hackebrot](https://github.com/hackebrot) (#489). * Don\'t use unicode\_literals to please click, thanks to [@vincentbernat](https://github.com/vincentbernat) (#503). * Remove warning for Python 2.6 from \_\_init\_\_.py, thanks to [@hackebrot](https://github.com/hackebrot). * Removed compat.py module, thanks to [@hackebrot](https://github.com/hackebrot). * Added future to requirements, thanks to [@hackebrot](https://github.com/hackebrot). * Fixed problem where expanduser does not resolve \"\~\" correctly on windows 10 using tox, thanks to [@maiksensi](https://github.com/maiksensi). (#527) * Added more cookiecutter templates to the mix: * [cookiecutter-beamer](https://github.com/luismartingil/cookiecutter-beamer) by [@luismartingil](https://github.com/luismartingil) (#307) * [cookiecutter-pytest-plugin](https://github.com/pytest-dev/cookiecutter-pytest-plugin) by [@pytest-dev](https://github.com/pytest-dev) and [@hackebrot](https://github.com/hackebrot) (#481) * [cookiecutter-csharp-objc-binding](https://github.com/SandyChapman/cookiecutter-csharp-objc-binding) by [@SandyChapman](https://github.com/SandyChapman) (#460) * [cookiecutter-flask-foundation](https://github.com/JackStouffer/cookiecutter-Flask-Foundation) by [@JackStouffer](https://github.com/JackStouffer) (#457) * [cookiecutter-tryton-fulfilio](https://github.com/fulfilio/cookiecutter-tryton) by [@fulfilio](https://github.com/fulfilio) (#465) * [cookiecutter-tapioca](https://github.com/vintasoftware/cookiecutter-tapioca) by [@vintasoftware](https://github.com/vintasoftware) (#496) * [cookiecutter-sublime-text-3-plugin](https://github.com/kkujawinski/cookiecutter-sublime-text-3-plugin) by [@kkujawinski](https://github.com/kkujawinski) (#500) * [cookiecutter-muffin](https://github.com/drgarcia1986/cookiecutter-muffin) by [@drgarcia1986](https://github.com/drgarcia1986) (#494) * [cookiecutter-django-rest](https://github.com/agconti/cookiecutter-django-rest) by [@agconti](https://github.com/agconti) (#520) * [cookiecutter-es6-boilerplate](https://github.com/agconti/cookiecutter-es6-boilerplate) by [@agconti](https://github.com/agconti) (#521) * [cookiecutter-tampermonkey](https://github.com/christabor/cookiecutter-tampermonkey) by [@christabor](https://github.com/christabor) (#516) * [cookiecutter-wagtail](https://github.com/torchbox/cookiecutter-wagtail) by [@torchbox](https://github.com/torchbox) (#533) ## 1.0.0 (2015-03-13) Chocolate Chip The goals of this release was to formally remove support for Python 2.6 and continue the move to using py.test. Features: * Convert the unittest suite to py.test for the sake of comprehensibility, thanks to [@hackebrot](https://github.com/hackebrot) (#322, #332, #334, #336, #337, #338, #340, #341, #343, #345, #347, #351, #412, #413, #414). * Generate pytest coverage, thanks to [@michaeljoseph](https://github.com/michaeljoseph) (#326). * Documenting of Pull Request merging and HISTORY.rst maintenance, thanks to [@michaeljoseph](https://github.com/michaeljoseph) (#330). * Large expansions to the tutorials thanks to [@hackebrot](https://github.com/hackebrot) (#384) * Switch to using Click for command-line options, thanks to [@michaeljoseph](https://github.com/michaeljoseph) (#391, #393). * Added support for working with private repos, thanks to [@marctc](https://github.com/marctc) (#265). * Wheel configuration thanks to [@michaeljoseph](https://github.com/michaeljoseph) (#118). Other Changes: * Formally removed support for 2.6, thanks to [@pydanny](https://github.com/pydanny) (#201). * Moved to codecov for continuous integration test coverage and badges, thanks to [@michaeljoseph](https://github.com/michaeljoseph) (#71, #369). * Made JSON parsing errors easier to debug, thanks to [@rsyring](https://github.com/rsyring) and [@mark0978](https://github.com/mark0978) (#355, #358, #388). * Updated to Jinja 2.7 or higher in order to control trailing new lines in templates, thanks to [@sfermigier](https://github.com/sfermigier) (#356). * Tweaked flake8 to ignore e731, thanks to [@michaeljoseph](https://github.com/michaeljoseph) (#390). * Fixed failing Windows tests and corrected AppVeyor badge link thanks to [@msabramo](https://github.com/msabramo) (#403). * Added more Cookiecutters to the list: * [cookiecutter-scala-spark](https://github.com/jpzk/cookiecutter-scala-spark) by [@jpzk](https://github.com/jpzk) * [cookiecutter-atari2600](https://github.com/joeyjoejoejr/cookiecutter-atari2600) by [@joeyjoejoejr](https://github.com/joeyjoejoejr) * [cookiecutter-bottle](https://github.com/avelino/cookiecutter-bottle) by [@avelino](https://github.com/avelino) * [cookiecutter-latex-article](https://github.com/Kreger51/cookiecutter-latex-article) by [@Kreger51](https://github.com/Kreger51) * [cookiecutter-django-rest-framework](https://github.com/jpadilla/cookiecutter-django-rest-framework) by [@jpadilla](https://github.com/jpadilla) * [cookiedozer](https://github.com/hackebrot/cookiedozer) by [@hackebrot](https://github.com/hackebrot) ## 0.9.0 (2015-01-13) The goals of this release were to add the ability to Jinja2ify the cookiecutter.json default values, and formally launch support for Python 3.4. Features: * Python 3.4 is now a first class citizen, thanks to everyone. * cookiecutter.json values are now rendered Jinja2 templates, thanks to \@bollwyvl (#291). * Move to py.test, thanks to [@pfmoore](https://github.com/pfmoore) (#319) and [@ramiroluz](https://github.com/ramiroluz) (#310). * Add PendingDeprecation warning for users of Python 2.6, as support for it is gone in Python 2.7, thanks to [@michaeljoseph](https://github.com/michaeljoseph) (#201). Bug Fixes: * Corrected typo in Makefile, thanks to [@inglesp](https://github.com/inglesp) (#297). * Raise an exception when users don\'t have git or hg installed, thanks to [@pydanny](https://github.com/pydanny) (#303). Other changes: * Creation of [gitter](https://gitter.im/audreyr/cookiecutter) account for logged chat, thanks to [@michaeljoseph](https://github.com/michaeljoseph). * Added ReadTheDocs badge, thanks to [@michaeljoseph](https://github.com/michaeljoseph). * Added AppVeyor badge, thanks to [@pydanny](https://github.com/pydanny) * Documentation and PyPI trove classifier updates, thanks to [@thedrow](https://github.com/thedrow) (#323 and #324) ## 0.8.0 (2014-10-30) The goal of this release was to allow for injection of extra context via the Cookiecutter API, and to fix minor bugs. Features: * cookiecutter() now takes an optional extra\_context parameter, thanks to [@michaeljoseph](https://github.com/michaeljoseph), [@fcurella](https://github.com/fcurella), [@aventurella](https://github.com/aventurella), [@emonty](https://github.com/emonty), [@schacki](https://github.com/schacki), [@ryanolson](https://github.com/ryanolson), [@pfmoore](https://github.com/pfmoore), [@pydanny](https://github.com/pydanny), [@audreyfeldroy](https://github.com/audreyfeldroy) (#260). * Context is now injected into hooks, thanks to [@michaeljoseph](https://github.com/michaeljoseph) and [@dinopetrone](https://github.com/dinopetrone). * Moved all Python 2/3 compatibility code into cookiecutter.compat, making the eventual move to six easier, thanks to [@michaeljoseph](https://github.com/michaeljoseph) (#60, #102). * Added cookiecutterrc defined aliases for cookiecutters, thanks to [@pfmoore](https://github.com/pfmoore) (#246) * Added flake8 to tox to check for pep8 violations, thanks to [@natim](https://github.com/Natim). Bug Fixes: * Newlines at the end of files are no longer stripped, thanks to [@treyhunner](https://github.com/treyhunner) (#183). * Cloning prompt suppressed by respecting the ```no\_input``` flag, thanks to [@trustrachel](https://github.com/trustrachel) (#285) * With Python 3, input is no longer converted to bytes, thanks to [@uranusjr](https://github.com/uranusjr) (#98). Other Changes: * Added more Cookiecutters to the list: * [Python-iOS-template](https://github.com/pybee/Python-iOS-template) by [@freakboy3742](https://github.com/freakboy3742) * [Python-Android-template](https://github.com/pybee/Python-Android-template) by [@freakboy3742](https://github.com/freakboy3742) * [cookiecutter-djangocms-plugin](https://github.com/mishbahr/cookiecutter-djangocms-plugin) by [@mishbahr](https://github.com/mishbahr) * [cookiecutter-pyvanguard](https://github.com/robinandeer/cookiecutter-pyvanguard) by [@robinandeer](https://github.com/robinandeer) ## 0.7.2 (2014-08-05) The goal of this release was to fix cross-platform compatibility, primarily Windows bugs that had crept in during the addition of new features. As of this release, Windows is a first-class citizen again, now complete with continuous integration. Bug Fixes: * Fixed the contributing file so it displays nicely in Github, thanks to [@pydanny](https://github.com/pydanny). * Updates 2.6 requirements to include simplejson, thanks to [@saxix](https://github.com/saxix). * Avoid unwanted extra spaces in string literal, thanks to [@merwok](https://github.com/merwok). * Fix @unittest.skipIf error on Python 2.6. * Let sphinx parse :param: properly by inserting newlines #213, thanks to [@mineo](https://github.com/mineo). * Fixed Windows test prompt failure by replacing stdin per [@cjrh](https://github.com/cjrh) in #195. * Made rmtree remove readonly files, thanks to [@pfmoore](https://github.com/pfmoore). * Now using tox to run tests on Appveyor, thanks to [@pfmoore](https://github.com/pfmoore) (#241). * Fixed tests that assumed the system encoding was utf-8, thanks to [@pfmoore](https://github.com/pfmoore) (#242, #244). * Added a tox ini file that uses py.test, thanks to [@pfmoore](https://github.com/pfmoore) (#245). Other Changes: * [@audreyfeldroy](https://github.com/audreyfeldroy) formally accepted position as **BDFL of cookiecutter**. * Elevated [@pydanny](https://github.com/pydanny), [@michaeljoseph](https://github.com/michaeljoseph), and [@pfmoore](https://github.com/pfmoore) to core committer status. * Added Core Committer guide, by [@audreyfeldroy](https://github.com/audreyfeldroy). * Generated apidocs from make docs, by [@audreyfeldroy](https://github.com/audreyfeldroy). * Added contributing command to the makedocs function, by [@pydanny](https://github.com/pydanny). * Refactored contributing documentation, included adding core committer instructions, by [@pydanny](https://github.com/pydanny) and [@audreyfeldroy](https://github.com/audreyfeldroy). * Do not convert input prompt to bytes, thanks to [@uranusjr](https://github.com/uranusjr) (#192). * Added troubleshooting info about Python 3.3 tests and tox. * Added documentation about command line arguments, thanks to [@saxix](https://github.com/saxix). * Style cleanups. * Added environment variable to disable network tests for environments without networking, thanks to [@vincentbernat](https://github.com/vincentbernat). * Added Appveyor support to aid Windows integrations, thanks to [@pydanny](https://github.com/pydanny) (#215). * CONTRIBUTING.rst is now generated via make contributing, thanks to [@pydanny](https://github.com/pydanny) (#220). * Removed unnecessary endoing argument to json.load, thanks to [@pfmoore](https://github.com/pfmoore) (#234). * Now generating shell hooks dynamically for Unix/Windows portability, thanks to [@pfmoore](https://github.com/pfmoore) (#236). * Removed non-portable assumptions about directory structure, thanks to [@pfmoore](https://github.com/pfmoore) (#238). * Added a note on portability to the hooks documentation, thanks to [@pfmoore](https://github.com/pfmoore) (#239). * Replaced unicode\_open with direct use of io.open, thanks to [@pfmoore](https://github.com/pfmoore) (#229). * Added more Cookiecutters to the list: * [cookiecutter-kivy](https://github.com/hackebrot/cookiecutter-kivy) by [@hackebrot](https://github.com/hackebrot) * [BoilerplatePP](https://github.com/Paspartout/BoilerplatePP) by [@Paspartout](https://github.com/Paspartout) * [cookiecutter-pypackage-minimal](https://github.com/kragniz/cookiecutter-pypackage-minimal) by [@borntyping](https://github.com/borntyping) * [cookiecutter-ansible-role](https://github.com/iknite/cookiecutter-ansible-role) by [@iknite](https://github.com/iknite) * [cookiecutter-pylibrary](https://github.com/ionelmc/cookiecutter-pylibrary) by [@ionelmc](https://github.com/ionelmc) * [cookiecutter-pylibrary-minimal](https://github.com/ionelmc/cookiecutter-pylibrary-minimal) by [@ionelmc](https://github.com/ionelmc) ## 0.7.1 (2014-04-26) Bug fixes: * Use the current Python interpreter to run Python hooks, thanks to [@coderanger](https://github.com/coderanger). * Include tests and documentation in source distribution, thanks to [@vincentbernat](https://github.com/vincentbernat). * Fix various warnings and missing things in the docs (#129, #130), thanks to [@nedbat](https://github.com/nedbat). * Add command line option to get version (#89), thanks to [@davedash](https://github.com/davedash) and [@cyberj](https://github.com/cyberj). Other changes: * Add more Cookiecutters to the list: * [cookiecutter-avr](https://github.com/solarnz/cookiecutter-avr) by [@solarnz](https://github.com/solarnz) * [cookiecutter-tumblr-theme](https://github.com/relekang/cookiecutter-tumblr-theme) by [@relekang](https://github.com/relekang) * [cookiecutter-django-paas](https://github.com/pbacterio/cookiecutter-django-paas) by [@pbacterio](https://github.com/pbacterio) ## 0.7.0 (2013-11-09) This is a release with significant improvements and changes. Please read through this list before you upgrade. New features: * Support for \--checkout argument, thanks to [@foobacca](https://github.com/foobacca/). * Support for pre-generate and post-generate hooks, thanks to [@raphigaziano](https://github.com/raphigaziano/). Hooks are Python or shell scripts that run before and/or after your project is generated. * Support for absolute paths to cookiecutters, thanks to [@krallin](https://github.com/krallin/). * Support for Mercurial version control system, thanks to [@pokoli](https://github.com/pokoli/). * When a cookiecutter contains invalid Jinja2 syntax, you get a better message that shows the location of the TemplateSyntaxError. Thanks to [@benjixx](https://github.com/benjixx/). * Can now prompt the user to enter values during generation from a local cookiecutter, thanks to [@ThomasChiroux](https://github.com/ThomasChiroux/). This is now always the default behavior. Prompts can also be suppressed with ```--no-input```. * Your cloned cookiecutters are stored by default in your ~/.cookiecutters/ directory (or Windows equivalent). The location is configurable. (This is a major change from the pre-0.7.0 behavior, where cloned cookiecutters were deleted at the end of project generation.) Thanks [@raphigaziano](https://github.com/raphigaziano/). * User config in a \~/.cookiecutterrc file, thanks to [@raphigaziano](https://github.com/raphigaziano/). Configurable settings are cookiecutters\_dir and default\_context. * File permissions are now preserved during project generation, thanks to [@benjixx](https://github.com/benjixx/). Bug fixes: * Unicode issues with prompts and answers are fixed, thanks to [@s-m-i-t-a](https://github.com/s-m-i-t-a/). * The test suite now runs on Windows, which was a major effort. Thanks to [@pydanny](https://github.com/pydanny), who collaborated on this with me. Other changes: * Quite a bit of refactoring and API changes. * Lots of documentation improvements. Thanks [@sloria](https://github.com/sloria/), [@alex](https://github.com/alex/), [@pydanny](https://github.com/pydanny), [@freakboy3742](https://github.com/freakboy3742), [@es128](https://github.com/es128/), [@rolo](https://github.com/rolo/). * Better naming and organization of test suite. * A CookiecutterCleanSystemTestCase to use for unit tests affected by the user\'s config and cookiecutters directory. * Improvements to the project\'s Makefile. * Improvements to tests. Thanks [@gperetin](https://github.com/gperetin/), [@s-m-i-t-a](https://github.com/s-m-i-t-a/). * Removal of subprocess32 dependency. Now using non-context manager version of subprocess.Popen for Python 2 compatibility. * Removal of cookiecutter\'s cleanup module. * A bit of setup.py cleanup, thanks to [@oubiga](https://github.com/oubiga/). * Now depends on binaryornot 0.2.0. ## 0.6.4 (2013-08-21) * Windows support officially added. * Fix TemplateNotFound Exception on Windows (#37). ## 0.6.3 (2013-08-20) * Fix copying of binary files in nested paths (#41), thanks to [@sloria](https://github.com/sloria/). ## 0.6.2 (2013-08-19) * Depend on Jinja2\>=2.4 instead of Jinja2==2.7. * Fix errors on attempt to render binary files. Copy them over from the project template without rendering. * Fix Python 2.6/2.7 UnicodeDecodeError when values containing Unicode chars are in cookiecutter.json. * Set encoding in Python 3 unicode_open() to always be utf-8. ## 0.6.1 (2013-08-12) * Improved project template finding. Now looks for the occurrence of {{,cookiecutter, and }} in a directory name. * Fix help message for input_dir arg at command prompt. * Minor edge cases found and corrected, as a result of improved test coverage. ## 0.6.0 (2013-08-08) * Config is now in a single ```cookiecutter.json``` instead of in ```json/```. * When you create a project from a git repo template, Cookiecutter prompts you to enter custom values for the fields defined in ```cookiecutter.json```. ## 0.5 (2013-07-28) * Friendlier, more simplified command line usage: ```bash # Create project from the cookiecutter-pypackage/ template $ cookiecutter cookiecutter-pypackage/ # Create project from the cookiecutter-pypackage.git repo template $ cookiecutter https://github.com/audreyfeldroy/cookiecutter-pypackage.git ``` * Can now use Cookiecutter from Python as a package: ```python from cookiecutter.main import cookiecutter # Create project from the cookiecutter-pypackage/ template cookiecutter('cookiecutter-pypackage/') # Create project from the cookiecutter-pypackage.git repo template cookiecutter('https://github.com/audreyfeldroy/cookiecutter-pypackage.git') ``` * Internal refactor to remove any code that changes the working directory. ## 0.4 (2013-07-22) * Only takes in one argument now: the input directory. The output directory is generated by rendering the name of the input directory. * Output directory cannot be the same as input directory. ## 0.3 (2013-07-17) * Takes in command line args for the input and output directories. ## 0.2.1 (2013-07-17) * Minor cleanup. ## 0.2 (2013-07-17) Bumped to "Development Status :: 3 - Alpha". * Works with any type of text file. * Directory names and filenames can be templated. ## 0.1.0 (2013-07-11) * First release on PyPI. cookiecutter-2.6.0/LICENSE000066400000000000000000000027251456543333500152450ustar00rootroot00000000000000Copyright (c) 2013-2022, Audrey Roy Greenfeld All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. cookiecutter-2.6.0/MANIFEST.in000066400000000000000000000006311456543333500157700ustar00rootroot00000000000000include AUTHORS.md include CODE_OF_CONDUCT.md include CONTRIBUTING.md include HISTORY.md include LICENSE include README.md include cookiecutter/VERSION.txt exclude Makefile exclude __main__.py exclude .* exclude codecov.yml exclude test_requirements.txt exclude tox.ini recursive-include tests * recursive-exclude * __pycache__ recursive-exclude * *.py[co] recursive-exclude docs * recursive-exclude logo * cookiecutter-2.6.0/Makefile000066400000000000000000000047531456543333500157030ustar00rootroot00000000000000PYPI_SERVER = pypitest define BROWSER_PYSCRIPT import os, webbrowser, sys try: from urllib import pathname2url except: from urllib.request import pathname2url webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) endef export BROWSER_PYSCRIPT BROWSER := python -c "$$BROWSER_PYSCRIPT" .DEFAULT_GOAL := help .PHONY: clean-tox clean-tox: ## Remove tox testing artifacts @echo "+ $@" @rm -rf .tox/ .PHONY: clean-coverage clean-coverage: ## Remove coverage reports @echo "+ $@" @rm -rf htmlcov/ @rm -rf .coverage @rm -rf coverage.xml .PHONY: clean-pytest clean-pytest: ## Remove pytest cache @echo "+ $@" @rm -rf .pytest_cache/ .PHONY: clean-docs-build clean-docs-build: ## Remove local docs @echo "+ $@" @rm -rf docs/_build .PHONY: clean-build clean-build: ## Remove build artifacts @echo "+ $@" @rm -fr build/ @rm -fr dist/ @rm -fr *.egg-info .PHONY: clean-pyc clean-pyc: ## Remove Python file artifacts @echo "+ $@" @find . -type d -name '__pycache__' -exec rm -rf {} + @find . -type f -name '*.py[co]' -exec rm -f {} + @find . -name '*~' -exec rm -f {} + .PHONY: clean ## Remove all file artifacts clean: clean-build clean-pyc clean-tox clean-coverage clean-pytest clean-docs-build .PHONY: lint lint: ## Check code style @echo "+ $@" @tox -e lint .PHONY: test test: ## Run tests quickly with the default Python @echo "+ $@" @tox -e py310 .PHONY: test-all test-all: ## Run tests on every Python version @echo "+ $@" @tox .PHONY: coverage coverage: ## Check code coverage quickly with the default Python @echo "+ $@" @tox -e py310 @$(BROWSER) htmlcov/index.html .PHONY: docs docs: ## Generate Sphinx HTML documentation, including API docs @echo "+ $@" @tox -e docs @$(BROWSER) docs/_build/html/index.html .PHONY: servedocs servedocs: ## Rebuild docs automatically @echo "+ $@" @tox -e servedocs .PHONY: submodules submodules: ## Pull and update git submodules recursively @echo "+ $@" @git pull --recurse-submodules @git submodule update --init --recursive .PHONY: release release: clean ## Package and upload release @echo "+ $@" @python -m build @twine upload -r $(PYPI_SERVER) dist/* .PHONY: sdist sdist: clean ## Build sdist distribution @echo "+ $@" @python -m build --sdist @ls -l dist .PHONY: wheel wheel: clean ## Build wheel distribution @echo "+ $@" @python -m build --wheel @ls -l dist .PHONY: help help: @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-16s\033[0m %s\n", $$1, $$2}' cookiecutter-2.6.0/README.md000066400000000000000000000125251456543333500155160ustar00rootroot00000000000000

cookiecutter Logo

[![pypi](https://img.shields.io/pypi/v/cookiecutter.svg)](https://pypi.org/project/cookiecutter/) [![python](https://img.shields.io/pypi/pyversions/cookiecutter.svg)](https://pypi.org/project/cookiecutter/) [![Build Status](https://github.com/cookiecutter/cookiecutter/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/cookiecutter/cookiecutter/actions) [![codecov](https://codecov.io/gh/cookiecutter/cookiecutter/branch/master/graphs/badge.svg?branch=master)](https://codecov.io/github/cookiecutter/cookiecutter?branch=master) [![discord](https://img.shields.io/badge/Discord-cookiecutter-5865F2?style=flat&logo=discord&logoColor=white)](https://discord.gg/9BrxzPKuEW) [![docs](https://readthedocs.org/projects/cookiecutter/badge/?version=latest)](https://readthedocs.org/projects/cookiecutter/?badge=latest) [![Code Quality](https://img.shields.io/scrutinizer/g/cookiecutter/cookiecutter.svg)](https://scrutinizer-ci.com/g/cookiecutter/cookiecutter/?branch=master)
# Cookiecutter Create projects swiftly from **cookiecutters** (project templates) with this command-line utility. Ideal for generating Python package projects and more. - [Documentation](https://cookiecutter.readthedocs.io) - [GitHub](https://github.com/cookiecutter/cookiecutter) - [PyPI](https://pypi.org/project/cookiecutter/) - [License (BSD)](https://github.com/cookiecutter/cookiecutter/blob/main/LICENSE) ## Installation Install cookiecutter using pip package manager: ``` # pipx is strongly recommended. pipx install cookiecutter # If pipx is not an option, # you can install cookiecutter in your Python user directory. python -m pip install --user cookiecutter ``` ## Features - **Cross-Platform:** Supports Windows, Mac, and Linux. - **User-Friendly:** No Python knowledge required. - **Versatile:** Compatible with Python 3.7 to 3.12. - **Multi-Language Support:** Use templates in any language or markup format. ### For Users #### Quick Start The recommended way to use Cookiecutter as a command line utility is to run it with [`pipx`](https://pypa.github.io/pipx/), which can be installed with `pip install pipx`, but if you plan to use Cookiecutter programmatically, please run `pip install cookiecutter`. **Use a GitHub template** ```bash # You'll be prompted to enter values. # Then it'll create your Python package in the current working directory, # based on those values. # For the sake of brevity, repos on GitHub can just use the 'gh' prefix $ pipx run cookiecutter gh:audreyfeldroy/cookiecutter-pypackage ``` **Use a local template** ```bash $ pipx run cookiecutter cookiecutter-pypackage/ ``` **Use it from Python** ```py from cookiecutter.main import cookiecutter # Create project from the cookiecutter-pypackage/ template cookiecutter('cookiecutter-pypackage/') # Create project from the cookiecutter-pypackage.git repo template cookiecutter('gh:audreyfeldroy//cookiecutter-pypackage.git') ``` #### Detailed Usage - Generate projects from local or remote templates. - Customize projects with `cookiecutter.json` prompts. - Utilize pre-prompt, pre- and post-generate hooks. [Learn More](https://cookiecutter.readthedocs.io/en/latest/usage.html) ### For Template Creators - Utilize unlimited directory nesting. - Employ Jinja2 for all templating needs. - Define template variables easily with `cookiecutter.json`. [Learn More](https://cookiecutter.readthedocs.io/en/latest/tutorials/) ## Available Templates Discover a variety of ready-to-use templates on [GitHub](https://github.com/search?q=cookiecutter&type=Repositories). ### Special Templates - [cookiecutter-pypackage](https://github.com/audreyfeldroy/cookiecutter-pypackage) - [cookiecutter-django](https://github.com/pydanny/cookiecutter-django) - [cookiecutter-pytest-plugin](https://github.com/pytest-dev/cookiecutter-pytest-plugin) - [cookiecutter-plone-starter](https://github.com/collective/cookiecutter-plone-starter) ## Community Join the community, contribute, or seek assistance. - [Troubleshooting Guide](https://cookiecutter.readthedocs.io/en/latest/troubleshooting.html) - [Stack Overflow](https://stackoverflow.com/questions/tagged/cookiecutter) - [Discord](https://discord.gg/9BrxzPKuEW) - [File an Issue](https://github.com/cookiecutter/cookiecutter/issues?q=is%3Aopen) - [Contributors](AUTHORS.md) - [Contribution Guide](CONTRIBUTING.md) ### Support - Star us on [GitHub](https://github.com/cookiecutter/cookiecutter). - Stay tuned for upcoming support options. ### Feedback We value your feedback. Share your criticisms or complaints constructively to help us improve. - [File an Issue](https://github.com/cookiecutter/cookiecutter/issues?q=is%3Aopen) ### Waiting for a Response? - Be patient and consider reaching out to the community for assistance. - For urgent matters, contact [@audreyfeldroy](https://github.com/audreyfeldroy) for consultation or custom development. ## Code of Conduct Adhere to the [PyPA Code of Conduct](https://www.pypa.io/en/latest/code-of-conduct/) during all interactions in the project's ecosystem. ## Acknowledgements Created and led by [Audrey Roy Greenfeld](https://github.com/audreyfeldroy), supported by a dedicated team of maintainers and contributors. cookiecutter-2.6.0/__main__.py000066400000000000000000000002531456543333500163240ustar00rootroot00000000000000"""Allow cookiecutter to be executable from a checkout or zip file.""" import runpy if __name__ == "__main__": runpy.run_module("cookiecutter", run_name="__main__") cookiecutter-2.6.0/codecov.yml000066400000000000000000000001211456543333500163710ustar00rootroot00000000000000# comment spam as user can always click the failed coverage check comment: false cookiecutter-2.6.0/cookiecutter/000077500000000000000000000000001456543333500167325ustar00rootroot00000000000000cookiecutter-2.6.0/cookiecutter/VERSION.txt000066400000000000000000000000061456543333500206140ustar00rootroot000000000000002.6.0 cookiecutter-2.6.0/cookiecutter/__init__.py000066400000000000000000000004761456543333500210520ustar00rootroot00000000000000"""Main package for Cookiecutter.""" from pathlib import Path def _get_version() -> str: """Read VERSION.txt and return its contents.""" path = Path(__file__).parent.resolve() version_file = path / "VERSION.txt" return version_file.read_text(encoding="utf-8").strip() __version__ = _get_version() cookiecutter-2.6.0/cookiecutter/__main__.py000066400000000000000000000003021456543333500210170ustar00rootroot00000000000000"""Allow cookiecutter to be executable through `python -m cookiecutter`.""" from cookiecutter.cli import main if __name__ == "__main__": # pragma: no cover main(prog_name="cookiecutter") cookiecutter-2.6.0/cookiecutter/cli.py000066400000000000000000000162301456543333500200550ustar00rootroot00000000000000"""Main `cookiecutter` CLI.""" import collections import json import os import sys import click from cookiecutter import __version__ from cookiecutter.config import get_user_config from cookiecutter.exceptions import ( ContextDecodingException, FailedHookException, InvalidModeException, InvalidZipRepository, OutputDirExistsException, RepositoryCloneFailed, RepositoryNotFound, UndefinedVariableInTemplate, UnknownExtension, ) from cookiecutter.log import configure_logger from cookiecutter.main import cookiecutter def version_msg(): """Return the Cookiecutter version, location and Python powering it.""" python_version = sys.version location = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) return f"Cookiecutter {__version__} from {location} (Python {python_version})" def validate_extra_context(ctx, param, value): """Validate extra context.""" for string in value: if '=' not in string: raise click.BadParameter( f"EXTRA_CONTEXT should contain items of the form key=value; " f"'{string}' doesn't match that form" ) # Convert tuple -- e.g.: ('program_name=foobar', 'startsecs=66') # to dict -- e.g.: {'program_name': 'foobar', 'startsecs': '66'} return collections.OrderedDict(s.split('=', 1) for s in value) or None def list_installed_templates(default_config, passed_config_file): """List installed (locally cloned) templates. Use cookiecutter --list-installed.""" config = get_user_config(passed_config_file, default_config) cookiecutter_folder = config.get('cookiecutters_dir') if not os.path.exists(cookiecutter_folder): click.echo( f"Error: Cannot list installed templates. " f"Folder does not exist: {cookiecutter_folder}" ) sys.exit(-1) template_names = [ folder for folder in os.listdir(cookiecutter_folder) if os.path.exists( os.path.join(cookiecutter_folder, folder, 'cookiecutter.json') ) ] click.echo(f'{len(template_names)} installed templates: ') for name in template_names: click.echo(f' * {name}') @click.command(context_settings=dict(help_option_names=['-h', '--help'])) @click.version_option(__version__, '-V', '--version', message=version_msg()) @click.argument('template', required=False) @click.argument('extra_context', nargs=-1, callback=validate_extra_context) @click.option( '--no-input', is_flag=True, help='Do not prompt for parameters and only use cookiecutter.json file content. ' 'Defaults to deleting any cached resources and redownloading them. ' 'Cannot be combined with the --replay flag.', ) @click.option( '-c', '--checkout', help='branch, tag or commit to checkout after git clone', ) @click.option( '--directory', help='Directory within repo that holds cookiecutter.json file ' 'for advanced repositories with multi templates in it', ) @click.option( '-v', '--verbose', is_flag=True, help='Print debug information', default=False ) @click.option( '--replay', is_flag=True, help='Do not prompt for parameters and only use information entered previously. ' 'Cannot be combined with the --no-input flag or with extra configuration passed.', ) @click.option( '--replay-file', type=click.Path(), default=None, help='Use this file for replay instead of the default.', ) @click.option( '-f', '--overwrite-if-exists', is_flag=True, help='Overwrite the contents of the output directory if it already exists', ) @click.option( '-s', '--skip-if-file-exists', is_flag=True, help='Skip the files in the corresponding directories if they already exist', default=False, ) @click.option( '-o', '--output-dir', default='.', type=click.Path(), help='Where to output the generated project dir into', ) @click.option( '--config-file', type=click.Path(), default=None, help='User configuration file' ) @click.option( '--default-config', is_flag=True, help='Do not load a config file. Use the defaults instead', ) @click.option( '--debug-file', type=click.Path(), default=None, help='File to be used as a stream for DEBUG logging', ) @click.option( '--accept-hooks', type=click.Choice(['yes', 'ask', 'no']), default='yes', help='Accept pre/post hooks', ) @click.option( '-l', '--list-installed', is_flag=True, help='List currently installed templates.' ) @click.option( '--keep-project-on-failure', is_flag=True, help='Do not delete project folder on failure', ) def main( template, extra_context, no_input, checkout, verbose, replay, overwrite_if_exists, output_dir, config_file, default_config, debug_file, directory, skip_if_file_exists, accept_hooks, replay_file, list_installed, keep_project_on_failure, ): """Create a project from a Cookiecutter project template (TEMPLATE). Cookiecutter is free and open source software, developed and managed by volunteers. If you would like to help out or fund the project, please get in touch at https://github.com/cookiecutter/cookiecutter. """ # Commands that should work without arguments if list_installed: list_installed_templates(default_config, config_file) sys.exit(0) # Raising usage, after all commands that should work without args. if not template or template.lower() == 'help': click.echo(click.get_current_context().get_help()) sys.exit(0) configure_logger(stream_level='DEBUG' if verbose else 'INFO', debug_file=debug_file) # If needed, prompt the user to ask whether or not they want to execute # the pre/post hooks. if accept_hooks == "ask": _accept_hooks = click.confirm("Do you want to execute hooks?") else: _accept_hooks = accept_hooks == "yes" if replay_file: replay = replay_file try: cookiecutter( template, checkout, no_input, extra_context=extra_context, replay=replay, overwrite_if_exists=overwrite_if_exists, output_dir=output_dir, config_file=config_file, default_config=default_config, password=os.environ.get('COOKIECUTTER_REPO_PASSWORD'), directory=directory, skip_if_file_exists=skip_if_file_exists, accept_hooks=_accept_hooks, keep_project_on_failure=keep_project_on_failure, ) except ( ContextDecodingException, OutputDirExistsException, InvalidModeException, FailedHookException, UnknownExtension, InvalidZipRepository, RepositoryNotFound, RepositoryCloneFailed, ) as e: click.echo(e) sys.exit(1) except UndefinedVariableInTemplate as undefined_err: click.echo(f'{undefined_err.message}') click.echo(f'Error message: {undefined_err.error.message}') context_str = json.dumps(undefined_err.context, indent=4, sort_keys=True) click.echo(f'Context: {context_str}') sys.exit(1) if __name__ == "__main__": main() cookiecutter-2.6.0/cookiecutter/config.py000066400000000000000000000112401456543333500205470ustar00rootroot00000000000000"""Global configuration handling.""" import collections import copy import logging import os import yaml from cookiecutter.exceptions import ConfigDoesNotExistException, InvalidConfiguration logger = logging.getLogger(__name__) USER_CONFIG_PATH = os.path.expanduser('~/.cookiecutterrc') BUILTIN_ABBREVIATIONS = { 'gh': 'https://github.com/{0}.git', 'gl': 'https://gitlab.com/{0}.git', 'bb': 'https://bitbucket.org/{0}', } DEFAULT_CONFIG = { 'cookiecutters_dir': os.path.expanduser('~/.cookiecutters/'), 'replay_dir': os.path.expanduser('~/.cookiecutter_replay/'), 'default_context': collections.OrderedDict([]), 'abbreviations': BUILTIN_ABBREVIATIONS, } def _expand_path(path): """Expand both environment variables and user home in the given path.""" path = os.path.expandvars(path) path = os.path.expanduser(path) return path def merge_configs(default, overwrite): """Recursively update a dict with the key/value pair of another. Dict values that are dictionaries themselves will be updated, whilst preserving existing keys. """ new_config = copy.deepcopy(default) for k, v in overwrite.items(): # Make sure to preserve existing items in # nested dicts, for example `abbreviations` if isinstance(v, dict): new_config[k] = merge_configs(default.get(k, {}), v) else: new_config[k] = v return new_config def get_config(config_path): """Retrieve the config from the specified path, returning a config dict.""" if not os.path.exists(config_path): raise ConfigDoesNotExistException(f'Config file {config_path} does not exist.') logger.debug('config_path is %s', config_path) with open(config_path, encoding='utf-8') as file_handle: try: yaml_dict = yaml.safe_load(file_handle) or {} except yaml.YAMLError as e: raise InvalidConfiguration( f'Unable to parse YAML file {config_path}.' ) from e if not isinstance(yaml_dict, dict): raise InvalidConfiguration( f'Top-level element of YAML file {config_path} should be an object.' ) config_dict = merge_configs(DEFAULT_CONFIG, yaml_dict) raw_replay_dir = config_dict['replay_dir'] config_dict['replay_dir'] = _expand_path(raw_replay_dir) raw_cookies_dir = config_dict['cookiecutters_dir'] config_dict['cookiecutters_dir'] = _expand_path(raw_cookies_dir) return config_dict def get_user_config(config_file=None, default_config=False): """Return the user config as a dict. If ``default_config`` is True, ignore ``config_file`` and return default values for the config parameters. If ``default_config`` is a dict, merge values with default values and return them for the config parameters. If a path to a ``config_file`` is given, that is different from the default location, load the user config from that. Otherwise look up the config file path in the ``COOKIECUTTER_CONFIG`` environment variable. If set, load the config from this path. This will raise an error if the specified path is not valid. If the environment variable is not set, try the default config file path before falling back to the default config values. """ # Do NOT load a config. Merge provided values with defaults and return them instead if default_config and isinstance(default_config, dict): return merge_configs(DEFAULT_CONFIG, default_config) # Do NOT load a config. Return defaults instead. if default_config: logger.debug("Force ignoring user config with default_config switch.") return copy.copy(DEFAULT_CONFIG) # Load the given config file if config_file and config_file is not USER_CONFIG_PATH: logger.debug("Loading custom config from %s.", config_file) return get_config(config_file) try: # Does the user set up a config environment variable? env_config_file = os.environ['COOKIECUTTER_CONFIG'] except KeyError: # Load an optional user config if it exists # otherwise return the defaults if os.path.exists(USER_CONFIG_PATH): logger.debug("Loading config from %s.", USER_CONFIG_PATH) return get_config(USER_CONFIG_PATH) else: logger.debug("User config not found. Loading default config.") return copy.copy(DEFAULT_CONFIG) else: # There is a config environment variable. Try to load it. # Do not check for existence, so invalid file paths raise an error. logger.debug("User config not found or not specified. Loading default config.") return get_config(env_config_file) cookiecutter-2.6.0/cookiecutter/environment.py000066400000000000000000000043511456543333500216530ustar00rootroot00000000000000"""Jinja2 environment and extensions loading.""" from jinja2 import Environment, StrictUndefined from cookiecutter.exceptions import UnknownExtension class ExtensionLoaderMixin: """Mixin providing sane loading of extensions specified in a given context. The context is being extracted from the keyword arguments before calling the next parent class in line of the child. """ def __init__(self, **kwargs): """Initialize the Jinja2 Environment object while loading extensions. Does the following: 1. Establishes default_extensions (currently just a Time feature) 2. Reads extensions set in the cookiecutter.json _extensions key. 3. Attempts to load the extensions. Provides useful error if fails. """ context = kwargs.pop('context', {}) default_extensions = [ 'cookiecutter.extensions.JsonifyExtension', 'cookiecutter.extensions.RandomStringExtension', 'cookiecutter.extensions.SlugifyExtension', 'cookiecutter.extensions.TimeExtension', 'cookiecutter.extensions.UUIDExtension', ] extensions = default_extensions + self._read_extensions(context) try: super().__init__(extensions=extensions, **kwargs) except ImportError as err: raise UnknownExtension(f'Unable to load extension: {err}') from err def _read_extensions(self, context): """Return list of extensions as str to be passed on to the Jinja2 env. If context does not contain the relevant info, return an empty list instead. """ try: extensions = context['cookiecutter']['_extensions'] except KeyError: return [] else: return [str(ext) for ext in extensions] class StrictEnvironment(ExtensionLoaderMixin, Environment): """Create strict Jinja2 environment. Jinja2 environment will raise error on undefined variable in template- rendering context. """ def __init__(self, **kwargs): """Set the standard Cookiecutter StrictEnvironment. Also loading extensions defined in cookiecutter.json's _extensions key. """ super().__init__(undefined=StrictUndefined, **kwargs) cookiecutter-2.6.0/cookiecutter/exceptions.py000066400000000000000000000074561456543333500215010ustar00rootroot00000000000000"""All exceptions used in the Cookiecutter code base are defined here.""" class CookiecutterException(Exception): """ Base exception class. All Cookiecutter-specific exceptions should subclass this class. """ class NonTemplatedInputDirException(CookiecutterException): """ Exception for when a project's input dir is not templated. The name of the input directory should always contain a string that is rendered to something else, so that input_dir != output_dir. """ class UnknownTemplateDirException(CookiecutterException): """ Exception for ambiguous project template directory. Raised when Cookiecutter cannot determine which directory is the project template, e.g. more than one dir appears to be a template dir. """ # unused locally class MissingProjectDir(CookiecutterException): """ Exception for missing generated project directory. Raised during cleanup when remove_repo() can't find a generated project directory inside of a repo. """ # unused locally class ConfigDoesNotExistException(CookiecutterException): """ Exception for missing config file. Raised when get_config() is passed a path to a config file, but no file is found at that path. """ class InvalidConfiguration(CookiecutterException): """ Exception for invalid configuration file. Raised if the global configuration file is not valid YAML or is badly constructed. """ class UnknownRepoType(CookiecutterException): """ Exception for unknown repo types. Raised if a repo's type cannot be determined. """ class VCSNotInstalled(CookiecutterException): """ Exception when version control is unavailable. Raised if the version control system (git or hg) is not installed. """ class ContextDecodingException(CookiecutterException): """ Exception for failed JSON decoding. Raised when a project's JSON context file can not be decoded. """ class OutputDirExistsException(CookiecutterException): """ Exception for existing output directory. Raised when the output directory of the project exists already. """ class InvalidModeException(CookiecutterException): """ Exception for incompatible modes. Raised when cookiecutter is called with both `no_input==True` and `replay==True` at the same time. """ class FailedHookException(CookiecutterException): """ Exception for hook failures. Raised when a hook script fails. """ class UndefinedVariableInTemplate(CookiecutterException): """ Exception for out-of-scope variables. Raised when a template uses a variable which is not defined in the context. """ def __init__(self, message, error, context): """Exception for out-of-scope variables.""" self.message = message self.error = error self.context = context def __str__(self): """Text representation of UndefinedVariableInTemplate.""" return ( f"{self.message}. " f"Error message: {self.error.message}. " f"Context: {self.context}" ) class UnknownExtension(CookiecutterException): """ Exception for un-importable extension. Raised when an environment is unable to import a required extension. """ class RepositoryNotFound(CookiecutterException): """ Exception for missing repo. Raised when the specified cookiecutter repository doesn't exist. """ class RepositoryCloneFailed(CookiecutterException): """ Exception for un-cloneable repo. Raised when a cookiecutter template can't be cloned. """ class InvalidZipRepository(CookiecutterException): """ Exception for bad zip repo. Raised when the specified cookiecutter repository isn't a valid Zip archive. """ cookiecutter-2.6.0/cookiecutter/extensions.py000066400000000000000000000075661456543333500215210ustar00rootroot00000000000000"""Jinja2 extensions.""" import json import string import uuid from secrets import choice import arrow from jinja2 import nodes from jinja2.ext import Extension from slugify import slugify as pyslugify class JsonifyExtension(Extension): """Jinja2 extension to convert a Python object to JSON.""" def __init__(self, environment): """Initialize the extension with the given environment.""" super().__init__(environment) def jsonify(obj): return json.dumps(obj, sort_keys=True, indent=4) environment.filters['jsonify'] = jsonify class RandomStringExtension(Extension): """Jinja2 extension to create a random string.""" def __init__(self, environment): """Jinja2 Extension Constructor.""" super().__init__(environment) def random_ascii_string(length, punctuation=False): if punctuation: corpus = "".join((string.ascii_letters, string.punctuation)) else: corpus = string.ascii_letters return "".join(choice(corpus) for _ in range(length)) environment.globals.update(random_ascii_string=random_ascii_string) class SlugifyExtension(Extension): """Jinja2 Extension to slugify string.""" def __init__(self, environment): """Jinja2 Extension constructor.""" super().__init__(environment) def slugify(value, **kwargs): """Slugifies the value.""" return pyslugify(value, **kwargs) environment.filters['slugify'] = slugify class UUIDExtension(Extension): """Jinja2 Extension to generate uuid4 string.""" def __init__(self, environment): """Jinja2 Extension constructor.""" super().__init__(environment) def uuid4(): """Generate UUID4.""" return str(uuid.uuid4()) environment.globals.update(uuid4=uuid4) class TimeExtension(Extension): """Jinja2 Extension for dates and times.""" tags = {'now'} def __init__(self, environment): """Jinja2 Extension constructor.""" super().__init__(environment) environment.extend(datetime_format='%Y-%m-%d') def _datetime(self, timezone, operator, offset, datetime_format): d = arrow.now(timezone) # parse shift params from offset and include operator shift_params = {} for param in offset.split(','): interval, value = param.split('=') shift_params[interval.strip()] = float(operator + value.strip()) d = d.shift(**shift_params) if datetime_format is None: datetime_format = self.environment.datetime_format return d.strftime(datetime_format) def _now(self, timezone, datetime_format): if datetime_format is None: datetime_format = self.environment.datetime_format return arrow.now(timezone).strftime(datetime_format) def parse(self, parser): """Parse datetime template and add datetime value.""" lineno = next(parser.stream).lineno node = parser.parse_expression() if parser.stream.skip_if('comma'): datetime_format = parser.parse_expression() else: datetime_format = nodes.Const(None) if isinstance(node, nodes.Add): call_method = self.call_method( '_datetime', [node.left, nodes.Const('+'), node.right, datetime_format], lineno=lineno, ) elif isinstance(node, nodes.Sub): call_method = self.call_method( '_datetime', [node.left, nodes.Const('-'), node.right, datetime_format], lineno=lineno, ) else: call_method = self.call_method( '_now', [node, datetime_format], lineno=lineno, ) return nodes.Output([call_method], lineno=lineno) cookiecutter-2.6.0/cookiecutter/find.py000066400000000000000000000020301456543333500202170ustar00rootroot00000000000000"""Functions for finding Cookiecutter templates and other components.""" import logging import os from pathlib import Path from jinja2 import Environment from cookiecutter.exceptions import NonTemplatedInputDirException logger = logging.getLogger(__name__) def find_template(repo_dir: "os.PathLike[str]", env: Environment) -> Path: """Determine which child directory of ``repo_dir`` is the project template. :param repo_dir: Local directory of newly cloned repo. :return: Relative path to project template. """ logger.debug('Searching %s for the project template.', repo_dir) for str_path in os.listdir(repo_dir): if ( 'cookiecutter' in str_path and env.variable_start_string in str_path and env.variable_end_string in str_path ): project_template = Path(repo_dir, str_path) break else: raise NonTemplatedInputDirException logger.debug('The project template appears to be %s', project_template) return project_template cookiecutter-2.6.0/cookiecutter/generate.py000066400000000000000000000401451456543333500211020ustar00rootroot00000000000000"""Functions for generating a project from a project template.""" import fnmatch import json import logging import os import shutil import warnings from collections import OrderedDict from pathlib import Path from binaryornot.check import is_binary from jinja2 import Environment, FileSystemLoader from jinja2.exceptions import TemplateSyntaxError, UndefinedError from cookiecutter.exceptions import ( ContextDecodingException, OutputDirExistsException, UndefinedVariableInTemplate, ) from cookiecutter.find import find_template from cookiecutter.hooks import run_hook_from_repo_dir from cookiecutter.utils import ( create_env_with_context, make_sure_path_exists, rmtree, work_in, ) logger = logging.getLogger(__name__) def is_copy_only_path(path, context): """Check whether the given `path` should only be copied and not rendered. Returns True if `path` matches a pattern in the given `context` dict, otherwise False. :param path: A file-system path referring to a file or dir that should be rendered or just copied. :param context: cookiecutter context. """ try: for dont_render in context['cookiecutter']['_copy_without_render']: if fnmatch.fnmatch(path, dont_render): return True except KeyError: return False return False def apply_overwrites_to_context( context, overwrite_context, *, in_dictionary_variable=False ): """Modify the given context in place based on the overwrite_context.""" for variable, overwrite in overwrite_context.items(): if variable not in context: if not in_dictionary_variable: # We are dealing with a new variable on first level, ignore continue # We are dealing with a new dictionary variable in a deeper level context[variable] = overwrite context_value = context[variable] if isinstance(context_value, list): if in_dictionary_variable: context[variable] = overwrite continue if isinstance(overwrite, list): # We are dealing with a multichoice variable # Let's confirm all choices are valid for the given context if set(overwrite).issubset(set(context_value)): context[variable] = overwrite else: raise ValueError( f"{overwrite} provided for multi-choice variable " f"{variable}, but valid choices are {context_value}" ) else: # We are dealing with a choice variable if overwrite in context_value: # This overwrite is actually valid for the given context # Let's set it as default (by definition first item in list) # see ``cookiecutter.prompt.prompt_choice_for_config`` context_value.remove(overwrite) context_value.insert(0, overwrite) else: raise ValueError( f"{overwrite} provided for choice variable " f"{variable}, but the choices are {context_value}." ) elif isinstance(context_value, dict) and isinstance(overwrite, dict): # Partially overwrite some keys in original dict apply_overwrites_to_context( context_value, overwrite, in_dictionary_variable=True ) context[variable] = context_value else: # Simply overwrite the value for this variable context[variable] = overwrite def generate_context( context_file='cookiecutter.json', default_context=None, extra_context=None ): """Generate the context for a Cookiecutter project template. Loads the JSON file as a Python object, with key being the JSON filename. :param context_file: JSON file containing key/value pairs for populating the cookiecutter's variables. :param default_context: Dictionary containing config to take into account. :param extra_context: Dictionary containing configuration overrides """ context = OrderedDict([]) try: with open(context_file, encoding='utf-8') as file_handle: obj = json.load(file_handle, object_pairs_hook=OrderedDict) except ValueError as e: # JSON decoding error. Let's throw a new exception that is more # friendly for the developer or user. full_fpath = os.path.abspath(context_file) json_exc_message = str(e) our_exc_message = ( f"JSON decoding error while loading '{full_fpath}'. " f"Decoding error details: '{json_exc_message}'" ) raise ContextDecodingException(our_exc_message) from e # Add the Python object to the context dictionary file_name = os.path.split(context_file)[1] file_stem = file_name.split('.')[0] context[file_stem] = obj # Overwrite context variable defaults with the default context from the # user's global config, if available if default_context: try: apply_overwrites_to_context(obj, default_context) except ValueError as error: warnings.warn(f"Invalid default received: {error}") if extra_context: apply_overwrites_to_context(obj, extra_context) logger.debug('Context generated is %s', context) return context def generate_file(project_dir, infile, context, env, skip_if_file_exists=False): """Render filename of infile as name of outfile, handle infile correctly. Dealing with infile appropriately: a. If infile is a binary file, copy it over without rendering. b. If infile is a text file, render its contents and write the rendered infile to outfile. Precondition: When calling `generate_file()`, the root template dir must be the current working directory. Using `utils.work_in()` is the recommended way to perform this directory change. :param project_dir: Absolute path to the resulting generated project. :param infile: Input file to generate the file from. Relative to the root template dir. :param context: Dict for populating the cookiecutter's variables. :param env: Jinja2 template execution environment. """ logger.debug('Processing file %s', infile) # Render the path to the output file (not including the root project dir) outfile_tmpl = env.from_string(infile) outfile = os.path.join(project_dir, outfile_tmpl.render(**context)) file_name_is_empty = os.path.isdir(outfile) if file_name_is_empty: logger.debug('The resulting file name is empty: %s', outfile) return if skip_if_file_exists and os.path.exists(outfile): logger.debug('The resulting file already exists: %s', outfile) return logger.debug('Created file at %s', outfile) # Just copy over binary files. Don't render. logger.debug("Check %s to see if it's a binary", infile) if is_binary(infile): logger.debug('Copying binary %s to %s without rendering', infile, outfile) shutil.copyfile(infile, outfile) shutil.copymode(infile, outfile) return # Force fwd slashes on Windows for get_template # This is a by-design Jinja issue infile_fwd_slashes = infile.replace(os.path.sep, '/') # Render the file try: tmpl = env.get_template(infile_fwd_slashes) except TemplateSyntaxError as exception: # Disable translated so that printed exception contains verbose # information about syntax error location exception.translated = False raise rendered_file = tmpl.render(**context) if context['cookiecutter'].get('_new_lines', False): # Use `_new_lines` from context, if configured. newline = context['cookiecutter']['_new_lines'] logger.debug('Using configured newline character %s', repr(newline)) else: # Detect original file newline to output the rendered file. # Note that newlines can be a tuple if file contains mixed line endings. # In this case, we pick the first line ending we detected. with open(infile, encoding='utf-8') as rd: rd.readline() # Read only the first line to load a 'newlines' value. newline = rd.newlines[0] if isinstance(rd.newlines, tuple) else rd.newlines logger.debug('Using detected newline character %s', repr(newline)) logger.debug('Writing contents to file %s', outfile) with open(outfile, 'w', encoding='utf-8', newline=newline) as fh: fh.write(rendered_file) # Apply file permissions to output file shutil.copymode(infile, outfile) def render_and_create_dir( dirname: str, context: dict, output_dir: "os.PathLike[str]", environment: Environment, overwrite_if_exists: bool = False, ): """Render name of a directory, create the directory, return its path.""" name_tmpl = environment.from_string(dirname) rendered_dirname = name_tmpl.render(**context) dir_to_create = Path(output_dir, rendered_dirname) logger.debug( 'Rendered dir %s must exist in output_dir %s', dir_to_create, output_dir ) output_dir_exists = dir_to_create.exists() if output_dir_exists: if overwrite_if_exists: logger.debug( 'Output directory %s already exists, overwriting it', dir_to_create ) else: msg = f'Error: "{dir_to_create}" directory already exists' raise OutputDirExistsException(msg) else: make_sure_path_exists(dir_to_create) return dir_to_create, not output_dir_exists def _run_hook_from_repo_dir( repo_dir, hook_name, project_dir, context, delete_project_on_failure ): """Run hook from repo directory, clean project directory if hook fails. :param repo_dir: Project template input directory. :param hook_name: The hook to execute. :param project_dir: The directory to execute the script from. :param context: Cookiecutter project context. :param delete_project_on_failure: Delete the project directory on hook failure? """ warnings.warn( "The '_run_hook_from_repo_dir' function is deprecated, " "use 'cookiecutter.hooks.run_hook_from_repo_dir' instead", DeprecationWarning, 2, ) run_hook_from_repo_dir( repo_dir, hook_name, project_dir, context, delete_project_on_failure ) def generate_files( repo_dir, context=None, output_dir='.', overwrite_if_exists=False, skip_if_file_exists=False, accept_hooks=True, keep_project_on_failure=False, ): """Render the templates and saves them to files. :param repo_dir: Project template input directory. :param context: Dict for populating the template's variables. :param output_dir: Where to output the generated project dir into. :param overwrite_if_exists: Overwrite the contents of the output directory if it exists. :param skip_if_file_exists: Skip the files in the corresponding directories if they already exist :param accept_hooks: Accept pre and post hooks if set to `True`. :param keep_project_on_failure: If `True` keep generated project directory even when generation fails """ context = context or OrderedDict([]) env = create_env_with_context(context) template_dir = find_template(repo_dir, env) logger.debug('Generating project from %s...', template_dir) unrendered_dir = os.path.split(template_dir)[1] try: project_dir, output_directory_created = render_and_create_dir( unrendered_dir, context, output_dir, env, overwrite_if_exists ) except UndefinedError as err: msg = f"Unable to create project directory '{unrendered_dir}'" raise UndefinedVariableInTemplate(msg, err, context) from err # We want the Jinja path and the OS paths to match. Consequently, we'll: # + CD to the template folder # + Set Jinja's path to '.' # # In order to build our files to the correct folder(s), we'll use an # absolute path for the target folder (project_dir) project_dir = os.path.abspath(project_dir) logger.debug('Project directory is %s', project_dir) # if we created the output directory, then it's ok to remove it # if rendering fails delete_project_on_failure = output_directory_created and not keep_project_on_failure if accept_hooks: run_hook_from_repo_dir( repo_dir, 'pre_gen_project', project_dir, context, delete_project_on_failure ) with work_in(template_dir): env.loader = FileSystemLoader(['.', '../templates']) for root, dirs, files in os.walk('.'): # We must separate the two types of dirs into different lists. # The reason is that we don't want ``os.walk`` to go through the # unrendered directories, since they will just be copied. copy_dirs = [] render_dirs = [] for d in dirs: d_ = os.path.normpath(os.path.join(root, d)) # We check the full path, because that's how it can be # specified in the ``_copy_without_render`` setting, but # we store just the dir name if is_copy_only_path(d_, context): logger.debug('Found copy only path %s', d) copy_dirs.append(d) else: render_dirs.append(d) for copy_dir in copy_dirs: indir = os.path.normpath(os.path.join(root, copy_dir)) outdir = os.path.normpath(os.path.join(project_dir, indir)) outdir = env.from_string(outdir).render(**context) logger.debug('Copying dir %s to %s without rendering', indir, outdir) # The outdir is not the root dir, it is the dir which marked as copy # only in the config file. If the program hits this line, which means # the overwrite_if_exists = True, and root dir exists if os.path.isdir(outdir): shutil.rmtree(outdir) shutil.copytree(indir, outdir) # We mutate ``dirs``, because we only want to go through these dirs # recursively dirs[:] = render_dirs for d in dirs: unrendered_dir = os.path.join(project_dir, root, d) try: render_and_create_dir( unrendered_dir, context, output_dir, env, overwrite_if_exists ) except UndefinedError as err: if delete_project_on_failure: rmtree(project_dir) _dir = os.path.relpath(unrendered_dir, output_dir) msg = f"Unable to create directory '{_dir}'" raise UndefinedVariableInTemplate(msg, err, context) from err for f in files: infile = os.path.normpath(os.path.join(root, f)) if is_copy_only_path(infile, context): outfile_tmpl = env.from_string(infile) outfile_rendered = outfile_tmpl.render(**context) outfile = os.path.join(project_dir, outfile_rendered) logger.debug( 'Copying file %s to %s without rendering', infile, outfile ) shutil.copyfile(infile, outfile) shutil.copymode(infile, outfile) continue try: generate_file( project_dir, infile, context, env, skip_if_file_exists ) except UndefinedError as err: if delete_project_on_failure: rmtree(project_dir) msg = f"Unable to create file '{infile}'" raise UndefinedVariableInTemplate(msg, err, context) from err if accept_hooks: run_hook_from_repo_dir( repo_dir, 'post_gen_project', project_dir, context, delete_project_on_failure, ) return project_dir cookiecutter-2.6.0/cookiecutter/hooks.py000066400000000000000000000136111456543333500204310ustar00rootroot00000000000000"""Functions for discovering and executing various cookiecutter hooks.""" import errno import logging import os import subprocess # nosec import sys import tempfile from pathlib import Path from jinja2.exceptions import UndefinedError from cookiecutter import utils from cookiecutter.exceptions import FailedHookException from cookiecutter.utils import ( create_env_with_context, create_tmp_repo_dir, rmtree, work_in, ) logger = logging.getLogger(__name__) _HOOKS = [ 'pre_prompt', 'pre_gen_project', 'post_gen_project', ] EXIT_SUCCESS = 0 def valid_hook(hook_file, hook_name): """Determine if a hook file is valid. :param hook_file: The hook file to consider for validity :param hook_name: The hook to find :return: The hook file validity """ filename = os.path.basename(hook_file) basename = os.path.splitext(filename)[0] matching_hook = basename == hook_name supported_hook = basename in _HOOKS backup_file = filename.endswith('~') return matching_hook and supported_hook and not backup_file def find_hook(hook_name, hooks_dir='hooks'): """Return a dict of all hook scripts provided. Must be called with the project template as the current working directory. Dict's key will be the hook/script's name, without extension, while values will be the absolute path to the script. Missing scripts will not be included in the returned dict. :param hook_name: The hook to find :param hooks_dir: The hook directory in the template :return: The absolute path to the hook script or None """ logger.debug('hooks_dir is %s', os.path.abspath(hooks_dir)) if not os.path.isdir(hooks_dir): logger.debug('No hooks/dir in template_dir') return None scripts = [] for hook_file in os.listdir(hooks_dir): if valid_hook(hook_file, hook_name): scripts.append(os.path.abspath(os.path.join(hooks_dir, hook_file))) if len(scripts) == 0: return None return scripts def run_script(script_path, cwd='.'): """Execute a script from a working directory. :param script_path: Absolute path to the script to run. :param cwd: The directory to run the script from. """ run_thru_shell = sys.platform.startswith('win') if script_path.endswith('.py'): script_command = [sys.executable, script_path] else: script_command = [script_path] utils.make_executable(script_path) try: proc = subprocess.Popen(script_command, shell=run_thru_shell, cwd=cwd) # nosec exit_status = proc.wait() if exit_status != EXIT_SUCCESS: raise FailedHookException( f'Hook script failed (exit status: {exit_status})' ) except OSError as err: if err.errno == errno.ENOEXEC: raise FailedHookException( 'Hook script failed, might be an empty file or missing a shebang' ) from err raise FailedHookException(f'Hook script failed (error: {err})') from err def run_script_with_context(script_path, cwd, context): """Execute a script after rendering it with Jinja. :param script_path: Absolute path to the script to run. :param cwd: The directory to run the script from. :param context: Cookiecutter project template context. """ _, extension = os.path.splitext(script_path) with open(script_path, encoding='utf-8') as file: contents = file.read() with tempfile.NamedTemporaryFile(delete=False, mode='wb', suffix=extension) as temp: env = create_env_with_context(context) template = env.from_string(contents) output = template.render(**context) temp.write(output.encode('utf-8')) run_script(temp.name, cwd) def run_hook(hook_name, project_dir, context): """ Try to find and execute a hook from the specified project directory. :param hook_name: The hook to execute. :param project_dir: The directory to execute the script from. :param context: Cookiecutter project context. """ scripts = find_hook(hook_name) if not scripts: logger.debug('No %s hook found', hook_name) return logger.debug('Running hook %s', hook_name) for script in scripts: run_script_with_context(script, project_dir, context) def run_hook_from_repo_dir( repo_dir, hook_name, project_dir, context, delete_project_on_failure ): """Run hook from repo directory, clean project directory if hook fails. :param repo_dir: Project template input directory. :param hook_name: The hook to execute. :param project_dir: The directory to execute the script from. :param context: Cookiecutter project context. :param delete_project_on_failure: Delete the project directory on hook failure? """ with work_in(repo_dir): try: run_hook(hook_name, project_dir, context) except ( FailedHookException, UndefinedError, ): if delete_project_on_failure: rmtree(project_dir) logger.error( "Stopping generation because %s hook " "script didn't exit successfully", hook_name, ) raise def run_pre_prompt_hook(repo_dir: "os.PathLike[str]") -> Path: """Run pre_prompt hook from repo directory. :param repo_dir: Project template input directory. """ # Check if we have a valid pre_prompt script with work_in(repo_dir): scripts = find_hook('pre_prompt') if not scripts: return repo_dir # Create a temporary directory repo_dir = create_tmp_repo_dir(repo_dir) with work_in(repo_dir): scripts = find_hook('pre_prompt') for script in scripts: try: run_script(script, repo_dir) except FailedHookException: raise FailedHookException('Pre-Prompt Hook script failed') return repo_dir cookiecutter-2.6.0/cookiecutter/log.py000066400000000000000000000030411456543333500200630ustar00rootroot00000000000000"""Module for setting up logging.""" import logging import sys LOG_LEVELS = { 'DEBUG': logging.DEBUG, 'INFO': logging.INFO, 'WARNING': logging.WARNING, 'ERROR': logging.ERROR, 'CRITICAL': logging.CRITICAL, } LOG_FORMATS = { 'DEBUG': '%(levelname)s %(name)s: %(message)s', 'INFO': '%(levelname)s: %(message)s', } def configure_logger(stream_level='DEBUG', debug_file=None): """Configure logging for cookiecutter. Set up logging to stdout with given level. If ``debug_file`` is given set up logging to file with DEBUG level. """ # Set up 'cookiecutter' logger logger = logging.getLogger('cookiecutter') logger.setLevel(logging.DEBUG) # Remove all attached handlers, in case there was # a logger with using the name 'cookiecutter' del logger.handlers[:] # Create a file handler if a log file is provided if debug_file is not None: debug_formatter = logging.Formatter(LOG_FORMATS['DEBUG']) file_handler = logging.FileHandler(debug_file) file_handler.setLevel(LOG_LEVELS['DEBUG']) file_handler.setFormatter(debug_formatter) logger.addHandler(file_handler) # Get settings based on the given stream_level log_formatter = logging.Formatter(LOG_FORMATS[stream_level]) log_level = LOG_LEVELS[stream_level] # Create a stream handler stream_handler = logging.StreamHandler(stream=sys.stdout) stream_handler.setLevel(log_level) stream_handler.setFormatter(log_formatter) logger.addHandler(stream_handler) return logger cookiecutter-2.6.0/cookiecutter/main.py000066400000000000000000000172001456543333500202300ustar00rootroot00000000000000""" Main entry point for the `cookiecutter` command. The code in this module is also a good example of how to use Cookiecutter as a library rather than a script. """ import logging import os import sys from copy import copy from pathlib import Path from cookiecutter.config import get_user_config from cookiecutter.exceptions import InvalidModeException from cookiecutter.generate import generate_context, generate_files from cookiecutter.hooks import run_pre_prompt_hook from cookiecutter.prompt import choose_nested_template, prompt_for_config from cookiecutter.replay import dump, load from cookiecutter.repository import determine_repo_dir from cookiecutter.utils import rmtree logger = logging.getLogger(__name__) def cookiecutter( template, checkout=None, no_input=False, extra_context=None, replay=None, overwrite_if_exists=False, output_dir='.', config_file=None, default_config=False, password=None, directory=None, skip_if_file_exists=False, accept_hooks=True, keep_project_on_failure=False, ): """ Run Cookiecutter just as if using it from the command line. :param template: A directory containing a project template directory, or a URL to a git repository. :param checkout: The branch, tag or commit ID to checkout after clone. :param no_input: Do not prompt for user input. Use default values for template parameters taken from `cookiecutter.json`, user config and `extra_dict`. Force a refresh of cached resources. :param extra_context: A dictionary of context that overrides default and user configuration. :param replay: Do not prompt for input, instead read from saved json. If ``True`` read from the ``replay_dir``. if it exists :param overwrite_if_exists: Overwrite the contents of the output directory if it exists. :param output_dir: Where to output the generated project dir into. :param config_file: User configuration file path. :param default_config: Use default values rather than a config file. :param password: The password to use when extracting the repository. :param directory: Relative path to a cookiecutter template in a repository. :param skip_if_file_exists: Skip the files in the corresponding directories if they already exist. :param accept_hooks: Accept pre and post hooks if set to `True`. :param keep_project_on_failure: If `True` keep generated project directory even when generation fails """ if replay and ((no_input is not False) or (extra_context is not None)): err_msg = ( "You can not use both replay and no_input or extra_context " "at the same time." ) raise InvalidModeException(err_msg) config_dict = get_user_config( config_file=config_file, default_config=default_config, ) base_repo_dir, cleanup_base_repo_dir = determine_repo_dir( template=template, abbreviations=config_dict['abbreviations'], clone_to_dir=config_dict['cookiecutters_dir'], checkout=checkout, no_input=no_input, password=password, directory=directory, ) repo_dir, cleanup = base_repo_dir, cleanup_base_repo_dir # Run pre_prompt hook repo_dir = run_pre_prompt_hook(base_repo_dir) if accept_hooks else repo_dir # Always remove temporary dir if it was created cleanup = True if repo_dir != base_repo_dir else False import_patch = _patch_import_path_for_repo(repo_dir) template_name = os.path.basename(os.path.abspath(repo_dir)) if replay: with import_patch: if isinstance(replay, bool): context_from_replayfile = load(config_dict['replay_dir'], template_name) else: path, template_name = os.path.split(os.path.splitext(replay)[0]) context_from_replayfile = load(path, template_name) context_file = os.path.join(repo_dir, 'cookiecutter.json') logger.debug('context_file is %s', context_file) if replay: context = generate_context( context_file=context_file, default_context=config_dict['default_context'], extra_context=None, ) logger.debug('replayfile context: %s', context_from_replayfile) items_for_prompting = { k: v for k, v in context['cookiecutter'].items() if k not in context_from_replayfile['cookiecutter'].keys() } context_for_prompting = {} context_for_prompting['cookiecutter'] = items_for_prompting context = context_from_replayfile logger.debug('prompting context: %s', context_for_prompting) else: context = generate_context( context_file=context_file, default_context=config_dict['default_context'], extra_context=extra_context, ) context_for_prompting = context # preserve the original cookiecutter options # print(context['cookiecutter']) context['_cookiecutter'] = { k: v for k, v in context['cookiecutter'].items() if not k.startswith("_") } # prompt the user to manually configure at the command line. # except when 'no-input' flag is set with import_patch: if {"template", "templates"} & set(context["cookiecutter"].keys()): nested_template = choose_nested_template(context, repo_dir, no_input) return cookiecutter( template=nested_template, checkout=checkout, no_input=no_input, extra_context=extra_context, replay=replay, overwrite_if_exists=overwrite_if_exists, output_dir=output_dir, config_file=config_file, default_config=default_config, password=password, directory=directory, skip_if_file_exists=skip_if_file_exists, accept_hooks=accept_hooks, keep_project_on_failure=keep_project_on_failure, ) if context_for_prompting['cookiecutter']: context['cookiecutter'].update( prompt_for_config(context_for_prompting, no_input) ) logger.debug('context is %s', context) # include template dir or url in the context dict context['cookiecutter']['_template'] = template # include output+dir in the context dict context['cookiecutter']['_output_dir'] = os.path.abspath(output_dir) # include repo dir or url in the context dict context['cookiecutter']['_repo_dir'] = f"{repo_dir}" # include checkout details in the context dict context['cookiecutter']['_checkout'] = checkout dump(config_dict['replay_dir'], template_name, context) # Create project from local context and project template. with import_patch: result = generate_files( repo_dir=repo_dir, context=context, overwrite_if_exists=overwrite_if_exists, skip_if_file_exists=skip_if_file_exists, output_dir=output_dir, accept_hooks=accept_hooks, keep_project_on_failure=keep_project_on_failure, ) # Cleanup (if required) if cleanup: rmtree(repo_dir) if cleanup_base_repo_dir: rmtree(base_repo_dir) return result class _patch_import_path_for_repo: def __init__(self, repo_dir: "os.PathLike[str]"): self._repo_dir = f"{repo_dir}" if isinstance(repo_dir, Path) else repo_dir self._path = None def __enter__(self): self._path = copy(sys.path) sys.path.append(self._repo_dir) def __exit__(self, type, value, traceback): sys.path = self._path cookiecutter-2.6.0/cookiecutter/prompt.py000066400000000000000000000335431456543333500206350ustar00rootroot00000000000000"""Functions for prompting the user for project info.""" import json import os import re import sys from collections import OrderedDict from pathlib import Path from jinja2.exceptions import UndefinedError from rich.prompt import Confirm, InvalidResponse, Prompt, PromptBase from cookiecutter.exceptions import UndefinedVariableInTemplate from cookiecutter.utils import create_env_with_context, rmtree def read_user_variable(var_name, default_value, prompts=None, prefix=""): """Prompt user for variable and return the entered value or given default. :param str var_name: Variable of the context to query the user :param default_value: Value that will be returned if no input happens """ question = ( prompts[var_name] if prompts and var_name in prompts.keys() and prompts[var_name] else var_name ) while True: variable = Prompt.ask(f"{prefix}{question}", default=default_value) if variable is not None: break return variable class YesNoPrompt(Confirm): """A prompt that returns a boolean for yes/no questions.""" yes_choices = ["1", "true", "t", "yes", "y", "on"] no_choices = ["0", "false", "f", "no", "n", "off"] def process_response(self, value: str) -> bool: """Convert choices to a bool.""" value = value.strip().lower() if value in self.yes_choices: return True elif value in self.no_choices: return False else: raise InvalidResponse(self.validate_error_message) def read_user_yes_no(var_name, default_value, prompts=None, prefix=""): """Prompt the user to reply with 'yes' or 'no' (or equivalent values). - These input values will be converted to ``True``: "1", "true", "t", "yes", "y", "on" - These input values will be converted to ``False``: "0", "false", "f", "no", "n", "off" Actual parsing done by :func:`prompt`; Check this function codebase change in case of unexpected behaviour. :param str question: Question to the user :param default_value: Value that will be returned if no input happens """ question = ( prompts[var_name] if prompts and var_name in prompts.keys() and prompts[var_name] else var_name ) return YesNoPrompt.ask(f"{prefix}{question}", default=default_value) def read_repo_password(question): """Prompt the user to enter a password. :param str question: Question to the user """ return Prompt.ask(question, password=True) def read_user_choice(var_name, options, prompts=None, prefix=""): """Prompt the user to choose from several options for the given variable. The first item will be returned if no input happens. :param str var_name: Variable as specified in the context :param list options: Sequence of options that are available to select from :return: Exactly one item of ``options`` that has been chosen by the user """ if not isinstance(options, list): raise TypeError if not options: raise ValueError choice_map = OrderedDict((f'{i}', value) for i, value in enumerate(options, 1)) choices = choice_map.keys() question = f"Select {var_name}" choice_lines = [ ' [bold magenta]{}[/] - [bold]{}[/]'.format(*c) for c in choice_map.items() ] # Handle if human-readable prompt is provided if prompts and var_name in prompts.keys(): if isinstance(prompts[var_name], str): question = prompts[var_name] else: if "__prompt__" in prompts[var_name]: question = prompts[var_name]["__prompt__"] choice_lines = [ ( f" [bold magenta]{i}[/] - [bold]{prompts[var_name][p]}[/]" if p in prompts[var_name] else f" [bold magenta]{i}[/] - [bold]{p}[/]" ) for i, p in choice_map.items() ] prompt = '\n'.join( ( f"{prefix}{question}", "\n".join(choice_lines), " Choose from", ) ) user_choice = Prompt.ask(prompt, choices=list(choices), default=list(choices)[0]) return choice_map[user_choice] DEFAULT_DISPLAY = 'default' def process_json(user_value, default_value=None): """Load user-supplied value as a JSON dict. :param str user_value: User-supplied value to load as a JSON dict """ try: user_dict = json.loads(user_value, object_pairs_hook=OrderedDict) except Exception as error: # Leave it up to click to ask the user again raise InvalidResponse('Unable to decode to JSON.') from error if not isinstance(user_dict, dict): # Leave it up to click to ask the user again raise InvalidResponse('Requires JSON dict.') return user_dict class JsonPrompt(PromptBase[dict]): """A prompt that returns a dict from JSON string.""" default = None response_type = dict validate_error_message = "[prompt.invalid] Please enter a valid JSON string" def process_response(self, value: str) -> dict: """Convert choices to a dict.""" return process_json(value, self.default) def read_user_dict(var_name, default_value, prompts=None, prefix=""): """Prompt the user to provide a dictionary of data. :param str var_name: Variable as specified in the context :param default_value: Value that will be returned if no input is provided :return: A Python dictionary to use in the context. """ if not isinstance(default_value, dict): raise TypeError question = ( prompts[var_name] if prompts and var_name in prompts.keys() and prompts[var_name] else var_name ) user_value = JsonPrompt.ask( f"{prefix}{question} [cyan bold]({DEFAULT_DISPLAY})[/]", default=default_value, show_default=False, ) return user_value def render_variable(env, raw, cookiecutter_dict): """Render the next variable to be displayed in the user prompt. Inside the prompting taken from the cookiecutter.json file, this renders the next variable. For example, if a project_name is "Peanut Butter Cookie", the repo_name could be be rendered with: `{{ cookiecutter.project_name.replace(" ", "_") }}`. This is then presented to the user as the default. :param Environment env: A Jinja2 Environment object. :param raw: The next value to be prompted for by the user. :param dict cookiecutter_dict: The current context as it's gradually being populated with variables. :return: The rendered value for the default variable. """ if raw is None or isinstance(raw, bool): return raw elif isinstance(raw, dict): return { render_variable(env, k, cookiecutter_dict): render_variable( env, v, cookiecutter_dict ) for k, v in raw.items() } elif isinstance(raw, list): return [render_variable(env, v, cookiecutter_dict) for v in raw] elif not isinstance(raw, str): raw = str(raw) template = env.from_string(raw) return template.render(cookiecutter=cookiecutter_dict) def _prompts_from_options(options: dict) -> dict: """Process template options and return friendly prompt information.""" prompts = {"__prompt__": "Select a template"} for option_key, option_value in options.items(): title = str(option_value.get("title", option_key)) description = option_value.get("description", option_key) label = title if title == description else f"{title} ({description})" prompts[option_key] = label return prompts def prompt_choice_for_template(key, options, no_input): """Prompt user with a set of options to choose from. :param no_input: Do not prompt for user input and return the first available option. """ opts = list(options.keys()) prompts = {"templates": _prompts_from_options(options)} return opts[0] if no_input else read_user_choice(key, opts, prompts, "") def prompt_choice_for_config( cookiecutter_dict, env, key, options, no_input, prompts=None, prefix="" ): """Prompt user with a set of options to choose from. :param no_input: Do not prompt for user input and return the first available option. """ rendered_options = [render_variable(env, raw, cookiecutter_dict) for raw in options] if no_input: return rendered_options[0] return read_user_choice(key, rendered_options, prompts, prefix) def prompt_for_config(context, no_input=False): """Prompt user to enter a new config. :param dict context: Source for field names and sample values. :param no_input: Do not prompt for user input and use only values from context. """ cookiecutter_dict = OrderedDict([]) env = create_env_with_context(context) prompts = context['cookiecutter'].pop('__prompts__', {}) # First pass: Handle simple and raw variables, plus choices. # These must be done first because the dictionaries keys and # values might refer to them. count = 0 all_prompts = context['cookiecutter'].items() visible_prompts = [k for k, _ in all_prompts if not k.startswith("_")] size = len(visible_prompts) for key, raw in all_prompts: if key.startswith('_') and not key.startswith('__'): cookiecutter_dict[key] = raw continue elif key.startswith('__'): cookiecutter_dict[key] = render_variable(env, raw, cookiecutter_dict) continue if not isinstance(raw, dict): count += 1 prefix = f" [dim][{count}/{size}][/] " try: if isinstance(raw, list): # We are dealing with a choice variable val = prompt_choice_for_config( cookiecutter_dict, env, key, raw, no_input, prompts, prefix ) cookiecutter_dict[key] = val elif isinstance(raw, bool): # We are dealing with a boolean variable if no_input: cookiecutter_dict[key] = render_variable( env, raw, cookiecutter_dict ) else: cookiecutter_dict[key] = read_user_yes_no(key, raw, prompts, prefix) elif not isinstance(raw, dict): # We are dealing with a regular variable val = render_variable(env, raw, cookiecutter_dict) if not no_input: val = read_user_variable(key, val, prompts, prefix) cookiecutter_dict[key] = val except UndefinedError as err: msg = f"Unable to render variable '{key}'" raise UndefinedVariableInTemplate(msg, err, context) from err # Second pass; handle the dictionaries. for key, raw in context['cookiecutter'].items(): # Skip private type dicts not to be rendered. if key.startswith('_') and not key.startswith('__'): continue try: if isinstance(raw, dict): # We are dealing with a dict variable count += 1 prefix = f" [dim][{count}/{size}][/] " val = render_variable(env, raw, cookiecutter_dict) if not no_input and not key.startswith('__'): val = read_user_dict(key, val, prompts, prefix) cookiecutter_dict[key] = val except UndefinedError as err: msg = f"Unable to render variable '{key}'" raise UndefinedVariableInTemplate(msg, err, context) from err return cookiecutter_dict def choose_nested_template(context: dict, repo_dir: str, no_input: bool = False) -> str: """Prompt user to select the nested template to use. :param context: Source for field names and sample values. :param repo_dir: Repository directory. :param no_input: Do not prompt for user input and use only values from context. :returns: Path to the selected template. """ cookiecutter_dict = OrderedDict([]) env = create_env_with_context(context) prefix = "" prompts = context['cookiecutter'].pop('__prompts__', {}) key = "templates" config = context['cookiecutter'].get(key, {}) if config: # Pass val = prompt_choice_for_template(key, config, no_input) template = config[val]["path"] else: # Old style key = "template" config = context['cookiecutter'].get(key, []) val = prompt_choice_for_config( cookiecutter_dict, env, key, config, no_input, prompts, prefix ) template = re.search(r'\((.+)\)', val).group(1) template = Path(template) if template else None if not (template and not template.is_absolute()): raise ValueError("Illegal template path") repo_dir = Path(repo_dir).resolve() template_path = (repo_dir / template).resolve() # Return path as string return f"{template_path}" def prompt_and_delete(path, no_input=False): """ Ask user if it's okay to delete the previously-downloaded file/directory. If yes, delete it. If no, checks to see if the old version should be reused. If yes, it's reused; otherwise, Cookiecutter exits. :param path: Previously downloaded zipfile. :param no_input: Suppress prompt to delete repo and just delete it. :return: True if the content was deleted """ # Suppress prompt if called via API if no_input: ok_to_delete = True else: question = ( f"You've downloaded {path} before. Is it okay to delete and re-download it?" ) ok_to_delete = read_user_yes_no(question, 'yes') if ok_to_delete: if os.path.isdir(path): rmtree(path) else: os.remove(path) return True else: ok_to_reuse = read_user_yes_no( "Do you want to re-use the existing version?", 'yes' ) if ok_to_reuse: return False sys.exit() cookiecutter-2.6.0/cookiecutter/replay.py000066400000000000000000000027361456543333500206100ustar00rootroot00000000000000""" cookiecutter.replay. ------------------- """ import json import os from cookiecutter.utils import make_sure_path_exists def get_file_name(replay_dir, template_name): """Get the name of file.""" suffix = '.json' if not template_name.endswith('.json') else '' file_name = f'{template_name}{suffix}' return os.path.join(replay_dir, file_name) def dump(replay_dir: "os.PathLike[str]", template_name: str, context: dict): """Write json data to file.""" make_sure_path_exists(replay_dir) if not isinstance(template_name, str): raise TypeError('Template name is required to be of type str') if not isinstance(context, dict): raise TypeError('Context is required to be of type dict') if 'cookiecutter' not in context: raise ValueError('Context is required to contain a cookiecutter key') replay_file = get_file_name(replay_dir, template_name) with open(replay_file, 'w', encoding="utf-8") as outfile: json.dump(context, outfile, indent=2) def load(replay_dir, template_name): """Read json data from file.""" if not isinstance(template_name, str): raise TypeError('Template name is required to be of type str') replay_file = get_file_name(replay_dir, template_name) with open(replay_file, encoding="utf-8") as infile: context = json.load(infile) if 'cookiecutter' not in context: raise ValueError('Context is required to contain a cookiecutter key') return context cookiecutter-2.6.0/cookiecutter/repository.py000066400000000000000000000102161456543333500215230ustar00rootroot00000000000000"""Cookiecutter repository functions.""" import os import re from cookiecutter.exceptions import RepositoryNotFound from cookiecutter.vcs import clone from cookiecutter.zipfile import unzip REPO_REGEX = re.compile( r""" # something like git:// ssh:// file:// etc. ((((git|hg)\+)?(git|ssh|file|https?):(//)?) | # or (\w+@[\w\.]+) # something like user@... ) """, re.VERBOSE, ) def is_repo_url(value): """Return True if value is a repository URL.""" return bool(REPO_REGEX.match(value)) def is_zip_file(value): """Return True if value is a zip file.""" return value.lower().endswith('.zip') def expand_abbreviations(template, abbreviations): """Expand abbreviations in a template name. :param template: The project template name. :param abbreviations: Abbreviation definitions. """ if template in abbreviations: return abbreviations[template] # Split on colon. If there is no colon, rest will be empty # and prefix will be the whole template prefix, sep, rest = template.partition(':') if prefix in abbreviations: return abbreviations[prefix].format(rest) return template def repository_has_cookiecutter_json(repo_directory): """Determine if `repo_directory` contains a `cookiecutter.json` file. :param repo_directory: The candidate repository directory. :return: True if the `repo_directory` is valid, else False. """ repo_directory_exists = os.path.isdir(repo_directory) repo_config_exists = os.path.isfile( os.path.join(repo_directory, 'cookiecutter.json') ) return repo_directory_exists and repo_config_exists def determine_repo_dir( template, abbreviations, clone_to_dir, checkout, no_input, password=None, directory=None, ): """ Locate the repository directory from a template reference. Applies repository abbreviations to the template reference. If the template refers to a repository URL, clone it. If the template is a path to a local repository, use it. :param template: A directory containing a project template directory, or a URL to a git repository. :param abbreviations: A dictionary of repository abbreviation definitions. :param clone_to_dir: The directory to clone the repository into. :param checkout: The branch, tag or commit ID to checkout after clone. :param no_input: Do not prompt for user input and eventually force a refresh of cached resources. :param password: The password to use when extracting the repository. :param directory: Directory within repo where cookiecutter.json lives. :return: A tuple containing the cookiecutter template directory, and a boolean describing whether that directory should be cleaned up after the template has been instantiated. :raises: `RepositoryNotFound` if a repository directory could not be found. """ template = expand_abbreviations(template, abbreviations) if is_zip_file(template): unzipped_dir = unzip( zip_uri=template, is_url=is_repo_url(template), clone_to_dir=clone_to_dir, no_input=no_input, password=password, ) repository_candidates = [unzipped_dir] cleanup = True elif is_repo_url(template): cloned_repo = clone( repo_url=template, checkout=checkout, clone_to_dir=clone_to_dir, no_input=no_input, ) repository_candidates = [cloned_repo] cleanup = False else: repository_candidates = [template, os.path.join(clone_to_dir, template)] cleanup = False if directory: repository_candidates = [ os.path.join(s, directory) for s in repository_candidates ] for repo_candidate in repository_candidates: if repository_has_cookiecutter_json(repo_candidate): return repo_candidate, cleanup raise RepositoryNotFound( 'A valid repository for "{}" could not be found in the following ' 'locations:\n{}'.format(template, '\n'.join(repository_candidates)) ) cookiecutter-2.6.0/cookiecutter/utils.py000066400000000000000000000054231456543333500204500ustar00rootroot00000000000000"""Helper functions used throughout Cookiecutter.""" import contextlib import logging import os import shutil import stat import tempfile from pathlib import Path from typing import Dict from jinja2.ext import Extension from cookiecutter.environment import StrictEnvironment logger = logging.getLogger(__name__) def force_delete(func, path, exc_info): """Error handler for `shutil.rmtree()` equivalent to `rm -rf`. Usage: `shutil.rmtree(path, onerror=force_delete)` From https://docs.python.org/3/library/shutil.html#rmtree-example """ os.chmod(path, stat.S_IWRITE) func(path) def rmtree(path): """Remove a directory and all its contents. Like rm -rf on Unix. :param path: A directory path. """ shutil.rmtree(path, onerror=force_delete) def make_sure_path_exists(path: "os.PathLike[str]") -> None: """Ensure that a directory exists. :param path: A directory tree path for creation. """ logger.debug('Making sure path exists (creates tree if not exist): %s', path) try: Path(path).mkdir(parents=True, exist_ok=True) except OSError as error: raise OSError(f'Unable to create directory at {path}') from error @contextlib.contextmanager def work_in(dirname=None): """Context manager version of os.chdir. When exited, returns to the working directory prior to entering. """ curdir = os.getcwd() try: if dirname is not None: os.chdir(dirname) yield finally: os.chdir(curdir) def make_executable(script_path): """Make `script_path` executable. :param script_path: The file to change """ status = os.stat(script_path) os.chmod(script_path, status.st_mode | stat.S_IEXEC) def simple_filter(filter_function): """Decorate a function to wrap it in a simplified jinja2 extension.""" class SimpleFilterExtension(Extension): def __init__(self, environment): super().__init__(environment) environment.filters[filter_function.__name__] = filter_function SimpleFilterExtension.__name__ = filter_function.__name__ return SimpleFilterExtension def create_tmp_repo_dir(repo_dir: "os.PathLike[str]") -> Path: """Create a temporary dir with a copy of the contents of repo_dir.""" repo_dir = Path(repo_dir).resolve() base_dir = tempfile.mkdtemp(prefix='cookiecutter') new_dir = f"{base_dir}/{repo_dir.name}" logger.debug(f'Copying repo_dir from {repo_dir} to {new_dir}') shutil.copytree(repo_dir, new_dir) return Path(new_dir) def create_env_with_context(context: Dict): """Create a jinja environment using the provided context.""" envvars = context.get('cookiecutter', {}).get('_jinja2_env_vars', {}) return StrictEnvironment(context=context, keep_trailing_newline=True, **envvars) cookiecutter-2.6.0/cookiecutter/vcs.py000066400000000000000000000104761456543333500201070ustar00rootroot00000000000000"""Helper functions for working with version control systems.""" import logging import os import subprocess # nosec from pathlib import Path from shutil import which from typing import Optional from cookiecutter.exceptions import ( RepositoryCloneFailed, RepositoryNotFound, UnknownRepoType, VCSNotInstalled, ) from cookiecutter.prompt import prompt_and_delete from cookiecutter.utils import make_sure_path_exists logger = logging.getLogger(__name__) BRANCH_ERRORS = [ 'error: pathspec', 'unknown revision', ] def identify_repo(repo_url): """Determine if `repo_url` should be treated as a URL to a git or hg repo. Repos can be identified by prepending "hg+" or "git+" to the repo URL. :param repo_url: Repo URL of unknown type. :returns: ('git', repo_url), ('hg', repo_url), or None. """ repo_url_values = repo_url.split('+') if len(repo_url_values) == 2: repo_type = repo_url_values[0] if repo_type in ["git", "hg"]: return repo_type, repo_url_values[1] else: raise UnknownRepoType else: if 'git' in repo_url: return 'git', repo_url elif 'bitbucket' in repo_url: return 'hg', repo_url else: raise UnknownRepoType def is_vcs_installed(repo_type): """ Check if the version control system for a repo type is installed. :param repo_type: """ return bool(which(repo_type)) def clone( repo_url: str, checkout: Optional[str] = None, clone_to_dir: "os.PathLike[str]" = ".", no_input: bool = False, ): """Clone a repo to the current directory. :param repo_url: Repo URL of unknown type. :param checkout: The branch, tag or commit ID to checkout after clone. :param clone_to_dir: The directory to clone to. Defaults to the current directory. :param no_input: Do not prompt for user input and eventually force a refresh of cached resources. :returns: str with path to the new directory of the repository. """ # Ensure that clone_to_dir exists clone_to_dir = Path(clone_to_dir).expanduser() make_sure_path_exists(clone_to_dir) # identify the repo_type repo_type, repo_url = identify_repo(repo_url) # check that the appropriate VCS for the repo_type is installed if not is_vcs_installed(repo_type): msg = f"'{repo_type}' is not installed." raise VCSNotInstalled(msg) repo_url = repo_url.rstrip('/') repo_name = os.path.split(repo_url)[1] if repo_type == 'git': repo_name = repo_name.split(':')[-1].rsplit('.git')[0] repo_dir = os.path.normpath(os.path.join(clone_to_dir, repo_name)) if repo_type == 'hg': repo_dir = os.path.normpath(os.path.join(clone_to_dir, repo_name)) logger.debug(f'repo_dir is {repo_dir}') if os.path.isdir(repo_dir): clone = prompt_and_delete(repo_dir, no_input=no_input) else: clone = True if clone: try: subprocess.check_output( # nosec [repo_type, 'clone', repo_url], cwd=clone_to_dir, stderr=subprocess.STDOUT, ) if checkout is not None: checkout_params = [checkout] # Avoid Mercurial "--config" and "--debugger" injection vulnerability if repo_type == "hg": checkout_params.insert(0, "--") subprocess.check_output( # nosec [repo_type, 'checkout', *checkout_params], cwd=repo_dir, stderr=subprocess.STDOUT, ) except subprocess.CalledProcessError as clone_error: output = clone_error.output.decode('utf-8') if 'not found' in output.lower(): raise RepositoryNotFound( f'The repository {repo_url} could not be found, ' 'have you made a typo?' ) from clone_error if any(error in output for error in BRANCH_ERRORS): raise RepositoryCloneFailed( f'The {checkout} branch of repository ' f'{repo_url} could not found, have you made a typo?' ) from clone_error logger.error('git clone failed with error: %s', output) raise return repo_dir cookiecutter-2.6.0/cookiecutter/zipfile.py000066400000000000000000000105441456543333500207520ustar00rootroot00000000000000"""Utility functions for handling and fetching repo archives in zip format.""" import os import tempfile from pathlib import Path from typing import Optional from zipfile import BadZipFile, ZipFile import requests from cookiecutter.exceptions import InvalidZipRepository from cookiecutter.prompt import prompt_and_delete, read_repo_password from cookiecutter.utils import make_sure_path_exists def unzip( zip_uri: str, is_url: bool, clone_to_dir: "os.PathLike[str]" = ".", no_input: bool = False, password: Optional[str] = None, ): """Download and unpack a zipfile at a given URI. This will download the zipfile to the cookiecutter repository, and unpack into a temporary directory. :param zip_uri: The URI for the zipfile. :param is_url: Is the zip URI a URL or a file? :param clone_to_dir: The cookiecutter repository directory to put the archive into. :param no_input: Do not prompt for user input and eventually force a refresh of cached resources. :param password: The password to use when unpacking the repository. """ # Ensure that clone_to_dir exists clone_to_dir = Path(clone_to_dir).expanduser() make_sure_path_exists(clone_to_dir) if is_url: # Build the name of the cached zipfile, # and prompt to delete if it already exists. identifier = zip_uri.rsplit('/', 1)[1] zip_path = os.path.join(clone_to_dir, identifier) if os.path.exists(zip_path): download = prompt_and_delete(zip_path, no_input=no_input) else: download = True if download: # (Re) download the zipfile r = requests.get(zip_uri, stream=True, timeout=100) with open(zip_path, 'wb') as f: for chunk in r.iter_content(chunk_size=1024): if chunk: # filter out keep-alive new chunks f.write(chunk) else: # Just use the local zipfile as-is. zip_path = os.path.abspath(zip_uri) # Now unpack the repository. The zipfile will be unpacked # into a temporary directory try: zip_file = ZipFile(zip_path) if len(zip_file.namelist()) == 0: raise InvalidZipRepository(f'Zip repository {zip_uri} is empty') # The first record in the zipfile should be the directory entry for # the archive. If it isn't a directory, there's a problem. first_filename = zip_file.namelist()[0] if not first_filename.endswith('/'): raise InvalidZipRepository( f"Zip repository {zip_uri} does not include a top-level directory" ) # Construct the final target directory project_name = first_filename[:-1] unzip_base = tempfile.mkdtemp() unzip_path = os.path.join(unzip_base, project_name) # Extract the zip file into the temporary directory try: zip_file.extractall(path=unzip_base) except RuntimeError: # File is password protected; try to get a password from the # environment; if that doesn't work, ask the user. if password is not None: try: zip_file.extractall(path=unzip_base, pwd=password.encode('utf-8')) except RuntimeError: raise InvalidZipRepository( 'Invalid password provided for protected repository' ) elif no_input: raise InvalidZipRepository( 'Unable to unlock password protected repository' ) else: retry = 0 while retry is not None: try: password = read_repo_password('Repo password') zip_file.extractall( path=unzip_base, pwd=password.encode('utf-8') ) retry = None except RuntimeError: retry += 1 if retry == 3: raise InvalidZipRepository( 'Invalid password provided for protected repository' ) except BadZipFile: raise InvalidZipRepository( f'Zip repository {zip_uri} is not a valid zip archive:' ) return unzip_path cookiecutter-2.6.0/docs/000077500000000000000000000000001456543333500151625ustar00rootroot00000000000000cookiecutter-2.6.0/docs/AUTHORS.md000077700000000000000000000000001456543333500205062../AUTHORS.mdustar00rootroot00000000000000cookiecutter-2.6.0/docs/CODE_OF_CONDUCT.md000077700000000000000000000000001456543333500227662../CODE_OF_CONDUCT.mdustar00rootroot00000000000000cookiecutter-2.6.0/docs/CONTRIBUTING.md000077700000000000000000000000001456543333500220522../CONTRIBUTING.mdustar00rootroot00000000000000cookiecutter-2.6.0/docs/HISTORY.md000077700000000000000000000000001456543333500205362../HISTORY.mdustar00rootroot00000000000000cookiecutter-2.6.0/docs/README.md000077700000000000000000000000001456543333500201262../README.mdustar00rootroot00000000000000cookiecutter-2.6.0/docs/__init__.py000066400000000000000000000000351456543333500172710ustar00rootroot00000000000000"""Main package for docs.""" cookiecutter-2.6.0/docs/_templates/000077500000000000000000000000001456543333500173175ustar00rootroot00000000000000cookiecutter-2.6.0/docs/_templates/package.rst_t000066400000000000000000000017441456543333500217750ustar00rootroot00000000000000{%- macro automodule(modname, options) -%} .. automodule:: {{ modname }} {%- for option in options %} :{{ option }}: {%- endfor %} {%- endmacro %} {%- macro toctree(docnames) -%} .. toctree:: :maxdepth: {{ maxdepth }} {% for docname in docnames %} {{ docname }} {%- endfor %} {%- endmacro -%} === API === {%- if modulefirst and not is_namespace %} {{ automodule(pkgname, automodule_options) }} {% endif %} {%- if subpackages %} Subpackages ----------- {{ toctree(subpackages) }} {% endif %} {%- if submodules %} This is the Cookiecutter modules API documentation. {% if separatemodules %} {{ toctree(submodules) }} {% else %} {%- for submodule in submodules %} {% if show_headings %} {{- [submodule, "module"] | join(" ") | e | heading(2) }} {% endif %} {{ automodule(submodule, automodule_options) }} {% endfor %} {%- endif %} {%- endif %} {%- if not modulefirst and not is_namespace %} Module contents --------------- {{ automodule(pkgname, automodule_options) }} {% endif %} cookiecutter-2.6.0/docs/advanced/000077500000000000000000000000001456543333500167275ustar00rootroot00000000000000cookiecutter-2.6.0/docs/advanced/boolean_variables.rst000066400000000000000000000025451456543333500231360ustar00rootroot00000000000000Boolean Variables ----------------- .. versionadded:: 2.2.0 Boolean variables are used for answering True/False questions. Basic Usage ~~~~~~~~~~~ Boolean variables are regular key / value pairs, but with the value being ``True``/``False``. For example, if you provide the following boolean variable in your ``cookiecutter.json``:: { "run_as_docker": true } you will get the following user input when running Cookiecutter:: run_as_docker [True]: User input will be parsed by :func:`~cookiecutter.prompt.read_user_yes_no`. The following values are considered as valid user input: - ``True`` values: "1", "true", "t", "yes", "y", "on" - ``False`` values: "0", "false", "f", "no", "n", "off" The above ``run_as_docker`` boolean variable creates ``cookiecutter.run_as_docker``, which can be used like this:: {%- if cookiecutter.run_as_docker -%} # In case of True add your content here {%- else -%} # In case of False add your content here {% endif %} Cookiecutter is using `Jinja2's if conditional expression `_ to determine the correct ``run_as_docker``. Input Validation ~~~~~~~~~~~~~~~~ If a non valid value is inserted to a boolean field, the following error will be printed: .. code-block:: bash run_as_docker [True]: docker Error: docker is not a valid boolean cookiecutter-2.6.0/docs/advanced/calling_from_python.rst000066400000000000000000000013001456543333500235100ustar00rootroot00000000000000.. _calling-from-python: Calling Cookiecutter Functions From Python ------------------------------------------ You can use Cookiecutter from Python: .. code-block:: python from cookiecutter.main import cookiecutter # Create project from the cookiecutter-pypackage/ template cookiecutter('cookiecutter-pypackage/') # Create project from the cookiecutter-pypackage.git repo template cookiecutter('https://github.com/audreyfeldroy/cookiecutter-pypackage.git') This is useful if, for example, you're writing a web framework and need to provide developers with a tool similar to `django-admin.py startproject` or `npm init`. See the :ref:`API Reference ` for more details. cookiecutter-2.6.0/docs/advanced/choice_variables.rst000066400000000000000000000047241456543333500227520ustar00rootroot00000000000000.. _choice-variables: Choice Variables ---------------- *New in Cookiecutter 1.1* Choice variables provide different choices when creating a project. Depending on a user's choice the template renders things differently. Basic Usage ~~~~~~~~~~~ Choice variables are regular key / value pairs, but with the value being a list of strings. For example, if you provide the following choice variable in your ``cookiecutter.json``: .. code-block:: JSON { "license": ["MIT", "BSD-3", "GNU GPL v3.0", "Apache Software License 2.0"] } you'd get the following choices when running Cookiecutter:: Select license: 1 - MIT 2 - BSD-3 3 - GNU GPL v3.0 4 - Apache Software License 2.0 Choose from 1, 2, 3, 4 [1]: Depending on an user's choice, a different license is rendered by Cookiecutter. The above ``license`` choice variable creates ``cookiecutter.license``, which can be used like this: .. code-block:: html+jinja {%- if cookiecutter.license == "MIT" -%} # Possible license content here {%- elif cookiecutter.license == "BSD-3" -%} # More possible license content here {% endif %} Cookiecutter is using `Jinja2's if conditional expression `_ to determine the correct license. The created choice variable is still a regular Cookiecutter variable and can be used like this: .. code-block:: html+jinja License ------- Distributed under the terms of the `{{cookiecutter.license}}`_ license, Overwriting Default Choice Values ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Choice Variables are overwritable using a :ref:`user-config` file. For example, a choice variable can be created in ``cookiecutter.json`` by using a list as value: .. code-block:: JSON { "license": ["MIT", "BSD-3", "GNU GPL v3.0", "Apache Software License 2.0"] } By default, the first entry in the values list serves as default value in the prompt. Setting the default ``license`` agreement to *Apache Software License 2.0* can be done using: .. code-block:: yaml default_context: license: "Apache Software License 2.0" in the :ref:`user-config` file. The resulting prompt changes and looks like:: Select license: 1 - Apache Software License 2.0 2 - MIT 3 - BSD-3 4 - GNU GPL v3.0 Choose from 1, 2, 3, 4 [1]: .. note:: As you can see the order of the options changed from ``1 - MIT`` to ``1 - Apache Software License 2.0``. **Cookiecutter** takes the first value in the list as the default. cookiecutter-2.6.0/docs/advanced/copy_without_render.rst000066400000000000000000000017061456543333500235610ustar00rootroot00000000000000.. _copy-without-render: Copy without Render ------------------- *New in Cookiecutter 1.1* To avoid rendering directories and files of a cookiecutter, the ``_copy_without_render`` key can be used in the ``cookiecutter.json``. The value of this key accepts a list of Unix shell-style wildcards: .. code-block:: JSON { "project_slug": "sample", "_copy_without_render": [ "*.html", "*not_rendered_dir", "rendered_dir/not_rendered_file.ini" ] } **Note**: Only the content of the files will be copied without being rendered. The paths are subject to rendering. This allows you to write: .. code-block:: JSON { "project_slug": "sample", "_copy_without_render": [ "{{cookiecutter.repo_name}}/templates/*.html", ] } In this example, ``{{cookiecutter.repo_name}}`` will be rendered as expected but the html file content will be copied without rendering. cookiecutter-2.6.0/docs/advanced/dict_variables.rst000066400000000000000000000033031456543333500224330ustar00rootroot00000000000000.. _dict-variables: Dictionary Variables -------------------- *New in Cookiecutter 1.5* Dictionary variables provide a way to define deep structured information when rendering a template. Basic Usage ~~~~~~~~~~~ Dictionary variables are, as the name suggests, dictionaries of key-value pairs. The dictionary values can, themselves, be other dictionaries and lists - the data structure can be as deep as you need. For example, you could provide the following dictionary variable in your ``cookiecutter.json``: .. code-block:: json { "project_slug": "new_project", "file_types": { "png": { "name": "Portable Network Graphic", "library": "libpng", "apps": [ "GIMP" ] }, "bmp": { "name": "Bitmap", "library": "libbmp", "apps": [ "Paint", "GIMP" ] } } } The above ``file_types`` dictionary variable creates ``cookiecutter.file_types``, which can be used like this: .. code-block:: html+jinja {% for extension, details in cookiecutter.file_types|dictsort %}
Format name:
{{ details.name }}
Extension:
{{ extension }}
Applications:
    {% for app in details.apps -%}
  • {{ app }}
  • {% endfor -%}
{% endfor %} Cookiecutter is using `Jinja2's for expression `_ to iterate over the items in the dictionary. cookiecutter-2.6.0/docs/advanced/directories.rst000066400000000000000000000015231456543333500217760ustar00rootroot00000000000000.. _directories: Organizing cookiecutters in directories --------------------------------------- *New in Cookiecutter 1.7* Cookiecutter introduces the ability to organize several templates in one repository or zip file, separating them by directories. This allows using symlinks for general files. Here's an example repository demonstrating this feature:: https://github.com/user/repo-name.git ├── directory1-name/ | ├── {{cookiecutter.project_slug}}/ | └── cookiecutter.json └── directory2-name/ ├── {{cookiecutter.project_slug}}/ └── cookiecutter.json To activate one of templates within a subdirectory, use the ``--directory`` option: .. code-block:: bash cookiecutter https://github.com/user/repo-name.git --directory="directory1-name" cookiecutter-2.6.0/docs/advanced/hooks.rst000066400000000000000000000115471456543333500206140ustar00rootroot00000000000000Hooks ===== Cookiecutter hooks are scripts executed at specific stages during the project generation process. They are either Python or shell scripts, facilitating automated tasks like data validation, pre-processing, and post-processing. These hooks are instrumental in customizing the generated project structure and executing initial setup tasks. Types of Hooks -------------- +------------------+------------------------------------------+------------------------------------------+--------------------+----------+ | Hook | Execution Timing | Working Directory | Template Variables | Version | +==================+==========================================+==========================================+====================+==========+ | pre_prompt | Before any question is rendered. | A copy of the repository directory | No | 2.4.0 | +------------------+------------------------------------------+------------------------------------------+--------------------+----------+ | pre_gen_project | After questions, before template process.| Root of the generated project | Yes | 0.7.0 | +------------------+------------------------------------------+------------------------------------------+--------------------+----------+ | post_gen_project | After the project generation. | Root of the generated project | Yes | 0.7.0 | +------------------+------------------------------------------+------------------------------------------+--------------------+----------+ Creating Hooks -------------- Hooks are added to the ``hooks/`` folder of your template. Both Python and Shell scripts are supported. **Python Hooks Structure:** .. code-block:: cookiecutter-something/ ├── {{cookiecutter.project_slug}}/ ├── hooks │ ├── pre_prompt.py │ ├── pre_gen_project.py │ └── post_gen_project.py └── cookiecutter.json **Shell Scripts Structure:** .. code-block:: cookiecutter-something/ ├── {{cookiecutter.project_slug}}/ ├── hooks │ ├── pre_prompt.sh │ ├── pre_gen_project.sh │ └── post_gen_project.sh └── cookiecutter.json Python scripts are recommended for cross-platform compatibility. However, shell scripts or `.bat` files can be used for platform-specific templates. Hook Execution -------------- Hooks should be robust and handle errors gracefully. If a hook exits with a nonzero status, the project generation halts, and the generated directory is cleaned. **Working Directory:** * ``pre_prompt``: Scripts run in the root directory of a copy of the repository directory. That allows the rewrite of ``cookiecutter.json`` to your own needs. * ``pre_gen_project`` and ``post_gen_project``: Scripts run in the root directory of the generated project, simplifying the process of locating generated files using relative paths. **Template Variables:** The ``pre_gen_project`` and ``post_gen_project`` hooks support Jinja template rendering, similar to project templates. For instance: .. code-block:: python module_name = '{{ cookiecutter.module_name }}' Examples -------- **Pre-Prompt Sanity Check:** A ``pre_prompt`` hook, like the one below in ``hooks/pre_prompt.py``, ensures prerequisites, such as Docker, are installed before prompting the user. .. code-block:: python import sys import subprocess def is_docker_installed() -> bool: try: subprocess.run(["docker", "--version"], capture_output=True, check=True) return True except Exception: return False if __name__ == "__main__": if not is_docker_installed(): print("ERROR: Docker is not installed.") sys.exit(1) **Validating Template Variables:** A ``pre_gen_project`` hook can validate template variables. The following script checks if the provided module name is valid. .. code-block:: python import re import sys MODULE_REGEX = r'^[_a-zA-Z][_a-zA-Z0-9]+$' module_name = '{{ cookiecutter.module_name }}' if not re.match(MODULE_REGEX, module_name): print(f'ERROR: {module_name} is not a valid Python module name!') sys.exit(1) **Conditional File/Directory Removal:** A ``post_gen_project`` hook can conditionally control files and directories. The example below removes unnecessary files based on the selected packaging option. .. code-block:: python import os REMOVE_PATHS = [ '{% if cookiecutter.packaging != "pip" %}requirements.txt{% endif %}', '{% if cookiecutter.packaging != "poetry" %}poetry.lock{% endif %}', ] for path in REMOVE_PATHS: path = path.strip() if path and os.path.exists(path): os.unlink(path) if os.path.isfile(path) else os.rmdir(path) cookiecutter-2.6.0/docs/advanced/human_readable_prompts.rst000066400000000000000000000030411456543333500241720ustar00rootroot00000000000000.. _human-readable-prompts: Human readable prompts -------------------------------- You can add human-readable prompts that will be shown to the user for each variable using the ``__prompts__`` key. For multiple choices questions you can also provide labels for each option. See the following cookiecutter config as example: .. code-block:: json { "package_name": "my-package", "module_name": "{{ cookiecutter.package_name.replace('-', '_') }}", "package_name_stylized": "{{ cookiecutter.module_name.replace('_', ' ').capitalize() }}", "short_description": "A nice python package", "github_username": "your-org-or-username", "full_name": "Firstname Lastname", "email": "email@example.com", "init_git": true, "linting": ["ruff", "flake8", "none"], "__prompts__": { "package_name": "Select your package name", "module_name": "Select your module name", "package_name_stylized": "Stylized package name", "short_description": "Short description", "github_username": "GitHub username or organization", "full_name": "Author full name", "email": "Author email", "command_line_interface": "Add CLI", "init_git": "Initialize a git repository", "linting": { "__prompt__": "Which linting tool do you want to use?", "ruff": "Ruff", "flake8": "Flake8", "none": "No linting tool" } } } cookiecutter-2.6.0/docs/advanced/index.rst000066400000000000000000000010241456543333500205650ustar00rootroot00000000000000.. Advanced Usage master index Advanced Usage ============== Various advanced topics regarding cookiecutter usage. .. toctree:: :maxdepth: 2 hooks user_config calling_from_python injecting_context suppressing_prompts templates_in_context private_variables copy_without_render replay choice_variables boolean_variables dict_variables templates template_extensions directories jinja_env new_line_characters local_extensions nested_config_files human_readable_prompts cookiecutter-2.6.0/docs/advanced/injecting_context.rst000066400000000000000000000025001456543333500231740ustar00rootroot00000000000000.. _injecting-extra-content: Injecting Extra Context ----------------------- You can specify an ``extra_context`` dictionary that will override values from ``cookiecutter.json`` or ``.cookiecutterrc``: .. code-block:: python cookiecutter( 'cookiecutter-pypackage/', extra_context={'project_name': 'TheGreatest'}, ) This works as command-line parameters as well: .. code-block:: bash cookiecutter --no-input cookiecutter-pypackage/ project_name=TheGreatest You will also need to add these keys to the ``cookiecutter.json`` or ``.cookiecutterrc``. Example: Injecting a Timestamp ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you have ``cookiecutter.json`` that has the following keys: .. code-block:: JSON { "timestamp": "{{ cookiecutter.timestamp }}" } This Python script will dynamically inject a timestamp value as the project is generated: .. code-block:: python from cookiecutter.main import cookiecutter from datetime import datetime cookiecutter( 'cookiecutter-django', extra_context={'timestamp': datetime.utcnow().isoformat()} ) How this works: 1. The script uses ``datetime`` to get the current UTC time in ISO format. 2. To generate the project, ``cookiecutter()`` is called, passing the timestamp in as context via the ``extra_context``` dict. cookiecutter-2.6.0/docs/advanced/jinja_env.rst000066400000000000000000000007651456543333500214340ustar00rootroot00000000000000.. _jinja-env: Customizing the Jinja2 environment ---------------------------------------------- The special template variable ``_jinja2_env_vars`` can be used to customize the [Jinja2 environment](https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.Environment). This example shows how to control whitespace with ``lstrip_blocks`` and ``trim_blocks``: .. code-block:: JSON { "project_slug": "sample", "_jinja2_env_vars": {"lstrip_blocks": true, "trim_blocks": true} } cookiecutter-2.6.0/docs/advanced/local_extensions.rst000066400000000000000000000035701456543333500230370ustar00rootroot00000000000000.. _`local extensions`: Local Extensions ---------------- *New in Cookiecutter 2.1* A template may extend the Cookiecutter environment with local extensions. These can be part of the template itself, providing it with more sophisticated custom tags and filters. To do so, a template author must specify the required extensions in ``cookiecutter.json`` as follows: .. code-block:: json { "project_slug": "Foobar", "year": "{% now 'utc', '%Y' %}", "_extensions": ["local_extensions.FoobarExtension"] } This example uses a simple module ``local_extensions.py`` which exists in the template root, containing the following (for instance): .. code-block:: python from jinja2.ext import Extension class FoobarExtension(Extension): def __init__(self, environment): super(FoobarExtension, self).__init__(environment) environment.filters['foobar'] = lambda v: v * 2 This will register the ``foobar`` filter for the template. For many cases, this will be unnecessarily complicated. It's likely that we'd only want to register a single function as a filter. For this, we can use the ``simple_filter`` decorator: .. code-block:: json { "project_slug": "Foobar", "year": "{% now 'utc', '%Y' %}", "_extensions": ["local_extensions.simplefilterextension"] } .. code-block:: python from cookiecutter.utils import simple_filter @simple_filter def simplefilterextension(v): return v * 2 This snippet will achieve the exact same result as the previous one. For complex use cases, a python module ``local_extensions`` (a folder with an ``__init__.py``) can also be created in the template root. Here, for example, a module ``main.py`` would have to export all extensions with ``from .main import FoobarExtension, simplefilterextension`` or ``from .main import *`` in the ``__init__.py``. cookiecutter-2.6.0/docs/advanced/nested_config_files.rst000066400000000000000000000045631456543333500234620ustar00rootroot00000000000000.. _nested-config-files: Nested configuration files -------------------------- *New in Cookiecutter 2.5.0* If you wish to create a hierarchy of templates and use cookiecutter to choose among them, you need just to specify the key ``templates`` in the main configuration file to reach the other ones. Let's imagine to have the following structure:: main-directory/ ├── project-1 │ ├── cookiecutter.json │ ├── {{cookiecutter.project_slug}} | │ ├── ... ├── package │ ├── cookiecutter.json │ ├── {{cookiecutter.project_slug}} | │ ├── ... └── cookiecutter.json It is possible to specify in the main ``cookiecutter.json`` how to reach the other config files as follows: .. code-block:: JSON { "templates": { "project-1": { "path": "./project-1", "title": "Project 1", "description": "A cookiecutter template for a project" }, "package": { "path": "./package", "title": "Package", "description": "A cookiecutter template for a package" } } } Then, when ``cookiecutter`` is launched in the main directory it will ask to choose among the possible templates: .. code-block:: Select template: 1 - Project 1 (A cookiecutter template for a project) 2 - Package (A cookiecutter template for a package) Choose from 1, 2 [1]: Once a template is chosen, for example ``1``, it will continue to ask the info required by ``cookiecutter.json`` in the ``project-1`` folder, such as ``project-slug`` Old Format ++++++++++ *New in Cookiecutter 2.2.0* In the main ``cookiecutter.json`` add a `template` key with the following format: .. code-block:: JSON { "template": [ "Project 1 (./project-1)", "Project 2 (./project-2)" ] } Then, when ``cookiecutter`` is launched in the main directory it will ask to choose among the possible templates: .. code-block:: Select template: 1 - Project 1 (./project-1) 2 - Project 2 (./project-2) Choose from 1, 2 [1]: Once a template is chosen, for example ``1``, it will continue to ask the info required by ``cookiecutter.json`` in the ``project-1`` folder, such as ``project-slug`` cookiecutter-2.6.0/docs/advanced/new_line_characters.rst000066400000000000000000000016771456543333500234730ustar00rootroot00000000000000.. _new-lines: Working with line-ends special symbols LF/CRLF ---------------------------------------------- *New in Cookiecutter 2.0* .. note:: Before version 2.0 Cookiecutter silently used system line end character. LF for POSIX and CRLF for Windows. Since version 2.0 this behaviour changed and now can be forced at template level. By default Cookiecutter checks every file at render stage and uses the same line end as in source. This allow template developers to have both types of files in the same template. Developers should correctly configure their ``.gitattributes`` file to avoid line-end character overwrite by git. The special template variable ``_new_lines`` enforces a specific line ending. Acceptable variables: ``'\r\n'`` for CRLF and ``'\n'`` for POSIX. Here is example how to force line endings to CRLF on any deployment: .. code-block:: JSON { "project_slug": "sample", "_new_lines": "\r\n" } cookiecutter-2.6.0/docs/advanced/private_variables.rst000066400000000000000000000034231456543333500231650ustar00rootroot00000000000000.. _private-variables: Private Variables ----------------- Cookiecutter allows the definition private variables by prepending an underscore to the variable name. The user will not be required to fill those variables in. These can either be not rendered, by using a prepending underscore, or rendered, prepending a double underscore. For example, the ``cookiecutter.json``: .. code-block:: JSON { "project_name": "Really cool project", "_not_rendered": "{{ cookiecutter.project_name|lower }}", "__rendered": "{{ cookiecutter.project_name|lower }}" } Will be rendered as: .. code-block:: JSON { "project_name": "Really cool project", "_not_rendered": "{{ cookiecutter.project_name|lower }}", "__rendered": "really cool project" } The user will only be asked for ``project_name``. Non-rendered private variables can be used for defining constants. An example of where you may wish to use private **rendered** variables is creating a Python package repository and want to enforce naming consistency. To ensure the repository and package name are based on the project name, you could create a ``cookiecutter.json`` such as: .. code-block:: JSON { "project_name": "Project Name", "__project_slug": "{{ cookiecutter.project_name|lower|replace(' ', '-') }}", "__package_name": "{{ cookiecutter.project_name|lower|replace(' ', '_') }}", } Which could create a structure like this:: project-name ├── Makefile ├── README.md ├── requirements.txt └── src ├── project_name │ └── __init__.py ├── setup.py └── tests └── __init__.py The ``README.md`` can then have a plain English project title. cookiecutter-2.6.0/docs/advanced/replay.rst000066400000000000000000000033751456543333500207650ustar00rootroot00000000000000.. _replay-feature: Replay Project Generation ------------------------- *New in Cookiecutter 1.1* On invocation **Cookiecutter** dumps a json file to ``~/.cookiecutter_replay/`` which enables you to *replay* later on. In other words, it persists your **input** for a template and fetches it when you run the same template again. Example for a replay file (which was created via ``cookiecutter gh:hackebrot/cookiedozer``): .. code-block:: JSON { "cookiecutter": { "app_class_name": "FooBarApp", "app_title": "Foo Bar", "email": "raphael@example.com", "full_name": "Raphael Pierzina", "github_username": "hackebrot", "kivy_version": "1.8.0", "project_slug": "foobar", "short_description": "A sleek slideshow app that supports swipe gestures.", "version": "0.1.0", "year": "2015" } } To fetch this context data without being prompted on the command line you can use either of the following methods. Pass the according option on the CLI: .. code-block:: bash cookiecutter --replay gh:hackebrot/cookiedozer Or use the Python API: .. code-block:: python from cookiecutter.main import cookiecutter cookiecutter('gh:hackebrot/cookiedozer', replay=True) This feature comes in handy if, for instance, you want to create a new project from an updated template. Custom replay file ~~~~~~~~~~~~~~~~~~ *New in Cookiecutter 2.0* To specify a custom filename, you can use the ``--replay-file`` option: .. code-block:: bash cookiecutter --replay-file ./cookiedozer.json gh:hackebrot/cookiedozer This may be useful to run the same replay file over several machines, in tests or when a user of the template reports a problem. cookiecutter-2.6.0/docs/advanced/suppressing_prompts.rst000066400000000000000000000023601456543333500236300ustar00rootroot00000000000000.. _suppressing-command-line-prompts: Suppressing Command-Line Prompts -------------------------------- To suppress the prompts asking for input, use ``no_input``. Note: this option will force a refresh of cached resources. Basic Example: Using the Defaults ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Cookiecutter will pick a default value if used with ``no_input``: .. code-block:: python from cookiecutter.main import cookiecutter cookiecutter( 'cookiecutter-django', no_input=True, ) In this case it will be using the default defined in ``cookiecutter.json`` or ``.cookiecutterrc``. .. note:: values from ``cookiecutter.json`` will be overridden by values from ``.cookiecutterrc`` Advanced Example: Defaults + Extra Context ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you combine an ``extra_context`` dict with the ``no_input`` argument, you can programmatically create the project with a set list of context parameters and without any command line prompts: .. code-block:: python cookiecutter('cookiecutter-pypackage/', no_input=True, extra_context={'project_name': 'TheGreatest'}) See also :ref:`injecting-extra-content` and the :ref:`API Reference ` for more details. cookiecutter-2.6.0/docs/advanced/template_extensions.rst000066400000000000000000000075261456543333500235650ustar00rootroot00000000000000.. _`template extensions`: Template Extensions ------------------- *New in Cookiecutter 1.4* A template may extend the Cookiecutter environment with custom `Jinja2 extensions`_. It can add extra filters, tests, globals or even extend the parser. To do so, a template author must specify the required extensions in ``cookiecutter.json`` as follows: .. code-block:: json { "project_slug": "Foobar", "year": "{% now 'utc', '%Y' %}", "_extensions": ["jinja2_time.TimeExtension"] } On invocation Cookiecutter tries to import the extensions and add them to its environment respectively. In the above example, Cookiecutter provides the additional tag `now`_, after installing the `jinja2_time.TimeExtension`_ and enabling it in ``cookiecutter.json``. Please note that Cookiecutter will **not** install any dependencies on its own! As a user you need to make sure you have all the extensions installed, before running Cookiecutter on a template that requires custom Jinja2 extensions. By default Cookiecutter includes the following extensions: - ``cookiecutter.extensions.JsonifyExtension`` - ``cookiecutter.extensions.RandomStringExtension`` - ``cookiecutter.extensions.SlugifyExtension`` - ``cookiecutter.extensions.TimeExtension`` - ``cookiecutter.extensions.UUIDExtension`` .. warning:: The above is just an example to demonstrate how this is used. There is no need to require ``jinja2_time.TimeExtension``, since its functionality is included by default (by ``cookiecutter.extensions.TimeExtension``) without needing an extra install. Jsonify extension ~~~~~~~~~~~~~~~~~ The ``cookiecutter.extensions.JsonifyExtension`` extension provides a ``jsonify`` filter in templates that converts a Python object to JSON: .. code-block:: jinja {% {'a': True} | jsonify %} Would output: .. code-block:: json {"a": true} Random string extension ~~~~~~~~~~~~~~~~~~~~~~~ *New in Cookiecutter 1.7* The ``cookiecutter.extensions.RandomStringExtension`` extension provides a ``random_ascii_string`` method in templates that generates a random fixed-length string, optionally with punctuation. Generate a random n-size character string. Example for n=12: .. code-block:: jinja {{ random_ascii_string(12) }} Outputs: .. code-block:: text bIIUczoNvswh The second argument controls if punctuation and special characters ``!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~`` should be present in the result: .. code-block:: jinja {{ random_ascii_string(12, punctuation=True) }} Outputs: .. code-block:: text fQupUkY}W!)! Slugify extension ~~~~~~~~~~~~~~~~~ The ``cookiecutter.extensions.SlugifyExtension`` extension provides a ``slugify`` filter in templates that converts string into its dashed ("slugified") version: .. code-block:: jinja {% "It's a random version" | slugify %} Would output: :: it-s-a-random-version It is different from a mere replace of spaces since it also treats some special characters differently such as ``'`` in the example above. The function accepts all arguments that can be passed to the ``slugify`` function of `python-slugify`_. For example to change the output from ``it-s-a-random-version``` to ``it_s_a_random_version``, the ``separator`` parameter would be passed: ``slugify(separator='_')``. .. _`Jinja2 extensions`: https://jinja.palletsprojects.com/en/latest/extensions/ .. _`now`: https://github.com/hackebrot/jinja2-time#now-tag .. _`jinja2_time.TimeExtension`: https://github.com/hackebrot/jinja2-time .. _`python-slugify`: https://pypi.org/project/python-slugify UUID4 extension ~~~~~~~~~~~~~~~~~~~~~~~ *New in Cookiecutter 1.x* The ``cookiecutter.extensions.UUIDExtension`` extension provides a ``uuid4()`` method in templates that generates a uuid4. Generate a uuid4 string: .. code-block:: jinja {{ uuid4() }} Outputs: .. code-block:: text 83b5de62-31b4-4a1e-83fa-8c548de65a11 cookiecutter-2.6.0/docs/advanced/templates.rst000066400000000000000000000016161456543333500214630ustar00rootroot00000000000000.. _templates: Templates inheritance (2.2+) --------------------------------------------------- *New in Cookiecutter 2.2+* Sometimes you need to extend a base template with a different configuration to avoid nested blocks. Cookiecutter introduces the ability to use common templates using the power of jinja: `extends`, `include` and `super`. Here's an example repository:: https://github.com/user/repo-name.git ├── {{cookiecutter.project_slug}}/ | └── file.txt ├── templates/ | └── base.txt └── cookiecutter.json every file in the `templates` directory will become referable inside the project itself, and the path should be relative from the `templates` folder like :: # file.txt {% extends "base.txt" %} ... or ... # file.txt {% include "base.txt" %} see more on https://jinja.palletsprojects.com/en/2.11.x/templates/ cookiecutter-2.6.0/docs/advanced/templates_in_context.rst000066400000000000000000000022611456543333500237120ustar00rootroot00000000000000.. _templates-in-context-values: Templates in Context Values -------------------------------- The values (but not the keys!) of `cookiecutter.json` are also Jinja2 templates. Values from user prompts are added to the context immediately, such that one context value can be derived from previous values. This approach can potentially save your user a lot of keystrokes by providing more sensible defaults. Basic Example: Templates in Context ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Python packages show some patterns for their naming conventions: - a human-readable project name - a lowercase, dashed repository name - an importable, dash-less package name Here is a `cookiecutter.json` with templated values for this pattern: .. code-block:: JSON { "project_name": "My New Project", "project_slug": "{{ cookiecutter.project_name|lower|replace(' ', '-') }}", "pkg_name": "{{ cookiecutter.project_slug|replace('-', '') }}" } If the user takes the defaults, or uses `no_input`, the templated values will be: - `my-new-project` - `mynewproject` Or, if the user gives `Yet Another New Project`, the values will be: - ``yet-another-new-project`` - ``yetanothernewproject`` cookiecutter-2.6.0/docs/advanced/user_config.rst000066400000000000000000000050241456543333500217650ustar00rootroot00000000000000.. _user-config: User Config =========== *New in Cookiecutter 0.7* If you use Cookiecutter a lot, you'll find it useful to have a user config file. By default Cookiecutter tries to retrieve settings from a `.cookiecutterrc` file in your home directory. *New in Cookiecutter 1.3* You can also specify a config file on the command line via ``--config-file``. .. code-block:: bash cookiecutter --config-file /home/audreyr/my-custom-config.yaml cookiecutter-pypackage Or you can set the ``COOKIECUTTER_CONFIG`` environment variable: .. code-block:: bash export COOKIECUTTER_CONFIG=/home/audreyr/my-custom-config.yaml If you wish to stick to the built-in config and not load any user config file at all, use the CLI option ``--default-config`` instead. Preventing Cookiecutter from loading user settings is crucial for writing integration tests in an isolated environment. Example user config: .. code-block:: yaml default_context: full_name: "Audrey Roy" email: "audreyr@example.com" github_username: "audreyr" cookiecutters_dir: "/home/audreyr/my-custom-cookiecutters-dir/" replay_dir: "/home/audreyr/my-custom-replay-dir/" abbreviations: pp: https://github.com/audreyfeldroy/cookiecutter-pypackage.git gh: https://github.com/{0}.git bb: https://bitbucket.org/{0} Possible settings are: ``default_context``: A list of key/value pairs that you want injected as context whenever you generate a project with Cookiecutter. These values are treated like the defaults in ``cookiecutter.json``, upon generation of any project. ``cookiecutters_dir`` Directory where your cookiecutters are cloned to when you use Cookiecutter with a repo argument. ``replay_dir`` Directory where Cookiecutter dumps context data to, which you can fetch later on when using the :ref:`replay feature `. ``abbreviations`` A list of abbreviations for cookiecutters. Abbreviations can be simple aliases for a repo name, or can be used as a prefix, in the form ``abbr:suffix``. Any suffix will be inserted into the expansion in place of the text ``{0}``, using standard Python string formatting. With the above aliases, you could use the ``cookiecutter-pypackage`` template simply by saying ``cookiecutter pp``, or ``cookiecutter gh:audreyr/cookiecutter-pypackage``. The ``gh`` (GitHub), ``bb`` (Bitbucket), and ``gl`` (Gitlab) abbreviations shown above are actually **built in**, and can be used without defining them yourself. Read also: :ref:`injecting-extra-content` cookiecutter-2.6.0/docs/case_studies.md000066400000000000000000000034251456543333500201630ustar00rootroot00000000000000# Case Studies This showcase is where organizations can describe how they are using Cookiecutter. ## [BeeWare](https://beeware.org/) Building Python tools for platforms like mobile phones and set top boxes requires a lot of boilerplate code just to get the project running. Cookiecutter has enabled us to very quickly stub out a starter project in which running Python code can be placed, and makes maintaining those templates very easy. With Cookiecutter we've been able to deliver support [Android devices](https://github.com/beeware/Python-Android-template), [iOS devices](https://github.com/beeware/Python-iOS-template), tvOS boxes, and we're planning to add native support for iOS and Windows devices in the future. [BeeWare](https://beeware.org/) is an organization building open source libraries for Python support on all platforms. ## [ChrisDev](https://chrisdev.com/) Anytime we start a new project we begin with a [Cookiecutter template that generates a Django/Wagtail project](https://github.com/chrisdev/wagtail-cookiecutter-foundation) Our developers like it for maintainability and our designers enjoy being able to spin up new sites using our tool chain very quickly. Cookiecutter is very useful for because it supports both Mac OSX and Windows users. [ChrisDev](https://chrisdev.com/) is a Trinidad-based consulting agency. ## [OpenStack](https://www.openstack.org/) OpenStack uses several Cookiecutter templates to generate: * [Openstack compliant puppet-modules](https://github.com/openstack/puppet-openstack-cookiecutter) * [Install guides](https://github.com/openstack/installguide-cookiecutter) * [New tempest plugins](https://github.com/openstack/tempest-plugin-cookiecutter) [OpenStack](https://www.openstack.org/) is open source software for creating private and public clouds. cookiecutter-2.6.0/docs/cli_options.rst000066400000000000000000000002021456543333500202300ustar00rootroot00000000000000.. _command_line_options: Command Line Options -------------------- .. click:: cookiecutter.__main__:main :prog: cookiecutter cookiecutter-2.6.0/docs/conf.py000066400000000000000000000263531456543333500164720ustar00rootroot00000000000000"""Documentation build configuration file.""" # # cookiecutter documentation build configuration file, created by # sphinx-quickstart on Thu Jul 11 11:31:49 2013. # # This file is execfile()d with the current directory set to its containing # dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # For building docs in foreign environments where we don't have all our # dependencies (like readthedocs), mock out imports that cause sphinx to fail. # see: https://docs.readthedocs.io/en/latest/faq.html#i-get-import-errors-on-libraries-that-depend-on-c-modules # noqa # flake8: noqa D107,D105 # Add parent dir to path cwd = os.getcwd() parent = os.path.dirname(cwd) sys.path.append(parent) import cookiecutter # noqa 402 # -- General configuration ---------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or # your custom ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.imgmath', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', 'sphinx_click.ext', 'myst_parser', 'sphinxcontrib.apidoc', 'sphinx_autodoc_typehints', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = {'.rst': 'restructuredtext', '.md': 'markdown'} # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'cookiecutter' copyright = '2013-2022, Audrey Roy and Cookiecutter community' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = cookiecutter.__version__ # The full version, including alpha/beta/rc tags. release = cookiecutter.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # Suppress nonlocal image warnings suppress_warnings = ['image.nonlocal_uri'] # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'cookiecutterdoc' # -- Options for LaTeX output ------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]) latex_documents = [ ( 'index', 'cookiecutter.tex', 'cookiecutter Documentation', 'Audrey Roy and Cookiecutter community', 'manual', ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output ------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ( 'index', 'cookiecutter', 'cookiecutter Documentation', ['Audrey Roy and Cookiecutter community'], 1, ) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ----------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( 'index', 'cookiecutter', 'cookiecutter Documentation', 'Audrey Roy and Cookiecutter community', 'cookiecutter', 'Creates projects from project templates', 'Miscellaneous', ), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False # -- Options for Epub output -------------------------------------------------- # Bibliographic Dublin Core info. epub_title = 'cookiecutter' epub_author = 'Audrey Roy' epub_publisher = 'Audrey Roy and Cookiecutter community' epub_copyright = '2013-2022, Audrey Roy and Cookiecutter community' # The language of the text. It defaults to the language option # or en if the language is not set. # epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. # epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. # epub_identifier = '' # A unique identification for the text. # epub_uid = '' # A tuple containing the cover image and cover page html template filenames. # epub_cover = () # A sequence of (type, uri, title) tuples for the guide element of content.opf. # epub_guide = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. # epub_pre_files = [] # HTML files that should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. # epub_post_files = [] # A list of files that should not be packed into the epub file. # epub_exclude_files = [] # The depth of the table of contents in toc.ncx. # epub_tocdepth = 3 # Allow duplicate toc entries. # epub_tocdup = True # Fix unsupported image types using the PIL. # epub_fix_images = False # Scale large images. # epub_max_image_width = 0 # If 'no', URL addresses will not be shown. # epub_show_urls = 'inline' # If false, no index is generated. # epub_use_index = True # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "requests": ("https://requests.readthedocs.io/en/latest/", None), "click": ("https://click.palletsprojects.com/en/latest", None), } myst_enable_extensions = [ "tasklist", "strikethrough", "fieldlist", ] myst_heading_anchors = 3 # Apidoc extension config apidoc_module_dir = "../cookiecutter" apidoc_output_dir = "." apidoc_toc_file = False apidoc_extra_args = ["-t", "_templates"] autodoc_member_order = "groupwise" autodoc_typehints = "none" cookiecutter-2.6.0/docs/cookiecutter.rst000066400000000000000000000047451456543333500204260ustar00rootroot00000000000000cookiecutter package ==================== Submodules ---------- cookiecutter.cli module ----------------------- .. automodule:: cookiecutter.cli :members: :undoc-members: :show-inheritance: cookiecutter.config module -------------------------- .. automodule:: cookiecutter.config :members: :undoc-members: :show-inheritance: cookiecutter.environment module ------------------------------- .. automodule:: cookiecutter.environment :members: :undoc-members: :show-inheritance: cookiecutter.exceptions module ------------------------------ .. automodule:: cookiecutter.exceptions :members: :undoc-members: :show-inheritance: cookiecutter.extensions module ------------------------------ .. automodule:: cookiecutter.extensions :members: :undoc-members: :show-inheritance: cookiecutter.find module ------------------------ .. automodule:: cookiecutter.find :members: :undoc-members: :show-inheritance: cookiecutter.generate module ---------------------------- .. automodule:: cookiecutter.generate :members: :undoc-members: :show-inheritance: cookiecutter.hooks module ------------------------- .. automodule:: cookiecutter.hooks :members: :undoc-members: :show-inheritance: cookiecutter.log module ----------------------- .. automodule:: cookiecutter.log :members: :undoc-members: :show-inheritance: cookiecutter.main module ------------------------ .. automodule:: cookiecutter.main :members: :undoc-members: :show-inheritance: cookiecutter.prompt module -------------------------- .. automodule:: cookiecutter.prompt :members: :undoc-members: :show-inheritance: cookiecutter.replay module -------------------------- .. automodule:: cookiecutter.replay :members: :undoc-members: :show-inheritance: cookiecutter.repository module ------------------------------ .. automodule:: cookiecutter.repository :members: :undoc-members: :show-inheritance: cookiecutter.utils module ------------------------- .. automodule:: cookiecutter.utils :members: :undoc-members: :show-inheritance: cookiecutter.vcs module ----------------------- .. automodule:: cookiecutter.vcs :members: :undoc-members: :show-inheritance: cookiecutter.zipfile module --------------------------- .. automodule:: cookiecutter.zipfile :members: :undoc-members: :show-inheritance: Module contents --------------- .. automodule:: cookiecutter :members: :undoc-members: :show-inheritance: cookiecutter-2.6.0/docs/index.rst000066400000000000000000000015461456543333500170310ustar00rootroot00000000000000.. cookiecutter documentation master file, created by sphinx-quickstart on Thu Jul 11 11:31:49 2013. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Cookiecutter: Better Project Templates ====================================== Cookiecutter creates projects from **cookiecutters** (project templates), e.g. Python package projects from Python package templates. Basics ------ .. toctree:: :maxdepth: 2 README overview installation usage cli_options tutorials/index advanced/index troubleshooting .. _apiref: API Reference ------------- .. toctree:: :maxdepth: 2 cookiecutter Project Info ------------ .. toctree:: :maxdepth: 2 CONTRIBUTING AUTHORS HISTORY case_studies CODE_OF_CONDUCT Index ----- * :ref:`genindex` * :ref:`modindex` cookiecutter-2.6.0/docs/installation.rst000066400000000000000000000075731456543333500204310ustar00rootroot00000000000000============ Installation ============ Prerequisites ------------- * Python interpreter * Adjust your path * Packaging tools Python interpreter ^^^^^^^^^^^^^^^^^^ Install Python for your operating system. On Windows and macOS this is usually necessary. Most Linux distributions come with Python pre-installed. Consult the official `Python documentation `_ for details. You can install the Python binaries from `python.org `_. Alternatively on macOS, you can use the `homebrew `_ package manager. .. code-block:: bash brew install python3 Adjust your path ^^^^^^^^^^^^^^^^ Ensure that your ``bin`` folder is on your path for your platform. Typically ``~/.local/`` for UNIX and macOS, or ``%APPDATA%\Python`` on Windows. (See the Python documentation for `site.USER_BASE `_ for full details.) UNIX and macOS """""""""""""" For bash shells, add the following to your ``.bash_profile`` (adjust for other shells): .. code-block:: bash # Add ~/.local/ to PATH export PATH=$HOME/.local/bin:$PATH Remember to load changes with ``source ~/.bash_profile`` or open a new shell session. Windows """"""" Ensure the directory where cookiecutter will be installed is in your environment's ``Path`` in order to make it possible to invoke it from a command prompt. To do so, search for "Environment Variables" on your computer (on Windows 10, it is under ``System Properties`` --> ``Advanced``) and add that directory to the ``Path`` environment variable, using the GUI to edit path segments. Example segments should look like ``%APPDATA%\Python\Python3x\Scripts``, where you have your version of Python instead of ``Python3x``. You may need to restart your command prompt session to load the environment variables. .. seealso:: See `Configuring Python (on Windows) `_ for full details. **Unix on Windows** You may also install `Windows Subsystem for Linux `_ or `GNU utilities for Win32 `_ to use Unix commands on Windows. Packaging tools ^^^^^^^^^^^^^^^ See the Python Packaging Authority's (PyPA) documentation `Requirements for Installing Packages `_ for full details. Install cookiecutter -------------------- At the command line: .. code-block:: bash python3 -m pip install --user cookiecutter Or, if you do not have pip: .. code-block:: bash easy_install --user cookiecutter Though, pip is recommended, easy_install is deprecated. Or, if you are using conda, first add conda-forge to your channels: .. code-block:: bash conda config --add channels conda-forge Once the conda-forge channel has been enabled, cookiecutter can be installed with: .. code-block:: bash conda install cookiecutter Alternate installations ----------------------- **Homebrew (Mac OS X only):** .. code-block:: bash brew install cookiecutter **Void Linux:** .. code-block:: bash xbps-install cookiecutter **Pipx (Linux, OSX and Windows):** .. code-block:: bash pipx install cookiecutter Upgrading --------- from 0.6.4 to 0.7.0 or greater ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ First, read :doc:`HISTORY` in detail. There are a lot of major changes. The big ones are: * Cookiecutter no longer deletes the cloned repo after generating a project. * Cloned repos are saved into `~/.cookiecutters/`. * You can optionally create a `~/.cookiecutterrc` config file. Or with pip: .. code-block:: bash python3 -m pip install --upgrade cookiecutter Upgrade Cookiecutter either with easy_install (deprecated): .. code-block:: bash easy_install --upgrade cookiecutter Then you should be good to go. cookiecutter-2.6.0/docs/overview.rst000066400000000000000000000031721456543333500175650ustar00rootroot00000000000000======== Overview ======== Cookiecutter takes a template provided as a directory structure with template-files. Templates can be located in the filesystem, as a ZIP-file or on a VCS-Server (Git/Hg) like GitHub. It reads a settings file and prompts the user interactively whether or not to change the settings. Then it takes both and generates an output directory structure from it. Additionally the template can provide code (Python or shell-script) to be executed before and after generation (pre-gen- and post-gen-hooks). Input ----- This is a directory structure for a simple cookiecutter:: cookiecutter-something/ ├── {{ cookiecutter.project_name }}/ <--------- Project template │ └── ... ├── blah.txt <--------- Non-templated files/dirs │ go outside │ └── cookiecutter.json <--------- Prompts & default values You must have: - A ``cookiecutter.json`` file. - A ``{{ cookiecutter.project_name }}/`` directory, where ``project_name`` is defined in your ``cookiecutter.json``. Beyond that, you can have whatever files/directories you want. See https://github.com/audreyfeldroy/cookiecutter-pypackage for a real-world example of this. Output ------ This is what will be generated locally, in your current directory:: mysomething/ <---------- Value corresponding to what you enter at the │ project_name prompt │ └── ... <-------- Files corresponding to those in your cookiecutter's `{{ cookiecutter.project_name }}/` dir cookiecutter-2.6.0/docs/requirements.txt000066400000000000000000000002471456543333500204510ustar00rootroot00000000000000sphinx-rtd-theme>=1.0.0 sphinx-click>=4.1.0 myst-parser>=0.17.2 sphinx-autobuild>=2021.3.14 Sphinx>=4.5.0 sphinxcontrib-apidoc>=0.3.0 sphinx-autodoc-typehints>=1.18.2 cookiecutter-2.6.0/docs/troubleshooting.rst000066400000000000000000000023111456543333500211400ustar00rootroot00000000000000=============== Troubleshooting =============== I created a cookiecutter, but it doesn't work, and I can't figure out why ------------------------------------------------------------------------- * Try upgrading to Cookiecutter 0.8.0, which prints better error messages and has fixes for several common bugs. I'm having trouble generating Jinja templates from Jinja templates ------------------------------------------------------------------ Make sure you escape things properly, like this:: {{ "{{" }} Or this:: {% raw %}

Go Home

{% endraw %} Or this:: {{ {{ url_for('home') }} }} See https://jinja.palletsprojects.com/en/latest/templates/#escaping for more info. You can also use the `_copy_without_render`_ key in your `cookiecutter.json` file to escape entire files and directories. .. _`_copy_without_render`: http://cookiecutter.readthedocs.io/en/latest/advanced/copy_without_render.html Other common issues ------------------- TODO: add a bunch of common new user issues here. This document is incomplete. If you have knowledge that could help other users, adding a section or filing an issue with details would be greatly appreciated. cookiecutter-2.6.0/docs/tutorials/000077500000000000000000000000001456543333500172105ustar00rootroot00000000000000cookiecutter-2.6.0/docs/tutorials/index.rst000066400000000000000000000024411456543333500210520ustar00rootroot00000000000000==================== Tutorials ==================== Tutorials by `@audreyfeldroy`_ .. toctree:: :maxdepth: 2 tutorial1 tutorial2 External Links -------------- - `Learn the Basics of Cookiecutter by Creating a Cookiecutter`_ - first steps tutorial with example template by `@BruceEckel`_ - `Project Templates Made Easy`_ by `@pydanny`_ - Cookiedozer Tutorials by `@hackebrot`_ - Part 1: `Create your own Cookiecutter template`_ - Part 2: `Extending our Cookiecutter template`_ - Part 3: `Wrapping up our Cookiecutter template`_ .. _`Learn the Basics of Cookiecutter by Creating a Cookiecutter`: https://github.com/BruceEckel/HelloCookieCutter1/blob/master/Readme.rst .. _`Project Templates Made Easy`: http://www.pydanny.com/cookie-project-templates-made-easy.html .. _`Create your own Cookiecutter template`: https://raphael.codes/blog/create-your-own-cookiecutter-template/ .. _`Extending our Cookiecutter template`: https://raphael.codes/blog/extending-our-cookiecutter-template/ .. _`Wrapping up our Cookiecutter template`: https://raphael.codes/blog/wrapping-up-our-cookiecutter-template/ .. _`@audreyfeldroy`: https://github.com/audreyfeldroy .. _`@pydanny`: https://github.com/pydanny .. _`@hackebrot`: https://github.com/hackebrot .. _`@BruceEckel`: https://github.com/BruceEckel cookiecutter-2.6.0/docs/tutorials/tutorial1.rst000066400000000000000000000121431456543333500216670ustar00rootroot00000000000000============================= Getting to Know Cookiecutter ============================= .. note:: Before you begin, please install Cookiecutter 0.7.0 or higher. Instructions are in :doc:`../installation`. Cookiecutter is a tool for creating projects from *cookiecutters* (project templates). What exactly does this mean? Read on! Case Study: cookiecutter-pypackage ----------------------------------- *cookiecutter-pypackage* is a cookiecutter template that creates the starter boilerplate for a Python package. .. note:: There are several variations of it, but for this tutorial we'll use the original version at https://github.com/audreyfeldroy/cookiecutter-pypackage/. Step 1: Generate a Python Package Project ------------------------------------------ Open your shell and cd into the directory where you'd like to create a starter Python package project. At the command line, run the cookiecutter command, passing in the link to cookiecutter-pypackage's HTTPS clone URL like this: .. code-block:: bash $ cookiecutter https://github.com/audreyfeldroy/cookiecutter-pypackage.git Local Cloning of Project Template ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ First, cookiecutter-pypackage gets cloned to `~/.cookiecutters/` (or equivalent on Windows). Cookiecutter does this for you, so sit back and wait. Local Generation of Project ~~~~~~~~~~~~~~~~~~~~~~~~~~~ When cloning is complete, you will be prompted to enter a bunch of values, such as `full_name`, `email`, and `project_name`. Either enter your info, or simply press return/enter to accept the default values. This info will be used to fill in the blanks for your project. For example, your name and the year will be placed into the LICENSE file. Step 2: Explore What Got Generated ---------------------------------- In your current directory, you should see that a project got generated: .. code-block:: bash $ ls boilerplate Looking inside the `boilerplate/` (or directory corresponding to your `project_slug`) directory, you should see something like this: .. code-block:: bash $ ls boilerplate/ AUTHORS.rst MANIFEST.in docs tox.ini CONTRIBUTING.rst Makefile requirements.txt HISTORY.rst README.rst setup.py LICENSE boilerplate tests That's your new project! If you open the AUTHORS.rst file, you should see something like this: .. code-block:: rst ======= Credits ======= Development Lead ---------------- * Audrey Roy Contributors ------------ None yet. Why not be the first? Notice how it was auto-populated with your (or my) name and email. Also take note of the fact that you are looking at a ReStructuredText file. Cookiecutter can generate a project with text files of any type. Great, you just generated a skeleton Python package. How did that work? Step 3: Observe How It Was Generated ------------------------------------ Let's take a look at cookiecutter-pypackage together. Open https://github.com/audreyfeldroy/cookiecutter-pypackage in a new browser window. {{ cookiecutter.project_slug }} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Find the directory called `{{ cookiecutter.project_slug }}`. Click on it. Observe the files inside of it. You should see that this directory and its contents corresponds to the project that you just generated. This happens in `find.py`, where the `find_template()` method looks for the first jinja-like directory name that starts with `cookiecutter`. AUTHORS.rst ~~~~~~~~~~~ Look at the raw version of `{{ cookiecutter.project_slug }}/AUTHORS.rst`, at https://raw.github.com/audreyfeldroy/cookiecutter-pypackage/master/%7B%7Bcookiecutter.project_slug%7D%7D/AUTHORS.rst. Observe how it corresponds to the `AUTHORS.rst` file that you generated. cookiecutter.json ~~~~~~~~~~~~~~~~~ Now navigate back up to `cookiecutter-pypackage/` and look at the `cookiecutter.json` file. You should see JSON that corresponds to the prompts and default values shown earlier during project generation: .. code-block:: json { "full_name": "Audrey Roy Greenfeld", "email": "aroy@alum.mit.edu", "github_username": "audreyr", "project_name": "Python Boilerplate", "project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '_') }}", "project_short_description": "Python Boilerplate contains all the boilerplate you need to create a Python package.", "pypi_username": "{{ cookiecutter.github_username }}", "version": "0.1.0", "use_pytest": "n", "use_pypi_deployment_with_travis": "y", "create_author_file": "y", "open_source_license": ["MIT", "BSD", "ISCL", "Apache Software License 2.0", "Not open source"] } Questions? ---------- If anything needs better explanation, please take a moment to file an issue at https://github.com/audreyfeldroy/cookiecutter/issues with what could be improved about this tutorial. Summary ------- You have learned how to use Cookiecutter to generate your first project from a cookiecutter project template. In tutorial 2 (:ref:`tutorial2`), you'll see how to create cookiecutters of your own, from scratch. cookiecutter-2.6.0/docs/tutorials/tutorial2.rst000066400000000000000000000073111456543333500216710ustar00rootroot00000000000000.. _tutorial2: ================================== Create a Cookiecutter From Scratch ================================== In this tutorial, we are creating `cookiecutter-website-simple`, a cookiecutter for generating simple, bare-bones websites. Step 1: Name Your Cookiecutter ------------------------------ Create the directory for your cookiecutter and cd into it: .. code-block:: bash $ mkdir cookiecutter-website-simple $ cd cookiecutter-website-simple/ Step 2: Create cookiecutter.json ---------------------------------- `cookiecutter.json` is a JSON file that contains fields which can be referenced in the cookiecutter template. For each, default value is defined and user will be prompted for input during cookiecutter execution. Only mandatory field is `project_slug` and it should comply with package naming conventions defined in `PEP8 Naming Conventions `_ . .. code-block:: json { "project_name": "Cookiecutter Website Simple", "project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '_') }}", "author": "Anonymous" } Step 3: Create project_slug Directory --------------------------------------- Create a directory called `{{ cookiecutter.project_slug }}`. This value will be replaced with the repo name of projects that you generate from this cookiecutter. Step 4: Create index.html -------------------------- Inside of `{{ cookiecutter.project_slug }}`, create `index.html` with following content: .. code-block:: html {{ cookiecutter.project_name }}

{{ cookiecutter.project_name }}

by {{ cookiecutter.author }}

Step 5: Pack cookiecutter into ZIP ---------------------------------- There are many ways to run Cookiecutter templates, and they are described in details in `Usage chapter `_. In this tutorial we are going to ZIP cookiecutter and then run it for testing. By running following command `cookiecutter.zip` will get generated which can be used to run cookiecutter. Script will generate `cookiecutter.zip` ZIP file and echo full path to the file. .. code-block:: bash $ (SOURCE_DIR=$(basename $PWD) ZIP=cookiecutter.zip && # Set variables pushd .. && # Set parent directory as working directory zip -r $ZIP $SOURCE_DIR --exclude $SOURCE_DIR/$ZIP --quiet && # ZIP cookiecutter mv $ZIP $SOURCE_DIR/$ZIP && # Move ZIP to original directory popd && # Restore original work directory echo "Cookiecutter full path: $PWD/$ZIP") Step 6: Run cookiecutter ------------------------ Set your work directory to whatever directory you would like to run cookiecutter at. Use cookiecutter full path and run the following command: .. code-block:: bash $ cookiecutter You can expect similar output: .. code-block:: bash $ cookiecutter /Users/admin/cookiecutter-website-simple/cookiecutter.zip project_name [Cookiecutter Website Simple]: Test web project_slug [test_web]: author [Anonymous]: Cookiecutter Developer Resulting directory should be inside your work directory with a name that matches `project_slug` you defined. Inside that directory there should be `index.html` with generated source: .. code-block:: html Test web

Test web

by Cookiecutter Developer

cookiecutter-2.6.0/docs/usage.rst000066400000000000000000000106671456543333500170320ustar00rootroot00000000000000===== Usage ===== Grab a Cookiecutter template ---------------------------- First, clone a Cookiecutter project template:: $ git clone https://github.com/audreyfeldroy/cookiecutter-pypackage.git Make your changes ----------------- Modify the variables defined in `cookiecutter.json`. Open up the skeleton project. If you need to change it around a bit, do so. You probably also want to create a repo, name it differently, and push it as your own new Cookiecutter project template, for handy future use. Generate your project --------------------- Then generate your project from the project template:: $ cookiecutter cookiecutter-pypackage/ The only argument is the input directory. (The output directory is generated by rendering that, and it can't be the same as the input directory.) .. note:: see :ref:`command_line_options` for extra command line arguments Try it out! Works directly with git and hg (mercurial) repos too ------------------------------------------------------ To create a project from the cookiecutter-pypackage.git repo template:: $ cookiecutter gh:audreyfeldroy/cookiecutter-pypackage Cookiecutter knows abbreviations for Github (``gh``), Bitbucket (``bb``), and GitLab (``gl``) projects, but you can also give it the full URL to any repository:: $ cookiecutter https://github.com/audreyfeldroy/cookiecutter-pypackage.git $ cookiecutter git+ssh://git@github.com/audreyfeldroy/cookiecutter-pypackage.git $ cookiecutter hg+ssh://hg@bitbucket.org/audreyr/cookiecutter-pypackage You will be prompted to enter a bunch of project config values. (These are defined in the project's `cookiecutter.json`.) Then, Cookiecutter will generate a project from the template, using the values that you entered. It will be placed in your current directory. And if you want to specify a branch you can do that with:: $ cookiecutter https://github.com/audreyfeldroy/cookiecutter-pypackage.git --checkout develop Works with private repos ------------------------ If you want to work with repos that are not hosted in github or bitbucket you can indicate explicitly the type of repo that you want to use prepending `hg+` or `git+` to repo url:: $ cookiecutter hg+https://example.com/repo In addition, one can provide a path to the cookiecutter stored on a local server:: $ cookiecutter file://server/folder/project.git Works with Zip files -------------------- You can also distribute cookiecutter templates as Zip files. To use a Zip file template, point cookiecutter at a Zip file on your local machine:: $ cookiecutter /path/to/template.zip Or, if the Zip file is online:: $ cookiecutter https://example.com/path/to/template.zip If the template has already been downloaded, or a template with the same name has already been downloaded, you will be prompted to delete the existing template before proceeding. The Zip file contents should be the same as a git/hg repository for a template - that is, the zipfile should unpack into a top level directory that contains the name of the template. The name of the zipfile doesn't have to match the name of the template - for example, you can label a zipfile with a version number, but omit the version number from the directory inside the Zip file. If you want to see an example Zipfile, find any Cookiecutter repository on Github and download that repository as a zip file - Github repository downloads are in a valid format for Cookiecutter. Password-protected Zip files ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If your repository Zip file is password protected, Cookiecutter will prompt you for that password whenever the template is used. Alternatively, if you want to use a password-protected Zip file in an automated environment, you can export the `COOKIECUTTER_REPO_PASSWORD` environment variable; the value of that environment variable will be used whenever a password is required. Keeping your cookiecutters organized ------------------------------------ As of the Cookiecutter 0.7.0 release: * Whenever you generate a project with a cookiecutter, the resulting project is output to your current directory. * Your cloned cookiecutters are stored by default in your `~/.cookiecutters/` directory (or Windows equivalent). The location is configurable: see :doc:`advanced/user_config` for details. Pre-0.7.0, this is how it worked: * Whenever you generate a project with a cookiecutter, the resulting project is output to your current directory. * Cloned cookiecutters were not saved locally. cookiecutter-2.6.0/logo/000077500000000000000000000000001456543333500151725ustar00rootroot00000000000000cookiecutter-2.6.0/logo/cookiecutter-logo-large.png000066400000000000000000002656331456543333500224450ustar00rootroot00000000000000PNG  IHDRIsBIT|d pHYsnnrtEXtSoftwarewww.inkscape.org< IDATxy^uag$9`gV;Xv6mU;xjmoj{vݮFP[1n@E) q?Cs $;|>k2߰VzW}_V˓dEp-voQIy|o)ZshW][-;Mrt(&ݎ$'9/I>U75e`skeIN`$Nrp({fz@uo,{h/][1yCP4 ώ$_`D~vMs$3q`VZI??}=(IU4 ׶$do+;wkI~.&YQڜ ? u($3~LVUdiH2U4 IL$w 9 hHtmuX`$$ǔ-tkg0 ?n`juv&$Xٞ {SF0aϙN.<u/`8LVe0nS ?Suo)31յՉyw$듬`@/ffhHVH N?p0{W$yOuu_\:`4#k<+cL->o(2ԵI$KSI-$]IΪ~K872\8Xx?ɺX:`4CkÒɋ릿t|0gum5.ɒ9L듼nkF,I_e}rdm^[%.M<`SSvKqLVNI/26ޔ\qMru͛[o geɱ+{29xܷpMM!shy׵!IޒWJmۓ ůvp|8d`<~e,HuZ:`3zd&OqOwe+ پ}=89^ɪB -G$jr KGU%NH~j5uEgM!w0gum[I"ɒ-W&~-ٺtpZz@S8 ےZ/Zo-2oO>[. G<+J֟'úw&8wks-}?99ƛJU'$?59pisVM!2g$Ktɹ_Km/]2X<䔟Ht ..N]:LF$N$SsFyH}YqO="Yq`0!1g][]IQemݖko(]2~Zɑ.Ŏ$Cb4$g%9lۺ-6._%g<8ϽJ&ya&8I$ǔnu[Xd2*y)IU.IMe`~!Teqc'<,Yt .M̺+յo%Yyks Kz{]쮭S:oNP][UIޘ俔n۷'g~6KL#Od%蓼n7Ɠ$N-⼯']Z$9IحJwTVV+9s5'\X`^0g'HV''|GnmN>׿|$!0][=:ɥ[g23|>JW㒜۵ C`4>zVO&YYe\kKW';v$|9ї.حS|kFI)Ɂ[ɶrk+KWqI>ӵOFjQVoM$Uqsm+vtHZ[:]FckIޛ䥥[ŗ.`oxsr5+Ғ$Jh|tmu@&yNqu`6q`4kwm3`VHV˒|8K2>rۘ\c N_T:FckJK.]0F ߮F$K-nd싍?*]0+Nvmu@`V$dGnm.]$뻶:t0܌GXVG%Tn)vh$jy`xQKrNn$N].gOJѮ. 'ԵI>-J;K0mOa0 d$29neEC/E`ޖ%;$ܐMMoM6ݚG1IRUA&/OVs(li2_S:_FumgI^]vɦC򛧇7mJn-ټu06߼5ٱtJ^6|v>|Y2U+M#c4^PVNg;[%[fo~pg<R Ռkj7v}n%k/׋KWHY7;JGh^m;#/M!3/k_J$U`blN?W:[F k3|<[sCGM!1_@][7(LK2t07JLV$93@Y''PVKKsh|tmu@Nt @$yWVU`/w$y|OKGh|umI~tnkO}醱յճ?IU`%yJ+yҵI>)w$~C`AV'$RK+^7K3U:`tmuhb0{'9kJc4>ZMRnIW#1[oI3#I{`vHV/O}IΨ!T}ߗny][=5ɇc>M!1O]['ɗ^`6%yA*ťFYV&` sYM!*0ޒ#f$0`4$/,0KLSu_W:UFNV|1ɲ-{i{߫ͥCe4>K][$-~yut0Aq`t\iu_T:SFIV/O;IN7V}_a$tm$$/~[`~J$,p7%ݺ^:XN;][M%y_ 1IS7٥Cc4~^䉥#EI~nV}醡յՓ*p>uT:XxSUV'$yO $O5ɵt0jQ'YQ`$ͺO,IY:`K̺-W}_atm$_Lt n|=ꦿt0J &yw >c w$uxC_~S`T}ߗn ][=>1mI~nxI$'N$O!r_`.'y8pw&~4޵Ӓ0׺ IDATZޗ䌺. tmuTo&9t @>M!X\:806%yA*wm6ɚI.KQ:=U\V'FJ$Ϭ!hƻ3@yL鷔`]zymkILxe w eNeIt0v$yUo.%]$L_+kɌw$[vs|u7@`k׬̑I&3{ӵ$A֭߰} Ci&Yk;]e%7(;NܲcH>sP>k|/ɥ3o45FDƻ_f0nJܺ?Z:5'99)I(q-_>U3+UoqZ9v#x"ɢ9w$٘;]k֭p fGf}3;_\1}]9__nkV/KR'yu}Eɮ=>O_WT5i}61vcs>s$~TɮytSv3&Orٺ&4ka2'te9$l,׀{$ORS_n LkVO%y5s ~Xɮ6%|#$M1$noK}:;Co1e0Be'2ϭ[e`t]zIG&ybL$Gl"7'97זs\n-eQv||$+F Cr5W&yhMOŖ$JFgG%$O9ےKyol{cRFE;-M!''ЭΑI(5:vdp#4Nӯ/`$ ^_*5:nˮ|a M5LrzvQ$$[as+33GWdsH~}$warVeNB* & QdpATqAE@eS@qcAQDjPѨ:~0"[EAA%Q;]U[oݿ+;1Iw<$IdߎοF G+y5]I$I$IӶLA}hJϙ=m]#-$iϋ7>b-flQ\ 4 y\|JN6jE+:T\Dg=I$`K:[ѹyfhT} Cgf=/=[x|et$ ہӹy5S$IzSm x5jlQEmVA:AuGz_?%&Cro2z؃T%%h{DGho!!q]oy?R&3Zh4[P5Y }̚Bk㻢tpXt'}ٚb.j4[KG^w asDOվtg^YxOyuUՌ%IR<:־;:D(9msS=w~BgevAg`o:gKNCgf2IS2ϠQ&CΦ3D~vٺ3L0πCOZg9A<~vSrӽ,`|PwF^﷒t}wa_>PЙIH=6=:@^ w?n-9|l]Uy3-%SrcK:l_T5S3kz K#tܼ\Tk3EFer?ߋ6_k"j;4>gZǣ;$IP9X4JgP-sԱ2 po{WfI3~VtDF Uؒt|l]Z&iBJN3W*~ ,-,:Cꏵ/;ćxhbby(9 fGKO C`)*<2>ԾE|[Ooz?wIgV;|R-ۍfc$U6佸o뗥ޙzoI$ t[w/_y8H~pKEy?4aF@~N_ I44?:>%:Dë:7hіt:7%3yubk4AQrj,IAhbf-%o4#4pa5޴%G;ȓ?t;A7[@S= WSE}[z?}IRyAo O*9]Bg?LIBxOt{UrZ 'I$;T>tpp&p~j?OZb3'^^d=E p|_KzZZϙ=F$i(8:־8:Dé"CtWfh6 |tW7iȕDw/ ԤKNsFGl4JNOsp[hj8t. @OKN9LnnXpnגs,:Rrzئl]"I:Ƌ-9}|ٺ$Ivݡ0[%I4ȦvS`_ƒ,-LEt7O$vFkvCsf,Gg˸$IR]qt Ra̓s+{{~ |f*!0Utw-R_>=~H f$ -X{;)2rqg[7YaKNӁ;@#684:N/Isxmvv.9] |afIZYz`I$ Zr:ëR<}|JNmKvClz$I&X 5s;[?,9h~?oy:G_rpFplT?%mR[+>Pr88l%i>C74ſx/ח$Irٜϔ h$ixM$I,|ڥFR97ܒM ϙ=2|$IR6:Nt4x a͂skK _5~|fp)GKN?}F9VxaͽKN_k4[ nݰm-]$I}WrZLJW Qe)9DMpO%e ;$I[}/tITI+;?_'fвӀ{-#$IRm 0:Utm| ؛ÁsT-^]r>wٺ5:Hlи$Iy^0_ovOfkNp4׼$Iq;C$-t`DZ% 7/BM9GV>!IjWW~ Z7)9+9Xr: =p4k4KN.9.:F:%Ӂ9tt`\4m7^rs~jD^ lݡ jtpgt$I%^eZ%6C&$IRӿEGH,tqAic$N=:־?:DUrz1pup@{ٺ-:HRrZ bp3KNh~$E*9=}Y9\K?|FuMp4,i !n4[h4[>K )9|X%8Ggx3)~f&ioEkI$ΏD^SrEoT3{d}`IT;mѱ.WJN/q`\K!R%Xesg%F%%=q`\1xKNϋP ^3 *l]$^JN+YHLs_Mt4d4y94.IaMiISr’釁>/JIit}DtI-` Uf8%-ˁsT_[畜/9,:FꇒkՂsTO#N%Nꩡ-N`vt<$IPrZtMaquZt}Lt4$?W~*IaI]JKN?-9 :zh|-L'I-k7CT?%JNɀ>ypqiW%ݣ5pQ%͢c^(9=s?Dh(L/9^r RUi/7#I4JN;^>\Yru;c6Ι=28*C$\`ѱGEǨ>JNJNědcEg%'jtp*T59$墒sc? t^s\>8GӾ%AҀ& sޖqpӸ$I.u8PX~Xr&84.IamtY8tp]& 9ώ*yQp:c˛nvf.g̙_.ln:i <:>+:D2d` YJNj4[M]i[`i1^ \Qr,Fuot(%݀᰸It w+9}l$ n/9 =CFU*6I ?0Kx1!R y7It$IhF:G/+9h474>g Ǣ;$ n#"82Vxх߇{m̥`t=x醰ɳa-UōckCT[>BY9t6̝TrMٺ<:FK2p"ipAJN_vn&a84ػl):H0qBsXM94.I" `/Sӱf"ՌCJt$Ih9:ΏS4iFuet̒V(w v9քopupGV v-UyfJN㪖YKN+EhJN/.ǁq g͒OJNDH)9mp`\k i bUrZxwt&[$IUrU]FGH5и I$U%C@3gӀ};$;9~~2nu?u;vt}Gt4t+`i!N-9DhJN. N5%C4JN˖ X#GUN,9-# nX::BU-qI%JNV&`$IQtYYi%1PC'e#$_x!cKGb:w,źjoN$I%k)<ӁDGH5qC$I%:,iffz%i u?Iqp5"] l>:>/:D1I9-$MЂv.Rtk疜> ꥒ xcopSiy:C㪾qIDiӏw4|]D._[:$IR֏w#JN9fWEwHRx/wEL9B0\vmt@g/kz(9-Sr:fDHK%#ӌgOh,.(9)9=^ \]rz}tTAu;f`-#&qI\ik*`iˡq%InjFH sh鄒Sg+8gplt$e]2y qwa韎^?:֮ :oNj?:B%'n/,9Gi`)jOJN_.9-#UHmKNKEwhBa8$)FizӢ{.xJtT3rt^s$I.mi%JRq` 4:Bz_G=G,m|  wk{J2`KVrrBӿWOa\::FZ:ƽY=_"U /6QA0([i4[DwH*9=8z}BtT3kI$B4.+[ IDATiE[^j>]W^~Gt6cSCT%U[.|0:b]HӳC4JN{?n*de!RT*9-|$CFI{t$I꟒MU?GH53#:@֍$I*ġqI.90IS~ sk ֿ{ҭpFGJNK MMn*mt*94 iaFoVՄI d%"LjkGGh|1:b$I1ۅ-R9:@7$IR Ec>\r:0:bA>4qIuiG]( 3z(9- 9EQ `$EUsJN~,b"ULԒӧJN~.a5C%iA4{#&M$ 끟+EH=и]kI$B.9͌T.9':b~3{d; 'eZ7.Q8:־7:DPrZ8xEtc+V&%;2 8t@t HP%C}_llź2KT{%]&LtCKи$I*fIN1nTKGW%-8 0wntjୣcCGCi%:^"ɳAie(-Ӊ% *9\l" s=DHCO DC.$JN3[z#:Ba3$I- RYQ3{d%m<"5R i\K$IOиY!9f쑗[GwHR5χ^]݆Oٖuut6<H-Rj|>!QrZFV.9"n/9EtF)ph\?{{[>*:@!и$IDGH5#*74 Ivp7+]{8 6pX{ѱ13 [ DQi%࿀WFH5pq!ꯒӲtx~tTCK_/9}$:D걁Є0EwFH(9vnC$Ii\Ңgd@ymt$uہ_Kt̽Nc#CT/%u "Sٺ%:nJN+@Vet4h/njӾRU}*9mR 4_DGL%IOƥsh\K$IOиLi4U9-j`΍pף+4@nk7CT/%5 uSH, `iH,Vi:-Ґ8Gu!ZGFGt$iJN<\!ՐCZ&:@$I%-2wJNKG\2Csfl !C?.s.xXۓU%eψn]P'%`i< Y)E7~ct4dPr=:Bꁁt^"U5s$Kt4 Sr :D=EDGHCh8[te74Ng{7gWGGtɟ.qXLP*9m|"::@!K$Iҿ+:DTrzpRtTߊӦpCu TFuWyi(9EH]00ݡ:b|;w u FihGH\!ՐC%I$Ige.>4ݤ#Iq0]pFFJN~̌n*{5JNk ,"_< 8jt&"`IO0v)GHST&#ZF nWi\09/ .j4[s#J?:@$I7KZǏs7̙=I+~ ]1~x^tl;:־#:DTrZ 8phLzע@OFHZQ쒓dS[$- %mC)CKNbh8иꠎ6%_uPtTGH5q%I94.iIlިN`I+.p0zktXSi=`Bi4[1JNK?3*^|=:BrIpzY!q`_oJ=иqI7~W; :@)K$Ish\Ғ:ː†$C7w.+gw.~Btj#q}&:B҄_t5pDt Y Qa5*?4^rZ 3Cu`jGGmR8 i|X-:B;#rh\GH$IsOt `^_$r /I=1w.\u}t`ѵcCTo%ki~AVr(&혒+#p%5A>IഒHt4IM'EGhi4[?!74.JN!C x0$I4?7KKNk7|0ڒ3VIꓳ-F7DJN'EwHFuGtĠ*9%IKd:ݒ!W%wDH7&"#z̡q GTg%_*84.I$ϡqISgzk#-IvuGku`b>1JNKfEHZbgѿ8xit%vXii*=4ENٺ<:נsARmFo+FHsAtTc3T DH$IPrͤzgi^=yԦ=-I=uo -=:tht`Nm4[1!i6s*4!iJFSKN~AQJN;HFGm:xUtT1wWGGH5q I$UȍjapH/Yحו~ޗH'mn_ft}JtCi=R=1JN>!kZrrHJN'GwH3JN+GHPM֌"hnFui TK%c; l#sh\7GH$I2':@Rm3zp]I.Lfc틣C4JNӀaIOpS{/VNASR}[r&:B!k |T\ *:4^rݡE8<: MCjg}S[ :@9K$Irh\RG  7>]0'@]tѱC4T*Qv2FtNgqaUr6CR׽ 8::BZJ;FGhn4[#5:BR'#v$)ܹ 7m̨m4٭VcDh(<7:Bٍf4Lt8)9m;CR=81:BZ*:zptZn { :@9StTQsoFGHCq=I$~h\3)۞Srzu7O5oƑyE;O՟ *~~DR7]ht REã#feaPr|0i8Pr9:BZJ YrxAtFUb{i 4.N*FuKt4*v I$UIO7/7̙=2 xo?%I3|nVY 6\:UФNgø7*qTfoBh~1hJN[GwH\rzSt(atbii>U;-k4[?ret4nT %큭; Ft4$c7:@$Ik$kJN֓kfg$ޓfDWḼ? 6 knFڻp*9xEtTQ$f_ ҰRuUr?6IhU:_IURˀEwhԢ48*iGEwHw! K$I etv֓kh|>]GBMk?5bwnwL[nkkRvk5>>h/:bl!)SEGQi*03ERN!Uft8RtJJi舺(9!)SGGH*14E:l]QkP94.i pFuYt4D׼.$I4:@PرOҏ҇kHRe c.5| x=`m֏HZRk"n 1hJNTkGFG!*m%#$*04^rZ{GGTCDw5?EGHҒ*9!U7!иqI$ $ ϙ=$UM]6f?0z&|כa`YFy5es5:~(:F*9=@tTa6@5I%#]i;$UWKN+EGhUs-fUhnn&-!R= ! OfzI$)oM†%LIz}s[% u֬OK ;o 3g|ꊻGEH8!UTl]1hJNoT93ODG*QzKND6hQ1'EHtit$-:^R}l5:B23T9_$I|/:@P5Ι=2 xK_jwu;ȿqnK>+FţcFH)9mE>j!ASrZ$-%FG 3DwHJNHCxu}l#:bN&4Ң<! !7k~k$I._4T;4l痤zˢ zcn~x(H÷-75zwSr*싍ft I5 Tt;or`KP /9 3ZDGTM ih4[WFGHҒ(9!UMrh\Oh̎$IIRh;|Ʒo!i༬mtDՕ =C@پYtNĦ=ś 1(V`t43GGHl<':B7s#!q-Tٺ _+$IR< l]"im?KSқ^ ##1v>; ޳9xaǎ,VYcC؇|* FG $ #3'i׏U%epsdh$f"`c:R;-n4*a<F"5Ӏn$I.fI*CsfL甤AѺanC|x.O~p,lx˫CncFڏFHRrZxwtTAs5CM)4^Pr_1 QrzKtVѲs IDAT*>}&AAhk4[Z=:sg5C}AJNC|7Xo4[иl] -$IzHT/Yc^<)Im߷J+DWĺ~8pi*o^1:Ft4AEGHFu@t ;Iinū/6[#Y:ؔm-R  :b LtqC_KNӁq--ޝKRwþ (7T@ ܗRq'QF}#1zqKTLhcQԸQ6am؇}`3afΜ߻߯k3{oFeU_ |l2t7Tb9\\{,.[$pr>eU_D{UsE= 8#&I$IXF ~ppݒ$?Y l;瓤X>FUiχ<&:D0N':ԧwD􉲪<xctIF,_D +lϲDQk+c+1Iz3z<[<߯{+vgWտʩ&]giCRV!$`i\RVEt7 \:@F@Tt7sC9in=n=n|_X R[ܻc{縝n9|ͣ$!|0K/sIR^甆aYzH\{,:p'dT{Ua=aYw_Rx'4jK`_t8zjA!xut5r R2j.c;y <wd5!CCHj72"_tI"VVAZ4~̈_!I&[zqmHX,WJ2U&t?ڧk L^l7."i!41X)$ NlpHBi.v IS,_Y-8[q0LD5xgOk^YV!$I; 7:}:o p|>}f8s[g[Hg-}y^yGR,K[`-0;ֽ%I-tqPoXE}u52E)4G}ߏ8xUώNzW/DWDP:tK'gU}Ө^ޔ[gKƙ]Fq>ܲkNg[Qz507EjoҸFgƏskpF$un9 {eU/NjU}7pZ"϶ˀۍ!F޲IK0\W,_FeU_ȳ0Ќ8%i^(]s/N3?m y+<$wOMw" k(2${˪&2LY_V&2F;"ȲokL o4b܊< xQtI"WVoFa]gsk!$Id+l 9%8TYw#~"`!]ϕU}[xXfXF+/Ijk/GxY[7ʢw2${>߇N':Q >N8$Nz_bj<:4,E=xlt]4/?LuvYի_*q%:Zgߋ_h1!$Iz=/:F,eGަ=$M.K(%Q[F[6o-=M(/:~t'Il$`щfb3xcox1lQxt (yd`3)GAF{ZN(lzs˪>*gyjO[eU_2.jރ";Yrndyfg[U,:Ș4R-}[eUE~"Ս &׳4jiȳsjhh$)m%6;`Yߋ"I#a,5r5j⿅Q$%eU_ʪW;Gvg-ȟ/~tYOa=bLOMwn"ЫhdΤ[?1:HW-諝n|ìVVUOU p`h06OKh$~ |^Y+U}&pfgLwxNLgXa.a,EgdJ$)8nm1iv+L8_1ǵ$ɩfSV{>N ۦ?<񵒤9d<v'/8m|;65‡Zȳt+].^V A4.>Ew0Y] /3 #bSi Ƌ<{8e7ϖU0һ<tϭӸ\eUDR&<{oB$ ߻,Hy0=87ʓ6I\X^&Fؘ/-tG5,{$is |]Zp9pŒѼ֎s~h^CSӝG0,{r?U}WtM"vΡNyxt7JYLrxt1ɣhh~ yY՗GGYw+쟀x7TӀP,Q8rϣaY'G$Iymt e˪mtI &E>n!_1ǵ$I#*^q`xUg>'AEmUVAFx]VEg<;x=A4BGbi\÷yN(l'MÉ!ZIwSITXo[H9i\ڸpFc,k/IjFʪ>ȳ#hO9IlYO>-b*Xvo]{.^85ݹ8:4FƛveG滶6BUAW?ȳxja\ f5I(VVx pit Eۯ4Ӏ6VVtKh6ay30hkCH$yZ4,Js48Fc,k/Ijkz1wmsh&rca|$Ir SӝwLMwVF@xqYo/zyti-}8bRU}:Y4o-y3"eU/2NeU*G?Σ4ax"϶1,뀏G$IZsCh`UVuA$)ئpn^1ǵ$5棬/'FA. 86:Vsh>\V˪^$%eU8 gf{Rtʠ_$I5Sӝc;0RMόΡ9 ڲGfqutٟUMPVbiձIԧۚeU2:Dʪ^X.lUl ey{gtI8*:򥲪1:$%jRC8 7kQKڢ5ojҸ$I8|j RBBu&𲲪ݱS粪IzƗҢ!EmShN>)ʪ Y7'k43oU}FtI6Oު8 xKtIJX>F{k58$-vIiϹ4Y$YSӝ3H92:6"eU/"́f88::DJ0Cs)_ta)l ΡY]AUtow3f٩ȳ}C5*y<1#ʢ,B$i=<9^ZV 0K8 7kQKڢUEP#4t`AY$IHߢ;a R,xm 첪o"mLgΡx7 /Ρ9;0:J5^+WU$:HU Y7հ;iؑ||KCH$52 / 8K8 7kQKڢUs굆bqIaSӝWNMw|$Og shV. = 76k7U8:D vMDЬ>^VI!eKh֘siȳHE PtIR4K7eUBҸ47584IJպxY7et 43 "I$Y |jAlB0GEFRVCAY-+}DF] Ct6F IDAT(tTsҸbSއВ$ q EgCMp&ƥY`584S??Iii]itnj|__t($IwptA=.:6ToCH}rbKVayʪ> NtmT+&ypptmбeU-ʪxpGtmq ˜>+FEltIRl@4,ץn9geU"I $inǵרyqRjV˪n@3IO $I49A4W{ȳGDЬGhҽQjˤ'[Fzjt 7uyI0כ*i ˪':$uRF۲Eq3inǵרyq4.5W+K=vk$g D A4U}etOGEЬnʪ|tj"v1O WEh CZ<2:ZaŰ"h Y4w_!Ij%'hjX,c!$a,Ks48Fc,k/ƗRs4~~t s]?xC"IR[55鎓9(lZ7CH滴}j7DЬ0m)^+!Z!4GGP+妊 FD}벪WF$q[g{&W@YNXbc^1ǵf4.5q ~FK㋎_UaxtA>:qtY՝R?< xZtmНCYY7C:0:|y pXtӰF;7FЬV" ɘhn~WtIRkyåY4IIqinܘ&kQK3u`Xװ,IvLMwN"5h-mChRVm!&NYLWKtϳ;F:N/=*MN0l9ױnz'I!'kY~R$inǵרyqRj֖˪^cB,-~`.e' F69 xt RCYO˽to1A3!&AYW?Ρ : :<=5:S&;+Ch,k6Xgh~UVCHZmj"όΡ ߲BI8 7kQK39VjFbq_08?$Irpt RYO˷˪8:4h~e jqKہ?Ρڵȳ]CfۉXKͱ$IKt5ޣ=Ch>@Ҹ47NÍk@j M++N$nxO R:$i0ƥq8Fc,k/=Rs4eq_IN$M 'NMw~Dj łȳ}qT&ԗqwT`@u|ۢCL%=5_Ƌ<xK@m eUB4,k4M+/EX $8Fc,k/ʹstIDD K$cSӝH-bi<˰ةf{LtW끓sh0GF:@|4:ֱt5&~AAD$M KXgVﲪ!,wm@jq5jcq\{i&'K͵utzQt /4.I}>`j7i@k\VM!ypbK~YV!&Gz5faKiItIWVOshcy5,Z,{t5ZR/Fhۻ6 5p584ƥjeiȳ}CsLZ$ ocj2:BПH4974`@!8m<5vZ|0:$iP4(7:MӕDhN":584ƥ*:7ʪ/_΢ Sӝ;H!yrbKN {QaxdtpRtTV9qIRu[I"<.:g`ZU2:4i4qinǵרyqu=;:m;hY>qˁN$M{LMwk4qrtiu\4$:fآȳCxZF5B/:fLYn$Ij7:MS@U4qinǵרyquY5"vR휲 -/:~$IRZ|#:4a,a)pVti/=D E:vЧGD[Kt@x n[G$M?+!(nt!͑9iI8 7HYՖJ5j~qu=ȳCH[&>:Ą9}/ŭ$%,A _t+?(Q>*:ܴ::ѴҸŹt@t&`? N|,>^V!$IXكC1,䲪!͑9iIXlk<}37Ƌ<{<h܆p$->5ݹ::44H;-p2NO˹!4CcJEm<0:;sy֦ݘ5^Bk\|&:$I=;,mchn"&46K]ƥqC ]qǵ`3!<>yU`,h %IJD+;&.(l3`,<窲/UoLMcJqrJJ,tY{EPc=/:fHYKCHC?yVx6ȳ}sh^UYkZNti1\wY^ڰ_-l 1n.7TO(f*X~/uܾn`ߥZ |j ҄{Y!y4 iutФ#h ˪^BZҸ|W_!Iz<Ԍy0JA^gFyC#=NÍk<^,. EFҬ=iȳ'1~TVmh,:p۝pmBͷ-wR|{l l)l)lٺn%=l ,pQ)uW/>:$@jwTYO4&:f5:@,kʪȳ%Y߁WV$iE8kxU}ct>fiIi>k5ݹ>:$x*N XjL4if )@jK!I,=>Y)t g`|=@8#:'KiN.'鱔u8xq[XQEtKK;ZV$ `[jyv9ڏ+˪sA5RNOϙ>Y9iIirӉx+q8KxDҍ"V- :s}JVg ˪nԵ"Ϟ=:|?WZ_iyBiV3W%xkJw[¾{{޻æ J xtA$cbxZ~@4&@\\V5!4'F{E4+2:$IC% P= :F^ \ *lS`*:ai\MpZƻ4&qn ]qǵg!}tyJhнFE |7Ջt|IJޝw/{˻ a]'lUt:Un^5591:﮲! 瓴@:ȣChVie<6e8i\jU<:$Ic%sֹȳD~ZA:a.5CH}6orx,äi1\wYNYվ_ԏHq )yp"޷|IJIg:r%\u]q9A;G'ESӝHڠ]!CZ,5Z"vRqrtY#>șƥfRtIb~Eϒ'gUx@t!L&4^yHc~@ڈEؘ"6<::˄MYK$NW~!l_+t{ = #:H'SKi4 j^qZF.~ұmg[U}Ot;:pS}v,Kɲ!IR l{Uٯly2p%43Hfwp5g1\wIrҸRהC$3J㋎_9T,ˢKF[rc!^n8mSӝA$mXo面s j[W\tT \b#9 WU}etUtoV,:Ȅۣȳ7 !$IjYD~?Wʪ!,Yx-%=D+hV?i<:6>,._"T1E}x}taDҜ@q\@sr~tфx~m"aN!$IP[~]iE=1:Pù_Z! gڜ4yHpb~9i\ۻȳmCOg-:Ⱎ޹@v o`ل uP.:: 'NMw~DR_,ҸxZ.94&LF4x~m4.5˪vIȳ/ӃZJZ)zyti>q'Gz&0,._D"ϞTfQԵ갞IJB'$ _ -K;N\x. Xjl hḿ7:k8i< \fdtIO <{Np&+:fpe5Ɲ4Ntû|cX*ȕU ϻ/7V$U/etZGg_+a=ƕ KS+SǯsMO/ =IcstqSYnɣ64Nǖ`Қxv iNCHuȳEi7K3TN Nq'Gz,7ưq{|/Wjd=y&j>7'sҸݳ >/:E ~x*\Țˁ7LMw15qge4wj=h+ʪðf!:h…UotNƛXj{E$IlgEmlC3\@ϴ-'Gz =qX?\R4&xFt"nSʪ>wO/x vvKsq"X[Yu:pಫH#w̩ΗH7K,-5,46t$=iȳ9G n,ꛓӰ]tIUU}etIQGy!ci\Me3m8݌Mq'U':Dcm\sIrҸ"vz"6) 2hVC23/n='6溛WDhNN>W <595:4ҐXO4g䘎Kt k3YOq)m+F$Is"ϒv0K.:4 KisҸS#=͊k\<5/' ωx"϶ڨkjOji\a:9)e*pmIpt x<'-R/\N'7DYwˢsH ҸoU($5Ǧ<1:Hu8i\M2:ff1,lj1\wjVj< qeU Wcueq ip$|ȧ;wF4T- aYn /ϯ4X>B$mW ZJz46';iwXhlKiH|_CaoCפ|(:+l`at,:4n@o7F$˖ܔ,󘔞ہoF$IC1 4:Db܈OMfi`2wqru=I@GOAE:|WQ Y≈'nZ(ȡA\lpE#A"G$L2GLW}Uz>AۚJw]oK=l̞a= I f@xho$  5:${,c[8Ɩ73 >I@i<>NPf. #43qz`Q^kcljR)bĤ3]I IIi`xv:*ă?(?&ÍQf,Ȏg9ƌG[AlsZq=bDi;)=OH:.I{Lqtٲ;_ڽeiSoZP*\<ƒʎq` nyQ2I3.a4cku"^ NJ:E7 a?P$Qf7ƙ43 qc(֊6aE*@w,$n"1i9.i emt4q0:$IYq` nywXK,c"IC`(ǃOf[$Ie!"Bi<>Qf8ƌV!~( {ĈxwP-I:s(VlQuJlnIگ`A'ⰱu (ǁ)Ťq{06:Va$}:7Ei<>NPf\S盘43q`( D14},IRGWyHu[$?0:U( sl=q{Lh8gP no g˃"CA9>LEq>#n3qX<9kxl814ƁVf&J調,N[[Wr;Y@|RdXa(0nF(ǃX@jc=ä(LJ)(3 q㦫ccF#N(xlsZq3 bDi;/%:p(k{\zQe])T$`0QF.l{LhR^3`'>bt4-㕤J(;f\_axX'6mZ+7HH,PG,u޲ iS"tx ><ƁI㨂(?H PnuHb&14&&!@GnX:FMW43ĉ c(֊67BlKZb"ؿ"IEf8bJ{Sj .PŁ8e6ql4q-]MWI` 1SҧCp yǬ`-X&)ƙ43 qbmC( ĺ`fqf7#Й% IG8eO[͕6:*RI g@pr(Q\V9¤8pX87[Nq6&i : wXm;ICDx624.麗ƌ59q`( {fu 4j2JN;֛*u ܠ ?0: JP IIG%ivuvPDŽ=TKy80NOJzeYPiFLF/;(&Ɓ;`c,_ˉx<(峇Yc:@8_'&(Gǯ:1n:5Q^kclj C*CjN:H(cn[䯈B춽զ)PbH:`'AT8Lu`"4{FRĹ24&c,_iuBi(6FŤQq9t&Ǎ59qb*n( {.B#SIV 蓴uۂH[@]''pr({ZxqA'cZNu㲡Y#Z)\u((ǭ:!&Ǎ59q`( {Ćx;-ICtOL8r7O:r_i{+%?0u=cuBiU:$1 X' 1_ikXI@y&xwu0$iu$iB%>Qf=2R)]<8Q*El(wƁ}9IZ>-LЂZ' .͞a%R{_?0u=C4*`zK[>X/f:$q|-+J4ۗwSC̷1WgkPV˓)mu +kU4PQԏRĆxw[>-L3J NQ}m|?k@O|<( X&Ϧ )iLƊr4J@*L`-[3U\8Q[/YZ("z+ I/<@<: J櫒\l@P\LaxbIiג ޭ'iuu]qtM!$Jo0mDixqOZ-7ƅl'i0 .ngOgPj,~WXd` ->[.{[*w:=I:D ~^vu`^4uhefX[,7Xlx1i1xJU!*80{2]7Oc/DgNts^;QU&*:0HZa4^&PBbJqZ$bpw[|:@Ie#ml˥q&Nj)X,7XlxQG >!*80T%zzJݳұIS&['AIMt[k4[!NvQJc D<hxPG;84:Vy:\͒tuzMI]a$:UtuC㶮uC-C:@a{HފqX$}:DEChZ`L˞~:E5@zҴ^>nB%zF6: qYGKCpuHQGǬ@Rs4h ^;>xMS߹"IAIwXtkqc8bĤx1\Xlo[b1,tJf# 5`44_L Aok_okspuHv ͰPI=cb [$i}C-{[Ɓp~nSTX=IY(۬`DGX:5>Jbx[,7Xlx1KzFS\ŤqnMfK/9DuTTS%֨ը&[ӸPXƉxjbxwI:$?e\-u-BT-hX\nuƤqĈIbe(bEiVJ:OTq`>I1Q}VX@V{:Em)x Fvu= Pґ!qJY͂?액4~u5rɒu :ݫCP1OHzEfOX)!&ǫ&P@4[GTEɱXlo&IAѷ)w(h I%ivff<)_Ɓ X>qmrI&Y'AUz?ac8cPj>"mxPg|5~GF~@o x7:P1Ib(@4{VYNgx(ċy(ⰭEEY&c%i{0=X!&Oғ!{NP^l+g=I77sa nwLE1i<[#IIT8{sw[Y!tuXҗCPJ+I묃n7cW@xX/&퍢+:^LGH:_q6JI=cP֧O4urr;I%ըfTIgKQ: 8m!&Т(cݮ!0`K-.quq=I3.\g xJ]^II-SfI= ZlL@/&#FLqFxm/J薇%^$.i$͞;4'bb& ãK'?aMsgZtI^{w `@e=^'HuCy:$IK:&iwX,{ńX/!0X 0ݕI-@ <)G.t7^$eSKbhŤqĈx[,7k8lxQG;.I$=,<4o`xJzJZ'(v:0$okߒG.nDIt"I%͵Iȹ!0yjARdbx<^['Ig`ݖs`J@Dҗ$Rq IDAT:Z,O$,I'l#U,J(NjIdbQ4^sa[NjUcpzbFݷ4+ݍm(cL?jI;Wcn$gCi8Ⱑ#%]ak9:Bim'.M?Pj^wYz,^#n$_۬`TG&ivu 㵾u#`TLW %Fxm/&WǠ%ݩI0ʈbQF7JuNQǘ-NP#/muc3$}Aiz]e@%dQG9P&̵Q7nOxQ(h+/J{$~equfOXBiF ֒NtuN8F1=C:H 㨔$59zm}0Jx =:!Jq92x7:!xms`EĤl. bI3C`%iVc,|.xwu@e= _$m@f$iv9qPߞݭCm4YJJqcx(TP ax=.6I$ޗ/)[:D'ĉGbqa+ä Y'n%-hk(Q:N!e8:8:ֲ:|=xu@,I%i9Wb8I=,90[x$mlQZ$b8bx|J4PfҢ4mKIZ1*&&#&DKJQ]ӨT(U,F*N>)!K`4+9$͖Iz:xen+XEJV7G7C*cW&i$X0m<~[@i;wC4M)x7[T+zF8X@a.w(iI'$ivuT q} mm1"˶Kk07C%Ԩ׎ jeً6C:ֲOpq]ax|7Y JK \`sue;I'ZFAinj^bk)[iIqzon*x7O<X]I-ʹBҹVX !tKfP AL,w[F͸Zi| =F8JQ4NjIX@%Q:!Bi#ZĤqtܡX'vF^;Qo@T(ÖfhtHd=IZZVga|61=)qos4{$-6΃P!SIYI΁;:0n^+3i<~LP*Eؿ/J~2J@M1J/otҜYIR*lI k8;x7:ІX>:Dzu T$tu ~&x΁aQG}?Ie|I/m$LdIYʏ}:0`TJqTZhaq{߱ʢ4tnWI1>IZ@:w^tT$@̕tI^Qc9J1[{CcIls`Xi m"91LdI_у^%zI]%MFY=;R}+gb80x*0Qmb`:FE8L"Fؿ/J3,n GwYI(`+VH=n:Qϗ]YJ IZ]@ # mih0vcNu$x?[1XFBCRZ/4K$} [ƒI(uXTY& 1>47VeEEc.w8a$x/`{ $}:Hu +^ +,,hkg6ց2MWCm4ɒ.+w}lú:8QgwXhú9I'CR~7ʿ+ ʁ|+xu@Jn΁m'!5PqĆIqTZg;^m-J4w[N_+1*O[W:z0>/Fvu8QT6/ ~o:LG8i9I}MdAmk4^> XVujO:BiI_΁Q4ޒYֱ];Jҋs`D1 ݵRmS%=coCI F@F|etQnuxP}`LK47~?y:zwqؿEiPim)鳒t/ElI:&cDͰN]6^vR<(̕tI^QqZ4^^Zh;aw)P sSr8Z҇CTI&90k4[nrwfI+9A(1 -91ix:D8|Cؿ/J㶸mwvO=I]ٯ4{GfIzkQo ͳJў;['莾>i.҉GHsfY0$ok_okXUL/u%Gn 0(5CM%G'x7Cݍk;I2_uCcCTIbQd:*Npf]L̟Jt3C4^)3i<~`LJCEc.w8f<1iI/Klp2Is%+iQ0}.F19N11̖Nm|h 18 @ؿ/~78e,RI[[gA[P::vzKt xK$6~Jxj*]Mҷw3bdt TmI߲Ei<~U4Ni(ǏRiq;^mo OƋuuf?J&I-IRf2Y:I/;L(㵚+_̷.3[Xoa`c]sPG$4^N ޽:D|Vґ!ж*Ư::2IPk;}W&YЖY*I-V1mKI_Jˬ`N }:z0O:ڲ::PCؿ/~7b}iaftF ?%o`ELE̒?Bowi$Sבة9Yy(\Iߑtu^PW M+-Iݫ =`+Y(ɒim{\!&*I'%:U ~AIGY@[L.;4Ѕ$~*)RyM5!&[@5$ivs`-xwuĔn"m%memax(i(wqؿ'nZk"? I٩B X=9qt&W@qlI/SmG8L[Eؿ/J8<1i87&iX~Э\ΤJi6}t>k_,f|)uבvZ:p#&N9>IHZШlk9t /︃!,]n6Yw[QnI)i=,tU0!gj$6%Dg˃ϒCW&i`jojAfXpn4^ %Wn7 )wY^mۂwS\{r4?JšT;^mqy4^+C4["wW[I^x"o$iQ ?@imgPsg饇JbyҜY RfH1a6Iinu:mSQkiM_IscY被I'IW6^~%iumX@%us7%]ş/.3,D$iI7[@Wl"YAXhczQnC]vͬCm`LJCEc.xqp=5bn$~#KyY~Kw䨞>iMC^f#SyI[l"@hFp#^}}w6H1@{vtE^Q`ZnuxIWﶲޓc΁ {UpMǂwy7M`|*3i59av=Xkݐx!IY@%uuJIgy'w:qtƫc3I d=auiCtٛ#Lb,OҌb]osתּRU].LtTIo/Q}MG1|}v' /G==vvR>:5;^RQӨט| Du9=,$=:zϭ+^$ݶAP88 gAh]%L%]{u+$mduTVWNz5'ܦJ^n] ±FiZY:*.h{wxк! s#UUEˬ ]xQU!b I-tqU4Uْnmkkk5@$ImL\n]ӯ (RK$B7=* .?w +U`\~h۳s(ε( .UIeX@Wm)wYA-lb*8McY 1JQ$͞Njߍ-a"OWIAҗwSz QԓP#Fu{z. }-x$G"I?K:f_. m`&oIJL ˭Jz:O'%]:Lwt($Z@euܝbIυ^dP ~l]w/bxy|0x7:DfIzutb0VNj>/~7ֳJ[iGܚWI]b\ 鬢lUi` I I7&a^ٽbIz[w;9jIz?֒GҿV-Ijr }0x7:LwӂwT&r[]j"ۯIcN?ũIvnu@aZ@%:r1KҏwXnOrh`b.Mah b0VNj>/J㶶Jcx1r+'iUg6+ۋxR I:E҂Fvf^x~`H%]:LQx5m-7w3tS$I7J:: $!r:jް}tSnWI%: &dPR:*-xf+%!.WiP\4\eoV󼊳hhiCGdI΁q|( ŠP +`q[[Y@q}yN $ݙs`KDO$%\`dK:Q`=ے>&$]X҅RFn9ƫ}5oj^jxcw©R mɹ`] 9%j5*8 IDAT~B!w%"@{iY0a%i"H)IJwϿܼ^;=I rGiyX@nv4?x15-JKv!жK* qKˁBi1( x1qx1Ó4{&x^1 ^RfI+ vtE^w aI5xkIG[gA&I:Uҩ՜$r[o-x7Ii7^)>kwHn.$͞ ])X,#dݺNt(<XOww$ib(MwY\fE7][%.Olmfykǜվu\+,+JItef'/ݧ$}<ώǨs̋r4^Oxޘt +/!Owku |>ϑ4,'X)SVhSQWwy 'zTIj=[,FIZIu{kL5Kݳ%sSŸ{l]tz2Hj ._TOQL$FG%h9:Y~))1}WLn5c|M74q!wJשyUTo4{:*-)ϹHҩ*%}BL *tEk4 Xһ05I/l=?%R:skFZ HҔ&Y@_g}?SE2* E- lZfF{,IifK-t7" wu%iTkzuj=Iݟ< I3iG %R͢,^e?+FCIޝ!jU3h:+x$(@u!JǏDiWMth񜥭b=jN~Xej^ZXw|L2L3HM͛n%]/Jչ~CwZK6xwY~[ϐ)Q^ &2=R);n~l͐$]h:@x{<LjxCJkkY<W$i~^X~o8ܢMD݅IqFJDiW#]f$r&S͢Iʗ[~,JlwIH:'xwKYI/V(25HVYTHi\46xH碞IJw{$iVkPw[&ivuTεj('#t6_m@ IΒ..sdnǚ*Y)>hǤ[Xaw{G$͘ n5|$Η"]5'Kiu(>IHze^ K%I!xIrY AW.4лK: &$ݔٸ6j}%FhҍIb ?S:A`#y|/ixoњ|环$iv}^OBi$ifMT?Xaw(E1ʡ4?0I<77KG%?8!Nkk=ߒ@E%JJݥlѨIڹ8- #>fAq53՜Eo[(JfB Y9k=:EվT3M%B Ɠ4{(xaI_,yW&~: .N ISt湩Kq(JUKz}"I-n݈Nt_ j::B4Ja;KlEn`MluT ^oݜcTscワ٭#PHe^w `K$+&Vp`$ͮD1u՜Puڳk`8FW}tNqGtoeIH}_ ]SzfK!IoՑw%j(~A 'I'Z@.U }[saKlI? x7Ytw!'Ci\(I?=+iuwNLҌ}Bi<=E&yFϏLYI/]mk_'e٢ݿKzu"Β$ͮ .iG,*$Q+$%EIq!Ex'lM^x~nbNlu@m'O%iC0EiKw4[h&g7Z@fI: "wXңjX_֟T>4̰uutRi +c34K!xf KX'KRf˃wOeϏ4xwtf4Jz[Z:Pb:P'(J7%T޷UuCgq?IJs#Iu@nH4xwm_8nH,?9%uOP$Q҇%}Qw$.IIt(*& -`LL]{KnK@(5Jd6&IJё&u WovUb5zm0@%ivY*O;ң3kI?a[" 'i:zƳ~h;: WJۂw'wuI::DZԗXJB4Ja;:W\jjNP=?>{>0ѡ*$͖g8K.'nkokwIھ[O@(i~^X9^oh3}8IA,%i0xwí%ifY83#KzuruÓ4+xww[e@WINn$(t x'isI[IںuwK]jY$XЉۭx$t{[տDrI+:kf,تtXL/UuìCJ;~=VDn=>,IEj|?ssk~MҌrq5qn:_빓4cI'[g-x$Z4rH@'Լ+g&Ï5! h41I$c(+kgT,젂BQ%(` j,4#X#i"M: c–̝w>k_ C&ˏ#4S׽x ep&^m%wfźmz;V^2$(H_YMnDVn௯~\f)rY=D6a~ %-ȊPx}Wp/הr~xetp3:m\])fz#rz*w٤I_?C=888)jq5sj3֗{_ݞVYh$MӀpw8c~Zd)^c@A S.ߎWIK59'\nDm|.:$B~~~{80 oΌ6:~s*2PҼl<8{{?ͪ=$C\7;4/XXsX$IAA!>i-R.'߉Z#\.2>rBtFx_tfR]x# Iό~{~!RNL*3m xf=sp j~~{:H!F^UH!^<i4xyҸjs`'F!u4e^ժg4o._^I'/ K۸:jƥyCt@M|X!.>Q3hNTFZݤ8 x ipf?{EH-n/:&> ~utDE Ec)oDwHjXnlMԦg!U蔳-s4۶N/ OZ*:"ߍT Ђݡ{[׽t${ݍ/nnvWC$-ȟ/:$gˢ# xItĤ\jE9T(^?K5k:'v!37ǢN_ixK4-[G {GH5vxmtDMxڸ8&OZqIw+X{@f~ItxjtG{? w)r-%^5m )ԩZ+fC]=sh\hcF6v03KYl܍/$Iip8;tÅ^)VҤnQ_Ί8{Ot:\NatY6SU{ rxot&Auҩxut~>!'GGTqiyx3]0|v(Wϡs6iJ Q!j*F:/mh_,E$-=Ӈ N':H r pntrYHZhj3)s#4x8\{K}`tpi^u7`or =Cj6n spYt07CsTQ]u}&Mn^ڮV-.м<{!C$=mciK%'2I5~qtD |yt-2)__Tk7uۣ;4q{MCxyt4O <+:B҂=v#"\2p\t[GGLxCEwH q-CsTQ]FYv7eڮVmS.7м܎n@"I&d[siʾOIKsO9ßo_M0{#$Ֆ.j94>v(P#4q":BZ60*nJKӡ )OǛ y*׆B\ǡy}ן׀fn':^ժ߃瘦xQfO$6^ 1tv:=FZRKP)ߌTk7])Ӂ/FwHEGHc Ub~E%:@Z7zfsDGDJ|9Cj~ sh\MGGh Rj94^u|v/!3I)Ewh^6ekJ;4lZ$I&f iR._ZA^_#jMIє Nu7Y~i inxLJ Ћo_EwH:j|BC7qTxgO@@~>:B~{ 4pc$Im 5t:`wT7DA4j2]Mr9xtZ%區Ox[t= DGHIh)s'i\tZ %2:dQ4ƫxWjx7Yw\.>ݡy#F%Ijpw8l#Ue|;xb׽otDM::BRrAҼ0:BRm&2:B?N |6Cأ>$:bUS=,:@Ң>65xI)c7@y}WgueEt6<Մh3.^NMK|N:/:Mtpu0oIr:d)I#\!Jފ^wyrh\M-\%͂{{eƛ3CjDGLR<-Cqh\Ѽ3fhs6J)x14r9rtesM_|,IIJ݃)DmcIK\! ،88?:BRmp'#rpdtPFGHkш,)Dwؐ~yek IDATq5Ki#Όn  ijo+Z̡qEsh:Fh^,F=:` 14>?0yym4pb$Itl 0t>9t#MR(JZ?vj)ã#Z數pCeOfUt\J>4:b2:BZƥf:H!5\>0:b*:BRVz6p:>{T4x}7Cȃ&kZr)0ߡc"Ih#AgᠳIt4ARVK|NtP1p\,:4fhkt_L|*:R.+p4K;rDtjg離;T#/%=,5S舺H|6Cj=:bƋvόT4VǡREw3jvct@X}4o/^?sMF$};';JK^#b<4zdt: U(c࿢;$ME}US.)SmuGpiu!iQEk#!:TߏD\b*Cnky`t_'r$prt1ڨ~'GKN:/: i1R.C}RuWFG4+nr::dF0v1rit P%uҬ6pN 񳠴81aRʥt3TǡREw3F'мtXƙ ?v2-$&|8~8l#-һӢ#{q,:.R.2:ERrYtĬ/p8Km˷# h/Dw2mn~ 5C㷒r>[ ukGGh^^$Im-pppйkt)[GGI%߈To\\h;ea;$Ut<4vh|lOhxb#&(Js}tfBTgIҚ0Aw|mt6/qpQ$ImpppyBt!5?{EGIw8L/*)ˢCfUe<Iw<}ѶxGtm2*5CR3uDGQ$QWEGH/Dh*)xe 4.oJ`˳o7$Ik5pp9|8;:FڐSC]8--Ϋ&EIΔ1.r0"i~hUjF4r ;Cyy׭Ðݢ $iqEGH />!i"~r9#:Bxu~ WFw3\k=[j3T[~ x-BD;Nb$Iᠳp,:FZEK;ڮ+:^`ICI|'CҒ\rifJ[ޯJxctڪ=|/KgurY d/iZ74>v pyt%|t&j\k_JUn Q tmtT#mzIZ /44lI͑$Iu)ppa8tuI|4Cj]#&r,%OjKժ>!ivJ::BZV \VGw2;{?nh6Rsz\~.Cj{֭sH\/CҒ|):B5*.ph*^Fvt5ˢ#A_,q#'#I@:DHF^uQ7)1w'EV˅!Zt_M|!:BZV \՟7ifCRsy\>|=CjGTF>r(:Bp:ws8X*ݬM!BrPt6h#`[B94.Il8q82t#Ze[3kGrIs[Z/`4/%:BZ P%^u |6-pl,5Cr\T/-ĿETah?;$-ʍ>CwEU^pTYC\OE34~"pZ$IRu3ήAg R.2iia^PW)C;$-)#~)!i^NvHsU\~ |!C3Ij;{-#$-fMrxYt Jk)#TJ'ρRՅݐr2:BILa`W cڠE /[>ē$IRm2t#Re3Cj{DG+m&OIPtxZEj4'":BvNE|?KIr&!!:B~!inSΡsh\u=y^bf{-#&ġqǁڠN8r!$:c[DGH 耺JlhhJR}r#:B zKF)Z74r97^iz6 94.5C zR6:*)[$'R.DGR;y.U+!)kHu2k'at4^WFҭ1.Ђ84.C{*;:@kե4p$I|p&:Fa#[rEJ; K5_R.E(F?gDH-r4T5#fqR.?Dvʯj;{M#$-'h|/[:: Bs[r#O8<<č'SU7|59LyT-Ik8:`\3KJtnsK8q82tK< ^tT#\Όh'4iRO\X)2ytO{f pи2|_K K<!E;DDI\< Oa5”Cc|ΓKͅ-Jڵ,9.)S~=i|_K6o{,pitdV6\C妔K[yeՆCP{rfF44fZ0oq_Ldh|r_K$i=AgᠳYt): O#&W[KQ\V\^ #R.r=Cr`kz",W\\|_4~"phR.{/¡i>ryctjϵP{rƥihܓƵTNQ/oLq/MkI$˦1tv: S]+pf &DgbFVy:C,)Gzim~ <"rdtlEt@c# 2l<5^w#`i4%4.5VLI -ҔK\"5/n0:@Z뻙n֦Kl)U;fJ`y[Iy_K$iC$ᠳMt) `{f{afˁ)FGYeuxMZg\Q3\N OtT#?)N\%2aCWpll :F4.50:`\. E"7 _[Qt0s[R.;KFǓR>@3] jڵ9j˷oEw̘\ߜsg'm7J$MֶAsOi7:bV\ <O`0%)]c2)K'0Z%Ͳ/M\"\Nݡ Z <7rfk{,LY*I E_EU76x.ǵ)}CT[Ny)+#[J\\+ySyҸ4򇖭u)Mpiy[еI46.Ibt3ήAg K8@n*r ]Srx,6jv ]7HNx:nٵW)!R\P{uxƬ-5'H<ht4ߌ˪;Ύj\)Z):N83rctœƥˢ&C3-)w:4.Ids 'FǨ]R.g"Mj)߼x ( `7F'ܢJ|89E+)CR.7/cse. lpVm{[jƃ7{5gtT7# r@S-R xhgZDwC㪫Z뻹tQ{\Kx c<&2Ә^H|x.UhW.pdtR./IF9Trӳn89:6I0xb +Ig N':Hr`ivКR.+S.opVm+)w\4l܅I:; mc) ,OoL F^l OcR./ u|rytwFw̨\rBOcV[ޖZSQ-} IDAT>6Hhcν5Rr\YҜr>:BZYE]tThD]R.R <&rB*NRE_W$i8t:wQO~_GH}7M5r17EyGH[q)ק\\hTG۵fUk\N\#ק\\J\ry%i)#C$: >elr9upVmu~g6踔˵Z\e__ {8F7FQ7)g0XjmS粚SҺ\z ͡q ~Ps>)hSe.r$6pybt/rm=LGSR.EhƋ>ԆEHq2)]LH<xn)Ԕ1Ry:\~< Uk\Nf;*:@6@;-<\ <9rytHۥ\x  j;[ʂlR.WFw4;Uw'G4w$K0'*oAg5_xFfދUOJ\I\r]T//r'r}MÁna5p)c{z\~-lrT2:@ooB'oE~_H2+R.7\>h{NrYܣv9;i_qZ5.3ωP;\QsJ/2/V If1jˊJtfڡ=]R._x+pepfەSeut)\<8=G38!)S.EH g29yܛ“fz0sIN8:GO2R.\^ l |цRdb)kj/ _R.+#x)qf)nvpCtZi]r9aR_e%IRl 1tv: 5[C]?hvO$rct&/r]=fFN^zjK聈4M/H<*nNN}{*F{uS.Ĭ6-ۡZYc]s\vI$X)GHGh&848ߍn&m+Mүl3㳣;N^ n\r,U4pp__$*wᠳMt-rSV0xf!^岔n?ST9K){,2rpϪލN:ENr``~R.O.nik}۔{~$foV1:S rK|{GEhfxp+!ZSԔӁGGGhr>rFtfC ~t4C`etD5qUݖCE0:^)Dw ]ֈUrOҡe~ x$Ij'Cݢcl)S.9GuȔףC4])sS./ftj7sN?)ociI\ry' To2:)kc\ \+7}ytb{w*иT__Nߟ~"͌}G\<R.ry ~ٲfH<-rbtf#iN){RCGw4%I5kVOO8:d ~ 83:"Pry{ʥuU4)$IR:AgᠳIt-rf?Qkr99:FqƧ6l< oi2SS.MfV˫xJeY1RK94@)0ZXۺ 8*AetT1R=] !:BrI6TmrsNj˷mgJ70:y)ٕrYYi\0MEGh".gR.ߏn)끗4)gNЦi O#$I9~ 'FǨR.[me&lJry$:͹\"rVٌ>7[:xM#u#(M`莚Yh}R.;\ڴ SvǥzzcSZ(rU}F'r0:H rKÀݤVpܨS5P]3^7|,:BsBt42?%:fZR.EwR.G1%WriUsW/$I>7/[>C&-x{/vc4WoJ8{|v/[Q%E'^;VrqUO\>u xGtK9;R.gDTSvǥ;U+r.~AIjoN<:Dr~@`w`9p*5AS.DHk]FϑaJxX; 2z~ &LF}e~F;!>rx/ޙ~3_H$in>{kc^ьǷÛjZ9 oARu] Q}\\NމzM w!+]t^C|E_ޞr)!UO*txu!MjF':Du _tcϩ\~ܢ)o%s ?reEt.^πKGU{_ON|+:BuO!M<(:-muum"c)FLeh`8 ,ʋI$Mve˧KufxsJKuo<'̮_1ZL1R{ݍ+Mpbr95:FR.{GGE};0K=ޚrYt4{*tlʥ> 9x f[O<48Gu.pFϏ{ݻ2)9wXki;j[)q kf)kk[~xmt4O7gg7V2߀~VF։oؾ_z$ItwFs2t^ld)Ӂ0J=b `) c)UW{0>0 V>r^t6)/{G3Z QhP)_HǀWNP^R#ϴoK2ew*{\u \nQׁ{c4<(JF/JQ/ǍgYǁCS.GHqh|C)|ЁVzp>8X+o79~/rc Stf>?.cܯN y p$Ij9˖>:Fu7bCg[ia!)k[4]mP 9KOO#͒~_+mv) .Zx<-6> l2AÀR.?učn\&:b]v>Y;MI`!P&Y봛4I ה^$LAIi?i@ߟEՎsh{߻p>?~w'yu ghCd-%"on9?#VÚ$Q'K0zIi?$)V%DM/ᭅ{F+vd3'dhM$W0z 3i/>#^M$JG=LC+fc g0t̬O`&swyKnj^$tÝ#bh {(|cA.hg'EX.*Rfz&i+~*ɣqv%lO{`E_Kcc#hs2K`L^sB&?)"ug;65;6}?:G5ni \elNrq \Me&G$o.qwݼmcfUƓda~nMǯ[^sj珎´חu;$ωrII;(`=au;JsiȟX8[}I7aNMr%yPrE/Y^s$w3Ie\GKu;v^8bWG]{.=_9]{zmJrpJ]M;@LNZw]#I>i&Vfcq%b=zqI.I[/v>;ȗa `g$tJIZ;r;[r]LIZo%U}P=;ͥcn7NK#I_4mIK;9 *G<0/I$-[4nK(Ϳ&90zNvx|Wyc~n R']~ݙvǕI.bnIItvRyG0bqv'CJw ?n/^KG)M}pϺ v|ɋ<-57>.eenIiǟ䩙($ẃ7ڍI[e |$}ʹgjg݅I^ o-u;M2,c \|-^ܖH`ˠIn7l[vV* cf%yf7QK3NfIٴoMvM򒴻۽a\v7'9IG^us>ϺzmMcI> -Ku;$yD(?5P:dHҶ'ut2:nvm]L4!zΉI8ɯd<{uw%yW0t k'$)I+5[$_O;$_휔$U٢-2Wv~2YiΪM*0mz-MH9-n?Ue { ѦVI{wlL2H;(op*>[$Y'y?0JUus~fO9!]Y}$ǔpI.IF3I8hKKFI)IO'p0pPb =7ɓu\i? .n4d~R$^vδ -lMe+W^I~'W$%$P"ɞ!ϻӍI.Krq!/{$u;}=D]eʝi9/ɹi@v0zKIpj{k0Rz}, 笶OKpGVFy\'yik&ܦ컃1vvYْ$vsm+XӞD}I\-cm=Cǎ){^f'qW99sɓ:\|-iK.HrvRF'Ɵ=C[ic?pu;$$N|I>vu+XL^󐴋Ǵ; ?!Ņ-5nK;0,B0hh?9ÊFM&3 ~^տ(&\yN$ vN5I C`~y|պ$3{nAi=+I>Emi$W>s|;k#9iwp0ܴ?{lOs^@kc$9+-26FKnV˶OI{`qpdzòS?sw$f΢UiSumOri}]Ef0 <(䎴+$|? pPn縴m"}$L[m\I.Ϟ[K8u;!?9ɃK6vWg$ (ZS<*-[lKI>fUyhg2ȬO'I?n)@YnI=C?)Mof z?i,n)94cI>Yۚ$QՍJ*q͞ANrjSF#R =~ua@$|cJv-I̞a\ Z9&;d [L{{I0^ ou;OJ;`_]3=Ox̺IMrv 0ziw?=av]irlʌfߣ'i>v=z^ǚ9nņƓda~nv$$!p4z΃g|itޕ qo7fvܞVpx< ߝƴx Ona3@EGc &ɺ{W$`G'ɉI6Ý{`oS0C~u8~ֵ3ξ?Ӻ 4ec^Kb[gao?Y.8ѐnѱ %+|6r_[&u;O=v!&F/l0T6 IvnF];;I/&V[)I=$+0'ygVut х8'{ߵIg`xJV$J;D˽^T)͹`7u0iF"?){x{/@ IDATj~w^Tѱa`xJrzI3`?0B'~nmtlv]vI o^&̤^sB>9\n^In! 휘lG}\uIޘ䬪.ivWqi7wl`e5.0M'ܛq$!x$>L&[ O3t٘-IάfGcJ+b $oK2\1@cx,'$!Ɠ&܋,, s,i`ٍNIS$Hrc3n;dMǖn"M'ySU7nX '/'Shc$9cqL%\]:` =(ے ~t $}~S34^$o(Qٞե#`LxTu$+Ӫn(dG~3Ɏo$0inh5IQUl-fGGKGpOI4C*%ISژ#`RMxTu$Wn֕I5C# ɻKG$nL,i1ɫU:&T'IU7dP{n.,nG^dC$_:L WusS)FU7KG4&|ɪnKGn$dkvWV: 'IU7W&yKnY:L YKJG̠Wu7#`xU7;$[f$tLOn.H3UU\:L~KG̀Vuϥ#`ZxU7["-S$-lfƓtjn,lGޑBQͿi7CU4I~1-S$o,`ƓJ)3+R:fnI[` JG04't;?Ybh|/UܕIvn@[C`OU7$wYS:fiϗ $0暦)0$.0nJnn)NPI~tۙC%@1nX:f\4܉I.L-,HnVrddæ~'qߙ҅0>\KKG34~$_Mb`ݲ>ښ[gs\':e`]Ul(!Z{Y*cSr%ɺoڟK~ I 5Wu!@rLIQ͇t[~Ɠi U+u`|?<䳥#8tW\|d#Ͽ˪Q:80CGl-/[ǹʵXPbƘePgnYv x|=ٱs xmU7甎2I>T`-.&^ekrUUW:84Ɨ/'tY6ټewxurWDž$Y:8tƗQU7w%yQJ̒+-󸋋9\U7J2憴K̂HX>֝MI^ٹXb_w$]QInvUR$/L2fLn)]+MRխI^T͆!04FLt K۵+^]hkTusm`_eUݜ4[&][;785&۶{XLʪnR:X^ _vIu o}4I~.,?CTu$.0o(]pp#Y\,]n04^|t}ƷnO֬-]IQͻJG+xAU,&yEn$04$ë)]{gU7X:XY fg%TImGyKCKW0.oV1PͶ$$/0&e.t3j>ɯV`UlI$_)06l*]pxnߐpK f']Cah|Tu9ɏ't {[gqVtU7;JfC't 8{k÷[mw`\'R:X]PU7<7ɕ[$'vg]ǪX:X}TU7$yNon':4v]as 7A(vfخ e|l\ (C <+M[V44$k6Α"}@U\I))if-eZv߲5L <[Kf]3_`5LNIfm&yISCeh|Uu>s|p ;[`}.P:oƧ@U7 ٥[V)]|&nاͥCgh|JTu5I7?nX);EZch}$KR: S4zGU7;B.R;Eo;$?SͶ!04>eUͫip}K,/\U7;JwtrƓddw&nK%$Mr)i|׮些KWPЮ$ U*L&CSw'$‰(]֮+]@!w'yiU7Y:lg@U7yIn/p[+Xe7'9&Q<-ɕ[ƃph|q1˓< J $OO-G'.Xkו.`|63Y[:gLU7'y~n8'/9ﺛ)] ;+ɏUusg`AUl旒^#9遥 ߶KWRMCch|UuI^dk.Xkו.`H򟫺Ceh|Uu$NrkCȇ.XƧΆ$/CfhTu$?dt 8+c +X&%yFU7V:~ITu6't9~I,]26>.Jn, C|[U7({J̴6nh|}03$j nvVu$oHt< VwHn^Y͖1l14Βyg$t Ryrr\銕qͥ 8L7'Ѫnt0 s@Uݜ)I.*<+;JpMnT:]ƹWU\䇓M=;KJp*ɳYW:mǕ`Uu-ɯ-}){P8 ISlVdymؔ,.&[ؒUUK$v0TusVJt @sLGX~+8k<80N sX,S|t @<;KJO'yJU7_+7Cn6WuI^d{`=A)+]Y4I8 t sĪ$Jr]`=K,?;I~߫fWTu$ON-:aɣO)]nߘjJW̼'nt14Qf}&$vaxj͕X>ɆM+f{^͚!{8J"XI'M u(fߍmn9[d 3s#!1lf7: S \81q,_Y)r]4=??^NΘ2u:5'ٷt 0y9Y}銩'^SbIrn_,\gJMMk`sX2{v銩$Gh)W7INN$  ZQblxtx:^7cvV}XV+|6[񍛓{/]-MN?tػ+/M[]8Ӫn;cI& G&}l.]02ɱsiӵ I>dy`=>7F{xd䬓JWGZT:`*4Ό$G$t`-^:&5¿骪tX28qi":3eIn'_h^?䄣JW&PQ:`UFY_I"ɗJm_̛[dG邱q{c qe4N1u?Z7[3ɺ=ZWrƉKv>Kg|4OMߕ.U ][T_L!Yt[gr$ﮛ!ͥqB%9%yI6Ԣ(]:tz2&Yi0L :][$'nӦgoݑ}_> +IutL2g(umU%yg]8SIn5Yt6 'o99YtHYumj][pI58ڲ%cure[*91_H[u?\:qFBVUKiɿݖ|xc_UGn(hѵ$%9?ɏ>gΟaeQ& IDAT3#OI~nuc8#kI>M[%ޟl4}sвdd1Iέ!hյ%DJk'G?/9UI.#NrQ?~cac4HjQ$y_مs1~C#cu;x١$fMor_]:`X3:*%I+L}*Y`DdΜddmMi(Hut3gltm5+{\d9Go\:`3vZIޝdNvЦ$L(0J[][I.K$M!h׵ՉI>desgߩ!hѵ'0kKL$ ɥuo.0ƙ(][KIOwI)ɧi􏗎FLHI>dAI$W7=Cƍ8keI<ےTsѕI.!htmud'yC1qMfqg4ѵ\-# זF<][JrN?JrxQ R:`ËڪJ$9$M!hv@Vgd0_Y`HܙO|ng$q ][x|UBN$W q][$n!w'0\:ma7tm2gn&&8W]NF0:2[Tsv$m릿t /hPV'$$]8`gO$1q][IޞdQ@O&n'Jsaum(;fCے\7` ڪJrZ'ySYe wU릿thfXV?Iޕdq`rlL w`C!][-L$Kp0$~M8 NJ$oN2p0J&nK0atm,{dI`ܞUMxf8 '9'ɻ*[ I.Ori.3!׵$Wp0 image/svg+xml cookiecutter-2.6.0/logo/cookiecutter_medium.png000066400000000000000000000207371456543333500217510ustar00rootroot00000000000000PNG  IHDR_SWsBIT|d pHYs(RtEXtSoftwarewww.inkscape.org< IDATxwU{sCBB- /@@i..IΜs~;3s~W nTʅ0lXP^^^/K~WAAR.JR.|88 $gK{ -  A~E俔ј (+@qhŽ70(SWg%Yβ Bh2Srap2p02A1Z,y # H]WʅA7ЎjN70X-  4"U%^).Om\\],FAjIEWʅ)=ΣŒrނ B5*t>81NۃerY mAbJR.nAzL>W, DAQra;6`؝?K̼A6q; Axisag`" k%^)\AȄ-W ==ƣap(EɃ9Œxނ #%^)סC1g>|0V|0~UdX|X[AaajN !xO9fnVfê= c\=A 'W9^FH>|LGKy ! "+gÞpqtxIszSB J<ƾU2d=:$z8R* Yʜ^)OD`j^ }UZBw¨ ` AhăT&Ll=A 3!ΙJ©T;W?F ;MWA5G_p/p(pA!oZ*=6a,dLæme+َ-Jk #@z=yӌ@MQB~ ;g!)o?^z=#!W඲ ]m~iˀ.c>xqD88=eÚ|xxxq%I [Y{cZYg>p<0=qޖv;TJFA?C˃}`zRdo+ !q?[ K{G% uxq_VgמkzDi+ѿk|x=}A'[e"`$[Y`}M ^>}5}|d }(pTFc8RvN43Fme ~G \yE /be5oތ؍>.B+0~\aBIYI2v\^#K+le}8 38q e xh۞խrg+HV}Y= r&ao)"='z?_TleMDGb0_ L.v\oa? i+aj4B(zO[Y7D֐}0P^=k+wD݆69v?j#~j?VADmd+k /ab+:Ƿ!mٴ9۶8^uЈ]memek{) ?"6 e<^rxV/meսkW>̌ǶvZ ;L5L \w^g|V!6 9zj+tb+kPND ~g+`?2k4h3m*w!iƥ=B+q[Y& ǖR7 <ΨNڌ>g/?SJ< @j7?~\ ׺'Iu2J+愐c ڑ.m Ŷ1iL6>T9H[YbyA}մdia6NQ)$cTcx8hgj\ă"'R"N^qWS(WoOC'!/t[Y{?#7ZyLmɒleM6hw=K7K&:.z6U:利 GM=lem8Od,fJ';q[^w ժpW#S~ec;xW౫ t8m!9lVV֓3ɶN@;T[YO Kg4Fhi̔GWﯴӳܜ][(MˌJ'~9Ml/`O󿣕z;(ŋRÀ[nSle+py"5dZR_1nmp8´GeMY$وiy*RQٳc JR. !0IT ٬&ޫ-9Պsa`y5ptZASP0<5DB4|Xai:<::@9Of Ϝ+*zӻ?]ۍW'&W#zh3 *HV45&g}FbF;w% m,MO6ӓΌ2w0SFph\ 3^w\/+XkBz)=gM{8x;1+Bd;5KЉ3VJe+kW͜@<'hqV;q Kdqov-^U3֪ߊp~%P }BSnq:<Ϣ>8 #i|>K58 {-{U*#3ͅ}I`LГIYr@`:pX+p0WFG+އ[kV}| 'xL[Y_0c :g-?=%ߤ+(U>$^9.< 7D8:q[\/kyʊq\/ OݔP_6L ^Gȼp\b+>)qqB}+:po&q#Fr\/G[cmu&K/pDǂFIޟIKyl wvCV`?*5/F517rD8;L-Eg7:3DleC+(#_v|#OsfʺUAH:3J΃5Je@ 5n+kk̾[;>[&D|Z^3B3twVݵ֫/*j9'M :$DOzaNNemIᰇAgÄqh( (J<8)@&#Y~f''=szz:)x(YBQ uZ@v Bڼtâ%l.ZTy<@ L7;hB'l,tvPv1RAq "z&NJI)U V$4iISAȁi]$A%>>QK0jT<]'*ĥ#yޠoUcESzOFl[jՔ#zi\y1V49a69Gʚ=뽑4PxIL@W]ӭ_F%J4R|x͠MFԦlgY*,Kd8p]svuI=a`qn/x_$cQsMJ̜>k.n5!&y]&j>KqD9Cu+Vdމ,2w\͛0MhC| [=$e+âK V/5N7i3QLJ">NJTDSz%m Aq\/ºX' ˁA $8QsoD'gZC"+;uV0O䁟VɊeXQ%aN9U.k%zQS .&?x̒g]R AbVFQ]Q\8=;gĤ߬ckӡȚT@Tc+4sݺ@k0=RAd k fV8E"+EnbKkgRڮvudb05jW+kle}$qf~QQ2a[*>]/6ի;P  lcfȻхBdFjL%&plY5z li#De bZQ}/jPLGŏ*iiuLVVMώQE#>$P&&S䷧+paNcE,&m*ނTė+ [m#V~i33KaՠI$Yɚgӂ=M^W]/a9_&Y_βp!I %l9kᥕrbɟBp,sviqE b4aˉVl[Y{3B$d5e1E*W[Y;9MV^)lm}J&ʺ S)RSiP=JhzB(^Zd,u ʆFM|'ɶ$z[58h1E2u/)8/v55pt҂BLD[BKz성va0==. &m:le%`"i&0ap)p4kj1z9Ir~N<2hW57{q95ٷo?RjV:p Εo(NZ"٬  j+ pY}ʷ%pkeq\oyDO4 un.}DcHk'jfӈ~_:z ߷%$T$/E :u"&ڻIW59Qp}_nor<#֌!:뚛Zkm+nv[Yʚi+kV v\u{[Y3leo(k5'a_/4&H›xc`'`vAw{$Ic.īLj"(7x\~V;8;F?2&sڊ'xU(j?q|Aش e9VV=7C1 3}Q%icvz;^a|]Ⲹm2˪/Hf!\vu4p\Q,^AYlL! > Rѥm&B*{o谳l|@D_XqW:w#pCF&;q?ץ/RSQ b8mbҊ|Ie-BGd#H#aМM"w"]cl[YgݤK\ES ӌV^ǑYbpSi ӂ+׻%׻sOksme\"5#hZ;q]~leK%:5g\9=ЉZ>,&f8U+񻀕R.H ;6ط֬B>( [RϺr@yqfS2UGp Xڌ}3؟?L)(isZ &Ȼ5 @;0j [=I]>kI<vv\cEU$ \,O;J^B;pzYM=AWYٿu8| :&$_!C _8w:~5c)o^ AOkdi^)˰2E{nD;? C{. ^ 9P>7E+A;EޏE/$^ZV;g+V>IюV8J+O~ $tf,E_'ﱧ`keow*-Qতk2SpCO7:Xxj; 1{𓨿A3c2z0pq ct6E7=U`tENۅq;zK%P)Jy/ JKBl8eBˇr^dֈ(H볶7SvB͇X:楝8%T9ZyAO^QB\[m5Rߓ"ASx%5'JЧ  dIݕx/r~t,PKy ! LZ9[NtﻁTY yTKS%_E. B2g!Z1R.dULAJX}+W XhRƜ+E YԱJ:FJᆵ[A ئXH' p˜(7e-"G3> a*蚟ٰ>0Y 8@?.JѶX ]U9κ'iŒLFA"*qb p] $`uUE~/e.)I}AA"8&IAaaFh_Y,Z ЀЎm (˖ë p>pj_h ӕ8Œrs'&M1.X=iQ BRm7 ҍ&|Q2qCĉMAțJX_H?S:/_`bɟ'AAI"JXGSȱh S_8X5m.HPIDATEA$c[#*¿wy `fڡf6GGYt璑NA!%K\8)A|%,Y:&nM!.rCnu  dMJR.t \l`u}*;UmEX-" H]R)d2h4f?-FAZR. ObAgX&  &P `` ';?K 8*j*PS6Mѵs .6JJP͚6U xbϮ  dxG7IENDB`cookiecutter-2.6.0/pyproject.toml000066400000000000000000000003011456543333500171400ustar00rootroot00000000000000[tool.black] skip-string-normalization = true exclude = '/(tests/hooks-abort-render/hooks|docs\/HelloCookieCutter1)/' line-length = 88 target-version = ['py39'] [tool.isort] profile = "black" cookiecutter-2.6.0/setup.cfg000066400000000000000000000010711456543333500160520ustar00rootroot00000000000000[flake8] # Excludes due to known issues or incompatibilities with black: # BLK100: Black would make changes. https://pypi.org/project/flake8-black/ # W503: https://github.com/psf/black/search?q=W503&unscoped_q=W503 # E231: https://github.com/psf/black/issues/1202 ignore = BLK100,E231,W503 statistics = 1 # black official is 88 max-line-length = 88 [tool:pytest] testpaths = tests addopts = -vvv --cov-report term-missing --cov=cookiecutter [doc8] # TODO: Remove current max-line-length ignore in follow-up and adopt black limit. # max-line-length = 88 ignore = D001 cookiecutter-2.6.0/setup.py000066400000000000000000000052441456543333500157510ustar00rootroot00000000000000"""cookiecutter distutils configuration.""" from pathlib import Path from setuptools import setup def _get_version() -> str: """Read cookiecutter/VERSION.txt and return its contents.""" path = Path("cookiecutter").resolve() version_file = path / "VERSION.txt" return version_file.read_text().strip() version = _get_version() with open('README.md', encoding='utf-8') as readme_file: readme = readme_file.read() requirements = [ 'binaryornot>=0.4.4', 'Jinja2>=2.7,<4.0.0', 'click>=7.0,<9.0.0', 'pyyaml>=5.3.1', 'python-slugify>=4.0.0', 'requests>=2.23.0', 'arrow', 'rich', ] setup( name='cookiecutter', version=version, description=( 'A command-line utility that creates projects from project ' 'templates, e.g. creating a Python package project from a ' 'Python package project template.' ), long_description=readme, long_description_content_type='text/markdown', author='Audrey Feldroy', author_email='audreyr@gmail.com', url='https://github.com/cookiecutter/cookiecutter', project_urls={ "Documentation": "https://cookiecutter.readthedocs.io", "Issues": "https://github.com/cookiecutter/cookiecutter/issues", "Discord": "https://discord.gg/9BrxzPKuEW", }, packages=['cookiecutter'], package_dir={'cookiecutter': 'cookiecutter'}, entry_points={'console_scripts': ['cookiecutter = cookiecutter.__main__:main']}, include_package_data=True, python_requires='>=3.7', install_requires=requirements, license='BSD', zip_safe=False, classifiers=[ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "Natural Language :: English", "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python", "Topic :: Software Development", ], keywords=[ "cookiecutter", "Python", "projects", "project templates", "Jinja2", "skeleton", "scaffolding", "project directory", "package", "packaging", ], ) cookiecutter-2.6.0/test_requirements.txt000066400000000000000000000000761456543333500205600ustar00rootroot00000000000000pytest pytest-cov pytest-mock freezegun safety pre-commit tox cookiecutter-2.6.0/tests/000077500000000000000000000000001456543333500153745ustar00rootroot00000000000000cookiecutter-2.6.0/tests/__init__.py000066400000000000000000000000461456543333500175050ustar00rootroot00000000000000"""Cookiecutter integration tests.""" cookiecutter-2.6.0/tests/conftest.py000066400000000000000000000162411456543333500175770ustar00rootroot00000000000000"""pytest fixtures which are globally available throughout the suite.""" import os import shutil import pytest from cookiecutter import utils from cookiecutter.config import DEFAULT_CONFIG USER_CONFIG = """ cookiecutters_dir: '{cookiecutters_dir}' replay_dir: '{replay_dir}' """ @pytest.fixture(autouse=True) def isolated_filesystem(monkeypatch, tmp_path): """Ensure filesystem isolation, set the user home to a tmp_path.""" root_path = tmp_path.joinpath("home") root_path.mkdir() cookiecutters_dir = root_path.joinpath(".cookiecutters/") replay_dir = root_path.joinpath(".cookiecutter_replay/") monkeypatch.setitem(DEFAULT_CONFIG, 'cookiecutters_dir', str(cookiecutters_dir)) monkeypatch.setitem(DEFAULT_CONFIG, 'replay_dir', str(replay_dir)) monkeypatch.setenv("HOME", str(root_path)) monkeypatch.setenv("USERPROFILE", str(root_path)) def backup_dir(original_dir, backup_dir): """Generate backup directory based on original directory.""" # If the default original_dir is pre-existing, move it to a temp location if not os.path.isdir(original_dir): return False # Remove existing stale backups before backing up. if os.path.isdir(backup_dir): utils.rmtree(backup_dir) shutil.copytree(original_dir, backup_dir) return True def restore_backup_dir(original_dir, backup_dir, original_dir_found): """Restore default contents.""" original_dir_is_dir = os.path.isdir(original_dir) if original_dir_found: # Delete original_dir if a backup exists if original_dir_is_dir and os.path.isdir(backup_dir): utils.rmtree(original_dir) else: # Delete the created original_dir. # There's no backup because it never existed if original_dir_is_dir: utils.rmtree(original_dir) # Restore the user's default original_dir contents if os.path.isdir(backup_dir): shutil.copytree(backup_dir, original_dir) if os.path.isdir(original_dir): utils.rmtree(backup_dir) @pytest.fixture(scope='function') def clean_system(request): """Fixture. Simulates a clean system with no configured or cloned cookiecutters. It runs code which can be regarded as setup code as known from a unittest TestCase. Additionally it defines a local function referring to values which have been stored to local variables in the setup such as the location of the cookiecutters on disk. This function is registered as a teardown hook with `request.addfinalizer` at the very end of the fixture. Pytest runs the named hook as soon as the fixture is out of scope, when the test finished to put it another way. During setup: * Back up the `~/.cookiecutterrc` config file to `~/.cookiecutterrc.backup` * Back up the `~/.cookiecutters/` dir to `~/.cookiecutters.backup/` * Back up the `~/.cookiecutter_replay/` dir to `~/.cookiecutter_replay.backup/` * Starts off a test case with no pre-existing `~/.cookiecutterrc` or `~/.cookiecutters/` or `~/.cookiecutter_replay/` During teardown: * Delete `~/.cookiecutters/` only if a backup is present at `~/.cookiecutters.backup/` * Delete `~/.cookiecutter_replay/` only if a backup is present at `~/.cookiecutter_replay.backup/` * Restore the `~/.cookiecutterrc` config file from `~/.cookiecutterrc.backup` * Restore the `~/.cookiecutters/` dir from `~/.cookiecutters.backup/` * Restore the `~/.cookiecutter_replay/` dir from `~/.cookiecutter_replay.backup/` """ # If ~/.cookiecutterrc is pre-existing, move it to a temp location user_config_path = os.path.expanduser('~/.cookiecutterrc') user_config_path_backup = os.path.expanduser('~/.cookiecutterrc.backup') if os.path.exists(user_config_path): user_config_found = True shutil.copy(user_config_path, user_config_path_backup) os.remove(user_config_path) else: user_config_found = False # If the default cookiecutters_dir is pre-existing, move it to a # temp location cookiecutters_dir = os.path.expanduser('~/.cookiecutters') cookiecutters_dir_backup = os.path.expanduser('~/.cookiecutters.backup') cookiecutters_dir_found = backup_dir(cookiecutters_dir, cookiecutters_dir_backup) # If the default cookiecutter_replay_dir is pre-existing, move it to a # temp location cookiecutter_replay_dir = os.path.expanduser('~/.cookiecutter_replay') cookiecutter_replay_dir_backup = os.path.expanduser('~/.cookiecutter_replay.backup') cookiecutter_replay_dir_found = backup_dir( cookiecutter_replay_dir, cookiecutter_replay_dir_backup ) def restore_backup(): # If it existed, restore ~/.cookiecutterrc # We never write to ~/.cookiecutterrc, so this logic is simpler. if user_config_found and os.path.exists(user_config_path_backup): shutil.copy(user_config_path_backup, user_config_path) os.remove(user_config_path_backup) # Carefully delete the created ~/.cookiecutters dir only in certain # conditions. restore_backup_dir( cookiecutters_dir, cookiecutters_dir_backup, cookiecutters_dir_found ) # Carefully delete the created ~/.cookiecutter_replay dir only in # certain conditions. restore_backup_dir( cookiecutter_replay_dir, cookiecutter_replay_dir_backup, cookiecutter_replay_dir_found, ) request.addfinalizer(restore_backup) @pytest.fixture(scope='session') def user_dir(tmp_path_factory): """Fixture that simulates the user's home directory.""" return tmp_path_factory.mktemp('user_dir') @pytest.fixture(scope='session') def user_config_data(user_dir): """Fixture that creates 2 Cookiecutter user config dirs. It will create it in the user's home directory. * `cookiecutters_dir` * `cookiecutter_replay` :returns: Dict with name of both user config dirs """ cookiecutters_dir = user_dir.joinpath('cookiecutters') cookiecutters_dir.mkdir() replay_dir = user_dir.joinpath('cookiecutter_replay') replay_dir.mkdir() return { 'cookiecutters_dir': str(cookiecutters_dir), 'replay_dir': str(replay_dir), } @pytest.fixture(scope='session') def user_config_file(user_dir, user_config_data): """Fixture that creates a config file called `config`. It will create it in the user's home directory, with YAML from `user_config_data`. :param user_dir: Simulated user's home directory :param user_config_data: Dict of config values :returns: String of path to config file """ config_file = user_dir.joinpath('config') config_text = USER_CONFIG.format(**user_config_data) config_file.write_text(config_text) return str(config_file) @pytest.fixture def output_dir(tmp_path): """Fixture to prepare test output directory.""" output_path = tmp_path.joinpath("output") output_path.mkdir() return str(output_path) @pytest.fixture def clone_dir(tmp_path): """Simulate creation of a directory called `clone_dir` inside of `tmp_path`. \ Returns a str to said directory.""" clone_dir = tmp_path.joinpath("clone_dir") clone_dir.mkdir() return clone_dir cookiecutter-2.6.0/tests/fake-nested-templates-old-style/000077500000000000000000000000001456543333500234705ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-nested-templates-old-style/cookiecutter.json000066400000000000000000000003131456543333500270600ustar00rootroot00000000000000{ "template": [ "fake-package (./fake-package)" ], "__prompts__": { "template": { "__prompt__": "Select a template", "fake-package (./fake-package)": "Fake Package" } } } cookiecutter-2.6.0/tests/fake-nested-templates-old-style/fake-package/000077500000000000000000000000001456543333500257675ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-nested-templates-old-style/fake-package/cookiecutter.json000066400000000000000000000000031456543333500313530ustar00rootroot00000000000000{} cookiecutter-2.6.0/tests/fake-nested-templates/000077500000000000000000000000001456543333500215565ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-nested-templates/cookiecutter.json000066400000000000000000000005161456543333500251530ustar00rootroot00000000000000{ "templates": { "fake-project": { "path": "./fake-project", "title": "A Fake Project", "description": "A cookiecutter template for a project" }, "fake-package": { "path": "./fake-package", "title": "A Fake Package", "description": "A cookiecutter template for a package" } } } cookiecutter-2.6.0/tests/fake-nested-templates/fake-package/000077500000000000000000000000001456543333500240555ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-nested-templates/fake-package/cookiecutter.json000066400000000000000000000000031456543333500274410ustar00rootroot00000000000000{} cookiecutter-2.6.0/tests/fake-nested-templates/fake-project/000077500000000000000000000000001456543333500241305ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-nested-templates/fake-project/cookiecutter.json000066400000000000000000000000031456543333500275140ustar00rootroot00000000000000{} cookiecutter-2.6.0/tests/fake-repo-bad-json/000077500000000000000000000000001456543333500207405ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-repo-bad-json/cookiecutter.json000066400000000000000000000000411456543333500243260ustar00rootroot00000000000000{"1": 2, "some_key"- "some_val"} cookiecutter-2.6.0/tests/fake-repo-bad-json/{{cookiecutter.repo_name}}/000077500000000000000000000000001456543333500263645ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-repo-bad-json/{{cookiecutter.repo_name}}/README.rst000066400000000000000000000001441456543333500300520ustar00rootroot00000000000000============ Fake Project ============ Project name: **{{ cookiecutter.project_name }}** Blah!!!! cookiecutter-2.6.0/tests/fake-repo-bad/000077500000000000000000000000001456543333500177715ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-repo-bad/no-project-in-here.txt000066400000000000000000000000121456543333500241300ustar00rootroot00000000000000Ha ha ha! cookiecutter-2.6.0/tests/fake-repo-dict/000077500000000000000000000000001456543333500201665ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-repo-dict/cookiecutter.json000066400000000000000000000004771456543333500235710ustar00rootroot00000000000000{ "project_slug": "fake-project-dict", "file_types": { "png": { "name": "Portable Network Graphic", "library": "libpng", "apps": [ "GIMP" ] }, "bmp": { "name": "Bitmap", "library": "libbmp", "apps": [ "Paint", "GIMP" ] } } } cookiecutter-2.6.0/tests/fake-repo-dict/{{cookiecutter.project_slug}}/000077500000000000000000000000001456543333500263455ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-repo-dict/{{cookiecutter.project_slug}}/README.md000066400000000000000000000005411456543333500276240ustar00rootroot00000000000000# README {% for extension, details in cookiecutter.file_types|dictsort %}
Format name:
{{ details.name }}
Extension:
{{ extension }}
Applications:
    {% for app in details.apps -%}
  • {{ app }}
  • {% endfor -%}
{% endfor %} cookiecutter-2.6.0/tests/fake-repo-dir/000077500000000000000000000000001456543333500200215ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-repo-dir/my-dir/000077500000000000000000000000001456543333500212225ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-repo-dir/my-dir/cookiecutter.json000066400000000000000000000004411456543333500246140ustar00rootroot00000000000000{ "full_name": "Audrey Roy", "email": "audreyr@gmail.com", "github_username": "audreyr", "project_name": "Fake Project", "repo_name": "fake-project", "project_short_description": "This is a fake project.", "release_date": "2013-07-28", "year": "2013", "version": "0.1" } cookiecutter-2.6.0/tests/fake-repo-dir/my-dir/{{cookiecutter.repo_name}}/000077500000000000000000000000001456543333500266465ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-repo-dir/my-dir/{{cookiecutter.repo_name}}/README.rst000066400000000000000000000001441456543333500303340ustar00rootroot00000000000000============ Fake Project ============ Project name: **{{ cookiecutter.project_name }}** Blah!!!! cookiecutter-2.6.0/tests/fake-repo-pre/000077500000000000000000000000001456543333500200315ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-repo-pre/cookiecutter.json000066400000000000000000000004411456543333500234230ustar00rootroot00000000000000{ "full_name": "Audrey Roy", "email": "audreyr@gmail.com", "github_username": "audreyr", "project_name": "Fake Project", "repo_name": "fake-project", "project_short_description": "This is a fake project.", "release_date": "2013-07-28", "year": "2013", "version": "0.1" } cookiecutter-2.6.0/tests/fake-repo-pre/{{cookiecutter.repo_name}}/000077500000000000000000000000001456543333500254555ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-repo-pre/{{cookiecutter.repo_name}}/README.rst000066400000000000000000000001441456543333500271430ustar00rootroot00000000000000============ Fake Project ============ Project name: **{{ cookiecutter.project_name }}** Blah!!!! cookiecutter-2.6.0/tests/fake-repo-pre2/000077500000000000000000000000001456543333500201135ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-repo-pre2/cookiecutter.json000066400000000000000000000004411456543333500235050ustar00rootroot00000000000000{ "full_name": "Audrey Roy", "email": "audreyr@gmail.com", "github_username": "audreyr", "project_name": "Fake Project", "repo_name": "fake-project", "project_short_description": "This is a fake project.", "release_date": "2013-07-28", "year": "2013", "version": "0.1" } cookiecutter-2.6.0/tests/fake-repo-pre2/whatever.some.thing000066400000000000000000000000001456543333500237230ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-repo-pre2/{%{cookiecutter.repo_name}%}/000077500000000000000000000000001456543333500256515ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-repo-pre2/{%{cookiecutter.repo_name}%}/README.rst000066400000000000000000000000611456543333500273350ustar00rootroot00000000000000============ Fake Project ============ Blah!!!! cookiecutter-2.6.0/tests/fake-repo-replay/000077500000000000000000000000001456543333500205375ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-repo-replay/cookiecutter.json000066400000000000000000000004611456543333500241330ustar00rootroot00000000000000{ "full_name": "Audrey Roy", "email": "audreyr@gmail.com", "github_username": "audreyr", "project_name": "Replay Project", "repo_name": "{{ cookiecutter.project_name|lower|replace(' ', '-') }}", "description": "original", "release_date": "2013-07-28", "year": "2013", "version": "0.1" } cookiecutter-2.6.0/tests/fake-repo-replay/{{cookiecutter.repo_name}}/000077500000000000000000000000001456543333500261635ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-repo-replay/{{cookiecutter.repo_name}}/README.md000066400000000000000000000000351456543333500274400ustar00rootroot00000000000000{{cookiecutter.description}} cookiecutter-2.6.0/tests/fake-repo-tmpl-_cookiecutter/000077500000000000000000000000001456543333500230545ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-repo-tmpl-_cookiecutter/cookiecutter.json000066400000000000000000000006561456543333500264560ustar00rootroot00000000000000{ "full_name": "Audrey Roy", "email": "audreyr@gmail.com", "github_username": "audreyr", "project_name": "Fake Project Templated", "test_list": [ 1, 2, 3, 4 ], "test_dict": { "foo": "bar" }, "repo_name": "{{ cookiecutter.project_name|lower|replace(' ', '-') }}", "project_short_description": "This is a fake project.", "release_date": "2013-07-28", "year": "2013", "version": "0.1" } cookiecutter-2.6.0/tests/fake-repo-tmpl-_cookiecutter/{{cookiecutter.repo_name}}/000077500000000000000000000000001456543333500305005ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-repo-tmpl-_cookiecutter/{{cookiecutter.repo_name}}/README.rst000066400000000000000000000000611456543333500321640ustar00rootroot00000000000000============ Fake Project ============ Blah!!!! cookiecutter-2.6.0/tests/fake-repo-tmpl/000077500000000000000000000000001456543333500202175ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-repo-tmpl/cookiecutter.json000066400000000000000000000005261456543333500236150ustar00rootroot00000000000000{ "full_name": "Audrey Roy", "email": "audreyr@gmail.com", "github_username": "audreyr", "project_name": "Fake Project Templated", "repo_name": "{{ cookiecutter.project_name|lower|replace(' ', '-') }}", "project_short_description": "This is a fake project.", "release_date": "2013-07-28", "year": "2013", "version": "0.1" } cookiecutter-2.6.0/tests/fake-repo-tmpl/{{cookiecutter.repo_name}}/000077500000000000000000000000001456543333500256435ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-repo-tmpl/{{cookiecutter.repo_name}}/README.rst000066400000000000000000000000611456543333500273270ustar00rootroot00000000000000============ Fake Project ============ Blah!!!! cookiecutter-2.6.0/tests/fake-repo/000077500000000000000000000000001456543333500172455ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-repo/cookiecutter.json000066400000000000000000000000001456543333500226260ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-repo/fake-project/000077500000000000000000000000001456543333500216175ustar00rootroot00000000000000cookiecutter-2.6.0/tests/fake-repo/fake-project/README.rst000066400000000000000000000000611456543333500233030ustar00rootroot00000000000000============ Fake Project ============ Blah!!!! cookiecutter-2.6.0/tests/files/000077500000000000000000000000001456543333500164765ustar00rootroot00000000000000cookiecutter-2.6.0/tests/files/bad-zip-file.zip000066400000000000000000000000341456543333500214620ustar00rootroot00000000000000Ceci n'est pas un zip file. cookiecutter-2.6.0/tests/files/empty.zip000066400000000000000000000000261456543333500203560ustar00rootroot00000000000000PKcookiecutter-2.6.0/tests/files/fake-repo-tmpl.zip000066400000000000000000000017421456543333500220510ustar00rootroot00000000000000PK <Ifake-repo-tmpl/UX ¨DYWPK<I fake-repo-tmpl/cookiecutter.jsonUX ¨DYWU1O0g+N^Re1ѥ3Bݣù6N/:۠('6iMٝ:k3v^@mC#4;j5gԡczܥh|>H^D aO]oSTo8f>{o7e-j!_A^0M~ 9-ϱx!-Q̛֏ETlS<'Eڪ\l~PKrNMPK <I*fake-repo-tmpl/{{cookiecutter.repo_name}}/UX ¨DYWPK<I4fake-repo-tmpl/{{cookiecutter.repo_name}}/README.rstUX ¨DYWE.T.[d. E PKyk1PK <I @Afake-repo-tmpl/UX¨DYWPK<IrNM @=fake-repo-tmpl/cookiecutter.jsonUX¨DYWPK <I* @Anfake-repo-tmpl/{{cookiecutter.repo_name}}/UX¨DYWPK<Iyk14 @fake-repo-tmpl/{{cookiecutter.repo_name}}/README.rstUX¨DYWPKuWcookiecutter-2.6.0/tests/files/not-a-repo.zip000066400000000000000000000003161456543333500212030ustar00rootroot00000000000000PK ܭ.Kq[not-a-repo.txtUT ?[Y@[Yux Ceci n'est pas un zip file. PK ܭ.Kq[not-a-repo.txtUT?[Yux PKTdcookiecutter-2.6.0/tests/files/protected-fake-repo-tmpl.zip000066400000000000000000000021321456543333500240320ustar00rootroot00000000000000PK ӄIfake-repo-tmpl/UT WaYux PK ӄIrNM fake-repo-tmpl/cookiecutter.jsonUT W¨DYux ȨTǣrV3Q{fq\Kiz"o_q!-PB^N,%wO oKI~׷\ZB}Tu}! p)m!* nS:Qg&Y1@| l,+7|~^{xw,O, r m8E+:>˝%ZݟG,nK!ZnBVg:3sxPKrNMPK ӄI*fake-repo-tmpl/{{cookiecutter.repo_name}}/UT WaYux PK ӄIyk+14fake-repo-tmpl/{{cookiecutter.repo_name}}/README.rstUT W¨DYux 4 #(8 ؅|k w5qr[jU PKyk+1PK ӄIAfake-repo-tmpl/UTWux PK ӄIrNM Ifake-repo-tmpl/cookiecutter.jsonUTWux PK ӄI*Afake-repo-tmpl/{{cookiecutter.repo_name}}/UTWux PK ӄIyk+14fake-repo-tmpl/{{cookiecutter.repo_name}}/README.rstUTWux PKcookiecutter-2.6.0/tests/files/syntax_error.txt000066400000000000000000000000701456543333500217730ustar00rootroot00000000000000I eat {{ syntax_error }} {# this comment is not closed} cookiecutter-2.6.0/tests/files/unicode.txt000066400000000000000000000001311456543333500206600ustar00rootroot00000000000000Polish: Ą Ł Ż Chinese: 倀 倁 倂 倃 倄 倅 倆 倇 倈 Musical Notes: ♬ ♫ ♯ cookiecutter-2.6.0/tests/files/{% if cookiecutter.generate_file == 'y' %}cheese.txt{% endif %}000066400000000000000000000001001456543333500314650ustar00rootroot00000000000000Testing that generate_file was {{ cookiecutter.generate_file }} cookiecutter-2.6.0/tests/files/{{cookiecutter.generate_file}}.txt000066400000000000000000000000511456543333500255030ustar00rootroot00000000000000Testing {{ cookiecutter.generate_file }} cookiecutter-2.6.0/tests/files/{{cookiecutter.generate_file}}_crlf_newlines.txt000066400000000000000000000000631456543333500304200ustar00rootroot00000000000000newline is CRLF newline is CRLF newline is CRLF cookiecutter-2.6.0/tests/files/{{cookiecutter.generate_file}}_lf_newlines.txt000066400000000000000000000000341456543333500300710ustar00rootroot00000000000000newline is LF newline is LF cookiecutter-2.6.0/tests/files/{{cookiecutter.jsonify_file}}.txt000066400000000000000000000000351456543333500253740ustar00rootroot00000000000000{{ cookiecutter | jsonify }} cookiecutter-2.6.0/tests/files/{{cookiecutter.random_string_file}}.txt000066400000000000000000000000571456543333500265650ustar00rootroot00000000000000{{ random_ascii_string(length, punctuation) }} cookiecutter-2.6.0/tests/hooks-abort-render/000077500000000000000000000000001456543333500211015ustar00rootroot00000000000000cookiecutter-2.6.0/tests/hooks-abort-render/hooks/000077500000000000000000000000001456543333500222245ustar00rootroot00000000000000cookiecutter-2.6.0/tests/hooks-abort-render/hooks/post_gen_project.py000066400000000000000000000003071456543333500261420ustar00rootroot00000000000000# flake8: noqa """Simple post-gen hook for testing the handling of different exit codes.""" import sys {% if cookiecutter.abort_post_gen == "yes" %} sys.exit(5) {% else %} sys.exit(0) {% endif %} cookiecutter-2.6.0/tests/hooks-abort-render/hooks/pre_gen_project.py000066400000000000000000000003051456543333500257410ustar00rootroot00000000000000# flake8: noqa """Simple pre-gen hook for testing the handling of different exit codes.""" import sys {% if cookiecutter.abort_pre_gen == "yes" %} sys.exit(5) {% else %} sys.exit(0) {% endif %} cookiecutter-2.6.0/tests/hooks-abort-render/{{cookiecutter.repo_dir}}/000077500000000000000000000000001456543333500263635ustar00rootroot00000000000000cookiecutter-2.6.0/tests/hooks-abort-render/{{cookiecutter.repo_dir}}/README.rst000066400000000000000000000000641456543333500300520ustar00rootroot00000000000000{{cookiecutter.repo_dir}} ========================= cookiecutter-2.6.0/tests/replay/000077500000000000000000000000001456543333500166705ustar00rootroot00000000000000cookiecutter-2.6.0/tests/replay/conftest.py000066400000000000000000000012371456543333500210720ustar00rootroot00000000000000"""pytest fixtures for testing cookiecutter's replay feature.""" import pytest @pytest.fixture def context(): """Fixture to return a valid context as known from a cookiecutter.json.""" return { 'cookiecutter': { 'email': 'raphael@hackebrot.de', 'full_name': 'Raphael Pierzina', 'github_username': 'hackebrot', 'version': '0.1.0', } } @pytest.fixture def replay_test_dir(): """Fixture to test directory.""" return 'tests/test-replay/' @pytest.fixture def mock_user_config(mocker): """Fixture to mock user config.""" return mocker.patch('cookiecutter.main.get_user_config') cookiecutter-2.6.0/tests/replay/test_dump.py000066400000000000000000000064361456543333500212570ustar00rootroot00000000000000"""test_dump.""" import json import os import pytest from cookiecutter import replay @pytest.fixture def template_name(): """Fixture to return a valid template_name.""" return 'cookiedozer' @pytest.fixture def replay_file(replay_test_dir, template_name): """Fixture to return a actual file name of the dump.""" file_name = f'{template_name}.json' return os.path.join(replay_test_dir, file_name) @pytest.fixture(autouse=True) def remove_replay_dump(request, replay_file): """Remove the replay file created by tests.""" def fin_remove_replay_file(): if os.path.exists(replay_file): os.remove(replay_file) request.addfinalizer(fin_remove_replay_file) def test_type_error_if_no_template_name(replay_test_dir, context): """Test that replay.dump raises if the template_name is not a valid str.""" with pytest.raises(TypeError): replay.dump(replay_test_dir, None, context) def test_type_error_if_not_dict_context(replay_test_dir, template_name): """Test that replay.dump raises if the context is not of type dict.""" with pytest.raises(TypeError): replay.dump(replay_test_dir, template_name, 'not_a_dict') def test_value_error_if_key_missing_in_context(replay_test_dir, template_name): """Test that replay.dump raises if the context does not contain a key \ named 'cookiecutter'.""" with pytest.raises(ValueError): replay.dump(replay_test_dir, template_name, {'foo': 'bar'}) @pytest.fixture def mock_ensure_failure(mocker): """Replace cookiecutter.replay.make_sure_path_exists function. Used to mock internal function and limit test scope. Always return expected value: False """ return mocker.patch( 'cookiecutter.replay.make_sure_path_exists', side_effect=OSError ) @pytest.fixture def mock_ensure_success(mocker): """Replace cookiecutter.replay.make_sure_path_exists function. Used to mock internal function and limit test scope. Always return expected value: True """ return mocker.patch('cookiecutter.replay.make_sure_path_exists', return_value=True) def test_ioerror_if_replay_dir_creation_fails(mock_ensure_failure, replay_test_dir): """Test that replay.dump raises when the replay_dir cannot be created.""" with pytest.raises(OSError): replay.dump(replay_test_dir, 'foo', {'cookiecutter': {'hello': 'world'}}) mock_ensure_failure.assert_called_once_with(replay_test_dir) def test_run_json_dump( mocker, mock_ensure_success, mock_user_config, template_name, context, replay_test_dir, replay_file, ): """Test that replay.dump runs json.dump under the hood and that the context \ is correctly written to the expected file in the replay_dir.""" spy_get_replay_file = mocker.spy(replay, 'get_file_name') mock_json_dump = mocker.patch('json.dump', side_effect=json.dump) replay.dump(replay_test_dir, template_name, context) assert not mock_user_config.called mock_ensure_success.assert_called_once_with(replay_test_dir) spy_get_replay_file.assert_called_once_with(replay_test_dir, template_name) assert mock_json_dump.call_count == 1 (dumped_context, outfile_handler), kwargs = mock_json_dump.call_args assert outfile_handler.name == replay_file assert dumped_context == context cookiecutter-2.6.0/tests/replay/test_load.py000066400000000000000000000035321456543333500212230ustar00rootroot00000000000000"""test_load.""" import json import os import pytest from cookiecutter import replay @pytest.fixture def template_name(): """Fixture to return a valid template_name.""" return 'cookiedozer_load' @pytest.fixture def replay_file(replay_test_dir, template_name): """Fixture to return a actual file name of the dump.""" file_name = f'{template_name}.json' return os.path.join(replay_test_dir, file_name) def test_type_error_if_no_template_name(replay_test_dir): """Test that replay.load raises if the template_name is not a valid str.""" with pytest.raises(TypeError): replay.load(replay_test_dir, None) def test_value_error_if_key_missing_in_context(mocker, replay_test_dir): """Test that replay.load raises if the loaded context does not contain \ 'cookiecutter'.""" with pytest.raises(ValueError): replay.load(replay_test_dir, 'invalid_replay') def test_io_error_if_no_replay_file(mocker, replay_test_dir): """Test that replay.load raises if it cannot find a replay file.""" with pytest.raises(IOError): replay.load(replay_test_dir, 'no_replay') def test_run_json_load( mocker, mock_user_config, template_name, context, replay_test_dir, replay_file ): """Test that replay.load runs json.load under the hood and that the context \ is correctly loaded from the file in replay_dir.""" spy_get_replay_file = mocker.spy(replay, 'get_file_name') mock_json_load = mocker.patch('json.load', side_effect=json.load) loaded_context = replay.load(replay_test_dir, template_name) assert not mock_user_config.called spy_get_replay_file.assert_called_once_with(replay_test_dir, template_name) assert mock_json_load.call_count == 1 (infile_handler,), kwargs = mock_json_load.call_args assert infile_handler.name == replay_file assert loaded_context == context cookiecutter-2.6.0/tests/replay/test_replay.py000066400000000000000000000043711456543333500216020ustar00rootroot00000000000000"""test_replay.""" import os import pytest from cookiecutter import exceptions, main, replay @pytest.mark.parametrize("replay_file_name", ['bar', 'bar.json']) def test_get_replay_file_name(replay_file_name): """Make sure that replay.get_file_name generates a valid json file path.""" exp_replay_file_path = os.path.join('foo', 'bar.json') replay_file_path = replay.get_file_name('foo', replay_file_name) assert replay_file_path == exp_replay_file_path @pytest.mark.parametrize( 'invalid_kwargs', ( {'no_input': True}, {'extra_context': {}}, {'no_input': True, 'extra_context': {}}, ), ) def test_raise_on_invalid_mode(invalid_kwargs): """Test `cookiecutter` raise exception on unacceptable `replay` request.""" with pytest.raises(exceptions.InvalidModeException): main.cookiecutter('foo', replay=True, **invalid_kwargs) def test_main_does_not_invoke_dump_but_load(mocker): """Test `cookiecutter` calling correct functions on `replay`.""" mock_prompt = mocker.patch('cookiecutter.main.prompt_for_config') mock_gen_context = mocker.patch('cookiecutter.main.generate_context') mock_gen_files = mocker.patch('cookiecutter.main.generate_files') mock_replay_dump = mocker.patch('cookiecutter.main.dump') mock_replay_load = mocker.patch('cookiecutter.main.load') main.cookiecutter('tests/fake-repo-tmpl/', replay=True) assert not mock_prompt.called assert mock_gen_context.called assert mock_replay_dump.called assert mock_replay_load.called assert mock_gen_files.called def test_main_does_not_invoke_load_but_dump(mocker): """Test `cookiecutter` calling correct functions on non-replay launch.""" mock_prompt = mocker.patch('cookiecutter.main.prompt_for_config') mock_gen_context = mocker.patch('cookiecutter.main.generate_context') mock_gen_files = mocker.patch('cookiecutter.main.generate_files') mock_replay_dump = mocker.patch('cookiecutter.main.dump') mock_replay_load = mocker.patch('cookiecutter.main.load') main.cookiecutter('tests/fake-repo-tmpl/', replay=False) assert mock_prompt.called assert mock_gen_context.called assert mock_replay_dump.called assert not mock_replay_load.called assert mock_gen_files.called cookiecutter-2.6.0/tests/repository/000077500000000000000000000000001456543333500176135ustar00rootroot00000000000000cookiecutter-2.6.0/tests/repository/test_abbreviation_expansion.py000066400000000000000000000034561456543333500257650ustar00rootroot00000000000000"""Collection of tests around common path and url shorthands.""" import pytest from cookiecutter.config import BUILTIN_ABBREVIATIONS from cookiecutter.repository import expand_abbreviations @pytest.mark.parametrize( ('template', 'abbreviations', 'expected_result'), [ ('foo', {'foo': 'bar'}, 'bar'), ('baz', {'foo': 'bar'}, 'baz'), ('xx:a', {'xx': '<{0}>'}, ''), ('gh:a', {'gh': '<{0}>'}, ''), ('xx:a', {'xx': '<>'}, '<>'), ( 'gh:pydanny/cookiecutter-django', BUILTIN_ABBREVIATIONS, 'https://github.com/pydanny/cookiecutter-django.git', ), ( 'gl:pydanny/cookiecutter-django', BUILTIN_ABBREVIATIONS, 'https://gitlab.com/pydanny/cookiecutter-django.git', ), ( 'bb:pydanny/cookiecutter-django', BUILTIN_ABBREVIATIONS, 'https://bitbucket.org/pydanny/cookiecutter-django', ), ], ids=( 'Simple expansion', 'Skip expansion (expansion not an abbreviation)', 'Expansion prefix', 'expansion_override_builtin', 'expansion_prefix_ignores_suffix', 'Correct expansion for builtin abbreviations (github)', 'Correct expansion for builtin abbreviations (gitlab)', 'Correct expansion for builtin abbreviations (bitbucket)', ), ) def test_abbreviation_expansion(template, abbreviations, expected_result): """Verify abbreviation unpacking.""" expanded = expand_abbreviations(template, abbreviations) assert expanded == expected_result def test_abbreviation_expansion_prefix_not_0_in_braces(): """Verify abbreviation unpacking raises error on incorrect index.""" with pytest.raises(IndexError): expand_abbreviations('xx:a', {'xx': '{1}'}) cookiecutter-2.6.0/tests/repository/test_determine_repo_dir_clones_repo.py000066400000000000000000000062261456543333500274610ustar00rootroot00000000000000"""Collection of tests around cloning cookiecutter template repositories.""" import os import pytest from cookiecutter import exceptions, repository @pytest.mark.parametrize( 'template, is_url', [ ('/path/to/zipfile.zip', False), ('https://example.com/path/to/zipfile.zip', True), ('http://example.com/path/to/zipfile.zip', True), ], ) def test_zipfile_unzip(mocker, template, is_url, user_config_data): """Verify zip files correctly handled for different source locations. `unzip()` should be called with correct args when `determine_repo_dir()` is passed a zipfile, or a URL to a zipfile. """ mock_clone = mocker.patch( 'cookiecutter.repository.unzip', return_value='tests/fake-repo-tmpl', autospec=True, ) project_dir, cleanup = repository.determine_repo_dir( template, abbreviations={}, clone_to_dir=user_config_data['cookiecutters_dir'], checkout=None, no_input=True, password=None, ) mock_clone.assert_called_once_with( zip_uri=template, is_url=is_url, clone_to_dir=user_config_data['cookiecutters_dir'], no_input=True, password=None, ) assert os.path.isdir(project_dir) assert cleanup assert 'tests/fake-repo-tmpl' == project_dir @pytest.fixture def template_url(): """URL to example Cookiecutter template on GitHub. Note: when used, git clone is mocked. """ return 'https://github.com/pytest-dev/cookiecutter-pytest-plugin.git' def test_repository_url_should_clone(mocker, template_url, user_config_data): """Verify repository url triggers clone function. `clone()` should be called with correct args when `determine_repo_dir()` is passed a repository template url. """ mock_clone = mocker.patch( 'cookiecutter.repository.clone', return_value='tests/fake-repo-tmpl', autospec=True, ) project_dir, cleanup = repository.determine_repo_dir( template_url, abbreviations={}, clone_to_dir=user_config_data['cookiecutters_dir'], checkout=None, no_input=True, ) mock_clone.assert_called_once_with( repo_url=template_url, checkout=None, clone_to_dir=user_config_data['cookiecutters_dir'], no_input=True, ) assert os.path.isdir(project_dir) assert not cleanup assert 'tests/fake-repo-tmpl' == project_dir def test_repository_url_with_no_context_file(mocker, template_url, user_config_data): """Verify cloned repository without `cookiecutter.json` file raises error.""" mocker.patch( 'cookiecutter.repository.clone', return_value='tests/fake-repo-bad', autospec=True, ) with pytest.raises(exceptions.RepositoryNotFound) as err: repository.determine_repo_dir( template_url, abbreviations={}, clone_to_dir=None, checkout=None, no_input=True, ) assert str(err.value) == ( 'A valid repository for "{}" could not be found in the following ' 'locations:\n{}'.format(template_url, 'tests/fake-repo-bad') ) cookiecutter-2.6.0/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py000066400000000000000000000024171456543333500327440ustar00rootroot00000000000000"""Tests around detection whether cookiecutter templates are cached locally.""" import os from pathlib import Path import pytest from cookiecutter import repository @pytest.fixture def template(): """Fixture. Return simple string as template name.""" return 'cookiecutter-pytest-plugin' @pytest.fixture def cloned_cookiecutter_path(user_config_data, template): """Fixture. Create fake project directory in special user folder.""" cookiecutters_dir = user_config_data['cookiecutters_dir'] cloned_template_path = os.path.join(cookiecutters_dir, template) os.mkdir(cloned_template_path) Path(cloned_template_path, "cookiecutter.json").touch() # creates file return cloned_template_path def test_should_find_existing_cookiecutter( template, user_config_data, cloned_cookiecutter_path ): """ Should find folder created by `cloned_cookiecutter_path` and return it. This folder is considered like previously cloned project directory. """ project_dir, cleanup = repository.determine_repo_dir( template=template, abbreviations={}, clone_to_dir=user_config_data['cookiecutters_dir'], checkout=None, no_input=True, ) assert cloned_cookiecutter_path == project_dir assert not cleanup cookiecutter-2.6.0/tests/repository/test_determine_repo_dir_finds_subdirectories.py000066400000000000000000000045261456543333500313630ustar00rootroot00000000000000"""Tests around locally cached cookiecutter template repositories.""" import os from pathlib import Path import pytest from cookiecutter import exceptions, repository @pytest.fixture def template(): """Fixture. Return simple string as template name.""" return 'cookiecutter-pytest-plugin' @pytest.fixture def cloned_cookiecutter_path(user_config_data, template): """Fixture. Prepare folder structure for tests in this file.""" cookiecutters_dir = user_config_data['cookiecutters_dir'] cloned_template_path = os.path.join(cookiecutters_dir, template) if not os.path.exists(cloned_template_path): os.mkdir(cloned_template_path) # might exist from other tests. subdir_template_path = os.path.join(cloned_template_path, 'my-dir') if not os.path.exists(subdir_template_path): os.mkdir(subdir_template_path) Path(subdir_template_path, 'cookiecutter.json').touch() # creates file return subdir_template_path def test_should_find_existing_cookiecutter( template, user_config_data, cloned_cookiecutter_path ): """Find `cookiecutter.json` in sub folder created by `cloned_cookiecutter_path`.""" project_dir, cleanup = repository.determine_repo_dir( template=template, abbreviations={}, clone_to_dir=user_config_data['cookiecutters_dir'], checkout=None, no_input=True, directory='my-dir', ) assert cloned_cookiecutter_path == project_dir assert not cleanup def test_local_repo_typo(template, user_config_data, cloned_cookiecutter_path): """Wrong pointing to `cookiecutter.json` sub-directory should raise.""" with pytest.raises(exceptions.RepositoryNotFound) as err: repository.determine_repo_dir( template=template, abbreviations={}, clone_to_dir=user_config_data['cookiecutters_dir'], checkout=None, no_input=True, directory='wrong-dir', ) wrong_full_cookiecutter_path = os.path.join( os.path.dirname(cloned_cookiecutter_path), 'wrong-dir' ) assert str(err.value) == ( 'A valid repository for "{}" could not be found in the following ' 'locations:\n{}'.format( template, '\n'.join( [os.path.join(template, 'wrong-dir'), wrong_full_cookiecutter_path] ), ) ) cookiecutter-2.6.0/tests/repository/test_determine_repository_should_use_local_repo.py000066400000000000000000000037601456543333500321360ustar00rootroot00000000000000"""Tests around using locally cached cookiecutter template repositories.""" from pathlib import Path import pytest from cookiecutter import exceptions, repository def test_finds_local_repo(tmp_path): """A valid local repository should be returned.""" project_dir, cleanup = repository.determine_repo_dir( 'tests/fake-repo', abbreviations={}, clone_to_dir=str(tmp_path), checkout=None, no_input=True, ) assert 'tests/fake-repo' == project_dir assert not cleanup def test_local_repo_with_no_context_raises(tmp_path): """A local repository without a cookiecutter.json should raise a \ `RepositoryNotFound` exception.""" template_path = str(Path('tests', 'fake-repo-bad')) with pytest.raises(exceptions.RepositoryNotFound) as err: repository.determine_repo_dir( template_path, abbreviations={}, clone_to_dir=str(tmp_path), checkout=None, no_input=True, ) assert str(err.value) == ( 'A valid repository for "{}" could not be found in the following ' 'locations:\n{}'.format( template_path, '\n'.join( [template_path, str(tmp_path.joinpath('tests', 'fake-repo-bad'))] ), ) ) def test_local_repo_typo(tmp_path): """An unknown local repository should raise a `RepositoryNotFound` \ exception.""" template_path = str(Path('tests', 'unknown-repo')) with pytest.raises(exceptions.RepositoryNotFound) as err: repository.determine_repo_dir( template_path, abbreviations={}, clone_to_dir=str(tmp_path), checkout=None, no_input=True, ) assert str(err.value) == ( 'A valid repository for "{}" could not be found in the following ' 'locations:\n{}'.format( template_path, '\n'.join([template_path, str(tmp_path.joinpath('tests', 'unknown-repo'))]), ) ) cookiecutter-2.6.0/tests/repository/test_is_repo_url.py000066400000000000000000000042661456543333500235560ustar00rootroot00000000000000"""Tests for all supported cookiecutter template repository locations.""" import pytest from cookiecutter.config import BUILTIN_ABBREVIATIONS from cookiecutter.repository import expand_abbreviations, is_repo_url, is_zip_file @pytest.fixture( params=[ '/path/to/zipfile.zip', 'https://example.com/path/to/zipfile.zip', 'http://example.com/path/to/zipfile.zip', ] ) def zipfile(request): """Fixture. Represent possible paths to zip file.""" return request.param def test_is_zip_file(zipfile): """Verify is_repo_url works.""" assert is_zip_file(zipfile) is True @pytest.fixture( params=[ 'gitolite@server:team/repo', 'git@github.com:audreyfeldroy/cookiecutter.git', 'https://github.com/cookiecutter/cookiecutter.git', 'git+https://private.com/gitrepo', 'hg+https://private.com/mercurialrepo', 'https://bitbucket.org/pokoli/cookiecutter.hg', 'file://server/path/to/repo.git', ] ) def remote_repo_url(request): """Fixture. Represent possible URI to different repositories types.""" return request.param def test_is_repo_url_for_remote_urls(remote_repo_url): """Verify is_repo_url works.""" assert is_repo_url(remote_repo_url) is True @pytest.fixture( params=[ '/audreyr/cookiecutter.git', '/home/audreyr/cookiecutter', ( 'c:\\users\\foo\\appdata\\local\\temp\\1\\pytest-0\\' 'test_default_output_dir0\\template' ), ] ) def local_repo_url(request): """Fixture. Represent possible paths to local resources.""" return request.param def test_is_repo_url_for_local_urls(local_repo_url): """Verify is_repo_url works.""" assert is_repo_url(local_repo_url) is False def test_expand_abbreviations(): """Validate `repository.expand_abbreviations` correctly translate url.""" template = 'gh:audreyfeldroy/cookiecutter-pypackage' # This is not a valid repo url just yet! # First `repository.expand_abbreviations` needs to translate it assert is_repo_url(template) is False expanded_template = expand_abbreviations(template, BUILTIN_ABBREVIATIONS) assert is_repo_url(expanded_template) is True cookiecutter-2.6.0/tests/repository/test_repository_has_cookiecutter_json.py000066400000000000000000000011371456543333500301110ustar00rootroot00000000000000"""Tests for `repository_has_cookiecutter_json` function.""" import pytest from cookiecutter.repository import repository_has_cookiecutter_json def test_valid_repository(): """Validate correct response if `cookiecutter.json` file exist.""" assert repository_has_cookiecutter_json('tests/fake-repo') @pytest.mark.parametrize( 'invalid_repository', (['tests/fake-repo-bad', 'tests/unknown-repo']) ) def test_invalid_repository(invalid_repository): """Validate correct response if `cookiecutter.json` file not exist.""" assert not repository_has_cookiecutter_json(invalid_repository) cookiecutter-2.6.0/tests/test-config/000077500000000000000000000000001456543333500176165ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-config/config-expand-user.yaml000066400000000000000000000000761456543333500242030ustar00rootroot00000000000000cookiecutters_dir: "~/templates" replay_dir: "~/replay-files" cookiecutter-2.6.0/tests/test-config/config-expand-vars.yaml000066400000000000000000000001141456543333500241710ustar00rootroot00000000000000cookiecutters_dir: "$COOKIES/templates" replay_dir: "$COOKIES/replay-files" cookiecutter-2.6.0/tests/test-config/empty-config.yaml000066400000000000000000000000001456543333500230710ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-config/invalid-config-w-array.yaml000066400000000000000000000000141456543333500247460ustar00rootroot00000000000000- 1 - 2 - 3 cookiecutter-2.6.0/tests/test-config/invalid-config-w-multiple-docs.yaml000066400000000000000000000006341456543333500264210ustar00rootroot00000000000000--- default_context: full_name: "Firstname Lastname" email: "firstname.lastname@gmail.com" github_username: "example" project: description: "description" tags: - "first" - "second" - "third" --- cookiecutters_dir: "/home/example/some-path-to-templates" replay_dir: "/home/example/some-path-to-replay-files" abbreviations: helloworld: "https://github.com/hackebrot/helloworld" cookiecutter-2.6.0/tests/test-config/invalid-config.yaml000066400000000000000000000002421456543333500233710ustar00rootroot00000000000000default_context full_name: email: "firstname.lastname@gmail.com" github_username: "example" cookiecutters_dir: "/home/example/some-path-to-templates" cookiecutter-2.6.0/tests/test-config/valid-config.yaml000066400000000000000000000006701456543333500230470ustar00rootroot00000000000000default_context: full_name: "Firstname Lastname" email: "firstname.lastname@gmail.com" github_username: "example" project: description: "description" tags: - "first" - "second" - "third" cookiecutters_dir: "/home/example/some-path-to-templates" replay_dir: "/home/example/some-path-to-replay-files" abbreviations: helloworld: "https://github.com/hackebrot/helloworld" cookiecutter-2.6.0/tests/test-config/valid-partial-config.yaml000066400000000000000000000001761456543333500245020ustar00rootroot00000000000000default_context: full_name: "Firstname Lastname" email: "firstname.lastname@gmail.com" github_username: "example" cookiecutter-2.6.0/tests/test-extensions/000077500000000000000000000000001456543333500205505ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-extensions/custom-extension-post/000077500000000000000000000000001456543333500250575ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-extensions/custom-extension-post/cookiecutter.json000066400000000000000000000001571456543333500304550ustar00rootroot00000000000000{ "project_slug": "foobar", "name": "World", "_extensions": [ "hello_extension.HelloExtension" ] } cookiecutter-2.6.0/tests/test-extensions/custom-extension-post/hooks/000077500000000000000000000000001456543333500262025ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-extensions/custom-extension-post/hooks/post_gen_project.py000066400000000000000000000003101456543333500321120ustar00rootroot00000000000000"""Test post-gen hook in extensions are available and exposed methods are callable.""" import sys if '{% hello cookiecutter.name %}' == 'Hello Cookiemonster!': sys.exit(0) else: sys.exit(1) cookiecutter-2.6.0/tests/test-extensions/custom-extension-post/{{cookiecutter.project_slug}}/000077500000000000000000000000001456543333500332365ustar00rootroot00000000000000README.rst000066400000000000000000000000361456543333500346450ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-extensions/custom-extension-post/{{cookiecutter.project_slug}}{% hello cookiecutter.name %} cookiecutter-2.6.0/tests/test-extensions/custom-extension-pre/000077500000000000000000000000001456543333500246605ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-extensions/custom-extension-pre/cookiecutter.json000066400000000000000000000001571456543333500302560ustar00rootroot00000000000000{ "project_slug": "foobar", "name": "World", "_extensions": [ "hello_extension.HelloExtension" ] } cookiecutter-2.6.0/tests/test-extensions/custom-extension-pre/hooks/000077500000000000000000000000001456543333500260035ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-extensions/custom-extension-pre/hooks/pre_gen_project.py000066400000000000000000000003071456543333500315220ustar00rootroot00000000000000"""Test pre-gen hook in extensions are available and exposed methods are callable.""" import sys if '{% hello cookiecutter.name %}' == 'Hello Cookiemonster!': sys.exit(0) else: sys.exit(1) cookiecutter-2.6.0/tests/test-extensions/custom-extension-pre/{{cookiecutter.project_slug}}/000077500000000000000000000000001456543333500330375ustar00rootroot00000000000000README.rst000066400000000000000000000000361456543333500344460ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-extensions/custom-extension-pre/{{cookiecutter.project_slug}}{% hello cookiecutter.name %} cookiecutter-2.6.0/tests/test-extensions/default/000077500000000000000000000000001456543333500221745ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-extensions/default/cookiecutter.json000066400000000000000000000001471456543333500255710ustar00rootroot00000000000000{ "project_slug": "{{ 'It\\'s slugified Foobar' | slugify() }}", "year": "{% now 'utc', '%Y' %}" } cookiecutter-2.6.0/tests/test-extensions/default/{{cookiecutter.project_slug}}/000077500000000000000000000000001456543333500303535ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-extensions/default/{{cookiecutter.project_slug}}/HISTORY.rst000066400000000000000000000001341456543333500322440ustar00rootroot00000000000000History ------- 0.1.0 ({% now 'utc', '%Y-%m-%d' %}) {{ "-" * 18 }} First release on PyPI. cookiecutter-2.6.0/tests/test-extensions/default/{{cookiecutter.project_slug}}/id000066400000000000000000000000161456543333500306670ustar00rootroot00000000000000{{ uuid4() }} cookiecutter-2.6.0/tests/test-extensions/hello_extension/000077500000000000000000000000001456543333500237475ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-extensions/hello_extension/__init__.py000066400000000000000000000000271456543333500260570ustar00rootroot00000000000000"""Hello Extension.""" cookiecutter-2.6.0/tests/test-extensions/hello_extension/hello_extension.py000066400000000000000000000014231456543333500275200ustar00rootroot00000000000000"""Provides custom extension, exposing a ``hello`` command.""" from jinja2 import nodes from jinja2.ext import Extension class HelloExtension(Extension): """Simple jinja2 extension for cookiecutter test purposes.""" tags = {'hello'} def __init__(self, environment): """Hello Extension Constructor.""" super().__init__(environment) def _hello(self, name): """Do actual tag replace when invoked by parser.""" return f'Hello {name}!' def parse(self, parser): """Work when something match `tags` variable.""" lineno = next(parser.stream).lineno node = parser.parse_expression() call_method = self.call_method('_hello', [node], lineno=lineno) return nodes.Output([call_method], lineno=lineno) cookiecutter-2.6.0/tests/test-extensions/local_extension/000077500000000000000000000000001456543333500237365ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-extensions/local_extension/cookiecutter.json000066400000000000000000000004521456543333500273320ustar00rootroot00000000000000{ "project_slug": "Foobar", "test_value_class_based": "{{cookiecutter.project_slug | foobar}}", "test_value_function_based": "{{cookiecutter.project_slug | simplefilterextension}}", "_extensions": [ "local_extensions.simplefilterextension", "local_extensions.FoobarExtension" ] } cookiecutter-2.6.0/tests/test-extensions/local_extension/local_extensions/000077500000000000000000000000001456543333500273075ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-extensions/local_extension/local_extensions/__init__.py000066400000000000000000000001011456543333500314100ustar00rootroot00000000000000from .main import FoobarExtension, simplefilterextension # noqa cookiecutter-2.6.0/tests/test-extensions/local_extension/local_extensions/main.py000066400000000000000000000010451456543333500306050ustar00rootroot00000000000000"""Provides custom extension, exposing a ``foobar`` filter.""" from jinja2.ext import Extension from cookiecutter.utils import simple_filter class FoobarExtension(Extension): """Simple jinja2 extension for cookiecutter test purposes.""" def __init__(self, environment): """Foobar Extension Constructor.""" super().__init__(environment) environment.filters['foobar'] = lambda v: v * 2 @simple_filter def simplefilterextension(v): """Provide a simple function-based filter extension.""" return v.upper() cookiecutter-2.6.0/tests/test-extensions/local_extension/{{cookiecutter.project_slug}}/000077500000000000000000000000001456543333500321155ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-extensions/local_extension/{{cookiecutter.project_slug}}/HISTORY.rst000066400000000000000000000002131456543333500340040ustar00rootroot00000000000000History ------- 0.1.0 ----- First release of {{cookiecutter.test_value_class_based}} on PyPI. {{cookiecutter.test_value_function_based}} cookiecutter-2.6.0/tests/test-extensions/unknown/000077500000000000000000000000001456543333500222475ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-extensions/unknown/cookiecutter.json000066400000000000000000000001711456543333500256410ustar00rootroot00000000000000{ "project_slug": "Foobar", "year": "{{cookiecutter.nope | foobar}}", "_extensions": [ "FoobarExtension" ] } cookiecutter-2.6.0/tests/test-extensions/unknown/{{cookiecutter.project_slug}}/000077500000000000000000000000001456543333500304265ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-extensions/unknown/{{cookiecutter.project_slug}}/HISTORY.rst000066400000000000000000000001671456543333500323250ustar00rootroot00000000000000History ------- 0.1.0 ({{cookiecutter.nope | foobar}}) -------------------------------------- First release on PyPI. cookiecutter-2.6.0/tests/test-generate-binaries/000077500000000000000000000000001456543333500217355ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-generate-binaries/input{{cookiecutter.binary_test}}/000077500000000000000000000000001456543333500310175ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-generate-binaries/input{{cookiecutter.binary_test}}/.DS_Store000066400000000000000000000360041456543333500325050ustar00rootroot00000000000000Bud1   Storedi  @ @ @ @ .DS_Storedilcblob >v1b682px-Oscypki.jpgdilcblob vze1bzcheeseland-notes.txtdilcblob v#1b{cookiecutter_todo.txtdilcblob rv1b|#Empty Tunnelblick VPN Configurationbwspblobbplist00  \WindowBounds[ShowSidebar]ShowStatusBar[ShowPathbar[ShowToolbar\SidebarWidth_{{408, 292}, {770, 438}}  ".v\) inspirationdilcblob ;"1b} resourcesbwspblobbplist00 \WindowBounds[ShowSidebar]ShowStatusBar[ShowPathbar[ShowToolbar\SidebarWidth_{{446, 338}, {770, 438}} ". VS`i̯ 8Rnkk22;A!.΁YϪV!}{G,Zy':jT:S@^p=BrlHL`(*RX ϺaL~O>P@ GG;M'<: A7z Oq/\?) c[ 'P l^Q^([&` ܥc[~ !Ӯ/ulJwʲOB2}Ixt]"m\8Rd^ E+fJXԡg5Jn5ql3` 5"K%pc[/]Q@`:0*w8z+1uW̑g?zO"F[q*(as;pppN6k>78 uN-pl+pp-)8T ] 3"vm L7юm%owFrbNul$zWW&(#z z|bdCR4 '>b/\Lj xuG nSu ,!^pűiplk0!9^wl; D_5o泌$9e; '8u p#тd8j2 QSaCj2?)dzpZѱ-AS3`헰~,tlkb>1 D :1`P5 c֥0br)Z= 8%dSQֱ oF TRbt ݿp=&T!!zD' MYRQGqy(;ݎm[Y%:􎒱_x$s=0? ^C+P O]_pl30mg.pk&0`2SԹ8Y|13ehBan#Zw5Щ5[Q~ mc`a_֫D0A3slKpLPԸ/&y9put=n'F:xҥGGVbND]?#Ev=L%8P,F*tkle 6`|z~ЭL8c[P(f?%=U n{c[ձhhKIK?2+(%u=6=Оvj~# o::wR:m+9}-kR9ސ~WHpMGX[D 3 x4iQ@pp=Dc+Z37Ю<\k}ޡ4>(vu.74*ݼ >5ӵ@ߡeaD?Mm[Vn {QVm eüdHِ^s8 "sn*QI SuMONjdc[=1{3@ݰ BqtlJ!r]%#*@3&z(f\izEk+mx?I#@L6¬~JxU P!r}_rte4(BBS>Sb,?pl"ʵ=fP\Ůo5E#۸SnmE?6}h:Lh2/3+NN1E$rlkdeh;ٗ_1zsڀ)E~}?zO":hޤOxHv>‰z҃e(7֘4E 3KZD9R38+ڿ?5ӞZ%sh 'Yx{uWOzen2$cLw'iA+JCI#ӈk0Nb2Ɯԛql#fNubɱ4!iYzgupտQ\VXyP2Hq:iPmoʅ0>TU%.qRǶn^C~*PA\m-#3=>wjR.Wu(}ǶBYEEefHs3㕡b4 HmMAyP#ˁn.ȪW~'iE΋}DؖzB{ +$J\#.(7S8u!݃c'O!αE?03A}0mB\|81c[VW>g:5/"'H/Ƕ*ZGǁ2उޓul+JU ߏ5slnG / RqJ pDEG4=Ci_BWP4˅xO^Pw`S|c[SDFrn(p̯?73 iIXR=D_8Uc:=72Y2N-& <6CcWN >;v=m0A6&Ѿ=wA1OԌe4 );h=a#S;f߯rRݸS)z3XsgCrc45׺oʮIIY1-wcZ ݠ]wGpC >%Lz $\v=e (E 26mYg8ՃAE?B{DX]ʸb,Nu=.S\Ͽ5 @}(w 5Ԑ+x (XuUpM/&M+}M yӅXI(QS,A'_hJ"ikcW#dI%GJfǥלṨ.|/cU$m}{;@=K҃v }2|/s8PzNxnx1ǶFfj@~0 ";:Rh9nj8 m!um3?NOr׼'!>?{5 -.=0Z>uI}ZűA(0uU7T:Tuo I؈݆UT21{OQF]C[e2L VҾ!#}BVcZ l8##VơZ |yALZ9+[fdZ oellgBEHLZkvglassmusicsearchuni2709heartstarstaremptyuserfilmthlargeththlistokremovezoominzoomoutoffsignalcogtrashhomefiletimeroaddownloadaltdownloaduploadinboxplaycirclerepeatrefreshlistaltlockflagheadphonesvolumeoffvolumedownvolumeupqrcodebarcodetagtagsbookbookmarkprintcamerafontbolditalictextheighttextwidthalignleftaligncenteralignrightalignjustifylistindentleftindentrightfacetimevideopictureuni270FmapmarkeradjusttinteditsharecheckmovestepbackwardfastbackwardbackwardplaypausestopforwardfastforwardstepforwardejectchevronleftchevronrightplussignminussignremovesignoksignquestionsigninfosignscreenshotremovecircleokcirclebancirclearrowleftarrowrightarrowuparrowdownsharealtresizefullresizesmallexclamationsigngiftleaffireeyeopeneyeclosewarningsignplanecalendarrandomcommentsmagnetchevronupchevrondownretweetshoppingcartfolderclosefolderopenresizeverticalresizehorizontalhddbullhornbellcertificatethumbsupthumbsdownhandrighthandlefthandtophanddowncirclearrowrightcirclearrowleftcirclearrowtopcirclearrowdownglobewrenchtasksfilterbriefcasefullscreendashboardpaperclipheartemptylinkphonepushpinEurousdgbpsortsortbyalphabetsortbyalphabetaltsortbyordersortbyorderaltsortbyattributessortbyattributesaltuncheckedexpandcollapsecollapsetopJan KovarikGLYPHICONS Halflings-RegularGLYPHICONS Halflings{&07@FMQZ_fmqv&+8=KX]aft} ).28CLQU_dhlpuy "&+048<@FJNR[^dinsw|+ ! = * ,\% [ ! ou)t - \- - f "k" . p % V \. uo 4  * ^ T> ^] A 4'k c '>G ( 1 BB e ; U X  % 3 +3 5M uo 1 k1 % PK < )uto E S|M  J) ((} ST pHHpi \\ Iv|E Jq / J J J P\ 0 ¸ |JE |/ N~ pHHp : `4` ouR % Y \ T^ ^T T^^T 5 ` d\ r' $  \\ |Z  fg d S \ ^T c$N $c \~ \ PP \' $ \  $ \ v\wR \D sm { |  T \ $Q \Z -'V Tg c  h 'Y'$' (J \$ h \ \ Z 9  '\'g  U } J t  ' \ o <  '\ -ouuo d \\ !! w  '  ` ;%;-QE 7SWq'mBo t  7 9 . l @OyFUa F{  :Pf|!Tqp*Gr n<O`^#Gk8jU? 0 !~!!!"#J###$/$%-&&'9''(),))*3*****+$+b++ wR : Q 'N$|:/FUK?2n"i#ssEU}>>KV2n"i#sse.( )6@l m: Y: ' : $$| IMO^B}~B^ = P$= YT vX"ggU yb N¸%N 9999 T%'$}: $pp: \z $gz L L $$g\P L :|:" |$" |$" 9 Pou\R \,P%$9 .bchh78pp hhhhhhhhe.\s t\6V{l @ ''dff.( )e ; X @l .$|[ [ *Pou$Y g*C 7^ ] ^ S7 :p91g! 1gpv\Pw$< | \: $ \ \i{||Kjlm:`jn{~+ %+;:, $,~zܵjie;nkk,epttqss %,ej~l{njaiifn܆n{k}ke+ottrrs $ *+ + * P\ xw'}~Y  }7'\¸T]p p p p v]\Z $_b PQ }}  7$$t [ ?*[ ?2$h \j  }w'xC: d6eC $$>tdwZ P~ ~$$~_C[ *[ \2**t ***4~ **Pm o[P\Po Y\g Y +)#)6 ' ]\f Q &&F6/^ ] SS> *+ & ]?v[ '$'*w[ B%Z%B*@P$$%%@)+ *> mY@ %%v@?]**^ ] /6Eph : : |t'XP\XP\XDP\r'v|\ChN 1g 11'c SM G \**Y w x''\$\\$|w \|h: }}Y} PPi}}Y} h@OLO 3[y!!DD!!D!!r !!  [$F1).k5UFU޳ NbN:͒ _<ˑ%('Y#/hct \\g ޳ FVE1*.k4UGdj i' ''$!'TZ\t !'\  EU i|t \''%"'J N @`j'P\~ U 'Z@''@'@dAB''r'J $\J h g 0wW\\'\z\|||||k|\||||\| h g ''H0D'E0k@_ v&'Q ~_ HccLcd˲ʋcdKddv&&Q }_ kPPVVYeudcKdc˳ˋcdKcdv*|||1n tpVOVP  \ UcV}\y' xP\ P }}~e,~e,}}h_h: \U|T^ J*\(**\*`0P\\PP\ƺƺ\Pw {: 5֤w|HIQ%i21hmasã#zYaa] F]| DC.(MDt{@RYjY2/dF[ЋWW0 *? gB!(;&##h & !K\;Q =Rp{5AJ2\'R'v \\\VQ R.iȋ\zN y -Sj;@;;@H\R.iɋ\zN y .R);@;;@i Q<+|<|+<+<i ;;I N~) $) |$|) $/I N~* ///I h*# \# \# \# @ *O '+3 J@]']@:7v@ O '% 3 @\@;7 D 1ibbiibbiU vxswwsswxr0Ju|OAAOOAAO~~iQ $--ymzy(/jj55jjjjon7o2a60[sxk&gsTbT]`(((([ *(   Wl#qdy&U!({NfXyx+dH0X0 o6-_2 NNl8u| 005Y>A %Ѷ뢉8\-5e㐋A FEba_g 8=| :#''BClZWU \| ZU Z\| U WCZw C$Dv~P{ {D| w P{| n \xPkPn ouY  x{k |w |hm Io{k |Kw hm I{KC'ouuo'.|- w ZYZZ0ZYZZ?9\\\\\\\\\Zf ?9\ f '!6!!hh!r "hh!"!"ih"""!f 'BB%CB//3W 9L{rcwez}z9NK1zHGL\\ ''fU lPe(3'\| \fGe'V\V)\eY\Y5 )_\kh\~ b'=kf\e_\`k?+| ]k)F?'k, ::::, Yd4Z$ br> sb #K@;A;@KZ D Pj YTj Z D : Yv ` Pb Z PQ QU[D : YAt/{h5"" $$Y""kkPY"!$gvZ Z PP$ $$$Z Z n |i\R PKK!!KK\KKKK\KKr KK\KK KK_$'p9ppQcupQppup\  \R : $\$fE$~}$hW@zuiBY.BS |$$ X\NpwY #]^}"\S5b] pCH)VK4"e.Ko3~nH|383{-$U^'<.J/I ~i"W~W~JG$XoTNB((36SU^.^/.""h‚{=*m8Cet4N*N cy55q5 $!Ev ::::yl2 T 3FuEVlTb N*edca qcc{i@f#(: (K=FGeWD$, De%Mq*), f%H,]Mݼv ("s\\EVl(K%|\YwĶ``wOY/Qg Q'  [H88HH8wyzepbp^bTTzKp~r=T^{zwd*W **puto|ou\7 P\7 P\7 \|\x\3\IH!">$!"IIYvD xc Sg ĂM G Z Q j $j kk! 1OggO1\ihZZZZZZZZ|h [$k_k$_X o xQ tw{ue6||||otb [\ouuooud j d ]UtpvD xc \': T v\|w\| : P b \ZZs ZZmZZs YYwylpo4opip@wr'3\z z |&\pttp|}pu\7\7 o9]*T^'^Tc`$ ALx3}q-q}xqkp3+x'L Azqvuopl\9\ދvqsK|c]\\c{}t=u~_H0H/~^tt^}/G/G^}t=t}G/G0}=ut=}/Hq hQ eiiiz+Z%X~zsugTw'^T *^TP$bv~o ܚ yykj{'NT^wTgusz%XZii b'Cu90zíz|yo|w6 [''5Y  uxdtv1]\.xz'K52bC\  46 |zidd{x@/twj 48%L|}zzyx &' cv$2\Zh w'm|09xzddiËz{ 74 !wu'uud2'\\-wzzz||K&85\wC3'\! Y4''Z 6w|oy|zIJz09ucwv c5L&yx-\\12 ]T Z&&V]C :V$&Z]T ]&Y%%V]C Y&&V]AT"v3XST8P9bqwů{H_Pgfht{pgaěƱȎtȪ|Íg}9duEnjsJlrlj|MplwVjkcg{piUq~`tWxÓEhulu̓vz}zHZ[]_rcnyw{vtŠkƲ|wN`H%|& )TCS*wvjwvnܹ3\\\F F vR x||$ Cj P$d|G c 'ic $ $G $ P\>N BB$ Z"""| ""C$gkm| YCZ$$[ ?*[ C * TTTS1%pa fbkg`[dd%nmrrmmsrmmrrm@@a |ȫʐ3z{|::<=~~|}{}@EDJ)2cvtyz||}~~~ FΒŀeTLyD]]yyxvwuMG^ZEjygab.8\K"Ϟu[.d22IĈ` INO_B}~B_ k'Ǵ; D(-HIEDC9DIH#»mQ}`tǢĶĢt`r `SON[:aannvqlzhh!"h`tǢöžfimy,,hhr 3"33aaRsOOS`dHddT^M M28/TPPR\ĺĺ\RR\\Rb Nxkxwwxkxgg fhcddchfj hwwIlJI}I*{ C 3O0Iuc@ 5 (\-=haZ3o''nkjh''mG\"VӵB+4LEN3j737PxS63{E'=*}',TRRd)Nτ+=PPi=b NoE/pf'IGΣΔune. "pJ<"_>u/}-P e[͠ a-55>2mQKUlhe:'q,'KAfaxgs%߸F‰Y'^q8G@Lu;{Wb h/ \Zs p\u u` ('d]P\'(\g y S \'u {u` '\'{@('u@uN \y'{'i'{\vdFz$'\ }\F\\ PZLC\c_aN I HL\ aN cC0 $x5lLFrdmLKl\2 `0 PMx lLKm'\rLFlT^^TV0 $B MlKLmrFLlH 0 Px BlFLr\mKLl\H \yp\>$7 {2JS]biqu| 05jq{ "'+8<AEJOTar~ #'+059EKOS[fmry~  & ouuo\ou\\uo\( uo'j&$ [ *# ouY *  !0 & ) $ $ *6 8  9' \\Zs  \H~H V $.$- $- T^^s5 )ttp ou T^ J 1I ''? "k,P%  mmmmmm S \ wCl&$&$s&S%U ^S ''G < 0 2 pHHp{ ]= N .- '|\3 T VT^^s \  '' <+ |\P J \ Q3 z z :  h W E ou9 8   W $ * \ S S }oOPe Z mrrmmrn vq U Y4 W x .%D   @ D  o| J){ xV \ } P'P : w !!  Ch n]zUQ v pHHpg '  t \\ (J \  aF q$  q  5y } {J .- $$ \ \ P OZ \  | puto f \' j  v$ !!  ¸  cicccccc4c t+c:g!cnccb:-IZcb8c&c{{cookiecutter.binary_test}}/000077500000000000000000000000001456543333500367425ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-generate-binaries/input{{cookiecutter.binary_test}}.DS_Store000066400000000000000000000300041456543333500404220ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-generate-binaries/input{{cookiecutter.binary_test}}/{{cookiecutter.binary_test}}Bud1Storedi .DS_Storedilcblob >v1b some_font.otfdilcblob SO1b  @ @ @ @ EDSDB ` @ @ @ E DSDB ` @ @ @ some_font.otfdilcblob SO1btmpbwspblobbplist00  \WindowBounds[ShowSidebar]ShowStatusBar[ShowPathbar[ShowToolbar\SidebarWidth_{{653, 209}, {770, 438}}  ". VS`i̯ 8Rnkk22;A!.΁YϪV!}{G,Zy':jT:S@^p=BrlHL`(*RX ϺaL~O>P@ GG;M'<: A7z Oq/\?) c[ 'P l^Q^([&` ܥc[~ !Ӯ/ulJwʲOB2}Ixt]"m\8Rd^ E+fJXԡg5Jn5ql3` 5"K%pc[/]Q@`:0*w8z+1uW̑g?zO"F[q*(as;pppN6k>78 uN-pl+pp-)8T ] 3"vm L7юm%owFrbNul$zWW&(#z z|bdCR4 '>b/\Lj xuG nSu ,!^pűiplk0!9^wl; D_5o泌$9e; '8u p#тd8j2 QSaCj2?)dzpZѱ-AS3`헰~,tlkb>1 D :1`P5 c֥0br)Z= 8%dSQֱ oF TRbt ݿp=&T!!zD' MYRQGqy(;ݎm[Y%:􎒱_x$s=0? ^C+P O]_pl30mg.pk&0`2SԹ8Y|13ehBan#Zw5Щ5[Q~ mc`a_֫D0A3slKpLPԸ/&y9put=n'F:xҥGGVbND]?#Ev=L%8P,F*tkle 6`|z~ЭL8c[P(f?%=U n{c[ձhhKIK?2+(%u=6=Оvj~# o::wR:m+9}-kR9ސ~WHpMGX[D 3 x4iQ@pp=Dc+Z37Ю<\k}ޡ4>(vu.74*ݼ >5ӵ@ߡeaD?Mm[Vn {QVm eüdHِ^s8 "sn*QI SuMONjdc[=1{3@ݰ BqtlJ!r]%#*@3&z(f\izEk+mx?I#@L6¬~JxU P!r}_rte4(BBS>Sb,?pl"ʵ=fP\Ůo5E#۸SnmE?6}h:Lh2/3+NN1E$rlkdeh;ٗ_1zsڀ)E~}?zO":hޤOxHv>‰z҃e(7֘4E 3KZD9R38+ڿ?5ӞZ%sh 'Yx{uWOzen2$cLw'iA+JCI#ӈk0Nb2Ɯԛql#fNubɱ4!iYzgupտQ\VXyP2Hq:iPmoʅ0>TU%.qRǶn^C~*PA\m-#3=>wjR.Wu(}ǶBYEEefHs3㕡b4 HmMAyP#ˁn.ȪW~'iE΋}DؖzB{ +$J\#.(7S8u!݃c'O!αE?03A}0mB\|81c[VW>g:5/"'H/Ƕ*ZGǁ2उޓul+JU ߏ5slnG / RqJ pDEG4=Ci_BWP4˅xO^Pw`S|c[SDFrn(p̯?73 iIXR=D_8Uc:=72Y2N-& <6CcWN >;v=m0A6&Ѿ=wA1OԌe4 );h=a#S;f߯rRݸS)z3XsgCrc45׺oʮIIY1-wcZ ݠ]wGpC >%Lz $\v=e (E 26mYg8ՃAE?B{DX]ʸb,Nu=.S\Ͽ5 @}(w 5Ԑ+x (XuUpM/&M+}M yӅXI(QS,A'_hJ"ikcW#dI%GJfǥלṨ.|/cU$m}{;@=K҃v }2|/s8PzNxnx1ǶFfj@~0 ";:Rh9nj8 m!um3?NOr׼'!>?{5 -.=0Z>uI}ZűA(0uU7T:Tuo I؈݆UT21{OQF]C[e2L VҾ!#}BVcZ l8##VơZ |yALZ9+[fdZ oellgBEHLZkvglassmusicsearchuni2709heartstarstaremptyuserfilmthlargeththlistokremovezoominzoomoutoffsignalcogtrashhomefiletimeroaddownloadaltdownloaduploadinboxplaycirclerepeatrefreshlistaltlockflagheadphonesvolumeoffvolumedownvolumeupqrcodebarcodetagtagsbookbookmarkprintcamerafontbolditalictextheighttextwidthalignleftaligncenteralignrightalignjustifylistindentleftindentrightfacetimevideopictureuni270FmapmarkeradjusttinteditsharecheckmovestepbackwardfastbackwardbackwardplaypausestopforwardfastforwardstepforwardejectchevronleftchevronrightplussignminussignremovesignoksignquestionsigninfosignscreenshotremovecircleokcirclebancirclearrowleftarrowrightarrowuparrowdownsharealtresizefullresizesmallexclamationsigngiftleaffireeyeopeneyeclosewarningsignplanecalendarrandomcommentsmagnetchevronupchevrondownretweetshoppingcartfolderclosefolderopenresizeverticalresizehorizontalhddbullhornbellcertificatethumbsupthumbsdownhandrighthandlefthandtophanddowncirclearrowrightcirclearrowleftcirclearrowtopcirclearrowdownglobewrenchtasksfilterbriefcasefullscreendashboardpaperclipheartemptylinkphonepushpinEurousdgbpsortsortbyalphabetsortbyalphabetaltsortbyordersortbyorderaltsortbyattributessortbyattributesaltuncheckedexpandcollapsecollapsetopJan KovarikGLYPHICONS Halflings-RegularGLYPHICONS Halflings{&07@FMQZ_fmqv&+8=KX]aft} ).28CLQU_dhlpuy "&+048<@FJNR[^dinsw|+ ! = * ,\% [ ! ou)t - \- - f "k" . p % V \. uo 4  * ^ T> ^] A 4'k c '>G ( 1 BB e ; U X  % 3 +3 5M uo 1 k1 % PK < )uto E S|M  J) ((} ST pHHpi \\ Iv|E Jq / J J J P\ 0 ¸ |JE |/ N~ pHHp : `4` ouR % Y \ T^ ^T T^^T 5 ` d\ r' $  \\ |Z  fg d S \ ^T c$N $c \~ \ PP \' $ \  $ \ v\wR \D sm { |  T \ $Q \Z -'V Tg c  h 'Y'$' (J \$ h \ \ Z 9  '\'g  U } J t  ' \ o <  '\ -ouuo d \\ !! w  '  ` ;%;-QE 7SWq'mBo t  7 9 . l @OyFUa F{  :Pf|!Tqp*Gr n<O`^#Gk8jU? 0 !~!!!"#J###$/$%-&&'9''(),))*3*****+$+b++ wR : Q 'N$|:/FUK?2n"i#ssEU}>>KV2n"i#sse.( )6@l m: Y: ' : $$| IMO^B}~B^ = P$= YT vX"ggU yb N¸%N 9999 T%'$}: $pp: \z $gz L L $$g\P L :|:" |$" |$" 9 Pou\R \,P%$9 .bchh78pp hhhhhhhhe.\s t\6V{l @ ''dff.( )e ; X @l .$|[ [ *Pou$Y g*C 7^ ] ^ S7 :p91g! 1gpv\Pw$< | \: $ \ \i{||Kjlm:`jn{~+ %+;:, $,~zܵjie;nkk,epttqss %,ej~l{njaiifn܆n{k}ke+ottrrs $ *+ + * P\ xw'}~Y  }7'\¸T]p p p p v]\Z $_b PQ }}  7$$t [ ?*[ ?2$h \j  }w'xC: d6eC $$>tdwZ P~ ~$$~_C[ *[ \2**t ***4~ **Pm o[P\Po Y\g Y +)#)6 ' ]\f Q &&F6/^ ] SS> *+ & ]?v[ '$'*w[ B%Z%B*@P$$%%@)+ *> mY@ %%v@?]**^ ] /6Eph : : |t'XP\XP\XDP\r'v|\ChN 1g 11'c SM G \**Y w x''\$\\$|w \|h: }}Y} PPi}}Y} h@OLO 3[y!!DD!!D!!r !!  [$F1).k5UFU޳ NbN:͒ _<ˑ%('Y#/hct \\g ޳ FVE1*.k4UGdj i' ''$!'TZ\t !'\  EU i|t \''%"'J N @`j'P\~ U 'Z@''@'@dAB''r'J $\J h g 0wW\\'\z\|||||k|\||||\| h g ''H0D'E0k@_ v&'Q ~_ HccLcd˲ʋcdKddv&&Q }_ kPPVVYeudcKdc˳ˋcdKcdv*|||1n tpVOVP  \ UcV}\y' xP\ P }}~e,~e,}}h_h: \U|T^ J*\(**\*`0P\\PP\ƺƺ\Pw {: 5֤w|HIQ%i21hmasã#zYaa] F]| DC.(MDt{@RYjY2/dF[ЋWW0 *? gB!(;&##h & !K\;Q =Rp{5AJ2\'R'v \\\VQ R.iȋ\zN y -Sj;@;;@H\R.iɋ\zN y .R);@;;@i Q<+|<|+<+<i ;;I N~) $) |$|) $/I N~* ///I h*# \# \# \# @ *O '+3 J@]']@:7v@ O '% 3 @\@;7 D 1ibbiibbiU vxswwsswxr0Ju|OAAOOAAO~~iQ $--ymzy(/jj55jjjjon7o2a60[sxk&gsTbT]`(((([ *(   Wl#qdy&U!({NfXyx+dH0X0 o6-_2 NNl8u| 005Y>A %Ѷ뢉8\-5e㐋A FEba_g 8=| :#''BClZWU \| ZU Z\| U WCZw C$Dv~P{ {D| w P{| n \xPkPn ouY  x{k |w |hm Io{k |Kw hm I{KC'ouuo'.|- w ZYZZ0ZYZZ?9\\\\\\\\\Zf ?9\ f '!6!!hh!r "hh!"!"ih"""!f 'BB%CB//3W 9L{rcwez}z9NK1zHGL\\ ''fU lPe(3'\| \fGe'V\V)\eY\Y5 )_\kh\~ b'=kf\e_\`k?+| ]k)F?'k, ::::, Yd4Z$ br> sb #K@;A;@KZ D Pj YTj Z D : Yv ` Pb Z PQ QU[D : YAt/{h5"" $$Y""kkPY"!$gvZ Z PP$ $$$Z Z n |i\R PKK!!KK\KKKK\KKr KK\KK KK_$'p9ppQcupQppup\  \R : $\$fE$~}$hW@zuiBY.BS |$$ X\NpwY #]^}"\S5b] pCH)VK4"e.Ko3~nH|383{-$U^'<.J/I ~i"W~W~JG$XoTNB((36SU^.^/.""h‚{=*m8Cet4N*N cy55q5 $!Ev ::::yl2 T 3FuEVlTb N*edca qcc{i@f#(: (K=FGeWD$, De%Mq*), f%H,]Mݼv ("s\\EVl(K%|\YwĶ``wOY/Qg Q'  [H88HH8wyzepbp^bTTzKp~r=T^{zwd*W **puto|ou\7 P\7 P\7 \|\x\3\IH!">$!"IIYvD xc Sg ĂM G Z Q j $j kk! 1OggO1\ihZZZZZZZZ|h [$k_k$_X o xQ tw{ue6||||otb [\ouuooud j d ]UtpvD xc \': T v\|w\| : P b \ZZs ZZmZZs YYwylpo4opip@wr'3\z z |&\pttp|}pu\7\7 o9]*T^'^Tc`$ ALx3}q-q}xqkp3+x'L Azqvuopl\9\ދvqsK|c]\\c{}t=u~_H0H/~^tt^}/G/G^}t=t}G/G0}=ut=}/Hq hQ eiiiz+Z%X~zsugTw'^T *^TP$bv~o ܚ yykj{'NT^wTgusz%XZii b'Cu90zíz|yo|w6 [''5Y  uxdtv1]\.xz'K52bC\  46 |zidd{x@/twj 48%L|}zzyx &' cv$2\Zh w'm|09xzddiËz{ 74 !wu'uud2'\\-wzzz||K&85\wC3'\! Y4''Z 6w|oy|zIJz09ucwv c5L&yx-\\12 ]T Z&&V]C :V$&Z]T ]&Y%%V]C Y&&V]AT"v3XST8P9bqwů{H_Pgfht{pgaěƱȎtȪ|Íg}9duEnjsJlrlj|MplwVjkcg{piUq~`tWxÓEhulu̓vz}zHZ[]_rcnyw{vtŠkƲ|wN`H%|& )TCS*wvjwvnܹ3\\\F F vR x||$ Cj P$d|G c 'ic $ $G $ P\>N BB$ Z"""| ""C$gkm| YCZ$$[ ?*[ C * TTTS1%pa fbkg`[dd%nmrrmmsrmmrrm@@a |ȫʐ3z{|::<=~~|}{}@EDJ)2cvtyz||}~~~ FΒŀeTLyD]]yyxvwuMG^ZEjygab.8\K"Ϟu[.d22IĈ` INO_B}~B_ k'Ǵ; D(-HIEDC9DIH#»mQ}`tǢĶĢt`r `SON[:aannvqlzhh!"h`tǢöžfimy,,hhr 3"33aaRsOOS`dHddT^M M28/TPPR\ĺĺ\RR\\Rb Nxkxwwxkxgg fhcddchfj hwwIlJI}I*{ C 3O0Iuc@ 5 (\-=haZ3o''nkjh''mG\"VӵB+4LEN3j737PxS63{E'=*}',TRRd)Nτ+=PPi=b NoE/pf'IGΣΔune. "pJ<"_>u/}-P e[͠ a-55>2mQKUlhe:'q,'KAfaxgs%߸F‰Y'^q8G@Lu;{Wb h/ \Zs p\u u` ('d]P\'(\g y S \'u {u` '\'{@('u@uN \y'{'i'{\vdFz$'\ }\F\\ PZLC\c_aN I HL\ aN cC0 $x5lLFrdmLKl\2 `0 PMx lLKm'\rLFlT^^TV0 $B MlKLmrFLlH 0 Px BlFLr\mKLl\H \yp\>$7 {2JS]biqu| 05jq{ "'+8<AEJOTar~ #'+059EKOS[fmry~  & ouuo\ou\\uo\( uo'j&$ [ *# ouY *  !0 & ) $ $ *6 8  9' \\Zs  \H~H V $.$- $- T^^s5 )ttp ou T^ J 1I ''? "k,P%  mmmmmm S \ wCl&$&$s&S%U ^S ''G < 0 2 pHHp{ ]= N .- '|\3 T VT^^s \  '' <+ |\P J \ Q3 z z :  h W E ou9 8   W $ * \ S S }oOPe Z mrrmmrn vq U Y4 W x .%D   @ D  o| J){ xV \ } P'P : w !!  Ch n]zUQ v pHHpg '  t \\ (J \  aF q$  q  5y } {J .- $$ \ \ P OZ \  | puto f \' j  v$ !!  ¸  cicccccc4c t+c:g!cnccb:-IZcb8c&c{{cookiecutter.binary_test}}/000077500000000000000000000000001456543333500447445ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-generate-binaries/input{{cookiecutter.binary_test}}/{{cookiecutter.binary_test}}logo.png000066400000000000000000000104351456543333500464150ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-generate-binaries/input{{cookiecutter.binary_test}}/{{cookiecutter.binary_test}}/{{cookiecutter.binary_test}}PNG  IHDR.NsBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<IDATx{ŝ3E hTh!.5Bha@Q;F=50' 㮯MVVeXQh #"SUGzzv߹;{nj!-,]zp)&)Ov`5ey7tЌ D1»-יsFRV7sr9#8R oU( J8 H4qri)WH,ճHӁARf6sgvGA]йlˇced]+<&43G?mˡc*ɔPsFF(o=!r<-u غлe%n\ m4gdS"Dr7oADY VS`i̯ 8Rnkk22;A!.΁YϪV!}{G,Zy':jT:S@^p=BrlHL`(*RX ϺaL~O>P@ GG;M'<: A7z Oq/\?) c[ 'P l^Q^([&` ܥc[~ !Ӯ/ulJwʲOB2}Ixt]"m\8Rd^ E+fJXԡg5Jn5ql3` 5"K%pc[/]Q@`:0*w8z+1uW̑g?zO"F[q*(as;pppN6k>78 uN-pl+pp-)8T ] 3"vm L7юm%owFrbNul$zWW&(#z z|bdCR4 '>b/\Lj xuG nSu ,!^pűiplk0!9^wl; D_5o泌$9e; '8u p#тd8j2 QSaCj2?)dzpZѱ-AS3`헰~,tlkb>1 D :1`P5 c֥0br)Z= 8%dSQֱ oF TRbt ݿp=&T!!zD' MYRQGqy(;ݎm[Y%:􎒱_x$s=0? ^C+P O]_pl30mg.pk&0`2SԹ8Y|13ehBan#Zw5Щ5[Q~ mc`a_֫D0A3slKpLPԸ/&y9put=n'F:xҥGGVbND]?#Ev=L%8P,F*tkle 6`|z~ЭL8c[P(f?%=U n{c[ձhhKIK?2+(%u=6=Оvj~# o::wR:m+9}-kR9ސ~WHpMGX[D 3 x4iQ@pp=Dc+Z37Ю<\k}ޡ4>(vu.74*ݼ >5ӵ@ߡeaD?Mm[Vn {QVm eüdHِ^s8 "sn*QI SuMONjdc[=1{3@ݰ BqtlJ!r]%#*@3&z(f\izEk+mx?I#@L6¬~JxU P!r}_rte4(BBS>Sb,?pl"ʵ=fP\Ůo5E#۸SnmE?6}h:Lh2/3+NN1E$rlkdeh;ٗ_1zsڀ)E~}?zO":hޤOxHv>‰z҃e(7֘4E 3KZD9R38+ڿ?5ӞZ%sh 'Yx{uWOzen2$cLw'iA+JCI#ӈk0Nb2Ɯԛql#fNubɱ4!iYzgupտQ\VXyP2Hq:iPmoʅ0>TU%.qRǶn^C~*PA\m-#3=>wjR.Wu(}ǶBYEEefHs3㕡b4 HmMAyP#ˁn.ȪW~'iE΋}DؖzB{ +$J\#.(7S8u!݃c'O!αE?03A}0mB\|81c[VW>g:5/"'H/Ƕ*ZGǁ2उޓul+JU ߏ5slnG / RqJ pDEG4=Ci_BWP4˅xO^Pw`S|c[SDFrn(p̯?73 iIXR=D_8Uc:=72Y2N-& <6CcWN >;v=m0A6&Ѿ=wA1OԌe4 );h=a#S;f߯rRݸS)z3XsgCrc45׺oʮIIY1-wcZ ݠ]wGpC >%Lz $\v=e (E 26mYg8ՃAE?B{DX]ʸb,Nu=.S\Ͽ5 @}(w 5Ԑ+x (XuUpM/&M+}M yӅXI(QS,A'_hJ"ikcW#dI%GJfǥלṨ.|/cU$m}{;@=K҃v }2|/s8PzNxnx1ǶFfj@~0 ";:Rh9nj8 m!um3?NOr׼'!>?{5 -.=0Z>uI}ZűA(0uU7T:Tuo I؈݆UT21{OQF]C[e2L VҾ!#}BVcZ l8##VơZ |yALZ9+[fdZ oellg Path: """Create a backup of cookiecutter.json.""" src_data = (cwd / 'cookiecutter.json').read_text() dst = cwd / '_cookiecutter.json' with open(dst, 'w') as fh: fh.write(src_data) return dst def main(): """Check if we can run the cookiecutter.""" if os.environ.get("COOKIECUTTER_FAIL_PRE_PROMPT", False): sys.exit(1) cwd = Path('.').resolve() bkp = backup_configuration(cwd) print(f"All good here, created {bkp}") if __name__ == "__main__": main() cookiecutter-2.6.0/tests/test-pyhooks/input{{cookiecutter.pyhooks}}/000077500000000000000000000000001456543333500263005ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-pyhooks/input{{cookiecutter.pyhooks}}/README.rst000066400000000000000000000000151456543333500277630ustar00rootroot00000000000000foo === bar cookiecutter-2.6.0/tests/test-pyshellhooks/000077500000000000000000000000001456543333500210755ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-pyshellhooks/cookiecutter.json000066400000000000000000000001111456543333500244610ustar00rootroot00000000000000{ "pyshellhooks": "fake-project", "project_title": "Project Title" } cookiecutter-2.6.0/tests/test-pyshellhooks/hooks/000077500000000000000000000000001456543333500222205ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-pyshellhooks/hooks/post_gen_project.py000066400000000000000000000002311456543333500261320ustar00rootroot00000000000000"""Simple post-gen hook for testing project folder and custom file creation.""" print('pre generation hook') f = open('python_post.txt', 'w') f.close() cookiecutter-2.6.0/tests/test-pyshellhooks/hooks/post_gen_project.sh000077500000000000000000000001011456543333500261130ustar00rootroot00000000000000#!/bin/bash echo 'post generation hook'; touch 'shell_post.txt' cookiecutter-2.6.0/tests/test-pyshellhooks/hooks/pre_gen_project.py000066400000000000000000000002271456543333500257400ustar00rootroot00000000000000"""Simple pre-gen hook for testing project folder and custom file creation.""" print('pre generation hook') f = open('python_pre.txt', 'w') f.close() cookiecutter-2.6.0/tests/test-pyshellhooks/hooks/pre_gen_project.sh000077500000000000000000000001001456543333500257130ustar00rootroot00000000000000#!/bin/bash echo 'post generation hook'; touch 'shell_pre.txt' cookiecutter-2.6.0/tests/test-pyshellhooks/hooks/pre_prompt.sh000077500000000000000000000001001456543333500247350ustar00rootroot00000000000000#!/bin/bash echo 'Pre-Prompt hook'; touch '_cookiecutter.json' cookiecutter-2.6.0/tests/test-pyshellhooks/input{{pyshellhooks}}/000077500000000000000000000000001456543333500256615ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-pyshellhooks/input{{pyshellhooks}}/README.rst000066400000000000000000000000151456543333500273440ustar00rootroot00000000000000foo === bar cookiecutter-2.6.0/tests/test-replay/000077500000000000000000000000001456543333500176455ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-replay/cookiedozer_load.json000066400000000000000000000002411456543333500240510ustar00rootroot00000000000000{ "cookiecutter": { "version": "0.1.0", "email": "raphael@hackebrot.de", "full_name": "Raphael Pierzina", "github_username": "hackebrot" } } cookiecutter-2.6.0/tests/test-replay/invalid_replay.json000066400000000000000000000002451456543333500235430ustar00rootroot00000000000000{ "not cookiecutter": { "version": "0.1.0", "email": "raphael@hackebrot.de", "full_name": "Raphael Pierzina", "github_username": "hackebrot" } } cookiecutter-2.6.0/tests/test-replay/valid_replay.json000066400000000000000000000004771456543333500232230ustar00rootroot00000000000000{ "cookiecutter": { "version": "0.1.0", "email": "raphael@hackebrot.de", "full_name": "Raphael Pierzina", "github_username": "hackebrot", "project_name": "Replay Project", "repo_name": "replay-project", "release_date": "2013-07-28", "year": "2013", "description": "replayed" } } cookiecutter-2.6.0/tests/test-shellhooks-empty/000077500000000000000000000000001456543333500216605ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-shellhooks-empty/hooks/000077500000000000000000000000001456543333500230035ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-shellhooks-empty/hooks/pre_gen_project.sh000066400000000000000000000000001456543333500264720ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-shellhooks-empty/input{{cookiecutter.shellhooks}}/000077500000000000000000000000001456543333500305725ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-shellhooks-empty/input{{cookiecutter.shellhooks}}/README.rst000066400000000000000000000000531456543333500322570ustar00rootroot00000000000000Empty, just like the hooks we are testing. cookiecutter-2.6.0/tests/test-shellhooks-win/000077500000000000000000000000001456543333500213175ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-shellhooks-win/hooks/000077500000000000000000000000001456543333500224425ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-shellhooks-win/hooks/post_gen_project.bat000066400000000000000000000000731456543333500264760ustar00rootroot00000000000000@echo off echo post generation hook echo. >shell_post.txt cookiecutter-2.6.0/tests/test-shellhooks-win/hooks/pre_gen_project.bat000066400000000000000000000000711456543333500262750ustar00rootroot00000000000000@echo off echo pre generation hook echo. >shell_pre.txt cookiecutter-2.6.0/tests/test-shellhooks-win/input{{cookiecutter.shellhooks}}/000077500000000000000000000000001456543333500302315ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-shellhooks-win/input{{cookiecutter.shellhooks}}/README.rst000066400000000000000000000000151456543333500317140ustar00rootroot00000000000000foo === bar cookiecutter-2.6.0/tests/test-shellhooks/000077500000000000000000000000001456543333500205245ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-shellhooks/hooks/000077500000000000000000000000001456543333500216475ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-shellhooks/hooks/post_gen_project.sh000077500000000000000000000001011456543333500255420ustar00rootroot00000000000000#!/bin/bash echo 'post generation hook'; touch 'shell_post.txt' cookiecutter-2.6.0/tests/test-shellhooks/hooks/pre_gen_project.sh000077500000000000000000000000771456543333500253570ustar00rootroot00000000000000#!/bin/bash echo 'pre generation hook'; touch 'shell_pre.txt' cookiecutter-2.6.0/tests/test-shellhooks/input{{cookiecutter.shellhooks}}/000077500000000000000000000000001456543333500274365ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-shellhooks/input{{cookiecutter.shellhooks}}/README.rst000066400000000000000000000000151456543333500311210ustar00rootroot00000000000000foo === bar cookiecutter-2.6.0/tests/test-templates/000077500000000000000000000000001456543333500203475ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-templates/extends/000077500000000000000000000000001456543333500220215ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-templates/extends/cookiecutter.json000066400000000000000000000001311456543333500254070ustar00rootroot00000000000000{ "project_slug": "foobar", "command_line_interface": "click", "use_pytest": "y" } cookiecutter-2.6.0/tests/test-templates/extends/templates/000077500000000000000000000000001456543333500240175ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-templates/extends/templates/base-requirements.jinja000066400000000000000000000003741456543333500304730ustar00rootroot00000000000000pip {% if cookiecutter.command_line_interface|lower == 'click' -%} {% include 'click-requirements.jinja' %}{% endif %} {% if cookiecutter.use_pytest == 'y' -%} {% include 'pytest-requirements.jinja' %}{% endif %} {% block dependencies %}{% endblock %} cookiecutter-2.6.0/tests/test-templates/extends/templates/click-requirements.jinja000066400000000000000000000000061456543333500306360ustar00rootroot00000000000000Click cookiecutter-2.6.0/tests/test-templates/extends/templates/pytest-requirements.jinja000066400000000000000000000000071456543333500311020ustar00rootroot00000000000000pytest cookiecutter-2.6.0/tests/test-templates/extends/{{cookiecutter.project_slug}}/000077500000000000000000000000001456543333500302005ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-templates/extends/{{cookiecutter.project_slug}}/requirements.txt000066400000000000000000000000501456543333500334570ustar00rootroot00000000000000{% extends "base-requirements.jinja" %} cookiecutter-2.6.0/tests/test-templates/include/000077500000000000000000000000001456543333500217725ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-templates/include/cookiecutter.json000066400000000000000000000001311456543333500253600ustar00rootroot00000000000000{ "project_slug": "foobar", "command_line_interface": "click", "use_pytest": "y" } cookiecutter-2.6.0/tests/test-templates/include/templates/000077500000000000000000000000001456543333500237705ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-templates/include/templates/click-requirements.jinja000066400000000000000000000000061456543333500306070ustar00rootroot00000000000000Click cookiecutter-2.6.0/tests/test-templates/include/templates/pytest-requirements.jinja000066400000000000000000000000071456543333500310530ustar00rootroot00000000000000pytest cookiecutter-2.6.0/tests/test-templates/include/{{cookiecutter.project_slug}}/000077500000000000000000000000001456543333500301515ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-templates/include/{{cookiecutter.project_slug}}/requirements.txt000066400000000000000000000003251456543333500334350ustar00rootroot00000000000000pip {% if cookiecutter.command_line_interface|lower == 'click' -%} {% include 'click-requirements.jinja' %}{% endif %} {% if cookiecutter.use_pytest == 'y' -%} {% include 'pytest-requirements.jinja' %}{% endif %} cookiecutter-2.6.0/tests/test-templates/no-templates/000077500000000000000000000000001456543333500227575ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-templates/no-templates/cookiecutter.json000066400000000000000000000001311456543333500263450ustar00rootroot00000000000000{ "project_slug": "foobar", "command_line_interface": "click", "use_pytest": "y" } cookiecutter-2.6.0/tests/test-templates/no-templates/{{cookiecutter.project_slug}}/000077500000000000000000000000001456543333500311365ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-templates/no-templates/{{cookiecutter.project_slug}}/requirements.txt000066400000000000000000000002171456543333500344220ustar00rootroot00000000000000pip {% if cookiecutter.command_line_interface|lower == 'click' -%} Click{% endif %} {% if cookiecutter.use_pytest == 'y' -%} pytest{% endif %} cookiecutter-2.6.0/tests/test-templates/super/000077500000000000000000000000001456543333500215055ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-templates/super/cookiecutter.json000066400000000000000000000001311456543333500250730ustar00rootroot00000000000000{ "project_slug": "foobar", "command_line_interface": "click", "use_pytest": "y" } cookiecutter-2.6.0/tests/test-templates/super/templates/000077500000000000000000000000001456543333500235035ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-templates/super/templates/base-requirements.jinja000066400000000000000000000004511456543333500301530ustar00rootroot00000000000000pip {% if cookiecutter.command_line_interface|lower == 'click' -%} {% include 'click-requirements.jinja' %}{% endif %} {%- block dev_dependencies %} {% if cookiecutter.use_pytest == 'y' -%}{% include 'pytest-requirements.jinja' %}{% endif %} {%- endblock %} {% block dependencies %}{% endblock %} cookiecutter-2.6.0/tests/test-templates/super/templates/click-requirements.jinja000066400000000000000000000000061456543333500303220ustar00rootroot00000000000000Click cookiecutter-2.6.0/tests/test-templates/super/templates/pytest-requirements.jinja000066400000000000000000000000071456543333500305660ustar00rootroot00000000000000pytest cookiecutter-2.6.0/tests/test-templates/super/{{cookiecutter.project_slug}}/000077500000000000000000000000001456543333500276645ustar00rootroot00000000000000cookiecutter-2.6.0/tests/test-templates/super/{{cookiecutter.project_slug}}/requirements.txt000066400000000000000000000001401456543333500331430ustar00rootroot00000000000000{% extends "base-requirements.jinja" %} {% block dev_dependencies %}{{ super() }}{% endblock %} cookiecutter-2.6.0/tests/test_abort_generate_on_hook_error.py000066400000000000000000000022631456543333500247160ustar00rootroot00000000000000""" test_abort_generate_on_hook_error. Tests to ensure cookiecutter properly exits with a non-zero exit code whenever errors occur in (optional) pre- or pos-gen hooks. """ import pytest from cookiecutter import exceptions, generate @pytest.mark.parametrize( ("abort_pre_gen", "abort_post_gen"), (("yes", "no"), ("no", "yes")), ids=("pre_gen_hook_raises_error", "post_gen_hook_raises_error"), ) @pytest.mark.usefixtures("clean_system") def test_hooks_raises_errors(tmp_path, abort_pre_gen, abort_post_gen): """Verify pre- and pos-gen errors raises correct error code from script. This allows developers to make different error codes in their code, for different errors. """ context = { "cookiecutter": { "repo_dir": "foobar", "abort_pre_gen": abort_pre_gen, "abort_post_gen": abort_post_gen, } } with pytest.raises(exceptions.FailedHookException) as error: generate.generate_files( repo_dir="tests/hooks-abort-render", context=context, output_dir=str(tmp_path), ) assert error.value.code == 5 assert not tmp_path.joinpath("foobar").is_dir() cookiecutter-2.6.0/tests/test_cli.py000066400000000000000000000532031456543333500175570ustar00rootroot00000000000000"""Collection of tests around cookiecutter's command-line interface.""" import json import os import re from pathlib import Path import pytest from click.testing import CliRunner from cookiecutter import utils from cookiecutter.__main__ import main from cookiecutter.environment import StrictEnvironment from cookiecutter.exceptions import UnknownExtension from cookiecutter.main import cookiecutter @pytest.fixture(scope='session') def cli_runner(): """Fixture that returns a helper function to run the cookiecutter cli.""" runner = CliRunner() def cli_main(*cli_args, **cli_kwargs): """Run cookiecutter cli main with the given args.""" return runner.invoke(main, cli_args, **cli_kwargs) return cli_main @pytest.fixture def remove_fake_project_dir(request): """Remove the fake project directory created during the tests.""" def fin_remove_fake_project_dir(): for prefix in ('', 'input'): dir_name = f'{prefix}fake-project' if os.path.isdir(dir_name): utils.rmtree(dir_name) request.addfinalizer(fin_remove_fake_project_dir) @pytest.fixture def remove_tmp_dir(request): """Remove the fake project directory created during the tests.""" if os.path.isdir('tests/tmp'): utils.rmtree('tests/tmp') def fin_remove_tmp_dir(): if os.path.isdir('tests/tmp'): utils.rmtree('tests/tmp') request.addfinalizer(fin_remove_tmp_dir) @pytest.fixture def make_fake_project_dir(request): """Create a fake project to be overwritten in the according tests.""" os.makedirs('fake-project') @pytest.fixture(params=['-V', '--version']) def version_cli_flag(request): """Pytest fixture return both version invocation options.""" return request.param def test_cli_version(cli_runner, version_cli_flag): """Verify Cookiecutter version output by `cookiecutter` on cli invocation.""" result = cli_runner(version_cli_flag) assert result.exit_code == 0 assert result.output.startswith('Cookiecutter') @pytest.mark.usefixtures('make_fake_project_dir', 'remove_fake_project_dir') def test_cli_error_on_existing_output_directory(cli_runner): """Test cli invocation without `overwrite-if-exists` fail if dir exist.""" result = cli_runner('tests/fake-repo-pre/', '--no-input') assert result.exit_code != 0 expected_error_msg = 'Error: "fake-project" directory already exists\n' assert result.output == expected_error_msg @pytest.mark.usefixtures('remove_fake_project_dir') def test_cli(cli_runner): """Test cli invocation work without flags if directory not exist.""" result = cli_runner('tests/fake-repo-pre/', '--no-input') assert result.exit_code == 0 assert os.path.isdir('fake-project') content = Path("fake-project", "README.rst").read_text() assert 'Project name: **Fake Project**' in content @pytest.mark.usefixtures('remove_fake_project_dir') def test_cli_verbose(cli_runner): """Test cli invocation display log if called with `verbose` flag.""" result = cli_runner('tests/fake-repo-pre/', '--no-input', '-v') assert result.exit_code == 0 assert os.path.isdir('fake-project') content = Path("fake-project", "README.rst").read_text() assert 'Project name: **Fake Project**' in content @pytest.mark.usefixtures('remove_fake_project_dir') def test_cli_replay(mocker, cli_runner): """Test cli invocation display log with `verbose` and `replay` flags.""" mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter') template_path = 'tests/fake-repo-pre/' result = cli_runner(template_path, '--replay', '-v') assert result.exit_code == 0 mock_cookiecutter.assert_called_once_with( template_path, None, False, replay=True, overwrite_if_exists=False, skip_if_file_exists=False, output_dir='.', config_file=None, default_config=False, extra_context=None, password=None, directory=None, accept_hooks=True, keep_project_on_failure=False, ) @pytest.mark.usefixtures('remove_fake_project_dir') def test_cli_replay_file(mocker, cli_runner): """Test cli invocation correctly pass --replay-file option.""" mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter') template_path = 'tests/fake-repo-pre/' result = cli_runner(template_path, '--replay-file', '~/custom-replay-file', '-v') assert result.exit_code == 0 mock_cookiecutter.assert_called_once_with( template_path, None, False, replay='~/custom-replay-file', overwrite_if_exists=False, skip_if_file_exists=False, output_dir='.', config_file=None, default_config=False, extra_context=None, password=None, directory=None, accept_hooks=True, keep_project_on_failure=False, ) @pytest.mark.usefixtures('remove_tmp_dir') def test_cli_replay_generated(mocker, cli_runner): """Test cli invocation correctly generates a project with replay.""" template_path = 'tests/fake-repo-replay/' result = cli_runner( template_path, '--replay-file', 'tests/test-replay/valid_replay.json', '-o', 'tests/tmp/', '-v', ) assert result.exit_code == 0 assert open('tests/tmp/replay-project/README.md').read().strip() == 'replayed' @pytest.mark.usefixtures('remove_fake_project_dir') def test_cli_exit_on_noinput_and_replay(mocker, cli_runner): """Test cli invocation fail if both `no-input` and `replay` flags passed.""" mock_cookiecutter = mocker.patch( 'cookiecutter.cli.cookiecutter', side_effect=cookiecutter ) template_path = 'tests/fake-repo-pre/' result = cli_runner(template_path, '--no-input', '--replay', '-v') assert result.exit_code == 1 expected_error_msg = ( "You can not use both replay and no_input or extra_context at the same time." ) assert expected_error_msg in result.output mock_cookiecutter.assert_called_once_with( template_path, None, True, replay=True, overwrite_if_exists=False, skip_if_file_exists=False, output_dir='.', config_file=None, default_config=False, extra_context=None, password=None, directory=None, accept_hooks=True, keep_project_on_failure=False, ) @pytest.fixture(params=['-f', '--overwrite-if-exists']) def overwrite_cli_flag(request): """Pytest fixture return all `overwrite-if-exists` invocation options.""" return request.param @pytest.mark.usefixtures('remove_fake_project_dir') def test_run_cookiecutter_on_overwrite_if_exists_and_replay( mocker, cli_runner, overwrite_cli_flag ): """Test cli invocation with `overwrite-if-exists` and `replay` flags.""" mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter') template_path = 'tests/fake-repo-pre/' result = cli_runner(template_path, '--replay', '-v', overwrite_cli_flag) assert result.exit_code == 0 mock_cookiecutter.assert_called_once_with( template_path, None, False, replay=True, overwrite_if_exists=True, skip_if_file_exists=False, output_dir='.', config_file=None, default_config=False, extra_context=None, password=None, directory=None, accept_hooks=True, keep_project_on_failure=False, ) @pytest.mark.usefixtures('remove_fake_project_dir') def test_cli_overwrite_if_exists_when_output_dir_does_not_exist( cli_runner, overwrite_cli_flag ): """Test cli invocation with `overwrite-if-exists` and `no-input` flags. Case when output dir not exist. """ result = cli_runner('tests/fake-repo-pre/', '--no-input', overwrite_cli_flag) assert result.exit_code == 0 assert os.path.isdir('fake-project') @pytest.mark.usefixtures('make_fake_project_dir', 'remove_fake_project_dir') def test_cli_overwrite_if_exists_when_output_dir_exists(cli_runner, overwrite_cli_flag): """Test cli invocation with `overwrite-if-exists` and `no-input` flags. Case when output dir already exist. """ result = cli_runner('tests/fake-repo-pre/', '--no-input', overwrite_cli_flag) assert result.exit_code == 0 assert os.path.isdir('fake-project') @pytest.fixture(params=['-o', '--output-dir']) def output_dir_flag(request): """Pytest fixture return all output-dir invocation options.""" return request.param def test_cli_output_dir(mocker, cli_runner, output_dir_flag, output_dir): """Test cli invocation with `output-dir` flag changes output directory.""" mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter') template_path = 'tests/fake-repo-pre/' result = cli_runner(template_path, output_dir_flag, output_dir) assert result.exit_code == 0 mock_cookiecutter.assert_called_once_with( template_path, None, False, replay=False, overwrite_if_exists=False, skip_if_file_exists=False, output_dir=output_dir, config_file=None, default_config=False, extra_context=None, password=None, directory=None, accept_hooks=True, keep_project_on_failure=False, ) @pytest.fixture(params=['-h', '--help', 'help']) def help_cli_flag(request): """Pytest fixture return all help invocation options.""" return request.param def test_cli_help(cli_runner, help_cli_flag): """Test cli invocation display help message with `help` flag.""" result = cli_runner(help_cli_flag) assert result.exit_code == 0 assert result.output.startswith('Usage') @pytest.fixture def user_config_path(tmp_path): """Pytest fixture return `user_config` argument as string.""" return str(tmp_path.joinpath("tests", "config.yaml")) def test_user_config(mocker, cli_runner, user_config_path): """Test cli invocation works with `config-file` option.""" mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter') template_path = 'tests/fake-repo-pre/' result = cli_runner(template_path, '--config-file', user_config_path) assert result.exit_code == 0 mock_cookiecutter.assert_called_once_with( template_path, None, False, replay=False, overwrite_if_exists=False, skip_if_file_exists=False, output_dir='.', config_file=user_config_path, default_config=False, extra_context=None, password=None, directory=None, accept_hooks=True, keep_project_on_failure=False, ) def test_default_user_config_overwrite(mocker, cli_runner, user_config_path): """Test cli invocation ignores `config-file` if `default-config` passed.""" mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter') template_path = 'tests/fake-repo-pre/' result = cli_runner( template_path, '--config-file', user_config_path, '--default-config', ) assert result.exit_code == 0 mock_cookiecutter.assert_called_once_with( template_path, None, False, replay=False, overwrite_if_exists=False, skip_if_file_exists=False, output_dir='.', config_file=user_config_path, default_config=True, extra_context=None, password=None, directory=None, accept_hooks=True, keep_project_on_failure=False, ) def test_default_user_config(mocker, cli_runner): """Test cli invocation accepts `default-config` flag correctly.""" mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter') template_path = 'tests/fake-repo-pre/' result = cli_runner(template_path, '--default-config') assert result.exit_code == 0 mock_cookiecutter.assert_called_once_with( template_path, None, False, replay=False, overwrite_if_exists=False, skip_if_file_exists=False, output_dir='.', config_file=None, default_config=True, extra_context=None, password=None, directory=None, accept_hooks=True, keep_project_on_failure=False, ) def test_echo_undefined_variable_error(output_dir, cli_runner): """Cli invocation return error if variable undefined in template.""" template_path = 'tests/undefined-variable/file-name/' result = cli_runner( '--no-input', '--default-config', '--output-dir', output_dir, template_path, ) assert result.exit_code == 1 error = "Unable to create file '{{cookiecutter.foobar}}'" assert error in result.output message = ( "Error message: 'collections.OrderedDict object' has no attribute 'foobar'" ) assert message in result.output context = { '_cookiecutter': { 'github_username': 'hackebrot', 'project_slug': 'testproject', }, 'cookiecutter': { 'github_username': 'hackebrot', 'project_slug': 'testproject', '_template': template_path, '_repo_dir': template_path, '_output_dir': output_dir, '_checkout': None, }, } context_str = json.dumps(context, indent=4, sort_keys=True) assert context_str in result.output def test_echo_unknown_extension_error(output_dir, cli_runner): """Cli return error if extension incorrectly defined in template.""" template_path = 'tests/test-extensions/unknown/' result = cli_runner( '--no-input', '--default-config', '--output-dir', output_dir, template_path, ) assert result.exit_code == 1 assert 'Unable to load extension: ' in result.output def test_local_extension(tmpdir, cli_runner): """Test to verify correct work of extension, included in template.""" output_dir = str(tmpdir.mkdir('output')) template_path = 'tests/test-extensions/local_extension/' result = cli_runner( '--no-input', '--default-config', '--output-dir', output_dir, template_path, ) assert result.exit_code == 0 content = Path(output_dir, 'Foobar', 'HISTORY.rst').read_text() assert 'FoobarFoobar' in content assert 'FOOBAR' in content def test_local_extension_not_available(tmpdir, cli_runner): """Test handling of included but unavailable local extension.""" context = {'cookiecutter': {'_extensions': ['foobar']}} with pytest.raises(UnknownExtension) as err: StrictEnvironment(context=context, keep_trailing_newline=True) assert 'Unable to load extension: ' in str(err.value) @pytest.mark.usefixtures('remove_fake_project_dir') def test_cli_extra_context(cli_runner): """Cli invocation replace content if called with replacement pairs.""" result = cli_runner( 'tests/fake-repo-pre/', '--no-input', '-v', 'project_name=Awesomez', ) assert result.exit_code == 0 assert os.path.isdir('fake-project') content = Path('fake-project', 'README.rst').read_text() assert 'Project name: **Awesomez**' in content @pytest.mark.usefixtures('remove_fake_project_dir') def test_cli_extra_context_invalid_format(cli_runner): """Cli invocation raise error if called with unknown argument.""" result = cli_runner( 'tests/fake-repo-pre/', '--no-input', '-v', 'ExtraContextWithNoEqualsSoInvalid', ) assert result.exit_code == 2 assert "Error: Invalid value for '[EXTRA_CONTEXT]...'" in result.output assert 'should contain items of the form key=value' in result.output @pytest.fixture def debug_file(tmp_path): """Pytest fixture return `debug_file` argument as path object.""" return tmp_path.joinpath('fake-repo.log') @pytest.mark.usefixtures('remove_fake_project_dir') def test_debug_file_non_verbose(cli_runner, debug_file): """Test cli invocation writes log to `debug-file` if flag enabled. Case for normal log output. """ assert not debug_file.exists() result = cli_runner( '--no-input', '--debug-file', str(debug_file), 'tests/fake-repo-pre/', ) assert result.exit_code == 0 assert debug_file.exists() context_log = ( "DEBUG cookiecutter.main: context_file is " "tests/fake-repo-pre/cookiecutter.json" ) assert context_log in debug_file.read_text() assert context_log not in result.output @pytest.mark.usefixtures('remove_fake_project_dir') def test_debug_file_verbose(cli_runner, debug_file): """Test cli invocation writes log to `debug-file` if flag enabled. Case for verbose log output. """ assert not debug_file.exists() result = cli_runner( '--verbose', '--no-input', '--debug-file', str(debug_file), 'tests/fake-repo-pre/', ) assert result.exit_code == 0 assert debug_file.exists() context_log = ( "DEBUG cookiecutter.main: context_file is " "tests/fake-repo-pre/cookiecutter.json" ) assert context_log in debug_file.read_text() assert context_log in result.output @pytest.mark.usefixtures('make_fake_project_dir', 'remove_fake_project_dir') def test_debug_list_installed_templates(cli_runner, debug_file, user_config_path): """Verify --list-installed command correct invocation.""" fake_template_dir = os.path.dirname(os.path.abspath('fake-project')) os.makedirs(os.path.dirname(user_config_path)) # Single quotes in YAML will not parse escape codes (\). Path(user_config_path).write_text(f"cookiecutters_dir: '{fake_template_dir}'") Path("fake-project", "cookiecutter.json").write_text('{}') result = cli_runner( '--list-installed', '--config-file', user_config_path, str(debug_file), ) assert "1 installed templates:" in result.output assert result.exit_code == 0 def test_debug_list_installed_templates_failure( cli_runner, debug_file, user_config_path ): """Verify --list-installed command error on invocation.""" os.makedirs(os.path.dirname(user_config_path)) Path(user_config_path).write_text('cookiecutters_dir: "/notarealplace/"') result = cli_runner( '--list-installed', '--config-file', user_config_path, str(debug_file) ) assert "Error: Cannot list installed templates." in result.output assert result.exit_code == -1 @pytest.mark.usefixtures('remove_fake_project_dir') def test_directory_repo(cli_runner): """Test cli invocation works with `directory` option.""" result = cli_runner( 'tests/fake-repo-dir/', '--no-input', '-v', '--directory=my-dir', ) assert result.exit_code == 0 assert os.path.isdir("fake-project") content = Path("fake-project", "README.rst").read_text() assert "Project name: **Fake Project**" in content cli_accept_hook_arg_testdata = [ ("--accept-hooks=yes", None, True), ("--accept-hooks=no", None, False), ("--accept-hooks=ask", "yes", True), ("--accept-hooks=ask", "no", False), ] @pytest.mark.parametrize( "accept_hooks_arg,user_input,expected", cli_accept_hook_arg_testdata ) def test_cli_accept_hooks( mocker, cli_runner, output_dir_flag, output_dir, accept_hooks_arg, user_input, expected, ): """Test cli invocation works with `accept-hooks` option.""" mock_cookiecutter = mocker.patch("cookiecutter.cli.cookiecutter") template_path = "tests/fake-repo-pre/" result = cli_runner( template_path, output_dir_flag, output_dir, accept_hooks_arg, input=user_input ) assert result.exit_code == 0 mock_cookiecutter.assert_called_once_with( template_path, None, False, replay=False, overwrite_if_exists=False, output_dir=output_dir, config_file=None, default_config=False, extra_context=None, password=None, directory=None, skip_if_file_exists=False, accept_hooks=expected, keep_project_on_failure=False, ) @pytest.mark.usefixtures('remove_fake_project_dir') def test_cli_with_json_decoding_error(cli_runner): """Test cli invocation with a malformed JSON file.""" template_path = 'tests/fake-repo-bad-json/' result = cli_runner(template_path, '--no-input') assert result.exit_code != 0 # Validate the error message. # original message from json module should be included pattern = 'Expecting \'{0,1}:\'{0,1} delimiter: line 1 column (19|20) \\(char 19\\)' assert re.search(pattern, result.output) # File name should be included too...for testing purposes, just test the # last part of the file. If we wanted to test the absolute path, we'd have # to do some additional work in the test which doesn't seem that needed at # this point. path = os.path.sep.join(['tests', 'fake-repo-bad-json', 'cookiecutter.json']) assert path in result.output @pytest.mark.usefixtures('remove_fake_project_dir') def test_cli_with_pre_prompt_hook(cli_runner): """Test cli invocation in a template with pre_prompt hook.""" template_path = 'tests/test-pyhooks/' result = cli_runner(template_path, '--no-input') assert result.exit_code == 0 dir_name = 'inputfake-project' assert os.path.isdir(dir_name) content = Path(dir_name, "README.rst").read_text() assert 'foo' in content def test_cli_with_pre_prompt_hook_fail(cli_runner, monkeypatch): """Test cli invocation will fail when a given env var is present.""" template_path = 'tests/test-pyhooks/' with monkeypatch.context() as m: m.setenv('COOKIECUTTER_FAIL_PRE_PROMPT', '1') result = cli_runner(template_path, '--no-input') assert result.exit_code == 1 dir_name = 'inputfake-project' assert not Path(dir_name).exists() cookiecutter-2.6.0/tests/test_cookiecutter_invocation.py000066400000000000000000000016011456543333500237340ustar00rootroot00000000000000""" test_cookiecutter_invocation. Tests to make sure that cookiecutter can be called from the cli without using the entry point set up for the package. """ import os import subprocess import sys import pytest from cookiecutter import utils @pytest.fixture def project_dir(): """Return test project folder name and remove it after the test.""" yield 'fake-project-templated' if os.path.isdir('fake-project-templated'): utils.rmtree('fake-project-templated') @pytest.mark.usefixtures('clean_system') def test_should_invoke_main(monkeypatch, project_dir): """Should create a project and exit with 0 code on cli invocation.""" monkeypatch.setenv('PYTHONPATH', '.') exit_code = subprocess.check_call( [sys.executable, '-m', 'cookiecutter.cli', 'tests/fake-repo-tmpl', '--no-input'] ) assert exit_code == 0 assert os.path.isdir(project_dir) cookiecutter-2.6.0/tests/test_cookiecutter_local_no_input.py000066400000000000000000000107201456543333500245720ustar00rootroot00000000000000"""Test cookiecutter for work without any input. Tests in this file execute `cookiecutter()` with `no_input=True` flag and verify result with different settings in `cookiecutter.json`. """ import os import textwrap from pathlib import Path import pytest from cookiecutter import main, utils @pytest.fixture(scope='function') def remove_additional_dirs(request): """Fixture. Remove special directories which are created during the tests.""" def fin_remove_additional_dirs(): if os.path.isdir('fake-project'): utils.rmtree('fake-project') if os.path.isdir('fake-project-extra'): utils.rmtree('fake-project-extra') if os.path.isdir('fake-project-templated'): utils.rmtree('fake-project-templated') if os.path.isdir('fake-project-dict'): utils.rmtree('fake-project-dict') if os.path.isdir('fake-tmp'): utils.rmtree('fake-tmp') request.addfinalizer(fin_remove_additional_dirs) @pytest.mark.parametrize('path', ['tests/fake-repo-pre/', 'tests/fake-repo-pre']) @pytest.mark.usefixtures('clean_system', 'remove_additional_dirs') def test_cookiecutter_no_input_return_project_dir(path): """Verify `cookiecutter` create project dir on input with or without slash.""" project_dir = main.cookiecutter(path, no_input=True) assert os.path.isdir('tests/fake-repo-pre/{{cookiecutter.repo_name}}') assert not os.path.isdir('tests/fake-repo-pre/fake-project') assert os.path.isdir(project_dir) assert os.path.isfile('fake-project/README.rst') assert not os.path.exists('fake-project/json/') @pytest.mark.usefixtures('clean_system', 'remove_additional_dirs') def test_cookiecutter_no_input_extra_context(): """Verify `cookiecutter` accept `extra_context` argument.""" main.cookiecutter( 'tests/fake-repo-pre', no_input=True, extra_context={'repo_name': 'fake-project-extra'}, ) assert os.path.isdir('fake-project-extra') @pytest.mark.usefixtures('clean_system', 'remove_additional_dirs') def test_cookiecutter_templated_context(): """Verify Jinja2 templating correctly works in `cookiecutter.json` file.""" main.cookiecutter('tests/fake-repo-tmpl', no_input=True) assert os.path.isdir('fake-project-templated') @pytest.mark.usefixtures('clean_system', 'remove_additional_dirs') def test_cookiecutter_no_input_return_rendered_file(): """Verify Jinja2 templating correctly works in `cookiecutter.json` file.""" project_dir = main.cookiecutter('tests/fake-repo-pre', no_input=True) assert project_dir == os.path.abspath('fake-project') content = Path(project_dir, 'README.rst').read_text() assert "Project name: **Fake Project**" in content @pytest.mark.usefixtures('clean_system', 'remove_additional_dirs') def test_cookiecutter_dict_values_in_context(): """Verify configured dictionary from `cookiecutter.json` correctly unpacked.""" project_dir = main.cookiecutter('tests/fake-repo-dict', no_input=True) assert project_dir == os.path.abspath('fake-project-dict') content = Path(project_dir, 'README.md').read_text() assert ( content == textwrap.dedent( """ # README
Format name:
Bitmap
Extension:
bmp
Applications:
  • Paint
  • GIMP
Format name:
Portable Network Graphic
Extension:
png
Applications:
  • GIMP
""" ).lstrip() ) @pytest.mark.usefixtures('clean_system', 'remove_additional_dirs') def test_cookiecutter_template_cleanup(mocker): """Verify temporary folder for zip unpacking dropped.""" mocker.patch('tempfile.mkdtemp', return_value='fake-tmp', autospec=True) mocker.patch( 'cookiecutter.prompt.prompt_and_delete', return_value=True, autospec=True ) main.cookiecutter('tests/files/fake-repo-tmpl.zip', no_input=True) assert os.path.isdir('fake-project-templated') # The tmp directory will still exist, but the # extracted template directory *in* the temp directory will not. assert os.path.exists('fake-tmp') assert not os.path.exists('fake-tmp/fake-repo-tmpl') cookiecutter-2.6.0/tests/test_cookiecutter_local_with_input.py000066400000000000000000000032421456543333500251320ustar00rootroot00000000000000"""Test main cookiecutter invocation with user input enabled (mocked).""" import os import pytest from cookiecutter import main, utils @pytest.fixture(scope='function') def remove_additional_dirs(request): """Remove special directories which are created during the tests.""" yield if os.path.isdir('fake-project'): utils.rmtree('fake-project') if os.path.isdir('fake-project-input-extra'): utils.rmtree('fake-project-input-extra') @pytest.mark.usefixtures('clean_system', 'remove_additional_dirs') def test_cookiecutter_local_with_input(monkeypatch): """Verify simple cookiecutter run results, without extra_context provided.""" monkeypatch.setattr( 'cookiecutter.prompt.read_user_variable', lambda var, default, prompts, prefix: default, ) main.cookiecutter('tests/fake-repo-pre/', no_input=False) assert os.path.isdir('tests/fake-repo-pre/{{cookiecutter.repo_name}}') assert not os.path.isdir('tests/fake-repo-pre/fake-project') assert os.path.isdir('fake-project') assert os.path.isfile('fake-project/README.rst') assert not os.path.exists('fake-project/json/') @pytest.mark.usefixtures('clean_system', 'remove_additional_dirs') def test_cookiecutter_input_extra_context(monkeypatch): """Verify simple cookiecutter run results, with extra_context provided.""" monkeypatch.setattr( 'cookiecutter.prompt.read_user_variable', lambda var, default, prompts, prefix: default, ) main.cookiecutter( 'tests/fake-repo-pre', no_input=False, extra_context={'repo_name': 'fake-project-input-extra'}, ) assert os.path.isdir('fake-project-input-extra') cookiecutter-2.6.0/tests/test_cookiecutter_nested_templates.py000066400000000000000000000014311456543333500251240ustar00rootroot00000000000000"""Test cookiecutter invocation with nested configuration structure.""" from pathlib import Path import pytest from cookiecutter import main @pytest.mark.parametrize( "template_dir,output_dir", [ ["fake-nested-templates", "fake-project"], ["fake-nested-templates-old-style", "fake-package"], ], ) def test_cookiecutter_nested_templates(mocker, template_dir: str, output_dir: str): """Verify cookiecutter nested configuration files mechanism.""" mock_generate_files = mocker.patch("cookiecutter.main.generate_files") main_dir = (Path("tests") / template_dir).resolve() main.cookiecutter(f"{main_dir}", no_input=True) expected = (Path(main_dir) / output_dir).resolve() assert mock_generate_files.call_args[1]["repo_dir"] == f"{expected}" cookiecutter-2.6.0/tests/test_custom_extensions_in_hooks.py000066400000000000000000000023511456543333500244700ustar00rootroot00000000000000""" test_custom_extension_in_hooks. Tests to ensure custom cookiecutter extensions are properly made available to pre- and post-gen hooks. """ from pathlib import Path import pytest from cookiecutter import main @pytest.fixture( params=['custom-extension-pre', 'custom-extension-post'], ids=['pre_gen_hook', 'post_gen_hook'], ) def template(request): """Fixture. Allows to split pre and post hooks test directories.""" return f"tests/test-extensions/{request.param}" @pytest.fixture(autouse=True) def modify_syspath(monkeypatch): """Fixture. Make sure that the custom extension can be loaded.""" monkeypatch.syspath_prepend('tests/test-extensions/hello_extension') def test_hook_with_extension(template, output_dir): """Verify custom Jinja2 extension correctly work in hooks and file rendering. Each file in hooks has simple tests inside and will raise error if not correctly rendered. """ project_dir = main.cookiecutter( template, no_input=True, output_dir=output_dir, extra_context={'project_slug': 'foobar', 'name': 'Cookiemonster'}, ) readme = Path(project_dir, 'README.rst').read_text(encoding="utf-8") assert readme.strip() == 'Hello Cookiemonster!' cookiecutter-2.6.0/tests/test_default_extensions.py000066400000000000000000000034721456543333500227160ustar00rootroot00000000000000"""Verify Jinja2 filters/extensions are available from pre-gen/post-gen hooks.""" import os import uuid from pathlib import Path import freezegun import pytest from cookiecutter.main import cookiecutter @pytest.fixture(autouse=True) def freeze(): """Fixture. Make time stating during all tests in this file.""" freezer = freezegun.freeze_time("2015-12-09 23:33:01") freezer.start() yield freezer.stop() def test_jinja2_time_extension(tmp_path): """Verify Jinja2 time extension work correctly.""" project_dir = cookiecutter( 'tests/test-extensions/default/', no_input=True, output_dir=str(tmp_path) ) changelog_file = os.path.join(project_dir, 'HISTORY.rst') assert os.path.isfile(changelog_file) with Path(changelog_file).open(encoding='utf-8') as f: changelog_lines = f.readlines() expected_lines = [ 'History\n', '-------\n', '\n', '0.1.0 (2015-12-09)\n', '------------------\n', '\n', 'First release on PyPI.\n', ] assert expected_lines == changelog_lines def test_jinja2_slugify_extension(tmp_path): """Verify Jinja2 slugify extension work correctly.""" project_dir = cookiecutter( 'tests/test-extensions/default/', no_input=True, output_dir=str(tmp_path) ) assert os.path.basename(project_dir) == "it-s-slugified-foobar" def test_jinja2_uuid_extension(tmp_path): """Verify Jinja2 uuid extension work correctly.""" project_dir = cookiecutter( 'tests/test-extensions/default/', no_input=True, output_dir=str(tmp_path) ) changelog_file = os.path.join(project_dir, 'id') assert os.path.isfile(changelog_file) with Path(changelog_file).open(encoding='utf-8') as f: changelog_lines = f.read().strip() uuid.UUID(changelog_lines, version=4) cookiecutter-2.6.0/tests/test_environment.py000066400000000000000000000020601456543333500213470ustar00rootroot00000000000000"""Collection of tests around loading extensions.""" import pytest from cookiecutter.environment import StrictEnvironment from cookiecutter.exceptions import UnknownExtension def test_env_should_raise_for_unknown_extension(): """Test should raise if extension not installed in system.""" context = {'cookiecutter': {'_extensions': ['foobar']}} with pytest.raises(UnknownExtension) as err: StrictEnvironment(context=context, keep_trailing_newline=True) assert 'Unable to load extension: ' in str(err.value) def test_env_should_come_with_default_extensions(): """Verify default extensions loaded with StrictEnvironment.""" env = StrictEnvironment(keep_trailing_newline=True) assert 'cookiecutter.extensions.JsonifyExtension' in env.extensions assert 'cookiecutter.extensions.RandomStringExtension' in env.extensions assert 'cookiecutter.extensions.SlugifyExtension' in env.extensions assert 'cookiecutter.extensions.TimeExtension' in env.extensions assert 'cookiecutter.extensions.UUIDExtension' in env.extensions cookiecutter-2.6.0/tests/test_exceptions.py000066400000000000000000000013111456543333500211620ustar00rootroot00000000000000"""Collection of tests around general exception handling.""" from jinja2.exceptions import UndefinedError from cookiecutter import exceptions def test_undefined_variable_to_str(): """Verify string representation of errors formatted in expected form.""" undefined_var_error = exceptions.UndefinedVariableInTemplate( 'Beautiful is better than ugly', UndefinedError('Errors should never pass silently'), {'cookiecutter': {'foo': 'bar'}}, ) expected_str = ( "Beautiful is better than ugly. " "Error message: Errors should never pass silently. " "Context: {'cookiecutter': {'foo': 'bar'}}" ) assert str(undefined_var_error) == expected_str cookiecutter-2.6.0/tests/test_find.py000066400000000000000000000042361456543333500177320ustar00rootroot00000000000000"""Tests for `cookiecutter.find` module.""" from contextlib import nullcontext as does_not_raise from pathlib import Path import pytest from cookiecutter import find from cookiecutter.exceptions import NonTemplatedInputDirException from cookiecutter.utils import create_env_with_context @pytest.fixture(params=['fake-repo-pre', 'fake-repo-pre2']) def repo_dir(request): """Fixture returning path for `test_find_template` test.""" return Path('tests', request.param) @pytest.fixture() def env(context): """Fixture return the env generated from context.""" return create_env_with_context(context) @pytest.mark.parametrize( "repo_name,context,error_expectation,expected", [ ("fake-repo-pre", {}, does_not_raise(), '{{cookiecutter.repo_name}}'), ( "fake-repo-pre2", { 'cookiecutter': { '_jinja2_env_vars': { 'variable_start_string': '{%{', 'variable_end_string': '}%}', } } }, does_not_raise(), '{%{cookiecutter.repo_name}%}', ), ( "fake-repo-pre", { 'cookiecutter': { '_jinja2_env_vars': { 'variable_start_string': '{%{', 'variable_end_string': '}%}', } } }, pytest.raises(NonTemplatedInputDirException), None, ), ("fake-repo-bad", {}, pytest.raises(NonTemplatedInputDirException), None), ], ids=[ 'template with default jinja strings', 'template with custom jinja strings', 'template with custom jinja strings but folder with default jinja strings', 'template missing folder', ], ) def test_find_template(repo_name, env, error_expectation, expected): """Verify correctness of `find.find_template` path detection.""" repo_dir = Path('tests', repo_name) with error_expectation: template = find.find_template(repo_dir, env) test_dir = Path(repo_dir, expected) assert template == test_dir cookiecutter-2.6.0/tests/test_generate_context.py000066400000000000000000000305671456543333500223560ustar00rootroot00000000000000"""Verify generate context behaviour and context overwrite priorities.""" import os import re from collections import OrderedDict import pytest from cookiecutter import generate from cookiecutter.exceptions import ContextDecodingException def context_data(): """Generate pytest parametrization variables for test. Return ('input_params, expected_context') tuples. """ context = ( {'context_file': 'tests/test-generate-context/test.json'}, {'test': {'1': 2, 'some_key': 'some_val'}}, ) context_with_default = ( { 'context_file': 'tests/test-generate-context/test.json', 'default_context': {'1': 3}, }, {'test': {'1': 3, 'some_key': 'some_val'}}, ) context_with_extra = ( { 'context_file': 'tests/test-generate-context/test.json', 'extra_context': {'1': 4}, }, {'test': {'1': 4, 'some_key': 'some_val'}}, ) context_with_default_and_extra = ( { 'context_file': 'tests/test-generate-context/test.json', 'default_context': {'1': 3}, 'extra_context': {'1': 5}, }, {'test': {'1': 5, 'some_key': 'some_val'}}, ) yield context yield context_with_default yield context_with_extra yield context_with_default_and_extra @pytest.mark.usefixtures('clean_system') @pytest.mark.parametrize('input_params, expected_context', context_data()) def test_generate_context(input_params, expected_context): """Verify input contexts combinations result in expected content on output.""" assert generate.generate_context(**input_params) == expected_context @pytest.mark.usefixtures('clean_system') def test_generate_context_with_json_decoding_error(): """Verify malformed JSON file generates expected error output.""" with pytest.raises(ContextDecodingException) as excinfo: generate.generate_context('tests/test-generate-context/invalid-syntax.json') # original message from json module should be included pattern = 'Expecting \'{0,1}:\'{0,1} delimiter: line 1 column (19|20) \\(char 19\\)' assert re.search(pattern, str(excinfo.value)) # File name should be included too...for testing purposes, just test the # last part of the file. If we wanted to test the absolute path, we'd have # to do some additional work in the test which doesn't seem that needed at # this point. path = os.path.sep.join(['tests', 'test-generate-context', 'invalid-syntax.json']) assert path in str(excinfo.value) def test_default_context_replacement_in_generate_context(): """Verify default content settings are correctly replaced by template settings. Make sure that the default for list variables of `orientation` is based on the user config (`choices_template.json`) and not changed to a single value from `default_context`. """ expected_context = { 'choices_template': OrderedDict( [ ('full_name', 'Raphael Pierzina'), ('github_username', 'hackebrot'), ('project_name', 'Kivy Project'), ('repo_name', '{{cookiecutter.project_name|lower}}'), ('orientation', ['landscape', 'all', 'portrait']), ] ) } generated_context = generate.generate_context( context_file='tests/test-generate-context/choices_template.json', default_context={ 'not_in_template': 'foobar', 'project_name': 'Kivy Project', 'orientation': 'landscape', }, extra_context={ 'also_not_in_template': 'foobar2', 'github_username': 'hackebrot', }, ) assert generated_context == expected_context def test_generate_context_decodes_non_ascii_chars(): """Verify `generate_context` correctly decodes non-ascii chars.""" expected_context = { 'non_ascii': OrderedDict( [ ('full_name', 'éèà'), ] ) } generated_context = generate.generate_context( context_file='tests/test-generate-context/non_ascii.json' ) assert generated_context == expected_context @pytest.fixture def template_context(): """Fixture. Populates template content for future tests.""" return OrderedDict( [ ('full_name', 'Raphael Pierzina'), ('github_username', 'hackebrot'), ('project_name', 'Kivy Project'), ('repo_name', '{{cookiecutter.project_name|lower}}'), ('orientation', ['all', 'landscape', 'portrait']), ('deployment_regions', ['eu', 'us', 'ap']), ( 'deployments', { 'preprod': ['eu', 'us', 'ap'], 'prod': ['eu', 'us', 'ap'], }, ), ] ) def test_apply_overwrites_does_include_unused_variables(template_context): """Verify `apply_overwrites_to_context` skips variables that are not in context.""" generate.apply_overwrites_to_context( context=template_context, overwrite_context={'not in template': 'foobar'} ) assert 'not in template' not in template_context def test_apply_overwrites_sets_non_list_value(template_context): """Verify `apply_overwrites_to_context` work with string variables.""" generate.apply_overwrites_to_context( context=template_context, overwrite_context={'repo_name': 'foobar'} ) assert template_context['repo_name'] == 'foobar' def test_apply_overwrites_does_not_modify_choices_for_invalid_overwrite(): """Verify variables overwrite for list if variable not in list ignored.""" expected_context = { 'choices_template': OrderedDict( [ ('full_name', 'Raphael Pierzina'), ('github_username', 'hackebrot'), ('project_name', 'Kivy Project'), ('repo_name', '{{cookiecutter.project_name|lower}}'), ('orientation', ['all', 'landscape', 'portrait']), ] ) } with pytest.warns(UserWarning, match="Invalid default received"): generated_context = generate.generate_context( context_file='tests/test-generate-context/choices_template.json', default_context={ 'not_in_template': 'foobar', 'project_name': 'Kivy Project', 'orientation': 'foobar', }, extra_context={ 'also_not_in_template': 'foobar2', 'github_username': 'hackebrot', }, ) assert generated_context == expected_context def test_apply_overwrites_invalid_overwrite(template_context): """Verify variables overwrite for list if variable not in list not ignored.""" with pytest.raises(ValueError): generate.apply_overwrites_to_context( context=template_context, overwrite_context={'orientation': 'foobar'} ) def test_apply_overwrites_sets_multichoice_values(template_context): """Verify variable overwrite for list given multiple valid values.""" generate.apply_overwrites_to_context( context=template_context, overwrite_context={'deployment_regions': ['eu']}, ) assert template_context['deployment_regions'] == ['eu'] def test_apply_overwrites_invalid_multichoice_values(template_context): """Verify variable overwrite for list given invalid list entries not ignored.""" with pytest.raises(ValueError): generate.apply_overwrites_to_context( context=template_context, overwrite_context={'deployment_regions': ['na']}, ) def test_apply_overwrites_error_additional_values(template_context): """Verify variable overwrite for list given additional entries not ignored.""" with pytest.raises(ValueError): generate.apply_overwrites_to_context( context=template_context, overwrite_context={'deployment_regions': ['eu', 'na']}, ) def test_apply_overwrites_in_dictionaries(template_context): """Verify variable overwrite for lists nested in dictionary variables.""" generate.apply_overwrites_to_context( context=template_context, overwrite_context={'deployments': {'preprod': ['eu'], 'prod': ['ap']}}, ) assert template_context['deployments']['preprod'] == ['eu'] assert template_context['deployments']['prod'] == ['ap'] def test_apply_overwrites_sets_default_for_choice_variable(template_context): """Verify overwritten list member became a default value.""" generate.apply_overwrites_to_context( context=template_context, overwrite_context={'orientation': 'landscape'} ) assert template_context['orientation'] == ['landscape', 'all', 'portrait'] def test_apply_overwrites_in_nested_dict(): """Verify nested dict in default content settings are correctly replaced.""" expected_context = { 'nested_dict': OrderedDict( [ ('full_name', 'Raphael Pierzina'), ('github_username', 'hackebrot'), ( 'project', OrderedDict( [ ('name', 'My Kivy Project'), ('description', 'My Kivy Project'), ('repo_name', '{{cookiecutter.project_name|lower}}'), ('orientation', ["all", "landscape", "portrait"]), ] ), ), ] ) } generated_context = generate.generate_context( context_file='tests/test-generate-context/nested_dict.json', default_context={ 'not_in_template': 'foobar', 'project': { 'description': 'My Kivy Project', }, }, extra_context={ 'also_not_in_template': 'foobar2', 'github_username': 'hackebrot', 'project': { 'name': 'My Kivy Project', }, }, ) assert generated_context == expected_context def test_apply_overwrite_context_as_in_nested_dict_with_additional_values(): """Verify nested dict in default content settings are correctly added. The `apply_overwrites_to_context` function should add the extra values to the dict. """ expected = OrderedDict({"key1": "value1", "key2": "value2"}) context = OrderedDict({"key1": "value1"}) overwrite_context = OrderedDict({"key2": "value2"}) generate.apply_overwrites_to_context( context, overwrite_context, in_dictionary_variable=True, ) assert context == expected def test_apply_overwrites_in_nested_dict_additional_values(): """Verify nested dict in default content settings are correctly added.""" expected_context = { 'nested_dict_additional': OrderedDict( [ ('mainkey1', 'mainvalue1'), ( 'mainkey2', OrderedDict( [ ('subkey1', 'subvalue1'), ( 'subkey2', OrderedDict( [ ('subsubkey1', 'subsubvalue1'), ('subsubkey2', 'subsubvalue2_default'), ('subsubkey3', 'subsubvalue3_extra'), ] ), ), ('subkey4', 'subvalue4_default'), ('subkey5', 'subvalue5_extra'), ] ), ), ] ) } generated_context = generate.generate_context( context_file='tests/test-generate-context/nested_dict_additional.json', default_context={ 'not_in_template': 'foobar', 'mainkey2': { 'subkey2': { 'subsubkey2': 'subsubvalue2_default', }, 'subkey4': 'subvalue4_default', }, }, extra_context={ 'also_not_in_template': 'foobar2', 'mainkey2': { 'subkey2': { 'subsubkey3': 'subsubvalue3_extra', }, 'subkey5': 'subvalue5_extra', }, }, ) assert generated_context == expected_context cookiecutter-2.6.0/tests/test_generate_copy_without_render.py000066400000000000000000000046541456543333500247640ustar00rootroot00000000000000"""Verify correct work of `_copy_without_render` context option.""" import os from pathlib import Path import pytest from cookiecutter import generate, utils @pytest.fixture def remove_test_dir(): """Fixture. Remove the folder that is created by the test.""" yield if os.path.exists('test_copy_without_render'): utils.rmtree('test_copy_without_render') @pytest.mark.usefixtures('clean_system', 'remove_test_dir') def test_generate_copy_without_render_extensions(): """Verify correct work of `_copy_without_render` context option. Some files/directories should be rendered during invocation, some just copied, without any modification. """ generate.generate_files( context={ 'cookiecutter': { 'repo_name': 'test_copy_without_render', 'render_test': 'I have been rendered!', '_copy_without_render': [ '*not-rendered', 'rendered/not_rendered.yml', '*.txt', '{{cookiecutter.repo_name}}-rendered/README.md', ], } }, repo_dir='tests/test-generate-copy-without-render', ) dir_contents = os.listdir('test_copy_without_render') assert 'test_copy_without_render-not-rendered' in dir_contents assert 'test_copy_without_render-rendered' in dir_contents file_1 = Path('test_copy_without_render/README.txt').read_text() assert '{{cookiecutter.render_test}}' in file_1 file_2 = Path('test_copy_without_render/README.rst').read_text() assert 'I have been rendered!' in file_2 file_3 = Path( 'test_copy_without_render/test_copy_without_render-rendered/README.txt' ).read_text() assert '{{cookiecutter.render_test}}' in file_3 file_4 = Path( 'test_copy_without_render/test_copy_without_render-rendered/README.rst' ).read_text() assert 'I have been rendered' in file_4 file_5 = Path( 'test_copy_without_render/' 'test_copy_without_render-not-rendered/' 'README.rst' ).read_text() assert '{{cookiecutter.render_test}}' in file_5 file_6 = Path('test_copy_without_render/rendered/not_rendered.yml').read_text() assert '{{cookiecutter.render_test}}' in file_6 file_7 = Path( 'test_copy_without_render/' 'test_copy_without_render-rendered/' 'README.md' ).read_text() assert '{{cookiecutter.render_test}}' in file_7 cookiecutter-2.6.0/tests/test_generate_copy_without_render_override.py000066400000000000000000000060421456543333500266540ustar00rootroot00000000000000"""Verify correct work of `_copy_without_render` context option.""" import os from pathlib import Path import pytest from cookiecutter import generate, utils @pytest.fixture def remove_test_dir(): """Fixture. Remove the folder that is created by the test.""" yield if os.path.exists('test_copy_without_render'): utils.rmtree('test_copy_without_render') @pytest.mark.usefixtures('clean_system', 'remove_test_dir') def test_generate_copy_without_render_extensions(): """Verify correct work of `_copy_without_render` context option. Some files/directories should be rendered during invocation, some just copied, without any modification. """ # first run generate.generate_files( context={ 'cookiecutter': { 'repo_name': 'test_copy_without_render', 'render_test': 'I have been rendered!', '_copy_without_render': [ '*not-rendered', 'rendered/not_rendered.yml', '*.txt', '{{cookiecutter.repo_name}}-rendered/README.md', ], } }, repo_dir='tests/test-generate-copy-without-render-override', ) # second run with override flag to True generate.generate_files( context={ 'cookiecutter': { 'repo_name': 'test_copy_without_render', 'render_test': 'I have been rendered!', '_copy_without_render': [ '*not-rendered', 'rendered/not_rendered.yml', '*.txt', '{{cookiecutter.repo_name}}-rendered/README.md', ], } }, overwrite_if_exists=True, repo_dir='tests/test-generate-copy-without-render', ) dir_contents = os.listdir('test_copy_without_render') assert 'test_copy_without_render-not-rendered' in dir_contents assert 'test_copy_without_render-rendered' in dir_contents file_1 = Path('test_copy_without_render/README.txt').read_text() assert '{{cookiecutter.render_test}}' in file_1 file_2 = Path('test_copy_without_render/README.rst').read_text() assert 'I have been rendered!' in file_2 file_3 = Path( 'test_copy_without_render/test_copy_without_render-rendered/README.txt' ).read_text() assert '{{cookiecutter.render_test}}' in file_3 file_4 = Path( 'test_copy_without_render/test_copy_without_render-rendered/README.rst' ).read_text() assert 'I have been rendered' in file_4 file_5 = Path( 'test_copy_without_render/' 'test_copy_without_render-not-rendered/' 'README.rst' ).read_text() assert '{{cookiecutter.render_test}}' in file_5 file_6 = Path('test_copy_without_render/rendered/not_rendered.yml').read_text() assert '{{cookiecutter.render_test}}' in file_6 file_7 = Path( 'test_copy_without_render/' 'test_copy_without_render-rendered/' 'README.md' ).read_text() assert '{{cookiecutter.render_test}}' in file_7 cookiecutter-2.6.0/tests/test_generate_file.py000066400000000000000000000161161456543333500216030ustar00rootroot00000000000000"""Tests for `generate_file` function, part of `generate_files` function workflow.""" import json import os import re from pathlib import Path import pytest from jinja2 import FileSystemLoader from jinja2.exceptions import TemplateSyntaxError from cookiecutter import generate from cookiecutter.environment import StrictEnvironment @pytest.fixture(scope='function', autouse=True) def tear_down(): """ Fixture. Remove the test text file which is created by the tests. Used for all tests in this file. """ yield if os.path.exists('tests/files/cheese.txt'): os.remove('tests/files/cheese.txt') if os.path.exists('tests/files/cheese_lf_newlines.txt'): os.remove('tests/files/cheese_lf_newlines.txt') if os.path.exists('tests/files/cheese_crlf_newlines.txt'): os.remove('tests/files/cheese_crlf_newlines.txt') if os.path.exists('tests/files/cheese_mixed_newlines.txt'): os.remove('tests/files/cheese_mixed_newlines.txt') if os.path.exists('tests/files/{{cookiecutter.generate_file}}_mixed_newlines.txt'): os.remove('tests/files/{{cookiecutter.generate_file}}_mixed_newlines.txt') @pytest.fixture def env(): """Fixture. Set Jinja2 environment settings for other tests.""" environment = StrictEnvironment() environment.loader = FileSystemLoader('.') return environment def test_generate_file(env): """Verify simple file is generated with rendered context data.""" infile = 'tests/files/{{cookiecutter.generate_file}}.txt' generate.generate_file( project_dir=".", infile=infile, context={'cookiecutter': {'generate_file': 'cheese'}}, env=env, ) assert os.path.isfile('tests/files/cheese.txt') generated_text = Path('tests/files/cheese.txt').read_text() assert generated_text == 'Testing cheese' def test_generate_file_jsonify_filter(env): """Verify jsonify filter works during files generation process.""" infile = 'tests/files/{{cookiecutter.jsonify_file}}.txt' data = {'jsonify_file': 'cheese', 'type': 'roquefort'} generate.generate_file( project_dir=".", infile=infile, context={'cookiecutter': data}, env=env ) assert os.path.isfile('tests/files/cheese.txt') generated_text = Path('tests/files/cheese.txt').read_text() assert json.loads(generated_text) == data @pytest.mark.parametrize("length", (10, 40)) @pytest.mark.parametrize("punctuation", (True, False)) def test_generate_file_random_ascii_string(env, length, punctuation): """Verify correct work of random_ascii_string extension on file generation.""" infile = 'tests/files/{{cookiecutter.random_string_file}}.txt' data = {'random_string_file': 'cheese'} context = {"cookiecutter": data, "length": length, "punctuation": punctuation} generate.generate_file(project_dir=".", infile=infile, context=context, env=env) assert os.path.isfile('tests/files/cheese.txt') generated_text = Path('tests/files/cheese.txt').read_text() assert len(generated_text) == length def test_generate_file_with_true_condition(env): """Verify correct work of boolean condition in file name on file generation. This test has positive answer, so file should be rendered. """ infile = ( 'tests/files/{% if cookiecutter.generate_file == \'y\' %}cheese.txt{% endif %}' ) generate.generate_file( project_dir=".", infile=infile, context={'cookiecutter': {'generate_file': 'y'}}, env=env, ) assert os.path.isfile('tests/files/cheese.txt') generated_text = Path('tests/files/cheese.txt').read_text() assert generated_text == 'Testing that generate_file was y' def test_generate_file_with_false_condition(env): """Verify correct work of boolean condition in file name on file generation. This test has negative answer, so file should not be rendered. """ infile = ( 'tests/files/{% if cookiecutter.generate_file == \'y\' %}cheese.txt{% endif %}' ) generate.generate_file( project_dir=".", infile=infile, context={'cookiecutter': {'generate_file': 'n'}}, env=env, ) assert not os.path.isfile('tests/files/cheese.txt') @pytest.fixture def expected_msg_regex(): """Fixture. Used to ensure that exception generated text contain full data.""" return re.compile( 'Missing end of comment tag\n' ' {2}File "(.[/\\\\])*tests[/\\\\]files[/\\\\]syntax_error.txt", line 1\n' ' {4}I eat {{ syntax_error }} {# this comment is not closed}' ) def test_generate_file_verbose_template_syntax_error(env, expected_msg_regex): """Verify correct exception raised on syntax error in file before generation.""" with pytest.raises(TemplateSyntaxError) as exception: generate.generate_file( project_dir=".", infile='tests/files/syntax_error.txt', context={'syntax_error': 'syntax_error'}, env=env, ) assert expected_msg_regex.match(str(exception.value)) def test_generate_file_does_not_translate_lf_newlines_to_crlf(env, tmp_path): """Verify that file generation use same line ending, as in source file.""" infile = 'tests/files/{{cookiecutter.generate_file}}_lf_newlines.txt' generate.generate_file( project_dir=".", infile=infile, context={'cookiecutter': {'generate_file': 'cheese'}}, env=env, ) # this generated file should have a LF line ending gf = 'tests/files/cheese_lf_newlines.txt' with Path(gf).open(encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is LF\n' assert f.newlines == '\n' def test_generate_file_does_not_translate_crlf_newlines_to_lf(env): """Verify that file generation use same line ending, as in source file.""" infile = 'tests/files/{{cookiecutter.generate_file}}_crlf_newlines.txt' generate.generate_file( project_dir=".", infile=infile, context={'cookiecutter': {'generate_file': 'cheese'}}, env=env, ) # this generated file should have a CRLF line ending gf = 'tests/files/cheese_crlf_newlines.txt' with Path(gf).open(encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is CRLF\r\n' assert f.newlines == '\r\n' def test_generate_file_handles_mixed_line_endings(env, tmp_path): """Verify that file generation gracefully handles mixed line endings.""" infile = 'tests/files/{{cookiecutter.generate_file}}_mixed_newlines.txt' with open(infile, mode='w', encoding='utf-8', newline='') as f: f.write('newline is CRLF\r\n') f.write('newline is LF\n') generate.generate_file( project_dir=".", infile=infile, context={'cookiecutter': {'generate_file': 'cheese'}}, env=env, ) # this generated file should have either CRLF or LF line ending gf = 'tests/files/cheese_mixed_newlines.txt' with Path(gf).open(encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text in ('newline is CRLF\r\n', 'newline is CRLF\n') assert f.newlines in ('\r\n', '\n') cookiecutter-2.6.0/tests/test_generate_files.py000066400000000000000000000363661456543333500217770ustar00rootroot00000000000000"""Tests for `generate_files` function and related errors raising. Use the global clean_system fixture and run additional teardown code to remove some special folders. """ from pathlib import Path import pytest from binaryornot.check import is_binary from cookiecutter import exceptions, generate def test_generate_files_nontemplated_exception(tmp_path): """ Verify `generate_files` raises when no directories to render exist. Note: Check `tests/test-generate-files-nontemplated` location to understand. """ with pytest.raises(exceptions.NonTemplatedInputDirException): generate.generate_files( context={'cookiecutter': {'food': 'pizza'}}, repo_dir='tests/test-generate-files-nontemplated', output_dir=tmp_path, ) def test_generate_files(tmp_path): """Verify directory name correctly rendered with unicode containing context.""" generate.generate_files( context={'cookiecutter': {'food': 'pizzä'}}, repo_dir='tests/test-generate-files', output_dir=tmp_path, ) simple_file = Path(tmp_path, 'inputpizzä/simple.txt') assert simple_file.exists() assert simple_file.is_file() simple_text = Path(simple_file).read_text(encoding='utf-8') assert simple_text == 'I eat pizzä\n' def test_generate_files_with_linux_newline(tmp_path): """Verify new line not removed by templating engine after folder generation.""" generate.generate_files( context={'cookiecutter': {'food': 'pizzä'}}, repo_dir='tests/test-generate-files', output_dir=tmp_path, ) newline_file = Path(tmp_path, 'inputpizzä/simple-with-newline.txt') assert newline_file.is_file() assert newline_file.exists() with Path(newline_file).open(encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is LF\n' assert f.newlines == '\n' def test_generate_files_with_jinja2_environment(tmp_path): """Extend StrictEnvironment with _jinja2_env_vars cookiecutter template option.""" generate.generate_files( context={ 'cookiecutter': { 'food': 'pizzä', '_jinja2_env_vars': {'lstrip_blocks': True, 'trim_blocks': True}, } }, repo_dir='tests/test-generate-files', output_dir=tmp_path, ) conditions_file = tmp_path.joinpath('inputpizzä/simple-with-conditions.txt') assert conditions_file.is_file() assert conditions_file.exists() simple_text = conditions_file.read_text(encoding='utf-8') assert simple_text == 'I eat pizzä\n' def test_generate_files_with_trailing_newline_forced_to_linux_by_context(tmp_path): """Verify new line not removed by templating engine after folder generation.""" generate.generate_files( context={'cookiecutter': {'food': 'pizzä', '_new_lines': '\r\n'}}, repo_dir='tests/test-generate-files', output_dir=tmp_path, ) # assert 'Overwritting endline character with %s' in caplog.messages newline_file = Path(tmp_path, 'inputpizzä/simple-with-newline.txt') assert newline_file.is_file() assert newline_file.exists() with Path(newline_file).open(encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is LF\r\n' assert f.newlines == '\r\n' def test_generate_files_with_windows_newline(tmp_path): """Verify windows source line end not changed during files generation.""" generate.generate_files( context={'cookiecutter': {'food': 'pizzä'}}, repo_dir='tests/test-generate-files', output_dir=tmp_path, ) newline_file = Path(tmp_path, 'inputpizzä/simple-with-newline-crlf.txt') assert newline_file.is_file() assert newline_file.exists() with Path(newline_file).open(encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is CRLF\r\n' assert f.newlines == '\r\n' def test_generate_files_with_windows_newline_forced_to_linux_by_context(tmp_path): """Verify windows line end changed to linux during files generation.""" generate.generate_files( context={'cookiecutter': {'food': 'pizzä', '_new_lines': '\n'}}, repo_dir='tests/test-generate-files', output_dir=tmp_path, ) newline_file = Path(tmp_path, 'inputpizzä/simple-with-newline-crlf.txt') assert newline_file.is_file() assert newline_file.exists() with Path(newline_file).open(encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is CRLF\n' assert f.newlines == '\n' def test_generate_files_binaries(tmp_path): """Verify binary files created during directory generation.""" generate.generate_files( context={'cookiecutter': {'binary_test': 'binary_files'}}, repo_dir='tests/test-generate-binaries', output_dir=tmp_path, ) dst_dir = Path(tmp_path, 'inputbinary_files') assert is_binary(str(Path(dst_dir, 'logo.png'))) assert is_binary(str(Path(dst_dir, '.DS_Store'))) assert not is_binary(str(Path(dst_dir, 'readme.txt'))) assert is_binary(str(Path(dst_dir, 'some_font.otf'))) assert is_binary(str(Path(dst_dir, 'binary_files/logo.png'))) assert is_binary(str(Path(dst_dir, 'binary_files/.DS_Store'))) assert not is_binary(str(Path(dst_dir, 'binary_files/readme.txt'))) assert is_binary(str(Path(dst_dir, 'binary_files/some_font.otf'))) assert is_binary(str(Path(dst_dir, 'binary_files/binary_files/logo.png'))) def test_generate_files_absolute_path(tmp_path): """Verify usage of absolute path does not change files generation behaviour.""" generate.generate_files( context={'cookiecutter': {'food': 'pizzä'}}, repo_dir=Path('tests/test-generate-files').absolute(), output_dir=tmp_path, ) assert Path(tmp_path, 'inputpizzä/simple.txt').is_file() def test_generate_files_output_dir(tmp_path): """Verify `output_dir` option for `generate_files` changing location correctly.""" output_dir = Path(tmp_path, 'custom_output_dir') output_dir.mkdir() project_dir = generate.generate_files( context={'cookiecutter': {'food': 'pizzä'}}, repo_dir=Path('tests/test-generate-files').absolute(), output_dir=output_dir, ) assert Path(output_dir, 'inputpizzä/simple.txt').exists() assert Path(output_dir, 'inputpizzä/simple.txt').is_file() assert Path(project_dir) == Path(tmp_path, 'custom_output_dir/inputpizzä') def test_generate_files_permissions(tmp_path): """Verify generates files respect source files permissions. simple.txt and script.sh should retain their respective 0o644 and 0o755 permissions. """ generate.generate_files( context={'cookiecutter': {'permissions': 'permissions'}}, repo_dir='tests/test-generate-files-permissions', output_dir=tmp_path, ) assert Path(tmp_path, 'inputpermissions/simple.txt').is_file() # Verify source simple.txt should still be 0o644 tests_simple_file = Path( 'tests', 'test-generate-files-permissions', 'input{{cookiecutter.permissions}}', 'simple.txt', ) tests_simple_file_mode = tests_simple_file.stat().st_mode input_simple_file = Path(tmp_path, 'inputpermissions', 'simple.txt') input_simple_file_mode = input_simple_file.stat().st_mode assert tests_simple_file_mode == input_simple_file_mode assert Path(tmp_path, 'inputpermissions/script.sh').exists() assert Path(tmp_path, 'inputpermissions/script.sh').is_file() # Verify source script.sh should still be 0o755 tests_script_file = Path( 'tests', 'test-generate-files-permissions', 'input{{cookiecutter.permissions}}', 'script.sh', ) tests_script_file_mode = tests_script_file.stat().st_mode input_script_file = Path(tmp_path, 'inputpermissions', 'script.sh') input_script_file_mode = input_script_file.stat().st_mode assert tests_script_file_mode == input_script_file_mode def test_generate_files_with_overwrite_if_exists_with_skip_if_file_exists(tmp_path): """Verify `skip_if_file_exist` has priority over `overwrite_if_exists`.""" simple_file = Path(tmp_path, 'inputpizzä/simple.txt') simple_with_new_line_file = Path(tmp_path, 'inputpizzä/simple-with-newline.txt') Path(tmp_path, 'inputpizzä').mkdir(parents=True) with Path(simple_file).open('w') as f: f.write('temp') generate.generate_files( context={'cookiecutter': {'food': 'pizzä'}}, repo_dir='tests/test-generate-files', overwrite_if_exists=True, skip_if_file_exists=True, output_dir=tmp_path, ) assert Path(simple_file).is_file() assert Path(simple_file).exists() assert Path(simple_with_new_line_file).is_file() assert Path(simple_with_new_line_file).exists() simple_text = Path(simple_file).read_text(encoding='utf-8') assert simple_text == 'temp' def test_generate_files_with_skip_if_file_exists(tmp_path): """Verify existed files not removed if error raised with `skip_if_file_exists`.""" simple_file = Path(tmp_path, 'inputpizzä/simple.txt') simple_with_new_line_file = Path(tmp_path, 'inputpizzä/simple-with-newline.txt') Path(tmp_path, 'inputpizzä').mkdir(parents=True) Path(simple_file).write_text('temp') with pytest.raises(exceptions.OutputDirExistsException): generate.generate_files( context={'cookiecutter': {'food': 'pizzä'}}, repo_dir='tests/test-generate-files', skip_if_file_exists=True, output_dir=tmp_path, ) assert Path(simple_file).is_file() assert not Path(simple_with_new_line_file).is_file() assert not Path(simple_with_new_line_file).exists() simple_text = Path(simple_file).read_text(encoding='utf-8') assert simple_text == 'temp' def test_generate_files_with_overwrite_if_exists(tmp_path): """Verify overwrite_if_exists overwrites old files.""" simple_file = Path(tmp_path, 'inputpizzä/simple.txt') simple_with_new_line_file = Path(tmp_path, 'inputpizzä/simple-with-newline.txt') Path(tmp_path, 'inputpizzä').mkdir(parents=True) Path(simple_file).write_text('temp') generate.generate_files( context={'cookiecutter': {'food': 'pizzä'}}, repo_dir='tests/test-generate-files', overwrite_if_exists=True, output_dir=tmp_path, ) assert Path(simple_file).is_file() assert Path(simple_file).exists() assert Path(simple_with_new_line_file).is_file() assert Path(simple_with_new_line_file).exists() simple_text = Path(simple_file).read_text(encoding='utf-8') assert simple_text == 'I eat pizzä\n' @pytest.fixture def undefined_context(): """Fixture. Populate context variable for future tests.""" return { 'cookiecutter': {'project_slug': 'testproject', 'github_username': 'hackebrot'} } def test_raise_undefined_variable_file_name(output_dir, undefined_context): """Verify correct error raised when file name cannot be rendered.""" with pytest.raises(exceptions.UndefinedVariableInTemplate) as err: generate.generate_files( repo_dir='tests/undefined-variable/file-name/', output_dir=output_dir, context=undefined_context, ) error = err.value assert "Unable to create file '{{cookiecutter.foobar}}'" == error.message assert error.context == undefined_context assert not Path(output_dir).joinpath('testproject').exists() def test_raise_undefined_variable_file_name_existing_project( output_dir, undefined_context ): """Verify correct error raised when file name cannot be rendered.""" testproj_path = Path(output_dir, 'testproject') testproj_path.mkdir() with pytest.raises(exceptions.UndefinedVariableInTemplate) as err: generate.generate_files( repo_dir='tests/undefined-variable/file-name/', output_dir=output_dir, context=undefined_context, overwrite_if_exists=True, ) error = err.value assert "Unable to create file '{{cookiecutter.foobar}}'" == error.message assert error.context == undefined_context assert testproj_path.exists() def test_raise_undefined_variable_file_content(output_dir, undefined_context): """Verify correct error raised when file content cannot be rendered.""" with pytest.raises(exceptions.UndefinedVariableInTemplate) as err: generate.generate_files( repo_dir='tests/undefined-variable/file-content/', output_dir=output_dir, context=undefined_context, ) error = err.value assert "Unable to create file 'README.rst'" == error.message assert error.context == undefined_context assert not Path(output_dir).joinpath('testproject').exists() def test_raise_undefined_variable_dir_name(output_dir, undefined_context): """Verify correct error raised when directory name cannot be rendered.""" with pytest.raises(exceptions.UndefinedVariableInTemplate) as err: generate.generate_files( repo_dir='tests/undefined-variable/dir-name/', output_dir=output_dir, context=undefined_context, ) error = err.value directory = Path('testproject', '{{cookiecutter.foobar}}') msg = f"Unable to create directory '{directory}'" assert msg == error.message assert error.context == undefined_context assert not Path(output_dir).joinpath('testproject').exists() def test_keep_project_dir_on_failure(output_dir, undefined_context): """Verify correct error raised when directory name cannot be rendered.""" with pytest.raises(exceptions.UndefinedVariableInTemplate): generate.generate_files( repo_dir='tests/undefined-variable/dir-name/', output_dir=output_dir, context=undefined_context, keep_project_on_failure=True, ) assert Path(output_dir).joinpath('testproject').exists() def test_raise_undefined_variable_dir_name_existing_project( output_dir, undefined_context ): """Verify correct error raised when directory name cannot be rendered.""" testproj_path = Path(output_dir, 'testproject') testproj_path.mkdir() with pytest.raises(exceptions.UndefinedVariableInTemplate) as err: generate.generate_files( repo_dir='tests/undefined-variable/dir-name/', output_dir=output_dir, context=undefined_context, overwrite_if_exists=True, ) error = err.value directory = Path('testproject', '{{cookiecutter.foobar}}') msg = f"Unable to create directory '{directory}'" assert msg == error.message assert error.context == undefined_context assert testproj_path.exists() def test_raise_undefined_variable_project_dir(tmp_path): """Verify correct error raised when directory name cannot be rendered.""" with pytest.raises(exceptions.UndefinedVariableInTemplate) as err: generate.generate_files( repo_dir='tests/undefined-variable/dir-name/', output_dir=tmp_path, context={}, ) error = err.value msg = "Unable to create project directory '{{cookiecutter.project_slug}}'" assert msg == error.message assert error.context == {} assert not Path(tmp_path, 'testproject').exists() cookiecutter-2.6.0/tests/test_generate_hooks.py000066400000000000000000000210311456543333500217770ustar00rootroot00000000000000"""Test work of python and shell hooks for generated projects.""" import errno import os import sys from pathlib import Path import pytest from cookiecutter import generate, utils from cookiecutter.exceptions import FailedHookException WINDOWS = sys.platform.startswith('win') @pytest.fixture(scope='function') def remove_additional_folders(tmp_path): """Remove some special folders which are created by the tests.""" yield directories_to_delete = [ 'tests/test-pyhooks/inputpyhooks', 'inputpyhooks', 'inputhooks', tmp_path.joinpath('test-shellhooks'), 'tests/test-hooks', ] for directory in directories_to_delete: if os.path.exists(directory): utils.rmtree(directory) @pytest.mark.usefixtures('clean_system', 'remove_additional_folders') def test_ignore_hooks_dirs(): """Verify hooks directory not created in target location on files generation.""" generate.generate_files( context={'cookiecutter': {'pyhooks': 'pyhooks'}}, repo_dir='tests/test-pyhooks/', output_dir='tests/test-pyhooks/', ) assert not os.path.exists('tests/test-pyhooks/inputpyhooks/hooks') @pytest.mark.usefixtures('clean_system', 'remove_additional_folders') def test_run_python_hooks(): """Verify pre and post generation python hooks executed and result in output_dir. Each hook should create in target directory. Test verifies that these files created. """ generate.generate_files( context={'cookiecutter': {'pyhooks': 'pyhooks'}}, repo_dir='tests/test-pyhooks/', output_dir='tests/test-pyhooks/', ) assert os.path.exists('tests/test-pyhooks/inputpyhooks/python_pre.txt') assert os.path.exists('tests/test-pyhooks/inputpyhooks/python_post.txt') @pytest.mark.usefixtures('clean_system', 'remove_additional_folders') def test_run_python_hooks_cwd(): """Verify pre and post generation python hooks executed and result in current dir. Each hook should create in target directory. Test verifies that these files created. """ generate.generate_files( context={'cookiecutter': {'pyhooks': 'pyhooks'}}, repo_dir='tests/test-pyhooks/' ) assert os.path.exists('inputpyhooks/python_pre.txt') assert os.path.exists('inputpyhooks/python_post.txt') @pytest.mark.skipif(WINDOWS, reason='OSError.errno=8 is not thrown on Windows') @pytest.mark.usefixtures('clean_system', 'remove_additional_folders') def test_empty_hooks(): """Verify error is raised on empty hook script. Ignored on windows. OSError.errno=8 is not thrown on Windows when the script is empty because it always runs through shell instead of needing a shebang. """ with pytest.raises(FailedHookException) as excinfo: generate.generate_files( context={'cookiecutter': {'shellhooks': 'shellhooks'}}, repo_dir='tests/test-shellhooks-empty/', overwrite_if_exists=True, ) assert 'shebang' in str(excinfo.value) @pytest.mark.usefixtures('clean_system', 'remove_additional_folders') def test_oserror_hooks(mocker): """Verify script error passed correctly to cookiecutter error. Here subprocess.Popen function mocked, ie we do not call hook script, just produce expected error. """ message = 'Out of memory' err = OSError(message) err.errno = errno.ENOMEM prompt = mocker.patch('subprocess.Popen') prompt.side_effect = err with pytest.raises(FailedHookException) as excinfo: generate.generate_files( context={'cookiecutter': {'shellhooks': 'shellhooks'}}, repo_dir='tests/test-shellhooks-empty/', overwrite_if_exists=True, ) assert message in str(excinfo.value) @pytest.mark.usefixtures('clean_system', 'remove_additional_folders') def test_run_failing_hook_removes_output_directory(): """Verify project directory not created or removed if hook failed.""" repo_path = os.path.abspath('tests/test-hooks/') hooks_path = os.path.abspath('tests/test-hooks/hooks') hook_dir = os.path.join(repo_path, 'hooks') template = os.path.join(repo_path, 'input{{cookiecutter.hooks}}') os.mkdir(repo_path) os.mkdir(hook_dir) os.mkdir(template) hook_path = os.path.join(hooks_path, 'pre_gen_project.py') with Path(hook_path).open('w') as f: f.write("#!/usr/bin/env python\n") f.write("import sys; sys.exit(1)\n") with pytest.raises(FailedHookException) as excinfo: generate.generate_files( context={'cookiecutter': {'hooks': 'hooks'}}, repo_dir='tests/test-hooks/', overwrite_if_exists=True, ) assert 'Hook script failed' in str(excinfo.value) assert not os.path.exists('inputhooks') @pytest.mark.usefixtures('clean_system', 'remove_additional_folders') def test_run_failing_hook_preserves_existing_output_directory(): """Verify project directory not removed if exist before hook failed.""" repo_path = os.path.abspath('tests/test-hooks/') hooks_path = os.path.abspath('tests/test-hooks/hooks') hook_dir = os.path.join(repo_path, 'hooks') template = os.path.join(repo_path, 'input{{cookiecutter.hooks}}') os.mkdir(repo_path) os.mkdir(hook_dir) os.mkdir(template) hook_path = os.path.join(hooks_path, 'pre_gen_project.py') with Path(hook_path).open('w') as f: f.write("#!/usr/bin/env python\n") f.write("import sys; sys.exit(1)\n") os.mkdir('inputhooks') with pytest.raises(FailedHookException) as excinfo: generate.generate_files( context={'cookiecutter': {'hooks': 'hooks'}}, repo_dir='tests/test-hooks/', overwrite_if_exists=True, ) assert 'Hook script failed' in str(excinfo.value) assert os.path.exists('inputhooks') @pytest.mark.skipif(sys.platform.startswith('win'), reason="Linux only test") @pytest.mark.usefixtures('clean_system', 'remove_additional_folders') def test_run_shell_hooks(tmp_path): """Verify pre and post generate project shell hooks executed. This test for .sh files. """ generate.generate_files( context={'cookiecutter': {'shellhooks': 'shellhooks'}}, repo_dir='tests/test-shellhooks/', output_dir=tmp_path.joinpath('test-shellhooks'), ) shell_pre_file = tmp_path.joinpath( 'test-shellhooks', 'inputshellhooks', 'shell_pre.txt' ) shell_post_file = tmp_path.joinpath( 'test-shellhooks', 'inputshellhooks', 'shell_post.txt' ) assert shell_pre_file.exists() assert shell_post_file.exists() @pytest.mark.skipif(not sys.platform.startswith('win'), reason="Win only test") @pytest.mark.usefixtures('clean_system', 'remove_additional_folders') def test_run_shell_hooks_win(tmp_path): """Verify pre and post generate project shell hooks executed. This test for .bat files. """ generate.generate_files( context={'cookiecutter': {'shellhooks': 'shellhooks'}}, repo_dir='tests/test-shellhooks-win/', output_dir=tmp_path.joinpath('test-shellhooks-win'), ) shell_pre_file = tmp_path.joinpath( 'test-shellhooks-win', 'inputshellhooks', 'shell_pre.txt' ) shell_post_file = tmp_path.joinpath( 'test-shellhooks-win', 'inputshellhooks', 'shell_post.txt' ) assert shell_pre_file.exists() assert shell_post_file.exists() @pytest.mark.usefixtures("clean_system", "remove_additional_folders") def test_ignore_shell_hooks(tmp_path): """Verify *.txt files not created, when accept_hooks=False.""" generate.generate_files( context={"cookiecutter": {"shellhooks": "shellhooks"}}, repo_dir="tests/test-shellhooks/", output_dir=tmp_path.joinpath('test-shellhooks'), accept_hooks=False, ) shell_pre_file = tmp_path.joinpath("test-shellhooks/inputshellhooks/shell_pre.txt") shell_post_file = tmp_path.joinpath( "test-shellhooks/inputshellhooks/shell_post.txt" ) assert not shell_pre_file.exists() assert not shell_post_file.exists() @pytest.mark.usefixtures("clean_system", "remove_additional_folders") def test_deprecate_run_hook_from_repo_dir(tmp_path): """Test deprecation warning in generate._run_hook_from_repo_dir.""" repo_dir = "tests/test-shellhooks/" project_dir = Path(tmp_path.joinpath('test-shellhooks')) project_dir.mkdir() with pytest.deprecated_call(): generate._run_hook_from_repo_dir( repo_dir=repo_dir, hook_name="pre_gen_project", project_dir=project_dir, context={}, delete_project_on_failure=False, ) cookiecutter-2.6.0/tests/test_get_config.py000066400000000000000000000126631456543333500211210ustar00rootroot00000000000000"""Collection of tests around loading cookiecutter config.""" from pathlib import Path import pytest import yaml from cookiecutter import config from cookiecutter.exceptions import ConfigDoesNotExistException, InvalidConfiguration def test_merge_configs(): """Verify default and user config merged in expected way.""" default = { 'cookiecutters_dir': '/home/example/some-path-to-templates', 'replay_dir': '/home/example/some-path-to-replay-files', 'default_context': {}, 'abbreviations': { 'gh': 'https://github.com/{0}.git', 'gl': 'https://gitlab.com/{0}.git', 'bb': 'https://bitbucket.org/{0}', }, } user_config = { 'default_context': { 'full_name': 'Raphael Pierzina', 'github_username': 'hackebrot', }, 'abbreviations': { 'gl': 'https://gitlab.com/hackebrot/{0}.git', 'pytest-plugin': 'https://github.com/pytest-dev/pytest-plugin.git', }, } expected_config = { 'cookiecutters_dir': '/home/example/some-path-to-templates', 'replay_dir': '/home/example/some-path-to-replay-files', 'default_context': { 'full_name': 'Raphael Pierzina', 'github_username': 'hackebrot', }, 'abbreviations': { 'gh': 'https://github.com/{0}.git', 'gl': 'https://gitlab.com/hackebrot/{0}.git', 'bb': 'https://bitbucket.org/{0}', 'pytest-plugin': 'https://github.com/pytest-dev/pytest-plugin.git', }, } assert config.merge_configs(default, user_config) == expected_config def test_get_config(): """Verify valid config opened and rendered correctly.""" conf = config.get_config('tests/test-config/valid-config.yaml') expected_conf = { 'cookiecutters_dir': '/home/example/some-path-to-templates', 'replay_dir': '/home/example/some-path-to-replay-files', 'default_context': { 'full_name': 'Firstname Lastname', 'email': 'firstname.lastname@gmail.com', 'github_username': 'example', 'project': { 'description': 'description', 'tags': [ 'first', 'second', 'third', ], }, }, 'abbreviations': { 'gh': 'https://github.com/{0}.git', 'gl': 'https://gitlab.com/{0}.git', 'bb': 'https://bitbucket.org/{0}', 'helloworld': 'https://github.com/hackebrot/helloworld', }, } assert conf == expected_conf def test_get_config_does_not_exist(): """Check that `exceptions.ConfigDoesNotExistException` is raised when \ attempting to get a non-existent config file.""" expected_error_msg = 'Config file tests/not-exist.yaml does not exist.' with pytest.raises(ConfigDoesNotExistException) as exc_info: config.get_config('tests/not-exist.yaml') assert str(exc_info.value) == expected_error_msg def test_invalid_config(): """An invalid config file should raise an `InvalidConfiguration` \ exception.""" expected_error_msg = ( 'Unable to parse YAML file tests/test-config/invalid-config.yaml.' ) with pytest.raises(InvalidConfiguration) as exc_info: config.get_config('tests/test-config/invalid-config.yaml') assert expected_error_msg in str(exc_info.value) assert isinstance(exc_info.value.__cause__, yaml.YAMLError) def test_get_config_with_defaults(): """A config file that overrides 1 of 3 defaults.""" conf = config.get_config('tests/test-config/valid-partial-config.yaml') default_cookiecutters_dir = Path('~/.cookiecutters').expanduser() default_replay_dir = Path('~/.cookiecutter_replay').expanduser() expected_conf = { 'cookiecutters_dir': str(default_cookiecutters_dir), 'replay_dir': str(default_replay_dir), 'default_context': { 'full_name': 'Firstname Lastname', 'email': 'firstname.lastname@gmail.com', 'github_username': 'example', }, 'abbreviations': { 'gh': 'https://github.com/{0}.git', 'gl': 'https://gitlab.com/{0}.git', 'bb': 'https://bitbucket.org/{0}', }, } assert conf == expected_conf def test_get_config_empty_config_file(): """An empty config file results in the default config.""" conf = config.get_config('tests/test-config/empty-config.yaml') assert conf == config.DEFAULT_CONFIG def test_get_config_invalid_file_with_array_as_top_level_element(): """An exception should be raised if top-level element is array.""" expected_error_msg = ( 'Top-level element of YAML file ' 'tests/test-config/invalid-config-w-array.yaml should be an object.' ) with pytest.raises(InvalidConfiguration) as exc_info: config.get_config('tests/test-config/invalid-config-w-array.yaml') assert expected_error_msg in str(exc_info.value) def test_get_config_invalid_file_with_multiple_docs(): """An exception should be raised if config file contains multiple docs.""" expected_error_msg = ( 'Unable to parse YAML file ' 'tests/test-config/invalid-config-w-multiple-docs.yaml.' ) with pytest.raises(InvalidConfiguration) as exc_info: config.get_config('tests/test-config/invalid-config-w-multiple-docs.yaml') assert expected_error_msg in str(exc_info.value) cookiecutter-2.6.0/tests/test_get_user_config.py000066400000000000000000000132231456543333500221500ustar00rootroot00000000000000"""Tests to verify correct work with user configs and system/user variables inside.""" import os import shutil import pytest from cookiecutter import config from cookiecutter.exceptions import InvalidConfiguration @pytest.fixture(scope='module') def user_config_path(): """Fixture. Return user config path for current user.""" return os.path.expanduser('~/.cookiecutterrc') @pytest.fixture(scope='function') def back_up_rc(user_config_path): """ Back up an existing cookiecutter rc and restore it after the test. If ~/.cookiecutterrc is pre-existing, move it to a temp location """ user_config_path_backup = os.path.expanduser('~/.cookiecutterrc.backup') if os.path.exists(user_config_path): shutil.copy(user_config_path, user_config_path_backup) os.remove(user_config_path) yield # Remove the ~/.cookiecutterrc that has been created in the test. if os.path.exists(user_config_path): os.remove(user_config_path) # If it existed, restore the original ~/.cookiecutterrc. if os.path.exists(user_config_path_backup): shutil.copy(user_config_path_backup, user_config_path) os.remove(user_config_path_backup) @pytest.fixture def custom_config(): """Fixture. Return expected custom configuration for future tests validation.""" return { 'default_context': { 'full_name': 'Firstname Lastname', 'email': 'firstname.lastname@gmail.com', 'github_username': 'example', 'project': { 'description': 'description', 'tags': [ 'first', 'second', 'third', ], }, }, 'cookiecutters_dir': '/home/example/some-path-to-templates', 'replay_dir': '/home/example/some-path-to-replay-files', 'abbreviations': { 'gh': 'https://github.com/{0}.git', 'gl': 'https://gitlab.com/{0}.git', 'bb': 'https://bitbucket.org/{0}', 'helloworld': 'https://github.com/hackebrot/helloworld', }, } @pytest.mark.usefixtures('back_up_rc') def test_get_user_config_valid(user_config_path, custom_config): """Validate user config correctly parsed if exist and correctly formatted.""" shutil.copy('tests/test-config/valid-config.yaml', user_config_path) conf = config.get_user_config() assert conf == custom_config @pytest.mark.usefixtures('back_up_rc') def test_get_user_config_invalid(user_config_path): """Validate `InvalidConfiguration` raised when provided user config malformed.""" shutil.copy('tests/test-config/invalid-config.yaml', user_config_path) with pytest.raises(InvalidConfiguration): config.get_user_config() @pytest.mark.usefixtures('back_up_rc') def test_get_user_config_nonexistent(): """Validate default app config returned, if user does not have own config.""" assert config.get_user_config() == config.DEFAULT_CONFIG @pytest.fixture def custom_config_path(): """Fixture. Return path to custom user config for tests.""" return 'tests/test-config/valid-config.yaml' def test_specify_config_path(mocker, custom_config_path, custom_config): """Validate provided custom config path should be respected and parsed.""" spy_get_config = mocker.spy(config, 'get_config') user_config = config.get_user_config(custom_config_path) spy_get_config.assert_called_once_with(custom_config_path) assert user_config == custom_config def test_default_config_path(user_config_path): """Validate app configuration. User config path should match default path.""" assert config.USER_CONFIG_PATH == user_config_path def test_default_config_from_env_variable( monkeypatch, custom_config_path, custom_config ): """Validate app configuration. User config path should be parsed from sys env.""" monkeypatch.setenv('COOKIECUTTER_CONFIG', custom_config_path) user_config = config.get_user_config() assert user_config == custom_config def test_force_default_config(mocker, custom_config_path): """Validate `default_config=True` should ignore provided custom user config.""" spy_get_config = mocker.spy(config, 'get_config') user_config = config.get_user_config(custom_config_path, default_config=True) assert user_config == config.DEFAULT_CONFIG assert not spy_get_config.called def test_expand_user_for_directories_in_config(monkeypatch): """Validate user pointers expanded in user configs.""" def _expanduser(path): return path.replace('~', 'Users/bob') monkeypatch.setattr('os.path.expanduser', _expanduser) config_file = 'tests/test-config/config-expand-user.yaml' user_config = config.get_user_config(config_file) assert user_config['replay_dir'] == 'Users/bob/replay-files' assert user_config['cookiecutters_dir'] == 'Users/bob/templates' def test_expand_vars_for_directories_in_config(monkeypatch): """Validate environment variables expanded in user configs.""" monkeypatch.setenv('COOKIES', 'Users/bob/cookies') config_file = 'tests/test-config/config-expand-vars.yaml' user_config = config.get_user_config(config_file) assert user_config['replay_dir'] == 'Users/bob/cookies/replay-files' assert user_config['cookiecutters_dir'] == 'Users/bob/cookies/templates' def test_specify_config_values(): """Validate provided custom config values should be respected.""" replay_dir = 'Users/bob/cookies/custom-replay-dir' custom_config_updated = {**config.DEFAULT_CONFIG, 'replay_dir': replay_dir} user_config = config.get_user_config(default_config={'replay_dir': replay_dir}) assert user_config == custom_config_updated cookiecutter-2.6.0/tests/test_hooks.py000066400000000000000000000244071456543333500201370ustar00rootroot00000000000000"""Tests for `cookiecutter.hooks` module.""" import errno import os import stat import sys import textwrap from pathlib import Path import pytest from cookiecutter import exceptions, hooks, utils def make_test_repo(name, multiple_hooks=False): """Create test repository for test setup methods.""" hook_dir = os.path.join(name, 'hooks') template = os.path.join(name, 'input{{hooks}}') os.mkdir(name) os.mkdir(hook_dir) os.mkdir(template) Path(template, 'README.rst').write_text("foo\n===\n\nbar\n") with Path(hook_dir, 'pre_gen_project.py').open('w') as f: f.write("#!/usr/bin/env python\n") f.write("# -*- coding: utf-8 -*-\n") f.write("from __future__ import print_function\n") f.write("\n") f.write("print('pre generation hook')\n") f.write("f = open('python_pre.txt', 'w')\n") f.write("f.close()\n") if sys.platform.startswith('win'): post = 'post_gen_project.bat' with Path(hook_dir, post).open('w') as f: f.write("@echo off\n") f.write("\n") f.write("echo post generation hook\n") f.write("echo. >shell_post.txt\n") else: post = 'post_gen_project.sh' filename = os.path.join(hook_dir, post) with Path(filename).open('w') as f: f.write("#!/bin/bash\n") f.write("\n") f.write("echo 'post generation hook';\n") f.write("touch 'shell_post.txt'\n") # Set the execute bit os.chmod(filename, os.stat(filename).st_mode | stat.S_IXUSR) # Adding an additional pre script if multiple_hooks: if sys.platform.startswith('win'): pre = 'pre_gen_project.bat' with Path(hook_dir, pre).open('w') as f: f.write("@echo off\n") f.write("\n") f.write("echo post generation hook\n") f.write("echo. >shell_pre.txt\n") else: pre = 'pre_gen_project.sh' filename = os.path.join(hook_dir, pre) with Path(filename).open('w') as f: f.write("#!/bin/bash\n") f.write("\n") f.write("echo 'post generation hook';\n") f.write("touch 'shell_pre.txt'\n") # Set the execute bit os.chmod(filename, os.stat(filename).st_mode | stat.S_IXUSR) return post class TestFindHooks: """Class to unite find hooks related tests in one place.""" repo_path = 'tests/test-hooks' def setup_method(self, method): """Find hooks related tests setup fixture.""" self.post_hook = make_test_repo(self.repo_path) def teardown_method(self, method): """Find hooks related tests teardown fixture.""" utils.rmtree(self.repo_path) def test_find_hook(self): """Finds the specified hook.""" with utils.work_in(self.repo_path): expected_pre = os.path.abspath('hooks/pre_gen_project.py') actual_hook_path = hooks.find_hook('pre_gen_project') assert expected_pre == actual_hook_path[0] expected_post = os.path.abspath(f'hooks/{self.post_hook}') actual_hook_path = hooks.find_hook('post_gen_project') assert expected_post == actual_hook_path[0] def test_no_hooks(self): """`find_hooks` should return None if the hook could not be found.""" with utils.work_in('tests/fake-repo'): assert None is hooks.find_hook('pre_gen_project') def test_unknown_hooks_dir(self): """`find_hooks` should return None if hook directory not found.""" with utils.work_in(self.repo_path): assert hooks.find_hook('pre_gen_project', hooks_dir='hooks_dir') is None def test_hook_not_found(self): """`find_hooks` should return None if the hook could not be found.""" with utils.work_in(self.repo_path): assert hooks.find_hook('unknown_hook') is None class TestExternalHooks: """Class to unite tests for hooks with different project paths.""" repo_path = os.path.abspath('tests/test-hooks/') hooks_path = os.path.abspath('tests/test-hooks/hooks') def setup_method(self, method): """External hooks related tests setup fixture.""" self.post_hook = make_test_repo(self.repo_path, multiple_hooks=True) def teardown_method(self, method): """External hooks related tests teardown fixture.""" utils.rmtree(self.repo_path) if os.path.exists('python_pre.txt'): os.remove('python_pre.txt') if os.path.exists('shell_post.txt'): os.remove('shell_post.txt') if os.path.exists('shell_pre.txt'): os.remove('shell_pre.txt') if os.path.exists('tests/shell_post.txt'): os.remove('tests/shell_post.txt') if os.path.exists('tests/test-hooks/input{{hooks}}/python_pre.txt'): os.remove('tests/test-hooks/input{{hooks}}/python_pre.txt') if os.path.exists('tests/test-hooks/input{{hooks}}/shell_post.txt'): os.remove('tests/test-hooks/input{{hooks}}/shell_post.txt') if os.path.exists('tests/context_post.txt'): os.remove('tests/context_post.txt') def test_run_script(self): """Execute a hook script, independently of project generation.""" hooks.run_script(os.path.join(self.hooks_path, self.post_hook)) assert os.path.isfile('shell_post.txt') def test_run_failing_script(self, mocker): """Test correct exception raise if run_script fails.""" err = OSError() prompt = mocker.patch('subprocess.Popen') prompt.side_effect = err with pytest.raises(exceptions.FailedHookException) as excinfo: hooks.run_script(os.path.join(self.hooks_path, self.post_hook)) assert f'Hook script failed (error: {err})' in str(excinfo.value) def test_run_failing_script_enoexec(self, mocker): """Test correct exception raise if run_script fails.""" err = OSError() err.errno = errno.ENOEXEC prompt = mocker.patch('subprocess.Popen') prompt.side_effect = err with pytest.raises(exceptions.FailedHookException) as excinfo: hooks.run_script(os.path.join(self.hooks_path, self.post_hook)) assert 'Hook script failed, might be an empty file or missing a shebang' in str( excinfo.value ) def test_run_script_cwd(self): """Change directory before running hook.""" hooks.run_script(os.path.join(self.hooks_path, self.post_hook), 'tests') assert os.path.isfile('tests/shell_post.txt') assert 'tests' not in os.getcwd() def test_run_script_with_context(self): """Execute a hook script, passing a context.""" hook_path = os.path.join(self.hooks_path, 'post_gen_project.sh') if sys.platform.startswith('win'): post = 'post_gen_project.bat' with Path(self.hooks_path, post).open('w') as f: f.write("@echo off\n") f.write("\n") f.write("echo post generation hook\n") f.write("echo. >{{cookiecutter.file}}\n") else: with Path(hook_path).open('w') as fh: fh.write("#!/bin/bash\n") fh.write("\n") fh.write("echo 'post generation hook';\n") fh.write("touch 'shell_post.txt'\n") fh.write("touch '{{cookiecutter.file}}'\n") os.chmod(hook_path, os.stat(hook_path).st_mode | stat.S_IXUSR) hooks.run_script_with_context( os.path.join(self.hooks_path, self.post_hook), 'tests', {'cookiecutter': {'file': 'context_post.txt'}}, ) assert os.path.isfile('tests/context_post.txt') assert 'tests' not in os.getcwd() def test_run_hook(self): """Execute hook from specified template in specified output \ directory.""" tests_dir = os.path.join(self.repo_path, 'input{{hooks}}') with utils.work_in(self.repo_path): hooks.run_hook('pre_gen_project', tests_dir, {}) assert os.path.isfile(os.path.join(tests_dir, 'python_pre.txt')) assert os.path.isfile(os.path.join(tests_dir, 'shell_pre.txt')) hooks.run_hook('post_gen_project', tests_dir, {}) assert os.path.isfile(os.path.join(tests_dir, 'shell_post.txt')) def test_run_failing_hook(self): """Test correct exception raise if hook exit code is not zero.""" hook_path = os.path.join(self.hooks_path, 'pre_gen_project.py') tests_dir = os.path.join(self.repo_path, 'input{{hooks}}') with Path(hook_path).open('w') as f: f.write("#!/usr/bin/env python\n") f.write("import sys; sys.exit(1)\n") with utils.work_in(self.repo_path): with pytest.raises(exceptions.FailedHookException) as excinfo: hooks.run_hook('pre_gen_project', tests_dir, {}) assert 'Hook script failed' in str(excinfo.value) @pytest.fixture() def dir_with_hooks(tmp_path): """Yield a directory that contains hook backup files.""" hooks_dir = tmp_path.joinpath('hooks') hooks_dir.mkdir() pre_hook_content = textwrap.dedent( """ #!/usr/bin/env python # -*- coding: utf-8 -*- print('pre_gen_project.py~') """ ) pre_gen_hook_file = hooks_dir.joinpath('pre_gen_project.py~') pre_gen_hook_file.write_text(pre_hook_content, encoding='utf8') post_hook_content = textwrap.dedent( """ #!/usr/bin/env python # -*- coding: utf-8 -*- print('post_gen_project.py~') """ ) post_gen_hook_file = hooks_dir.joinpath('post_gen_project.py~') post_gen_hook_file.write_text(post_hook_content, encoding='utf8') # Make sure to yield the parent directory as `find_hooks()` # looks into `hooks/` in the current working directory yield str(tmp_path) pre_gen_hook_file.unlink() post_gen_hook_file.unlink() def test_ignore_hook_backup_files(monkeypatch, dir_with_hooks): """Test `find_hook` correctly use `valid_hook` verification function.""" # Change the current working directory that contains `hooks/` monkeypatch.chdir(dir_with_hooks) assert hooks.find_hook('pre_gen_project') is None assert hooks.find_hook('post_gen_project') is None cookiecutter-2.6.0/tests/test_log.py000066400000000000000000000075661456543333500176040ustar00rootroot00000000000000"""Collection of tests around log handling.""" import logging import pytest from cookiecutter.log import configure_logger def create_log_records(): """Test function, create log entries in expected stage of test.""" cookiecutter_logger = logging.getLogger('cookiecutter') foo_logger = logging.getLogger('cookiecutter.foo') foobar_logger = logging.getLogger('cookiecutter.foo.bar') cookiecutter_logger.info('Welcome to Cookiecutter') cookiecutter_logger.debug('Generating project from pytest-plugin') foo_logger.info('Loading user config from home dir') foobar_logger.debug("I don't know.") foobar_logger.debug('I wanted to save the world.') foo_logger.error('Aw, snap! Something went wrong') cookiecutter_logger.debug('Successfully generated project') @pytest.fixture def info_messages(): """Fixture. List of test info messages.""" return [ 'INFO: Welcome to Cookiecutter', 'INFO: Loading user config from home dir', 'ERROR: Aw, snap! Something went wrong', ] @pytest.fixture def debug_messages(): """Fixture. List of test debug messages.""" return [ "INFO cookiecutter: Welcome to Cookiecutter", "DEBUG cookiecutter: Generating project from pytest-plugin", "INFO cookiecutter.foo: Loading user config from home dir", "DEBUG cookiecutter.foo.bar: I don't know.", "DEBUG cookiecutter.foo.bar: I wanted to save the world.", "ERROR cookiecutter.foo: Aw, snap! Something went wrong", "DEBUG cookiecutter: Successfully generated project", ] @pytest.fixture def info_logger(): """Fixture. Call cookiecutter logger setup with `info` debug level.""" return configure_logger(stream_level='INFO') @pytest.fixture def debug_logger(): """Fixture. Call cookiecutter logger setup with `debug` debug level.""" return configure_logger(stream_level='DEBUG') @pytest.fixture def debug_file(tmp_path): """Fixture. Generate debug file location for tests.""" return tmp_path.joinpath('pytest-plugin.log') @pytest.fixture def info_logger_with_file(debug_file): """Fixture. Call cookiecutter logger setup with `info` debug level + `file`.""" return configure_logger(stream_level='INFO', debug_file=str(debug_file)) def test_info_stdout_logging(caplog, info_logger, info_messages): """Test that stdout logs use info format and level.""" [stream_handler] = info_logger.handlers assert isinstance(stream_handler, logging.StreamHandler) assert stream_handler.level == logging.INFO create_log_records() stream_messages = [ stream_handler.format(r) for r in caplog.records if r.levelno >= stream_handler.level ] assert stream_messages == info_messages def test_debug_stdout_logging(caplog, debug_logger, debug_messages): """Test that stdout logs use debug format and level.""" [stream_handler] = debug_logger.handlers assert isinstance(stream_handler, logging.StreamHandler) assert stream_handler.level == logging.DEBUG create_log_records() stream_messages = [ stream_handler.format(r) for r in caplog.records if r.levelno >= stream_handler.level ] assert stream_messages == debug_messages def test_debug_file_logging(caplog, info_logger_with_file, debug_file, debug_messages): """Test that logging to stdout uses a different format and level than \ the the file handler.""" [file_handler, stream_handler] = info_logger_with_file.handlers assert isinstance(file_handler, logging.FileHandler) assert isinstance(stream_handler, logging.StreamHandler) assert stream_handler.level == logging.INFO assert file_handler.level == logging.DEBUG create_log_records() assert debug_file.exists() # Last line in the log file is an empty line with debug_file.open() as f: assert f.read().split('\n') == debug_messages + [''] cookiecutter-2.6.0/tests/test_main.py000066400000000000000000000065261456543333500177420ustar00rootroot00000000000000"""Collection of tests around cookiecutter's replay feature.""" from cookiecutter.main import cookiecutter def test_original_cookiecutter_options_preserved_in__cookiecutter( monkeypatch, mocker, user_config_file, ): """Preserve original context options. Tests you can access the original context options via `context['_cookiecutter']`. """ monkeypatch.chdir('tests/fake-repo-tmpl-_cookiecutter') mock_generate_files = mocker.patch('cookiecutter.main.generate_files') cookiecutter( '.', no_input=True, replay=False, config_file=user_config_file, ) assert mock_generate_files.call_args[1]['context']['_cookiecutter'][ 'test_list' ] == [1, 2, 3, 4] assert mock_generate_files.call_args[1]['context']['_cookiecutter'][ 'test_dict' ] == {"foo": "bar"} def test_replay_dump_template_name( monkeypatch, mocker, user_config_data, user_config_file ): """Check that replay_dump is called with a valid template_name. Template name must not be a relative path. Otherwise files such as ``..json`` are created, which are not just cryptic but also later mistaken for replay files of other templates if invoked with '.' and '--replay'. Change the current working directory temporarily to 'tests/fake-repo-tmpl' for this test and call cookiecutter with '.' for the target template. """ monkeypatch.chdir('tests/fake-repo-tmpl') mock_replay_dump = mocker.patch('cookiecutter.main.dump') mocker.patch('cookiecutter.main.generate_files') cookiecutter( '.', no_input=True, replay=False, config_file=user_config_file, ) mock_replay_dump.assert_called_once_with( user_config_data['replay_dir'], 'fake-repo-tmpl', mocker.ANY, ) def test_replay_load_template_name( monkeypatch, mocker, user_config_data, user_config_file ): """Check that replay_load is called correctly. Calls require valid template_name that is not a relative path. Change the current working directory temporarily to 'tests/fake-repo-tmpl' for this test and call cookiecutter with '.' for the target template. """ monkeypatch.chdir('tests/fake-repo-tmpl') mock_replay_load = mocker.patch('cookiecutter.main.load') mocker.patch('cookiecutter.main.generate_context').return_value = { 'cookiecutter': {} } mocker.patch('cookiecutter.main.generate_files') mocker.patch('cookiecutter.main.dump') cookiecutter( '.', replay=True, config_file=user_config_file, ) mock_replay_load.assert_called_once_with( user_config_data['replay_dir'], 'fake-repo-tmpl', ) def test_custom_replay_file(monkeypatch, mocker, user_config_file): """Check that reply.load is called with the custom replay_file.""" monkeypatch.chdir('tests/fake-repo-tmpl') mock_replay_load = mocker.patch('cookiecutter.main.load') mocker.patch('cookiecutter.main.generate_context').return_value = { 'cookiecutter': {} } mocker.patch('cookiecutter.main.generate_files') mocker.patch('cookiecutter.main.dump') cookiecutter( '.', replay='./custom-replay-file', config_file=user_config_file, ) mock_replay_load.assert_called_once_with( '.', 'custom-replay-file', ) cookiecutter-2.6.0/tests/test_output_folder.py000066400000000000000000000035171456543333500217060ustar00rootroot00000000000000""" tests_output_folder. Test formerly known from a unittest residing in test_generate.py named TestOutputFolder.test_output_folder """ import os from pathlib import Path import pytest from cookiecutter import exceptions, generate, utils @pytest.fixture(scope='function') def remove_output_folder(request): """Remove the output folder after test.""" yield if os.path.exists('output_folder'): utils.rmtree('output_folder') @pytest.mark.usefixtures('clean_system', 'remove_output_folder') def test_output_folder(): """Tests should correctly create content, as output_folder does not yet exist.""" context = generate.generate_context( context_file='tests/test-output-folder/cookiecutter.json' ) generate.generate_files(context=context, repo_dir='tests/test-output-folder') something = """Hi! My name is Audrey Greenfeld. It is 2014. """ something2 = Path('output_folder/something.txt').read_text() assert something == something2 in_folder = "The color is green and the letter is D.\n" in_folder2 = Path('output_folder/folder/in_folder.txt').read_text() assert in_folder == in_folder2 assert os.path.isdir('output_folder/im_a.dir') assert os.path.isfile('output_folder/im_a.dir/im_a.file.py') @pytest.mark.usefixtures('clean_system', 'remove_output_folder') def test_exception_when_output_folder_exists(): """Tests should raise error as output folder created before `generate_files`.""" context = generate.generate_context( context_file='tests/test-output-folder/cookiecutter.json' ) output_folder = context['cookiecutter']['test_name'] if not os.path.exists(output_folder): os.makedirs(output_folder) with pytest.raises(exceptions.OutputDirExistsException): generate.generate_files(context=context, repo_dir='tests/test-output-folder') cookiecutter-2.6.0/tests/test_pre_prompt_hooks.py000066400000000000000000000032711456543333500224020ustar00rootroot00000000000000"""Test work of python and shell hooks on repository.""" import sys from pathlib import Path import pytest from cookiecutter import hooks, utils from cookiecutter.exceptions import FailedHookException WINDOWS = sys.platform.startswith('win') @pytest.fixture(scope='function') def remove_tmp_repo_dir(): """Remove the generate repo_dir.""" def _func(repo_dir: Path): if repo_dir.exists(): utils.rmtree(repo_dir) return _func def test_run_pre_prompt_python_hook(remove_tmp_repo_dir): """Verify pre_prompt.py runs and creates a copy of cookiecutter.json.""" new_repo_dir = hooks.run_pre_prompt_hook(repo_dir='tests/test-pyhooks/') assert new_repo_dir.exists() bkp_config = new_repo_dir / "_cookiecutter.json" assert bkp_config.exists() remove_tmp_repo_dir(new_repo_dir) def test_run_pre_prompt_python_hook_fail(monkeypatch): """Verify pre_prompt.py will fail when a given env var is present.""" message = 'Pre-Prompt Hook script failed' with monkeypatch.context() as m: m.setenv('COOKIECUTTER_FAIL_PRE_PROMPT', '1') with pytest.raises(FailedHookException) as excinfo: hooks.run_pre_prompt_hook(repo_dir='tests/test-pyhooks/') assert message in str(excinfo.value) @pytest.mark.skipif(WINDOWS, reason='shell script will not run in Windows') def test_run_pre_prompt_shell_hook(remove_tmp_repo_dir): """Verify pre_prompt.sh runs and creates a copy of cookiecutter.json.""" new_repo_dir = hooks.run_pre_prompt_hook(repo_dir='tests/test-pyshellhooks/') assert new_repo_dir.exists() bkp_config = new_repo_dir / "_cookiecutter.json" assert bkp_config.exists() remove_tmp_repo_dir(new_repo_dir) cookiecutter-2.6.0/tests/test_preferred_encoding.py000066400000000000000000000015201456543333500226270ustar00rootroot00000000000000"""Collection of tests around character encodings.""" import codecs import locale import sys import pytest PY3 = sys.version_info[0] == 3 @pytest.mark.skipif(not PY3, reason='Only necessary on Python3') def test_not_ascii(): """Make sure that the systems preferred encoding is not `ascii`. Otherwise `click` is raising a RuntimeError for Python3. For a detailed description of this very problem please consult the following gist: https://gist.github.com/hackebrot/937245251887197ef542 This test also checks that `tox.ini` explicitly copies the according system environment variables to the test environments. """ try: preferred_encoding = locale.getpreferredencoding() fs_enc = codecs.lookup(preferred_encoding).name except Exception: fs_enc = 'ascii' assert fs_enc != 'ascii' cookiecutter-2.6.0/tests/test_prompt.py000066400000000000000000000655461456543333500203460ustar00rootroot00000000000000"""Tests for `cookiecutter.prompt` module.""" import json import platform import sys from collections import OrderedDict from pathlib import Path import click import pytest from cookiecutter import environment, exceptions, prompt @pytest.fixture(autouse=True) def patch_readline_on_win(monkeypatch): """Fixture. Overwrite windows end of line to linux standard.""" if 'windows' in platform.platform().lower(): monkeypatch.setattr('sys.stdin.readline', lambda: '\n') class TestRenderVariable: """Class to unite simple and complex tests for render_variable function.""" @pytest.mark.parametrize( 'raw_var, rendered_var', [ (1, '1'), (True, True), ('foo', 'foo'), ('{{cookiecutter.project}}', 'foobar'), (None, None), ], ) def test_convert_to_str(self, mocker, raw_var, rendered_var): """Verify simple items correctly rendered to strings.""" env = environment.StrictEnvironment() from_string = mocker.patch( 'cookiecutter.utils.StrictEnvironment.from_string', wraps=env.from_string ) context = {'project': 'foobar'} result = prompt.render_variable(env, raw_var, context) assert result == rendered_var # Make sure that non None non str variables are converted beforehand if raw_var is not None and not isinstance(raw_var, bool): if not isinstance(raw_var, str): raw_var = str(raw_var) from_string.assert_called_once_with(raw_var) else: assert not from_string.called @pytest.mark.parametrize( 'raw_var, rendered_var', [ ({1: True, 'foo': False}, {'1': True, 'foo': False}), ( {'{{cookiecutter.project}}': ['foo', 1], 'bar': False}, {'foobar': ['foo', '1'], 'bar': False}, ), (['foo', '{{cookiecutter.project}}', None], ['foo', 'foobar', None]), ], ) def test_convert_to_str_complex_variables(self, raw_var, rendered_var): """Verify tree items correctly rendered.""" env = environment.StrictEnvironment() context = {'project': 'foobar'} result = prompt.render_variable(env, raw_var, context) assert result == rendered_var class TestPrompt: """Class to unite user prompt related tests.""" @pytest.mark.parametrize( 'context', [ {'cookiecutter': {'full_name': 'Your Name'}}, {'cookiecutter': {'full_name': 'Řekni či napiš své jméno'}}, ], ids=['ASCII default prompt/input', 'Unicode default prompt/input'], ) def test_prompt_for_config(self, monkeypatch, context): """Verify `prompt_for_config` call `read_user_variable` on text request.""" monkeypatch.setattr( 'cookiecutter.prompt.read_user_variable', lambda var, default, prompts, prefix: default, ) cookiecutter_dict = prompt.prompt_for_config(context) assert cookiecutter_dict == context['cookiecutter'] @pytest.mark.parametrize( 'context', [ { 'cookiecutter': { 'full_name': 'Your Name', 'check': ['yes', 'no'], 'nothing': 'ok', '__prompts__': { 'full_name': 'Name please', 'check': 'Checking', }, } }, ], ids=['ASCII default prompt/input'], ) def test_prompt_for_config_with_human_prompts(self, monkeypatch, context): """Verify call `read_user_variable` on request when human-readable prompts.""" monkeypatch.setattr( 'cookiecutter.prompt.read_user_variable', lambda var, default, prompts, prefix: default, ) monkeypatch.setattr( 'cookiecutter.prompt.read_user_yes_no', lambda var, default, prompts, prefix: default, ) monkeypatch.setattr( 'cookiecutter.prompt.read_user_choice', lambda var, default, prompts, prefix: default, ) cookiecutter_dict = prompt.prompt_for_config(context) assert cookiecutter_dict == context['cookiecutter'] @pytest.mark.parametrize( 'context', [ { 'cookiecutter': { 'full_name': 'Your Name', 'check': ['yes', 'no'], '__prompts__': { 'check': 'Checking', }, } }, { 'cookiecutter': { 'full_name': 'Your Name', 'check': ['yes', 'no'], '__prompts__': { 'full_name': 'Name please', 'check': {'__prompt__': 'Checking', 'yes': 'Yes', 'no': 'No'}, }, } }, { 'cookiecutter': { 'full_name': 'Your Name', 'check': ['yes', 'no'], '__prompts__': { 'full_name': 'Name please', 'check': {'no': 'No'}, }, } }, ], ) def test_prompt_for_config_with_human_choices(self, monkeypatch, context): """Test prompts when human-readable labels for user choices.""" runner = click.testing.CliRunner() with runner.isolation(input="\n\n\n"): cookiecutter_dict = prompt.prompt_for_config(context) assert dict(cookiecutter_dict) == {'full_name': 'Your Name', 'check': 'yes'} def test_prompt_for_config_dict(self, monkeypatch): """Verify `prompt_for_config` call `read_user_variable` on dict request.""" monkeypatch.setattr( 'cookiecutter.prompt.read_user_dict', lambda var, default, prompts, prefix: {"key": "value", "integer": 37}, ) context = {'cookiecutter': {'details': {}}} cookiecutter_dict = prompt.prompt_for_config(context) assert cookiecutter_dict == {'details': {'key': 'value', 'integer': 37}} def test_should_render_dict(self): """Verify template inside dictionary variable rendered.""" context = { 'cookiecutter': { 'project_name': 'Slartibartfast', 'details': { '{{cookiecutter.project_name}}': '{{cookiecutter.project_name}}' }, } } cookiecutter_dict = prompt.prompt_for_config(context, no_input=True) assert cookiecutter_dict == { 'project_name': 'Slartibartfast', 'details': {'Slartibartfast': 'Slartibartfast'}, } def test_should_render_deep_dict(self): """Verify nested structures like dict in dict, rendered correctly.""" context = { 'cookiecutter': { 'project_name': "Slartibartfast", 'details': { "key": "value", "integer_key": 37, "other_name": '{{cookiecutter.project_name}}', "dict_key": { "deep_key": "deep_value", "deep_integer": 42, "deep_other_name": '{{cookiecutter.project_name}}', "deep_list": [ "deep value 1", "{{cookiecutter.project_name}}", "deep value 3", ], }, "list_key": [ "value 1", "{{cookiecutter.project_name}}", "value 3", ], }, } } cookiecutter_dict = prompt.prompt_for_config(context, no_input=True) assert cookiecutter_dict == { 'project_name': "Slartibartfast", 'details': { "key": "value", "integer_key": "37", "other_name": "Slartibartfast", "dict_key": { "deep_key": "deep_value", "deep_integer": "42", "deep_other_name": "Slartibartfast", "deep_list": ["deep value 1", "Slartibartfast", "deep value 3"], }, "list_key": ["value 1", "Slartibartfast", "value 3"], }, } def test_should_render_deep_dict_with_human_prompts(self): """Verify dict rendered correctly when human-readable prompts.""" context = { 'cookiecutter': { 'project_name': "Slartibartfast", 'details': { "key": "value", "integer_key": 37, "other_name": '{{cookiecutter.project_name}}', "dict_key": { "deep_key": "deep_value", }, }, '__prompts__': {'project_name': 'Project name'}, } } cookiecutter_dict = prompt.prompt_for_config(context, no_input=True) assert cookiecutter_dict == { 'project_name': "Slartibartfast", 'details': { "key": "value", "integer_key": "37", "other_name": "Slartibartfast", "dict_key": { "deep_key": "deep_value", }, }, } def test_internal_use_no_human_prompts(self): """Verify dict rendered correctly when human-readable prompts empty.""" context = { 'cookiecutter': { 'project_name': "Slartibartfast", '__prompts__': {}, } } cookiecutter_dict = prompt.prompt_for_config(context, no_input=True) assert cookiecutter_dict == { 'project_name': "Slartibartfast", } def test_prompt_for_templated_config(self, monkeypatch): """Verify Jinja2 templating works in unicode prompts.""" monkeypatch.setattr( 'cookiecutter.prompt.read_user_variable', lambda var, default, prompts, prefix: default, ) context = { 'cookiecutter': OrderedDict( [ ('project_name', 'A New Project'), ( 'pkg_name', '{{ cookiecutter.project_name|lower|replace(" ", "") }}', ), ] ) } exp_cookiecutter_dict = { 'project_name': 'A New Project', 'pkg_name': 'anewproject', } cookiecutter_dict = prompt.prompt_for_config(context) assert cookiecutter_dict == exp_cookiecutter_dict def test_dont_prompt_for_private_context_var(self, monkeypatch): """Verify `read_user_variable` not called for private context variables.""" monkeypatch.setattr( 'cookiecutter.prompt.read_user_variable', lambda var, default: pytest.fail( 'Should not try to read a response for private context var' ), ) context = {'cookiecutter': {'_copy_without_render': ['*.html']}} cookiecutter_dict = prompt.prompt_for_config(context) assert cookiecutter_dict == {'_copy_without_render': ['*.html']} def test_should_render_private_variables_with_two_underscores(self): """Test rendering of private variables with two underscores. There are three cases: 1. Variables beginning with a single underscore are private and not rendered. 2. Variables beginning with a double underscore are private and are rendered. 3. Variables beginning with anything other than underscores are not private and are rendered. """ context = { 'cookiecutter': OrderedDict( [ ('foo', 'Hello world'), ('bar', 123), ('rendered_foo', '{{ cookiecutter.foo|lower }}'), ('rendered_bar', 123), ('_hidden_foo', '{{ cookiecutter.foo|lower }}'), ('_hidden_bar', 123), ('__rendered_hidden_foo', '{{ cookiecutter.foo|lower }}'), ('__rendered_hidden_bar', 123), ] ) } cookiecutter_dict = prompt.prompt_for_config(context, no_input=True) assert cookiecutter_dict == OrderedDict( [ ('foo', 'Hello world'), ('bar', '123'), ('rendered_foo', 'hello world'), ('rendered_bar', '123'), ('_hidden_foo', '{{ cookiecutter.foo|lower }}'), ('_hidden_bar', 123), ('__rendered_hidden_foo', 'hello world'), ('__rendered_hidden_bar', '123'), ] ) def test_should_not_render_private_variables(self): """Verify private(underscored) variables not rendered by `prompt_for_config`. Private variables designed to be raw, same as context input. """ context = { 'cookiecutter': { 'project_name': 'Skip render', '_skip_jinja_template': '{{cookiecutter.project_name}}', '_skip_float': 123.25, '_skip_integer': 123, '_skip_boolean': True, '_skip_nested': True, } } cookiecutter_dict = prompt.prompt_for_config(context, no_input=True) assert cookiecutter_dict == context['cookiecutter'] DEFAULT_PREFIX = ' [dim][1/1][/] ' class TestReadUserChoice: """Class to unite choices prompt related tests.""" def test_should_invoke_read_user_choice(self, mocker): """Verify correct function called for select(list) variables.""" prompt_choice = mocker.patch( 'cookiecutter.prompt.prompt_choice_for_config', wraps=prompt.prompt_choice_for_config, ) read_user_choice = mocker.patch('cookiecutter.prompt.read_user_choice') read_user_choice.return_value = 'all' read_user_variable = mocker.patch('cookiecutter.prompt.read_user_variable') choices = ['landscape', 'portrait', 'all'] context = {'cookiecutter': {'orientation': choices}} cookiecutter_dict = prompt.prompt_for_config(context) assert not read_user_variable.called assert prompt_choice.called read_user_choice.assert_called_once_with( 'orientation', choices, {}, DEFAULT_PREFIX ) assert cookiecutter_dict == {'orientation': 'all'} def test_should_invoke_read_user_variable(self, mocker): """Verify correct function called for string input variables.""" read_user_variable = mocker.patch('cookiecutter.prompt.read_user_variable') read_user_variable.return_value = 'Audrey Roy' prompt_choice = mocker.patch('cookiecutter.prompt.prompt_choice_for_config') read_user_choice = mocker.patch('cookiecutter.prompt.read_user_choice') context = {'cookiecutter': {'full_name': 'Your Name'}} cookiecutter_dict = prompt.prompt_for_config(context) assert not prompt_choice.called assert not read_user_choice.called read_user_variable.assert_called_once_with( 'full_name', 'Your Name', {}, DEFAULT_PREFIX ) assert cookiecutter_dict == {'full_name': 'Audrey Roy'} def test_should_render_choices(self, mocker): """Verify Jinja2 templating engine works inside choices variables.""" read_user_choice = mocker.patch('cookiecutter.prompt.read_user_choice') read_user_choice.return_value = 'anewproject' read_user_variable = mocker.patch('cookiecutter.prompt.read_user_variable') read_user_variable.return_value = 'A New Project' rendered_choices = ['foo', 'anewproject', 'bar'] context = { 'cookiecutter': OrderedDict( [ ('project_name', 'A New Project'), ( 'pkg_name', [ 'foo', '{{ cookiecutter.project_name|lower|replace(" ", "") }}', 'bar', ], ), ] ) } expected = { 'project_name': 'A New Project', 'pkg_name': 'anewproject', } cookiecutter_dict = prompt.prompt_for_config(context) read_user_variable.assert_called_once_with( 'project_name', 'A New Project', {}, ' [dim][1/2][/] ' ) read_user_choice.assert_called_once_with( 'pkg_name', rendered_choices, {}, ' [dim][2/2][/] ' ) assert cookiecutter_dict == expected class TestPromptChoiceForConfig: """Class to unite choices prompt related tests with config test.""" @pytest.fixture def choices(self): """Fixture. Just populate choices variable.""" return ['landscape', 'portrait', 'all'] @pytest.fixture def context(self, choices): """Fixture. Just populate context variable.""" return {'cookiecutter': {'orientation': choices}} def test_should_return_first_option_if_no_input(self, mocker, choices, context): """Verify prompt_choice_for_config return first list option on no_input=True.""" read_user_choice = mocker.patch('cookiecutter.prompt.read_user_choice') expected_choice = choices[0] actual_choice = prompt.prompt_choice_for_config( cookiecutter_dict=context, env=environment.StrictEnvironment(), key='orientation', options=choices, no_input=True, # Suppress user input ) assert not read_user_choice.called assert expected_choice == actual_choice def test_should_read_user_choice(self, mocker, choices, context): """Verify prompt_choice_for_config return user selection on no_input=False.""" read_user_choice = mocker.patch('cookiecutter.prompt.read_user_choice') read_user_choice.return_value = 'all' expected_choice = 'all' actual_choice = prompt.prompt_choice_for_config( cookiecutter_dict=context, env=environment.StrictEnvironment(), key='orientation', options=choices, no_input=False, # Ask the user for input ) read_user_choice.assert_called_once_with('orientation', choices, None, '') assert expected_choice == actual_choice class TestReadUserYesNo(object): """Class to unite boolean prompt related tests.""" @pytest.mark.parametrize( 'run_as_docker', ( True, False, ), ) def test_should_invoke_read_user_yes_no(self, mocker, run_as_docker): """Verify correct function called for boolean variables.""" read_user_yes_no = mocker.patch('cookiecutter.prompt.read_user_yes_no') read_user_yes_no.return_value = run_as_docker read_user_variable = mocker.patch('cookiecutter.prompt.read_user_variable') context = {'cookiecutter': {'run_as_docker': run_as_docker}} cookiecutter_dict = prompt.prompt_for_config(context) assert not read_user_variable.called read_user_yes_no.assert_called_once_with( 'run_as_docker', run_as_docker, {}, DEFAULT_PREFIX ) assert cookiecutter_dict == {'run_as_docker': run_as_docker} def test_boolean_parameter_no_input(self): """Verify boolean parameter sent to prompt for config with no input.""" context = { 'cookiecutter': { 'run_as_docker': True, } } cookiecutter_dict = prompt.prompt_for_config(context, no_input=True) assert cookiecutter_dict == context['cookiecutter'] @pytest.mark.parametrize( 'context', ( {'cookiecutter': {'foo': '{{cookiecutter.nope}}'}}, {'cookiecutter': {'foo': ['123', '{{cookiecutter.nope}}', '456']}}, {'cookiecutter': {'foo': {'{{cookiecutter.nope}}': 'value'}}}, {'cookiecutter': {'foo': {'key': '{{cookiecutter.nope}}'}}}, ), ids=[ 'Undefined variable in cookiecutter dict', 'Undefined variable in cookiecutter dict with choices', 'Undefined variable in cookiecutter dict with dict_key', 'Undefined variable in cookiecutter dict with key_value', ], ) def test_undefined_variable(context): """Verify `prompt.prompt_for_config` raises correct error.""" with pytest.raises(exceptions.UndefinedVariableInTemplate) as err: prompt.prompt_for_config(context, no_input=True) error = err.value assert error.message == "Unable to render variable 'foo'" assert error.context == context @pytest.mark.parametrize( "template_dir,expected", [ ["fake-nested-templates", "fake-project"], ["fake-nested-templates-old-style", "fake-package"], ], ) def test_cookiecutter_nested_templates(template_dir: str, expected: str): """Test nested_templates generation.""" from cookiecutter import prompt main_dir = (Path("tests") / template_dir).resolve() cookiecuter_context = json.loads((main_dir / "cookiecutter.json").read_text()) context = {"cookiecutter": cookiecuter_context} output_dir = prompt.choose_nested_template(context, main_dir, no_input=True) expected = (Path(main_dir) / expected).resolve() assert output_dir == f"{expected}" @pytest.mark.skipif(sys.platform.startswith('win'), reason="Linux / macos test") @pytest.mark.parametrize( "path", [ "", "/tmp", "/foo", ], ) def test_cookiecutter_nested_templates_invalid_paths(path: str): """Test nested_templates generation.""" from cookiecutter import prompt main_dir = (Path("tests") / "fake-nested-templates").resolve() cookiecuter_context = json.loads((main_dir / "cookiecutter.json").read_text()) cookiecuter_context["templates"]["fake-project"]["path"] = path context = {"cookiecutter": cookiecuter_context} with pytest.raises(ValueError) as exc: prompt.choose_nested_template(context, main_dir, no_input=True) assert "Illegal template path" in str(exc) @pytest.mark.skipif(not sys.platform.startswith('win'), reason="Win only test") @pytest.mark.parametrize( "path", [ "", "C:/tmp", "D:/tmp", ], ) def test_cookiecutter_nested_templates_invalid_win_paths(path: str): """Test nested_templates generation.""" from cookiecutter import prompt main_dir = (Path("tests") / "fake-nested-templates").resolve() cookiecuter_context = json.loads((main_dir / "cookiecutter.json").read_text()) cookiecuter_context["templates"]["fake-project"]["path"] = path context = {"cookiecutter": cookiecuter_context} with pytest.raises(ValueError) as exc: prompt.choose_nested_template(context, main_dir, no_input=True) assert "Illegal template path" in str(exc) def test_prompt_should_ask_and_rm_repo_dir(mocker, tmp_path): """In `prompt_and_delete()`, if the user agrees to delete/reclone the \ repo, the repo should be deleted.""" mock_read_user = mocker.patch( 'cookiecutter.prompt.read_user_yes_no', return_value=True ) repo_dir = Path(tmp_path, 'repo') repo_dir.mkdir() deleted = prompt.prompt_and_delete(str(repo_dir)) assert mock_read_user.called assert not repo_dir.exists() assert deleted def test_prompt_should_ask_and_exit_on_user_no_answer(mocker, tmp_path): """In `prompt_and_delete()`, if the user decline to delete/reclone the \ repo, cookiecutter should exit.""" mock_read_user = mocker.patch( 'cookiecutter.prompt.read_user_yes_no', return_value=False, ) mock_sys_exit = mocker.patch('sys.exit', return_value=True) repo_dir = Path(tmp_path, 'repo') repo_dir.mkdir() deleted = prompt.prompt_and_delete(str(repo_dir)) assert mock_read_user.called assert repo_dir.exists() assert not deleted assert mock_sys_exit.called def test_prompt_should_ask_and_rm_repo_file(mocker, tmp_path): """In `prompt_and_delete()`, if the user agrees to delete/reclone a \ repo file, the repo should be deleted.""" mock_read_user = mocker.patch( 'cookiecutter.prompt.read_user_yes_no', return_value=True, autospec=True ) repo_file = tmp_path.joinpath('repo.zip') repo_file.write_text('this is zipfile content') deleted = prompt.prompt_and_delete(str(repo_file)) assert mock_read_user.called assert not repo_file.exists() assert deleted def test_prompt_should_ask_and_keep_repo_on_no_reuse(mocker, tmp_path): """In `prompt_and_delete()`, if the user wants to keep their old \ cloned template repo, it should not be deleted.""" mock_read_user = mocker.patch( 'cookiecutter.prompt.read_user_yes_no', return_value=False, autospec=True ) repo_dir = Path(tmp_path, 'repo') repo_dir.mkdir() with pytest.raises(SystemExit): prompt.prompt_and_delete(str(repo_dir)) assert mock_read_user.called assert repo_dir.exists() def test_prompt_should_ask_and_keep_repo_on_reuse(mocker, tmp_path): """In `prompt_and_delete()`, if the user wants to keep their old \ cloned template repo, it should not be deleted.""" def answer(question, default): return 'okay to delete' not in question mock_read_user = mocker.patch( 'cookiecutter.prompt.read_user_yes_no', side_effect=answer, autospec=True ) repo_dir = Path(tmp_path, 'repo') repo_dir.mkdir() deleted = prompt.prompt_and_delete(str(repo_dir)) assert mock_read_user.called assert repo_dir.exists() assert not deleted def test_prompt_should_not_ask_if_no_input_and_rm_repo_dir(mocker, tmp_path): """Prompt should not ask if no input and rm dir. In `prompt_and_delete()`, if `no_input` is True, the call to `prompt.read_user_yes_no()` should be suppressed. """ mock_read_user = mocker.patch( 'cookiecutter.prompt.read_user_yes_no', return_value=True, autospec=True ) repo_dir = Path(tmp_path, 'repo') repo_dir.mkdir() deleted = prompt.prompt_and_delete(str(repo_dir), no_input=True) assert not mock_read_user.called assert not repo_dir.exists() assert deleted def test_prompt_should_not_ask_if_no_input_and_rm_repo_file(mocker, tmp_path): """Prompt should not ask if no input and rm file. In `prompt_and_delete()`, if `no_input` is True, the call to `prompt.read_user_yes_no()` should be suppressed. """ mock_read_user = mocker.patch( 'cookiecutter.prompt.read_user_yes_no', return_value=True, autospec=True ) repo_file = tmp_path.joinpath('repo.zip') repo_file.write_text('this is zipfile content') deleted = prompt.prompt_and_delete(str(repo_file), no_input=True) assert not mock_read_user.called assert not repo_file.exists() assert deleted cookiecutter-2.6.0/tests/test_read_repo_password.py000066400000000000000000000007511456543333500226720ustar00rootroot00000000000000"""Tests around handling repositories which require authentication.""" from cookiecutter.prompt import read_repo_password def test_click_invocation(mocker): """Test click function called correctly by cookiecutter. Test for password (hidden input) type invocation. """ prompt = mocker.patch('rich.prompt.Prompt.ask') prompt.return_value = 'sekrit' assert read_repo_password('Password') == 'sekrit' prompt.assert_called_once_with('Password', password=True) cookiecutter-2.6.0/tests/test_read_user_choice.py000066400000000000000000000023421456543333500222710ustar00rootroot00000000000000"""Tests around prompting for and handling of choice variables.""" import pytest from cookiecutter.prompt import read_user_choice OPTIONS = ['hello', 'world', 'foo', 'bar'] OPTIONS_INDEX = ['1', '2', '3', '4'] EXPECTED_PROMPT = """Select varname [bold magenta]1[/] - [bold]hello[/] [bold magenta]2[/] - [bold]world[/] [bold magenta]3[/] - [bold]foo[/] [bold magenta]4[/] - [bold]bar[/] Choose from""" @pytest.mark.parametrize('user_choice, expected_value', enumerate(OPTIONS, 1)) def test_click_invocation(mocker, user_choice, expected_value): """Test click function called correctly by cookiecutter. Test for choice type invocation. """ prompt = mocker.patch('rich.prompt.Prompt.ask') prompt.return_value = f'{user_choice}' assert read_user_choice('varname', OPTIONS) == expected_value prompt.assert_called_once_with(EXPECTED_PROMPT, choices=OPTIONS_INDEX, default='1') def test_raise_if_options_is_not_a_non_empty_list(): """Test function called by cookiecutter raise expected errors. Test for choice type invocation. """ with pytest.raises(TypeError): read_user_choice('foo', 'NOT A LIST') with pytest.raises(ValueError): read_user_choice('foo', []) cookiecutter-2.6.0/tests/test_read_user_dict.py000066400000000000000000000074751456543333500217760ustar00rootroot00000000000000"""Test `process_json`, `read_user_dict` functions in `cookiecutter.prompt`.""" import click import pytest from rich.prompt import InvalidResponse from cookiecutter.prompt import JsonPrompt, process_json, read_user_dict def test_process_json_invalid_json(): """Test `process_json` for correct error on malformed input.""" with pytest.raises(InvalidResponse) as exc_info: process_json('nope]') assert str(exc_info.value) == 'Unable to decode to JSON.' def test_process_json_non_dict(): """Test `process_json` for correct error on non-JSON input.""" with pytest.raises(InvalidResponse) as exc_info: process_json('[1, 2]') assert str(exc_info.value) == 'Requires JSON dict.' def test_process_json_valid_json(): """Test `process_json` for correct output on JSON input. Test for simple dict with list. """ user_value = '{"name": "foobar", "bla": ["a", 1, "b", false]}' assert process_json(user_value) == { 'name': 'foobar', 'bla': ['a', 1, 'b', False], } def test_process_json_deep_dict(): """Test `process_json` for correct output on JSON input. Test for dict in dict case. """ user_value = '''{ "key": "value", "integer_key": 37, "dict_key": { "deep_key": "deep_value", "deep_integer": 42, "deep_list": [ "deep value 1", "deep value 2", "deep value 3" ] }, "list_key": [ "value 1", "value 2", "value 3" ] }''' assert process_json(user_value) == { "key": "value", "integer_key": 37, "dict_key": { "deep_key": "deep_value", "deep_integer": 42, "deep_list": ["deep value 1", "deep value 2", "deep value 3"], }, "list_key": ["value 1", "value 2", "value 3"], } def test_should_raise_type_error(mocker): """Test `default_value` arg verification in `read_user_dict` function.""" prompt = mocker.patch('cookiecutter.prompt.JsonPrompt.ask') with pytest.raises(TypeError): read_user_dict('name', 'russell') assert not prompt.called def test_should_call_prompt_with_process_json(mocker): """Test to make sure that `process_json` is actually being used. Verifies generation of a processor for the user input. """ mock_prompt = mocker.patch('cookiecutter.prompt.JsonPrompt.ask', autospec=True) read_user_dict('name', {'project_slug': 'pytest-plugin'}) print(mock_prompt.call_args) args, kwargs = mock_prompt.call_args assert args == ('name [cyan bold](default)[/]',) assert kwargs['default'] == {'project_slug': 'pytest-plugin'} def test_should_not_load_json_from_sentinel(mocker): """Make sure that `json.loads` is not called when using default value.""" mock_json_loads = mocker.patch( 'cookiecutter.prompt.json.loads', autospec=True, return_value={} ) runner = click.testing.CliRunner() with runner.isolation(input="\n"): read_user_dict('name', {'project_slug': 'pytest-plugin'}) mock_json_loads.assert_not_called() @pytest.mark.parametrize("input", ["\n", "\ndefault\n"]) def test_read_user_dict_default_value(mocker, input): """Make sure that `read_user_dict` returns the default value. Verify return of a dict variable rather than the display value. """ runner = click.testing.CliRunner() with runner.isolation(input=input): val = read_user_dict('name', {'project_slug': 'pytest-plugin'}) assert val == {'project_slug': 'pytest-plugin'} def test_json_prompt_process_response(): """Test `JsonPrompt` process_response to convert str to json.""" jp = JsonPrompt() assert jp.process_response('{"project_slug": "something"}') == { 'project_slug': 'something' } cookiecutter-2.6.0/tests/test_read_user_variable.py000066400000000000000000000020211456543333500226160ustar00rootroot00000000000000"""test_read_user_variable.""" import pytest from cookiecutter.prompt import read_user_variable VARIABLE = 'project_name' DEFAULT = 'Kivy Project' @pytest.fixture def mock_prompt(mocker): """Return a mocked version of the 'Prompt.ask' function.""" return mocker.patch('rich.prompt.Prompt.ask') def test_click_invocation(mock_prompt): """Test click function called correctly by cookiecutter. Test for string type invocation. """ mock_prompt.return_value = DEFAULT assert read_user_variable(VARIABLE, DEFAULT) == DEFAULT mock_prompt.assert_called_once_with(VARIABLE, default=DEFAULT) def test_input_loop_with_null_default_value(mock_prompt): """Test `Prompt.ask` is run repeatedly until a valid answer is provided. Test for `default_value` parameter equal to None. """ # Simulate user providing None input initially and then a valid input mock_prompt.side_effect = [None, DEFAULT] assert read_user_variable(VARIABLE, None) == DEFAULT assert mock_prompt.call_count == 2 cookiecutter-2.6.0/tests/test_read_user_yes_no.py000066400000000000000000000015671456543333500223430ustar00rootroot00000000000000"""test_read_user_yes_no.""" import pytest from rich.prompt import InvalidResponse from cookiecutter.prompt import YesNoPrompt, read_user_yes_no QUESTION = 'Is it okay to delete and re-clone it?' DEFAULT = 'y' def test_click_invocation(mocker): """Test click function called correctly by cookiecutter. Test for boolean type invocation. """ prompt = mocker.patch('cookiecutter.prompt.YesNoPrompt.ask') prompt.return_value = DEFAULT assert read_user_yes_no(QUESTION, DEFAULT) == DEFAULT prompt.assert_called_once_with(QUESTION, default=DEFAULT) def test_yesno_prompt_process_response(): """Test `YesNoPrompt` process_response to convert str to bool.""" ynp = YesNoPrompt() with pytest.raises(InvalidResponse): ynp.process_response('wrong') assert ynp.process_response('t') is True assert ynp.process_response('f') is False cookiecutter-2.6.0/tests/test_repo_not_found.py000066400000000000000000000005601456543333500220260ustar00rootroot00000000000000"""Testing invalid cookiecutter template repositories.""" import pytest from cookiecutter import exceptions, main def test_should_raise_error_if_repo_does_not_exist(): """Cookiecutter invocation with non-exist repository should raise error.""" with pytest.raises(exceptions.RepositoryNotFound): main.cookiecutter('definitely-not-a-valid-repo-dir') cookiecutter-2.6.0/tests/test_specify_output_dir.py000066400000000000000000000044241456543333500227310ustar00rootroot00000000000000"""Tests for cookiecutter's output directory customization feature.""" import pytest from cookiecutter import main @pytest.fixture def context(): """Fixture to return a valid context as known from a cookiecutter.json.""" return { 'cookiecutter': { 'email': 'raphael@hackebrot.de', 'full_name': 'Raphael Pierzina', 'github_username': 'hackebrot', 'version': '0.1.0', } } @pytest.fixture def template(tmp_path): """Fixture to prepare test template directory.""" template_dir = tmp_path.joinpath("template") template_dir.mkdir() template_dir.joinpath('cookiecutter.json').touch() return str(template_dir) @pytest.fixture(autouse=True) def mock_gen_context(mocker, context): """Fixture. Automatically mock cookiecutter's function with expected output.""" mocker.patch('cookiecutter.main.generate_context', return_value=context) @pytest.fixture(autouse=True) def mock_prompt(mocker): """Fixture. Automatically mock cookiecutter's function with expected output.""" mocker.patch('cookiecutter.main.prompt_for_config') @pytest.fixture(autouse=True) def mock_replay(mocker): """Fixture. Automatically mock cookiecutter's function with expected output.""" mocker.patch('cookiecutter.main.dump') def test_api_invocation(mocker, template, output_dir, context): """Verify output dir location is correctly passed.""" mock_gen_files = mocker.patch('cookiecutter.main.generate_files') main.cookiecutter(template, output_dir=output_dir) mock_gen_files.assert_called_once_with( repo_dir=template, context=context, overwrite_if_exists=False, skip_if_file_exists=False, output_dir=output_dir, accept_hooks=True, keep_project_on_failure=False, ) def test_default_output_dir(mocker, template, context): """Verify default output dir is current working folder.""" mock_gen_files = mocker.patch('cookiecutter.main.generate_files') main.cookiecutter(template) mock_gen_files.assert_called_once_with( repo_dir=template, context=context, overwrite_if_exists=False, skip_if_file_exists=False, output_dir='.', accept_hooks=True, keep_project_on_failure=False, ) cookiecutter-2.6.0/tests/test_templates.py000066400000000000000000000016461456543333500210120ustar00rootroot00000000000000""" test_custom_extension_in_hooks. Tests to ensure custom cookiecutter extensions are properly made available to pre- and post-gen hooks. """ from pathlib import Path import pytest from cookiecutter import main @pytest.fixture def output_dir(tmpdir): """Fixture. Create and return custom temp directory for test.""" return str(tmpdir.mkdir('templates')) @pytest.mark.parametrize("template", ["include", "no-templates", "extends", "super"]) def test_build_templates(template, output_dir): """ Verify Templates Design keywords. no-templates is a compatibility tests for repo without `templates` directory """ project_dir = main.cookiecutter( f'tests/test-templates/{template}', no_input=True, output_dir=output_dir, ) readme = Path(project_dir, 'requirements.txt').read_text() assert readme.split() == [ "pip", "Click", "pytest", ] cookiecutter-2.6.0/tests/test_time_extension.py000066400000000000000000000046071456543333500220460ustar00rootroot00000000000000"""Tests for the TimeExtension Jinja2 extension.""" import freezegun import pytest from jinja2 import Environment, exceptions @pytest.fixture def environment(): """Fixture. Add tested extension to environment.""" return Environment(extensions=['cookiecutter.extensions.TimeExtension']) @pytest.fixture(autouse=True) def freeze(): """Fixture. Freeze time for all tests.""" freezer = freezegun.freeze_time("2015-12-09 23:33:01") freezer.start() yield freezer.stop() def test_tz_is_required(environment): """Verify template parsing fails without a timezone.""" with pytest.raises(exceptions.TemplateSyntaxError): environment.from_string('{% now %}') def test_utc_default_datetime_format(environment): """Verify default datetime format can be parsed.""" template = environment.from_string("{% now 'utc' %}") assert template.render() == "2015-12-09" @pytest.mark.parametrize("valid_tz", ['utc', 'local', 'Europe/Berlin']) def test_accept_valid_timezones(environment, valid_tz): """Verify that valid timezones are accepted.""" template = environment.from_string(f"{{% now '{valid_tz}', '%Y-%m' %}}") assert template.render() == '2015-12' def test_environment_datetime_format(environment): """Verify datetime format can be parsed from environment.""" environment.datetime_format = '%a, %d %b %Y %H:%M:%S' template = environment.from_string("{% now 'utc' %}") assert template.render() == "Wed, 09 Dec 2015 23:33:01" def test_add_time(environment): """Verify that added time offset can be parsed.""" environment.datetime_format = '%a, %d %b %Y %H:%M:%S' template = environment.from_string("{% now 'utc' + 'hours=2,seconds=30' %}") assert template.render() == "Thu, 10 Dec 2015 01:33:31" def test_substract_time(environment): """Verify that substracted time offset can be parsed.""" environment.datetime_format = '%a, %d %b %Y %H:%M:%S' template = environment.from_string("{% now 'utc' - 'minutes=11' %}") assert template.render() == "Wed, 09 Dec 2015 23:22:01" def test_offset_with_format(environment): """Verify that offset works together with datetime format.""" environment.datetime_format = '%d %b %Y %H:%M:%S' template = environment.from_string( "{% now 'utc' - 'days=2,minutes=33,seconds=1', '%d %b %Y %H:%M:%S' %}" ) assert template.render() == "07 Dec 2015 23:00:00" cookiecutter-2.6.0/tests/test_utils.py000066400000000000000000000061341456543333500201510ustar00rootroot00000000000000"""Tests for `cookiecutter.utils` module.""" import stat import sys from pathlib import Path import pytest from cookiecutter import utils def make_readonly(path): """Change the access permissions to readonly for a given file.""" mode = Path.stat(path).st_mode Path.chmod(path, mode & ~stat.S_IWRITE) def test_force_delete(mocker, tmp_path): """Verify `utils.force_delete` makes files writable.""" ro_file = Path(tmp_path, 'bar') ro_file.write_text("Test data") make_readonly(ro_file) rmtree = mocker.Mock() utils.force_delete(rmtree, ro_file, sys.exc_info()) assert (ro_file.stat().st_mode & stat.S_IWRITE) == stat.S_IWRITE rmtree.assert_called_once_with(ro_file) utils.rmtree(tmp_path) def test_rmtree(tmp_path): """Verify `utils.rmtree` remove files marked as read-only.""" file_path = Path(tmp_path, "bar") file_path.write_text("Test data") make_readonly(file_path) utils.rmtree(tmp_path) assert not Path(tmp_path).exists() def test_make_sure_path_exists(tmp_path): """Verify correct True/False response from `utils.make_sure_path_exists`. Should return True if directory exist or created. Should return False if impossible to create directory (for example protected) """ existing_directory = tmp_path directory_to_create = Path(tmp_path, "not_yet_created") utils.make_sure_path_exists(existing_directory) utils.make_sure_path_exists(directory_to_create) # Ensure by base system methods. assert existing_directory.is_dir() assert existing_directory.exists() assert directory_to_create.is_dir() assert directory_to_create.exists() def test_make_sure_path_exists_correctly_handle_os_error(mocker): """Verify correct True/False response from `utils.make_sure_path_exists`. Should return True if directory exist or created. Should return False if impossible to create directory (for example protected) """ mocker.patch("pathlib.Path.mkdir", side_effect=OSError) with pytest.raises(OSError) as err: utils.make_sure_path_exists(Path('protected_path')) assert str(err.value) == "Unable to create directory at protected_path" def test_work_in(tmp_path): """Verify returning to original folder after `utils.work_in` use.""" cwd = Path.cwd() ch_to = tmp_path assert ch_to != Path.cwd() # Under context manager we should work in tmp_path. with utils.work_in(ch_to): assert ch_to == Path.cwd() # Make sure we return to the correct folder assert cwd == Path.cwd() def test_work_in_without_path(): """Folder is not changed if no path provided.""" cwd = Path.cwd() with utils.work_in(): assert cwd == Path.cwd() assert cwd == Path.cwd() def test_create_tmp_repo_dir(tmp_path): """Verify `utils.create_tmp_repo_dir` creates a copy.""" repo_dir = Path(tmp_path) / 'bar' repo_dir.mkdir() subdirs = ('foo', 'bar', 'foobar') for name in subdirs: (repo_dir / name).mkdir() new_repo_dir = utils.create_tmp_repo_dir(repo_dir) assert new_repo_dir.exists() assert new_repo_dir.glob('*') cookiecutter-2.6.0/tests/undefined-variable/000077500000000000000000000000001456543333500211205ustar00rootroot00000000000000cookiecutter-2.6.0/tests/undefined-variable/dir-name/000077500000000000000000000000001456543333500226145ustar00rootroot00000000000000cookiecutter-2.6.0/tests/undefined-variable/dir-name/{{cookiecutter.project_slug}}/000077500000000000000000000000001456543333500307735ustar00rootroot00000000000000cookiecutter-2.6.0/tests/undefined-variable/dir-name/{{cookiecutter.project_slug}}/README.rst000066400000000000000000000001241456543333500324570ustar00rootroot00000000000000{{cookiecutter.project_slug}} {% for _ in cookiecutter.project_slug %}={% endfor %} {{cookiecutter.foobar}}/000077500000000000000000000000001456543333500356435ustar00rootroot00000000000000cookiecutter-2.6.0/tests/undefined-variable/dir-name/{{cookiecutter.project_slug}}helloworld.py000066400000000000000000000001711456543333500403670ustar00rootroot00000000000000cookiecutter-2.6.0/tests/undefined-variable/dir-name/{{cookiecutter.project_slug}}/{{cookiecutter.foobar}}""" Sample Python file. Expected not to be created in a test due to the undefined variable ``cookiecutter.foobar``. """ cookiecutter-2.6.0/tests/undefined-variable/file-content/000077500000000000000000000000001456543333500235075ustar00rootroot00000000000000cookiecutter-2.6.0/tests/undefined-variable/file-content/{{cookiecutter.project_slug}}/000077500000000000000000000000001456543333500316665ustar00rootroot00000000000000cookiecutter-2.6.0/tests/undefined-variable/file-content/{{cookiecutter.project_slug}}/README.rst000066400000000000000000000003001456543333500333460ustar00rootroot00000000000000{{cookiecutter.project_slug}} {% for _ in cookiecutter.project_slug %}={% endfor %} {{cookiecutter.foobar}} https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.project_slug}} cookiecutter-2.6.0/tests/undefined-variable/file-name/000077500000000000000000000000001456543333500227555ustar00rootroot00000000000000cookiecutter-2.6.0/tests/undefined-variable/file-name/cookiecutter.json000066400000000000000000000001061456543333500263450ustar00rootroot00000000000000{ "project_slug": "testproject", "github_username": "hackebrot" } cookiecutter-2.6.0/tests/undefined-variable/file-name/{{cookiecutter.project_slug}}/000077500000000000000000000000001456543333500311345ustar00rootroot00000000000000{{cookiecutter.foobar}}000066400000000000000000000001241456543333500357240ustar00rootroot00000000000000cookiecutter-2.6.0/tests/undefined-variable/file-name/{{cookiecutter.project_slug}}{{cookiecutter.project_slug}} {% for _ in cookiecutter.project_slug %}={% endfor %} cookiecutter-2.6.0/tests/vcs/000077500000000000000000000000001456543333500161675ustar00rootroot00000000000000cookiecutter-2.6.0/tests/vcs/test_clone.py000066400000000000000000000157511456543333500207110ustar00rootroot00000000000000"""Tests around cloning repositories and detection of errors at it.""" import os import subprocess import pytest from cookiecutter import exceptions, vcs def test_clone_should_raise_if_vcs_not_installed(mocker, clone_dir): """In `clone()`, a `VCSNotInstalled` exception should be raised if no VCS \ is installed.""" mocker.patch('cookiecutter.vcs.is_vcs_installed', autospec=True, return_value=False) repo_url = 'https://github.com/pytest-dev/cookiecutter-pytest-plugin.git' with pytest.raises(exceptions.VCSNotInstalled): vcs.clone(repo_url, clone_to_dir=str(clone_dir)) def test_clone_should_rstrip_trailing_slash_in_repo_url(mocker, clone_dir): """In `clone()`, repo URL's trailing slash should be stripped if one is \ present.""" mocker.patch('cookiecutter.vcs.is_vcs_installed', autospec=True, return_value=True) mock_subprocess = mocker.patch( 'cookiecutter.vcs.subprocess.check_output', autospec=True, ) vcs.clone('https://github.com/foo/bar/', clone_to_dir=clone_dir, no_input=True) mock_subprocess.assert_called_once_with( ['git', 'clone', 'https://github.com/foo/bar'], cwd=clone_dir, stderr=subprocess.STDOUT, ) def test_clone_should_abort_if_user_does_not_want_to_reclone(mocker, clone_dir): """In `clone()`, if user doesn't want to reclone, Cookiecutter should exit \ without cloning anything.""" mocker.patch('cookiecutter.vcs.is_vcs_installed', autospec=True, return_value=True) mocker.patch( 'cookiecutter.vcs.prompt_and_delete', side_effect=SystemExit, autospec=True ) mock_subprocess = mocker.patch( 'cookiecutter.vcs.subprocess.check_output', autospec=True, ) # Create repo_dir to trigger prompt_and_delete repo_dir = clone_dir.joinpath('cookiecutter-pytest-plugin') repo_dir.mkdir() repo_url = 'https://github.com/pytest-dev/cookiecutter-pytest-plugin.git' with pytest.raises(SystemExit): vcs.clone(repo_url, clone_to_dir=str(clone_dir)) assert not mock_subprocess.called def test_clone_should_silent_exit_if_ok_to_reuse(mocker, tmpdir): """In `clone()`, if user doesn't want to reclone, Cookiecutter should exit \ without cloning anything.""" mocker.patch('cookiecutter.vcs.is_vcs_installed', autospec=True, return_value=True) mocker.patch( 'cookiecutter.vcs.prompt_and_delete', return_value=False, autospec=True ) mock_subprocess = mocker.patch( 'cookiecutter.vcs.subprocess.check_output', autospec=True, ) clone_to_dir = tmpdir.mkdir('clone') # Create repo_dir to trigger prompt_and_delete clone_to_dir.mkdir('cookiecutter-pytest-plugin') repo_url = 'https://github.com/pytest-dev/cookiecutter-pytest-plugin.git' vcs.clone(repo_url, clone_to_dir=str(clone_to_dir)) assert not mock_subprocess.called @pytest.mark.parametrize( 'repo_type, repo_url, repo_name', [ ('git', 'https://github.com/hello/world.git', 'world'), ('hg', 'https://bitbucket.org/foo/bar', 'bar'), ('git', 'git@host:gitoliterepo', 'gitoliterepo'), ('git', 'git@gitlab.com:cookiecutter/cookiecutter.git', 'cookiecutter'), ('git', 'git@github.com:cookiecutter/cookiecutter.git', 'cookiecutter'), ], ) def test_clone_should_invoke_vcs_command( mocker, clone_dir, repo_type, repo_url, repo_name ): """When `clone()` is called with a git/hg repo, the corresponding VCS \ command should be run via `subprocess.check_output()`. This should take place: * In the correct dir * With the correct args. """ mocker.patch('cookiecutter.vcs.is_vcs_installed', autospec=True, return_value=True) mock_subprocess = mocker.patch( 'cookiecutter.vcs.subprocess.check_output', autospec=True, ) expected_repo_dir = os.path.normpath(os.path.join(clone_dir, repo_name)) branch = 'foobar' repo_dir = vcs.clone( repo_url, checkout=branch, clone_to_dir=clone_dir, no_input=True ) assert repo_dir == expected_repo_dir mock_subprocess.assert_any_call( [repo_type, 'clone', repo_url], cwd=clone_dir, stderr=subprocess.STDOUT ) branch_info = [branch] # We sanitize branch information for Mercurial if repo_type == "hg": branch_info.insert(0, "--") mock_subprocess.assert_any_call( [repo_type, 'checkout', *branch_info], cwd=expected_repo_dir, stderr=subprocess.STDOUT, ) @pytest.mark.parametrize( 'error_message', [ (b"fatal: repository 'https://github.com/hackebro/cookiedozer' not found"), b'hg: abort: HTTP Error 404: Not Found', ], ) def test_clone_handles_repo_typo(mocker, clone_dir, error_message): """In `clone()`, repository not found errors should raise an \ appropriate exception.""" # side_effect is set to an iterable here (and below), # because of a Python 3.4 unittest.mock regression # http://bugs.python.org/issue23661 mocker.patch( 'cookiecutter.vcs.subprocess.check_output', autospec=True, side_effect=[subprocess.CalledProcessError(-1, 'cmd', output=error_message)], ) repository_url = 'https://github.com/hackebro/cookiedozer' with pytest.raises(exceptions.RepositoryNotFound) as err: vcs.clone(repository_url, clone_to_dir=str(clone_dir), no_input=True) assert str(err.value) == ( f'The repository {repository_url} could not be found, have you made a typo?' ) @pytest.mark.parametrize( 'error_message', [ b"error: pathspec 'unknown_branch' did not match any file(s) known to git", b"hg: abort: unknown revision 'unknown_branch'!", ], ) def test_clone_handles_branch_typo(mocker, clone_dir, error_message): """In `clone()`, branch not found errors should raise an \ appropriate exception.""" mocker.patch( 'cookiecutter.vcs.subprocess.check_output', autospec=True, side_effect=[subprocess.CalledProcessError(-1, 'cmd', output=error_message)], ) repository_url = 'https://github.com/pytest-dev/cookiecutter-pytest-plugin' with pytest.raises(exceptions.RepositoryCloneFailed) as err: vcs.clone( repository_url, clone_to_dir=str(clone_dir), checkout='unknown_branch', no_input=True, ) assert str(err.value) == ( 'The unknown_branch branch of repository ' f'{repository_url} could not found, have you made a typo?' ) def test_clone_unknown_subprocess_error(mocker, clone_dir): """In `clone()`, unknown subprocess errors should be raised.""" mocker.patch( 'cookiecutter.vcs.subprocess.check_output', autospec=True, side_effect=[ subprocess.CalledProcessError(-1, 'cmd', output=b'Something went wrong') ], ) with pytest.raises(subprocess.CalledProcessError): vcs.clone( 'https://github.com/pytest-dev/cookiecutter-pytest-plugin', clone_to_dir=str(clone_dir), no_input=True, ) cookiecutter-2.6.0/tests/vcs/test_identify_repo.py000066400000000000000000000050161456543333500224420ustar00rootroot00000000000000"""Collection of tests around repository type identification.""" import pytest from cookiecutter import exceptions, vcs @pytest.mark.parametrize( 'repo_url, exp_repo_type, exp_repo_url', [ ( 'git+https://github.com/pytest-dev/cookiecutter-pytest-plugin.git', 'git', 'https://github.com/pytest-dev/cookiecutter-pytest-plugin.git', ), ( 'hg+https://bitbucket.org/foo/bar.hg', 'hg', 'https://bitbucket.org/foo/bar.hg', ), ( 'https://github.com/pytest-dev/cookiecutter-pytest-plugin.git', 'git', 'https://github.com/pytest-dev/cookiecutter-pytest-plugin.git', ), ('https://bitbucket.org/foo/bar.hg', 'hg', 'https://bitbucket.org/foo/bar.hg'), ( 'https://github.com/audreyfeldroy/cookiecutter-pypackage.git', 'git', 'https://github.com/audreyfeldroy/cookiecutter-pypackage.git', ), ( 'https://github.com/audreyfeldroy/cookiecutter-pypackage', 'git', 'https://github.com/audreyfeldroy/cookiecutter-pypackage', ), ( 'git@gitorious.org:cookiecutter-gitorious/cookiecutter-gitorious.git', 'git', 'git@gitorious.org:cookiecutter-gitorious/cookiecutter-gitorious.git', ), ( 'https://audreyr@bitbucket.org/audreyr/cookiecutter-bitbucket', 'hg', 'https://audreyr@bitbucket.org/audreyr/cookiecutter-bitbucket', ), ], ) def test_identify_known_repo(repo_url, exp_repo_type, exp_repo_url): """Verify different correct repositories url syntax is correctly transformed.""" assert vcs.identify_repo(repo_url) == (exp_repo_type, exp_repo_url) @pytest.fixture( params=[ 'foo+git', # uses explicit identifier with 'git' in the wrong place 'foo+hg', # uses explicit identifier with 'hg' in the wrong place 'foo+bar', # uses explicit identifier with neither 'git' nor 'hg' 'foobar', # no identifier but neither 'git' nor 'bitbucket' in url 'http://norepotypespecified.com', ] ) def unknown_repo_type_url(request): """Fixture. Return wrong formatted repository url.""" return request.param def test_identify_raise_on_unknown_repo(unknown_repo_type_url): """Verify different incorrect repositories url syntax trigger error raising.""" with pytest.raises(exceptions.UnknownRepoType): vcs.identify_repo(unknown_repo_type_url) cookiecutter-2.6.0/tests/vcs/test_is_vcs_installed.py000066400000000000000000000007651456543333500231350ustar00rootroot00000000000000"""Collection of tests around VCS detection.""" import pytest from cookiecutter import vcs @pytest.mark.parametrize( 'which_return, result', [('', False), (None, False), (False, False), ('/usr/local/bin/git', True)], ) def test_is_vcs_installed(mocker, which_return, result): """Verify `is_vcs_installed` function correctly handles `which` answer.""" mocker.patch('cookiecutter.vcs.which', autospec=True, return_value=which_return) assert vcs.is_vcs_installed('git') == result cookiecutter-2.6.0/tests/zipfile/000077500000000000000000000000001456543333500170365ustar00rootroot00000000000000cookiecutter-2.6.0/tests/zipfile/test_unzip.py000066400000000000000000000226141456543333500216210ustar00rootroot00000000000000"""Tests for function unzip() from zipfile module.""" import shutil import tempfile from pathlib import Path import pytest from cookiecutter import zipfile from cookiecutter.exceptions import InvalidZipRepository def mock_download(): """Fake download function.""" with Path('tests/files/fake-repo-tmpl.zip').open('rb') as zf: chunk = zf.read(1024) while chunk: yield chunk chunk = zf.read(1024) def mock_download_with_empty_chunks(): """Fake download function.""" yield with Path('tests/files/fake-repo-tmpl.zip').open('rb') as zf: chunk = zf.read(1024) while chunk: yield chunk chunk = zf.read(1024) def test_unzip_local_file(mocker, clone_dir): """Local file reference can be unzipped.""" mock_prompt_and_delete = mocker.patch( 'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True ) output_dir = zipfile.unzip( 'tests/files/fake-repo-tmpl.zip', is_url=False, clone_to_dir=str(clone_dir) ) assert output_dir.startswith(tempfile.gettempdir()) assert not mock_prompt_and_delete.called def test_unzip_protected_local_file_environment_password(mocker, clone_dir): """In `unzip()`, the environment can be used to provide a repo password.""" mock_prompt_and_delete = mocker.patch( 'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True ) output_dir = zipfile.unzip( 'tests/files/protected-fake-repo-tmpl.zip', is_url=False, clone_to_dir=str(clone_dir), password='sekrit', ) assert output_dir.startswith(tempfile.gettempdir()) assert not mock_prompt_and_delete.called def test_unzip_protected_local_file_bad_environment_password(mocker, clone_dir): """In `unzip()`, an error occurs if the environment has a bad password.""" mocker.patch( 'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True ) with pytest.raises(InvalidZipRepository): zipfile.unzip( 'tests/files/protected-fake-repo-tmpl.zip', is_url=False, clone_to_dir=str(clone_dir), password='not-the-right-password', ) def test_unzip_protected_local_file_user_password_with_noinput(mocker, clone_dir): """Can't unpack a password-protected repo in no_input mode.""" mocker.patch( 'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True ) with pytest.raises(InvalidZipRepository): zipfile.unzip( 'tests/files/protected-fake-repo-tmpl.zip', is_url=False, clone_to_dir=str(clone_dir), no_input=True, ) def test_unzip_protected_local_file_user_password(mocker, clone_dir): """A password-protected local file reference can be unzipped.""" mock_prompt_and_delete = mocker.patch( 'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True ) mocker.patch('cookiecutter.zipfile.read_repo_password', return_value='sekrit') output_dir = zipfile.unzip( 'tests/files/protected-fake-repo-tmpl.zip', is_url=False, clone_to_dir=str(clone_dir), ) assert output_dir.startswith(tempfile.gettempdir()) assert not mock_prompt_and_delete.called def test_unzip_protected_local_file_user_bad_password(mocker, clone_dir): """Error in `unzip()`, if user can't provide a valid password.""" mocker.patch( 'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True ) mocker.patch( 'cookiecutter.zipfile.read_repo_password', return_value='not-the-right-password' ) with pytest.raises(InvalidZipRepository): zipfile.unzip( 'tests/files/protected-fake-repo-tmpl.zip', is_url=False, clone_to_dir=str(clone_dir), ) def test_empty_zip_file(mocker, clone_dir): """In `unzip()`, an empty file raises an error.""" mocker.patch( 'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True ) with pytest.raises(InvalidZipRepository): zipfile.unzip( 'tests/files/empty.zip', is_url=False, clone_to_dir=str(clone_dir) ) def test_non_repo_zip_file(mocker, clone_dir): """In `unzip()`, a repository must have a top level directory.""" mocker.patch( 'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True ) with pytest.raises(InvalidZipRepository): zipfile.unzip( 'tests/files/not-a-repo.zip', is_url=False, clone_to_dir=str(clone_dir) ) def test_bad_zip_file(mocker, clone_dir): """In `unzip()`, a corrupted zip file raises an error.""" mocker.patch( 'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True ) with pytest.raises(InvalidZipRepository): zipfile.unzip( 'tests/files/bad-zip-file.zip', is_url=False, clone_to_dir=str(clone_dir) ) def test_unzip_url(mocker, clone_dir): """In `unzip()`, a url will be downloaded and unzipped.""" mock_prompt_and_delete = mocker.patch( 'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True ) request = mocker.MagicMock() request.iter_content.return_value = mock_download() mocker.patch( 'cookiecutter.zipfile.requests.get', return_value=request, autospec=True, ) output_dir = zipfile.unzip( 'https://example.com/path/to/fake-repo-tmpl.zip', is_url=True, clone_to_dir=str(clone_dir), ) assert output_dir.startswith(tempfile.gettempdir()) assert not mock_prompt_and_delete.called def test_unzip_url_with_empty_chunks(mocker, clone_dir): """In `unzip()` empty chunk must be ignored.""" mock_prompt_and_delete = mocker.patch( 'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True ) request = mocker.MagicMock() request.iter_content.return_value = mock_download_with_empty_chunks() mocker.patch( 'cookiecutter.zipfile.requests.get', return_value=request, autospec=True, ) output_dir = zipfile.unzip( 'https://example.com/path/to/fake-repo-tmpl.zip', is_url=True, clone_to_dir=str(clone_dir), ) assert output_dir.startswith(tempfile.gettempdir()) assert not mock_prompt_and_delete.called def test_unzip_url_existing_cache(mocker, clone_dir): """Url should be downloaded and unzipped, old zip file will be removed.""" mock_prompt_and_delete = mocker.patch( 'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True ) request = mocker.MagicMock() request.iter_content.return_value = mock_download() mocker.patch( 'cookiecutter.zipfile.requests.get', return_value=request, autospec=True, ) # Create an existing cache of the zipfile existing_zip = clone_dir.joinpath('fake-repo-tmpl.zip') existing_zip.write_text('This is an existing zipfile') output_dir = zipfile.unzip( 'https://example.com/path/to/fake-repo-tmpl.zip', is_url=True, clone_to_dir=str(clone_dir), ) assert output_dir.startswith(tempfile.gettempdir()) assert mock_prompt_and_delete.call_count == 1 def test_unzip_url_existing_cache_no_input(mocker, clone_dir): """If no_input is provided, the existing file should be removed.""" request = mocker.MagicMock() request.iter_content.return_value = mock_download() mocker.patch( 'cookiecutter.zipfile.requests.get', return_value=request, autospec=True, ) # Create an existing cache of the zipfile existing_zip = clone_dir.joinpath('fake-repo-tmpl.zip') existing_zip.write_text('This is an existing zipfile') output_dir = zipfile.unzip( 'https://example.com/path/to/fake-repo-tmpl.zip', is_url=True, clone_to_dir=str(clone_dir), no_input=True, ) assert output_dir.startswith(tempfile.gettempdir()) def test_unzip_should_abort_if_no_redownload(mocker, clone_dir): """Should exit without cloning anything If no redownload.""" mocker.patch( 'cookiecutter.zipfile.prompt_and_delete', side_effect=SystemExit, autospec=True ) mock_requests_get = mocker.patch( 'cookiecutter.zipfile.requests.get', autospec=True, ) # Create an existing cache of the zipfile existing_zip = clone_dir.joinpath('fake-repo-tmpl.zip') existing_zip.write_text('This is an existing zipfile') zipfile_url = 'https://example.com/path/to/fake-repo-tmpl.zip' with pytest.raises(SystemExit): zipfile.unzip(zipfile_url, is_url=True, clone_to_dir=str(clone_dir)) assert not mock_requests_get.called def test_unzip_is_ok_to_reuse(mocker, clone_dir): """Already downloaded zip should not be downloaded again.""" mock_prompt_and_delete = mocker.patch( 'cookiecutter.zipfile.prompt_and_delete', return_value=False, autospec=True ) request = mocker.MagicMock() existing_zip = clone_dir.joinpath('fake-repo-tmpl.zip') shutil.copy('tests/files/fake-repo-tmpl.zip', existing_zip) output_dir = zipfile.unzip( 'https://example.com/path/to/fake-repo-tmpl.zip', is_url=True, clone_to_dir=str(clone_dir), ) assert output_dir.startswith(tempfile.gettempdir()) assert mock_prompt_and_delete.call_count == 1 assert request.iter_content.call_count == 0 cookiecutter-2.6.0/tox.ini000066400000000000000000000025041456543333500155460ustar00rootroot00000000000000[tox] envlist = lint py{312, 311, 310, 39, 38, 37} safety docs skip_missing_interpreters = True isolated_build = True [testenv:lint] description = Run pre-commit hooks deps = pre-commit commands = pre-commit run --all --all-files [testenv] description = Run the test suite ({basepython}) package = wheel wheel_build_env = build_wheel depends = # The test suite currently cannot run in parallel due to I/O conflicts. # To allow tox to run other test environments (like 'docs') in parallel, # sequential dependencies are listed here for the test suite. py37: py38 py38: py39 py39: py310 py310: py311 py311: py312 py312: lint deps = -rtest_requirements.txt commands = pytest --color=yes --cov-report=html --cov-report=xml --cov-branch --cov-fail-under=100 {posargs} [testenv:safety] description = Check with safety deps = safety==2.3.5 commands = safety --disable-telemetry check --full-report [testenv:docs] description = Build the documentation deps = -rdocs/requirements.txt commands = sphinx-build -Wab html docs/ docs/_build/html [testenv:servedocs] description = Host the docs locally and rebuild on changes deps = -rdocs/requirements.txt commands = sphinx-autobuild -Wa docs/ docs/_build/html --open-browser --port 9812 --watch *.md --watch *.rst --watch *.py --watch cookiecutter