├── .gitignore ├── .travis.yml ├── AUTHORS ├── CHANGES.md ├── LICENSE ├── README.rst ├── example.py ├── setup.cfg ├── setup.py ├── tests.py └── virtualenvapi ├── __init__.py ├── exceptions.py ├── manage.py └── util.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | /build/ 3 | /dist/ 4 | /virtualenv_api.egg-info/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.6" 5 | - "3.7" 6 | - "3.8" 7 | install: 8 | - pip install -U pip six virtualenv 9 | - pip freeze 10 | script: python tests.py 11 | branches: 12 | only: 13 | - master 14 | sudo: false 15 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | * Sam Kingston / sjkingo - https://github.com/sjkingo 2 | * Connor Shearwood / Drakekin - https://github.com/Drakekin 3 | * Stephane Poss / sposs - https://github.com/sposs 4 | * r1s - https://github.com/r1s 5 | * Philippe O. Wagner / philippeowagner - https://github.com/philippeowagner 6 | * Yannik Ammann / yannik-ammann - https://github.com/yannik-ammann 7 | * ColMcp - https://github.com/ColMcp 8 | * Jharrod LaFon / jlafon - https://github.com/jlafon 9 | * Ryan Belgrave / rmb938 - https://github.com/rmb938 10 | * Michal Cyprian / mcyprian - https://github.com/mcyprian 11 | * Eoghan Cunneen / eoghancunneen - https://github.com/eoghancunneen 12 | * Dave Evans / evansde77 - https://github.com/evansde77 13 | * Jeremy Zafran / jzafran - https://github.com/jzafran 14 | * Irvin Lim / irvinlim - https://github.com/irvinlim 15 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## 2.1.18 - 2020-02-03 4 | 5 | * #46: Blacklist env var in subprocess calls to fix bug in MacOS/Homebrew installs (@irvinlim) 6 | * Change build versions to match supported Python versions (@sjkingo) 7 | 8 | ## 2.1.17 - 2018-11-15 9 | 10 | * #40: Fix for test cases failing (@irvinlim) 11 | * #39: Fix for `is_installed` not handling capitilized names properly (@irvinlim) 12 | * Support Python 3.7 (@sjkingo) 13 | 14 | ## 2.1.16 - 2017-03-17 15 | 16 | * Fix bug in test script where temporary directories were not being removed (@sjkingo) 17 | * #35: Don't pass unsupported argument to old versions of pip (@sjkingo) 18 | * Always call pip through the Python interpreter to avoid a too-long shebang error (@sjkingo) 19 | * #26: Fix incorrect subclassing of base test class that was causing extreme test durations (@sjkingo) 20 | 21 | ## 2.1.15 - 2017-03-17 22 | 23 | * #34: Add support for `pip -r` requirements files (@jzafran and @sjkingo) 24 | 25 | ## 2.1.14 - 2017-02-22 26 | 27 | * #33: Add support for `system-site-packages` option (@evansde77) 28 | 29 | ## 2.1.13 - 2016-10-26 30 | 31 | * #31: Workaround to prevent shebang length errors when calling pip (@rmb938) 32 | 33 | ## 2.1.12 - 2016-10-22 34 | 35 | * #29: Fix AttributeError when raising OSError from inside environment (@rmb938) 36 | 37 | ## 2.1.11 - 2016-07-14 38 | 39 | * #28: Add support for locating pip on win32 (@eoghancunneen) 40 | 41 | ## 2.1.10 - 2016-06-03 42 | 43 | * #27: Support installing an editable package (`-e`) (@sjkingo) 44 | 45 | ## 2.1.9 - 2016-05-01 46 | 47 | * Move version number to library instead of `setup.py` (@sjkingo) 48 | * Support Python 3.5 (@sjkingo) 49 | * #24: Fixed failing test suite by adding missing dependency (@mcyprian) 50 | 51 | ## 2.1.8 - 2016-04-01 52 | 53 | * #21: Converted `README.md` to an rST document to remove dependency on 54 | `setuptools-markdown` (@sjkingo) 55 | 56 | ## 2.1.7 - 2015-08-10 57 | 58 | * Added `upgrade_all()` function to upgrade all packages in an environment (@sjkingo) 59 | * Added `readonly` argument to constructing environment that can be used to prevent 60 | operations that could potentially modify the environment (@sjkingo) 61 | * Added support for passing ~ to construct environment (e.g. `~user/venv`) (@sjkingo) 62 | 63 | ## 2.1.6 - 2015-06-10 64 | 65 | * Version bump for broken PyPi release (@sjkingo) 66 | (no new changes to code) 67 | 68 | ## 2.1.5 - 2015-06-10 69 | 70 | * Improved search function that will return more accurate results. This 71 | includes a breaking change where the package list returned by `env.search()` 72 | is now a dictionary. (@sjkingo) 73 | * Prevent pip from checking for new version of itself and polluting the output 74 | of some commands (@sjkingo) 75 | 76 | ## 2.1.4 - 2015-06-04 77 | 78 | * Support for creating a wheel of packages (@rmb938) 79 | 80 | ## 2.1.3 - 2015-03-28 81 | 82 | * Support changing the interpreter path (@jlafon) 83 | * Improve Unicode support in pip search that broke tests (@jlafon) 84 | 85 | ## 2.1.2 - 2014-11-25 86 | 87 | * Added test builds through Travis CI (@sjkingo) 88 | * Fixed default `options` bug introduced in 2.0.1 (@ColMcp, @sposs) 89 | * Updated example.py (@sjkingo) 90 | * Fix typo in logging (@yannik-ammann) 91 | 92 | ## 2.1.1 - 2014-11-19 93 | 94 | * Fix typo that broke from 2.1.0 release (@sjkingo) 95 | 96 | ## 2.1.0 - 2014-11-19 97 | 98 | * Python 3 support (@r1s) 99 | * Unit tests for base functionality (@r1s) 100 | * Better Unicode handling (@r1s) 101 | * Tuple support for specifying package versions (@philippeowagner) 102 | 103 | ## 2.0.1 - 2014-11-14 104 | 105 | * Support passing command line options directly to pip (@sposs) 106 | * Misc. PEP8 fixes (@sposs) 107 | 108 | ## 2.0.0 - 2014-04-09 109 | 110 | * Added pip search functionality 111 | * Re-worked underlying pip processing and error handling 112 | 113 | ## 1.0.0 - 2013-03-27 114 | 115 | * Initial release with basic install/uninstall 116 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2017 Sam Kingston and AUTHORS. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | virtualenv-api - an API for virtualenv 2 | ====================================== 3 | 4 | |Build Status| 5 | |Latest version| 6 | |BSD License| 7 | 8 | `virtualenv`_ is a tool to create isolated Python environments. Unfortunately, 9 | it does not expose a native Python API. This package aims to provide an API in 10 | the form of a wrapper around virtualenv. 11 | 12 | It can be used to create and delete environments and perform package management 13 | inside the environment. 14 | 15 | Full support is provided for all supported versions of Python. 16 | 17 | .. _virtualenv: http://www.virtualenv.org/ 18 | .. |Build Status| image:: https://travis-ci.org/sjkingo/virtualenv-api.svg 19 | :target: https://travis-ci.org/sjkingo/virtualenv-api 20 | .. |Latest version| image:: https://img.shields.io/pypi/v/virtualenv-api.svg 21 | :target: https://pypi.python.org/pypi/virtualenv-api 22 | .. |BSD License| image:: https://img.shields.io/pypi/l/virtualenv-api.svg 23 | :target: https://github.com/sjkingo/virtualenv-api/blob/master/LICENSE 24 | 25 | 26 | Installation 27 | ------------ 28 | 29 | The latest stable release is available on `PyPi`_: 30 | 31 | :: 32 | 33 | $ pip install virtualenv-api 34 | 35 | Please note that the distribution is named ``virtualenv-api``, yet the Python 36 | package is named ``virtualenvapi``. 37 | 38 | Alternatively, you may fetch the latest version from git: 39 | 40 | :: 41 | 42 | $ pip install git+https://github.com/sjkingo/virtualenv-api.git 43 | 44 | .. _PyPi: https://pypi.python.org/pypi/virtualenv-api 45 | 46 | Usage 47 | ----- 48 | 49 | To begin managing an environment (it will be created if it does not exist): 50 | 51 | .. code:: python 52 | 53 | from virtualenvapi.manage import VirtualEnvironment 54 | env = VirtualEnvironment('/path/to/environment/name') 55 | 56 | If you have already activated a virtualenv and wish to operate on it, simply 57 | call ``VirtualEnvironment`` without the path argument: 58 | 59 | .. code:: python 60 | 61 | env = VirtualEnvironment() 62 | 63 | The `VirtualEnvironment` constructor takes some optional arguments (their defaults are shown below): 64 | 65 | * ``python=None`` - specify the Python interpreter to use. Defaults to the default system interpreter *(new in 2.1.3)* 66 | * ``cache=None`` - existing directory to override the default pip download cache 67 | * ``readonly=False`` - prevent all operations that could potentially modify the environment *(new in 2.1.7)* 68 | * ``system_site_packages=False`` - include system site packages in operations on the environment *(new in 2.1.14)* 69 | 70 | Operations 71 | ---------- 72 | 73 | Once you have a `VirtualEnvironment` object, you can perform operations on it. 74 | 75 | - Check if the ``mezzanine`` package is installed: 76 | 77 | .. code:: python 78 | 79 | >>> env.is_installed('mezzanine') 80 | False 81 | 82 | - Install the latest version of the ``mezzanine`` package: 83 | 84 | .. code:: python 85 | 86 | >>> env.install('mezzanine') 87 | 88 | - A wheel of the latest version of the ``mezzanine`` package (new in 89 | 2.1.4): 90 | 91 | .. code:: python 92 | 93 | >>> env.wheel('mezzanine') 94 | 95 | - Install version 1.4 of the ``django`` package (this is pip’s syntax): 96 | 97 | .. code:: python 98 | 99 | >>> env.install('django==1.4') 100 | 101 | - Upgrade the ``django`` package to the latest version: 102 | 103 | .. code:: python 104 | 105 | >>> env.upgrade('django') 106 | 107 | - Upgrade all packages to their latest versions (new in 2.1.7): 108 | 109 | .. code:: python 110 | 111 | >>> env.upgrade_all() 112 | 113 | - Uninstall the ``mezzanine`` package: 114 | 115 | .. code:: python 116 | 117 | >>> env.uninstall('mezzanine') 118 | 119 | Packages may be specified as name only (to work on the latest version), using 120 | pip’s package syntax (e.g. ``django==1.4``) or as a tuple of ``('name', 121 | 'ver')`` (e.g. ``('django', '1.4')``). 122 | 123 | - A package may be installed directly from a git repository (must end 124 | with ``.git``): 125 | 126 | .. code:: python 127 | 128 | >>> env.install('git+git://github.com/sjkingo/cartridge-payments.git') 129 | 130 | *New in 2.1.10:* 131 | 132 | - A package can be installed in pip's *editable* mode by prefixing the package 133 | name with `-e` (this is pip's syntax): 134 | 135 | .. code:: python 136 | 137 | >>> env.install('-e git+https://github.com/stephenmcd/cartridge.git') 138 | 139 | *New in 2.1.15:* 140 | 141 | - Packages in a pip requirements file can be installed by prefixing the 142 | requirements file path with `-r`: 143 | 144 | .. code:: python 145 | 146 | >>> env.install('-r requirements.txt') 147 | 148 | - Instances of the environment provide an ``installed_packages`` 149 | property: 150 | 151 | .. code:: python 152 | 153 | >>> env.installed_packages 154 | [('django', '1.5'), ('wsgiref', '0.1.2')] 155 | 156 | - A list of package names is also available in the same manner: 157 | 158 | .. code:: python 159 | 160 | >>> env.installed_package_names 161 | ['django', 'wsgiref'] 162 | 163 | - Search for a package on PyPI (changed in 2.1.5: this now returns a 164 | dictionary instead of list): 165 | 166 | .. code:: python 167 | 168 | >>> env.search('virtualenv-api') 169 | {'virtualenv-api': 'An API for virtualenv/pip'} 170 | >>> len(env.search('requests')) 171 | 231 172 | 173 | - The old functionality (pre 2.1.5) of ``env.search`` may be used: 174 | 175 | .. code:: python 176 | 177 | >>> list(env.search('requests').items()) 178 | [('virtualenv-api', 'An API for virtualenv/pip')] 179 | 180 | Verbose output from each command is available in the environment's 181 | ``build.log`` file, which is appended to with each operation. Any errors are 182 | logged to ``build.err``. 183 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import tempfile 3 | 4 | from virtualenvapi.manage import VirtualEnvironment 5 | 6 | 7 | def example(path=tempfile.mkdtemp('virtualenv.test')): 8 | print('Env path', path) 9 | env = VirtualEnvironment(path) 10 | 11 | print('django 1.5 installed?', env.is_installed('django==1.5')) 12 | 13 | print('mezzanine installed?', env.is_installed('mezzanine')) 14 | env.install('mezzanine') 15 | print('mezzanine installed?', env.is_installed('mezzanine')) 16 | 17 | print(env.installed_packages) 18 | 19 | repo = 'git+git://github.com/sjkingo/django_auth_ldap3.git' 20 | pkg = repo.split('/')[-1].replace('.git', '') 21 | print('django_auth_ldap3 installed?', env.is_installed(pkg)) 22 | env.install(repo) 23 | print('django_auth_ldap3 installed?', env.is_installed(pkg)) 24 | print(env.installed_packages) 25 | 26 | env.uninstall('mezzanine') 27 | print('mezzanine installed?', env.is_installed('mezzanine')) 28 | print(env.installed_packages) 29 | 30 | pkgs = env.search('requests') 31 | print('search for \'requests\':') 32 | print(len(pkgs), 'found:') 33 | print(pkgs) 34 | 35 | 36 | if __name__ == '__main__': 37 | example() 38 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | from virtualenvapi import __version__ 4 | 5 | setup( 6 | name='virtualenv-api', 7 | version=__version__, 8 | license='BSD', 9 | author='Sam Kingston and AUTHORS', 10 | author_email='sam@sjkwi.com.au', 11 | description='An API for virtualenv/pip', 12 | long_description=open('README.rst', 'r').read(), 13 | url='https://github.com/sjkingo/virtualenv-api', 14 | install_requires=['six'], 15 | packages=find_packages(), 16 | classifiers=[ 17 | 'Development Status :: 5 - Production/Stable', 18 | 'Intended Audience :: Developers', 19 | 'License :: OSI Approved :: BSD License', 20 | 'Operating System :: OS Independent', 21 | 'Programming Language :: Python', 22 | 'Programming Language :: Python :: 2', 23 | 'Programming Language :: Python :: 2.7', 24 | 'Programming Language :: Python :: 3', 25 | 'Programming Language :: Python :: 3.6', 26 | 'Programming Language :: Python :: 3.7', 27 | 'Programming Language :: Python :: 3.8', 28 | 'Topic :: Software Development :: Libraries :: Python Modules', 29 | ], 30 | ) 31 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import shutil 4 | import string 5 | import sys 6 | import tempfile 7 | import unittest 8 | 9 | from virtualenvapi.manage import VirtualEnvironment 10 | 11 | packages_for_tests = ['pep8'] 12 | non_lowercase_packages_for_test = ['Pillow'] 13 | all_packages_for_tests = packages_for_tests + non_lowercase_packages_for_test 14 | 15 | def which(program): 16 | def is_exe(fpath): 17 | return os.path.isfile(fpath) and os.access(fpath, os.X_OK) 18 | 19 | fpath, fname = os.path.split(program) 20 | 21 | if fpath: 22 | if is_exe(program): 23 | return program 24 | else: 25 | for path in os.environ["PATH"].split(os.pathsep): 26 | path = path.strip('"') 27 | exe_file = os.path.join(path, program) 28 | if is_exe(exe_file): 29 | return exe_file 30 | 31 | return None 32 | 33 | 34 | class TestBase(unittest.TestCase): 35 | """ 36 | Base class for test cases to inherit from. 37 | """ 38 | 39 | env_path = None 40 | 41 | def setUp(self): 42 | self.env_path = tempfile.mkdtemp() 43 | self.virtual_env_obj = VirtualEnvironment(self.env_path) 44 | 45 | def tearDown(self): 46 | if os.path.exists(self.env_path): 47 | shutil.rmtree(self.env_path) 48 | 49 | def _install_packages(self, packages): 50 | for pack in packages: 51 | self.virtual_env_obj.install(pack) 52 | 53 | def _uninstall_packages(self, packages): 54 | for pack in packages: 55 | self.virtual_env_obj.uninstall(pack) 56 | 57 | 58 | class InstalledTestCase(TestBase): 59 | """ 60 | Test install/uninstall operations. 61 | """ 62 | 63 | def test_installed(self): 64 | self.assertFalse(self.virtual_env_obj.is_installed(''.join(random.sample(string.ascii_letters, 30)))) 65 | 66 | def test_install(self): 67 | for pack in all_packages_for_tests: 68 | self.virtual_env_obj.install(pack) 69 | self.assertTrue(self.virtual_env_obj.is_installed(pack)) 70 | 71 | def test_install_requirements(self): 72 | """test installing Python packages from a pip requirements file""" 73 | 74 | # create a temporary requirements.txt file with some packages 75 | with tempfile.NamedTemporaryFile('w') as tmp_requirements_file: 76 | tmp_requirements_file.write('\n'.join(packages_for_tests)) 77 | tmp_requirements_file.flush() 78 | 79 | self.virtual_env_obj.install('-r {}'.format(tmp_requirements_file.name)) 80 | for pack in packages_for_tests: 81 | self.assertTrue(self.virtual_env_obj.is_installed(pack)) 82 | 83 | def test_uninstall(self): 84 | self._install_packages(all_packages_for_tests) 85 | for pack in all_packages_for_tests: 86 | if pack.endswith('.git'): 87 | pack = pack.split('/')[-1].replace('.git', '') 88 | self.virtual_env_obj.uninstall(pack) 89 | self.assertFalse(self.virtual_env_obj.is_installed(pack)) 90 | 91 | def test_wheel(self): 92 | self.virtual_env_obj.install('wheel') # required for this test 93 | for pack in packages_for_tests: 94 | self.virtual_env_obj.wheel(pack, options=['--wheel-dir=/tmp/wheelhouse']) 95 | self.virtual_env_obj.install(pack, options=['--no-index', '--find-links=/tmp/wheelhouse']) 96 | self.assertTrue(self.virtual_env_obj.is_installed(pack)) 97 | 98 | 99 | class SearchTestCase(TestBase): 100 | """ 101 | Test pip search. 102 | """ 103 | 104 | def test_search(self): 105 | for pack in all_packages_for_tests: 106 | result = self.virtual_env_obj.search(pack) 107 | self.assertIsInstance(result, dict) 108 | self.assertTrue(bool(result)) 109 | if result: 110 | self.assertIn(pack.lower(), [k.split(' (')[0].lower() for k in result.keys()]) 111 | 112 | def test_search_names(self): 113 | for pack in all_packages_for_tests: 114 | result = self.virtual_env_obj.search_names(pack) 115 | self.assertIsInstance(result, list) 116 | self.assertIn(pack.lower(), [k.split(' (')[0].lower() for k in result]) 117 | 118 | 119 | class PythonArgumentTestCase(TestBase): 120 | """ 121 | Test passing a different interpreter path to `VirtualEnvironment` (`virtualenv -p`). 122 | """ 123 | 124 | def setUp(self): 125 | self.env_path = tempfile.mkdtemp() 126 | self.python = which('python') 127 | self.assertIsNotNone(self.python) 128 | self.virtual_env_obj = VirtualEnvironment(self.env_path, python=self.python) 129 | 130 | def test_python_version(self): 131 | self.assertEqual(self.virtual_env_obj.python, self.python) 132 | self.assertEqual( 133 | os.path.dirname(self.python), 134 | os.path.dirname(which('pip')) 135 | ) 136 | 137 | 138 | class SystemSitePackagesTest(unittest.TestCase): 139 | """ 140 | test coverage for using system-site-packages flag 141 | 142 | This verifies the presence of the no-global-site-packages 143 | file in the venv for non-system packages venv and 144 | verified it is not present for the true case 145 | 146 | """ 147 | def setUp(self): 148 | """ 149 | snapshot system package list 150 | """ 151 | self.dir = tempfile.mkdtemp() 152 | self.no_global = ( 153 | "lib/python{}.{}" 154 | "/no-global-site-packages.txt" 155 | ).format( 156 | sys.version_info.major, 157 | sys.version_info.minor 158 | ) 159 | 160 | def tearDown(self): 161 | if os.path.exists(self.dir): 162 | shutil.rmtree(self.dir) 163 | 164 | def test_system_site_packages(self): 165 | """ 166 | test that creating a venv with system_site_packages=True 167 | results in a venv that does not contain the no-global-site-packages file 168 | 169 | """ 170 | venv = VirtualEnvironment(self.dir, system_site_packages=True) 171 | venv._create() 172 | expected = os.path.join(venv.path, self.no_global) 173 | self.assertTrue( 174 | not os.path.exists(expected) 175 | ) 176 | 177 | def test_no_system_site_packages(self): 178 | """ 179 | test that creating a venv with system_site_packages=False 180 | results in a venv that contains the no-global-site-packages 181 | file 182 | 183 | """ 184 | venv = VirtualEnvironment(self.dir) 185 | venv._create() 186 | expected = os.path.join(venv.path, self.no_global) 187 | self.assertTrue( 188 | os.path.exists(expected) 189 | ) 190 | 191 | 192 | if __name__ == '__main__': 193 | unittest.main() 194 | -------------------------------------------------------------------------------- /virtualenvapi/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '2.1.18' 2 | -------------------------------------------------------------------------------- /virtualenvapi/exceptions.py: -------------------------------------------------------------------------------- 1 | class VirtualenvPathNotFound(EnvironmentError): 2 | pass 3 | 4 | 5 | class VirtualenvCreationException(EnvironmentError): 6 | pass 7 | 8 | 9 | class PackageInstallationException(EnvironmentError): 10 | pass 11 | 12 | 13 | class PackageRemovalException(EnvironmentError): 14 | pass 15 | 16 | class PackageWheelException(EnvironmentError): 17 | pass 18 | 19 | class VirtualenvReadonlyException(Exception): 20 | message = 'The virtualenv was constructed readonly and cannot be modified' 21 | -------------------------------------------------------------------------------- /virtualenvapi/manage.py: -------------------------------------------------------------------------------- 1 | from os import linesep, environ 2 | import os.path 3 | import subprocess 4 | import six 5 | import sys 6 | 7 | from virtualenvapi.util import split_package_name, to_text, get_env_path, to_ascii 8 | from virtualenvapi.exceptions import * 9 | 10 | 11 | class VirtualEnvironment(object): 12 | 13 | def __init__(self, path=None, python=None, cache=None, readonly=False, system_site_packages=False): 14 | 15 | if path is None: 16 | path = get_env_path() 17 | 18 | if not path: 19 | raise VirtualenvPathNotFound('Path for virtualenv is not define or virtualenv is not activate') 20 | 21 | self.python = python 22 | self.system_site_packages = system_site_packages 23 | 24 | # remove trailing slash so os.path.split() behaves correctly 25 | if path[-1] == os.path.sep: 26 | path = path[:-1] 27 | 28 | # Expand path so shell shortcuts may be used such as ~ 29 | self.path = os.path.abspath(os.path.expanduser(path)) 30 | 31 | self.env = environ.copy() 32 | 33 | # Blacklist environment variables that will break pip in virtualenvs 34 | # See https://github.com/pypa/virtualenv/issues/845 35 | self.env.pop('__PYVENV_LAUNCHER__', None) 36 | 37 | if cache is not None: 38 | self.env['PIP_DOWNLOAD_CACHE'] = os.path.expanduser(os.path.expandvars(cache)) 39 | 40 | self.readonly = readonly 41 | 42 | # True if the virtual environment has been set up through open_or_create() 43 | self._ready = False 44 | 45 | def __str__(self): 46 | return six.u(self.path) 47 | 48 | @property 49 | def _pip(self): 50 | """The arguments used to call pip.""" 51 | # pip is called using the python interpreter to get around a long path 52 | # issue detailed in https://github.com/sjkingo/virtualenv-api/issues/30 53 | return [self._python_rpath, '-m', 'pip'] 54 | 55 | @property 56 | def _python_rpath(self): 57 | """The relative path (from environment root) to python.""" 58 | # Windows virtualenv installation installs pip to the [Ss]cripts 59 | # folder. Here's a simple check to support: 60 | if sys.platform == 'win32': 61 | return os.path.join('Scripts', 'python.exe') 62 | return os.path.join('bin', 'python') 63 | 64 | @property 65 | def pip_version(self): 66 | """Version of installed pip.""" 67 | if not self._pip_exists: 68 | return None 69 | if not hasattr(self, '_pip_version'): 70 | # don't call `self._execute_pip` here as that method calls this one 71 | output = self._execute(self._pip + ['-V'], log=False).split()[1] 72 | self._pip_version = tuple([int(n) for n in output.split('.')]) 73 | return self._pip_version 74 | 75 | @property 76 | def root(self): 77 | """The root directory that this virtual environment exists in.""" 78 | return os.path.split(self.path)[0] 79 | 80 | @property 81 | def name(self): 82 | """The name of this virtual environment (taken from its path).""" 83 | return os.path.basename(self.path) 84 | 85 | @property 86 | def _logfile(self): 87 | """Absolute path of the log file for recording installation output.""" 88 | return os.path.join(self.path, 'build.log') 89 | 90 | @property 91 | def _errorfile(self): 92 | """Absolute path of the log file for recording installation errors.""" 93 | return os.path.join(self.path, 'build.err') 94 | 95 | def _create(self): 96 | """Executes `virtualenv` to create a new environment.""" 97 | if self.readonly: 98 | raise VirtualenvReadonlyException() 99 | args = ['virtualenv'] 100 | if self.system_site_packages: 101 | args.append('--system-site-packages') 102 | if self.python is None: 103 | args.append(self.name) 104 | else: 105 | args.extend(['-p', self.python, self.name]) 106 | proc = subprocess.Popen(args, cwd=self.root, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 107 | output, error = proc.communicate() 108 | returncode = proc.returncode 109 | if returncode: 110 | raise VirtualenvCreationException((returncode, output, self.name)) 111 | self._write_to_log(output, truncate=True) 112 | self._write_to_error(error, truncate=True) 113 | 114 | def _execute_pip(self, args, log=True): 115 | """ 116 | Executes pip commands. 117 | 118 | :param args: Arguments to pass to pip (list[str]) 119 | :param log: Log the output to a file [default: True] (boolean) 120 | :return: See _execute 121 | """ 122 | 123 | # Copy the pip calling arguments so they can be extended 124 | exec_args = list(self._pip) 125 | 126 | # Older versions of pip don't support the version check argument. 127 | # Fixes https://github.com/sjkingo/virtualenv-api/issues/35 128 | if self.pip_version[0] >= 6: 129 | exec_args.append('--disable-pip-version-check') 130 | 131 | exec_args.extend(args) 132 | return self._execute(exec_args, log=log) 133 | 134 | def _execute(self, args, log=True): 135 | """Executes the given command inside the environment and returns the output.""" 136 | if not self._ready: 137 | self.open_or_create() 138 | output = '' 139 | error = '' 140 | try: 141 | proc = subprocess.Popen(args, cwd=self.path, env=self.env, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 142 | output, error = proc.communicate() 143 | returncode = proc.returncode 144 | if returncode: 145 | raise subprocess.CalledProcessError(returncode, proc, (output, error)) 146 | return to_text(output) 147 | except OSError as e: 148 | # raise a more meaningful error with the program name 149 | prog = args[0] 150 | if prog[0] != os.sep: 151 | prog = os.path.join(self.path, prog) 152 | raise OSError('%s: %s' % (prog, six.u(str(e)))) 153 | except subprocess.CalledProcessError as e: 154 | output, error = e.output 155 | e.output = output 156 | raise e 157 | finally: 158 | if log: 159 | try: 160 | self._write_to_log(to_text(output)) 161 | self._write_to_error(to_text(error)) 162 | except NameError: 163 | pass # We tried 164 | 165 | def _write_to_log(self, s, truncate=False): 166 | """Writes the given output to the log file, appending unless `truncate` is True.""" 167 | # if truncate is True, set write mode to truncate 168 | with open(self._logfile, 'w' if truncate else 'a') as fp: 169 | fp.writelines((to_text(s) if six.PY2 else to_text(s), )) 170 | 171 | def _write_to_error(self, s, truncate=False): 172 | """Writes the given output to the error file, appending unless `truncate` is True.""" 173 | # if truncate is True, set write mode to truncate 174 | with open(self._errorfile, 'w' if truncate else 'a') as fp: 175 | fp.writelines((to_text(s)), ) 176 | 177 | def _pip_exists(self): 178 | """Returns True if pip exists inside the virtual environment. Can be 179 | used as a naive way to verify that the environment is installed.""" 180 | return os.path.isfile(os.path.join(self.path, 'bin', 'pip')) 181 | 182 | def open_or_create(self): 183 | """Attempts to open the virtual environment or creates it if it 184 | doesn't exist. 185 | XXX this should probably be expanded to do some proper checking?""" 186 | if not self._pip_exists(): 187 | self._create() 188 | self._ready = True 189 | 190 | def install(self, package, force=False, upgrade=False, options=None): 191 | """Installs the given package into this virtual environment, as 192 | specified in pip's package syntax or a tuple of ('name', 'ver'), 193 | only if it is not already installed. Some valid examples: 194 | 195 | 'Django' 196 | 'Django==1.5' 197 | ('Django', '1.5') 198 | '-e .' 199 | '-r requirements.txt' 200 | 201 | If `force` is True, force an installation. If `upgrade` is True, 202 | attempt to upgrade the package in question. If both `force` and 203 | `upgrade` are True, reinstall the package and its dependencies. 204 | The `options` is a list of strings that can be used to pass to 205 | pip.""" 206 | if self.readonly: 207 | raise VirtualenvReadonlyException() 208 | if options is None: 209 | options = [] 210 | if isinstance(package, tuple): 211 | package = '=='.join(package) 212 | if package.startswith(('-e', '-r')): 213 | package_args = package.split() 214 | else: 215 | package_args = [package] 216 | if not (force or upgrade) and (package_args[0] != '-r' and self.is_installed(package_args[-1])): 217 | self._write_to_log('%s is already installed, skipping (use force=True to override)' % package_args[-1]) 218 | return 219 | if not isinstance(options, list): 220 | raise ValueError("Options must be a list of strings.") 221 | if upgrade: 222 | options += ['--upgrade'] 223 | if force: 224 | options += ['--force-reinstall'] 225 | elif force: 226 | options += ['--ignore-installed'] 227 | try: 228 | self._execute_pip(['install'] + package_args + options) 229 | except subprocess.CalledProcessError as e: 230 | raise PackageInstallationException((e.returncode, e.output, package)) 231 | 232 | def uninstall(self, package): 233 | """Uninstalls the given package (given in pip's package syntax or a tuple of 234 | ('name', 'ver')) from this virtual environment.""" 235 | if isinstance(package, tuple): 236 | package = '=='.join(package) 237 | if not self.is_installed(package): 238 | self._write_to_log('%s is not installed, skipping' % package) 239 | return 240 | try: 241 | self._execute_pip(['uninstall', '-y', package]) 242 | except subprocess.CalledProcessError as e: 243 | raise PackageRemovalException((e.returncode, e.output, package)) 244 | 245 | def wheel(self, package, options=None): 246 | """Creates a wheel of the given package from this virtual environment, 247 | as specified in pip's package syntax or a tuple of ('name', 'ver'), 248 | only if it is not already installed. Some valid examples: 249 | 250 | 'Django' 251 | 'Django==1.5' 252 | ('Django', '1.5') 253 | 254 | The `options` is a list of strings that can be used to pass to 255 | pip.""" 256 | if self.readonly: 257 | raise VirtualenvReadonlyException() 258 | if options is None: 259 | options = [] 260 | if isinstance(package, tuple): 261 | package = '=='.join(package) 262 | if not self.is_installed('wheel'): 263 | raise PackageWheelException((0, "Wheel package must be installed in the virtual environment", package)) 264 | if not isinstance(options, list): 265 | raise ValueError("Options must be a list of strings.") 266 | try: 267 | self._execute_pip(['wheel', package] + options) 268 | except subprocess.CalledProcessError as e: 269 | raise PackageWheelException((e.returncode, e.output, package)) 270 | 271 | def is_installed(self, package): 272 | """Returns True if the given package (given in pip's package syntax or a 273 | tuple of ('name', 'ver')) is installed in the virtual environment.""" 274 | if isinstance(package, tuple): 275 | package = '=='.join(package) 276 | if package.endswith('.git'): 277 | pkg_name = os.path.split(package)[1][:-4] 278 | return pkg_name in self.installed_package_names or \ 279 | pkg_name.replace('_', '-') in self.installed_package_names 280 | pkg_tuple = split_package_name(package) 281 | if pkg_tuple[1] is not None: 282 | return pkg_tuple in self.installed_packages 283 | else: 284 | return pkg_tuple[0].lower() in self.installed_package_names 285 | 286 | def upgrade(self, package, force=False): 287 | """Shortcut method to upgrade a package. If `force` is set to True, 288 | the package and all of its dependencies will be reinstalled, otherwise 289 | if the package is up to date, this command is a no-op.""" 290 | self.install(package, upgrade=True, force=force) 291 | 292 | def upgrade_all(self): 293 | """ 294 | Upgrades all installed packages to their latest versions. 295 | """ 296 | for pkg in self.installed_package_names: 297 | self.install(pkg, upgrade=True) 298 | 299 | def search(self, term): 300 | """ 301 | Searches the PyPi repository for the given `term` and returns a 302 | dictionary of results. 303 | 304 | New in 2.1.5: returns a dictionary instead of list of tuples 305 | """ 306 | packages = {} 307 | results = self._execute_pip(['search', term], log=False) # Don't want to log searches 308 | for result in results.split(linesep): 309 | try: 310 | name, description = result.split(six.u(' - '), 1) 311 | except ValueError: 312 | # '-' not in result so unable to split into tuple; 313 | # this could be from a multi-line description 314 | continue 315 | else: 316 | name = name.strip() 317 | if len(name) == 0: 318 | continue 319 | packages[name] = description.split(six.u('= (8, 1, 0) else ['-l'] 332 | return list(map(split_package_name, filter(None, self._execute_pip( 333 | ['freeze'] + freeze_options).split(linesep)))) 334 | 335 | @property 336 | def installed_package_names(self): 337 | """List of all package names that are installed in this environment.""" 338 | return [name.lower() for name, _ in self.installed_packages] 339 | -------------------------------------------------------------------------------- /virtualenvapi/util.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | import six 3 | import sys 4 | 5 | 6 | def to_text(source): 7 | if six.PY3: 8 | if isinstance(source, str): 9 | return source 10 | else: 11 | return source.decode("utf-8") 12 | elif six.PY2: 13 | if isinstance(source, unicode): 14 | return source.encode("utf-8") 15 | return source 16 | else: 17 | return source 18 | 19 | 20 | def to_ascii(source): 21 | if isinstance(source, six.string_types): 22 | return "".join([c for c in source if ord(c) < 128]) 23 | 24 | 25 | def get_env_path(): 26 | prefix_name = 'real_prefix' 27 | virtual_env_path_environ_key = 'VIRTUAL_ENV' 28 | 29 | path = None 30 | 31 | real_prefix = (hasattr(sys, prefix_name) and getattr(sys, prefix_name)) or None 32 | if real_prefix: 33 | path = environ.get(virtual_env_path_environ_key) 34 | if not path: 35 | path = sys.prefix 36 | 37 | return path 38 | 39 | 40 | def split_package_name(p): 41 | """Splits the given package name and returns a tuple (name, ver).""" 42 | s = p.split(six.u('==')) 43 | if len(s) == 1: 44 | return (to_text(s[0]), None) 45 | else: 46 | return (to_text(s[0]), to_text(s[1])) 47 | --------------------------------------------------------------------------------