summaryrefslogtreecommitdiff
path: root/src/virtualenv/create/creator.py
diff options
context:
space:
mode:
authorBernát Gábor <bgabor8@bloomberg.net>2020-01-21 12:20:43 +0000
committerGitHub <noreply@github.com>2020-01-21 12:20:43 +0000
commit886ba275110f0a1f4777445dab4757b90f7ac6da (patch)
treed2d65198ee8cda7cd42b6033e84a8879adf00277 /src/virtualenv/create/creator.py
parent8d6af57d76edcf425beab6d53d4c14f1e49f7ca5 (diff)
downloadvirtualenv-886ba275110f0a1f4777445dab4757b90f7ac6da.tar.gz
separate describe/create - check upfront if can create (#1502)
* start Signed-off-by: Bernat Gabor <bgabor8@bloomberg.net> * test for Windows Signed-off-by: Bernat Gabor <bgabor8@bloomberg.net> * test for Windows Signed-off-by: Bernat Gabor <bgabor8@bloomberg.net> * fix Windows symlink cache
Diffstat (limited to 'src/virtualenv/create/creator.py')
-rw-r--r--src/virtualenv/create/creator.py193
1 files changed, 193 insertions, 0 deletions
diff --git a/src/virtualenv/create/creator.py b/src/virtualenv/create/creator.py
new file mode 100644
index 0000000..2ec19ae
--- /dev/null
+++ b/src/virtualenv/create/creator.py
@@ -0,0 +1,193 @@
+from __future__ import absolute_import, print_function, unicode_literals
+
+import json
+import logging
+import os
+import shutil
+import sys
+from abc import ABCMeta, abstractmethod
+from argparse import ArgumentTypeError
+from ast import literal_eval
+from collections import OrderedDict
+from stat import S_IWUSR
+
+import six
+from six import add_metaclass
+
+from virtualenv.discovery.py_info import Cmd
+from virtualenv.info import IS_ZIPAPP
+from virtualenv.pyenv_cfg import PyEnvCfg
+from virtualenv.util.path import Path
+from virtualenv.util.subprocess import run_cmd
+from virtualenv.util.zipapp import extract_to_app_data
+from virtualenv.version import __version__
+
+HERE = Path(__file__).absolute().parent
+DEBUG_SCRIPT = HERE / "debug.py"
+
+
+@add_metaclass(ABCMeta)
+class Creator(object):
+ def __init__(self, options, interpreter):
+ self.interpreter = interpreter
+ self._debug = None
+ self.dest = Path(options.dest)
+ self.enable_system_site_package = options.system_site
+ self.clear = options.clear
+ self.pyenv_cfg = PyEnvCfg.from_folder(self.dest)
+
+ def __repr__(self):
+ return six.ensure_str(self.__unicode__())
+
+ def __unicode__(self):
+ return "{}({})".format(self.__class__.__name__, ", ".join("{}={}".format(k, v) for k, v in self._args()))
+
+ def _args(self):
+ return [
+ ("dest", six.ensure_text(str(self.dest))),
+ ("global", self.enable_system_site_package),
+ ("clear", self.clear),
+ ]
+
+ @classmethod
+ def add_parser_arguments(cls, parser, interpreter, meta):
+ parser.add_argument(
+ "dest", help="directory to create virtualenv at", type=cls.validate_dest, default="venv", nargs="?",
+ )
+ parser.add_argument(
+ "--clear",
+ dest="clear",
+ action="store_true",
+ help="clear out the non-root install and start from scratch",
+ default=False,
+ )
+ parser.add_argument(
+ "--system-site-packages",
+ default=False,
+ action="store_true",
+ dest="system_site",
+ help="give the virtual environment access to the system site-packages dir",
+ )
+
+ @classmethod
+ def validate_dest(cls, raw_value):
+ """No path separator in the path, valid chars and must be write-able"""
+
+ def non_write_able(dest, value):
+ common = Path(*os.path.commonprefix([value.parts, dest.parts]))
+ raise ArgumentTypeError(
+ "the destination {} is not write-able at {}".format(dest.relative_to(common), common)
+ )
+
+ # the file system must be able to encode
+ # note in newer CPython this is always utf-8 https://www.python.org/dev/peps/pep-0529/
+ encoding = sys.getfilesystemencoding()
+ refused = OrderedDict()
+ kwargs = {"errors": "ignore"} if encoding != "mbcs" else {}
+ for char in six.ensure_text(raw_value):
+ try:
+ trip = char.encode(encoding, **kwargs).decode(encoding)
+ if trip == char:
+ continue
+ raise ValueError(trip)
+ except ValueError:
+ refused[char] = None
+ if refused:
+ raise ArgumentTypeError(
+ "the file system codec ({}) cannot handle characters {!r} within {!r}".format(
+ encoding, "".join(refused.keys()), raw_value
+ )
+ )
+ for char in (i for i in (os.pathsep, os.altsep) if i is not None):
+ if char in raw_value:
+ raise ArgumentTypeError(
+ "destination {!r} must not contain the path separator ({}) as this would break "
+ "the activation scripts".format(raw_value, char)
+ )
+
+ value = Path(raw_value)
+ if value.exists() and value.is_file():
+ raise ArgumentTypeError("the destination {} already exists and is a file".format(value))
+ if (3, 3) <= sys.version_info <= (3, 6):
+ # pre 3.6 resolve is always strict, aka must exists, sidestep by using os.path operation
+ dest = Path(os.path.realpath(raw_value))
+ else:
+ dest = value.resolve()
+ value = dest
+ while dest:
+ if dest.exists():
+ if os.access(six.ensure_text(str(dest)), os.W_OK):
+ break
+ else:
+ non_write_able(dest, value)
+ base, _ = dest.parent, dest.name
+ if base == dest:
+ non_write_able(dest, value) # pragma: no cover
+ dest = base
+ return str(value)
+
+ def run(self):
+ if self.dest.exists() and self.clear:
+ logging.debug("delete %s", self.dest)
+
+ def onerror(func, path, exc_info):
+ if not os.access(path, os.W_OK):
+ os.chmod(path, S_IWUSR)
+ func(path)
+ else:
+ raise
+
+ shutil.rmtree(str(self.dest), ignore_errors=True, onerror=onerror)
+ self.create()
+ self.set_pyenv_cfg()
+
+ @abstractmethod
+ def create(self):
+ raise NotImplementedError
+
+ @classmethod
+ def can_create(cls, interpreter):
+ """Default is that we can"""
+ return True
+
+ def set_pyenv_cfg(self):
+ self.pyenv_cfg.content = {
+ "home": self.interpreter.system_exec_prefix,
+ "include-system-site-packages": "true" if self.enable_system_site_package else "false",
+ "implementation": self.interpreter.implementation,
+ "version_info": ".".join(str(i) for i in self.interpreter.version_info),
+ "virtualenv": __version__,
+ }
+
+ @property
+ def debug(self):
+ if self._debug is None and self.exe is not None:
+ self._debug = get_env_debug_info(self.exe, self.debug_script())
+ return self._debug
+
+ # noinspection PyMethodMayBeStatic
+ def debug_script(self):
+ return DEBUG_SCRIPT
+
+
+def get_env_debug_info(env_exe, debug_script):
+ if IS_ZIPAPP:
+ debug_script = extract_to_app_data(debug_script)
+ cmd = [six.ensure_text(str(env_exe)), six.ensure_text(str(debug_script))]
+ logging.debug("debug via %r", Cmd(cmd))
+ env = os.environ.copy()
+ env.pop(str("PYTHONPATH"), None)
+ code, out, err = run_cmd(cmd)
+ # noinspection PyBroadException
+ try:
+ if code != 0:
+ result = literal_eval(out)
+ else:
+ result = json.loads(out)
+ if err:
+ result["err"] = err
+ except Exception as exception:
+ return {"out": out, "err": err, "returncode": code, "exception": repr(exception)}
+ if "sys" in result and "path" in result["sys"]:
+ del result["sys"]["path"][0]
+ return result