# -*- coding: utf-8 -*- from __future__ import absolute_import, print_function import atexit import itertools import os import shutil import stat import sys import tempfile import warnings from contextlib import contextmanager import click import six if sys.version_info[:2] < (3, 5): from pathlib2 import Path else: from pathlib import Path TRACKED_TEMPORARY_DIRECTORIES = [] def set_write_bit(fn): # type: (str) -> None """ Set read-write permissions for the current user on the target path. Fail silently if the path doesn't exist. :param str fn: The target filename or path :return: None """ if not os.path.exists(fn): return file_stat = os.stat(fn).st_mode os.chmod(fn, file_stat | stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) if not os.path.isdir(fn): for path in [fn, os.path.dirname(fn)]: try: os.chflags(path, 0) except AttributeError: pass return None for root, dirs, files in os.walk(fn, topdown=False): for dir_ in [os.path.join(root, d) for d in dirs]: set_write_bit(dir_) for file_ in [os.path.join(root, f) for f in files]: set_write_bit(file_) def create_tracked_tempdir(*args, **kwargs): """Create a tracked temporary directory. This uses ``tempfile.mkdtemp``, but does not remove the directory when the return value goes out of scope, instead registers a handler to cleanup on program exit. The return value is the path to the created directory. """ tempdir = tempfile.mkdtemp(*args, **kwargs) TRACKED_TEMPORARY_DIRECTORIES.append(tempdir) atexit.register(shutil.rmtree, tempdir) if six.PY3: warnings.simplefilter("ignore", ResourceWarning) return tempdir @contextmanager def cd(path): if not path: return prev_cwd = Path.cwd().as_posix() if isinstance(path, Path): path = path.as_posix() os.chdir(str(path)) try: yield finally: os.chdir(prev_cwd) @contextmanager def temp_environ(): """Allow the ability to set os.environ temporarily""" environ = dict(os.environ) try: yield finally: os.environ.clear() os.environ.update(environ) def normalize_path(path): # type: (AnyStr) -> AnyStr """ Return a case-normalized absolute variable-expanded path. :param str path: The non-normalized path :return: A normalized, expanded, case-normalized path :rtype: str """ path = os.path.abspath(os.path.expandvars(os.path.expanduser(str(path)))) if os.name == "nt" and os.path.exists(path): try: from ctypes import create_unicode_buffer, windll except ImportError: path = os.path.normpath(os.path.normcase(path)) else: BUFSIZE = 500 buffer = create_unicode_buffer(BUFSIZE) get_long_path_name = windll.kernel32.GetLongPathNameW get_long_path_name(six.ensure_text(path), buffer, BUFSIZE) path = buffer.value return path return os.path.normpath(os.path.normcase(path)) def is_in_path(path, parent): # type: (AnyStr, AnyStr) -> bool """ Determine if the provided full path is in the given parent root. :param str path: The full path to check the location of. :param str parent: The parent path to check for membership in :return: Whether the full path is a member of the provided parent. :rtype: bool """ return normalize_path(str(path)).startswith(normalize_path(str(parent))) def normalized_match(path, parent): return is_in_path( normalize_path(path), normalize_path(parent) ) def is_in_ospath(path): ospath = os.environ["PATH"].split(os.pathsep) return any(normalized_match(path, entry) for entry in ospath) def print_python_versions(versions): versions = list(versions) if versions: click.echo("Found python at the following locations:", err=True) for v in versions: py = v.py_version comes_from = getattr(py, "comes_from", None) if comes_from is not None: comes_from_path = getattr(comes_from, "path", v.path) else: comes_from_path = v.path click.echo( "{py.name!s}: {py.version!s} ({py.architecture!s}) @ {comes_from!s}".format( py=py, comes_from=comes_from_path ), err=True, ) STACKLESS = [("stackless-2.7.14", "2.7.14"), ("stackless-3.5.4", "3.5.4")] PYPY = [ ("pypy2.7-5.10.0", "2.7"), ("pypy2.7-6.0.0", "2.7"), ("pypy3.5-5.10.1", "3.5"), ("pypy3.5-6.0.0", "3.5"), ] # These are definitely no longer supported but people may still have them installed # PYSTON = [("pyston-0.5.1", "2.7.8"), ("pyston-0.6.0", "2.7.8"), ("pyston-0.6.1", "2.7.8")] MINICONDA = [ ("miniconda3-3.18.3", "3.5.4"), ("miniconda3-3.19.0", "3.5.5"), ("miniconda3-4.0.5", "3.6.2"), ("miniconda3-4.1.11", "3.6.2"), ("miniconda3-4.3.27", "3.7.1"), ("miniconda3-4.3.30", "3.7.2"), ] # I have no idea how these are represented # MICROPYTHON = [("micropython-1.9.3", "1.9.3"), ("micropython-1.9.4", "1.9.4")] JYTHON = [ # ("jython-2.5-dev", "2.5-dev"), # ("jython-2.5.1", "2.5.1"), # ("jython-2.5.2", "2.5.2"), # ("jython-2.5.3", "2.5.3"), # ("jython-2.5.4-rc1", "2.5.4-rc1"), # ("jython-2.7.0", "2.7.0"), ("jython-2.7.1", "2.7.1") ] ANACONDA = [ ("anaconda2-2.4.1", "2.7.8"), ("anaconda2-2.5.0", "2.7.10"), ("anaconda2-4.0.0", "2.7.12"), ("anaconda2-4.1.0", "2.7.13"), ("anaconda2-4.1.1", "2.7.13"), ("anaconda2-4.2.0", "2.7.14"), ("anaconda2-4.3.0", "2.7.14"), ("anaconda2-4.3.1", "2.7.15"), ("anaconda2-4.4.0", "2.7.15"), ("anaconda3-4.1.1", "3.4.5"), ("anaconda3-4.2.0", "3.5.2"), ("anaconda3-4.3.0", "3.5.3"), ("anaconda3-4.3.1", "3.5.4"), ("anaconda3-4.4.0", "3.5.5"), ("anaconda3-5.0.0", "3.6.2"), ("anaconda3-5.0.1", "3.6.2"), ("anaconda3-5.1.0", "3.6.3"), ("anaconda3-5.2.0", "3.6.4"), ("anaconda3-5.3.0", "3.6.5"), ("anaconda3-2019.03", "3.7.2"), ] IRONPYTHON = [ # ("ironpython-dev", "2.7.8"), # ("ironpython-2.7.4", "2.7.4"), # ("ironpython-2.7.5", "2.7.5"), # ("ironpython-2.7.6.3", "2.7.6"), ("ironpython-2.7.7", "2.7.7") ] ACTIVEPYTHON = [ ("activepython-2.7.14", "2.7.14"), ("activepython-3.5.4", "3.5.4"), ("activepython-3.6.0", "3.6.0"), ] PYTHON = [ ("2.7.14", "2.7.14"), ("2.7.15", "2.7.15"), ("3.6.6", "3.6.6"), ("3.6.7", "3.6.7"), ("3.6.8", "3.6.8"), ("3.7.0", "3.7.0"), ("3.7-dev", "3.7-dev"), ("3.7.1", "3.7.1"), ("3.7.2", "3.7.2"), ("3.7.3", "3.7.3"), ("3.8-dev", "3.8-dev"), ] def yield_versions(): return itertools.chain( STACKLESS, PYPY, # PYSTON, MINICONDA, # MICROPYTHON, JYTHON, ANACONDA, IRONPYTHON, ACTIVEPYTHON, PYTHON, )