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):