diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /third_party/python/cookiecutter | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
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 |