Skip to content

Commit

Permalink
start making the app-data more explicit and robust
Browse files Browse the repository at this point in the history
Signed-off-by: Bernat Gabor <[email protected]>
  • Loading branch information
gaborbernat committed Feb 25, 2020
1 parent a9277cc commit 107c479
Show file tree
Hide file tree
Showing 24 changed files with 199 additions and 161 deletions.
3 changes: 2 additions & 1 deletion src/virtualenv/config/cli/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def __init__(self, options=None, *args, **kwargs):
self._verbosity = None
self._options = options
self._interpreter = None
self._app_data = None

def _fix_defaults(self):
for action in self._actions:
Expand Down Expand Up @@ -56,7 +57,7 @@ def parse_args(self, args=None, namespace=None):

class HelpFormatter(ArgumentDefaultsHelpFormatter):
def __init__(self, prog):
super(HelpFormatter, self).__init__(prog, max_help_position=35, width=240)
super(HelpFormatter, self).__init__(prog, max_help_position=32, width=240)

def _get_help_string(self, action):
# noinspection PyProtectedMember
Expand Down
10 changes: 8 additions & 2 deletions src/virtualenv/config/ini.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import logging
import os

from virtualenv.dirs import default_config_dir
from appdirs import user_config_dir

from virtualenv.info import PY3
from virtualenv.util import ConfigParser
from virtualenv.util.path import Path
Expand All @@ -21,7 +22,12 @@ class IniConfig(object):
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_dir() / "virtualenv.ini")
config_file = (
Path(config_file)
if config_file is not None
else Path(user_config_dir(appname="virtualenv", appauthor="pypa")) / "virtualenv.ini"
)
self.config_file = config_file
self._cache = {}

exception = None
Expand Down
9 changes: 5 additions & 4 deletions src/virtualenv/create/creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def __init__(self, options, interpreter):
self.dest = Path(options.dest)
self.clear = options.clear
self.pyenv_cfg = PyEnvCfg.from_folder(self.dest)
self.app_data = options.app_data.folder

def __repr__(self):
return ensure_str(self.__unicode__())
Expand All @@ -65,7 +66,7 @@ def can_create(cls, interpreter):
return True

@classmethod
def add_parser_arguments(cls, parser, interpreter, meta):
def add_parser_arguments(cls, parser, interpreter, meta, app_data):
"""Add CLI arguments for the creator.
:param parser: the CLI parser
Expand Down Expand Up @@ -172,19 +173,19 @@ def debug(self):
:return: debug information about the virtual environment (only valid after :meth:`create` has run)
"""
if self._debug is None and self.exe is not None:
self._debug = get_env_debug_info(self.exe, self.debug_script())
self._debug = get_env_debug_info(self.exe, self.debug_script(), self.app_data)
return self._debug

# noinspection PyMethodMayBeStatic
def debug_script(self):
return DEBUG_SCRIPT


def get_env_debug_info(env_exe, debug_script):
def get_env_debug_info(env_exe, debug_script, app_data):
env = os.environ.copy()
env.pop(str("PYTHONPATH"), None)

with ensure_file_on_disk(debug_script) as debug_script:
with ensure_file_on_disk(debug_script, app_data) as debug_script:
cmd = [str(env_exe), str(debug_script)]
if WIN_CPYTHON_2:
cmd = [ensure_text(i) for i in cmd]
Expand Down
6 changes: 3 additions & 3 deletions src/virtualenv/create/via_global_ref/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ def __init__(self, options, interpreter):
self.enable_system_site_package = options.system_site

@classmethod
def add_parser_arguments(cls, parser, interpreter, meta):
super(ViaGlobalRefApi, cls).add_parser_arguments(parser, interpreter, meta)
def add_parser_arguments(cls, parser, interpreter, meta, app_data):
super(ViaGlobalRefApi, cls).add_parser_arguments(parser, interpreter, meta, app_data)
parser.add_argument(
"--system-site-packages",
default=False,
Expand Down Expand Up @@ -54,7 +54,7 @@ def create(self):
def patch_distutils_via_pth(self):
"""Patch the distutils package to not be derailed by its configuration files"""
patch_file = Path(__file__).parent / "_distutils_patch_virtualenv.py"
with ensure_file_on_disk(patch_file) as resolved_path:
with ensure_file_on_disk(patch_file, self.app_data) as resolved_path:
text = resolved_path.read_text()
text = text.replace('"__SCRIPT_DIR__"', repr(os.path.relpath(str(self.script_dir), str(self.purelib))))
patch_path = self.purelib / "_distutils_patch_virtualenv.py"
Expand Down
56 changes: 0 additions & 56 deletions src/virtualenv/dirs.py

This file was deleted.

17 changes: 9 additions & 8 deletions src/virtualenv/discovery/builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class Builtin(Discover):
def __init__(self, options):
super(Builtin, self).__init__(options)
self.python_spec = options.python
self.app_data = options.app_data

@classmethod
def add_parser_arguments(cls, parser):
Expand All @@ -29,7 +30,7 @@ def add_parser_arguments(cls, parser):
)

