diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..f7bf90cea --- /dev/null +++ b/.coveragerc @@ -0,0 +1,28 @@ +[coverage:report] +skip_covered = False +show_missing = True +exclude_lines = + \#\s*pragma: no cover + ^\s*raise AssertionError\b + ^\s*raise NotImplementedError\b + ^\s*raise$ + ^if __name__ == ['"]__main__['"]:$ +omit = + # site.py is ran before the coverage can be enabled, no way to measure coverage on this + src/virtualenv/interpreters/create/impl/cpython/site.py + src/virtualenv/seed/embed/wheels/pip-*.whl/* + +[coverage:paths] +source = + src + .tox/*/lib/python*/site-packages + .tox/pypy*/site-packages + .tox\*\Lib\site-packages\ + */src + *\src + +[coverage:run] +branch = false +parallel = true +source = + ${_COVERAGE_SRC} diff --git a/.gitignore b/.gitignore index 6beeba9e8..0799b4e80 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ dist .vscode /docs/_draft.rst +/pip-wheel-metadata +/src/virtualenv/version.py +/src/virtualenv/out +/*env* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0b8234b7d..d1d09b9c6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: rev: v1.9.3 hooks: - id: seed-isort-config - args: [--application-directories, '.'] + args: [--application-directories, '.:src'] - repo: https://github.com/pre-commit/mirrors-isort rev: v4.3.21 hooks: diff --git a/MANIFEST.in b/MANIFEST.in index d47418dec..7db538b00 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,22 +1,12 @@ -include virtualenv.py -recursive-include virtualenv_support *.whl -recursive-include virtualenv_embedded * -include virtualenv_support/__init__.py -include pyproject.toml - -include AUTHORS.txt -include LICENSE.txt - -recursive-include tests * -recursive-include docs * -include tasks/* -include tox.ini +# setuptools-scm by default adds all SCM tracked files, we prune the following maintenance related ones (sdist only) +exclude .gitattributes +exclude .gitignore +exclude .github/* -exclude readthedocs.yml -exclude CONTRIBUTING.rst -exclude .pre-commit-config.yaml -exclude azure-run-tox-env.yml exclude azure-pipelines.yml -exclude .gitignore -exclude .gitattributes -recursive-exclude .github * +exclude CONTRIBUTING.rst +exclude readthedocs.yml +exclude MANIFEST.in + +exclude tasks/release.py +exclude tasks/upgrade_wheels.py diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a50c5dcb6..5ab9f21f9 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -28,19 +28,15 @@ schedules: always: true variables: - PYTEST_ADDOPTS: "-v -v -ra --showlocals" + PYTEST_ADDOPTS: "-v -v -ra --showlocals --durations=15" PYTEST_XDIST_PROC_NR: 'auto' CI_RUN: 'yes' + UPGRADE_ADVISORY: 'yes' jobs: - template: run-tox-env.yml@tox parameters: jobs: - fix_lint: null - embed: null - cross_python2: null - cross_python3: null - docs: null py38: image: [linux, windows, macOs] py37: @@ -51,15 +47,32 @@ jobs: image: [linux, windows, macOs] py27: image: [linux, windows, macOs] + fix_lint: + image: [linux, windows] + docs: + image: [linux, windows] + package_readme: + image: [linux, windows] + upgrade: + image: [linux, windows] dev: null - package_readme: null before: - script: 'sudo apt-get update -y && sudo apt-get install fish csh' - condition: and(succeeded(), eq(variables['image_name'], 'linux'), in(variables['TOXENV'], 'py38', 'py37', 'py36', 'py35', 'py34', 'py27')) + condition: and(succeeded(), eq(variables['image_name'], 'linux'), in(variables['TOXENV'], 'py38', 'py37', 'py36', 'py35', 'py27')) displayName: install fish and csh via apt-get - script: 'brew update -vvv && brew install fish tcsh' - condition: and(succeeded(), eq(variables['image_name'], 'macOs'), in(variables['TOXENV'], 'py38', 'py37', 'py36', 'py35', 'py34', 'py27')) + condition: and(succeeded(), eq(variables['image_name'], 'macOs'), in(variables['TOXENV'], 'py38', 'py37', 'py36', 'py35', 'py27')) displayName: install fish and csh via brew + - task: UsePythonVersion@0 + condition: and(succeeded(), in(variables['TOXENV'], 'py27')) + displayName: provision python 3 + inputs: + versionSpec: '3.8' + - task: UsePythonVersion@0 + condition: and(succeeded(), in(variables['TOXENV'], 'py38', 'py37', 'py36', 'py35')) + displayName: provision python 2 + inputs: + versionSpec: '2.7' coverage: with_toxenv: 'coverage' # generate .tox/.coverage, .tox/coverage.xml after test run for_envs: [py38, py37, py36, py35, py27] diff --git a/docs/conf.py b/docs/conf.py index 7dbf3c372..988f93669 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,6 +12,7 @@ source_suffix = ".rst" master_doc = "index" project = "virtualenv" +# noinspection PyShadowingBuiltins copyright = "2007-2018, Ian Bicking, The Open Planning Project, PyPA" ROOT_SRC_TREE_DIR = Path(__file__).parents[1] @@ -30,8 +31,8 @@ def generate_draft_news(): env = os.environ.copy() env["PATH"] += os.pathsep.join([os.path.dirname(sys.executable)] + env["PATH"].split(os.pathsep)) changelog = subprocess.check_output( - ["towncrier", "--draft", "--version", "DRAFT"], cwd=str(ROOT_SRC_TREE_DIR), env=env - ).decode("utf-8") + ["towncrier", "--draft", "--version", "DRAFT"], cwd=str(ROOT_SRC_TREE_DIR), env=env, universal_newlines=True + ) if "No significant changes" in changelog: content = "" else: diff --git a/pyproject.toml b/pyproject.toml index 7587ffdfe..678880c57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,8 @@ [build-system] requires = [ - "setuptools >= 40.6.3", + "setuptools >= 40.0.0", "wheel >= 0.29.0", + "setuptools-scm >= 2, < 4", ] build-backend = 'setuptools.build_meta' diff --git a/setup.cfg b/setup.cfg index cd5cea46d..361b600a7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,81 +1,108 @@ [metadata] name = virtualenv +version = attr: virtualenv.__version__ description = Virtual Python Environment builder long_description = file: README.rst keywords = virtual, environments, isolated maintainer = Bernat Gabor -author = Ian Bicking +author = Bernat Gabor maintainer-email = gaborjbernat@gmail.com -author-email = ianb@colorstudy.com +author-email = gaborjbernat@gmail.com url = https://virtualenv.pypa.io/ project_urls = Source=https://github.com/pypa/virtualenv Tracker=https://github.com/pypa/virtualenv/issues -classifiers = Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: MIT License - Operating System :: POSIX - Operating System :: Microsoft :: Windows - Operating System :: MacOS :: MacOS X - Topic :: Software Development :: Testing - Topic :: Software Development :: Libraries - Topic :: Utilities - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 +classifiers = + Development Status :: 3 - Alpha + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3 + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Operating System :: POSIX + Operating System :: Microsoft :: Windows + Operating System :: MacOS :: MacOS X + Topic :: Software Development :: Testing + Topic :: Software Development :: Libraries + Topic :: Utilities platforms = any license = MIT license_file = LICENSE.txt [options] packages = find: -include_package_data = True +package_dir = + =src zip_safe = True python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* +install_requires = + six >= 1.12.0, < 2 + appdirs >= 1.4.3 + entrypoints >= 0.3, <1 + pathlib2 >= 2.3.3, < 3; python_version < '3.4' and sys.platform != 'win32' + distlib >= 0.3.0, <1; sys.platform == 'win32' +[options.packages.find] +where = src [options.extras_require] -testing = mock;python_version<"3.3" - pytest >= 4.0.0, <5 - coverage >= 4.5.0, <5 - pytest-timeout >= 1.3.0, <2 - xonsh; python_version>="3.5" - six >= 1.10.0, < 2 - pytest-xdist - pytest-localserver - pypiserver -docs = sphinx >= 1.8.0, < 2 - towncrier >= 18.5.0 - sphinx_rtd_theme >= 0.4.2, < 1 - -[options.packages.find] -where = . +testing = + pytest >= 4.0.0, <6 + coverage >= 4.5.1, <6 + pytest-mock >= 1.12.1, <2 + xonsh >= 0.9.13, <1; python_version > '3.4' +docs = + sphinx >= 2.0.0, < 3 + towncrier >= 18.5.0 + sphinx_rtd_theme >= 0.4.2, < 1 [options.package_data] -virtualenv_support = *.whl +virtualenv.seed.embed.wheels = *.whl +virtualenv.activation.bash = *.sh +virtualenv.activation.cshell = *.csh +virtualenv.activation.batch = *.bat +virtualenv.activation.fish = *.fish +virtualenv.activation.powershell = *.ps1 +virtualenv.activation.xonosh = *.xsh [options.entry_points] -console_scripts = virtualenv=virtualenv:main - +console_scripts = + virtualenv=virtualenv.__main__:run +virtualenv.discovery = + builtin = virtualenv.interpreters.discovery.builtin:Builtin +virtualenv.create = + cpython3-posix = virtualenv.interpreters.create.cpython.cpython3:CPython3Posix + cpython3-win = virtualenv.interpreters.create.cpython.cpython3:CPython3Windows + cpython2-posix = virtualenv.interpreters.create.cpython.cpython2:CPython2Posix + cpython2-win = virtualenv.interpreters.create.cpython.cpython2:CPython2Windows + venv = virtualenv.interpreters.create.venv:Venv +virtualenv.seed = + none = virtualenv.seed.none:NoneSeeder + pip = virtualenv.seed.embed.pip_invoke:PipInvoke + app-data = virtualenv.seed.via_app_data.via_app_data:FromAppData +virtualenv.activate = + bash = virtualenv.activation.bash:BashActivator + cshell = virtualenv.activation.cshell:CShellActivator + batch = virtualenv.activation.batch:BatchActivator + fish = virtualenv.activation.fish:FishActivator + power-shell = virtualenv.activation.powershell:PowerShellActivator + python = virtualenv.activation.python:PythonActivator + xonosh = virtualenv.activation.xonosh:XonoshActivator [sdist] formats = gztar [bdist_wheel] universal = true -[coverage:run] -branch = false -parallel = true - -[coverage:report] -skip_covered = True -show_missing = True - -[coverage:paths] -source = . - .tox/*/*/site-packages - .tox/*/*/*/site-packages - */s +[tool:pytest] +markers = + bash + csh + fish + pwsh + xonsh +junit_family = xunit2 diff --git a/setup.py b/setup.py index cc84098e7..6137f306b 100644 --- a/setup.py +++ b/setup.py @@ -1,16 +1,17 @@ -import os -import re +# -*- coding: utf-8 -*- +import textwrap from setuptools import setup - -def get_version(): - with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "virtualenv.py")) as file_handler: - version_file = file_handler.read() - version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) - if version_match: - return version_match.group(1) - raise RuntimeError("Unable to find version string.") - - -setup(version=get_version(), py_modules=["virtualenv"], setup_requires=["setuptools >= 40.6.3"]) +setup( + use_scm_version={ + "write_to": "src/virtualenv/version.py", + "write_to_template": textwrap.dedent( + """ + # coding: utf-8 + from __future__ import unicode_literals + __version__ = {version!r} + """ + ).lstrip(), + } +) diff --git a/virtualenv_support/__init__.py b/src/__init__.py similarity index 100% rename from virtualenv_support/__init__.py rename to src/__init__.py diff --git a/src/virtualenv/__init__.py b/src/virtualenv/__init__.py new file mode 100644 index 000000000..127d67ce7 --- /dev/null +++ b/src/virtualenv/__init__.py @@ -0,0 +1,5 @@ +from __future__ import absolute_import, unicode_literals + +from .version import __version__ + +__all__ = ("__version__", "run") diff --git a/src/virtualenv/__main__.py b/src/virtualenv/__main__.py new file mode 100644 index 000000000..30831d2b2 --- /dev/null +++ b/src/virtualenv/__main__.py @@ -0,0 +1,22 @@ +from __future__ import absolute_import, print_function, unicode_literals + +import sys + +from virtualenv.error import ProcessCallFailed +from virtualenv.run import run_via_cli + + +def run(args=None): + if args is None: + args = sys.argv[1:] + try: + run_via_cli(args) + except ProcessCallFailed as exception: + print("subprocess call failed for {}".format(exception.cmd)) + print(exception.out, file=sys.stdout, end="") + print(exception.err, file=sys.stderr, end="") + raise SystemExit(exception.code) + + +if __name__ == "__main__": + run() diff --git a/src/virtualenv/activation/__init__.py b/src/virtualenv/activation/__init__.py new file mode 100644 index 000000000..dbe54fc1b --- /dev/null +++ b/src/virtualenv/activation/__init__.py @@ -0,0 +1,19 @@ +from __future__ import absolute_import, unicode_literals + +from .bash import BashActivator +from .batch import BatchActivator +from .cshell import CShellActivator +from .fish import FishActivator +from .powershell import PowerShellActivator +from .python import PythonActivator +from .xonosh import XonoshActivator + +__all__ = [ + BashActivator, + PowerShellActivator, + XonoshActivator, + CShellActivator, + PythonActivator, + BatchActivator, + FishActivator, +] diff --git a/src/virtualenv/activation/activator.py b/src/virtualenv/activation/activator.py new file mode 100644 index 000000000..d5cd10caa --- /dev/null +++ b/src/virtualenv/activation/activator.py @@ -0,0 +1,23 @@ +from __future__ import absolute_import, unicode_literals + +from abc import ABCMeta, abstractmethod + +import six + + +@six.add_metaclass(ABCMeta) +class Activator(object): + def __init__(self, options): + self.flag_prompt = options.prompt + + @classmethod + def add_parser_arguments(cls, parser): + """add activator options""" + + @classmethod + def supports(cls, interpreter): + return True + + @abstractmethod + def generate(self, creator): + raise NotImplementedError diff --git a/src/virtualenv/activation/bash/__init__.py b/src/virtualenv/activation/bash/__init__.py new file mode 100644 index 000000000..c89b42edb --- /dev/null +++ b/src/virtualenv/activation/bash/__init__.py @@ -0,0 +1,14 @@ +from __future__ import absolute_import, unicode_literals + +from virtualenv.util.path import Path + +from ..via_template import ViaTemplateActivator + + +class BashActivator(ViaTemplateActivator): + @classmethod + def supports(cls, interpreter): + return interpreter.os != "nt" + + def templates(self): + yield Path("activate.sh") diff --git a/virtualenv_embedded/activate.sh b/src/virtualenv/activation/bash/activate.sh similarity index 98% rename from virtualenv_embedded/activate.sh rename to src/virtualenv/activation/bash/activate.sh index d9b878154..19bf552bd 100644 --- a/virtualenv_embedded/activate.sh +++ b/src/virtualenv/activation/bash/activate.sh @@ -46,7 +46,7 @@ deactivate () { # unset irrelevant variables deactivate nondestructive -VIRTUAL_ENV="__VIRTUAL_ENV__" +VIRTUAL_ENV='__VIRTUAL_ENV__' export VIRTUAL_ENV _OLD_VIRTUAL_PATH="$PATH" diff --git a/src/virtualenv/activation/batch/__init__.py b/src/virtualenv/activation/batch/__init__.py new file mode 100644 index 000000000..89b03e436 --- /dev/null +++ b/src/virtualenv/activation/batch/__init__.py @@ -0,0 +1,16 @@ +from __future__ import absolute_import, unicode_literals + +from virtualenv.util.path import Path + +from ..via_template import ViaTemplateActivator + + +class BatchActivator(ViaTemplateActivator): + @classmethod + def supports(cls, interpreter): + return interpreter.os == "nt" + + def templates(self): + yield Path("activate.bat") + yield Path("deactivate.bat") + yield Path("pydoc.bat") diff --git a/virtualenv_embedded/activate.bat b/src/virtualenv/activation/batch/activate.bat similarity index 94% rename from virtualenv_embedded/activate.bat rename to src/virtualenv/activation/batch/activate.bat index 9c5d3720a..96e835b52 100644 --- a/virtualenv_embedded/activate.bat +++ b/src/virtualenv/activation/batch/activate.bat @@ -13,7 +13,7 @@ if defined _OLD_VIRTUAL_PROMPT ( ) ) if not defined VIRTUAL_ENV_DISABLE_PROMPT ( - set "PROMPT=__VIRTUAL_WINPROMPT__%PROMPT%" + set "PROMPT=__VIRTUAL_PROMPT__%PROMPT%" ) REM Don't use () to avoid problems with them in %PATH% diff --git a/virtualenv_embedded/deactivate.bat b/src/virtualenv/activation/batch/deactivate.bat similarity index 100% rename from virtualenv_embedded/deactivate.bat rename to src/virtualenv/activation/batch/deactivate.bat diff --git a/src/virtualenv/activation/batch/pydoc.bat b/src/virtualenv/activation/batch/pydoc.bat new file mode 100644 index 000000000..3d46a231a --- /dev/null +++ b/src/virtualenv/activation/batch/pydoc.bat @@ -0,0 +1 @@ +python.exe -m pydoc %* diff --git a/src/virtualenv/activation/cshell/__init__.py b/src/virtualenv/activation/cshell/__init__.py new file mode 100644 index 000000000..b25c602a5 --- /dev/null +++ b/src/virtualenv/activation/cshell/__init__.py @@ -0,0 +1,14 @@ +from __future__ import absolute_import, unicode_literals + +from virtualenv.util.path import Path + +from ..via_template import ViaTemplateActivator + + +class CShellActivator(ViaTemplateActivator): + @classmethod + def supports(cls, interpreter): + return interpreter.os != "nt" + + def templates(self): + yield Path("activate.csh") diff --git a/virtualenv_embedded/activate.csh b/src/virtualenv/activation/cshell/activate.csh similarity index 92% rename from virtualenv_embedded/activate.csh rename to src/virtualenv/activation/cshell/activate.csh index c4a6d584c..72b2cf8ef 100644 --- a/virtualenv_embedded/activate.csh +++ b/src/virtualenv/activation/cshell/activate.csh @@ -10,15 +10,15 @@ alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PA # Unset irrelevant variables. deactivate nondestructive -setenv VIRTUAL_ENV "__VIRTUAL_ENV__" +setenv VIRTUAL_ENV '__VIRTUAL_ENV__' set _OLD_VIRTUAL_PATH="$PATH:q" setenv PATH "$VIRTUAL_ENV:q/__BIN_NAME__:$PATH:q" -if ("__VIRTUAL_PROMPT__" != "") then - set env_name = "__VIRTUAL_PROMPT__" +if ('__VIRTUAL_PROMPT__' != "") then + set env_name = '__VIRTUAL_PROMPT__' else set env_name = '('"$VIRTUAL_ENV:t:q"') ' endif diff --git a/src/virtualenv/activation/fish/__init__.py b/src/virtualenv/activation/fish/__init__.py new file mode 100644 index 000000000..3671093a1 --- /dev/null +++ b/src/virtualenv/activation/fish/__init__.py @@ -0,0 +1,14 @@ +from __future__ import absolute_import, unicode_literals + +from virtualenv.util.path import Path + +from ..via_template import ViaTemplateActivator + + +class FishActivator(ViaTemplateActivator): + def templates(self): + yield Path("activate.fish") + + @classmethod + def supports(cls, interpreter): + return interpreter.os != "nt" diff --git a/virtualenv_embedded/activate.fish b/src/virtualenv/activation/fish/activate.fish similarity index 81% rename from virtualenv_embedded/activate.fish rename to src/virtualenv/activation/fish/activate.fish index 4e2976864..770fb0d4f 100644 --- a/virtualenv_embedded/activate.fish +++ b/src/virtualenv/activation/fish/activate.fish @@ -18,16 +18,16 @@ function deactivate -d 'Exit virtualenv mode and return to the normal environmen # reset old environment variables if test -n "$_OLD_VIRTUAL_PATH" # https://github.com/fish-shell/fish-shell/issues/436 altered PATH handling - if test (echo $FISH_VERSION | tr "." "\n")[1] -lt 3 - set -gx PATH (_fishify_path $_OLD_VIRTUAL_PATH) + if test (echo $FISH_VERSION | head -c 1) -lt 3 + set -gx PATH (_fishify_path "$_OLD_VIRTUAL_PATH") else - set -gx PATH $_OLD_VIRTUAL_PATH + set -gx PATH "$_OLD_VIRTUAL_PATH" end set -e _OLD_VIRTUAL_PATH end if test -n "$_OLD_VIRTUAL_PYTHONHOME" - set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME + set -gx PYTHONHOME "$_OLD_VIRTUAL_PYTHONHOME" set -e _OLD_VIRTUAL_PYTHONHOME end @@ -57,15 +57,15 @@ end # Unset irrelevant variables. deactivate nondestructive -set -gx VIRTUAL_ENV "__VIRTUAL_ENV__" +set -gx VIRTUAL_ENV '__VIRTUAL_ENV__' # https://github.com/fish-shell/fish-shell/issues/436 altered PATH handling -if test (echo $FISH_VERSION | tr "." "\n")[1] -lt 3 +if test (echo $FISH_VERSION | head -c 1) -lt 3 set -gx _OLD_VIRTUAL_PATH (_bashify_path $PATH) else - set -gx _OLD_VIRTUAL_PATH $PATH + set -gx _OLD_VIRTUAL_PATH "$PATH" end -set -gx PATH "$VIRTUAL_ENV/__BIN_NAME__" $PATH +set -gx PATH "$VIRTUAL_ENV"'/__BIN_NAME__' $PATH # Unset `$PYTHONHOME` if set. if set -q PYTHONHOME @@ -87,10 +87,10 @@ if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" # Prompt override provided? # If not, just prepend the environment name. - if test -n "__VIRTUAL_PROMPT__" - printf '%s%s' "__VIRTUAL_PROMPT__" (set_color normal) + if test -n '__VIRTUAL_PROMPT__' + printf '%s%s' '__VIRTUAL_PROMPT__' (set_color normal) else - printf '%s(%s) ' (set_color normal) (basename "$VIRTUAL_ENV") + printf '%s(%s) ' (set_color normal) (basename '$VIRTUAL_ENV') end # Restore the original $status diff --git a/src/virtualenv/activation/powershell/__init__.py b/src/virtualenv/activation/powershell/__init__.py new file mode 100644 index 000000000..4fadc63bc --- /dev/null +++ b/src/virtualenv/activation/powershell/__init__.py @@ -0,0 +1,10 @@ +from __future__ import absolute_import, unicode_literals + +from virtualenv.util.path import Path + +from ..via_template import ViaTemplateActivator + + +class PowerShellActivator(ViaTemplateActivator): + def templates(self): + yield Path("activate.ps1") diff --git a/virtualenv_embedded/activate.ps1 b/src/virtualenv/activation/powershell/activate.ps1 similarity index 100% rename from virtualenv_embedded/activate.ps1 rename to src/virtualenv/activation/powershell/activate.ps1 diff --git a/src/virtualenv/activation/python/__init__.py b/src/virtualenv/activation/python/__init__.py new file mode 100644 index 000000000..b06af78e4 --- /dev/null +++ b/src/virtualenv/activation/python/__init__.py @@ -0,0 +1,19 @@ +from __future__ import absolute_import, unicode_literals + +import json +import os + +from virtualenv.util.path import Path + +from ..via_template import ViaTemplateActivator + + +class PythonActivator(ViaTemplateActivator): + def templates(self): + yield Path("activate_this.py") + + def replacements(self, creator, dest_folder): + replacements = super(PythonActivator, self).replacements(creator, dest_folder) + site_dump = json.dumps([os.path.relpath(str(i), str(dest_folder)) for i in creator.site_packages], indent=2) + replacements.update({"__SITE_PACKAGES__": site_dump}) + return replacements diff --git a/src/virtualenv/activation/python/activate_this.py b/src/virtualenv/activation/python/activate_this.py new file mode 100644 index 000000000..3311fe813 --- /dev/null +++ b/src/virtualenv/activation/python/activate_this.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +"""Activate virtualenv for current interpreter: + +Use exec(open(this_file).read(), {'__file__': this_file}). + +This can be used when you must use an existing Python interpreter, not the virtualenv bin/python. +""" +import json +import os +import site +import sys + +try: + __file__ +except NameError: + raise AssertionError("You must use exec(open(this_file).read(), {'__file__': this_file}))") + + +def set_env(key, value, encoding): + if sys.version_info[0] == 2: + value = value.encode(encoding) + os.environ[key] = value + + +# prepend bin to PATH (this file is inside the bin directory) +bin_dir = os.path.dirname(os.path.abspath(__file__)) +set_env("PATH", os.pathsep.join([bin_dir] + os.environ.get("PATH", "").split(os.pathsep)), sys.getfilesystemencoding()) + +base = os.path.dirname(bin_dir) + +# virtual env is right above bin directory +set_env("VIRTUAL_ENV", base, sys.getfilesystemencoding()) + +# add the virtual environments site-packages to the host python import mechanism +prev = set(sys.path) + +# fmt: off +# turn formatter off as json dumps will contain " characters - so we really need here ' black +site_packages = r''' +__SITE_PACKAGES__ +''' + +for site_package in json.loads(site_packages): + if sys.version_info[0] == 2: + site_package = site_package.encode('utf-8').decode(sys.getfilesystemencoding()) + path = os.path.realpath(os.path.join(os.path.dirname(__file__), site_package)) + if sys.version_info[0] == 2: + path = path.encode(sys.getfilesystemencoding()) + site.addsitedir(path) +# fmt: on + +sys.real_prefix = sys.prefix +sys.prefix = base + +# Move the added items to the front of the path, in place +new = list(sys.path) +sys.path[:] = [i for i in new if i not in prev] + [i for i in new if i in prev] diff --git a/src/virtualenv/activation/via_template.py b/src/virtualenv/activation/via_template.py new file mode 100644 index 000000000..e7b3aadf4 --- /dev/null +++ b/src/virtualenv/activation/via_template.py @@ -0,0 +1,38 @@ +from __future__ import absolute_import, unicode_literals + +import os +import pkgutil +from abc import ABCMeta, abstractmethod + +import six + +from .activator import Activator + + +@six.add_metaclass(ABCMeta) +class ViaTemplateActivator(Activator): + @abstractmethod + def templates(self): + raise NotImplementedError + + def generate(self, creator): + dest_folder = creator.bin_dir + self._generate(self.replacements(creator, dest_folder), self.templates(), dest_folder) + if self.flag_prompt is not None: + creator.pyenv_cfg["prompt"] = self.flag_prompt + + def replacements(self, creator, dest_folder): + return { + "__VIRTUAL_PROMPT__": "" if self.flag_prompt is None else self.flag_prompt, + "__VIRTUAL_ENV__": six.ensure_text(str(creator.dest_dir)), + "__VIRTUAL_NAME__": creator.env_name, + "__BIN_NAME__": six.ensure_text(str(creator.bin_name)), + "__PATH_SEP__": os.pathsep, + } + + def _generate(self, replacements, templates, to_folder): + for template in templates: + text = pkgutil.get_data(self.__module__, str(template)).decode("utf-8") + for start, end in replacements.items(): + text = text.replace(start, end) + (to_folder / template).write_text(text, encoding="utf-8") diff --git a/src/virtualenv/activation/xonosh/__init__.py b/src/virtualenv/activation/xonosh/__init__.py new file mode 100644 index 000000000..0340dcb47 --- /dev/null +++ b/src/virtualenv/activation/xonosh/__init__.py @@ -0,0 +1,14 @@ +from __future__ import absolute_import, unicode_literals + +from virtualenv.util.path import Path + +from ..via_template import ViaTemplateActivator + + +class XonoshActivator(ViaTemplateActivator): + def templates(self): + yield Path("activate.xsh") + + @classmethod + def supports(cls, interpreter): + return True if interpreter.version_info >= (3, 5) else False diff --git a/virtualenv_embedded/activate.xsh b/src/virtualenv/activation/xonosh/activate.xsh similarity index 100% rename from virtualenv_embedded/activate.xsh rename to src/virtualenv/activation/xonosh/activate.xsh diff --git a/src/virtualenv/config/__init__.py b/src/virtualenv/config/__init__.py new file mode 100644 index 000000000..01e6d4f49 --- /dev/null +++ b/src/virtualenv/config/__init__.py @@ -0,0 +1 @@ +from __future__ import absolute_import, unicode_literals diff --git a/src/virtualenv/config/cli/__init__.py b/src/virtualenv/config/cli/__init__.py new file mode 100644 index 000000000..01e6d4f49 --- /dev/null +++ b/src/virtualenv/config/cli/__init__.py @@ -0,0 +1 @@ +from __future__ import absolute_import, unicode_literals diff --git a/src/virtualenv/config/cli/parser.py b/src/virtualenv/config/cli/parser.py new file mode 100644 index 000000000..02e9cc562 --- /dev/null +++ b/src/virtualenv/config/cli/parser.py @@ -0,0 +1,64 @@ +from __future__ import absolute_import, unicode_literals + +from argparse import SUPPRESS, ArgumentDefaultsHelpFormatter, ArgumentParser + +from ..env_var import get_env_var +from ..ini import IniConfig + + +class VirtualEnvConfigParser(ArgumentParser): + """ + Custom option parser which updates its defaults by checking the configuration files and environment variables + """ + + def __init__(self, *args, **kwargs): + self.file_config = IniConfig() + self.epilog_list = [] + kwargs["epilog"] = self.file_config.epilog + kwargs["add_help"] = False + kwargs["formatter_class"] = HelpFormatter + kwargs["prog"] = "virtualenv" + super(VirtualEnvConfigParser, self).__init__(*args, **kwargs) + self._fixed = set() + + def _fix_defaults(self): + for action in self._actions: + action_id = id(action) + if action_id not in self._fixed: + self._fix_default(action) + self._fixed.add(action_id) + + def _fix_default(self, action): + if hasattr(action, "default") and hasattr(action, "dest") and action.default != SUPPRESS: + as_type = type(action.default) + outcome = get_env_var(action.dest, as_type) + if outcome is None and self.file_config: + outcome = self.file_config.get(action.dest, as_type) + if outcome is not None: + action.default, action.default_source = outcome + + def enable_help(self): + self._fix_defaults() + self.add_argument("-h", "--help", action="help", default=SUPPRESS, help="show this help message and exit") + + def parse_known_args(self, args=None, namespace=None): + self._fix_defaults() + return super(VirtualEnvConfigParser, self).parse_known_args(args, namespace=namespace) + + def parse_args(self, args=None, namespace=None): + self._fix_defaults() + return super(VirtualEnvConfigParser, self).parse_args(args, namespace=namespace) + + +class HelpFormatter(ArgumentDefaultsHelpFormatter): + def __init__(self, prog): + super(HelpFormatter, self).__init__(prog, max_help_position=37, width=240) + + def _get_help_string(self, action): + # noinspection PyProtectedMember + text = super(HelpFormatter, self)._get_help_string(action) + if hasattr(action, "default_source"): + default = " (default: %(default)s)" + if text.endswith(default): + text = "{} (default: %(default)s -> from %(default_source)s)".format(text[: -len(default)]) + return text diff --git a/src/virtualenv/config/convert.py b/src/virtualenv/config/convert.py new file mode 100644 index 000000000..c3852cd09 --- /dev/null +++ b/src/virtualenv/config/convert.py @@ -0,0 +1,69 @@ +from __future__ import absolute_import, unicode_literals + +import logging + +BOOLEAN_STATES = { + "1": True, + "yes": True, + "true": True, + "on": True, + "0": False, + "no": False, + "false": False, + "off": False, +} + + +def _convert_to_boolean(value): + if value.lower() not in BOOLEAN_STATES: + raise ValueError("Not a boolean: %s" % value) + return BOOLEAN_STATES[value.lower()] + + +def _expand_to_list(value): + if isinstance(value, (str, bytes)): + value = filter(None, [x.strip() for x in value.splitlines()]) + return list(value) + + +def _as_list(value, flatten=True): + values = _expand_to_list(value) + if not flatten: + return values # pragma: no cover + result = [] + for value in values: + sub_values = value.split() + result.extend(sub_values) + return result + + +def _as_none(value): + if not value: + return None + return str(value) + + +CONVERT = {bool: _convert_to_boolean, list: _as_list, type(None): _as_none} + + +def _get_converter(as_type): + for of_type, func in CONVERT.items(): + if issubclass(as_type, of_type): + getter = func + break + else: + getter = as_type + return getter + + +def convert(value, as_type, source): + """Convert the value as a given type where the value comes from the given source""" + getter = _get_converter(as_type) + try: + return getter(value) + except Exception as exception: + logging.warning("%s failed to convert %r as %r because %r", source, value, getter, exception) + raise + + +__all__ = ("convert",) diff --git a/src/virtualenv/config/env_var.py b/src/virtualenv/config/env_var.py new file mode 100644 index 000000000..8baa26787 --- /dev/null +++ b/src/virtualenv/config/env_var.py @@ -0,0 +1,27 @@ +from __future__ import absolute_import, unicode_literals + +import os + +from .convert import convert + + +def get_env_var(key, as_type): + """Get the environment variable option. + + :param key: the config key requested + :param as_type: the type we would like to convert it to + :return: + """ + environ_key = "VIRTUALENV_{}".format(key.upper()) + if environ_key in os.environ: + value = os.environ[environ_key] + # noinspection PyBroadException + try: + source = "env var {}".format(environ_key) + as_type = convert(value, as_type, source) + return as_type, source + except Exception: + pass + + +__all__ = ("get_env_var",) diff --git a/src/virtualenv/config/ini.py b/src/virtualenv/config/ini.py new file mode 100644 index 000000000..a799abe1d --- /dev/null +++ b/src/virtualenv/config/ini.py @@ -0,0 +1,68 @@ +from __future__ import absolute_import, unicode_literals + +import logging +import os + +from virtualenv.info import PY3, get_default_config_dir +from virtualenv.util import ConfigParser +from virtualenv.util.path import Path + +from .convert import convert + +DEFAULT_CONFIG_FILE = get_default_config_dir() / "virtualenv.ini" + + +class IniConfig(object): + VIRTUALENV_CONFIG_FILE_ENV_VAR = str("VIRTUALENV_CONFIG_FILE") + STATE = {None: "failed to parse", True: "active", False: "missing"} + + section = "virtualenv" + + def __init__(self): + config_file = os.environ.get(self.VIRTUALENV_CONFIG_FILE_ENV_VAR, None) + self.is_env_var = config_file is not None + self.config_file = Path(config_file) if config_file is not None else DEFAULT_CONFIG_FILE + self._cache = {} + + self.has_config_file = self.config_file.exists() + if self.has_config_file: + self.config_file = self.config_file.resolve() + self.config_parser = ConfigParser.ConfigParser() + try: + with self.config_file.open("rt") as file_handler: + reader = getattr(self.config_parser, "read_file" if PY3 else "readfp") + reader(file_handler) + self.has_virtualenv_section = self.config_parser.has_section(self.section) + except Exception as exception: + logging.error("failed to read config file %s because %r", config_file, exception) + self.has_config_file = None + + def get(self, key, as_type): + cache_key = key, as_type + if cache_key in self._cache: + result = self._cache[cache_key] + else: + # noinspection PyBroadException + try: + source = "file" + raw_value = self.config_parser.get(self.section, key.lower()) + value = convert(raw_value, as_type, source) + result = value, source + except Exception: + result = None + self._cache[cache_key] = result + return result + + def __bool__(self): + return bool(self.has_config_file) and bool(self.has_virtualenv_section) + + @property + def epilog(self): + msg = "{}config file {} {} (change{} via env var {})" + return msg.format( + os.linesep, + self.config_file, + self.STATE[self.has_config_file], + "d" if self.is_env_var else "", + self.VIRTUALENV_CONFIG_FILE_ENV_VAR, + ) diff --git a/src/virtualenv/error.py b/src/virtualenv/error.py new file mode 100644 index 000000000..e18e53bb6 --- /dev/null +++ b/src/virtualenv/error.py @@ -0,0 +1,12 @@ +"""Errors""" +from __future__ import absolute_import, unicode_literals + + +class ProcessCallFailed(RuntimeError): + """Failed a process call""" + + def __init__(self, code, out, err, cmd): + self.code = code + self.out = out + self.err = err + self.cmd = cmd diff --git a/src/virtualenv/info.py b/src/virtualenv/info.py new file mode 100644 index 000000000..96050b037 --- /dev/null +++ b/src/virtualenv/info.py @@ -0,0 +1,26 @@ +from __future__ import absolute_import, unicode_literals + +import sys + +from appdirs import user_config_dir, user_data_dir + +from virtualenv.util.path import Path + +IS_PYPY = hasattr(sys, "pypy_version_info") +PY3 = sys.version_info[0] == 3 +IS_WIN = sys.platform == "win32" + + +_DATA_DIR = Path(user_data_dir(appname="virtualenv", appauthor="pypa")) +_CONFIG_DIR = Path(user_config_dir(appname="virtualenv", appauthor="pypa")) + + +def get_default_data_dir(): + return _DATA_DIR + + +def get_default_config_dir(): + return _CONFIG_DIR + + +__all__ = ("IS_PYPY", "PY3", "IS_WIN", "get_default_data_dir", "get_default_config_dir") diff --git a/src/virtualenv/interpreters/__init__.py b/src/virtualenv/interpreters/__init__.py new file mode 100644 index 000000000..01e6d4f49 --- /dev/null +++ b/src/virtualenv/interpreters/__init__.py @@ -0,0 +1 @@ +from __future__ import absolute_import, unicode_literals diff --git a/src/virtualenv/interpreters/create/__init__.py b/src/virtualenv/interpreters/create/__init__.py new file mode 100644 index 000000000..01e6d4f49 --- /dev/null +++ b/src/virtualenv/interpreters/create/__init__.py @@ -0,0 +1 @@ +from __future__ import absolute_import, unicode_literals diff --git a/src/virtualenv/interpreters/create/cpython/__init__.py b/src/virtualenv/interpreters/create/cpython/__init__.py new file mode 100644 index 000000000..01e6d4f49 --- /dev/null +++ b/src/virtualenv/interpreters/create/cpython/__init__.py @@ -0,0 +1 @@ +from __future__ import absolute_import, unicode_literals diff --git a/src/virtualenv/interpreters/create/cpython/common.py b/src/virtualenv/interpreters/create/cpython/common.py new file mode 100644 index 000000000..b5796de0d --- /dev/null +++ b/src/virtualenv/interpreters/create/cpython/common.py @@ -0,0 +1,136 @@ +from __future__ import absolute_import, unicode_literals + +import abc +from os import X_OK, access, chmod + +import six + +from virtualenv.interpreters.create.via_global_ref import ViaGlobalRef +from virtualenv.util.path import Path, copy, ensure_dir, symlink + + +@six.add_metaclass(abc.ABCMeta) +class CPython(ViaGlobalRef): + def __init__(self, options, interpreter): + super(CPython, self).__init__(options, interpreter) + self.copier = symlink if self.symlinks is True else copy + + @classmethod + def supports(cls, interpreter): + return interpreter.implementation == "CPython" + + def create(self): + for directory in self.ensure_directories(): + ensure_dir(directory) + self.set_pyenv_cfg() + self.pyenv_cfg.write() + true_system_site = self.system_site_package + try: + self.system_site_package = False + self.setup_python() + finally: + if true_system_site != self.system_site_package: + self.system_site_package = true_system_site + + def ensure_directories(self): + dirs = [self.dest_dir, self.bin_dir] + dirs.extend(self.site_packages) + return dirs + + def setup_python(self): + python_dir = Path(self.interpreter.system_executable).parent + for name in self.exe_names(): + self.add_executable(python_dir, self.bin_dir, name) + + @abc.abstractmethod + def lib_name(self): + raise NotImplementedError + + @property + def lib_base(self): + raise NotImplementedError + + @property + def lib_dir(self): + return self.dest_dir / self.lib_base + + @property + def system_stdlib(self): + return Path(self.interpreter.system_prefix) / self.lib_base + + def exe_names(self): + yield Path(self.interpreter.system_executable).name + + def add_exe_method(self): + if self.copier is symlink: + return self.symlink_exe + return self.copier + + @staticmethod + def symlink_exe(src, dest): + symlink(src, dest) + dest_str = str(dest) + if not access(dest_str, X_OK): + chmod(dest_str, 0o755) # pragma: no cover + + def add_executable(self, src, dest, name): + src_ex = src / name + if src_ex.exists(): + add_exe_method_ = self.add_exe_method() + add_exe_method_(src_ex, dest / name) + + +@six.add_metaclass(abc.ABCMeta) +class CPythonPosix(CPython): + """Create a CPython virtual environment on POSIX platforms""" + + @classmethod + def supports(cls, interpreter): + return super(CPythonPosix, cls).supports(interpreter) and interpreter.os == "posix" + + @property + def bin_name(self): + return "bin" + + @property + def lib_name(self): + return "lib" + + @property + def lib_base(self): + return Path(self.lib_name) / self.interpreter.python_name + + def setup_python(self): + """Just create an exe in the provisioned virtual environment skeleton directory""" + super(CPythonPosix, self).setup_python() + major, minor = self.interpreter.version_info.major, self.interpreter.version_info.minor + target = self.bin_dir / next(self.exe_names()) + for suffix in ("python", "python{}".format(major), "python{}.{}".format(major, minor)): + path = self.bin_dir / suffix + if not path.exists(): + symlink(target, path, relative_symlinks_ok=True) + + +@six.add_metaclass(abc.ABCMeta) +class CPythonWindows(CPython): + @classmethod + def supports(cls, interpreter): + return super(CPythonWindows, cls).supports(interpreter) and interpreter.os == "nt" + + @property + def bin_name(self): + return "Scripts" + + @property + def lib_name(self): + return "Lib" + + @property + def lib_base(self): + return Path(self.lib_name) + + def exe_names(self): + yield Path(self.interpreter.system_executable).name + for name in ["python", "pythonw"]: + for suffix in ["exe"]: + yield "{}.{}".format(name, suffix) diff --git a/src/virtualenv/interpreters/create/cpython/cpython2.py b/src/virtualenv/interpreters/create/cpython/cpython2.py new file mode 100644 index 000000000..e41abbb44 --- /dev/null +++ b/src/virtualenv/interpreters/create/cpython/cpython2.py @@ -0,0 +1,69 @@ +from __future__ import absolute_import, unicode_literals + +import abc + +import six + +from virtualenv.util.path import Path, copy + +from .common import CPython, CPythonPosix, CPythonWindows + +HERE = Path(__file__).absolute().parent + + +@six.add_metaclass(abc.ABCMeta) +class CPython2(CPython): + """Create a CPython version 2 virtual environment""" + + def set_pyenv_cfg(self): + """ + We directly inject the base prefix and base exec prefix to avoid site.py needing to discover these + from home (which usually is done within the interpreter itself) + """ + super(CPython2, self).set_pyenv_cfg() + self.pyenv_cfg["base-prefix"] = self.interpreter.system_prefix + self.pyenv_cfg["base-exec-prefix"] = self.interpreter.system_exec_prefix + + @classmethod + def supports(cls, interpreter): + return super(CPython2, cls).supports(interpreter) and interpreter.version_info.major == 2 + + def setup_python(self): + super(CPython2, self).setup_python() # install the core first + self.fixup_python2() # now patch + + def add_exe_method(self): + return copy + + def fixup_python2(self): + """Perform operations needed to make the created environment work on Python 2""" + # 1. add landmarks for detecting the python home + self.add_module("os") + # 2. install a patched site-package, the default Python 2 site.py is not smart enough to understand pyvenv.cfg, + # so we inject a small shim that can do this + copy(HERE / "site.py", self.lib_dir / "site.py") + + def add_module(self, req): + for ext in self.module_extensions: + file_path = "{}.{}".format(req, ext) + self.copier(self.system_stdlib / file_path, self.lib_dir / file_path) + + @property + def module_extensions(self): + return ["py", "pyc"] + + +class CPython2Posix(CPython2, CPythonPosix): + """CPython 2 on POSIX""" + + def fixup_python2(self): + super(CPython2Posix, self).fixup_python2() + # linux needs the lib-dynload, these are builtins on Windows + self.add_folder("lib-dynload") + + def add_folder(self, folder): + self.copier(self.system_stdlib / folder, self.lib_dir / folder) + + +class CPython2Windows(CPython2, CPythonWindows): + """CPython 2 on Windows""" diff --git a/src/virtualenv/interpreters/create/cpython/cpython3.py b/src/virtualenv/interpreters/create/cpython/cpython3.py new file mode 100644 index 000000000..4d833b4d9 --- /dev/null +++ b/src/virtualenv/interpreters/create/cpython/cpython3.py @@ -0,0 +1,36 @@ +from __future__ import absolute_import, unicode_literals + +import abc + +import six + +from virtualenv.util.path import Path, copy + +from .common import CPython, CPythonPosix, CPythonWindows + + +@six.add_metaclass(abc.ABCMeta) +class CPython3(CPython): + @classmethod + def supports(cls, interpreter): + return super(CPython3, cls).supports(interpreter) and interpreter.version_info.major == 3 + + +class CPython3Posix(CPythonPosix, CPython3): + """""" + + +class CPython3Windows(CPythonWindows, CPython3): + """""" + + def setup_python(self): + super(CPython3Windows, self).setup_python() + self.include_dll() + + def include_dll(self): + dll_folder = Path(self.interpreter.system_prefix) / "DLLs" + host_exe_folder = Path(self.interpreter.system_executable).parent + for folder in [host_exe_folder, dll_folder]: + for file in folder.iterdir(): + if file.suffix in (".pyd", ".dll"): + copy(file, self.bin_dir / file.name) diff --git a/src/virtualenv/interpreters/create/cpython/site.py b/src/virtualenv/interpreters/create/cpython/site.py new file mode 100644 index 000000000..447ea8b4c --- /dev/null +++ b/src/virtualenv/interpreters/create/cpython/site.py @@ -0,0 +1,101 @@ +""" +A simple shim module to fix up things on Python 2 only. + +Note: until we setup correctly the paths we can only import built-ins. +""" +import sys + + +def main(): + """Patch what needed, and invoke the original site.py""" + config = read_pyvenv() + sys.real_prefix = sys.base_prefix = config["base-prefix"] + sys.base_exec_prefix = config["base-exec-prefix"] + global_site_package_enabled = config.get("include-system-site-packages", False) == "true" + rewrite_standard_library_sys_path() + disable_user_site_package() + load_host_site() + if global_site_package_enabled: + add_global_site_package() + + +def load_host_site(): + """trigger reload of site.py - now it will use the standard library instance that will take care of init""" + # the standard library will be the first element starting with the real prefix, not zip, must be present + import os + + std_lib = os.path.dirname(os.__file__) + std_lib_suffix = std_lib[len(sys.real_prefix) :] # strip away the real prefix to keep just the suffix + + reload(sys.modules["site"]) # noqa: F821 + + # ensure standard library suffix/site-packages is on the new path + # notably Debian derivatives change site-packages constant to dist-packages, so will not get added + target = os.path.join("{}{}".format(sys.prefix, std_lib_suffix), "site-packages") + if target not in reversed(sys.path): # if wasn't automatically added do it explicitly + sys.path.append(target) + + +def read_pyvenv(): + """read pyvenv.cfg""" + os_sep = "\\" if sys.platform == "win32" else "/" # no os module here yet - poor mans version + config_file = "{}{}pyvenv.cfg".format(sys.prefix, os_sep) + with open(config_file) as file_handler: + lines = file_handler.readlines() + config = {} + for line in lines: + try: + split_at = line.index("=") + except ValueError: + continue # ignore bad/empty lines + else: + config[line[:split_at].strip()] = line[split_at + 1 :].strip() + return config + + +def rewrite_standard_library_sys_path(): + """Once this site file is loaded the standard library paths have already been set, fix them up""" + sep = "\\" if sys.platform == "win32" else "/" + exe_dir = sys.executable[: sys.executable.rfind(sep)] + for at, value in enumerate(sys.path): + # replace old sys prefix path starts with new + if value == exe_dir: + pass # don't fix the current executable location, notably on Windows this gets added + elif value.startswith(sys.prefix): + value = "{}{}".format(sys.base_prefix, value[len(sys.prefix) :]) + elif value.startswith(sys.exec_prefix): + value = "{}{}".format(sys.base_exec_prefix, value[len(sys.exec_prefix) :]) + sys.path[at] = value + + +def disable_user_site_package(): + """Flip the switch on enable user site package""" + # sys.flags is a c-extension type, so we cannot monkey patch it, replace it with a python class to flip it + sys.original_flags = sys.flags + + class Flags(object): + def __init__(self): + self.__dict__ = {key: getattr(sys.flags, key) for key in dir(sys.flags) if not key.startswith("_")} + + sys.flags = Flags() + sys.flags.no_user_site = 1 + + +def add_global_site_package(): + """add the global site package""" + import site + + # add user site package + sys.flags = sys.original_flags # restore original + site.ENABLE_USER_SITE = None # reset user site check + # add the global site package to the path - use new prefix and delegate to site.py + orig_prefixes = None + try: + orig_prefixes = site.PREFIXES + site.PREFIXES = [sys.base_prefix, sys.base_exec_prefix] + site.main() + finally: + site.PREFIXES = orig_prefixes + + +main() diff --git a/src/virtualenv/interpreters/create/creator.py b/src/virtualenv/interpreters/create/creator.py new file mode 100644 index 000000000..fa3caa056 --- /dev/null +++ b/src/virtualenv/interpreters/create/creator.py @@ -0,0 +1,182 @@ +from __future__ import absolute_import, print_function, unicode_literals + +import json +import logging +import os +import shutil +import sys +from abc import ABCMeta, abstractmethod +from argparse import ArgumentTypeError + +import six +from six import add_metaclass + +from virtualenv.info import IS_WIN +from virtualenv.pyenv_cfg import PyEnvCfg +from virtualenv.util.path import Path +from virtualenv.util.subprocess import run_cmd +from virtualenv.version import __version__ + +HERE = Path(__file__).absolute().parent +DEBUG_SCRIPT = HERE / "debug.py" + + +@add_metaclass(ABCMeta) +class Creator(object): + def __init__(self, options, interpreter): + self.interpreter = interpreter + self._debug = None + self.dest_dir = Path(options.dest_dir) + self.system_site_package = options.system_site + self.clear = options.clear + self.pyenv_cfg = PyEnvCfg.from_folder(self.dest_dir) + + @classmethod + def add_parser_arguments(cls, parser, interpreter): + parser.add_argument( + "--clear", + dest="clear", + action="store_true", + help="clear out the non-root install and start from scratch", + default=False, + ) + + parser.add_argument( + "--system-site-packages", + default=False, + action="store_true", + dest="system_site", + help="Give the virtual environment access to the system site-packages dir.", + ) + + parser.add_argument( + "dest_dir", help="directory to create virtualenv at", type=cls.validate_dest_dir, default="env", nargs="?", + ) + + @classmethod + def validate_dest_dir(cls, raw_value): + """No path separator in the path, valid chars and must be write-able""" + + def non_write_able(dest, value): + common = Path(*os.path.commonprefix([value.parts, dest.parts])) + raise ArgumentTypeError( + "the destination {} is not write-able at {}".format(dest.relative_to(common), common) + ) + + # the file system must be able to encode + # note in newer CPython this is always utf-8 https://www.python.org/dev/peps/pep-0529/ + encoding = sys.getfilesystemencoding() + path_converted = raw_value.encode(encoding, errors="ignore").decode(encoding) + if path_converted != raw_value: + refused = set(raw_value) - { + c + for c, i in ((char, char.encode(encoding)) for char in raw_value) + if c == "?" or i != six.ensure_str("?") + } + raise ArgumentTypeError( + "the file system codec ({}) does not support characters {!r}".format(encoding, refused) + ) + if os.pathsep in raw_value: + raise ArgumentTypeError( + "destination {!r} must not contain the path separator ({}) as this would break " + "the activation scripts".format(raw_value, os.pathsep) + ) + + value = Path(raw_value) + if value.exists() and value.is_file(): + raise ArgumentTypeError("the destination {} already exists and is a file".format(value)) + if (3, 3) <= sys.version_info <= (3, 6): + # pre 3.6 resolve is always strict, aka must exists, sidestep by using os.path operation + dest = Path(os.path.realpath(raw_value)) + else: + dest = value.resolve() + value = dest + while dest: + if dest.exists(): + if os.access(six.ensure_text(str(dest)), os.W_OK): + break + else: + non_write_able(dest, value) + base, _ = dest.parent, dest.name + if base == dest: + non_write_able(dest, value) # pragma: no cover + dest = base + return str(value) + + def run(self): + if self.dest_dir.exists() and self.clear: + logging.debug("delete %s", self.dest_dir) + shutil.rmtree(str(self.dest_dir), ignore_errors=True) + self.create() + self.set_pyenv_cfg() + + @abstractmethod + def create(self): + raise NotImplementedError + + @classmethod + def supports(cls, interpreter): + raise NotImplementedError + + def set_pyenv_cfg(self): + self.pyenv_cfg.content = { + "home": self.interpreter.system_exec_prefix, + "include-system-site-packages": "true" if self.system_site_package else "false", + "implementation": self.interpreter.implementation, + "virtualenv": __version__, + } + + @property + def env_name(self): + return six.ensure_text(self.dest_dir.parts[-1]) + + @property + def bin_name(self): + raise NotImplementedError + + @property + def bin_dir(self): + return self.dest_dir / self.bin_name + + @property + def lib_dir(self): + raise NotImplementedError + + @property + def site_packages(self): + return [self.lib_dir / "site-packages"] + + @property + def exe(self): + return self.bin_dir / "python{}".format(".exe" if IS_WIN else "") + + @property + def debug(self): + if self._debug is None: + self._debug = get_env_debug_info(self.exe, self.debug_script()) + return self._debug + + # noinspection PyMethodMayBeStatic + def debug_script(self): + return DEBUG_SCRIPT + + +def get_env_debug_info(env_exe, debug_script): + cmd = [six.ensure_text(str(env_exe)), six.ensure_text(str(debug_script))] + logging.debug(" ".join(six.ensure_text(i) for i in cmd)) + env = os.environ.copy() + env.pop("PYTHONPATH", None) + code, out, err = run_cmd(cmd) + # noinspection PyBroadException + try: + if code != 0: + result = eval(out) + else: + result = json.loads(out) + if err: + result["err"] = err + except Exception as exception: + return {"out": out, "err": err, "returncode": code, "exception": repr(exception)} + if "sys" in result and "path" in result["sys"]: + del result["sys"]["path"][0] + return result diff --git a/src/virtualenv/interpreters/create/debug.py b/src/virtualenv/interpreters/create/debug.py new file mode 100644 index 000000000..d9f6d525e --- /dev/null +++ b/src/virtualenv/interpreters/create/debug.py @@ -0,0 +1,71 @@ +"""Inspect a target Python interpreter virtual environment wise""" +import sys # built-in + + +def encode_path(value): + if value is None: + return None + if isinstance(value, bytes): + return value.decode(sys.getfilesystemencoding()) + elif not isinstance(value, str): + return repr(value if isinstance(value, type) else type(value)) + return value + + +def encode_list_path(value): + return [encode_path(i) for i in value] + + +def run(): + """print debug data about the virtual environment""" + try: + from collections import OrderedDict + except ImportError: # pragma: no cover + # this is possible if the standard library cannot be accessed + # noinspection PyPep8Naming + OrderedDict = dict # pragma: no cover + result = OrderedDict([("sys", OrderedDict())]) + path_keys = ( + "executable", + "_base_executable", + "prefix", + "base_prefix", + "real_prefix", + "exec_prefix", + "base_exec_prefix", + "path", + "meta_path", + ) + for key in path_keys: + value = getattr(sys, key, None) + if isinstance(value, list): + value = encode_list_path(value) + else: + value = encode_path(value) + result["sys"][key] = value + result["version"] = sys.version + import os # landmark + + result["os"] = os.__file__ + + try: + # noinspection PyUnresolvedReferences + import site # site + + result["site"] = site.__file__ + except ImportError as exception: # pragma: no cover + result["site"] = repr(exception) # pragma: no cover + # try to print out, this will validate if other core modules are available (json in this case) + try: + import json + + result["json"] = repr(json) + print(json.dumps(result, indent=2)) + except (ImportError, ValueError, TypeError) as exception: # pragma: no cover + result["json"] = repr(exception) # pragma: no cover + print(repr(result)) # pragma: no cover + raise SystemExit(1) # pragma: no cover + + +if __name__ == "__main__": + run() diff --git a/src/virtualenv/interpreters/create/util.py b/src/virtualenv/interpreters/create/util.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/virtualenv/interpreters/create/venv.py b/src/virtualenv/interpreters/create/venv.py new file mode 100644 index 000000000..feb160b4b --- /dev/null +++ b/src/virtualenv/interpreters/create/venv.py @@ -0,0 +1,72 @@ +from __future__ import absolute_import, unicode_literals + +import logging +from copy import copy + +from virtualenv.error import ProcessCallFailed +from virtualenv.interpreters.discovery.py_info import CURRENT +from virtualenv.util.subprocess import run_cmd + +from .via_global_ref import ViaGlobalRef + + +class Venv(ViaGlobalRef): + def __init__(self, options, interpreter): + super(Venv, self).__init__(options, interpreter) + self.can_be_inline = interpreter is CURRENT and interpreter.executable == interpreter.system_executable + self._context = None + + @classmethod + def supports(cls, interpreter): + return interpreter.has_venv + + def create(self): + if self.can_be_inline: + self.create_inline() + else: + self.create_via_sub_process() + # TODO: cleanup activation scripts + + def create_inline(self): + from venv import EnvBuilder + + builder = EnvBuilder( + system_site_packages=self.system_site_package, + clear=False, + symlinks=self.symlinks, + with_pip=False, + prompt=None, + ) + builder.create(self.dest_dir) + + def create_via_sub_process(self): + cmd = self.get_host_create_cmd() + logging.info("using host built-in venv to create via %s", " ".join(cmd)) + code, out, err = run_cmd(cmd) + if code != 0: + raise ProcessCallFailed(code, out, err, cmd) + + def get_host_create_cmd(self): + cmd = [self.interpreter.system_executable, "-m", "venv", "--without-pip"] + if self.system_site_package: + cmd.append("--system-site-packages") + cmd.append("--symlinks" if self.symlinks else "--copies") + cmd.append(str(self.dest_dir)) + return cmd + + def set_pyenv_cfg(self): + # prefer venv options over ours, but keep our extra + venv_content = copy(self.pyenv_cfg.refresh()) + super(Venv, self).set_pyenv_cfg() + self.pyenv_cfg.update(venv_content) + + @property + def bin_name(self): + return "Scripts" if self.interpreter.os == "nt" else "bin" + + @property + def lib_dir(self): + base = self.dest_dir / ("Lib" if self.interpreter.os == "nt" else "lib") + if self.interpreter.os != "nt": + base = base / self.interpreter.python_name + return base diff --git a/src/virtualenv/interpreters/create/via_global_ref.py b/src/virtualenv/interpreters/create/via_global_ref.py new file mode 100644 index 000000000..240b313a0 --- /dev/null +++ b/src/virtualenv/interpreters/create/via_global_ref.py @@ -0,0 +1,35 @@ +from __future__ import absolute_import, unicode_literals + +from abc import ABCMeta + +from six import add_metaclass + +from .creator import Creator + + +@add_metaclass(ABCMeta) +class ViaGlobalRef(Creator): + def __init__(self, options, interpreter): + super(ViaGlobalRef, self).__init__(options, interpreter) + self.symlinks = options.symlinks + + @classmethod + def add_parser_arguments(cls, parser, interpreter): + super(ViaGlobalRef, cls).add_parser_arguments(parser, interpreter) + group = parser.add_mutually_exclusive_group() + symlink = False if interpreter.os == "nt" else True + group.add_argument( + "--symlinks", + default=symlink, + action="store_true", + dest="symlinks", + help="Try to use symlinks rather than copies, when symlinks are not the default for the platform.", + ) + group.add_argument( + "--copies", + "--always-copy", + default=not symlink, + action="store_false", + dest="symlinks", + help="Try to use copies rather than symlinks, even when symlinks are the default for the platform.", + ) diff --git a/src/virtualenv/interpreters/discovery/__init__.py b/src/virtualenv/interpreters/discovery/__init__.py new file mode 100644 index 000000000..01e6d4f49 --- /dev/null +++ b/src/virtualenv/interpreters/discovery/__init__.py @@ -0,0 +1 @@ +from __future__ import absolute_import, unicode_literals diff --git a/src/virtualenv/interpreters/discovery/builtin.py b/src/virtualenv/interpreters/discovery/builtin.py new file mode 100644 index 000000000..b90b05045 --- /dev/null +++ b/src/virtualenv/interpreters/discovery/builtin.py @@ -0,0 +1,135 @@ +from __future__ import absolute_import, unicode_literals + +import logging +import os +import sys + +import six + +from virtualenv.info import IS_WIN + +from .discover import Discover +from .py_info import CURRENT, PythonInfo +from .py_spec import PythonSpec + + +class Builtin(Discover): + def __init__(self, options): + super(Builtin, self).__init__() + self.python_spec = options.python + + @classmethod + def add_parser_arguments(cls, parser): + parser.add_argument( + "-p", + "--python", + dest="python", + metavar="py", + help="target interpreter for which to create a virtual (either absolute path or identifier string)", + default=sys.executable, + ) + + def run(self): + return get_interpreter(self.python_spec) + + def __str__(self): + return "{} discover of python_spec={!r}".format(self.__class__.__name__, self.python_spec) + + +def get_interpreter(key): + spec = PythonSpec.from_string_spec(key) + logging.info("find interpreter for spec %r", spec) + proposed_paths = set() + for interpreter, impl_must_match in propose_interpreters(spec): + if interpreter.executable not in proposed_paths: + logging.debug("proposed %s", interpreter) + if interpreter.satisfies(spec, impl_must_match): + logging.info("accepted target interpreter %s", interpreter) + return interpreter + proposed_paths.add(interpreter.executable) + + +def propose_interpreters(spec): + # 1. we always try with the lowest hanging fruit first, the current interpreter + yield CURRENT, True + + # 2. if it's an absolute path and exists, use that + if spec.is_abs and os.path.exists(spec.path): + yield PythonInfo.from_exe(spec.path), True + + # 3. otherwise fallback to platform default logic + if IS_WIN: + from .windows import propose_interpreters + + for interpreter in propose_interpreters(spec): + yield interpreter, True + + paths = get_paths() + # find on path, the path order matters (as the candidates are less easy to control by end user) + for pos, path in enumerate(paths): + path = six.ensure_text(path) + logging.debug(LazyPathDump(pos, path)) + for candidate, match in possible_specs(spec): + found = check_path(candidate, path) + if found is not None: + exe = os.path.abspath(found) + interpreter = PathPythonInfo.from_exe(exe, raise_on_error=False) + if interpreter is not None: + yield interpreter, match + + +def get_paths(): + path = os.environ.get(str("PATH"), None) + if path is None: + try: + path = os.confstr("CS_PATH") + except (AttributeError, ValueError): + path = os.defpath + if not path: + paths = [] + else: + paths = [p for p in path.split(os.pathsep) if os.path.exists(p)] + return paths + + +class LazyPathDump(object): + def __init__(self, pos, path): + self.pos = pos + self.path = path + + def __str__(self): + content = "discover from PATH[{}]:{} with =>".format(self.pos, self.path) + for file_name in os.listdir(self.path): + try: + file_path = os.path.join(self.path, file_name) + if os.path.isdir(file_path) or not os.access(file_path, os.X_OK): + continue + except OSError: + pass + content += " " + content += file_name + return content + + +def check_path(candidate, path): + _, ext = os.path.splitext(candidate) + if sys.platform == "win32" and ext != ".exe": + candidate = candidate + ".exe" + if os.path.isfile(candidate): + return candidate + candidate = os.path.join(path, candidate) + if os.path.isfile(candidate): + return candidate + return None + + +def possible_specs(spec): + # 4. then maybe it's something exact on PATH - if it was direct lookup implementation no longer counts + yield spec.str_spec, False + # 5. or from the spec we can deduce a name on path that matches + for exe, match in spec.generate_names(): + yield exe, match + + +class PathPythonInfo(PythonInfo): + """""" diff --git a/src/virtualenv/interpreters/discovery/discover.py b/src/virtualenv/interpreters/discovery/discover.py new file mode 100644 index 000000000..13e258f38 --- /dev/null +++ b/src/virtualenv/interpreters/discovery/discover.py @@ -0,0 +1,27 @@ +from __future__ import absolute_import, unicode_literals + +from abc import ABCMeta, abstractmethod + +import six + + +@six.add_metaclass(ABCMeta) +class Discover(object): + def __init__(self): + self._has_run = False + self._interpreter = None + + @classmethod + def add_parser_arguments(cls, parser): + raise NotImplementedError + + @abstractmethod + def run(self): + raise NotImplementedError + + @property + def interpreter(self): + if self._has_run is False: + self._interpreter = self.run() + self._has_run = True + return self._interpreter diff --git a/src/virtualenv/interpreters/discovery/py_info.py b/src/virtualenv/interpreters/discovery/py_info.py new file mode 100644 index 000000000..6f9bcfe56 --- /dev/null +++ b/src/virtualenv/interpreters/discovery/py_info.py @@ -0,0 +1,256 @@ +""" +The PythonInfo contains information about a concrete instance of a Python interpreter + +Note: this file is also used to query target interpreters, so can only use standard library methods +""" +from __future__ import absolute_import, print_function, unicode_literals + +import copy +import json +import logging +import os +import platform +import sys +from collections import OrderedDict, namedtuple + +VersionInfo = namedtuple("VersionInfo", ["major", "minor", "micro", "releaselevel", "serial"]) + + +def _get_path_extensions(): + return list(OrderedDict.fromkeys([""] + os.environ.get("PATHEXT", "").lower().split(os.pathsep))) + + +EXTENSIONS = _get_path_extensions() + + +class PythonInfo(object): + """Contains information for a Python interpreter""" + + def __init__(self): + # qualifies the python + self.platform = sys.platform + self.implementation = platform.python_implementation() + + # this is a tuple in earlier, struct later, unify to our own named tuple + self.version_info = VersionInfo(*list(sys.version_info)) + self.architecture = 64 if sys.maxsize > 2 ** 32 else 32 + + self.executable = sys.executable # executable we were called with + self.original_executable = self.executable + self.base_executable = getattr(sys, "_base_executable", None) # some platforms may set this + + self.version = sys.version + self.os = os.name + + # information about the prefix - determines python home + self.prefix = getattr(sys, "prefix", None) # prefix we think + self.base_prefix = getattr(sys, "base_prefix", None) # venv + self.real_prefix = getattr(sys, "real_prefix", None) # old virtualenv + + # information about the exec prefix - dynamic stdlib modules + self.base_exec_prefix = getattr(sys, "base_exec_prefix", None) + self.exec_prefix = getattr(sys, "exec_prefix", None) + + try: + __import__("venv") + has = True + except ImportError: + has = False + self.has_venv = has + self.path = sys.path + + @property + def version_str(self): + return ".".join(str(i) for i in self.version_info[:3]) + + @property + def version_release_str(self): + return ".".join(str(i) for i in self.version_info[:2]) + + @property + def python_name(self): + version_info = self.version_info + return "python{}.{}".format(version_info.major, version_info.minor) + + @property + def is_old_virtualenv(self): + return self.real_prefix is not None + + @property + def is_venv(self): + return self.base_prefix is not None and self.version_info.major == 3 + + def __repr__(self): + return "{}({!r})".format(self.__class__.__name__, self.__dict__) + + def __str__(self): + return "{}({})".format( + self.__class__.__name__, + ", ".join( + "{}={}".format(k, v) + for k, v in ( + ( + "spec", + "{}{}-{}".format( + self.implementation, ".".join(str(i) for i in self.version_info), self.architecture + ), + ), + ("exe", self.executable), + ("original" if self.original_executable != self.executable else None, self.original_executable), + ( + "base" + if self.base_executable is not None and self.base_executable != self.executable + else None, + self.base_executable, + ), + ("platform", self.platform), + ("version", repr(self.version)), + ) + if k is not None + ), + ) + + def to_json(self): + data = copy.deepcopy(self.__dict__) + # noinspection PyProtectedMember + data["version_info"] = data["version_info"]._asdict() # namedtuple to dictionary + return json.dumps(data, indent=2) + + @classmethod + def from_json(cls, payload): + data = json.loads(payload) + data["version_info"] = VersionInfo(**data["version_info"]) # restore this to a named tuple structure + info = copy.deepcopy(CURRENT) + info.__dict__ = data + return info + + @property + def system_prefix(self): + return self.real_prefix or self.base_prefix or self.prefix + + @property + def system_exec_prefix(self): + return self.real_prefix or self.base_exec_prefix or self.exec_prefix + + @property + def system_executable(self): + env_prefix = self.real_prefix or self.base_prefix + if env_prefix: # if this is a virtual environment + if self.real_prefix is None and self.base_executable is not None: # use the saved host if present + return self.base_executable + # otherwise fallback to discovery mechanism + return self.find_exe_based_of(inside_folder=env_prefix) + else: + # need original executable here, as if we need to copy we want to copy the interpreter itself, not the + # setup script things may be wrapped up in + return self.original_executable + + def find_exe_based_of(self, inside_folder): + # we don't know explicitly here, do some guess work - our executable name should tell + possible_names = self._find_possible_exe_names() + possible_folders = self._find_possible_folders(inside_folder) + for folder in possible_folders: + for name in possible_names: + candidate = os.path.join(folder, name) + if os.path.exists(candidate): + info = PythonInfo.from_exe(candidate) + keys = {"implementation", "architecture", "version_info"} + if all(getattr(info, k) == getattr(self, k) for k in keys): + return candidate + what = "|".join(possible_names) # pragma: no cover + raise RuntimeError("failed to detect {} in {}".format(what, "|".join(possible_folders))) # pragma: no cover + + def _find_possible_folders(self, inside_folder): + candidate_folder = OrderedDict() + base = os.path.dirname(self.executable) + # following path pattern of the current + if base.startswith(self.prefix): + relative = base[len(self.prefix) :] + candidate_folder["{}{}".format(inside_folder, relative)] = None + + # or at root level + candidate_folder[inside_folder] = None + return list(candidate_folder.keys()) + + def _find_possible_exe_names(self): + name_candidate = OrderedDict() + for name in [self.implementation, "python"]: + for at in range(3, -1, -1): + version = ".".join(str(i) for i in self.version_info[:at]) + for arch in ["-{}".format(self.architecture), ""]: + for ext in EXTENSIONS: + candidate = "{}{}{}{}".format(name, version, arch, ext) + name_candidate[candidate] = None + return list(name_candidate.keys()) + + _cache_from_exe = {} + + @classmethod + def from_exe(cls, exe, raise_on_error=True): + key = os.path.realpath(exe) + if key in cls._cache_from_exe: + result, failure = cls._cache_from_exe[key] + else: + failure, result = cls._load_for_exe(exe) + cls._cache_from_exe[key] = result, failure + if failure is not None: + if raise_on_error: + raise failure + else: + logging.debug("%s", str(failure)) + return result + + @classmethod + def _load_for_exe(cls, exe): + from virtualenv.util.subprocess import subprocess, Popen + + path = "{}.py".format(os.path.splitext(__file__)[0]) + cmd = [exe, path] + # noinspection DuplicatedCode + # this is duplicated here because this file is executed on its own, so cannot be refactored otherwise + try: + process = Popen( + cmd, universal_newlines=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE + ) + out, err = process.communicate() + code = process.returncode + except OSError as os_error: + out, err, code = "", os_error.strerror, os_error.errno + result, failure = None, None + if code == 0: + result = cls.from_json(out) + result.executable = exe # keep original executable as this may contain initialization code + else: + msg = "failed to query {} with code {}{}{}".format( + exe, code, " out: []".format(out) if out else "", " err: []".format(err) if err else "" + ) + failure = RuntimeError(msg) + return failure, result + + def satisfies(self, spec, impl_must_match): + """check if a given specification can be satisfied by the this python interpreter instance""" + if self.executable == spec.path: # if the path is a our own executable path we're done + return True + + if spec.path is not None: # if path set, and is not our original executable name, this does not match + root, _ = os.path.splitext(os.path.basename(self.original_executable)) + if root != spec.path: + return False + + if impl_must_match: + if spec.implementation is not None and spec.implementation != self.implementation: + return False + + if spec.architecture is not None and spec.architecture != self.architecture: + return False + + for our, req in zip(self.version_info[0:3], (spec.major, spec.minor, spec.patch)): + if req is not None and our is not None and our != req: + return False + return True + + +CURRENT = PythonInfo() + +if __name__ == "__main__": + print(CURRENT.to_json()) diff --git a/src/virtualenv/interpreters/discovery/py_spec.py b/src/virtualenv/interpreters/discovery/py_spec.py new file mode 100644 index 000000000..54155781b --- /dev/null +++ b/src/virtualenv/interpreters/discovery/py_spec.py @@ -0,0 +1,115 @@ +"""A Python specification is an abstract requirement definition of a interpreter""" +from __future__ import absolute_import, unicode_literals + +import os +import re +import sys +from collections import OrderedDict + +PATTERN = re.compile(r"^(?P[a-zA-Z]+)?(?P[0-9.]+)?(?:-(?P32|64))?$") +IS_WIN = sys.platform == "win32" + + +class PythonSpec(object): + """Contains specification about a Python Interpreter""" + + def __init__(self, str_spec, implementation, major, minor, patch, architecture, path): + self.str_spec = str_spec + self.implementation = implementation + self.major = major + self.minor = minor + self.patch = patch + self.architecture = architecture + self.path = path + + @classmethod + def from_string_spec(cls, string_spec): + impl = major = minor = patch = arch = path = None + if os.path.isabs(string_spec): + path = string_spec + else: + ok = False + match = re.match(PATTERN, string_spec) + if match: + + def _int_or_none(val): + return None if val is None else int(val) + + try: + groups = match.groupdict() + version = groups["version"] + if version is not None: + versions = tuple(int(i) for i in version.split(".") if i) + if len(versions) > 3: + raise ValueError + if len(versions) == 3: + major, minor, patch = versions + elif len(versions) == 2: + major, minor = versions + elif len(versions) == 1: + version_data = versions[0] + major = int(str(version_data)[0]) # first digit major + if version_data > 9: + minor = int(str(version_data)[1:]) + ok = True + except ValueError: + pass + else: + impl = groups["impl"] + if impl == "py" or impl == "python": + impl = "CPython" + arch = _int_or_none(groups["arch"]) + + if not ok: + path = string_spec + + return cls(string_spec, impl, major, minor, patch, arch, path) + + def generate_names(self): + impls = OrderedDict() + if self.implementation: + # first consider implementation as it is + impls[self.implementation] = False + # for case sensitive file systems consider lower and upper case versions too + # trivia: MacBooks and all pre 2018 Windows-es were case insensitive by default + impls[self.implementation.lower()] = False + impls[self.implementation.upper()] = False + impls["python"] = True # finally consider python as alias, implementation must match now + version = self.major, self.minor, self.patch + try: + version = version[: version.index(None)] + except ValueError: + pass + for impl, match in impls.items(): + for at in range(len(version), -1, -1): + cur_ver = version[0:at] + spec = "{}{}".format(impl, ".".join(str(i) for i in cur_ver)) + yield spec, match + + @property + def is_abs(self): + return self.path is not None and os.path.isabs(self.path) + + def satisfies(self, spec): + """called when there's a candidate metadata spec to see if compatible - e.g. PEP-514 on Windows""" + if spec.is_abs and self.is_abs and self.path != spec.path: + return False + if spec.implementation is not None and spec.implementation != self.implementation: + return False + if spec.architecture is not None and spec.architecture != self.architecture: + return False + + for our, req in zip((self.major, self.minor, self.patch), (spec.major, spec.minor, spec.patch)): + if req is not None and our is not None and our != req: + return False + return True + + def __repr__(self): + return "{}({})".format( + type(self).__name__, + ", ".join( + "{}={}".format(k, getattr(self, k)) + for k in ("str_spec", "implementation", "major", "minor", "patch", "architecture", "path") + if getattr(self, k) is not None + ), + ) diff --git a/src/virtualenv/interpreters/discovery/windows/__init__.py b/src/virtualenv/interpreters/discovery/windows/__init__.py new file mode 100644 index 000000000..f321d7f82 --- /dev/null +++ b/src/virtualenv/interpreters/discovery/windows/__init__.py @@ -0,0 +1,21 @@ +from __future__ import absolute_import, unicode_literals + +from ..py_info import PythonInfo +from ..py_spec import PythonSpec +from .pep514 import discover_pythons + + +class Pep514PythonInfo(PythonInfo): + """""" + + +def propose_interpreters(spec): + # see if PEP-514 entries are good + for name, major, minor, arch, exe, _ in discover_pythons(): + # pre-filter + registry_spec = PythonSpec(None, name, major, minor, None, arch, exe) + if registry_spec.satisfies(spec): + interpreter = Pep514PythonInfo.from_exe(exe, raise_on_error=False) + if interpreter is not None: + if interpreter.satisfies(spec, impl_must_match=True): + yield interpreter diff --git a/src/virtualenv/interpreters/discovery/windows/pep514.py b/src/virtualenv/interpreters/discovery/windows/pep514.py new file mode 100644 index 000000000..8bdd30cad --- /dev/null +++ b/src/virtualenv/interpreters/discovery/windows/pep514.py @@ -0,0 +1,162 @@ +"""Implement https://www.python.org/dev/peps/pep-0514/ to discover interpreters - Windows only""" +from __future__ import absolute_import, print_function, unicode_literals + +import os +import re +from logging import basicConfig, getLogger + +import six + +if six.PY3: + import winreg +else: + # noinspection PyUnresolvedReferences + import _winreg as winreg + +LOGGER = getLogger(__name__) + + +def enum_keys(key): + at = 0 + while True: + try: + yield winreg.EnumKey(key, at) + except OSError: + break + at += 1 + + +def get_value(key, value_name): + try: + return winreg.QueryValueEx(key, value_name)[0] + except OSError: + return None + + +def discover_pythons(): + for hive, hive_name, key, flags, default_arch in [ + (winreg.HKEY_CURRENT_USER, "HKEY_CURRENT_USER", r"Software\Python", 0, 64), + (winreg.HKEY_LOCAL_MACHINE, "HKEY_LOCAL_MACHINE", r"Software\Python", winreg.KEY_WOW64_64KEY, 64), + (winreg.HKEY_LOCAL_MACHINE, "HKEY_LOCAL_MACHINE", r"Software\Python", winreg.KEY_WOW64_32KEY, 32), + ]: + for spec in process_set(hive, hive_name, key, flags, default_arch): + yield spec + + +def process_set(hive, hive_name, key, flags, default_arch): + try: + with winreg.OpenKeyEx(hive, key, 0, winreg.KEY_READ | flags) as root_key: + for company in enum_keys(root_key): + if company == "PyLauncher": # reserved + continue + for spec in process_company(hive_name, company, root_key, default_arch): + yield spec + except OSError: + pass + + +def process_company(hive_name, company, root_key, default_arch): + with winreg.OpenKeyEx(root_key, company) as company_key: + for tag in enum_keys(company_key): + spec = process_tag(hive_name, company, company_key, tag, default_arch) + if spec is not None: + yield spec + + +def process_tag(hive_name, company, company_key, tag, default_arch): + with winreg.OpenKeyEx(company_key, tag) as tag_key: + version = load_version_data(hive_name, company, tag, tag_key) + if version is not None: # if failed to get version bail + major, minor, _ = version + arch = load_arch_data(hive_name, company, tag, tag_key, default_arch) + if arch is not None: + exe_data = load_exe(hive_name, company, company_key, tag) + if exe_data is not None: + exe, args = exe_data + name = str("python") if company == "PythonCore" else company + return name, major, minor, arch, exe, args + + +def load_exe(hive_name, company, company_key, tag): + key_path = "{}/{}/{}".format(hive_name, company, tag) + try: + with winreg.OpenKeyEx(company_key, r"{}\InstallPath".format(tag)) as ip_key: + with ip_key: + exe = get_value(ip_key, "ExecutablePath") + if exe is None: + ip = get_value(ip_key, None) + if ip is None: + msg(key_path, "no ExecutablePath or default for it") + + else: + exe = os.path.join(ip, str("python.exe")) + if exe is not None and os.path.exists(exe): + args = get_value(ip_key, "ExecutableArguments") + return exe, args + else: + msg(key_path, "exe does not exists {}".format(key_path, exe)) + except OSError: + msg("{}/{}".format(key_path, "InstallPath"), "missing") + return None + + +def load_arch_data(hive_name, company, tag, tag_key, default_arch): + arch_str = get_value(tag_key, "SysArchitecture") + if arch_str is not None: + key_path = "{}/{}/{}/SysArchitecture".format(hive_name, company, tag) + try: + return parse_arch(arch_str) + except ValueError as sys_arch: + msg(key_path, sys_arch) + return default_arch + + +def parse_arch(arch_str): + if isinstance(arch_str, six.string_types): + match = re.match(r"^(\d+)bit$", arch_str) + if match: + return int(next(iter(match.groups()))) + error = "invalid format {}".format(arch_str) + else: + error = "arch is not string: {}".format(repr(arch_str)) + raise ValueError(error) + + +def load_version_data(hive_name, company, tag, tag_key): + for candidate, key_path in [ + (get_value(tag_key, "SysVersion"), "{}/{}/{}/SysVersion".format(hive_name, company, tag)), + (tag, "{}/{}/{}".format(hive_name, company, tag)), + ]: + if candidate is not None: + try: + return parse_version(candidate) + except ValueError as sys_version: + msg(key_path, sys_version) + return None + + +def parse_version(version_str): + if isinstance(version_str, six.string_types): + match = re.match(r"^(\d+)(?:\.(\d+))?(?:\.(\d+))?$", version_str) + if match: + return tuple(int(i) if i is not None else None for i in match.groups()) + error = "invalid format {}".format(version_str) + else: + error = "version is not string: {}".format(repr(version_str)) + raise ValueError(error) + + +def msg(path, what): + LOGGER.warning("PEP-514 violation in Windows Registry at {} error: {}".format(path, what)) + + +def _run(): + basicConfig() + interpreters = [] + for spec in discover_pythons(): + interpreters.append(repr(spec)) + print("\n".join(sorted(interpreters))) + + +if __name__ == "__main__": + _run() diff --git a/src/virtualenv/pyenv_cfg.py b/src/virtualenv/pyenv_cfg.py new file mode 100644 index 000000000..05daa5a3e --- /dev/null +++ b/src/virtualenv/pyenv_cfg.py @@ -0,0 +1,56 @@ +from __future__ import absolute_import, unicode_literals + +import logging + +import six + + +class PyEnvCfg(object): + def __init__(self, content, path): + self.content = content + self.path = path + + @classmethod + def from_folder(cls, folder): + return cls.from_file(folder / "pyvenv.cfg") + + @classmethod + def from_file(cls, path): + content = cls._read_values(path) if path.exists() else {} + return PyEnvCfg(content, path) + + @staticmethod + def _read_values(path): + content = {} + for line in path.read_text(encoding="utf-8").splitlines(): + equals_at = line.index("=") + key = line[:equals_at].strip() + value = line[equals_at + 1 :].strip() + content[key] = value + return content + + def write(self): + with open(six.ensure_text(str(self.path)), "wb") as file_handler: + logging.debug("write %s", six.ensure_text(str(self.path))) + for key, value in self.content.items(): + line = "{} = {}".format(key, value) + logging.debug("\t%s", line) + file_handler.write(line.encode("utf-8")) + file_handler.write(b"\n") + + def refresh(self): + self.content = self._read_values(self.path) + return self.content + + def __setitem__(self, key, value): + self.content[key] = value + + def __getitem__(self, key): + return self.content[key] + + def __contains__(self, item): + return item in self.content + + def update(self, other): + self.content.update(other) + return self diff --git a/src/virtualenv/report.py b/src/virtualenv/report.py new file mode 100644 index 000000000..be7506eaa --- /dev/null +++ b/src/virtualenv/report.py @@ -0,0 +1,45 @@ +from __future__ import absolute_import, unicode_literals + +import logging +import sys + +import six + +LEVELS = { + 0: logging.CRITICAL, + 1: logging.ERROR, + 2: logging.WARNING, + 3: logging.INFO, + 4: logging.DEBUG, + 5: logging.NOTSET, +} + +MAX_LEVEL = max(LEVELS.keys()) +LOGGER = logging.getLogger() + + +def setup_report(verbose, quiet): + verbosity = max(verbose - quiet, 0) + _clean_handlers(LOGGER) + if verbosity > MAX_LEVEL: + verbosity = MAX_LEVEL # pragma: no cover + level = LEVELS[verbosity] + msg_format = "%(message)s" + if level <= logging.DEBUG: + locate = "pathname" if level < logging.DEBUG else "module" + msg_format = "%(relativeCreated)d {} [%(levelname)s %({})s:%(lineno)d]".format(msg_format, locate) + + formatter = logging.Formatter(six.ensure_str(msg_format)) + stream_handler = logging.StreamHandler(stream=sys.stdout) + stream_handler.setLevel(level) + LOGGER.setLevel(logging.NOTSET) + stream_handler.setFormatter(formatter) + LOGGER.addHandler(stream_handler) + level_name = logging.getLevelName(level) + logging.debug("setup logging to %s", level_name) + return verbosity + + +def _clean_handlers(log): + for log_handler in list(log.handlers): # remove handlers of libraries + log.removeHandler(log_handler) diff --git a/src/virtualenv/run.py b/src/virtualenv/run.py new file mode 100644 index 000000000..5e9428606 --- /dev/null +++ b/src/virtualenv/run.py @@ -0,0 +1,190 @@ +from __future__ import absolute_import, unicode_literals + +import logging +from argparse import ArgumentTypeError + +from entrypoints import get_group_named + +from .config.cli.parser import VirtualEnvConfigParser +from .report import LEVELS, setup_report +from .session import Session +from .version import __version__ + + +def run_via_cli(args): + """Run the virtual environment creation via CLI arguments + + :param args: the command line arguments + :return: the creator used + """ + session = session_via_cli(args) + session.run() + return session + + +def session_via_cli(args): + parser = VirtualEnvConfigParser() + add_version_flag(parser) + options, verbosity = _do_report_setup(parser, args) + discover = _get_discover(parser, args, options) + interpreter = discover.interpreter + logging.debug("target interpreter %r", interpreter) + if interpreter is None: + raise RuntimeError("failed to find interpreter for {}".format(discover)) + elements = [ + _get_creator(interpreter, parser, options), + _get_seeder(parser, options), + _get_activation(interpreter, parser, options), + ] + [next(elem) for elem in elements] # add choice of types + parser.parse_known_args(args, namespace=options) + [next(elem) for elem in elements] # add type flags + parser.enable_help() + parser.parse_args(args, namespace=options) + creator, seeder, activators = tuple(next(e) for e in elements) # create types + session = Session(verbosity, interpreter, creator, seeder, activators) + return session + + +def add_version_flag(parser): + import virtualenv + + parser.add_argument( + "--version", action="version", version="%(prog)s {} from {}".format(__version__, virtualenv.__file__) + ) + + +def _do_report_setup(parser, args): + level_map = ", ".join("{}:{}".format(c, logging.getLevelName(l)) for c, l in sorted(list(LEVELS.items()))) + msg = "verbosity = verbose - quiet, default {}, count mapping = {{{}}}" + verbosity_group = parser.add_argument_group(msg.format(logging.getLevelName(LEVELS[3]), level_map)) + verbosity = verbosity_group.add_mutually_exclusive_group() + verbosity.add_argument("-v", "--verbose", action="count", dest="verbose", help="increase verbosity", default=2) + verbosity.add_argument("-q", "--quiet", action="count", dest="quiet", help="decrease verbosity", default=0) + options, _ = parser.parse_known_args(args) + verbosity_value = setup_report(options.verbose, options.quiet) + return options, verbosity_value + + +def _get_discover(parser, args, options): + discover_types = _collect_discovery_types() + discovery_parser = parser.add_argument_group("target interpreter identifier") + discovery_parser.add_argument( + "--discovery", + choices=list(discover_types.keys()), + default=next(i for i in discover_types.keys()), + required=False, + help="interpreter discovery method", + ) + options, _ = parser.parse_known_args(args, namespace=options) + discover_class = discover_types[options.discovery] + discover_class.add_parser_arguments(discovery_parser) + options, _ = parser.parse_known_args(args, namespace=options) + discover = discover_class(options) + return discover + + +def _collect_discovery_types(): + discover_types = {e.name: e.load() for e in get_group_named("virtualenv.discovery").values()} + return discover_types + + +def _get_creator(interpreter, parser, options): + creators = _collect_creators(interpreter) + creator_parser = parser.add_argument_group("creator options") + creator_parser.add_argument( + "--creator", + choices=list(creators), + # prefer the built-in venv if present, otherwise fallback to first defined type + default="venv" if "venv" in creators else next(iter(creators), None), + required=False, + help="create environment via", + ) + yield + if options.creator not in creators: + raise RuntimeError("No virtualenv implementation for {}".format(interpreter)) + creator_class = creators[options.creator] + creator_class.add_parser_arguments(creator_parser, interpreter) + yield + creator = creator_class(options, interpreter) + yield creator + + +def _collect_creators(interpreter): + all_creators = {e.name: e.load() for e in get_group_named("virtualenv.create").values()} + creators = {k: v for k, v in all_creators.items() if v.supports(interpreter)} + return creators + + +def _get_seeder(parser, options): + seed_parser = parser.add_argument_group("package seeder") + seeder_types = _collect_seeders() + seed_parser.add_argument( + "--seeder", + choices=list(seeder_types.keys()), + default="app-data", + required=False, + help="seed packages install method", + ) + seed_parser.add_argument( + "--without-pip", + help="if set forces the none seeder, used for compatibility with venv", + action="store_true", + dest="without_pip", + ) + yield + seeder_class = seeder_types["none" if options.without_pip is True else options.seeder] + seeder_class.add_parser_arguments(seed_parser) + yield + seeder = seeder_class(options) + yield seeder + + +def _collect_seeders(): + seeder_types = {e.name: e.load() for e in get_group_named("virtualenv.seed").values()} + return seeder_types + + +def _get_activation(interpreter, parser, options): + activator_parser = parser.add_argument_group("activation script generator") + compatible = collect_activators(interpreter) + default = ",".join(compatible.keys()) + + def _extract_activators(entered_str): + elements = [e.strip() for e in entered_str.split(",") if e.strip()] + missing = [e for e in elements if e not in compatible] + if missing: + raise ArgumentTypeError("the following activators are not available {}".format(",".join(missing))) + return elements + + activator_parser.add_argument( + "--activators", + default=default, + metavar="comma_separated_list", + required=False, + help="activators to generate together with virtual environment - default is all available and compatible", + type=_extract_activators, + ) + yield + + selected_activators = _extract_activators(default) if options.activators is default else options.activators + active_activators = {k: v for k, v in compatible.items() if k in selected_activators} + activator_parser.add_argument( + "--prompt", + dest="prompt", + metavar="prompt", + help="provides an alternative prompt prefix for this environment", + default=None, + ) + for activator in active_activators.values(): + activator.add_parser_arguments(parser) + yield + + activator_instances = [activator_class(options) for activator_class in active_activators.values()] + yield activator_instances + + +def collect_activators(interpreter): + all_activators = {e.name: e.load() for e in get_group_named("virtualenv.activate").values()} + activators = {k: v for k, v in all_activators.items() if v.supports(interpreter)} + return activators diff --git a/src/virtualenv/seed/__init__.py b/src/virtualenv/seed/__init__.py new file mode 100644 index 000000000..01e6d4f49 --- /dev/null +++ b/src/virtualenv/seed/__init__.py @@ -0,0 +1 @@ +from __future__ import absolute_import, unicode_literals diff --git a/src/virtualenv/seed/embed/__init__.py b/src/virtualenv/seed/embed/__init__.py new file mode 100644 index 000000000..01e6d4f49 --- /dev/null +++ b/src/virtualenv/seed/embed/__init__.py @@ -0,0 +1 @@ +from __future__ import absolute_import, unicode_literals diff --git a/src/virtualenv/seed/embed/base_embed.py b/src/virtualenv/seed/embed/base_embed.py new file mode 100644 index 000000000..c212f39a6 --- /dev/null +++ b/src/virtualenv/seed/embed/base_embed.py @@ -0,0 +1,81 @@ +from __future__ import absolute_import, unicode_literals + +from abc import ABCMeta + +import six + +from virtualenv.util.path import Path + +from ..seeder import Seeder + + +@six.add_metaclass(ABCMeta) +class BaseEmbed(Seeder): + def __init__(self, options): + super(BaseEmbed, self).__init__(options, enabled=options.without_pip is False) + + self.download = options.download + self.extra_search_dir = [i.resolve() for i in options.extra_search_dir if i.exists()] + self.pip_version = None if options.pip == "latest" else options.pip + self.setuptools_version = None if options.setuptools == "latest" else options.setuptools + self.no_pip = options.no_pip + self.no_setuptools = options.no_setuptools + + @classmethod + def add_parser_arguments(cls, parser): + group = parser.add_mutually_exclusive_group() + group.add_argument( + "--download", + dest="download", + action="store_true", + help="download latest pip/setuptools from PyPi", + default=False, + ) + group.add_argument( + "--no-download", + "--never-download", + dest="download", + action="store_false", + help="do not download latest pip/setuptools from PyPi", + default=True, + ) + parser.add_argument( + "--extra-search-dir", + metavar="d", + type=Path, + nargs="+", + help="a location containing wheels candidates to install from", + default=[], + ) + for package in ["pip", "setuptools"]: + parser.add_argument( + "--{}".format(package), + dest=package, + metavar="version", + help="{} version to install, bundle for bundled".format(package), + default="latest", + ) + for extra, package in [ + ("", "pip"), + ("", "setuptools"), + ("N/A - kept only for backwards compatibility; ", "wheel"), + ]: + parser.add_argument( + "--no-{}".format(package), + dest="no_{}".format(package), + action="store_true", + help="{}do not install {}".format(extra, package), + default=False, + ) + + def __str__(self): + result = self.__class__.__name__ + if self.extra_search_dir: + result += " extra search dirs = {}".format( + ", ".join(six.ensure_text(str(i)) for i in self.extra_search_dir) + ) + if self.no_pip is False: + result += " pip{}".format("={}".format(self.pip_version or "latest")) + if self.no_setuptools is False: + result += " setuptools{}".format("={}".format(self.setuptools_version or "latest")) + return result diff --git a/src/virtualenv/seed/embed/pip_invoke.py b/src/virtualenv/seed/embed/pip_invoke.py new file mode 100644 index 000000000..f64741060 --- /dev/null +++ b/src/virtualenv/seed/embed/pip_invoke.py @@ -0,0 +1,27 @@ +from __future__ import absolute_import, unicode_literals + +from virtualenv.seed.embed.base_embed import BaseEmbed +from virtualenv.seed.embed.wheels.acquire import get_bundled_wheel, pip_wheel_env_run +from virtualenv.util.subprocess import Popen + + +class PipInvoke(BaseEmbed): + def __init__(self, options): + super(PipInvoke, self).__init__(options) + + def run(self, creator): + cmd = self.get_pip_install_cmd(creator.exe, creator.interpreter.version_release_str) + env = pip_wheel_env_run(creator.interpreter.version_release_str) + process = Popen(cmd, env=env) + process.communicate() + + def get_pip_install_cmd(self, exe, version): + cmd = [str(exe), "-m", "pip", "install", "--only-binary", ":all:"] + for folder in {get_bundled_wheel(p, version).parent for p in ("pip", "setuptools")}: + cmd.extend(["--find-links", str(folder)]) + cmd.extend(self.extra_search_dir) + if not self.download: + cmd.append("--no-index") + for key, version in {"pip": self.pip_version, "setuptools": self.setuptools_version}.items(): + cmd.append("{}{}".format(key, "=={}".format(version) if version is not None else "")) + return cmd diff --git a/src/virtualenv/seed/embed/wheels/__init__.py b/src/virtualenv/seed/embed/wheels/__init__.py new file mode 100644 index 000000000..55e5f53d7 --- /dev/null +++ b/src/virtualenv/seed/embed/wheels/__init__.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import, unicode_literals + +BUNDLE_SUPPORT = { + "3.9": {"pip": "pip-19.3.1-py2.py3-none-any.whl", "setuptools": "setuptools-42.0.2-py2.py3-none-any.whl"}, + "3.8": {"pip": "pip-19.3.1-py2.py3-none-any.whl", "setuptools": "setuptools-42.0.2-py2.py3-none-any.whl"}, + "3.7": {"pip": "pip-19.3.1-py2.py3-none-any.whl", "setuptools": "setuptools-42.0.2-py2.py3-none-any.whl"}, + "3.6": {"pip": "pip-19.3.1-py2.py3-none-any.whl", "setuptools": "setuptools-42.0.2-py2.py3-none-any.whl"}, + "3.5": {"pip": "pip-19.3.1-py2.py3-none-any.whl", "setuptools": "setuptools-42.0.2-py2.py3-none-any.whl"}, + "3.4": {"pip": "pip-19.1.1-py2.py3-none-any.whl", "setuptools": "setuptools-42.0.2-py2.py3-none-any.whl"}, + "2.7": {"pip": "pip-19.3.1-py2.py3-none-any.whl", "setuptools": "setuptools-42.0.2-py2.py3-none-any.whl"}, +} +MAX = "3.9" diff --git a/src/virtualenv/seed/embed/wheels/acquire.py b/src/virtualenv/seed/embed/wheels/acquire.py new file mode 100644 index 000000000..558f0f479 --- /dev/null +++ b/src/virtualenv/seed/embed/wheels/acquire.py @@ -0,0 +1,158 @@ +"""Bootstrap""" +from __future__ import absolute_import, unicode_literals + +import logging +import os +import sys +from collections import defaultdict +from shutil import copy2 +from zipfile import ZipFile + +import six + +from virtualenv.util.path import Path +from virtualenv.util.subprocess import Popen, subprocess + +from . import BUNDLE_SUPPORT, MAX + +BUNDLE_FOLDER = Path(__file__).parent + + +def get_wheels(for_py_version, wheel_cache_dir, extra_search_dir, download, pip, setuptools): + # not all wheels are compatible with all python versions, so we need to py version qualify it + packages = {"pip": pip, "setuptools": setuptools} + + # 1. acquire from bundle + acquire_from_bundle(packages, for_py_version, wheel_cache_dir) + # 2. acquire from extra search dir + acquire_from_dir(packages, for_py_version, wheel_cache_dir, extra_search_dir) + # 3. download from the internet + if download and packages: + download_wheel(packages, for_py_version, wheel_cache_dir) + + # in the end just get the wheels + wheels = _get_wheels(wheel_cache_dir, {"pip": pip, "setuptools": setuptools}) + return {p: next(iter(ver_to_files))[1] for p, ver_to_files in wheels.items()} + + +def acquire_from_bundle(packages, for_py_version, to_folder): + for pkg, version in list(packages.items()): + bundle = get_bundled_wheel(pkg, for_py_version) + if bundle is not None: + pkg_version = bundle.stem.split("-")[1] + exact_version_match = version == pkg_version + if exact_version_match: + del packages[pkg] + if version is None or exact_version_match: + bundled_wheel_file = to_folder / bundle.name + if not bundled_wheel_file.exists(): + logging.debug("get bundled wheel %s", bundle) + copy2(str(bundle), str(bundled_wheel_file)) + + +def get_bundled_wheel(package, version_release): + return BUNDLE_FOLDER / (BUNDLE_SUPPORT.get(version_release, {}) or BUNDLE_SUPPORT[MAX]).get(package) + + +def acquire_from_dir(packages, for_py_version, to_folder, extra_search_dir): + if not packages: + return + for search_dir in extra_search_dir: + wheels = _get_wheels(search_dir, packages) + for pkg, ver_wheels in wheels.items(): + stop = False + for _, filename in ver_wheels: + dest = to_folder / filename.name + if not dest.exists(): + if wheel_support_py(filename, for_py_version): + logging.debug("get extra search dir wheel %s", filename) + copy2(str(filename), str(dest)) + stop = True + else: + stop = True + if stop and packages[pkg] is not None: + del packages[pkg] + break + + +def wheel_support_py(filename, py_version): + name = "{}.dist-info/METADATA".format("-".join(filename.stem.split("-")[0:2])) + with ZipFile(six.ensure_text(str(filename)), "r") as zip_file: + metadata = zip_file.read(name).decode("utf-8") + marker = "Requires-Python:" + requires = next(i[len(marker) :] for i in metadata.splitlines() if i.startswith(marker)) + py_version_int = tuple(int(i) for i in py_version.split(".")) + for require in (i.strip() for i in requires.split(",")): + # https://www.python.org/dev/peps/pep-0345/#version-specifiers + for operator, check in [ + ("!=", lambda v: py_version_int != v), + ("==", lambda v: py_version_int == v), + ("<=", lambda v: py_version_int <= v), + (">=", lambda v: py_version_int >= v), + ("<", lambda v: py_version_int < v), + (">", lambda v: py_version_int > v), + ]: + if require.startswith(operator): + ver_str = require[len(operator) :].strip() + version = tuple((int(i) if i != "*" else None) for i in ver_str.split("."))[0:2] + if not check(version): + return False + break + return True + + +def _get_wheels(from_folder, packages): + wheels = defaultdict(list) + for filename in from_folder.iterdir(): + if filename.suffix == ".whl": + data = filename.stem.split("-") + if len(data) >= 2: + pkg, version = data[0:2] + if pkg in packages: + pkg_version = packages[pkg] + if pkg_version is None or pkg_version == version: + wheels[pkg].append((version, filename)) + for versions in wheels.values(): + versions.sort( + key=lambda a: tuple(int(i) if i.isdigit() else i for i in a[0].split(".")), reverse=True, + ) + return wheels + + +def download_wheel(packages, for_py_version, to_folder): + to_download = list(p if v is None else "{}={}".format(p, v) for p, v in packages.items()) + logging.debug("download wheels %s", to_download) + cmd = [ + sys.executable, + "-m", + "pip", + "download", + "--disable-pip-version-check", + "--only-binary=:all:", + "--no-deps", + "--python-version", + for_py_version, + "-d", + str(to_folder), + ] + cmd.extend(to_download) + # pip has no interface in python - must be a new sub-process + process = Popen(cmd, env=pip_wheel_env_run("{}{}".format(*sys.version_info[0:2])), stdout=subprocess.PIPE) + process.communicate() + + +def pip_wheel_env_run(version): + env = os.environ.copy() + env.update( + { + str(k): str(v) # python 2 requires these to be string only (non-unicode) + for k, v in { + # put the bundled wheel onto the path, and use it to do the bootstrap operation + "PYTHONPATH": get_bundled_wheel("pip", version), + "PIP_USE_WHEEL": "1", + "PIP_USER": "0", + "PIP_NO_INPUT": "1", + }.items() + } + ) + return env diff --git a/virtualenv_support/pip-19.1.1-py2.py3-none-any.whl b/src/virtualenv/seed/embed/wheels/pip-19.1.1-py2.py3-none-any.whl similarity index 100% rename from virtualenv_support/pip-19.1.1-py2.py3-none-any.whl rename to src/virtualenv/seed/embed/wheels/pip-19.1.1-py2.py3-none-any.whl diff --git a/virtualenv_support/pip-19.3.1-py2.py3-none-any.whl b/src/virtualenv/seed/embed/wheels/pip-19.3.1-py2.py3-none-any.whl similarity index 100% rename from virtualenv_support/pip-19.3.1-py2.py3-none-any.whl rename to src/virtualenv/seed/embed/wheels/pip-19.3.1-py2.py3-none-any.whl diff --git a/virtualenv_support/setuptools-42.0.2-py2.py3-none-any.whl b/src/virtualenv/seed/embed/wheels/setuptools-42.0.2-py2.py3-none-any.whl similarity index 100% rename from virtualenv_support/setuptools-42.0.2-py2.py3-none-any.whl rename to src/virtualenv/seed/embed/wheels/setuptools-42.0.2-py2.py3-none-any.whl diff --git a/src/virtualenv/seed/none.py b/src/virtualenv/seed/none.py new file mode 100644 index 000000000..d3ccc8c5a --- /dev/null +++ b/src/virtualenv/seed/none.py @@ -0,0 +1,15 @@ +from __future__ import absolute_import, unicode_literals + +from virtualenv.seed.seeder import Seeder + + +class NoneSeeder(Seeder): + def __init__(self, options): + super(NoneSeeder, self).__init__(options, False) + + @classmethod + def add_parser_arguments(cls, parser): + pass + + def run(self, creator): + pass diff --git a/src/virtualenv/seed/seeder.py b/src/virtualenv/seed/seeder.py new file mode 100644 index 000000000..1a79853c9 --- /dev/null +++ b/src/virtualenv/seed/seeder.py @@ -0,0 +1,19 @@ +from __future__ import absolute_import, unicode_literals + +from abc import ABCMeta, abstractmethod + +import six + + +@six.add_metaclass(ABCMeta) +class Seeder(object): + def __init__(self, options, enabled): + self.enabled = enabled + + @classmethod + def add_parser_arguments(cls, parser): + raise NotImplementedError + + @abstractmethod + def run(self, creator): + raise NotImplementedError diff --git a/src/virtualenv/seed/via_app_data/__init__.py b/src/virtualenv/seed/via_app_data/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/virtualenv/seed/via_app_data/pip_install/__init__.py b/src/virtualenv/seed/via_app_data/pip_install/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/virtualenv/seed/via_app_data/pip_install/base.py b/src/virtualenv/seed/via_app_data/pip_install/base.py new file mode 100644 index 000000000..d8c3e8559 --- /dev/null +++ b/src/virtualenv/seed/via_app_data/pip_install/base.py @@ -0,0 +1,186 @@ +from __future__ import absolute_import, unicode_literals + +import logging +import os +import re +import shutil +import zipfile +from abc import ABCMeta, abstractmethod +from tempfile import mkdtemp +from textwrap import dedent + +import six +from six import PY3 + +from virtualenv.info import IS_WIN +from virtualenv.util import ConfigParser +from virtualenv.util.path import Path + + +@six.add_metaclass(ABCMeta) +class PipInstall(object): + def __init__(self, wheel, creator, image_folder): + self._wheel = wheel + self._creator = creator + self._image_dir = image_folder + self._extracted = False + self.__dist_info = None + self._console_entry_points = None + + @abstractmethod + def _sync(self, src, dst): + raise NotImplementedError + + def install(self): + self._extracted = True + # sync image + site_package = self._creator.site_packages[0] + for filename in self._image_dir.iterdir(): + into = site_package / filename.name + logging.debug("link %s of %s", filename, into) + if into.exists(): + if into.is_dir() and not into.is_symlink(): + shutil.rmtree(str(into)) + else: + into.unlink() + self._sync(filename, into) + # generate console executables + consoles = set() + bin_dir = self._creator.bin_dir + for name, module in self._console_scripts.items(): + consoles.update(self._create_console_entry_point(name, module, bin_dir)) + logging.debug("generated console scripts %s", " ".join(i.name for i in consoles)) + + def build_image(self): + # 1. first extract the wheel + logging.debug("build install image to %s of %s", self._image_dir, self._wheel.name) + with zipfile.ZipFile(str(self._wheel)) as zip_ref: + zip_ref.extractall(str(self._image_dir)) + self._extracted = True + # 2. now add additional files not present in the package + new_files = self._generate_new_files() + # 3. finally fix the records file + self._fix_records(new_files) + + def _records_text(self, files): + record_data = "\n".join( + "{},,".format(os.path.relpath(six.ensure_text(str(rec)), six.ensure_text(str(self._image_dir)))) + for rec in files + ) + return record_data + + def _generate_new_files(self): + new_files = set() + installer = self._dist_info / "INSTALLER" + installer.write_text("pip\n") + new_files.add(installer) + # inject a no-op root element, as workaround for bug added + # by https://github.com/pypa/pip/commit/c7ae06c79#r35523722 + marker = self._image_dir / "{}.virtualenv".format(self._dist_info.name) + marker.write_text("") + new_files.add(marker) + folder = mkdtemp() + try: + to_folder = Path(folder) + rel = os.path.relpath( + six.ensure_text(str(self._creator.bin_dir)), six.ensure_text(str(self._creator.site_packages[0])) + ) + for name, module in self._console_scripts.items(): + new_files.update( + Path(os.path.normpath(six.ensure_text(str(self._image_dir / rel / i.name)))) + for i in self._create_console_entry_point(name, module, to_folder) + ) + finally: + shutil.rmtree(folder, ignore_errors=True) + return new_files + + @property + def _dist_info(self): + if self._extracted is False: + return None # pragma: no cover + if self.__dist_info is None: + for filename in self._image_dir.iterdir(): + if filename.suffix == ".dist-info": + self.__dist_info = filename + break + else: + raise RuntimeError("no dist info") # pragma: no cover + return self.__dist_info + + @abstractmethod + def _fix_records(self, extra_record_data): + raise NotImplementedError + + @property + def _console_scripts(self): + if self._extracted is False: + return None # pragma: no cover + if self._console_entry_points is None: + self._console_entry_points = {} + entry_points = self._dist_info / "entry_points.txt" + if entry_points.exists(): + parser = ConfigParser.ConfigParser() + with entry_points.open() as file_handler: + reader = getattr(parser, "read_file" if PY3 else "readfp") + reader(file_handler) + if "console_scripts" in parser.sections(): + for name, value in parser.items("console_scripts"): + match = re.match(r"(.*?)-?\d\.?\d*", name) + if match: + name = match.groups(1)[0] + self._console_entry_points[name] = value + return self._console_entry_points + + def _create_console_entry_point(self, name, value, to_folder): + result = [] + if IS_WIN: + # windows doesn't support simple script files, so fallback to more complicated exe generator + from distlib.scripts import ScriptMaker + + maker = ScriptMaker(None, str(to_folder)) + maker.clobber = True # overwrite + maker.variants = {"", "X", "X.Y"} # create all variants + maker.set_mode = True # ensure they are executable + maker.executable = str(self._creator.exe) + specification = "{} = {}".format(name, value) + new_files = maker.make(specification) + result.extend(Path(i) for i in new_files) + else: + module, func = value.split(":") + content = ( + dedent( + """ + #!{0} + # -*- coding: utf-8 -*- + import re + import sys + + from {1} import {2} + + if __name__ == "__main__": + sys.argv[0] = re.sub(r"(-script.pyw?|.exe)?$", "", sys.argv[0]) + sys.exit({2}()) + """ + ) + .lstrip() + .format(self._creator.exe, module, func) + ) + + version = self._creator.interpreter.version_info + for new_name in ( + name, + "{}{}".format(name, version.major), + "{}-{}.{}".format(name, version.major, version.minor), + ): + exe = to_folder / new_name + exe.write_text(content, encoding="utf-8") + exe.chmod(0o755) + result.append(exe) + return result + + def clear(self): + if self._image_dir.exists(): + shutil.rmtree(six.ensure_text(str(self._image_dir))) + + def has_image(self): + return self._image_dir.exists() and next(self._image_dir.iterdir()) is not None diff --git a/src/virtualenv/seed/via_app_data/pip_install/copy.py b/src/virtualenv/seed/via_app_data/pip_install/copy.py new file mode 100644 index 000000000..5a3424e46 --- /dev/null +++ b/src/virtualenv/seed/via_app_data/pip_install/copy.py @@ -0,0 +1,42 @@ +from __future__ import absolute_import, unicode_literals + +import os +import shutil + +import six + +from virtualenv.util.path import Path + +from .base import PipInstall + + +class CopyPipInstall(PipInstall): + def _sync(self, src, dst): + src_str = six.ensure_text(str(src)) + dest_str = six.ensure_text(str(dst)) + if src.is_dir(): + shutil.copytree(src_str, dest_str) + else: + shutil.copy(src_str, dest_str) + + def _generate_new_files(self): + # create the pyc files + new_files = super(CopyPipInstall, self)._generate_new_files() + new_files.update(self._cache_files()) + return new_files + + def _cache_files(self): + version = self._creator.interpreter.version_info + py_c_ext = ".{}-{}{}.pyc".format(self._creator.interpreter.implementation.lower(), version.major, version.minor) + for root, dirs, files in os.walk(six.ensure_text(str(self._image_dir)), topdown=True): + root_path = Path(root) + for name in files: + if name.endswith(".py"): + yield root_path / "{}{}".format(name[:-3], py_c_ext) + for name in dirs: + yield root_path / name / "__pycache__" + + def _fix_records(self, new_files): + extra_record_data_str = self._records_text(new_files) + with open(six.ensure_text(str(self._dist_info / "RECORD")), "ab") as file_handler: + file_handler.write(extra_record_data_str.encode("utf-8")) diff --git a/src/virtualenv/seed/via_app_data/pip_install/symlink.py b/src/virtualenv/seed/via_app_data/pip_install/symlink.py new file mode 100644 index 000000000..1d1a52e7c --- /dev/null +++ b/src/virtualenv/seed/via_app_data/pip_install/symlink.py @@ -0,0 +1,68 @@ +from __future__ import absolute_import, unicode_literals + +import os +import shutil +import subprocess +from stat import S_IREAD, S_IRGRP, S_IROTH, S_IWUSR + +import six + +from virtualenv.util.subprocess import Popen + +from .base import PipInstall + + +class SymlinkPipInstall(PipInstall): + def _sync(self, src, dst): + src_str = six.ensure_text(str(src)) + dest_str = six.ensure_text(str(dst)) + os.symlink(src_str, dest_str) + + def _generate_new_files(self): + # create the pyc files, as the build image will be R/O + process = Popen( + [six.ensure_text(str(self._creator.exe)), "-m", "compileall", six.ensure_text(str(self._image_dir))], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + process.communicate() + # the root pyc is shared, so we'll not symlink that - but still add the pyc files to the RECORD for cleanup + root_py_cache = self._image_dir / "__pycache__" + new_files = set() + if root_py_cache.exists(): + new_files.update(root_py_cache.iterdir()) + new_files.add(root_py_cache) + shutil.rmtree(six.ensure_text(str(root_py_cache))) + core_new_files = super(SymlinkPipInstall, self)._generate_new_files() + # remove files that are within the image folder deeper than one level (as these will be not linked directly) + for file in core_new_files: + try: + rel = file.relative_to(self._image_dir) + if len(rel.parts) > 1: + continue + except ValueError: + pass + new_files.add(file) + return new_files + + def _fix_records(self, new_files): + new_files.update(i for i in self._image_dir.iterdir()) + extra_record_data_str = self._records_text(sorted(new_files, key=str)) + with open(six.ensure_text(str(self._dist_info / "RECORD")), "wb") as file_handler: + file_handler.write(extra_record_data_str.encode("utf-8")) + + def build_image(self): + super(SymlinkPipInstall, self).build_image() + # protect the image by making it read only + self._set_tree(self._image_dir, S_IREAD | S_IRGRP | S_IROTH) + + def clear(self): + if self._image_dir.exists(): + self._set_tree(self._image_dir, S_IWUSR) + super(SymlinkPipInstall, self).clear() + + @staticmethod + def _set_tree(folder, stat): + for root, _, files in os.walk(six.ensure_text(str(folder))): + for filename in files: + os.chmod(os.path.join(root, filename), stat) diff --git a/src/virtualenv/seed/via_app_data/via_app_data.py b/src/virtualenv/seed/via_app_data/via_app_data.py new file mode 100644 index 000000000..881e1c9be --- /dev/null +++ b/src/virtualenv/seed/via_app_data/via_app_data.py @@ -0,0 +1,71 @@ +"""Bootstrap""" +from __future__ import absolute_import, unicode_literals + +import logging +import shutil + +import six + +from virtualenv.info import IS_WIN, get_default_data_dir +from virtualenv.seed.embed.base_embed import BaseEmbed +from virtualenv.seed.embed.wheels.acquire import get_wheels + +from .pip_install.copy import CopyPipInstall +from .pip_install.symlink import SymlinkPipInstall + + +class FromAppData(BaseEmbed): + def __init__(self, options): + super(FromAppData, self).__init__(options) + self.clear = options.clear_app_data + self.app_data_dir = get_default_data_dir() / "seed-v1" + + @classmethod + def add_parser_arguments(cls, parser): + super(FromAppData, cls).add_parser_arguments(parser) + parser.add_argument( + "--clear-app-data", + dest="clear_app_data", + action="store_true", + help="clear the app data folder", + default=False, + ) + + def run(self, creator): + base_cache = self.app_data_dir / creator.interpreter.version_release_str + name_to_whl = self._get_seed_wheels(creator, base_cache) + installer_class = self.installer_class(name_to_whl["pip"].stem.split("-")[1]) + for name, wheel in name_to_whl.items(): + logging.debug("install %s from wheel %s", name, wheel) + image_folder = base_cache / "image" / installer_class.__name__ / wheel.stem + installer = installer_class(wheel, creator, image_folder) + if self.clear: + installer.clear() + if not installer.has_image(): + installer.build_image() + installer.install() + + def _get_seed_wheels(self, creator, base_cache): + wheels_to = base_cache / "wheels" + if self.clear and wheels_to.exists(): + shutil.rmtree(six.ensure_text(str(wheels_to))) + wheels_to.mkdir(parents=True, exist_ok=True) + name_to_whl = get_wheels( + creator.interpreter.version_release_str, + wheels_to, + self.extra_search_dir, + self.download, + self.pip_version, + self.setuptools_version, + ) + return name_to_whl + + @staticmethod + def installer_class(pip_version): + # tbd: on Windows symlinks are unreliable, we have junctions for folders, however pip does not work well with it + if not IS_WIN: + # symlink support requires pip 19.3+ + pip_version_int = tuple(int(i) for i in pip_version.split(".")[0:2]) + if pip_version_int >= (19, 3): + return SymlinkPipInstall + return CopyPipInstall diff --git a/src/virtualenv/session.py b/src/virtualenv/session.py new file mode 100644 index 000000000..e07f49058 --- /dev/null +++ b/src/virtualenv/session.py @@ -0,0 +1,50 @@ +from __future__ import absolute_import, unicode_literals + +import json +import logging + + +class Session(object): + def __init__(self, verbosity, interpreter, creator, seeder, activators): + self.verbosity = verbosity + self.interpreter = interpreter + self.creator = creator + self.seeder = seeder + self.activators = activators + + def run(self): + self._create() + self._seed() + self._activate() + self.creator.pyenv_cfg.write() + + def _create(self): + self.creator.run() + logging.debug(_DEBUG_MARKER) + logging.debug("%s", _Debug(self.creator)) + + def _seed(self): + if self.seeder is not None and self.seeder.enabled: + logging.info("add seed packages via %s", self.seeder) + self.seeder.run(self.creator) + + def _activate(self): + if self.activators: + logging.info( + "add activators for %s", ", ".join(type(i).__name__.replace("Activator", "") for i in self.activators) + ) + for activator in self.activators: + activator.generate(self.creator) + + +_DEBUG_MARKER = "=" * 30 + " target debug " + "=" * 30 + + +class _Debug(object): + """lazily populate debug""" + + def __init__(self, creator): + self.creator = creator + + def __str__(self): + return json.dumps(self.creator.debug, indent=2) diff --git a/src/virtualenv/util/__init__.py b/src/virtualenv/util/__init__.py new file mode 100644 index 000000000..39e5eb8ba --- /dev/null +++ b/src/virtualenv/util/__init__.py @@ -0,0 +1,10 @@ +from __future__ import absolute_import, unicode_literals + +try: + import ConfigParser +except ImportError: + # noinspection PyPep8Naming + import configparser as ConfigParser + + +__all__ = ("ConfigParser",) diff --git a/src/virtualenv/util/path/__init__.py b/src/virtualenv/util/path/__init__.py new file mode 100644 index 000000000..ab5db8ef2 --- /dev/null +++ b/src/virtualenv/util/path/__init__.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import, unicode_literals + +from ._pathlib import Path +from ._sync import copy, ensure_dir, symlink, symlink_or_copy + +__all__ = ( + "ensure_dir", + "symlink_or_copy", + "symlink", + "copy", + "Path", +) diff --git a/src/virtualenv/util/path/_pathlib/__init__.py b/src/virtualenv/util/path/_pathlib/__init__.py new file mode 100644 index 000000000..3b18d3d4b --- /dev/null +++ b/src/virtualenv/util/path/_pathlib/__init__.py @@ -0,0 +1,40 @@ +from __future__ import absolute_import, unicode_literals + +import sys + +import six + +if six.PY3: + from pathlib import Path + + if sys.version_info[0:2] == (3, 4): + # no read/write text on python3.4 + BuiltinPath = Path + + class Path(type(BuiltinPath())): + def read_text(self, encoding=None, errors=None): + """ + Open the file in text mode, read it, and close the file. + """ + with self.open(mode="r", encoding=encoding, errors=errors) as f: + return f.read() + + def write_text(self, data, encoding=None, errors=None): + """ + Open the file in text mode, write to it, and close the file. + """ + if not isinstance(data, str): + raise TypeError("data must be str, not %s" % data.__class__.__name__) + with self.open(mode="w", encoding=encoding, errors=errors) as f: + return f.write(data) + + +else: + if sys.platform == "win32": + # workaround for https://github.com/mcmtroffaes/pathlib2/issues/56 + from .via_os_path import Path + else: + from pathlib2 import Path + + +__all__ = ("Path",) diff --git a/src/virtualenv/util/path/_pathlib/via_os_path.py b/src/virtualenv/util/path/_pathlib/via_os_path.py new file mode 100644 index 000000000..b77ea72db --- /dev/null +++ b/src/virtualenv/util/path/_pathlib/via_os_path.py @@ -0,0 +1,115 @@ +from __future__ import absolute_import, unicode_literals + +import os +from contextlib import contextmanager + +import six + + +class Path(object): + def __init__(self, path): + self._path = path._path if isinstance(path, Path) else six.ensure_text(path) + + def __repr__(self): + return six.ensure_str("Path({})".format(self._path)) + + def __str__(self): + return six.ensure_str(self._path) + + def __div__(self, other): + return Path(os.path.join(self._path, other._path if isinstance(other, Path) else six.ensure_text(other))) + + def __truediv__(self, other): + return self.__div__(other) + + def __eq__(self, other): + return self._path == (other._path if isinstance(other, Path) else None) + + def __ne__(self, other): + return not (self == other) + + def __hash__(self): + return hash(self._path) + + def exists(self): + return os.path.exists(self._path) + + def absolute(self): + return Path(os.path.abspath(self._path)) + + @property + def parent(self): + return Path(os.path.abspath(os.path.join(self._path, os.path.pardir))) + + def resolve(self): + return Path(os.path.realpath(self._path)) + + @property + def name(self): + return os.path.basename(self._path) + + @property + def parts(self): + return self._path.split(os.sep) + + def is_file(self): + return os.path.isfile(self._path) + + def is_dir(self): + return os.path.isdir(self._path) + + def mkdir(self, parents=True, exist_ok=True): + if not self.exists() and exist_ok: + os.makedirs(self._path) + + def read_text(self, encoding="utf-8"): + with open(self._path, "rb") as file_handler: + return file_handler.read().decode(encoding) + + def write_text(self, text, encoding="utf-8"): + with open(self._path, "wb") as file_handler: + file_handler.write(text.encode(encoding)) + + def iterdir(self): + for p in os.listdir(self._path): + yield Path(os.path.join(self._path, p)) + + @property + def suffix(self): + _, ext = os.path.splitext(self.name) + return ext + + @property + def stem(self): + base, _ = os.path.splitext(self.name) + return base + + @contextmanager + def open(self, mode="r"): + with open(self._path, mode) as file_handler: + yield file_handler + + @property + def parents(self): + result = [] + parts = self.parts + for i in range(len(parts)): + result.append(Path(os.sep.join(parts[0 : i + 1]))) + return result + + def unlink(self): + os.remove(self._path) + + def with_name(self, name): + return self.parent / name + + def is_symlink(self): + return os.path.islink(self._path) + + def relative_to(self, other): + if not self._path.startswith(other._path): + raise ValueError("{} does not start with {}".format(self._path, other._path)) + return Path(os.sep.join(self.parts[len(other.parts) :])) + + +__all__ = ("Path",) diff --git a/src/virtualenv/util/path/_sync.py b/src/virtualenv/util/path/_sync.py new file mode 100644 index 000000000..c3d611117 --- /dev/null +++ b/src/virtualenv/util/path/_sync.py @@ -0,0 +1,50 @@ +from __future__ import absolute_import, unicode_literals + +import logging +import os +import shutil +from functools import partial + +import six + +HAS_SYMLINK = hasattr(os, "symlink") + + +def ensure_dir(path): + if not path.exists(): + logging.debug("created %s", six.ensure_text(str(path))) + os.makedirs(six.ensure_text(str(path))) + + +def symlink_or_copy(do_copy, src, dst, relative_symlinks_ok=False): + """ + Try symlinking a target, and if that fails, fall back to copying. + """ + if do_copy is False and HAS_SYMLINK is False: # if no symlink, always use copy + do_copy = True + if not do_copy: + try: + if not dst.is_symlink(): # can't link to itself! + if relative_symlinks_ok: + assert src.parent == dst.parent + os.symlink(six.ensure_text(src.name), six.ensure_text(str(dst))) + else: + os.symlink(six.ensure_text(str(src)), six.ensure_text(str(dst))) + except OSError as exception: + logging.warning( + "symlink failed %r, for %s to %s, will try copy", + exception, + six.ensure_text(str(src)), + six.ensure_text(str(dst)), + ) + do_copy = True + if do_copy: + copier = shutil.copy2 if src.is_file() else shutil.copytree + copier(six.ensure_text(str(src)), six.ensure_text(str(dst))) + logging.debug("%s %s to %s", "copy" if do_copy else "symlink", six.ensure_text(str(src)), six.ensure_text(str(dst))) + + +symlink = partial(symlink_or_copy, False) +copy = partial(symlink_or_copy, True) + +__all__ = ("ensure_dir", "symlink", "copy", "symlink_or_copy") diff --git a/src/virtualenv/util/subprocess/__init__.py b/src/virtualenv/util/subprocess/__init__.py new file mode 100644 index 000000000..01076587f --- /dev/null +++ b/src/virtualenv/util/subprocess/__init__.py @@ -0,0 +1,32 @@ +from __future__ import absolute_import, unicode_literals + +import subprocess +import sys + +import six + +if six.PY2 and sys.platform == "win32": + from . import _win_subprocess + + Popen = _win_subprocess.Popen +else: + Popen = subprocess.Popen + + +def run_cmd(cmd): + try: + process = Popen( + cmd, universal_newlines=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE + ) + out, err = process.communicate() # input disabled + code = process.returncode + except OSError as os_error: + code, out, err = os_error.errno, "", os_error.strerror + return code, out, err + + +__all__ = ( + "subprocess", + "Popen", + "run_cmd", +) diff --git a/src/virtualenv/util/subprocess/_win_subprocess.py b/src/virtualenv/util/subprocess/_win_subprocess.py new file mode 100644 index 000000000..e8fdaf00a --- /dev/null +++ b/src/virtualenv/util/subprocess/_win_subprocess.py @@ -0,0 +1,153 @@ +# flake8: noqa +# fmt: off +## issue: https://bugs.python.org/issue19264 + +import ctypes +import os +import subprocess +import sys +from ctypes import Structure, WinError, byref, c_char_p, c_void_p, c_wchar, c_wchar_p, sizeof, windll +from ctypes.wintypes import BOOL, BYTE, DWORD, HANDLE, LPVOID, LPWSTR, WORD + +import _subprocess + +## +## Types +## + +CREATE_UNICODE_ENVIRONMENT = 0x00000400 +LPCTSTR = c_char_p +LPTSTR = c_wchar_p +LPSECURITY_ATTRIBUTES = c_void_p +LPBYTE = ctypes.POINTER(BYTE) + +class STARTUPINFOW(Structure): + _fields_ = [ + ("cb", DWORD), ("lpReserved", LPWSTR), + ("lpDesktop", LPWSTR), ("lpTitle", LPWSTR), + ("dwX", DWORD), ("dwY", DWORD), + ("dwXSize", DWORD), ("dwYSize", DWORD), + ("dwXCountChars", DWORD), ("dwYCountChars", DWORD), + ("dwFillAtrribute", DWORD), ("dwFlags", DWORD), + ("wShowWindow", WORD), ("cbReserved2", WORD), + ("lpReserved2", LPBYTE), ("hStdInput", HANDLE), + ("hStdOutput", HANDLE), ("hStdError", HANDLE), + ] + +LPSTARTUPINFOW = ctypes.POINTER(STARTUPINFOW) + + +class PROCESS_INFORMATION(Structure): + _fields_ = [ + ("hProcess", HANDLE), ("hThread", HANDLE), + ("dwProcessId", DWORD), ("dwThreadId", DWORD), + ] + +LPPROCESS_INFORMATION = ctypes.POINTER(PROCESS_INFORMATION) + + +class DUMMY_HANDLE(ctypes.c_void_p): + + def __init__(self, *a, **kw): + super(DUMMY_HANDLE, self).__init__(*a, **kw) + self.closed = False + + def Close(self): + if not self.closed: + windll.kernel32.CloseHandle(self) + self.closed = True + + def __int__(self): + return self.value + + +CreateProcessW = windll.kernel32.CreateProcessW +CreateProcessW.argtypes = [ + LPCTSTR, LPTSTR, LPSECURITY_ATTRIBUTES, + LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCTSTR, + LPSTARTUPINFOW, LPPROCESS_INFORMATION, +] +CreateProcessW.restype = BOOL + + +## +## Patched functions/classes +## + +def CreateProcess(executable, args, _p_attr, _t_attr, + inherit_handles, creation_flags, env, cwd, + startup_info): + """Create a process supporting unicode executable and args for win32 + + Python implementation of CreateProcess using CreateProcessW for Win32 + + """ + + si = STARTUPINFOW( + dwFlags=startup_info.dwFlags, + wShowWindow=startup_info.wShowWindow, + cb=sizeof(STARTUPINFOW), + ## XXXvlab: not sure of the casting here to ints. + hStdInput=startup_info.hStdInput if startup_info.hStdInput is None else int(startup_info.hStdInput), + hStdOutput=startup_info.hStdOutput if startup_info.hStdOutput is None else int(startup_info.hStdOutput), + hStdError=startup_info.hStdError if startup_info.hStdError is None else int(startup_info.hStdError), + ) + + wenv = None + if env is not None: + ## LPCWSTR seems to be c_wchar_p, so let's say CWSTR is c_wchar + env = (unicode("").join([ + unicode("%s=%s\0") % (k, v) + for k, v in env.items()])) + unicode("\0") + wenv = (c_wchar * len(env))() + wenv.value = env + + pi = PROCESS_INFORMATION() + creation_flags |= CREATE_UNICODE_ENVIRONMENT + + if CreateProcessW(executable, args, None, None, + inherit_handles, creation_flags, + wenv, cwd, byref(si), byref(pi)): + return (DUMMY_HANDLE(pi.hProcess), DUMMY_HANDLE(pi.hThread), + pi.dwProcessId, pi.dwThreadId) + raise WinError() + + +class Popen(subprocess.Popen): + """This superseeds Popen and corrects a bug in cPython 2.7 implem""" + + def _execute_child(self, args, executable, preexec_fn, close_fds, + cwd, env, universal_newlines, + startupinfo, creationflags, shell, to_close, + p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite): + """Code from part of _execute_child from Python 2.7 (9fbb65e) + + There are only 2 little changes concerning the construction of + the the final string in shell mode: we preempt the creation of + the command string when shell is True, because original function + will try to encode unicode args which we want to avoid to be able to + sending it as-is to ``CreateProcess``. + + """ + if startupinfo is None: + startupinfo = subprocess.STARTUPINFO() + if not isinstance(args, subprocess.types.StringTypes): + args = subprocess.list2cmdline(args) + startupinfo.dwFlags |= _subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = _subprocess.SW_HIDE + comspec = os.environ.get("COMSPEC", unicode("cmd.exe")) + if (_subprocess.GetVersion() >= 0x80000000 or + os.path.basename(comspec).lower() == "command.com"): + w9xpopen = self._find_w9xpopen() + args = unicode('"%s" %s') % (w9xpopen, args) + creationflags |= _subprocess.CREATE_NEW_CONSOLE + + super(Popen, self)._execute_child(args, executable, + preexec_fn, close_fds, cwd, env, universal_newlines, + startupinfo, creationflags, False, to_close, p2cread, + p2cwrite, c2pread, c2pwrite, errread, errwrite) + +_subprocess.CreateProcess = CreateProcess +# fmt: on diff --git a/tasks/make_zipapp.py b/tasks/make_zipapp.py index 781a52f07..7e5bee34a 100644 --- a/tasks/make_zipapp.py +++ b/tasks/make_zipapp.py @@ -17,16 +17,17 @@ def main(): else: dest = os.path.join(args.root, "virtualenv.pyz") - bio = io.BytesIO() - with zipfile.ZipFile(bio, "w") as zipf: - filenames = ["LICENSE.txt", "virtualenv.py"] - for whl in os.listdir(os.path.join(args.root, "virtualenv_support")): - filenames.append(os.path.join("virtualenv_support", whl)) + filenames = {"LICENSE.txt": "LICENSE.txt", os.path.join("src", "virtualenv.py"): "virtualenv.py"} + for support in os.listdir(os.path.join(args.root, "src", "virtualenv_support")): + support_file = os.path.join("virtualenv_support", support) + filenames[os.path.join("src", support_file)] = support_file + bio = io.BytesIO() + with zipfile.ZipFile(bio, "w") as zip_file: for filename in filenames: - zipf.write(os.path.join(args.root, filename), filename) + zip_file.write(os.path.join(args.root, filename), filename) - zipf.writestr("__main__.py", "import virtualenv; virtualenv.main()") + zip_file.writestr("__main__.py", "import virtualenv; virtualenv.main()") bio.seek(0) zipapp.create_archive(bio, dest) diff --git a/tasks/update_embedded.py b/tasks/update_embedded.py index bcdb0dede..814455941 100755 --- a/tasks/update_embedded.py +++ b/tasks/update_embedded.py @@ -21,12 +21,12 @@ def crc32(data): here = os.path.realpath(os.path.dirname(__file__)) -script = os.path.realpath(os.path.join(here, "..", "virtualenv.py")) +script = os.path.realpath(os.path.join(here, "..", "src", "virtualenv.py")) gzip = codecs.lookup("zlib") b64 = codecs.lookup("base64") -file_regex = re.compile(r'# file (.*?)\n([a-zA-Z][a-zA-Z0-9_]+) = convert\(\n """\n(.*?)"""\n\)', re.S) +file_regex = re.compile(r'# file (.*?)\n([a-zA-Z][a-zA-Z0-9_]+) = convert\(\n {4}"""\n(.*?)"""\n\)', re.S) file_template = '# file {filename}\n{variable} = convert(\n """\n{data}"""\n)' @@ -54,7 +54,7 @@ def rebuild(script_path): def handle_file(previous_content, filename, variable_name, previous_encoded): print("Found file {}".format(filename)) - current_path = os.path.realpath(os.path.join(here, "..", "virtualenv_embedded", filename)) + current_path = os.path.realpath(os.path.join(here, "..", "src", "virtualenv_embedded", filename)) _, file_type = os.path.splitext(current_path) keep_line_ending = file_type in (".bat",) with open(current_path, "rt", encoding="utf-8", newline="" if keep_line_ending else None) as current_fh: diff --git a/tasks/upgrade_wheels.py b/tasks/upgrade_wheels.py index 9e5202e5e..83944f023 100644 --- a/tasks/upgrade_wheels.py +++ b/tasks/upgrade_wheels.py @@ -3,45 +3,104 @@ """ from __future__ import absolute_import, unicode_literals -import glob import os +import shutil import subprocess +import sys +from collections import OrderedDict, defaultdict +from tempfile import TemporaryDirectory +from threading import Thread +from pathlib2 import Path -def virtualenv_support_path(): - return os.path.join(os.path.dirname(__file__), "..", "virtualenv_support") +STRICT = "UPGRADE_ADVISORY" not in os.environ +BUNDLED = ["pip", "setuptools"] +SUPPORT = list(reversed([(2, 7)] + [(3, i) for i in range(4, 10)])) +DEST = Path(__file__).resolve().parents[1] / "src" / "virtualenv" / "seed" / "embed" / "wheels" -def collect_wheels(): - for filename in glob.glob(os.path.join(virtualenv_support_path(), "*.whl")): - name, version = os.path.basename(filename).split("-")[:2] - yield filename, name, version +def download(ver, dest, package): + subprocess.call( + [sys.executable, "-m", "pip", "download", "--only-binary=:all:", "--python-version", ver, "-d", dest, package] + ) -def remove_wheel_files(): - old_versions = {} - for filename, name, version in collect_wheels(): - old_versions[name] = version - os.remove(filename) - return old_versions +def run(): + old_batch = {i.name for i in DEST.iterdir() if i.suffix == ".whl"} + with TemporaryDirectory() as temp: + temp_path = Path(temp) + folders = {} + targets = [] + for support in SUPPORT: + support_ver = ".".join(str(i) for i in support) + into = temp_path / support_ver + into.mkdir() + folders[into] = support_ver + for package in BUNDLED: + thread = Thread(target=download, args=(support_ver, str(into), package)) + targets.append(thread) + thread.start() + for thread in targets: + thread.join() + new_batch = {i.name: i for f in folders.keys() for i in Path(f).iterdir()} -def download(package): - subprocess.call(["pip", "download", "-d", virtualenv_support_path(), package]) + new_packages = new_batch.keys() - old_batch + remove_packages = old_batch - new_batch.keys() + for package in remove_packages: + (DEST / package).unlink() + for package in new_packages: + shutil.copy2(str(new_batch[package]), DEST / package) -def run(): - old = remove_wheel_files() - for package in ("pip", "wheel", "setuptools"): - download(package) - new = {name: version for _, name, version in collect_wheels()} + added = collect_package_versions(new_packages) + removed = collect_package_versions(remove_packages) + + outcome = (1 if STRICT else 0) if (added or removed) else 0 + for key, versions in added.items(): + text = "* upgrade embedded {} to {}".format(key, fmt_version(versions)) + if key in removed: + text += " from {}".format(removed[key]) + del removed[key] + print(text) + for key, versions in removed.items(): + print("* removed embedded {} of {}".format(key, fmt_version(versions))) + + support_table = OrderedDict((".".join(str(j) for j in i), list()) for i in SUPPORT) + for package in sorted(new_batch.keys()): + for folder, version in sorted(folders.items()): + if (folder / package).exists(): + support_table[version].append(package) + support_table = {k: OrderedDict((i.split("-")[0], i) for i in v) for k, v in support_table.items()} + + msg = "from __future__ import absolute_import, unicode_literals; BUNDLE_SUPPORT = {{ {} }}; MAX = {!r}".format( + ",".join( + "{!r}: {{ {} }}".format(v, ",".join("{!r}: {!r}".format(p, f) for p, f in l.items())) + for v, l in support_table.items() + ), + next(iter(support_table.keys())), + ) + dest_target = DEST / "__init__.py" + dest_target.write_text(msg) + + subprocess.run([sys.executable, "-m", "black", str(dest_target)]) + + raise SystemExit(outcome) + + +def fmt_version(versions): + return ", ".join("``{}``".format(v) for v in versions) - changes = [] - for package, version in old.items(): - if new[package] != version: - changes.append((package, version, new[package])) - print("\n".join(" * upgrade {} from {} to {}".format(p, o, n) for p, o, n in changes)) +def collect_package_versions(new_packages): + result = defaultdict(list) + for package in new_packages: + split = package.split("-") + if len(split) < 2: + raise ValueError(package) + key, version = split[0:2] + result[key].append(version) + return result if __name__ == "__main__": diff --git a/tests/activation/test_activate_this.py b/tests/activation/test_activate_this.py deleted file mode 100644 index 205594fe9..000000000 --- a/tests/activation/test_activate_this.py +++ /dev/null @@ -1,104 +0,0 @@ -"""Activate this does not mangles with the shell itself to provision the python, but instead mangles -with the caller interpreter, effectively making so that the virtualenv activation constraints are met once -it's loaded. - -While initially may feel like a import is all we need, import is executed only once not at every activation. To -work around this we'll use Python 2 execfile, and Python 3 exec(read()). - -Virtual env activation constraints that we guarantee: -- the virtualenv site-package will be visible from the activator Python -- virtualenv packages take priority over python 2 -- virtualenv bin PATH pre-pended -- VIRTUAL_ENV env var will be set. -- if the user tries to import we'll raise - -""" -from __future__ import absolute_import, unicode_literals - -import os -import re -import subprocess -import sys -import textwrap - - -def test_activate_this(clean_python, tmp_path, monkeypatch): - # to test this, we'll try to use the activation env from this Python - monkeypatch.delenv(str("VIRTUAL_ENV"), raising=False) - monkeypatch.delenv(str("PYTHONPATH"), raising=False) - paths = [str(tmp_path), str(tmp_path / "other")] - start_path = os.pathsep.join(paths) - monkeypatch.setenv(str("PATH"), start_path) - activator = tmp_path.__class__(clean_python[1]) / "activate_this.py" - assert activator.exists() - - activator_at = str(activator) - script = textwrap.dedent( - """ - import os - import sys - print(os.environ.get("VIRTUAL_ENV")) - print(os.environ.get("PATH")) - try: - import pydoc_test - raise RuntimeError("this should not happen") - except ImportError: - pass - print(os.pathsep.join(sys.path)) - file_at = {!r} - exec(open(file_at).read(), {{'__file__': file_at}}) - print(os.environ.get("VIRTUAL_ENV")) - print(os.environ.get("PATH")) - print(os.pathsep.join(sys.path)) - import pydoc_test - print(pydoc_test.__file__) - """.format( - str(activator_at) - ) - ) - script_path = tmp_path / "test.py" - script_path.write_text(script) - try: - raw = subprocess.check_output( - [sys.executable, str(script_path)], stderr=subprocess.STDOUT, universal_newlines=True - ) - - out = re.sub(r"pydev debugger: process \d+ is connecting\n\n", "", raw, re.M).strip().split("\n") - - assert out[0] == "None" - assert out[1] == start_path - prev_sys_path = out[2].split(os.path.pathsep) - - assert out[3] == clean_python[0] # virtualenv set as the activated env - - # PATH updated with activated - assert out[4].endswith(start_path) - assert out[4][: -len(start_path)].split(os.pathsep) == [clean_python[1], ""] - - # sys path contains the site package at its start - new_sys_path = out[5].split(os.path.pathsep) - assert new_sys_path[-len(prev_sys_path) :] == prev_sys_path - extra_start = new_sys_path[0 : -len(prev_sys_path)] - assert len(extra_start) == 1 - assert extra_start[0].startswith(clean_python[0]) - assert tmp_path.__class__(extra_start[0]).exists() - - # manage to import from activate site package - assert os.path.realpath(out[6]) == os.path.realpath(str(clean_python[2])) - except subprocess.CalledProcessError as exception: - assert not exception.returncode, exception.output - - -def test_activate_this_no_file(clean_python, tmp_path, monkeypatch): - activator = tmp_path.__class__(clean_python[1]) / "activate_this.py" - assert activator.exists() - try: - subprocess.check_output( - [sys.executable, "-c", "exec(open({!r}).read())".format(str(activator))], - stderr=subprocess.STDOUT, - universal_newlines=True, - ) - raise RuntimeError("this should not happen") - except subprocess.CalledProcessError as exception: - out = re.sub(r"pydev debugger: process \d+ is connecting\n\n", "", exception.output, re.M).strip() - assert "You must use exec(open(this_file).read(), {'__file__': this_file}))" in out, out diff --git a/tests/activation/test_activation.py b/tests/activation/test_activation.py deleted file mode 100644 index 2b180b5e0..000000000 --- a/tests/activation/test_activation.py +++ /dev/null @@ -1,236 +0,0 @@ -from __future__ import absolute_import, unicode_literals - -import os -import pipes -import re -import subprocess -import sys -from os.path import dirname, join, normcase, realpath - -import pytest -import six - -import virtualenv - -IS_INSIDE_CI = "CI_RUN" in os.environ - - -def need_executable(name, check_cmd): - """skip running this locally if executable not found, unless we're inside the CI""" - - def wrapper(fn): - fn = getattr(pytest.mark, name)(fn) - if not IS_INSIDE_CI: - # locally we disable, so that contributors don't need to have everything setup - # noinspection PyBroadException - try: - fn.version = subprocess.check_output(check_cmd, env=get_env()) - except Exception as exception: - return pytest.mark.skip(reason="{} is not available due {}".format(name, exception))(fn) - return fn - - return wrapper - - -def requires(on): - def wrapper(fn): - return need_executable(on.cmd.replace(".exe", ""), on.check)(fn) - - return wrapper - - -def norm_path(path): - # python may return Windows short paths, normalize - path = realpath(path) - if virtualenv.IS_WIN: - from ctypes import create_unicode_buffer, windll - - buffer_cont = create_unicode_buffer(256) - get_long_path_name = windll.kernel32.GetLongPathNameW - get_long_path_name(six.text_type(path), buffer_cont, 256) # noqa: F821 - result = buffer_cont.value - else: - result = path - return normcase(result) - - -class Activation(object): - cmd = "" - extension = "test" - invoke_script = [] - command_separator = os.linesep - activate_cmd = "source" - activate_script = "" - check_has_exe = [] - check = [] - env = {} - also_test_error_if_not_sourced = False - - def __init__(self, activation_env, tmp_path): - self.home_dir = activation_env[0] - self.bin_dir = activation_env[1] - self.path = tmp_path - - def quote(self, s): - return pipes.quote(s) - - def python_cmd(self, cmd): - return "{} -c {}".format(self.quote(virtualenv.EXPECTED_EXE), self.quote(cmd)) - - def python_script(self, script): - return "{} {}".format(self.quote(virtualenv.EXPECTED_EXE), self.quote(script)) - - def print_python_exe(self): - return self.python_cmd("import sys; print(sys.executable)") - - def print_os_env_var(self, var): - val = '"{}"'.format(var) - return self.python_cmd("import os; print(os.environ.get({}, None))".format(val)) - - def __call__(self, monkeypatch): - absolute_activate_script = norm_path(join(self.bin_dir, self.activate_script)) - - commands = [ - self.print_python_exe(), - self.print_os_env_var("VIRTUAL_ENV"), - self.activate_call(absolute_activate_script), - self.print_python_exe(), - self.print_os_env_var("VIRTUAL_ENV"), - # pydoc loads documentation from the virtualenv site packages - "pydoc -w pydoc_test", - "deactivate", - self.print_python_exe(), - self.print_os_env_var("VIRTUAL_ENV"), - "", # just finish with an empty new line - ] - script = self.command_separator.join(commands) - test_script = self.path / "script.{}".format(self.extension) - test_script.write_text(script) - assert test_script.exists() - - monkeypatch.chdir(str(self.path)) - invoke_shell = self.invoke_script + [str(test_script)] - - monkeypatch.delenv(str("VIRTUAL_ENV"), raising=False) - - # in case the tool is provided by the dev environment (e.g. xonosh) - env = get_env() - env.update(self.env) - - try: - raw = subprocess.check_output(invoke_shell, universal_newlines=True, stderr=subprocess.STDOUT, env=env) - except subprocess.CalledProcessError as exception: - assert not exception.returncode, exception.output - out = re.sub(r"pydev debugger: process \d+ is connecting\n\n", "", raw, re.M).strip().split("\n") - - # pre-activation - assert out[0], raw - assert out[1] == "None", raw - - # post-activation - exe = "{}.exe".format(virtualenv.EXPECTED_EXE) if virtualenv.IS_WIN else virtualenv.EXPECTED_EXE - assert norm_path(out[2]) == norm_path(join(self.bin_dir, exe)), raw - assert norm_path(out[3]) == norm_path(str(self.home_dir)).replace("\\\\", "\\"), raw - - assert out[4] == "wrote pydoc_test.html" - content = self.path / "pydoc_test.html" - assert content.exists(), raw - - # post deactivation, same as before - assert out[-2] == out[0], raw - assert out[-1] == "None", raw - - if self.also_test_error_if_not_sourced: - invoke_shell = self.invoke_script + [absolute_activate_script] - - with pytest.raises(subprocess.CalledProcessError) as c: - subprocess.check_output(invoke_shell, stderr=subprocess.STDOUT, env=env) - assert c.value.returncode, c - - def activate_call(self, script): - return "{} {}".format(pipes.quote(self.activate_cmd), pipes.quote(script)).strip() - - -def get_env(): - env = os.environ.copy() - env[str("PATH")] = os.pathsep.join([dirname(sys.executable)] + env.get(str("PATH"), str("")).split(os.pathsep)) - return env - - -class BashActivation(Activation): - cmd = "bash.exe" if virtualenv.IS_WIN else "bash" - invoke_script = [cmd] - extension = "sh" - activate_script = "activate" - check = [cmd, "--version"] - also_test_error_if_not_sourced = True - - -@pytest.mark.skipif(sys.platform == "win32", reason="no sane way to provision bash on Windows yet") -@requires(BashActivation) -def test_bash(clean_python, monkeypatch, tmp_path): - BashActivation(clean_python, tmp_path)(monkeypatch) - - -class CshActivation(Activation): - cmd = "csh.exe" if virtualenv.IS_WIN else "csh" - invoke_script = [cmd] - extension = "csh" - activate_script = "activate.csh" - check = [cmd, "--version"] - - -@pytest.mark.skipif(sys.platform == "win32", reason="no sane way to provision csh on Windows yet") -@requires(CshActivation) -def test_csh(clean_python, monkeypatch, tmp_path): - CshActivation(clean_python, tmp_path)(monkeypatch) - - -class FishActivation(Activation): - cmd = "fish.exe" if virtualenv.IS_WIN else "fish" - invoke_script = [cmd] - extension = "fish" - activate_script = "activate.fish" - check = [cmd, "--version"] - - -@pytest.mark.skipif(sys.platform == "win32", reason="no sane way to provision fish on Windows yet") -@requires(FishActivation) -def test_fish(clean_python, monkeypatch, tmp_path): - FishActivation(clean_python, tmp_path)(monkeypatch) - - -class PowershellActivation(Activation): - cmd = "powershell.exe" if virtualenv.IS_WIN else "pwsh" - extension = "ps1" - invoke_script = [cmd, "-File"] - activate_script = "activate.ps1" - activate_cmd = "." - check = [cmd, "-c", "$PSVersionTable"] - - def quote(self, s): - """powershell double double quote needed for quotes within single quotes""" - return pipes.quote(s).replace('"', '""') - - -@requires(PowershellActivation) -def test_powershell(clean_python, monkeypatch, tmp_path): - PowershellActivation(clean_python, tmp_path)(monkeypatch) - - -class XonoshActivation(Activation): - cmd = "xonsh" - extension = "xsh" - invoke_script = [sys.executable, "-m", "xonsh"] - activate_script = "activate.xsh" - check = [sys.executable, "-m", "xonsh", "--version"] - env = {"XONSH_DEBUG": "1", "XONSH_SHOW_TRACEBACK": "True"} - - def activate_call(self, script): - return "{} {}".format(self.activate_cmd, repr(script)).strip() - - -@pytest.mark.skipif(sys.version_info < (3, 5), reason="xonosh requires Python 3.5 at least") -@requires(XonoshActivation) -def test_xonosh(clean_python, monkeypatch, tmp_path): - XonoshActivation(clean_python, tmp_path)(monkeypatch) diff --git a/tests/activation/test_prompts.py b/tests/activation/test_prompts.py deleted file mode 100644 index 2d7b8b9d1..000000000 --- a/tests/activation/test_prompts.py +++ /dev/null @@ -1,440 +0,0 @@ -"""test that prompt behavior is correct in supported shells""" -from __future__ import absolute_import, unicode_literals - -import os -import subprocess -import sys -from textwrap import dedent - -import pytest - -import virtualenv -from virtualenv import IS_DARWIN, IS_WIN - -try: - from pathlib import Path -except ImportError: - from pathlib2 import Path - -VIRTUAL_ENV_DISABLE_PROMPT = "VIRTUAL_ENV_DISABLE_PROMPT" - -# This must match the DEST_DIR provided in the ../conftest.py:clean_python fixture -ENV_DEFAULT = "env" - -# This can be anything -ENV_CUSTOM = "envy" - -# Standard prefix, surround the env name in parentheses and separate by a space -PREFIX_DEFAULT = "({}) ".format(ENV_DEFAULT) - -# Arbitrary prefix for the environment that's provided a 'prompt' arg -PREFIX_CUSTOM = "---ENV---" - -# Temp script filename template: {shell}.script.(normal|suppress).(default|custom)[extension] -SCRIPT_TEMPLATE = "{}.script.{}.{}{}" - -# Temp output filename template: {shell}.out.(normal|suppress).(default|custom) -OUTPUT_TEMPLATE = "{}.out.{}.{}" - -# For skipping shells not installed by default if absent on a contributor's system -IS_INSIDE_CI = "CI_RUN" in os.environ - - -# Py2 doesn't like unicode in the environment -def env_compat(string): - return string.encode("utf-8") if sys.version_info.major < 3 else string - - -class ShellInfo(object): - """Parent class for shell information for prompt testing.""" - - # Typo insurance - __slots__ = [] - - # Equality check based on .name, but only if both are not None - def __eq__(self, other): - if type(self) != type(other): - return False - if self.name is None or other.name is None: - return False - return self.name == other.name - - # Helper formatting string - @property - def platform_incompat_msg(self): - return "No sane provision for {} on {{}} yet".format(self.name) - - # Each shell must specify - name = None - avail_cmd = None - execute_cmd = None - prompt_cmd = None - activate_script = None - - # Default values defined here - # 'preamble_cmd' *MUST NOT* emit anything to stdout! - testscript_extension = "" - preamble_cmd = "" - activate_cmd = "source " - deactivate_cmd = "deactivate" - clean_env_update = {} - - # Skip check function; must be specified per-shell - platform_check_skip = None - - # Test assert method for comparing activated prompt to deactivated. - # Default defined, but can be overridden per-shell. Takes the captured - # lines of output as the lone argument. - def overall_prompt_test(self, lines, prefix): - """Perform all tests on (de)activated prompts. - - From a Python 3 perspective, 'lines' is expected to be *bytes*, - and 'prefix' is expected to be *str*. - - Happily, this all seems to translate smoothly enough to 2.7. - - """ - # Prompts before activation and after deactivation should be identical. - assert lines[1] == lines[3], lines - - # The .partition here operates on the environment marker text expected to occur - # in the prompt. A non-empty 'env_marker' thus tests that the correct marker text - # has been applied into the prompt string. - before, env_marker, after = lines[2].partition(prefix.encode("utf-8")) - assert env_marker != b"", lines - - # Some shells need custom activated-prompt tests, so this is split into - # its own submethod. - self.activated_prompt_test(lines, after) - - def activated_prompt_test(self, lines, after): - """Perform just the check for the deactivated prompt contents in the activated prompt text. - - The default is a strict requirement that the portion of the activated prompt following the environment - marker must exactly match the non-activated prompt. - - Some shells require weaker tests, due to idiosyncrasies. - - """ - assert after == lines[1], lines - - -class BashInfo(ShellInfo): - name = "bash" - avail_cmd = "bash -c 'echo foo'" - execute_cmd = "bash" - prompt_cmd = 'echo "$PS1"' - activate_script = "activate" - - def platform_check_skip(self): - if IS_WIN: - return self.platform_incompat_msg.format(sys.platform) - - -class FishInfo(ShellInfo): - name = "fish" - avail_cmd = "fish -c 'echo foo'" - execute_cmd = "fish" - prompt_cmd = "fish_prompt; echo ' '" - activate_script = "activate.fish" - - # Azure Devops doesn't set a terminal type, which breaks fish's colorization - # machinery in a way that spuriously fouls the activation script. - clean_env_update = {"TERM": "linux"} - - def platform_check_skip(self): - if IS_WIN: - return self.platform_incompat_msg.format(sys.platform) - - def activated_prompt_test(self, lines, after): - """Require a looser match here, due to interposed ANSI color codes. - - This construction allows coping with the messiness of fish's ANSI codes for colorizing. - It's not as rigorous as I would like---it doesn't ensure no space is inserted between - a custom env prompt (argument to --prompt) and the base prompt---but it does provide assurance as - to the key pieces of content that should be present. - - """ - assert lines[1] in after, lines - - -class CshInfo(ShellInfo): - name = "csh" - avail_cmd = "csh -c 'echo foo'" - execute_cmd = "csh" - prompt_cmd = r"set | grep -E 'prompt\s' | sed -E 's/^prompt\s+(.*)$/\1/'" - activate_script = "activate.csh" - - # csh defaults to an unset 'prompt' in non-interactive shells - preamble_cmd = "set prompt=%" - - def platform_check_skip(self): - if IS_WIN: - return self.platform_incompat_msg.format(sys.platform) - - def activated_prompt_test(self, lines, after): - """Test with special handling on MacOS, which does funny things to stdout under (t)csh.""" - if IS_DARWIN: - # Looser assert for (t)csh on MacOS, which prepends extra text to - # what gets sent to stdout - assert lines[1].endswith(after), lines - else: - # Otherwise, use the rigorous default - # Full 2-arg form for super() used for 2.7 compat - super(CshInfo, self).activated_prompt_test(lines, after) - - -class XonshInfo(ShellInfo): - name = "xonsh" - avail_cmd = "xonsh -c 'echo foo'" - execute_cmd = "xonsh" - prompt_cmd = "print(__xonsh__.shell.prompt)" - activate_script = "activate.xsh" - - # Sets consistent initial state - preamble_cmd = ( - "$VIRTUAL_ENV = ''; $PROMPT = '{env_name}$ '; " - "$PROMPT_FIELDS['env_prefix'] = '('; $PROMPT_FIELDS['env_postfix'] = ') '" - ) - - @staticmethod - def platform_check_skip(): - if IS_WIN: - return "Provisioning xonsh on windows is unreliable" - - if sys.version_info < (3, 5): - return "xonsh requires Python 3.5 at least" - - -class CmdInfo(ShellInfo): - name = "cmd" - avail_cmd = "echo foo" - execute_cmd = "" - prompt_cmd = "echo %PROMPT%" - activate_script = "activate.bat" - - testscript_extension = ".bat" - preamble_cmd = "@echo off & set PROMPT=$P$G" # For consistent initial state - activate_cmd = "call " - deactivate_cmd = "call deactivate" - - def platform_check_skip(self): - if not IS_WIN: - return self.platform_incompat_msg.format(sys.platform) - - -class PoshInfo(ShellInfo): - name = "powershell" - avail_cmd = "powershell 'echo foo'" - execute_cmd = "powershell -File " - prompt_cmd = "prompt" - activate_script = "activate.ps1" - - testscript_extension = ".ps1" - activate_cmd = ". " - - def platform_check_skip(self): - if not IS_WIN: - return self.platform_incompat_msg.format(sys.platform) - - -SHELL_INFO_LIST = [BashInfo(), FishInfo(), CshInfo(), XonshInfo(), CmdInfo(), PoshInfo()] - - -@pytest.fixture(scope="module") -def posh_execute_enabled(tmp_path_factory): - """Return check value for whether Powershell script execution is enabled. - - Posh may be available interactively, but the security settings may not allow - execution of script files. - - # Enable with: PS> Set-ExecutionPolicy -scope currentuser -ExecutionPolicy Bypass -Force; - # Disable with: PS> Set-ExecutionPolicy -scope currentuser -ExecutionPolicy Restricted -Force; - - """ - if not IS_WIN: - return False - - test_ps1 = tmp_path_factory.mktemp("posh_test") / "test.ps1" - with open(str(test_ps1), "w") as f: - f.write("echo 'foo bar baz'\n") - - out = subprocess.check_output(["powershell", "-File", "{}".format(str(test_ps1))], shell=True) - return b"foo bar baz" in out - - -@pytest.fixture(scope="module") -def shell_avail(posh_execute_enabled): - """Generate mapping of ShellInfo.name strings to bools of shell availability.""" - retvals = {si.name: subprocess.call(si.avail_cmd, shell=True) for si in SHELL_INFO_LIST} - avails = {si.name: retvals[si.name] == 0 for si in SHELL_INFO_LIST} - - # Extra check for whether powershell scripts are enabled - avails[PoshInfo().name] = avails[PoshInfo().name] and posh_execute_enabled - - return avails - - -@pytest.fixture(scope="module") -def custom_prompt_root(tmp_path_factory): - """Provide Path to root with default and custom venvs created.""" - root = tmp_path_factory.mktemp("custom_prompt") - virtualenv.create_environment( - str(root / ENV_CUSTOM), prompt=PREFIX_CUSTOM, no_setuptools=True, no_pip=True, no_wheel=True - ) - - _, _, _, bin_dir = virtualenv.path_locations(str(root / ENV_DEFAULT)) - - bin_dir_name = os.path.split(bin_dir)[-1] - - return root, bin_dir_name - - -@pytest.fixture(scope="module") -def clean_python_root(clean_python): - root = Path(clean_python[0]).resolve().parent - bin_dir_name = os.path.split(clean_python[1])[-1] - - return root, bin_dir_name - - -@pytest.fixture(scope="module") -def get_work_root(clean_python_root, custom_prompt_root): - def pick_root(env): - if env == ENV_DEFAULT: - return clean_python_root - elif env == ENV_CUSTOM: - return custom_prompt_root - else: - raise ValueError("Invalid test virtualenv") - - return pick_root - - -@pytest.fixture(scope="function") -def clean_env(): - """Provide a fresh copy of the shell environment. - - VIRTUAL_ENV_DISABLE_PROMPT is always removed, if present, because - the prompt tests assume it to be unset. - - """ - clean_env = os.environ.copy() - clean_env.pop(env_compat(VIRTUAL_ENV_DISABLE_PROMPT), None) - return clean_env - - -@pytest.mark.parametrize("shell_info", SHELL_INFO_LIST, ids=[i.name for i in SHELL_INFO_LIST]) -@pytest.mark.parametrize("env", [ENV_DEFAULT, ENV_CUSTOM], ids=["default", "custom"]) -@pytest.mark.parametrize(("value", "disable"), [("", False), ("0", True), ("1", True)]) -def test_suppressed_prompt(shell_info, shell_avail, env, value, disable, get_work_root, clean_env): - """Confirm non-empty VIRTUAL_ENV_DISABLE_PROMPT suppresses prompt changes on activate.""" - skip_test = shell_info.platform_check_skip() - if skip_test: - pytest.skip(skip_test) - - if not IS_INSIDE_CI and not shell_avail[shell_info.name]: - pytest.skip( - "Shell '{}' not provisioned{}".format( - shell_info.name, " - is Powershell script execution disabled?" if shell_info == PoshInfo() else "" - ) - ) - - script_name = SCRIPT_TEMPLATE.format(shell_info.name, "suppress", env, shell_info.testscript_extension) - output_name = OUTPUT_TEMPLATE.format(shell_info.name, "suppress", env) - - clean_env.update({env_compat(VIRTUAL_ENV_DISABLE_PROMPT): env_compat(value)}) - - work_root = get_work_root(env) - - # The extra "{prompt}" here copes with some oddity of xonsh in certain emulated terminal - # contexts: xonsh can dump stuff into the first line of the recorded script output, - # so we have to include a dummy line of output that can get munged w/o consequence. - with open(str(work_root[0] / script_name), "w") as f: - f.write( - dedent( - """\ - {preamble} - {prompt} - {prompt} - {act_cmd}{env}/{bindir}/{act_script} - {prompt} - """.format( - env=env, - act_cmd=shell_info.activate_cmd, - preamble=shell_info.preamble_cmd, - prompt=shell_info.prompt_cmd, - act_script=shell_info.activate_script, - bindir=work_root[1], - ) - ) - ) - - command = "{} {} > {}".format(shell_info.execute_cmd, script_name, output_name) - - assert 0 == subprocess.call(command, cwd=str(work_root[0]), shell=True, env=clean_env) - - with open(str(work_root[0] / output_name), "rb") as f: - text = f.read() - lines = text.split(b"\n") - - # Is the prompt suppressed based on the env var value? - assert (lines[1] == lines[2]) == disable, text - - -@pytest.mark.parametrize("shell_info", SHELL_INFO_LIST, ids=[i.name for i in SHELL_INFO_LIST]) -@pytest.mark.parametrize(["env", "prefix"], [(ENV_DEFAULT, PREFIX_DEFAULT), (ENV_CUSTOM, PREFIX_CUSTOM)]) -def test_activated_prompt(shell_info, shell_avail, env, prefix, get_work_root, clean_env): - """Confirm prompt modification behavior with and without --prompt specified.""" - skip_test = shell_info.platform_check_skip() - if skip_test: - pytest.skip(skip_test) - - if not IS_INSIDE_CI and not shell_avail[shell_info.name]: - pytest.skip( - "Shell '{}' not provisioned".format(shell_info.name) - + (" - is Powershell script execution disabled?" if shell_info == PoshInfo() else "") - ) - - for k, v in shell_info.clean_env_update.items(): - clean_env.update({env_compat(k): env_compat(v)}) - - script_name = SCRIPT_TEMPLATE.format(shell_info.name, "normal", env, shell_info.testscript_extension) - output_name = OUTPUT_TEMPLATE.format(shell_info.name, "normal", env) - - work_root = get_work_root(env) - - # The extra "{prompt}" here copes with some oddity of xonsh in certain emulated terminal - # contexts: xonsh can dump stuff into the first line of the recorded script output, - # so we have to include a dummy line of output that can get munged w/o consequence. - with open(str(work_root[0] / script_name), "w") as f: - f.write( - dedent( - """\ - {preamble} - {prompt} - {prompt} - {act_cmd}{env}/{bindir}/{act_script} - {prompt} - {deactivate} - {prompt} - """.format( - env=env, - act_cmd=shell_info.activate_cmd, - deactivate=shell_info.deactivate_cmd, - preamble=shell_info.preamble_cmd, - prompt=shell_info.prompt_cmd, - act_script=shell_info.activate_script, - bindir=work_root[1], - ) - ) - ) - - command = "{} {} > {}".format(shell_info.execute_cmd, script_name, output_name) - - assert 0 == subprocess.call(command, cwd=str(work_root[0]), shell=True, env=clean_env) - - with open(str(work_root[0] / output_name), "rb") as f: - lines = f.read().split(b"\n") - - shell_info.overall_prompt_test(lines, prefix) diff --git a/tests/conftest.py b/tests/conftest.py index c8990ce89..7e021d047 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,139 +1,249 @@ +# -*- coding: utf-8 -*- from __future__ import absolute_import, unicode_literals import os -import pipes -import subprocess +import shutil import sys -import textwrap +from functools import partial +import coverage import pytest +import six -import virtualenv +from virtualenv.interpreters.discovery.py_info import PythonInfo +from virtualenv.util.path import Path -try: - from pathlib import Path -except ImportError: - from pathlib2 import Path -ROOT_DIR = Path(__file__).parents[1] +@pytest.fixture(scope="session") +def has_symlink_support(tmp_path_factory): + platform_supports = hasattr(os, "symlink") + if platform_supports and sys.platform == "win32": + # on Windows correct functioning of this is tied to SeCreateSymbolicLinkPrivilege, try if it works + test_folder = tmp_path_factory.mktemp("symlink-tests") + src = test_folder / "src" + try: + src.symlink_to(test_folder / "dest") + except OSError: + return False + finally: + shutil.rmtree(str(test_folder)) + + return platform_supports @pytest.fixture(scope="session") -def clean_python(tmp_path_factory): - path = tmp_path_factory.mktemp("activation-test-env") - prev_cwd = os.getcwd() - try: - os.chdir(str(path)) - home_dir, _, __, bin_dir = virtualenv.path_locations(str(path / "env")) - virtualenv.create_environment(home_dir, no_pip=True, no_setuptools=True, no_wheel=True) - - site_packages = subprocess.check_output( - [ - os.path.join(bin_dir, virtualenv.EXPECTED_EXE), - "-c", - "from distutils.sysconfig import get_python_lib; print(get_python_lib())", - ], - universal_newlines=True, - ).strip() - - pydoc_test = path.__class__(site_packages) / "pydoc_test.py" - pydoc_test.write_text('"""This is pydoc_test.py"""') - finally: - os.chdir(str(prev_cwd)) +def link_folder(has_symlink_support): + if has_symlink_support: + return os.symlink + elif sys.platform == "win32" and sys.version_info[0:2] > (3, 4): + # on Windows junctions may be used instead + import _winapi # python3.5 has builtin implementation for junctions + + return _winapi.CreateJunction + else: + return None - yield home_dir, bin_dir, pydoc_test +@pytest.fixture(scope="session") +def link_file(has_symlink_support): + if has_symlink_support: + return os.symlink + else: + return None -@pytest.fixture() -def sdist(tmp_path): - """make assertions on what we package""" - import tarfile - - path = os.environ.get("TOX_PACKAGE") - if path is not None: - dest_path = tmp_path / "sdist" - dest_path.mkdir() - prev = os.getcwd() + +@pytest.fixture(scope="session") +def link(link_folder, link_file): + def _link(src, dest): + clean = dest.unlink + s_dest = str(dest) + s_src = str(src) + if src.is_dir(): + if link_folder: + link_folder(s_src, s_dest) + else: + shutil.copytree(s_src, s_dest) + clean = partial(shutil.rmtree, str(dest)) + else: + if link_file: + link_file(s_src, s_dest) + else: + shutil.copy2(s_src, s_dest) + return clean + + return _link + + +@pytest.fixture(autouse=True) +def check_cwd_not_changed_by_test(): + old = os.getcwd() + yield + new = os.getcwd() + if old != new: + pytest.fail("tests changed cwd: {!r} => {!r}".format(old, new)) + + +@pytest.fixture(autouse=True) +def ensure_py_info_cache_empty(): + yield + PythonInfo._cache_from_exe.clear() + + +@pytest.fixture(autouse=True) +def clean_data_dir(tmp_path, monkeypatch): + from virtualenv import info + + monkeypatch.setattr(info, "_DATA_DIR", Path(str(tmp_path))) + yield + + +@pytest.fixture(autouse=True) +def check_os_environ_stable(): + old = os.environ.copy() + # ensure we don't inherit parent env variables + to_clean = { + k for k in os.environ.keys() if k.startswith("VIRTUALENV_") or "VIRTUAL_ENV" in k or k.startswith("TOX_") + } + cleaned = {k: os.environ[k] for k, v in os.environ.items()} + os.environ[str("VIRTUALENV_NO_DOWNLOAD")] = str("1") + is_exception = False + try: + yield + except BaseException: + is_exception = True + raise + finally: try: - os.chdir(str(dest_path)) - tar = tarfile.open(path, "r:gz") - tar.extractall() - return next(dest_path.iterdir()) + del os.environ[str("VIRTUALENV_NO_DOWNLOAD")] + if is_exception is False: + new = os.environ + extra = {k: new[k] for k in set(new) - set(old)} + miss = {k: old[k] for k in set(old) - set(new) - to_clean} + diff = { + "{} = {} vs {}".format(k, old[k], new[k]) + for k in set(old) & set(new) + if old[k] != new[k] and not k.startswith("PYTEST_") + } + if extra or miss or diff: + msg = "tests changed environ" + if extra: + msg += " extra {}".format(extra) + if miss: + msg += " miss {}".format(miss) + if diff: + msg += " diff {}".format(diff) + pytest.fail(msg) finally: - os.chdir(prev) - return None + os.environ.update(cleaned) -@pytest.fixture(scope="session") -def wheel(tmp_path_factory): - """test that we can create a virtual environment by feeding to a clean python the wheels content""" - dest_path = tmp_path_factory.mktemp("wheel") - env = os.environ.copy() - try: - subprocess.check_output( - [sys.executable, "-m", "pip", "wheel", "-w", str(dest_path), "--no-deps", str(ROOT_DIR)], - universal_newlines=True, - stderr=subprocess.STDOUT, - env=env, - ) - except subprocess.CalledProcessError as exception: - assert not exception.returncode, exception.output +COV_ENV_VAR = "COVERAGE_PROCESS_START" +COVERAGE_RUN = os.environ.get(COV_ENV_VAR) - wheels = list(dest_path.glob("*.whl")) - assert len(wheels) == 1 - wheel = wheels[0] - return wheel +@pytest.fixture(autouse=True) +def coverage_env(monkeypatch, link): + """ + Enable coverage report collection on the created virtual environments by injecting the coverage project + """ + if COVERAGE_RUN: + # we inject right after creation, we cannot collect coverage on site.py - used for helper scripts, such as debug + from virtualenv import run + + def via_cli(args): + session = prev_run(args) + old_run = session.creator.run + + def create_run(): + result = old_run() + obj["cov"] = EnableCoverage(link) + obj["cov"].__enter__(session.creator) + return result + + monkeypatch.setattr(session.creator, "run", create_run) + return session + + obj = {"cov": None} + prev_run = run.session_via_cli + monkeypatch.setattr(run, "session_via_cli", via_cli) + + def finish(): + cov = obj["cov"] + obj["cov"] = None + cov.__exit__(None, None, None) + + yield finish + if obj["cov"]: + finish() -@pytest.fixture() -def extracted_wheel(tmp_path, wheel): - dest_path = tmp_path / "wheel-extracted" - - import zipfile - - with zipfile.ZipFile(str(wheel), "r") as zip_ref: - zip_ref.extractall(str(dest_path)) - return dest_path - - -def _call(cmd, env=None, stdin=None, allow_fail=False, shell=False, **kwargs): - env = os.environ if env is None else env - process = subprocess.Popen( - cmd, - universal_newlines=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - stdin=subprocess.PIPE, - env=env, - shell=shell, - **kwargs - ) - out, err = process.communicate(input=stdin) - if allow_fail is False: - msg = textwrap.dedent( - """ - cmd: - {} - out: - {} - err: - {} - env: - {} - """ - ).format( - cmd if shell else " ".join(pipes.quote(str(i)) for i in cmd), - out, - err, - os.linesep.join("{}={!r}".format(k, v) for k, v in env.items()), - ) - msg = msg.lstrip() - assert process.returncode == 0, msg - return out, err else: - return process.returncode, out, err + + def finish(): + pass + + yield finish + + +class EnableCoverage(object): + _COV_FILE = Path(coverage.__file__) + _COV_SITE_PACKAGES = _COV_FILE.parents[1] + _ROOT_COV_FILES_AND_FOLDERS = [i for i in _COV_SITE_PACKAGES.iterdir() if i.name.startswith("coverage")] + _SUBPROCESS_TRIGGER_PTH_NAME = "coverage-virtual-sub.pth" + + def __init__(self, link): + self.link = link + self.targets = [] + self.cov_pth = self._COV_SITE_PACKAGES / self._SUBPROCESS_TRIGGER_PTH_NAME + + def __enter__(self, creator): + assert not self.cov_pth.exists() + site_packages = creator.site_packages[0] + p_th = site_packages / self._SUBPROCESS_TRIGGER_PTH_NAME + + if not str(p_th).startswith(str(self._COV_SITE_PACKAGES)): + p_th.write_text("import coverage; coverage.process_startup()") + self.targets.append((p_th, p_th.unlink)) + for entry in self._ROOT_COV_FILES_AND_FOLDERS: + target = site_packages / entry.name + if not target.exists(): + clean = self.link(entry, target) + self.targets.append((target, clean)) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + assert self._COV_FILE.exists() + for target, clean in self.targets: + if target.exists(): + clean() + assert not self.cov_pth.exists() + assert self._COV_FILE.exists() + + +@pytest.fixture(scope="session") +def is_inside_ci(): + yield "CI_RUN" in os.environ @pytest.fixture(scope="session") -def call_subprocess(): - return _call +def special_char_name(): + base = "$ èрт🚒♞中片" + encoding = sys.getfilesystemencoding() + # let's not include characters that the file system cannot encode) + result = "" + for char in base: + try: + encoded = char.encode(encoding, errors="strict") + if char == "?" or encoded != b"?": # mbcs notably on Python 2 uses replace even for strict + result += char + except ValueError: + continue + assert result + return result + + +@pytest.fixture() +def special_name_dir(tmp_path, special_char_name): + dest = Path(str(tmp_path)) / special_char_name + yield dest + if six.PY2 and sys.platform == "win32": # pytest python2 windows does not support unicode delete + shutil.rmtree(six.ensure_text(str(dest))) diff --git a/tests/old-wheels/pip-9.0.1-py2.py3-none-any.whl b/tests/old-wheels/pip-9.0.1-py2.py3-none-any.whl deleted file mode 100644 index 4b8ecc69d..000000000 Binary files a/tests/old-wheels/pip-9.0.1-py2.py3-none-any.whl and /dev/null differ diff --git a/tests/old-wheels/setuptools-30.4.0-py2.py3-none-any.whl b/tests/old-wheels/setuptools-30.4.0-py2.py3-none-any.whl deleted file mode 100644 index 225531ab7..000000000 Binary files a/tests/old-wheels/setuptools-30.4.0-py2.py3-none-any.whl and /dev/null differ diff --git a/tests/ssl/rootCA.key b/tests/ssl/rootCA.key deleted file mode 100644 index 164c7e8ad..000000000 --- a/tests/ssl/rootCA.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEA0D+NxiOUkvWp45C75ZmCdzl3Cqu09MV9GvVjv5vU2So92pJI -sMmZDavteLEdA1g/T3Wzh6vop0NGXskrGIQ0QXPcnbtJtWH2n+R8/N4xlFliEwX+ -JZOrzWFeZGzVJd7VKr2ncO/B5VGIYPB+HTD3VYzm2ACZnyB7NvTJXM/IH0x19TI8 -Bad3rNNX3XnL0c6N8Zd9Hqyj25ciNSiY8a/TnvOEtLw1yo1HNUX6V5xgI5fslDXl -t3tKg3zmuGZXSLhYMjEN4q7NOdI8qW7mQrabCvyiIDCxJeLHdL+ks6FWmoHL9j5r -vHq8lyaMM28d/928KSlZtNpwxh5nhHEChLlf8QIDAQABAoIBAQCxII13LYZO5ZNm -ExIuvT5iKEeflOLqmxvJFVWNgX8uY6aOxYP8ksyS+1yWHped466d6HAWgtr1gdxV -/OeiB7jmvyS0KLwOAlAiOdcxwdAL7Wbk5WEBFzS3EQ2Xf5ZgisNngj7saZHTemD0 -iznJnH+TjbA/o2sHFTqYSOcJAVanfviqCxH/3xKPAS9vnFPYcpPr8xrQaR2sX6Zy -aZUNVE4udSl3+GqMhMeq1dxssua4PQVsplwIJbY3KUIQIkXNLR8Zal8ygi5HI29U -q+PPiM89O9lgzUz246x1ruNKIl+zgOQsn1yg0Wx+ho1b5kVE4yqi58tVPCN/Bc8H -lWai906RAoGBAPjFWDnrBo5PBVuXOYmPvo10J9n2iIOKOBr3V4OlUb46LcRk19E9 -nV+zidoNKxTaB+g7dlKqYpvOnZ0MUKLPPE1G1U3RlKO+qVhnQ/Pf1N1e88ojCWDu -pFWezwOAO/BJGKQk7sYqMz47hfnLRydeipMv3ASEhKr1rP/b3M1ICrd1AoGBANZM -wOHm0fd87w3WS8eBivI3+W1mL6mHzNJ20G3OM1kUPcbUBzqocwYp3AUXZmO6X8GC -RSmkQ7U81yA5GcdhPIlGa8emSuYPyKBAjgX3fOGgZGNMrcmc+o+uRdyzfxKR1Oph -nnLKj2f7xIXGgGs6gqv00omLEScZ4oqBL+I4ovMNAoGBAM9fem81as6gMqAqDI2O -ZNL3u+ym5R95zdE01B/qZJzFVLd9NKa4zQIk4MoC5iHIqoS9ZKH+ZJrq/loXFPTS -+bqVTGRFS7m/ytzloDCgKoqqh2C+GihSZmz1KC4L7GseE8to+h34uaSr67/R4yt/ -VNbjM24UpZ75ks/qEEKTRlOtAoGAad6nV7MJvgO5shNRgrGL7Fgc4KAgIdfWJ4/N -LsI0+egXEPhEzgTUNpJNgTJrQg+cKORruMPPM2VoPiIXizmNh6ADtTGBRSE9E3Zw -85+t0WXE0o3aOYVU/9Cv8PW4bHshPd2WQs2xhUSLdS942ACi98LOlGbgxXlzQsvJ -1S/3yK0CgYEAwu+gAfwpuJSSk7yyiUjIh6PA00lZnuxgj2AeqS2RSRk+YkV8ZJOP -MhoZpJv+OCrAQFY9ybptJfbGyJQNkpkRHt82aNbevFxly2tTVT+zbeUbeMnX5qn4 -nOCaAc9u4QDTrSE3DS/JSWelv4gjq782THmWSSZfrwS/qV6KrhqZfXk= ------END RSA PRIVATE KEY----- diff --git a/tests/ssl/rootCA.pem b/tests/ssl/rootCA.pem deleted file mode 100644 index dc544753a..000000000 --- a/tests/ssl/rootCA.pem +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDXzCCAkegAwIBAgIJAJyQNZE5SUeEMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV -BAYTAlJVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX -aWRnaXRzIFB0eSBMdGQwIBcNMTkwMTExMTkwNjM4WhgPMjI5MjEwMjYxOTA2Mzha -MEUxCzAJBgNVBAYTAlJVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJ -bnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw -ggEKAoIBAQDQP43GI5SS9anjkLvlmYJ3OXcKq7T0xX0a9WO/m9TZKj3akkiwyZkN -q+14sR0DWD9PdbOHq+inQ0ZeySsYhDRBc9ydu0m1Yfaf5Hz83jGUWWITBf4lk6vN -YV5kbNUl3tUqvadw78HlUYhg8H4dMPdVjObYAJmfIHs29Mlcz8gfTHX1MjwFp3es -01fdecvRzo3xl30erKPblyI1KJjxr9Oe84S0vDXKjUc1RfpXnGAjl+yUNeW3e0qD -fOa4ZldIuFgyMQ3irs050jypbuZCtpsK/KIgMLEl4sd0v6SzoVaagcv2Pmu8eryX -Jowzbx3/3bwpKVm02nDGHmeEcQKEuV/xAgMBAAGjUDBOMB0GA1UdDgQWBBTYiCMT -/U1XaXKam/gNiQK28/f1jzAfBgNVHSMEGDAWgBTYiCMT/U1XaXKam/gNiQK28/f1 -jzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBll3abJLJxh8LNAz/B -xpPly54SttwEqDeFC6y35ilgq53aNAu5Smx+ka9OotnPcfuUEIuOf7kWre02OuAU -FCEcViXLM9+NWMVKTD+ZQ8H0VL0GMJOidt/FReTSixawJY1YB6HhFwI1yVd+x8+l -vwyqoHNz/Uk6unsHIAJk5Esj3IuiYwOw6KmsiZv9IsIX7IXv3g6eKYC7162ArLyw -8nPFWcKJTAHFzmGhvrS6vV08mv+6Helgp/B3OFm/OqDm2LwASIUDlzQjdeowNo9Z -z6O/JNjoBLCMAVm/an/zONUb/XZXsw3CAYAta3fuRT7FptMBIJCtMuVgrmzzG79X -PcF/ ------END CERTIFICATE----- diff --git a/tests/ssl/server.crt b/tests/ssl/server.crt deleted file mode 100644 index c085d00f0..000000000 --- a/tests/ssl/server.crt +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDHDCCAgQCCQD8eKhHCWYb1TANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJS -VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 -cyBQdHkgTHRkMCAXDTE5MDExMTE5MDc0N1oYDzIyOTIxMDI2MTkwNzQ3WjBZMQsw -CQYDVQQGEwJSVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJu -ZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqG -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDr/u2BS0mKqz+rLwyd79LrcvBZOpXoRVib -lE2zhz9ZBlQZ/nmSg40QPZqOsgrGZKYFMfLY1AYNpKCJYe4lBQ2YqU1Jac0WYXdh -WEfEKW1hYCsuY9wpz1kVfCAfUD2+1/hOBd2ghgpx3YJgkjIPUeywpalOxD+1j+8r -RjfouVl4kB9UOsRoJzZHP2L6yzCABeoZgVgupkJNHx1KMSYyETLvwxni/FE1lmFA -kxdRV7B8YcWlG+0ewl23KTdMtxtfzUJ+OrShs5fl1UoG8BqRxLVxf889cfkYy6aF -q3tKbRtAgIpmET0UoYzrBbWIVoKgqYnASTFR/hXfGhozN8chStW1AgMBAAEwDQYJ -KoZIhvcNAQELBQADggEBAC/2/sdFcsPSv48dk5GPqutOhQnFDH4qoW9B4cCQXISD -GZ4hDme/RDB/cIgpHBWrtrSq0893d7cXl57aMWjDJmWH1iXTFJ5h9mYhvaWZekR0 -t4D0ZJYVhTiSOdavMc50payWYuTwaUqeXHPkOKKNgGrlcVV4n+tkQq/dvFBZXA8b -ql4x6PmU/Oqf0dY/f5PzgNb1rEHhsdqpJEZdjLsFFKdJX0ikaGak13iEDPR3uEZt -/Eu8VOVGuMwK8+Q4/AUctsfh03wqP82RBlCWTk5eTe34y0dNui3hc+oWIjLf6+pc -xjEnh5EWq/Hf4AEUT6PwP3m3ZmxeUlWMMx6GA27ovrc= ------END CERTIFICATE----- diff --git a/tests/ssl/server.key b/tests/ssl/server.key deleted file mode 100644 index 17cf3dbea..000000000 --- a/tests/ssl/server.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDr/u2BS0mKqz+r -Lwyd79LrcvBZOpXoRViblE2zhz9ZBlQZ/nmSg40QPZqOsgrGZKYFMfLY1AYNpKCJ -Ye4lBQ2YqU1Jac0WYXdhWEfEKW1hYCsuY9wpz1kVfCAfUD2+1/hOBd2ghgpx3YJg -kjIPUeywpalOxD+1j+8rRjfouVl4kB9UOsRoJzZHP2L6yzCABeoZgVgupkJNHx1K -MSYyETLvwxni/FE1lmFAkxdRV7B8YcWlG+0ewl23KTdMtxtfzUJ+OrShs5fl1UoG -8BqRxLVxf889cfkYy6aFq3tKbRtAgIpmET0UoYzrBbWIVoKgqYnASTFR/hXfGhoz -N8chStW1AgMBAAECggEAVpw0piLvVokK0NRvmPcPPYHtW5H4uknY/yAqdBzKzu1X -qEKQc4j8GF3Df2MwOSdvFrECIzmNDyzADit2rvdvyfs4dhzyO6iBm6Q+kmtxzS7y -KhBUGLQUSaJIV7WnM4cnhdr5P5Rx+OAGnVKKNL0oVJw3ysSTbRrp98PJeQutHmVx -7wIBePrmrIeRS3uESty3rqSyypKO6DYhIwsV3yzsr0kad+o6MT1gb/EB7qEj5uNX -E81lnhoHlYtPJujJUCucQmfqKX7LRSq7bEfxFcJXOwKB2/2ILT3tXBTlYlKuvC43 -TaajaaUTSBFhL9zuYv1XWCyX/MXXxA27SvaBIjFE8QKBgQD3I4vGQ94juCCNaUT8 -dPSBn+dnkfqKdxH8WF6q2KdsdYFbFEfW7/rY5jSRQMu6VF/AXiJFh5amQiDUnfWb -QDmtyqGk2kb1g5gUm6plRPL3P3pQ4x1FVtJgVvxV26rQxTV6Bv6zM8SoJUMZJaEz -/z8QX5/dF7vlFGmy8LH4efYsXwKBgQD0dRnvQGFtwnD7xI/0ihltdogrzFb6sOpU -vsXporJS19vjfunCJKjXL9JvFAw2Gk6XUQ9wCUYjLMu1ovahiLm5FMsfhDtTMhvS -9HQvV8RdMk0NSorP/2LfpEsUqqkTXB5dxelbmNJeLr3fK+9azsK0wLXCT3vUo/kV -uPISrh32awKBgEn9jWplhUtCZBSSUMIYrd9lJV2/ubfc4DihqG4UAUQahgjjsIJs -RLjNay2Vrajye9xXEoGoj3TlVXjydcbuWpZqlSyK4TW+GTkKReCd3PQjQBaZeHj6 -/m8ze8akxqZMdK89CuJR/G2vAkC0IGg14gaf6nfCFFIIY4DcSRwwP4CXAoGAantz -r46oocnXsyNc3VUmXFMMX5+jp5FWkVGEHg/7gzB5nK/UnPehABLZo/7kjtCIuUra -4Z94iKvjlBwHODe5RpBzJihQOx4RlqNa1KBzbXEStR5qNs30wJvtBHSOL1up8ojZ -7Ec2I0ZS+JpHqZN0po1m3twGgYpnXnnwIAjuDj8CgYBJHcoMJCRnuYqvDBJzqZDj -6DAGT60qa6E+p/xkKAmys69Un1KDsnLTYNvV7wivqfve0/2Ja9gg3Neocq39tsTF -rTjeKHO3D8FN3C79diWj65ohrS6vOyhkBroAmOxDQgD7nKQrkyd865qlTNsyGFmC -KEFgzjGWDx2Wk9vUYUxBOg== ------END PRIVATE KEY----- diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py deleted file mode 100644 index 2095dc589..000000000 --- a/tests/test_bootstrap.py +++ /dev/null @@ -1,87 +0,0 @@ -from __future__ import absolute_import, unicode_literals - -import inspect -import os -import re -import subprocess -import sys -import textwrap - -import six - -import virtualenv - - -def bootstrap(): - print("startup") - import subprocess - import os - - # noinspection PyUnusedLocal - def extend_parser(opt_parse_parser): - print("extend parser with count") - opt_parse_parser.add_option("-c", action="count", dest="count", default=0, help="Count number of times passed") - - # noinspection PyUnusedLocal - def adjust_options(options, args): - print("adjust options") - options.count += 1 - - # noinspection PyUnusedLocal - def after_install(options, home_dir): - print("after install {} with options {}".format(home_dir, options.count)) - # noinspection PyUnresolvedReferences - _, _, _, bin_dir = path_locations(home_dir) # noqa: F821 - # noinspection PyUnresolvedReferences - print( - "exe at {}".format( - subprocess.check_output( - [os.path.join(bin_dir, EXPECTED_EXE), "-c", "import sys; print(sys.executable)"], # noqa: F821 - universal_newlines=True, - ) - ) - ) - - -def test_bootstrap(tmp_path, monkeypatch): - monkeypatch.chdir(tmp_path) - - extra_code = inspect.getsource(bootstrap) - extra_code = textwrap.dedent(extra_code[extra_code.index("\n") + 1 :]) - - output = virtualenv.create_bootstrap_script(extra_code) - assert extra_code in output - if six.PY2: - output = output.decode() - write_at = tmp_path / "blog-bootstrap.py" - write_at.write_text(output) - - try: - monkeypatch.chdir(tmp_path) - cmd = [ - sys.executable, - str(write_at), - "--no-download", - "--no-pip", - "--no-wheel", - "--no-setuptools", - "-ccc", - "-qqq", - "env", - ] - raw = subprocess.check_output(cmd, universal_newlines=True, stderr=subprocess.STDOUT) - out = re.sub(r"pydev debugger: process \d+ is connecting\n\n", "", raw, re.M).strip().split("\n") - - _, _, _, bin_dir = virtualenv.path_locations(str(tmp_path / "env")) - exe = os.path.realpath( - os.path.join(bin_dir, "{}{}".format(virtualenv.EXPECTED_EXE, ".exe" if virtualenv.IS_WIN else "")) - ) - assert out == [ - "startup", - "extend parser with count", - "adjust options", - "after install env with options 4", - "exe at {}".format(exe), - ] - except subprocess.CalledProcessError as exception: - assert not exception.returncode, exception.output diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py deleted file mode 100644 index 5fb368ab5..000000000 --- a/tests/test_cmdline.py +++ /dev/null @@ -1,69 +0,0 @@ -from __future__ import absolute_import, unicode_literals - -import os -import subprocess -import sys - -import pytest - -import virtualenv - - -def test_commandline_basic(tmpdir): - """Simple command line usage should work and files should be generated""" - home_dir, lib_dir, inc_dir, bin_dir = virtualenv.path_locations(str(tmpdir.join("venv"))) - subprocess.check_call([sys.executable, "-m", "virtualenv", "-vvv", home_dir, "--no-download"]) - - assert os.path.exists(home_dir) - assert os.path.exists(bin_dir) - - assert os.path.exists(os.path.join(bin_dir, "activate")) - assert os.path.exists(os.path.join(bin_dir, "activate_this.py")) - assert os.path.exists(os.path.join(bin_dir, "activate.ps1")) - - exe = os.path.join(bin_dir, os.path.basename(sys.executable)) - assert os.path.exists(exe) - - def _check_no_warnings(module): - subprocess.check_call((exe, "-Werror", "-c", "import {}".format(module))) - - _check_no_warnings("distutils") - - -def test_commandline_os_path_sep(tmp_path): - path = tmp_path / "bad{}0".format(os.pathsep) - assert not path.exists() - process = subprocess.Popen( - [sys.executable, "-m", "virtualenv", str(path)], - cwd=str(tmp_path), - universal_newlines=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - stdin=subprocess.PIPE, - ) - out, err = process.communicate() - assert process.returncode == 3 - msg = ( - "ERROR: target path contains the operating system path separator '{}'\n" - "This is not allowed as would make the activation scripts unusable.\n".format(os.pathsep) - ) - assert not err - assert out == msg - assert not path.exists() - - -def test_commandline_explicit_interp(tmpdir): - """Specifying the Python interpreter should work""" - subprocess.check_call([sys.executable, "-m", "virtualenv", "-p", sys.executable, str(tmpdir.join("venv"))]) - - -# The registry lookups to support the abbreviated "-p 3.5" form of specifying -# a Python interpreter on Windows don't seem to work with Python 3.5. The -# registry layout is not well documented, and it's not clear that the feature -# is sufficiently widely used to be worth fixing. -# See https://github.com/pypa/virtualenv/issues/864 -@pytest.mark.skipif("sys.platform == 'win32' and sys.version_info[:1] >= (3,)") -def test_commandline_abbrev_interp(tmpdir): - """Specifying abbreviated forms of the Python interpreter should work""" - abbrev = "{}{}.{}".format("" if sys.platform == "win32" else "python", *sys.version_info[0:2]) - subprocess.check_call([sys.executable, "-m", "virtualenv", "-p", abbrev, str(tmpdir.join("venv"))]) diff --git a/tests/test_from_source.py b/tests/test_from_source.py deleted file mode 100644 index 39d7c050a..000000000 --- a/tests/test_from_source.py +++ /dev/null @@ -1,49 +0,0 @@ -"""test using the project from source/package rather than install""" -from __future__ import absolute_import, unicode_literals - -import os - -import pytest - -import virtualenv - -try: - from pathlib import Path -except ImportError: - from pathlib2 import Path - -ROOT_DIR = Path(__file__).parents[1] - - -def test_use_from_source_tree(tmp_path, clean_python, monkeypatch, call_subprocess): - """test that we can create a virtual environment by feeding to a clean python the wheels content""" - monkeypatch.chdir(tmp_path) - call_subprocess( - [ - str(Path(clean_python[1]) / virtualenv.EXPECTED_EXE), - str(Path(ROOT_DIR) / "virtualenv.py"), - "--no-download", - "env", - ] - ) - - -@pytest.mark.skipif(os.environ.get("TOX_PACKAGE") is None, reason="needs tox provisioned sdist") -def test_use_from_source_sdist(sdist, tmp_path, clean_python, monkeypatch, call_subprocess): - """test that we can create a virtual environment by feeding to a clean python the sdist content""" - virtualenv_file = sdist / "virtualenv.py" - assert virtualenv_file.exists() - - monkeypatch.chdir(tmp_path) - call_subprocess( - [str(Path(clean_python[1]) / virtualenv.EXPECTED_EXE), str(virtualenv_file), "--no-download", "env"] - ) - - -def test_use_from_wheel(tmp_path, extracted_wheel, clean_python, monkeypatch, call_subprocess): - """test that we can create a virtual environment by feeding to a clean python the wheels content""" - virtualenv_file = extracted_wheel / "virtualenv.py" - monkeypatch.chdir(tmp_path) - call_subprocess( - [str(Path(clean_python[1]) / virtualenv.EXPECTED_EXE), str(virtualenv_file), "--no-download", "env"] - ) diff --git a/tests/test_source_content.py b/tests/test_source_content.py deleted file mode 100644 index 8eae6bc4e..000000000 --- a/tests/test_source_content.py +++ /dev/null @@ -1,62 +0,0 @@ -import os - -import pytest - -import virtualenv - - -@pytest.mark.skipif(os.environ.get("TOX_PACKAGE") is None, reason="needs tox provisioned sdist") -def test_sdist_contains(sdist): - """make assertions on what we package""" - content = set(sdist.iterdir()) - - names = {i.name for i in content} - - must_have = { - # sources - "virtualenv.py", - "virtualenv_embedded", - "virtualenv_support", - "setup.py", - "setup.cfg", - "MANIFEST.in", - "pyproject.toml", - # test files - "tests", - # documentation - "docs", - "README.rst", - # helpers - "tasks", - "tox.ini", - # meta-data - "AUTHORS.txt", - "LICENSE.txt", - } - - missing = must_have - names - assert not missing - - extra = names - must_have - {"PKG-INFO", "virtualenv.egg-info"} - assert not extra, " | ".join(extra) - - -def test_wheel_contains(extracted_wheel): - content = set(extracted_wheel.iterdir()) - - names = {i.name for i in content} - must_have = { - # sources - "virtualenv.py", - "virtualenv_support", - "virtualenv-{}.dist-info".format(virtualenv.__version__), - } - assert must_have == names - - support = {i.name for i in (extracted_wheel / "virtualenv_support").iterdir()} - assert "__init__.py" in support - for package in ("pip", "wheel", "setuptools"): - assert any(package in i for i in support) - - meta = {i.name for i in (extracted_wheel / "virtualenv-{}.dist-info".format(virtualenv.__version__)).iterdir()} - assert {"entry_points.txt", "WHEEL", "RECORD", "METADATA", "top_level.txt", "zip-safe", "LICENSE.txt"} == meta diff --git a/tests/test_virtualenv.py b/tests/test_virtualenv.py deleted file mode 100644 index 620c0cbb0..000000000 --- a/tests/test_virtualenv.py +++ /dev/null @@ -1,668 +0,0 @@ -from __future__ import absolute_import, unicode_literals - -import inspect -import optparse -import os -import shutil -import subprocess -import sys -import tempfile -import textwrap -import zipfile - -import pypiserver -import pytest -import pytest_localserver.http -import six - -import virtualenv - -try: - from pathlib import Path - from unittest.mock import NonCallableMock, call, patch -except ImportError: - from mock import NonCallableMock, call, patch - from pathlib2 import Path - - -try: - import venv as std_venv -except ImportError: - std_venv = None - - -def test_version(): - """Should have a version string""" - assert virtualenv.virtualenv_version, "Should have version" - - -class TestGetInstalledPythons: - key_local_machine = "key-local-machine" - key_current_user = "key-current-user" - key_local_machine_64 = "key-local-machine-64" - key_current_user_64 = "key-current-user-64" - - @classmethod - def mock_virtualenv_winreg(cls, monkeypatch, data): - def enum_key(key, index): - try: - return data.get(key, [])[index] - except IndexError: - raise WindowsError - - def query_value(key, path): - installed_version_tags = data.get(key, []) - suffix = "\\InstallPath" - if path.endswith(suffix): - version_tag = path[: -len(suffix)] - if version_tag in installed_version_tags: - return "{}-{}-path".format(key, version_tag) - raise WindowsError - - mock_winreg = NonCallableMock( - spec_set=[ - "HKEY_LOCAL_MACHINE", - "HKEY_CURRENT_USER", - "KEY_READ", - "KEY_WOW64_32KEY", - "KEY_WOW64_64KEY", - "OpenKey", - "EnumKey", - "QueryValue", - "CloseKey", - ] - ) - mock_winreg.HKEY_LOCAL_MACHINE = "HKEY_LOCAL_MACHINE" - mock_winreg.HKEY_CURRENT_USER = "HKEY_CURRENT_USER" - mock_winreg.KEY_READ = 0x10 - mock_winreg.KEY_WOW64_32KEY = 0x1 - mock_winreg.KEY_WOW64_64KEY = 0x2 - mock_winreg.OpenKey.side_effect = [ - cls.key_local_machine, - cls.key_current_user, - cls.key_local_machine_64, - cls.key_current_user_64, - ] - mock_winreg.EnumKey.side_effect = enum_key - mock_winreg.QueryValue.side_effect = query_value - mock_winreg.CloseKey.return_value = None - monkeypatch.setattr(virtualenv, "winreg", mock_winreg) - return mock_winreg - - @pytest.mark.skipif(sys.platform == "win32", reason="non-windows specific test") - def test_on_non_windows(self, monkeypatch): - assert not virtualenv.IS_WIN - assert not hasattr(virtualenv, "winreg") - assert virtualenv.get_installed_pythons() == {} - - @pytest.mark.skipif(sys.platform != "win32", reason="windows specific test") - def test_on_windows(self, monkeypatch): - assert virtualenv.IS_WIN - mock_winreg = self.mock_virtualenv_winreg( - monkeypatch, - { - self.key_local_machine: ( - "2.4", - "2.7", - "3.2", - "3.4", - "3.6-32", # 32-bit only - "3.7-32", # both 32 & 64-bit with a 64-bit user install - ), - self.key_current_user: ("2.5", "2.7", "3.8-32"), - self.key_local_machine_64: ( - "2.6", - "3.5", # 64-bit only - "3.7", - "3.8", # 64-bit with a 32-bit user install - ), - self.key_current_user_64: ("3.7",), - }, - ) - monkeypatch.setattr(virtualenv, "join", "{}\\{}".format) - - installed_pythons = virtualenv.get_installed_pythons() - - assert installed_pythons == { - "2": self.key_current_user + "-2.7-path\\python.exe", - "2-32": self.key_current_user + "-2.7-path\\python.exe", - "2-64": self.key_local_machine_64 + "-2.6-path\\python.exe", - "2.4": self.key_local_machine + "-2.4-path\\python.exe", - "2.4-32": self.key_local_machine + "-2.4-path\\python.exe", - "2.5": self.key_current_user + "-2.5-path\\python.exe", - "2.5-32": self.key_current_user + "-2.5-path\\python.exe", - "2.6": self.key_local_machine_64 + "-2.6-path\\python.exe", - "2.6-64": self.key_local_machine_64 + "-2.6-path\\python.exe", - "2.7": self.key_current_user + "-2.7-path\\python.exe", - "2.7-32": self.key_current_user + "-2.7-path\\python.exe", - "3": self.key_local_machine_64 + "-3.8-path\\python.exe", - "3-32": self.key_current_user + "-3.8-32-path\\python.exe", - "3-64": self.key_local_machine_64 + "-3.8-path\\python.exe", - "3.2": self.key_local_machine + "-3.2-path\\python.exe", - "3.2-32": self.key_local_machine + "-3.2-path\\python.exe", - "3.4": self.key_local_machine + "-3.4-path\\python.exe", - "3.4-32": self.key_local_machine + "-3.4-path\\python.exe", - "3.5": self.key_local_machine_64 + "-3.5-path\\python.exe", - "3.5-64": self.key_local_machine_64 + "-3.5-path\\python.exe", - "3.6": self.key_local_machine + "-3.6-32-path\\python.exe", - "3.6-32": self.key_local_machine + "-3.6-32-path\\python.exe", - "3.7": self.key_current_user_64 + "-3.7-path\\python.exe", - "3.7-32": self.key_local_machine + "-3.7-32-path\\python.exe", - "3.7-64": self.key_current_user_64 + "-3.7-path\\python.exe", - "3.8": self.key_local_machine_64 + "-3.8-path\\python.exe", - "3.8-32": self.key_current_user + "-3.8-32-path\\python.exe", - "3.8-64": self.key_local_machine_64 + "-3.8-path\\python.exe", - } - assert mock_winreg.mock_calls == [ - call.OpenKey(mock_winreg.HKEY_LOCAL_MACHINE, "Software\\Python\\PythonCore", 0, 0x11), - call.EnumKey(self.key_local_machine, 0), - call.QueryValue(self.key_local_machine, "2.4\\InstallPath"), - call.EnumKey(self.key_local_machine, 1), - call.QueryValue(self.key_local_machine, "2.7\\InstallPath"), - call.EnumKey(self.key_local_machine, 2), - call.QueryValue(self.key_local_machine, "3.2\\InstallPath"), - call.EnumKey(self.key_local_machine, 3), - call.QueryValue(self.key_local_machine, "3.4\\InstallPath"), - call.EnumKey(self.key_local_machine, 4), - call.QueryValue(self.key_local_machine, "3.6-32\\InstallPath"), - call.EnumKey(self.key_local_machine, 5), - call.QueryValue(self.key_local_machine, "3.7-32\\InstallPath"), - call.EnumKey(self.key_local_machine, 6), - call.CloseKey(self.key_local_machine), - call.OpenKey(mock_winreg.HKEY_CURRENT_USER, "Software\\Python\\PythonCore", 0, 0x11), - call.EnumKey(self.key_current_user, 0), - call.QueryValue(self.key_current_user, "2.5\\InstallPath"), - call.EnumKey(self.key_current_user, 1), - call.QueryValue(self.key_current_user, "2.7\\InstallPath"), - call.EnumKey(self.key_current_user, 2), - call.QueryValue(self.key_current_user, "3.8-32\\InstallPath"), - call.EnumKey(self.key_current_user, 3), - call.CloseKey(self.key_current_user), - call.OpenKey(mock_winreg.HKEY_LOCAL_MACHINE, "Software\\Python\\PythonCore", 0, 0x12), - call.EnumKey(self.key_local_machine_64, 0), - call.QueryValue(self.key_local_machine_64, "2.6\\InstallPath"), - call.EnumKey(self.key_local_machine_64, 1), - call.QueryValue(self.key_local_machine_64, "3.5\\InstallPath"), - call.EnumKey(self.key_local_machine_64, 2), - call.QueryValue(self.key_local_machine_64, "3.7\\InstallPath"), - call.EnumKey(self.key_local_machine_64, 3), - call.QueryValue(self.key_local_machine_64, "3.8\\InstallPath"), - call.EnumKey(self.key_local_machine_64, 4), - call.CloseKey(self.key_local_machine_64), - call.OpenKey(mock_winreg.HKEY_CURRENT_USER, "Software\\Python\\PythonCore", 0, 0x12), - call.EnumKey(self.key_current_user_64, 0), - call.QueryValue(self.key_current_user_64, "3.7\\InstallPath"), - call.EnumKey(self.key_current_user_64, 1), - call.CloseKey(self.key_current_user_64), - ] - - @pytest.mark.skipif(sys.platform != "win32", reason="windows specific test") - def test_on_windows_with_no_installations(self, monkeypatch): - assert virtualenv.IS_WIN - mock_winreg = self.mock_virtualenv_winreg(monkeypatch, {}) - - installed_pythons = virtualenv.get_installed_pythons() - - assert installed_pythons == {} - assert mock_winreg.mock_calls == [ - call.OpenKey(mock_winreg.HKEY_LOCAL_MACHINE, "Software\\Python\\PythonCore", 0, 0x11), - call.EnumKey(self.key_local_machine, 0), - call.CloseKey(self.key_local_machine), - call.OpenKey(mock_winreg.HKEY_CURRENT_USER, "Software\\Python\\PythonCore", 0, 0x11), - call.EnumKey(self.key_current_user, 0), - call.CloseKey(self.key_current_user), - call.OpenKey(mock_winreg.HKEY_LOCAL_MACHINE, "Software\\Python\\PythonCore", 0, 0x12), - call.EnumKey(self.key_local_machine_64, 0), - call.CloseKey(self.key_local_machine_64), - call.OpenKey(mock_winreg.HKEY_CURRENT_USER, "Software\\Python\\PythonCore", 0, 0x12), - call.EnumKey(self.key_current_user_64, 0), - call.CloseKey(self.key_current_user_64), - ] - - -@patch("distutils.spawn.find_executable") -@patch("virtualenv.is_executable", return_value=True) -@patch("virtualenv.get_installed_pythons") -@patch("os.path.exists", return_value=True) -@patch("os.path.abspath") -def test_resolve_interpreter_with_installed_python( - mock_abspath, mock_exists, mock_get_installed_pythons, mock_is_executable, mock_find_executable -): - test_tag = "foo" - test_path = "/path/to/foo/python.exe" - test_abs_path = "some-abs-path" - test_found_path = "some-found-path" - mock_get_installed_pythons.return_value = {test_tag: test_path, test_tag + "2": test_path + "2"} - mock_abspath.return_value = test_abs_path - mock_find_executable.return_value = test_found_path - - exe = virtualenv.resolve_interpreter("foo") - - assert exe == test_found_path, "installed python should be accessible by key" - - mock_get_installed_pythons.assert_called_once_with() - mock_abspath.assert_called_once_with(test_path) - mock_find_executable.assert_called_once_with(test_path) - mock_exists.assert_called_once_with(test_found_path) - mock_is_executable.assert_called_once_with(test_found_path) - - -@patch("virtualenv.is_executable", return_value=True) -@patch("virtualenv.get_installed_pythons", return_value={"foo": "bar"}) -@patch("os.path.exists", return_value=True) -def test_resolve_interpreter_with_absolute_path(mock_exists, mock_get_installed_pythons, mock_is_executable): - """Should return absolute path if given and exists""" - test_abs_path = os.path.abspath("/usr/bin/python53") - - exe = virtualenv.resolve_interpreter(test_abs_path) - - assert exe == test_abs_path, "Absolute path should return as is" - - mock_exists.assert_called_with(test_abs_path) - mock_is_executable.assert_called_with(test_abs_path) - - -@patch("virtualenv.get_installed_pythons", return_value={"foo": "bar"}) -@patch("os.path.exists", return_value=False) -def test_resolve_interpreter_with_nonexistent_interpreter(mock_exists, mock_get_installed_pythons): - """Should SystemExit with an nonexistent python interpreter path""" - with pytest.raises(SystemExit): - virtualenv.resolve_interpreter("/usr/bin/python53") - - mock_exists.assert_called_with("/usr/bin/python53") - - -@patch("virtualenv.is_executable", return_value=False) -@patch("os.path.exists", return_value=True) -def test_resolve_interpreter_with_invalid_interpreter(mock_exists, mock_is_executable): - """Should exit when with absolute path if not exists""" - invalid = os.path.abspath("/usr/bin/pyt_hon53") - - with pytest.raises(SystemExit): - virtualenv.resolve_interpreter(invalid) - - mock_exists.assert_called_with(invalid) - mock_is_executable.assert_called_with(invalid) - - -def test_activate_after_future_statements(): - """Should insert activation line after last future statement""" - script = [ - "#!/usr/bin/env python", - "from __future__ import with_statement", - "from __future__ import print_function", - 'print("Hello, world!")', - ] - out = virtualenv.relative_script(script) - assert out == [ - "#!/usr/bin/env python", - "from __future__ import with_statement", - "from __future__ import print_function", - "", - "import os; " - "activate_this=os.path.join(os.path.dirname(os.path.realpath(__file__)), 'activate_this.py'); " - "exec(compile(open(activate_this).read(), activate_this, 'exec'), { '__file__': activate_this}); " - "del os, activate_this", - "", - 'print("Hello, world!")', - ], out - - -def test_cop_update_defaults_with_store_false(): - """store_false options need reverted logic""" - - class MyConfigOptionParser(virtualenv.ConfigOptionParser): - def __init__(self, *args, **kwargs): - self.config = virtualenv.ConfigParser.RawConfigParser() - self.files = [] - optparse.OptionParser.__init__(self, *args, **kwargs) - - def get_environ_vars(self, prefix="VIRTUALENV_"): - yield ("no_site_packages", "1") - - cop = MyConfigOptionParser() - cop.add_option( - "--no-site-packages", - dest="system_site_packages", - action="store_false", - help="Don't give access to the global site-packages dir to the " "virtual environment (default)", - ) - - defaults = {} - cop.update_defaults(defaults) - assert defaults == {"system_site_packages": 0} - - -def test_install_python_bin(): - """Should create the right python executables and links""" - tmp_virtualenv = tempfile.mkdtemp() - try: - home_dir, lib_dir, inc_dir, bin_dir = virtualenv.path_locations(tmp_virtualenv) - virtualenv.install_python(home_dir, lib_dir, inc_dir, bin_dir, False, False) - - if virtualenv.IS_WIN: - required_executables = ["python.exe", "pythonw.exe"] - else: - py_exe_no_version = "python" - py_exe_version_major = "python%s" % sys.version_info[0] - py_exe_version_major_minor = "python{}.{}".format(sys.version_info[0], sys.version_info[1]) - required_executables = [py_exe_no_version, py_exe_version_major, py_exe_version_major_minor] - - for pth in required_executables: - assert os.path.exists(os.path.join(bin_dir, pth)), "%s should exist in bin_dir" % pth - root_inc_dir = os.path.join(home_dir, "include") - assert not os.path.islink(root_inc_dir) - finally: - shutil.rmtree(tmp_virtualenv) - - -@pytest.mark.skipif("platform.python_implementation() == 'PyPy'") -def test_always_copy_option(): - """Should be no symlinks in directory tree""" - tmp_virtualenv = tempfile.mkdtemp() - ve_path = os.path.join(tmp_virtualenv, "venv") - try: - virtualenv.create_environment(ve_path, symlink=False) - - for root, dirs, files in os.walk(tmp_virtualenv): - for f in files + dirs: - full_name = os.path.join(root, f) - assert not os.path.islink(full_name), "%s should not be a" " symlink (to %s)" % ( - full_name, - os.readlink(full_name), - ) - finally: - shutil.rmtree(tmp_virtualenv) - - -@pytest.mark.skipif(not hasattr(os, "symlink"), reason="requires working symlink implementation") -def test_relative_symlink(tmpdir): - """ Test if a virtualenv works correctly if it was created via a symlink and this symlink is removed """ - - tmpdir = str(tmpdir) - ve_path = os.path.join(tmpdir, "venv") - os.mkdir(ve_path) - - workdir = os.path.join(tmpdir, "work") - os.mkdir(workdir) - - ve_path_linked = os.path.join(workdir, "venv") - os.symlink(ve_path, ve_path_linked) - - lib64 = os.path.join(ve_path, "lib64") - - virtualenv.create_environment(ve_path_linked, symlink=True) - if not os.path.lexists(lib64): - # no lib 64 on this platform - return - - assert os.path.exists(lib64) - - shutil.rmtree(workdir) - - assert os.path.exists(lib64) - - -@pytest.mark.skipif(not hasattr(os, "symlink"), reason="requires working symlink implementation") -def test_copyfile_from_symlink(tmp_path): - """Test that copyfile works correctly when the source is a symlink with a - relative target, and a symlink to a symlink. (This can occur when creating - an environment if Python was installed using stow or homebrew.)""" - - # Set up src/link2 -> ../src/link1 -> file. - # We will copy to a different directory, so misinterpreting either symlink - # will be detected. - src_dir = tmp_path / "src" - src_dir.mkdir() - with open(str(src_dir / "file"), "w") as f: - f.write("contents") - os.symlink("file", str(src_dir / "link1")) - os.symlink(str(Path("..") / "src" / "link1"), str(src_dir / "link2")) - - # Check that copyfile works on link2. - # This may produce a symlink or a regular file depending on the platform -- - # which doesn't matter as long as it has the right contents. - copy_path = tmp_path / "copy" - virtualenv.copyfile(str(src_dir / "link2"), str(copy_path)) - with open(str(copy_path), "r") as f: - assert f.read() == "contents" - - shutil.rmtree(str(src_dir)) - os.remove(str(copy_path)) - - -def test_missing_certifi_pem(tmp_path): - """Make sure that we can still create virtual environment if pip is - patched to not use certifi's cacert.pem and the file is removed. - This can happen if pip is packaged by Linux distributions.""" - proj_dir = Path(__file__).parent.parent - support_original = proj_dir / "virtualenv_support" - pip_wheel = sorted(support_original.glob("pip*whl"))[0] - whl_name = pip_wheel.name - - wheeldir = tmp_path / "wheels" - wheeldir.mkdir() - tmpcert = tmp_path / "tmpcert.pem" - cacert = "pip/_vendor/certifi/cacert.pem" - certifi = "pip/_vendor/certifi/core.py" - oldpath = b"os.path.join(f, 'cacert.pem')" - newpath = "r'{}'".format(tmpcert).encode() - removed = False - replaced = False - - with zipfile.ZipFile(str(pip_wheel), "r") as whlin: - with zipfile.ZipFile(str(wheeldir / whl_name), "w") as whlout: - for item in whlin.infolist(): - buff = whlin.read(item.filename) - if item.filename == cacert: - tmpcert.write_bytes(buff) - removed = True - continue - if item.filename == certifi: - nbuff = buff.replace(oldpath, newpath) - assert nbuff != buff - buff = nbuff - replaced = True - whlout.writestr(item, buff) - - assert removed and replaced - - venvdir = tmp_path / "venv" - search_dirs = [str(wheeldir), str(support_original)] - virtualenv.create_environment(str(venvdir), search_dirs=search_dirs) - - -def test_create_environment_from_dir_with_spaces(tmpdir): - """Should work with wheel sources read from a dir with spaces.""" - ve_path = str(tmpdir / "venv") - spaced_support_dir = str(tmpdir / "support with spaces") - from virtualenv_support import __file__ as support_dir - - support_dir = os.path.dirname(os.path.abspath(support_dir)) - shutil.copytree(support_dir, spaced_support_dir) - virtualenv.create_environment(ve_path, search_dirs=[spaced_support_dir]) - - -def test_create_environment_in_dir_with_spaces(tmpdir): - """Should work with environment path containing spaces.""" - ve_path = str(tmpdir / "venv with spaces") - virtualenv.create_environment(ve_path) - - -def test_create_environment_with_local_https_pypi(tmpdir): - """Create virtual environment using local PyPI listening https with - certificate signed with custom certificate authority - """ - test_dir = Path(__file__).parent - ssl_dir = test_dir / "ssl" - proj_dir = test_dir.parent - support_dir = proj_dir / "virtualenv_support" - local_pypi_app = pypiserver.app(root=str(support_dir)) - local_pypi = pytest_localserver.http.WSGIServer( - host="localhost", - port=0, - application=local_pypi_app, - ssl_context=(str(ssl_dir / "server.crt"), str(ssl_dir / "server.key")), - ) - local_pypi.start() - local_pypi_url = "https://localhost:{}/".format(local_pypi.server_address[1]) - venvdir = tmpdir / "venv" - pip_log = tmpdir / "pip.log" - env_addition = { - "PIP_CERT": str(ssl_dir / "rootCA.pem"), - "PIP_INDEX_URL": local_pypi_url, - "PIP_LOG": str(pip_log), - "PIP_RETRIES": "0", - } - if six.PY2: - env_addition = {key.encode("utf-8"): value.encode("utf-8") for key, value in env_addition.items()} - env_backup = {} - for key, value in env_addition.items(): - if key in os.environ: - env_backup[key] = os.environ[key] - os.environ[key] = value - try: - virtualenv.create_environment(str(venvdir), download=True) - with pip_log.open("rb") as f: - assert b"SSLError" not in f.read() - finally: - local_pypi.stop() - for key in env_addition.keys(): - os.environ.pop(key) - if key in env_backup: - os.environ[key] = env_backup[key] - - -def check_pypy_pre_import(): - import sys - - # These modules(module_name, optional) are taken from PyPy's site.py: - # https://bitbucket.org/pypy/pypy/src/d0187cf2f1b70ec4b60f10673ff081bdd91e9a17/lib-python/2.7/site.py#lines-532:539 - modules = [ - ("encodings", False), - ("exceptions", True), # "exceptions" module does not exist in Python3 - ("zipimport", True), - ] - - for module, optional in modules: - if not optional or module in sys.builtin_module_names: - assert module in sys.modules, "missing {!r} in sys.modules".format(module) - - -@pytest.mark.skipif("platform.python_implementation() != 'PyPy'") -def test_pypy_pre_import(tmp_path): - """For PyPy, some built-in modules should be pre-imported because - some programs expect them to be in sys.modules on startup. - """ - check_code = inspect.getsource(check_pypy_pre_import) - check_code = textwrap.dedent(check_code[check_code.index("\n") + 1 :]) - if six.PY2: - check_code = check_code.decode() - - check_prog = tmp_path / "check-pre-import.py" - check_prog.write_text(check_code) - - ve_path = str(tmp_path / "venv") - virtualenv.create_environment(ve_path) - - bin_dir = virtualenv.path_locations(ve_path)[-1] - - try: - cmd = [ - os.path.join(bin_dir, "{}{}".format(virtualenv.EXPECTED_EXE, ".exe" if virtualenv.IS_WIN else "")), - str(check_prog), - ] - subprocess.check_output(cmd, universal_newlines=True, stderr=subprocess.STDOUT) - except subprocess.CalledProcessError as exception: - assert not exception.returncode, exception.output - - -@pytest.mark.skipif(not hasattr(os, "symlink"), reason="requires working symlink implementation") -def test_create_environment_with_exec_prefix_pointing_to_prefix(tmpdir): - """Create virtual environment for Python with ``sys.exec_prefix`` pointing - to ``sys.prefix`` or ``sys.base_prefix`` or ``sys.real_prefix`` under a - different name - """ - venvdir = str(tmpdir / "venv") - python_dir = tmpdir / "python" - python_dir.mkdir() - path_key = str("PATH") - old_path = os.environ[path_key] - if hasattr(sys, "real_prefix"): - os.environ[path_key] = os.pathsep.join( - p for p in os.environ[path_key].split(os.pathsep) if not p.startswith(sys.prefix) - ) - python = virtualenv.resolve_interpreter(os.path.basename(sys.executable)) - try: - subprocess.check_call([sys.executable, "-m", "virtualenv", "-p", python, venvdir]) - home_dir, lib_dir, inc_dir, bin_dir = virtualenv.path_locations(venvdir) - assert not os.path.islink(os.path.join(lib_dir, "distutils")) - finally: - os.environ[path_key] = old_path - - -@pytest.mark.skipif(not hasattr(sys, "real_prefix"), reason="requires running from inside virtualenv") -def test_create_environment_from_virtual_environment(tmpdir): - """Create virtual environment using Python from another virtual environment - """ - venvdir = str(tmpdir / "venv") - home_dir, lib_dir, inc_dir, bin_dir = virtualenv.path_locations(venvdir) - virtualenv.create_environment(venvdir) - assert not os.path.islink(os.path.join(lib_dir, "distutils")) - - -@pytest.mark.skipif(std_venv is None, reason="needs standard venv module") -def test_create_environment_from_venv(tmpdir): - std_venv_dir = str(tmpdir / "stdvenv") - ve_venv_dir = str(tmpdir / "vevenv") - home_dir, lib_dir, inc_dir, bin_dir = virtualenv.path_locations(ve_venv_dir) - builder = std_venv.EnvBuilder() - ctx = builder.ensure_directories(std_venv_dir) - builder.create_configuration(ctx) - builder.setup_python(ctx) - builder.setup_scripts(ctx) - subprocess.check_call([ctx.env_exe, virtualenv.__file__, "--no-setuptools", "--no-pip", "--no-wheel", ve_venv_dir]) - ve_exe = os.path.join(bin_dir, "python") - out = subprocess.check_output([ve_exe, "-c", "import sys; print(sys.real_prefix)"], universal_newlines=True) - # Test against real_prefix if present - we might be running the test from a virtualenv (e.g. tox). - assert out.strip() == getattr(sys, "real_prefix", sys.prefix) - - -@pytest.mark.skipif(std_venv is None, reason="needs standard venv module") -def test_create_environment_from_venv_no_pip(tmpdir): - std_venv_dir = str(tmpdir / "stdvenv") - ve_venv_dir = str(tmpdir / "vevenv") - home_dir, lib_dir, inc_dir, bin_dir = virtualenv.path_locations(ve_venv_dir) - builder = std_venv.EnvBuilder() - ctx = builder.ensure_directories(std_venv_dir) - builder.create_configuration(ctx) - builder.setup_python(ctx) - builder.setup_scripts(ctx) - subprocess.check_call([ctx.env_exe, virtualenv.__file__, "--no-pip", ve_venv_dir]) - ve_exe = os.path.join(bin_dir, "python") - out = subprocess.check_output([ve_exe, "-c", "import sys; print(sys.real_prefix)"], universal_newlines=True) - # Test against real_prefix if present - we might be running the test from a virtualenv (e.g. tox). - assert out.strip() == getattr(sys, "real_prefix", sys.prefix) - - -def test_create_environment_with_old_pip(tmpdir): - old = Path(__file__).parent / "old-wheels" - old_pip = old / "pip-9.0.1-py2.py3-none-any.whl" - old_setuptools = old / "setuptools-30.4.0-py2.py3-none-any.whl" - support_dir = str(tmpdir / "virtualenv_support") - os.makedirs(support_dir) - for old_dep in [old_pip, old_setuptools]: - shutil.copy(str(old_dep), support_dir) - venvdir = str(tmpdir / "venv") - virtualenv.create_environment(venvdir, search_dirs=[support_dir], no_wheel=True) - - -def test_license_builtin(clean_python): - _, bin_dir, _ = clean_python - proc = subprocess.Popen( - (os.path.join(bin_dir, "python"), "-c", "license()"), stdin=subprocess.PIPE, stdout=subprocess.PIPE - ) - out_b, _ = proc.communicate(b"q\n") - out = out_b.decode() - assert not proc.returncode - assert "Ian Bicking and Contributors" not in out diff --git a/tests/test_zipapp.py b/tests/test_zipapp.py deleted file mode 100644 index 8ef8bd12b..000000000 --- a/tests/test_zipapp.py +++ /dev/null @@ -1,100 +0,0 @@ -from __future__ import unicode_literals - -import json -import os.path -import subprocess -import sys - -import pytest -import six - -import virtualenv - -HERE = os.path.dirname(os.path.dirname(__file__)) - - -def _python(v): - return virtualenv.get_installed_pythons().get(v, "python{}".format(v)) - - -@pytest.fixture(scope="session") -def call_zipapp(tmp_path_factory, call_subprocess): - if sys.version_info < (3, 5): - pytest.skip("zipapp was introduced in python3.5") - pyz = str(tmp_path_factory.mktemp(basename="zipapp") / "virtualenv.pyz") - call_subprocess( - (sys.executable, os.path.join(HERE, "tasks/make_zipapp.py"), "--root", virtualenv.HERE, "--dest", pyz) - ) - - def zipapp_make_env(path, python=None): - cmd = (sys.executable, pyz, "--no-download", path) - if python: - cmd += ("-p", python) - call_subprocess(cmd) - - return zipapp_make_env - - -@pytest.fixture(scope="session") -def call_wheel(tmp_path_factory, call_subprocess): - wheels = tmp_path_factory.mktemp(basename="wheel") - call_subprocess((sys.executable, "-m", "pip", "wheel", "--no-deps", "-w", str(wheels), HERE)) - (wheel,) = wheels.iterdir() - - def wheel_make_env(path, python=None): - cmd = (sys.executable, "-m", "virtualenv", "--no-download", path) - if python: - cmd += ("-p", python) - env = dict(os.environ, PYTHONPATH=str(wheel)) - call_subprocess(cmd, env=env) - - return wheel_make_env - - -def test_zipapp_basic_invocation(call_zipapp, tmp_path): - _test_basic_invocation(call_zipapp, tmp_path) - - -def test_wheel_basic_invocation(call_wheel, tmp_path): - _test_basic_invocation(call_wheel, tmp_path) - - -def _test_basic_invocation(make_env, tmp_path): - venv = tmp_path / "venv" - make_env(str(venv)) - assert_venv_looks_good( - venv, list(sys.version_info), "{}{}".format(virtualenv.EXPECTED_EXE, ".exe" if virtualenv.IS_WIN else "") - ) - - -def version_exe(venv, exe_name): - _, _, _, bin_dir = virtualenv.path_locations(str(venv)) - exe = os.path.join(bin_dir, exe_name) - script = "import sys; import json; print(json.dumps(dict(v=list(sys.version_info), e=sys.executable)))" - cmd = [exe, "-c", script] - out = json.loads(subprocess.check_output(cmd, universal_newlines=True)) - return out["v"], out["e"] - - -def assert_venv_looks_good(venv, version_info, exe_name): - assert venv.exists() - version, exe = version_exe(venv, exe_name=exe_name) - assert version[: len(version_info)] == version_info - assert exe != sys.executable - - -def _test_invocation_dash_p(make_env, tmp_path): - venv = tmp_path / "venv" - python = {2: _python("3"), 3: _python("2.7")}[sys.version_info[0]] - make_env(str(venv), python) - expected = {3: 2, 2: 3}[sys.version_info[0]] - assert_venv_looks_good(venv, [expected], "python{}".format(".exe" if virtualenv.IS_WIN else "")) - - -def test_zipapp_invocation_dash_p(call_zipapp, tmp_path): - _test_invocation_dash_p(call_zipapp, tmp_path) - - -@pytest.mark.skipif(sys.platform == "win32" and six.PY2, reason="no python 3 for windows on CI") -def test_wheel_invocation_dash_p(call_wheel, tmp_path): - _test_invocation_dash_p(call_wheel, tmp_path) diff --git a/tests/unit/activation/conftest.py b/tests/unit/activation/conftest.py new file mode 100644 index 000000000..c689c76bd --- /dev/null +++ b/tests/unit/activation/conftest.py @@ -0,0 +1,210 @@ +from __future__ import absolute_import, unicode_literals + +import os +import pipes +import re +import shutil +import subprocess +import sys +from os.path import dirname, normcase, realpath + +import pytest +import six + +from virtualenv.run import run_via_cli +from virtualenv.util.path import Path +from virtualenv.util.subprocess import Popen + + +class ActivationTester(object): + def __init__(self, of_class, session, cmd, activate_script, extension): + self.of_class = of_class + self._creator = session.creator + self._version_cmd = [cmd, "--version"] + self._invoke_script = [cmd] + self.activate_script = activate_script + self.extension = extension + self.activate_cmd = "source" + self.deactivate = "deactivate" + self.pydoc_call = "pydoc -w pydoc_test" + self.script_encoding = "utf-8" + + def get_version(self, raise_on_fail): + # locally we disable, so that contributors don't need to have everything setup + try: + process = Popen(self._version_cmd, universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = process.communicate() + if out: + return out + return err + except Exception as exception: + if raise_on_fail: + raise + return RuntimeError("{} is not available due {}".format(self, exception)) + + def __call__(self, monkeypatch, tmp_path): + activate_script = self._creator.bin_dir / self.activate_script + test_script = self._generate_test_script(activate_script, tmp_path) + monkeypatch.chdir(six.ensure_text(str(tmp_path))) + + monkeypatch.delenv(str("VIRTUAL_ENV"), raising=False) + invoke, env = self._invoke_script + [six.ensure_text(str(test_script))], self.env(tmp_path) + + try: + process = Popen(invoke, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env) + _raw, _ = process.communicate() + raw = "\n{}".format(_raw.decode("utf-8")).replace("\r\n", "\n") + except subprocess.CalledProcessError as exception: + assert not exception.returncode, six.ensure_text(exception.output) + return + + out = re.sub(r"pydev debugger: process \d+ is connecting\n\n", "", raw, re.M).strip().split("\n") + self.assert_output(out, raw, tmp_path) + return env, activate_script + + def non_source_activate(self, activate_script): + return self._invoke_script + [str(activate_script)] + + # noinspection PyMethodMayBeStatic + def env(self, tmp_path): + env = os.environ.copy() + # add the current python executable folder to the path so we already have another python on the path + # also keep the path so the shells (fish, bash, etc can be discovered) + env[str("PYTHONIOENCODING")] = str("utf-8") + env[str("PATH")] = os.pathsep.join([dirname(sys.executable)] + env.get(str("PATH"), str("")).split(os.pathsep)) + # clear up some environment variables so they don't affect the tests + for key in [k for k in env.keys() if k.startswith("_OLD") or k.startswith("VIRTUALENV_")]: + del env[key] + return env + + def _generate_test_script(self, activate_script, tmp_path): + commands = self._get_test_lines(activate_script) + script = os.linesep.join(commands) + test_script = tmp_path / "script.{}".format(self.extension) + with open(six.ensure_text(str(test_script)), "wb") as file_handler: + file_handler.write(script.encode(self.script_encoding)) + return test_script + + def _get_test_lines(self, activate_script): + commands = [ + self.print_python_exe(), + self.print_os_env_var("VIRTUAL_ENV"), + self.activate_call(activate_script), + self.print_python_exe(), + self.print_os_env_var("VIRTUAL_ENV"), + # \\ loads documentation from the virtualenv site packages + self.pydoc_call, + self.deactivate, + self.print_python_exe(), + self.print_os_env_var("VIRTUAL_ENV"), + "", # just finish with an empty new line + ] + return commands + + def assert_output(self, out, raw, tmp_path): + # pre-activation + assert out[0], raw + assert out[1] == "None", raw + # post-activation + assert self.norm_path(out[2]) == self.norm_path(self._creator.exe), raw + assert self.norm_path(out[3]) == self.norm_path(self._creator.dest_dir).replace("\\\\", "\\"), raw + assert out[4] == "wrote pydoc_test.html" + content = tmp_path / "pydoc_test.html" + assert content.exists(), raw + # post deactivation, same as before + assert out[-2] == out[0], raw + assert out[-1] == "None", raw + + def quote(self, s): + return pipes.quote(s) + + def python_cmd(self, cmd): + return "{} -c {}".format(os.path.basename(sys.executable), self.quote(cmd)) + + def print_python_exe(self): + return self.python_cmd( + "import sys; e = sys.executable;" + "print(e.decode(sys.getfilesystemencoding()) if sys.version_info[0] == 2 else e)" + ) + + def print_os_env_var(self, var): + val = '"{}"'.format(var) + return self.python_cmd( + "import os; import sys; v = os.environ.get({}, None);" + "print(v if v is None else " + "(v.decode(sys.getfilesystemencoding()) if sys.version_info[0] == 2 else v))".format(val) + ) + + def activate_call(self, script): + cmd = self.quote(six.ensure_text(str(self.activate_cmd))) + scr = self.quote(six.ensure_text(str(script))) + return "{} {}".format(cmd, scr).strip() + + @staticmethod + def norm_path(path): + # python may return Windows short paths, normalize + path = realpath(six.ensure_text(str(path)) if isinstance(path, Path) else path) + if sys.platform == "win32": + from ctypes import create_unicode_buffer, windll + + buffer_cont = create_unicode_buffer(256) + get_long_path_name = windll.kernel32.GetLongPathNameW + get_long_path_name(six.text_type(path), buffer_cont, 256) # noqa: F821 + result = buffer_cont.value + else: + result = path + return normcase(result) + + +class RaiseOnNonSourceCall(ActivationTester): + def __init__(self, of_class, session, cmd, activate_script, extension, non_source_fail_message): + super(RaiseOnNonSourceCall, self).__init__(of_class, session, cmd, activate_script, extension) + self.non_source_fail_message = non_source_fail_message + + def __call__(self, monkeypatch, tmp_path): + env, activate_script = super(RaiseOnNonSourceCall, self).__call__(monkeypatch, tmp_path) + process = Popen( + self.non_source_activate(activate_script), stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, + ) + out, err = process.communicate() + assert process.returncode + assert self.non_source_fail_message in err.decode("utf-8") + + +@pytest.fixture(scope="session") +def activation_tester_class(): + return ActivationTester + + +@pytest.fixture(scope="session") +def raise_on_non_source_class(): + return RaiseOnNonSourceCall + + +@pytest.fixture(scope="session") +def activation_python(tmp_path_factory, special_char_name): + dest = os.path.join( + six.ensure_text(str(tmp_path_factory.mktemp("activation-tester-env"))), + six.ensure_text("env-{}-v".format(special_char_name)), + ) + session = run_via_cli(["--seed", "none", dest, "--prompt", special_char_name]) + pydoc_test = session.creator.site_packages[0] / "pydoc_test.py" + with open(six.ensure_text(str(pydoc_test)), "wb") as file_handler: + file_handler.write(b'"""This is pydoc_test.py"""') + yield session + if six.PY2 and sys.platform == "win32": # PY2 windows does not support unicode delete + shutil.rmtree(dest) + + +@pytest.fixture() +def activation_tester(activation_python, monkeypatch, tmp_path, is_inside_ci): + def _tester(tester_class): + tester = tester_class(activation_python) + if not tester.of_class.supports(activation_python.creator.interpreter): + pytest.skip("{} not supported on current environment".format(tester.of_class.__name__)) + version = tester.get_version(raise_on_fail=is_inside_ci) + if not isinstance(version, six.string_types): + pytest.skip(msg=six.text_type(version)) + return tester(monkeypatch, tmp_path) + + return _tester diff --git a/tests/unit/activation/test_activation_support.py b/tests/unit/activation/test_activation_support.py new file mode 100644 index 000000000..cff940e7c --- /dev/null +++ b/tests/unit/activation/test_activation_support.py @@ -0,0 +1,49 @@ +from argparse import Namespace + +import pytest + +from virtualenv.activation import ( + BashActivator, + BatchActivator, + CShellActivator, + FishActivator, + PowerShellActivator, + PythonActivator, +) +from virtualenv.interpreters.discovery.py_info import PythonInfo + + +@pytest.mark.parametrize("activator_class", [BatchActivator, PowerShellActivator, PythonActivator]) +def test_activator_support_windows(mocker, activator_class): + activator = activator_class(Namespace(prompt=None)) + + interpreter = mocker.Mock(spec=PythonInfo) + interpreter.os = "nt" + assert activator.supports(interpreter) + + +@pytest.mark.parametrize("activator_class", [BashActivator, CShellActivator, FishActivator]) +def test_activator_no_support_windows(mocker, activator_class): + activator = activator_class(Namespace(prompt=None)) + + interpreter = mocker.Mock(spec=PythonInfo) + interpreter.os = "nt" + assert not activator.supports(interpreter) + + +@pytest.mark.parametrize( + "activator_class", [BashActivator, CShellActivator, FishActivator, PowerShellActivator, PythonActivator] +) +def test_activator_support_posix(mocker, activator_class): + activator = activator_class(Namespace(prompt=None)) + interpreter = mocker.Mock(spec=PythonInfo) + interpreter.os = "posix" + assert activator.supports(interpreter) + + +@pytest.mark.parametrize("activator_class", [BatchActivator]) +def test_activator_no_support_posix(mocker, activator_class): + activator = activator_class(Namespace(prompt=None)) + interpreter = mocker.Mock(spec=PythonInfo) + interpreter.os = "posix" + assert not activator.supports(interpreter) diff --git a/tests/unit/activation/test_bash.py b/tests/unit/activation/test_bash.py new file mode 100644 index 000000000..d5d8ad9b0 --- /dev/null +++ b/tests/unit/activation/test_bash.py @@ -0,0 +1,13 @@ +from __future__ import absolute_import, unicode_literals + +from virtualenv.activation import BashActivator + + +def test_bash(raise_on_non_source_class, activation_tester): + class Bash(raise_on_non_source_class): + def __init__(self, session): + super(Bash, self).__init__( + BashActivator, session, "bash", "activate.sh", "sh", "You must source this script: $ source " + ) + + activation_tester(Bash) diff --git a/tests/unit/activation/test_batch.py b/tests/unit/activation/test_batch.py new file mode 100644 index 000000000..92b141b03 --- /dev/null +++ b/tests/unit/activation/test_batch.py @@ -0,0 +1,29 @@ +from __future__ import absolute_import, unicode_literals + +import pipes + +from virtualenv.activation import BatchActivator + + +def test_batch(activation_tester_class, activation_tester, tmp_path, activation_python): + version_script = tmp_path / "version.bat" + version_script.write_text("ver") + + class Batch(activation_tester_class): + def __init__(self, session): + super(Batch, self).__init__(BatchActivator, session, None, "activate.bat", "bat") + self._version_cmd = [str(version_script)] + self._invoke_script = [] + self.deactivate = "call deactivate" + self.activate_cmd = "call" + self.pydoc_call = "call {}".format(self.pydoc_call) + + def _get_test_lines(self, activate_script): + # for BATCH utf-8 support need change the character code page to 650001 + return ["@echo off", "", "chcp 65001 1>NUL"] + super(Batch, self)._get_test_lines(activate_script) + + def quote(self, s): + """double quotes needs to be single, and single need to be double""" + return "".join(("'" if c == '"' else ('"' if c == "'" else c)) for c in pipes.quote(s)) + + activation_tester(Batch) diff --git a/tests/unit/activation/test_csh.py b/tests/unit/activation/test_csh.py new file mode 100644 index 000000000..69b23b68f --- /dev/null +++ b/tests/unit/activation/test_csh.py @@ -0,0 +1,11 @@ +from __future__ import absolute_import, unicode_literals + +from virtualenv.activation import CShellActivator + + +def test_csh(activation_tester_class, activation_tester): + class Csh(activation_tester_class): + def __init__(self, session): + super(Csh, self).__init__(CShellActivator, session, "csh", "activate.csh", "csh") + + activation_tester(Csh) diff --git a/tests/unit/activation/test_fish.py b/tests/unit/activation/test_fish.py new file mode 100644 index 000000000..4b8533895 --- /dev/null +++ b/tests/unit/activation/test_fish.py @@ -0,0 +1,11 @@ +from __future__ import absolute_import, unicode_literals + +from virtualenv.activation import FishActivator + + +def test_fish(activation_tester_class, activation_tester): + class Fish(activation_tester_class): + def __init__(self, session): + super(Fish, self).__init__(FishActivator, session, "fish", "activate.fish", "fish") + + activation_tester(Fish) diff --git a/tests/unit/activation/test_powershell.py b/tests/unit/activation/test_powershell.py new file mode 100644 index 000000000..88567eb42 --- /dev/null +++ b/tests/unit/activation/test_powershell.py @@ -0,0 +1,30 @@ +from __future__ import absolute_import, unicode_literals + +import pipes +import sys + +from virtualenv.activation import PowerShellActivator + + +def test_powershell(activation_tester_class, activation_tester): + class PowerShell(activation_tester_class): + def __init__(self, session): + cmd = "powershell.exe" if sys.platform == "win32" else "pwsh" + super(PowerShell, self).__init__(PowerShellActivator, session, cmd, "activate.ps1", "ps1") + self._version_cmd = [cmd, "-c", "$PSVersionTable"] + self._invoke_script = [cmd, "-ExecutionPolicy", "ByPass", "-File"] + self.activate_cmd = "." + self.script_encoding = "utf-16" + + def quote(self, s): + """powershell double double quote needed for quotes within single quotes""" + return pipes.quote(s).replace('"', '""') + + def _get_test_lines(self, activate_script): + # for BATCH utf-8 support need change the character code page to 650001 + return super(PowerShell, self)._get_test_lines(activate_script) + + def invoke_script(self): + return [self.cmd, "-File"] + + activation_tester(PowerShell) diff --git a/tests/unit/activation/test_python_activator.py b/tests/unit/activation/test_python_activator.py new file mode 100644 index 000000000..2a125c767 --- /dev/null +++ b/tests/unit/activation/test_python_activator.py @@ -0,0 +1,91 @@ +from __future__ import absolute_import, unicode_literals + +import inspect +import os +import sys + +import six + +from virtualenv.activation import PythonActivator + + +def test_python(raise_on_non_source_class, activation_tester): + class Python(raise_on_non_source_class): + def __init__(self, session): + super(Python, self).__init__( + PythonActivator, + session, + sys.executable, + activate_script="activate_this.py", + extension="py", + non_source_fail_message="You must use exec(open(this_file).read(), {'__file__': this_file}))", + ) + + def env(self, tmp_path): + env = os.environ.copy() + env[str("PYTHONIOENCODING")] = str("utf-8") + for key in {"VIRTUAL_ENV", "PYTHONPATH"}: + env.pop(str(key), None) + env[str("PATH")] = os.pathsep.join([str(tmp_path), str(tmp_path / "other")]) + return env + + def _get_test_lines(self, activate_script): + raw = inspect.getsource(self.activate_this_test) + return [ + i[12:] + for i in raw.replace('"__FILENAME__"', repr(six.ensure_text(str(activate_script)))).splitlines()[2:] + ] + + # noinspection PyUnresolvedReferences + @staticmethod + def activate_this_test(): + import os + import sys + + def print_path(value): + if value is not None and sys.version_info[0] == 2: + value = value.decode(sys.getfilesystemencoding()) + print(value) + + print_path(os.environ.get("VIRTUAL_ENV")) + print_path(os.environ.get("PATH")) + print_path(os.pathsep.join(sys.path)) + file_at = "__FILENAME__" + with open(file_at, "rb") as file_handler: + content = file_handler.read() + exec(content, {"__file__": file_at}) + print_path(os.environ.get("VIRTUAL_ENV")) + print_path(os.environ.get("PATH")) + print_path(os.pathsep.join(sys.path)) + import inspect + import pydoc_test + + print_path(inspect.getsourcefile(pydoc_test)) + + def assert_output(self, out, raw, tmp_path): + assert out[0] == "None" # start with VIRTUAL_ENV None + + prev_path = out[1].split(os.path.pathsep) + prev_sys_path = out[2].split(os.path.pathsep) + + assert out[3] == six.ensure_text( + str(self._creator.dest_dir) + ) # VIRTUAL_ENV now points to the virtual env folder + + new_path = out[4].split(os.pathsep) # PATH now starts with bin path of current + assert ([six.ensure_text(str(self._creator.bin_dir))] + prev_path) == new_path + + # sys path contains the site package at its start + new_sys_path = out[5].split(os.path.pathsep) + assert ([six.ensure_text(str(i)) for i in self._creator.site_packages] + prev_sys_path) == new_sys_path + + # manage to import from activate site package + assert self.norm_path(out[6]) == self.norm_path(self._creator.site_packages[0] / "pydoc_test.py") + + def non_source_activate(self, activate_script): + return self._invoke_script + [ + "-c", + 'exec(open(r"{}").read())'.format(six.ensure_text(str(activate_script))), + ] + + activation_tester(Python) diff --git a/tests/unit/activation/test_xonosh.py b/tests/unit/activation/test_xonosh.py new file mode 100644 index 000000000..0091dd3a9 --- /dev/null +++ b/tests/unit/activation/test_xonosh.py @@ -0,0 +1,25 @@ +from __future__ import absolute_import, unicode_literals + +import sys + +from virtualenv.activation import XonoshActivator + + +def test_xonosh(activation_tester_class, activation_tester): + class Xonosh(activation_tester_class): + def __init__(self, session): + super(Xonosh, self).__init__( + XonoshActivator, session, "xonsh.exe" if sys.platform == "win32" else "xonsh", "activate.xsh", "xsh" + ) + self._invoke_script = [sys.executable, "-m", "xonsh"] + self._version_cmd = [sys.executable, "-m", "xonsh", "--version"] + + def env(self, tmp_path): + env = super(Xonosh, self).env(tmp_path) + env.update({"XONSH_DEBUG": "1", "XONSH_SHOW_TRACEBACK": "True"}) + return env + + def activate_call(self, script): + return "{} {}".format(self.activate_cmd, repr(str(script))).strip() + + activation_tester(Xonosh) diff --git a/tests/unit/config/cli/test_parser.py b/tests/unit/config/cli/test_parser.py new file mode 100644 index 000000000..babefd25c --- /dev/null +++ b/tests/unit/config/cli/test_parser.py @@ -0,0 +1,38 @@ +from __future__ import absolute_import, unicode_literals + +import os +from contextlib import contextmanager + +import pytest + +from virtualenv.config.cli.parser import VirtualEnvConfigParser +from virtualenv.config.ini import IniConfig + + +@pytest.fixture() +def gen_parser_no_conf_env(monkeypatch, tmp_path): + keys_to_delete = {key for key in os.environ if key.startswith("VIRTUALENV_")} + for key in keys_to_delete: + monkeypatch.delenv(key) + monkeypatch.setenv(IniConfig.VIRTUALENV_CONFIG_FILE_ENV_VAR, str(tmp_path / "missing")) + + @contextmanager + def _build(): + parser = VirtualEnvConfigParser() + + def _run(*args): + return parser.parse_args(args=args) + + yield parser, _run + parser.enable_help() + + return _build + + +def test_flag(gen_parser_no_conf_env): + with gen_parser_no_conf_env() as (parser, run): + parser.add_argument("--clear", dest="clear", action="store_true", help="it", default=False) + result = run() + assert result.clear is False + result = run("--clear") + assert result.clear is True diff --git a/tests/unit/config/test___main__.py b/tests/unit/config/test___main__.py new file mode 100644 index 000000000..91fef2774 --- /dev/null +++ b/tests/unit/config/test___main__.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import, unicode_literals + +import sys + +from virtualenv.util.subprocess import Popen, subprocess + + +def test_main(): + process = Popen([sys.executable, "-m", "virtualenv", "--help"], universal_newlines=True, stdout=subprocess.PIPE) + out, _ = process.communicate() + assert not process.returncode + assert out diff --git a/tests/unit/config/test_env_var.py b/tests/unit/config/test_env_var.py new file mode 100644 index 000000000..2caae9114 --- /dev/null +++ b/tests/unit/config/test_env_var.py @@ -0,0 +1,39 @@ +from __future__ import absolute_import, unicode_literals + +import pytest + +from virtualenv.config.ini import IniConfig +from virtualenv.run import session_via_cli + + +def parse_cli(args): + return session_via_cli(args) + + +@pytest.fixture() +def empty_conf(tmp_path, monkeypatch): + conf = tmp_path / "conf.ini" + monkeypatch.setenv(IniConfig.VIRTUALENV_CONFIG_FILE_ENV_VAR, str(conf)) + conf.write_text("[virtualenv]") + + +def test_value_ok(monkeypatch, empty_conf): + monkeypatch.setenv(str("VIRTUALENV_VERBOSE"), str("5")) + result = parse_cli([]) + assert result.verbosity == 5 + + +def _exc(of): + try: + int(of) + except ValueError as exception: + return exception + + +def test_value_bad(monkeypatch, caplog, empty_conf): + monkeypatch.setenv(str("VIRTUALENV_VERBOSE"), str("a")) + result = parse_cli([]) + assert result.verbosity == 2 + msg = "env var VIRTUALENV_VERBOSE failed to convert 'a' as {!r} because {!r}".format(int, _exc("a")) + # one for the core parse, one for the normal one + assert caplog.messages == [msg], "{}{}".format(caplog.text, msg) diff --git a/tests/unit/interpreters/boostrap/perf.py b/tests/unit/interpreters/boostrap/perf.py new file mode 100644 index 000000000..f30099197 --- /dev/null +++ b/tests/unit/interpreters/boostrap/perf.py @@ -0,0 +1,18 @@ +from __future__ import absolute_import, unicode_literals + +from virtualenv.interpreters.discovery import CURRENT +from virtualenv.run import run_via_cli +from virtualenv.seed.wheels import BUNDLE_SUPPORT + +dest = r"C:\Users\traveler\git\virtualenv\test\unit\interpreters\boostrap\perf" +bundle_ver = BUNDLE_SUPPORT[CURRENT.version_release_str] +cmd = [ + dest, + "--download", + "--pip", + bundle_ver["pip"].split("-")[1], + "--setuptools", + bundle_ver["setuptools"].split("-")[1], +] +result = run_via_cli(cmd) +assert result diff --git a/tests/unit/interpreters/boostrap/test_boostrap_link_via_app_data.py b/tests/unit/interpreters/boostrap/test_boostrap_link_via_app_data.py new file mode 100644 index 000000000..e8495b78c --- /dev/null +++ b/tests/unit/interpreters/boostrap/test_boostrap_link_via_app_data.py @@ -0,0 +1,92 @@ +from __future__ import absolute_import, unicode_literals + +import os +import sys + +import six + +from virtualenv.interpreters.discovery.py_info import CURRENT +from virtualenv.run import run_via_cli +from virtualenv.seed.embed.wheels import BUNDLE_SUPPORT +from virtualenv.seed.embed.wheels.acquire import BUNDLE_FOLDER +from virtualenv.util.subprocess import Popen + + +def test_base_bootstrap_link_via_app_data(tmp_path, coverage_env): + bundle_ver = BUNDLE_SUPPORT[CURRENT.version_release_str] + create_cmd = [ + six.ensure_text(str(tmp_path / "env")), + "--seeder", + "app-data", + "--extra-search-dir", + six.ensure_text(str(BUNDLE_FOLDER)), + "--download", + "--pip", + bundle_ver["pip"].split("-")[1], + "--setuptools", + bundle_ver["setuptools"].split("-")[1], + "--clear-app-data", + ] + result = run_via_cli(create_cmd) + coverage_env() + assert result + + # uninstalling pip/setuptools now should leave us with a clean env + site_package = result.creator.site_packages[0] + pip = site_package / "pip" + setuptools = site_package / "setuptools" + + files_post_first_create = list(site_package.iterdir()) + assert pip in files_post_first_create + assert setuptools in files_post_first_create + + env_exe = result.creator.exe + for pip_exe in [ + env_exe.with_name("pip{}{}".format(suffix, env_exe.suffix)) + for suffix in ( + "", + "{}".format(CURRENT.version_info.major), + "-{}.{}".format(CURRENT.version_info.major, CURRENT.version_info.minor), + ) + ]: + assert pip_exe.exists() + process = Popen([six.ensure_text(str(pip_exe)), "--version", "--disable-pip-version-check"]) + _, __ = process.communicate() + assert not process.returncode + + remove_cmd = [ + str(env_exe), + "-m", + "pip", + "--verbose", + "--disable-pip-version-check", + "uninstall", + "-y", + "setuptools", + ] + process = Popen(remove_cmd) + _, __ = process.communicate() + assert not process.returncode + assert site_package.exists() + + files_post_first_uninstall = list(site_package.iterdir()) + assert pip in files_post_first_uninstall + assert setuptools not in files_post_first_uninstall + + # check we can run it again and will work - checks both overwrite and reuse cache + result = run_via_cli(create_cmd) + coverage_env() + assert result + files_post_second_create = list(site_package.iterdir()) + assert files_post_first_create == files_post_second_create + + process = Popen(remove_cmd + ["pip"]) + _, __ = process.communicate() + assert not process.returncode + # pip is greedy here, removing all packages removes the site-package too + if site_package.exists(): + post_run = list(site_package.iterdir()) + assert not post_run, "\n".join(str(i) for i in post_run) + + if sys.version_info[0:2] == (3, 4) and "PIP_REQ_TRACKER" in os.environ: + os.environ.pop("PIP_REQ_TRACKER") diff --git a/tests/unit/interpreters/boostrap/test_pip_invoke.py b/tests/unit/interpreters/boostrap/test_pip_invoke.py new file mode 100644 index 000000000..9a69d40ef --- /dev/null +++ b/tests/unit/interpreters/boostrap/test_pip_invoke.py @@ -0,0 +1,31 @@ +from __future__ import absolute_import, unicode_literals + +from virtualenv.interpreters.discovery.py_info import CURRENT +from virtualenv.run import run_via_cli +from virtualenv.seed.embed.wheels import BUNDLE_SUPPORT + + +def test_base_bootstrap_via_pip_invoke(tmp_path, coverage_env): + bundle_ver = BUNDLE_SUPPORT[CURRENT.version_release_str] + create_cmd = [ + "--seeder", + "pip", + str(tmp_path / "env"), + "--download", + "--pip", + bundle_ver["pip"].split("-")[1], + "--setuptools", + bundle_ver["setuptools"].split("-")[1], + ] + result = run_via_cli(create_cmd) + coverage_env() + assert result + + # uninstalling pip/setuptools now should leave us with a clean env + site_package = result.creator.site_packages[0] + pip = site_package / "pip" + setuptools = site_package / "setuptools" + + files_post_first_create = list(site_package.iterdir()) + assert pip in files_post_first_create + assert setuptools in files_post_first_create diff --git a/tests/unit/interpreters/create/conftest.py b/tests/unit/interpreters/create/conftest.py new file mode 100644 index 000000000..d673a8a78 --- /dev/null +++ b/tests/unit/interpreters/create/conftest.py @@ -0,0 +1,70 @@ +""" +It's possible to use multiple types of host pythons to create virtual environments and all should work: + +- host installation +- invoking from a venv (if Python 3.3+) +- invoking from an old style virtualenv (<17.0.0) +- invoking from our own venv +""" +from __future__ import absolute_import, unicode_literals + +import sys + +import pytest + +from virtualenv.info import IS_WIN +from virtualenv.interpreters.discovery.py_info import CURRENT +from virtualenv.util.subprocess import Popen + + +# noinspection PyUnusedLocal +def get_root(tmp_path_factory): + return CURRENT.system_executable + + +def get_venv(tmp_path_factory): + if CURRENT.is_venv: + return sys.executable + elif CURRENT.version_info.major == 3: + root_python = get_root(tmp_path_factory) + dest = tmp_path_factory.mktemp("venv") + process = Popen([str(root_python), "-m", "venv", "--without-pip", str(dest)]) + process.communicate() + # sadly creating a virtual environment does not tell us where the executable lives in general case + # so discover using some heuristic + return CURRENT.find_exe_based_of(inside_folder=str(dest)) + + +def get_virtualenv(tmp_path_factory): + if CURRENT.is_old_virtualenv: + return CURRENT.executable + elif CURRENT.version_info.major == 3: + # noinspection PyCompatibility + from venv import EnvBuilder + + virtualenv_at = str(tmp_path_factory.mktemp("venv-for-virtualenv")) + builder = EnvBuilder(symlinks=not IS_WIN) + builder.create(virtualenv_at) + venv_for_virtualenv = CURRENT.find_exe_based_of(inside_folder=virtualenv_at) + cmd = venv_for_virtualenv, "-m", "pip", "install", "virtualenv==16.6.1" + process = Popen(cmd) + _, __ = process.communicate() + assert not process.returncode + + virtualenv_python = tmp_path_factory.mktemp("virtualenv") + cmd = venv_for_virtualenv, "-m", "virtualenv", virtualenv_python + process = Popen(cmd) + _, __ = process.communicate() + assert not process.returncode + return CURRENT.find_exe_based_of(inside_folder=virtualenv_python) + + +PYTHON = {"root": get_root, "venv": get_venv, "virtualenv": get_virtualenv} + + +@pytest.fixture(params=list(PYTHON.values()), ids=list(PYTHON.keys()), scope="session") +def python(request, tmp_path_factory): + result = request.param(tmp_path_factory) + if result is None: + pytest.skip("could not resolve {}".format(request.param)) + return result diff --git a/tests/unit/interpreters/create/test_creator.py b/tests/unit/interpreters/create/test_creator.py new file mode 100644 index 000000000..b26ec1536 --- /dev/null +++ b/tests/unit/interpreters/create/test_creator.py @@ -0,0 +1,243 @@ +from __future__ import absolute_import, unicode_literals + +import difflib +import os +import stat +import sys + +import pytest +import six + +from virtualenv.__main__ import run +from virtualenv.interpreters.create.creator import DEBUG_SCRIPT, get_env_debug_info +from virtualenv.interpreters.discovery.builtin import get_interpreter +from virtualenv.interpreters.discovery.py_info import CURRENT, PythonInfo +from virtualenv.pyenv_cfg import PyEnvCfg +from virtualenv.run import run_via_cli, session_via_cli +from virtualenv.util.path import Path + + +def test_os_path_sep_not_allowed(tmp_path, capsys): + target = str(tmp_path / "a{}b".format(os.pathsep)) + err = _non_success_exit_code(capsys, target) + msg = ( + "destination {!r} must not contain the path separator ({}) as this" + " would break the activation scripts".format(target, os.pathsep) + ) + assert msg in err, err + + +def _non_success_exit_code(capsys, target): + with pytest.raises(SystemExit) as context: + run_via_cli(args=[target]) + assert context.value.code != 0 + out, err = capsys.readouterr() + assert not out, out + return err + + +def test_destination_exists_file(tmp_path, capsys): + target = tmp_path / "out" + target.write_text("") + err = _non_success_exit_code(capsys, str(target)) + msg = "the destination {} already exists and is a file".format(str(target)) + assert msg in err, err + + +@pytest.mark.skipif(sys.platform == "win32", reason="no chmod on Windows") +def test_destination_not_write_able(tmp_path, capsys): + target = tmp_path + prev_mod = target.stat().st_mode + target.chmod(0o444) + try: + err = _non_success_exit_code(capsys, str(target)) + msg = "the destination . is not write-able at {}".format(str(target)) + assert msg in err, err + finally: + target.chmod(prev_mod) + + +SYSTEM = get_env_debug_info(Path(CURRENT.system_executable), DEBUG_SCRIPT) + + +def cleanup_sys_path(paths): + from virtualenv.interpreters.create.creator import HERE + + paths = [Path(i).absolute() for i in paths] + to_remove = [Path(HERE)] + if str("PYCHARM_HELPERS_DIR") in os.environ: + to_remove.append(Path(os.environ[str("PYCHARM_HELPERS_DIR")]).parent) + to_remove.append(Path(os.path.expanduser("~")) / ".PyCharm") + result = [i for i in paths if not any(str(i).startswith(str(t)) for t in to_remove)] + return result + + +@pytest.mark.parametrize("global_access", [False, True], ids=["no_global", "ok_global"]) +@pytest.mark.parametrize( + "use_venv", [False, True] if six.PY3 else [False], ids=["no_venv", "venv"] if six.PY3 else ["no_venv"] +) +def test_create_no_seed(python, use_venv, global_access, tmp_path, coverage_env, special_name_dir): + dest = special_name_dir + cmd = [ + "-v", + "-v", + "-p", + six.ensure_text(python), + six.ensure_text(str(dest)), + "--without-pip", + "--activators", + "", + ] + if global_access: + cmd.append("--system-site-packages") + if use_venv: + cmd.extend(["--creator", "venv"]) + result = run_via_cli(cmd) + coverage_env() + for site_package in result.creator.site_packages: + content = list(site_package.iterdir()) + assert not content, "\n".join(str(i) for i in content) + assert result.creator.env_name == six.ensure_text(dest.name) + debug = result.creator.debug + sys_path = cleanup_sys_path(debug["sys"]["path"]) + system_sys_path = cleanup_sys_path(SYSTEM["sys"]["path"]) + our_paths = set(sys_path) - set(system_sys_path) + our_paths_repr = "\n".join(six.ensure_text(repr(i)) for i in our_paths) + + # ensure we have at least one extra path added + assert len(our_paths) >= 1, our_paths_repr + # ensure all additional paths are related to the virtual environment + for path in our_paths: + assert str(path).startswith(str(dest)), "{} does not start with {}".format( + six.ensure_text(str(path)), six.ensure_text(str(dest)) + ) + # ensure there's at least a site-packages folder as part of the virtual environment added + assert any(p for p in our_paths if p.parts[-1] == "site-packages"), our_paths_repr + + # ensure the global site package is added or not, depending on flag + last_from_system_path = next(i for i in reversed(system_sys_path) if str(i).startswith(SYSTEM["sys"]["prefix"])) + if global_access: + common = [] + for left, right in zip(reversed(system_sys_path), reversed(sys_path)): + if left == right: + common.append(left) + else: + break + + def list_to_str(iterable): + return [six.ensure_text(str(i)) for i in iterable] + + assert common, "\n".join(difflib.unified_diff(list_to_str(sys_path), list_to_str(system_sys_path))) + else: + assert last_from_system_path not in sys_path + + +@pytest.mark.skipif(not CURRENT.has_venv, reason="requires venv interpreter") +def test_venv_fails_not_inline(tmp_path, capsys, mocker): + def _session_via_cli(args): + session = session_via_cli(args) + assert session.creator.can_be_inline is False + return session + + mocker.patch("virtualenv.run.session_via_cli", side_effect=_session_via_cli) + before = tmp_path.stat().st_mode + cfg_path = tmp_path / "pyvenv.cfg" + cfg_path.write_text(six.ensure_text("")) + cfg = str(cfg_path) + try: + os.chmod(cfg, stat.S_IREAD | stat.S_IRGRP | stat.S_IROTH) + cmd = ["-p", str(CURRENT.executable), str(tmp_path), "--without-pip", "--creator", "venv"] + with pytest.raises(SystemExit) as context: + run(cmd) + assert context.value.code != 0 + finally: + os.chmod(cfg, before) + out, err = capsys.readouterr() + assert "subprocess call failed for" in out, out + assert "Error:" in err, err + + +@pytest.mark.skipif(not sys.version_info[0] == 2, reason="python 2 only tests") +def test_debug_bad_virtualenv(tmp_path): + cmd = [str(tmp_path), "--without-pip"] + result = run_via_cli(cmd) + # if the site.py is removed/altered the debug should fail as no one is around to fix the paths + site_py = result.creator.lib_dir / "site.py" + site_py.unlink() + # insert something that writes something on the stdout + site_py.write_text('import sys; sys.stdout.write(repr("std-out")); sys.stderr.write("std-err"); raise ValueError') + debug_info = result.creator.debug + assert debug_info["returncode"] + assert debug_info["err"].startswith("std-err") + assert debug_info["out"] == "'std-out'" + assert debug_info["exception"] + + +@pytest.mark.parametrize( + "use_venv", [False, True] if six.PY3 else [False], ids=["no_venv", "venv"] if six.PY3 else ["no_venv"] +) +@pytest.mark.parametrize("clear", [True, False], ids=["clear", "no_clear"]) +def test_create_clear_resets(tmp_path, use_venv, clear): + marker = tmp_path / "magic" + cmd = [str(tmp_path), "--seeder", "none"] + if use_venv: + cmd.extend(["--creator", "venv"]) + run_via_cli(cmd) + + marker.write_text("") # if we a marker file this should be gone on a clear run, remain otherwise + assert marker.exists() + + run_via_cli(cmd + (["--clear"] if clear else [])) + assert marker.exists() is not clear + + +@pytest.mark.parametrize( + "use_venv", [False, True] if six.PY3 else [False], ids=["no_venv", "venv"] if six.PY3 else ["no_venv"] +) +@pytest.mark.parametrize("prompt", [None, "magic"]) +def test_prompt_set(tmp_path, use_venv, prompt): + cmd = [str(tmp_path), "--seeder", "none"] + if prompt is not None: + cmd.extend(["--prompt", "magic"]) + if not use_venv and six.PY3: + cmd.extend(["--creator", "venv"]) + + result = run_via_cli(cmd) + actual_prompt = tmp_path.name if prompt is None else prompt + cfg = PyEnvCfg.from_file(result.creator.pyenv_cfg.path) + if prompt is None: + assert "prompt" not in cfg + else: + if use_venv is False: + assert "prompt" in cfg, list(cfg.content.keys()) + assert cfg["prompt"] == actual_prompt + + +@pytest.fixture(scope="session") +def cross_python(is_inside_ci): + spec = "{}{}".format(CURRENT.implementation, 2 if CURRENT.version_info.major == 3 else 3) + interpreter = get_interpreter(spec) + if interpreter is None: + msg = "could not find {}".format(spec) + if is_inside_ci: + raise RuntimeError(msg) + pytest.skip(msg=msg) + yield interpreter + + +def test_cross_major(cross_python, coverage_env, tmp_path): + cmd = [ + "-v", + "-v", + "-p", + six.ensure_text(cross_python.executable), + six.ensure_text(str(tmp_path)), + "--seeder", + "none", + "--activators", + "", + ] + result = run_via_cli(cmd) + coverage_env() + env = PythonInfo.from_exe(str(result.creator.exe)) + assert env.version_info.major != CURRENT.version_info.major diff --git a/tests/unit/interpreters/discovery/py_info/test_py_info.py b/tests/unit/interpreters/discovery/py_info/test_py_info.py new file mode 100644 index 000000000..06786a719 --- /dev/null +++ b/tests/unit/interpreters/discovery/py_info/test_py_info.py @@ -0,0 +1,111 @@ +from __future__ import absolute_import, unicode_literals + +import itertools +import json +import logging +import sys + +import pytest + +from virtualenv.interpreters.discovery.py_info import CURRENT, PythonInfo +from virtualenv.interpreters.discovery.py_spec import PythonSpec + + +def test_current_as_json(): + result = CURRENT.to_json() + parsed = json.loads(result) + a, b, c, d, e = sys.version_info + assert parsed["version_info"] == {"major": a, "minor": b, "micro": c, "releaselevel": d, "serial": e} + + +def test_bad_exe_py_info_raise(tmp_path): + exe = str(tmp_path) + with pytest.raises(RuntimeError) as context: + PythonInfo.from_exe(exe) + msg = str(context.value) + assert "code" in msg + assert exe in msg + + +def test_bad_exe_py_info_no_raise(tmp_path, caplog, capsys): + caplog.set_level(logging.NOTSET) + exe = str(tmp_path) + result = PythonInfo.from_exe(exe, raise_on_error=False) + assert result is None + out, _ = capsys.readouterr() + assert not out + assert len(caplog.messages) == 1 + msg = caplog.messages[0] + assert str(exe) in msg + assert "code" in msg + + +@pytest.mark.parametrize( + "spec", + itertools.chain( + [sys.executable], + list( + "{}{}{}".format(impl, ".".join(str(i) for i in ver), arch) + for impl, ver, arch in itertools.product( + ([CURRENT.implementation] + (["python"] if CURRENT.implementation == "CPython" else [])), + [sys.version_info[0 : i + 1] for i in range(3)], + ["", "-{}".format(CURRENT.architecture)], + ) + ), + ), +) +def test_satisfy_py_info(spec): + parsed_spec = PythonSpec.from_string_spec(spec) + matches = CURRENT.satisfies(parsed_spec, True) + assert matches is True + + +def test_satisfy_not_arch(): + parsed_spec = PythonSpec.from_string_spec( + "{}-{}".format(CURRENT.implementation, 64 if CURRENT.architecture == 32 else 32) + ) + matches = CURRENT.satisfies(parsed_spec, True) + assert matches is False + + +def _generate_not_match_current_interpreter_version(): + result = [] + for i in range(3): + ver = sys.version_info[0 : i + 1] + for a in range(len(ver)): + for o in [-1, 1]: + temp = list(ver) + temp[a] += o + result.append(".".join(str(i) for i in temp)) + return result + + +_NON_MATCH_VER = _generate_not_match_current_interpreter_version() + + +@pytest.mark.parametrize("spec", _NON_MATCH_VER) +def test_satisfy_not_version(spec): + parsed_spec = PythonSpec.from_string_spec("{}{}".format(CURRENT.implementation, spec)) + matches = CURRENT.satisfies(parsed_spec, True) + assert matches is False + + +def test_py_info_cached(mocker, tmp_path): + mocker.spy(PythonInfo, "_load_for_exe") + with pytest.raises(RuntimeError): + PythonInfo.from_exe(str(tmp_path)) + with pytest.raises(RuntimeError): + PythonInfo.from_exe(str(tmp_path)) + assert PythonInfo._load_for_exe.call_count == 1 + + +@pytest.mark.skipif(sys.platform == "win32", reason="symlink is not guaranteed to work on windows") +def test_py_info_cached_symlink(mocker, tmp_path): + mocker.spy(PythonInfo, "_load_for_exe") + with pytest.raises(RuntimeError): + PythonInfo.from_exe(str(tmp_path)) + symlinked = tmp_path / "a" + symlinked.symlink_to(tmp_path) + with pytest.raises(RuntimeError): + PythonInfo.from_exe(str(symlinked)) + assert PythonInfo._load_for_exe.call_count == 1 diff --git a/tests/unit/interpreters/discovery/py_info/test_py_info_exe_based_of.py b/tests/unit/interpreters/discovery/py_info/test_py_info_exe_based_of.py new file mode 100644 index 000000000..aebda2728 --- /dev/null +++ b/tests/unit/interpreters/discovery/py_info/test_py_info_exe_based_of.py @@ -0,0 +1,33 @@ +import logging +import os +import sys + +import pytest + +from virtualenv.interpreters.discovery.py_info import CURRENT, EXTENSIONS + + +def test_discover_empty_folder(tmp_path, monkeypatch): + with pytest.raises(RuntimeError): + CURRENT.find_exe_based_of(inside_folder=str(tmp_path)) + + +@pytest.mark.skipif(sys.platform == "win32", reason="symlink is not guaranteed to work on windows") +@pytest.mark.parametrize("suffix", EXTENSIONS) +@pytest.mark.parametrize("arch", [CURRENT.architecture, ""]) +@pytest.mark.parametrize("version", [".".join(str(i) for i in CURRENT.version_info[0:i]) for i in range(3, 0, -1)]) +@pytest.mark.parametrize("impl", [CURRENT.implementation, "python"]) +@pytest.mark.parametrize("into", [CURRENT.prefix[len(CURRENT.executable) :], ""]) +def test_discover_ok(tmp_path, monkeypatch, suffix, impl, version, arch, into, caplog): + caplog.set_level(logging.DEBUG) + folder = tmp_path / into + folder.mkdir(parents=True, exist_ok=True) + dest = folder / "{}{}".format(impl, version, arch, suffix) + os.symlink(CURRENT.executable, str(dest)) + inside_folder = str(tmp_path) + assert CURRENT.find_exe_based_of(inside_folder) == str(dest) + assert not caplog.text + + dest.rename(dest.parent / (dest.name + "-1")) + with pytest.raises(RuntimeError): + CURRENT.find_exe_based_of(inside_folder) diff --git a/tests/unit/interpreters/discovery/test_discovery.py b/tests/unit/interpreters/discovery/test_discovery.py new file mode 100644 index 000000000..f66a5b099 --- /dev/null +++ b/tests/unit/interpreters/discovery/test_discovery.py @@ -0,0 +1,36 @@ +from __future__ import absolute_import, unicode_literals + +import os +import sys +from uuid import uuid4 + +import pytest +import six + +from virtualenv.interpreters.discovery.builtin import get_interpreter +from virtualenv.interpreters.discovery.py_info import CURRENT + + +@pytest.mark.skipif(sys.platform == "win32", reason="symlink is not guaranteed to work on windows") +@pytest.mark.parametrize("case", ["mixed", "lower", "upper"]) +def test_discovery_via_path(monkeypatch, case, special_name_dir): + core = "somethingVeryCryptic{}".format(".".join(str(i) for i in CURRENT.version_info[0:3])) + name = "somethingVeryCryptic" + if case == "lower": + name = name.lower() + elif case == "upper": + name = name.upper() + exe_name = "{}{}{}".format(name, CURRENT.version_info.major, ".exe" if sys.platform == "win32" else "") + special_name_dir.mkdir() + executable = special_name_dir / exe_name + os.symlink(sys.executable, six.ensure_text(str(executable))) + new_path = os.pathsep.join([str(special_name_dir)] + os.environ.get(str("PATH"), str("")).split(os.pathsep)) + monkeypatch.setenv(str("PATH"), new_path) + interpreter = get_interpreter(core) + + assert interpreter is not None + + +def test_discovery_via_path_not_found(): + interpreter = get_interpreter(uuid4().hex) + assert interpreter is None diff --git a/tests/unit/interpreters/discovery/test_py_spec.py b/tests/unit/interpreters/discovery/test_py_spec.py new file mode 100644 index 000000000..1bb5f946b --- /dev/null +++ b/tests/unit/interpreters/discovery/test_py_spec.py @@ -0,0 +1,106 @@ +from __future__ import absolute_import, unicode_literals + +import itertools +import sys +from copy import copy + +import pytest + +from virtualenv.interpreters.discovery.py_spec import PythonSpec + + +def test_bad_py_spec(): + text = "python2.3.4.5" + spec = PythonSpec.from_string_spec(text) + assert text in repr(spec) + assert spec.str_spec == text + assert spec.path == text + content = vars(spec) + del content[str("str_spec")] + del content[str("path")] + assert all(v is None for v in content.values()) + + +def test_py_spec_first_digit_only_major(): + spec = PythonSpec.from_string_spec("278") + assert spec.major == 2 + assert spec.minor == 78 + + +def test_spec_satisfies_path_ok(): + spec = PythonSpec.from_string_spec(sys.executable) + assert spec.satisfies(spec) is True + + +def test_spec_satisfies_path_nok(tmp_path): + spec = PythonSpec.from_string_spec(sys.executable) + of = PythonSpec.from_string_spec(str(tmp_path)) + assert spec.satisfies(of) is False + + +def test_spec_satisfies_arch(): + spec_1 = PythonSpec.from_string_spec("python-32") + spec_2 = PythonSpec.from_string_spec("python-64") + + assert spec_1.satisfies(spec_1) is True + assert spec_2.satisfies(spec_1) is False + + +@pytest.mark.parametrize( + "req, spec", list(itertools.combinations(["py", "CPython", "python"], 2)) + [("jython", "jython")] +) +def test_spec_satisfies_implementation_ok(req, spec): + spec_1 = PythonSpec.from_string_spec(req) + spec_2 = PythonSpec.from_string_spec(spec) + assert spec_1.satisfies(spec_1) is True + assert spec_2.satisfies(spec_1) is True + + +def test_spec_satisfies_implementation_nok(): + spec_1 = PythonSpec.from_string_spec("python") + spec_2 = PythonSpec.from_string_spec("jython") + assert spec_2.satisfies(spec_1) is False + assert spec_1.satisfies(spec_2) is False + + +def _version_satisfies_pairs(): + target = set() + version = tuple(str(i) for i in sys.version_info[0:3]) + for i in range(len(version) + 1): + req = ".".join(version[0:i]) + for j in range(i + 1): + sat = ".".join(version[0:j]) + # can be satisfied in both directions + target.add((req, sat)) + target.add((sat, req)) + return sorted(target) + + +@pytest.mark.parametrize("req, spec", _version_satisfies_pairs()) +def test_version_satisfies_ok(req, spec): + req_spec = PythonSpec.from_string_spec("python{}".format(req)) + sat_spec = PythonSpec.from_string_spec("python{}".format(spec)) + assert sat_spec.satisfies(req_spec) is True + + +def _version_not_satisfies_pairs(): + target = set() + version = tuple(str(i) for i in sys.version_info[0:3]) + for i in range(len(version)): + req = ".".join(version[0 : i + 1]) + for j in range(i + 1): + sat_ver = list(sys.version_info[0 : j + 1]) + for l in range(j + 1): + for o in [1, -1]: + temp = copy(sat_ver) + temp[l] += o + sat = ".".join(str(i) for i in temp) + target.add((req, sat)) + return sorted(target) + + +@pytest.mark.parametrize("req, spec", _version_not_satisfies_pairs()) +def test_version_satisfies_nok(req, spec): + req_spec = PythonSpec.from_string_spec("python{}".format(req)) + sat_spec = PythonSpec.from_string_spec("python{}".format(spec)) + assert sat_spec.satisfies(req_spec) is False diff --git a/tests/unit/interpreters/discovery/windows/test_windows_pep514.py b/tests/unit/interpreters/discovery/windows/test_windows_pep514.py new file mode 100644 index 000000000..604d0a980 --- /dev/null +++ b/tests/unit/interpreters/discovery/windows/test_windows_pep514.py @@ -0,0 +1,196 @@ +from __future__ import absolute_import, unicode_literals + +import sys +import textwrap +from collections import defaultdict +from contextlib import contextmanager + +import pytest +import six + +from virtualenv.util.path import Path + + +@pytest.mark.skipif(sys.platform != "win32", reason="Windows registry only on Windows platform") +def test_pep517(_mock_registry): + from virtualenv.interpreters.discovery.windows.pep514 import discover_pythons + + interpreters = list(discover_pythons()) + assert interpreters == [ + ("ContinuumAnalytics", 3, 7, 32, "C:\\Users\\traveler\\Miniconda3\\python.exe", None), + ("ContinuumAnalytics", 3, 7, 64, "C:\\Users\\traveler\\Miniconda3-64\\python.exe", None), + ("python", 3, 6, 64, "C:\\Users\\traveler\\AppData\\Local\\Programs\\Python\\Python36\\python.exe", None), + ("python", 3, 6, 64, "C:\\Users\\traveler\\AppData\\Local\\Programs\\Python\\Python36\\python.exe", None), + ("python", 3, 5, 64, "C:\\Users\\traveler\\AppData\\Local\\Programs\\Python\\Python35\\python.exe", None), + ("python", 3, 6, 64, "C:\\Users\\traveler\\AppData\\Local\\Programs\\Python\\Python36\\python.exe", None), + ("python", 3, 7, 32, "C:\\Users\\traveler\\AppData\\Local\\Programs\\Python\\Python37-32\\python.exe", None), + ("python", 3, 9, 64, "C:\\Users\\traveler\\AppData\\Local\\Programs\\Python\\Python36\\python.exe", None), + ("python", 2, 7, 64, "C:\\Python27\\python.exe", None), + ("python", 3, 4, 64, "C:\\Python34\\python.exe", None), + ] + + +@pytest.mark.skipif(sys.platform != "win32", reason="Windows registry only on Windows platform") +def test_pep517_run(_mock_registry, capsys, caplog): + from virtualenv.interpreters.discovery.windows import pep514 + + pep514._run() + out, err = capsys.readouterr() + expected = textwrap.dedent( + r""" + ('ContinuumAnalytics', 3, 7, 32, 'C:\\Users\\traveler\\Miniconda3\\python.exe', None) + ('ContinuumAnalytics', 3, 7, 64, 'C:\\Users\\traveler\\Miniconda3-64\\python.exe', None) + ('python', 2, 7, 64, 'C:\\Python27\\python.exe', None) + ('python', 3, 4, 64, 'C:\\Python34\\python.exe', None) + ('python', 3, 5, 64, 'C:\\Users\\traveler\\AppData\\Local\\Programs\\Python\\Python35\\python.exe', None) + ('python', 3, 6, 64, 'C:\\Users\\traveler\\AppData\\Local\\Programs\\Python\\Python36\\python.exe', None) + ('python', 3, 6, 64, 'C:\\Users\\traveler\\AppData\\Local\\Programs\\Python\\Python36\\python.exe', None) + ('python', 3, 6, 64, 'C:\\Users\\traveler\\AppData\\Local\\Programs\\Python\\Python36\\python.exe', None) + ('python', 3, 7, 32, 'C:\\Users\\traveler\\AppData\\Local\\Programs\\Python\\Python37-32\\python.exe', None) + ('python', 3, 9, 64, 'C:\\Users\\traveler\\AppData\\Local\\Programs\\Python\\Python36\\python.exe', None) + """ + ).strip() + assert out.strip() == expected + assert not err + prefix = "PEP-514 violation in Windows Registry at " + expected_logs = [ + "{}HKEY_CURRENT_USER/PythonCore/3.1/SysArchitecture error: invalid format magic".format(prefix), + "{}HKEY_CURRENT_USER/PythonCore/3.2/SysArchitecture error: arch is not string: 100".format(prefix), + "{}HKEY_CURRENT_USER/PythonCore/3.3 error: no ExecutablePath or default for it".format(prefix), + "{}HKEY_CURRENT_USER/PythonCore/3.3 error: exe does not exists HKEY_CURRENT_USER/PythonCore/3.3".format(prefix), + "{}HKEY_CURRENT_USER/PythonCore/3.8/InstallPath error: missing".format(prefix), + "{}HKEY_CURRENT_USER/PythonCore/3.9/SysVersion error: invalid format magic".format(prefix), + "{}HKEY_CURRENT_USER/PythonCore/3.X/SysVersion error: version is not string: 2778".format(prefix), + "{}HKEY_CURRENT_USER/PythonCore/3.X error: invalid format 3.X".format(prefix), + ] + assert caplog.messages == expected_logs + + +@pytest.fixture() +def _mock_registry(mocker): + from virtualenv.interpreters.discovery.windows.pep514 import winreg + + loc, glob = {}, {} + mock_value_str = (Path(__file__).parent / "winreg-mock-values.py").read_text() + six.exec_(mock_value_str, glob, loc) + enum_collect = loc["enum_collect"] + value_collect = loc["value_collect"] + key_open = loc["key_open"] + hive_open = loc["hive_open"] + + def _e(key, at): + key_id = key.value if isinstance(key, Key) else key + result = enum_collect[key_id][at] + if isinstance(result, OSError): + raise result + return result + + mocker.patch.object(winreg, "EnumKey", side_effect=_e) + + def _v(key, value_name): + key_id = key.value if isinstance(key, Key) else key + result = value_collect[key_id][value_name] + if isinstance(result, OSError): + raise result + return result + + mocker.patch.object(winreg, "QueryValueEx", side_effect=_v) + + class Key(object): + def __init__(self, value): + self.value = value + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + return None + + @contextmanager + def _o(*args): + if len(args) == 2: + key, value = args + key_id = key.value if isinstance(key, Key) else key + result = Key(key_open[key_id][value]) # this needs to be something that can be with-ed, so let's wrap it + elif len(args) == 4: + result = hive_open[args] + else: + raise RuntimeError + value = result.value if isinstance(result, Key) else result + if isinstance(value, OSError): + raise value + yield result + + mocker.patch.object(winreg, "OpenKeyEx", side_effect=_o) + mocker.patch("os.path.exists", return_value=True) + + +@pytest.fixture() +def _collect_winreg_access(mocker): + if six.PY3: + # noinspection PyUnresolvedReferences + from winreg import EnumKey, OpenKeyEx, QueryValueEx + else: + # noinspection PyUnresolvedReferences + from _winreg import EnumKey, OpenKeyEx, QueryValueEx + from virtualenv.interpreters.discovery.windows.pep514 import winreg + + hive_open = {} + key_open = defaultdict(dict) + + @contextmanager + def _c(*args): + res = None + key_id = id(args[0]) if len(args) == 2 else None + try: + with OpenKeyEx(*args) as c: + res = id(c) + yield c + except Exception as exception: + res = exception + raise exception + finally: + if len(args) == 4: + hive_open[args] = res + elif len(args) == 2: + key_open[key_id][args[1]] = res + + enum_collect = defaultdict(list) + + def _e(key, at): + result = None + key_id = id(key) + try: + result = EnumKey(key, at) + return result + except Exception as exception: + result = exception + raise result + finally: + enum_collect[key_id].append(result) + + value_collect = defaultdict(dict) + + def _v(key, value_name): + result = None + key_id = id(key) + try: + result = QueryValueEx(key, value_name) + return result + except Exception as exception: + result = exception + raise result + finally: + value_collect[key_id][value_name] = result + + mocker.patch.object(winreg, "EnumKey", side_effect=_e) + mocker.patch.object(winreg, "QueryValueEx", side_effect=_v) + mocker.patch.object(winreg, "OpenKeyEx", side_effect=_c) + + yield + + print("") + print("hive_open = {}".format(hive_open)) + print("key_open = {}".format(dict(key_open.items()))) + print("value_collect = {}".format(dict(value_collect.items()))) + print("enum_collect = {}".format(dict(enum_collect.items()))) diff --git a/tests/unit/interpreters/discovery/windows/winreg-mock-values.py b/tests/unit/interpreters/discovery/windows/winreg-mock-values.py new file mode 100644 index 000000000..5426ccbf1 --- /dev/null +++ b/tests/unit/interpreters/discovery/windows/winreg-mock-values.py @@ -0,0 +1,136 @@ +import six + +if six.PY3: + import winreg +else: + # noinspection PyUnresolvedReferences + import _winreg as winreg + +hive_open = { + (winreg.HKEY_CURRENT_USER, "Software\\Python", 0, winreg.KEY_READ): 78701856, + (winreg.HKEY_LOCAL_MACHINE, "Software\\Python", 0, winreg.KEY_READ | winreg.KEY_WOW64_64KEY): 78701840, + (winreg.HKEY_LOCAL_MACHINE, "Software\\Python", 0, winreg.KEY_READ | winreg.KEY_WOW64_32KEY): OSError( + 2, "The system cannot find the file specified" + ), +} +key_open = { + 78701152: { + "Anaconda37-32\\InstallPath": 78703200, + "Anaconda37-32": 78703568, + "Anaconda37-64\\InstallPath": 78703520, + "Anaconda37-64": 78702368, + }, + 78701856: {"ContinuumAnalytics": 78701152, "PythonCore": 78702656}, + 78702656: { + "3.1\\InstallPath": 78701824, + "3.1": 78700704, + "3.2\\InstallPath": 78704048, + "3.2": 78704368, + "3.3\\InstallPath": 78701936, + "3.3": 78703024, + "3.5\\InstallPath": 78703792, + "3.5": 78701792, + "3.6\\InstallPath": 78701888, + "3.6": 78703424, + "3.7-32\\InstallPath": 78703600, + "3.7-32": 78704512, + "3.8\\InstallPath": OSError(2, "The system cannot find the file specified"), + "3.8": 78700656, + "3.9\\InstallPath": 78703632, + "3.9": 78702608, + "3.X": 78703088, + }, + 78702960: {"2.7\\InstallPath": 78700912, "2.7": 78703136, "3.4\\InstallPath": 78703648, "3.4": 78704032}, + 78701840: {"PythonCore": 78702960}, +} +value_collect = { + 78703568: {"SysVersion": ("3.7", 1), "SysArchitecture": ("32bit", 1)}, + 78703200: { + "ExecutablePath": ("C:\\Users\\traveler\\Miniconda3\\python.exe", 1), + "ExecutableArguments": OSError(2, "The system cannot find the file specified"), + }, + 78702368: {"SysVersion": ("3.7", 1), "SysArchitecture": ("64bit", 1)}, + 78703520: { + "ExecutablePath": ("C:\\Users\\traveler\\Miniconda3-64\\python.exe", 1), + "ExecutableArguments": OSError(2, "The system cannot find the file specified"), + }, + 78700704: {"SysVersion": ("3.6", 1), "SysArchitecture": ("magic", 1)}, + 78701824: { + "ExecutablePath": ("C:\\Users\\traveler\\AppData\\Local\\Programs\\Python\\Python36\\python.exe", 1), + "ExecutableArguments": OSError(2, "The system cannot find the file specified"), + }, + 78704368: {"SysVersion": ("3.6", 1), "SysArchitecture": (100, 4)}, + 78704048: { + "ExecutablePath": ("C:\\Users\\traveler\\AppData\\Local\\Programs\\Python\\Python36\\python.exe", 1), + "ExecutableArguments": OSError(2, "The system cannot find the file specified"), + }, + 78703024: {"SysVersion": ("3.6", 1), "SysArchitecture": ("64bit", 1)}, + 78701936: { + "ExecutablePath": OSError(2, "The system cannot find the file specified"), + None: OSError(2, "The system cannot find the file specified"), + }, + 78701792: { + "SysVersion": OSError(2, "The system cannot find the file specified"), + "SysArchitecture": OSError(2, "The system cannot find the file specified"), + }, + 78703792: { + "ExecutablePath": ("C:\\Users\\traveler\\AppData\\Local\\Programs\\Python\\Python35\\python.exe", 1), + "ExecutableArguments": OSError(2, "The system cannot find the file specified"), + }, + 78703424: {"SysVersion": ("3.6", 1), "SysArchitecture": ("64bit", 1)}, + 78701888: { + "ExecutablePath": ("C:\\Users\\traveler\\AppData\\Local\\Programs\\Python\\Python36\\python.exe", 1), + "ExecutableArguments": OSError(2, "The system cannot find the file specified"), + }, + 78704512: {"SysVersion": ("3.7", 1), "SysArchitecture": ("32bit", 1)}, + 78703600: { + "ExecutablePath": ("C:\\Users\\traveler\\AppData\\Local\\Programs\\Python\\Python37-32\\python.exe", 1), + "ExecutableArguments": OSError(2, "The system cannot find the file specified"), + }, + 78700656: { + "SysVersion": OSError(2, "The system cannot find the file specified"), + "SysArchitecture": OSError(2, "The system cannot find the file specified"), + }, + 78702608: {"SysVersion": ("magic", 1), "SysArchitecture": ("64bit", 1)}, + 78703632: { + "ExecutablePath": ("C:\\Users\\traveler\\AppData\\Local\\Programs\\Python\\Python36\\python.exe", 1), + "ExecutableArguments": OSError(2, "The system cannot find the file specified"), + }, + 78703088: {"SysVersion": (2778, 11)}, + 78703136: { + "SysVersion": OSError(2, "The system cannot find the file specified"), + "SysArchitecture": OSError(2, "The system cannot find the file specified"), + }, + 78700912: { + "ExecutablePath": OSError(2, "The system cannot find the file specified"), + None: ("C:\\Python27\\", 1), + "ExecutableArguments": OSError(2, "The system cannot find the file specified"), + }, + 78704032: { + "SysVersion": OSError(2, "The system cannot find the file specified"), + "SysArchitecture": OSError(2, "The system cannot find the file specified"), + }, + 78703648: { + "ExecutablePath": OSError(2, "The system cannot find the file specified"), + None: ("C:\\Python34\\", 1), + "ExecutableArguments": OSError(2, "The system cannot find the file specified"), + }, +} +enum_collect = { + 78701856: ["ContinuumAnalytics", "PythonCore", OSError(22, "No more data is available", None, 259, None)], + 78701152: ["Anaconda37-32", "Anaconda37-64", OSError(22, "No more data is available", None, 259, None)], + 78702656: [ + "3.1", + "3.2", + "3.3", + "3.5", + "3.6", + "3.7-32", + "3.8", + "3.9", + "3.X", + OSError(22, "No more data is available", None, 259, None), + ], + 78701840: ["PyLauncher", "PythonCore", OSError(22, "No more data is available", None, 259, None)], + 78702960: ["2.7", "3.4", OSError(22, "No more data is available", None, 259, None)], +} diff --git a/tests/unit/interpreters/test_interpreters.py b/tests/unit/interpreters/test_interpreters.py new file mode 100644 index 000000000..0b282878c --- /dev/null +++ b/tests/unit/interpreters/test_interpreters.py @@ -0,0 +1,25 @@ +from __future__ import absolute_import, unicode_literals + +import sys +from uuid import uuid4 + +import pytest + +from virtualenv.interpreters.discovery.py_info import CURRENT +from virtualenv.run import run_via_cli + + +def test_failed_to_find_bad_spec(): + of_id = uuid4().hex + with pytest.raises(RuntimeError) as context: + run_via_cli(["-p", of_id]) + msg = repr(RuntimeError("failed to find interpreter for Builtin discover of python_spec={!r}".format(of_id))) + assert repr(context.value) == msg + + +@pytest.mark.parametrize("of_id", [sys.executable, CURRENT.implementation]) +def test_failed_to_find_implementation(of_id, mocker): + mocker.patch("virtualenv.run._collect_creators", return_value={}) + with pytest.raises(RuntimeError) as context: + run_via_cli(["-p", of_id]) + assert repr(context.value) == repr(RuntimeError("No virtualenv implementation for {}".format(CURRENT))) diff --git a/tests/unit/test_run.py b/tests/unit/test_run.py new file mode 100644 index 000000000..95d50ebf0 --- /dev/null +++ b/tests/unit/test_run.py @@ -0,0 +1,33 @@ +from __future__ import absolute_import, unicode_literals + +import pytest +import six + +from virtualenv import __version__ +from virtualenv.run import run_via_cli + + +def test_help(capsys): + with pytest.raises(SystemExit) as context: + run_via_cli(args=["-h"]) + assert context.value.code == 0 + + out, err = capsys.readouterr() + assert not err + assert out + + +def test_version(capsys): + with pytest.raises(SystemExit) as context: + run_via_cli(args=["--version"]) + assert context.value.code == 0 + + out, err = capsys.readouterr() + extra = out if six.PY2 else err + content = out if six.PY3 else err + assert not extra + + assert __version__ in content + import virtualenv + + assert virtualenv.__file__ in content diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py new file mode 100644 index 000000000..7eb3dc8f4 --- /dev/null +++ b/tests/unit/test_util.py @@ -0,0 +1,48 @@ +from __future__ import absolute_import, unicode_literals + +import logging +import os + +import pytest + +from virtualenv.util.path import symlink_or_copy +from virtualenv.util.subprocess import run_cmd + + +@pytest.mark.skipif(not hasattr(os, "symlink"), reason="requires symlink support") +def test_fallback_to_copy_if_symlink_fails(caplog, capsys, tmp_path, mocker): + caplog.set_level(logging.DEBUG) + mocker.patch("os.symlink", side_effect=OSError()) + dst, src = _try_symlink(caplog, tmp_path, level=logging.WARNING) + msg = "symlink failed {!r}, for {} to {}, will try copy".format(OSError(), src, dst) + assert len(caplog.messages) == 1, caplog.text + message = caplog.messages[0] + assert msg == message + out, err = capsys.readouterr() + assert not out + assert err + + +def _try_symlink(caplog, tmp_path, level): + caplog.set_level(level) + src = tmp_path / "src" + src.write_text("a") + dst = tmp_path / "dst" + symlink_or_copy(do_copy=False, src=src, dst=dst) + assert dst.exists() + assert not dst.is_symlink() + assert dst.read_text() == "a" + return dst, src + + +@pytest.mark.skipif(hasattr(os, "symlink"), reason="requires no symlink") +def test_os_no_symlink_use_copy(caplog, tmp_path): + dst, src = _try_symlink(caplog, tmp_path, level=logging.DEBUG) + assert caplog.messages == ["copy {} to {}".format(src, dst)] + + +def test_run_fail(tmp_path): + code, out, err = run_cmd([str(tmp_path)]) + assert err + assert not out + assert code diff --git a/tox.ini b/tox.ini index c80f26fd9..16a869906 100644 --- a/tox.ini +++ b/tox.ini @@ -1,81 +1,93 @@ [tox] -minversion = 3.6.1 -envlist = fix_lint, embed, py{27,34,35,36,37}, pypy{,3}, cross_python{2,3}, docs, package_readme +minversion = 3.14.0 +envlist = + fix_lint, + py38, + py37, + py36, + py35, + py34, + py27, + coverage isolated_build = true skip_missing_interpreters = true [testenv] description = run tests with {basepython} -deps = pip >= 19.1.1 -setenv = COVERAGE_FILE = {env:COVERAGE_FILE:{toxworkdir}/.coverage.{envname}} +deps = + pip >= 19.1.1 +setenv = + COVERAGE_FILE = {toxworkdir}/.coverage.{envname} + COVERAGE_PROCESS_START = {toxinidir}/.coveragerc + _COVERAGE_SRC = {envsitepackagesdir}/virtualenv passenv = https_proxy http_proxy no_proxy HOME PYTEST_* PIP_* CI_RUN TERM extras = testing install_command = python -m pip install {opts} {packages} --disable-pip-version-check -commands = coverage run --source=virtualenv \ - -m pytest tests \ - {posargs:\ - --junitxml={env:JUNIT_XML_FILE:{toxworkdir}/junit.{envname}.xml} \ - } - coverage combine - coverage report --show-missing +commands = + python -c 'from os.path import sep; file = open(r"{envsitepackagesdir}\{\}coverage-virtualenv.pth".format(sep), "w"); file.write("import coverage; coverage.process_startup()")' + coverage erase + + coverage run\ + -m pytest \ + --junitxml {toxworkdir}/junit.{envname}.xml \ + tests {posargs} + + coverage combine + coverage report [testenv:coverage] description = [run locally after tests]: combine coverage data and create report; generates a diff coverage against origin/master (can be changed by setting DIFF_AGAINST env var) -deps = {[testenv]deps} - coverage >= 4.4.1, < 5 - diff_cover +deps = + {[testenv]deps} + coverage >= 5.0.1, <6 + diff_cover extras = skip_install = True passenv = DIFF_AGAINST -setenv = COVERAGE_FILE={toxworkdir}/.coverage -commands = coverage combine - coverage report --show-missing - coverage xml -o {toxworkdir}/coverage.xml - coverage html -d {toxworkdir}/htmlcov - diff-cover --compare-branch {env:DIFF_AGAINST:origin/master} {toxworkdir}/coverage.xml - -[testenv:cross_python2] -description = test creating a python3 venv with a python2-based virtualenv -basepython = python2 -extras = -commands = virtualenv -p python3 {envtmpdir}/{envname} - {envtmpdir}/{envname}/bin/python -V 2>&1 | grep "Python 3" - - -[testenv:cross_python3] -description = test creating a python2 venv with a python3-based virtualenv -basepython = python3 -extras = -commands = virtualenv -p python2 {envtmpdir}/{envname} - {envtmpdir}/{envname}/bin/python -V 2>&1 | grep "Python 2" +setenv = + COVERAGE_FILE={toxworkdir}/.coverage +commands = + coverage combine + coverage report --show-missing + coverage xml -o {toxworkdir}/coverage.xml + coverage html -d {toxworkdir}/htmlcov + diff-cover --compare-branch {env:DIFF_AGAINST:origin/rewrite} {toxworkdir}/coverage.xml +depends = + py38, + py37, + py36, + py35, + py34, + py27, +parallel_show_output = True [testenv:docs] -basepython = python3 +basepython = python3.8 description = build documentation extras = docs -commands = sphinx-build -d "{envtmpdir}/doctree" -W docs "{toxworkdir}/docs_out" --color -bhtml {posargs} - python -c 'import pathlib; print("documentation available under file://\{0\}".format(pathlib.Path(r"{toxworkdir}") / "docs_out" / "index.html"))' +commands = + sphinx-build -d "{envtmpdir}/doctree" -W docs "{toxworkdir}/docs_out" --color -bhtml {posargs} + python -c 'import pathlib; print("documentation available under file://\{0\}".format(pathlib.Path(r"{toxworkdir}") / "docs_out" / "index.html"))' [testenv:package_readme] description = check that the long description is valid (need for PyPi) -deps = {[testenv]deps} - twine >= 1.12.1 +deps = + {[testenv]deps} + twine >= 1.12.1 skip_install = true extras = -commands = pip wheel -w {envtmpdir}/build --no-deps . - twine check {envtmpdir}/build/* - -[testenv:embed] -description = embed dependencies into virtualenv.py -skip_install = true -changedir = {toxinidir}/tasks -extras = -commands = python update_embedded.py +commands = + pip wheel -w {envtmpdir}/build --no-deps . + twine check {envtmpdir}/build/* [testenv:upgrade] description = upgrade pip/wheels/setuptools to latest skip_install = true +deps = + pathlib2 + black +passenv = UPGRADE_ADVISORY changedir = {toxinidir}/tasks commands = python upgrade_wheels.py @@ -84,10 +96,11 @@ description = format the code base to adhere to our styles, and complain about w basepython = python3.8 passenv = * deps = {[testenv]deps} - pre-commit >= 1.12.0, <2 + pre-commit >= 1.17.0, <2 skip_install = True -commands = pre-commit run --all-files --show-diff-on-failure - python -c 'import pathlib; print("hint: run \{\} install to add checks as pre-commit hook".format(pathlib.Path(r"{envdir}") / "bin" / "pre-commit"))' +commands = + pre-commit run --all-files --show-diff-on-failure + python -c 'import pathlib; print("hint: run \{\} install to add checks as pre-commit hook".format(pathlib.Path(r"{envdir}") / "bin" / "pre-commit"))' [isort] multi_line_output = 3 @@ -96,13 +109,12 @@ force_grid_wrap = 0 line_length = 120 known_standard_library = ConfigParser known_first_party = virtualenv -known_third_party = git,packaging,pypiserver,pytest,pytest_localserver,setuptools,six +known_third_party = _subprocess,appdirs,coverage,entrypoints,git,packaging,pathlib2,pytest,setuptools,six [flake8] max-complexity = 22 max-line-length = 120 ignore = E203, W503, C901, E402 -exclude = virtualenv_embedded/site.py [pep8] max-line-length = 120 @@ -111,17 +123,20 @@ max-line-length = 120 description = generate a DEV environment extras = testing, docs usedevelop = True -commands = python -m pip list --format=columns - python -c 'import sys; print(sys.executable)' +commands = + python -m pip list --format=columns + python -c 'import sys; print(sys.executable)' [testenv:release] description = do a release, required posarg of the version number basepython = python3.8 skip_install = true passenv = * -deps = {[testenv]deps} - gitpython >= 2.1.10, < 3 - towncrier >= 18.5.0 - packaging >= 17.1 +deps = + {[testenv]deps} + gitpython >= 2.1.10, < 3 + towncrier >= 18.5.0 + packaging >= 17.1 changedir = {toxinidir}/tasks -commands = python release.py --version {posargs} +commands = + python release.py --version {posargs} diff --git a/virtualenv.py b/virtualenv.py deleted file mode 100755 index 000d50a99..000000000 --- a/virtualenv.py +++ /dev/null @@ -1,2634 +0,0 @@ -#!/usr/bin/env python -"""Create a "virtual" Python installation""" - -# fmt: off -import os # isort:skip -import sys # isort:skip - -# If we are running in a new interpreter to create a virtualenv, -# we do NOT want paths from our existing location interfering with anything, -# So we remove this file's directory from sys.path - most likely to be -# the previous interpreter's site-packages. Solves #705, #763, #779 - - -if os.environ.get("VIRTUALENV_INTERPRETER_RUNNING"): - for path in sys.path[:]: - if os.path.realpath(os.path.dirname(__file__)) == os.path.realpath(path): - sys.path.remove(path) -# fmt: on - -import ast -import base64 -import codecs -import contextlib -import distutils.spawn -import distutils.sysconfig -import errno -import glob -import logging -import optparse -import os -import re -import shutil -import struct -import subprocess -import sys -import tempfile -import textwrap -import zipfile -import zlib -from distutils.util import strtobool -from os.path import join - -try: - import ConfigParser -except ImportError: - # noinspection PyPep8Naming - import configparser as ConfigParser - -__version__ = "16.7.9" -virtualenv_version = __version__ # legacy -DEBUG = os.environ.get("_VIRTUALENV_DEBUG", None) == "1" -if sys.version_info < (2, 7): - print("ERROR: {}".format(sys.exc_info()[1])) - print("ERROR: this script requires Python 2.7 or greater.") - sys.exit(101) - -HERE = os.path.dirname(os.path.abspath(__file__)) -IS_ZIPAPP = os.path.isfile(HERE) - -try: - # noinspection PyUnresolvedReferences,PyUnboundLocalVariable - basestring -except NameError: - basestring = str - -VERSION = "{}.{}".format(*sys.version_info) -PY_VERSION = "python{}.{}".format(*sys.version_info) - -IS_PYPY = hasattr(sys, "pypy_version_info") -IS_WIN = sys.platform == "win32" -IS_CYGWIN = sys.platform == "cygwin" -IS_DARWIN = sys.platform == "darwin" -ABI_FLAGS = getattr(sys, "abiflags", "") - -USER_DIR = os.path.expanduser("~") -if IS_WIN: - DEFAULT_STORAGE_DIR = os.path.join(USER_DIR, "virtualenv") -else: - DEFAULT_STORAGE_DIR = os.path.join(USER_DIR, ".virtualenv") -DEFAULT_CONFIG_FILE = os.path.join(DEFAULT_STORAGE_DIR, "virtualenv.ini") - -if IS_PYPY: - EXPECTED_EXE = "pypy" -else: - EXPECTED_EXE = "python" - -# Return a mapping of version -> Python executable -# Only provided for Windows, where the information in the registry is used -if not IS_WIN: - - def get_installed_pythons(): - return {} - - -else: - try: - import winreg - except ImportError: - # noinspection PyUnresolvedReferences - import _winreg as winreg - - def get_installed_pythons(): - final_exes = dict() - - # Grab exes from 32-bit registry view - exes = _get_installed_pythons_for_view("-32", winreg.KEY_WOW64_32KEY) - # Grab exes from 64-bit registry view - exes_64 = _get_installed_pythons_for_view("-64", winreg.KEY_WOW64_64KEY) - # Check if exes are unique - if set(exes.values()) != set(exes_64.values()): - exes.update(exes_64) - - # Create dict with all versions found - for version, bitness in sorted(exes): - exe = exes[(version, bitness)] - # Add minor version (X.Y-32 or X.Y-64) - final_exes[version + bitness] = exe - # Add minor extensionless version (X.Y); 3.2-64 wins over 3.2-32 - final_exes[version] = exe - # Add major version (X-32 or X-64) - final_exes[version[0] + bitness] = exe - # Add major extensionless version (X); 3.3-32 wins over 3.2-64 - final_exes[version[0]] = exe - - return final_exes - - def _get_installed_pythons_for_view(bitness, view): - exes = dict() - # If both system and current user installations are found for a - # particular Python version, the current user one is used - for key in (winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER): - try: - python_core = winreg.OpenKey(key, "Software\\Python\\PythonCore", 0, view | winreg.KEY_READ) - except WindowsError: - # No registered Python installations - continue - i = 0 - while True: - try: - version = winreg.EnumKey(python_core, i) - i += 1 - try: - at_path = winreg.QueryValue(python_core, "{}\\InstallPath".format(version)) - except WindowsError: - continue - # Remove bitness from version - if version.endswith(bitness): - version = version[: -len(bitness)] - exes[(version, bitness)] = join(at_path, "python.exe") - except WindowsError: - break - winreg.CloseKey(python_core) - - return exes - - -REQUIRED_MODULES = [ - "os", - "posix", - "posixpath", - "nt", - "ntpath", - "genericpath", - "fnmatch", - "locale", - "encodings", - "codecs", - "stat", - "UserDict", - "readline", - "copy_reg", - "types", - "re", - "sre", - "sre_parse", - "sre_constants", - "sre_compile", - "zlib", -] - -REQUIRED_FILES = ["lib-dynload", "config"] - -MAJOR, MINOR = sys.version_info[:2] -if MAJOR == 2: - if MINOR >= 6: - REQUIRED_MODULES.extend(["warnings", "linecache", "_abcoll", "abc"]) - if MINOR >= 7: - REQUIRED_MODULES.extend(["_weakrefset"]) -elif MAJOR == 3: - # Some extra modules are needed for Python 3, but different ones - # for different versions. - REQUIRED_MODULES.extend( - [ - "_abcoll", - "warnings", - "linecache", - "abc", - "io", - "_weakrefset", - "copyreg", - "tempfile", - "random", - "__future__", - "collections", - "keyword", - "tarfile", - "shutil", - "struct", - "copy", - "tokenize", - "token", - "functools", - "heapq", - "bisect", - "weakref", - "reprlib", - ] - ) - if MINOR >= 2: - REQUIRED_FILES[-1] = "config-{}".format(MAJOR) - if MINOR >= 3: - import sysconfig - - platform_dir = sysconfig.get_config_var("PLATDIR") - REQUIRED_FILES.append(platform_dir) - REQUIRED_MODULES.extend(["base64", "_dummy_thread", "hashlib", "hmac", "imp", "importlib", "rlcompleter"]) - if MINOR >= 4: - REQUIRED_MODULES.extend(["operator", "_collections_abc", "_bootlocale"]) - if MINOR >= 6: - REQUIRED_MODULES.extend(["enum"]) - -if IS_PYPY: - # these are needed to correctly display the exceptions that may happen - # during the bootstrap - REQUIRED_MODULES.extend(["traceback", "linecache"]) - - if MAJOR == 3: - # _functools is needed to import locale during stdio initialization and - # needs to be copied on PyPy because it's not built in - REQUIRED_MODULES.append("_functools") - - -class Logger(object): - - """ - Logging object for use in command-line script. Allows ranges of - levels, to avoid some redundancy of displayed information. - """ - - DEBUG = logging.DEBUG - INFO = logging.INFO - NOTIFY = (logging.INFO + logging.WARN) / 2 - WARN = WARNING = logging.WARN - ERROR = logging.ERROR - FATAL = logging.FATAL - - LEVELS = [DEBUG, INFO, NOTIFY, WARN, ERROR, FATAL] - - def __init__(self, consumers): - self.consumers = consumers - self.indent = 0 - self.in_progress = None - self.in_progress_hanging = False - - def debug(self, msg, *args, **kw): - self.log(self.DEBUG, msg, *args, **kw) - - def info(self, msg, *args, **kw): - self.log(self.INFO, msg, *args, **kw) - - def notify(self, msg, *args, **kw): - self.log(self.NOTIFY, msg, *args, **kw) - - def warn(self, msg, *args, **kw): - self.log(self.WARN, msg, *args, **kw) - - def error(self, msg, *args, **kw): - self.log(self.ERROR, msg, *args, **kw) - - def fatal(self, msg, *args, **kw): - self.log(self.FATAL, msg, *args, **kw) - - def log(self, level, msg, *args, **kw): - if args: - if kw: - raise TypeError("You may give positional or keyword arguments, not both") - args = args or kw - rendered = None - for consumer_level, consumer in self.consumers: - if self.level_matches(level, consumer_level): - if self.in_progress_hanging and consumer in (sys.stdout, sys.stderr): - self.in_progress_hanging = False - print("") - sys.stdout.flush() - if rendered is None: - if args: - rendered = msg % args - else: - rendered = msg - rendered = " " * self.indent + rendered - if hasattr(consumer, "write"): - consumer.write(rendered + "\n") - else: - consumer(rendered) - - def start_progress(self, msg): - assert not self.in_progress, "Tried to start_progress({!r}) while in_progress {!r}".format( - msg, self.in_progress - ) - if self.level_matches(self.NOTIFY, self._stdout_level()): - print(msg) - sys.stdout.flush() - self.in_progress_hanging = True - else: - self.in_progress_hanging = False - self.in_progress = msg - - def end_progress(self, msg="done."): - assert self.in_progress, "Tried to end_progress without start_progress" - if self.stdout_level_matches(self.NOTIFY): - if not self.in_progress_hanging: - # Some message has been printed out since start_progress - print("...{}{}".format(self.in_progress, msg)) - sys.stdout.flush() - else: - print(msg) - sys.stdout.flush() - self.in_progress = None - self.in_progress_hanging = False - - def show_progress(self): - """If we are in a progress scope, and no log messages have been - shown, write out another '.'""" - if self.in_progress_hanging: - print(".") - sys.stdout.flush() - - def stdout_level_matches(self, level): - """Returns true if a message at this level will go to stdout""" - return self.level_matches(level, self._stdout_level()) - - def _stdout_level(self): - """Returns the level that stdout runs at""" - for level, consumer in self.consumers: - if consumer is sys.stdout: - return level - return self.FATAL - - @staticmethod - def level_matches(level, consumer_level): - """ - >>> l = Logger([]) - >>> l.level_matches(3, 4) - False - >>> l.level_matches(3, 2) - True - >>> l.level_matches(slice(None, 3), 3) - False - >>> l.level_matches(slice(None, 3), 2) - True - >>> l.level_matches(slice(1, 3), 1) - True - >>> l.level_matches(slice(2, 3), 1) - False - """ - if isinstance(level, slice): - start, stop = level.start, level.stop - if start is not None and start > consumer_level: - return False - if stop is not None and stop <= consumer_level: - return False - return True - else: - return level >= consumer_level - - @classmethod - def level_for_integer(cls, level): - levels = cls.LEVELS - if level < 0: - return levels[0] - if level >= len(levels): - return levels[-1] - return levels[level] - - -# create a silent logger just to prevent this from being undefined -# will be overridden with requested verbosity main() is called. -logger = Logger([(Logger.LEVELS[-1], sys.stdout)]) - - -def mkdir(at_path): - if not os.path.exists(at_path): - logger.info("Creating %s", at_path) - os.makedirs(at_path) - else: - logger.info("Directory %s already exists", at_path) - - -def copy_file_or_folder(src, dest, symlink=True): - if os.path.isdir(src): - shutil.copytree(src, dest, symlink) - else: - shutil.copy2(src, dest) - - -def copyfile(src, dest, symlink=True): - if not os.path.exists(src): - # Some bad symlink in the src - logger.warn("Cannot find file %s (bad symlink)", src) - return - if os.path.exists(dest): - logger.debug("File %s already exists", dest) - return - if not os.path.exists(os.path.dirname(dest)): - logger.info("Creating parent directories for %s", os.path.dirname(dest)) - os.makedirs(os.path.dirname(dest)) - if symlink and hasattr(os, "symlink") and not IS_WIN: - logger.info("Symlinking %s", dest) - try: - os.symlink(os.path.realpath(src), dest) - except (OSError, NotImplementedError): - logger.info("Symlinking failed, copying to %s", dest) - copy_file_or_folder(src, dest, symlink) - else: - logger.info("Copying to %s", dest) - copy_file_or_folder(src, dest, symlink) - - -def writefile(dest, content, overwrite=True): - if not os.path.exists(dest): - logger.info("Writing %s", dest) - with open(dest, "wb") as f: - f.write(content.encode("utf-8")) - return - else: - with open(dest, "rb") as f: - c = f.read() - if c != content.encode("utf-8"): - if not overwrite: - logger.notify("File %s exists with different content; not overwriting", dest) - return - logger.notify("Overwriting %s with new content", dest) - with open(dest, "wb") as f: - f.write(content.encode("utf-8")) - else: - logger.info("Content %s already in place", dest) - - -def rm_tree(folder): - if os.path.exists(folder): - logger.notify("Deleting tree %s", folder) - shutil.rmtree(folder) - else: - logger.info("Do not need to delete %s; already gone", folder) - - -def make_exe(fn): - if hasattr(os, "chmod"): - old_mode = os.stat(fn).st_mode & 0xFFF # 0o7777 - new_mode = (old_mode | 0x16D) & 0xFFF # 0o555, 0o7777 - os.chmod(fn, new_mode) - logger.info("Changed mode of %s to %s", fn, oct(new_mode)) - - -def _find_file(filename, folders): - for folder in reversed(folders): - files = glob.glob(os.path.join(folder, filename)) - if files and os.path.isfile(files[0]): - return True, files[0] - return False, filename - - -@contextlib.contextmanager -def virtualenv_support_dirs(): - """Context manager yielding either [virtualenv_support_dir] or []""" - - # normal filesystem installation - if os.path.isdir(join(HERE, "virtualenv_support")): - yield [join(HERE, "virtualenv_support")] - elif IS_ZIPAPP: - tmpdir = tempfile.mkdtemp() - try: - with zipfile.ZipFile(HERE) as zipf: - for member in zipf.namelist(): - if os.path.dirname(member) == "virtualenv_support": - zipf.extract(member, tmpdir) - yield [join(tmpdir, "virtualenv_support")] - finally: - shutil.rmtree(tmpdir) - # probably a bootstrap script - elif os.path.splitext(os.path.dirname(__file__))[0] != "virtualenv": - try: - # noinspection PyUnresolvedReferences - import virtualenv - except ImportError: - yield [] - else: - yield [join(os.path.dirname(virtualenv.__file__), "virtualenv_support")] - # we tried! - else: - yield [] - - -class UpdatingDefaultsHelpFormatter(optparse.IndentedHelpFormatter): - """ - Custom help formatter for use in ConfigOptionParser that updates - the defaults before expanding them, allowing them to show up correctly - in the help listing - """ - - def expand_default(self, option): - if self.parser is not None: - self.parser.update_defaults(self.parser.defaults) - return optparse.IndentedHelpFormatter.expand_default(self, option) - - -class ConfigOptionParser(optparse.OptionParser): - """ - Custom option parser which updates its defaults by checking the - configuration files and environmental variables - """ - - def __init__(self, *args, **kwargs): - self.config = ConfigParser.RawConfigParser() - self.files = self.get_config_files() - self.config.read(self.files) - optparse.OptionParser.__init__(self, *args, **kwargs) - - @staticmethod - def get_config_files(): - config_file = os.environ.get("VIRTUALENV_CONFIG_FILE", False) - if config_file and os.path.exists(config_file): - return [config_file] - return [DEFAULT_CONFIG_FILE] - - def update_defaults(self, defaults): - """ - Updates the given defaults with values from the config files and - the environ. Does a little special handling for certain types of - options (lists). - """ - # Then go and look for the other sources of configuration: - config = {} - # 1. config files - config.update(dict(self.get_config_section("virtualenv"))) - # 2. environmental variables - config.update(dict(self.get_environ_vars())) - # Then set the options with those values - for key, val in config.items(): - key = key.replace("_", "-") - if not key.startswith("--"): - key = "--{}".format(key) # only prefer long opts - option = self.get_option(key) - if option is not None: - # ignore empty values - if not val: - continue - # handle multiline configs - if option.action == "append": - val = val.split() - else: - option.nargs = 1 - if option.action == "store_false": - val = not strtobool(val) - elif option.action in ("store_true", "count"): - val = strtobool(val) - try: - val = option.convert_value(key, val) - except optparse.OptionValueError: - e = sys.exc_info()[1] - print("An error occurred during configuration: {!r}".format(e)) - sys.exit(3) - defaults[option.dest] = val - return defaults - - def get_config_section(self, name): - """ - Get a section of a configuration - """ - if self.config.has_section(name): - return self.config.items(name) - return [] - - def get_environ_vars(self, prefix="VIRTUALENV_"): - """ - Returns a generator with all environmental vars with prefix VIRTUALENV - """ - for key, val in os.environ.items(): - if key.startswith(prefix): - yield (key.replace(prefix, "").lower(), val) - - def get_default_values(self): - """ - Overriding to make updating the defaults after instantiation of - the option parser possible, update_defaults() does the dirty work. - """ - if not self.process_default_values: - # Old, pre-Optik 1.5 behaviour. - return optparse.Values(self.defaults) - - defaults = self.update_defaults(self.defaults.copy()) # ours - for option in self._get_all_options(): - default = defaults.get(option.dest) - if isinstance(default, basestring): - opt_str = option.get_opt_string() - defaults[option.dest] = option.check_value(opt_str, default) - return optparse.Values(defaults) - - -def main(): - parser = ConfigOptionParser( - version=virtualenv_version, usage="%prog [OPTIONS] DEST_DIR", formatter=UpdatingDefaultsHelpFormatter() - ) - - parser.add_option( - "-v", "--verbose", action="count", dest="verbose", default=5 if DEBUG else 0, help="Increase verbosity." - ) - - parser.add_option("-q", "--quiet", action="count", dest="quiet", default=0, help="Decrease verbosity.") - - parser.add_option( - "-p", - "--python", - dest="python", - metavar="PYTHON_EXE", - help="The Python interpreter to use, e.g., --python=python3.5 will use the python3.5 " - "interpreter to create the new environment. The default is the interpreter that " - "virtualenv was installed with ({})".format(sys.executable), - ) - - parser.add_option( - "--clear", dest="clear", action="store_true", help="Clear out the non-root install and start from scratch." - ) - - parser.set_defaults(system_site_packages=False) - parser.add_option( - "--no-site-packages", - dest="system_site_packages", - action="store_false", - help="DEPRECATED. Retained only for backward compatibility. " - "Not having access to global site-packages is now the default behavior.", - ) - - parser.add_option( - "--system-site-packages", - dest="system_site_packages", - action="store_true", - help="Give the virtual environment access to the global site-packages.", - ) - - parser.add_option( - "--always-copy", - dest="symlink", - action="store_false", - default=True, - help="Always copy files rather than symlinking.", - ) - - parser.add_option( - "--relocatable", - dest="relocatable", - action="store_true", - help="Make an EXISTING virtualenv environment relocatable. " - "This fixes up scripts and makes all .pth files relative.", - ) - - parser.add_option( - "--no-setuptools", - dest="no_setuptools", - action="store_true", - help="Do not install setuptools in the new virtualenv.", - ) - - parser.add_option("--no-pip", dest="no_pip", action="store_true", help="Do not install pip in the new virtualenv.") - - parser.add_option( - "--no-wheel", dest="no_wheel", action="store_true", help="Do not install wheel in the new virtualenv." - ) - - parser.add_option( - "--extra-search-dir", - dest="search_dirs", - action="append", - metavar="DIR", - default=[], - help="Directory to look for setuptools/pip distributions in. " "This option can be used multiple times.", - ) - - parser.add_option( - "--download", - dest="download", - default=True, - action="store_true", - help="Download pre-installed packages from PyPI.", - ) - - parser.add_option( - "--no-download", - "--never-download", - dest="download", - action="store_false", - help="Do not download pre-installed packages from PyPI.", - ) - - parser.add_option("--prompt", dest="prompt", help="Provides an alternative prompt prefix for this environment.") - - parser.add_option( - "--setuptools", - dest="setuptools", - action="store_true", - help="DEPRECATED. Retained only for backward compatibility. This option has no effect.", - ) - - parser.add_option( - "--distribute", - dest="distribute", - action="store_true", - help="DEPRECATED. Retained only for backward compatibility. This option has no effect.", - ) - - parser.add_option( - "--unzip-setuptools", - action="store_true", - help="DEPRECATED. Retained only for backward compatibility. This option has no effect.", - ) - - if "extend_parser" in globals(): - # noinspection PyUnresolvedReferences - extend_parser(parser) # noqa: F821 - - options, args = parser.parse_args() - - global logger - - if "adjust_options" in globals(): - # noinspection PyUnresolvedReferences - adjust_options(options, args) # noqa: F821 - - verbosity = options.verbose - options.quiet - logger = Logger([(Logger.level_for_integer(2 - verbosity), sys.stdout)]) - - def should_reinvoke(options): - """Do we need to reinvoke ourself?""" - # Did the user specify the --python option? - if options.python and not os.environ.get("VIRTUALENV_INTERPRETER_RUNNING"): - interpreter = resolve_interpreter(options.python) - if interpreter != sys.executable: - # The user specified a different interpreter, so we have to reinvoke. - return interpreter - - # At this point, we know the user wants to use sys.executable to create the - # virtual environment. But on Windows, sys.executable may be a venv redirector, - # in which case we still need to locate the underlying actual interpreter, and - # reinvoke using that. - if IS_WIN: - # OK. Now things get really fun... - # - # If we are running from a venv, with a redirector, then what happens is as - # follows: - # - # 1. The redirector sets __PYVENV_LAUNCHER__ in the environment to point - # to the redirector executable. - # 2. The redirector launches the "base" Python (from the home value in - # pyvenv.cfg). - # 3. The base Python executable sees __PYVENV_LAUNCHER__ in the environment - # and sets sys.executable to that value. - # 4. If site.py gets run, it sees __PYVENV_LAUNCHER__, and sets - # sys._base_executable to _winapi.GetModuleFileName(0) and removes - # __PYVENV_LAUNCHER__. - # - # Unfortunately, that final step (site.py) may not happen. There are 2 key - # times when that is the case: - # - # 1. Python 3.7.2, which had the redirector but not the site.py code. - # 2. Running a venv from a virtualenv, which uses virtualenv's custom - # site.py. - # - # So, we check for sys._base_executable, but if it's not present and yet we - # have __PYVENV_LAUNCHER__, we do what site.py would have done and get our - # interpreter from GetModuleFileName(0). We also remove __PYVENV_LAUNCHER__ - # from the environment, to avoid loops (actually, mainly because site.py - # does so, and my head hurts enough buy now that I just want to be safe!) - - # In Python 3.7.4, the rules changed so that sys._base_executable is always - # set. So we now only return sys._base_executable if it's set *and does not - # match sys.executable* (we still have to check that it's set, as we need to - # support Python 3.7.3 and earlier). - - # Phew. - - if getattr(sys, "_base_executable", sys.executable) != sys.executable: - return sys._base_executable - - if "__PYVENV_LAUNCHER__" in os.environ: - import _winapi - - del os.environ["__PYVENV_LAUNCHER__"] - return _winapi.GetModuleFileName(0) - - # We don't need to reinvoke - return None - - interpreter = should_reinvoke(options) - if interpreter is None: - # We don't need to reinvoke - if the user asked us to, tell them why we - # aren't. - if options.python: - logger.warn("Already using interpreter {}".format(sys.executable)) - else: - env = os.environ.copy() - logger.notify("Running virtualenv with interpreter {}".format(interpreter)) - env["VIRTUALENV_INTERPRETER_RUNNING"] = "true" - # Remove the variable __PYVENV_LAUNCHER__ if it's present, as it causes the - # interpreter to redirect back to the virtual environment. - if "__PYVENV_LAUNCHER__" in env: - del env["__PYVENV_LAUNCHER__"] - file = __file__ - if file.endswith(".pyc"): - file = file[:-1] - elif IS_ZIPAPP: - file = HERE - sub_process_call = subprocess.Popen([interpreter, file] + sys.argv[1:], env=env) - raise SystemExit(sub_process_call.wait()) - - if not args: - print("You must provide a DEST_DIR") - parser.print_help() - sys.exit(2) - if len(args) > 1: - print("There must be only one argument: DEST_DIR (you gave {})".format(" ".join(args))) - parser.print_help() - sys.exit(2) - - home_dir = args[0] - - if os.path.exists(home_dir) and os.path.isfile(home_dir): - logger.fatal("ERROR: File already exists and is not a directory.") - logger.fatal("Please provide a different path or delete the file.") - sys.exit(3) - - if os.pathsep in home_dir: - logger.fatal("ERROR: target path contains the operating system path separator '{}'".format(os.pathsep)) - logger.fatal("This is not allowed as would make the activation scripts unusable.".format(os.pathsep)) - sys.exit(3) - - if os.environ.get("WORKING_ENV"): - logger.fatal("ERROR: you cannot run virtualenv while in a working env") - logger.fatal("Please deactivate your working env, then re-run this script") - sys.exit(3) - - if "PYTHONHOME" in os.environ: - logger.warn("PYTHONHOME is set. You *must* activate the virtualenv before using it") - del os.environ["PYTHONHOME"] - - if options.relocatable: - make_environment_relocatable(home_dir) - return - - with virtualenv_support_dirs() as search_dirs: - create_environment( - home_dir, - site_packages=options.system_site_packages, - clear=options.clear, - prompt=options.prompt, - search_dirs=search_dirs + options.search_dirs, - download=options.download, - no_setuptools=options.no_setuptools, - no_pip=options.no_pip, - no_wheel=options.no_wheel, - symlink=options.symlink, - ) - if "after_install" in globals(): - # noinspection PyUnresolvedReferences - after_install(options, home_dir) # noqa: F821 - - -def call_subprocess( - cmd, - show_stdout=True, - filter_stdout=None, - cwd=None, - raise_on_return_code=True, - extra_env=None, - remove_from_env=None, - stdin=None, -): - cmd_parts = [] - for part in cmd: - if len(part) > 45: - part = part[:20] + "..." + part[-20:] - if " " in part or "\n" in part or '"' in part or "'" in part: - part = '"{}"'.format(part.replace('"', '\\"')) - if hasattr(part, "decode"): - try: - part = part.decode(sys.getdefaultencoding()) - except UnicodeDecodeError: - part = part.decode(sys.getfilesystemencoding()) - cmd_parts.append(part) - cmd_desc = " ".join(cmd_parts) - if show_stdout: - stdout = None - else: - stdout = subprocess.PIPE - logger.debug("Running command {}".format(cmd_desc)) - if extra_env or remove_from_env: - env = os.environ.copy() - if extra_env: - env.update(extra_env) - if remove_from_env: - for var_name in remove_from_env: - env.pop(var_name, None) - else: - env = None - try: - proc = subprocess.Popen( - cmd, - stderr=subprocess.STDOUT, - stdin=None if stdin is None else subprocess.PIPE, - stdout=stdout, - cwd=cwd, - env=env, - ) - except Exception: - e = sys.exc_info()[1] - logger.fatal("Error {} while executing command {}".format(e, cmd_desc)) - raise - all_output = [] - if stdout is not None: - if stdin is not None: - with proc.stdin: - proc.stdin.write(stdin) - - encoding = sys.getdefaultencoding() - fs_encoding = sys.getfilesystemencoding() - with proc.stdout as stdout: - while 1: - line = stdout.readline() - try: - line = line.decode(encoding) - except UnicodeDecodeError: - line = line.decode(fs_encoding) - if not line: - break - line = line.rstrip() - all_output.append(line) - if filter_stdout: - level = filter_stdout(line) - if isinstance(level, tuple): - level, line = level - logger.log(level, line) - if not logger.stdout_level_matches(level): - logger.show_progress() - else: - logger.info(line) - else: - proc.communicate(stdin) - proc.wait() - if proc.returncode: - if raise_on_return_code: - if all_output: - logger.notify("Complete output from command {}:".format(cmd_desc)) - logger.notify("\n".join(all_output) + "\n----------------------------------------") - raise OSError("Command {} failed with error code {}".format(cmd_desc, proc.returncode)) - else: - logger.warn("Command {} had error code {}".format(cmd_desc, proc.returncode)) - return all_output - - -def filter_install_output(line): - if line.strip().startswith("running"): - return Logger.INFO - return Logger.DEBUG - - -def find_wheels(projects, search_dirs): - """Find wheels from which we can import PROJECTS. - - Scan through SEARCH_DIRS for a wheel for each PROJECT in turn. Return - a list of the first wheel found for each PROJECT - """ - - wheels = [] - - # Look through SEARCH_DIRS for the first suitable wheel. Don't bother - # about version checking here, as this is simply to get something we can - # then use to install the correct version. - for project in projects: - for dirname in search_dirs: - # This relies on only having "universal" wheels available. - # The pattern could be tightened to require -py2.py3-none-any.whl. - files = glob.glob(os.path.join(dirname, "{}-*.whl".format(project))) - if files: - versions = list( - reversed( - sorted( - [(tuple(int(i) for i in os.path.basename(f).split("-")[1].split(".")), f) for f in files] - ) - ) - ) - if project == "pip" and sys.version_info[0:2] == (3, 4): - wheel = next(p for v, p in versions if v <= (19, 1, 1)) - else: - wheel = versions[0][1] - wheels.append(wheel) - break - else: - # We're out of luck, so quit with a suitable error - logger.fatal("Cannot find a wheel for {}".format(project)) - - return wheels - - -def install_wheel(project_names, py_executable, search_dirs=None, download=False): - if search_dirs is None: - search_dirs_context = virtualenv_support_dirs - else: - - @contextlib.contextmanager - def search_dirs_context(): - yield search_dirs - - with search_dirs_context() as search_dirs: - _install_wheel_with_search_dir(download, project_names, py_executable, search_dirs) - - -def _install_wheel_with_search_dir(download, project_names, py_executable, search_dirs): - wheels = find_wheels(["setuptools", "pip"], search_dirs) - python_path = os.pathsep.join(wheels) - - # PIP_FIND_LINKS uses space as the path separator and thus cannot have paths - # with spaces in them. Convert any of those to local file:// URL form. - try: - from urlparse import urljoin - from urllib import pathname2url - except ImportError: - from urllib.parse import urljoin - from urllib.request import pathname2url - - def space_path2url(p): - if " " not in p: - return p - return urljoin("file:", pathname2url(os.path.abspath(p))) - - find_links = " ".join(space_path2url(d) for d in search_dirs) - - extra_args = ["--ignore-installed", "-v"] - if DEBUG: - extra_args.append("-v") - - config = _pip_config(py_executable, python_path) - defined_cert = bool(config.get("install.cert") or config.get(":env:.cert") or config.get("global.cert")) - - script = textwrap.dedent( - """ - import sys - import pkgutil - import tempfile - import os - - defined_cert = {defined_cert} - - try: - from pip._internal import main as _main - if type(_main) is type(sys): # - _main = _main.main # nested starting in Pip 19.3 - cert_data = pkgutil.get_data("pip._vendor.certifi", "cacert.pem") - except ImportError: - from pip import main as _main - cert_data = pkgutil.get_data("pip._vendor.requests", "cacert.pem") - except IOError: - cert_data = None - - if not defined_cert and cert_data is not None: - cert_file = tempfile.NamedTemporaryFile(delete=False) - cert_file.write(cert_data) - cert_file.close() - else: - cert_file = None - - try: - args = ["install"] + [{extra_args}] - if cert_file is not None: - args += ["--cert", cert_file.name] - args += sys.argv[1:] - - sys.exit(_main(args)) - finally: - if cert_file is not None: - os.remove(cert_file.name) - """.format( - defined_cert=defined_cert, extra_args=", ".join(repr(i) for i in extra_args) - ) - ).encode("utf8") - - if sys.version_info[0:2] == (3, 4) and "pip" in project_names: - at = project_names.index("pip") - project_names[at] = "pip<19.2" - - cmd = [py_executable, "-"] + project_names - logger.start_progress("Installing {}...".format(", ".join(project_names))) - logger.indent += 2 - - env = { - "PYTHONPATH": python_path, - "PIP_FIND_LINKS": find_links, - "PIP_USE_WHEEL": "1", - "PIP_ONLY_BINARY": ":all:", - "PIP_USER": "0", - "PIP_NO_INPUT": "1", - } - - if not download: - env["PIP_NO_INDEX"] = "1" - - try: - call_subprocess(cmd, show_stdout=False, extra_env=env, stdin=script) - finally: - logger.indent -= 2 - logger.end_progress() - - -def _pip_config(py_executable, python_path): - cmd = [py_executable, "-m", "pip", "config", "list"] - config = {} - for line in call_subprocess( - cmd, - show_stdout=False, - extra_env={"PYTHONPATH": python_path}, - remove_from_env=["PIP_VERBOSE", "PIP_QUIET"], - raise_on_return_code=False, - ): - key, _, value = line.partition("=") - if value: - config[key] = ast.literal_eval(value) - return config - - -def create_environment( - home_dir, - site_packages=False, - clear=False, - prompt=None, - search_dirs=None, - download=False, - no_setuptools=False, - no_pip=False, - no_wheel=False, - symlink=True, -): - """ - Creates a new environment in ``home_dir``. - - If ``site_packages`` is true, then the global ``site-packages/`` - directory will be on the path. - - If ``clear`` is true (default False) then the environment will - first be cleared. - """ - home_dir, lib_dir, inc_dir, bin_dir = path_locations(home_dir) - - py_executable = os.path.abspath( - install_python(home_dir, lib_dir, inc_dir, bin_dir, site_packages=site_packages, clear=clear, symlink=symlink) - ) - - install_distutils(home_dir) - - to_install = [] - - if not no_setuptools: - to_install.append("setuptools") - - if not no_pip: - to_install.append("pip") - - if not no_wheel: - to_install.append("wheel") - - if to_install: - install_wheel(to_install, py_executable, search_dirs, download=download) - - install_activate(home_dir, bin_dir, prompt) - - install_python_config(home_dir, bin_dir, prompt) - - -def is_executable_file(fpath): - return os.path.isfile(fpath) and is_executable(fpath) - - -def path_locations(home_dir, dry_run=False): - """Return the path locations for the environment (where libraries are, - where scripts go, etc)""" - home_dir = os.path.abspath(home_dir) - lib_dir, inc_dir, bin_dir = None, None, None - # XXX: We'd use distutils.sysconfig.get_python_inc/lib but its - # prefix arg is broken: http://bugs.python.org/issue3386 - if IS_WIN: - # Windows has lots of problems with executables with spaces in - # the name; this function will remove them (using the ~1 - # format): - if not dry_run: - mkdir(home_dir) - if " " in home_dir: - import ctypes - - get_short_path_name = ctypes.windll.kernel32.GetShortPathNameW - size = max(len(home_dir) + 1, 256) - buf = ctypes.create_unicode_buffer(size) - try: - # noinspection PyUnresolvedReferences - u = unicode - except NameError: - u = str - ret = get_short_path_name(u(home_dir), buf, size) - if not ret: - print('Error: the path "{}" has a space in it'.format(home_dir)) - print("We could not determine the short pathname for it.") - print("Exiting.") - sys.exit(3) - home_dir = str(buf.value) - lib_dir = join(home_dir, "Lib") - inc_dir = join(home_dir, "Include") - bin_dir = join(home_dir, "Scripts") - if IS_PYPY: - lib_dir = home_dir - inc_dir = join(home_dir, "include") - bin_dir = join(home_dir, "bin") - elif not IS_WIN: - lib_dir = join(home_dir, "lib", PY_VERSION) - inc_dir = join(home_dir, "include", PY_VERSION + ABI_FLAGS) - bin_dir = join(home_dir, "bin") - return home_dir, lib_dir, inc_dir, bin_dir - - -def change_prefix(filename, dst_prefix): - prefixes = [sys.prefix] - - if IS_DARWIN: - prefixes.extend( - ( - os.path.join("/Library/Python", VERSION, "site-packages"), - os.path.join(sys.prefix, "Extras", "lib", "python"), - os.path.join("~", "Library", "Python", VERSION, "site-packages"), - # Python 2.6 no-frameworks - os.path.join("~", ".local", "lib", "python", VERSION, "site-packages"), - # System Python 2.7 on OSX Mountain Lion - os.path.join("~", "Library", "Python", VERSION, "lib", "python", "site-packages"), - ) - ) - - if hasattr(sys, "real_prefix"): - prefixes.append(sys.real_prefix) - if hasattr(sys, "base_prefix"): - prefixes.append(sys.base_prefix) - prefixes = list(map(os.path.expanduser, prefixes)) - prefixes = list(map(os.path.abspath, prefixes)) - # Check longer prefixes first so we don't split in the middle of a filename - prefixes = sorted(prefixes, key=len, reverse=True) - filename = os.path.abspath(filename) - # On Windows, make sure drive letter is uppercase - if IS_WIN and filename[0] in "abcdefghijklmnopqrstuvwxyz": - filename = filename[0].upper() + filename[1:] - for i, prefix in enumerate(prefixes): - if IS_WIN and prefix[0] in "abcdefghijklmnopqrstuvwxyz": - prefixes[i] = prefix[0].upper() + prefix[1:] - for src_prefix in prefixes: - if filename.startswith(src_prefix): - _, relative_path = filename.split(src_prefix, 1) - if src_prefix != os.sep: # sys.prefix == "/" - assert relative_path[0] == os.sep - relative_path = relative_path[1:] - return join(dst_prefix, relative_path) - raise AssertionError("Filename {} does not start with any of these prefixes: {}".format(filename, prefixes)) - - -def find_module_filename(modname): - - if sys.version_info < (3, 4): - # noinspection PyDeprecation - import imp - - try: - file_handler, filepath, _ = imp.find_module(modname) - except ImportError: - return None - else: - if file_handler is not None: - file_handler.close() - return filepath - else: - import importlib.util - - if sys.version_info < (3, 5): - - def find_spec(modname): - # noinspection PyDeprecation - loader = importlib.find_loader(modname) - if loader is None: - return None - else: - return importlib.util.spec_from_loader(modname, loader) - - else: - find_spec = importlib.util.find_spec - - spec = find_spec(modname) - if spec is None: - return None - if not os.path.exists(spec.origin): - # https://bitbucket.org/pypy/pypy/issues/2944/origin-for-several-builtin-modules - # on pypy3, some builtin modules have a bogus build-time file path, ignore them - return None - filepath = spec.origin - # https://www.python.org/dev/peps/pep-3147/#file guarantee to be non-cached - if os.path.basename(filepath) == "__init__.py": - filepath = os.path.dirname(filepath) - return filepath - - -def copy_required_modules(dst_prefix, symlink): - for modname in REQUIRED_MODULES: - if modname in sys.builtin_module_names: - logger.info("Ignoring built-in bootstrap module: %s" % modname) - continue - filename = find_module_filename(modname) - if filename is None: - logger.info("Cannot import bootstrap module: %s" % modname) - else: - # special-case custom readline.so on OS X, but not for pypy: - if ( - modname == "readline" - and IS_DARWIN - and not (IS_PYPY or filename.endswith(join("lib-dynload", "readline.so"))) - ): - dst_filename = join(dst_prefix, "lib", PY_VERSION, "readline.so") - elif modname == "readline" and IS_WIN: - # special-case for Windows, where readline is not a standard module, though it may have been installed - # in site-packages by a third-party package - dst_filename = None - else: - dst_filename = change_prefix(filename, dst_prefix) - if dst_filename is not None: - copyfile(filename, dst_filename, symlink) - if filename.endswith(".pyc"): - py_file = filename[:-1] - if os.path.exists(py_file): - copyfile(py_file, dst_filename[:-1], symlink) - - -def copy_required_files(src_dir, lib_dir, symlink): - if not os.path.isdir(src_dir): - return - for fn in os.listdir(src_dir): - bn = os.path.splitext(fn)[0] - if fn != "site-packages" and bn in REQUIRED_FILES: - copyfile(join(src_dir, fn), join(lib_dir, fn), symlink) - - -def copy_license(prefix, dst_prefix, lib_dir, symlink): - """Copy the license file so `license()` builtin works""" - lib64_dir = lib_dir.replace("lib", "lib64") - for license_path in ( - # posix cpython - os.path.join(prefix, os.path.relpath(lib_dir, dst_prefix), "LICENSE.txt"), - # posix cpython installed in /usr/lib64 - os.path.join(prefix, os.path.relpath(lib64_dir, dst_prefix), "LICENSE.txt"), - # windows cpython - os.path.join(prefix, "LICENSE.txt"), - # pypy - os.path.join(prefix, "LICENSE"), - ): - if os.path.exists(license_path): - dest = subst_path(license_path, prefix, dst_prefix) - copyfile(license_path, dest, symlink) - return - logger.warn("No LICENSE.txt / LICENSE found in source") - - -def copy_include_dir(include_src, include_dest, symlink): - """Copy headers from *include_src* to *include_dest* symlinking if required""" - if not os.path.isdir(include_src): - return - # PyPy headers are located in ``pypy-dir/include`` and following code - # avoids making ``venv-dir/include`` symlink to it - if IS_PYPY: - for fn in os.listdir(include_src): - copyfile(join(include_src, fn), join(include_dest, fn), symlink) - else: - copyfile(include_src, include_dest, symlink) - - -def copy_tcltk(src, dest, symlink): - """ copy tcl/tk libraries on Windows (issue #93) """ - for lib_version in "8.5", "8.6": - for libname in "tcl", "tk": - src_dir = join(src, "tcl", libname + lib_version) - dest_dir = join(dest, "tcl", libname + lib_version) - # Only copy the dirs from the above combinations that exist - if os.path.exists(src_dir) and not os.path.exists(dest_dir): - copy_file_or_folder(src_dir, dest_dir, symlink) - - -def subst_path(prefix_path, prefix, home_dir): - prefix_path = os.path.normpath(prefix_path) - prefix = os.path.normpath(prefix) - home_dir = os.path.normpath(home_dir) - if not prefix_path.startswith(prefix): - logger.warn("Path not in prefix %r %r", prefix_path, prefix) - return - return prefix_path.replace(prefix, home_dir, 1) - - -def install_python(home_dir, lib_dir, inc_dir, bin_dir, site_packages, clear, symlink=True): - """Install just the base environment, no distutils patches etc""" - if sys.executable.startswith(bin_dir): - print("Please use the *system* python to run this script") - return - - if clear: - rm_tree(lib_dir) - # FIXME: why not delete it? - # Maybe it should delete everything with #!/path/to/venv/python in it - logger.notify("Not deleting %s", bin_dir) - - if hasattr(sys, "real_prefix"): - logger.notify("Using real prefix %r", sys.real_prefix) - prefix = sys.real_prefix - elif hasattr(sys, "base_prefix"): - logger.notify("Using base prefix %r", sys.base_prefix) - prefix = sys.base_prefix - else: - prefix = sys.prefix - prefix = os.path.abspath(prefix) - mkdir(lib_dir) - fix_lib64(lib_dir, symlink) - stdlib_dirs = [os.path.dirname(os.__file__)] - if IS_WIN: - stdlib_dirs.append(join(os.path.dirname(stdlib_dirs[0]), "DLLs")) - elif IS_DARWIN: - stdlib_dirs.append(join(stdlib_dirs[0], "site-packages")) - if hasattr(os, "symlink"): - logger.info("Symlinking Python bootstrap modules") - else: - logger.info("Copying Python bootstrap modules") - logger.indent += 2 - try: - # copy required files... - for stdlib_dir in stdlib_dirs: - copy_required_files(stdlib_dir, lib_dir, symlink) - # ...and modules - copy_required_modules(home_dir, symlink) - copy_license(prefix, home_dir, lib_dir, symlink) - finally: - logger.indent -= 2 - # ...copy tcl/tk - if IS_WIN: - copy_tcltk(prefix, home_dir, symlink) - mkdir(join(lib_dir, "site-packages")) - import site - - site_filename = site.__file__ - if site_filename.endswith(".pyc") or site_filename.endswith(".pyo"): - site_filename = site_filename[:-1] - elif site_filename.endswith("$py.class"): - site_filename = site_filename.replace("$py.class", ".py") - site_filename_dst = change_prefix(site_filename, home_dir) - site_dir = os.path.dirname(site_filename_dst) - writefile(site_filename_dst, SITE_PY) - writefile(join(site_dir, "orig-prefix.txt"), prefix) - site_packages_filename = join(site_dir, "no-global-site-packages.txt") - if not site_packages: - writefile(site_packages_filename, "") - - if IS_PYPY or IS_WIN: - standard_lib_include_dir = join(prefix, "include") - else: - standard_lib_include_dir = join(prefix, "include", PY_VERSION + ABI_FLAGS) - if os.path.exists(standard_lib_include_dir): - copy_include_dir(standard_lib_include_dir, inc_dir, symlink) - else: - logger.debug("No include dir %s", standard_lib_include_dir) - - platform_include_dir = distutils.sysconfig.get_python_inc(plat_specific=1) - if platform_include_dir != standard_lib_include_dir: - platform_include_dest = distutils.sysconfig.get_python_inc(plat_specific=1, prefix=home_dir) - if platform_include_dir == platform_include_dest: - # Do platinc_dest manually due to a CPython bug; - # not http://bugs.python.org/issue3386 but a close cousin - platform_include_dest = subst_path(platform_include_dir, prefix, home_dir) - if platform_include_dest: - # PyPy's stdinc_dir and prefix are relative to the original binary - # (traversing virtualenvs), whereas the platinc_dir is relative to - # the inner virtualenv and ignores the prefix argument. - # This seems more evolved than designed. - copy_include_dir(platform_include_dir, platform_include_dest, symlink) - - # pypy never uses exec_prefix, just ignore it - if os.path.realpath(sys.exec_prefix) != os.path.realpath(prefix) and not IS_PYPY: - if IS_WIN: - exec_dir = join(sys.exec_prefix, "lib") - else: - exec_dir = join(sys.exec_prefix, "lib", PY_VERSION) - copy_required_files(exec_dir, lib_dir, symlink) - - mkdir(bin_dir) - py_executable = join(bin_dir, os.path.basename(sys.executable)) - if "Python.framework" in prefix or "Python3.framework" in prefix: - # OS X framework builds cause validation to break - # https://github.com/pypa/virtualenv/issues/322 - if os.environ.get("__PYVENV_LAUNCHER__"): - del os.environ["__PYVENV_LAUNCHER__"] - if re.search(r"/Python(?:-32|-64)*$", py_executable): - # The name of the python executable is not quite what - # we want, rename it. - py_executable = os.path.join(os.path.dirname(py_executable), "python") - - logger.notify("New %s executable in %s", EXPECTED_EXE, py_executable) - pc_build_dir = os.path.dirname(sys.executable) - pyd_pth = os.path.join(lib_dir, "site-packages", "virtualenv_builddir_pyd.pth") - if IS_WIN and os.path.exists(os.path.join(pc_build_dir, "build.bat")): - logger.notify("Detected python running from build directory %s", pc_build_dir) - logger.notify("Writing .pth file linking to build directory for *.pyd files") - writefile(pyd_pth, pc_build_dir) - else: - if os.path.exists(pyd_pth): - logger.info("Deleting %s (not Windows env or not build directory python)", pyd_pth) - os.unlink(pyd_pth) - - if sys.executable != py_executable: - # FIXME: could I just hard link? - executable = sys.executable - shutil.copyfile(executable, py_executable) - make_exe(py_executable) - if IS_WIN or IS_CYGWIN: - python_w = os.path.join(os.path.dirname(sys.executable), "pythonw.exe") - if os.path.exists(python_w): - logger.info("Also created pythonw.exe") - shutil.copyfile(python_w, os.path.join(os.path.dirname(py_executable), "pythonw.exe")) - python_d = os.path.join(os.path.dirname(sys.executable), "python_d.exe") - python_d_dest = os.path.join(os.path.dirname(py_executable), "python_d.exe") - if os.path.exists(python_d): - logger.info("Also created python_d.exe") - shutil.copyfile(python_d, python_d_dest) - elif os.path.exists(python_d_dest): - logger.info("Removed python_d.exe as it is no longer at the source") - os.unlink(python_d_dest) - - # we need to copy the DLL to enforce that windows will load the correct one. - # may not exist if we are cygwin. - if IS_PYPY: - py_executable_dll_s = [("libpypy-c.dll", "libpypy_d-c.dll")] - else: - py_executable_dll_s = [ - ("python{}.dll".format(sys.version_info[0]), "python{}_d.dll".format(sys.version_info[0])), - ( - "python{}{}.dll".format(sys.version_info[0], sys.version_info[1]), - "python{}{}_d.dll".format(sys.version_info[0], sys.version_info[1]), - ), - ] - - for py_executable_dll, py_executable_dll_d in py_executable_dll_s: - python_dll = os.path.join(os.path.dirname(sys.executable), py_executable_dll) - python_dll_d = os.path.join(os.path.dirname(sys.executable), py_executable_dll_d) - python_dll_d_dest = os.path.join(os.path.dirname(py_executable), py_executable_dll_d) - if os.path.exists(python_dll): - logger.info("Also created %s", py_executable_dll) - shutil.copyfile(python_dll, os.path.join(os.path.dirname(py_executable), py_executable_dll)) - if os.path.exists(python_dll_d): - logger.info("Also created %s", py_executable_dll_d) - shutil.copyfile(python_dll_d, python_dll_d_dest) - elif os.path.exists(python_dll_d_dest): - logger.info("Removed %s as the source does not exist", python_dll_d_dest) - os.unlink(python_dll_d_dest) - if IS_PYPY: - # make a symlink python --> pypy-c - python_executable = os.path.join(os.path.dirname(py_executable), "python") - if IS_WIN or IS_CYGWIN: - python_executable += ".exe" - logger.info("Also created executable %s", python_executable) - copyfile(py_executable, python_executable, symlink) - - if IS_WIN: - for name in ["libexpat.dll", "libeay32.dll", "ssleay32.dll", "sqlite3.dll", "tcl85.dll", "tk85.dll"]: - src = join(prefix, name) - if os.path.exists(src): - copyfile(src, join(bin_dir, name), symlink) - - for d in sys.path: - if d.endswith("lib_pypy"): - break - else: - logger.fatal("Could not find lib_pypy in sys.path") - raise SystemExit(3) - logger.info("Copying lib_pypy") - copyfile(d, os.path.join(home_dir, "lib_pypy"), symlink) - - if os.path.splitext(os.path.basename(py_executable))[0] != EXPECTED_EXE: - secondary_exe = os.path.join(os.path.dirname(py_executable), EXPECTED_EXE) - py_executable_ext = os.path.splitext(py_executable)[1] - if py_executable_ext.lower() == ".exe": - # python2.4 gives an extension of '.4' :P - secondary_exe += py_executable_ext - if os.path.exists(secondary_exe): - logger.warn( - "Not overwriting existing {} script {} (you must use {})".format( - EXPECTED_EXE, secondary_exe, py_executable - ) - ) - else: - logger.notify("Also creating executable in %s", secondary_exe) - shutil.copyfile(sys.executable, secondary_exe) - make_exe(secondary_exe) - - if ".framework" in prefix: - original_python = None - if "Python.framework" in prefix or "Python3.framework" in prefix: - logger.debug("MacOSX Python framework detected") - # Make sure we use the embedded interpreter inside - # the framework, even if sys.executable points to - # the stub executable in ${sys.prefix}/bin - # See http://groups.google.com/group/python-virtualenv/ - # browse_thread/thread/17cab2f85da75951 - original_python = os.path.join(prefix, "Resources/Python.app/Contents/MacOS/Python") - if "EPD" in prefix: - logger.debug("EPD framework detected") - original_python = os.path.join(prefix, "bin/python") - shutil.copy(original_python, py_executable) - - # Copy the framework's dylib into the virtual - # environment - virtual_lib = os.path.join(home_dir, ".Python") - - if os.path.exists(virtual_lib): - os.unlink(virtual_lib) - lib_name = "Python3" if "Python3.framework" in prefix else "Python" - copyfile(os.path.join(prefix, lib_name), virtual_lib, symlink) - - # And then change the install_name of the copied python executable - search = ( - "@executable_path/../../../../Python3" if "Python3.framework" in prefix else os.path.join(prefix, lib_name) - ) - # noinspection PyBroadException - try: - mach_o_change(py_executable, search, "@executable_path/../.Python") - except Exception: - e = sys.exc_info()[1] - logger.warn("Could not call mach_o_change: %s. " "Trying to call install_name_tool instead.", e) - try: - call_subprocess(["install_name_tool", "-change", search, "@executable_path/../.Python", py_executable]) - except Exception: - logger.fatal("Could not call install_name_tool -- you must " "have Apple's development tools installed") - raise - - if not IS_WIN: - # Ensure that 'python', 'pythonX' and 'pythonX.Y' all exist - py_exe_version_major = "python{}".format(sys.version_info[0]) - py_exe_version_major_minor = "python{}.{}".format(sys.version_info[0], sys.version_info[1]) - py_exe_no_version = "python" - required_symlinks = [py_exe_no_version, py_exe_version_major, py_exe_version_major_minor] - - py_executable_base = os.path.basename(py_executable) - - if py_executable_base in required_symlinks: - # Don't try to symlink to yourself. - required_symlinks.remove(py_executable_base) - - for pth in required_symlinks: - full_pth = join(bin_dir, pth) - if os.path.exists(full_pth): - os.unlink(full_pth) - if symlink: - os.symlink(py_executable_base, full_pth) - else: - copyfile(py_executable, full_pth, symlink) - - cmd = [ - py_executable, - "-c", - "import sys;out=sys.stdout;" 'getattr(out, "buffer", out).write(sys.prefix.encode("utf-8"))', - ] - logger.info('Testing executable with %s %s "%s"', *cmd) - try: - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) - proc_stdout, proc_stderr = proc.communicate() - except OSError: - e = sys.exc_info()[1] - if e.errno == errno.EACCES: - logger.fatal("ERROR: The executable {} could not be run: {}".format(py_executable, e)) - sys.exit(100) - else: - raise e - - proc_stdout = proc_stdout.strip().decode("utf-8") - # normalize paths using realpath to ensure that a virtualenv correctly identifies itself even - # when addressed over a symlink - proc_stdout = os.path.normcase(os.path.realpath(proc_stdout)) - norm_home_dir = os.path.normcase(os.path.realpath(home_dir)) - if hasattr(norm_home_dir, "decode"): - norm_home_dir = norm_home_dir.decode(sys.getfilesystemencoding()) - if proc_stdout != norm_home_dir: - logger.fatal("ERROR: The executable %s is not functioning", py_executable) - logger.fatal("ERROR: It thinks sys.prefix is {!r} (should be {!r})".format(proc_stdout, norm_home_dir)) - logger.fatal("ERROR: virtualenv is not compatible with this system or executable") - if IS_WIN: - logger.fatal( - "Note: some Windows users have reported this error when they " - 'installed Python for "Only this user" or have multiple ' - "versions of Python installed. Copying the appropriate " - "PythonXX.dll to the virtualenv Scripts/ directory may fix " - "this problem." - ) - sys.exit(100) - else: - logger.info("Got sys.prefix result: %r", proc_stdout) - - pydistutils = os.path.expanduser("~/.pydistutils.cfg") - if os.path.exists(pydistutils): - logger.notify("Please make sure you remove any previous custom paths from " "your %s file.", pydistutils) - # FIXME: really this should be calculated earlier - - fix_local_scheme(home_dir, symlink) - - if site_packages: - if os.path.exists(site_packages_filename): - logger.info("Deleting %s", site_packages_filename) - os.unlink(site_packages_filename) - - return py_executable - - -def install_activate(home_dir, bin_dir, prompt=None): - if IS_WIN: - files = {"activate.bat": ACTIVATE_BAT, "deactivate.bat": DEACTIVATE_BAT, "activate.ps1": ACTIVATE_PS} - - # MSYS needs paths of the form /c/path/to/file - drive, tail = os.path.splitdrive(home_dir.replace(os.sep, "/")) - home_dir_msys = (drive and "/{}{}" or "{}{}").format(drive[:1], tail) - - # Run-time conditional enables (basic) Cygwin compatibility - home_dir_sh = """$(if [ "$OSTYPE" "==" "cygwin" ]; then cygpath -u '{}'; else echo '{}'; fi;)""".format( - home_dir, home_dir_msys - ) - files["activate"] = ACTIVATE_SH.replace("__VIRTUAL_ENV__", home_dir_sh) - - else: - files = { - "activate": ACTIVATE_SH, - "activate.fish": ACTIVATE_FISH, - "activate.csh": ACTIVATE_CSH, - "activate.ps1": ACTIVATE_PS, - } - files["activate_this.py"] = ACTIVATE_THIS - - if sys.version_info >= (3, 4): - # Add xonsh support - files["activate.xsh"] = ACTIVATE_XSH - - install_files(home_dir, bin_dir, prompt, files) - - -def install_files(home_dir, bin_dir, prompt, files): - if hasattr(home_dir, "decode"): - home_dir = home_dir.decode(sys.getfilesystemencoding()) - virtualenv_name = os.path.basename(home_dir) - for name, content in files.items(): - content = content.replace("__VIRTUAL_PROMPT__", prompt or "") - content = content.replace("__VIRTUAL_WINPROMPT__", prompt or "({}) ".format(virtualenv_name)) - content = content.replace("__VIRTUAL_ENV__", home_dir) - content = content.replace("__VIRTUAL_NAME__", virtualenv_name) - content = content.replace("__BIN_NAME__", os.path.basename(bin_dir)) - content = content.replace("__PATH_SEP__", os.pathsep) - writefile(os.path.join(bin_dir, name), content) - - -def install_python_config(home_dir, bin_dir, prompt=None): - if IS_WIN: - files = {} - else: - files = {"python-config": PYTHON_CONFIG} - install_files(home_dir, bin_dir, prompt, files) - for name, _ in files.items(): - make_exe(os.path.join(bin_dir, name)) - - -def install_distutils(home_dir): - distutils_path = change_prefix(distutils.__path__[0], home_dir) - mkdir(distutils_path) - # FIXME: maybe this prefix setting should only be put in place if - # there's a local distutils.cfg with a prefix setting? - # FIXME: this is breaking things, removing for now: - # distutils_cfg = DISTUTILS_CFG + "\n[install]\nprefix=%s\n" home_dir - writefile(os.path.join(distutils_path, "__init__.py"), DISTUTILS_INIT) - writefile(os.path.join(distutils_path, "distutils.cfg"), DISTUTILS_CFG, overwrite=False) - - -def fix_local_scheme(home_dir, symlink=True): - """ - Platforms that use the "posix_local" install scheme (like Ubuntu with - Python 2.7) need to be given an additional "local" location, sigh. - """ - try: - import sysconfig - except ImportError: - pass - else: - # noinspection PyProtectedMember - if sysconfig._get_default_scheme() == "posix_local": - local_path = os.path.join(home_dir, "local") - if not os.path.exists(local_path): - os.mkdir(local_path) - for subdir_name in os.listdir(home_dir): - if subdir_name == "local": - continue - copyfile( - os.path.abspath(os.path.join(home_dir, subdir_name)), - os.path.join(local_path, subdir_name), - symlink, - ) - - -def fix_lib64(lib_dir, symlink=True): - """ - Some platforms (particularly Gentoo on x64) put things in lib64/pythonX.Y - instead of lib/pythonX.Y. If this is such a platform we'll just create a - symlink so lib64 points to lib - """ - # PyPy's library path scheme is not affected by this. - # Return early or we will die on the following assert. - if IS_PYPY: - logger.debug("PyPy detected, skipping lib64 symlinking") - return - # Check we have a lib64 library path - if not [p for p in distutils.sysconfig.get_config_vars().values() if isinstance(p, basestring) and "lib64" in p]: - return - - logger.debug("This system uses lib64; symlinking lib64 to lib") - - assert os.path.basename(lib_dir) == PY_VERSION, "Unexpected python lib dir: {!r}".format(lib_dir) - lib_parent = os.path.dirname(lib_dir) - top_level = os.path.dirname(lib_parent) - lib_dir = os.path.join(top_level, "lib") - lib64_link = os.path.join(top_level, "lib64") - assert os.path.basename(lib_parent) == "lib", "Unexpected parent dir: {!r}".format(lib_parent) - if os.path.lexists(lib64_link): - return - if symlink: - os.symlink("lib", lib64_link) - else: - copyfile(lib_dir, lib64_link, symlink=False) - - -def resolve_interpreter(exe): - """ - If the executable given isn't an absolute path, search $PATH for the interpreter - """ - # If the "executable" is a version number, get the installed executable for - # that version - orig_exe = exe - python_versions = get_installed_pythons() - if exe in python_versions: - exe = python_versions[exe] - - if os.path.abspath(exe) != exe: - exe = distutils.spawn.find_executable(exe) or exe - if not os.path.exists(exe): - logger.fatal("The path {} (from --python={}) does not exist".format(exe, orig_exe)) - raise SystemExit(3) - if not is_executable(exe): - logger.fatal("The path {} (from --python={}) is not an executable file".format(exe, orig_exe)) - raise SystemExit(3) - return exe - - -def is_executable(exe): - """Checks a file is executable""" - return os.path.isfile(exe) and os.access(exe, os.X_OK) - - -# Relocating the environment: -def make_environment_relocatable(home_dir): - """ - Makes the already-existing environment use relative paths, and takes out - the #!-based environment selection in scripts. - """ - home_dir, lib_dir, inc_dir, bin_dir = path_locations(home_dir) - activate_this = os.path.join(bin_dir, "activate_this.py") - if not os.path.exists(activate_this): - logger.fatal( - "The environment doesn't have a file %s -- please re-run virtualenv " "on this environment to update it", - activate_this, - ) - fixup_scripts(home_dir, bin_dir) - fixup_pth_and_egg_link(home_dir) - # FIXME: need to fix up distutils.cfg - - -OK_ABS_SCRIPTS = [ - "python", - PY_VERSION, - "activate", - "activate.bat", - "activate_this.py", - "activate.fish", - "activate.csh", - "activate.xsh", -] - - -def fixup_scripts(_, bin_dir): - if IS_WIN: - new_shebang_args = ("{} /c".format(os.path.normcase(os.environ.get("COMSPEC", "cmd.exe"))), "", ".exe") - else: - new_shebang_args = ("/usr/bin/env", VERSION, "") - - # This is what we expect at the top of scripts: - shebang = "#!{}".format( - os.path.normcase(os.path.join(os.path.abspath(bin_dir), "python{}".format(new_shebang_args[2]))) - ) - # This is what we'll put: - new_shebang = "#!{} python{}{}".format(*new_shebang_args) - - for filename in os.listdir(bin_dir): - filename = os.path.join(bin_dir, filename) - if not os.path.isfile(filename): - # ignore child directories, e.g. .svn ones. - continue - with open(filename, "rb") as f: - try: - lines = f.read().decode("utf-8").splitlines() - except UnicodeDecodeError: - # This is probably a binary program instead - # of a script, so just ignore it. - continue - if not lines: - logger.warn("Script %s is an empty file", filename) - continue - - old_shebang = lines[0].strip() - old_shebang = old_shebang[0:2] + os.path.normcase(old_shebang[2:]) - - if not old_shebang.startswith(shebang): - if os.path.basename(filename) in OK_ABS_SCRIPTS: - logger.debug("Cannot make script %s relative", filename) - elif lines[0].strip() == new_shebang: - logger.info("Script %s has already been made relative", filename) - else: - logger.warn( - "Script %s cannot be made relative (it's not a normal script that starts with %s)", - filename, - shebang, - ) - continue - logger.notify("Making script %s relative", filename) - script = relative_script([new_shebang] + lines[1:]) - with open(filename, "wb") as f: - f.write("\n".join(script).encode("utf-8")) - - -def relative_script(lines): - """Return a script that'll work in a relocatable environment.""" - activate = ( - "import os; " - "activate_this=os.path.join(os.path.dirname(os.path.realpath(__file__)), 'activate_this.py'); " - "exec(compile(open(activate_this).read(), activate_this, 'exec'), { '__file__': activate_this}); " - "del os, activate_this" - ) - # Find the last future statement in the script. If we insert the activation - # line before a future statement, Python will raise a SyntaxError. - activate_at = None - for idx, line in reversed(list(enumerate(lines))): - if line.split()[:3] == ["from", "__future__", "import"]: - activate_at = idx + 1 - break - if activate_at is None: - # Activate after the shebang. - activate_at = 1 - return lines[:activate_at] + ["", activate, ""] + lines[activate_at:] - - -def fixup_pth_and_egg_link(home_dir, sys_path=None): - """Makes .pth and .egg-link files use relative paths""" - home_dir = os.path.normcase(os.path.abspath(home_dir)) - if sys_path is None: - sys_path = sys.path - for a_path in sys_path: - if not a_path: - a_path = "." - if not os.path.isdir(a_path): - continue - a_path = os.path.normcase(os.path.abspath(a_path)) - if not a_path.startswith(home_dir): - logger.debug("Skipping system (non-environment) directory %s", a_path) - continue - for filename in os.listdir(a_path): - filename = os.path.join(a_path, filename) - if filename.endswith(".pth"): - if not os.access(filename, os.W_OK): - logger.warn("Cannot write .pth file %s, skipping", filename) - else: - fixup_pth_file(filename) - if filename.endswith(".egg-link"): - if not os.access(filename, os.W_OK): - logger.warn("Cannot write .egg-link file %s, skipping", filename) - else: - fixup_egg_link(filename) - - -def fixup_pth_file(filename): - lines = [] - with open(filename) as f: - prev_lines = f.readlines() - for line in prev_lines: - line = line.strip() - if not line or line.startswith("#") or line.startswith("import ") or os.path.abspath(line) != line: - lines.append(line) - else: - new_value = make_relative_path(filename, line) - if line != new_value: - logger.debug("Rewriting path {} as {} (in {})".format(line, new_value, filename)) - lines.append(new_value) - if lines == prev_lines: - logger.info("No changes to .pth file %s", filename) - return - logger.notify("Making paths in .pth file %s relative", filename) - with open(filename, "w") as f: - f.write("\n".join(lines) + "\n") - - -def fixup_egg_link(filename): - with open(filename) as f: - link = f.readline().strip() - if os.path.abspath(link) != link: - logger.debug("Link in %s already relative", filename) - return - new_link = make_relative_path(filename, link) - logger.notify("Rewriting link {} in {} as {}".format(link, filename, new_link)) - with open(filename, "w") as f: - f.write(new_link) - - -def make_relative_path(source, dest, dest_is_directory=True): - """ - Make a filename relative, where the filename is dest, and it is - being referred to from the filename source. - - >>> make_relative_path('/usr/share/something/a-file.pth', - ... '/usr/share/another-place/src/Directory') - '../another-place/src/Directory' - >>> make_relative_path('/usr/share/something/a-file.pth', - ... '/home/user/src/Directory') - '../../../home/user/src/Directory' - >>> make_relative_path('/usr/share/a-file.pth', '/usr/share/') - './' - """ - source = os.path.dirname(source) - if not dest_is_directory: - dest_filename = os.path.basename(dest) - dest = os.path.dirname(dest) - else: - dest_filename = None - dest = os.path.normpath(os.path.abspath(dest)) - source = os.path.normpath(os.path.abspath(source)) - dest_parts = dest.strip(os.path.sep).split(os.path.sep) - source_parts = source.strip(os.path.sep).split(os.path.sep) - while dest_parts and source_parts and dest_parts[0] == source_parts[0]: - dest_parts.pop(0) - source_parts.pop(0) - full_parts = [".."] * len(source_parts) + dest_parts - if not dest_is_directory and dest_filename is not None: - full_parts.append(dest_filename) - if not full_parts: - # Special case for the current directory (otherwise it'd be '') - return "./" - return os.path.sep.join(full_parts) - - -FILE_PATH = __file__ if os.path.isabs(__file__) else os.path.join(os.getcwd(), __file__) - - -# Bootstrap script creation: -def create_bootstrap_script(extra_text, python_version=""): - """ - Creates a bootstrap script, which is like this script but with - extend_parser, adjust_options, and after_install hooks. - - This returns a string that (written to disk of course) can be used - as a bootstrap script with your own customizations. The script - will be the standard virtualenv.py script, with your extra text - added (your extra text should be Python code). - - If you include these functions, they will be called: - - ``extend_parser(optparse_parser)``: - You can add or remove options from the parser here. - - ``adjust_options(options, args)``: - You can change options here, or change the args (if you accept - different kinds of arguments, be sure you modify ``args`` so it is - only ``[DEST_DIR]``). - - ``after_install(options, home_dir)``: - - After everything is installed, this function is called. This - is probably the function you are most likely to use. An - example would be:: - - def after_install(options, home_dir): - subprocess.call([join(home_dir, 'bin', 'easy_install'), - 'MyPackage']) - subprocess.call([join(home_dir, 'bin', 'my-package-script'), - 'setup', home_dir]) - - This example immediately installs a package, and runs a setup - script from that package. - - If you provide something like ``python_version='2.5'`` then the - script will start with ``#!/usr/bin/env python2.5`` instead of - ``#!/usr/bin/env python``. You can use this when the script must - be run with a particular Python version. - """ - filename = FILE_PATH - if filename.endswith(".pyc"): - filename = filename[:-1] - with codecs.open(filename, "r", encoding="utf-8") as f: - content = f.read() - py_exe = "python{}".format(python_version) - content = "#!/usr/bin/env {}\n# WARNING: This file is generated\n{}".format(py_exe, content) - # we build the string as two, to avoid replacing here, but yes further done - return content.replace("# EXTEND - " "bootstrap here", extra_text) - - -# EXTEND - bootstrap here - - -def convert(s): - b = base64.b64decode(s.encode("ascii")) - return zlib.decompress(b).decode("utf-8") - - -# file site.py -SITE_PY = convert( - """ -eJy1Pf1z2zaWv/OvwNKTseTKdOK0va5T98ZJnNZzbpKN09ncpj4tJUES1xTJEqRlbSb7t9/7AECA -pGRn29V0XIkEHh4e3jcekDAMz4pCZjOxymd1KoWScTldiiKulkrM81JUy6ScHRZxWW3g6fQmXkgl -qlyojYqwVRQEB7/zExyI98tEGRTgW1xX+SqukmmcphuRrIq8rORMzOoyyRYiyZIqidPkn9AizyJx -8PsxCC4yATNPE1mKW1kqgKtEPhdvN9Uyz8SgLnDOT6Jv4qfDkVDTMikqaFBqnIEiy7gKMilngCa0 -rBWQMqnkoSrkNJknU9twndfpTBRpPJXi73/nqVHT/f1A5Su5XspSigyQAZgSYBWIB3xNSjHNZzIS -4rmcxjgAP2+IFTC0Ea6ZQjJmuUjzbAFzyuRUKhWXGzGY1BUBIpTFLAecEsCgStI0WOfljRrCktJ6 -rOGRiJk9/Mkwe8A8cfwu5wCOb7Lglyy5GzFs4B4EVy2ZbUo5T+5EjGDhp7yT07F+NkjmYpbM50CD -rBpik4ARUCJNJkcFLcf3eoV+OCKsLFfGMIZElLkxv6QeUXBRiThVwLZ1gTRShPlLOUniDKiR3cJw -ABFIGvSNM0tUZceh2YkcAJS4jhVIyUqJwSpOMmDWn+Mpof3XJJvlazUkCsBqKfGPWlXu/Ac9BIDW -DgFGAS6WWc06S5MbmW6GgMB7wL6Uqk4rFIhZUspplZeJVAQAUNsIeQdIj0RcSk1C5kwjtyOiP9Ek -yXBhUcBQ4PElkmSeLOqSJEzME+Bc4IpXb96Jl+fPL85eax4zwFhmFyvAGaDQQjs4wQDiqFblUZqD -QEfBJf5PxLMZCtkCxwe8mgZH9650MIC5F1G7j7PgQHa9uHoYmGMFyoTGCqjfJ+gyUkugz+d71jsI -zrZRhSbO39bLHGQyi1dSLGPmL+SM4HsN54eoqJbPgBsUwqmAVAoXBxFMEB6QxKXZIM+kKIDF0iST -wwAoNKG2/ioCK7zOs0Na6xYnAIQyyOCl82xII2YSJtqF9Qz1hWm8oZnpJoFd51VekuIA/s+mpIvS -OLshHBUxFH+byEWSZYgQ8kKwv7dPA6ubBDhxFolLakV6wTQS+6y9uCWKRA28hEwHPCnv4lWRyhGL -L+rW3WqEBpOVMGudMsdBy4rUK61aM9Ve3juOPrS4jtCslqUE4PXEE7p5no/EBHQ2YVPEKxavap0T -5wQ98kSdkCeoJfTF70DRM6XqlbQvkVdAsxBDBfM8TfM1kOwkCITYw0bGKPvMCW/hHfwFuPg3ldV0 -GQTOSBawBoXIbwOFQMAkyExztUbC4zbNym0lk2SsKfJyJksa6mHEPmLEH9gY5xq8zitt1Hi6uMr5 -KqlQJU20yUzY4mX7FevHZzxvmAZYbkU0M00bOq1wemmxjCfSuCQTOUdJ0Iv0zC47jBn0jEm2uBIr -tjLwDsgiE7Yg/YoFlc68kuQEAAwWvjhLijqlRgoZTMQw0Kog+KsYTXqunSVgbzbLASokNt9TsD+A -2z9BjNbLBOgzBQigYVBLwfJNkqpEB6HRR4Fv9E1/Hh849WKubRMPOY+TVFv5OAsu6OF5WZL4TmWB -vUaaGApmmFXo2i0yoCOKeRiGgXZgRK7MN2CkIKjKzQnwgjADjceTOkHLNx6jrdc/VMDDCGdkr5tt -Z+GBijCdXgOZnC7zMl/hazu5K9AmMBb2CPbEW1Izkj1kjxWfIf1cnV6Ypmi8HX4WqIiCt+/OX118 -OL8Sp+Jjo9NGbYV2DWOeZzHwNZkE4KrWsI0yg5ao+RJUfuIV2HfiCjBo1JvkV8ZVDcwLqL8va3oN -05h6L4Pz12fPL8/Hv1ydvxtfXbw/BwTB0Mhgj6aM9rEGj1FFIB3AljMVaQMbdHrQg+dnV/ZBME7U -+Nuvgd/gyWAhK+DicgAzHolwFd8p4NBwRE2HiGOnAZjwcDgUP4hjcXAgnh4TvGJTbAAcWF6nMT4c -a6M+TrJ5Hg6DIJjJOUjLjUSZGhyQKzvkVQciAoxcm9Z/5Elm3ve8jieKIMBTfl1KoFyGrUa2EXD3 -ahorya14bOg4HqOMj8cDPTAwPzEYOCgstvvCNEEZLxPwA2mhUOYnKk/xJw6AUkP8iqEIahVkHB1q -RLdxWktlxqBmgL+hJ5io0Axi6G0bghM5R0HFp013/KDZSLJa2oeryKLaJc7cTLqUq/xWzsB8Iz2d -eYt39AZiuyIF5QrzAs1AFoVl0HgeMUYyrF1g8dD6ALuuCIqhiCHGHoeTMlPAyRyaEW/ruJGVaVHm -twmaq8lGvwRtC9KGOteYRg0tR7/eIzsqVWAw8KMyJNVa7oM8lTW7PIQ3gkSFM2skMyJwlyjq1/T1 -JsvX2ZhjqVOU2sHQLibyml5ObNCswZ54BWoMkMwhNGiIxlDAaRTIboeAPEwfpguUJe8UAIGpUOTw -O7BMsEBT5LgDh0UYw2eC+LmUaHFuzRDkrBtiOJDobWQfkBTAH4QEk7PyZqVFcxmaRdMMBnZI4rPd -ZcRBjA+gRcUI9O5AQ+NGhn4fT64Bi0tXTp1+Aer0Dx8+MN+oJYXoiNkEZ40GaU7qNio2oJoT8HyN -UeeAn/gAAvcMwNRK86Y4vBJ5wQYdFpQzCWA1r8B9XFZVcXJ0tF6vIx2g5uXiSM2Pvvnu22+/e8xq -YjYjBoL5OOKiszXREb1Dpyj63gShP5ilazFkkvnsSLAGkgw7eTOI3491MsvFyeHQqhRk40bR419j -DEGFjM2gAdMZqBs2KH36fPjpM/wNI2wSVwO3xwCCswNcGFcz83IBQ/gaHPpVOdgVsILTvEbF37CF -El/BoBDwzeSkXoQWD09/mx8wbRTagWWIwyfXmMnx2cQwmTJqa4w6g5gEkXTW4R0zUUzGVusLpDWq -8E40tunXaYbSs4dLv3VdHBEyU0wUsgrKh9/kweJoG3flCD/aU3q/KVxPyXw8u2BMobF4s5l2VAYo -RoQMrsbIFUKHx9GDAtlas6YGfeNqStDX4HRMmNwaHPnf+whyX5C/SdEjL61uAYRqpaJMwGmWAVq4 -43SsX5sX7AswMhJ9mSf0RILLddJ595jXtk5TyhC0uNSjCgP2Vhrtdg6cOTAAQDTKkBsar/dNa1F4 -DXpg5ZxTQAabd5gJ30RMJSTSINwLe9ip4wRs680k7gOBazTg3MaDoBPKpzxCqUCaioHfcxuLW9p2 -B9tpf4inzCqRSKstwtXWHr1CtdNMzZMMFbGzSNE0zcFttGqR+Kh577sO5Fbj417TpiVQ06Ghh9Pq -lLw/TwD3dTvMxyxqjFzdwB5RWiWKbB3SaQl/wMuggJmyG0BMgmbBPFTK/Jn9ATJn56u/bOEPS2nk -CLfpNq+kYzM0HHSGkIA6sAcByIB4XTkkH5IVQQrM5SyNJ9fwWm4VbIIRKRAxx3iQggGs6WXTDacG -TyJMppNwIuS7SslCfAWhEpijFms/TGv/sQxq4tmB04KiYR0In7pBshMgn7YCZp+X/VCZ8u5FDsw7 -Ad/HTRq7HG741cbv4Lb7OtsiBcqYQvpw6KJ6bSjjJib/dOq0aKhlBjG85A3kbQ+YkYaBXW8NGlbc -gPEWvT2Wfkzz1B4Z9h2EuTqWq7sQTUuiprnq09qaEbrUsPhdJhME4ZE8HF57kGSaoGvFUfu/M8j9 -0L3pnYKfOIvLdZKFpK00xU79xejgYYnnmbSjKwqljmCimC87elWCTNDG2RFQjKS/KCCCV9rl78Jt -z7G3AX68yYd2RIaLVPad7K5T3SXV6GGDWUqf31VlrHCslBeWRWUboOvubEk3I1nibKN3zfSuKgYX -Za4g+BRvrj4IpCEnFNfx5l6i9aPrIfnl1GmhT5wEA6GORB46Cnczay/S/xFE+6m/cyhH0X1Z/wfj -CAMdzjZZmsezvhGuO0+gw7dfj3uybi7u3379ewjVJ9Qtp85iMfRcvlLGKTkIznv0DUBX7l5o27EY -sn6mUE6zSZcqXZbSaNo0aX8L/BiomH2V4AQ8HjU07U4dP76ntBWetkM7gHUiUfPZI90LgXs++QdE -v0qnz27jJKUUNBDj8BBVqYncOTHRL/AepJ06wSFBX2ilPj6+Bu7gVMGwOx3tbZ2ZZGtPhGs+Ray6 -qOzp/eZmu8TbQ3a3yrbrEEP2LxFuszf2RULi4dYjJL1i0wYEFEWteNxPpQfNace8/uAZ9c9quzT8 -sehb3Hto+O9j38ZxJyo8hFb/Pqx+KriGzQB7gIHZ4pTtcMm6Q/Mm09w4VqwglDhATXIg1rTRTClN -8MsygDJjl6sHDoqH3q58UZclbzqSQipkeYj7aCOBNTbGt6LSnS6Yo9eyQkxssymliJ2KjLxPYkKd -9LUzCRsvHfNU+v3T3gb9fLnMTfZIZrdJCcBBPw7Cn978fL6FbzCnCp0ervS3NsSPxwEIl11+JAry -wO9xTbeO678xW64mR6qx786vkxo14XU/Ke5JkXh7fLyPSYHrdCmnN2NJm7PIT9jXyRO/wNeIit2z -9UtsVDynOiGYyjStkTzsgmKB17zOprR/UEnwU3Q1JlZn0JYrZ8TmabwQA+o8w2SM5grK19zGpXbD -ijLH+j9RJ7OjRTIT8rc6TjHalfM54IK7O/pVxMNTTka85F1jrgtTclqXSbUBGsQq15tjtMHsNJxs -eKIDD0neBmEK4pbzibjCaeN7JtzMkMvEzP4uAE4SGIQ6ONvBET2H91k+xlHHSF7gPUJq2E6Y8OOg -PUKutxlg/nqE9htJr9wdOFpzl6iozjxSugF4Tj4MQhkMMQHAv+mnz4kuc23BcrEdy8VuLBdtLBe9 -WC58LBe7sXRlAhe2SeYYUehL6LQz/b0lDW4uhsc5j6dLbof1dVhHBxBFYWJJI1RcZuplfHgDjICQ -/nS2ZOlhU6KQcOFemXNaWINE9seNHR23mgJhpzMVPOjOPBXjBm4r0/D7HkURleNMqDsL3Cyu4sgT -jEWaT0BuLbqjBsBItCs2OImY3Y4nnPBsm4y3//v+pzevsTmCshUA1A0XETU8TmVwEJcL1RWnJooq -gB+ppV85Qd00wL3elNM+p5z2R2KfU077epg9/vMyx0It5Byxpk38XBRgrKlyxjZz60v291vPdSGK -fs5szhsw4H9kleN7bKHS2du3L8/en4VUihL+K3RFxhDXlw8XH9PCNui6Wm5zS3Ls01jTxv65c/KI -7bCE7vXp879jfn38/qNzBGICEpHOZ37ZFH9n9sQagU6Vk5sAYKfBvnMkwHEVHAHsy4q3B/C34dAn -H8NctCszMPNqgrqW7rVWbgdxHKB/VADV8aQsHj2+lEMc22y7J9XdCVCyen7+48Xry4vnb8/e/+T4 -UugTvbk6OhbnP38QVIiAhoCdixi33SuseQEF7R7KELMc/qsx8zCrK84rQq+Xl5d6J2CFZflYp4m6 -O4LnXDBjoXEShxOX9qGudEGMUh0SOOcfqC6EzkdghLDi2nuV61pOOlYxQa+v1sGGPtdizr/QnmkE -ogCNXVIwCC5mgldUcVuZOKjkLSZ9JqQHKW3rbNFBSkmqzk60s79icvleXo86w5Oms9aXH0MX1/A6 -UkWagAZ9Flpp0N2w9qLhG/3Qbp4yXn3qyOkOI+uGPOutWGBdyrOQ56b7DxtG+60GDBsGewnzziRV -HlCxKJZRiX1stM8VBvIOvtql12ugYMFwI6nCRTRMl8DsYwgnxTIBTxx4cglGDB1ugNBaCT/HfOLY -JJnjxn/4YjU7/EuoCeK3/vXXnuZVmR7+TRQQTgguUwl7iOk2fgkRRCQjcf7m1TBk5KZpDE7jX2os -ZQbDTgk4R9ipNoY3Z8cDJdO5Ll3w1QG+0OaWXget/qUsSt2/38kMUQQ+fR6Q9f302RCwSaeYAUY4 -n2EbPtZqW/zwzJO7z20+e+JqKdNUF+hevLw8B08My8dRjniv5xzG5DwB7tTqWi8+k9UChfu48LpE -Zi7RIaRt/FnkNetNnaLgUW9v59+uFqUnu706ucgyTpSL9gCnrQljCqAj5GhYErO6If7WUmrbIJ3d -NkR3FB3mjPHbkiomfdYAlqanMYcYEHtgdbpJBPNmZZJVpkIuTaagTkHzgl4dgawgdfEIFjFgnnEq -Ni+VObkBD4tNmSyWFWbioXNEVePY/OezD5cXr6mQ+vhp48T28OiIHOsRlymcYjEapg/gi1tahnw1 -Hrus23qFMFAJwf/ar7j+4ZQH6PTjjJq3FaBf8dGZUyey4hmAnqqLtpCgO+1065OeRhgYVxtX4set -Mmsw88FQEg4r9XVBgTu/Livali2LQn6IefkF+wjzwhY96c5O0VP7o6c4L3D3ZTbobwRv+2TLfCbQ -9abzZlt5lfvpSCEe4gOMuq39MUz9Uaepno7Da9uYhYJEbWl/dUMFp900Q0kGGg7czkOXx/o1sW7O -DOiV7XaAie81ukYQ+/U5oKj9DA8TS+xO6GA6YtWheKQGXKMh9WGFGjypR4r0RygeicHAEdzRUByI -Y2+Wjj24f5ZaeYGN/Ak0oa73pDr7vARWhC+/sQPJrwgxVKQnogkScXpZbkuR8LNeonf5xJ9lrxBQ -VhDlroyzhRwwrJGB+ZVP7i1JTVK3HrE/Jtd9pkVcgJN6t4XHu5LRv2VgUGuxQqfdjdy09ZFPHmzQ -e/pgJ8F88GW8BuVf1NWAV3LLfmjv8Z/tUO+HiAVu0FRvFQ9C9KB/66ul8QH3UkPDQk/vt559Evzw -2liP1lrI1tGSfftCu7LTEkK0Su0jks6BKuOUWj+gMbmnjdEP7dOQzyrZ39bX75b3NCB5aB8gP+M9 -hLcbPtGWUFl1c0aD3szkrUzBLoC1MnX0OA5V0PdmMXaN65G0QcJ/HIa/apc/zm4orHvx14uRePH6 -Hfx9Lt+AjcJzUCPxN0BLvMhLiN/4JB8dscaS/IoDs7xWeFiKoFFOnU+joz9kamJ4dpi/12cF/EMC -VgMJDNzRCcYrEADFBmemAZ1zbUyxqX+H3+a4TsvhM85YH3VC/dIZJTTnGFT3IEOh5ke6x5HT5WN4 -efHi/PXVeVTdIeOYn+G108YNQP3NJklqFx+VuIszEvbJtMYnGorjo/4k06LHRdVhnjkTgWGe2IcY -oLChHR+4j60jH5cYq4tiM8unEbYUAz7nKKo1+KxDJ6K716h6Fg1hDYZ6A6hxnPEx0EeE8Jya6ClQ -Qxo/nuD5H34chVuM3EhQEhb+d3Cznrk5XH2QgyYUtFFrpjnw+zdqZsmU1RAtDxFqp5bw9sRbmsRq -NZm6577eZEJfkQAahJLych7XaSVkBiELRdJ0Vh3UqHtUi8WEV5ptBZ1folxIuo43yqk0iZUIcVTa -VJW4e0CJNQh0f45vWNniGTJR89lIgE6IUnSSO11VPV2yGHPAoTVcZz97nWRPj8MOkXlQDkCnqqE2 -TBTdMkZpIStNAH4wGH580uygUyJ26pUhTgtdbQjfDg4OQvHf93sUjEGU5vkNuDoAsdcTuKTXW6yh -npNdpJ56P/MqAlacLuVHeHBNeWL7vM4o7bejKy2EtP83MHhNGjY0HVrGjlNiJe+HcgveVtF25Jcs -oRtQMHEjUdXqi2QwqWMkiXgRlMB+rKZJss/hP6zDJq/xbBUm8TSjyDvg9QTBjPAt7uNwBLtEv40q -Gi3bWHRgLgQ45NIhHo4ObNKJIkB0/Haj8RxfZEnVHAV47G7y6VPBlb3ZRDOUiNcoE2YiLWo4B/U8 -Jm1WE37vYk4vFsinH5+0yticefLr+5AHpgYhy+dzgyo8NMs0zWU5NeYU1yyZJpUDxrRDONyZbnSh -/HYU9KAUgjInGzCzwmzf/smujIvpG9rwPDQj6YKUyt6Sw2mXOGsVkEVRMz4leCwhLeeaL3Ru4DXl -jbUr4A0m/qS5HqvBvdP87qG0OtOn9LnSoDm6D3DoZhirHC1HeorCuY7Iwme+3XK0Hj8U/TJyt0lZ -1XE61ofBx+jCje0WsEbUHmbaeVDPeikjtILJ4lCXYqPrMGxO7WGxpSmuh/hfh/+Re0DIP0tT5OgA -HrOPBJ4EkmY2NodcTX7mo2VYe2CQq91Chy0Q1FfmCEqvy9tTNSd+EIOnIwhMW8fnig1eygIDPJph -KNtTKOZEyQav9vl28cOpGDwZiW92QI+2DHBy7I7QDu9aELYCeXrt8AUf/ugcm3AXjbcXiGx4fOL+ -pqaGbaRRGl63qd2lyvdElD+3iMJnsXTZ6JMvGztcgdDOEzk7fKSQEBqXRi9uZy0aFs8j6zI3MlZ8 -FFfsc0ncPp/inUAksG6UkKOOjIdqRzFnfLXq4AOQKOdst+wZblNU0aows/c+YfWZxq9FLAAw7tsw -4mteQnyvaeH6RVv3EXXttQXbEx/rs3K8LdQ0DHwR2OWAPZQDcZ/rfubDLUNT1ugLyBdw+sO3rR3G -/uOZiQu7t20AdrD+khp8zzfoLbXvWe/d5e6epNoa94YbzKc/K+XxUV/X3yVPDpRGssCVNHfLkRN4 -gkWruClMd3jo4tUV3t5G27NorFLpujQc2vI1PehjUDRPlUmwNODl1HPcTb2ly+jOFFVQvT1/K74+ -fjIyJy8ZkJ7A0+jpV/rONupmTpf7HvBIh1TwUvc7jv7LAZZUfm/7qhNt0LROW3e3+INx5mgYjS0d -GvNzfzF+s2axwEvOGmKy42uvW3gFfsgEPBBzW2EOvhTEiVEfuq5CaYR7m+b7nfLtK03EYJfkbFOU -u5TkF/O17tVvLTBMRzZexvwGPa66OLG5LGM/cBNv4Nb5EYPc9B7zYduBlrW6CXdNn/vvmrtuEdiZ -m+35ztz943kuDXTf7RazjwacQtCEeFCZe6POreHw5uXRyNX4I10i0i2IdlvhPLdYjE4/U3KCiWri -Ro2sYc5VPHW/Y03SId9+2lMq5JyecxmiNd2O4m9z+kO8AS7jwOf2GDbV5sw05+l4hO61GnPZ5Fgu -Fmoc46VqYwpjqTKpE52YuOgVXYklY7Ux0Qxe9AIgDBvpYky34Bo4AjQeX+7KlT3OJR6ChqY6Uqcw -VSUzVuw6dAJwEWcgqb/JgXNlaZhKTCWruixKiDdDfQUol5z0Vbs2QE12bBWrG4O66THSN0KSeqQ6 -J3MAk/OerXoVIAKb4o6CH4/tO+Cax82R9GRkuUFm9UqWcdVcxeLvaicQIDUj0GldXF0n9dWIY4tN -XMQSyxwOUpiFst+/gsDNj1p3nEEfWj33cH6iHjql+iXH2x2bMnSCfiymuyfo9yuPvzDo9+B/SdDP -JfxlincdpHh/loOczvCYNBRG53QiwL/wEP7rlo0AYSYbTcMFlt1QZifWa+q0X+b5zXjMBv1ibrwk -HkfrN11abS6SHlEj7MfZG6Vzhu/jCe63MesroSeEGOIuHPKiQUX860hveIxBVKlIHDqYr3yBI0Ih -A6xHz0HGymQ2wzvDWIV4eRbcUfbWQCPPVok2CGD2XLV99f7s3ftf3joDeVsPBsuxocOgm/oHCbxL -qkZjd28W4nYGRO+7Zs3bvlsv0zQC0Yy7J97BCKYiyL/aG4+RIJ8MuSRzFW/MrVwyy+vFkmsBYC4O -NH/RjMHGVnih7hyMYGXKiTKaHFcSWEIyjjzn8YyyoEbJmcek6eAV3ZsX+n6L11Vf+4lbuzQmejq4 -w0i7Cm7LNon4De50KTmGnuNJAqYtxL/i/y6A7mND+HB38cQWQODunAgHxHYusCDwCxdTIa2cDLBe -8DdXvV76WWbe8I4kH5wj/2mFFwLQFamKoyq+TFxVgu+5Fu0d8T0+YcE1ryW6cFkuItqRL6fMIgMY -J0ISmwda0igE1pVlDTi8q/vNlfgAtkCvzNB0wKQ+XYltThDifacZjz6l6vx1G7l1zC4A32mqoeiL -T/1wg5VnD9dgpn085YOKRrGMwXwvwOwN0Zl83KbvBZYQWcWzxqvP83iGKs7oXMCopaqiFgysYl7U -eIySrrQzN8VjqHqbJzMun0YJNcPgFZ2gQFpgaIfNvUaRSgjXRs5IHyOCeJtbPNtYGyBnLUDVsiTp -bms7VNgjLP5pE0GXAUzqhXK3oROlavnNd19/s7dSiyd//u7bp996Hc10tlwZ5xxsCf+F+aGwRcdW -LVrvlpsvP2ZJSYT0j949uF5p6rIOflDhr0t0PTTA9oGtrbh5+HkgdiDoIDl4Ba1e59UrdBIJ35F4 -K0u6FyrP6MGWspe91jks/mcIUFdSEEDWjn8jWuhCbAOjCxB6lx6W/M9Pejt2qcjmMDKWc+CRwwk1 -ejwPrDVqG1xwjbwNAWua2XLaehvK5LgLBnacqlyMF3fCm+O2TsEcbdCXVOIFlaU8tP9Chr5z2oKj -zuBPLcp4pbRS44iVDyf4N8aiI0aOdl1ENnfAkJvIDm8wRBT3lbkTM3Kxb/sZPTvFeDLaaka9xQ5I -GJowMuPW9VUuSNvXh/nPpND3zX0xSNvV3g8MikAvi46Wes/X3bPhZXICPTtDvXzAnqd7P3DA7Apx -whibbRRxm3+XrhnFPdtW5Cq5C+3l+ByAOoeb8MRW90JM6vozM4OTsPCuIqWnP16+eX52SXQYvz17 -8T9nP1LpNUpsK5x/8CZflh8ypQ+901ruhp+ul+0bvEG25+ZqPvmrIXTedwoNeyD0n/LuW5x2pOe+ -3tahc1i22wkQ3z3tFtRtMW0v5E7KSl8F5tbItg5lBfopn6Axv5zqQ/PIVAq5Y/XcIvLUOZnSjSdZ -LpoqIgO8qf/QAWtnb3zbqjvDdWNrnQfUN1Nv2bgf2uNYtHwY5SFz2qoPc0TVVhZw/qf9jxeR94UH -/M1dbiCXU+lcLkz3CjOoyv9XkkqI/NGf0v8e0cj+iwPUjitclP2nNLBUbaqDnM4J++783IrVmUy3 -UAHUJatKc18uo2JUp64HsxWUj5T4eEjXbhyiArq2v3DVdPrwrwnWSVb22knFRcxsyqDxvE7d2kfb -p9OBsmpUUZPPnSOooAWPgNKNdCuQDkxIsY2bbMT+I7Wvc154hIYoqe+MdZBHJ8XB3lDrsTjcdteD -e9eBEE+2N5y1rlPQPY65h7qnh6rNgXrH0uFRjm2XOIgfCDIXZQm6bdJL+GENoN4dga+3H5+c2OoI -5Hh87eghqjgPrdE5FR8dP3nn5cZOd/z66U/l55HdQMHk/rA9ynXYOvO1PYfcOcO6Jc9syjMZUui9 -799fND28f1gkDNq4Wg48oZmJwafPQzs75/yunoJ9MuxOu9Fi20DxiWQXFB1h7oLq6EXxqMT9qPZz -52DJhg+HDR7bY2V0bbxbNBM6ckLOmM8j3MMk85uZtrvTfR4P6s4omv7coM2TevlB3NkZ4Vb+fvV2 -M9GeesMTW3wKvhqlv/+TB/TvHn6w3Y93JX1sq6e9R/rZ38UTM1jc3OZS8zwCywOqdEAK/JEYWNnH -W9QaQroM1UwOGYPCH3K4MUA6xYwZ+cZj7VRYQxH8Pz99ANE= -""" -) - -# file activate.sh -ACTIVATE_SH = convert( - """ -eJytVW1P2zAQ/u5fcaQVG9s6VvjGVLQyKoEELWo6JjZQMMmVWEudynYK5eW/75w3kgbYh9EPbe27 -s5977p5zCyah0DAVEcIs0QauEBKNAdwIE4Kj40T5CFdCbnLfiAU36MCHqYpncMV1+IG1YBkn4HMp -YwMqkSAMBEKhb6IlY0xM4Tc47fu9vnvguaMf4++DzqMDPdr74sDFVzAhSgb0QT+MwTmjw1IY+cXG -gtO+EnOzA+ftYtsG765vZYG3dOX2NpsKxgIsUML7DbhP7YnUaKAzhfkyiH3Y3QxwsSmTKIKt3fUu -S31aoNB6xVEAKBdCxXKG0sCCK8GvItS51xpl07mD9v1pf/zRe4QLijOJkhqMShAoWzIAQQ7Qj7gi -GrkBHkVpOFnzeCLEGx3te6eH48mP/pF30p8c7NB5xAhUKLEfa+o57Ya7U3rg7TxWJnUs97KcG0Gp -nXj6B5qzycFoeDA6HrwAqbQ3gJWWJrzS9CrIupctaUZ82qQ6jBMqUICG2ivtP+AygDsdfoKbUPgh -hHyBwOmHTH48m1mzCakGtqfyo6jBfSoJ1cbEcE0IqH3o3zRWdjHn1Hx5qP4M8JNkECcmNxshr/Nj -ao6WIGhbisEPubxGDTekJx7YryVYbdC11GNzQo5BUQCiXxbq6KRUPzyUm79IMaeDsXs4GnaeK0Oa -ZEdRF5cdXSPtlQK73Rcq63YbJXW7zVq63VeLmJsLIJlLYR0MT5/SX7PYuvlEkLEMUJOQrIRxBV4L -XIymUDisrQDoWFOh/eL2R0bjKbMLpTDCBa9pujIt6nczVkHbczyvsvQ8h+U8VFNiDbERk5lQ80XF -e9Pz9g6H3rB/PPC8ndytquMS95MgLGG0w2plfU2qLwjLwlqR6epV6SjN2jO9pZr9/qHb3zsaeCfj -0fHJpNGYq43QsyBdW+Gnoju3T4RmxxCnsNaD2xc6suleOxQjjfWA95c0HFDyGcJ5jfhz53IDasH5 -NKx0tk2+Bcf8D4JOFNrZkEgeCa7zF4SSEOadprmukAdLC1ghq3pUJFl9b9bXV04itdt3g7FsWT5Z -8yUNHQmdWe7ntL85WTe/yRx8gxn4n/Pvf2bfc3OPavYXWw6XFQ== -""" -) - -# file activate.fish -ACTIVATE_FISH = convert( - """ -eJytVm1v2zYQ/q5fcZUdyClqGVuHfQgwDGnjIQYSO3DcAMM6yLREWxxo0iMpty7243ekLImy5RQY -lg+xTd7rc3cPrweLnGlYM05hW2gDKwqFphn+Y2IDSy0LlVJYMTEiqWF7Ymi8ZjpfwtsvzORMAAFV -CGGF7TkMIDdmdzMa2V86p5zHqdzCNWiqNZPibRz04E6CkMYqAjOQMUVTww9xEKwLgV6kgGRFdM7W -h2RHTA7DDMKPUuypMhodOkfuwkjQckttIBuwKpASAWhObgT7RsMA8E9T41SOxvpEbfb1hVWqLhqh -P37400mspXKO8FAZwGx9mR/jeHiU67AW9psfN/3aSBkSFVn5meYSPMHAXngoWG+XUHDpnqPgwOlA -oXRlc4d/wCiIbiKIPovoxGVGqzpbf9H4KxZoz5QpCKdiD1uZUSAiQ+umUMK6NjnFaqot4ZgWikqx -pcLEkfPaQ0ELjOSZfwt7ohhZcaqdFFuDodh8Q4GwJbOHu+RlMl98un1Inm4X92ENcc81l8bu2mDz -FSvbWq7Rhq7T/K9M64Lq0U/vfwbCDVXY0tYW5Bg8R5xqm5XvQQnQb5Pn++RlPH+ezKYlUGEcQvhZ -hNfYFDDkBt7XulXZh5uvpfVBu2LnuVzXupRretnQuWajeOydWodCt7Ar7Hfg/X1xP5vezx7HYXAW -R313Gk198XocbbFXonGYP81nj0+LZIbYzyd3TTy22aru1DD8GxLsJQdzslNyuzNedzxjGNj5FE8P -wGWKLbl0E5tUFlxdlrZtCefyi2teRbdyj6JyDUvP7rLiQM87XcbtnDmcmw+8iMaKaOoNUKRPfJSz -pI1U1AUjFdswQXjjx3cPXXl7AukZOt/ToJfx9IvaVaJ2WY/SVfXH05d2uUPHPThDIbz5BSIhRYbH -qrBsQ6NWEfl6WN296Y55d8hk2n3VENiFdP2X5YKIP8R1li7THnwSNlOmFOV0T3wqiwOPPNv5BUE1 -VR4+ECaJ9zNJQmv//2O4/8hsVaRnpILs1nqV+yWh1UR2WdFJOgBbJBf2vfRHSfJhMk2mt49jROKo -UuO97Dd0srQ9hYdxUH5aUjghm+5QPELrkqe+lfar2PTb7mByPBhuy7PjNuGka2L71s4suZs83354 -GB/nJzw+jB/l7uBGPi2wmbCR2sRQ+yZIGaczeqSh1uT7Q3820y3xTk7AwSN72gqorw0xhX7n1iBP -R6MUsXub3nFywBXujBSt+1K5MuKT4lMZpMRNRjHcJ9DqHj+zXz2ZydquiO/gL7uU7hTdIcQuOH+L -EGRLG9/+w9JM1pG0kuaBc2VUTJg1RFf6Sked4jDAXJJUcsy9XG9eebsbc4MrfQ1RhzIMcHiojbjd -HaFntuLSEoJ5x7NQw1nr2NkOqV3T+g3qIQ54ubrXgp0032bvamC6yP4kaNfx/wIsXsYv -""" -) - -# file activate.csh -ACTIVATE_CSH = convert( - """ -eJx9VNtO4zAQffdXDKEiUEFhX8t22bJFWqRyEVuQVkKy3Hi6sZQ44Dit+sK379hJittG5KGqPZdz -fOZyCLNUlbBQGUJelRbmCFWJElbKphCVRWUShLnS5yKxaiksDpIyjaC/MEUO9Lc/YIfwt6ggEVoX -FkylQVmQymBis7Wz/jJIcRLma5iIpZIIEwXXmSgVfJf+Qs5//suFygZJkf8YMFaiBY2rTGkcxa8s -ZkxkSpQgsWUBsUVi27viD9MJf7l9mj2Pp/xxPPsNByO4gKMjoCSol+Dvot6e3/A9cl6VdmB71ksw -mIoyvYROnKeHu8dZiARvpMebHe0CeccvoLz9sjY5tq3h5v6lgY5eD4b9yGFFutCSrkzlRMAm554y -we3bWhYJqXcIzx5bGYMZLoW2sBRGiXmG5YAFsdsIvhA7rCDiPDhyHtXl2lOQpGhkZtuVCKKH7+ec -X9/e8/vx3Q3nw00EfWoBxwFWrRTBeSWiE7Apagb0OXRKz7XIEUbQFcMwK7HLOT6OtwlZQo9PIGao -pVrULKj64Ysnt3/G19ObtgkCJrXzF74jRz2MaCnJgtcN5B7wLfK2DedOp4vGydPcet5urq2XBEZv -DcnQpBZVJt0KUBqEa4YzpS0a3x7odFOm0Dlqe9oEkN8qVUlK01/iKfSa3LRRKmqkBc2vBKFpmyCs -XG4d2yYyEQZBzIvKOgLN+JDveiVoaXyqedVYOkTrmCRqutrfNVHr6xMFBhh9QD/qNQuGLvq72d03 -3Jy2CtGCf0rca/tp+N4BXqsflKquRr0L2sjmuClOu+/8/NKvTQsNZ3l9ZqxeTew//1a6EA== -""" -) - -# file activate.xsh -ACTIVATE_XSH = convert( - """ -eJyNU11PwjAUfe+vuNY9sIj7ASQ+YCSBRD6i02gIaSq7gyWjXdqyaIz/3XYwVmB+9GFZ78c57T2n -lNIXKfQa+NJkJTcIeqmywkAqFZSZMlueoygppSRVcgPvrjgyUuYask0hlYEVGqaxAK6B7f8JSTAF -lmCN2uFqpcMeAbuyFGjxkcglhUwAzzOuUe9SbiWY18H5vm5B6sbgM4qir8jSdCib3t+x59FD/NS/ -Z7N+PKRdoDRskAIXhBsIziqPyFrSf9O9xsPpZDgdD85JD6lz6kPqtwM0RYdx1bnB5Lka2u5cxzML -vKLWTjZ7mI5n8b8A9rUNjpAiQW3U1gmKFIQ0lXpW1gblEh4xT6EuvGjXtHGFE5ZcwlZotGhKYY4l -FwZKrjL+lqMmvoXmp4dYhKQV1M7d6yPEv5jNKcqYf1VGbcmZB5x4lRcCfzfvLXaBiCdJ5wj46uD+ -Tmg3luR2NGGT/nhgGbpgX48wN7HaYhcUFjlfYrULCTkxWru36jF59rJ9NlJlf7JQde5j11VS+yZr -0d22eUPaxdycLKMTvqWjR3610emDtgTu36ylcJe83rhv/di/AYN1UZY= -""" -) - -# file activate.bat -ACTIVATE_BAT = convert( - """ -eJyVk1FLhEAUhd8X/A8XWSkf28dCyMUpBR3FzAiCS+WYwq4TOdXfb0Z3dTJdyCfveO85n8frNXut -OPCyNFbGqmUCzDxIs3s3REJzB1GrEE3VVJdQsLJuWAEYh97QkaRxlGRwbqxAXp1Uf+RYM32W1LKB -7Vp2nJC6DReD9m+5qeQ6Wd+a/SN7dlzn9oI7dxsSXJCcoXOskfLgYXdv/j8LnXiM8iGg/RmiZmOr -bFMSgcebMwGfKhgbBIfnL14X8P7BX3Zs38J3LSoQFdtD3YCVuJlvnfgmj5kfUz+OCLxxqUWoF9zk -qtYAFyZkBsO9ArzUh/td0ZqP9IskElTFMsnwb4/GqeoLPUlZT5dJvf8Id5hQIONynUSa2G0Wc+m8 -Z+w2w4/Tt2hbYT0hbgOK1I0I4tUw/QOTZfLE -""" -) - -# file deactivate.bat -DEACTIVATE_BAT = convert( - """ -eJyFkN0KgkAUhO8F32EQpHqFQEjQUPAPMaErqVxzId3IrV6/3ST/UDp3c86c4WN25FIysKJQFVVp -CEfqxsnB9DI7SA25i20fFqtXHM+GYL0BZzi9GM1xf7DzjVQN3pSX4CWpQGvokZk4uqrQAjXjyElB -a5IjCz0r+2VHcehHCa5MZNmB5e7TdqMqECMptHZh6DN/utb7Zs6CejsO/QNzTJwwcELfHgJJPcTp -TFOk7rCM1f92aG38HzBR5KgjoYdIQk5hZPWLGNLfd/MN+wANyJE5 -""" -) - -# file activate.ps1 -ACTIVATE_PS = convert( - """ -eJytVU1v4jAQvftXTNNoF9QNvSP1kG6RikQpIiyX3ZXlJkOx5NiR46RFK/77Oh+QkEBVrXYOSPZ8 -+L2ZN8FNQ80TM149TgO68FePcAduvOMyVyEzXMlRvAtVHDMZjRJmtsStE+79YEIfpksbHySCG29h -vTBYYqpEjtXJcY9lb0cjZwj2WqM0hGwyGRbV4VWoFybGETJ7zpnBwc/0jZtw+xvcuZIPmBqdFS4c -wh8C1vgGBit7XT2RM83Zi8AxfZ490PV0ufrhz8oXD/GFuSjz8YHd5ZRj/BJjZUms60hweqEOeEGo -EqwJlJl7cgbggemYKhHRnGuTMUETreLEnEA8Bla+AulHuV2sU4Nx89ivSxktjGVTDpwm83UbTbto -Jwy8idZK+9X8Ai7sQMXuu5KGywy7j1xdmGJh1xCg2EBUe68+ptRI5OO4ZBepsIax7yutdNcgkp3Z -Wo8XQ3XrMv2aFknXkMkUDXCtUWDOpDkKLSUNEPCkklFDjhC33Sg7wcOWkG6zC2frSMgc3xq9nWgL -vDmLEXoSBBsvMmzETUhb5073yVtK76dzOvefJtRaEUaDyYJSB25aRaqpdXIth8C/n03oYvn8tFgd -ptht7hnVtebtOPVYTvV+Lumutw+NpBzatKFEUzDwpN1Sp62u3uC7cCoJ+lEEZosQZqlRVggaN/wd -jCov8Z2nVtav0Nm5koANzbnK0hozzctp3MGXTy5uYefJ3FwoPjzm7ludRJHiv/FmHbphpovPc832 -G7xkBiIlv9pfnoZMR8DN6P83wZX41s13Bu6g/cfS2x9vhmwDwyE4pw3tF/t8N/fkLzQ3ME8= -""" -) - -# file distutils-init.py -DISTUTILS_INIT = convert( - """ -eJytV21v5DQQ/p5fMaRCJLANcAcSqlghuBdUcRzo6BdUnSI3cXZNs3bO9m679+uZsbOJnWR7fKBS -u65nPC/PvK7YdUpbUCYR/mSOw/GBaSnkxiTJBaiuUjUHYUAqCwwOQts9a7k8wE7V+5avwCh44FAx -CXuDnBasgkbIGuyWg7F1K+5Q0LWTzaT9DG7wgdL3oCR0x+64QkaUv9sbC3ccdXjBeMssaG5EzQ0I -SeJQDkq77I52q+TXyCcawevLx+JYfIRaaF5ZpY8nP7ztSYIEyXYc1uhu0TG7LfobIhm7t6I1Jd0H -HP8oIbMJe+YFFmXZiJaXZb6CdBCQ5olohudS6R0dslhBDuuZEdnszSA/v0oAf07xKOiQpTcIaxCG -QQN0rLpnG0TQwucGWNdxpg1FA1H1+IEhHFpVMSsQfWb85dFYvhsF/YS+8NZwr710lpdlIaTh2mbf -rGDqFFxgdnxgV/D6h2ffukcIBUotDlwbVFQK2Sj4EbLnK/iud8px+TjhRzLcac7acvRpTdSiVawu -fVpkaTk6PzKmK3irJJ/atoIsRRL9kpw/f/u1fHn97tWLmz/e/Z3nTunoaWwSfmCuFTtWbYXkmFUD -z9NJMzUgLdF9YRHA7pjmgxByiWvv31RV8Zfa64q/xix449jOOz0JxejH2QB8HwQg8NgeO26SiDIL -heMpfndxuMFz5p0oKI1H1TGgi6CSwFiX6XgVgUEsBd2WjVa70msKFa56CPOnbZ5I9EnkZZL0jP5M -o1LwR9Tb51ssMfdmX8AL1R1d9Wje8gP2NSw7q8Xd3iKMxGL1cUShLDU/CBeKEo2KZRYh1efkY8U7 -Cz+fJL7SWulRWseM6WvzFOBFqQMxScjhoFX0EaGLFSVKpWQjNuSXMEi4MvcCa3Jw4Y4ZbtAWuUl6 -095iBAKrRga0Aw80OjAhqy3c7UVbl/zRwlgZUCtu5BcW7qV6gC3+YpPacOvwxFCZoJc7OVuaFQ84 -U9SDgUuaMVuma2rGvoMRC3Y8rfb92HG6ee1qoNO8EY8YuL4mupbZBnst9eIUhT5/lnonYoyKSu12 -TNbF6EGP2niBDVThcbjwyVG1GJ+RK4tYguqreUODkrXiIy9VRy3ZZIa3zbRC0W68LRAZzfQRQ4xt -HScmNbyY01XSjHUNt+8jNt6iSMw3aXAgVzybPVkFAc3/m4rZHRZvK+xpuhne5ZOKnz0YB0zUUClm -LrV9ILGjvsEUSfO48COQi2VYkyfCvBjc4Z++GXgB09sgQ9YQ5MJFoIVOfVaaqyQha2lHKn3huYFP -KBJb8VIYX/doeTHjSnBr8YkT34eZ07hCWMOimh6LPrMQar8cYTF0yojHdIw37nPavenXpxRHWABc -s0kXJujs0eKbKdcs4qdgR4yh1Y5dGCJlMdNoC5Y5NgvcbXD9adGIzAEzLy/iKbiszYPA/Wtm8UIJ -OEGYljt14Bk9z5OYROuXrLMF8zW3ey09W+JX0E+EHPFZSIMwvcYWHucYNtXSb8u4AtCAHRiLmNRn -1UCevMyoabqBiRt3tcYS9fFZUw/q4UEc/eW8N/X3Tn1YyyEec3NjpSeVWMXJOTNx5tWqcsNwLu5E -TM5hEMJTTuGZyMPGdQ5N+r7zBJpInqNJjbjGkUbUs+iGTEAt63+Ee2ZVbNMnwacF6yz4AXEZ/Ama -5RTNk7yefGB+5ESiAtoi/AE9+5LpjemBdfj0Ehf09Lzht5qzCwT9oL00zZZaWjzEWjfEwoU9mMiD -UbThVzZ34U7fXP+C315S91UcO9rAFLen4fr29OA9WnOyC1c8Zu5xNaLeyNo2WNvPmkCtc2ICqidc -zmg+LaPu/BXc9srfx9pJbJiSw5NZkgXxWMiyBWpyNjdmeRbmzb+31cHS -""" -) - -# file distutils.cfg -DISTUTILS_CFG = convert( - """ -eJxNj00KwkAMhfc9xYNuxe4Ft57AjYiUtDO1wXSmNJnK3N5pdSEEAu8nH6lxHVlRhtDHMPATA4uH -xJ4EFmGbvfJiicSHFRzUSISMY6hq3GLCRLnIvSTnEefN0FIjw5tF0Hkk9Q5dRunBsVoyFi24aaLg -9FDOlL0FPGluf4QjcInLlxd6f6rqkgPu/5nHLg0cXCscXoozRrP51DRT3j9QNl99AP53T2Q= -""" -) - -# file activate_this.py -ACTIVATE_THIS = convert( - """ -eJylVE1v2zAMvetXENqh9pZ5wHYL0EMOBdqh64It7RAEhaE4TKzOlgxJ+ULR/15Sdhr341BsOcSW -9fj4SD5JSjkqgt6ogLDRLqxVhWYDS+ugWDuHJoA2AV3jkP6HQlx7BNxhkdgGTRJK7fOlrjDNHKpF -kg7g/iSPX/L8ZAhP+w9pJsSEVlAoA3OEtccFbEs0sLdrqNc+8CegTdxpH7RZwXgfSmv6+QdgbCDS -Z1rn2nxpIjQTUkqh68a6ANYf3rwO+PS+90IEtx8KoN9BqcBdgU2AK1XjmXPWtdtOaZI08h5d0NbE -nURO+3r/qRWpTIX4AFQTBS64AAgWxqPJOUQaYBjQUxuvFxgLZtBCOyyCdftU0DKnJZxSnVmjQpnR -ypD85LBWc8/P5CAhTQVtUcO0s2YmOZu8PcZ7bLI7q00y66hv4RMcA7IVhqQNGoCUaeabSofkGEz0 -Yq6oI68VdYSx5m5uwIOjAp1elQHU3G5eVPhM683Fr8n16DI/u7qJkjkPk6nFom8G6AJqcq2PU//c -qOKvWiG3l4GlpbE1na1aQ9RYlMpoX4uL3/l4Op4Sf6m8CsElZBYqttk3+3yDzpMFcm2WlqZH2O/T -yfnPK0ITKmsqFejM1JkPygW/1dR4eac2irB6CU/w1lcsLe+k+V7DYv+5OMp6qefc6X4VnsiwaulY -6fvJXrN4bKOJ6s/Fyyrg9BTkVptvX2UEtSkJ18b8ZwkcfhTwXrKqJWuHd/+Q3T/IjLWqkHxk7f0B -pW9kFXTaNlwn+TjWSglSwaiMbMRP8l7yTAltE5AOc5VT8FLvDm2KC3F87VnyBzuZrUakdMERXe0P -7luSN+leWsYF5x/QAQdqeoHC4JZYKrr5+uq6t9mQXT/T8VrWHMRwmoqO9yGTUHF8YN/EHPbFI/bz -/no= -""" -) - -# file python-config -PYTHON_CONFIG = convert( - """ -eJyNVV1P2zAUfc+v8ODBiSABxlulTipbO6p1LWqBgVhlhcZpPYUkctzSivHfd6+dpGloGH2Ja/ue -e+65Hz78xNhtf3x90xmw7vCWsRPGLvpDNuz87MKfdKMWSWxZ4ilNpCLZJiuWc66SVFUOZkkcirll -rfxIBAzOMtImDzSVPBRrekwoX/OZu/0r4lm0DHiG60g86u8sjPw5rCyy86NRkB8QuuBRSqfAKESn -3orLTCQxE3GYkC9tYp8fk89OSwNsmXgizrhUtnumeSgeo5GbLUMk49Rv+2nK48Cm/qMwfp333J2/ -dVcAGE0CIQHBsgIeEr4Wij0LtWDLzJ9ze5YEvH2WI6CHTAVcSu9ZCsXtgxu81CIvp6/k4eXsdfo7 -PvDCRD75yi41QitfzlcPp1OI7i/1/iQitqnr0iMgQ+A6wa+IKwwdxyk9IiXNAzgquTFU8NIxAVjM -osm1Zz526e+shQ4hKRVci69nPC3Kw4NQEmkQ65E7OodxorSvxjvpBjQHDmWFIQ1mlmzlS5vedseT -/mgIEsMJ7Lxz2bLAF9M5xeLEhdbHxpWOw0GdkJApMVBRF1y+a0z3c9WZPAXGFcFrJgCIB+024uad -0CrzmEoRa3Ub4swNIHPGf7QDV+2uj2OiFWsChgCwjKqN6rp5izpbH6Wc1O1TclQTP/XVwi6anTr1 -1sbubjZLI1+VptPSdCfwnFBrB1jvebrTA9uUhU2/9gad7xPqeFkaQcnnLbCViZK8d7R1kxzFrIJV -8EaLYmKYpvGVkig+3C5HCXbM1jGCGekiM2pRCVPyRyXYdPf6kcbWEQ36F5V4Gq9N7icNNw+JHwRE -LTgxRXACpvnQv/PuT0xCCAywY/K4hE6Now2qDwaSE5FB+1agsoUveYDepS83qFcF1NufvULD3fTl -g6Hgf7WBt6lzMeiyyWVn3P1WVbwaczHmTzE9A5SyItTVgFYyvs/L/fXlaNgbw8v3azT+0eikVlWD -/vBHbzQumP23uBCjsYdrL9OWARwxs/nuLOzeXbPJTa/Xv6sUmQir5pC1YRLz3eA+CD8Z0XpcW8v9 -MZWF36ryyXXf3yBIz6nzqz8Muyz0m5Qj7OexfYo/Ph3LqvkHUg7AuA== -""" -) - -MH_MAGIC = 0xFEEDFACE -MH_CIGAM = 0xCEFAEDFE -MH_MAGIC_64 = 0xFEEDFACF -MH_CIGAM_64 = 0xCFFAEDFE -FAT_MAGIC = 0xCAFEBABE -BIG_ENDIAN = ">" -LITTLE_ENDIAN = "<" -LC_LOAD_DYLIB = 0xC -maxint = getattr(sys, "maxsize", getattr(sys, "maxint", None)) - - -class FileView(object): - """ - A proxy for file-like objects that exposes a given view of a file. - Modified from macholib. - """ - - def __init__(self, file_obj, start=0, size=maxint): - if isinstance(file_obj, FileView): - self._file_obj = file_obj._file_obj - else: - self._file_obj = file_obj - self._start = start - self._end = start + size - self._pos = 0 - - def __repr__(self): - return "".format(self._start, self._end, self._file_obj) - - def tell(self): - return self._pos - - def _checkwindow(self, seek_to, op): - if not (self._start <= seek_to <= self._end): - raise IOError( - "{} to offset {:d} is outside window [{:d}, {:d}]".format(op, seek_to, self._start, self._end) - ) - - def seek(self, offset, whence=0): - seek_to = offset - if whence == os.SEEK_SET: - seek_to += self._start - elif whence == os.SEEK_CUR: - seek_to += self._start + self._pos - elif whence == os.SEEK_END: - seek_to += self._end - else: - raise IOError("Invalid whence argument to seek: {!r}".format(whence)) - self._checkwindow(seek_to, "seek") - self._file_obj.seek(seek_to) - self._pos = seek_to - self._start - - def write(self, content): - here = self._start + self._pos - self._checkwindow(here, "write") - self._checkwindow(here + len(content), "write") - self._file_obj.seek(here, os.SEEK_SET) - self._file_obj.write(content) - self._pos += len(content) - - def read(self, size=maxint): - assert size >= 0 - here = self._start + self._pos - self._checkwindow(here, "read") - size = min(size, self._end - here) - self._file_obj.seek(here, os.SEEK_SET) - read_bytes = self._file_obj.read(size) - self._pos += len(read_bytes) - return read_bytes - - -def read_data(file, endian, num=1): - """ - Read a given number of 32-bits unsigned integers from the given file - with the given endianness. - """ - res = struct.unpack(endian + "L" * num, file.read(num * 4)) - if len(res) == 1: - return res[0] - return res - - -def mach_o_change(at_path, what, value): - """ - Replace a given name (what) in any LC_LOAD_DYLIB command found in - the given binary with a new name (value), provided it's shorter. - """ - - def do_macho(file, bits, endian): - # Read Mach-O header (the magic number is assumed read by the caller) - cpu_type, cpu_sub_type, file_type, n_commands, size_of_commands, flags = read_data(file, endian, 6) - # 64-bits header has one more field. - if bits == 64: - read_data(file, endian) - # The header is followed by n commands - for _ in range(n_commands): - where = file.tell() - # Read command header - cmd, cmd_size = read_data(file, endian, 2) - if cmd == LC_LOAD_DYLIB: - # The first data field in LC_LOAD_DYLIB commands is the - # offset of the name, starting from the beginning of the - # command. - name_offset = read_data(file, endian) - file.seek(where + name_offset, os.SEEK_SET) - # Read the NUL terminated string - load = file.read(cmd_size - name_offset).decode() - load = load[: load.index("\0")] - # If the string is what is being replaced, overwrite it. - if load == what: - file.seek(where + name_offset, os.SEEK_SET) - file.write(value.encode() + b"\0") - # Seek to the next command - file.seek(where + cmd_size, os.SEEK_SET) - - def do_file(file, offset=0, size=maxint): - file = FileView(file, offset, size) - # Read magic number - magic = read_data(file, BIG_ENDIAN) - if magic == FAT_MAGIC: - # Fat binaries contain nfat_arch Mach-O binaries - n_fat_arch = read_data(file, BIG_ENDIAN) - for _ in range(n_fat_arch): - # Read arch header - cpu_type, cpu_sub_type, offset, size, align = read_data(file, BIG_ENDIAN, 5) - do_file(file, offset, size) - elif magic == MH_MAGIC: - do_macho(file, 32, BIG_ENDIAN) - elif magic == MH_CIGAM: - do_macho(file, 32, LITTLE_ENDIAN) - elif magic == MH_MAGIC_64: - do_macho(file, 64, BIG_ENDIAN) - elif magic == MH_CIGAM_64: - do_macho(file, 64, LITTLE_ENDIAN) - - assert len(what) >= len(value) - - with open(at_path, "r+b") as f: - do_file(f) - - -if __name__ == "__main__": - main() diff --git a/virtualenv_embedded/activate_this.py b/virtualenv_embedded/activate_this.py deleted file mode 100644 index aa96457f2..000000000 --- a/virtualenv_embedded/activate_this.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Activate virtualenv for current interpreter: - -Use exec(open(this_file).read(), {'__file__': this_file}). - -This can be used when you must use an existing Python interpreter, not the virtualenv bin/python. -""" -import os -import site -import sys - -try: - __file__ -except NameError: - raise AssertionError("You must use exec(open(this_file).read(), {'__file__': this_file}))") - -# prepend bin to PATH (this file is inside the bin directory) -bin_dir = os.path.dirname(os.path.abspath(__file__)) -os.environ["PATH"] = os.pathsep.join([bin_dir] + os.environ.get("PATH", "").split(os.pathsep)) - -base = os.path.dirname(bin_dir) - -# virtual env is right above bin directory -os.environ["VIRTUAL_ENV"] = base - -# add the virtual environments site-package to the host python import mechanism -IS_PYPY = hasattr(sys, "pypy_version_info") -IS_JYTHON = sys.platform.startswith("java") -if IS_JYTHON: - site_packages = os.path.join(base, "Lib", "site-packages") -elif IS_PYPY: - site_packages = os.path.join(base, "site-packages") -else: - IS_WIN = sys.platform == "win32" - if IS_WIN: - site_packages = os.path.join(base, "Lib", "site-packages") - else: - site_packages = os.path.join(base, "lib", "python{}.{}".format(*sys.version_info), "site-packages") - -prev = set(sys.path) -site.addsitedir(site_packages) -sys.real_prefix = sys.prefix -sys.prefix = base - -# Move the added items to the front of the path, in place -new = list(sys.path) -sys.path[:] = [i for i in new if i not in prev] + [i for i in new if i in prev] diff --git a/virtualenv_embedded/distutils-init.py b/virtualenv_embedded/distutils-init.py deleted file mode 100644 index b9b0f24f7..000000000 --- a/virtualenv_embedded/distutils-init.py +++ /dev/null @@ -1,134 +0,0 @@ -import os -import sys -import warnings - -# opcode is not a virtualenv module, so we can use it to find the stdlib -# Important! To work on pypy, this must be a module that resides in the -# lib-python/modified-x.y.z directory -import opcode - -dirname = os.path.dirname - -distutils_path = os.path.join(os.path.dirname(opcode.__file__), "distutils") -if os.path.normpath(distutils_path) == os.path.dirname(os.path.normpath(__file__)): - warnings.warn("The virtualenv distutils package at %s appears to be in the same location as the system distutils?") -else: - __path__.insert(0, distutils_path) # noqa: F821 - if sys.version_info < (3, 4): - import imp - - real_distutils = imp.load_module("_virtualenv_distutils", None, distutils_path, ("", "", imp.PKG_DIRECTORY)) - else: - import importlib.machinery - - distutils_path = os.path.join(distutils_path, "__init__.py") - loader = importlib.machinery.SourceFileLoader("_virtualenv_distutils", distutils_path) - if sys.version_info < (3, 5): - import types - - real_distutils = types.ModuleType(loader.name) - else: - import importlib.util - - spec = importlib.util.spec_from_loader(loader.name, loader) - real_distutils = importlib.util.module_from_spec(spec) - loader.exec_module(real_distutils) - - # Copy the relevant attributes - try: - __revision__ = real_distutils.__revision__ - except AttributeError: - pass - __version__ = real_distutils.__version__ - -from distutils import dist, sysconfig # isort:skip - -try: - basestring -except NameError: - basestring = str - -# patch build_ext (distutils doesn't know how to get the libs directory -# path on windows - it hardcodes the paths around the patched sys.prefix) - -if sys.platform == "win32": - from distutils.command.build_ext import build_ext as old_build_ext - - class build_ext(old_build_ext): - def finalize_options(self): - if self.library_dirs is None: - self.library_dirs = [] - elif isinstance(self.library_dirs, basestring): - self.library_dirs = self.library_dirs.split(os.pathsep) - - self.library_dirs.insert(0, os.path.join(sys.real_prefix, "Libs")) - old_build_ext.finalize_options(self) - - from distutils.command import build_ext as build_ext_module - - build_ext_module.build_ext = build_ext - -# distutils.dist patches: - -old_find_config_files = dist.Distribution.find_config_files - - -def find_config_files(self): - found = old_find_config_files(self) - if os.name == "posix": - user_filename = ".pydistutils.cfg" - else: - user_filename = "pydistutils.cfg" - user_filename = os.path.join(sys.prefix, user_filename) - if os.path.isfile(user_filename): - for item in list(found): - if item.endswith("pydistutils.cfg"): - found.remove(item) - found.append(user_filename) - return found - - -dist.Distribution.find_config_files = find_config_files - -# distutils.sysconfig patches: - -old_get_python_inc = sysconfig.get_python_inc - - -def sysconfig_get_python_inc(plat_specific=0, prefix=None): - if prefix is None: - prefix = sys.real_prefix - return old_get_python_inc(plat_specific, prefix) - - -sysconfig_get_python_inc.__doc__ = old_get_python_inc.__doc__ -sysconfig.get_python_inc = sysconfig_get_python_inc - -old_get_python_lib = sysconfig.get_python_lib - - -def sysconfig_get_python_lib(plat_specific=0, standard_lib=0, prefix=None): - if standard_lib and prefix is None: - prefix = sys.real_prefix - return old_get_python_lib(plat_specific, standard_lib, prefix) - - -sysconfig_get_python_lib.__doc__ = old_get_python_lib.__doc__ -sysconfig.get_python_lib = sysconfig_get_python_lib - -old_get_config_vars = sysconfig.get_config_vars - - -def sysconfig_get_config_vars(*args): - real_vars = old_get_config_vars(*args) - if sys.platform == "win32": - lib_dir = os.path.join(sys.real_prefix, "libs") - if isinstance(real_vars, dict) and "LIBDIR" not in real_vars: - real_vars["LIBDIR"] = lib_dir # asked for all - elif isinstance(real_vars, list) and "LIBDIR" in args: - real_vars = real_vars + [lib_dir] # asked for list - return real_vars - - -sysconfig_get_config_vars.__doc__ = old_get_config_vars.__doc__ -sysconfig.get_config_vars = sysconfig_get_config_vars diff --git a/virtualenv_embedded/distutils.cfg b/virtualenv_embedded/distutils.cfg deleted file mode 100644 index 1af230ec9..000000000 --- a/virtualenv_embedded/distutils.cfg +++ /dev/null @@ -1,6 +0,0 @@ -# This is a config file local to this virtualenv installation -# You may include options that will be used by all distutils commands, -# and by easy_install. For instance: -# -# [easy_install] -# find_links = http://mylocalsite diff --git a/virtualenv_embedded/python-config b/virtualenv_embedded/python-config deleted file mode 100644 index 5e7a7c901..000000000 --- a/virtualenv_embedded/python-config +++ /dev/null @@ -1,78 +0,0 @@ -#!__VIRTUAL_ENV__/__BIN_NAME__/python - -import sys -import getopt -import sysconfig - -valid_opts = ['prefix', 'exec-prefix', 'includes', 'libs', 'cflags', - 'ldflags', 'help'] - -if sys.version_info >= (3, 2): - valid_opts.insert(-1, 'extension-suffix') - valid_opts.append('abiflags') -if sys.version_info >= (3, 3): - valid_opts.append('configdir') - - -def exit_with_usage(code=1): - sys.stderr.write("Usage: {0} [{1}]\n".format( - sys.argv[0], '|'.join('--'+opt for opt in valid_opts))) - sys.exit(code) - -try: - opts, args = getopt.getopt(sys.argv[1:], '', valid_opts) -except getopt.error: - exit_with_usage() - -if not opts: - exit_with_usage() - -pyver = sysconfig.get_config_var('VERSION') -getvar = sysconfig.get_config_var - -opt_flags = [flag for (flag, val) in opts] - -if '--help' in opt_flags: - exit_with_usage(code=0) - -for opt in opt_flags: - if opt == '--prefix': - print(sysconfig.get_config_var('prefix')) - - elif opt == '--exec-prefix': - print(sysconfig.get_config_var('exec_prefix')) - - elif opt in ('--includes', '--cflags'): - flags = ['-I' + sysconfig.get_path('include'), - '-I' + sysconfig.get_path('platinclude')] - if opt == '--cflags': - flags.extend(getvar('CFLAGS').split()) - print(' '.join(flags)) - - elif opt in ('--libs', '--ldflags'): - abiflags = getattr(sys, 'abiflags', '') - libs = ['-lpython' + pyver + abiflags] - libs += getvar('LIBS').split() - libs += getvar('SYSLIBS').split() - # add the prefix/lib/pythonX.Y/config dir, but only if there is no - # shared library in prefix/lib/. - if opt == '--ldflags': - if not getvar('Py_ENABLE_SHARED'): - libs.insert(0, '-L' + getvar('LIBPL')) - if not getvar('PYTHONFRAMEWORK'): - libs.extend(getvar('LINKFORSHARED').split()) - print(' '.join(libs)) - - elif opt == '--extension-suffix': - ext_suffix = sysconfig.get_config_var('EXT_SUFFIX') - if ext_suffix is None: - ext_suffix = sysconfig.get_config_var('SO') - print(ext_suffix) - - elif opt == '--abiflags': - if not getattr(sys, 'abiflags', None): - exit_with_usage() - print(sys.abiflags) - - elif opt == '--configdir': - print(sysconfig.get_config_var('LIBPL')) diff --git a/virtualenv_embedded/site.py b/virtualenv_embedded/site.py deleted file mode 100644 index ef1cd1a2c..000000000 --- a/virtualenv_embedded/site.py +++ /dev/null @@ -1,829 +0,0 @@ -"""Append module search paths for third-party packages to sys.path. - -**************************************************************** -* This module is automatically imported during initialization. * -**************************************************************** - -In earlier versions of Python (up to 1.5a3), scripts or modules that -needed to use site-specific modules would place ``import site'' -somewhere near the top of their code. Because of the automatic -import, this is no longer necessary (but code that does it still -works). - -This will append site-specific paths to the module search path. On -Unix, it starts with sys.prefix and sys.exec_prefix (if different) and -appends lib/python/site-packages as well as lib/site-python. -It also supports the Debian convention of -lib/python/dist-packages. On other platforms (mainly Mac and -Windows), it uses just sys.prefix (and sys.exec_prefix, if different, -but this is unlikely). The resulting directories, if they exist, are -appended to sys.path, and also inspected for path configuration files. - -FOR DEBIAN, this sys.path is augmented with directories in /usr/local. -Local addons go into /usr/local/lib/python/site-packages -(resp. /usr/local/lib/site-python), Debian addons install into -/usr/{lib,share}/python/dist-packages. - -A path configuration file is a file whose name has the form -.pth; its contents are additional directories (one per line) -to be added to sys.path. Non-existing directories (or -non-directories) are never added to sys.path; no directory is added to -sys.path more than once. Blank lines and lines beginning with -'#' are skipped. Lines starting with 'import' are executed. - -For example, suppose sys.prefix and sys.exec_prefix are set to -/usr/local and there is a directory /usr/local/lib/python2.X/site-packages -with three subdirectories, foo, bar and spam, and two path -configuration files, foo.pth and bar.pth. Assume foo.pth contains the -following: - - # foo package configuration - foo - bar - bletch - -and bar.pth contains: - - # bar package configuration - bar - -Then the following directories are added to sys.path, in this order: - - /usr/local/lib/python2.X/site-packages/bar - /usr/local/lib/python2.X/site-packages/foo - -Note that bletch is omitted because it doesn't exist; bar precedes foo -because bar.pth comes alphabetically before foo.pth; and spam is -omitted because it is not mentioned in either path configuration file. - -After these path manipulations, an attempt is made to import a module -named sitecustomize, which can perform arbitrary additional -site-specific customizations. If this import fails with an -ImportError exception, it is silently ignored. - -""" - -import os -import sys - -try: - import __builtin__ as builtins -except ImportError: - import builtins -try: - set -except NameError: - from sets import Set as set - -# Prefixes for site-packages; add additional prefixes like /usr/local here -PREFIXES = [sys.prefix, sys.exec_prefix] -# Enable per user site-packages directory -# set it to False to disable the feature or True to force the feature -ENABLE_USER_SITE = None -# for distutils.commands.install -USER_SITE = None -USER_BASE = None - -_is_64bit = (getattr(sys, "maxsize", None) or getattr(sys, "maxint")) > 2 ** 32 -_is_pypy = hasattr(sys, "pypy_version_info") - - -def makepath(*paths): - dir = os.path.join(*paths) - dir = os.path.abspath(dir) - return dir, os.path.normcase(dir) - - -def abs__file__(): - """Set all module' __file__ attribute to an absolute path""" - for m in sys.modules.values(): - f = getattr(m, "__file__", None) - if f is None: - continue - m.__file__ = os.path.abspath(f) - - -def removeduppaths(): - """ Remove duplicate entries from sys.path along with making them - absolute""" - # This ensures that the initial path provided by the interpreter contains - # only absolute pathnames, even if we're running from the build directory. - L = [] - known_paths = set() - for dir in sys.path: - # Filter out duplicate paths (on case-insensitive file systems also - # if they only differ in case); turn relative paths into absolute - # paths. - dir, dircase = makepath(dir) - if not dircase in known_paths: - L.append(dir) - known_paths.add(dircase) - sys.path[:] = L - return known_paths - - -# XXX This should not be part of site.py, since it is needed even when -# using the -S option for Python. See http://www.python.org/sf/586680 -def addbuilddir(): - """Append ./build/lib. in case we're running in the build dir - (especially for Guido :-)""" - from distutils.util import get_platform - - s = "build/lib.{}-{}.{}".format(get_platform(), *sys.version_info) - if hasattr(sys, "gettotalrefcount"): - s += "-pydebug" - s = os.path.join(os.path.dirname(sys.path[-1]), s) - sys.path.append(s) - - -def _init_pathinfo(): - """Return a set containing all existing directory entries from sys.path""" - d = set() - for dir in sys.path: - try: - if os.path.isdir(dir): - dir, dircase = makepath(dir) - d.add(dircase) - except TypeError: - continue - return d - - -def addpackage(sitedir, name, known_paths): - """Add a new path to known_paths by combining sitedir and 'name' or execute - sitedir if it starts with 'import'""" - if known_paths is None: - _init_pathinfo() - reset = 1 - else: - reset = 0 - fullname = os.path.join(sitedir, name) - try: - f = open(fullname, "r") - except IOError: - return - try: - for line in f: - if line.startswith("#"): - continue - if line.startswith("import"): - exec(line) - continue - line = line.rstrip() - dir, dircase = makepath(sitedir, line) - if not dircase in known_paths and os.path.exists(dir): - sys.path.append(dir) - known_paths.add(dircase) - finally: - f.close() - if reset: - known_paths = None - return known_paths - - -def addsitedir(sitedir, known_paths=None): - """Add 'sitedir' argument to sys.path if missing and handle .pth files in - 'sitedir'""" - if known_paths is None: - known_paths = _init_pathinfo() - reset = 1 - else: - reset = 0 - sitedir, sitedircase = makepath(sitedir) - if not sitedircase in known_paths: - sys.path.append(sitedir) # Add path component - try: - names = os.listdir(sitedir) - except os.error: - return - names.sort() - for name in names: - if name.endswith(os.extsep + "pth"): - addpackage(sitedir, name, known_paths) - if reset: - known_paths = None - return known_paths - - -def addsitepackages(known_paths, sys_prefix=sys.prefix, exec_prefix=sys.exec_prefix): - """Add site-packages (and possibly site-python) to sys.path""" - prefixes = [os.path.join(sys_prefix, "local"), sys_prefix] - if exec_prefix != sys_prefix: - prefixes.append(os.path.join(exec_prefix, "local")) - - for prefix in prefixes: - if prefix: - if sys.platform in ("os2emx", "riscos"): - sitedirs = [os.path.join(prefix, "Lib", "site-packages")] - elif _is_pypy: - sitedirs = [os.path.join(prefix, "site-packages")] - elif sys.platform == "darwin" and prefix == sys_prefix: - - if prefix.startswith("/System/Library/Frameworks/"): # Apple's Python - - sitedirs = [ - os.path.join("/Library/Python", "{}.{}".format(*sys.version_info), "site-packages"), - os.path.join(prefix, "Extras", "lib", "python"), - ] - - else: # any other Python distros on OSX work this way - sitedirs = [os.path.join(prefix, "lib", "python{}.{}".format(*sys.version_info), "site-packages")] - - elif os.sep == "/": - sitedirs = [ - os.path.join(prefix, "lib", "python{}.{}".format(*sys.version_info), "site-packages"), - os.path.join(prefix, "lib", "site-python"), - os.path.join(prefix, "python{}.{}".format(*sys.version_info), "lib-dynload"), - ] - lib64_dir = os.path.join(prefix, "lib64", "python{}.{}".format(*sys.version_info), "site-packages") - if os.path.exists(lib64_dir) and os.path.realpath(lib64_dir) not in [ - os.path.realpath(p) for p in sitedirs - ]: - if _is_64bit: - sitedirs.insert(0, lib64_dir) - else: - sitedirs.append(lib64_dir) - try: - # sys.getobjects only available in --with-pydebug build - sys.getobjects - sitedirs.insert(0, os.path.join(sitedirs[0], "debug")) - except AttributeError: - pass - # Debian-specific dist-packages directories: - sitedirs.append( - os.path.join(prefix, "local/lib", "python{}.{}".format(*sys.version_info), "dist-packages") - ) - if sys.version_info[0] == 2: - sitedirs.append( - os.path.join(prefix, "lib", "python{}.{}".format(*sys.version_info), "dist-packages") - ) - else: - sitedirs.append( - os.path.join(prefix, "lib", "python{}".format(sys.version_info[0]), "dist-packages") - ) - sitedirs.append(os.path.join(prefix, "lib", "dist-python")) - else: - sitedirs = [prefix, os.path.join(prefix, "lib", "site-packages")] - if sys.platform == "darwin": - # for framework builds *only* we add the standard Apple - # locations. Currently only per-user, but /Library and - # /Network/Library could be added too - if "Python.framework" in prefix or "Python3.framework" in prefix: - home = os.environ.get("HOME") - if home: - sitedirs.append( - os.path.join(home, "Library", "Python", "{}.{}".format(*sys.version_info), "site-packages") - ) - for sitedir in sitedirs: - if os.path.isdir(sitedir): - addsitedir(sitedir, known_paths) - return None - - -def check_enableusersite(): - """Check if user site directory is safe for inclusion - - The function tests for the command line flag (including environment var), - process uid/gid equal to effective uid/gid. - - None: Disabled for security reasons - False: Disabled by user (command line option) - True: Safe and enabled - """ - if hasattr(sys, "flags") and getattr(sys.flags, "no_user_site", False): - return False - - if hasattr(os, "getuid") and hasattr(os, "geteuid"): - # check process uid == effective uid - if os.geteuid() != os.getuid(): - return None - if hasattr(os, "getgid") and hasattr(os, "getegid"): - # check process gid == effective gid - if os.getegid() != os.getgid(): - return None - - return True - - -def addusersitepackages(known_paths): - """Add a per user site-package to sys.path - - Each user has its own python directory with site-packages in the - home directory. - - USER_BASE is the root directory for all Python versions - - USER_SITE is the user specific site-packages directory - - USER_SITE/.. can be used for data. - """ - global USER_BASE, USER_SITE, ENABLE_USER_SITE - env_base = os.environ.get("PYTHONUSERBASE", None) - - def joinuser(*args): - return os.path.expanduser(os.path.join(*args)) - - # if sys.platform in ('os2emx', 'riscos'): - # # Don't know what to put here - # USER_BASE = '' - # USER_SITE = '' - if os.name == "nt": - base = os.environ.get("APPDATA") or "~" - if env_base: - USER_BASE = env_base - else: - USER_BASE = joinuser(base, "Python") - USER_SITE = os.path.join(USER_BASE, "Python{}{}".format(*sys.version_info), "site-packages") - else: - if env_base: - USER_BASE = env_base - else: - USER_BASE = joinuser("~", ".local") - USER_SITE = os.path.join(USER_BASE, "lib", "python{}.{}".format(*sys.version_info), "site-packages") - - if ENABLE_USER_SITE and os.path.isdir(USER_SITE): - addsitedir(USER_SITE, known_paths) - if ENABLE_USER_SITE: - for dist_libdir in ("lib", "local/lib"): - user_site = os.path.join(USER_BASE, dist_libdir, "python{}.{}".format(*sys.version_info), "dist-packages") - if os.path.isdir(user_site): - addsitedir(user_site, known_paths) - return known_paths - - -def setBEGINLIBPATH(): - """The OS/2 EMX port has optional extension modules that do double duty - as DLLs (and must use the .DLL file extension) for other extensions. - The library search path needs to be amended so these will be found - during module import. Use BEGINLIBPATH so that these are at the start - of the library search path. - - """ - dllpath = os.path.join(sys.prefix, "Lib", "lib-dynload") - libpath = os.environ["BEGINLIBPATH"].split(";") - if libpath[-1]: - libpath.append(dllpath) - else: - libpath[-1] = dllpath - os.environ["BEGINLIBPATH"] = ";".join(libpath) - - -def setquit(): - """Define new built-ins 'quit' and 'exit'. - These are simply strings that display a hint on how to exit. - - """ - if os.sep == ":": - eof = "Cmd-Q" - elif os.sep == "\\": - eof = "Ctrl-Z plus Return" - else: - eof = "Ctrl-D (i.e. EOF)" - - class Quitter(object): - def __init__(self, name): - self.name = name - - def __repr__(self): - return "Use {}() or {} to exit".format(self.name, eof) - - def __call__(self, code=None): - # Shells like IDLE catch the SystemExit, but listen when their - # stdin wrapper is closed. - try: - sys.stdin.close() - except: - pass - raise SystemExit(code) - - builtins.quit = Quitter("quit") - builtins.exit = Quitter("exit") - - -class _Printer(object): - """interactive prompt objects for printing the license text, a list of - contributors and the copyright notice.""" - - MAXLINES = 23 - - def __init__(self, name, data, files=(), dirs=()): - self.__name = name - self.__data = data - self.__files = files - self.__dirs = dirs - self.__lines = None - - def __setup(self): - if self.__lines: - return - data = None - for dir in self.__dirs: - for filename in self.__files: - filename = os.path.join(dir, filename) - try: - fp = open(filename, "r") - data = fp.read() - fp.close() - break - except IOError: - pass - if data: - break - if not data: - data = self.__data - self.__lines = data.split("\n") - self.__linecnt = len(self.__lines) - - def __repr__(self): - self.__setup() - if len(self.__lines) <= self.MAXLINES: - return "\n".join(self.__lines) - else: - return "Type %s() to see the full %s text" % ((self.__name,) * 2) - - def __call__(self): - self.__setup() - prompt = "Hit Return for more, or q (and Return) to quit: " - lineno = 0 - while 1: - try: - for i in range(lineno, lineno + self.MAXLINES): - print(self.__lines[i]) - except IndexError: - break - else: - lineno += self.MAXLINES - key = None - while key is None: - try: - key = raw_input(prompt) - except NameError: - key = input(prompt) - if key not in ("", "q"): - key = None - if key == "q": - break - - -def setcopyright(): - """Set 'copyright' and 'credits' in __builtin__""" - builtins.copyright = _Printer("copyright", sys.copyright) - if _is_pypy: - builtins.credits = _Printer("credits", "PyPy is maintained by the PyPy developers: http://pypy.org/") - else: - builtins.credits = _Printer( - "credits", - """\ - Thanks to CWI, CNRI, BeOpen.com, Zope Corporation and a cast of thousands - for supporting Python development. See www.python.org for more information.""", - ) - here = os.path.dirname(os.__file__) - builtins.license = _Printer( - "license", - "See https://www.python.org/psf/license/", - ["LICENSE.txt", "LICENSE"], - [sys.prefix, os.path.join(here, os.pardir), here, os.curdir], - ) - - -class _Helper(object): - """Define the built-in 'help'. - This is a wrapper around pydoc.help (with a twist). - - """ - - def __repr__(self): - return "Type help() for interactive help, " "or help(object) for help about object." - - def __call__(self, *args, **kwds): - import pydoc - - return pydoc.help(*args, **kwds) - - -def sethelper(): - builtins.help = _Helper() - - -def aliasmbcs(): - """On Windows, some default encodings are not provided by Python, - while they are always available as "mbcs" in each locale. Make - them usable by aliasing to "mbcs" in such a case.""" - if sys.platform == "win32": - import locale, codecs - - enc = locale.getdefaultlocale()[1] - if enc.startswith("cp"): # "cp***" ? - try: - codecs.lookup(enc) - except LookupError: - import encodings - - encodings._cache[enc] = encodings._unknown - encodings.aliases.aliases[enc] = "mbcs" - - -def setencoding(): - """Set the string encoding used by the Unicode implementation. The - default is 'ascii', but if you're willing to experiment, you can - change this.""" - encoding = "ascii" # Default value set by _PyUnicode_Init() - if 0: - # Enable to support locale aware default string encodings. - import locale - - loc = locale.getdefaultlocale() - if loc[1]: - encoding = loc[1] - if 0: - # Enable to switch off string to Unicode coercion and implicit - # Unicode to string conversion. - encoding = "undefined" - if encoding != "ascii": - # On Non-Unicode builds this will raise an AttributeError... - sys.setdefaultencoding(encoding) # Needs Python Unicode build ! - - -def execsitecustomize(): - """Run custom site specific code, if available.""" - try: - import sitecustomize - except ImportError: - pass - - -def virtual_install_main_packages(): - f = open(os.path.join(os.path.dirname(__file__), "orig-prefix.txt")) - sys.real_prefix = f.read().strip() - f.close() - pos = 2 - hardcoded_relative_dirs = [] - if sys.path[0] == "": - pos += 1 - if _is_pypy: - if sys.version_info > (3, 2): - cpyver = "%d" % sys.version_info[0] - elif sys.pypy_version_info >= (1, 5): - cpyver = "%d.%d" % sys.version_info[:2] - else: - cpyver = "%d.%d.%d" % sys.version_info[:3] - paths = [os.path.join(sys.real_prefix, "lib_pypy"), os.path.join(sys.real_prefix, "lib-python", cpyver)] - if sys.pypy_version_info < (1, 9): - paths.insert(1, os.path.join(sys.real_prefix, "lib-python", "modified-%s" % cpyver)) - hardcoded_relative_dirs = paths[:] # for the special 'darwin' case below - # - # This is hardcoded in the Python executable, but relative to sys.prefix: - for path in paths[:]: - plat_path = os.path.join(path, "plat-%s" % sys.platform) - if os.path.exists(plat_path): - paths.append(plat_path) - elif sys.platform == "win32": - paths = [os.path.join(sys.real_prefix, "Lib"), os.path.join(sys.real_prefix, "DLLs")] - else: - paths = [os.path.join(sys.real_prefix, "lib", "python{}.{}".format(*sys.version_info))] - hardcoded_relative_dirs = paths[:] # for the special 'darwin' case below - lib64_path = os.path.join(sys.real_prefix, "lib64", "python{}.{}".format(*sys.version_info)) - if os.path.exists(lib64_path): - if _is_64bit: - paths.insert(0, lib64_path) - else: - paths.append(lib64_path) - # This is hardcoded in the Python executable, but relative to - # sys.prefix. Debian change: we need to add the multiarch triplet - # here, which is where the real stuff lives. As per PEP 421, in - # Python 3.3+, this lives in sys.implementation, while in Python 2.7 - # it lives in sys. - try: - arch = getattr(sys, "implementation", sys)._multiarch - except AttributeError: - # This is a non-multiarch aware Python. Fallback to the old way. - arch = sys.platform - plat_path = os.path.join(sys.real_prefix, "lib", "python{}.{}".format(*sys.version_info), "plat-%s" % arch) - if os.path.exists(plat_path): - paths.append(plat_path) - # This is hardcoded in the Python executable, but - # relative to sys.prefix, so we have to fix up: - for path in list(paths): - tk_dir = os.path.join(path, "lib-tk") - if os.path.exists(tk_dir): - paths.append(tk_dir) - - # These are hardcoded in the Apple's Python executable, - # but relative to sys.prefix, so we have to fix them up: - if sys.platform == "darwin": - hardcoded_paths = [ - os.path.join(relative_dir, module) - for relative_dir in hardcoded_relative_dirs - for module in ("plat-darwin", "plat-mac", "plat-mac/lib-scriptpackages") - ] - - for path in hardcoded_paths: - if os.path.exists(path): - paths.append(path) - - sys.path.extend(paths) - - -def force_global_eggs_after_local_site_packages(): - """ - Force easy_installed eggs in the global environment to get placed - in sys.path after all packages inside the virtualenv. This - maintains the "least surprise" result that packages in the - virtualenv always mask global packages, never the other way - around. - - """ - egginsert = getattr(sys, "__egginsert", 0) - for i, path in enumerate(sys.path): - if i > egginsert and path.startswith(sys.prefix): - egginsert = i - sys.__egginsert = egginsert + 1 - - -def virtual_addsitepackages(known_paths): - force_global_eggs_after_local_site_packages() - return addsitepackages(known_paths, sys_prefix=sys.real_prefix) - - -def execusercustomize(): - """Run custom user specific code, if available.""" - try: - import usercustomize - except ImportError: - pass - - -def enablerlcompleter(): - """Enable default readline configuration on interactive prompts, by - registering a sys.__interactivehook__. - If the readline module can be imported, the hook will set the Tab key - as completion key and register ~/.python_history as history file. - This can be overridden in the sitecustomize or usercustomize module, - or in a PYTHONSTARTUP file. - """ - - def register_readline(): - import atexit - - try: - import readline - import rlcompleter - except ImportError: - return - - # Reading the initialization (config) file may not be enough to set a - # completion key, so we set one first and then read the file. - readline_doc = getattr(readline, "__doc__", "") - if readline_doc is not None and "libedit" in readline_doc: - readline.parse_and_bind("bind ^I rl_complete") - else: - readline.parse_and_bind("tab: complete") - - try: - readline.read_init_file() - except OSError: - # An OSError here could have many causes, but the most likely one - # is that there's no .inputrc file (or .editrc file in the case of - # Mac OS X + libedit) in the expected location. In that case, we - # want to ignore the exception. - pass - - if readline.get_current_history_length() == 0: - # If no history was loaded, default to .python_history. - # The guard is necessary to avoid doubling history size at - # each interpreter exit when readline was already configured - # through a PYTHONSTARTUP hook, see: - # http://bugs.python.org/issue5845#msg198636 - history = os.path.join(os.path.expanduser("~"), ".python_history") - try: - readline.read_history_file(history) - except OSError: - pass - - def write_history(): - try: - readline.write_history_file(history) - except (FileNotFoundError, PermissionError): - # home directory does not exist or is not writable - # https://bugs.python.org/issue19891 - pass - - atexit.register(write_history) - - sys.__interactivehook__ = register_readline - - -if _is_pypy: - - def import_builtin_stuff(): - """PyPy specific: some built-in modules should be pre-imported because - some programs expect them to be in sys.modules on startup. This is ported - from PyPy's site.py. - """ - import encodings - - if "exceptions" in sys.builtin_module_names: - import exceptions - - if "zipimport" in sys.builtin_module_names: - import zipimport - - -def main(): - global ENABLE_USER_SITE - virtual_install_main_packages() - if _is_pypy: - import_builtin_stuff() - abs__file__() - paths_in_sys = removeduppaths() - if os.name == "posix" and sys.path and os.path.basename(sys.path[-1]) == "Modules": - addbuilddir() - GLOBAL_SITE_PACKAGES = not os.path.exists(os.path.join(os.path.dirname(__file__), "no-global-site-packages.txt")) - if not GLOBAL_SITE_PACKAGES: - ENABLE_USER_SITE = False - if ENABLE_USER_SITE is None: - ENABLE_USER_SITE = check_enableusersite() - paths_in_sys = addsitepackages(paths_in_sys) - paths_in_sys = addusersitepackages(paths_in_sys) - if GLOBAL_SITE_PACKAGES: - paths_in_sys = virtual_addsitepackages(paths_in_sys) - if sys.platform == "os2emx": - setBEGINLIBPATH() - setquit() - setcopyright() - sethelper() - if sys.version_info[0] == 3: - enablerlcompleter() - aliasmbcs() - setencoding() - execsitecustomize() - if ENABLE_USER_SITE: - execusercustomize() - # Remove sys.setdefaultencoding() so that users cannot change the - # encoding after initialization. The test for presence is needed when - # this module is run as a script, because this code is executed twice. - if hasattr(sys, "setdefaultencoding"): - del sys.setdefaultencoding - - -main() - - -def _script(): - help = """\ - %s [--user-base] [--user-site] - - Without arguments print some useful information - With arguments print the value of USER_BASE and/or USER_SITE separated - by '%s'. - - Exit codes with --user-base or --user-site: - 0 - user site directory is enabled - 1 - user site directory is disabled by user - 2 - uses site directory is disabled by super user - or for security reasons - >2 - unknown error - """ - args = sys.argv[1:] - if not args: - print("sys.path = [") - for dir in sys.path: - print(" {!r},".format(dir)) - print("]") - - def exists(path): - if os.path.isdir(path): - return "exists" - else: - return "doesn't exist" - - print("USER_BASE: {!r} ({})".format(USER_BASE, exists(USER_BASE))) - print("USER_SITE: {!r} ({})".format(USER_SITE, exists(USER_SITE))) - print("ENABLE_USER_SITE: %r" % ENABLE_USER_SITE) - sys.exit(0) - - buffer = [] - if "--user-base" in args: - buffer.append(USER_BASE) - if "--user-site" in args: - buffer.append(USER_SITE) - - if buffer: - print(os.pathsep.join(buffer)) - if ENABLE_USER_SITE: - sys.exit(0) - elif ENABLE_USER_SITE is False: - sys.exit(1) - elif ENABLE_USER_SITE is None: - sys.exit(2) - else: - sys.exit(3) - else: - import textwrap - - print(textwrap.dedent(help % (sys.argv[0], os.pathsep))) - sys.exit(10) - - -if __name__ == "__main__": - _script() diff --git a/virtualenv_support/wheel-0.33.6-py2.py3-none-any.whl b/virtualenv_support/wheel-0.33.6-py2.py3-none-any.whl deleted file mode 100644 index 2a71896be..000000000 Binary files a/virtualenv_support/wheel-0.33.6-py2.py3-none-any.whl and /dev/null differ