diff --git a/news/2718.behavior b/news/2718.behavior new file mode 100644 index 00000000..c00b1d46 --- /dev/null +++ b/news/2718.behavior @@ -0,0 +1 @@ +Fallback to shell mode if `run` fails with Windows error 193 to handle non-executable commands. This should improve usability on Windows, where some users run non-executable files without specifying a command, relying on Windows file association to choose the current command. diff --git a/pipenv/core.py b/pipenv/core.py index baea6373..bab12f93 100644 --- a/pipenv/core.py +++ b/pipenv/core.py @@ -2102,15 +2102,31 @@ def inline_activate_virtual_environment(): os.environ["VIRTUAL_ENV"] = root -def do_run_nt(script): +def _launch_windows_subprocess(script): import subprocess command = system_which(script.command) options = {"universal_newlines": True} - if command: # Try to use CreateProcess directly if possible. - p = subprocess.Popen([command] + script.args, **options) - else: # Command not found, maybe this is a shell built-in? - p = subprocess.Popen(script.cmdify(), shell=True, **options) + + # Command not found, maybe this is a shell built-in? + if not command: + return subprocess.Popen(script.cmdify(), shell=True, **options) + + # Try to use CreateProcess directly if possible. Specifically catch + # Windows error 193 "Command is not a valid Win32 application" to handle + # a "command" that is non-executable. See pypa/pipenv#2727. + try: + return subprocess.Popen([command] + script.args, **options) + except WindowsError as e: + if e.winerror != 193: + raise + + # Try shell mode to use Windows's file association for file launch. + return subprocess.Popen(script.cmdify(), shell=True, **options) + + +def do_run_nt(script): + p = _launch_windows_subprocess(script) p.communicate() sys.exit(p.returncode) diff --git a/pipenv/project.py b/pipenv/project.py index e32296e5..c6e9e4fa 100644 --- a/pipenv/project.py +++ b/pipenv/project.py @@ -34,6 +34,7 @@ from .utils import ( is_star, get_workon_home, is_virtual_environment, + looks_like_dir ) from .environments import ( PIPENV_MAX_DEPTH, @@ -267,7 +268,17 @@ class Project(object): def get_location_for_virtualenv(self): if self.is_venv_in_project(): return os.path.join(self.project_directory, ".venv") - return str(get_workon_home().joinpath(self.virtualenv_name)) + + name = self.virtualenv_name + if self.project_directory: + venv_path = os.path.join(self.project_directory, ".venv") + if os.path.exists(venv_path) and not os.path.isdir(".venv"): + with io.open(venv_path, "r") as f: + name = f.read().strip() + # Assume file's contents is a path if it contains slashes. + if looks_like_dir(name): + return Path(name).absolute().as_posix() + return str(get_workon_home().joinpath(name)) def get_installed_packages(self): from . import PIPENV_ROOT, PIPENV_VENDOR, PIPENV_PATCHED diff --git a/pipenv/utils.py b/pipenv/utils.py index 21a1ec20..fa7a809b 100644 --- a/pipenv/utils.py +++ b/pipenv/utils.py @@ -1376,3 +1376,8 @@ def chdir(path): yield finally: os.chdir(prev_cwd) + + +def looks_like_dir(path): + seps = (sep for sep in (os.path.sep, os.path.altsep) if sep is not None) + return any(sep in path for sep in seps) diff --git a/run-tests.sh b/run-tests.sh index 1243e033..64d79648 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -6,6 +6,7 @@ set -eo pipefail export PYTHONIOENCODING="utf-8" export LANG=C.UTF-8 +export PIP_PROCESS_DEPENDENCY_LINKS="1" prefix() { sed "s/^/ $1: /" diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 9cb4b7b1..76a429e0 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -16,6 +16,9 @@ if six.PY2: pass +HAS_WARNED_GITHUB = False + + def check_internet(): try: # Kenneth represents the Internet LGTM. @@ -40,13 +43,15 @@ def check_github_ssh(): res = True if c.return_code == 1 else False except Exception: pass - if not res: + global HAS_WARNED_GITHUB + if not res and not HAS_WARNED_GITHUB: warnings.warn( 'Cannot connect to GitHub via SSH', ResourceWarning ) warnings.warn( 'Will skip tests requiring SSH access to GitHub', ResourceWarning ) + HAS_WARNED_GITHUB = True return res diff --git a/tests/integration/test_dot_venv.py b/tests/integration/test_dot_venv.py index 2063701b..53f5fb1d 100644 --- a/tests/integration/test_dot_venv.py +++ b/tests/integration/test_dot_venv.py @@ -41,15 +41,17 @@ def test_reuse_previous_venv(PipenvInstance, pypi): assert c.return_code == 0 assert normalize_drive(p.path) in p.pipenv('--venv').out + @pytest.mark.dotvenv -def test_venv_file_exists(PipenvInstance, pypi): - """Tests virtualenv creation & package installation when a .venv file exists - at the project root. +@pytest.mark.parametrize('venv_name', ('test-venv', os.path.join('foo', 'test-venv'))) +def test_venv_file(venv_name, PipenvInstance, pypi): + """Tests virtualenv creation when a .venv file exists at the project root + and contains a venv name. """ with PipenvInstance(pypi=pypi, chdir=True) as p: file_path = os.path.join(p.path, '.venv') with open(file_path, 'w') as f: - f.write('') + f.write(venv_name) with temp_environ(), TemporaryDirectory( prefix='pipenv-', suffix='temp_workon_home' @@ -58,12 +60,43 @@ def test_venv_file_exists(PipenvInstance, pypi): if 'PIPENV_VENV_IN_PROJECT' in os.environ: del os.environ['PIPENV_VENV_IN_PROJECT'] - c = p.pipenv('install requests') + c = p.pipenv('install') assert c.return_code == 0 - venv_loc = None - for line in c.err.splitlines(): - if line.startswith('Virtualenv location:'): - venv_loc = Path(line.split(':', 1)[-1].strip()) - assert venv_loc is not None + c = p.pipenv('--venv') + assert c.return_code == 0 + venv_loc = Path(c.out.strip()).absolute() + assert venv_loc.exists() assert venv_loc.joinpath('.project').exists() + venv_path = venv_loc.as_posix() + if os.path.sep in venv_name: + venv_expected_path = Path(p.path).joinpath(venv_name).absolute().as_posix() + else: + venv_expected_path = Path(workon_home.name).joinpath(venv_name).absolute().as_posix() + assert venv_path == venv_expected_path + + +@pytest.mark.dotvenv +def test_venv_file_with_path(PipenvInstance, pypi): + """Tests virtualenv creation when a .venv file exists at the project root + and contains an absolute path. + """ + with temp_environ(), PipenvInstance(chdir=True, pypi=pypi) as p: + with TemporaryDirectory( + prefix='pipenv-', suffix='-test_venv' + ) as venv_path: + if 'PIPENV_VENV_IN_PROJECT' in os.environ: + del os.environ['PIPENV_VENV_IN_PROJECT'] + + file_path = os.path.join(p.path, '.venv') + with open(file_path, 'w') as f: + f.write(venv_path.name) + + c = p.pipenv('install') + assert c.return_code == 0 + c = p.pipenv('--venv') + assert c.return_code == 0 + venv_loc = Path(c.out.strip()) + + assert venv_loc.joinpath('.project').exists() + assert venv_loc == Path(venv_path.name) diff --git a/tests/integration/test_install_basic.py b/tests/integration/test_install_basic.py index ab6648f4..c04dc3f6 100644 --- a/tests/integration/test_install_basic.py +++ b/tests/integration/test_install_basic.py @@ -44,7 +44,7 @@ def test_basic_install(PipenvInstance, pypi): @pytest.mark.install @flaky def test_mirror_install(PipenvInstance, pypi): - with temp_environ(), PipenvInstance(chdir=True) as p: + with temp_environ(), PipenvInstance(chdir=True, pypi=pypi) as p: mirror_url = os.environ.pop( "PIPENV_TEST_INDEX", "https://pypi.python.org/simple" ) diff --git a/tests/pytest-pypi/pytest_pypi/certs.py b/tests/pytest-pypi/pytest_pypi/certs.py index c099cfa2..f9e33870 100644 --- a/tests/pytest-pypi/pytest_pypi/certs.py +++ b/tests/pytest-pypi/pytest_pypi/certs.py @@ -15,7 +15,7 @@ import os.path def where(): """Return the preferred certificate bundle.""" # vendored bundle inside Requests - return os.path.join(os.path.dirname(__file__), 'certs', 'cacert.pem') + return os.path.join(os.path.abspath(os.path.dirname(__file__)), 'certs', 'cacert.pem') if __name__ == '__main__': print(where())