diff options
| author | Bernát Gábor <bgabor8@bloomberg.net> | 2020-01-02 16:32:54 +0000 |
|---|---|---|
| committer | Bernat Gabor <bgabor8@bloomberg.net> | 2020-01-10 15:38:36 +0000 |
| commit | ff6dc73d447a3c6276af64df2eb91e2709e450a3 (patch) | |
| tree | cf2c4be51c557ce2157cc32279cbd53464df3bf5 /src/virtualenv | |
| parent | 1cb5216252dbb144a3ee3976f9ec92def3dfc6db (diff) | |
| download | virtualenv-ff6dc73d447a3c6276af64df2eb91e2709e450a3.tar.gz | |
unicode support (#1477)
* creator unicode support
Signed-off-by: Bernat Gabor <bgabor8@bloomberg.net>
* activator support
Signed-off-by: Bernat Gabor <bgabor8@bloomberg.net>
* fix
* add space
* python3.4 support
* Windows fixes
* some fixes
* fix powershell requires utf-16
* try to fix python2 windows
Signed-off-by: Bernat Gabor <bgabor8@bloomberg.net>
* use utf-8 for activation scripts
Signed-off-by: Bernat Gabor <bgabor8@bloomberg.net>
* fix
* more fix
Signed-off-by: Bernat Gabor <bgabor8@bloomberg.net>
* fix
Signed-off-by: Bernat Gabor <bgabor8@bloomberg.net>
* windows path py2.7
* fixes for Python 2 and unicode on Windows
* do not single out mbcs, but the file system encoder
* do not install pathlib python 2 windows
Signed-off-by: Bernat Gabor <bgabor8@bloomberg.net>
* fix encoding on py35
Signed-off-by: Bernat Gabor <bgabor8@bloomberg.net>
Diffstat (limited to 'src/virtualenv')
30 files changed, 481 insertions, 120 deletions
diff --git a/src/virtualenv/activation/bash/__init__.py b/src/virtualenv/activation/bash/__init__.py index fd53741..7c85564 100644 --- a/src/virtualenv/activation/bash/__init__.py +++ b/src/virtualenv/activation/bash/__init__.py @@ -1,6 +1,6 @@ from __future__ import absolute_import, unicode_literals -from pathlib2 import Path +from virtualenv.util import Path from ..via_template import ViaTemplateActivator diff --git a/src/virtualenv/activation/bash/activate.sh b/src/virtualenv/activation/bash/activate.sh index d9b8781..19bf552 100644 --- a/src/virtualenv/activation/bash/activate.sh +++ b/src/virtualenv/activation/bash/activate.sh @@ -46,7 +46,7 @@ deactivate () { # unset irrelevant variables deactivate nondestructive -VIRTUAL_ENV="__VIRTUAL_ENV__" +VIRTUAL_ENV='__VIRTUAL_ENV__' export VIRTUAL_ENV _OLD_VIRTUAL_PATH="$PATH" diff --git a/src/virtualenv/activation/batch/__init__.py b/src/virtualenv/activation/batch/__init__.py index 4e4b839..2c3da44 100644 --- a/src/virtualenv/activation/batch/__init__.py +++ b/src/virtualenv/activation/batch/__init__.py @@ -1,6 +1,6 @@ from __future__ import absolute_import, unicode_literals -from pathlib2 import Path +from virtualenv.util import Path from ..via_template import ViaTemplateActivator diff --git a/src/virtualenv/activation/cshell/__init__.py b/src/virtualenv/activation/cshell/__init__.py index e818ede..6a96d56 100644 --- a/src/virtualenv/activation/cshell/__init__.py +++ b/src/virtualenv/activation/cshell/__init__.py @@ -1,6 +1,6 @@ from __future__ import absolute_import, unicode_literals -from pathlib2 import Path +from virtualenv.util import Path from ..via_template import ViaTemplateActivator diff --git a/src/virtualenv/activation/cshell/activate.csh b/src/virtualenv/activation/cshell/activate.csh index c4a6d58..72b2cf8 100644 --- a/src/virtualenv/activation/cshell/activate.csh +++ b/src/virtualenv/activation/cshell/activate.csh @@ -10,15 +10,15 @@ alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PA # Unset irrelevant variables. deactivate nondestructive -setenv VIRTUAL_ENV "__VIRTUAL_ENV__" +setenv VIRTUAL_ENV '__VIRTUAL_ENV__' set _OLD_VIRTUAL_PATH="$PATH:q" setenv PATH "$VIRTUAL_ENV:q/__BIN_NAME__:$PATH:q" -if ("__VIRTUAL_PROMPT__" != "") then - set env_name = "__VIRTUAL_PROMPT__" +if ('__VIRTUAL_PROMPT__' != "") then + set env_name = '__VIRTUAL_PROMPT__' else set env_name = '('"$VIRTUAL_ENV:t:q"') ' endif diff --git a/src/virtualenv/activation/fish/__init__.py b/src/virtualenv/activation/fish/__init__.py index 0e54406..0dfe63c 100644 --- a/src/virtualenv/activation/fish/__init__.py +++ b/src/virtualenv/activation/fish/__init__.py @@ -1,6 +1,6 @@ from __future__ import absolute_import, unicode_literals -from pathlib2 import Path +from virtualenv.util import Path from ..via_template import ViaTemplateActivator diff --git a/src/virtualenv/activation/fish/activate.fish b/src/virtualenv/activation/fish/activate.fish index 4e29768..770fb0d 100644 --- a/src/virtualenv/activation/fish/activate.fish +++ b/src/virtualenv/activation/fish/activate.fish @@ -18,16 +18,16 @@ function deactivate -d 'Exit virtualenv mode and return to the normal environmen # reset old environment variables if test -n "$_OLD_VIRTUAL_PATH" # https://github.com/fish-shell/fish-shell/issues/436 altered PATH handling - if test (echo $FISH_VERSION | tr "." "\n")[1] -lt 3 - set -gx PATH (_fishify_path $_OLD_VIRTUAL_PATH) + if test (echo $FISH_VERSION | head -c 1) -lt 3 + set -gx PATH (_fishify_path "$_OLD_VIRTUAL_PATH") else - set -gx PATH $_OLD_VIRTUAL_PATH + set -gx PATH "$_OLD_VIRTUAL_PATH" end set -e _OLD_VIRTUAL_PATH end if test -n "$_OLD_VIRTUAL_PYTHONHOME" - set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME + set -gx PYTHONHOME "$_OLD_VIRTUAL_PYTHONHOME" set -e _OLD_VIRTUAL_PYTHONHOME end @@ -57,15 +57,15 @@ end # Unset irrelevant variables. deactivate nondestructive -set -gx VIRTUAL_ENV "__VIRTUAL_ENV__" +set -gx VIRTUAL_ENV '__VIRTUAL_ENV__' # https://github.com/fish-shell/fish-shell/issues/436 altered PATH handling -if test (echo $FISH_VERSION | tr "." "\n")[1] -lt 3 +if test (echo $FISH_VERSION | head -c 1) -lt 3 set -gx _OLD_VIRTUAL_PATH (_bashify_path $PATH) else - set -gx _OLD_VIRTUAL_PATH $PATH + set -gx _OLD_VIRTUAL_PATH "$PATH" end -set -gx PATH "$VIRTUAL_ENV/__BIN_NAME__" $PATH +set -gx PATH "$VIRTUAL_ENV"'/__BIN_NAME__' $PATH # Unset `$PYTHONHOME` if set. if set -q PYTHONHOME @@ -87,10 +87,10 @@ if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" # Prompt override provided? # If not, just prepend the environment name. - if test -n "__VIRTUAL_PROMPT__" - printf '%s%s' "__VIRTUAL_PROMPT__" (set_color normal) + if test -n '__VIRTUAL_PROMPT__' + printf '%s%s' '__VIRTUAL_PROMPT__' (set_color normal) else - printf '%s(%s) ' (set_color normal) (basename "$VIRTUAL_ENV") + printf '%s(%s) ' (set_color normal) (basename '$VIRTUAL_ENV') end # Restore the original $status diff --git a/src/virtualenv/activation/powershell/__init__.py b/src/virtualenv/activation/powershell/__init__.py index b5b0a75..a3c6f9e 100644 --- a/src/virtualenv/activation/powershell/__init__.py +++ b/src/virtualenv/activation/powershell/__init__.py @@ -1,6 +1,6 @@ from __future__ import absolute_import, unicode_literals -from pathlib2 import Path +from virtualenv.util import Path from ..via_template import ViaTemplateActivator diff --git a/src/virtualenv/activation/python/__init__.py b/src/virtualenv/activation/python/__init__.py index 1d73e99..36b2b2c 100644 --- a/src/virtualenv/activation/python/__init__.py +++ b/src/virtualenv/activation/python/__init__.py @@ -3,7 +3,7 @@ from __future__ import absolute_import, unicode_literals import json import os -from pathlib2 import Path +from virtualenv.util import Path from ..via_template import ViaTemplateActivator diff --git a/src/virtualenv/activation/python/activate_this.py b/src/virtualenv/activation/python/activate_this.py index fc8d449..3311fe8 100644 --- a/src/virtualenv/activation/python/activate_this.py +++ b/src/virtualenv/activation/python/activate_this.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Activate virtualenv for current interpreter: Use exec(open(this_file).read(), {'__file__': this_file}). @@ -14,14 +15,21 @@ try: except NameError: raise AssertionError("You must use exec(open(this_file).read(), {'__file__': this_file}))") + +def set_env(key, value, encoding): + if sys.version_info[0] == 2: + value = value.encode(encoding) + os.environ[key] = value + + # prepend bin to PATH (this file is inside the bin directory) bin_dir = os.path.dirname(os.path.abspath(__file__)) -os.environ["PATH"] = os.pathsep.join([bin_dir] + os.environ.get("PATH", "").split(os.pathsep)) +set_env("PATH", os.pathsep.join([bin_dir] + os.environ.get("PATH", "").split(os.pathsep)), sys.getfilesystemencoding()) base = os.path.dirname(bin_dir) # virtual env is right above bin directory -os.environ["VIRTUAL_ENV"] = base +set_env("VIRTUAL_ENV", base, sys.getfilesystemencoding()) # add the virtual environments site-packages to the host python import mechanism prev = set(sys.path) @@ -33,7 +41,11 @@ __SITE_PACKAGES__ ''' for site_package in json.loads(site_packages): + if sys.version_info[0] == 2: + site_package = site_package.encode('utf-8').decode(sys.getfilesystemencoding()) path = os.path.realpath(os.path.join(os.path.dirname(__file__), site_package)) + if sys.version_info[0] == 2: + path = path.encode(sys.getfilesystemencoding()) site.addsitedir(path) # fmt: on diff --git a/src/virtualenv/activation/via_template.py b/src/virtualenv/activation/via_template.py index 864454f..e7b3aad 100644 --- a/src/virtualenv/activation/via_template.py +++ b/src/virtualenv/activation/via_template.py @@ -24,9 +24,9 @@ class ViaTemplateActivator(Activator): def replacements(self, creator, dest_folder): return { "__VIRTUAL_PROMPT__": "" if self.flag_prompt is None else self.flag_prompt, - "__VIRTUAL_ENV__": str(creator.dest_dir), - "__VIRTUAL_NAME__": str(creator.env_name), - "__BIN_NAME__": str(creator.bin_name), + "__VIRTUAL_ENV__": six.ensure_text(str(creator.dest_dir)), + "__VIRTUAL_NAME__": creator.env_name, + "__BIN_NAME__": six.ensure_text(str(creator.bin_name)), "__PATH_SEP__": os.pathsep, } @@ -35,4 +35,4 @@ class ViaTemplateActivator(Activator): text = pkgutil.get_data(self.__module__, str(template)).decode("utf-8") for start, end in replacements.items(): text = text.replace(start, end) - (to_folder / template).write_text(text) + (to_folder / template).write_text(text, encoding="utf-8") diff --git a/src/virtualenv/activation/xonosh/__init__.py b/src/virtualenv/activation/xonosh/__init__.py index ceb5340..94546ff 100644 --- a/src/virtualenv/activation/xonosh/__init__.py +++ b/src/virtualenv/activation/xonosh/__init__.py @@ -1,6 +1,6 @@ from __future__ import absolute_import, unicode_literals -from pathlib2 import Path +from virtualenv.util import Path from ..via_template import ViaTemplateActivator diff --git a/src/virtualenv/config/ini.py b/src/virtualenv/config/ini.py index 9c86e48..4acb70f 100644 --- a/src/virtualenv/config/ini.py +++ b/src/virtualenv/config/ini.py @@ -3,9 +3,8 @@ from __future__ import absolute_import, unicode_literals import logging import os -from pathlib2 import Path - from virtualenv.info import PY3, get_default_config_dir +from virtualenv.util import Path from .convert import convert diff --git a/src/virtualenv/info.py b/src/virtualenv/info.py index 149f852..294110e 100644 --- a/src/virtualenv/info.py +++ b/src/virtualenv/info.py @@ -3,7 +3,8 @@ from __future__ import absolute_import, unicode_literals import sys from appdirs import user_config_dir, user_data_dir -from pathlib2 import Path + +from virtualenv.util import Path IS_PYPY = hasattr(sys, "pypy_version_info") PY3 = sys.version_info[0] == 3 diff --git a/src/virtualenv/interpreters/create/cpython/common.py b/src/virtualenv/interpreters/create/cpython/common.py index d05d47b..89a91c6 100644 --- a/src/virtualenv/interpreters/create/cpython/common.py +++ b/src/virtualenv/interpreters/create/cpython/common.py @@ -4,10 +4,9 @@ import abc from os import X_OK, access, chmod import six -from pathlib2 import Path from virtualenv.interpreters.create.via_global_ref import ViaGlobalRef -from virtualenv.util import copy, ensure_dir, symlink +from virtualenv.util import Path, copy, ensure_dir, symlink @six.add_metaclass(abc.ABCMeta) diff --git a/src/virtualenv/interpreters/create/cpython/cpython2.py b/src/virtualenv/interpreters/create/cpython/cpython2.py index fbaee88..7c79158 100644 --- a/src/virtualenv/interpreters/create/cpython/cpython2.py +++ b/src/virtualenv/interpreters/create/cpython/cpython2.py @@ -3,9 +3,8 @@ from __future__ import absolute_import, unicode_literals import abc import six -from pathlib2 import Path -from virtualenv.util import copy +from virtualenv.util import Path, copy from .common import CPython, CPythonPosix, CPythonWindows diff --git a/src/virtualenv/interpreters/create/cpython/cpython3.py b/src/virtualenv/interpreters/create/cpython/cpython3.py index 3249306..b14ce0a 100644 --- a/src/virtualenv/interpreters/create/cpython/cpython3.py +++ b/src/virtualenv/interpreters/create/cpython/cpython3.py @@ -3,9 +3,8 @@ from __future__ import absolute_import, unicode_literals import abc import six -from pathlib2 import Path -from virtualenv.util import copy +from virtualenv.util import Path, copy from .common import CPython, CPythonPosix, CPythonWindows diff --git a/src/virtualenv/interpreters/create/creator.py b/src/virtualenv/interpreters/create/creator.py index 872d8d1..b917a37 100644 --- a/src/virtualenv/interpreters/create/creator.py +++ b/src/virtualenv/interpreters/create/creator.py @@ -4,15 +4,16 @@ import json import logging import os import shutil +import sys from abc import ABCMeta, abstractmethod from argparse import ArgumentTypeError -from pathlib2 import Path +import six from six import add_metaclass from virtualenv.info import IS_WIN from virtualenv.pyenv_cfg import PyEnvCfg -from virtualenv.util import run_cmd +from virtualenv.util import Path, run_cmd from virtualenv.version import __version__ HERE = Path(__file__).absolute().parent @@ -47,28 +48,13 @@ class Creator(object): help="Give the virtual environment access to the system site-packages dir.", ) - def validate_dest_dir(value): - """No path separator in the path and must be write-able""" - if os.pathsep in value: - raise ArgumentTypeError( - "destination {!r} must not contain the path separator ({}) as this would break " - "the activation scripts".format(value, os.pathsep) - ) - value = Path(value) - if value.exists() and value.is_file(): - raise ArgumentTypeError("the destination {} already exists and is a file".format(value)) - value = dest = value.resolve() - while dest: - if dest.exists(): - if os.access(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) + parser.add_argument( + "dest_dir", help="directory to create virtualenv at", type=cls.validate_dest_dir, default="env", nargs="?", + ) + + @classmethod + def validate_dest_dir(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])) @@ -76,9 +62,45 @@ class Creator(object): "the destination {} is not write-able at {}".format(dest.relative_to(common), common) ) - parser.add_argument( - "dest_dir", help="directory to create virtualenv at", type=validate_dest_dir, default="env", nargs="?", - ) + # 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() + path_converted = raw_value.encode(encoding, errors="ignore").decode(encoding) + if path_converted != raw_value: + refused = set(raw_value) - { + c + for c, i in ((char, char.encode(encoding)) for char in raw_value) + if c == "?" or i != six.ensure_str("?") + } + raise ArgumentTypeError( + "the file system codec ({}) does not support characters {!r}".format(encoding, refused) + ) + if os.pathsep in raw_value: + raise ArgumentTypeError( + "destination {!r} must not contain the path separator ({}) as this would break " + "the activation scripts".format(raw_value, os.pathsep) + ) + + 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_dir.exists() and self.clear: @@ -104,7 +126,7 @@ class Creator(object): @property def env_name(self): - return self.dest_dir.parts[-1] + return six.ensure_text(self.dest_dir.parts[-1]) @property def bin_name(self): @@ -138,8 +160,8 @@ class Creator(object): def get_env_debug_info(env_exe, debug_script): - cmd = [str(env_exe), str(debug_script)] - logging.debug(" ".join(cmd)) + cmd = [six.ensure_text(str(env_exe)), six.ensure_text(str(debug_script))] + logging.debug(" ".join(six.ensure_text(i) for i in cmd)) env = os.environ.copy() env.pop("PYTHONPATH", None) code, out, err = run_cmd(cmd) diff --git a/src/virtualenv/interpreters/create/debug.py b/src/virtualenv/interpreters/create/debug.py index cb4da8e..37f1a45 100644 --- a/src/virtualenv/interpreters/create/debug.py +++ b/src/virtualenv/interpreters/create/debug.py @@ -2,6 +2,20 @@ import sys # built-in +def encode_path(value): + if value is None: + return None + if isinstance(value, bytes): + return value.decode(sys.getfilesystemencoding()) + if isinstance(value, type): + return repr(value) + return value + + +def encode_list_path(value): + return [encode_path(i) for i in value] + + def run(): """print debug data about the virtual environment""" try: @@ -11,7 +25,7 @@ def run(): # noinspection PyPep8Naming OrderedDict = dict # pragma: no cover result = OrderedDict([("sys", OrderedDict())]) - for key in ( + path_keys = ( "executable", "_base_executable", "prefix", @@ -21,13 +35,15 @@ def run(): "base_exec_prefix", "path", "meta_path", - "version", - ): + ) + for key in path_keys: value = getattr(sys, key, None) - if key == "meta_path" and value is not None: - value = [repr(i) for i in value] + if isinstance(value, list): + value = encode_list_path(value) + else: + value = encode_path(value) result["sys"][key] = value - + result["version"] = sys.version import os # landmark result["os"] = os.__file__ @@ -45,7 +61,7 @@ def run(): result["json"] = repr(json) print(json.dumps(result, indent=2)) - except ImportError as exception: # pragma: no cover + except (ImportError, ValueError, TypeError) as exception: # pragma: no cover result["json"] = repr(exception) # pragma: no cover print(repr(result)) # pragma: no cover raise SystemExit(1) # pragma: no cover diff --git a/src/virtualenv/interpreters/create/venv.py b/src/virtualenv/interpreters/create/venv.py index 6c71d94..e80a45e 100644 --- a/src/virtualenv/interpreters/create/venv.py +++ b/src/virtualenv/interpreters/create/venv.py @@ -47,7 +47,7 @@ class Venv(ViaGlobalRef): raise ProcessCallFailed(code, out, err, cmd) def get_host_create_cmd(self): - cmd = [str(self.interpreter.system_executable), "-m", "venv", "--without-pip"] + cmd = [self.interpreter.system_executable, "-m", "venv", "--without-pip"] if self.system_site_package: cmd.append("--system-site-packages") cmd.append("--symlinks" if self.symlinks else "--copies") diff --git a/src/virtualenv/interpreters/discovery/builtin.py b/src/virtualenv/interpreters/discovery/builtin.py index 1dc80c0..166066d 100644 --- a/src/virtualenv/interpreters/discovery/builtin.py +++ b/src/virtualenv/interpreters/discovery/builtin.py @@ -4,7 +4,7 @@ import logging import os import sys -from pathlib2 import Path +import six from virtualenv.info import IS_WIN @@ -64,7 +64,10 @@ def propose_interpreters(spec): yield interpreter, True paths = get_paths() - for path in paths: # find on path, the path order matters (as the candidates are less easy to control by end user) + # find on path, the path order matters (as the candidates are less easy to control by end user) + for pos, path in enumerate(paths): + path = six.ensure_text(path) + logging.debug(LazyPathDump(pos, path)) for candidate, match in possible_specs(spec): found = check_path(candidate, path) if found is not None: @@ -85,31 +88,25 @@ def get_paths(): paths = [] else: paths = [p for p in path.split(os.pathsep) if os.path.exists(p)] - logging.debug(LazyPathDump(paths)) return paths class LazyPathDump(object): - def __init__(self, paths): - self.paths = paths + def __init__(self, pos, path): + self.pos = pos + self.path = path def __str__(self): - content = "PATH =>{}".format(os.linesep) - for i, p in enumerate(self.paths): - files = [] - for file in Path(p).iterdir(): - try: - if file.is_dir(): - continue - except OSError: - pass - files.append(file.name) - content += str(i) + content = "discover from PATH[{}]:{} with =>".format(self.pos, self.path) + for file_name in os.listdir(self.path): + try: + file_path = os.path.join(self.path, file_name) + if os.path.isdir(file_path) or not os.access(file_path, os.X_OK): + continue + except OSError: + pass content += " " - content += str(p) - content += " with " - content += " ".join(files) - content += os.linesep + content += file_name return content diff --git a/src/virtualenv/interpreters/discovery/py_info.py b/src/virtualenv/interpreters/discovery/py_info.py index 2af2394..13e9b79 100644 --- a/src/virtualenv/interpreters/discovery/py_info.py +++ b/src/virtualenv/interpreters/discovery/py_info.py @@ -10,12 +10,9 @@ import json import logging import os import platform -import subprocess import sys from collections import OrderedDict, namedtuple -IS_WIN = sys.platform == "win32" - VersionInfo = namedtuple("VersionInfo", ["major", "minor", "micro", "releaselevel", "serial"]) @@ -205,12 +202,14 @@ class PythonInfo(object): @classmethod def _load_for_exe(cls, exe): + from virtualenv.util.subprocess import subprocess, Popen + path = "{}.py".format(os.path.splitext(__file__)[0]) cmd = [exe, path] # noinspection DuplicatedCode # this is duplicated here because this file is executed on its own, so cannot be refactored otherwise try: - process = subprocess.Popen( + process = Popen( cmd, universal_newlines=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE ) out, err = process.communicate() diff --git a/src/virtualenv/pyenv_cfg.py b/src/virtualenv/pyenv_cfg.py index 06a8c81..05daa5a 100644 --- a/src/virtualenv/pyenv_cfg.py +++ b/src/virtualenv/pyenv_cfg.py @@ -2,6 +2,8 @@ from __future__ import absolute_import, unicode_literals import logging +import six + class PyEnvCfg(object): def __init__(self, content, path): @@ -20,7 +22,7 @@ class PyEnvCfg(object): @staticmethod def _read_values(path): content = {} - for line in path.read_text().splitlines(): + for line in path.read_text(encoding="utf-8").splitlines(): equals_at = line.index("=") key = line[:equals_at].strip() value = line[equals_at + 1 :].strip() @@ -28,13 +30,13 @@ class PyEnvCfg(object): return content def write(self): - with open(str(self.path), "wt") as file_handler: - logging.debug("write %s", self.path) + with open(six.ensure_text(str(self.path)), "wb") as file_handler: + logging.debug("write %s", six.ensure_text(str(self.path))) for key, value in self.content.items(): line = "{} = {}".format(key, value) logging.debug("\t%s", line) - file_handler.write(line) - file_handler.write("\n") + file_handler.write(line.encode("utf-8")) + file_handler.write(b"\n") def refresh(self): self.content = self._read_values(self.path) diff --git a/src/virtualenv/seed/embed/link_app_data.py b/src/virtualenv/seed/embed/link_app_data.py index c400da7..d660574 100644 --- a/src/virtualenv/seed/embed/link_app_data.py +++ b/src/virtualenv/seed/embed/link_app_data.py @@ -9,10 +9,10 @@ import zipfile from shutil import copytree from textwrap import dedent -from pathlib2 import Path from six import PY3 from virtualenv.info import get_default_data_dir +from virtualenv.util import Path from .base_embed import BaseEmbed from .wheels.acquire import get_wheel @@ -172,7 +172,7 @@ def create_console_entry_point(bin_dir, name, value, env_exe, creator): "{}-{}.{}".format(name, version.major, version.minor), ): exe = bin_dir / new_name - exe.write_text(content) + exe.write_text(content, encoding="utf-8") exe.chmod(0o755) result.append(exe) return result @@ -210,7 +210,7 @@ def fix_records(creator, dist_info, site_package, folder_linked, added, extra_fi record = dist_info / "RECORD" content = ("" if folder_linked else record.read_text()) + "\n".join(new_records) - record.write_text(content) + record.write_text(content, encoding="utf-8") def add_record_line(name): diff --git a/src/virtualenv/seed/embed/pip_invoke.py b/src/virtualenv/seed/embed/pip_invoke.py index 4de49f3..d65e89a 100644 --- a/src/virtualenv/seed/embed/pip_invoke.py +++ b/src/virtualenv/seed/embed/pip_invoke.py @@ -1,10 +1,10 @@ from __future__ import absolute_import, unicode_literals import os -import subprocess from virtualenv.seed.embed.base_embed import BaseEmbed from virtualenv.seed.embed.wheels.acquire import get_bundled_wheel +from virtualenv.util.subprocess import Popen class PipInvoke(BaseEmbed): @@ -39,4 +39,5 @@ class PipInvoke(BaseEmbed): } ) - subprocess.call(cmd, env=env) + process = Popen(cmd, env=env) + process.communicate() diff --git a/src/virtualenv/seed/embed/wheels/acquire.py b/src/virtualenv/seed/embed/wheels/acquire.py index a04c1b8..6460b31 100644 --- a/src/virtualenv/seed/embed/wheels/acquire.py +++ b/src/virtualenv/seed/embed/wheels/acquire.py @@ -1,11 +1,11 @@ """Bootstrap""" from __future__ import absolute_import, unicode_literals -import subprocess from collections import defaultdict from shutil import copy2 -from pathlib2 import Path +from virtualenv.util import Path +from virtualenv.util.subprocess import subprocess from . import BUNDLE_SUPPORT, MAX diff --git a/src/virtualenv/util.py b/src/virtualenv/util/__init__.py index 09c38e6..9e7d13b 100644 --- a/src/virtualenv/util.py +++ b/src/virtualenv/util/__init__.py @@ -3,17 +3,20 @@ from __future__ import absolute_import, unicode_literals import logging import os import shutil -import subprocess from functools import partial from os import makedirs import six +from virtualenv.util.subprocess import Popen, subprocess + +from .path import Path + def ensure_dir(path): if not path.exists(): - logging.debug("created %s", path) - makedirs(six.text_type(path)) + logging.debug("created %s", six.ensure_text(str(path))) + makedirs(six.ensure_text(str(path))) HAS_SYMLINK = hasattr(os, "symlink") @@ -30,21 +33,26 @@ def symlink_or_copy(do_copy, src, dst, relative_symlinks_ok=False): if not dst.is_symlink(): # can't link to itself! if relative_symlinks_ok: assert src.parent == dst.parent - os.symlink(src.name, six.text_type(dst)) + os.symlink(six.ensure_text(src.name), six.ensure_text(str(dst))) else: - os.symlink(six.text_type(src), six.text_type(dst)) + os.symlink(six.ensure_text(str(src)), six.ensure_text(str(dst))) except OSError as exception: - logging.warning("symlink failed %r, for %s to %s, will try copy", exception, src, dst) + logging.warning( + "symlink failed %r, for %s to %s, will try copy", + exception, + six.ensure_text(str(src)), + six.ensure_text(str(dst)), + ) do_copy = True if do_copy: copier = shutil.copy2 if src.is_file() else shutil.copytree - copier(six.text_type(src), six.text_type(dst)) - logging.debug("%s %s to %s", "copy" if do_copy else "symlink", src, dst) + copier(six.ensure_text(str(src)), six.ensure_text(str(dst))) + logging.debug("%s %s to %s", "copy" if do_copy else "symlink", six.ensure_text(str(src)), six.ensure_text(str(dst))) def run_cmd(cmd): try: - process = subprocess.Popen( + process = Popen( cmd, universal_newlines=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE ) out, err = process.communicate() # input disabled @@ -56,3 +64,5 @@ def run_cmd(cmd): symlink = partial(symlink_or_copy, False) copy = partial(symlink_or_copy, True) + +__all__ = ("Path", "symlink", "copy", "run_cmd", "ensure_dir") diff --git a/src/virtualenv/util/path.py b/src/virtualenv/util/path.py new file mode 100644 index 0000000..5ef5186 --- /dev/null +++ b/src/virtualenv/util/path.py @@ -0,0 +1,137 @@ +import sys +from contextlib import contextmanager + +import six + +if six.PY3: + from pathlib import Path + + if sys.version_info[0:2] == (3, 4): + # no read/write text on python3.4 + BuiltinPath = Path + + class Path(type(BuiltinPath())): + def read_text(self, encoding=None, errors=None): + """ + Open the file in text mode, read it, and close the file. + """ + with self.open(mode="r", encoding=encoding, errors=errors) as f: + return f.read() + + def write_text(self, data, encoding=None, errors=None): + """ + Open the file in text mode, write to it, and close the file. + """ + if not isinstance(data, str): + raise TypeError("data must be str, not %s" % data.__class__.__name__) + with self.open(mode="w", encoding=encoding, errors=errors) as f: + return f.write(data) + + +else: + if sys.platform == "win32": + # workaround for https://github.com/mcmtroffaes/pathlib2/issues/56 + import os + + class Path(object): + def __init__(self, path): + self._path = path._path if isinstance(path, Path) else six.ensure_text(path) + + def __repr__(self): + return six.ensure_str(u"Path({})".format(self._path)) + + def __str__(self): + return six.ensure_str(self._path) + + def __div__(self, other): + return Path( + os.path.join(self._path, other._path if isinstance(other, Path) else six.ensure_text(other)) + ) + + def __eq__(self, other): + return self._path == (other._path if isinstance(other, Path) else None) + + def __ne__(self, other): + return not (self == other) + + def __hash__(self): + return hash(self._path) + + def exists(self): + return os.path.exists(self._path) + + def absolute(self): + return Path(os.path.abspath(self._path)) + + @property + def parent(self): + return Path(os.path.abspath(os.path.join(self._path, os.path.pardir))) + + def resolve(self): + return Path(os.path.realpath(self._path)) + + @property + def name(self): + return os.path.basename(self._path) + + @property + def parts(self): + return self._path.split(os.sep) + + def is_file(self): + return os.path.isfile(self._path) + + def is_dir(self): + return os.path.isdir(self._path) + + def mkdir(self, parents=True, exist_ok=True): + if not self.exists() and exist_ok: + os.makedirs(self._path) + + def read_text(self, encoding="utf-8"): + with open(self._path, "rb") as file_handler: + return file_handler.read().decode(encoding) + + def write_text(self, text, encoding="utf-8"): + with open(self._path, "wb") as file_handler: + file_handler.write(text.encode(encoding)) + + def iterdir(self): + for p in os.listdir(self._path): + yield Path(os.path.join(self._path, p)) + + @property + def suffix(self): + _, ext = os.path.splitext(self.name) + return ext + + @property + def stem(self): + base, _ = os.path.splitext(self.name) + return base + + @contextmanager + def open(self, mode="r"): + with open(self._path, mode) as file_handler: + yield file_handler + + @property + def parents(self): + result = [] + parts = self.parts + for i in range(len(parts)): + result.append(Path(os.sep.join(parts[0 : i + 1]))) + return result + + def unlink(self): + os.remove(self._path) + + def with_name(self, name): + return self.parent / name + + def is_symlink(self): + return os.path.islink(self._path) + + else: + from pathlib2 import Path +__all__ = ("Path",) diff --git a/src/virtualenv/util/subprocess/__init__.py b/src/virtualenv/util/subprocess/__init__.py new file mode 100644 index 0000000..a980cae --- /dev/null +++ b/src/virtualenv/util/subprocess/__init__.py @@ -0,0 +1,15 @@ +from __future__ import absolute_import, unicode_literals + +import subprocess +import sys + +import six + +if six.PY2 and sys.platform == "win32": + from . import win_subprocess + + Popen = win_subprocess.Popen +else: + Popen = subprocess.Popen + +__all__ = ("subprocess", "Popen") diff --git a/src/virtualenv/util/subprocess/win_subprocess.py b/src/virtualenv/util/subprocess/win_subprocess.py new file mode 100644 index 0000000..e8fdaf0 --- /dev/null +++ b/src/virtualenv/util/subprocess/win_subprocess.py @@ -0,0 +1,153 @@ +# flake8: noqa +# fmt: off +## issue: https://bugs.python.org/issue19264 + +import ctypes +import os +import subprocess +import sys +from ctypes import Structure, WinError, byref, c_char_p, c_void_p, c_wchar, c_wchar_p, sizeof, windll +from ctypes.wintypes import BOOL, BYTE, DWORD, HANDLE, LPVOID, LPWSTR, WORD + +import _subprocess + +## +## Types +## + +CREATE_UNICODE_ENVIRONMENT = 0x00000400 +LPCTSTR = c_char_p +LPTSTR = c_wchar_p +LPSECURITY_ATTRIBUTES = c_void_p +LPBYTE = ctypes.POINTER(BYTE) + +class STARTUPINFOW(Structure): + _fields_ = [ + ("cb", DWORD), ("lpReserved", LPWSTR), + ("lpDesktop", LPWSTR), ("lpTitle", LPWSTR), + ("dwX", DWORD), ("dwY", DWORD), + ("dwXSize", DWORD), ("dwYSize", DWORD), + ("dwXCountChars", DWORD), ("dwYCountChars", DWORD), + ("dwFillAtrribute", DWORD), ("dwFlags", DWORD), + ("wShowWindow", WORD), ("cbReserved2", WORD), + ("lpReserved2", LPBYTE), ("hStdInput", HANDLE), + ("hStdOutput", HANDLE), ("hStdError", HANDLE), + ] + +LPSTARTUPINFOW = ctypes.POINTER(STARTUPINFOW) + + +class PROCESS_INFORMATION(Structure): + _fields_ = [ + ("hProcess", HANDLE), ("hThread", HANDLE), + ("dwProcessId", DWORD), ("dwThreadId", DWORD), + ] + +LPPROCESS_INFORMATION = ctypes.POINTER(PROCESS_INFORMATION) + + +class DUMMY_HANDLE(ctypes.c_void_p): + + def __init__(self, *a, **kw): + super(DUMMY_HANDLE, self).__init__(*a, **kw) + self.closed = False + + def Close(self): + if not self.closed: + windll.kernel32.CloseHandle(self) + self.closed = True + + def __int__(self): + return self.value + + +CreateProcessW = windll.kernel32.CreateProcessW +CreateProcessW.argtypes = [ + LPCTSTR, LPTSTR, LPSECURITY_ATTRIBUTES, + LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCTSTR, + LPSTARTUPINFOW, LPPROCESS_INFORMATION, +] +CreateProcessW.restype = BOOL + + +## +## Patched functions/classes +## + +def CreateProcess(executable, args, _p_attr, _t_attr, + inherit_handles, creation_flags, env, cwd, + startup_info): + """Create a process supporting unicode executable and args for win32 + + Python implementation of CreateProcess using CreateProcessW for Win32 + + """ + + si = STARTUPINFOW( + dwFlags=startup_info.dwFlags, + wShowWindow=startup_info.wShowWindow, + cb=sizeof(STARTUPINFOW), + ## XXXvlab: not sure of the casting here to ints. + hStdInput=startup_info.hStdInput if startup_info.hStdInput is None else int(startup_info.hStdInput), + hStdOutput=startup_info.hStdOutput if startup_info.hStdOutput is None else int(startup_info.hStdOutput), + hStdError=startup_info.hStdError if startup_info.hStdError is None else int(startup_info.hStdError), + ) + + wenv = None + if env is not None: + ## LPCWSTR seems to be c_wchar_p, so let's say CWSTR is c_wchar + env = (unicode("").join([ + unicode("%s=%s\0") % (k, v) + for k, v in env.items()])) + unicode("\0") + wenv = (c_wchar * len(env))() + wenv.value = env + + pi = PROCESS_INFORMATION() + creation_flags |= CREATE_UNICODE_ENVIRONMENT + + if CreateProcessW(executable, args, None, None, + inherit_handles, creation_flags, + wenv, cwd, byref(si), byref(pi)): + return (DUMMY_HANDLE(pi.hProcess), DUMMY_HANDLE(pi.hThread), + pi.dwProcessId, pi.dwThreadId) + raise WinError() + + +class Popen(subprocess.Popen): + """This superseeds Popen and corrects a bug in cPython 2.7 implem""" + + def _execute_child(self, args, executable, preexec_fn, close_fds, + cwd, env, universal_newlines, + startupinfo, creationflags, shell, to_close, + p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite): + """Code from part of _execute_child from Python 2.7 (9fbb65e) + + There are only 2 little changes concerning the construction of + the the final string in shell mode: we preempt the creation of + the command string when shell is True, because original function + will try to encode unicode args which we want to avoid to be able to + sending it as-is to ``CreateProcess``. + + """ + if startupinfo is None: + startupinfo = subprocess.STARTUPINFO() + if not isinstance(args, subprocess.types.StringTypes): + args = subprocess.list2cmdline(args) + startupinfo.dwFlags |= _subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = _subprocess.SW_HIDE + comspec = os.environ.get("COMSPEC", unicode("cmd.exe")) + if (_subprocess.GetVersion() >= 0x80000000 or + os.path.basename(comspec).lower() == "command.com"): + w9xpopen = self._find_w9xpopen() + args = unicode('"%s" %s') % (w9xpopen, args) + creationflags |= _subprocess.CREATE_NEW_CONSOLE + + super(Popen, self)._execute_child(args, executable, + preexec_fn, close_fds, cwd, env, universal_newlines, + startupinfo, creationflags, False, to_close, p2cread, + p2cwrite, c2pread, c2pwrite, errread, errwrite) + +_subprocess.CreateProcess = CreateProcess +# fmt: on |
