From 19fc7bca28642b80d0a0ed193f47da3a9e96d9b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bern=C3=A1t=20G=C3=A1bor?= Date: Sun, 3 May 2020 13:42:45 +0100 Subject: [PATCH] Fix python 2 activator when generated from python 3 is invalid (#1805) --- docs/changelog/1776.bugfix.rst | 1 + src/virtualenv/activation/python/__init__.py | 4 ++- src/virtualenv/activation/via_template.py | 6 ++++- tests/conftest.py | 13 ++++++++++ tests/unit/activation/test_activate_this.py | 26 ++++++++++++++++++++ tests/unit/activation/test_fish.py | 7 +++++- tests/unit/create/test_creator.py | 13 ---------- tox.ini | 2 +- 8 files changed, 55 insertions(+), 17 deletions(-) create mode 100644 docs/changelog/1776.bugfix.rst create mode 100644 tests/unit/activation/test_activate_this.py diff --git a/docs/changelog/1776.bugfix.rst b/docs/changelog/1776.bugfix.rst new file mode 100644 index 000000000..493085579 --- /dev/null +++ b/docs/changelog/1776.bugfix.rst @@ -0,0 +1 @@ +Fix generating a Python 2 environment from Python 3 creates invalid python activator - by :user:`gaborbernat`. diff --git a/src/virtualenv/activation/python/__init__.py b/src/virtualenv/activation/python/__init__.py index d37432e8e..43e908c86 100644 --- a/src/virtualenv/activation/python/__init__.py +++ b/src/virtualenv/activation/python/__init__.py @@ -1,6 +1,7 @@ from __future__ import absolute_import, unicode_literals import os +import sys from collections import OrderedDict from virtualenv.util.path import Path @@ -29,5 +30,6 @@ def replacements(self, creator, dest_folder): def _repr_unicode(creator, value): py2 = creator.interpreter.version_info.major == 2 if py2: # on Python 2 we need to encode this into explicit utf-8, py3 supports unicode literals - value = ensure_text(repr(value.encode("utf-8"))[1:-1]) + start = 2 if sys.version_info[0] == 3 else 1 + value = ensure_text(repr(value.encode("utf-8"))[start:-1]) return value diff --git a/src/virtualenv/activation/via_template.py b/src/virtualenv/activation/via_template.py index 651ed8fe9..7a9d3c8e6 100644 --- a/src/virtualenv/activation/via_template.py +++ b/src/virtualenv/activation/via_template.py @@ -25,9 +25,10 @@ def templates(self): def generate(self, creator): dest_folder = creator.bin_dir replacements = self.replacements(creator, dest_folder) - self._generate(replacements, self.templates(), dest_folder, creator) + generated = self._generate(replacements, self.templates(), dest_folder, creator) if self.flag_prompt is not None: creator.pyenv_cfg["prompt"] = self.flag_prompt + return generated def replacements(self, creator, dest_folder): return { @@ -39,10 +40,13 @@ def replacements(self, creator, dest_folder): } def _generate(self, replacements, templates, to_folder, creator): + generated = [] for template in templates: text = self.instantiate_template(replacements, template, creator) dest = to_folder / self.as_name(template) dest.write_text(text, encoding="utf-8") + generated.append(dest) + return generated def as_name(self, template): return template.name diff --git a/tests/conftest.py b/tests/conftest.py index 6fb71f4e5..a92b893b1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,6 +12,7 @@ import pytest import six +from virtualenv.discovery.builtin import get_interpreter from virtualenv.discovery.py_info import PythonInfo from virtualenv.info import IS_PYPY, IS_WIN, fs_supports_symlink from virtualenv.report import LOGGER @@ -318,3 +319,15 @@ def temp_app_data(monkeypatch, tmp_path): app_data = tmp_path / "app-data" monkeypatch.setenv(str("VIRTUALENV_OVERRIDE_APP_DATA"), str(app_data)) return app_data + + +@pytest.fixture(scope="session") +def cross_python(is_inside_ci, session_app_data): + spec = str(2 if sys.version_info[0] == 3 else 3) + interpreter = get_interpreter(spec, session_app_data) + if interpreter is None: + msg = "could not find {}".format(spec) + if is_inside_ci: + raise RuntimeError(msg) + pytest.skip(msg=msg) + yield interpreter diff --git a/tests/unit/activation/test_activate_this.py b/tests/unit/activation/test_activate_this.py new file mode 100644 index 000000000..9446a4c48 --- /dev/null +++ b/tests/unit/activation/test_activate_this.py @@ -0,0 +1,26 @@ +from virtualenv.activation import PythonActivator +from virtualenv.config.cli.parser import VirtualEnvOptions +from virtualenv.run import session_via_cli + + +def test_python_activator_cross(session_app_data, cross_python, special_name_dir): + options = VirtualEnvOptions() + cli_args = [ + str(special_name_dir), + "-p", + str(cross_python.executable), + "--app-data", + str(session_app_data.path), + "--without-pip", + "--activators", + "", + ] + session = session_via_cli(cli_args, options) + activator = PythonActivator(options) + session.creator.bin_dir.mkdir(parents=True) + results = activator.generate(session.creator) + assert len(results) == 1 + result = results[0] + content = result.read_text() + # check that the repr strings have been correctly stripped + assert "\"'" not in content diff --git a/tests/unit/activation/test_fish.py b/tests/unit/activation/test_fish.py index 4e55415d0..8604ff990 100644 --- a/tests/unit/activation/test_fish.py +++ b/tests/unit/activation/test_fish.py @@ -7,7 +7,12 @@ @pytest.mark.skipif(IS_WIN, reason="we have not setup fish in CI yet") -def test_fish(activation_tester_class, activation_tester): +def test_fish(activation_tester_class, activation_tester, monkeypatch, tmp_path): + monkeypatch.setenv(str("HOME"), str(tmp_path)) + fish_conf_dir = tmp_path / ".config" / "fish" + fish_conf_dir.mkdir(parents=True) + (fish_conf_dir / "config.fish").write_text("") + class Fish(activation_tester_class): def __init__(self, session): super(Fish, self).__init__(FishActivator, session, "fish", "activate.fish", "fish") diff --git a/tests/unit/create/test_creator.py b/tests/unit/create/test_creator.py index 01944ad46..fa4154492 100644 --- a/tests/unit/create/test_creator.py +++ b/tests/unit/create/test_creator.py @@ -23,7 +23,6 @@ from virtualenv.create.via_global_ref.builtin.cpython.cpython2 import CPython2PosixBase from virtualenv.create.via_global_ref.builtin.cpython.cpython3 import CPython3Posix from virtualenv.create.via_global_ref.builtin.python2.python2 import Python2 -from virtualenv.discovery.builtin import get_interpreter from virtualenv.discovery.py_info import PythonInfo from virtualenv.info import IS_PYPY, IS_WIN, PY2, PY3, fs_is_case_sensitive from virtualenv.pyenv_cfg import PyEnvCfg @@ -315,18 +314,6 @@ def test_prompt_set(tmp_path, creator, prompt): assert cfg["prompt"] == actual_prompt -@pytest.fixture(scope="session") -def cross_python(is_inside_ci, session_app_data): - spec = "{}{}".format(CURRENT.implementation, 2 if CURRENT.version_info.major == 3 else 3) - interpreter = get_interpreter(spec, session_app_data) - if interpreter is None: - msg = "could not find {}".format(spec) - if is_inside_ci: - raise RuntimeError(msg) - pytest.skip(msg=msg) - yield interpreter - - @pytest.mark.slow def test_cross_major(cross_python, coverage_env, tmp_path, session_app_data, current_fastest): cmd = [ diff --git a/tox.ini b/tox.ini index b58a3e0ed..91e51ba7d 100644 --- a/tox.ini +++ b/tox.ini @@ -33,7 +33,7 @@ commands = coverage erase coverage run -m pytest \ --junitxml {toxworkdir}/junit.{envname}.xml \ - tests {posargs:--int --timeout 600 -n {env:PYTEST_XDIST:0}} + tests {posargs:--int --timeout 600 -n {env:PYTEST_XDIST:auto}} coverage combine coverage report