summaryrefslogtreecommitdiffstats
path: root/third_party/python/cookiecutter
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/python/cookiecutter')
-rw-r--r--third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/AUTHORS.md215
-rw-r--r--third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/LICENSE32
-rw-r--r--third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/METADATA256
-rw-r--r--third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/RECORD25
-rw-r--r--third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/WHEEL6
-rw-r--r--third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/entry_points.txt2
-rw-r--r--third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/top_level.txt1
-rw-r--r--third_party/python/cookiecutter/cookiecutter/__init__.py2
-rw-r--r--third_party/python/cookiecutter/cookiecutter/__main__.py6
-rw-r--r--third_party/python/cookiecutter/cookiecutter/cli.py231
-rw-r--r--third_party/python/cookiecutter/cookiecutter/config.py122
-rw-r--r--third_party/python/cookiecutter/cookiecutter/environment.py65
-rw-r--r--third_party/python/cookiecutter/cookiecutter/exceptions.py163
-rw-r--r--third_party/python/cookiecutter/cookiecutter/extensions.py66
-rw-r--r--third_party/python/cookiecutter/cookiecutter/find.py31
-rw-r--r--third_party/python/cookiecutter/cookiecutter/generate.py391
-rw-r--r--third_party/python/cookiecutter/cookiecutter/hooks.py131
-rw-r--r--third_party/python/cookiecutter/cookiecutter/log.py51
-rw-r--r--third_party/python/cookiecutter/cookiecutter/main.py140
-rw-r--r--third_party/python/cookiecutter/cookiecutter/prompt.py236
-rw-r--r--third_party/python/cookiecutter/cookiecutter/replay.py52
-rw-r--r--third_party/python/cookiecutter/cookiecutter/repository.py130
-rw-r--r--third_party/python/cookiecutter/cookiecutter/utils.py120
-rw-r--r--third_party/python/cookiecutter/cookiecutter/vcs.py125
-rw-r--r--third_party/python/cookiecutter/cookiecutter/zipfile.py112
25 files changed, 2711 insertions, 0 deletions
diff --git a/third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/AUTHORS.md b/third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/AUTHORS.md
new file mode 100644
index 0000000000..9e5a014943
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/AUTHORS.md
@@ -0,0 +1,215 @@
+# Credits
+
+## Development Leads
+
+- Audrey Roy Greenfeld ([@audreyfeldroy](https://github.com/audreyfeldroy))
+- Daniel Roy Greenfeld ([@pydanny](https://github.com/pydanny))
+- Raphael Pierzina ([@hackebrot](https://github.com/hackebrot))
+
+## Core Committers
+
+- Michael Joseph ([@michaeljoseph](https://github.com/michaeljoseph))
+- Paul Moore ([@pfmoore](https://github.com/pfmoore))
+- Andrey Shpak ([@insspb](https://github.com/insspb))
+- Sorin Sbarnea ([@ssbarnea](https://github.com/ssbarnea))
+- Fábio C. Barrionuevo da Luz ([@luzfcb](https://github.com/luzfcb))
+- Simone Basso ([@simobasso](https://github.com/simobasso))
+- Jens Klein ([@jensens](https://github.com/jensens))
+- Érico Andrei ([@ericof](https://github.com/ericof))
+
+## Contributors
+
+- Steven Loria ([@sloria](https://github.com/sloria))
+- Goran Peretin ([@gperetin](https://github.com/gperetin))
+- Hamish Downer ([@foobacca](https://github.com/foobacca))
+- Thomas Orozco ([@krallin](https://github.com/krallin))
+- Jindrich Smitka ([@s-m-i-t-a](https://github.com/s-m-i-t-a))
+- Benjamin Schwarze ([@benjixx](https://github.com/benjixx))
+- Raphi ([@raphigaziano](https://github.com/raphigaziano))
+- Thomas Chiroux ([@ThomasChiroux](https://github.com/ThomasChiroux))
+- Sergi Almacellas Abellana ([@pokoli](https://github.com/pokoli))
+- Alex Gaynor ([@alex](https://github.com/alex))
+- Rolo ([@rolo](https://github.com/rolo))
+- Pablo ([@oubiga](https://github.com/oubiga))
+- Bruno Rocha ([@rochacbruno](https://github.com/rochacbruno))
+- Alexander Artemenko ([@svetlyak40wt](https://github.com/svetlyak40wt))
+- Mahmoud Abdelkader ([@mahmoudimus](https://github.com/mahmoudimus))
+- Leonardo Borges Avelino ([@lborgav](https://github.com/lborgav))
+- Chris Trotman ([@solarnz](https://github.com/solarnz))
+- Rolf ([@relekang](https://github.com/relekang))
+- Noah Kantrowitz ([@coderanger](https://github.com/coderanger))
+- Vincent Bernat ([@vincentbernat](https://github.com/vincentbernat))
+- Germán Moya ([@pbacterio](https://github.com/pbacterio))
+- Ned Batchelder ([@nedbat](https://github.com/nedbat))
+- Dave Dash ([@davedash](https://github.com/davedash))
+- Johan Charpentier ([@cyberj](https://github.com/cyberj))
+- Éric Araujo ([@merwok](https://github.com/merwok))
+- saxix ([@saxix](https://github.com/saxix))
+- Tzu-ping Chung ([@uranusjr](https://github.com/uranusjr))
+- Caleb Hattingh ([@cjrh](https://github.com/cjrh))
+- Flavio Curella ([@fcurella](https://github.com/fcurella))
+- Adam Venturella ([@aventurella](https://github.com/aventurella))
+- Monty Taylor ([@emonty](https://github.com/emonty))
+- schacki ([@schacki](https://github.com/schacki))
+- Ryan Olson ([@ryanolson](https://github.com/ryanolson))
+- Trey Hunner ([@treyhunner](https://github.com/treyhunner))
+- Russell Keith-Magee ([@freakboy3742](https://github.com/freakboy3742))
+- Mishbah Razzaque ([@mishbahr](https://github.com/mishbahr))
+- Robin Andeer ([@robinandeer](https://github.com/robinandeer))
+- Rachel Sanders ([@trustrachel](https://github.com/trustrachel))
+- Rémy Hubscher ([@Natim](https://github.com/Natim))
+- Dino Petron3 ([@dinopetrone](https://github.com/dinopetrone))
+- Peter Inglesby ([@inglesp](https://github.com/inglesp))
+- Ramiro Batista da Luz ([@ramiroluz](https://github.com/ramiroluz))
+- Omer Katz ([@thedrow](https://github.com/thedrow))
+- lord63 ([@lord63](https://github.com/lord63))
+- Randy Syring ([@rsyring](https://github.com/rsyring))
+- Mark Jones ([@mark0978](https://github.com/mark0978))
+- Marc Abramowitz ([@msabramo](https://github.com/msabramo))
+- Lucian Ursu ([@LucianU](https://github.com/LucianU))
+- Osvaldo Santana Neto ([@osantana](https://github.com/osantana))
+- Matthias84 ([@Matthias84](https://github.com/Matthias84))
+- Simeon Visser ([@svisser](https://github.com/svisser))
+- Guruprasad ([@lgp171188](https://github.com/lgp171188))
+- Charles-Axel Dein ([@charlax](https://github.com/charlax))
+- Diego Garcia ([@drgarcia1986](https://github.com/drgarcia1986))
+- maiksensi ([@maiksensi](https://github.com/maiksensi))
+- Andrew Conti ([@agconti](https://github.com/agconti))
+- Valentin Lab ([@vaab](https://github.com/vaab))
+- Ilja Bauer ([@iljabauer](https://github.com/iljabauer))
+- Elias Dorneles ([@eliasdorneles](https://github.com/eliasdorneles))
+- Matias Saguir ([@mativs](https://github.com/mativs))
+- Johannes ([@johtso](https://github.com/johtso))
+- macrotim ([@macrotim](https://github.com/macrotim))
+- Will McGinnis ([@wdm0006](https://github.com/wdm0006))
+- Cédric Krier ([@cedk](https://github.com/cedk))
+- Tim Osborn ([@ptim](https://github.com/ptim))
+- Aaron Gallagher ([@habnabit](https://github.com/habnabit))
+- mozillazg ([@mozillazg](https://github.com/mozillazg))
+- Joachim Jablon ([@ewjoachim](https://github.com/ewjoachim))
+- Andrew Ittner ([@tephyr](https://github.com/tephyr))
+- Diane DeMers Chen ([@purplediane](https://github.com/purplediane))
+- zzzirk ([@zzzirk](https://github.com/zzzirk))
+- Carol Willing ([@willingc](https://github.com/willingc))
+- phoebebauer ([@phoebebauer](https://github.com/phoebebauer))
+- Adam Chainz ([@adamchainz](https://github.com/adamchainz))
+- Sulé ([@suledev](https://github.com/suledev))
+- Evan Palmer ([@palmerev](https://github.com/palmerev))
+- Bruce Eckel ([@BruceEckel](https://github.com/BruceEckel))
+- Robert Lyon ([@ivanlyon](https://github.com/ivanlyon))
+- Terry Bates ([@terryjbates](https://github.com/terryjbates))
+- Brett Cannon ([@brettcannon](https://github.com/brettcannon))
+- Michael Warkentin ([@mwarkentin](https://github.com/mwarkentin))
+- Bartłomiej Kurzeja ([@B3QL](https://github.com/B3QL))
+- Thomas O'Donnell ([@andytom](https://github.com/andytom))
+- Jeremy Carbaugh ([@jcarbaugh](https://github.com/jcarbaugh))
+- Nathan Cheung ([@cheungnj](https://github.com/cheungnj))
+- Abdó Roig-Maranges ([@aroig](https://github.com/aroig))
+- Steve Piercy ([@stevepiercy](https://github.com/stevepiercy))
+- Corey ([@coreysnyder04](https://github.com/coreysnyder04))
+- Dmitry Evstratov ([@devstrat](https://github.com/devstrat))
+- Eyal Levin ([@eyalev](https://github.com/eyalev))
+- mathagician ([@mathagician](https://github.com/mathagician))
+- Guillaume Gelin ([@ramnes](https://github.com/ramnes))
+- @delirious-lettuce ([@delirious-lettuce](https://github.com/delirious-lettuce))
+- Gasper Vozel ([@karantan](https://github.com/karantan))
+- Joshua Carp ([@jmcarp](https://github.com/jmcarp))
+- @meahow ([@meahow](https://github.com/meahow))
+- Andrea Grandi ([@andreagrandi](https://github.com/andreagrandi))
+- Issa Jubril ([@jubrilissa](https://github.com/jubrilissa))
+- Nytiennzo Madooray ([@Nythiennzo](https://github.com/Nythiennzo))
+- Erik Bachorski ([@dornheimer](https://github.com/dornheimer))
+- cclauss ([@cclauss](https://github.com/cclauss))
+- Andy Craze ([@accraze](https://github.com/accraze))
+- Anthony Sottile ([@asottile](https://github.com/asottile))
+- Jonathan Sick ([@jonathansick](https://github.com/jonathansick))
+- Hugo ([@hugovk](https://github.com/hugovk))
+- Min ho Kim ([@minho42](https://github.com/minho42))
+- Ryan Ly ([@rly](https://github.com/rly))
+- Akintola Rahmat ([@mihrab34](https://github.com/mihrab34))
+- Jai Ram Rideout ([@jairideout](https://github.com/jairideout))
+- Diego Carrasco Gubernatis ([@dacog](https://github.com/dacog))
+- Wagner Negrão ([@wagnernegrao](https://github.com/wagnernegrao))
+- Josh Barnes ([@jcb91](https://github.com/jcb91))
+- Nikita Sobolev ([@sobolevn](https://github.com/sobolevn))
+- Matt Stibbs ([@mattstibbs](https://github.com/mattstibbs))
+- MinchinWeb ([@MinchinWeb](https://github.com/MinchinWeb))
+- kishan ([@kishan](https://github.com/kishan3))
+- tonytheleg ([@tonytheleg](https://github.com/tonytheleg))
+- Roman Hartmann ([@RomHartmann](https://github.com/RomHartmann))
+- DSEnvel ([@DSEnvel](https://github.com/DSEnvel))
+- kishan ([@kishan](https://github.com/kishan3))
+- Bruno Alla ([@browniebroke](https://github.com/browniebroke))
+- nicain ([@nicain](https://github.com/nicain))
+- Carsten Rösnick-Neugebauer ([@croesnick](https://github.com/croesnick))
+- igorbasko01 ([@igorbasko01](https://github.com/igorbasko01))
+- Dan Booth Dev ([@DanBoothDev](https://github.com/DanBoothDev))
+- Pablo Panero ([@ppanero](https://github.com/ppanero))
+- Chuan-Heng Hsiao ([@chhsiao1981](https://github.com/chhsiao1981))
+- Mohammad Hossein Sekhavat ([@mhsekhavat](https://github.com/mhsekhavat))
+- Amey Joshi ([@amey589](https://github.com/amey589))
+- Paul Harrison ([@smoothml](https://github.com/smoothml))
+- Fabio Todaro ([@SharpEdgeMarshall](https://github.com/SharpEdgeMarshall))
+- Nicholas Bollweg ([@bollwyvl](https://github.com/bollwyvl))
+- Jace Browning ([@jacebrowning](https://github.com/jacebrowning))
+- Ionel Cristian Mărieș ([@ionelmc](https://github.com/ionelmc))
+- Kishan Mehta ([@kishan3](https://github.com/kishan3))
+- Wieland Hoffmann ([@mineo](https://github.com/mineo))
+- Antony Lee ([@anntzer](https://github.com/anntzer))
+- Aurélien Gâteau ([@agateau](https://github.com/agateau))
+- Axel H. ([@noirbizarre](https://github.com/noirbizarre))
+- Chris ([@chrisbrake](https://github.com/chrisbrake))
+- Chris Streeter ([@streeter](https://github.com/streeter))
+- Gábor Lipták ([@gliptak](https://github.com/gliptak))
+- Javier Sánchez Portero ([@javiersanp](https://github.com/javiersanp))
+- Nimrod Milo ([@milonimrod](https://github.com/milonimrod))
+- Philipp Kats ([@Casyfill](https://github.com/Casyfill))
+- Reinout van Rees ([@reinout](https://github.com/reinout))
+- Rémy Greinhofer ([@rgreinho](https://github.com/rgreinho))
+- Sebastian ([@sebix](https://github.com/sebix))
+- Stuart Mumford ([@Cadair](https://github.com/Cadair))
+- Tom Forbes ([@orf](https://github.com/orf))
+- Xie Yanbo ([@xyb](https://github.com/xyb))
+- Maxim Ivanov ([@ivanovmg](https://github.com/ivanovmg))
+
+## Backers
+
+We would like to thank the following people for supporting us in our efforts to maintain and improve Cookiecutter:
+
+- Alex DeBrie
+- Alexandre Y. Harano
+- Bruno Alla
+- Carol Willing
+- Russell Keith-Magee
+
+## Sprint Contributors
+
+### PyCon 2016 Sprint
+
+The following people made contributions to the cookiecutter project at the PyCon sprints in Portland, OR from June 2-5 2016.
+Contributions include user testing, debugging, improving documentation, reviewing issues, writing tutorials, creating and updating project templates, and teaching each other.
+
+- Adam Chainz ([@adamchainz](https://github.com/adamchainz))
+- Andrew Ittner ([@tephyr](https://github.com/tephyr))
+- Audrey Roy Greenfeld ([@audreyr](https://github.com/audreyr))
+- 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/third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/LICENSE b/third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/LICENSE
new file mode 100644
index 0000000000..06486a8f39
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/LICENSE
@@ -0,0 +1,32 @@
+Copyright (c) 2013-2021, Audrey Roy Greenfeld
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or
+without modification, are permitted provided that the following
+conditions are met:
+
+* Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following
+disclaimer in the documentation and/or other materials provided
+with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+contributors may be used to endorse or promote products derived
+from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/METADATA b/third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/METADATA
new file mode 100644
index 0000000000..43b7238d8f
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/METADATA
@@ -0,0 +1,256 @@
+Metadata-Version: 2.1
+Name: cookiecutter
+Version: 2.1.1
+Summary: A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project template.
+Home-page: https://github.com/cookiecutter/cookiecutter
+Author: Audrey Feldroy
+Author-email: audreyr@gmail.com
+License: BSD
+Keywords: cookiecutter,Python,projects,project templates,Jinja2,skeleton,scaffolding,project directory,package,packaging
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Console
+Classifier: Intended Audience :: Developers
+Classifier: Natural Language :: English
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Programming Language :: Python
+Classifier: Topic :: Software Development
+Requires-Python: >=3.7
+Description-Content-Type: text/markdown
+License-File: LICENSE
+License-File: AUTHORS.md
+Requires-Dist: binaryornot (>=0.4.4)
+Requires-Dist: Jinja2 (<4.0.0,>=2.7)
+Requires-Dist: click (<9.0.0,>=7.0)
+Requires-Dist: pyyaml (>=5.3.1)
+Requires-Dist: jinja2-time (>=0.2.0)
+Requires-Dist: python-slugify (>=4.0.0)
+Requires-Dist: requests (>=2.23.0)
+
+# Cookiecutter
+
+[![pypi](https://img.shields.io/pypi/v/cookiecutter.svg)](https://pypi.org/project/cookiecutter/)
+[![python](https://img.shields.io/pypi/pyversions/cookiecutter.svg)](https://pypi.org/project/cookiecutter/)
+[![Build Status](https://github.com/cookiecutter/cookiecutter/actions/workflows/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)
+[![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.
+
+- 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)
+
+## 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.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.
+
+### For users of existing templates
+
+- 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
+ ```
+
+- Use it at the command line with a local template:
+
+ ```bash
+ # Create project in the current working directory, from the local
+ # cookiecutter-pypackage/ template
+ $ cookiecutter cookiecutter-pypackage/
+ ```
+
+- Or use it from Python:
+
+ ```py
+ from cookiecutter.main import cookiecutter
+
+ # Create project from the cookiecutter-pypackage/ template
+ cookiecutter('cookiecutter-pypackage/')
+
+ # Create project from the cookiecutter-pypackage.git repo template
+ cookiecutter('https://github.com/audreyfeldroy/cookiecutter-pypackage.git')
+ ```
+
+- 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 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",
+ "project_short_description": "Refreshingly simple static site generator.",
+ "release_date": "2013-07-10",
+ "year": "2013",
+ "version": "0.1.1"
+ }
+ ```
+- 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.
+
+### 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).
+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.
+
+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
+
+These Cookiecutters are maintained by the cookiecutter team:
+
+- [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
+
+The core committer team can be found in [authors section](AUTHORS.md).
+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.
+ 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 [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:
+
+- 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."
+
+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.
+
+## Support This 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 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/third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/RECORD b/third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/RECORD
new file mode 100644
index 0000000000..e9bbcd08c4
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/RECORD
@@ -0,0 +1,25 @@
+cookiecutter/__init__.py,sha256=mvVTrlACc_h48704u9mn8R7qybdzRDm3kUJYaag2U5I,59
+cookiecutter/__main__.py,sha256=0i3swGdJG0xGGydZ8oVXMVv17yQm3kHzRaP35B6uEas,194
+cookiecutter/cli.py,sha256=CWBPBcFBe6P-JqKymRLGmtOU9SBed_gso7nsffi_uCw,6926
+cookiecutter/config.py,sha256=Sy3a9nrybICBvI5YLYS2yUdA2JEbrBJhWUGB2RIDWZ8,4250
+cookiecutter/environment.py,sha256=nCEeEc8puQlMJsU6OGtWzj2QIwbR_cSLY07Z9gmOEHQ,2259
+cookiecutter/exceptions.py,sha256=ri744cAhzcMR86NCzbqq0QzpKZ5Mrsq0Hhl0_6sg8vg,3886
+cookiecutter/extensions.py,sha256=FrxrIxgYnnXTMa8hy38n6E5b9WS70PdpkJlLjip_hoI,1861
+cookiecutter/find.py,sha256=L1JE51TWguZPqAGteLpDsnp3AZVJpVa1Cjl7j5tuYoQ,1008
+cookiecutter/generate.py,sha256=vA6q8IOgrXLrUj2LZAH4vSL8SJXtZdYK8FPVmT56EPw,14827
+cookiecutter/hooks.py,sha256=eT_wRfWXBhiSfMhHerrx8ACAMeexu91fbqtk7NE1oI8,4227
+cookiecutter/log.py,sha256=4KwD0yjS5jGK17pJm94jYbJDib0r_hhd_1bdNN-C5y0,1568
+cookiecutter/main.py,sha256=XA9GKJbrDCaVDivkGkBLuFbTS7rqDTWIRnrEDcwb22c,4657
+cookiecutter/prompt.py,sha256=jipyemmYF-HE3K0cmkrycoZ8TFUhJpIYMr6dxhErvg8,8197
+cookiecutter/replay.py,sha256=D2vKyMfMbZ1So0IrIk8KWzYj5l0Jbvr9yQoIO_1wb4Y,1512
+cookiecutter/repository.py,sha256=C8jk4OhGc1ldCjdZ5IXHMSJyaK-AX9GD0-jXOcPGv3g,4206
+cookiecutter/utils.py,sha256=wYctIUKvuh7yEBaBaNu_H_ohNIOlE-e8FYCIbemgARw,3136
+cookiecutter/vcs.py,sha256=RNb_L_pRbezB3tlvEVwBBxDiW7yLPJ1ku978V1BFFzc,4184
+cookiecutter/zipfile.py,sha256=uFvUACBImliMpFfZDOfly0FAGggikvJQzjsc6li4KKc,4264
+cookiecutter-2.1.1.dist-info/AUTHORS.md,sha256=wr92S5G373_A1QbLpGehSS6AqWMqz_GMbNSCRVAzxaU,11784
+cookiecutter-2.1.1.dist-info/LICENSE,sha256=iPRc_2ncuesJXa8jA4O-ytRyYZSrjJsEHURe72StH2s,1493
+cookiecutter-2.1.1.dist-info/METADATA,sha256=iSZkpbgaptuhc1KYr2okLKP5M0alCGPxalF7GXOUleU,12593
+cookiecutter-2.1.1.dist-info/WHEEL,sha256=z9j0xAa_JmUKMpmz72K0ZGALSM_n-wQVmGbleXx2VHg,110
+cookiecutter-2.1.1.dist-info/entry_points.txt,sha256=NzYmXG0J4ML4EmM5POUl9I5PxNN1qlDVgOtkjVR8Fng,60
+cookiecutter-2.1.1.dist-info/top_level.txt,sha256=UE0NGj4iqLNgC-5CAY4V94Tqp9mAD8HqwvZpG9z6cGY,13
+cookiecutter-2.1.1.dist-info/RECORD,,
diff --git a/third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/WHEEL b/third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/WHEEL
new file mode 100644
index 0000000000..0b18a28110
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/WHEEL
@@ -0,0 +1,6 @@
+Wheel-Version: 1.0
+Generator: bdist_wheel (0.37.1)
+Root-Is-Purelib: true
+Tag: py2-none-any
+Tag: py3-none-any
+
diff --git a/third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/entry_points.txt b/third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/entry_points.txt
new file mode 100644
index 0000000000..a1a3da0ad2
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/entry_points.txt
@@ -0,0 +1,2 @@
+[console_scripts]
+cookiecutter = cookiecutter.__main__:main
diff --git a/third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/top_level.txt b/third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/top_level.txt
new file mode 100644
index 0000000000..c8e988bc24
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter-2.1.1.dist-info/top_level.txt
@@ -0,0 +1 @@
+cookiecutter
diff --git a/third_party/python/cookiecutter/cookiecutter/__init__.py b/third_party/python/cookiecutter/cookiecutter/__init__.py
new file mode 100644
index 0000000000..f0e3a2c38d
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter/__init__.py
@@ -0,0 +1,2 @@
+"""Main package for Cookiecutter."""
+__version__ = "2.1.1"
diff --git a/third_party/python/cookiecutter/cookiecutter/__main__.py b/third_party/python/cookiecutter/cookiecutter/__main__.py
new file mode 100644
index 0000000000..9ac3661726
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter/__main__.py
@@ -0,0 +1,6 @@
+"""Allow cookiecutter to be executable through `python -m cookiecutter`."""
+from cookiecutter.cli import main
+
+
+if __name__ == "__main__": # pragma: no cover
+ main(prog_name="cookiecutter")
diff --git a/third_party/python/cookiecutter/cookiecutter/cli.py b/third_party/python/cookiecutter/cookiecutter/cli.py
new file mode 100644
index 0000000000..a792fa5f56
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter/cli.py
@@ -0,0 +1,231 @@
+"""Main `cookiecutter` CLI."""
+import collections
+import json
+import os
+import sys
+
+import click
+
+from cookiecutter import __version__
+from cookiecutter.exceptions import (
+ ContextDecodingException,
+ FailedHookException,
+ InvalidModeException,
+ InvalidZipRepository,
+ OutputDirExistsException,
+ RepositoryCloneFailed,
+ RepositoryNotFound,
+ UndefinedVariableInTemplate,
+ UnknownExtension,
+)
+from cookiecutter.log import configure_logger
+from cookiecutter.main import cookiecutter
+from cookiecutter.config import get_user_config
+
+
+def version_msg():
+ """Return the Cookiecutter version, location and Python powering it."""
+ python_version = sys.version
+ location = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+ message = 'Cookiecutter %(version)s from {} (Python {})'
+ return message.format(location, python_version)
+
+
+def validate_extra_context(ctx, param, value):
+ """Validate extra context."""
+ for s in value:
+ if '=' not in s:
+ raise click.BadParameter(
+ 'EXTRA_CONTEXT should contain items of the form key=value; '
+ "'{}' doesn't match that form".format(s)
+ )
+
+ # Convert tuple -- e.g.: ('program_name=foobar', 'startsecs=66')
+ # to dict -- e.g.: {'program_name': 'foobar', 'startsecs': '66'}
+ return collections.OrderedDict(s.split('=', 1) for s in value) or None
+
+
+def list_installed_templates(default_config, passed_config_file):
+ """List installed (locally cloned) templates. Use cookiecutter --list-installed."""
+ config = get_user_config(passed_config_file, default_config)
+ cookiecutter_folder = config.get('cookiecutters_dir')
+ if not os.path.exists(cookiecutter_folder):
+ click.echo(
+ 'Error: Cannot list installed templates. Folder does not exist: '
+ '{}'.format(cookiecutter_folder)
+ )
+ sys.exit(-1)
+
+ template_names = [
+ folder
+ for folder in os.listdir(cookiecutter_folder)
+ if os.path.exists(
+ os.path.join(cookiecutter_folder, folder, 'cookiecutter.json')
+ )
+ ]
+ click.echo(f'{len(template_names)} installed templates: ')
+ for name in template_names:
+ click.echo(f' * {name}')
+
+
+@click.command(context_settings=dict(help_option_names=['-h', '--help']))
+@click.version_option(__version__, '-V', '--version', message=version_msg())
+@click.argument('template', required=False)
+@click.argument('extra_context', nargs=-1, callback=validate_extra_context)
+@click.option(
+ '--no-input',
+ is_flag=True,
+ help='Do not prompt for parameters and only use cookiecutter.json file content',
+)
+@click.option(
+ '-c',
+ '--checkout',
+ help='branch, tag or commit to checkout after git clone',
+)
+@click.option(
+ '--directory',
+ help='Directory within repo that holds cookiecutter.json file '
+ 'for advanced repositories with multi templates in it',
+)
+@click.option(
+ '-v', '--verbose', is_flag=True, help='Print debug information', default=False
+)
+@click.option(
+ '--replay',
+ is_flag=True,
+ help='Do not prompt for parameters and only use information entered previously',
+)
+@click.option(
+ '--replay-file',
+ type=click.Path(),
+ default=None,
+ help='Use this file for replay instead of the default.',
+)
+@click.option(
+ '-f',
+ '--overwrite-if-exists',
+ is_flag=True,
+ help='Overwrite the contents of the output directory if it already exists',
+)
+@click.option(
+ '-s',
+ '--skip-if-file-exists',
+ is_flag=True,
+ help='Skip the files in the corresponding directories if they already exist',
+ default=False,
+)
+@click.option(
+ '-o',
+ '--output-dir',
+ default='.',
+ type=click.Path(),
+ help='Where to output the generated project dir into',
+)
+@click.option(
+ '--config-file', type=click.Path(), default=None, help='User configuration file'
+)
+@click.option(
+ '--default-config',
+ is_flag=True,
+ help='Do not load a config file. Use the defaults instead',
+)
+@click.option(
+ '--debug-file',
+ type=click.Path(),
+ default=None,
+ help='File to be used as a stream for DEBUG logging',
+)
+@click.option(
+ '--accept-hooks',
+ type=click.Choice(['yes', 'ask', 'no']),
+ default='yes',
+ help='Accept pre/post hooks',
+)
+@click.option(
+ '-l', '--list-installed', is_flag=True, help='List currently installed templates.'
+)
+def main(
+ template,
+ extra_context,
+ no_input,
+ checkout,
+ verbose,
+ replay,
+ overwrite_if_exists,
+ output_dir,
+ config_file,
+ default_config,
+ debug_file,
+ directory,
+ skip_if_file_exists,
+ accept_hooks,
+ replay_file,
+ list_installed,
+):
+ """Create a project from a Cookiecutter project template (TEMPLATE).
+
+ Cookiecutter is free and open source software, developed and managed by
+ volunteers. If you would like to help out or fund the project, please get
+ in touch at https://github.com/cookiecutter/cookiecutter.
+ """
+ # Commands that should work without arguments
+ if list_installed:
+ list_installed_templates(default_config, config_file)
+ sys.exit(0)
+
+ # Raising usage, after all commands that should work without args.
+ if not template or template.lower() == 'help':
+ click.echo(click.get_current_context().get_help())
+ sys.exit(0)
+
+ configure_logger(stream_level='DEBUG' if verbose else 'INFO', debug_file=debug_file)
+
+ # If needed, prompt the user to ask whether or not they want to execute
+ # the pre/post hooks.
+ if accept_hooks == "ask":
+ _accept_hooks = click.confirm("Do you want to execute hooks?")
+ else:
+ _accept_hooks = accept_hooks == "yes"
+
+ if replay_file:
+ replay = replay_file
+
+ try:
+ cookiecutter(
+ template,
+ checkout,
+ no_input,
+ extra_context=extra_context,
+ replay=replay,
+ overwrite_if_exists=overwrite_if_exists,
+ output_dir=output_dir,
+ config_file=config_file,
+ default_config=default_config,
+ password=os.environ.get('COOKIECUTTER_REPO_PASSWORD'),
+ directory=directory,
+ skip_if_file_exists=skip_if_file_exists,
+ accept_hooks=_accept_hooks,
+ )
+ except (
+ ContextDecodingException,
+ OutputDirExistsException,
+ InvalidModeException,
+ FailedHookException,
+ UnknownExtension,
+ InvalidZipRepository,
+ RepositoryNotFound,
+ RepositoryCloneFailed,
+ ) as e:
+ click.echo(e)
+ sys.exit(1)
+ except UndefinedVariableInTemplate as undefined_err:
+ click.echo(f'{undefined_err.message}')
+ click.echo(f'Error message: {undefined_err.error.message}')
+
+ context_str = json.dumps(undefined_err.context, indent=4, sort_keys=True)
+ click.echo(f'Context: {context_str}')
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/third_party/python/cookiecutter/cookiecutter/config.py b/third_party/python/cookiecutter/cookiecutter/config.py
new file mode 100644
index 0000000000..0d0fa8c7e1
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter/config.py
@@ -0,0 +1,122 @@
+"""Global configuration handling."""
+import collections
+import copy
+import logging
+import os
+
+import yaml
+
+from cookiecutter.exceptions import ConfigDoesNotExistException, InvalidConfiguration
+
+logger = logging.getLogger(__name__)
+
+USER_CONFIG_PATH = os.path.expanduser('~/.cookiecutterrc')
+
+BUILTIN_ABBREVIATIONS = {
+ 'gh': 'https://github.com/{0}.git',
+ 'gl': 'https://gitlab.com/{0}.git',
+ 'bb': 'https://bitbucket.org/{0}',
+}
+
+DEFAULT_CONFIG = {
+ 'cookiecutters_dir': os.path.expanduser('~/.cookiecutters/'),
+ 'replay_dir': os.path.expanduser('~/.cookiecutter_replay/'),
+ 'default_context': collections.OrderedDict([]),
+ 'abbreviations': BUILTIN_ABBREVIATIONS,
+}
+
+
+def _expand_path(path):
+ """Expand both environment variables and user home in the given path."""
+ path = os.path.expandvars(path)
+ path = os.path.expanduser(path)
+ return path
+
+
+def merge_configs(default, overwrite):
+ """Recursively update a dict with the key/value pair of another.
+
+ Dict values that are dictionaries themselves will be updated, whilst
+ preserving existing keys.
+ """
+ new_config = copy.deepcopy(default)
+
+ for k, v in overwrite.items():
+ # Make sure to preserve existing items in
+ # nested dicts, for example `abbreviations`
+ if isinstance(v, dict):
+ new_config[k] = merge_configs(default.get(k, {}), v)
+ else:
+ new_config[k] = v
+
+ return new_config
+
+
+def get_config(config_path):
+ """Retrieve the config from the specified path, returning a config dict."""
+ if not os.path.exists(config_path):
+ raise ConfigDoesNotExistException(f'Config file {config_path} does not exist.')
+
+ logger.debug('config_path is %s', config_path)
+ with open(config_path, encoding='utf-8') as file_handle:
+ try:
+ yaml_dict = yaml.safe_load(file_handle)
+ except yaml.YAMLError as e:
+ raise InvalidConfiguration(
+ f'Unable to parse YAML file {config_path}.'
+ ) from e
+
+ config_dict = merge_configs(DEFAULT_CONFIG, yaml_dict)
+
+ raw_replay_dir = config_dict['replay_dir']
+ config_dict['replay_dir'] = _expand_path(raw_replay_dir)
+
+ raw_cookies_dir = config_dict['cookiecutters_dir']
+ config_dict['cookiecutters_dir'] = _expand_path(raw_cookies_dir)
+
+ return config_dict
+
+
+def get_user_config(config_file=None, default_config=False):
+ """Return the user config as a dict.
+
+ If ``default_config`` is True, ignore ``config_file`` and return default
+ values for the config parameters.
+
+ If a path to a ``config_file`` is given, that is different from the default
+ location, load the user config from that.
+
+ Otherwise look up the config file path in the ``COOKIECUTTER_CONFIG``
+ environment variable. If set, load the config from this path. This will
+ raise an error if the specified path is not valid.
+
+ If the environment variable is not set, try the default config file path
+ before falling back to the default config values.
+ """
+ # Do NOT load a config. Return defaults instead.
+ if default_config:
+ logger.debug("Force ignoring user config with default_config switch.")
+ return copy.copy(DEFAULT_CONFIG)
+
+ # Load the given config file
+ if config_file and config_file is not USER_CONFIG_PATH:
+ logger.debug("Loading custom config from %s.", config_file)
+ return get_config(config_file)
+
+ try:
+ # Does the user set up a config environment variable?
+ env_config_file = os.environ['COOKIECUTTER_CONFIG']
+ except KeyError:
+ # Load an optional user config if it exists
+ # otherwise return the defaults
+ if os.path.exists(USER_CONFIG_PATH):
+ logger.debug("Loading config from %s.", USER_CONFIG_PATH)
+ return get_config(USER_CONFIG_PATH)
+ else:
+ logger.debug("User config not found. Loading default config.")
+ return copy.copy(DEFAULT_CONFIG)
+ else:
+ # There is a config environment variable. Try to load it.
+ # Do not check for existence, so invalid file paths raise an error.
+ logger.debug("User config not found or not specified. Loading default config.")
+ return get_config(env_config_file)
diff --git a/third_party/python/cookiecutter/cookiecutter/environment.py b/third_party/python/cookiecutter/cookiecutter/environment.py
new file mode 100644
index 0000000000..f2804c5950
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter/environment.py
@@ -0,0 +1,65 @@
+"""Jinja2 environment and extensions loading."""
+from jinja2 import Environment, StrictUndefined
+
+from cookiecutter.exceptions import UnknownExtension
+
+
+class ExtensionLoaderMixin:
+ """Mixin providing sane loading of extensions specified in a given context.
+
+ The context is being extracted from the keyword arguments before calling
+ the next parent class in line of the child.
+ """
+
+ def __init__(self, **kwargs):
+ """Initialize the Jinja2 Environment object while loading extensions.
+
+ Does the following:
+
+ 1. Establishes default_extensions (currently just a Time feature)
+ 2. Reads extensions set in the cookiecutter.json _extensions key.
+ 3. Attempts to load the extensions. Provides useful error if fails.
+ """
+ context = kwargs.pop('context', {})
+
+ default_extensions = [
+ 'cookiecutter.extensions.JsonifyExtension',
+ 'cookiecutter.extensions.RandomStringExtension',
+ 'cookiecutter.extensions.SlugifyExtension',
+ 'cookiecutter.extensions.UUIDExtension',
+ 'jinja2_time.TimeExtension',
+ ]
+ extensions = default_extensions + self._read_extensions(context)
+
+ try:
+ super().__init__(extensions=extensions, **kwargs)
+ except ImportError as err:
+ raise UnknownExtension(f'Unable to load extension: {err}')
+
+ def _read_extensions(self, context):
+ """Return list of extensions as str to be passed on to the Jinja2 env.
+
+ If context does not contain the relevant info, return an empty
+ list instead.
+ """
+ try:
+ extensions = context['cookiecutter']['_extensions']
+ except KeyError:
+ return []
+ else:
+ return [str(ext) for ext in extensions]
+
+
+class StrictEnvironment(ExtensionLoaderMixin, Environment):
+ """Create strict Jinja2 environment.
+
+ Jinja2 environment will raise error on undefined variable in template-
+ rendering context.
+ """
+
+ def __init__(self, **kwargs):
+ """Set the standard Cookiecutter StrictEnvironment.
+
+ Also loading extensions defined in cookiecutter.json's _extensions key.
+ """
+ super().__init__(undefined=StrictUndefined, **kwargs)
diff --git a/third_party/python/cookiecutter/cookiecutter/exceptions.py b/third_party/python/cookiecutter/cookiecutter/exceptions.py
new file mode 100644
index 0000000000..4acf6dc47c
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter/exceptions.py
@@ -0,0 +1,163 @@
+"""All exceptions used in the Cookiecutter code base are defined here."""
+
+
+class CookiecutterException(Exception):
+ """
+ Base exception class.
+
+ All Cookiecutter-specific exceptions should subclass this class.
+ """
+
+
+class NonTemplatedInputDirException(CookiecutterException):
+ """
+ Exception for when a project's input dir is not templated.
+
+ The name of the input directory should always contain a string that is
+ rendered to something else, so that input_dir != output_dir.
+ """
+
+
+class UnknownTemplateDirException(CookiecutterException):
+ """
+ Exception for ambiguous project template directory.
+
+ Raised when Cookiecutter cannot determine which directory is the project
+ template, e.g. more than one dir appears to be a template dir.
+ """
+
+ # unused locally
+
+
+class MissingProjectDir(CookiecutterException):
+ """
+ Exception for missing generated project directory.
+
+ Raised during cleanup when remove_repo() can't find a generated project
+ directory inside of a repo.
+ """
+
+ # unused locally
+
+
+class ConfigDoesNotExistException(CookiecutterException):
+ """
+ Exception for missing config file.
+
+ Raised when get_config() is passed a path to a config file, but no file
+ is found at that path.
+ """
+
+
+class InvalidConfiguration(CookiecutterException):
+ """
+ Exception for invalid configuration file.
+
+ Raised if the global configuration file is not valid YAML or is
+ badly constructed.
+ """
+
+
+class UnknownRepoType(CookiecutterException):
+ """
+ Exception for unknown repo types.
+
+ Raised if a repo's type cannot be determined.
+ """
+
+
+class VCSNotInstalled(CookiecutterException):
+ """
+ Exception when version control is unavailable.
+
+ Raised if the version control system (git or hg) is not installed.
+ """
+
+
+class ContextDecodingException(CookiecutterException):
+ """
+ Exception for failed JSON decoding.
+
+ Raised when a project's JSON context file can not be decoded.
+ """
+
+
+class OutputDirExistsException(CookiecutterException):
+ """
+ Exception for existing output directory.
+
+ Raised when the output directory of the project exists already.
+ """
+
+
+class InvalidModeException(CookiecutterException):
+ """
+ Exception for incompatible modes.
+
+ Raised when cookiecutter is called with both `no_input==True` and
+ `replay==True` at the same time.
+ """
+
+
+class FailedHookException(CookiecutterException):
+ """
+ Exception for hook failures.
+
+ Raised when a hook script fails.
+ """
+
+
+class UndefinedVariableInTemplate(CookiecutterException):
+ """
+ Exception for out-of-scope variables.
+
+ Raised when a template uses a variable which is not defined in the
+ context.
+ """
+
+ def __init__(self, message, error, context):
+ """Exception for out-of-scope variables."""
+ self.message = message
+ self.error = error
+ self.context = context
+
+ def __str__(self):
+ """Text representation of UndefinedVariableInTemplate."""
+ return (
+ f"{self.message}. "
+ f"Error message: {self.error.message}. "
+ f"Context: {self.context}"
+ )
+
+
+class UnknownExtension(CookiecutterException):
+ """
+ Exception for un-importable extention.
+
+ Raised when an environment is unable to import a required extension.
+ """
+
+
+class RepositoryNotFound(CookiecutterException):
+ """
+ Exception for missing repo.
+
+ Raised when the specified cookiecutter repository doesn't exist.
+ """
+
+
+class RepositoryCloneFailed(CookiecutterException):
+ """
+ Exception for un-cloneable repo.
+
+ Raised when a cookiecutter template can't be cloned.
+ """
+
+
+class InvalidZipRepository(CookiecutterException):
+ """
+ Exception for bad zip repo.
+
+ Raised when the specified cookiecutter repository isn't a valid
+ Zip archive.
+ """
diff --git a/third_party/python/cookiecutter/cookiecutter/extensions.py b/third_party/python/cookiecutter/cookiecutter/extensions.py
new file mode 100644
index 0000000000..6a3161abab
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter/extensions.py
@@ -0,0 +1,66 @@
+"""Jinja2 extensions."""
+import json
+import string
+import uuid
+from secrets import choice
+
+from jinja2.ext import Extension
+from slugify import slugify as pyslugify
+
+
+class JsonifyExtension(Extension):
+ """Jinja2 extension to convert a Python object to JSON."""
+
+ def __init__(self, environment):
+ """Initialize the extension with the given environment."""
+ super().__init__(environment)
+
+ def jsonify(obj):
+ return json.dumps(obj, sort_keys=True, indent=4)
+
+ environment.filters['jsonify'] = jsonify
+
+
+class RandomStringExtension(Extension):
+ """Jinja2 extension to create a random string."""
+
+ def __init__(self, environment):
+ """Jinja2 Extension Constructor."""
+ super().__init__(environment)
+
+ def random_ascii_string(length, punctuation=False):
+ if punctuation:
+ corpus = "".join((string.ascii_letters, string.punctuation))
+ else:
+ corpus = string.ascii_letters
+ return "".join(choice(corpus) for _ in range(length))
+
+ environment.globals.update(random_ascii_string=random_ascii_string)
+
+
+class SlugifyExtension(Extension):
+ """Jinja2 Extension to slugify string."""
+
+ def __init__(self, environment):
+ """Jinja2 Extension constructor."""
+ super().__init__(environment)
+
+ def slugify(value, **kwargs):
+ """Slugifies the value."""
+ return pyslugify(value, **kwargs)
+
+ environment.filters['slugify'] = slugify
+
+
+class UUIDExtension(Extension):
+ """Jinja2 Extension to generate uuid4 string."""
+
+ def __init__(self, environment):
+ """Jinja2 Extension constructor."""
+ super().__init__(environment)
+
+ def uuid4():
+ """Generate UUID4."""
+ return str(uuid.uuid4())
+
+ environment.globals.update(uuid4=uuid4)
diff --git a/third_party/python/cookiecutter/cookiecutter/find.py b/third_party/python/cookiecutter/cookiecutter/find.py
new file mode 100644
index 0000000000..054e286f4a
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter/find.py
@@ -0,0 +1,31 @@
+"""Functions for finding Cookiecutter templates and other components."""
+import logging
+import os
+
+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.
+
+ :param repo_dir: Local directory of newly cloned repo.
+ :returns project_template: 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
+ 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
diff --git a/third_party/python/cookiecutter/cookiecutter/generate.py b/third_party/python/cookiecutter/cookiecutter/generate.py
new file mode 100644
index 0000000000..7bdce5a8bb
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter/generate.py
@@ -0,0 +1,391 @@
+"""Functions for generating a project from a project template."""
+import fnmatch
+import json
+import logging
+import os
+import shutil
+import warnings
+from collections import OrderedDict
+
+from binaryornot.check import is_binary
+from jinja2 import FileSystemLoader
+from jinja2.exceptions import TemplateSyntaxError, UndefinedError
+
+from cookiecutter.environment import StrictEnvironment
+from cookiecutter.exceptions import (
+ ContextDecodingException,
+ FailedHookException,
+ NonTemplatedInputDirException,
+ OutputDirExistsException,
+ UndefinedVariableInTemplate,
+)
+from cookiecutter.find import find_template
+from cookiecutter.hooks import run_hook
+from cookiecutter.utils import make_sure_path_exists, rmtree, work_in
+
+logger = logging.getLogger(__name__)
+
+
+def is_copy_only_path(path, context):
+ """Check whether the given `path` should only be copied and not rendered.
+
+ Returns True if `path` matches a pattern in the given `context` dict,
+ otherwise False.
+
+ :param path: A file-system path referring to a file or dir that
+ should be rendered or just copied.
+ :param context: cookiecutter context.
+ """
+ try:
+ for dont_render in context['cookiecutter']['_copy_without_render']:
+ if fnmatch.fnmatch(path, dont_render):
+ return True
+ except KeyError:
+ return False
+
+ return False
+
+
+def apply_overwrites_to_context(context, overwrite_context):
+ """Modify the given context in place based on the overwrite_context."""
+ for variable, overwrite in overwrite_context.items():
+ if variable not in context:
+ # Do not include variables which are not used in the template
+ continue
+
+ context_value = context[variable]
+
+ if isinstance(context_value, list):
+ # We are dealing with a choice variable
+ if overwrite in context_value:
+ # This overwrite is actually valid for the given context
+ # Let's set it as default (by definition first item in list)
+ # see ``cookiecutter.prompt.prompt_choice_for_config``
+ context_value.remove(overwrite)
+ context_value.insert(0, overwrite)
+ else:
+ raise ValueError(
+ "{} provided for choice variable {}, but the "
+ "choices are {}.".format(overwrite, variable, context_value)
+ )
+ else:
+ # Simply overwrite the value for this variable
+ context[variable] = overwrite
+
+
+def generate_context(
+ context_file='cookiecutter.json', default_context=None, extra_context=None
+):
+ """Generate the context for a Cookiecutter project template.
+
+ Loads the JSON file as a Python object, with key being the JSON filename.
+
+ :param context_file: JSON file containing key/value pairs for populating
+ the cookiecutter's variables.
+ :param default_context: Dictionary containing config to take into account.
+ :param extra_context: Dictionary containing configuration overrides
+ """
+ context = OrderedDict([])
+
+ try:
+ with open(context_file, encoding='utf-8') as file_handle:
+ obj = json.load(file_handle, object_pairs_hook=OrderedDict)
+ except ValueError as e:
+ # JSON decoding error. Let's throw a new exception that is more
+ # friendly for the developer or user.
+ full_fpath = os.path.abspath(context_file)
+ json_exc_message = str(e)
+ our_exc_message = (
+ 'JSON decoding error while loading "{}". Decoding'
+ ' error details: "{}"'.format(full_fpath, json_exc_message)
+ )
+ raise ContextDecodingException(our_exc_message)
+
+ # Add the Python object to the context dictionary
+ file_name = os.path.split(context_file)[1]
+ file_stem = file_name.split('.')[0]
+ context[file_stem] = obj
+
+ # Overwrite context variable defaults with the default context from the
+ # user's global config, if available
+ if default_context:
+ try:
+ apply_overwrites_to_context(obj, default_context)
+ except ValueError as ex:
+ warnings.warn("Invalid default received: " + str(ex))
+ if extra_context:
+ apply_overwrites_to_context(obj, extra_context)
+
+ logger.debug('Context generated is %s', context)
+ return context
+
+
+def generate_file(project_dir, infile, context, env, skip_if_file_exists=False):
+ """Render filename of infile as name of outfile, handle infile correctly.
+
+ Dealing with infile appropriately:
+
+ a. If infile is a binary file, copy it over without rendering.
+ b. If infile is a text file, render its contents and write the
+ rendered infile to outfile.
+
+ Precondition:
+
+ When calling `generate_file()`, the root template dir must be the
+ current working directory. Using `utils.work_in()` is the recommended
+ way to perform this directory change.
+
+ :param project_dir: Absolute path to the resulting generated project.
+ :param infile: Input file to generate the file from. Relative to the root
+ template dir.
+ :param context: Dict for populating the cookiecutter's variables.
+ :param env: Jinja2 template execution environment.
+ """
+ logger.debug('Processing file %s', infile)
+
+ # Render the path to the output file (not including the root project dir)
+ outfile_tmpl = env.from_string(infile)
+
+ outfile = os.path.join(project_dir, outfile_tmpl.render(**context))
+ file_name_is_empty = os.path.isdir(outfile)
+ if file_name_is_empty:
+ logger.debug('The resulting file name is empty: %s', outfile)
+ return
+
+ if skip_if_file_exists and os.path.exists(outfile):
+ logger.debug('The resulting file already exists: %s', outfile)
+ return
+
+ logger.debug('Created file at %s', outfile)
+
+ # Just copy over binary files. Don't render.
+ logger.debug("Check %s to see if it's a binary", infile)
+ if is_binary(infile):
+ logger.debug('Copying binary %s to %s without rendering', infile, outfile)
+ shutil.copyfile(infile, outfile)
+ else:
+ # Force fwd slashes on Windows for get_template
+ # This is a by-design Jinja issue
+ infile_fwd_slashes = infile.replace(os.path.sep, '/')
+
+ # Render the file
+ try:
+ tmpl = env.get_template(infile_fwd_slashes)
+ except TemplateSyntaxError as exception:
+ # Disable translated so that printed exception contains verbose
+ # information about syntax error location
+ exception.translated = False
+ raise
+ rendered_file = tmpl.render(**context)
+
+ # Detect original file newline to output the rendered file
+ # note: newline='' ensures newlines are not converted
+ 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.
+ newline = rd.newlines
+ if context['cookiecutter'].get('_new_lines', False):
+ newline = context['cookiecutter']['_new_lines']
+ logger.debug('Overwriting end line character with %s', newline)
+
+ logger.debug('Writing contents to file %s', outfile)
+
+ with open(outfile, 'w', encoding='utf-8', newline=newline) as fh:
+ fh.write(rendered_file)
+
+ # Apply file permissions to output file
+ shutil.copymode(infile, outfile)
+
+
+def render_and_create_dir(
+ dirname, context, output_dir, environment, overwrite_if_exists=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))
+
+ 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)
+
+ if output_dir_exists:
+ if overwrite_if_exists:
+ logger.debug(
+ 'Output directory %s already exists, overwriting it', dir_to_create
+ )
+ else:
+ msg = f'Error: "{dir_to_create}" directory already exists'
+ raise OutputDirExistsException(msg)
+ else:
+ make_sure_path_exists(dir_to_create)
+
+ return dir_to_create, not output_dir_exists
+
+
+def ensure_dir_is_templated(dirname):
+ """Ensure that dirname is a templated directory name."""
+ if '{{' in dirname and '}}' in dirname:
+ return True
+ else:
+ raise NonTemplatedInputDirException
+
+
+def _run_hook_from_repo_dir(
+ repo_dir, hook_name, project_dir, context, delete_project_on_failure
+):
+ """Run hook from repo directory, clean project directory if hook fails.
+
+ :param repo_dir: Project template input directory.
+ :param hook_name: The hook to execute.
+ :param project_dir: The directory to execute the script from.
+ :param context: Cookiecutter project context.
+ :param delete_project_on_failure: Delete the project directory on hook
+ failure?
+ """
+ with work_in(repo_dir):
+ try:
+ run_hook(hook_name, project_dir, context)
+ except FailedHookException:
+ if delete_project_on_failure:
+ rmtree(project_dir)
+ logger.error(
+ "Stopping generation because %s hook "
+ "script didn't exit successfully",
+ hook_name,
+ )
+ raise
+
+
+def generate_files(
+ repo_dir,
+ context=None,
+ output_dir='.',
+ overwrite_if_exists=False,
+ skip_if_file_exists=False,
+ accept_hooks=True,
+):
+ """Render the templates and saves them to files.
+
+ :param repo_dir: Project template input directory.
+ :param context: Dict for populating the template's variables.
+ :param output_dir: Where to output the generated project dir into.
+ :param overwrite_if_exists: Overwrite the contents of the output directory
+ if it exists.
+ :param accept_hooks: Accept pre and post hooks if set to `True`.
+ """
+ template_dir = find_template(repo_dir)
+ logger.debug('Generating project from %s...', template_dir)
+ context = context or OrderedDict([])
+
+ envvars = context.get('cookiecutter', {}).get('_jinja2_env_vars', {})
+
+ unrendered_dir = os.path.split(template_dir)[1]
+ ensure_dir_is_templated(unrendered_dir)
+ env = StrictEnvironment(context=context, keep_trailing_newline=True, **envvars)
+ try:
+ project_dir, output_directory_created = render_and_create_dir(
+ unrendered_dir, context, output_dir, env, overwrite_if_exists
+ )
+ except UndefinedError as err:
+ msg = f"Unable to create project directory '{unrendered_dir}'"
+ raise UndefinedVariableInTemplate(msg, err, context)
+
+ # We want the Jinja path and the OS paths to match. Consequently, we'll:
+ # + CD to the template folder
+ # + Set Jinja's path to '.'
+ #
+ # In order to build our files to the correct folder(s), we'll use an
+ # absolute path for the target folder (project_dir)
+
+ project_dir = os.path.abspath(project_dir)
+ logger.debug('Project directory is %s', project_dir)
+
+ # if we created the output directory, then it's ok to remove it
+ # if rendering fails
+ delete_project_on_failure = output_directory_created
+
+ if accept_hooks:
+ _run_hook_from_repo_dir(
+ repo_dir, 'pre_gen_project', project_dir, context, delete_project_on_failure
+ )
+
+ with work_in(template_dir):
+ env.loader = FileSystemLoader('.')
+
+ for root, dirs, files in os.walk('.'):
+ # We must separate the two types of dirs into different lists.
+ # The reason is that we don't want ``os.walk`` to go through the
+ # unrendered directories, since they will just be copied.
+ copy_dirs = []
+ render_dirs = []
+
+ for d in dirs:
+ d_ = os.path.normpath(os.path.join(root, d))
+ # We check the full path, because that's how it can be
+ # specified in the ``_copy_without_render`` setting, but
+ # we store just the dir name
+ if is_copy_only_path(d_, context):
+ copy_dirs.append(d)
+ else:
+ render_dirs.append(d)
+
+ for copy_dir in copy_dirs:
+ indir = os.path.normpath(os.path.join(root, copy_dir))
+ outdir = os.path.normpath(os.path.join(project_dir, indir))
+ outdir = env.from_string(outdir).render(**context)
+ logger.debug('Copying dir %s to %s without rendering', indir, outdir)
+ shutil.copytree(indir, outdir)
+
+ # We mutate ``dirs``, because we only want to go through these dirs
+ # recursively
+ dirs[:] = render_dirs
+ for d in dirs:
+ unrendered_dir = os.path.join(project_dir, root, d)
+ try:
+ render_and_create_dir(
+ unrendered_dir, context, output_dir, env, overwrite_if_exists
+ )
+ except UndefinedError as err:
+ if delete_project_on_failure:
+ rmtree(project_dir)
+ _dir = os.path.relpath(unrendered_dir, output_dir)
+ msg = f"Unable to create directory '{_dir}'"
+ raise UndefinedVariableInTemplate(msg, err, context)
+
+ for f in files:
+ infile = os.path.normpath(os.path.join(root, f))
+ if is_copy_only_path(infile, context):
+ outfile_tmpl = env.from_string(infile)
+ outfile_rendered = outfile_tmpl.render(**context)
+ outfile = os.path.join(project_dir, outfile_rendered)
+ logger.debug(
+ 'Copying file %s to %s without rendering', infile, outfile
+ )
+ shutil.copyfile(infile, outfile)
+ shutil.copymode(infile, outfile)
+ continue
+ try:
+ generate_file(
+ project_dir, infile, context, env, skip_if_file_exists
+ )
+ except UndefinedError as err:
+ if delete_project_on_failure:
+ rmtree(project_dir)
+ msg = f"Unable to create file '{infile}'"
+ raise UndefinedVariableInTemplate(msg, err, context)
+
+ if accept_hooks:
+ _run_hook_from_repo_dir(
+ repo_dir,
+ 'post_gen_project',
+ project_dir,
+ context,
+ delete_project_on_failure,
+ )
+
+ return project_dir
diff --git a/third_party/python/cookiecutter/cookiecutter/hooks.py b/third_party/python/cookiecutter/cookiecutter/hooks.py
new file mode 100644
index 0000000000..763287c584
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter/hooks.py
@@ -0,0 +1,131 @@
+"""Functions for discovering and executing various cookiecutter hooks."""
+import errno
+import logging
+import os
+import subprocess # nosec
+import sys
+import tempfile
+
+from cookiecutter import utils
+from cookiecutter.environment import StrictEnvironment
+from cookiecutter.exceptions import FailedHookException
+
+logger = logging.getLogger(__name__)
+
+_HOOKS = [
+ 'pre_gen_project',
+ 'post_gen_project',
+]
+EXIT_SUCCESS = 0
+
+
+def valid_hook(hook_file, hook_name):
+ """Determine if a hook file is valid.
+
+ :param hook_file: The hook file to consider for validity
+ :param hook_name: The hook to find
+ :return: The hook file validity
+ """
+ filename = os.path.basename(hook_file)
+ basename = os.path.splitext(filename)[0]
+
+ matching_hook = basename == hook_name
+ supported_hook = basename in _HOOKS
+ backup_file = filename.endswith('~')
+
+ return matching_hook and supported_hook and not backup_file
+
+
+def find_hook(hook_name, hooks_dir='hooks'):
+ """Return a dict of all hook scripts provided.
+
+ Must be called with the project template as the current working directory.
+ Dict's key will be the hook/script's name, without extension, while values
+ will be the absolute path to the script. Missing scripts will not be
+ included in the returned dict.
+
+ :param hook_name: The hook to find
+ :param hooks_dir: The hook directory in the template
+ :return: The absolute path to the hook script or None
+ """
+ logger.debug('hooks_dir is %s', os.path.abspath(hooks_dir))
+
+ if not os.path.isdir(hooks_dir):
+ logger.debug('No hooks/dir in template_dir')
+ return None
+
+ scripts = []
+ for hook_file in os.listdir(hooks_dir):
+ if valid_hook(hook_file, hook_name):
+ scripts.append(os.path.abspath(os.path.join(hooks_dir, hook_file)))
+
+ if len(scripts) == 0:
+ return None
+ return scripts
+
+
+def run_script(script_path, cwd='.'):
+ """Execute a script from a working directory.
+
+ :param script_path: Absolute path to the script to run.
+ :param cwd: The directory to run the script from.
+ """
+ run_thru_shell = sys.platform.startswith('win')
+ if script_path.endswith('.py'):
+ script_command = [sys.executable, script_path]
+ else:
+ script_command = [script_path]
+
+ utils.make_executable(script_path)
+
+ try:
+ proc = subprocess.Popen(script_command, shell=run_thru_shell, cwd=cwd) # nosec
+ exit_status = proc.wait()
+ if exit_status != EXIT_SUCCESS:
+ raise FailedHookException(
+ f'Hook script failed (exit status: {exit_status})'
+ )
+ except OSError as os_error:
+ if os_error.errno == errno.ENOEXEC:
+ raise FailedHookException(
+ 'Hook script failed, might be an empty file or missing a shebang'
+ )
+ raise FailedHookException(f'Hook script failed (error: {os_error})')
+
+
+def run_script_with_context(script_path, cwd, context):
+ """Execute a script after rendering it with Jinja.
+
+ :param script_path: Absolute path to the script to run.
+ :param cwd: The directory to run the script from.
+ :param context: Cookiecutter project template context.
+ """
+ _, extension = os.path.splitext(script_path)
+
+ with open(script_path, encoding='utf-8') as file:
+ contents = file.read()
+
+ with tempfile.NamedTemporaryFile(delete=False, mode='wb', suffix=extension) as temp:
+ env = 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)
+
+
+def run_hook(hook_name, project_dir, context):
+ """
+ Try to find and execute a hook from the specified project directory.
+
+ :param hook_name: The hook to execute.
+ :param project_dir: The directory to execute the script from.
+ :param context: Cookiecutter project context.
+ """
+ scripts = find_hook(hook_name)
+ if not scripts:
+ logger.debug('No %s hook found', hook_name)
+ return
+ logger.debug('Running hook %s', hook_name)
+ for script in scripts:
+ run_script_with_context(script, project_dir, context)
diff --git a/third_party/python/cookiecutter/cookiecutter/log.py b/third_party/python/cookiecutter/cookiecutter/log.py
new file mode 100644
index 0000000000..d7633c5715
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter/log.py
@@ -0,0 +1,51 @@
+"""Module for setting up logging."""
+import logging
+import sys
+
+LOG_LEVELS = {
+ 'DEBUG': logging.DEBUG,
+ 'INFO': logging.INFO,
+ 'WARNING': logging.WARNING,
+ 'ERROR': logging.ERROR,
+ 'CRITICAL': logging.CRITICAL,
+}
+
+LOG_FORMATS = {
+ 'DEBUG': '%(levelname)s %(name)s: %(message)s',
+ 'INFO': '%(levelname)s: %(message)s',
+}
+
+
+def configure_logger(stream_level='DEBUG', debug_file=None):
+ """Configure logging for cookiecutter.
+
+ Set up logging to stdout with given level. If ``debug_file`` is given set
+ up logging to file with DEBUG level.
+ """
+ # Set up 'cookiecutter' logger
+ logger = logging.getLogger('cookiecutter')
+ logger.setLevel(logging.DEBUG)
+
+ # Remove all attached handlers, in case there was
+ # a logger with using the name 'cookiecutter'
+ del logger.handlers[:]
+
+ # Create a file handler if a log file is provided
+ if debug_file is not None:
+ debug_formatter = logging.Formatter(LOG_FORMATS['DEBUG'])
+ file_handler = logging.FileHandler(debug_file)
+ file_handler.setLevel(LOG_LEVELS['DEBUG'])
+ file_handler.setFormatter(debug_formatter)
+ logger.addHandler(file_handler)
+
+ # Get settings based on the given stream_level
+ log_formatter = logging.Formatter(LOG_FORMATS[stream_level])
+ log_level = LOG_LEVELS[stream_level]
+
+ # Create a stream handler
+ stream_handler = logging.StreamHandler(stream=sys.stdout)
+ stream_handler.setLevel(log_level)
+ stream_handler.setFormatter(log_formatter)
+ logger.addHandler(stream_handler)
+
+ return logger
diff --git a/third_party/python/cookiecutter/cookiecutter/main.py b/third_party/python/cookiecutter/cookiecutter/main.py
new file mode 100644
index 0000000000..bc2f262dfb
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter/main.py
@@ -0,0 +1,140 @@
+"""
+Main entry point for the `cookiecutter` command.
+
+The code in this module is also a good example of how to use Cookiecutter as a
+library rather than a script.
+"""
+from copy import copy
+import logging
+import os
+import sys
+
+from cookiecutter.config import get_user_config
+from cookiecutter.exceptions import InvalidModeException
+from cookiecutter.generate import generate_context, generate_files
+from cookiecutter.prompt import prompt_for_config
+from cookiecutter.replay import dump, load
+from cookiecutter.repository import determine_repo_dir
+from cookiecutter.utils import rmtree
+
+logger = logging.getLogger(__name__)
+
+
+def cookiecutter(
+ template,
+ checkout=None,
+ no_input=False,
+ extra_context=None,
+ replay=None,
+ overwrite_if_exists=False,
+ output_dir='.',
+ config_file=None,
+ default_config=False,
+ password=None,
+ directory=None,
+ skip_if_file_exists=False,
+ accept_hooks=True,
+):
+ """
+ Run Cookiecutter just as if using it from the command line.
+
+ :param template: A directory containing a project template directory,
+ or a URL to a git repository.
+ :param checkout: The branch, tag or commit ID to checkout after clone.
+ :param no_input: Prompt the user at command line for manual configuration?
+ :param extra_context: A dictionary of context that overrides default
+ and user configuration.
+ :param replay: Do not prompt for input, instead read from saved json. If
+ ``True`` read from the ``replay_dir``.
+ if it exists
+ :param output_dir: Where to output the generated project dir into.
+ :param config_file: User configuration file path.
+ :param default_config: Use default values rather than a config file.
+ :param password: The password to use when extracting the repository.
+ :param directory: Relative path to a cookiecutter template in a repository.
+ :param accept_hooks: Accept pre and post hooks if set to `True`.
+ """
+ if replay and ((no_input is not False) or (extra_context is not None)):
+ err_msg = (
+ "You can not use both replay and no_input or extra_context "
+ "at the same time."
+ )
+ raise InvalidModeException(err_msg)
+
+ config_dict = get_user_config(
+ config_file=config_file,
+ default_config=default_config,
+ )
+
+ repo_dir, cleanup = determine_repo_dir(
+ template=template,
+ abbreviations=config_dict['abbreviations'],
+ clone_to_dir=config_dict['cookiecutters_dir'],
+ checkout=checkout,
+ no_input=no_input,
+ password=password,
+ directory=directory,
+ )
+ import_patch = _patch_import_path_for_repo(repo_dir)
+
+ template_name = os.path.basename(os.path.abspath(repo_dir))
+
+ if replay:
+ with import_patch:
+ if isinstance(replay, bool):
+ context = 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:
+ 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,
+ )
+
+ # prompt the user to manually configure at the command line.
+ # except when 'no-input' flag is set
+ with import_patch:
+ 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.
+ with import_patch:
+ result = generate_files(
+ repo_dir=repo_dir,
+ context=context,
+ overwrite_if_exists=overwrite_if_exists,
+ skip_if_file_exists=skip_if_file_exists,
+ output_dir=output_dir,
+ accept_hooks=accept_hooks,
+ )
+
+ # 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/third_party/python/cookiecutter/cookiecutter/prompt.py b/third_party/python/cookiecutter/cookiecutter/prompt.py
new file mode 100644
index 0000000000..f06cdc3c0b
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter/prompt.py
@@ -0,0 +1,236 @@
+"""Functions for prompting the user for project info."""
+import functools
+import json
+from collections import OrderedDict
+
+import click
+from jinja2.exceptions import UndefinedError
+
+from cookiecutter.environment import StrictEnvironment
+from cookiecutter.exceptions import UndefinedVariableInTemplate
+
+
+def read_user_variable(var_name, default_value):
+ """Prompt user for variable and return the entered value or given default.
+
+ :param str var_name: Variable of the context to query the user
+ :param default_value: Value that will be returned if no input happens
+ """
+ # 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'
+
+ :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)
+
+
+def read_repo_password(question):
+ """Prompt the user to enter a password.
+
+ :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)
+
+
+def read_user_choice(var_name, options):
+ """Prompt the user to choose from several options for the given variable.
+
+ The first item will be returned if no input happens.
+
+ :param str var_name: Variable as specified in the context
+ :param list options: Sequence of options that are available to select from
+ :return: Exactly one item of ``options`` that has been chosen by the user
+ """
+ # 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((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(
+ (
+ f'Select {var_name}:',
+ '\n'.join(choice_lines),
+ 'Choose from {}'.format(', '.join(choices)),
+ )
+ )
+
+ user_choice = click.prompt(
+ prompt, type=click.Choice(choices), default=default, show_choices=False
+ )
+ return choice_map[user_choice]
+
+
+DEFAULT_DISPLAY = 'default'
+
+
+def process_json(user_value, default_value=None):
+ """Load user-supplied value as a JSON dict.
+
+ :param str user_value: User-supplied value to load as a JSON dict
+ """
+ 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:
+ # Leave it up to click to ask the user again
+ raise click.UsageError('Unable to decode to JSON.')
+
+ if not isinstance(user_dict, dict):
+ # Leave it up to click to ask the user again
+ raise click.UsageError('Requires JSON dict.')
+
+ return user_dict
+
+
+def read_user_dict(var_name, default_value):
+ """Prompt the user to provide a dictionary of data.
+
+ :param str var_name: Variable as specified in the context
+ :param default_value: Value that will be returned if no input is provided
+ :return: A Python dictionary to use in the context.
+ """
+ # Please see https://click.palletsprojects.com/en/7.x/api/#click.prompt
+ if not isinstance(default_value, dict):
+ raise TypeError
+
+ user_value = click.prompt(
+ var_name,
+ default=DEFAULT_DISPLAY,
+ type=click.STRING,
+ value_proc=functools.partial(process_json, default_value=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
+
+
+def render_variable(env, raw, cookiecutter_dict):
+ """Render the next variable to be displayed in the user prompt.
+
+ Inside the prompting taken from the cookiecutter.json file, this renders
+ the next variable. For example, if a project_name is "Peanut Butter
+ Cookie", the repo_name could be be rendered with:
+
+ `{{ cookiecutter.project_name.replace(" ", "_") }}`.
+
+ This is then presented to the user as the default.
+
+ :param Environment env: A Jinja2 Environment object.
+ :param raw: The next value to be prompted for by the user.
+ :param dict cookiecutter_dict: The current context as it's gradually
+ being populated with variables.
+ :return: The rendered value for the default variable.
+ """
+ if raw is None:
+ return None
+ elif isinstance(raw, dict):
+ return {
+ render_variable(env, k, cookiecutter_dict): render_variable(
+ env, v, cookiecutter_dict
+ )
+ for k, v in raw.items()
+ }
+ elif isinstance(raw, list):
+ return [render_variable(env, v, cookiecutter_dict) for v in raw]
+ elif not isinstance(raw, str):
+ raw = str(raw)
+
+ template = env.from_string(raw)
+
+ rendered_template = template.render(cookiecutter=cookiecutter_dict)
+ return rendered_template
+
+
+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.
+ """
+ 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)
+
+
+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?
+ """
+ cookiecutter_dict = OrderedDict([])
+ env = StrictEnvironment(context=context)
+
+ # First pass: Handle simple and raw variables, plus choices.
+ # These must be done first because the dictionaries keys and
+ # values might refer to them.
+ for key, raw in context['cookiecutter'].items():
+ if key.startswith('_') and not key.startswith('__'):
+ cookiecutter_dict[key] = raw
+ continue
+ elif key.startswith('__'):
+ cookiecutter_dict[key] = render_variable(env, raw, cookiecutter_dict)
+ continue
+
+ try:
+ if isinstance(raw, list):
+ # We are dealing with a choice variable
+ val = prompt_choice_for_config(
+ cookiecutter_dict, env, key, raw, no_input
+ )
+ cookiecutter_dict[key] = val
+ elif not isinstance(raw, dict):
+ # We are dealing with a regular variable
+ val = render_variable(env, raw, cookiecutter_dict)
+
+ if not no_input:
+ val = read_user_variable(key, val)
+
+ cookiecutter_dict[key] = val
+ except UndefinedError as err:
+ msg = f"Unable to render variable '{key}'"
+ raise UndefinedVariableInTemplate(msg, err, context)
+
+ # Second pass; handle the dictionaries.
+ for key, raw in context['cookiecutter'].items():
+ # Skip private type dicts not ot be rendered.
+ if key.startswith('_') and not key.startswith('__'):
+ continue
+
+ try:
+ if isinstance(raw, dict):
+ # We are dealing with a dict variable
+ val = render_variable(env, raw, cookiecutter_dict)
+
+ if not no_input and not key.startswith('__'):
+ val = read_user_dict(key, val)
+
+ cookiecutter_dict[key] = val
+ except UndefinedError as err:
+ msg = f"Unable to render variable '{key}'"
+ raise UndefinedVariableInTemplate(msg, err, context)
+
+ return cookiecutter_dict
diff --git a/third_party/python/cookiecutter/cookiecutter/replay.py b/third_party/python/cookiecutter/cookiecutter/replay.py
new file mode 100644
index 0000000000..9730e84da8
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter/replay.py
@@ -0,0 +1,52 @@
+"""
+cookiecutter.replay.
+
+-------------------
+"""
+import json
+import os
+
+from cookiecutter.utils import make_sure_path_exists
+
+
+def get_file_name(replay_dir, template_name):
+ """Get the name of file."""
+ suffix = '.json' if not template_name.endswith('.json') else ''
+ file_name = f'{template_name}{suffix}'
+ return os.path.join(replay_dir, file_name)
+
+
+def dump(replay_dir, template_name, context):
+ """Write json data to file."""
+ if not make_sure_path_exists(replay_dir):
+ raise OSError(f'Unable to create replay dir at {replay_dir}')
+
+ if not isinstance(template_name, str):
+ raise TypeError('Template name is required to be of type str')
+
+ if not isinstance(context, dict):
+ raise TypeError('Context is required to be of type dict')
+
+ if 'cookiecutter' not in context:
+ raise ValueError('Context is required to contain a cookiecutter key')
+
+ replay_file = get_file_name(replay_dir, template_name)
+
+ with open(replay_file, 'w') as outfile:
+ json.dump(context, outfile, indent=2)
+
+
+def load(replay_dir, template_name):
+ """Read json data from file."""
+ if not isinstance(template_name, str):
+ raise TypeError('Template name is required to be of type str')
+
+ replay_file = get_file_name(replay_dir, template_name)
+
+ with open(replay_file) as infile:
+ context = json.load(infile)
+
+ if 'cookiecutter' not in context:
+ raise ValueError('Context is required to contain a cookiecutter key')
+
+ return context
diff --git a/third_party/python/cookiecutter/cookiecutter/repository.py b/third_party/python/cookiecutter/cookiecutter/repository.py
new file mode 100644
index 0000000000..f8e6fcbcc5
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter/repository.py
@@ -0,0 +1,130 @@
+"""Cookiecutter repository functions."""
+import os
+import re
+
+from cookiecutter.exceptions import RepositoryNotFound
+from cookiecutter.vcs import clone
+from cookiecutter.zipfile import unzip
+
+REPO_REGEX = re.compile(
+ r"""
+# something like git:// ssh:// file:// etc.
+((((git|hg)\+)?(git|ssh|file|https?):(//)?)
+ | # or
+ (\w+@[\w\.]+) # something like user@...
+)
+""",
+ re.VERBOSE,
+)
+
+
+def is_repo_url(value):
+ """Return True if value is a repository URL."""
+ return bool(REPO_REGEX.match(value))
+
+
+def is_zip_file(value):
+ """Return True if value is a zip file."""
+ return value.lower().endswith('.zip')
+
+
+def expand_abbreviations(template, abbreviations):
+ """Expand abbreviations in a template name.
+
+ :param template: The project template name.
+ :param abbreviations: Abbreviation definitions.
+ """
+ if template in abbreviations:
+ return abbreviations[template]
+
+ # Split on colon. If there is no colon, rest will be empty
+ # and prefix will be the whole template
+ prefix, sep, rest = template.partition(':')
+ if prefix in abbreviations:
+ return abbreviations[prefix].format(rest)
+
+ return template
+
+
+def repository_has_cookiecutter_json(repo_directory):
+ """Determine if `repo_directory` contains a `cookiecutter.json` file.
+
+ :param repo_directory: The candidate repository directory.
+ :return: True if the `repo_directory` is valid, else False.
+ """
+ repo_directory_exists = os.path.isdir(repo_directory)
+
+ repo_config_exists = os.path.isfile(
+ os.path.join(repo_directory, 'cookiecutter.json')
+ )
+ return repo_directory_exists and repo_config_exists
+
+
+def determine_repo_dir(
+ template,
+ abbreviations,
+ clone_to_dir,
+ checkout,
+ no_input,
+ password=None,
+ directory=None,
+):
+ """
+ Locate the repository directory from a template reference.
+
+ Applies repository abbreviations to the template reference.
+ If the template refers to a repository URL, clone it.
+ If the template is a path to a local repository, use it.
+
+ :param template: A directory containing a project template directory,
+ or a URL to a git repository.
+ :param abbreviations: A dictionary of repository abbreviation
+ definitions.
+ :param clone_to_dir: The directory to clone the repository into.
+ :param checkout: The branch, tag or commit ID to checkout after clone.
+ :param no_input: Prompt the user at command line for manual configuration?
+ :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
+ after the template has been instantiated.
+ :raises: `RepositoryNotFound` if a repository directory could not be found.
+ """
+ template = expand_abbreviations(template, abbreviations)
+
+ if is_zip_file(template):
+ unzipped_dir = unzip(
+ zip_uri=template,
+ is_url=is_repo_url(template),
+ clone_to_dir=clone_to_dir,
+ no_input=no_input,
+ password=password,
+ )
+ repository_candidates = [unzipped_dir]
+ cleanup = True
+ elif is_repo_url(template):
+ cloned_repo = clone(
+ repo_url=template,
+ checkout=checkout,
+ clone_to_dir=clone_to_dir,
+ no_input=no_input,
+ )
+ repository_candidates = [cloned_repo]
+ cleanup = False
+ else:
+ repository_candidates = [template, os.path.join(clone_to_dir, template)]
+ cleanup = False
+
+ if directory:
+ repository_candidates = [
+ os.path.join(s, directory) for s in repository_candidates
+ ]
+
+ for repo_candidate in repository_candidates:
+ if repository_has_cookiecutter_json(repo_candidate):
+ return repo_candidate, cleanup
+
+ raise RepositoryNotFound(
+ 'A valid repository for "{}" could not be found in the following '
+ 'locations:\n{}'.format(template, '\n'.join(repository_candidates))
+ )
diff --git a/third_party/python/cookiecutter/cookiecutter/utils.py b/third_party/python/cookiecutter/cookiecutter/utils.py
new file mode 100644
index 0000000000..4750a2663e
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter/utils.py
@@ -0,0 +1,120 @@
+"""Helper functions used throughout Cookiecutter."""
+import contextlib
+import errno
+import logging
+import os
+import shutil
+import stat
+import sys
+
+from cookiecutter.prompt import read_user_yes_no
+from jinja2.ext import Extension
+
+logger = logging.getLogger(__name__)
+
+
+def force_delete(func, path, exc_info):
+ """Error handler for `shutil.rmtree()` equivalent to `rm -rf`.
+
+ Usage: `shutil.rmtree(path, onerror=force_delete)`
+ From https://docs.python.org/3/library/shutil.html#rmtree-example
+ """
+ os.chmod(path, stat.S_IWRITE)
+ func(path)
+
+
+def rmtree(path):
+ """Remove a directory and all its contents. Like rm -rf on Unix.
+
+ :param path: A directory path.
+ """
+ shutil.rmtree(path, onerror=force_delete)
+
+
+def make_sure_path_exists(path):
+ """Ensure that a directory exists.
+
+ :param path: A directory path.
+ """
+ logger.debug('Making sure path exists: %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
+
+
+@contextlib.contextmanager
+def work_in(dirname=None):
+ """Context manager version of os.chdir.
+
+ When exited, returns to the working directory prior to entering.
+ """
+ curdir = os.getcwd()
+ try:
+ if dirname is not None:
+ os.chdir(dirname)
+ yield
+ finally:
+ os.chdir(curdir)
+
+
+def make_executable(script_path):
+ """Make `script_path` executable.
+
+ :param script_path: The file to change
+ """
+ status = os.stat(script_path)
+ os.chmod(script_path, status.st_mode | stat.S_IEXEC)
+
+
+def prompt_and_delete(path, no_input=False):
+ """
+ Ask user if it's okay to delete the previously-downloaded file/directory.
+
+ If yes, delete it. If no, checks to see if the old version should be
+ reused. If yes, it's reused; otherwise, Cookiecutter exits.
+
+ :param path: Previously downloaded zipfile.
+ :param no_input: Suppress prompt to delete repo and just delete it.
+ :return: True if the content was deleted
+ """
+ # Suppress prompt if called via API
+ if no_input:
+ ok_to_delete = True
+ else:
+ question = (
+ "You've downloaded {} before. Is it okay to delete and re-download it?"
+ ).format(path)
+
+ ok_to_delete = read_user_yes_no(question, 'yes')
+
+ if ok_to_delete:
+ if os.path.isdir(path):
+ rmtree(path)
+ else:
+ os.remove(path)
+ return True
+ else:
+ ok_to_reuse = read_user_yes_no(
+ "Do you want to re-use the existing version?", 'yes'
+ )
+
+ if ok_to_reuse:
+ return False
+
+ sys.exit()
+
+
+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/third_party/python/cookiecutter/cookiecutter/vcs.py b/third_party/python/cookiecutter/cookiecutter/vcs.py
new file mode 100644
index 0000000000..bb4356b317
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter/vcs.py
@@ -0,0 +1,125 @@
+"""Helper functions for working with version control systems."""
+import logging
+import os
+import subprocess # nosec
+from shutil import which
+
+from cookiecutter.exceptions import (
+ RepositoryCloneFailed,
+ RepositoryNotFound,
+ UnknownRepoType,
+ VCSNotInstalled,
+)
+from cookiecutter.utils import make_sure_path_exists, prompt_and_delete
+
+logger = logging.getLogger(__name__)
+
+
+BRANCH_ERRORS = [
+ 'error: pathspec',
+ 'unknown revision',
+]
+
+
+def identify_repo(repo_url):
+ """Determine if `repo_url` should be treated as a URL to a git or hg repo.
+
+ Repos can be identified by prepending "hg+" or "git+" to the repo URL.
+
+ :param repo_url: Repo URL of unknown type.
+ :returns: ('git', repo_url), ('hg', repo_url), or None.
+ """
+ repo_url_values = repo_url.split('+')
+ if len(repo_url_values) == 2:
+ repo_type = repo_url_values[0]
+ if repo_type in ["git", "hg"]:
+ return repo_type, repo_url_values[1]
+ else:
+ raise UnknownRepoType
+ else:
+ if 'git' in repo_url:
+ return 'git', repo_url
+ elif 'bitbucket' in repo_url:
+ return 'hg', repo_url
+ else:
+ raise UnknownRepoType
+
+
+def is_vcs_installed(repo_type):
+ """
+ Check if the version control system for a repo type is installed.
+
+ :param repo_type:
+ """
+ return bool(which(repo_type))
+
+
+def clone(repo_url, checkout=None, clone_to_dir='.', no_input=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.
+ :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)
+ make_sure_path_exists(clone_to_dir)
+
+ # identify the repo_type
+ repo_type, repo_url = identify_repo(repo_url)
+
+ # check that the appropriate VCS for the repo_type is installed
+ if not is_vcs_installed(repo_type):
+ msg = f"'{repo_type}' is not installed."
+ raise VCSNotInstalled(msg)
+
+ repo_url = repo_url.rstrip('/')
+ repo_name = os.path.split(repo_url)[1]
+ if repo_type == 'git':
+ repo_name = repo_name.split(':')[-1].rsplit('.git')[0]
+ repo_dir = os.path.normpath(os.path.join(clone_to_dir, repo_name))
+ if repo_type == 'hg':
+ repo_dir = os.path.normpath(os.path.join(clone_to_dir, repo_name))
+ logger.debug(f'repo_dir is {repo_dir}')
+
+ if os.path.isdir(repo_dir):
+ clone = prompt_and_delete(repo_dir, no_input=no_input)
+ else:
+ clone = True
+
+ if clone:
+ try:
+ subprocess.check_output( # nosec
+ [repo_type, 'clone', repo_url],
+ cwd=clone_to_dir,
+ stderr=subprocess.STDOUT,
+ )
+ if checkout is not None:
+ checkout_params = [checkout]
+ # Avoid Mercurial "--config" and "--debugger" injection vulnerability
+ if repo_type == "hg":
+ checkout_params.insert(0, "--")
+ subprocess.check_output( # nosec
+ [repo_type, 'checkout', *checkout_params],
+ cwd=repo_dir,
+ stderr=subprocess.STDOUT,
+ )
+ except subprocess.CalledProcessError as clone_error:
+ output = clone_error.output.decode('utf-8')
+ if 'not found' in output.lower():
+ raise RepositoryNotFound(
+ f'The repository {repo_url} could not be found, '
+ 'have you made a typo?'
+ )
+ if any(error in output for error in BRANCH_ERRORS):
+ raise RepositoryCloneFailed(
+ f'The {checkout} branch of repository '
+ f'{repo_url} could not found, have you made a typo?'
+ )
+ logger.error('git clone failed with error: %s', output)
+ raise
+
+ return repo_dir
diff --git a/third_party/python/cookiecutter/cookiecutter/zipfile.py b/third_party/python/cookiecutter/cookiecutter/zipfile.py
new file mode 100644
index 0000000000..7395ce61bc
--- /dev/null
+++ b/third_party/python/cookiecutter/cookiecutter/zipfile.py
@@ -0,0 +1,112 @@
+"""Utility functions for handling and fetching repo archives in zip format."""
+import os
+import tempfile
+from zipfile import BadZipFile, ZipFile
+
+import requests
+
+from cookiecutter.exceptions import InvalidZipRepository
+from cookiecutter.prompt import read_repo_password
+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):
+ """Download and unpack a zipfile at a given URI.
+
+ This will download the zipfile to the cookiecutter repository,
+ and unpack into a temporary directory.
+
+ :param zip_uri: The URI for the zipfile.
+ :param is_url: Is the zip URI a URL or a file?
+ :param clone_to_dir: The cookiecutter repository directory
+ to put the archive into.
+ :param no_input: Suppress any prompts
+ :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)
+ make_sure_path_exists(clone_to_dir)
+
+ if is_url:
+ # Build the name of the cached zipfile,
+ # and prompt to delete if it already exists.
+ identifier = zip_uri.rsplit('/', 1)[1]
+ zip_path = os.path.join(clone_to_dir, identifier)
+
+ if os.path.exists(zip_path):
+ download = prompt_and_delete(zip_path, no_input=no_input)
+ else:
+ download = True
+
+ if download:
+ # (Re) download the zipfile
+ r = requests.get(zip_uri, stream=True)
+ with open(zip_path, 'wb') as f:
+ for chunk in r.iter_content(chunk_size=1024):
+ if chunk: # filter out keep-alive new chunks
+ f.write(chunk)
+ else:
+ # Just use the local zipfile as-is.
+ zip_path = os.path.abspath(zip_uri)
+
+ # Now unpack the repository. The zipfile will be unpacked
+ # into a temporary directory
+ try:
+ zip_file = ZipFile(zip_path)
+
+ if len(zip_file.namelist()) == 0:
+ raise InvalidZipRepository(f'Zip repository {zip_uri} is empty')
+
+ # The first record in the zipfile should be the directory entry for
+ # the archive. If it isn't a directory, there's a problem.
+ first_filename = zip_file.namelist()[0]
+ if not first_filename.endswith('/'):
+ raise InvalidZipRepository(
+ 'Zip repository {} does not include '
+ 'a top-level directory'.format(zip_uri)
+ )
+
+ # Construct the final target directory
+ project_name = first_filename[:-1]
+ unzip_base = tempfile.mkdtemp()
+ unzip_path = os.path.join(unzip_base, project_name)
+
+ # Extract the zip file into the temporary directory
+ try:
+ zip_file.extractall(path=unzip_base)
+ except RuntimeError:
+ # File is password protected; try to get a password from the
+ # environment; if that doesn't work, ask the user.
+ if password is not None:
+ try:
+ zip_file.extractall(path=unzip_base, pwd=password.encode('utf-8'))
+ except RuntimeError:
+ raise InvalidZipRepository(
+ 'Invalid password provided for protected repository'
+ )
+ elif no_input:
+ raise InvalidZipRepository(
+ 'Unable to unlock password protected repository'
+ )
+ else:
+ retry = 0
+ while retry is not None:
+ try:
+ password = read_repo_password('Repo password')
+ zip_file.extractall(
+ path=unzip_base, pwd=password.encode('utf-8')
+ )
+ retry = None
+ except RuntimeError:
+ retry += 1
+ if retry == 3:
+ raise InvalidZipRepository(
+ 'Invalid password provided for protected repository'
+ )
+
+ except BadZipFile:
+ raise InvalidZipRepository(
+ f'Zip repository {zip_uri} is not a valid zip archive:'
+ )
+
+ return unzip_path