Use {project} as the placeholder
[cookiecutter.git] / project_template / ez_setup.py
1 #!/usr/bin/env python
2
3 """
4 Setuptools bootstrapping installer.
5
6 Maintained at https://github.com/pypa/setuptools/tree/bootstrap.
7
8 Run this script to install or upgrade setuptools.
9 """
10
11 import os
12 import shutil
13 import sys
14 import tempfile
15 import zipfile
16 import optparse
17 import subprocess
18 import platform
19 import textwrap
20 import contextlib
21 import json
22 import codecs
23
24 from distutils import log
25
26 try:
27     from urllib.request import urlopen
28     from urllib.parse import urljoin
29 except ImportError:
30     from urllib2 import urlopen
31     from urlparse import urljoin
32
33 try:
34     from site import USER_SITE
35 except ImportError:
36     USER_SITE = None
37
38 LATEST = object()
39 DEFAULT_VERSION = LATEST
40 DEFAULT_URL = "https://pypi.io/packages/source/s/setuptools/"
41 DEFAULT_SAVE_DIR = os.curdir
42
43
44 def _python_cmd(*args):
45     """
46     Execute a command.
47
48     Return True if the command succeeded.
49     """
50     args = (sys.executable,) + args
51     return subprocess.call(args) == 0
52
53
54 def _install(archive_filename, install_args=()):
55     """Install Setuptools."""
56     with archive_context(archive_filename):
57         # installing
58         log.warn('Installing Setuptools')
59         if not _python_cmd('setup.py', 'install', *install_args):
60             log.warn('Something went wrong during the installation.')
61             log.warn('See the error message above.')
62             # exitcode will be 2
63             return 2
64
65
66 def _build_egg(egg, archive_filename, to_dir):
67     """Build Setuptools egg."""
68     with archive_context(archive_filename):
69         # building an egg
70         log.warn('Building a Setuptools egg in %s', to_dir)
71         _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
72     # returning the result
73     log.warn(egg)
74     if not os.path.exists(egg):
75         raise IOError('Could not build the egg.')
76
77
78 class ContextualZipFile(zipfile.ZipFile):
79
80     """Supplement ZipFile class to support context manager for Python 2.6."""
81
82     def __enter__(self):
83         return self
84
85     def __exit__(self, type, value, traceback):
86         self.close()
87
88     def __new__(cls, *args, **kwargs):
89         """Construct a ZipFile or ContextualZipFile as appropriate."""
90         if hasattr(zipfile.ZipFile, '__exit__'):
91             return zipfile.ZipFile(*args, **kwargs)
92         return super(ContextualZipFile, cls).__new__(cls)
93
94
95 @contextlib.contextmanager
96 def archive_context(filename):
97     """
98     Unzip filename to a temporary directory, set to the cwd.
99
100     The unzipped target is cleaned up after.
101     """
102     tmpdir = tempfile.mkdtemp()
103     log.warn('Extracting in %s', tmpdir)
104     old_wd = os.getcwd()
105     try:
106         os.chdir(tmpdir)
107         with ContextualZipFile(filename) as archive:
108             archive.extractall()
109
110         # going in the directory
111         subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
112         os.chdir(subdir)
113         log.warn('Now working in %s', subdir)
114         yield
115
116     finally:
117         os.chdir(old_wd)
118         shutil.rmtree(tmpdir)
119
120
121 def _do_download(version, download_base, to_dir, download_delay):
122     """Download Setuptools."""
123     py_desig = 'py{sys.version_info[0]}.{sys.version_info[1]}'.format(sys=sys)
124     tp = 'setuptools-{version}-{py_desig}.egg'
125     egg = os.path.join(to_dir, tp.format(**locals()))
126     if not os.path.exists(egg):
127         archive = download_setuptools(version, download_base,
128             to_dir, download_delay)
129         _build_egg(egg, archive, to_dir)
130     sys.path.insert(0, egg)
131
132     # Remove previously-imported pkg_resources if present (see
133     # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details).
134     if 'pkg_resources' in sys.modules:
135         _unload_pkg_resources()
136
137     import setuptools
138     setuptools.bootstrap_install_from = egg
139
140
141 def use_setuptools(
142         version=DEFAULT_VERSION, download_base=DEFAULT_URL,
143         to_dir=DEFAULT_SAVE_DIR, download_delay=15):
144     """
145     Ensure that a setuptools version is installed.
146
147     Return None. Raise SystemExit if the requested version
148     or later cannot be installed.
149     """
150     version = _resolve_version(version)
151     to_dir = os.path.abspath(to_dir)
152
153     # prior to importing, capture the module state for
154     # representative modules.
155     rep_modules = 'pkg_resources', 'setuptools'
156     imported = set(sys.modules).intersection(rep_modules)
157
158     try:
159         import pkg_resources
160         pkg_resources.require("setuptools>=" + version)
161         # a suitable version is already installed
162         return
163     except ImportError:
164         # pkg_resources not available; setuptools is not installed; download
165         pass
166     except pkg_resources.DistributionNotFound:
167         # no version of setuptools was found; allow download
168         pass
169     except pkg_resources.VersionConflict as VC_err:
170         if imported:
171             _conflict_bail(VC_err, version)
172
173         # otherwise, unload pkg_resources to allow the downloaded version to
174         #  take precedence.
175         del pkg_resources
176         _unload_pkg_resources()
177
178     return _do_download(version, download_base, to_dir, download_delay)
179
180
181 def _conflict_bail(VC_err, version):
182     """
183     Setuptools was imported prior to invocation, so it is
184     unsafe to unload it. Bail out.
185     """
186     conflict_tmpl = textwrap.dedent("""
187         The required version of setuptools (>={version}) is not available,
188         and can't be installed while this script is running. Please
189         install a more recent version first, using
190         'easy_install -U setuptools'.
191
192         (Currently using {VC_err.args[0]!r})
193         """)
194     msg = conflict_tmpl.format(**locals())
195     sys.stderr.write(msg)
196     sys.exit(2)
197
198
199 def _unload_pkg_resources():
200     sys.meta_path = [
201         importer
202         for importer in sys.meta_path
203         if importer.__class__.__module__ != 'pkg_resources.extern'
204     ]
205     del_modules = [
206         name for name in sys.modules
207         if name.startswith('pkg_resources')
208     ]
209     for mod_name in del_modules:
210         del sys.modules[mod_name]
211
212
213 def _clean_check(cmd, target):
214     """
215     Run the command to download target.
216
217     If the command fails, clean up before re-raising the error.
218     """
219     try:
220         subprocess.check_call(cmd)
221     except subprocess.CalledProcessError:
222         if os.access(target, os.F_OK):
223             os.unlink(target)
224         raise
225
226
227 def download_file_powershell(url, target):
228     """
229     Download the file at url to target using Powershell.
230
231     Powershell will validate trust.
232     Raise an exception if the command cannot complete.
233     """
234     target = os.path.abspath(target)
235     ps_cmd = (
236         "[System.Net.WebRequest]::DefaultWebProxy.Credentials = "
237         "[System.Net.CredentialCache]::DefaultCredentials; "
238         '(new-object System.Net.WebClient).DownloadFile("%(url)s", "%(target)s")'
239         % locals()
240     )
241     cmd = [
242         'powershell',
243         '-Command',
244         ps_cmd,
245     ]
246     _clean_check(cmd, target)
247
248
249 def has_powershell():
250     """Determine if Powershell is available."""
251     if platform.system() != 'Windows':
252         return False
253     cmd = ['powershell', '-Command', 'echo test']
254     with open(os.path.devnull, 'wb') as devnull:
255         try:
256             subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
257         except Exception:
258             return False
259     return True
260 download_file_powershell.viable = has_powershell
261
262
263 def download_file_curl(url, target):
264     cmd = ['curl', url, '--location', '--silent', '--output', target]
265     _clean_check(cmd, target)
266
267
268 def has_curl():
269     cmd = ['curl', '--version']
270     with open(os.path.devnull, 'wb') as devnull:
271         try:
272             subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
273         except Exception:
274             return False
275     return True
276 download_file_curl.viable = has_curl
277
278
279 def download_file_wget(url, target):
280     cmd = ['wget', url, '--quiet', '--output-document', target]
281     _clean_check(cmd, target)
282
283
284 def has_wget():
285     cmd = ['wget', '--version']
286     with open(os.path.devnull, 'wb') as devnull:
287         try:
288             subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
289         except Exception:
290             return False
291     return True
292 download_file_wget.viable = has_wget
293
294
295 def download_file_insecure(url, target):
296     """Use Python to download the file, without connection authentication."""
297     src = urlopen(url)
298     try:
299         # Read all the data in one block.
300         data = src.read()
301     finally:
302         src.close()
303
304     # Write all the data in one block to avoid creating a partial file.
305     with open(target, "wb") as dst:
306         dst.write(data)
307 download_file_insecure.viable = lambda: True
308
309
310 def get_best_downloader():
311     downloaders = (
312         download_file_powershell,
313         download_file_curl,
314         download_file_wget,
315         download_file_insecure,
316     )
317     viable_downloaders = (dl for dl in downloaders if dl.viable())
318     return next(viable_downloaders, None)
319
320
321 def download_setuptools(
322         version=DEFAULT_VERSION, download_base=DEFAULT_URL,
323         to_dir=DEFAULT_SAVE_DIR, delay=15,
324         downloader_factory=get_best_downloader):
325     """
326     Download setuptools from a specified location and return its filename.
327
328     `version` should be a valid setuptools version number that is available
329     as an sdist for download under the `download_base` URL (which should end
330     with a '/'). `to_dir` is the directory where the egg will be downloaded.
331     `delay` is the number of seconds to pause before an actual download
332     attempt.
333
334     ``downloader_factory`` should be a function taking no arguments and
335     returning a function for downloading a URL to a target.
336     """
337     version = _resolve_version(version)
338     # making sure we use the absolute path
339     to_dir = os.path.abspath(to_dir)
340     zip_name = "setuptools-%s.zip" % version
341     url = download_base + zip_name
342     saveto = os.path.join(to_dir, zip_name)
343     if not os.path.exists(saveto):  # Avoid repeated downloads
344         log.warn("Downloading %s", url)
345         downloader = downloader_factory()
346         downloader(url, saveto)
347     return os.path.realpath(saveto)
348
349
350 def _resolve_version(version):
351     """
352     Resolve LATEST version
353     """
354     if version is not LATEST:
355         return version
356
357     meta_url = urljoin(DEFAULT_URL, '/pypi/setuptools/json')
358     resp = urlopen(meta_url)
359     with contextlib.closing(resp):
360         try:
361             charset = resp.info().get_content_charset()
362         except Exception:
363             # Python 2 compat; assume UTF-8
364             charset = 'UTF-8'
365         reader = codecs.getreader(charset)
366         doc = json.load(reader(resp))
367
368     return str(doc['info']['version'])
369
370
371 def _build_install_args(options):
372     """
373     Build the arguments to 'python setup.py install' on the setuptools package.
374
375     Returns list of command line arguments.
376     """
377     return ['--user'] if options.user_install else []
378
379
380 def _parse_args():
381     """Parse the command line for options."""
382     parser = optparse.OptionParser()
383     parser.add_option(
384         '--user', dest='user_install', action='store_true', default=False,
385         help='install in user site package')
386     parser.add_option(
387         '--download-base', dest='download_base', metavar="URL",
388         default=DEFAULT_URL,
389         help='alternative URL from where to download the setuptools package')
390     parser.add_option(
391         '--insecure', dest='downloader_factory', action='store_const',
392         const=lambda: download_file_insecure, default=get_best_downloader,
393         help='Use internal, non-validating downloader'
394     )
395     parser.add_option(
396         '--version', help="Specify which version to download",
397         default=DEFAULT_VERSION,
398     )
399     parser.add_option(
400         '--to-dir',
401         help="Directory to save (and re-use) package",
402         default=DEFAULT_SAVE_DIR,
403     )
404     options, args = parser.parse_args()
405     # positional arguments are ignored
406     return options
407
408
409 def _download_args(options):
410     """Return args for download_setuptools function from cmdline args."""
411     return dict(
412         version=options.version,
413         download_base=options.download_base,
414         downloader_factory=options.downloader_factory,
415         to_dir=options.to_dir,
416     )
417
418
419 def main():
420     """Install or upgrade setuptools and EasyInstall."""
421     options = _parse_args()
422     archive = download_setuptools(**_download_args(options))
423     return _install(archive, _build_install_args(options))
424
425 if __name__ == '__main__':
426     sys.exit(main())