From 2d2a2755a8db32fe5e504507d8b3f59fd68aa9d7 Mon Sep 17 00:00:00 2001 From: Kevin Conway Date: Sun, 7 Jan 2024 20:03:11 -0600 Subject: [PATCH] Add provisional stdlib venv support This patch modifies the way the bash activate script is handled to work with both virtualenv and the python3 bundled venv. The venv version had a recent change for windows support that caused it to diverge from how virtualenv solved the same cygwin related problems. So there are now two totally different bash activation scripts depending on which tool you use. The venv style puts the literal path text in several location whereas every other script in both venv and virtualenv puts the literal text only once in a variable and then references the variable. To change multiple path strings I had to rewrite the path replacement mechanism for bash to bulk rewrite every line that contains the path rather than a targeted line replacement like before. I'm not sure how much more venv will diverge from virtualenv in the future but this should enable it to work for now. --- .github/workflows/ci.yml | 6 +-- setup.py | 2 +- tests/conftest.py | 35 ++++++++++++ tests/test_virtual_environment.py | 17 ------ tox.ini | 2 +- venvctrl/venv/base.py | 88 ++++++++++++++++++++++++++++++- 6 files changed, 126 insertions(+), 24 deletions(-) create mode 100644 tests/conftest.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad06e60..e779c47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,10 +16,10 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - - name: Setup Python 3.7 + - name: Setup Python 3.8 uses: actions/setup-python@v2 with: - python-version: 3.7 + python-version: 3.8 - name: Cache PyPI uses: actions/cache@v2 with: @@ -45,7 +45,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - pyver: ["3.7", "3.8", "3.9", "3.10", "3.11"] + pyver: ["3.8", "3.9", "3.10", "3.11", "3.12"] fail-fast: true steps: - name: Install deps diff --git a/setup.py b/setup.py index 24c69fe..843da74 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="venvctrl", - version="0.6.0", + version="0.7.0", url="https://github.com/kevinconway/venvctrl", description="API for virtual environments.", author="Kevin Conway", diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..84216b1 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,35 @@ +"""Test fixtures and configuration.""" + +from __future__ import division +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import uuid + +import pytest + +from venvctrl import api + + +def pytest_generate_tests(metafunc): + if "use_stdlib_venv" in metafunc.fixturenames: + metafunc.parametrize("use_stdlib_venv", (True, False)) + + +@pytest.fixture(scope="function") +def random(): + """Get a random UUID.""" + return str(uuid.uuid4()) + + +@pytest.fixture(scope="function") +def venv(random, tmpdir, use_stdlib_venv): + """Get an initialized venv.""" + path = str(tmpdir.join(random)) + v = api.VirtualEnvironment(path) + if not use_stdlib_venv: + v.create() + else: + v._execute("python -m venv {0}".format(path)) + return v diff --git a/tests/test_virtual_environment.py b/tests/test_virtual_environment.py index 8bb0e45..89798c6 100644 --- a/tests/test_virtual_environment.py +++ b/tests/test_virtual_environment.py @@ -7,27 +7,10 @@ import os import subprocess -import uuid - -import pytest from venvctrl import api -@pytest.fixture(scope="function") -def random(): - """Get a random UUID.""" - return str(uuid.uuid4()) - - -@pytest.fixture(scope="function") -def venv(random, tmpdir): - """Get an initialized venv.""" - v = api.VirtualEnvironment(str(tmpdir.join(random))) - v.create() - return v - - def test_create(random, tmpdir): """Test if new virtual environments can be created.""" path = str(tmpdir.join(random)) diff --git a/tox.ini b/tox.ini index 74c0368..281b06d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38,py39,py310,pyt311,pep8,pyflakes +envlist = py38,py39,py310,py311,py312,pep8,pyflakes [testenv] deps= diff --git a/venvctrl/venv/base.py b/venvctrl/venv/base.py index 6eeb116..bb87149 100644 --- a/venvctrl/venv/base.py +++ b/venvctrl/venv/base.py @@ -101,6 +101,33 @@ def writeline(self, line, line_number): tmp_file.close() + def replace(self, old, new): + """Replace old with new in every occurrence. + + Args: + old (str): The original text. + new (str): The new text. + """ + tmp_file = tempfile.TemporaryFile("w+") + try: + + with open(self.path, "r") as file_handle: + + for line in file_handle: + + line = line.replace(old, new) + tmp_file.write(line) + + tmp_file.seek(0) + with open(self.path, "w") as file_handle: + + for new_line in tmp_file: + + file_handle.write(new_line) + finally: + + tmp_file.close() + class VenvDir(VenvPath): @@ -205,7 +232,12 @@ def shebang(self, new_shebang): class ActivateFile(BinFile): - """The virtual environment /bin/activate script.""" + """A common base for all activate scripts. + + Implementations should replace the read_pattern for cases where the path can + be extracted with a regex. More complex use cases should override the + _find_vpath method to perform a search and return the appropriate path. + """ read_pattern = re.compile(r"""^VIRTUAL_ENV=["'](.*)["']$""") @@ -241,6 +273,58 @@ def vpath(self, new_vpath): self.writeline(new_line, line_number) +class ActivateFileBash(ActivateFile): + + """The virtual environment /bin/activate script. + + This version accounts for differences between the virtualenv and venv + activation scripts for bash. + """ + + read_pattern = re.compile(r"""^VIRTUAL_ENV=["'](.*)["']$""") + read_pattern_stdlib_venv = re.compile(r"""^ *export VIRTUAL_ENV=["'](.*)["']$""") + + def _find_vpath(self): + """ + Find the VIRTUAL_ENV path entry. + + Returns: + tuple: A tuple containing the matched line, the old vpath, and the line number where the virtual + path was found. If the virtual path is not found, returns a tuple of three None values. + """ + with open(self.path, "r") as file_handle: + + for count, line in enumerate(file_handle): + + match = self.read_pattern.match(line) + if match: + + return match.group(0), match.group(1), count + match = self.read_pattern_stdlib_venv.match(line) + if match: + + return match.group(0), match.group(1), count + + return None, None, None + + @property + def vpath(self): + """Get the path to the virtual environment.""" + return self._find_vpath()[1] + + @vpath.setter + def vpath(self, new_vpath): + """Change the path to the virtual environment. + + The bash activate file from the standard library venv duplicates the + full path in multiple places instead of only one place like in + virtualenv. To account, this code now does a line by line replacement + of the old path to ensure that it is replaced everywhere. + """ + _, old_vpath, _ = self._find_vpath() + self.replace(old_vpath, new_vpath) + + class ActivateFishFile(ActivateFile): """The virtual environment /bin/activate.fish script.""" @@ -338,7 +422,7 @@ def activates(self): @property def activate_sh(self): """Get the /bin/activate script.""" - return ActivateFile(os.path.join(self.path, "activate")) + return ActivateFileBash(os.path.join(self.path, "activate")) @property def activate_csh(self):