def run(self):
return get_interpreter(self.python_spec)
return get_interpreter(self.python_spec, self.app_data.folder)

def __repr__(self):
return ensure_str(self.__unicode__())
Expand All @@ -38,11 +39,11 @@ def __unicode__(self):
return "{} discover of python_spec={!r}".format(self.__class__.__name__, self.python_spec)


def get_interpreter(key):
def get_interpreter(key, cache_dir=None):
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):
for interpreter, impl_must_match in propose_interpreters(spec, cache_dir):
key = interpreter.system_executable, impl_must_match
if key in proposed_paths:
continue
Expand All @@ -53,19 +54,19 @@ def get_interpreter(key):
proposed_paths.add(key)


def propose_interpreters(spec):
def propose_interpreters(spec, cache_dir):
# 1. 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
yield PythonInfo.from_exe(spec.path, cache_dir), True

# 2. try with the current
yield PythonInfo.current_system(), True
yield PythonInfo.current_system(cache_dir), True

# 3. otherwise fallback to platform default logic
if IS_WIN:
from .windows import propose_interpreters

for interpreter in propose_interpreters(spec):
for interpreter in propose_interpreters(spec, cache_dir):
yield interpreter, True

paths = get_paths()
Expand All @@ -80,7 +81,7 @@ def propose_interpreters(spec):
exe = os.path.abspath(found)
if exe not in tested_exes:
tested_exes.add(exe)
interpreter = PathPythonInfo.from_exe(exe, raise_on_error=False)
interpreter = PathPythonInfo.from_exe(exe, cache_dir, raise_on_error=False)
if interpreter is not None:
yield interpreter, match

Expand Down
35 changes: 13 additions & 22 deletions src/virtualenv/discovery/cached_py_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,21 @@
from collections import OrderedDict
from hashlib import sha256

from virtualenv.dirs import default_data_dir
from virtualenv.discovery.py_info import PythonInfo
from virtualenv.info import PY2, PY3
from virtualenv.util.path import Path
from virtualenv.util.six import ensure_text
from virtualenv.util.subprocess import Popen, subprocess
from virtualenv.util.zipapp import ensure_file_on_disk
from virtualenv.version import __version__

_CACHE = OrderedDict()
_CACHE[Path(sys.executable)] = PythonInfo()
_FS_PATH = None


def from_exe(cls, exe, raise_on_error=True, ignore_cache=False):
def from_exe(cls, cache_dir, exe, raise_on_error=True, ignore_cache=False):
""""""
result = _get_from_cache(cls, exe, ignore_cache=ignore_cache)
result = _get_from_cache(cls, cache_dir, exe, ignore_cache=ignore_cache)
if isinstance(result, Exception):
if raise_on_error:
raise result
Expand All @@ -40,30 +38,31 @@ def from_exe(cls, exe, raise_on_error=True, ignore_cache=False):
return result


def _get_from_cache(cls, exe, ignore_cache=True):
def _get_from_cache(cls, cache_dir, exe, ignore_cache=True):
# note here we cannot resolve symlinks, as the symlink may trigger different prefix information if there's a
# pyenv.cfg somewhere alongside on python3.4+
exe_path = Path(exe)
if not ignore_cache and exe_path in _CACHE: # check in the in-memory cache
result = _CACHE[exe_path]
elif cache_dir is None: # cache disabled
failure, py_info = _run_subprocess(cls, exe, cache_dir)
result = py_info if failure is None else failure
else: # then check the persisted cache
py_info = _get_via_file_cache(cls, exe_path, exe)
py_info = _get_via_file_cache(cls, cache_dir, exe_path, exe)
result = _CACHE[exe_path] = py_info
# independent if it was from the file or in-memory cache fix the original executable location
if isinstance(result, PythonInfo):
result.executable = exe
return result


