diff --git a/appveyor.yml b/appveyor.yml index 788b00cc..9540ee04 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,83 +2,83 @@ environment: matrix: - TOXENV: py27 PYTHON_PATH: C:\Python27 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python27\python.exe + JEDI_TEST_ENVIRONMENT: 27 - TOXENV: py27 PYTHON_PATH: C:\Python27 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python33\python.exe + JEDI_TEST_ENVIRONMENT: 33 - TOXENV: py27 PYTHON_PATH: C:\Python27 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python34\python.exe + JEDI_TEST_ENVIRONMENT: 34 - TOXENV: py27 PYTHON_PATH: C:\Python27 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python35\python.exe + JEDI_TEST_ENVIRONMENT: 35 - TOXENV: py27 PYTHON_PATH: C:\Python27 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python36\python.exe + JEDI_TEST_ENVIRONMENT: 36 - TOXENV: py33 PYTHON_PATH: C:\Python33 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python27\python.exe + JEDI_TEST_ENVIRONMENT: 27 - TOXENV: py33 PYTHON_PATH: C:\Python33 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python33\python.exe + JEDI_TEST_ENVIRONMENT: 33 - TOXENV: py33 PYTHON_PATH: C:\Python33 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python34\python.exe + JEDI_TEST_ENVIRONMENT: 34 - TOXENV: py33 PYTHON_PATH: C:\Python33 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python35\python.exe + JEDI_TEST_ENVIRONMENT: 35 - TOXENV: py33 PYTHON_PATH: C:\Python33 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python36\python.exe + JEDI_TEST_ENVIRONMENT: 36 - TOXENV: py34 PYTHON_PATH: C:\Python34 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python27\python.exe + JEDI_TEST_ENVIRONMENT: 27 - TOXENV: py34 PYTHON_PATH: C:\Python34 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python33\python.exe + JEDI_TEST_ENVIRONMENT: 33 - TOXENV: py34 PYTHON_PATH: C:\Python34 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python34\python.exe + JEDI_TEST_ENVIRONMENT: 34 - TOXENV: py34 PYTHON_PATH: C:\Python34 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python35\python.exe + JEDI_TEST_ENVIRONMENT: 35 - TOXENV: py34 PYTHON_PATH: C:\Python34 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python36\python.exe + JEDI_TEST_ENVIRONMENT: 36 - TOXENV: py35 PYTHON_PATH: C:\Python35 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python27\python.exe + JEDI_TEST_ENVIRONMENT: 27 - TOXENV: py35 PYTHON_PATH: C:\Python35 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python33\python.exe + JEDI_TEST_ENVIRONMENT: 33 - TOXENV: py35 PYTHON_PATH: C:\Python35 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python34\python.exe + JEDI_TEST_ENVIRONMENT: 34 - TOXENV: py35 PYTHON_PATH: C:\Python35 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python35\python.exe + JEDI_TEST_ENVIRONMENT: 35 - TOXENV: py35 PYTHON_PATH: C:\Python35 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python36\python.exe + JEDI_TEST_ENVIRONMENT: 36 - TOXENV: py36 PYTHON_PATH: C:\Python36 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python27\python.exe + JEDI_TEST_ENVIRONMENT: 27 - TOXENV: py36 PYTHON_PATH: C:\Python36 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python33\python.exe + JEDI_TEST_ENVIRONMENT: 33 - TOXENV: py36 PYTHON_PATH: C:\Python36 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python34\python.exe + JEDI_TEST_ENVIRONMENT: 34 - TOXENV: py36 PYTHON_PATH: C:\Python36 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python35\python.exe + JEDI_TEST_ENVIRONMENT: 35 - TOXENV: py36 PYTHON_PATH: C:\Python36 - JEDI_TEST_ENVIRONMENT_EXECUTABLE: C:\Python36\python.exe + JEDI_TEST_ENVIRONMENT: 36 install: - set PATH=%PYTHON_PATH%;%PYTHON_PATH%\Scripts;%PATH% - pip install --disable-pip-version-check --user --upgrade pip diff --git a/conftest.py b/conftest.py index c3ee3789..ae888af1 100644 --- a/conftest.py +++ b/conftest.py @@ -87,10 +87,6 @@ def clean_jedi_cache(request): @pytest.fixture(scope='session') def environment(request): - python = os.environ.get('JEDI_TEST_ENVIRONMENT_EXECUTABLE') - if python: - return get_python_environment(python) - version = request.config.option.env if version is None: version = os.environ.get('JEDI_TEST_ENVIRONMENT', str(py_version)) diff --git a/jedi/api/environment.py b/jedi/api/environment.py index 831ef19a..cd12702f 100644 --- a/jedi/api/environment.py +++ b/jedi/api/environment.py @@ -183,12 +183,41 @@ def find_python_environments(): pass -def get_python_environment(python_name): - exe = find_executable(python_name) - if exe is None: - raise InvalidPythonEnvironment("This executable doesn't exist.") - path = os.path.dirname(os.path.dirname(exe)) - return Environment(path, exe) +# TODO: the logic to find the Python prefix is much more complicated than that. +# See Modules/getpath.c for UNIX and PC/getpathp.c for Windows in CPython's +# source code. A solution would be to deduce it by running the Python +# interpreter and printing the value of sys.prefix. +def _get_python_prefix(executable): + if os.name != 'nt': + return os.path.dirname(os.path.dirname(executable)) + landmark = os.path.join('Lib', 'os.py') + prefix = os.path.dirname(executable) + while prefix: + if os.path.join(prefix, landmark): + return prefix + prefix = os.path.dirname(prefix) + raise InvalidPythonEnvironment( + "Cannot find prefix of executable %s." % executable) + + +# TODO: this function should probably return a list of environments since +# multiple Python installations can be found on a system for the same version. +def get_python_environment(python): + """ + Return the first Python environment found for a given path or for a string + of the form 'pythonX.Y' where X and Y are the major and minor versions of + Python. + """ + exe = find_executable(python) + if exe: + return Environment(_get_python_prefix(exe), exe) + if os.name == 'nt': + match = re.search('python(\d+\.\d+)$', python) + if match: + version = match.group(1) + for prefix, exe in _get_executables_from_windows_registry(version): + return Environment(prefix, exe) + raise InvalidPythonEnvironment("Cannot find executable %s." % python) def create_environment(path): @@ -226,6 +255,33 @@ def _get_executable_path(path, safe=True): return python +def _get_executables_from_windows_registry(version): + # The winreg module is named _winreg on Python 2. + try: + import winreg + except ImportError: + import _winreg as winreg + + # TODO: support Python Anaconda. + sub_keys = [ + r'SOFTWARE\Python\PythonCore\{version}\InstallPath', + r'SOFTWARE\Wow6432Node\Python\PythonCore\{version}\InstallPath', + r'SOFTWARE\Python\PythonCore\{version}-32\InstallPath', + r'SOFTWARE\Wow6432Node\Python\PythonCore\{version}-32\InstallPath' + ] + for root_key in [winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE]: + for sub_key in sub_keys: + sub_key = sub_key.format(version=version) + try: + with winreg.OpenKey(root_key, sub_key) as key: + prefix = winreg.QueryValueEx(key, '')[0] + exe = os.path.join(prefix, 'python.exe') + if os.path.isfile(exe): + yield prefix, exe + except WindowsError: + pass + + def _is_safe(executable_path): real_path = os.path.realpath(executable_path) if _is_admin(): diff --git a/test/test_api/test_environment.py b/test/test_api/test_environment.py index 8105f2b7..dda82068 100644 --- a/test/test_api/test_environment.py +++ b/test/test_api/test_environment.py @@ -1,13 +1,12 @@ import os from contextlib import contextmanager -from distutils.spawn import find_executable import pytest import jedi from jedi._compatibility import py_version -from jedi.api.environment import Environment, get_default_environment, \ - InvalidPythonEnvironment, find_python_environments, find_virtualenvs +from jedi.api.environment import get_default_environment, \ + InvalidPythonEnvironment, find_python_environments, get_python_environment def test_sys_path(): @@ -24,27 +23,21 @@ def test_find_python_environments(): assert parser_version[:2] == env.version_info[:2] -# Cannot deduce the environment from Python executable name on Windows. -@pytest.mark.skipif("os.name == 'nt'") @pytest.mark.parametrize( 'version', ['2.7', '3.3', '3.4', '3.5', '3.6', '3.7'] ) def test_versions(version): - executable_name = 'python' + version - executable = find_executable(executable_name) - if executable is None: - executable = executable_name try: - env = Environment('some path', executable) + env = get_python_environment('python' + version) except InvalidPythonEnvironment: if int(version.replace('.', '')) == py_version: # At least the current version has to work raise - return + pytest.skip() - sys_path = env.get_sys_path() - assert any(executable_name in p for p in sys_path) + assert version == str(env.version_info[0]) + '.' + str(env.version_info[1]) + assert env.get_sys_path() def test_load_module(evaluator):