|
""" |
|
Environments are a way to activate different Python versions or Virtualenvs for |
|
static analysis. The Python binary in that environment is going to be executed. |
|
""" |
|
import os |
|
import sys |
|
import hashlib |
|
import filecmp |
|
from collections import namedtuple |
|
from shutil import which |
|
|
|
from jedi.cache import memoize_method, time_cache |
|
from jedi.inference.compiled.subprocess import CompiledSubprocess, \ |
|
InferenceStateSameProcess, InferenceStateSubprocess |
|
|
|
import parso |
|
|
|
_VersionInfo = namedtuple('VersionInfo', 'major minor micro') |
|
|
|
_SUPPORTED_PYTHONS = ['3.12', '3.11', '3.10', '3.9', '3.8', '3.7', '3.6'] |
|
_SAFE_PATHS = ['/usr/bin', '/usr/local/bin'] |
|
_CONDA_VAR = 'CONDA_PREFIX' |
|
_CURRENT_VERSION = '%s.%s' % (sys.version_info.major, sys.version_info.minor) |
|
|
|
|
|
class InvalidPythonEnvironment(Exception): |
|
""" |
|
If you see this exception, the Python executable or Virtualenv you have |
|
been trying to use is probably not a correct Python version. |
|
""" |
|
|
|
|
|
class _BaseEnvironment: |
|
@memoize_method |
|
def get_grammar(self): |
|
version_string = '%s.%s' % (self.version_info.major, self.version_info.minor) |
|
return parso.load_grammar(version=version_string) |
|
|
|
@property |
|
def _sha256(self): |
|
try: |
|
return self._hash |
|
except AttributeError: |
|
self._hash = _calculate_sha256_for_file(self.executable) |
|
return self._hash |
|
|
|
|
|
def _get_info(): |
|
return ( |
|
sys.executable, |
|
sys.prefix, |
|
sys.version_info[:3], |
|
) |
|
|
|
|
|
class Environment(_BaseEnvironment): |
|
""" |
|
This class is supposed to be created by internal Jedi architecture. You |
|
should not create it directly. Please use create_environment or the other |
|
functions instead. It is then returned by that function. |
|
""" |
|
_subprocess = None |
|
|
|
def __init__(self, executable, env_vars=None): |
|
self._start_executable = executable |
|
self._env_vars = env_vars |
|
|
|
self._get_subprocess() |
|
|
|
def _get_subprocess(self): |
|
if self._subprocess is not None and not self._subprocess.is_crashed: |
|
return self._subprocess |
|
|
|
try: |
|
self._subprocess = CompiledSubprocess(self._start_executable, |
|
env_vars=self._env_vars) |
|
info = self._subprocess._send(None, _get_info) |
|
except Exception as exc: |
|
raise InvalidPythonEnvironment( |
|
"Could not get version information for %r: %r" % ( |
|
self._start_executable, |
|
exc)) |
|
|
|
|
|
|
|
self.executable = info[0] |
|
""" |
|
The Python executable, matches ``sys.executable``. |
|
""" |
|
self.path = info[1] |
|
""" |
|
The path to an environment, matches ``sys.prefix``. |
|
""" |
|
self.version_info = _VersionInfo(*info[2]) |
|
""" |
|
Like :data:`sys.version_info`: a tuple to show the current |
|
Environment's Python version. |
|
""" |
|
return self._subprocess |
|
|
|
def __repr__(self): |
|
version = '.'.join(str(i) for i in self.version_info) |
|
return '<%s: %s in %s>' % (self.__class__.__name__, version, self.path) |
|
|
|
def get_inference_state_subprocess(self, inference_state): |
|
return InferenceStateSubprocess(inference_state, self._get_subprocess()) |
|
|
|
@memoize_method |
|
def get_sys_path(self): |
|
""" |
|
The sys path for this environment. Does not include potential |
|
modifications from e.g. appending to :data:`sys.path`. |
|
|
|
:returns: list of str |
|
""" |
|
|
|
|
|
|
|
|
|
|
|
return self._get_subprocess().get_sys_path() |
|
|
|
|
|
class _SameEnvironmentMixin: |
|
def __init__(self): |
|
self._start_executable = self.executable = sys.executable |
|
self.path = sys.prefix |
|
self.version_info = _VersionInfo(*sys.version_info[:3]) |
|
self._env_vars = None |
|
|
|
|
|
class SameEnvironment(_SameEnvironmentMixin, Environment): |
|
pass |
|
|
|
|
|
class InterpreterEnvironment(_SameEnvironmentMixin, _BaseEnvironment): |
|
def get_inference_state_subprocess(self, inference_state): |
|
return InferenceStateSameProcess(inference_state) |
|
|
|
def get_sys_path(self): |
|
return sys.path |
|
|
|
|
|
def _get_virtual_env_from_var(env_var='VIRTUAL_ENV'): |
|
"""Get virtualenv environment from VIRTUAL_ENV environment variable. |
|
|
|
It uses `safe=False` with ``create_environment``, because the environment |
|
variable is considered to be safe / controlled by the user solely. |
|
""" |
|
var = os.environ.get(env_var) |
|
if var: |
|
|
|
|
|
|
|
|
|
if os.path.realpath(var) == os.path.realpath(sys.prefix): |
|
return _try_get_same_env() |
|
|
|
try: |
|
return create_environment(var, safe=False) |
|
except InvalidPythonEnvironment: |
|
pass |
|
|
|
|
|
def _calculate_sha256_for_file(path): |
|
sha256 = hashlib.sha256() |
|
with open(path, 'rb') as f: |
|
for block in iter(lambda: f.read(filecmp.BUFSIZE), b''): |
|
sha256.update(block) |
|
return sha256.hexdigest() |
|
|
|
|
|
def get_default_environment(): |
|
""" |
|
Tries to return an active Virtualenv or conda environment. |
|
If there is no VIRTUAL_ENV variable or no CONDA_PREFIX variable set |
|
set it will return the latest Python version installed on the system. This |
|
makes it possible to use as many new Python features as possible when using |
|
autocompletion and other functionality. |
|
|
|
:returns: :class:`.Environment` |
|
""" |
|
virtual_env = _get_virtual_env_from_var() |
|
if virtual_env is not None: |
|
return virtual_env |
|
|
|
conda_env = _get_virtual_env_from_var(_CONDA_VAR) |
|
if conda_env is not None: |
|
return conda_env |
|
|
|
return _try_get_same_env() |
|
|
|
|
|
def _try_get_same_env(): |
|
env = SameEnvironment() |
|
if not os.path.basename(env.executable).lower().startswith('python'): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if os.name == 'nt': |
|
|
|
|
|
checks = (r'Scripts\python.exe', 'python.exe') |
|
else: |
|
|
|
checks = ( |
|
'bin/python%s.%s' % (sys.version_info[0], sys.version[1]), |
|
'bin/python%s' % (sys.version_info[0]), |
|
'bin/python', |
|
) |
|
for check in checks: |
|
guess = os.path.join(sys.exec_prefix, check) |
|
if os.path.isfile(guess): |
|
|
|
return Environment(guess) |
|
|
|
return InterpreterEnvironment() |
|
|
|
|
|
return env |
|
|
|
|
|
def get_cached_default_environment(): |
|
var = os.environ.get('VIRTUAL_ENV') or os.environ.get(_CONDA_VAR) |
|
environment = _get_cached_default_environment() |
|
|
|
|
|
|
|
|
|
|
|
if var and os.path.realpath(var) != os.path.realpath(environment.path): |
|
_get_cached_default_environment.clear_cache() |
|
return _get_cached_default_environment() |
|
return environment |
|
|
|
|
|
@time_cache(seconds=10 * 60) |
|
def _get_cached_default_environment(): |
|
try: |
|
return get_default_environment() |
|
except InvalidPythonEnvironment: |
|
|
|
|
|
|
|
|
|
return InterpreterEnvironment() |
|
|
|
|
|
def find_virtualenvs(paths=None, *, safe=True, use_environment_vars=True): |
|
""" |
|
:param paths: A list of paths in your file system to be scanned for |
|
Virtualenvs. It will search in these paths and potentially execute the |
|
Python binaries. |
|
:param safe: Default True. In case this is False, it will allow this |
|
function to execute potential `python` environments. An attacker might |
|
be able to drop an executable in a path this function is searching by |
|
default. If the executable has not been installed by root, it will not |
|
be executed. |
|
:param use_environment_vars: Default True. If True, the VIRTUAL_ENV |
|
variable will be checked if it contains a valid VirtualEnv. |
|
CONDA_PREFIX will be checked to see if it contains a valid conda |
|
environment. |
|
|
|
:yields: :class:`.Environment` |
|
""" |
|
if paths is None: |
|
paths = [] |
|
|
|
_used_paths = set() |
|
|
|
if use_environment_vars: |
|
|
|
|
|
virtual_env = _get_virtual_env_from_var() |
|
if virtual_env is not None: |
|
yield virtual_env |
|
_used_paths.add(virtual_env.path) |
|
|
|
conda_env = _get_virtual_env_from_var(_CONDA_VAR) |
|
if conda_env is not None: |
|
yield conda_env |
|
_used_paths.add(conda_env.path) |
|
|
|
for directory in paths: |
|
if not os.path.isdir(directory): |
|
continue |
|
|
|
directory = os.path.abspath(directory) |
|
for path in os.listdir(directory): |
|
path = os.path.join(directory, path) |
|
if path in _used_paths: |
|
|
|
continue |
|
_used_paths.add(path) |
|
|
|
try: |
|
executable = _get_executable_path(path, safe=safe) |
|
yield Environment(executable) |
|
except InvalidPythonEnvironment: |
|
pass |
|
|
|
|
|
def find_system_environments(*, env_vars=None): |
|
""" |
|
Ignores virtualenvs and returns the Python versions that were installed on |
|
your system. This might return nothing, if you're running Python e.g. from |
|
a portable version. |
|
|
|
The environments are sorted from latest to oldest Python version. |
|
|
|
:yields: :class:`.Environment` |
|
""" |
|
for version_string in _SUPPORTED_PYTHONS: |
|
try: |
|
yield get_system_environment(version_string, env_vars=env_vars) |
|
except InvalidPythonEnvironment: |
|
pass |
|
|
|
|
|
|
|
|
|
def get_system_environment(version, *, env_vars=None): |
|
""" |
|
Return the first Python environment found for a string of the form 'X.Y' |
|
where X and Y are the major and minor versions of Python. |
|
|
|
:raises: :exc:`.InvalidPythonEnvironment` |
|
:returns: :class:`.Environment` |
|
""" |
|
exe = which('python' + version) |
|
if exe: |
|
if exe == sys.executable: |
|
return SameEnvironment() |
|
return Environment(exe) |
|
|
|
if os.name == 'nt': |
|
for exe in _get_executables_from_windows_registry(version): |
|
try: |
|
return Environment(exe, env_vars=env_vars) |
|
except InvalidPythonEnvironment: |
|
pass |
|
raise InvalidPythonEnvironment("Cannot find executable python%s." % version) |
|
|
|
|
|
def create_environment(path, *, safe=True, env_vars=None): |
|
""" |
|
Make it possible to manually create an Environment object by specifying a |
|
Virtualenv path or an executable path and optional environment variables. |
|
|
|
:raises: :exc:`.InvalidPythonEnvironment` |
|
:returns: :class:`.Environment` |
|
""" |
|
if os.path.isfile(path): |
|
_assert_safe(path, safe) |
|
return Environment(path, env_vars=env_vars) |
|
return Environment(_get_executable_path(path, safe=safe), env_vars=env_vars) |
|
|
|
|
|
def _get_executable_path(path, safe=True): |
|
""" |
|
Returns None if it's not actually a virtual env. |
|
""" |
|
|
|
if os.name == 'nt': |
|
python = os.path.join(path, 'Scripts', 'python.exe') |
|
else: |
|
python = os.path.join(path, 'bin', 'python') |
|
if not os.path.exists(python): |
|
raise InvalidPythonEnvironment("%s seems to be missing." % python) |
|
|
|
_assert_safe(python, safe) |
|
return python |
|
|
|
|
|
def _get_executables_from_windows_registry(version): |
|
import winreg |
|
|
|
|
|
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 exe |
|
except WindowsError: |
|
pass |
|
|
|
|
|
def _assert_safe(executable_path, safe): |
|
if safe and not _is_safe(executable_path): |
|
raise InvalidPythonEnvironment( |
|
"The python binary is potentially unsafe.") |
|
|
|
|
|
def _is_safe(executable_path): |
|
|
|
|
|
real_path = os.path.realpath(executable_path) |
|
|
|
if _is_unix_safe_simple(real_path): |
|
return True |
|
|
|
|
|
|
|
|
|
for environment in find_system_environments(): |
|
if environment.executable == real_path: |
|
return True |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if environment._sha256 == _calculate_sha256_for_file(real_path): |
|
return True |
|
return False |
|
|
|
|
|
def _is_unix_safe_simple(real_path): |
|
if _is_unix_admin(): |
|
|
|
|
|
return any(real_path.startswith(p) for p in _SAFE_PATHS) |
|
|
|
uid = os.stat(real_path).st_uid |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return uid == 0 |
|
|
|
|
|
def _is_unix_admin(): |
|
try: |
|
return os.getuid() == 0 |
|
except AttributeError: |
|
return False |
|
|