def _get_via_file_cache(cls, resolved_path, exe):
def _get_via_file_cache(cls, cache_dir, resolved_path, exe):
key = sha256(str(resolved_path).encode("utf-8") if PY3 else str(resolved_path)).hexdigest()
py_info = None
resolved_path_text = ensure_text(str(resolved_path))
resolved_path_modified_timestamp = resolved_path.stat().st_mtime
fs_path = _get_fs_path()
data_file = fs_path / "{}.json".format(key)

with fs_path.lock_for_key(key):
data_file = cache_dir / "{}.json".format(key)
with cache_dir.lock_for_key(key):
data_file_path = data_file.path
if data_file_path.exists(): # if exists and matches load
try:
Expand Down Expand Up @@ -91,16 +90,9 @@ def _get_via_file_cache(cls, resolved_path, exe):
return py_info


def _get_fs_path():
global _FS_PATH
if _FS_PATH is None:
_FS_PATH = default_data_dir() / "py-info" / __version__
return _FS_PATH


def _run_subprocess(cls, exe):
def _run_subprocess(cls, exe, app_data):
resolved_path = Path(os.path.abspath(__file__)).parent / "py_info.py"
with ensure_file_on_disk(resolved_path) as resolved_path:
with ensure_file_on_disk(resolved_path, app_data) as resolved_path:

cmd = [exe, "-s", str(resolved_path)]
# prevent sys.prefix from leaking into the child process - see https://bugs.python.org/issue22490
Expand Down Expand Up @@ -156,8 +148,7 @@ def __unicode__(self):
return raw


def clear():
fs_path = _get_fs_path()
def clear(fs_path):
with fs_path:
for filename in fs_path.path.iterdir():
if filename.suffix == ".json":
Expand Down
12 changes: 6 additions & 6 deletions src/virtualenv/discovery/py_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,11 @@ def spec(self):
return "{}{}-{}".format(self.implementation, ".".join(str(i) for i in self.version_info), self.architecture)

@classmethod
def clear_cache(cls):
def clear_cache(cls, fs_path):
# this method is not used by itself, so here and called functions can import stuff locally
from virtualenv.discovery.cached_py_info import clear

clear()
clear(fs_path)
cls._cache_exe_discovery.clear()

def satisfies(self, spec, impl_must_match):
Expand Down Expand Up @@ -263,13 +263,13 @@ def current(cls):
return cls._current

@classmethod
def current_system(cls):
def current_system(cls, cache_dir=None):
"""
This locates the current host interpreter information. This might be different than what we run into in case
the host python has been upgraded from underneath us.
"""
if cls._current_system is None:
cls._current_system = cls.from_exe(sys.executable, raise_on_error=True, resolve_to_host=True)
cls._current_system = cls.from_exe(sys.executable, cache_dir, raise_on_error=True, resolve_to_host=True)
return cls._current_system

def _to_json(self):
Expand All @@ -283,12 +283,12 @@ def _to_dict(self):
return data

@classmethod
def from_exe(cls, exe, raise_on_error=True, ignore_cache=False, resolve_to_host=True):
def from_exe(cls, exe, cache_dir=None, raise_on_error=True, ignore_cache=False, resolve_to_host=True):
"""Given a path to an executable get the python information"""
# this method is not used by itself, so here and called functions can import stuff locally
from virtualenv.discovery.cached_py_info import from_exe

proposed = from_exe(cls, exe, raise_on_error=raise_on_error, ignore_cache=ignore_cache)
proposed = from_exe(cls, cache_dir, exe, raise_on_error=raise_on_error, ignore_cache=ignore_cache)
# noinspection PyProtectedMember
if isinstance(proposed, PythonInfo) and resolve_to_host:
proposed = proposed._resolve_to_system(proposed)
Expand Down
4 changes: 2 additions & 2 deletions src/virtualenv/discovery/windows/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ class Pep514PythonInfo(PythonInfo):
""""""


def propose_interpreters(spec):
def propose_interpreters(spec, cache_dir):
# 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)
interpreter = Pep514PythonInfo.from_exe(exe, cache_dir, raise_on_error=False)
if interpreter is not None:
if interpreter.satisfies(spec, impl_must_match=True):
yield interpreter
Loading

0 comments on commit 107c479

Please sign in to comment.