diff --git a/jedi/_compatibility.py b/jedi/_compatibility.py index 8b55fb8f..3aef7eb9 100644 --- a/jedi/_compatibility.py +++ b/jedi/_compatibility.py @@ -513,3 +513,67 @@ class GeneralizedPopen(subprocess.Popen): CREATE_NO_WINDOW = 0x08000000 kwargs['creationflags'] = CREATE_NO_WINDOW super(GeneralizedPopen, self).__init__(*args, **kwargs) + + +# shutil.which is not available on Python 2.7. +def which(cmd, mode=os.F_OK | os.X_OK, path=None): + """Given a command, mode, and a PATH string, return the path which + conforms to the given mode on the PATH, or None if there is no such + file. + + `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result + of os.environ.get("PATH"), or can be overridden with a custom search + path. + + """ + # Check that a given file can be accessed with the correct mode. + # Additionally check that `file` is not a directory, as on Windows + # directories pass the os.access check. + def _access_check(fn, mode): + return (os.path.exists(fn) and os.access(fn, mode) + and not os.path.isdir(fn)) + + # If we're given a path with a directory part, look it up directly rather + # than referring to PATH directories. This includes checking relative to the + # current directory, e.g. ./script + if os.path.dirname(cmd): + if _access_check(cmd, mode): + return cmd + return None + + if path is None: + path = os.environ.get("PATH", os.defpath) + if not path: + return None + path = path.split(os.pathsep) + + if sys.platform == "win32": + # The current directory takes precedence on Windows. + if not os.curdir in path: + path.insert(0, os.curdir) + + # PATHEXT is necessary to check on Windows. + pathext = os.environ.get("PATHEXT", "").split(os.pathsep) + # See if the given file matches any of the expected path extensions. + # This will allow us to short circuit when given "python.exe". + # If it does match, only test that one, otherwise we have to try + # others. + if any(cmd.lower().endswith(ext.lower()) for ext in pathext): + files = [cmd] + else: + files = [cmd + ext for ext in pathext] + else: + # On other platforms you don't have things like PATHEXT to tell you + # what file suffixes are executable, so just pass on cmd as-is. + files = [cmd] + + seen = set() + for dir in path: + normdir = os.path.normcase(dir) + if not normdir in seen: + seen.add(normdir) + for thefile in files: + name = os.path.join(dir, thefile) + if _access_check(name, mode): + return name + return None diff --git a/jedi/api/environment.py b/jedi/api/environment.py index d67427bd..e866b888 100644 --- a/jedi/api/environment.py +++ b/jedi/api/environment.py @@ -9,11 +9,8 @@ import hashlib import filecmp from subprocess import PIPE from collections import namedtuple -# When dropping Python 2.7 support we should consider switching to -# `shutil.which`. -from distutils.spawn import find_executable -from jedi._compatibility import GeneralizedPopen +from jedi._compatibility import GeneralizedPopen, which from jedi.cache import memoize_method, time_cache from jedi.evaluate.compiled.subprocess import get_subprocess, \ EvaluatorSameProcess, EvaluatorSubprocess @@ -276,7 +273,7 @@ def get_system_environment(version): :raises: :exc:`.InvalidPythonEnvironment` :returns: :class:`Environment` """ - exe = find_executable('python' + version) + exe = which('python' + version) if exe: if exe == sys.executable: return SameEnvironment()