├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── changelog.md ├── clonevirtualenv.py ├── setup.py ├── tests ├── __init__.py ├── test_dirmatch.py ├── test_fixup_scripts.py ├── test_virtualenv_clone.py └── test_virtualenv_sys.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.pyc 3 | build/ 4 | dist/ 5 | virtualenv_clone.egg-info/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | - "3.5" 6 | - "3.6" 7 | - "3.7" 8 | - "3.8" 9 | - "3.9" 10 | - "3.10" 11 | - "3.11" 12 | - "3.12" 13 | dist: xenial 14 | sudo: true 15 | 16 | install: 17 | - pip install pytest virtualenv 18 | script: 19 | - py.test -v tests 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Edward George, based on code contained within the 2 | virtualenv project. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include tox.ini 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | virtualenv cloning script. 2 | 3 | [![Build Status](https://travis-ci.org/edwardgeorge/virtualenv-clone.svg?branch=master)](https://travis-ci.org/edwardgeorge/virtualenv-clone) 4 | 5 | A script for cloning a non-relocatable virtualenv. 6 | 7 | Virtualenv provides a way to make virtualenv's relocatable which could then be 8 | copied as we wanted. However making a virtualenv relocatable this way breaks 9 | the no-site-packages isolation of the virtualenv as well as other aspects that 10 | come with relative paths and `/usr/bin/env` shebangs that may be undesirable. 11 | 12 | Also, the .pth and .egg-link rewriting doesn't seem to work as intended. This 13 | attempts to overcome these issues and provide a way to easily clone an 14 | existing virtualenv. 15 | 16 | It performs the following: 17 | 18 | - copies `sys.argv[1]` dir to `sys.argv[2]` 19 | - updates the hardcoded `VIRTUAL_ENV` variable in the activate script to the 20 | new repo location. (`--relocatable` doesn't touch this) 21 | - updates the shebangs of the various scripts in bin to the new Python if 22 | they pointed to the old Python. (version numbering is retained.) 23 | 24 | it can also change `/usr/bin/env python` shebangs to be absolute too, 25 | though this functionality is not exposed at present. 26 | 27 | - checks `sys.path` of the cloned virtualenv and if any of the paths are from 28 | the old environment it finds any `.pth` or `.egg` link files within sys.path 29 | located in the new environment and makes sure any absolute paths to the 30 | old environment are updated to the new environment. 31 | 32 | - finally it double checks `sys.path` again and will fail if there are still 33 | paths from the old environment present. 34 | 35 | NOTE: This script requires Python 2.7 or 3.4+ 36 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## version 0.5.7 4 | 5 | - Added tox.ini to source distributions. 6 | 7 | ## version 0.5.6 8 | 9 | - Added support for Python 3.10. 10 | 11 | ## version 0.5.5 12 | 13 | - Added support for Python 3.9. 14 | 15 | ## version 0.5.4 16 | 17 | - Added support for Python 3.8. 18 | 19 | ## version 0.5.3 20 | 21 | - Fixed a minor bug in the package MANIFEST file. 22 | 23 | ## version 0.5.2 24 | 25 | - Fixed minor Python3 typing issue. 26 | 27 | ## version 0.5.1 28 | 29 | - Fixed incorrect Python __version__ value. 30 | 31 | ## version 0.5.0 32 | 33 | - Added support for Python 3.7. 34 | 35 | ## version 0.4.0 36 | 37 | - Dropped support for EOL Python versions that no longer 38 | receive any updates from the core CPython team. 39 | 40 | ## version 0.3.0 41 | 42 | - Added support for Python 3. 43 | - Fixed a bug with writing unicode in .pth files incorrectly. 44 | - Fixed support for paths involving symbolic links. 45 | -------------------------------------------------------------------------------- /clonevirtualenv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import with_statement 3 | 4 | import logging 5 | import optparse 6 | import os 7 | import os.path 8 | import re 9 | import shutil 10 | import subprocess 11 | import sys 12 | import itertools 13 | 14 | __version__ = '0.5.7' 15 | 16 | 17 | logger = logging.getLogger() 18 | 19 | 20 | env_bin_dir = 'bin' 21 | if sys.platform == 'win32': 22 | env_bin_dir = 'Scripts' 23 | 24 | 25 | class UserError(Exception): 26 | pass 27 | 28 | 29 | def _dirmatch(path, matchwith): 30 | """Check if path is within matchwith's tree. 31 | 32 | >>> _dirmatch('/home/foo/bar', '/home/foo/bar') 33 | True 34 | >>> _dirmatch('/home/foo/bar/', '/home/foo/bar') 35 | True 36 | >>> _dirmatch('/home/foo/bar/etc', '/home/foo/bar') 37 | True 38 | >>> _dirmatch('/home/foo/bar2', '/home/foo/bar') 39 | False 40 | >>> _dirmatch('/home/foo/bar2/etc', '/home/foo/bar') 41 | False 42 | """ 43 | matchlen = len(matchwith) 44 | if (path.startswith(matchwith) 45 | and path[matchlen:matchlen + 1] in [os.sep, '']): 46 | return True 47 | return False 48 | 49 | 50 | def _virtualenv_sys(venv_path): 51 | "obtain version and path info from a virtualenv." 52 | executable = os.path.join(venv_path, env_bin_dir, 'python') 53 | # Must use "executable" as the first argument rather than as the 54 | # keyword argument "executable" to get correct value from sys.path 55 | p = subprocess.Popen([executable, 56 | '-c', 'import sys;' 57 | 'print ("%d.%d" % (sys.version_info.major, sys.version_info.minor));' 58 | 'print ("\\n".join(sys.path));'], 59 | env={}, 60 | stdout=subprocess.PIPE) 61 | stdout, err = p.communicate() 62 | assert not p.returncode and stdout 63 | lines = stdout.decode('utf-8').splitlines() 64 | return lines[0], list(filter(bool, lines[1:])) 65 | 66 | 67 | def clone_virtualenv(src_dir, dst_dir): 68 | if not os.path.exists(src_dir): 69 | raise UserError('src dir %r does not exist' % src_dir) 70 | if os.path.exists(dst_dir): 71 | raise UserError('dest dir %r exists' % dst_dir) 72 | #sys_path = _virtualenv_syspath(src_dir) 73 | logger.info('cloning virtualenv \'%s\' => \'%s\'...' % 74 | (src_dir, dst_dir)) 75 | shutil.copytree(src_dir, dst_dir, symlinks=True, 76 | ignore=shutil.ignore_patterns('*.pyc')) 77 | version, sys_path = _virtualenv_sys(dst_dir) 78 | logger.info('fixing scripts in bin...') 79 | fixup_scripts(src_dir, dst_dir, version) 80 | 81 | has_old = lambda s: any(i for i in s if _dirmatch(i, src_dir)) 82 | 83 | if has_old(sys_path): 84 | # only need to fix stuff in sys.path if we have old 85 | # paths in the sys.path of new python env. right? 86 | logger.info('fixing paths in sys.path...') 87 | fixup_syspath_items(sys_path, src_dir, dst_dir) 88 | v_sys = _virtualenv_sys(dst_dir) 89 | remaining = has_old(v_sys[1]) 90 | assert not remaining, v_sys 91 | fix_symlink_if_necessary(src_dir, dst_dir) 92 | 93 | def fix_symlink_if_necessary(src_dir, dst_dir): 94 | #sometimes the source virtual environment has symlinks that point to itself 95 | #one example is $OLD_VIRTUAL_ENV/local/lib points to $OLD_VIRTUAL_ENV/lib 96 | #this function makes sure 97 | #$NEW_VIRTUAL_ENV/local/lib will point to $NEW_VIRTUAL_ENV/lib 98 | #usually this goes unnoticed unless one tries to upgrade a package though pip, so this bug is hard to find. 99 | logger.info("scanning for internal symlinks that point to the original virtual env") 100 | for dirpath, dirnames, filenames in os.walk(dst_dir): 101 | for a_file in itertools.chain(filenames, dirnames): 102 | full_file_path = os.path.join(dirpath, a_file) 103 | if os.path.islink(full_file_path): 104 | target = os.path.realpath(full_file_path) 105 | if target.startswith(src_dir): 106 | new_target = target.replace(src_dir, dst_dir) 107 | logger.debug('fixing symlink in %s' % (full_file_path,)) 108 | os.remove(full_file_path) 109 | os.symlink(new_target, full_file_path) 110 | 111 | 112 | def fixup_scripts(old_dir, new_dir, version, rewrite_env_python=False): 113 | bin_dir = os.path.join(new_dir, env_bin_dir) 114 | root, dirs, files = next(os.walk(bin_dir)) 115 | pybinre = re.compile(r'pythonw?([0-9]+(\.[0-9]+(\.[0-9]+)?)?)?$') 116 | for file_ in files: 117 | filename = os.path.join(root, file_) 118 | if file_ in ['python', 'python%s' % version, 'activate_this.py']: 119 | continue 120 | elif file_.startswith('python') and pybinre.match(file_): 121 | # ignore other possible python binaries 122 | continue 123 | elif file_.endswith('.pyc'): 124 | # ignore compiled files 125 | continue 126 | elif file_ == 'activate' or file_.startswith('activate.'): 127 | fixup_activate(os.path.join(root, file_), old_dir, new_dir) 128 | elif os.path.islink(filename): 129 | fixup_link(filename, old_dir, new_dir) 130 | elif os.path.isfile(filename): 131 | fixup_script_(root, file_, old_dir, new_dir, version, 132 | rewrite_env_python=rewrite_env_python) 133 | 134 | 135 | def fixup_script_(root, file_, old_dir, new_dir, version, 136 | rewrite_env_python=False): 137 | old_shebang = '#!%s/bin/python' % os.path.normcase(os.path.abspath(old_dir)) 138 | new_shebang = '#!%s/bin/python' % os.path.normcase(os.path.abspath(new_dir)) 139 | env_shebang = '#!/usr/bin/env python' 140 | 141 | filename = os.path.join(root, file_) 142 | with open(filename, 'rb') as f: 143 | if f.read(2) != b'#!': 144 | # no shebang 145 | return 146 | f.seek(0) 147 | lines = f.readlines() 148 | 149 | if not lines: 150 | # warn: empty script 151 | return 152 | 153 | def rewrite_shebang(version=None): 154 | logger.debug('fixing %s' % filename) 155 | shebang = new_shebang 156 | if version: 157 | shebang = shebang + version 158 | shebang = (shebang + '\n').encode('utf-8') 159 | with open(filename, 'wb') as f: 160 | f.write(shebang) 161 | f.writelines(lines[1:]) 162 | 163 | try: 164 | bang = lines[0].decode('utf-8').strip() 165 | except UnicodeDecodeError: 166 | # binary file 167 | return 168 | 169 | # This takes care of the scheme in which shebang is of type 170 | # '#!/venv/bin/python3' while the version of system python 171 | # is of type 3.x e.g. 3.5. 172 | short_version = bang[len(old_shebang):] 173 | 174 | if not bang.startswith('#!'): 175 | return 176 | elif bang == old_shebang: 177 | rewrite_shebang() 178 | elif (bang.startswith(old_shebang) 179 | and bang[len(old_shebang):] == version): 180 | rewrite_shebang(version) 181 | elif (bang.startswith(old_shebang) 182 | and short_version 183 | and bang[len(old_shebang):] == short_version): 184 | rewrite_shebang(short_version) 185 | elif rewrite_env_python and bang.startswith(env_shebang): 186 | if bang == env_shebang: 187 | rewrite_shebang() 188 | elif bang[len(env_shebang):] == version: 189 | rewrite_shebang(version) 190 | else: 191 | # can't do anything 192 | return 193 | 194 | 195 | def fixup_activate(filename, old_dir, new_dir): 196 | logger.debug('fixing %s' % filename) 197 | with open(filename, 'rb') as f: 198 | data = f.read().decode('utf-8') 199 | 200 | data = data.replace(old_dir, new_dir) 201 | with open(filename, 'wb') as f: 202 | f.write(data.encode('utf-8')) 203 | 204 | 205 | def fixup_link(filename, old_dir, new_dir, target=None): 206 | logger.debug('fixing %s' % filename) 207 | if target is None: 208 | target = os.readlink(filename) 209 | 210 | origdir = os.path.dirname(os.path.abspath(filename)).replace( 211 | new_dir, old_dir) 212 | if not os.path.isabs(target): 213 | target = os.path.abspath(os.path.join(origdir, target)) 214 | rellink = True 215 | else: 216 | rellink = False 217 | 218 | if _dirmatch(target, old_dir): 219 | if rellink: 220 | # keep relative links, but don't keep original in case it 221 | # traversed up out of, then back into the venv. 222 | # so, recreate a relative link from absolute. 223 | target = target[len(origdir):].lstrip(os.sep) 224 | else: 225 | target = target.replace(old_dir, new_dir, 1) 226 | 227 | # else: links outside the venv, replaced with absolute path to target. 228 | _replace_symlink(filename, target) 229 | 230 | 231 | def _replace_symlink(filename, newtarget): 232 | tmpfn = "%s.new" % filename 233 | os.symlink(newtarget, tmpfn) 234 | os.rename(tmpfn, filename) 235 | 236 | 237 | def fixup_syspath_items(syspath, old_dir, new_dir): 238 | for path in syspath: 239 | if not os.path.isdir(path): 240 | continue 241 | path = os.path.normcase(os.path.abspath(path)) 242 | if _dirmatch(path, old_dir): 243 | path = path.replace(old_dir, new_dir, 1) 244 | if not os.path.exists(path): 245 | continue 246 | elif not _dirmatch(path, new_dir): 247 | continue 248 | root, dirs, files = next(os.walk(path)) 249 | for file_ in files: 250 | filename = os.path.join(root, file_) 251 | if filename.endswith('.pth'): 252 | fixup_pth_file(filename, old_dir, new_dir) 253 | elif filename.endswith('.egg-link'): 254 | fixup_egglink_file(filename, old_dir, new_dir) 255 | 256 | 257 | def fixup_pth_file(filename, old_dir, new_dir): 258 | logger.debug('fixup_pth_file %s' % filename) 259 | 260 | with open(filename, 'r') as f: 261 | lines = f.readlines() 262 | 263 | has_change = False 264 | 265 | for num, line in enumerate(lines): 266 | line = (line.decode('utf-8') if hasattr(line, 'decode') else line).strip() 267 | 268 | if not line or line.startswith('#') or line.startswith('import '): 269 | continue 270 | elif _dirmatch(line, old_dir): 271 | lines[num] = line.replace(old_dir, new_dir, 1) 272 | has_change = True 273 | 274 | if has_change: 275 | with open(filename, 'w') as f: 276 | payload = os.linesep.join([l.strip() for l in lines]) + os.linesep 277 | f.write(payload) 278 | 279 | 280 | def fixup_egglink_file(filename, old_dir, new_dir): 281 | logger.debug('fixing %s' % filename) 282 | with open(filename, 'rb') as f: 283 | link = f.read().decode('utf-8').strip() 284 | if _dirmatch(link, old_dir): 285 | link = link.replace(old_dir, new_dir, 1) 286 | with open(filename, 'wb') as f: 287 | link = (link + '\n').encode('utf-8') 288 | f.write(link) 289 | 290 | 291 | def main(): 292 | parser = optparse.OptionParser("usage: %prog [options]" 293 | " /path/to/existing/venv /path/to/cloned/venv") 294 | parser.add_option('-v', 295 | action="count", 296 | dest='verbose', 297 | default=False, 298 | help='verbosity') 299 | options, args = parser.parse_args() 300 | try: 301 | old_dir, new_dir = args 302 | except ValueError: 303 | print("virtualenv-clone %s" % (__version__,)) 304 | parser.error("not enough arguments given.") 305 | old_dir = os.path.realpath(old_dir) 306 | new_dir = os.path.realpath(new_dir) 307 | loglevel = (logging.WARNING, logging.INFO, logging.DEBUG)[min(2, 308 | options.verbose)] 309 | logging.basicConfig(level=loglevel, format='%(message)s') 310 | try: 311 | clone_virtualenv(old_dir, new_dir) 312 | except UserError: 313 | e = sys.exc_info()[1] 314 | parser.error(str(e)) 315 | 316 | 317 | if __name__ == '__main__': 318 | main() 319 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from setuptools.command.test import test as TestCommand 3 | from setuptools import setup 4 | 5 | 6 | if __name__ == '__main__' and sys.version_info < (2, 7): 7 | raise SystemExit("Python >= 2.7 required for virtualenv-clone") 8 | 9 | with open('README.md') as f: 10 | long_description = f.read() 11 | 12 | 13 | test_requirements = [ 14 | 'virtualenv', 15 | 'tox', 16 | 'pytest' 17 | ] 18 | 19 | 20 | class ToxTest(TestCommand): 21 | def finalize_options(self): 22 | TestCommand.finalize_options(self) 23 | self.test_args = [] 24 | self.test_suite = True 25 | 26 | def run_tests(self): 27 | import tox 28 | tox.cmdline() 29 | 30 | 31 | setup(name="virtualenv-clone", 32 | version='0.5.7', 33 | description='script to clone virtualenvs.', 34 | long_description=long_description, 35 | long_description_content_type='text/markdown', 36 | author='Edward George', 37 | author_email='edwardgeorge@gmail.com', 38 | url='https://github.com/edwardgeorge/virtualenv-clone', 39 | license="MIT", 40 | py_modules=["clonevirtualenv"], 41 | entry_points={ 42 | 'console_scripts': [ 43 | 'virtualenv-clone=clonevirtualenv:main', 44 | ]}, 45 | python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", 46 | classifiers=[ 47 | "License :: OSI Approved :: MIT License", 48 | "Programming Language :: Python", 49 | "Intended Audience :: Developers", 50 | "Development Status :: 3 - Alpha", 51 | "Programming Language :: Python :: 2", 52 | "Programming Language :: Python :: 2.7", 53 | "Programming Language :: Python :: 3", 54 | "Programming Language :: Python :: 3.4", 55 | "Programming Language :: Python :: 3.5", 56 | "Programming Language :: Python :: 3.6", 57 | "Programming Language :: Python :: 3.7", 58 | "Programming Language :: Python :: 3.8", 59 | "Programming Language :: Python :: 3.9", 60 | "Programming Language :: Python :: 3.10", 61 | "Programming Language :: Python :: 3.11", 62 | "Programming Language :: Python :: 3.12", 63 | ], 64 | tests_require=test_requirements, 65 | cmdclass={'test': ToxTest} 66 | ) 67 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import subprocess 4 | import tempfile 5 | from unittest import TestCase 6 | 7 | # Global test variables 8 | tmplocation = tempfile.mkdtemp() 9 | venv_path = os.path.realpath(os.path.join(tmplocation,'srs_venv')) 10 | clone_path = os.path.realpath(os.path.join(tmplocation,'clone_venv')) 11 | versions = ['2.7', '3.4', '3.5', '3.6', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] 12 | 13 | def clean(): 14 | if os.path.exists(tmplocation): shutil.rmtree(tmplocation) 15 | 16 | 17 | class TestBase(TestCase): 18 | 19 | def setUp(self): 20 | """Clean from previous testing""" 21 | clean() 22 | 23 | """Create a virtualenv to clone""" 24 | assert subprocess.call(['virtualenv', venv_path]) == 0,\ 25 | "Error running virtualenv" 26 | 27 | # verify starting point... 28 | assert os.path.exists(venv_path), 'Virtualenv to clone does not exists' 29 | 30 | def tearDown(self): 31 | """Clean up our testing""" 32 | clean() 33 | -------------------------------------------------------------------------------- /tests/test_dirmatch.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | import clonevirtualenv 3 | 4 | class TestVirtualenvDirmatch(TestCase): 5 | 6 | def test_dirmatch(self): 7 | assert clonevirtualenv._dirmatch('/home/foo/bar', '/home/foo/bar') 8 | assert clonevirtualenv._dirmatch('/home/foo/bar/', '/home/foo/bar') 9 | assert clonevirtualenv._dirmatch('/home/foo/bar/etc', '/home/foo/bar') 10 | assert clonevirtualenv._dirmatch('/home/foo/bar/etc/', '/home/foo/bar') 11 | assert not clonevirtualenv._dirmatch('/home/foo/bar2', '/home/foo/bar') 12 | assert not clonevirtualenv._dirmatch('/home/foo/bar2/etc', '/home/foo/bar') 13 | assert not clonevirtualenv._dirmatch('/home/foo/bar/', '/home/foo/bar/etc') 14 | assert not clonevirtualenv._dirmatch('/home/foo/bar', '/home/foo/bar/etc') 15 | -------------------------------------------------------------------------------- /tests/test_fixup_scripts.py: -------------------------------------------------------------------------------- 1 | from tests import venv_path, clone_path, TestBase, tmplocation 2 | import os 3 | import clonevirtualenv 4 | import sys 5 | import time 6 | import stat 7 | 8 | """ 9 | Fixup scripts meerly just walks bin dir and calls to 10 | fixup_link, fixup_activate, fixup_script_ 11 | """ 12 | 13 | 14 | class TestFixupScripts(TestBase): 15 | 16 | def test_fixup_activate(self): 17 | file_ = os.path.join(venv_path, 'bin', 'activate') 18 | with open(file_, 'rb') as f: 19 | data = f.read().decode('utf-8') 20 | 21 | # verify initial state 22 | assert venv_path in data 23 | assert clone_path not in data 24 | assert os.path.basename(clone_path) not in data 25 | clonevirtualenv.fixup_activate(file_, venv_path, clone_path) 26 | 27 | with open(file_, 'rb') as f: 28 | data = f.read().decode('utf-8') 29 | 30 | # verify changes 31 | assert venv_path not in data 32 | assert os.path.basename(venv_path) not in data 33 | assert clone_path in data 34 | 35 | def test_fixup_abs_link(self): 36 | link = os.path.join(tmplocation, 'symlink') 37 | assert os.path.isabs(link), 'Expected abs path link for test' 38 | 39 | os.symlink('original_location', link) 40 | assert os.readlink(link) == 'original_location' 41 | 42 | clonevirtualenv._replace_symlink(link, 'new_location') 43 | assert link == os.path.join(tmplocation, 'symlink') 44 | assert os.readlink(link) == 'new_location' 45 | 46 | def test_fixup_rel_link(self): 47 | link = os.path.relpath(os.path.join(tmplocation, 'symlink')) 48 | assert not os.path.isabs(link),\ 49 | 'Expected relative path link for test' 50 | 51 | os.symlink('original_location', link) 52 | assert os.readlink(link) == 'original_location' 53 | 54 | clonevirtualenv._replace_symlink(link, 'new_location') 55 | assert link == os.path.relpath(os.path.join(tmplocation, 'symlink')) 56 | assert os.readlink(link) == 'new_location' 57 | 58 | def test_fixup_script_(self): 59 | 60 | script = '''#!%s/bin/python 61 | # EASY-INSTALL-ENTRY-SCRIPT: 'console_scripts' 62 | import sys 63 | 64 | if __name__ == '__main__': 65 | print('Testing VirtualEnv-Clone!') 66 | sys.exit()''' % venv_path 67 | 68 | # want to do this manually, 69 | # so going to call clone before the file exists 70 | # run virtualenv-clone 71 | sys.argv = ['virtualenv-clone', venv_path, clone_path] 72 | clonevirtualenv.main() 73 | 74 | root = os.path.join(clone_path, 'bin') 75 | new_ = os.path.join(clone_path, 'bin', 'clonetest') 76 | # write the file straight to the cloned 77 | with open(new_, 'w') as f: 78 | f.write(script) 79 | 80 | # run fixup 81 | clonevirtualenv.fixup_script_(root, 82 | 'clonetest', venv_path, clone_path, '2.7') 83 | 84 | with open(new_, 'r') as f: 85 | data = f.read() 86 | 87 | assert venv_path not in data 88 | assert clone_path in data 89 | 90 | def test_fixup_script_no_shebang(self): 91 | '''Verify if there is no shebang nothing is altered''' 92 | 93 | script = '''%s/bin/python 94 | # EASY-INSTALL-ENTRY-SCRIPT: 'console_scripts' 95 | import sys 96 | 97 | if __name__ == '__main__': 98 | print('Testing VirtualEnv-Clone!') 99 | sys.exit()''' % venv_path 100 | 101 | # want to do this manually, 102 | # so going to call clone before the file exists 103 | # run virtualenv-clone 104 | sys.argv = ['virtualenv-clone', venv_path, clone_path] 105 | clonevirtualenv.main() 106 | 107 | root = os.path.join(clone_path, 'bin') 108 | new_ = os.path.join(clone_path, 'bin', 'clonetest') 109 | # write the file straight to the cloned 110 | with open(new_, 'w') as f: 111 | f.write(script) 112 | 113 | # run fixup 114 | clonevirtualenv.fixup_script_(root, 115 | 'clonetest', venv_path, clone_path, '2.7') 116 | 117 | with open(new_, 'r') as f: 118 | data = f.read() 119 | 120 | assert venv_path in data 121 | assert clone_path not in data 122 | assert clone_path + '/bin/python2.7' not in data 123 | 124 | def test_fixup_script_version(self): 125 | '''Verify if version is updated''' 126 | 127 | script = '''#!%s/bin/python2.7 128 | # EASY-INSTALL-ENTRY-SCRIPT: 'console_scripts' 129 | import sys 130 | 131 | if __name__ == '__main__': 132 | print('Testing VirtualEnv-Clone!') 133 | sys.exit()''' % venv_path 134 | 135 | # want to do this manually, 136 | # so going to call clone before the file exists 137 | # run virtualenv-clone 138 | sys.argv = ['virtualenv-clone', venv_path, clone_path] 139 | clonevirtualenv.main() 140 | 141 | root = os.path.join(clone_path, 'bin') 142 | new_ = os.path.join(clone_path, 'bin', 'clonetest') 143 | # write the file straight to the cloned 144 | with open(new_, 'w') as f: 145 | f.write(script) 146 | 147 | # run fixup 148 | clonevirtualenv.fixup_script_(root, 149 | 'clonetest', venv_path, clone_path, '2.7') 150 | 151 | with open(new_, 'r') as f: 152 | data = f.read() 153 | 154 | assert venv_path not in data 155 | assert clone_path in data 156 | 157 | assert clone_path + '/bin/python2.7' in data 158 | 159 | def test_fixup_script_different_version(self): 160 | '''Verify if version is updated''' 161 | 162 | script = '''#!%s/bin/python2 163 | # EASY-INSTALL-ENTRY-SCRIPT: 'console_scripts' 164 | import sys 165 | 166 | if __name__ == '__main__': 167 | print('Testing VirtualEnv-Clone!') 168 | sys.exit()''' % venv_path 169 | 170 | # want to do this manually, 171 | # so going to call clone before the file exists 172 | # run virtualenv-clone 173 | sys.argv = ['virtualenv-clone', venv_path, clone_path] 174 | clonevirtualenv.main() 175 | 176 | root = os.path.join(clone_path, 'bin') 177 | new_ = os.path.join(clone_path, 'bin', 'clonetest') 178 | # write the file straight to the cloned 179 | with open(new_, 'w') as f: 180 | f.write(script) 181 | 182 | # run fixup 183 | clonevirtualenv.fixup_script_(root, 184 | 'clonetest', venv_path, clone_path, '2.7') 185 | 186 | with open(new_, 'r') as f: 187 | data = f.read() 188 | 189 | assert venv_path not in data 190 | assert clone_path in data 191 | 192 | assert clone_path + '/bin/python2' in data 193 | 194 | def test_fixup_pth_file(self): 195 | '''Prior to version 0.2.7, the method fixup_pth_file() wrongly wrote unicode files''' 196 | 197 | content = os.linesep.join(('/usr/local/bin/someFile', '/tmp/xyzzy', '/usr/local/someOtherFile', '')) 198 | pth = '/tmp/test_fixup_pth_file.pth' 199 | 200 | with open(pth, 'w') as fd: 201 | fd.write(content) 202 | 203 | # Find size of file - should be plain ASCII/UTF-8... 204 | 205 | org_stat = os.stat(pth) 206 | assert len(content) == org_stat[stat.ST_SIZE] 207 | 208 | # Now sleep for 2 seconds, then call fixup_pth_file(). This should ensure that the stat.ST_MTIME has 209 | # changed if the file has been changed/rewritten 210 | 211 | time.sleep(2) 212 | clonevirtualenv.fixup_pth_file(pth, '/usr/local', '/usr/xyzzy') 213 | new_stat = os.stat(pth) 214 | assert org_stat[stat.ST_MTIME] != new_stat[stat.ST_MTIME] # File should have changed 215 | assert org_stat[stat.ST_SIZE] == new_stat[stat.ST_SIZE] # Substituting local->xyzzy - size should be the same 216 | 217 | os.remove(pth) 218 | -------------------------------------------------------------------------------- /tests/test_virtualenv_clone.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pytest import raises 3 | import clonevirtualenv 4 | import sys 5 | from tests import venv_path, clone_path, TestBase 6 | 7 | 8 | class TestVirtualenvClone(TestBase): 9 | 10 | def test_clone_with_no_args(self): 11 | sys.argv = ['virtualenv-clone'] 12 | 13 | with raises(SystemExit): 14 | clonevirtualenv.main() 15 | 16 | def test_clone_with_1_arg(self): 17 | sys.argv = ['virtualenv-clone', venv_path] 18 | 19 | with raises(SystemExit): 20 | clonevirtualenv.main() 21 | 22 | def test_clone_with_bad_src(self): 23 | sys.argv = ['virtualenv-clone', 24 | os.path.join('this','venv','does','not','exist'), clone_path] 25 | 26 | with raises(SystemExit): 27 | clonevirtualenv.main() 28 | 29 | def test_clone_exists(self): 30 | """Verify a cloned virtualenv exists""" 31 | # run virtualenv-clone 32 | sys.argv = ['virtualenv-clone', venv_path, clone_path] 33 | clonevirtualenv.main() 34 | 35 | # verify cloned venv exists at the path specified 36 | assert os.path.exists(clone_path), 'Cloned Virtualenv does not exists' 37 | 38 | def test_clone_contents(self): 39 | """Walk the virtualenv and verify clonedenv contents""" 40 | 41 | sys.argv = ['virtualenv-clone', venv_path, clone_path] 42 | clonevirtualenv.main() 43 | 44 | version = clonevirtualenv._virtualenv_sys(venv_path)[0] 45 | 46 | for root, dirs, files in os.walk(venv_path): 47 | clone_root = root.replace(venv_path,clone_path) 48 | for dir_ in dirs: 49 | dir_in_clone = os.path.join(clone_root,dir_) 50 | assert os.path.exists(dir_in_clone),\ 51 | 'Directory %s is missing from cloned virtualenv' % dir_ 52 | 53 | for file_ in files: 54 | if file_.endswith('.pyc') or\ 55 | file_.endswith('.exe') or\ 56 | file_.endswith('.egg') or\ 57 | file_.endswith('.zip') or\ 58 | file_ in ['python', 'python%s' % version]: 59 | # binarys fail reading and 60 | # compiled will be recompiled 61 | continue 62 | 63 | file_in_clone = os.path.join(clone_root,file_) 64 | assert os.path.exists(file_in_clone),\ 65 | 'File %s is missing from cloned virtualenv' % file_ 66 | 67 | if os.path.islink(file_in_clone): 68 | target = os.readlink(file_in_clone) 69 | assert venv_path != target 70 | assert venv_path not in target 71 | assert os.path.basename(venv_path) not in target 72 | continue 73 | 74 | with open(file_in_clone, 'rb') as f: 75 | lines = f.read().decode('utf-8') 76 | assert venv_path not in lines,\ 77 | 'Found source path in cloned file %s' % file_in_clone 78 | 79 | # def test_clone_egglink_file(self): 80 | # subprocess(['touch',os.path.join(venv_path,'lib','python','site-packages','mypackage.py']) 81 | -------------------------------------------------------------------------------- /tests/test_virtualenv_sys.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from unittest import TestCase 4 | import clonevirtualenv 5 | import sys 6 | from tests import venv_path, clone_path, versions, clean 7 | 8 | 9 | def start_version_test(): 10 | ran_once = False 11 | for version in versions: 12 | # create a virtualenv 13 | if subprocess.call(['virtualenv', '-p', 'python' + version, 14 | venv_path]) != 0: 15 | continue 16 | 17 | ran_once = True 18 | yield version 19 | 20 | assert ran_once, "All versions were skipped." 21 | 22 | 23 | class TestVirtualenvSys(TestCase): 24 | 25 | def setUp(self): 26 | """Clean from previous testing""" 27 | clean() 28 | 29 | def tearDown(self): 30 | """Clean up our testing""" 31 | clean() 32 | 33 | def test_virtualenv_versions(self): 34 | """Verify version for created virtualenvs""" 35 | for version in start_version_test(): 36 | venv_version = clonevirtualenv._virtualenv_sys(venv_path)[0] 37 | assert version == venv_version, 'Expected version %s' % version 38 | 39 | # clean so next venv can be made 40 | clean() 41 | 42 | def test_virtualenv_syspath(self): 43 | """Verify syspath for created virtualenvs""" 44 | for version in start_version_test(): 45 | sys_path = clonevirtualenv._virtualenv_sys(venv_path)[1] 46 | 47 | paths = [path for path in sys_path] 48 | assert paths, "There should be path information" 49 | 50 | # clean so next venv can be made 51 | clean() 52 | 53 | def test_clone_version(self): 54 | """Verify version for cloned virtualenvs""" 55 | for version in start_version_test(): 56 | # run virtualenv-clone 57 | sys.argv = ['virtualenv-clone', venv_path, clone_path] 58 | clonevirtualenv.main() 59 | 60 | clone_version = clonevirtualenv._virtualenv_sys(clone_path)[0] 61 | assert version == clone_version, 'Expected version %s' % version 62 | 63 | # clean so next venv can be made 64 | clean() 65 | 66 | def test_clone_syspath(self): 67 | """ 68 | Verify syspath for cloned virtualenvs 69 | 70 | This really is a test for fixup_syspath as well 71 | """ 72 | for version in start_version_test(): 73 | # run virtualenv-clone 74 | sys.argv = ['virtualenv-clone', venv_path, clone_path] 75 | clonevirtualenv.main() 76 | 77 | sys_path = clonevirtualenv._virtualenv_sys(clone_path)[1] 78 | assert isinstance(sys_path, list), "Path information needs to be a list" 79 | 80 | paths = [path for path in sys_path] 81 | assert paths, "There should be path information" 82 | 83 | assert venv_path not in paths,\ 84 | "There is reference to the source virtualenv" 85 | 86 | for path in paths: 87 | assert os.path.basename(venv_path) not in path,\ 88 | "There is reference to the source virtualenv:\n%s" % path 89 | 90 | # clean so next venv can be made 91 | clean() 92 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py34,py35,py36,py37,py38,py39,py310,py311,py312 3 | 4 | [testenv] 5 | commands = py.test -v [] 6 | deps = pytest 7 | virtualenv 8 | --------------------------------------------------------------------------------