diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 6691c2c63..13ec354d8 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,3 +1,42 @@ +filter-by-commitish: true +commitish: master +name-template: '$RESOLVED_VERSION' +tag-template: '$RESOLVED_VERSION' +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' + - 'noxfile.py' + - 'setup.*' + - 'docs/conf.py' + - 'Makefile' + - 'make.bat' + - '*requirements*.txt' + - label: 'documentation' + files: + - '*.md' + - '*.rst' categories: - title: 'Breaking Changes' labels: @@ -23,6 +62,8 @@ categories: - title: 'Deprecations' labels: - 'deprecated' +sort-by: title +sort-direction: ascending exclude-labels: - 'skip-changelog' template: | @@ -30,6 +71,6 @@ template: | $CHANGES - ## This release is made by wonderfull contributors: + ## This release is made by wonderful contributors: $CONTRIBUTORS diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index d3744b683..000000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: "Documentation build check" -on: - pull_request: - branches: - - "*" - -jobs: - docs: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - submodules: 'recursive' - - uses: actions/setup-python@v1 - with: - python-version: "3.7" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox virtualenv - - name: Build docs - run: tox -e docs - - uses: actions/upload-artifact@v1 - with: - name: DocumentationHTML - path: docs/_build/html/ diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/drafter.yml similarity index 60% rename from .github/workflows/release-drafter.yml rename to .github/workflows/drafter.yml index 17fdb961d..c7cd0bbcc 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/drafter.yml @@ -4,9 +4,18 @@ 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 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 73c66aff7..000000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,135 +0,0 @@ -name: CI Tests - -on: - push: - branches: - - master - tags: - - "*" - pull_request: - branches: - - "*" - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v1 - with: - python-version: "3.9" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox virtualenv - - name: Lint - run: "tox -e lint" - - name: Safety - run: "tox -e safety" - build: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - name: - - "ubuntu-py36" - - "ubuntu-py37" - - "ubuntu-py38" - - "ubuntu-py39" - - "ubuntu-pypy3" - - - "macos-py36" - - "macos-py37" - - "macos-py38" - - "macos-py39" - - "macos-pypy3" - - - "windows-py36" - - "windows-py37" - - "windows-py38" - - "windows-py39" - - include: - - name: "ubuntu-py36" - python: "3.6" - os: ubuntu-latest - tox_env: "py36" - - name: "ubuntu-py37" - python: "3.7" - os: ubuntu-latest - tox_env: "py37" - - name: "ubuntu-py38" - python: "3.8" - os: ubuntu-latest - tox_env: "py38" - - name: "ubuntu-py39" - python: "3.9" - os: ubuntu-latest - tox_env: "py39" - - name: "ubuntu-pypy3" - python: "pypy3" - os: ubuntu-latest - tox_env: "pypy3" - - - name: "macos-py36" - python: "3.6" - os: macos-latest - tox_env: "py36" - - name: "macos-py37" - python: "3.7" - os: macos-latest - tox_env: "py37" - - name: "macos-py38" - python: "3.8" - os: macos-latest - tox_env: "py38" - - name: "macos-py39" - python: "3.9" - os: macos-latest - tox_env: "py39" - - name: "macos-pypy3" - python: "pypy3" - os: macos-latest - tox_env: "pypy3" - - - name: "windows-py36" - python: "3.6" - os: windows-latest - tox_env: "py36" - - name: "windows-py37" - python: "3.7" - os: windows-latest - tox_env: "py37" - - name: "windows-py38" - python: "3.8" - os: windows-latest - tox_env: "py38" - - name: "windows-py39" - python: "3.9" - os: windows-latest - tox_env: "py39" - - steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install tox virtualenv - - name: Test build - run: "tox -e ${{ matrix.tox_env }}-cov-report" - - name: Send coverage report to codeclimate - uses: paambaati/codeclimate-action@v2.6.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@v1 - with: - env_vars: OS=${{ matrix.os }},PYTHON=${{ matrix.python }} - file: ./coverage.xml diff --git a/.github/workflows/pip-publish.yml b/.github/workflows/pip-publish.yml index a4803d54c..ae4cb3c7a 100644 --- a/.github/workflows/pip-publish.yml +++ b/.github/workflows/pip-publish.yml @@ -7,20 +7,38 @@ on: jobs: upload: runs-on: ubuntu-latest + + environment: + name: pypi.org + url: https://pypi.org/project/cookiecutter/ + steps: - - uses: actions/checkout@v2 + + - uses: actions/checkout@v3 + - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v3 + with: + python-version: 3.10 + + - 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: - python-version: "3.7" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install setuptools wheel twine - - name: Package project - run: python setup.py sdist bdist_wheel - - name: Upload distributions - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} - run: twine upload dist/* + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..a63151166 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,87 @@ +name: CI/CD Tests + +on: + push: + branches: + - master + - main + tags: + - "*" + pull_request: + branches: + - "*" + +jobs: + documentation_build_test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: 'recursive' + - uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install nox virtualenv + - name: Build docs + run: nox -s docs + - uses: actions/upload-artifact@v3 + with: + name: DocumentationHTML + path: docs/_build/html/ + linting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install nox virtualenv + - name: Lint + run: "nox -s lint" + 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" + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install nox virtualenv + - name: Project internals test build + run: "nox -p ${{ matrix.python }} -s tests" + - name: Project security test + run: "nox -p ${{ matrix.python }} -s safety_tests" + - name: Send coverage report to codeclimate + continue-on-error: true + uses: paambaati/codeclimate-action@v3.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 diff --git a/.gitignore b/.gitignore index 66b3c501a..c162866af 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ +.nox/ .coverage .coverage.* .cache @@ -69,5 +70,9 @@ target/ # PyEnv .python-version - .venv/ + +# OSX +.DS_Store +.AppleDouble +.LSOverride diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e0372d2b5..000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "docs/HelloCookieCutter1"] - path = docs/HelloCookieCutter1 - url = https://github.com/BruceEckel/HelloCookieCutter1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c1f2a0886..e5d1e7f52 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,33 +1,42 @@ --- repos: - repo: https://github.com/PyCQA/doc8 - rev: 0.8.1 + rev: 0.11.2 hooks: - id: doc8 name: doc8 - description: This hook runs doc8 for linting docs + description: This hook runs doc8 for linting docs. entry: python -m doc8 language: python files: \.rst$ require_serial: true - - repo: https://github.com/python/black.git - rev: 19.10b0 + - repo: https://github.com/psf/black.git + rev: 22.3.0 hooks: - id: black language_version: python3 exclude: ^(tests\/hooks-abort-render\/hooks|docs\/HelloCookieCutter1) - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.4.0 + rev: v4.2.0 hooks: - id: trailing-whitespace args: [--markdown-linebreak-ext=md] - id: mixed-line-ending - - id: check-byte-order-marker + - id: fix-byte-order-marker - id: check-executables-have-shebangs + - id: check-shebang-scripts-are-executable - id: check-merge-conflict - id: check-symlinks - - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.9 + - id: check-case-conflict + - id: check-docstring-first + - id: check-json + 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" + - repo: https://github.com/PyCQA/flake8 + rev: 4.0.1 hooks: - id: flake8 additional_dependencies: @@ -35,7 +44,11 @@ repos: - flake8-black - flake8-docstrings - repo: https://github.com/PyCQA/bandit - rev: 1.6.0 + rev: 1.7.4 hooks: - id: bandit args: [--ini, .bandit] + - repo: https://github.com/mgedmin/check-manifest + rev: "0.48" + hooks: + - id: check-manifest \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..988f6ffbf --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,19 @@ +# .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: . diff --git a/AUTHORS.md b/AUTHORS.md index d991d54fa..76ebc2622 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -2,171 +2,214 @@ ## Development Leads -* Audrey Feldroy ([@audreyfeldroy](https://github.com/audreyfeldroy)) -* Daniel Feldroy ([@pydanny](https://github.com/pydanny)) -* Raphael Pierzina ([@hackebrot](https://github.com/hackebrot)) - +- 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)) +- 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)) +- 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)) diff --git a/BACKERS.md b/BACKERS.md deleted file mode 100644 index 0c120e061..000000000 --- a/BACKERS.md +++ /dev/null @@ -1,9 +0,0 @@ -# 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 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 5b484056f..3043a6bab 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,5 +1,4 @@ # Code of Conduct -Everyone interacting in the Cookiecutter project's codebases, issue trackers, -chat rooms, and mailing lists is expected to follow the -[PyPA Code of Conduct](https://www.pypa.io/en/latest/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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b9a5dff4..283506e9b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,13 @@ # Contributing -Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. +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](#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 @@ -18,22 +19,27 @@ Report bugs at [https://github.com/cookiecutter/cookiecutter/issues](https://git 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. +- 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. +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. +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. +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 @@ -54,9 +60,9 @@ The best way to send feedback is to file an issue at [https://github.com/cookiec 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 :) +- 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 @@ -65,56 +71,58 @@ 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 -``` + ```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: +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 -mkvirtualenv cookiecutter -cd cookiecutter/ -python setup.py develop -``` + ```bash + cd cookiecutter/ + pip install -e . + ``` 4. Create a branch for local development: -```bash -git checkout -b name-of-your-bugfix-or-feature -``` + ```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 -``` + ```bash + pip install tox + tox + ``` -Please note that tox runs lint check automatically, since we have a test environment for it. + Please note that tox runs lint check automatically, since we have a test environment for it. -If you feel like running only the lint environment, please use the following command: + If you feel like running only the lint environment, please use the following command: -```bash -make lint -``` + ```bash + make lint + ``` -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: +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 -``` + ```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. + 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 -``` + ```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. @@ -125,34 +133,36 @@ git push origin name-of-your-bugfix-or-feature 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. +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 -``` +- 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 @@ -186,15 +196,16 @@ To run all tests using various versions of python in virtualenvs defined in tox. tox ``` -This configuration file setup the pytest-cov plugin and it is an additional dependency. It generate a coverage report after the tests. +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 tests with some versions of python, to do this the command is: +It is possible to test with specific versions of Python. To do this, the command is: ```bash -tox -e py36,pypy3 +tox -e py37,py38 ``` -Will run py.test with the python3.6 and pypy3 interpreters, for example. +This will run `py.test` with the `python3.7` and `python3.8` interpreters. ## Core Committer Guide @@ -202,93 +213,97 @@ Will run py.test with the python3.6 and pypy3 interpreters, for example. Core committers, use this section to: -* Guide your instinct and decisions as a core committer -* Limit the codebase from growing infinitely +- 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 +- 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 +- 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 +- 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 +- 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 +- 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 +- 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 +- 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 +- 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 +- 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 +- 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. +- 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`. +- 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 @@ -307,12 +322,12 @@ Due dates are flexible. Core committers can change them as needed. Note that Git How to number milestones: -* Follow semantic versioning. Look at: [http://semver.org](http://semver.org) +- 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. +- 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 @@ -321,23 +336,29 @@ 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. +- 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). +- 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. +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. diff --git a/HISTORY.md b/HISTORY.md index 132a1e028..cc11b368a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,10 +2,152 @@ History is important, but our current roadmap can be found [here](https://github.com/cookiecutter/cookiecutter/projects) -## 1.8.0 (Current master, in development) -* Do not modify this file, since 1.7.1 Changes are generated on Pull request -title and will be added before release. +## 2.1.2 (unreleased) + +## 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) @@ -14,7 +156,7 @@ title and will be added before release. ## 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. +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. @@ -84,7 +226,7 @@ Other Changes: * 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/audreyr/cookiecutter/blob/master/CODE_OF_CONDUCT.md) file to the project, thanks to [@andreagrandi](https://github.com/andreagrandi) (#1009) +* 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) @@ -261,7 +403,7 @@ Other Changes: * 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 [@audreyr](https://github.com/audreyr) for raising the issue and [@hackebrot](https://github.com/hackebrot)for the PR (#741, #802) +* 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) @@ -339,11 +481,11 @@ Other Changes: * 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 [@audreyr](https://github.com/audreyr) +* 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 [@audreyr](https://github.com/audreyr) +* 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) @@ -406,7 +548,7 @@ 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/audreyr/cookiecutter-pypackage), thanks to [@eliasdorneles](https://github.com/eliasdorneles)(#555). +* 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) @@ -529,7 +671,7 @@ The goal of this release was to allow for injection of extra context via the Coo 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), [@audreyr](https://github.com/audreyr) (#260). +* 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) @@ -569,12 +711,12 @@ Bug Fixes: Other Changes: -* [@audreyr](https://github.com/audreyr) formally accepted position as **BDFL of cookiecutter**. +* [@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 [@audreyr](https://github.com/audreyr). -* Generated apidocs from make docs, by [@audreyr](https://github.com/audreyr). +* 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 [@audreyr](https://github.com/audreyr). +* 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). @@ -681,7 +823,7 @@ Other changes: # Create project from the cookiecutter-pypackage/ template $ cookiecutter cookiecutter-pypackage/ # Create project from the cookiecutter-pypackage.git repo template - $ cookiecutter https://github.com/audreyr/cookiecutter-pypackage.git + $ cookiecutter https://github.com/audreyfeldroy/cookiecutter-pypackage.git ``` * Can now use Cookiecutter from Python as a package: @@ -693,7 +835,7 @@ Other changes: cookiecutter('cookiecutter-pypackage/') # Create project from the cookiecutter-pypackage.git repo template - cookiecutter('https://github.com/audreyr/cookiecutter-pypackage.git') + cookiecutter('https://github.com/audreyfeldroy/cookiecutter-pypackage.git') ``` * Internal refactor to remove any code that changes the working diff --git a/LICENSE b/LICENSE index 5e75b2cfd..9661fad7a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2013-2021, Audrey Feldroy +Copyright (c) 2013-2022, Audrey Roy Greenfeld All rights reserved. Redistribution and use in source and binary forms, with or diff --git a/MANIFEST.in b/MANIFEST.in index 2a2838a1d..4e36e27df 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,11 +1,20 @@ include AUTHORS.md +include CODE_OF_CONDUCT.md include CONTRIBUTING.md include HISTORY.md include LICENSE include README.md +exclude Makefile +exclude __main__.py +exclude .* +exclude codecov.yml +exclude test_requirements.txt +exclude tox.ini +exclude noxfile.py + recursive-include tests * recursive-exclude * __pycache__ recursive-exclude * *.py[co] - -recursive-include docs *.rst conf.py Makefile make.bat +recursive-exclude docs * +recursive-exclude logo * diff --git a/Makefile b/Makefile index 71ea835bd..293aedef6 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,28 @@ clean-tox: ## Remove tox testing artifacts @echo "+ $@" @rm -rf .tox/ +.PHONY: clean-nox +clean-nox: ## Remove nox testing artifacts + @echo "+ $@" + @rm -rf .nox/ + +.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 "+ $@" @@ -33,44 +55,40 @@ clean-pyc: ## Remove Python file artifacts @find . -type f -name '*.py[co]' -exec rm -f {} + @find . -name '*~' -exec rm -f {} + -.PHONY: clean -clean: clean-tox clean-build clean-pyc ## Remove all file artifacts +.PHONY: clean ## Remove all file artifacts +clean: clean-tox clean-build clean-pyc clean-nox clean-coverage clean-pytest clean-docs-build .PHONY: lint lint: ## Check code style @echo "+ $@" - @tox -e lint + @nox -s lint .PHONY: test test: ## Run tests quickly with the default Python @echo "+ $@" - @tox -e py + @nox -p 3.10 .PHONY: test-all test-all: ## Run tests on every Python version with tox @echo "+ $@" - @tox + @nox .PHONY: coverage coverage: ## Check code coverage quickly with the default Python @echo "+ $@" - @tox -e cov-report + @nox -s tests -p 3.10 @$(BROWSER) htmlcov/index.html .PHONY: docs docs: ## Generate Sphinx HTML documentation, including API docs @echo "+ $@" - @rm -f docs/cookiecutter.rst - @sphinx-apidoc -o docs/ cookiecutter - @rm -f docs/modules.rst - @$(MAKE) -C docs clean - @$(MAKE) -C docs html + @nox --non-interactive -s docs @$(BROWSER) docs/_build/html/index.html .PHONY: servedocs -servedocs: docs ## Rebuild docs automatically +servedocs: ## Rebuild docs automatically @echo "+ $@" - @watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . + @nox -s docs .PHONY: submodules submodules: ## Pull and update git submodules recursively @@ -81,19 +99,19 @@ submodules: ## Pull and update git submodules recursively .PHONY: release release: clean ## Package and upload release @echo "+ $@" - @python setup.py sdist bdist_wheel + @python -m build @twine upload -r $(PYPI_SERVER) dist/* .PHONY: sdist sdist: clean ## Build sdist distribution @echo "+ $@" - @python setup.py sdist + @python -m build --sdist @ls -l dist .PHONY: wheel -wheel: clean ## Build bdist_wheel distribution +wheel: clean ## Build wheel distribution @echo "+ $@" - @python setup.py bdist_wheel + @python -m build --wheel @ls -l dist .PHONY: help diff --git a/README.md b/README.md index 8e1e6673a..38de8e205 100644 --- a/README.md +++ b/README.md @@ -4,79 +4,114 @@ [![python](https://img.shields.io/pypi/pyversions/cookiecutter.svg)](https://pypi.org/project/cookiecutter/) [![Build Status](https://github.com/cookiecutter/cookiecutter/actions/workflows/main.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) -[![slack](https://img.shields.io/badge/cookiecutter-Join%20on%20Slack-green?style=flat&logo=slack)](https://join.slack.com/t/cookie-cutter/shared_invite/enQtNzI0Mzg5NjE5Nzk5LTRlYWI2YTZhYmQ4YmU1Y2Q2NmE1ZjkwOGM0NDQyNTIwY2M4ZTgyNDVkNjMxMDdhZGI5ZGE5YmJjM2M3ODJlY2U) +[![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) -A command-line utility that creates projects from **cookiecutters** (project -templates), e.g. creating a Python package project from a Python package project -template. +A command-line utility that creates projects from **cookiecutters** (project templates), e.g. creating a Python package project from a Python package project template. -* Documentation: [https://cookiecutter.readthedocs.io](https://cookiecutter.readthedocs.io) -* GitHub: [https://github.com/cookiecutter/cookiecutter](https://github.com/cookiecutter/cookiecutter) -* PyPI: [https://pypi.org/project/cookiecutter/](https://pypi.org/project/cookiecutter/) -* Free and open source software: [BSD license](https://github.com/cookiecutter/cookiecutter/blob/master/LICENSE) +- Documentation: [https://cookiecutter.readthedocs.io](https://cookiecutter.readthedocs.io) +- GitHub: [https://github.com/cookiecutter/cookiecutter](https://github.com/cookiecutter/cookiecutter) +- PyPI: [https://pypi.org/project/cookiecutter/](https://pypi.org/project/cookiecutter/) +- Free and open source software: [BSD license](https://github.com/cookiecutter/cookiecutter/blob/master/LICENSE) ![Cookiecutter](https://raw.githubusercontent.com/cookiecutter/cookiecutter/3ac078356adf5a1a72042dfe72ebfa4a9cd5ef38/logo/cookiecutter_medium.png) -We are proud to be an open source sponsor of -[PyCon 2016](https://us.pycon.org/2016/sponsors/). - ## Features -Did someone say features? - -* Cross-platform: Windows, Mac, and Linux are officially supported. -* You don't have to know/write Python code to use Cookiecutter -* Works with Python 3.6, 3.7, 3.8, 3.9 and PyPy3. -* Project templates can be in any programming language or markup format: +- Cross-platform: Windows, Mac, and Linux are officially supported. +- You don't have to know/write Python code to use Cookiecutter. +- Works with Python 3.7, 3.8, 3.9., 3.10 +- Project templates can be in any programming language or markup format: Python, JavaScript, Ruby, CoffeeScript, RST, Markdown, CSS, HTML, you name it. You can use multiple languages in the same project template. -* Simple command line usage: -```bash -# Create project from the cookiecutter-pypackage.git repo template -# You'll be prompted to enter values. -# Then it'll create your Python package in the current working directory, -# based on those values. -$ cookiecutter https://github.com/audreyfeldroy/cookiecutter-pypackage -# For the sake of brevity, repos on GitHub can just use the 'gh' prefix -$ cookiecutter gh:audreyfeldroy/cookiecutter-pypackage -``` +### For users of existing templates -* Use it at the command line with a local template: +- Simple command line usage: -```bash -# Create project in the current working directory, from the local -# cookiecutter-pypackage/ template -$ cookiecutter cookiecutter-pypackage/ -``` + ```bash + # Create project from the cookiecutter-pypackage.git repo template + # You'll be prompted to enter values. + # Then it'll create your Python package in the current working directory, + # based on those values. + $ cookiecutter https://github.com/audreyfeldroy/cookiecutter-pypackage + # For the sake of brevity, repos on GitHub can just use the 'gh' prefix + $ cookiecutter gh:audreyfeldroy/cookiecutter-pypackage + ``` -* Or use it from Python: +- Use it at the command line with a local template: -```py -from cookiecutter.main import cookiecutter + ```bash + # Create project in the current working directory, from the local + # cookiecutter-pypackage/ template + $ cookiecutter cookiecutter-pypackage/ + ``` -# Create project from the cookiecutter-pypackage/ template -cookiecutter('cookiecutter-pypackage/') +- Or use it from Python: -# Create project from the cookiecutter-pypackage.git repo template -cookiecutter('https://github.com/audreyfeldroy/cookiecutter-pypackage.git') -``` + ```py + from cookiecutter.main import cookiecutter -* Directory names and filenames can be templated. For example: + # Create project from the cookiecutter-pypackage/ template + cookiecutter('cookiecutter-pypackage/') -```py -{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}.py -``` + # Create project from the cookiecutter-pypackage.git repo template + cookiecutter('https://github.com/audreyfeldroy/cookiecutter-pypackage.git') + ``` -* Supports unlimited levels of directory nesting. -* 100% of templating is done with Jinja2. This includes file and directory names. -* Simply define your template variables in a ``cookiecutter.json`` file. For example: +- Unless you suppress it with `--no-input`, you are prompted for input: + - Prompts are the keys in `cookiecutter.json`. + - Default responses are the values in `cookiecutter.json`. + - Prompts are shown in order. +- Cross-platform support for `~/.cookiecutterrc` files: -```json -{ - "full_name": "Audrey Feldroy", + ```yaml + default_context: + full_name: "Audrey Roy Greenfeld" + email: "audreyr@gmail.com" + github_username: "audreyfeldroy" + cookiecutters_dir: "~/.cookiecutters/" + ``` + +- Cookiecutters (cloned Cookiecutter project templates) are put into `~/.cookiecutters/` by default, or cookiecutters_dir if specified. +- If you have already cloned a cookiecutter into `~/.cookiecutters/`, you can reference it by directory name: + + ```bash + # Clone cookiecutter-pypackage + $ cookiecutter gh:audreyfeldroy/cookiecutter-pypackage + # Now you can use the already cloned cookiecutter by name + $ cookiecutter cookiecutter-pypackage + ``` + +- You can use local cookiecutters, or remote cookiecutters directly from Git repos or from Mercurial repos on Bitbucket. +- Default context: specify key/value pairs that you want used as defaults whenever you generate a project. +- Inject extra context with command-line arguments: + + ```bash + cookiecutter --no-input gh:msabramo/cookiecutter-supervisor program_name=foobar startsecs=10 + ``` + +- Direct access to the Cookiecutter API allows for injection of extra context. +- Paths to local projects can be specified as absolute or relative. +- Projects generated to your current directory or to target directory if specified with `-o` option. + +### For template creators + +- Supports unlimited levels of directory nesting. +- 100% of templating is done with Jinja2. +- Both, directory names and filenames can be templated. + For example: + + ```py + {{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}.py + ``` +- Simply define your template variables in a `cookiecutter.json` file. + For example: + + ```json + { + "full_name": "Audrey Roy Greenfeld", "email": "audreyr@gmail.com", "project_name": "Complexity", "repo_name": "complexity", @@ -84,87 +119,41 @@ cookiecutter('https://github.com/audreyfeldroy/cookiecutter-pypackage.git') "release_date": "2013-07-10", "year": "2013", "version": "0.1.1" -} -``` - -* Unless you suppress it with ``--no-input``, you are prompted for input: - * Prompts are the keys in ``cookiecutter.json``. - * Default responses are the values in ``cookiecutter.json``. - * Prompts are shown in order. -* Cross-platform support for ``~/.cookiecutterrc`` files: - -```yaml -default_context: - full_name: "Audrey Feldroy" - email: "audreyr@gmail.com" - github_username: "audreyfeldroy" -cookiecutters_dir: "~/.cookiecutters/" -``` - -* Cookiecutters (cloned Cookiecutter project templates) are put into -``~/.cookiecutters/`` by default, or cookiecutters_dir if specified. -* If you have already cloned a cookiecutter into ``~/.cookiecutters/``, -you can reference it by directory name: - -```bash -# Clone cookiecutter-pypackage -$ cookiecutter gh:audreyfeldroy/cookiecutter-pypackage -# Now you can use the already cloned cookiecutter by name -$ cookiecutter cookiecutter-pypackage -``` - -* You can use local cookiecutters, or remote cookiecutters directly from Git -repos or from Mercurial repos on Bitbucket. -* Default context: specify key/value pairs that you want used as defaults -whenever you generate a project. -* Inject extra context with command-line arguments: - -```bash -cookiecutter --no-input gh:msabramo/cookiecutter-supervisor program_name=foobar startsecs=10 -``` - -* Direct access to the Cookiecutter API allows for injection of extra context. -* Pre- and post-generate hooks: Python or shell scripts to run before or after -generating a project. -* Paths to local projects can be specified as absolute or relative. -* Projects generated to your current directory or to target directory if -specified with `-o` option. + } + ``` +- Pre- and post-generate hooks: Python or shell scripts to run before or after generating a project. ## Available Cookiecutters -Making great cookies takes a lot of cookiecutters and contributors. We're so -pleased that there are many Cookiecutter project templates to choose from. We -hope you find a cookiecutter that is just right for your needs. +Making great cookies takes a lot of cookiecutters and contributors. +We're so pleased that there are many Cookiecutter project templates to choose from. +We hope you find a cookiecutter that is just right for your needs. -## A Pantry Full of Cookiecutters +### A Pantry Full of Cookiecutters -The best place to start searching for specific and ready to use cookiecutter -template is [Github search](https://github.com/search?q=cookiecutter&type=Repositories). +The best place to start searching for specific and ready to use cookiecutter template is [Github search](https://github.com/search?q=cookiecutter&type=Repositories). Just type `cookiecutter` and you will discover over 4000 related repositories. -We also recommend you to check related GitHub topics. For general search use -[cookiecutter-template](https://github.com/topics/cookiecutter-template). -For specific topics try to use `cookiecutter-yourtopic`, like -`cookiecutter-python` or `cookiecutter-datascience`. This is a new GitHub feature, -so not all active repositories use it at the moment. +We also recommend you to check related GitHub topics. +For general search use [cookiecutter-template](https://github.com/topics/cookiecutter-template). +For specific topics try to use `cookiecutter-yourtopic`, like `cookiecutter-python` or `cookiecutter-datascience`. +This is a new GitHub feature, so not all active repositories use it at the moment. -If you are template developer please add related -[topics](https://help.github.com/en/github/administering-a-repository/classifying-your-repository-with-topics) -with `cookiecutter` prefix to you repository. We believe it will make it more -discoverable. You are almost not limited in topics amount, use it! +If you are template developer please add related [topics](https://help.github.com/en/github/administering-a-repository/classifying-your-repository-with-topics) with `cookiecutter` prefix to you repository. +We believe it will make it more discoverable. +You are almost not limited in topics amount, use it! -## Cookiecutter Specials +### Cookiecutter Specials These Cookiecutters are maintained by the cookiecutter team: -* [cookiecutter-pypackage](https://github.com/audreyfeldroy/cookiecutter-pypackage): -[@audreyfeldroy's](https://github.com/audreyfeldroy) ultimate Python package project template. -* [cookiecutter-django](https://github.com/pydanny/cookiecutter-django): -A bleeding edge Django project template with Bootstrap 4, customizable users app, -starter templates, working user registration, celery setup, and much more. -* [cookiecutter-pytest-plugin](https://github.com/pytest-dev/cookiecutter-pytest-plugin): -Minimal Cookiecutter template for authoring [pytest](https://docs.pytest.org/) -plugins that help you to write better programs. +- [cookiecutter-pypackage](https://github.com/audreyfeldroy/cookiecutter-pypackage): + ultimate Python package project template by [@audreyfeldroy's](https://github.com/audreyfeldroy). +- [cookiecutter-django](https://github.com/pydanny/cookiecutter-django): + a framework for jumpstarting production-ready Django projects quickly. + It is bleeding edge with Bootstrap 5, customizable users app, starter templates, working user registration, celery setup, and much more. +- [cookiecutter-pytest-plugin](https://github.com/pytest-dev/cookiecutter-pytest-plugin): + Minimal Cookiecutter template for authoring [pytest](https://docs.pytest.org/) plugins that help you to write better programs. ## Community @@ -173,65 +162,59 @@ We are always welcome and invite you to participate. Stuck? Try one of the following: -* See the [Troubleshooting](https://cookiecutter.readthedocs.io/en/latest/troubleshooting.html) page. -* Ask for help on [Stack Overflow](https://stackoverflow.com/questions/tagged/cookiecutter). -* You are strongly encouraged to -[file an issue](https://github.com/cookiecutter/cookiecutter/issues?q=is%3Aopen) -about the problem, even if it's just "I can't get it to work on this cookiecutter" -with a link to your cookiecutter. Don't worry about naming/pinpointing the issue -properly. -* Ask for help on [Slack](https://join.slack.com/t/cookie-cutter/shared_invite/enQtNzI0Mzg5NjE5Nzk5LTRlYWI2YTZhYmQ4YmU1Y2Q2NmE1ZjkwOGM0NDQyNTIwY2M4ZTgyNDVkNjMxMDdhZGI5ZGE5YmJjM2M3ODJlY2U) -if you must (but please try one of the other options first, so that others -can benefit from the discussion). +- See the [Troubleshooting](https://cookiecutter.readthedocs.io/en/latest/troubleshooting.html) page. +- Ask for help on [Stack Overflow](https://stackoverflow.com/questions/tagged/cookiecutter). +- You are strongly encouraged to [file an issue](https://github.com/cookiecutter/cookiecutter/issues?q=is%3Aopen) about the problem. + Do it even if it's just "I can't get it to work on this cookiecutter" with a link to your cookiecutter. + Don't worry about naming/pinpointing the issue properly. +- Ask for help on [Discord](https://discord.gg/9BrxzPKuEW) if you must (but please try one of the other options first, so that others can benefit from the discussion). Development on Cookiecutter is community-driven: -* Huge thanks to all the [contributors](AUTHORS.md) who have pitched in to help -make Cookiecutter an even better tool. -* Everyone is invited to contribute. Read the -[contributing instructions](CONTRIBUTING.md), then get started. -* Connect with other Cookiecutter contributors and users on -[Slack](https://join.slack.com/t/cookie-cutter/shared_invite/enQtNzI0Mzg5NjE5Nzk5LTRlYWI2YTZhYmQ4YmU1Y2Q2NmE1ZjkwOGM0NDQyNTIwY2M4ZTgyNDVkNjMxMDdhZGI5ZGE5YmJjM2M3ODJlY2U) -(note: due to work and commitments, a core committer might not always be available) +- Huge thanks to all the [contributors](AUTHORS.md) who have pitched in to help make Cookiecutter an even better tool. +- Everyone is invited to contribute. + Read the [contributing instructions](CONTRIBUTING.md), then get started. +- Connect with other Cookiecutter contributors and users on [Discord](https://discord.gg/9BrxzPKuEW) + (note: due to work and other commitments, a core committer might not always be available) -Encouragement is unbelievably motivating. If you want more work done on -Cookiecutter, show support: +Encouragement is unbelievably motivating. +If you want more work done on Cookiecutter, show support: -* Thank a core committer for their efforts. -* Star [Cookiecutter on GitHub](https://github.com/cookiecutter/cookiecutter). -* [Support this project](#support-this-project) +- Thank a core committer for their efforts. +- Star [Cookiecutter on GitHub](https://github.com/cookiecutter/cookiecutter). +- [Support this project](#support-this-project) Got criticism or complaints? -* [File an issue](https://github.com/cookiecutter/cookiecutter/issues?q=is%3Aopen) -so that Cookiecutter can be improved. Be friendly and constructive about what -could be better. Make detailed suggestions. -* **Keep us in the loop so that we can help.** For example, if you are -discussing problems with Cookiecutter on a mailing list, -[file an issue](https://github.com/cookiecutter/cookiecutter/issues?q=is%3Aopen) -where you link to the discussion thread and/or cc at least 1 core committer on the email. -* Be encouraging. A comment like "This function ought to be rewritten like this" -is much more likely to result in action than a comment like "Eww, look how bad -this function is." +- [File an issue](https://github.com/cookiecutter/cookiecutter/issues?q=is%3Aopen) so that Cookiecutter can be improved. + Be friendly and constructive about what could be better. + Make detailed suggestions. +- **Keep us in the loop so that we can help.** + For example, if you are discussing problems with Cookiecutter on a mailing list, [file an issue](https://github.com/cookiecutter/cookiecutter/issues?q=is%3Aopen) where you link to the discussion thread and/or cc at least 1 core committer on the email. +- Be encouraging. + A comment like "This function ought to be rewritten like this" is much more likely to result in action than a comment like "Eww, look how bad this function is." Waiting for a response to an issue/question? -* Be patient and persistent. All issues are on the core committer team's radar -and will be considered thoughtfully, but we have a lot of issues to work through. -If urgent, it's fine to ping a core committer in the issue with a reminder. -* Ask others to comment, discuss, review, etc. -* Search the Cookiecutter repo for issues related to yours. -* Need a fix/feature/release/help urgently, and can't wait? -[@audreyfeldroy](https://github.com/audreyfeldroy) is available for hire for consultation -or custom development. +- Be patient and persistent. All issues are on the core committer team's radar and will be considered thoughtfully, but we have a lot of issues to work through. + If urgent, it's fine to ping a core committer in the issue with a reminder. +- Ask others to comment, discuss, review, etc. +- Search the Cookiecutter repo for issues related to yours. +- Need a fix/feature/release/help urgently, and can't wait? + [@audreyfeldroy](https://github.com/audreyfeldroy) is available for hire for consultation or custom development. ## Support This Project -This project is run by volunteers. Shortly we will be providing means for -organizations and individuals to support the project. +This project is run by volunteers. +Shortly we will be providing means for organizations and individuals to support the project. ## Code of Conduct -Everyone interacting in the Cookiecutter project's codebases, issue trackers, -chat rooms, and mailing lists is expected to follow the -[PyPA Code of Conduct](https://www.pypa.io/en/latest/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. + +## Creator / Leader + +This project was created and is led by [Audrey Roy Greenfeld](https://github.com/audreyfeldroy). + +She is supported by a team of maintainers. diff --git a/case_studies.md b/case_studies.md deleted file mode 100644 index db101b979..000000000 --- a/case_studies.md +++ /dev/null @@ -1,25 +0,0 @@ -# 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. diff --git a/cookiecutter/__init__.py b/cookiecutter/__init__.py index 35bff4753..a54486529 100644 --- a/cookiecutter/__init__.py +++ b/cookiecutter/__init__.py @@ -1,4 +1,2 @@ -# -*- coding: utf-8 -*- """Main package for Cookiecutter.""" - -__version__ = '2.0.0-alpha+1.6.0' +__version__ = "2.1.2.dev0" diff --git a/cookiecutter/cli.py b/cookiecutter/cli.py index 991e62c50..ebc8dd0db 100644 --- a/cookiecutter/cli.py +++ b/cookiecutter/cli.py @@ -25,19 +25,18 @@ def version_msg(): """Return the Cookiecutter version, location and Python powering it.""" - python_version = sys.version[:3] + python_version = sys.version location = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - message = 'Cookiecutter %(version)s from {} (Python {})' - return message.format(location, python_version) + return f"Cookiecutter {__version__} from {location} (Python {python_version})" def validate_extra_context(ctx, param, value): """Validate extra context.""" - for s in value: - if '=' not in s: + for string in value: + if '=' not in string: raise click.BadParameter( - 'EXTRA_CONTEXT should contain items of the form key=value; ' - "'{}' doesn't match that form".format(s) + 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') @@ -51,8 +50,8 @@ def list_installed_templates(default_config, passed_config_file): cookiecutter_folder = config.get('cookiecutters_dir') if not os.path.exists(cookiecutter_folder): click.echo( - 'Error: Cannot list installed templates. Folder does not exist: ' - '{}'.format(cookiecutter_folder) + f"Error: Cannot list installed templates. " + f"Folder does not exist: {cookiecutter_folder}" ) sys.exit(-1) @@ -63,9 +62,9 @@ def list_installed_templates(default_config, passed_config_file): os.path.join(cookiecutter_folder, folder, 'cookiecutter.json') ) ] - click.echo('{} installed templates: '.format(len(template_names))) + click.echo(f'{len(template_names)} installed templates: ') for name in template_names: - click.echo(' * {}'.format(name)) + click.echo(f' * {name}') @click.command(context_settings=dict(help_option_names=['-h', '--help'])) @@ -75,10 +74,14 @@ def list_installed_templates(default_config, passed_config_file): @click.option( '--no-input', is_flag=True, - help='Do not prompt for parameters and only use cookiecutter.json file content', + 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', + '-c', + '--checkout', + help='branch, tag or commit to checkout after git clone', ) @click.option( '--directory', @@ -91,7 +94,8 @@ def list_installed_templates(default_config, passed_config_file): @click.option( '--replay', is_flag=True, - help='Do not prompt for parameters and only use information entered previously', + 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', @@ -142,6 +146,11 @@ def list_installed_templates(default_config, passed_config_file): @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, @@ -159,6 +168,7 @@ def main( accept_hooks, replay_file, list_installed, + keep_project_on_failure, ): """Create a project from a Cookiecutter project template (TEMPLATE). @@ -203,6 +213,7 @@ def main( directory=directory, skip_if_file_exists=skip_if_file_exists, accept_hooks=_accept_hooks, + keep_project_on_failure=keep_project_on_failure, ) except ( ContextDecodingException, @@ -217,11 +228,11 @@ def main( click.echo(e) sys.exit(1) except UndefinedVariableInTemplate as undefined_err: - click.echo('{}'.format(undefined_err.message)) - click.echo('Error message: {}'.format(undefined_err.error.message)) + 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('Context: {}'.format(context_str)) + click.echo(f'Context: {context_str}') sys.exit(1) diff --git a/cookiecutter/config.py b/cookiecutter/config.py index 90483f532..0d0fa8c7e 100644 --- a/cookiecutter/config.py +++ b/cookiecutter/config.py @@ -55,9 +55,7 @@ def merge_configs(default, overwrite): 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( - 'Config file {} does not exist.'.format(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: @@ -65,7 +63,7 @@ def get_config(config_path): yaml_dict = yaml.safe_load(file_handle) except yaml.YAMLError as e: raise InvalidConfiguration( - 'Unable to parse YAML file {}.'.format(config_path) + f'Unable to parse YAML file {config_path}.' ) from e config_dict = merge_configs(DEFAULT_CONFIG, yaml_dict) diff --git a/cookiecutter/context.py b/cookiecutter/context.py index 9a51cf473..960c8cfb1 100644 --- a/cookiecutter/context.py +++ b/cookiecutter/context.py @@ -14,6 +14,7 @@ https://github.com/hackebrot/cookiecutter/tree/new-context-format """ +import shutil import collections import json @@ -35,7 +36,6 @@ DEFAULT_PROMPT = 'Please enter a value for "{variable.name}"' - REGEX_COMPILE_FLAGS = { 'ascii': re.ASCII, 'debug': re.DEBUG, @@ -154,13 +154,16 @@ def prompt_uuid(variable, default): def prompt_json(variable, default): """Prompts the user for a JSON entry.""" # The JSON object from cookiecutter.json might be very large - # We only show 'default' + # We only show 'default' default_json = 'default' def process_json(user_value): try: - return json.loads(user_value, object_pairs_hook=collections.OrderedDict,) + return json.loads( + user_value, + object_pairs_hook=collections.OrderedDict, + ) except ValueError: # json.decoder.JSONDecodeError raised in Python 3.5, 3.6 # but it inherits from ValueError which is raised in Python 3.4 @@ -440,7 +443,8 @@ def __init__(self, name: str, type: str, **info): def __repr__(self): """Provide a representation with variable name.""" return "<{class_name} {variable_name}>".format( - class_name=self.__class__.__name__, variable_name=self.name, + class_name=self.__class__.__name__, + variable_name=self.name, ) def __str__(self): @@ -475,7 +479,7 @@ def __init__(self, template, requires=None, extensions=None, **kwargs): self.extensions = extensions if self.requirements: - self.cookiecutter_version = self.requirements.get('cookiecutter') + self.cookiecutter_version = self.requirements.get('cookiecutter', None) if self.cookiecutter_version: validate_requirement( self.cookiecutter_version, @@ -503,7 +507,8 @@ def __init__(self, template, requires=None, extensions=None, **kwargs): def __repr__(self): """Provide a classname with template name.""" return "<{class_name} {template_name}>".format( - class_name=self.__class__.__name__, template_name=self.name, + class_name=self.__class__.__name__, + template_name=self.name, ) def __iter__(self): @@ -598,7 +603,7 @@ def jinja_render(string): # for prompt esthetics if verbose: - width, _ = click.get_terminal_size() + width, _ = shutil.get_terminal_size() click.echo('-' * width) # updating the skipping variables for the continuation diff --git a/cookiecutter/environment.py b/cookiecutter/environment.py index 3997963cf..7fd52e056 100644 --- a/cookiecutter/environment.py +++ b/cookiecutter/environment.py @@ -4,7 +4,7 @@ from cookiecutter.exceptions import UnknownExtension -class ExtensionLoaderMixin(object): +class ExtensionLoaderMixin: """Mixin providing sane loading of extensions specified in a given context. The context is being extracted from the keyword arguments before calling @@ -32,9 +32,9 @@ def __init__(self, **kwargs): extensions = default_extensions + self._read_extensions(context) try: - super(ExtensionLoaderMixin, self).__init__(extensions=extensions, **kwargs) + super().__init__(extensions=extensions, **kwargs) except ImportError as err: - raise UnknownExtension('Unable to load extension: {}'.format(err)) + raise UnknownExtension(f'Unable to load extension: {err}') from err @staticmethod def _read_extensions(context): @@ -63,4 +63,4 @@ def __init__(self, **kwargs): Also loading extensions defined in cookiecutter.json's _extensions key. """ - super(StrictEnvironment, self).__init__(undefined=StrictUndefined, **kwargs) + super().__init__(undefined=StrictUndefined, **kwargs) diff --git a/cookiecutter/exceptions.py b/cookiecutter/exceptions.py index d3d4846b1..ea2416e92 100644 --- a/cookiecutter/exceptions.py +++ b/cookiecutter/exceptions.py @@ -124,15 +124,15 @@ def __init__(self, message, error, context): def __str__(self): """Text representation of UndefinedVariableInTemplate.""" return ( - "{self.message}. " - "Error message: {self.error.message}. " - "Context: {self.context}" - ).format(**locals()) + f"{self.message}. " + f"Error message: {self.error.message}. " + f"Context: {self.context}" + ) class UnknownExtension(CookiecutterException): """ - Exception for un-importable extention. + Exception for un-importable extension. Raised when an environment is unable to import a required extension. """ diff --git a/cookiecutter/extensions.py b/cookiecutter/extensions.py index e3a42b244..6a3161aba 100644 --- a/cookiecutter/extensions.py +++ b/cookiecutter/extensions.py @@ -13,7 +13,7 @@ class JsonifyExtension(Extension): def __init__(self, environment): """Initialize the extension with the given environment.""" - super(JsonifyExtension, self).__init__(environment) + super().__init__(environment) def jsonify(obj): return json.dumps(obj, sort_keys=True, indent=4) @@ -26,7 +26,7 @@ class RandomStringExtension(Extension): def __init__(self, environment): """Jinja2 Extension Constructor.""" - super(RandomStringExtension, self).__init__(environment) + super().__init__(environment) def random_ascii_string(length, punctuation=False): if punctuation: @@ -43,7 +43,7 @@ class SlugifyExtension(Extension): def __init__(self, environment): """Jinja2 Extension constructor.""" - super(SlugifyExtension, self).__init__(environment) + super().__init__(environment) def slugify(value, **kwargs): """Slugifies the value.""" @@ -57,7 +57,7 @@ class UUIDExtension(Extension): def __init__(self, environment): """Jinja2 Extension constructor.""" - super(UUIDExtension, self).__init__(environment) + super().__init__(environment) def uuid4(): """Generate UUID4.""" diff --git a/cookiecutter/find.py b/cookiecutter/find.py index 054e286f4..409e4ce9a 100644 --- a/cookiecutter/find.py +++ b/cookiecutter/find.py @@ -1,31 +1,27 @@ """Functions for finding Cookiecutter templates and other components.""" import logging import os +from pathlib import Path from cookiecutter.exceptions import NonTemplatedInputDirException logger = logging.getLogger(__name__) -def find_template(repo_dir): - """Determine which child directory of `repo_dir` is the project template. +def find_template(repo_dir: "os.PathLike[str]") -> Path: + """Determine which child directory of ``repo_dir`` is the project template. :param repo_dir: Local directory of newly cloned repo. - :returns project_template: Relative path to project template. + :return: Relative path to project template. """ logger.debug('Searching %s for the project template.', repo_dir) - repo_dir_contents = os.listdir(repo_dir) - - project_template = None - for item in repo_dir_contents: - if 'cookiecutter' in item and '{{' in item and '}}' in item: - project_template = item + for str_path in os.listdir(repo_dir): + if 'cookiecutter' in str_path and '{{' in str_path and '}}' in str_path: + project_template = Path(repo_dir, str_path) break - - if project_template: - project_template = os.path.join(repo_dir, project_template) - logger.debug('The project template appears to be %s', project_template) - return project_template else: raise NonTemplatedInputDirException + + logger.debug('The project template appears to be %s', project_template) + return project_template diff --git a/cookiecutter/generate.py b/cookiecutter/generate.py index b7b0122f3..8ca140e39 100644 --- a/cookiecutter/generate.py +++ b/cookiecutter/generate.py @@ -6,9 +6,9 @@ import shutil import warnings from collections import OrderedDict - +from pathlib import Path from binaryornot.check import is_binary -from jinja2 import FileSystemLoader +from jinja2 import FileSystemLoader, Environment from jinja2.exceptions import TemplateSyntaxError, UndefinedError from cookiecutter.environment import StrictEnvironment @@ -68,9 +68,13 @@ def apply_overwrites_to_context(context, overwrite_context): context_value.insert(0, overwrite) else: raise ValueError( - "{} provided for choice variable {}, but the " - "choices are {}.".format(overwrite, variable, context_value) + f"{overwrite} provided for choice variable {variable}, " + f"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) + context[variable] = context_value else: # Simply overwrite the value for this variable context[variable] = overwrite @@ -121,7 +125,7 @@ def resolve_changed_variable_names(context, variables_to_resolve): ) # noqa elif isinstance(variable[field_name], list): - # a choices field could have an str item to update + # a choices field could have a str item to update for i, item in enumerate(variable[field_name]): if isinstance(item, str): if var_name_to_resolve in item: @@ -159,20 +163,17 @@ def apply_overwrites_to_context_v2(context, extra_context): Changing the 'name' field requires a special syntax. Because the algorithm chosen to find a variable’s dictionary entry in the variables list of OrderDicts uses the variable’s ‘name’ field; it could not be used to - simultaneously hold a new ‘name’ field value. Therefore the following - extra context dictionary entry snytax was introduced to allow the ‘name’ + simultaneously hold a new ‘name’ field value. Therefor the following + extra context dictionary entry sytax was introduced to allow the ‘name’ field of a variable to be changed: - { - 'name': 'CURRENT_VARIABLE_NAME::NEW_VARIABLE_NAME', - } + { 'name': 'CURRENT_VARIABLE_NAME::NEW_VARIABLE_NAME',} So, for example, to change a variable’s ‘name’ field from ‘director_credit’ to ‘producer_credit’, would require: - { - 'name': 'director_credit::producer_credit', - } + { 'name': 'director_credit::producer_credit', } + Removing a Field from a Variable -------------------------------- @@ -182,10 +183,8 @@ def apply_overwrites_to_context_v2(context, extra_context): In order to accomplish this a remove field token is used in the extra context as follows: - { - 'name': 'director_cut', - 'skip_if': '<>', - } + { 'name': 'director_cut', + 'skip_if': '<>', } In the example above, the extra context overwrite results in the variable named ‘director_cut’ having it’s ‘skip_if’ field removed. @@ -310,10 +309,10 @@ def generate_context( full_fpath = os.path.abspath(context_file) json_exc_message = str(e) our_exc_message = ( - 'JSON decoding error while loading "{0}". Decoding' - ' error details: "{1}"'.format(full_fpath, json_exc_message) + f"JSON decoding error while loading '{full_fpath}'. " + f"Decoding error details: '{json_exc_message}'" ) - raise ContextDecodingException(our_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] @@ -402,7 +401,7 @@ def generate_file(project_dir, infile, context, env, skip_if_file_exists=False): # Detect original file newline to output the rendered file # note: newline='' ensures newlines are not converted - with open(infile, 'r', encoding='utf-8', newline='') as rd: + with open(infile, encoding='utf-8', newline='') as rd: rd.readline() # Read the first line to load 'newlines' value # Use `_new_lines` overwrite from context, if configured. @@ -421,19 +420,23 @@ def generate_file(project_dir, infile, context, env, skip_if_file_exists=False): def render_and_create_dir( - dirname, context, output_dir, environment, overwrite_if_exists=False + 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 = os.path.normpath(os.path.join(output_dir, rendered_dirname)) + 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 = os.path.exists(dir_to_create) + output_dir_exists = dir_to_create.exists() if output_dir_exists: if overwrite_if_exists: @@ -441,7 +444,7 @@ def render_and_create_dir( 'Output directory %s already exists, overwriting it', dir_to_create ) else: - msg = 'Error: "{}" directory already exists'.format(dir_to_create) + msg = f'Error: "{dir_to_create}" directory already exists' raise OutputDirExistsException(msg) else: make_sure_path_exists(dir_to_create) @@ -490,6 +493,7 @@ def generate_files( 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. @@ -498,7 +502,11 @@ def generate_files( :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 """ template_dir = find_template(repo_dir) logger.debug('Generating project from %s...', template_dir) @@ -514,8 +522,8 @@ def generate_files( unrendered_dir, context, output_dir, env, overwrite_if_exists ) except UndefinedError as err: - msg = "Unable to create project directory '{}'".format(unrendered_dir) - raise UndefinedVariableInTemplate(msg, err, context) + 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 @@ -529,7 +537,7 @@ def generate_files( # if we created the output directory, then it's ok to remove it # if rendering fails - delete_project_on_failure = output_directory_created + delete_project_on_failure = output_directory_created and not keep_project_on_failure if accept_hooks: _run_hook_from_repo_dir( @@ -537,7 +545,7 @@ def generate_files( ) with work_in(template_dir): - env.loader = FileSystemLoader('.') + env.loader = FileSystemLoader(['.', '../templates']) for root, dirs, files in os.walk('.'): # We must separate the two types of dirs into different lists. @@ -552,6 +560,7 @@ def generate_files( # 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) @@ -561,6 +570,12 @@ def generate_files( 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 @@ -576,8 +591,8 @@ def generate_files( if delete_project_on_failure: rmtree(project_dir) _dir = os.path.relpath(unrendered_dir, output_dir) - msg = "Unable to create directory '{}'".format(_dir) - raise UndefinedVariableInTemplate(msg, err, context) + 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)) @@ -598,8 +613,8 @@ def generate_files( except UndefinedError as err: if delete_project_on_failure: rmtree(project_dir) - msg = "Unable to create file '{}'".format(infile) - raise UndefinedVariableInTemplate(msg, err, context) + msg = f"Unable to create file '{infile}'" + raise UndefinedVariableInTemplate(msg, err, context) from err if accept_hooks: _run_hook_from_repo_dir( diff --git a/cookiecutter/hooks.py b/cookiecutter/hooks.py index b6a31a1e0..8551df32d 100644 --- a/cookiecutter/hooks.py +++ b/cookiecutter/hooks.py @@ -83,14 +83,14 @@ def run_script(script_path, cwd='.'): exit_status = proc.wait() if exit_status != EXIT_SUCCESS: raise FailedHookException( - 'Hook script failed (exit status: {})'.format(exit_status) + f'Hook script failed (exit status: {exit_status})' ) - except OSError as os_error: - if os_error.errno == errno.ENOEXEC: + except OSError as err: + if err.errno == errno.ENOEXEC: raise FailedHookException( 'Hook script failed, might be an empty file or missing a shebang' - ) - raise FailedHookException('Hook script failed (error: {})'.format(os_error)) + ) from err + raise FailedHookException(f'Hook script failed (error: {err})') from err def run_script_with_context(script_path, cwd, context): @@ -102,16 +102,44 @@ def run_script_with_context(script_path, cwd, context): """ _, extension = os.path.splitext(script_path) - with open(script_path, 'r', encoding='utf-8') as file: + with open(script_path, encoding='utf-8') as file: contents = file.read() + temp_name = None # Just to make sure it's defined in this scope. with tempfile.NamedTemporaryFile(delete=False, mode='wb', suffix=extension) as temp: env = StrictEnvironment(context=context, keep_trailing_newline=True) template = env.from_string(contents) output = template.render(**context) - temp.write(output.encode('utf-8')) - - run_script(temp.name, cwd) + debug_hooks_path = os.getenv('COOKIECUTTER_DEBUG_HOOKS', None) + if debug_hooks_path: + import pathlib + + debug_hooks_path = pathlib.Path(debug_hooks_path) + if not debug_hooks_path.exists(): + debug_hooks_path = tempfile.gettempdir() + os.environ['COOKIECUTTER_DEBUG_HOOKS'] = debug_hooks_path + with tempfile.NamedTemporaryFile( + delete=False, + mode='wb', + suffix=extension, + dir=debug_hooks_path, + prefix=os.path.basename(_) + '+', + ) as debug_temp: + debug_temp = pathlib.Path(debug_temp.name) + debug_temp = pathlib.Path( + os.path.join( + debug_temp.parent, + debug_temp.stem.split('+')[0] + debug_temp.suffix, + ) + ) + debug_temp.write_text(output, encoding='utf-8') + temp_name = str(debug_temp) + sys.stderr.write(f"DEBUG: Hook {script_path} rendered to {debug_temp}") + else: + temp.write(output.encode('utf-8')) + temp_name = temp.name + + run_script(temp_name, cwd) def run_hook(hook_name, project_dir, context): diff --git a/cookiecutter/main.py b/cookiecutter/main.py index 4328ab080..5fdabd086 100644 --- a/cookiecutter/main.py +++ b/cookiecutter/main.py @@ -4,8 +4,10 @@ The code in this module is also a good example of how to use Cookiecutter as a library rather than a script. """ +from copy import copy import logging import os +import sys from cookiecutter.config import get_user_config from cookiecutter.exceptions import InvalidModeException @@ -35,6 +37,7 @@ def cookiecutter( 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. @@ -42,7 +45,9 @@ def cookiecutter( :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: Prompt the user at command line for manual configuration? + :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 @@ -54,6 +59,8 @@ def cookiecutter( :param password: The password to use when extracting the repository. :param directory: Relative path to a cookiecutter template in a repository. :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 = ( @@ -63,7 +70,8 @@ def cookiecutter( raise InvalidModeException(err_msg) config_dict = get_user_config( - config_file=config_file, default_config=default_config, + config_file=config_file, + default_config=default_config, ) repo_dir, cleanup = determine_repo_dir( @@ -75,54 +83,72 @@ def cookiecutter( password=password, directory=directory, ) - template_name = os.path.basename(os.path.abspath(repo_dir)) - - if replay: - if isinstance(replay, bool): - context = load(config_dict['replay_dir'], template_name) + import_patch = _patch_import_path_for_repo(repo_dir) + + with import_patch: + if replay: + if isinstance(replay, bool): + context = load(config_dict['replay_dir'], template_name) + else: + path, template_name = os.path.split(os.path.splitext(replay)[0]) + context = load(path, template_name) else: - path, template_name = os.path.split(os.path.splitext(replay)[0]) - context = load(path, template_name) - else: - context_file = os.path.join(repo_dir, 'cookiecutter.json') - logger.debug('context_file is %s', context_file) - - context = generate_context( - context_file=context_file, - default_context=config_dict['default_context'], - extra_context=extra_context, - ) + context_file = os.path.join(repo_dir, 'cookiecutter.json') + logger.debug('context_file is %s', context_file) - # prompt the user to manually configure at the command line. - # except when 'no-input' flag is set - if infer_schema_version(context['cookiecutter']) in ['2.0']: - context['cookiecutter'] = load_context( - context[u'cookiecutter'], no_input=no_input, verbose=True + context = generate_context( + context_file=context_file, + default_context=config_dict['default_context'], + extra_context=extra_context, ) - else: - context['cookiecutter'] = prompt_for_config(context, no_input) - - # 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) - - dump(config_dict['replay_dir'], template_name, context) - # Create project from local context and project template. - 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, - ) + # prompt the user to manually configure at the command line. + # except when 'no-input' flag is set + if infer_schema_version(context['cookiecutter']) in ['2.0']: + context['cookiecutter'] = load_context( + context[u'cookiecutter'], no_input=no_input, verbose=True + ) + else: + context['cookiecutter'] = prompt_for_config(context, no_input) + + # include template dir or url in the context dict + context['cookiecutter']['_template'] = template + + # include repo dir or url in the context dict + context['cookiecutter']['_repo_dir'] = repo_dir + + # include output+dir in the context dict + context['cookiecutter']['_output_dir'] = os.path.abspath(output_dir) + + dump(config_dict['replay_dir'], template_name, context) + + # Create project from local context and project template. + 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) return result + + +class _patch_import_path_for_repo: + def __init__(self, repo_dir): + self._repo_dir = 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 diff --git a/cookiecutter/prompt.py b/cookiecutter/prompt.py index dfb8f3218..f4c19c97a 100644 --- a/cookiecutter/prompt.py +++ b/cookiecutter/prompt.py @@ -1,4 +1,5 @@ """Functions for prompting the user for project info.""" +import functools import json from collections import OrderedDict @@ -15,20 +16,23 @@ def read_user_variable(var_name, default_value): :param str var_name: Variable of the context to query the user :param default_value: Value that will be returned if no input happens """ - # Please see https://click.palletsprojects.com/en/7.x/api/#click.prompt return click.prompt(var_name, default=default_value) def read_user_yes_no(question, default_value): """Prompt the user to reply with 'yes' or 'no' (or equivalent values). - Note: - Possible choices are 'true', '1', 'yes', 'y' or 'false', '0', 'no', 'n' + - 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:`click.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 """ - # Please see https://click.palletsprojects.com/en/7.x/api/#click.prompt return click.prompt(question, default=default_value, type=click.BOOL) @@ -37,7 +41,6 @@ def read_repo_password(question): :param str question: Question to the user """ - # Please see https://click.palletsprojects.com/en/7.x/api/#click.prompt return click.prompt(question, hide_input=True) @@ -50,25 +53,22 @@ def read_user_choice(var_name, options): :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 """ - # Please see https://click.palletsprojects.com/en/7.x/api/#click.prompt if not isinstance(options, list): raise TypeError if not options: raise ValueError - choice_map = OrderedDict( - ('{}'.format(i), value) for i, value in enumerate(options, 1) - ) + choice_map = OrderedDict((f'{i}', value) for i, value in enumerate(options, 1)) choices = choice_map.keys() default = '1' choice_lines = ['{} - {}'.format(*c) for c in choice_map.items()] prompt = '\n'.join( ( - 'Select {}:'.format(var_name), - '\n'.join(choice_lines), - 'Choose from {}'.format(', '.join(choices)), + f"Select {var_name}:", + "\n".join(choice_lines), + f"Choose from {', '.join(choices)}", ) ) @@ -78,16 +78,23 @@ def read_user_choice(var_name, options): return choice_map[user_choice] -def process_json(user_value): +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 """ + if user_value == DEFAULT_DISPLAY: + # Return the given default w/o any processing + return default_value + try: user_dict = json.loads(user_value, object_pairs_hook=OrderedDict) - except Exception: + except Exception as error: # Leave it up to click to ask the user again - raise click.UsageError('Unable to decode to JSON.') + raise click.UsageError('Unable to decode to JSON.') from error if not isinstance(user_dict, dict): # Leave it up to click to ask the user again @@ -103,19 +110,19 @@ def read_user_dict(var_name, default_value): :param default_value: Value that will be returned if no input is provided :return: A Python dictionary to use in the context. """ - # Please see https://click.palletsprojects.com/en/7.x/api/#click.prompt if not isinstance(default_value, dict): raise TypeError - default_display = 'default' - user_value = click.prompt( - var_name, default=default_display, type=click.STRING, value_proc=process_json + var_name, + default=DEFAULT_DISPLAY, + type=click.STRING, + value_proc=functools.partial(process_json, default_value=default_value), ) - if user_value == default_display: - # Return the given default w/o any processing - return default_value + if click.__version__.startswith("7.") and user_value == DEFAULT_DISPLAY: + # click 7.x does not invoke value_proc on the default value. + return default_value # pragma: no cover return user_value @@ -136,8 +143,8 @@ def render_variable(env, raw, cookiecutter_dict): being populated with variables. :return: The rendered value for the default variable. """ - if raw is None: - return None + if raw is None or isinstance(raw, bool): + return raw elif isinstance(raw, dict): return { render_variable(env, k, cookiecutter_dict): render_variable( @@ -152,17 +159,15 @@ def render_variable(env, raw, cookiecutter_dict): template = env.from_string(raw) - rendered_template = template.render(cookiecutter=cookiecutter_dict) - return rendered_template + return template.render(cookiecutter=cookiecutter_dict) def prompt_choice_for_config(cookiecutter_dict, env, key, options, no_input): """Prompt user with a set of options to choose from. - Each of the possible choices is rendered beforehand. + :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) @@ -172,7 +177,7 @@ 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: Prompt the user at command line for manual configuration? + :param no_input: Do not prompt for user input and use only values from context. """ cookiecutter_dict = OrderedDict([]) env = StrictEnvironment(context=context) @@ -195,6 +200,14 @@ def prompt_for_config(context, no_input=False): cookiecutter_dict, env, key, raw, no_input ) 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) elif not isinstance(raw, dict): # We are dealing with a regular variable val = render_variable(env, raw, cookiecutter_dict) @@ -204,12 +217,12 @@ def prompt_for_config(context, no_input=False): cookiecutter_dict[key] = val except UndefinedError as err: - msg = "Unable to render variable '{}'".format(key) - raise UndefinedVariableInTemplate(msg, err, context) + 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 ot be rendered. + # Skip private type dicts not to be rendered. if key.startswith('_') and not key.startswith('__'): continue @@ -223,7 +236,7 @@ def prompt_for_config(context, no_input=False): cookiecutter_dict[key] = val except UndefinedError as err: - msg = "Unable to render variable '{}'".format(key) - raise UndefinedVariableInTemplate(msg, err, context) + msg = f"Unable to render variable '{key}'" + raise UndefinedVariableInTemplate(msg, err, context) from err return cookiecutter_dict diff --git a/cookiecutter/replay.py b/cookiecutter/replay.py index 504a6a32c..d3c989879 100644 --- a/cookiecutter/replay.py +++ b/cookiecutter/replay.py @@ -12,14 +12,13 @@ def get_file_name(replay_dir, template_name): """Get the name of file.""" suffix = '.json' if not template_name.endswith('.json') else '' - file_name = '{}{}'.format(template_name, suffix) + file_name = f'{template_name}{suffix}' return os.path.join(replay_dir, file_name) -def dump(replay_dir, template_name, context): +def dump(replay_dir: "os.PathLike[str]", template_name: str, context: dict): """Write json data to file.""" - if not make_sure_path_exists(replay_dir): - raise IOError('Unable to create replay dir at {}'.format(replay_dir)) + make_sure_path_exists(replay_dir) if not isinstance(template_name, str): raise TypeError('Template name is required to be of type str') @@ -43,7 +42,7 @@ def load(replay_dir, template_name): replay_file = get_file_name(replay_dir, template_name) - with open(replay_file, 'r') as infile: + with open(replay_file) as infile: context = json.load(infile) if 'cookiecutter' not in context: diff --git a/cookiecutter/repository.py b/cookiecutter/repository.py index f8e6fcbcc..e407910fc 100644 --- a/cookiecutter/repository.py +++ b/cookiecutter/repository.py @@ -82,11 +82,12 @@ def determine_repo_dir( 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: Prompt the user at command line for manual configuration? + :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 descriving whether that directory should be cleaned up + 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. """ diff --git a/cookiecutter/utils.py b/cookiecutter/utils.py index 19b727a52..d90b3a81d 100644 --- a/cookiecutter/utils.py +++ b/cookiecutter/utils.py @@ -1,11 +1,13 @@ """Helper functions used throughout Cookiecutter.""" import contextlib -import errno import logging import os import shutil import stat import sys +from pathlib import Path + +from jinja2.ext import Extension from cookiecutter.prompt import read_user_yes_no @@ -30,19 +32,16 @@ def rmtree(path): shutil.rmtree(path, onerror=force_delete) -def make_sure_path_exists(path): +def make_sure_path_exists(path: "os.PathLike[str]") -> None: """Ensure that a directory exists. - :param path: A directory path. + :param path: A directory tree path for creation. """ - logger.debug('Making sure path exists: %s', path) + logger.debug('Making sure path exists (creates tree if not exist): %s', path) try: - os.makedirs(path) - logger.debug('Created directory at: %s', path) - except OSError as exception: - if exception.errno != errno.EEXIST: - return False - return True + 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 @@ -85,8 +84,8 @@ def prompt_and_delete(path, no_input=False): ok_to_delete = True else: question = ( - "You've downloaded {} before. Is it okay to delete and re-download it?" - ).format(path) + f"You've downloaded {path} before. Is it okay to delete and re-download it?" + ) ok_to_delete = read_user_yes_no(question, 'yes') @@ -105,3 +104,15 @@ def prompt_and_delete(path, no_input=False): return False sys.exit() + + +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 diff --git a/cookiecutter/vcs.py b/cookiecutter/vcs.py index 746dfade9..8e61417ac 100644 --- a/cookiecutter/vcs.py +++ b/cookiecutter/vcs.py @@ -2,7 +2,9 @@ import logging import os import subprocess # nosec +from pathlib import Path from shutil import which +from typing import Optional from cookiecutter.exceptions import ( RepositoryCloneFailed, @@ -54,18 +56,24 @@ def is_vcs_installed(repo_type): return bool(which(repo_type)) -def clone(repo_url, checkout=None, clone_to_dir='.', no_input=False): +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: Suppress all user prompts when calling via API. + :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 = os.path.expanduser(clone_to_dir) + clone_to_dir = Path(clone_to_dir).expanduser() make_sure_path_exists(clone_to_dir) # identify the repo_type @@ -73,7 +81,7 @@ def clone(repo_url, checkout=None, clone_to_dir='.', no_input=False): # check that the appropriate VCS for the repo_type is installed if not is_vcs_installed(repo_type): - msg = "'{0}' is not installed.".format(repo_type) + msg = f"'{repo_type}' is not installed." raise VCSNotInstalled(msg) repo_url = repo_url.rstrip('/') @@ -81,9 +89,9 @@ def clone(repo_url, checkout=None, clone_to_dir='.', no_input=False): 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)) - elif repo_type == 'hg': + if repo_type == 'hg': repo_dir = os.path.normpath(os.path.join(clone_to_dir, repo_name)) - logger.debug('repo_dir is {0}'.format(repo_dir)) + logger.debug(f'repo_dir is {repo_dir}') if os.path.isdir(repo_dir): clone = prompt_and_delete(repo_dir, no_input=no_input) @@ -98,8 +106,12 @@ def clone(repo_url, checkout=None, clone_to_dir='.', no_input=False): 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], + [repo_type, 'checkout', *checkout_params], cwd=repo_dir, stderr=subprocess.STDOUT, ) @@ -107,14 +119,14 @@ def clone(repo_url, checkout=None, clone_to_dir='.', no_input=False): output = clone_error.output.decode('utf-8') if 'not found' in output.lower(): raise RepositoryNotFound( - 'The repository {} could not be found, ' - 'have you made a typo?'.format(repo_url) - ) + 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( - 'The {} branch of repository {} could not found, ' - 'have you made a typo?'.format(checkout, repo_url) - ) + 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 diff --git a/cookiecutter/zipfile.py b/cookiecutter/zipfile.py index 24925c7fc..bab90a264 100644 --- a/cookiecutter/zipfile.py +++ b/cookiecutter/zipfile.py @@ -1,6 +1,8 @@ """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 @@ -10,7 +12,13 @@ from cookiecutter.utils import make_sure_path_exists, prompt_and_delete -def unzip(zip_uri, is_url, clone_to_dir='.', no_input=False, password=None): +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, @@ -20,11 +28,12 @@ def unzip(zip_uri, is_url, clone_to_dir='.', no_input=False, password=None): :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: Suppress any prompts + :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 = os.path.expanduser(clone_to_dir) + clone_to_dir = Path(clone_to_dir).expanduser() make_sure_path_exists(clone_to_dir) if is_url: @@ -55,15 +64,14 @@ def unzip(zip_uri, is_url, clone_to_dir='.', no_input=False, password=None): zip_file = ZipFile(zip_path) if len(zip_file.namelist()) == 0: - raise InvalidZipRepository('Zip repository {} is empty'.format(zip_uri)) + 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( - 'Zip repository {} does not include ' - 'a top-level directory'.format(zip_uri) + f"Zip repository {zip_uri} does not include a top-level directory" ) # Construct the final target directory @@ -106,7 +114,7 @@ def unzip(zip_uri, is_url, clone_to_dir='.', no_input=False, password=None): except BadZipFile: raise InvalidZipRepository( - 'Zip repository {} is not a valid zip archive:'.format(zip_uri) + f'Zip repository {zip_uri} is not a valid zip archive:' ) return unzip_path diff --git a/docs/BACKERS.md b/docs/BACKERS.md deleted file mode 120000 index 6e7e6e905..000000000 --- a/docs/BACKERS.md +++ /dev/null @@ -1 +0,0 @@ -../BACKERS.md \ No newline at end of file diff --git a/docs/HelloCookieCutter1 b/docs/HelloCookieCutter1 deleted file mode 160000 index 239ea6928..000000000 --- a/docs/HelloCookieCutter1 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 239ea692896301eaa280dd407fdd4d5c55cf6998 diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 01325d08b..000000000 --- a/docs/Makefile +++ /dev/null @@ -1,177 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/cookiecutter.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/cookiecutter.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/cookiecutter" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/cookiecutter" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/_templates/package.rst_t b/docs/_templates/package.rst_t new file mode 100644 index 000000000..b18365dec --- /dev/null +++ b/docs/_templates/package.rst_t @@ -0,0 +1,51 @@ +{%- 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 %} diff --git a/docs/advanced/boolean_variables.rst b/docs/advanced/boolean_variables.rst new file mode 100644 index 000000000..8d9cf6997 --- /dev/null +++ b/docs/advanced/boolean_variables.rst @@ -0,0 +1,53 @@ +Boolean 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 + diff --git a/docs/advanced/calling_from_python.rst b/docs/advanced/calling_from_python.rst index 4c7f2e9ad..c51b09f0b 100644 --- a/docs/advanced/calling_from_python.rst +++ b/docs/advanced/calling_from_python.rst @@ -3,7 +3,9 @@ Calling Cookiecutter Functions From Python ------------------------------------------ -You can use Cookiecutter from Python:: +You can use Cookiecutter from Python: + +.. code-block:: python from cookiecutter.main import cookiecutter @@ -11,8 +13,8 @@ You can use Cookiecutter from Python:: cookiecutter('cookiecutter-pypackage/') # Create project from the cookiecutter-pypackage.git repo template - cookiecutter('https://github.com/audreyr/cookiecutter-pypackage.git') + 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`. -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. diff --git a/docs/advanced/choice_variables.rst b/docs/advanced/choice_variables.rst index c86eb11c1..706d53947 100644 --- a/docs/advanced/choice_variables.rst +++ b/docs/advanced/choice_variables.rst @@ -1,17 +1,21 @@ .. _choice-variables: -Choice Variables (1.1+) ------------------------ +Choice Variables +---------------- -Choice variables provide different choices when creating a project. Depending on a user's choice -the template renders things differently. +*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``:: +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"] @@ -28,8 +32,9 @@ you'd get the following choices when running Cookiecutter:: 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:: +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 @@ -39,9 +44,12 @@ can be used like this:: {% endif %} -Cookiecutter is using `Jinja2's if conditional expression `_ to determine the correct license. +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 -The created choice variable is still a regular Cookiecutter variable and can be used like this:: License ------- @@ -53,7 +61,9 @@ 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:: +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"] diff --git a/docs/advanced/copy_without_render.rst b/docs/advanced/copy_without_render.rst index 2cdb68084..5e4bc008d 100644 --- a/docs/advanced/copy_without_render.rst +++ b/docs/advanced/copy_without_render.rst @@ -5,7 +5,10 @@ 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:: +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", @@ -16,7 +19,12 @@ To avoid rendering directories and files of a cookiecutter, the `_copy_without_r ] } -**Note**: Only the content of the files will be copied without being rendered. The paths are subject to rendering. This allows you to write:: +**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", @@ -25,4 +33,4 @@ To avoid rendering directories and files of a cookiecutter, the `_copy_without_r ] } -In this example, `{{cookiecutter.repo_name}}` will be rendered as expected but the html file content will be copied without rendering. +In this example, ``{{cookiecutter.repo_name}}`` will be rendered as expected but the html file content will be copied without rendering. diff --git a/docs/advanced/dict_variables.rst b/docs/advanced/dict_variables.rst index 1db841fa1..63241c0a0 100644 --- a/docs/advanced/dict_variables.rst +++ b/docs/advanced/dict_variables.rst @@ -1,20 +1,21 @@ .. _dict-variables: -Dictionary Variables (1.5+) ---------------------------- +Dictionary Variables +-------------------- -Dictionary variables provide a way to define deep structured information when -rendering a template. +*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. +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``: -For example, you could provide the following dictionary variable in your -``cookiecutter.json``:: +.. code-block:: json { "project_slug": "new_project", @@ -38,8 +39,9 @@ For example, you could provide the following dictionary variable in your } -The above ``file_type`` dictionary variable creates -``cookiecutter.file_types``, which can be used like this:: +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 %}
@@ -61,5 +63,4 @@ The above ``file_type`` dictionary variable creates {% endfor %} -Cookiecutter is using `Jinja2's for expression `_ to iterate over the items in the dictionary. - +Cookiecutter is using `Jinja2's for expression `_ to iterate over the items in the dictionary. diff --git a/docs/advanced/directories.rst b/docs/advanced/directories.rst index cbec8e4ae..ba0596ded 100644 --- a/docs/advanced/directories.rst +++ b/docs/advanced/directories.rst @@ -1,14 +1,13 @@ .. _directories: -Organizing cookiecutters in directories (1.7+) ---------------------------------------------------- +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:: +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/ @@ -18,6 +17,8 @@ this feature:: ├── {{cookiecutter.project_slug}}/ └── cookiecutter.json -To activate one of templates within a subdirectory, use the ``--directory`` option:: +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" diff --git a/docs/advanced/hooks.rst b/docs/advanced/hooks.rst index c659fd407..d08cf9e75 100644 --- a/docs/advanced/hooks.rst +++ b/docs/advanced/hooks.rst @@ -1,12 +1,13 @@ .. _user-hooks: -Using Pre/Post-Generate Hooks (0.7.0+) -====================================== +Using Pre/Post-Generate Hooks +============================= -You can have Python or Shell scripts that run before and/or after your project -is generated. +*New in cookiecutter 0.7* -Put them in `hooks/` like this:: +You can have Python or Shell scripts that run before and/or after your project is generated. + +Put them in ``hooks/`` like this:: cookiecutter-something/ ├── {{cookiecutter.project_slug}}/ @@ -24,13 +25,11 @@ Shell scripts work similarly:: │ └── post_gen_project.sh └── cookiecutter.json -It shouldn't be too hard to extend Cookiecutter to work with other types of -scripts too. Pull requests are welcome. +It shouldn't be too hard to extend Cookiecutter to work with other types of scripts too. +Pull requests are welcome. -For portability, you should use Python scripts (with extension `.py`) for your -hooks, as these can be run on any platform. However, if you intend for your -template to only be run on a single platform, a shell script (or `.bat` file -on Windows) can be a quicker alternative. +For portability, you should use Python scripts (with extension `.py`) for your hooks, as these can be run on any platform. +However, if you intend for your template to only be run on a single platform, a shell script (or `.bat` file on Windows) can be a quicker alternative. Writing hooks ------------- @@ -40,25 +39,21 @@ Here are some details on how to write pre/post-generate hook scripts. Exit with an appropriate status ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Make sure your hook scripts work in a robust manner. If a hook script fails -(that is, `if it finishes with a nonzero exit status -`_), the project -generation will stop and the generated directory will be cleaned up. +Make sure your hook scripts work in a robust manner. +If a hook script fails (that is, `if it finishes with a nonzero exit status `_), the project generation will stop and the generated directory will be cleaned up. Current working directory ^^^^^^^^^^^^^^^^^^^^^^^^^ -When the hook scripts script are run, their current working directory is the -root of the generated project. This makes it easy for a post-generate hook to -find generated files using relative paths. +When the hook scripts script are run, their current working directory is the root of the generated project. +This makes it easy for a post-generate hook to find generated files using relative paths. Template variables are rendered in the script ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Just like your project template, Cookiecutter also renders Jinja template -syntax in your scripts. This lets you incorporate Jinja template variables in -your scripts. For example, this line of Python sets ``module_name`` to the -value of the ``cookiecutter.module_name`` template variable: +Just like your project template, Cookiecutter also renders Jinja template syntax in your scripts. +This lets you incorporate Jinja template variables in your scripts. +For example, this line of Python sets ``module_name`` to the value of the ``cookiecutter.module_name`` template variable: .. code-block:: python @@ -67,9 +62,7 @@ value of the ``cookiecutter.module_name`` template variable: Example: Validating template variables -------------------------------------- -Here is an example of a pre-generate hook script, defined at -``hooks/pre_gen_project.py``, that validates a template variable before generating the -project: +Here is an example of a pre-generate hook script, defined at ``hooks/pre_gen_project.py``, that validates a template variable before generating the project: .. code-block:: python @@ -90,12 +83,10 @@ project: Example: Conditional files / directories ---------------------------------------- -Here is an example of a post-generate hook script, defined at -``hooks/post_gen_project.py``, on how to achieve conditional control of files and -directories after generating the project. +Here is an example of a post-generate hook script. +The file ``hooks/post_gen_project.py`` shows how to achieve conditional control of files and directories after generating the project. -The script ensures that the directory structure is as expected by -removing unwanted files and directories: +The script ensures that the directory structure is as expected by removing unwanted files and directories: .. code-block:: python diff --git a/docs/advanced/index.rst b/docs/advanced/index.rst index 90e7f2933..89150af5d 100644 --- a/docs/advanced/index.rst +++ b/docs/advanced/index.rst @@ -17,9 +17,11 @@ Various advanced topics regarding cookiecutter usage. private_variables copy_without_render replay - cli_options choice_variables + boolean_variables dict_variables + templates template_extensions directories new_line_characters + local_extensions diff --git a/docs/advanced/injecting_context.rst b/docs/advanced/injecting_context.rst index 32927c07f..5561b3d83 100644 --- a/docs/advanced/injecting_context.rst +++ b/docs/advanced/injecting_context.rst @@ -3,17 +3,30 @@ Injecting Extra Context ----------------------- -You can specify an `extra_context` dictionary that will override values from `cookiecutter.json` or `.cookiecutterrc`:: +You can specify an ``extra_context`` dictionary that will override values from ``cookiecutter.json`` or ``.cookiecutterrc``: - cookiecutter('cookiecutter-pypackage/', - extra_context={'project_name': 'TheGreatest'}) +.. 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``. -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:: +If you have ``cookiecutter.json`` that has the following keys: + +.. code-block:: JSON { "timestamp": "{{ cookiecutter.timestamp }}" @@ -21,7 +34,9 @@ If you have ``cookiecutter.json`` that has the following keys:: This Python script will dynamically inject a timestamp value as the project is -generated:: +generated: + +.. code-block:: python from cookiecutter.main import cookiecutter @@ -34,6 +49,6 @@ generated:: 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. +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. diff --git a/docs/advanced/local_extensions.rst b/docs/advanced/local_extensions.rst new file mode 100644 index 000000000..a9eb0a3fa --- /dev/null +++ b/docs/advanced/local_extensions.rst @@ -0,0 +1,59 @@ +.. _`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``. + diff --git a/docs/advanced/new_line_characters.rst b/docs/advanced/new_line_characters.rst index 64f009898..e75ca9fcc 100644 --- a/docs/advanced/new_line_characters.rst +++ b/docs/advanced/new_line_characters.rst @@ -5,19 +5,22 @@ Working with line-ends special symbols LF/CRLF *New in Cookiecutter 2.0* -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. +.. note:: -By default Cookiecutter now check every file at render stage and use 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. + 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. -Special template variable `_new_lines` was added in Cookiecutter 2.0. -Acceptable variables: `'\n\r'` for CRLF and `'\n'` for POSIX. +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. -Here is example how to force line endings to CRLF on any deployment:: +The special template variable ``_new_lines`` enforces a specific line ending. +Acceptable variables: ``'\n\r'`` 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", diff --git a/docs/advanced/private_variables.rst b/docs/advanced/private_variables.rst index 08ba9d943..6d17268b3 100644 --- a/docs/advanced/private_variables.rst +++ b/docs/advanced/private_variables.rst @@ -3,7 +3,12 @@ Private Variables ----------------- -Cookiecutter allows the definition private variables - those the user will not be required to fill in - by prepending an underscore to the variable name. These can either be not rendered, by using a prepending underscore, or rendered, prepending a double underscore. For example, the ``cookiecutter.json``:: +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", @@ -11,7 +16,9 @@ Cookiecutter allows the definition private variables - those the user will not b "__rendered": "{{ cookiecutter.project_name|lower }}" } -Will be rendered as:: +Will be rendered as: + +.. code-block:: JSON { "project_name": "Really cool project", @@ -21,7 +28,11 @@ Will be rendered as:: 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:: +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", diff --git a/docs/advanced/replay.rst b/docs/advanced/replay.rst index 14afbfd24..79672e284 100644 --- a/docs/advanced/replay.rst +++ b/docs/advanced/replay.rst @@ -9,7 +9,9 @@ On invocation **Cookiecutter** dumps a json file to ``~/.cookiecutter_replay/`` 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``):: +Example for a replay file (which was created via ``cookiecutter gh:hackebrot/cookiedozer``): + +.. code-block:: JSON { "cookiecutter": { @@ -28,27 +30,31 @@ Example for a replay file (which was created via ``cookiecutter gh:hackebrot/coo 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:: +Pass the according option on the CLI: + +.. code-block:: bash cookiecutter --replay gh:hackebrot/cookiedozer -Or use the Python API:: +Or use the Python API: + +.. code-block:: python from cookiecutter.main import cookiecutter cookiecutter('gh:hackebrot/cookiedozer', replay=True) - -This feature is comes in handy if, for instance, you want to create a new project from an updated template. +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:: +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 +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. diff --git a/docs/advanced/suppressing_prompts.rst b/docs/advanced/suppressing_prompts.rst index 1f73ff9fd..68f4a292e 100644 --- a/docs/advanced/suppressing_prompts.rst +++ b/docs/advanced/suppressing_prompts.rst @@ -3,12 +3,16 @@ Suppressing Command-Line Prompts -------------------------------- -To suppress the prompts asking for input, use `no_input`. +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`:: +Cookiecutter will pick a default value if used with ``no_input``: + +.. code-block:: python from cookiecutter.main import cookiecutter cookiecutter( @@ -16,18 +20,21 @@ Cookiecutter will pick a default value if used with `no_input`:: no_input=True, ) -In this case it will be using the default defined in `cookiecutter.json` or `.cookiecutterrc`. +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` + 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:: +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 the :ref:`API Reference ` for more details. + +See also :ref:`injecting-extra-content` and the :ref:`API Reference ` for more details. diff --git a/docs/advanced/template_extensions.rst b/docs/advanced/template_extensions.rst index 940b87b2f..272c42b0c 100644 --- a/docs/advanced/template_extensions.rst +++ b/docs/advanced/template_extensions.rst @@ -5,8 +5,8 @@ Template Extensions *New in Cookiecutter 1.4* -A template may extend the Cookiecutter environment with custom `Jinja2 extensions`_, -that can add extra filters, tests, globals or even extend the parser. +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: @@ -20,12 +20,10 @@ To do so, a template author must specify the required extensions in ``cookiecutt 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``. +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. +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: @@ -38,8 +36,7 @@ By default Cookiecutter includes the following extensions: Jsonify extension ~~~~~~~~~~~~~~~~~ -The ``cookiecutter.extensions.JsonifyExtension`` extension provides a ``jsonify`` filter in templates -that converts a Python object to JSON: +The ``cookiecutter.extensions.JsonifyExtension`` extension provides a ``jsonify`` filter in templates that converts a Python object to JSON: .. code-block:: jinja @@ -56,10 +53,10 @@ 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. +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: +Generate a random n-size character string. +Example for n=12: .. code-block:: jinja @@ -71,8 +68,7 @@ Outputs: bIIUczoNvswh -The second argument controls if punctuation and special characters -``!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~`` should be present in the result: +The second argument controls if punctuation and special characters ``!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~`` should be present in the result: .. code-block:: jinja @@ -87,8 +83,7 @@ Outputs: Slugify extension ~~~~~~~~~~~~~~~~~ -The ``cookiecutter.extensions.SlugifyExtension`` extension provides a ``slugify`` filter in templates -that converts string into its underscored ("slugified") version: +The ``cookiecutter.extensions.SlugifyExtension`` extension provides a ``slugify`` filter in templates that converts string into its dashed ("slugified") version: .. code-block:: jinja @@ -96,20 +91,18 @@ that converts string into its underscored ("slugified") version: Would output: -.. code-block:: json +:: it-s-a-random-version -It is diferent from a mere replace of spaces since it also trates some special characters -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 parameter `separator='_'` would -be passed to `slugify()`. +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`: http://jinja.pocoo.org/docs/latest/extensions/ +.. _`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://github.com/un33k/python-slugify +.. _`python-slugify`: https://pypi.org/project/python-slugify UUID4 extension ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/advanced/templates.rst b/docs/advanced/templates.rst new file mode 100644 index 000000000..5113bf940 --- /dev/null +++ b/docs/advanced/templates.rst @@ -0,0 +1,34 @@ +.. _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/ diff --git a/docs/advanced/templates_in_context.rst b/docs/advanced/templates_in_context.rst index faccb3c72..f324c60fe 100644 --- a/docs/advanced/templates_in_context.rst +++ b/docs/advanced/templates_in_context.rst @@ -4,20 +4,21 @@ 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. +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 +- 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:: +Here is a `cookiecutter.json` with templated values for this pattern: + +.. code-block:: JSON { "project_name": "My New Project", @@ -25,13 +26,12 @@ Here is a `cookiecutter.json` with templated values for this pattern:: "pkg_name": "{{ cookiecutter.project_slug|replace('-', '') }}" } -If the user takes the defaults, or uses `no_input`, the templated values will -be: +If the user takes the defaults, or uses `no_input`, the templated values will be: -* `my-new-project` -* `mynewproject` +- `my-new-project` +- `mynewproject` Or, if the user gives `Yet Another New Project`, the values will be: -* `yet-another-new-project` -* `yetanothernewproject` +- ``yet-another-new-project`` +- ``yetanothernewproject`` diff --git a/docs/advanced/user_config.rst b/docs/advanced/user_config.rst index 96f500898..59063b291 100644 --- a/docs/advanced/user_config.rst +++ b/docs/advanced/user_config.rst @@ -1,23 +1,29 @@ .. _user-config: -User Config (0.7.0+) -==================== +User Config +=========== -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 0.7* -From version 1.3.0 you can also specify a config file on the command line via ``--config-file``:: +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. - $ cookiecutter --config-file /home/audreyr/my-custom-config.yaml cookiecutter-pypackage +*New in Cookiecutter 1.3* -Or you can set the ``COOKIECUTTER_CONFIG`` environment variable:: +You can also specify a config file on the command line via ``--config-file``. - $ export COOKIECUTTER_CONFIG=/home/audreyr/my-custom-config.yaml +.. code-block:: bash -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. + 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: @@ -30,24 +36,25 @@ Example user config: cookiecutters_dir: "/home/audreyr/my-custom-cookiecutters-dir/" replay_dir: "/home/audreyr/my-custom-replay-dir/" abbreviations: - pp: https://github.com/audreyr/cookiecutter-pypackage.git + 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. +``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` diff --git a/docs/case_studies.md b/docs/case_studies.md deleted file mode 120000 index 214ddc2b6..000000000 --- a/docs/case_studies.md +++ /dev/null @@ -1 +0,0 @@ -../case_studies.md \ No newline at end of file diff --git a/docs/case_studies.md b/docs/case_studies.md new file mode 100644 index 000000000..db101b979 --- /dev/null +++ b/docs/case_studies.md @@ -0,0 +1,25 @@ +# 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. diff --git a/docs/advanced/cli_options.rst b/docs/cli_options.rst similarity index 100% rename from docs/advanced/cli_options.rst rename to docs/cli_options.rst diff --git a/docs/conf.py b/docs/conf.py index 082a25d03..562baddb0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,31 +28,6 @@ # flake8: noqa D107,D105 - -class Mock(object): - def __init__(self, *args, **kwargs): - pass - - def __call__(self, *args, **kwargs): - return Mock() - - @classmethod - def __getattr__(cls, name): - if name in ('__file__', '__path__'): - return '/dev/null' - elif name[0] == name[0].upper(): - mockType = type(name, (), {}) - mockType.__module__ = __name__ - return mockType - else: - return Mock() - - -MOCK_MODULES = ['yaml'] -for mod_name in MOCK_MODULES: - sys.modules[mod_name] = Mock() - - # Add parent dir to path cwd = os.getcwd() parent = os.path.dirname(cwd) @@ -78,7 +53,9 @@ def __getattr__(cls, name): 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', 'sphinx_click.ext', - 'recommonmark', + 'myst_parser', + 'sphinxcontrib.apidoc', + 'sphinx_autodoc_typehints', ] # Add any paths that contain templates here, relative to this directory. @@ -95,7 +72,7 @@ def __getattr__(cls, name): # General information about the project. project = 'cookiecutter' -copyright = '2013-2019, Audrey Roy and Cookiecutter community' +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 @@ -324,7 +301,7 @@ def __getattr__(cls, name): epub_title = 'cookiecutter' epub_author = 'Audrey Roy' epub_publisher = 'Audrey Roy and Cookiecutter community' -epub_copyright = '2013-2019, 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. @@ -350,7 +327,7 @@ def __getattr__(cls, name): # The format is a list of tuples containing the path and title. # epub_pre_files = [] -# HTML files shat should be inserted after the pages created by sphinx. +# 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 = [] @@ -377,4 +354,22 @@ def __getattr__(cls, name): # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} +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" diff --git a/docs/cookiecutter.rst b/docs/cookiecutter.rst index 05f58a0af..f1086a079 100644 --- a/docs/cookiecutter.rst +++ b/docs/cookiecutter.rst @@ -1,8 +1,9 @@ +=== API === -Submodules ----------- +This is the Cookiecutter modules API documentation. + cookiecutter.cli module ----------------------- @@ -24,9 +25,9 @@ cookiecutter.context module --------------------------- .. automodule:: cookiecutter.context - :members: - :undoc-members: - :show-inheritance: + :members: + :undoc-members: + :show-inheritance: cookiecutter.environment module ------------------------------- @@ -140,7 +141,6 @@ cookiecutter.zipfile module :undoc-members: :show-inheritance: - Module contents --------------- diff --git a/docs/index.rst b/docs/index.rst index 750db5093..7e82fc2db 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,7 +6,7 @@ Cookiecutter: Better Project Templates ====================================== -Cookiecutter creates projects from project templates, e.g. Python package projects. +Cookiecutter creates projects from **cookiecutters** (project templates), e.g. Python package projects from Python package templates. Basics ------ @@ -18,9 +18,8 @@ Basics overview installation usage - tutorials - tutorial1 - tutorial2 + cli_options + tutorials/index advanced/index troubleshooting @@ -42,10 +41,8 @@ Project Info CONTRIBUTING AUTHORS - sprint-contributors HISTORY case_studies - BACKERS CODE_OF_CONDUCT Index diff --git a/docs/installation.rst b/docs/installation.rst index 44c0d841a..995fd077b 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -12,13 +12,17 @@ Prerequisites Python interpreter ^^^^^^^^^^^^^^^^^^ -Install Python for your operating system. Consult the official `Python documentation `_ for details. +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. +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 + brew install python3 Adjust your path @@ -59,7 +63,7 @@ You may also install `Windows Subsystem for Linux =3.6. See the Python Packaging Authority's (PyPA) documentation `Requirements for Installing Packages `_ for full details. +See the Python Packaging Authority's (PyPA) documentation `Requirements for Installing Packages `_ for full details. Install cookiecutter @@ -69,27 +73,27 @@ At the command line: .. code-block:: bash - $ python3 -m pip install --user cookiecutter + python3 -m pip install --user cookiecutter Or, if you do not have pip: .. code-block:: bash - $ easy_install --user cookiecutter + easy_install --user cookiecutter -Though, pip is recommended. +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 + 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 + conda install cookiecutter Alternate installations ----------------------- @@ -98,40 +102,40 @@ Alternate installations .. code-block:: bash - $ brew install cookiecutter + brew install cookiecutter **Pipx (Linux, OSX and Windows):** .. code-block:: bash - $ pipx install cookiecutter + pipx install cookiecutter -**Debian/Ubuntu:** -.. code-block:: bash - - $ sudo apt-get install cookiecutter +Upgrading +--------- -Upgrading from 0.6.4 to 0.7.0 or greater ----------------------------------------- +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: +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. -Upgrade Cookiecutter either with easy_install: + +Or with pip: .. code-block:: bash - $ easy_install --upgrade cookiecutter + python3 -m pip install --upgrade cookiecutter -Or with pip: +Upgrade Cookiecutter either with easy_install (deprecated): .. code-block:: bash - $ python3 -m pip install --upgrade cookiecutter + easy_install --upgrade cookiecutter Then you should be good to go. diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 908517ffa..000000000 --- a/docs/make.bat +++ /dev/null @@ -1,242 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\cookiecutter.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\cookiecutter.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end diff --git a/docs/overview.rst b/docs/overview.rst index 0a823305b..026cb1bb1 100644 --- a/docs/overview.rst +++ b/docs/overview.rst @@ -2,10 +2,20 @@ 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 the directory structure for a simple cookiecutter:: +This is a directory structure for a simple cookiecutter:: cookiecutter-something/ ├── {{ cookiecutter.project_name }}/ <--------- Project template @@ -17,13 +27,12 @@ This is the directory structure for a simple cookiecutter:: You must have: -* A `cookiecutter.json` file. -* A `{{ cookiecutter.project_name }}/` directory, where - `project_name` is defined in your `cookiecutter.json`. +- 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/audreyr/cookiecutter-pypackage for a real-world example +See https://github.com/audreyfeldroy/cookiecutter-pypackage for a real-world example of this. Output diff --git a/docs/requirements.txt b/docs/requirements.txt index 40157c3ae..582a2052e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,7 @@ -watchdog>=0.10.2 -sphinx-rtd-theme>=0.4.3 -sphinx-click>=2.3.2 -recommonmark>=0.6.0 +sphinx-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 \ No newline at end of file diff --git a/docs/sprint-contributors.rst b/docs/sprint-contributors.rst deleted file mode 100644 index 1a8419fb8..000000000 --- a/docs/sprint-contributors.rst +++ /dev/null @@ -1,66 +0,0 @@ -=================== -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`_) -* Andrew Ittner (`@tephyr`_) -* Audrey Roy Greenfeld (`@audreyr`_) -* Carol Willing (`@willingc`_) -* Christopher Clarke (`@chrisdev`_) -* Citlalli Murillo (`@citmusa`_) -* Daniel Roy Greenfeld (`@pydanny`_) -* Diane DeMers Chen (`@purplediane`_) -* Elaine Wong (`@elainewong`_) -* Elias Dorneles (`@eliasdorneles`_) -* Emily Cain (`@emcain`_) -* John Roa (`@jhonjairoroa87`_) -* Jonan Scheffler (`@1337807`_) -* Phoebe Bauer (`@phoebebauer`_) -* Kartik Sundararajan (`@skarbot`_) -* Katia Lira (`@katialira`_) -* Leonardo Jimenez (`@xpostudio4`_) -* Lindsay Slazakowski (`@lslaz1`_) -* Meghan Heintz (`@dot2dotseurat`_) -* Raphael Pierzina (`@hackebrot`_) -* Umair Ashraf (`@umrashrf`_) -* Valdir Stumm Junior (`@stummjr`_) -* Vivian Guillen (`@viviangb`_) -* Zaro (`@zaro0508`_) - - - - -.. _`@1337807`: https://github.com/1337807 -.. _`@adamchainz`: https://github.com/adamchainz -.. _`@audreyr`: https://github.com/audreyr -.. _`@chrisdev`: https://github.com/chrisdev -.. _`@citmusa`: https://github.com/citmusa -.. _`@dot2dotseurat`: https://github.com/dot2dotseurat -.. _`@elainewong`: https://github.com/elainewong -.. _`@eliasdorneles`: https://github.com/eliasdorneles -.. _`@emcain`: https://github.com/emcain -.. _`@hackebrot`: https://github.com/hackebrot -.. _`@jhonjairoroa87`: https://github.com/jhonjairoroa87 -.. _`@katialira`: https://github.com/katialira -.. _`@lslaz1`: https://github.com/lslaz1 -.. _`@phoebebauer`: https://github.com/phoebebauer -.. _`@purplediane`: https://github.com/purplediane -.. _`@pydanny`: https://github.com/pydanny -.. _`@skarbot`: https://github.com/skarbot -.. _`@stummjr`: https://github.com/stummjr -.. _`@tephyr`: https://github.com/tephyr -.. _`@umrashrf`: https://github.com/umrashrf -.. _`@viviangb`: https://github.com/viviangb -.. _`@willingc`: https://github.com/willingc -.. _`@xpostudio4`: https://github.com/xpostudio4 -.. _`@zaro0508`: https://github.com/zaro0508 - diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 2e45a91d5..cca4b82fd 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -25,7 +25,7 @@ Or this:: {{ {{ url_for('home') }} }} -See http://jinja.pocoo.org/docs/templates/#escaping for more info. +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. diff --git a/docs/tutorial2.rst b/docs/tutorial2.rst deleted file mode 100644 index 830206788..000000000 --- a/docs/tutorial2.rst +++ /dev/null @@ -1,32 +0,0 @@ -================================== -Create a Cookiecutter From Scratch -================================== - -Step 1: Name Your Cookiecutter ------------------------------- - -In this tutorial, we are creating *cookiecutter-website-simple*, a cookiecutter -for generating simple, bare-bones websites. - -Create the directory for your cookiecutter and cd into it: - -.. code-block:: bash - - $ mkdir cookiecutter-website-simple - $ cd cookiecutter-website-simple/ - -Step 2: 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 3: Create Files --------------------- - -Inside of `{{ cookiecutter.project_slug }}`, create `index.html`, `site.css`, and -`site.js`. - -To be continued... diff --git a/docs/tutorials.rst b/docs/tutorials.rst deleted file mode 100644 index 16903235e..000000000 --- a/docs/tutorials.rst +++ /dev/null @@ -1,33 +0,0 @@ -==================== -Additional Tutorials -==================== - -Learn How to Use Cookiecutter ------------------------------ - -* :doc:`tutorial1` by `@audreyr`_ - - -Create Your Very Own Cookiecutter Project Template --------------------------------------------------- - -* :doc:`tutorial2` by `@audreyr`_ - -* `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`_ - - -.. _`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/ - -.. _`@audreyr`: https://github.com/audreyr -.. _`@pydanny`: https://github.com/pydanny -.. _`@hackebrot`: https://github.com/hackebrot diff --git a/docs/tutorials/index.rst b/docs/tutorials/index.rst new file mode 100644 index 000000000..0fa1c98b0 --- /dev/null +++ b/docs/tutorials/index.rst @@ -0,0 +1,36 @@ +==================== +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 diff --git a/docs/tutorial1.rst b/docs/tutorials/tutorial1.rst similarity index 69% rename from docs/tutorial1.rst rename to docs/tutorials/tutorial1.rst index 2659db8b2..e7133fe91 100644 --- a/docs/tutorial1.rst +++ b/docs/tutorials/tutorial1.rst @@ -3,50 +3,45 @@ Getting to Know Cookiecutter ============================= .. note:: Before you begin, please install Cookiecutter 0.7.0 or higher. - Instructions are in :doc:`installation`. + Instructions are in :doc:`../installation`. -Cookiecutter is a tool for creating projects from *cookiecutters* (project -templates). +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. +*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/audreyr/cookiecutter-pypackage/. + 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. +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: +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/audreyr/cookiecutter-pypackage.git + $ 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. +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. +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. +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 ---------------------------------- @@ -58,8 +53,7 @@ In your current directory, you should see that a project got generated: $ ls boilerplate -Looking inside the `boilerplate/` (or directory corresponding to your `project_slug`) -directory, you should see something like this: +Looking inside the `boilerplate/` (or directory corresponding to your `project_slug`) directory, you should see something like this: .. code-block:: bash @@ -94,19 +88,21 @@ 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? +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/audreyr/cookiecutter-pypackage in a new browser window. +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. +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`. @@ -114,18 +110,16 @@ AUTHORS.rst ~~~~~~~~~~~ Look at the raw version of `{{ cookiecutter.project_slug }}/AUTHORS.rst`, at -https://raw.github.com/audreyr/cookiecutter-pypackage/master/%7B%7Bcookiecutter.project_slug%7D%7D/AUTHORS.rst. +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. +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: +You should see JSON that corresponds to the prompts and default values shown earlier during project generation: .. code-block:: json @@ -147,13 +141,12 @@ earlier during project generation: Questions? ---------- -If anything needs better explanation, please take a moment to file an issue at https://github.com/audreyr/cookiecutter/issues with what could be improved +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. +You have learned how to use Cookiecutter to generate your first project from a cookiecutter project template. -In Tutorial 2, you'll see how to create cookiecutters of your own, from scratch. +In tutorial 2 (:ref:`tutorial2`), you'll see how to create cookiecutters of your own, from scratch. diff --git a/docs/tutorials/tutorial2.rst b/docs/tutorials/tutorial2.rst new file mode 100644 index 000000000..d16f6b7fa --- /dev/null +++ b/docs/tutorials/tutorial2.rst @@ -0,0 +1,107 @@ +.. _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 + + + +

Cookiecutter Developer

+

by Test web

+ + diff --git a/docs/usage.rst b/docs/usage.rst index 9b7b95fe5..872cbe2b7 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -7,7 +7,7 @@ Grab a Cookiecutter template First, clone a Cookiecutter project template:: - $ git clone git@github.com:audreyr/cookiecutter-pypackage.git + $ git clone https://github.com/audreyfeldroy/cookiecutter-pypackage.git Make your changes ----------------- @@ -40,14 +40,14 @@ Works directly with git and hg (mercurial) repos too To create a project from the cookiecutter-pypackage.git repo template:: - $ cookiecutter gh:audreyr/cookiecutter-pypackage + $ 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/audreyr/cookiecutter-pypackage.git - $ cookiecutter git+ssh://git@github.com/audreyr/cookiecutter-pypackage.git + $ 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 @@ -58,7 +58,7 @@ 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/audreyr/cookiecutter-pypackage.git --checkout develop + $ cookiecutter https://github.com/audreyfeldroy/cookiecutter-pypackage.git --checkout develop Works with private repos ------------------------ diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 000000000..25189967e --- /dev/null +++ b/noxfile.py @@ -0,0 +1,82 @@ +"""Nox tool configuration file. + +Nox is Tox tool replacement. +""" +import shutil +from pathlib import Path + +import nox + +nox.options.keywords = "not docs" + + +def base_install(session): + """Create basic environment setup for tests and linting.""" + session.install("-r", "test_requirements.txt") + session.install("-e", ".") + return session + + +@nox.session(python="3.10") +def lint(session): + """Run linting check locally.""" + session.install("pre-commit") + session.run("pre-commit", "run", "-a") + + +@nox.session(python=["3.7", "3.8", "3.9", "3.10"]) +def tests(session): + """Run test suite with pytest.""" + session = base_install(session) + session.run( + "pytest", + "--cov-report=html", + "--cov-report=xml", + "--cov-branch", + "--cov-fail-under=100", + ) + + +@nox.session(python=["3.7", "3.8", "3.9", "3.10"]) +def safety_tests(session): + """Run safety tests.""" + session = base_install(session) + session.run("safety", "check", "--full-report") + + +@nox.session(python="3.10") +def documentation_tests(session): + """Run documentation tests.""" + return docs(session, batch_run=True) + + +@nox.session(python="3.10") +def docs(session, batch_run: bool = False): + """Build the documentation or serve documentation interactively.""" + shutil.rmtree(Path("docs").joinpath("_build"), ignore_errors=True) + session.install("-r", "docs/requirements.txt") + session.install("-e", ".") + session.cd("docs") + sphinx_args = ["-b", "html", "-W", ".", "_build/html"] + + if not session.interactive or batch_run: + sphinx_cmd = "sphinx-build" + else: + sphinx_cmd = "sphinx-autobuild" + sphinx_args.extend( + [ + "--open-browser", + "--port", + "9812", + "--watch", + "../*.md", + "--watch", + "../*.rst", + "--watch", + "../*.py", + "--watch", + "../cookiecutter", + ] + ) + + session.run(sphinx_cmd, *sphinx_args) diff --git a/pyproject.toml b/pyproject.toml index 9c25cb239..2240e4443 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,4 +2,4 @@ skip-string-normalization = true exclude = '/(tests/hooks-abort-render/hooks|docs\/HelloCookieCutter1)/' line-length = 88 -target-version = ['py36'] +target-version = ['py39'] diff --git a/setup.cfg b/setup.cfg index aaf136bcb..29242fa98 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,23 +1,93 @@ -[flake8] -ignore = BLK100,E231,W503 +[metadata] +name = cookiecutter +url = https://github.com/cookiecutter/cookiecutter +project_urls = + Bug Tracker = https://github.com/cookiecutter/cookiecutter/issues + CI: GitHub = https://github.com/cookiecutter/cookiecutter/actions + Documentation = https://cookiecutter.readthedocs.io/ + Source Code = https://github.com/cookiecutter/cookiecutter +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 = file: README.md +long_description_content_type = text/markdown +author = Audrey Feldroy +author_email = audreyr@gmail.com +maintainer = Audrey Feldroy +maintainer_email = audreyr@gmail.com +license = BSD +license_file = LICENSE +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.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + 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 + +[options] +use_scm_version = True +python_requires = >=3.6 +; package_dir = +; = src +packages = cookiecutter +zip_safe = False + +# These are required during `setup.py` run: +setup_requires = + setuptools_scm>=1.15.0 + setuptools_scm_git_archive>=1.0 + +install_requires = + binaryornot>=0.4.4 + Jinja2>=2.7,<4.0.0 + click>=7.0,<9.0.0 + pyyaml>=5.3.1 + jinja2-time>=0.2.0 + python-slugify>=4.0.0 + requests>=2.23.0 + jsonschema>=3.2.0 +[options.entry_points] +console_scripts = + cookiecutter = cookiecutter.__main__:main + +[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 -[bdist_wheel] -universal = 1 - [tool:pytest] testpaths = tests -addopts = -vvv --cov-report term-missing --cov=cookiecutter +addopts = --cov-report=html --cov-report=xml --cov-branch --cov-fail-under=100 --cov-report term-missing --cov=cookiecutter [doc8] -# TODO: Remove current max-line-lengh ignore in follow-up and adopt black limit. +# TODO: Remove current max-line-length ignore in follow-up and adopt black limit. # max-line-length = 88 ignore = D001 diff --git a/setup.py b/setup.py index f022aa7b2..47f00a7e7 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,7 @@ -#!/usr/bin/env python """cookiecutter distutils configuration.""" from setuptools import setup -version = "2.0.0" +version = "2.1.2.dev0" with open('README.md', encoding='utf-8') as readme_file: readme = readme_file.read() @@ -10,7 +9,7 @@ requirements = [ 'binaryornot>=0.4.4', 'Jinja2>=2.7,<4.0.0', - 'click>=7.0,<8.0.0', + 'click>=7.0,<9.0.0', 'pyyaml>=5.3.1', 'jinja2-time>=0.2.0', 'python-slugify>=4.0.0', @@ -35,7 +34,7 @@ package_dir={'cookiecutter': 'cookiecutter'}, entry_points={'console_scripts': ['cookiecutter = cookiecutter.__main__:main']}, include_package_data=True, - python_requires='>=3.6', + python_requires='>=3.7', install_requires=requirements, license='BSD', zip_safe=False, @@ -47,10 +46,10 @@ "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python", diff --git a/test_requirements.txt b/test_requirements.txt index 4607cefe1..febcb2ae3 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -2,3 +2,5 @@ pytest pytest-cov pytest-mock freezegun +safety +pre-commit diff --git a/tests/conftest.py b/tests/conftest.py index e21071304..0990c07d3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,10 +12,6 @@ cookiecutters_dir: '{cookiecutters_dir}' replay_dir: '{replay_dir}' """ -# In YAML, double quotes mean to use escape sequences. -# Single quotes mean we will have unescaped backslahes. -# http://blogs.perl.org/users/tinita/2018/03/ -# strings-in-yaml---to-quote-or-not-to-quote.html @pytest.fixture(autouse=True) @@ -38,7 +34,7 @@ def backup_dir(original_dir, backup_dir): if not os.path.isdir(original_dir): return False - # Remove existing backups before backing up. If they exist, they're stale. + # Remove existing stale backups before backing up. if os.path.isdir(backup_dir): utils.rmtree(backup_dir) @@ -48,12 +44,9 @@ def backup_dir(original_dir, backup_dir): def restore_backup_dir(original_dir, backup_dir, original_dir_found): """Restore default contents.""" - # Carefully delete the created original_dir only in certain - # conditions. original_dir_is_dir = os.path.isdir(original_dir) if original_dir_found: - # Delete the created original_dir as long as a backup - # exists + # Delete original_dir if a backup exists if original_dir_is_dir and os.path.isdir(backup_dir): utils.rmtree(original_dir) else: diff --git a/tests/hooks-abort-render/hooks/post_gen_project.py b/tests/hooks-abort-render/hooks/post_gen_project.py index 706cc440d..d95ca59fa 100644 --- a/tests/hooks-abort-render/hooks/post_gen_project.py +++ b/tests/hooks-abort-render/hooks/post_gen_project.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # flake8: noqa """Simple post-gen hook for testing the handling of different exit codes.""" diff --git a/tests/hooks-abort-render/hooks/pre_gen_project.py b/tests/hooks-abort-render/hooks/pre_gen_project.py index a132af807..3bd59868c 100644 --- a/tests/hooks-abort-render/hooks/pre_gen_project.py +++ b/tests/hooks-abort-render/hooks/pre_gen_project.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # flake8: noqa """Simple pre-gen hook for testing the handling of different exit codes.""" diff --git a/tests/replay/test_dump.py b/tests/replay/test_dump.py index ec8010b3e..57ad8ee74 100644 --- a/tests/replay/test_dump.py +++ b/tests/replay/test_dump.py @@ -16,7 +16,7 @@ def template_name(): @pytest.fixture def replay_file(replay_test_dir, template_name): """Fixture to return a actual file name of the dump.""" - file_name = '{}.json'.format(template_name) + file_name = f'{template_name}.json' return os.path.join(replay_test_dir, file_name) @@ -57,7 +57,9 @@ def mock_ensure_failure(mocker): Used to mock internal function and limit test scope. Always return expected value: False """ - return mocker.patch('cookiecutter.replay.make_sure_path_exists', return_value=False) + return mocker.patch( + 'cookiecutter.replay.make_sure_path_exists', side_effect=OSError + ) @pytest.fixture @@ -72,7 +74,7 @@ def mock_ensure_success(mocker): 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(IOError): + with pytest.raises(OSError): replay.dump(replay_test_dir, 'foo', {'cookiecutter': {'hello': 'world'}}) mock_ensure_failure.assert_called_once_with(replay_test_dir) diff --git a/tests/replay/test_load.py b/tests/replay/test_load.py index a64a285e1..c8bc453e0 100644 --- a/tests/replay/test_load.py +++ b/tests/replay/test_load.py @@ -16,7 +16,7 @@ def template_name(): @pytest.fixture def replay_file(replay_test_dir, template_name): """Fixture to return a actual file name of the dump.""" - file_name = '{}.json'.format(template_name) + file_name = f'{template_name}.json' return os.path.join(replay_test_dir, file_name) diff --git a/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py b/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py index 107e64aa3..3810668f6 100644 --- a/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py +++ b/tests/repository/test_determine_repo_dir_finds_existing_cookiecutter.py @@ -1,5 +1,6 @@ """Tests around detection whether cookiecutter templates are cached locally.""" import os +from pathlib import Path import pytest @@ -20,7 +21,7 @@ def cloned_cookiecutter_path(user_config_data, template): cloned_template_path = os.path.join(cookiecutters_dir, template) os.mkdir(cloned_template_path) - open(os.path.join(cloned_template_path, 'cookiecutter.json'), 'w') + Path(cloned_template_path, "cookiecutter.json").touch() # creates file return cloned_template_path diff --git a/tests/repository/test_determine_repo_dir_finds_subdirectories.py b/tests/repository/test_determine_repo_dir_finds_subdirectories.py index f40e6063d..bcea1b387 100644 --- a/tests/repository/test_determine_repo_dir_finds_subdirectories.py +++ b/tests/repository/test_determine_repo_dir_finds_subdirectories.py @@ -1,5 +1,6 @@ """Tests around locally cached cookiecutter template repositories.""" import os +from pathlib import Path import pytest @@ -24,7 +25,7 @@ def cloned_cookiecutter_path(user_config_data, template): subdir_template_path = os.path.join(cloned_template_path, 'my-dir') if not os.path.exists(subdir_template_path): os.mkdir(subdir_template_path) - open(os.path.join(subdir_template_path, 'cookiecutter.json'), 'w') + Path(subdir_template_path, 'cookiecutter.json').touch() # creates file return subdir_template_path diff --git a/tests/repository/test_is_repo_url.py b/tests/repository/test_is_repo_url.py index 64238e02e..5591be0fc 100644 --- a/tests/repository/test_is_repo_url.py +++ b/tests/repository/test_is_repo_url.py @@ -25,8 +25,8 @@ def test_is_zip_file(zipfile): @pytest.fixture( params=[ 'gitolite@server:team/repo', - 'git@github.com:audreyr/cookiecutter.git', - 'https://github.com/audreyr/cookiecutter.git', + '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', @@ -65,7 +65,7 @@ def test_is_repo_url_for_local_urls(local_repo_url): def test_expand_abbreviations(): """Validate `repository.expand_abbreviations` correctly translate url.""" - template = 'gh:audreyr/cookiecutter-pypackage' + template = 'gh:audreyfeldroy/cookiecutter-pypackage' # This is not a valid repo url just yet! # First `repository.expand_abbreviations` needs to translate it diff --git a/tests/test-context/cookiecutter-no-requires.json b/tests/test-context/cookiecutter-no-requires.json new file mode 100644 index 000000000..32523b078 --- /dev/null +++ b/tests/test-context/cookiecutter-no-requires.json @@ -0,0 +1,154 @@ +{ + "version": "2.0", + "requires": { + "python": ">=3" + }, + "jinja": { + "optimized": true, + "extensions": [ + "cookiecutter.extensions.SlugifyExtension", + "jinja2_time.TimeExtension" + ] + }, + "template": { + "name": "cookiecutter-pytest-plugin", + "version": "0.1", + "description": "a cookiecutter to create pytest plugins with ease.", + "authors": [ + "Raphael Pierzina ", + "Audrey Roy Greenfeld " + ], + "license": "MIT", + "keywords": [ + "pytest", + "python", + "plugin" + ], + "url": "https://github.com/pytest-dev/cookiecutter-pytest-plugin", + "variables": [ + { + "name": "full_name", + "default": "Raphael Pierzina", + "prompt": "What's your full name?", + "description": "Please enter your full name. It will be displayed on the README file and used for the PyPI package definition.", + "type": "string" + }, + { + "name": "email", + "default": "raphael@hackebrot.de", + "prompt": "What's your email?", + "description": "Please enter an email address for the meta information in setup.py.", + "type": "string" + }, + { + "name": "secret_token", + "prompt": "Please enter your secret token", + "type": "string", + "hide_input": true + }, + { + "name": "plugin_name", + "default": "emoji", + "prompt": "What should be the name for your plugin?", + "description": "Please enter a name for your plugin. We will prepend the name with 'pytest-'", + "type": "string" + }, + { + "name": "module_name", + "default": "{{cookiecutter.plugin_name|lower|replace('-','_')}}", + "prompt": "Please enter a name for your base python module", + "type": "string", + "validation": "^[a-z_]+$" + }, + { + "name": "license", + "default": "MIT", + "prompt": "Please choose a license!", + "description": "Cookiecutter will add an according LICENSE file for you and set the according classifier in setup.py.", + "type": "string", + "choices": [ + "MIT", + "BSD-3", + "GNU GPL v3.0", + "Apache Software License 2.0", + "Mozilla Public License 2.0" + ] + }, + { + "name": "docs", + "default": false, + "prompt": "Do you want to generate a base for docs?", + "description": "Would you like to generate documentation for your plugin? You will be able to choose from a number of generators.", + "type": "yes_no" + }, + { + "name": "docs_tool", + "default": "mkdocs", + "prompt": "Which tool do you want to choose for generating docs?", + "description": "There are a number of options for documentation generators. Please choose one. We will create a separate folder for you", + "type": "string", + "choices": [ + "mkdocs", + "sphinx" + ], + "skip_if": "{{cookiecutter.docs == False}}" + }, + { + "name": "year", + "default": "{% now 'utc', '%Y' %}", + "prompt_user": false, + "type": "string" + }, + { + "name": "incept_year", + "default": 2017, + "prompt_user": false, + "type": "int" + }, + { + "name": "released", + "default": false, + "prompt_user": false, + "type": "boolean" + }, + { + "name": "temperature", + "default": 77.3, + "prompt_user": false, + "type": "float" + }, + { + "name": "Release-GUID", + "default": "04f5eaa9ee7345469dccffc538b27194", + "prompt_user": false, + "type": "uuid" + }, + { + "name": "copy_with_out_render", + "default": [ + "*.html", + "*not_rendered_dir", + "rendered_dir/not_rendered_file.ini" + ], + "prompt_user": false, + "type": "string" + }, + { + "name": "fixtures", + "default": { + "foo": { + "scope": "session", + "autouse": true + }, + "bar": { + "scope": "function", + "autouse": false + } + }, + "description": "Please enter a valid JSON string to set up fixtures for your plugin.", + "prompt_user": true, + "type": "json" + } + ] + } +} diff --git a/tests/test-context/cookiecutter.json b/tests/test-context/cookiecutter.json index d637e2b22..895f612f6 100644 --- a/tests/test-context/cookiecutter.json +++ b/tests/test-context/cookiecutter.json @@ -152,4 +152,4 @@ } ] } -} \ No newline at end of file +} diff --git a/tests/test-extensions/hello_extension/hello_extension.py b/tests/test-extensions/hello_extension/hello_extension.py index f54b6efdd..07f3753b9 100644 --- a/tests/test-extensions/hello_extension/hello_extension.py +++ b/tests/test-extensions/hello_extension/hello_extension.py @@ -6,15 +6,15 @@ class HelloExtension(Extension): """Simple jinja2 extension for cookiecutter test purposes.""" - tags = set(['hello']) + tags = {'hello'} def __init__(self, environment): """Hello Extension Constructor.""" - super(HelloExtension, self).__init__(environment) + super().__init__(environment) def _hello(self, name): """Do actual tag replace when invoked by parser.""" - return 'Hello {name}!'.format(name=name) + return f'Hello {name}!' def parse(self, parser): """Work when something match `tags` variable.""" diff --git a/tests/test-extensions/local_extension/cookiecutter.json b/tests/test-extensions/local_extension/cookiecutter.json new file mode 100644 index 000000000..8141fd508 --- /dev/null +++ b/tests/test-extensions/local_extension/cookiecutter.json @@ -0,0 +1,10 @@ +{ + "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" + ] +} + diff --git a/tests/test-extensions/local_extension/local_extensions/__init__.py b/tests/test-extensions/local_extension/local_extensions/__init__.py new file mode 100644 index 000000000..94e854abd --- /dev/null +++ b/tests/test-extensions/local_extension/local_extensions/__init__.py @@ -0,0 +1 @@ +from .main import FoobarExtension, simplefilterextension # noqa diff --git a/tests/test-extensions/local_extension/local_extensions/main.py b/tests/test-extensions/local_extension/local_extensions/main.py new file mode 100644 index 000000000..b18a25c91 --- /dev/null +++ b/tests/test-extensions/local_extension/local_extensions/main.py @@ -0,0 +1,19 @@ +"""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() diff --git a/tests/test-extensions/local_extension/{{cookiecutter.project_slug}}/HISTORY.rst b/tests/test-extensions/local_extension/{{cookiecutter.project_slug}}/HISTORY.rst new file mode 100644 index 000000000..8bb7c6136 --- /dev/null +++ b/tests/test-extensions/local_extension/{{cookiecutter.project_slug}}/HISTORY.rst @@ -0,0 +1,8 @@ +History +------- + +0.1.0 +----- + +First release of {{cookiecutter.test_value_class_based}} on PyPI. +{{cookiecutter.test_value_function_based}} diff --git a/tests/test-generate-context-v2/min-v2-cookiecutter/{{cookiecutter.repo_name}}/file b/tests/test-generate-context-v2/min-v2-cookiecutter/{{cookiecutter.repo_name}}/file new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test-generate-context-v2/representative-director.json b/tests/test-generate-context-v2/representative-director.json new file mode 100644 index 000000000..708e653c7 --- /dev/null +++ b/tests/test-generate-context-v2/representative-director.json @@ -0,0 +1,49 @@ +{ + "version": "2.0", + "template": { + "name": "cc-representative", + "variables": [ + { + "name": "director_credit", + "default": true, + "prompt": "Is there a director credit on this film?", + "description": "Directors take credit for most of their films, usually...", + "type": "boolean" + }, + { + "name": "director_exists", + "default": false, + "prompt": "Is there a Director?", + "prompt_user": true, + "description": "The director exists.", + "hide_input": false, + "choices": [ + true, + false + ], + "type": "boolean" + }, + { + "name": "director_name", + "default": "Allan Smithe", + "prompt": "What's the Director's full name?", + "prompt_user": true, + "description": "The default director is not proud of their work, we hope you are.", + "hide_input": false, + "choices": [ + "Allan Smithe", + "Ridley Scott", + "Victor Fleming", + "John Houston" + ], + "validation": "^[a-z][A-Z]+$", + "validation_flags": [ + "verbose", + "ascii" + ], + "skip_if": "{{cookiecutter.director_credit == False}}", + "type": "string" + } + ] + } +} diff --git a/tests/test-generate-context-v2/representative-var.json b/tests/test-generate-context-v2/representative-var.json new file mode 100644 index 000000000..3be4d675a --- /dev/null +++ b/tests/test-generate-context-v2/representative-var.json @@ -0,0 +1,26 @@ +{ + "name": "cc-representative", + "cookiecutter_version": "2.0.0", + "variables" : [ + { + "name": "director_credit", + "default": true, + "prompt": "Is there a director credit on this film?", + "description": "Directors take credit for most of their films, usually...", + "type": "boolean" + }, + { + "name": "director_name", + "default": "Allan Smithe", + "prompt": "What's the Director's full name?", + "prompt_user": true, + "description": "The default director is not proud of their work, we hope you are.", + "hide_input": false, + "choices": ["Allan Smithe", "Ridley Scott", "Victor Fleming", "John Houston","{{cookiecutter.director_name}}"], + "validation": "^[a-z][A-Z]+$", + "validation_flags": ["verbose", "ascii"], + "skip_if": "{{cookiecutter.director_credit == False}}", + "type": "string" + } + ] +} diff --git a/tests/test-generate-context-v2/test_choices-miss.json b/tests/test-generate-context-v2/test_choices-miss.json new file mode 100644 index 000000000..b1ff8e3aa --- /dev/null +++ b/tests/test-generate-context-v2/test_choices-miss.json @@ -0,0 +1,20 @@ +{ + "version": "2.0", + "template": { + "name": "test_choices-miss", + "variables": [ + { + "name": "license", + "type": "string", + "default": "MIT", + "choices": [ + "MIT", + "BSD3", + "GNU-GPL3", + "Apache2", + "Mozilla2" + ] + } + ] + } +} diff --git a/tests/test-generate-context/nested_dict.json b/tests/test-generate-context/nested_dict.json new file mode 100644 index 000000000..c13ea10f3 --- /dev/null +++ b/tests/test-generate-context/nested_dict.json @@ -0,0 +1,10 @@ +{ + "full_name": "Raphael Pierzina", + "github_username": "hackebrot", + "project": { + "name": "Kivy Project", + "description": "Kivy Project", + "repo_name": "{{cookiecutter.project_name|lower}}", + "orientation": ["all", "landscape", "portrait"] + } +} \ No newline at end of file diff --git a/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/README.rst b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/README.rst new file mode 100644 index 000000000..2413548bf --- /dev/null +++ b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/README.rst @@ -0,0 +1,5 @@ +============ +Fake Project +============ + +{{cookiecutter.render_test}} diff --git a/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/README.txt b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/README.txt new file mode 100644 index 000000000..2413548bf --- /dev/null +++ b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/README.txt @@ -0,0 +1,5 @@ +============ +Fake Project +============ + +{{cookiecutter.render_test}} diff --git a/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/rendered/not_rendered.yml b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/rendered/not_rendered.yml new file mode 100644 index 000000000..a31cf752c --- /dev/null +++ b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/rendered/not_rendered.yml @@ -0,0 +1,2 @@ +--- +- name: {{cookiecutter.render_test}} diff --git a/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-not-rendered/README.rst b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-not-rendered/README.rst new file mode 100644 index 000000000..2413548bf --- /dev/null +++ b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-not-rendered/README.rst @@ -0,0 +1,5 @@ +============ +Fake Project +============ + +{{cookiecutter.render_test}} diff --git a/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.md b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.md new file mode 100644 index 000000000..0e74081d8 --- /dev/null +++ b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.md @@ -0,0 +1,3 @@ +# Fake Project + +{{cookiecutter.render_test}} diff --git a/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.rst b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.rst new file mode 100644 index 000000000..2413548bf --- /dev/null +++ b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.rst @@ -0,0 +1,5 @@ +============ +Fake Project +============ + +{{cookiecutter.render_test}} diff --git a/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.txt b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.txt new file mode 100644 index 000000000..2413548bf --- /dev/null +++ b/tests/test-generate-copy-without-render-override/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}-rendered/README.txt @@ -0,0 +1,5 @@ +============ +Fake Project +============ + +{{cookiecutter.render_test}} diff --git a/tests/test-pyhooks/hooks/post_gen_project.py b/tests/test-pyhooks/hooks/post_gen_project.py index c8b7c194f..98a5a353b 100644 --- a/tests/test-pyhooks/hooks/post_gen_project.py +++ b/tests/test-pyhooks/hooks/post_gen_project.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """Simple post-gen hook for testing project folder and custom file creation.""" print('pre generation hook') diff --git a/tests/test-pyhooks/hooks/pre_gen_project.py b/tests/test-pyhooks/hooks/pre_gen_project.py index 4d84bd3ec..6f1887bd4 100644 --- a/tests/test-pyhooks/hooks/pre_gen_project.py +++ b/tests/test-pyhooks/hooks/pre_gen_project.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """Simple pre-gen hook for testing project folder and custom file creation.""" print('pre generation hook') diff --git a/tests/test-pyshellhooks/hooks/post_gen_project.py b/tests/test-pyshellhooks/hooks/post_gen_project.py index c8b7c194f..98a5a353b 100644 --- a/tests/test-pyshellhooks/hooks/post_gen_project.py +++ b/tests/test-pyshellhooks/hooks/post_gen_project.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """Simple post-gen hook for testing project folder and custom file creation.""" print('pre generation hook') diff --git a/tests/test-pyshellhooks/hooks/pre_gen_project.py b/tests/test-pyshellhooks/hooks/pre_gen_project.py index db8bfc6a7..daeb59acb 100644 --- a/tests/test-pyshellhooks/hooks/pre_gen_project.py +++ b/tests/test-pyshellhooks/hooks/pre_gen_project.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """Simple pre-gen hook for testing project folder and custom file creation.""" diff --git a/tests/test-templates/extends/cookiecutter.json b/tests/test-templates/extends/cookiecutter.json new file mode 100644 index 000000000..e8798e956 --- /dev/null +++ b/tests/test-templates/extends/cookiecutter.json @@ -0,0 +1,5 @@ +{ + "project_slug": "foobar", + "command_line_interface": "click", + "use_pytest": "y" +} diff --git a/tests/test-templates/extends/templates/base-requirements.jinja b/tests/test-templates/extends/templates/base-requirements.jinja new file mode 100644 index 000000000..964fa3379 --- /dev/null +++ b/tests/test-templates/extends/templates/base-requirements.jinja @@ -0,0 +1,6 @@ +pip +{% 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 %} \ No newline at end of file diff --git a/tests/test-templates/extends/templates/click-requirements.jinja b/tests/test-templates/extends/templates/click-requirements.jinja new file mode 100644 index 000000000..4d441badc --- /dev/null +++ b/tests/test-templates/extends/templates/click-requirements.jinja @@ -0,0 +1 @@ +Click \ No newline at end of file diff --git a/tests/test-templates/extends/templates/pytest-requirements.jinja b/tests/test-templates/extends/templates/pytest-requirements.jinja new file mode 100644 index 000000000..55b033e90 --- /dev/null +++ b/tests/test-templates/extends/templates/pytest-requirements.jinja @@ -0,0 +1 @@ +pytest \ No newline at end of file diff --git a/tests/test-templates/extends/{{cookiecutter.project_slug}}/requirements.txt b/tests/test-templates/extends/{{cookiecutter.project_slug}}/requirements.txt new file mode 100644 index 000000000..910e3721e --- /dev/null +++ b/tests/test-templates/extends/{{cookiecutter.project_slug}}/requirements.txt @@ -0,0 +1 @@ +{% extends "base-requirements.jinja" %} \ No newline at end of file diff --git a/tests/test-templates/include/cookiecutter.json b/tests/test-templates/include/cookiecutter.json new file mode 100644 index 000000000..e8798e956 --- /dev/null +++ b/tests/test-templates/include/cookiecutter.json @@ -0,0 +1,5 @@ +{ + "project_slug": "foobar", + "command_line_interface": "click", + "use_pytest": "y" +} diff --git a/tests/test-templates/include/templates/click-requirements.jinja b/tests/test-templates/include/templates/click-requirements.jinja new file mode 100644 index 000000000..4d441badc --- /dev/null +++ b/tests/test-templates/include/templates/click-requirements.jinja @@ -0,0 +1 @@ +Click \ No newline at end of file diff --git a/tests/test-templates/include/templates/pytest-requirements.jinja b/tests/test-templates/include/templates/pytest-requirements.jinja new file mode 100644 index 000000000..55b033e90 --- /dev/null +++ b/tests/test-templates/include/templates/pytest-requirements.jinja @@ -0,0 +1 @@ +pytest \ No newline at end of file diff --git a/tests/test-templates/include/{{cookiecutter.project_slug}}/requirements.txt b/tests/test-templates/include/{{cookiecutter.project_slug}}/requirements.txt new file mode 100644 index 000000000..62645fd16 --- /dev/null +++ b/tests/test-templates/include/{{cookiecutter.project_slug}}/requirements.txt @@ -0,0 +1,5 @@ +pip +{% if cookiecutter.command_line_interface|lower == 'click' -%} +{% include 'click-requirements.jinja' %}{% endif %} +{% if cookiecutter.use_pytest == 'y' -%} +{% include 'pytest-requirements.jinja' %}{% endif %} \ No newline at end of file diff --git a/tests/test-templates/no-templates/cookiecutter.json b/tests/test-templates/no-templates/cookiecutter.json new file mode 100644 index 000000000..e8798e956 --- /dev/null +++ b/tests/test-templates/no-templates/cookiecutter.json @@ -0,0 +1,5 @@ +{ + "project_slug": "foobar", + "command_line_interface": "click", + "use_pytest": "y" +} diff --git a/tests/test-templates/no-templates/{{cookiecutter.project_slug}}/requirements.txt b/tests/test-templates/no-templates/{{cookiecutter.project_slug}}/requirements.txt new file mode 100644 index 000000000..b8b92ac2b --- /dev/null +++ b/tests/test-templates/no-templates/{{cookiecutter.project_slug}}/requirements.txt @@ -0,0 +1,5 @@ +pip +{% if cookiecutter.command_line_interface|lower == 'click' -%} +Click{% endif %} +{% if cookiecutter.use_pytest == 'y' -%} +pytest{% endif %} \ No newline at end of file diff --git a/tests/test-templates/super/cookiecutter.json b/tests/test-templates/super/cookiecutter.json new file mode 100644 index 000000000..e8798e956 --- /dev/null +++ b/tests/test-templates/super/cookiecutter.json @@ -0,0 +1,5 @@ +{ + "project_slug": "foobar", + "command_line_interface": "click", + "use_pytest": "y" +} diff --git a/tests/test-templates/super/templates/base-requirements.jinja b/tests/test-templates/super/templates/base-requirements.jinja new file mode 100644 index 000000000..23c1ca511 --- /dev/null +++ b/tests/test-templates/super/templates/base-requirements.jinja @@ -0,0 +1,7 @@ +pip +{% 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 %} \ No newline at end of file diff --git a/tests/test-templates/super/templates/click-requirements.jinja b/tests/test-templates/super/templates/click-requirements.jinja new file mode 100644 index 000000000..4d441badc --- /dev/null +++ b/tests/test-templates/super/templates/click-requirements.jinja @@ -0,0 +1 @@ +Click \ No newline at end of file diff --git a/tests/test-templates/super/templates/pytest-requirements.jinja b/tests/test-templates/super/templates/pytest-requirements.jinja new file mode 100644 index 000000000..55b033e90 --- /dev/null +++ b/tests/test-templates/super/templates/pytest-requirements.jinja @@ -0,0 +1 @@ +pytest \ No newline at end of file diff --git a/tests/test-templates/super/{{cookiecutter.project_slug}}/requirements.txt b/tests/test-templates/super/{{cookiecutter.project_slug}}/requirements.txt new file mode 100644 index 000000000..602b5772f --- /dev/null +++ b/tests/test-templates/super/{{cookiecutter.project_slug}}/requirements.txt @@ -0,0 +1,2 @@ +{% extends "base-requirements.jinja" %} +{% block dev_dependencies %}{{ super() }}{% endblock %} \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py index e357fef2e..0364e2cef 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -3,12 +3,15 @@ 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 @@ -48,7 +51,7 @@ def version_cli_flag(request): def test_cli_version(cli_runner, version_cli_flag): - """Verify correct version output by `cookiecutter` on cli invocation.""" + """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') @@ -69,8 +72,8 @@ def test_cli(cli_runner): result = cli_runner('tests/fake-repo-pre/', '--no-input') assert result.exit_code == 0 assert os.path.isdir('fake-project') - with open(os.path.join('fake-project', 'README.rst')) as f: - assert 'Project name: **Fake Project**' in f.read() + content = Path("fake-project", "README.rst").read_text() + assert 'Project name: **Fake Project**' in content @pytest.mark.usefixtures('remove_fake_project_dir') @@ -79,8 +82,8 @@ def test_cli_verbose(cli_runner): result = cli_runner('tests/fake-repo-pre/', '--no-input', '-v') assert result.exit_code == 0 assert os.path.isdir('fake-project') - with open(os.path.join('fake-project', 'README.rst')) as f: - assert 'Project name: **Fake Project**' in f.read() + content = Path("fake-project", "README.rst").read_text() + assert 'Project name: **Fake Project**' in content @pytest.mark.usefixtures('remove_fake_project_dir') @@ -106,6 +109,7 @@ def test_cli_replay(mocker, cli_runner): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -132,6 +136,7 @@ def test_cli_replay_file(mocker, cli_runner): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -167,6 +172,7 @@ def test_cli_exit_on_noinput_and_replay(mocker, cli_runner): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -202,6 +208,7 @@ def test_run_cookiecutter_on_overwrite_if_exists_and_replay( password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -258,6 +265,7 @@ def test_cli_output_dir(mocker, cli_runner, output_dir_flag, output_dir): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -302,6 +310,7 @@ def test_user_config(mocker, cli_runner, user_config_path): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -311,7 +320,10 @@ def test_default_user_config_overwrite(mocker, cli_runner, user_config_path): template_path = 'tests/fake-repo-pre/' result = cli_runner( - template_path, '--config-file', user_config_path, '--default-config', + template_path, + '--config-file', + user_config_path, + '--default-config', ) assert result.exit_code == 0 @@ -329,6 +341,7 @@ def test_default_user_config_overwrite(mocker, cli_runner, user_config_path): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -354,6 +367,7 @@ def test_default_user_config(mocker, cli_runner): password=None, directory=None, accept_hooks=True, + keep_project_on_failure=False, ) @@ -362,7 +376,11 @@ def test_echo_undefined_variable_error(output_dir, cli_runner): template_path = 'tests/undefined-variable/file-name/' result = cli_runner( - '--no-input', '--default-config', '--output-dir', output_dir, template_path, + '--no-input', + '--default-config', + '--output-dir', + output_dir, + template_path, ) assert result.exit_code == 1 @@ -380,6 +398,7 @@ def test_echo_undefined_variable_error(output_dir, cli_runner): 'github_username': 'hackebrot', 'project_slug': 'testproject', '_template': template_path, + '_repo_dir': template_path, '_output_dir': output_dir, } } @@ -392,7 +411,11 @@ def test_echo_unknown_extension_error(output_dir, cli_runner): template_path = 'tests/test-extensions/unknown/' result = cli_runner( - '--no-input', '--default-config', '--output-dir', output_dir, template_path, + '--no-input', + '--default-config', + '--output-dir', + output_dir, + template_path, ) assert result.exit_code == 1 @@ -400,23 +423,57 @@ def test_echo_unknown_extension_error(output_dir, cli_runner): 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', + 'tests/fake-repo-pre/', + '--no-input', + '-v', + 'project_name=Awesomez', ) assert result.exit_code == 0 assert os.path.isdir('fake-project') - with open(os.path.join('fake-project', 'README.rst')) as f: - assert 'Project name: **Awesomez**' in f.read() + 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', + 'tests/fake-repo-pre/', + '--no-input', + '-v', + 'ExtraContextWithNoEqualsSoInvalid', ) assert result.exit_code == 2 assert "Error: Invalid value for '[EXTRA_CONTEXT]...'" in result.output @@ -438,7 +495,10 @@ def test_debug_file_non_verbose(cli_runner, debug_file): assert not debug_file.exists() result = cli_runner( - '--no-input', '--debug-file', str(debug_file), 'tests/fake-repo-pre/', + '--no-input', + '--debug-file', + str(debug_file), + 'tests/fake-repo-pre/', ) assert result.exit_code == 0 @@ -484,16 +544,15 @@ 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)) - with open(user_config_path, 'w') as config_file: - # In YAML, double quotes mean to use escape sequences. - # Single quotes mean we will have unescaped backslahes. - # http://blogs.perl.org/users/tinita/2018/03/ - # strings-in-yaml---to-quote-or-not-to-quote.html - config_file.write("cookiecutters_dir: '%s'" % fake_template_dir) - open(os.path.join('fake-project', 'cookiecutter.json'), 'w').write('{}') + # 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), + '--list-installed', + '--config-file', + user_config_path, + str(debug_file), ) assert "1 installed templates:" in result.output @@ -505,8 +564,7 @@ def test_debug_list_installed_templates_failure( ): """Verify --list-installed command error on invocation.""" os.makedirs(os.path.dirname(user_config_path)) - with open(user_config_path, 'w') as config_file: - config_file.write('cookiecutters_dir: "/notarealplace/"') + Path(user_config_path).write_text('cookiecutters_dir: "/notarealplace/"') result = cli_runner( '--list-installed', '--config-file', user_config_path, str(debug_file) @@ -520,12 +578,15 @@ def test_debug_list_installed_templates_failure( 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', + 'tests/fake-repo-dir/', + '--no-input', + '-v', + '--directory=my-dir', ) assert result.exit_code == 0 assert os.path.isdir("fake-project") - with open(os.path.join("fake-project", "README.rst")) as f: - assert "Project name: **Fake Project**" in f.read() + content = Path("fake-project", "README.rst").read_text() + assert "Project name: **Fake Project**" in content cli_accept_hook_arg_testdata = [ @@ -571,6 +632,7 @@ def test_cli_accept_hooks( directory=None, skip_if_file_exists=False, accept_hooks=expected, + keep_project_on_failure=False, ) diff --git a/tests/test_context.py b/tests/test_context.py index e414580f7..58149da1c 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -90,6 +90,43 @@ def test_load_context_defaults(): ) +@pytest.mark.usefixtures('clean_system') +def test_load_context_defaults_no_requires(): + + cc = load_cookiecutter('tests/test-context/cookiecutter-no-requires.json') + cc_cfg = context.load_context( + cc['cookiecutter-no-requires'], no_input=True, verbose=False + ) + + assert cc_cfg['full_name'] == 'Raphael Pierzina' + assert cc_cfg['email'] == 'raphael@hackebrot.de' + assert cc_cfg['plugin_name'] == 'emoji' + assert cc_cfg['module_name'] == 'emoji' + assert cc_cfg['license'] == 'MIT' + assert cc_cfg['docs'] is False + assert 'docs_tool' not in cc_cfg.keys() # skip_if worked + assert cc_cfg['year'] == time.strftime('%Y') + assert cc_cfg['incept_year'] == 2017 + assert cc_cfg['released'] is False + assert cc_cfg['temperature'] == 77.3 + assert cc_cfg['Release-GUID'] == UUID('04f5eaa9ee7345469dccffc538b27194').hex + assert cc_cfg['_extensions'] == [ + 'cookiecutter.extensions.SlugifyExtension', + 'jinja2_time.TimeExtension', + ] + assert cc_cfg['_jinja2_env_vars'] == {"optimized": True} + assert ( + cc_cfg['copy_with_out_render'] + == "['*.html', '*not_rendered_dir', 'rendered_dir/not_rendered_file.ini']" + ) + assert cc_cfg['fixtures'] == OrderedDict( + [ + ('foo', OrderedDict([('scope', 'session'), ('autouse', True)])), + ('bar', OrderedDict([('scope', 'function'), ('autouse', False)])), + ] + ) + + @pytest.mark.usefixtures('clean_system') def test_load_context_defaults_skips_branch(): """ @@ -168,7 +205,9 @@ def test_prompt_string(mocker): expected_value = 'Input String' mock_prompt = mocker.patch( - 'cookiecutter.prompt.click.prompt', autospec=True, return_value=expected_value, + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=expected_value, ) m = mocker.Mock() @@ -180,7 +219,10 @@ def test_prompt_string(mocker): r = context.prompt_string(v, default='Alpha') assert mock_prompt.call_args == mocker.call( - v.prompt, default='Alpha', hide_input=v.hide_input, type=click.STRING, + v.prompt, + default='Alpha', + hide_input=v.hide_input, + type=click.STRING, ) assert r == expected_value @@ -191,7 +233,9 @@ def test_prompt_bool(mocker): expected_value = True mock_prompt = mocker.patch( - 'cookiecutter.prompt.click.prompt', autospec=True, return_value=expected_value, + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=expected_value, ) m = mocker.Mock() @@ -203,7 +247,10 @@ def test_prompt_bool(mocker): r = context.prompt_boolean(v, default=False) assert mock_prompt.call_args == mocker.call( - v.prompt, default=False, hide_input=v.hide_input, type=click.BOOL, + v.prompt, + default=False, + hide_input=v.hide_input, + type=click.BOOL, ) assert r # expected_value @@ -214,7 +261,9 @@ def test_prompt_int(mocker): expected_value = 777 mock_prompt = mocker.patch( - 'cookiecutter.prompt.click.prompt', autospec=True, return_value=expected_value, + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=expected_value, ) m = mocker.Mock() @@ -226,7 +275,10 @@ def test_prompt_int(mocker): r = context.prompt_int(v, default=1000) assert mock_prompt.call_args == mocker.call( - v.prompt, default=1000, hide_input=v.hide_input, type=click.INT, + v.prompt, + default=1000, + hide_input=v.hide_input, + type=click.INT, ) assert r == expected_value @@ -237,7 +289,9 @@ def test_prompt_float(mocker): expected_value = 3.14 mock_prompt = mocker.patch( - 'cookiecutter.prompt.click.prompt', autospec=True, return_value=expected_value, + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=expected_value, ) m = mocker.Mock() @@ -249,7 +303,10 @@ def test_prompt_float(mocker): r = context.prompt_float(v, default=3.0) assert mock_prompt.call_args == mocker.call( - v.prompt, default=3.0, hide_input=v.hide_input, type=click.FLOAT, + v.prompt, + default=3.0, + hide_input=v.hide_input, + type=click.FLOAT, ) assert r == expected_value @@ -260,7 +317,9 @@ def test_prompt_uuid(mocker): expected_value = '931ef56c3e7b45eea0427bac386f0a98' mock_prompt = mocker.patch( - 'cookiecutter.prompt.click.prompt', autospec=True, return_value=expected_value, + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=expected_value, ) m = mocker.Mock() @@ -272,7 +331,10 @@ def test_prompt_uuid(mocker): r = context.prompt_uuid(v, default=None) assert mock_prompt.call_args == mocker.call( - v.prompt, default=None, hide_input=v.hide_input, type=click.UUID, + v.prompt, + default=None, + hide_input=v.hide_input, + type=click.UUID, ) assert r == expected_value @@ -283,7 +345,9 @@ def test_prompt_json(monkeypatch, mocker): expected_value = '{"port": 67888, "colors": ["red", "green", "blue"]}' mocker.patch( - 'click.termui.visible_prompt_func', autospec=True, return_value=expected_value, + 'click.termui.visible_prompt_func', + autospec=True, + return_value=expected_value, ) m = mocker.Mock() m.side_effect = context.Variable @@ -325,7 +389,9 @@ def test_prompt_json_default(mocker): cfg = '{"port": 67888, "colors": ["red", "green", "blue"]}' mock_prompt = mocker.patch( - 'cookiecutter.prompt.click.prompt', autospec=True, return_value=expected_value, + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=expected_value, ) m = mocker.Mock() @@ -352,7 +418,9 @@ def test_prompt_yes_no_default_no(mocker): expected_value = 'y' mock_prompt = mocker.patch( - 'cookiecutter.prompt.click.prompt', autospec=True, return_value=expected_value, + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=expected_value, ) m = mocker.Mock() @@ -368,7 +436,10 @@ def test_prompt_yes_no_default_no(mocker): r = context.prompt_yes_no(v, default=False) assert mock_prompt.call_args == mocker.call( - v.prompt, default='n', hide_input=v.hide_input, type=click.BOOL, + v.prompt, + default='n', + hide_input=v.hide_input, + type=click.BOOL, ) assert r # expected_value @@ -379,7 +450,9 @@ def test_prompt_yes_no_default_yes(mocker): expected_value = 'y' mock_prompt = mocker.patch( - 'cookiecutter.prompt.click.prompt', autospec=True, return_value=expected_value, + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=expected_value, ) m = mocker.Mock() @@ -395,7 +468,10 @@ def test_prompt_yes_no_default_yes(mocker): r = context.prompt_yes_no(v, default=True) assert mock_prompt.call_args == mocker.call( - v.prompt, default='y', hide_input=v.hide_input, type=click.BOOL, + v.prompt, + default='y', + hide_input=v.hide_input, + type=click.BOOL, ) assert r # expected_value @@ -411,7 +487,9 @@ def test_prompt_choice(mocker): expected_license = 'MIT' mocker.patch( - 'cookiecutter.prompt.click.prompt', autospec=True, return_value=expected_value, + 'cookiecutter.prompt.click.prompt', + autospec=True, + return_value=expected_value, ) m = mocker.Mock() diff --git a/tests/test_cookiecutter_local_no_input.py b/tests/test_cookiecutter_local_no_input.py index 0031cbcbf..9e89c9539 100644 --- a/tests/test_cookiecutter_local_no_input.py +++ b/tests/test_cookiecutter_local_no_input.py @@ -5,6 +5,7 @@ """ import os import textwrap +from pathlib import Path import pytest @@ -65,9 +66,8 @@ 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') - with open(os.path.join(project_dir, 'README.rst')) as fh: - contents = fh.read() - assert "Project name: **Fake Project**" in contents + content = Path(project_dir, 'README.rst').read_text() + assert "Project name: **Fake Project**" in content @pytest.mark.usefixtures('clean_system', 'remove_additional_dirs') @@ -76,11 +76,9 @@ def test_cookiecutter_dict_values_in_context(): project_dir = main.cookiecutter('tests/fake-repo-dict', no_input=True) assert project_dir == os.path.abspath('fake-project-dict') - with open(os.path.join(project_dir, 'README.md')) as fh: - contents = fh.read() - + content = Path(project_dir, 'README.md').read_text() assert ( - contents + content == textwrap.dedent( """ # README diff --git a/tests/test_custom_extensions_in_hooks.py b/tests/test_custom_extensions_in_hooks.py index cd5f8aa3a..fdf2861be 100644 --- a/tests/test_custom_extensions_in_hooks.py +++ b/tests/test_custom_extensions_in_hooks.py @@ -4,8 +4,7 @@ Tests to ensure custom cookiecutter extensions are properly made available to pre- and post-gen hooks. """ -import codecs -import os +from pathlib import Path import pytest @@ -18,7 +17,7 @@ ) def template(request): """Fixture. Allows to split pre and post hooks test directories.""" - return 'tests/test-extensions/' + request.param + return f"tests/test-extensions/{request.param}" @pytest.fixture(autouse=True) @@ -40,9 +39,5 @@ def test_hook_with_extension(template, output_dir): extra_context={'project_slug': 'foobar', 'name': 'Cookiemonster'}, ) - readme_file = os.path.join(project_dir, 'README.rst') - - with codecs.open(readme_file, encoding='utf8') as f: - readme = f.read().strip() - - assert readme == 'Hello Cookiemonster!' + readme = Path(project_dir, 'README.rst').read_text(encoding="utf-8") + assert readme.strip() == 'Hello Cookiemonster!' diff --git a/tests/test_default_extensions.py b/tests/test_default_extensions.py index e73ef9c1b..d229e3d8c 100644 --- a/tests/test_default_extensions.py +++ b/tests/test_default_extensions.py @@ -1,9 +1,10 @@ """Verify Jinja2 filters/extensions are available from pre-gen/post-gen hooks.""" import os +import uuid +from pathlib import Path import freezegun import pytest -import uuid from cookiecutter.main import cookiecutter @@ -25,7 +26,7 @@ def test_jinja2_time_extension(tmp_path): changelog_file = os.path.join(project_dir, 'HISTORY.rst') assert os.path.isfile(changelog_file) - with open(changelog_file, 'r', encoding='utf-8') as f: + with Path(changelog_file).open(encoding='utf-8') as f: changelog_lines = f.readlines() expected_lines = [ @@ -57,8 +58,7 @@ def test_jinja2_uuid_extension(tmp_path): changelog_file = os.path.join(project_dir, 'id') assert os.path.isfile(changelog_file) - with open(changelog_file, 'r', encoding='utf-8') as f: + with Path(changelog_file).open(encoding='utf-8') as f: changelog_lines = f.readlines() uuid.UUID(changelog_lines[0], version=4) - assert True diff --git a/tests/test_find.py b/tests/test_find.py index 761c02235..affbd806e 100644 --- a/tests/test_find.py +++ b/tests/test_find.py @@ -1,5 +1,5 @@ """Tests for `cookiecutter.find` module.""" -import os +from pathlib import Path import pytest @@ -9,12 +9,12 @@ @pytest.fixture(params=['fake-repo-pre', 'fake-repo-pre2']) def repo_dir(request): """Fixture returning path for `test_find_template` test.""" - return os.path.join('tests', request.param) + return Path('tests', request.param) def test_find_template(repo_dir): """Verify correctness of `find.find_template` path detection.""" template = find.find_template(repo_dir=repo_dir) - test_dir = os.path.join(repo_dir, '{{cookiecutter.repo_name}}') + test_dir = Path(repo_dir, '{{cookiecutter.repo_name}}') assert template == test_dir diff --git a/tests/test_generate_context.py b/tests/test_generate_context.py index 2a2bc06dd..892a7588b 100644 --- a/tests/test_generate_context.py +++ b/tests/test_generate_context.py @@ -110,7 +110,13 @@ def test_default_context_replacement_in_generate_context(): def test_generate_context_decodes_non_ascii_chars(): """Verify `generate_context` correctly decodes non-ascii chars.""" - expected_context = {'non_ascii': OrderedDict([('full_name', 'éèà'),])} + expected_context = { + 'non_ascii': OrderedDict( + [ + ('full_name', 'éèà'), + ] + ) + } generated_context = generate.generate_context( context_file='tests/test-generate-context/non_ascii.json' @@ -196,3 +202,45 @@ def test_apply_overwrites_sets_default_for_choice_variable(template_context): ) 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 diff --git a/tests/test_generate_context_v2.py b/tests/test_generate_context_v2.py index e23205cc5..c2750e337 100644 --- a/tests/test_generate_context_v2.py +++ b/tests/test_generate_context_v2.py @@ -220,6 +220,23 @@ 'John Houston', ] +expected_file2_v8 = deepcopy(expected_file2_v1) +expected_file2_v8['template']['variables'].insert( + 1, + OrderedDict( + [ + ('name', 'director_exists'), + ('default', False), + ('prompt', 'Is there a Director?'), + ('prompt_user', True), + ('description', 'The director exists.'), + ('hide_input', False), + ('choices', [True, False]), + ('type', 'boolean'), + ] + ), +) + def context_data_serializer(): """ @@ -309,12 +326,103 @@ def context_data_serializer(): }, {"test": expected_file1_v0}, ) + + context_choices_with_default_misses = ( + { + 'context_file': 'tests/test-generate-context-v2/test_choices-miss.json', + 'default_context': {'license': 'Cherokee'}, + }, + { + "test_choices-miss": OrderedDict( + [ + ("version", "2.0"), + ( + "template", + OrderedDict( + [ + ("name", "test_choices-miss"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "license"), + ("type", "string"), + ("default", "MIT"), + ( + "choices", + [ + "MIT", + "BSD3", + "GNU-GPL3", + "Apache2", + "Mozilla2", + ], + ), + ] + ) + ], + ), + ] + ), + ), + ] + ) + }, + ) + + context_choices_with_extra_misses = ( + { + 'context_file': 'tests/test-generate-context-v2/test_choices-miss.json', + 'extra_context': {'license': 'MIT'}, + }, + { + "test_choices-miss": OrderedDict( + [ + ("version", "2.0"), + ( + "template", + OrderedDict( + [ + ("name", "test_choices-miss"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "license"), + ("type", "string"), + ("default", "MIT"), + ( + "choices", + [ + "MIT", + "BSD3", + "GNU-GPL3", + "Apache2", + "Mozilla2", + ], + ), + ] + ) + ], + ), + ] + ), + ), + ] + ) + }, + ) + yield context yield context_with_default yield context_with_extra yield context_with_default_and_extra yield context_choices_with_default yield context_choices_with_default_not_in_choices + yield context_choices_with_default_misses + yield context_choices_with_extra_misses @pytest.mark.usefixtures('clean_system') @@ -327,6 +435,100 @@ def test_generate_context(input_params, expected_context): assert generate.generate_context(**input_params) == expected_context +def context_data_value_errors(): + context_choices_with_default_value_error = ( + { + 'context_file': 'tests/test-generate-context-v2/test_choices.json', + 'default_context': [{'license': 'MIT'}], + }, + { + "test_choices": OrderedDict( + [ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "license"), + ("default", "MIT"), + ( + "choices", + [ + "MIT", + "Apache2", + "BSD3", + "GNU-GPL3", + "Mozilla2", + ], + ), + ] + ) + ], + ), + ] + ) + }, + True, + ) + context_choices_with_extra_value_error = ( + { + 'context_file': 'tests/test-generate-context-v2/test_choices.json', + 'default_context': {'license': 'Apache2'}, + 'extra_context': [{'name': 'license', 'default': 'MIT'}], + }, + { + "test_choices": OrderedDict( + [ + ("name", "cookiecutter-pytest-plugin"), + ("cookiecutter_version", "2.0.0"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "license"), + ("default", "MIT"), + ( + "choices", + [ + "MIT", + "Apache2", + "BSD3", + "GNU-GPL3", + "Mozilla2", + ], + ), + ] + ) + ], + ), + ] + ) + }, + False, + ) + yield context_choices_with_default_value_error + yield context_choices_with_extra_value_error + + +@pytest.mark.usefixtures('clean_system') +@pytest.mark.parametrize( + 'input_params, expected_context, raise_exception', context_data_value_errors() +) +def test_generate_context_value_error(input_params, expected_context, raise_exception): + """ + Test the generated context for several input parameters against the + according expected context. + """ + if raise_exception: + with pytest.raises(ValueError) as excinfo: + generate.generate_context(**input_params) + else: + generate.generate_context(**input_params) + + @pytest.mark.usefixtures('clean_system') def test_generate_context_extra_ctx_invalid(): """ @@ -415,6 +617,239 @@ def test_generate_context_extra_ctx_list_item_dict_no_name_field_match(): assert msg in str(excinfo.value) +@pytest.mark.usefixtures('clean_system') +def test_raise_exception_when_attempting_to_remove_mandatory_field(): + """ + Test that ValueError is raised if attempt is made to remove a mandatory + field -- the default field. + The other mandatory field, name, cannot be removed because it has to be + used to specify which variable to remove. + """ + xtra_context = [ + { + 'name': 'director_name', + 'default': '<>', + }, + ] + + with pytest.raises(ValueError) as excinfo: + generate.generate_context( + context_file='tests/test-generate-context-v2/representative.json', + default_context=None, + extra_context=xtra_context, + ) + + assert "Cannot remove mandatory 'default' field" in str(excinfo.value) + + +def gen_context_data_inputs_expected_var(): + # Test the ability to change the variable's name field (since it is used + # to identify the variable to be modifed) with extra context and to remove + # a key from the context via the removal token: '<>' + context_with_valid_extra_2 = ( + { + 'context_file': 'tests/test-generate-context-v2/representative.json', + 'extra_context': [ + { + 'name': 'director_credit::producer_credit', + 'prompt': 'Is there a producer credit on this film?', + 'description': 'There are usually a lot of producers...', + }, + { + 'name': 'director_name', + 'skip_if': '<>', + }, + ], + }, + OrderedDict( + [ + ( + "representative", + OrderedDict( + [ + ('version', '2.0'), + ( + "template", + OrderedDict( + [ + ("name", "cc-representative"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "producer_credit"), + ("default", True), + ( + "prompt", + "Is there a producer credit on this film?", + ), + ( + "description", + "There are usually a lot of producers...", + ), + ("type", "boolean"), + ] + ), + OrderedDict( + [ + ("name", "director_name"), + ("default", "Allan Smithe"), + ( + "prompt", + "What's the Director's full name?", + ), + ("prompt_user", True), + ( + "description", + "The default director is not proud of their work, we hope you are.", + ), + ("hide_input", False), + ( + "choices", + [ + "Allan Smithe", + "Ridley Scott", + "Victor Fleming", + "John Houston", + ], + ), + ("validation", "^[a-z][A-Z]+$"), + ( + "validation_flags", + ["verbose", "ascii"], + ), + ("type", "string"), + ] + ), + ], + ), + ] + ), + ), + ] + ), + ) + ] + ), + ) + # Test the ability to change the variable's name field (since it is used + # to identify the variable to be modifed) with extra context and to also + # test that any other references in other variables that might use the + # original variable name get updated as well. + context_with_valid_extra_2_B = ( + { + 'context_file': 'tests/test-generate-context-v2/representative_2B.json', + 'extra_context': [ + { + 'name': 'director_credit::producer_credit', + 'prompt': 'Is there a producer credit on this film?', + 'description': 'There are usually a lot of producers...', + }, + ], + }, + OrderedDict( + [ + ( + "representative_2B", + OrderedDict( + [ + ('version', '2.0'), + ( + 'requires', + OrderedDict( + [('cookiecutter', '>1'), ('python', '>=3.0')] + ), + ), + ( + "template", + OrderedDict( + [ + ("name", "cc-representative"), + ( + "variables", + [ + OrderedDict( + [ + ("name", "producer_credit"), + ("default", True), + ( + "prompt", + "Is there a producer credit on this film?", + ), + ( + "description", + "There are usually a lot of producers...", + ), + ("type", "boolean"), + ] + ), + OrderedDict( + [ + ("name", "director_name"), + ("default", "Allan Smithe"), + ( + "prompt", + "What's the Director's full name?", + ), + ("prompt_user", True), + ( + "description", + "The default director is not proud of their work, we hope you are.", + ), + ("hide_input", False), + ( + "choices", + [ + "Allan Smithe", + "Ridley Scott", + "Victor Fleming", + "John Houston", + "{{cookiecutter.producer_credit}}", + ], + ), + ("validation", "^[a-z][A-Z]+$"), + ( + "validation_flags", + ["verbose", "ascii"], + ), + ( + "skip_if", + "{{cookiecutter.producer_credit == False}}", + ), + ("type", "string"), + ] + ), + ], + ), + ] + ), + ), + ] + ), + ) + ] + ), + ) + + yield context_with_valid_extra_2 + yield context_with_valid_extra_2_B + + +@pytest.mark.usefixtures('clean_system') +@pytest.mark.parametrize( + 'input_params, expected_context', gen_context_data_inputs_expected_var() +) +def test_generate_context_with_extra_context_dictionary_var( + input_params, expected_context, monkeypatch +): + """ + Test the generated context with extra content overwrite to multiple fields, + with creation of new fields NOT allowed. + """ + assert generate.generate_context(**input_params) == expected_context + + def gen_context_data_inputs_expected(): """ Creates a generator of combination: @@ -448,17 +883,20 @@ def gen_context_data_inputs_expected(): # a key from the context via the removal token: '<>' context_with_valid_extra_2 = ( { - 'context_file': 'tests/test-generate-context-v2/representative.json', + 'context_file': 'tests/test-generate-context-v2/representative-director.json', 'extra_context': [ { 'name': 'director_credit::producer_credit', 'prompt': 'Is there a producer credit on this film?', 'description': 'There are usually a lot of producers...', }, - {'name': 'director_name', 'skip_if': '<>',}, + { + 'name': 'director_name', + 'skip_if': '<>', + }, ], }, - {"representative": expected_file2_v1}, + {"representative-director": expected_file2_v8}, ) # Test the ability to change the variable's name field (since it is used # to identify the variable to be modifed) with extra context and to also @@ -617,7 +1055,12 @@ def gen_context_data_inputs_expected(): context_with_valid_extra_6 = ( { 'context_file': 'tests/test-generate-context-v2/representative.json', - 'extra_context': [{'name': 'director_name', 'default': 'John Ford',}], + 'extra_context': [ + { + 'name': 'director_name', + 'default': 'John Ford', + } + ], }, {"representative": expected_file2_v5}, ) @@ -647,7 +1090,12 @@ def gen_context_data_inputs_expected(): context_with_valid_extra_8 = ( { 'context_file': 'tests/test-generate-context-v2/representative.json', - 'extra_context': [{'name': 'director_name', 'default': 'Peter Sellers',}], + 'extra_context': [ + { + 'name': 'director_name', + 'default': 'Peter Sellers', + } + ], }, {"representative": expected_file2_v7}, ) @@ -677,25 +1125,3 @@ def test_generate_context_with_extra_context_dictionary( assert OrderedDict(generate.generate_context(**input_params)) == OrderedDict( expected_context ) - - -@pytest.mark.usefixtures('clean_system') -def test_raise_exception_when_attempting_to_remove_mandatory_field(): - """ - Test that ValueError is raised if attempt is made to remove a mandatory - field -- the default field. - The other mandatory field, name, cannot be removed because it has to be - used to specify which variable to remove. - """ - xtra_context = [ - {'name': 'director_name', 'default': '<>',}, - ] - - with pytest.raises(ValueError) as excinfo: - generate.generate_context( - context_file='tests/test-generate-context-v2/representative.json', - default_context=None, - extra_context=xtra_context, - ) - - assert "Cannot remove mandatory 'default' field" in str(excinfo.value) diff --git a/tests/test_generate_copy_without_render.py b/tests/test_generate_copy_without_render.py index 7d614824c..9e6039787 100644 --- a/tests/test_generate_copy_without_render.py +++ b/tests/test_generate_copy_without_render.py @@ -1,5 +1,6 @@ """Verify correct work of `_copy_without_render` context option.""" import os +from pathlib import Path import pytest @@ -43,33 +44,33 @@ def test_generate_copy_without_render_extensions(): assert 'test_copy_without_render-not-rendered' in dir_contents assert 'test_copy_without_render-rendered' in dir_contents - with open('test_copy_without_render/README.txt') as f: - assert '{{cookiecutter.render_test}}' in f.read() + file_1 = Path('test_copy_without_render/README.txt').read_text() + assert '{{cookiecutter.render_test}}' in file_1 - with open('test_copy_without_render/README.rst') as f: - assert 'I have been rendered!' in f.read() + file_2 = Path('test_copy_without_render/README.rst').read_text() + assert 'I have been rendered!' in file_2 - with open( + file_3 = Path( 'test_copy_without_render/test_copy_without_render-rendered/README.txt' - ) as f: - assert '{{cookiecutter.render_test}}' in f.read() + ).read_text() + assert '{{cookiecutter.render_test}}' in file_3 - with open( + file_4 = Path( 'test_copy_without_render/test_copy_without_render-rendered/README.rst' - ) as f: - assert 'I have been rendered' in f.read() + ).read_text() + assert 'I have been rendered' in file_4 - with open( + file_5 = Path( 'test_copy_without_render/' 'test_copy_without_render-not-rendered/' 'README.rst' - ) as f: - assert '{{cookiecutter.render_test}}' in f.read() + ).read_text() + assert '{{cookiecutter.render_test}}' in file_5 - with open('test_copy_without_render/rendered/not_rendered.yml') as f: - assert '{{cookiecutter.render_test}}' in f.read() + file_6 = Path('test_copy_without_render/rendered/not_rendered.yml').read_text() + assert '{{cookiecutter.render_test}}' in file_6 - with open( + file_7 = Path( 'test_copy_without_render/' 'test_copy_without_render-rendered/' 'README.md' - ) as f: - assert '{{cookiecutter.render_test}}' in f.read() + ).read_text() + assert '{{cookiecutter.render_test}}' in file_7 diff --git a/tests/test_generate_copy_without_render_override.py b/tests/test_generate_copy_without_render_override.py new file mode 100644 index 000000000..c2d836e3d --- /dev/null +++ b/tests/test_generate_copy_without_render_override.py @@ -0,0 +1,95 @@ +"""Verify correct work of `_copy_without_render` context option.""" +import os +from pathlib import Path + +import pytest + +from cookiecutter import generate +from cookiecutter import 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 diff --git a/tests/test_generate_file.py b/tests/test_generate_file.py index 2ca30dff2..9ff622168 100644 --- a/tests/test_generate_file.py +++ b/tests/test_generate_file.py @@ -1,6 +1,8 @@ """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 @@ -44,9 +46,8 @@ def test_generate_file(env): env=env, ) assert os.path.isfile('tests/files/cheese.txt') - with open('tests/files/cheese.txt', 'rt') as f: - generated_text = f.read() - assert generated_text == 'Testing cheese' + generated_text = Path('tests/files/cheese.txt').read_text() + assert generated_text == 'Testing cheese' def test_generate_file_jsonify_filter(env): @@ -57,9 +58,8 @@ def test_generate_file_jsonify_filter(env): project_dir=".", infile=infile, context={'cookiecutter': data}, env=env ) assert os.path.isfile('tests/files/cheese.txt') - with open('tests/files/cheese.txt', 'rt') as f: - generated_text = f.read() - assert json.loads(generated_text) == data + generated_text = Path('tests/files/cheese.txt').read_text() + assert json.loads(generated_text) == data @pytest.mark.parametrize("length", (10, 40)) @@ -71,9 +71,8 @@ def test_generate_file_random_ascii_string(env, length, punctuation): 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') - with open('tests/files/cheese.txt', 'rt') as f: - generated_text = f.read() - assert len(generated_text) == length + generated_text = Path('tests/files/cheese.txt').read_text() + assert len(generated_text) == length def test_generate_file_with_true_condition(env): @@ -91,9 +90,8 @@ def test_generate_file_with_true_condition(env): env=env, ) assert os.path.isfile('tests/files/cheese.txt') - with open('tests/files/cheese.txt', 'rt') as f: - generated_text = f.read() - assert generated_text == 'Testing that generate_file was y' + 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): @@ -114,17 +112,16 @@ def test_generate_file_with_false_condition(env): @pytest.fixture -def expected_msg(): +def expected_msg_regex(): """Fixture. Used to ensure that exception generated text contain full data.""" - msg = ( + return re.compile( 'Missing end of comment tag\n' - ' File "./tests/files/syntax_error.txt", line 1\n' - ' I eat {{ syntax_error }} {# this comment is not closed}' + ' {2}File "(.[/\\\\])*tests[/\\\\]files[/\\\\]syntax_error.txt", line 1\n' + ' {4}I eat {{ syntax_error }} {# this comment is not closed}' ) - return msg.replace("/", os.sep) -def test_generate_file_verbose_template_syntax_error(env, expected_msg): +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( @@ -133,7 +130,7 @@ def test_generate_file_verbose_template_syntax_error(env, expected_msg): context={'syntax_error': 'syntax_error'}, env=env, ) - assert str(exception.value) == expected_msg + assert expected_msg_regex.match(str(exception.value)) def test_generate_file_does_not_translate_lf_newlines_to_crlf(env, tmp_path): @@ -148,7 +145,7 @@ def test_generate_file_does_not_translate_lf_newlines_to_crlf(env, tmp_path): # this generated file should have a LF line ending gf = 'tests/files/cheese_lf_newlines.txt' - with open(gf, 'r', encoding='utf-8', newline='') as f: + 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' @@ -166,7 +163,7 @@ def test_generate_file_does_not_translate_crlf_newlines_to_lf(env): # this generated file should have a CRLF line ending gf = 'tests/files/cheese_crlf_newlines.txt' - with open(gf, 'r', encoding='utf-8', newline='') as f: + 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' diff --git a/tests/test_generate_files.py b/tests/test_generate_files.py index 4e21c7053..bb4075cae 100644 --- a/tests/test_generate_files.py +++ b/tests/test_generate_files.py @@ -44,7 +44,7 @@ def test_generate_files(tmp_path): assert simple_file.exists() assert simple_file.is_file() - simple_text = open(simple_file, 'rt', encoding='utf-8').read() + simple_text = Path(simple_file).read_text(encoding='utf-8') assert simple_text == 'I eat pizzä' @@ -60,7 +60,7 @@ def test_generate_files_with_linux_newline(tmp_path): assert newline_file.is_file() assert newline_file.exists() - with open(newline_file, 'r', encoding='utf-8', newline='') as f: + 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' @@ -83,8 +83,8 @@ def test_generate_files_with_jinja2_environment(tmp_path): assert conditions_file.is_file() assert conditions_file.exists() - simple_text = conditions_file.open('rt', encoding='utf-8').read() - assert simple_text == u'I eat pizzä\n' + 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): @@ -100,7 +100,7 @@ def test_generate_files_with_trailing_newline_forced_to_linux_by_context(tmp_pat assert newline_file.is_file() assert newline_file.exists() - with open(newline_file, 'r', encoding='utf-8', newline='') as f: + 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' @@ -118,7 +118,7 @@ def test_generate_files_with_windows_newline(tmp_path): assert newline_file.is_file() assert newline_file.exists() - with open(newline_file, 'r', encoding='utf-8', newline='') as f: + 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' @@ -136,7 +136,7 @@ def test_generate_files_with_windows_newline_forced_to_linux_by_context(tmp_path assert newline_file.is_file() assert newline_file.exists() - with open(newline_file, 'r', encoding='utf-8', newline='') as f: + with Path(newline_file).open(encoding='utf-8', newline='') as f: simple_text = f.readline() assert simple_text == 'newline is CRLF\n' @@ -202,7 +202,6 @@ def test_generate_files_permissions(tmp_path): output_dir=tmp_path, ) - assert Path(tmp_path, 'inputpermissions/simple.txt').exists() assert Path(tmp_path, 'inputpermissions/simple.txt').is_file() # Verify source simple.txt should still be 0o644 @@ -241,7 +240,7 @@ def test_generate_files_with_overwrite_if_exists_with_skip_if_file_exists(tmp_pa simple_with_new_line_file = Path(tmp_path, 'inputpizzä/simple-with-newline.txt') Path(tmp_path, 'inputpizzä').mkdir(parents=True) - with open(simple_file, 'w') as f: + with Path(simple_file).open('w') as f: f.write('temp') generate.generate_files( @@ -257,7 +256,7 @@ def test_generate_files_with_overwrite_if_exists_with_skip_if_file_exists(tmp_pa assert Path(simple_with_new_line_file).is_file() assert Path(simple_with_new_line_file).exists() - simple_text = open(simple_file, 'rt', encoding='utf-8').read() + simple_text = Path(simple_file).read_text(encoding='utf-8') assert simple_text == 'temp' @@ -267,8 +266,7 @@ def test_generate_files_with_skip_if_file_exists(tmp_path): simple_with_new_line_file = Path(tmp_path, 'inputpizzä/simple-with-newline.txt') Path(tmp_path, 'inputpizzä').mkdir(parents=True) - with open(simple_file, 'w') as f: - f.write('temp') + Path(simple_file).write_text('temp') with pytest.raises(exceptions.OutputDirExistsException): generate.generate_files( @@ -279,11 +277,10 @@ def test_generate_files_with_skip_if_file_exists(tmp_path): ) assert Path(simple_file).is_file() - assert Path(simple_file).exists() assert not Path(simple_with_new_line_file).is_file() assert not Path(simple_with_new_line_file).exists() - simple_text = open(simple_file, 'rt', encoding='utf-8').read() + simple_text = Path(simple_file).read_text(encoding='utf-8') assert simple_text == 'temp' @@ -293,8 +290,7 @@ def test_generate_files_with_overwrite_if_exists(tmp_path): simple_with_new_line_file = Path(tmp_path, 'inputpizzä/simple-with-newline.txt') Path(tmp_path, 'inputpizzä').mkdir(parents=True) - with open(simple_file, 'w') as f: - f.write('temp') + Path(simple_file).write_text('temp') generate.generate_files( context={'cookiecutter': {'food': 'pizzä'}}, @@ -308,7 +304,7 @@ def test_generate_files_with_overwrite_if_exists(tmp_path): assert Path(simple_with_new_line_file).is_file() assert Path(simple_with_new_line_file).exists() - simple_text = open(simple_file, 'rt', encoding='utf-8').read() + simple_text = Path(simple_file).read_text(encoding='utf-8') assert simple_text == 'I eat pizzä' @@ -382,7 +378,7 @@ def test_raise_undefined_variable_dir_name(output_dir, undefined_context): error = err.value directory = Path('testproject', '{{cookiecutter.foobar}}') - msg = "Unable to create directory '{}'".format(directory) + msg = f"Unable to create directory '{directory}'" assert msg == error.message assert error.context == undefined_context @@ -390,6 +386,18 @@ def test_raise_undefined_variable_dir_name(output_dir, 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 ): @@ -407,7 +415,7 @@ def test_raise_undefined_variable_dir_name_existing_project( error = err.value directory = Path('testproject', '{{cookiecutter.foobar}}') - msg = "Unable to create directory '{}'".format(directory) + msg = f"Unable to create directory '{directory}'" assert msg == error.message assert error.context == undefined_context diff --git a/tests/test_generate_hooks.py b/tests/test_generate_hooks.py index 9624bb8ae..a57e0dbde 100644 --- a/tests/test_generate_hooks.py +++ b/tests/test_generate_hooks.py @@ -2,6 +2,7 @@ import errno import os import sys +from pathlib import Path import pytest @@ -123,7 +124,7 @@ def test_run_failing_hook_removes_output_directory(): hook_path = os.path.join(hooks_path, 'pre_gen_project.py') - with open(hook_path, 'w') as f: + with Path(hook_path).open('w') as f: f.write("#!/usr/bin/env python\n") f.write("import sys; sys.exit(1)\n") @@ -152,7 +153,7 @@ def test_run_failing_hook_preserves_existing_output_directory(): hook_path = os.path.join(hooks_path, 'pre_gen_project.py') - with open(hook_path, 'w') as f: + with Path(hook_path).open('w') as f: f.write("#!/usr/bin/env python\n") f.write("import sys; sys.exit(1)\n") diff --git a/tests/test_get_config.py b/tests/test_get_config.py index 760db3cdb..a37317413 100644 --- a/tests/test_get_config.py +++ b/tests/test_get_config.py @@ -62,7 +62,11 @@ def test_get_config(): 'github_username': 'example', 'project': { 'description': 'description', - 'tags': ['first', 'second', 'third',], + 'tags': [ + 'first', + 'second', + 'third', + ], }, }, 'abbreviations': { diff --git a/tests/test_get_user_config.py b/tests/test_get_user_config.py index 560c1d873..551502c5c 100644 --- a/tests/test_get_user_config.py +++ b/tests/test_get_user_config.py @@ -48,7 +48,11 @@ def custom_config(): 'github_username': 'example', 'project': { 'description': 'description', - 'tags': ['first', 'second', 'third',], + 'tags': [ + 'first', + 'second', + 'third', + ], }, }, 'cookiecutters_dir': '/home/example/some-path-to-templates', diff --git a/tests/test_hooks.py b/tests/test_hooks.py index d8b55dff2..03795cab5 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -1,9 +1,14 @@ """Tests for `cookiecutter.hooks` module.""" -import os +import pathlib +import shutil +import tempfile + import errno +import os import stat import sys import textwrap +from pathlib import Path import pytest @@ -18,10 +23,9 @@ def make_test_repo(name, multiple_hooks=False): os.mkdir(hook_dir) os.mkdir(template) - with open(os.path.join(template, 'README.rst'), 'w') as f: - f.write("foo\n===\n\nbar\n") + Path(template, 'README.rst').write_text("foo\n===\n\nbar\n") - with open(os.path.join(hook_dir, 'pre_gen_project.py'), 'w') as f: + 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") @@ -32,7 +36,7 @@ def make_test_repo(name, multiple_hooks=False): if sys.platform.startswith('win'): post = 'post_gen_project.bat' - with open(os.path.join(hook_dir, post), 'w') as f: + with Path(hook_dir, post).open('w') as f: f.write("@echo off\n") f.write("\n") f.write("echo post generation hook\n") @@ -40,7 +44,7 @@ def make_test_repo(name, multiple_hooks=False): else: post = 'post_gen_project.sh' filename = os.path.join(hook_dir, post) - with open(filename, 'w') as f: + with Path(filename).open('w') as f: f.write("#!/bin/bash\n") f.write("\n") f.write("echo 'post generation hook';\n") @@ -52,7 +56,7 @@ def make_test_repo(name, multiple_hooks=False): if multiple_hooks: if sys.platform.startswith('win'): pre = 'pre_gen_project.bat' - with open(os.path.join(hook_dir, pre), 'w') as f: + with Path(hook_dir, pre).open('w') as f: f.write("@echo off\n") f.write("\n") f.write("echo post generation hook\n") @@ -60,7 +64,7 @@ def make_test_repo(name, multiple_hooks=False): else: pre = 'pre_gen_project.sh' filename = os.path.join(hook_dir, pre) - with open(filename, 'w') as f: + with Path(filename).open('w') as f: f.write("#!/bin/bash\n") f.write("\n") f.write("echo 'post generation hook';\n") @@ -71,7 +75,7 @@ def make_test_repo(name, multiple_hooks=False): return post -class TestFindHooks(object): +class TestFindHooks: """Class to unite find hooks related tests in one place.""" repo_path = 'tests/test-hooks' @@ -91,7 +95,7 @@ def test_find_hook(self): actual_hook_path = hooks.find_hook('pre_gen_project') assert expected_pre == actual_hook_path[0] - expected_post = os.path.abspath('hooks/{}'.format(self.post_hook)) + 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] @@ -111,7 +115,7 @@ def test_hook_not_found(self): assert hooks.find_hook('unknown_hook') is None -class TestExternalHooks(object): +class TestExternalHooks: """Class to unite tests for hooks with different project paths.""" repo_path = os.path.abspath('tests/test-hooks/') @@ -154,7 +158,7 @@ def test_run_failing_script(self, mocker): with pytest.raises(exceptions.FailedHookException) as excinfo: hooks.run_script(os.path.join(self.hooks_path, self.post_hook)) - assert 'Hook script failed (error: {})'.format(err) in str(excinfo.value) + 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.""" @@ -182,13 +186,13 @@ def test_run_script_with_context(self): if sys.platform.startswith('win'): post = 'post_gen_project.bat' - with open(os.path.join(self.hooks_path, post), 'w') as f: + 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 open(hook_path, 'w') as fh: + with Path(hook_path).open('w') as fh: fh.write("#!/bin/bash\n") fh.write("\n") fh.write("echo 'post generation hook';\n") @@ -216,12 +220,41 @@ def test_run_hook(self): hooks.run_hook('post_gen_project', tests_dir, {}) assert os.path.isfile(os.path.join(tests_dir, 'shell_post.txt')) + def test_run_hook_debug(self): + """Execute hook from specified template in specified output \ + directory.""" + tests_dir = os.path.join(self.repo_path, 'input{{hooks}}') + assert os.path.isdir(tempfile.gettempdir()), tempfile.gettempdir() + debug_hooks_dir = os.path.join( + tempfile.gettempdir(), 'cookiecutter-debug-hooks' + ) + if os.path.isdir(debug_hooks_dir): + shutil.rmtree(debug_hooks_dir) + os.mkdir(debug_hooks_dir) + for location in (debug_hooks_dir, '1'): + os.environ['COOKIECUTTER_DEBUG_HOOKS'] = location + 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')) + hook_traces = list( + pathlib.Path(os.environ['COOKIECUTTER_DEBUG_HOOKS']).glob('*') + ) + assert len(hook_traces) > 2, os.system( + "ls -l " + os.environ['COOKIECUTTER_DEBUG_HOOKS'] + ) + del os.environ['COOKIECUTTER_DEBUG_HOOKS'] + shutil.rmtree(debug_hooks_dir) + 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 open(hook_path, 'w') as f: + with Path(hook_path).open('w') as f: f.write("#!/usr/bin/env python\n") f.write("import sys; sys.exit(1)\n") diff --git a/tests/test_main.py b/tests/test_main.py index 65aaf45d2..0af183596 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,4 +1,8 @@ """Collection of tests around cookiecutter's replay feature.""" +import collections +import os +import shutil + from cookiecutter.main import cookiecutter @@ -22,11 +26,16 @@ def test_replay_dump_template_name( mocker.patch('cookiecutter.main.generate_files') cookiecutter( - '.', no_input=True, replay=False, config_file=user_config_file, + '.', + 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, + user_config_data['replay_dir'], + 'fake-repo-tmpl', + mocker.ANY, ) @@ -46,27 +55,14 @@ def test_replay_load_template_name( mocker.patch('cookiecutter.main.generate_files') 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_files') - - cookiecutter( - '.', replay='./custom-replay-file', config_file=user_config_file, + '.', + replay=True, + config_file=user_config_file, ) mock_replay_load.assert_called_once_with( - '.', 'custom-replay-file', + user_config_data['replay_dir'], + 'fake-repo-tmpl', ) @@ -78,20 +74,63 @@ def test_version_2_load_context_call(monkeypatch, mocker, user_config_file): for this test and call cookiecutter with '.' for the target template. """ monkeypatch.chdir('tests/test-generate-context-v2/min-v2-cookiecutter') - + if os.path.exists('test-repo'): + shutil.rmtree('test-repo') mock_replay_dump = mocker.patch('cookiecutter.main.dump') - mock_version_1_prompt_for_config = mocker.patch( - 'cookiecutter.main.prompt_for_config' + counts = {} + + def patch_load_context(counts): + counts['load_context'] = 0 + + def load_context(json_object, no_input=False, verbose=True, counts=counts): + counts["load_context"] += 1 + return collections.OrderedDict( + { + 'repo_name': 'test-repo', + } + ) + + return load_context + + def patch_prompt_for_config(counts): + counts['prompt_for_config'] = 0 + + def prompt_for_config(context, no_input=False): + counts["prompt_for_config"] += 1 + return {} + + mocker.patch('cookiecutter.main.prompt_for_config', patch_prompt_for_config(counts)) + mocker.patch('cookiecutter.main.load_context', patch_load_context(counts)) + + cookiecutter( + '.', + no_input=True, + replay=False, + config_file=user_config_file, ) - mock_version_2_load_context = mocker.patch('cookiecutter.main.load_context') + if os.path.exists('test-repo'): + shutil.rmtree('test-repo') + assert mock_replay_dump.call_count == 1 + assert counts["load_context"] == 1 + assert counts["prompt_for_config"] == 0 + + +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_files') cookiecutter( - '.', no_input=True, replay=False, config_file=user_config_file, + '.', + replay='./custom-replay-file', + config_file=user_config_file, ) - assert mock_version_1_prompt_for_config.call_count == 0 - assert mock_version_2_load_context.call_count == 1 - assert mock_replay_dump.call_count == 1 + mock_replay_load.assert_called_once_with( + '.', + 'custom-replay-file', + ) diff --git a/tests/test_output_folder.py b/tests/test_output_folder.py index 166a45005..00c4f7846 100644 --- a/tests/test_output_folder.py +++ b/tests/test_output_folder.py @@ -5,6 +5,7 @@ TestOutputFolder.test_output_folder """ import os +from pathlib import Path import pytest @@ -32,11 +33,11 @@ def test_output_folder(): something = """Hi! My name is Audrey Greenfeld. It is 2014.""" - something2 = open('output_folder/something.txt').read() + something2 = Path('output_folder/something.txt').read_text() assert something == something2 in_folder = "The color is green and the letter is D." - in_folder2 = open('output_folder/folder/in_folder.txt').read() + 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') diff --git a/tests/test_prompt.py b/tests/test_prompt.py index 0932ab573..9e85bcd9e 100644 --- a/tests/test_prompt.py +++ b/tests/test_prompt.py @@ -21,7 +21,7 @@ class TestRenderVariable: 'raw_var, rendered_var', [ (1, '1'), - (True, 'True'), + (True, True), ('foo', 'foo'), ('{{cookiecutter.project}}', 'foobar'), (None, None), @@ -39,7 +39,7 @@ def test_convert_to_str(self, mocker, raw_var, rendered_var): assert result == rendered_var # Make sure that non None non str variables are converted beforehand - if raw_var is not None: + 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) @@ -49,10 +49,10 @@ def test_convert_to_str(self, mocker, raw_var, rendered_var): @pytest.mark.parametrize( 'raw_var, rendered_var', [ - ({1: True, 'foo': False}, {'1': 'True', 'foo': 'False'}), + ({1: True, 'foo': False}, {'1': True, 'foo': False}), ( {'{{cookiecutter.project}}': ['foo', 1], 'bar': False}, - {'foobar': ['foo', '1'], 'bar': 'False'}, + {'foobar': ['foo', '1'], 'bar': False}, ), (['foo', '{{cookiecutter.project}}', None], ['foo', 'foobar', None]), ], @@ -66,7 +66,7 @@ def test_convert_to_str_complex_variables(self, raw_var, rendered_var): assert result == rendered_var -class TestPrompt(object): +class TestPrompt: """Class to unite user prompt related tests.""" @pytest.mark.parametrize( @@ -80,7 +80,8 @@ class TestPrompt(object): 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: default, + 'cookiecutter.prompt.read_user_variable', + lambda var, default: default, ) cookiecutter_dict = prompt.prompt_for_config(context) @@ -209,11 +210,11 @@ def test_should_render_private_variables_with_two_underscores(self): [ ('foo', 'Hello world'), ('bar', 123), - ('rendered_foo', u'{{ cookiecutter.foo|lower }}'), + ('rendered_foo', '{{ cookiecutter.foo|lower }}'), ('rendered_bar', 123), - ('_hidden_foo', u'{{ cookiecutter.foo|lower }}'), + ('_hidden_foo', '{{ cookiecutter.foo|lower }}'), ('_hidden_bar', 123), - ('__rendered_hidden_foo', u'{{ cookiecutter.foo|lower }}'), + ('__rendered_hidden_foo', '{{ cookiecutter.foo|lower }}'), ('__rendered_hidden_bar', 123), ] ) @@ -225,7 +226,7 @@ def test_should_render_private_variables_with_two_underscores(self): ('bar', '123'), ('rendered_foo', 'hello world'), ('rendered_bar', '123'), - ('_hidden_foo', u'{{ cookiecutter.foo|lower }}'), + ('_hidden_foo', '{{ cookiecutter.foo|lower }}'), ('_hidden_bar', 123), ('__rendered_hidden_foo', 'hello world'), ('__rendered_hidden_bar', '123'), @@ -251,7 +252,7 @@ def test_should_not_render_private_variables(self): assert cookiecutter_dict == context['cookiecutter'] -class TestReadUserChoice(object): +class TestReadUserChoice: """Class to unite choices prompt related tests.""" def test_should_invoke_read_user_choice(self, mocker): @@ -331,7 +332,7 @@ def test_should_render_choices(self, mocker): assert cookiecutter_dict == expected -class TestPromptChoiceForConfig(object): +class TestPromptChoiceForConfig: """Class to unite choices prompt related tests with config test.""" @pytest.fixture @@ -379,6 +380,42 @@ def test_should_read_user_choice(self, mocker, choices, context): 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) + 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', ( diff --git a/tests/test_read_user_choice.py b/tests/test_read_user_choice.py index ef9ae62f4..f3573593c 100644 --- a/tests/test_read_user_choice.py +++ b/tests/test_read_user_choice.py @@ -24,7 +24,7 @@ def test_click_invocation(mocker, user_choice, expected_value): choice.return_value = click.Choice(OPTIONS) prompt = mocker.patch('click.prompt') - prompt.return_value = '{}'.format(user_choice) + prompt.return_value = f'{user_choice}' assert read_user_choice('varname', OPTIONS) == expected_value diff --git a/tests/test_read_user_dict.py b/tests/test_read_user_dict.py index ccf632258..b4227a43b 100644 --- a/tests/test_read_user_dict.py +++ b/tests/test_read_user_dict.py @@ -1,6 +1,6 @@ """Test `process_json`, `read_user_dict` functions in `cookiecutter.prompt`.""" -import click import pytest +import click.testing from cookiecutter.prompt import ( process_json, @@ -92,35 +92,50 @@ def test_should_call_prompt_with_process_json(mocker): read_user_dict('name', {'project_slug': 'pytest-plugin'}) - assert mock_prompt.call_args == mocker.call( - 'name', type=click.STRING, default='default', value_proc=process_json, + args, kwargs = mock_prompt.call_args + + assert args == ('name',) + assert kwargs['type'] == click.STRING + assert kwargs['default'] == 'default' + assert kwargs['value_proc'].func == process_json + + +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() + def test_should_not_call_process_json_default_value(mocker, monkeypatch): """Make sure that `process_json` is not called when using default value.""" - mock_process_json = mocker.patch('cookiecutter.prompt.process_json', autospec=True) + mock_process_json = mocker.patch( + 'cookiecutter.prompt.process_json', autospec=True, return_value='default' + ) runner = click.testing.CliRunner() - with runner.isolation(input="\n"): + with runner.isolation(input="\n") as streams: read_user_dict('name', {'project_slug': 'pytest-plugin'}) + stdout, stderr = streams + assert not stdout.getvalue().decode().strip() == 'name [default]:\n' mock_process_json.assert_not_called() -def test_read_user_dict_default_value(mocker): +@pytest.mark.parametrize("input", ["\n", "default\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. """ - mock_prompt = mocker.patch( - 'cookiecutter.prompt.click.prompt', autospec=True, return_value='default', - ) - - val = read_user_dict('name', {'project_slug': 'pytest-plugin'}) - - assert mock_prompt.call_args == mocker.call( - 'name', type=click.STRING, default='default', value_proc=process_json, - ) + runner = click.testing.CliRunner() + with runner.isolation(input=input): + val = read_user_dict('name', {'project_slug': 'pytest-plugin'}) assert val == {'project_slug': 'pytest-plugin'} diff --git a/tests/test_specify_output_dir.py b/tests/test_specify_output_dir.py index 56c9eda6a..c907f2855 100644 --- a/tests/test_specify_output_dir.py +++ b/tests/test_specify_output_dir.py @@ -57,6 +57,7 @@ def test_api_invocation(mocker, template, output_dir, context): skip_if_file_exists=False, output_dir=output_dir, accept_hooks=True, + keep_project_on_failure=False, ) @@ -73,4 +74,5 @@ def test_default_output_dir(mocker, template, context): skip_if_file_exists=False, output_dir='.', accept_hooks=True, + keep_project_on_failure=False, ) diff --git a/tests/test_templates.py b/tests/test_templates.py new file mode 100644 index 000000000..44b9475d2 --- /dev/null +++ b/tests/test_templates.py @@ -0,0 +1,39 @@ +""" +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.splitlines() == [ + "pip", + "Click", + "pytest", + ] diff --git a/tests/test_utils.py b/tests/test_utils.py index 5b089ae4a..fdd3692e2 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -17,9 +17,7 @@ def make_readonly(path): def test_force_delete(mocker, tmp_path): """Verify `utils.force_delete` makes files writable.""" ro_file = Path(tmp_path, 'bar') - - with open(ro_file, "w") as f: - f.write("Test data") + ro_file.write_text("Test data") make_readonly(ro_file) rmtree = mocker.Mock() @@ -33,9 +31,9 @@ def test_force_delete(mocker, tmp_path): def test_rmtree(tmp_path): """Verify `utils.rmtree` remove files marked as read-only.""" - with open(Path(tmp_path, 'bar'), "w") as f: - f.write("Test data") - make_readonly(Path(tmp_path, 'bar')) + file_path = Path(tmp_path, "bar") + file_path.write_text("Test data") + make_readonly(file_path) utils.rmtree(tmp_path) @@ -51,8 +49,8 @@ def test_make_sure_path_exists(tmp_path): existing_directory = tmp_path directory_to_create = Path(tmp_path, "not_yet_created") - assert utils.make_sure_path_exists(existing_directory) - assert utils.make_sure_path_exists(directory_to_create) + 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() @@ -67,14 +65,10 @@ def test_make_sure_path_exists_correctly_handle_os_error(mocker): Should return True if directory exist or created. Should return False if impossible to create directory (for example protected) """ - - def raiser(*args, **kwargs): - raise OSError() - - mocker.patch("os.makedirs", raiser) - uncreatable_directory = Path('protected_path') - - assert not utils.make_sure_path_exists(uncreatable_directory) + 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): @@ -92,6 +86,16 @@ def test_work_in(tmp_path): 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_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.""" @@ -112,7 +116,8 @@ 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.utils.read_user_yes_no', return_value=False, + 'cookiecutter.utils.read_user_yes_no', + return_value=False, ) mock_sys_exit = mocker.patch('sys.exit', return_value=True) repo_dir = Path(tmp_path, 'repo') @@ -164,10 +169,7 @@ def test_prompt_should_ask_and_keep_repo_on_reuse(mocker, tmp_path): cloned template repo, it should not be deleted.""" def answer(question, default): - if 'okay to delete' in question: - return False - else: - return True + return 'okay to delete' not in question mock_read_user = mocker.patch( 'cookiecutter.utils.read_user_yes_no', side_effect=answer, autospec=True diff --git a/tests/vcs/test_clone.py b/tests/vcs/test_clone.py index 9e3b78496..ab9598ed8 100644 --- a/tests/vcs/test_clone.py +++ b/tests/vcs/test_clone.py @@ -24,14 +24,15 @@ def test_clone_should_rstrip_trailing_slash_in_repo_url(mocker, clone_dir): mocker.patch('cookiecutter.vcs.is_vcs_installed', autospec=True, return_value=True) mock_subprocess = mocker.patch( - 'cookiecutter.vcs.subprocess.check_output', autospec=True, + 'cookiecutter.vcs.subprocess.check_output', + autospec=True, ) - vcs.clone('https://github.com/foo/bar/', clone_to_dir=str(clone_dir), no_input=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=str(clone_dir), + cwd=clone_dir, stderr=subprocess.STDOUT, ) @@ -44,7 +45,8 @@ def test_clone_should_abort_if_user_does_not_want_to_reclone(mocker, clone_dir): 'cookiecutter.vcs.prompt_and_delete', side_effect=SystemExit, autospec=True ) mock_subprocess = mocker.patch( - 'cookiecutter.vcs.subprocess.check_output', autospec=True, + 'cookiecutter.vcs.subprocess.check_output', + autospec=True, ) # Create repo_dir to trigger prompt_and_delete @@ -58,6 +60,29 @@ def test_clone_should_abort_if_user_does_not_want_to_reclone(mocker, 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', [ @@ -81,33 +106,40 @@ def test_clone_should_invoke_vcs_command( mocker.patch('cookiecutter.vcs.is_vcs_installed', autospec=True, return_value=True) mock_subprocess = mocker.patch( - 'cookiecutter.vcs.subprocess.check_output', autospec=True, + '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=str(clone_dir), no_input=True + 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=str(clone_dir), stderr=subprocess.STDOUT + [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], cwd=expected_repo_dir, stderr=subprocess.STDOUT + [repo_type, 'checkout', *branch_info], + cwd=expected_repo_dir, + stderr=subprocess.STDOUT, ) @pytest.mark.parametrize( 'error_message', [ - ( - "fatal: repository 'https://github.com/hackebro/cookiedozer' not found" - ).encode('utf-8'), - 'hg: abort: HTTP Error 404: Not Found'.encode('utf-8'), + (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): @@ -127,17 +159,15 @@ def test_clone_handles_repo_typo(mocker, clone_dir, error_message): vcs.clone(repository_url, clone_to_dir=str(clone_dir), no_input=True) assert str(err.value) == ( - 'The repository {} could not be found, have you made a typo?' - ).format(repository_url) + f'The repository {repository_url} could not be found, have you made a typo?' + ) @pytest.mark.parametrize( 'error_message', [ - ( - "error: pathspec 'unknown_branch' did not match any file(s) known to git" - ).encode('utf-8'), - "hg: abort: unknown revision 'unknown_branch'!".encode('utf-8'), + 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): @@ -160,8 +190,8 @@ def test_clone_handles_branch_typo(mocker, clone_dir, error_message): assert str(err.value) == ( 'The unknown_branch branch of repository ' - '{} could not found, have you made a typo?' - ).format(repository_url) + f'{repository_url} could not found, have you made a typo?' + ) def test_clone_unknown_subprocess_error(mocker, clone_dir): @@ -170,9 +200,7 @@ def test_clone_unknown_subprocess_error(mocker, clone_dir): 'cookiecutter.vcs.subprocess.check_output', autospec=True, side_effect=[ - subprocess.CalledProcessError( - -1, 'cmd', output='Something went wrong'.encode('utf-8') - ) + subprocess.CalledProcessError(-1, 'cmd', output=b'Something went wrong') ], ) diff --git a/tests/vcs/test_identify_repo.py b/tests/vcs/test_identify_repo.py index fe264edcd..bfb3d56a2 100644 --- a/tests/vcs/test_identify_repo.py +++ b/tests/vcs/test_identify_repo.py @@ -24,14 +24,14 @@ ), ('https://bitbucket.org/foo/bar.hg', 'hg', 'https://bitbucket.org/foo/bar.hg'), ( - 'https://github.com/audreyr/cookiecutter-pypackage.git', + 'https://github.com/audreyfeldroy/cookiecutter-pypackage.git', 'git', - 'https://github.com/audreyr/cookiecutter-pypackage.git', + 'https://github.com/audreyfeldroy/cookiecutter-pypackage.git', ), ( - 'https://github.com/audreyr/cookiecutter-pypackage', + 'https://github.com/audreyfeldroy/cookiecutter-pypackage', 'git', - 'https://github.com/audreyr/cookiecutter-pypackage', + 'https://github.com/audreyfeldroy/cookiecutter-pypackage', ), ( 'git@gitorious.org:cookiecutter-gitorious/cookiecutter-gitorious.git', diff --git a/tests/zipfile/test_unzip.py b/tests/zipfile/test_unzip.py index 5135f4eec..9d4448e59 100644 --- a/tests/zipfile/test_unzip.py +++ b/tests/zipfile/test_unzip.py @@ -1,5 +1,7 @@ """Tests for function unzip() from zipfile module.""" +import shutil import tempfile +from pathlib import Path import pytest @@ -9,7 +11,17 @@ def mock_download(): """Fake download function.""" - with open('tests/files/fake-repo-tmpl.zip', 'rb') as zf: + 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 @@ -157,7 +169,34 @@ def test_unzip_url(mocker, clone_dir): request.iter_content.return_value = mock_download() mocker.patch( - 'cookiecutter.zipfile.requests.get', return_value=request, autospec=True, + '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( @@ -180,7 +219,9 @@ def test_unzip_url_existing_cache(mocker, clone_dir): request.iter_content.return_value = mock_download() mocker.patch( - 'cookiecutter.zipfile.requests.get', return_value=request, autospec=True, + 'cookiecutter.zipfile.requests.get', + return_value=request, + autospec=True, ) # Create an existing cache of the zipfile @@ -203,7 +244,9 @@ def test_unzip_url_existing_cache_no_input(mocker, clone_dir): request.iter_content.return_value = mock_download() mocker.patch( - 'cookiecutter.zipfile.requests.get', return_value=request, autospec=True, + 'cookiecutter.zipfile.requests.get', + return_value=request, + autospec=True, ) # Create an existing cache of the zipfile @@ -227,7 +270,8 @@ def test_unzip_should_abort_if_no_redownload(mocker, clone_dir): ) mock_requests_get = mocker.patch( - 'cookiecutter.zipfile.requests.get', autospec=True, + 'cookiecutter.zipfile.requests.get', + autospec=True, ) # Create an existing cache of the zipfile @@ -240,3 +284,25 @@ def test_unzip_should_abort_if_no_redownload(mocker, clone_dir): 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 diff --git a/tox.ini b/tox.ini index 1097d7e6a..4cde29296 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,10 @@ [tox] envlist = lint - py36 py37 py38 py39 - pypy3 + py310 minversion = 3.14.2 requires = virtualenv >= 20.4.5 @@ -18,7 +17,7 @@ passenv = HOME commands = pip install -e . - pytest --cov=cookiecutter --cov-report=term --cov-fail-under=100 {posargs:tests} + pytest --cov=cookiecutter --cov-report=term --cov-fail-under=100 --cov-branch {posargs:tests} cov-report: coverage html cov-report: coverage xml deps = -rtest_requirements.txt