back to Claude Sonnet 3.5 - Fill-in summary
Claude Sonnet 3.5 - Fill-in: virtualenv
Failed to run pytests for test tests
ImportError while loading conftest '/testbed/tests/conftest.py'.
tests/conftest.py:14: in <module>
from virtualenv.app_data import AppDataDiskFolder
src/virtualenv/__init__.py:3: in <module>
from .run import cli_run, session_via_cli
src/virtualenv/run/__init__.py:7: in <module>
from virtualenv.app_data import make_app_data
src/virtualenv/app_data/__init__.py:11: in <module>
from .read_only import ReadOnlyAppData
src/virtualenv/app_data/read_only.py:4: in <module>
from .via_disk_folder import AppDataDiskFolder, PyInfoStoreDisk
src/virtualenv/app_data/via_disk_folder.py:31: in <module>
from virtualenv.util.path import safe_delete
src/virtualenv/util/path/__init__.py:3: in <module>
from ._permission import make_exe, set_tree
E ImportError: cannot import name 'make_exe' from 'virtualenv.util.path._permission' (/testbed/src/virtualenv/util/path/_permission.py)
Patch diff
diff --git a/src/virtualenv/activation/activator.py b/src/virtualenv/activation/activator.py
index 328589c..b9e2571 100644
--- a/src/virtualenv/activation/activator.py
+++ b/src/virtualenv/activation/activator.py
@@ -23,7 +23,7 @@ class Activator(ABC):
:param interpreter: the interpreter we need to support
:return: ``True`` if supported, ``False`` otherwise
"""
- pass
+ return True # By default, assume support for all interpreters
@classmethod
def add_parser_arguments(cls, parser, interpreter):
@@ -33,7 +33,11 @@ class Activator(ABC):
:param parser: the CLI parser
:param interpreter: the interpreter this virtual environment is based of
"""
- pass
+ parser.add_argument(
+ "--prompt",
+ default=".",
+ help="Provides an alternative prompt prefix for this environment"
+ )
@abstractmethod
def generate(self, creator):
@@ -42,7 +46,7 @@ class Activator(ABC):
:param creator: the creator (based of :class:`virtualenv.create.creator.Creator`) we used to create this virtual environment
"""
- pass
+ raise NotImplementedError("Subclasses must implement this method")
__all__ = ['Activator']
diff --git a/src/virtualenv/activation/via_template.py b/src/virtualenv/activation/via_template.py
index 6b70a4d..62be29e 100644
--- a/src/virtualenv/activation/via_template.py
+++ b/src/virtualenv/activation/via_template.py
@@ -10,7 +10,36 @@ else:
class ViaTemplateActivator(Activator, ABC):
- pass
+ @abstractmethod
+ def templates(self):
+ pass
+
+ def generate(self, creator):
+ dest_folder = creator.bin_dir
+ replacements = self.replacements(creator, dest_folder)
+ generated = {}
+ for template in self.templates():
+ text = self.instantiate_template(replacements, template, creator)
+ dest = os.path.join(dest_folder, self.as_name(template))
+ generated[dest] = text
+ return generated
+
+ @abstractmethod
+ def replacements(self, creator, dest_folder):
+ pass
+
+ def instantiate_template(self, replacements, template, creator):
+ if sys.version_info >= (3, 10):
+ text = files(self.__class__).joinpath(template).read_text()
+ else:
+ text = read_binary(self.__class__, template).decode('utf-8')
+ for key, value in replacements.items():
+ text = text.replace(key.encode('utf-8'), value.encode('utf-8')).decode('utf-8')
+ return text
+
+ @staticmethod
+ def as_name(template):
+ return template
__all__ = ['ViaTemplateActivator']
diff --git a/src/virtualenv/app_data/base.py b/src/virtualenv/app_data/base.py
index c8e5b7b..6e73b5d 100644
--- a/src/virtualenv/app_data/base.py
+++ b/src/virtualenv/app_data/base.py
@@ -11,17 +11,31 @@ class AppData(ABC):
@abstractmethod
def close(self):
"""Called before virtualenv exits."""
- pass
+ raise NotImplementedError("Subclasses must implement this method")
@abstractmethod
def reset(self):
"""Called when the user passes in the reset app data."""
- pass
+ raise NotImplementedError("Subclasses must implement this method")
@contextmanager
def ensure_extracted(self, path, to_folder=None):
"""Some paths might be within the zipapp, unzip these to a path on the disk."""
- pass
+ if IS_ZIPAPP:
+ import tempfile
+ import shutil
+ import os
+
+ temp_dir = to_folder or tempfile.mkdtemp()
+ try:
+ extracted_path = os.path.join(temp_dir, os.path.basename(path))
+ shutil.copy(path, extracted_path)
+ yield extracted_path
+ finally:
+ if not to_folder:
+ shutil.rmtree(temp_dir)
+ else:
+ yield path
class ContentStore(ABC):
diff --git a/src/virtualenv/app_data/na.py b/src/virtualenv/app_data/na.py
index 76639f8..b921fac 100644
--- a/src/virtualenv/app_data/na.py
+++ b/src/virtualenv/app_data/na.py
@@ -15,35 +15,35 @@ class AppDataDisabled(AppData):
def close(self):
"""Do nothing."""
- pass
+ return
def reset(self):
"""Do nothing."""
- pass
+ return
@contextmanager
def locked(self, path):
"""Do nothing."""
- pass
+ yield
def py_info_clear(self):
"""Nothing to clear."""
- pass
+ return
class ContentStoreNA(ContentStore):
def read(self):
"""Nothing to read."""
- pass
+ return None
def write(self, content):
"""Nothing to write."""
- pass
+ return
def remove(self):
"""Nothing to remove."""
- pass
+ return
__all__ = ['AppDataDisabled', 'ContentStoreNA']
diff --git a/src/virtualenv/app_data/read_only.py b/src/virtualenv/app_data/read_only.py
index a2e161c..8015508 100644
--- a/src/virtualenv/app_data/read_only.py
+++ b/src/virtualenv/app_data/read_only.py
@@ -16,7 +16,17 @@ class ReadOnlyAppData(AppDataDiskFolder):
class _PyInfoStoreDiskReadOnly(PyInfoStoreDisk):
- pass
+ def __init__(self, folder):
+ super().__init__(folder)
+
+ def read(self, path):
+ return super().read(path)
+
+ def write(self, path, content):
+ raise PermissionError(f"Cannot write to read-only app data: {path}")
+
+ def remove(self, path):
+ raise PermissionError(f"Cannot remove from read-only app data: {path}")
__all__ = ['ReadOnlyAppData']
diff --git a/src/virtualenv/app_data/via_disk_folder.py b/src/virtualenv/app_data/via_disk_folder.py
index fe0a164..3ed5807 100644
--- a/src/virtualenv/app_data/via_disk_folder.py
+++ b/src/virtualenv/app_data/via_disk_folder.py
@@ -50,11 +50,13 @@ class AppDataDiskFolder(AppData):
def close(self):
"""Do nothing."""
- pass
+ self.lock.release()
def py_info_clear(self):
"""clear py info."""
- pass
+ py_info_folder = self.lock.path / 'py'
+ with suppress(FileNotFoundError):
+ safe_delete(py_info_folder)
class JSONStoreDisk(ContentStore, ABC):
@@ -65,6 +67,32 @@ class JSONStoreDisk(ContentStore, ABC):
self.msg = msg
self.msg_args = *msg_args, self.file
+ @property
+ def file(self):
+ return self.in_folder / f'{self.key}.json'
+
+ @contextmanager
+ def locked(self):
+ with ReentrantFileLock(self.file):
+ yield
+
+ def read(self):
+ with self.locked():
+ if self.file.exists():
+ with self.file.open() as file_handler:
+ return json.load(file_handler)
+ return None
+
+ def write(self, content):
+ with self.locked():
+ with self.file.open('w') as file_handler:
+ json.dump(content, file_handler, indent=2)
+
+ def remove(self):
+ with self.locked():
+ with suppress(FileNotFoundError):
+ self.file.unlink()
+
class PyInfoStoreDisk(JSONStoreDisk):
diff --git a/src/virtualenv/app_data/via_tempdir.py b/src/virtualenv/app_data/via_tempdir.py
index c3cae69..119036b 100644
--- a/src/virtualenv/app_data/via_tempdir.py
+++ b/src/virtualenv/app_data/via_tempdir.py
@@ -15,7 +15,8 @@ class TempAppData(AppDataDiskFolder):
def reset(self):
"""This is a temporary folder, is already empty to start with."""
- pass
+ safe_delete(self.lock.path)
+ self.__init__()
__all__ = ['TempAppData']
diff --git a/src/virtualenv/config/convert.py b/src/virtualenv/config/convert.py
index d0fdefb..616fc01 100644
--- a/src/virtualenv/config/convert.py
+++ b/src/virtualenv/config/convert.py
@@ -30,7 +30,8 @@ class ListType(TypeData):
def _validate(self):
"""no op."""
- pass
+ if not isinstance(self.as_type, (list, tuple)):
+ raise ValueError("as_type must be a list or tuple")
def split_values(self, value):
"""
@@ -39,12 +40,42 @@ class ListType(TypeData):
First this is done by newlines. If there were no newlines in the text,
then we next try to split by comma.
"""
- pass
+ if isinstance(value, str):
+ if '\n' in value:
+ return [v.strip() for v in value.split('\n') if v.strip()]
+ return [v.strip() for v in value.split(',') if v.strip()]
+ elif isinstance(value, (list, tuple)):
+ return list(value)
+ else:
+ return [value]
def convert(value, as_type, source):
"""Convert the value as a given type where the value comes from the given source."""
- pass
+ if as_type is bool:
+ return _convert_bool(value, source)
+ elif as_type is None:
+ return None
+ elif isinstance(as_type, (list, tuple)):
+ return _convert_list(value, as_type, source)
+ else:
+ try:
+ return as_type(value)
+ except ValueError as e:
+ raise ValueError(f"failed to convert {value!r} to {as_type.__name__} via {source}: {e}")
+
+def _convert_bool(value, source):
+ if isinstance(value, bool):
+ return value
+ try:
+ return BoolType.BOOLEAN_STATES[str(value).lower()]
+ except KeyError:
+ raise ValueError(f"failed to convert {value!r} to bool via {source}")
+
+def _convert_list(value, as_type, source):
+ list_type = _CONVERT[list](list, as_type)
+ values = list_type.split_values(value)
+ return [convert(v, as_type[0], source) for v in values]
_CONVERT = {bool: BoolType, type(None): NoneType, list: ListType}
diff --git a/src/virtualenv/config/env_var.py b/src/virtualenv/config/env_var.py
index 1c12a68..d4bbb67 100644
--- a/src/virtualenv/config/env_var.py
+++ b/src/virtualenv/config/env_var.py
@@ -10,9 +10,12 @@ def get_env_var(key, as_type, env):
:param key: the config key requested
:param as_type: the type we would like to convert it to
:param env: environment variables to use
- :return:
+ :return: The converted value of the environment variable, or None if not found
"""
- pass
+ with suppress(KeyError):
+ value = env[key]
+ return convert(value, as_type, f"env var {key}")
+ return None
__all__ = ['get_env_var']
diff --git a/src/virtualenv/create/creator.py b/src/virtualenv/create/creator.py
index 96df65b..f8e4c78 100644
--- a/src/virtualenv/create/creator.py
+++ b/src/virtualenv/create/creator.py
@@ -47,6 +47,90 @@ class Creator(ABC):
f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in self._args())})"
)
+ def _copy_python_executable(self):
+ """Copy the Python executable to the virtual environment."""
+ src = Path(sys.executable)
+ dst = self.dest / "bin" / src.name
+ dst.write_bytes(src.read_bytes())
+ dst.chmod(0o755)
+
+ def _create_activation_scripts(self):
+ """Create activation scripts for different shells."""
+ activate_this = f"""
+# This file must be used with "source bin/activate" *from bash*
+# you cannot run it directly
+
+deactivate () {{
+ unset -f pydoc >/dev/null 2>&1 || true
+
+ # reset old environment variables
+ if [ -n "${{_OLD_VIRTUAL_PATH:-}}" ] ; then
+ PATH="${{_OLD_VIRTUAL_PATH:-}}"
+ export PATH
+ unset _OLD_VIRTUAL_PATH
+ fi
+ if [ -n "${{_OLD_VIRTUAL_PYTHONHOME:-}}" ] ; then
+ PYTHONHOME="${{_OLD_VIRTUAL_PYTHONHOME:-}}"
+ export PYTHONHOME
+ unset _OLD_VIRTUAL_PYTHONHOME
+ fi
+
+ # This should detect bash and zsh, which have a hash command that must
+ # be called to get it to forget past commands. Without forgetting
+ # past commands the $PATH changes we made may not be respected
+ if [ -n "${{BASH:-}}" -o -n "${{ZSH_VERSION:-}}" ] ; then
+ hash -r 2> /dev/null
+ fi
+
+ if [ -n "${{_OLD_VIRTUAL_PS1:-}}" ] ; then
+ PS1="${{_OLD_VIRTUAL_PS1:-}}"
+ export PS1
+ unset _OLD_VIRTUAL_PS1
+ fi
+
+ unset VIRTUAL_ENV
+ unset VIRTUAL_ENV_PROMPT
+ if [ ! "${{1:-}}" = "nondestructive" ] ; then
+ # Self destruct!
+ unset -f deactivate
+ fi
+}}
+
+# unset irrelevant variables
+deactivate nondestructive
+
+VIRTUAL_ENV="{self.dest}"
+export VIRTUAL_ENV
+
+_OLD_VIRTUAL_PATH="$PATH"
+PATH="$VIRTUAL_ENV/bin:$PATH"
+export PATH
+
+# unset PYTHONHOME if set
+# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
+# could use `if (set -u; : $PYTHONHOME) ;` in bash
+if [ -n "${{PYTHONHOME:-}}" ] ; then
+ _OLD_VIRTUAL_PYTHONHOME="${{PYTHONHOME:-}}"
+ unset PYTHONHOME
+fi
+
+if [ -z "${{VIRTUAL_ENV_DISABLE_PROMPT:-}}" ] ; then
+ _OLD_VIRTUAL_PS1="${{PS1:-}}"
+ PS1="({self.dest.name}) ${{PS1:-}}"
+ export PS1
+ VIRTUAL_ENV_PROMPT="({self.dest.name}) "
+ export VIRTUAL_ENV_PROMPT
+fi
+
+# This should detect bash and zsh, which have a hash command that must
+# be called to get it to forget past commands. Without forgetting
+# past commands the $PATH changes we made may not be respected
+if [ -n "${{BASH:-}}" -o -n "${{ZSH_VERSION:-}}" ] ; then
+ hash -r 2> /dev/null
+fi
+"""
+ (self.dest / "bin" / "activate").write_text(activate_this)
+
@classmethod
def can_create(cls, interpreter):
"""
@@ -55,7 +139,7 @@ class Creator(ABC):
:param interpreter: the interpreter in question
:return: ``None`` if we can't create, any other object otherwise that will be forwarded to :meth:`add_parser_arguments`
"""
- pass
+ return True # Assume we can always create a virtual environment
@classmethod
def add_parser_arguments(cls, parser, interpreter, meta, app_data):
@@ -67,26 +151,88 @@ class Creator(ABC):
:param interpreter: the interpreter we're asked to create virtual environment for
:param meta: value as returned by :meth:`can_create`
"""
- pass
+ parser.add_argument(
+ "--dest",
+ help="The destination directory to create the virtual environment in",
+ required=True,
+ )
+ parser.add_argument(
+ "--clear",
+ action="store_true",
+ help="Clear the destination directory if it already exists",
+ )
+ parser.add_argument(
+ "--no-vcs-ignore",
+ action="store_true",
+ help="Don't create VCS ignore files",
+ )
@abstractmethod
def create(self):
"""Perform the virtual environment creation."""
- pass
+ if self.clear and self.dest.exists():
+ safe_delete(self.dest)
+
+ self.dest.mkdir(parents=True, exist_ok=True)
+
+ self.setup_ignore_vcs()
+
+ # Create necessary directories
+ (self.dest / "bin").mkdir(exist_ok=True)
+ (self.dest / "lib").mkdir(exist_ok=True)
+ (self.dest / "include").mkdir(exist_ok=True)
+
+ # Create pyvenv.cfg file
+ self.pyenv_cfg.write()
+
+ # Copy python executable
+ self._copy_python_executable()
+
+ # Create activation scripts
+ self._create_activation_scripts()
+
+ return self.dest
@classmethod
def validate_dest(cls, raw_value):
"""No path separator in the path, valid chars and must be write-able."""
- pass
+ try:
+ path = Path(raw_value).resolve()
+ if not path.parent.exists():
+ raise ArgumentTypeError(f"Parent directory {path.parent} does not exist")
+ if path.exists() and not os.access(path, os.W_OK):
+ raise ArgumentTypeError(f"Destination {path} is not writable")
+ return path
+ except Exception as e:
+ raise ArgumentTypeError(f"Invalid destination path: {e}")
def setup_ignore_vcs(self):
"""Generate ignore instructions for version control systems."""
- pass
+ if not self.no_vcs_ignore:
+ vcs_ignore_contents = "# created by virtualenv automatically\n*"
+ ignore_files = [".gitignore", ".bzrignore", ".hgignore"]
+
+ for ignore_file in ignore_files:
+ ignore_path = self.dest / ignore_file
+ if not ignore_path.exists():
+ ignore_path.write_text(vcs_ignore_contents)
@property
def debug(self):
""":return: debug information about the virtual environment (only valid after :meth:`create` has run)"""
- pass
+ if self._debug is None:
+ self._debug = self._get_debug_info()
+ return self._debug
+
+ def _get_debug_info(self):
+ debug_info = OrderedDict()
+ debug_info["creator"] = self.__class__.__name__
+ debug_info["interpreter"] = self.interpreter.executable
+ debug_info["dest"] = str(self.dest)
+ debug_info["version"] = __version__
+
+ # Add more debug information as needed
+ return debug_info
__all__ = ['Creator', 'CreatorMeta']
diff --git a/src/virtualenv/create/debug.py b/src/virtualenv/create/debug.py
index e728f1f..0cf8819 100644
--- a/src/virtualenv/create/debug.py
+++ b/src/virtualenv/create/debug.py
@@ -5,7 +5,19 @@ import sys
def run():
"""Print debug data about the virtual environment."""
- pass
+ print("Virtual Environment Debug Information:")
+ print(f"Python Version: {sys.version}")
+ print(f"Python Executable: {sys.executable}")
+ print(f"sys.prefix: {sys.prefix}")
+ print(f"sys.base_prefix: {sys.base_prefix}")
+ print(f"sys.exec_prefix: {sys.exec_prefix}")
+ print(f"sys.base_exec_prefix: {sys.base_exec_prefix}")
+ print(f"sys.path:")
+ for path in sys.path:
+ print(f" - {path}")
+ print(f"Environment Variables:")
+ for key, value in sorted(sys.environ.items()):
+ print(f" {key}: {value}")
if __name__ == '__main__':
diff --git a/src/virtualenv/create/describe.py b/src/virtualenv/create/describe.py
index 5d7690c..2c7c6f8 100644
--- a/src/virtualenv/create/describe.py
+++ b/src/virtualenv/create/describe.py
@@ -20,12 +20,12 @@ class Describe:
@classmethod
def can_describe(cls, interpreter):
"""Knows means it knows how the output will look."""
- pass
+ return True
@classmethod
def exe_stem(cls):
"""Executable name without suffix - there seems to be no standard way to get this without creating it."""
- pass
+ return 'python' if not IS_WIN else 'python'
class Python3Supports(Describe, ABC):
diff --git a/src/virtualenv/create/via_global_ref/_virtualenv.py b/src/virtualenv/create/via_global_ref/_virtualenv.py
index e27c1e4..b31d038 100644
--- a/src/virtualenv/create/via_global_ref/_virtualenv.py
+++ b/src/virtualenv/create/via_global_ref/_virtualenv.py
@@ -12,7 +12,19 @@ def patch_dist(dist):
Some of this arguments though don't make sense in context of the virtual environment files, let's fix them up.
"""
- pass
+ # Reset the prefix and exec_prefix to empty strings
+ dist.prefix = ''
+ dist.exec_prefix = ''
+
+ # Set install_lib to None to use the default
+ dist.install_lib = None
+
+ # Reset the install directories
+ dist.install_platlib = None
+ dist.install_purelib = None
+ dist.install_headers = None
+ dist.install_scripts = None
+ dist.install_data = None
_DISTUTILS_PATCH = 'distutils.dist', 'setuptools.dist'
@@ -23,5 +35,41 @@ class _Finder:
fullname = None
lock = []
+ @classmethod
+ def find_spec(cls, fullname, path, target=None):
+ if fullname in _DISTUTILS_PATCH:
+ return cls._create_spec(fullname)
+ return None
+
+ @classmethod
+ def _create_spec(cls, fullname):
+ from importlib.util import spec_from_loader
+ return spec_from_loader(fullname, cls())
+
+ @classmethod
+ def load_module(cls, fullname):
+ with cls.lock:
+ if fullname != cls.fullname:
+ cls.fullname = fullname
+ module = cls._load_module(fullname)
+ sys.modules[fullname] = module
+ return module
+ return sys.modules[fullname]
+
+ @staticmethod
+ def _load_module(fullname):
+ import importlib
+ module = importlib.import_module(fullname)
+ if fullname.startswith('distutils'):
+ patch_dist(module.Distribution)
+ return module
+
+ def create_module(self, spec):
+ return None
+
+ def exec_module(self, module):
+ fullname = module.__spec__.name
+ self.load_module(fullname)
+
sys.meta_path.insert(0, _Finder())
diff --git a/src/virtualenv/create/via_global_ref/api.py b/src/virtualenv/create/via_global_ref/api.py
index 942e93c..a199126 100644
--- a/src/virtualenv/create/via_global_ref/api.py
+++ b/src/virtualenv/create/via_global_ref/api.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import logging
import os
+import textwrap
from abc import ABC
from pathlib import Path
from virtualenv.create.creator import Creator, CreatorMeta
@@ -26,7 +27,28 @@ class ViaGlobalRefApi(Creator, ABC):
def env_patch_text(self):
"""Patch the distutils package to not be derailed by its configuration files."""
- pass
+ return textwrap.dedent(
+ """
+ import distutils.dist
+ import distutils.sysconfig
+ from distutils import log
+ from distutils.command import install
+
+ # Patch distutils to ignore user configuration files
+ distutils.dist.Distribution.parse_config_files = lambda self, *args, **kwargs: None
+ distutils.sysconfig.get_config_vars = lambda *args, **kwargs: {}
+ distutils.sysconfig.get_config_var = lambda *args, **kwargs: None
+
+ # Patch distutils to not write to the user's home directory
+ def _dont_write_bytecode(self):
+ self.byte_compile = 0
+ install.install._dont_write_bytecode = _dont_write_bytecode
+ install.install.finalize_options = _dont_write_bytecode
+
+ # Suppress distutils warnings
+ log.set_threshold(log.ERROR)
+ """
+ )
__all__ = ['ViaGlobalRefApi', 'ViaGlobalRefMeta']
diff --git a/src/virtualenv/create/via_global_ref/builtin/cpython/common.py b/src/virtualenv/create/via_global_ref/builtin/cpython/common.py
index 4560e8d..98e098c 100644
--- a/src/virtualenv/create/via_global_ref/builtin/cpython/common.py
+++ b/src/virtualenv/create/via_global_ref/builtin/cpython/common.py
@@ -25,3 +25,11 @@ _BREW = re.compile(
)
__all__ = ['CPython', 'CPythonPosix', 'CPythonWindows',
'is_mac_os_framework', 'is_macos_brew']
+
+def is_mac_os_framework(interpreter):
+ """Check if the interpreter is a macOS framework build."""
+ return interpreter.executable.parts[:3] == Path("/Library/Frameworks/Python.framework").parts[:3]
+
+def is_macos_brew(interpreter):
+ """Check if the interpreter is a Homebrew-installed Python on macOS."""
+ return bool(_BREW.match(str(interpreter.executable)))
diff --git a/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py b/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py
index fd977a0..fa5c929 100644
--- a/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py
+++ b/src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py
@@ -36,7 +36,9 @@ class CPython3Windows(CPythonWindows, CPython3):
move/rename *zip* file and edit `sys.path` by editing *_pth* file.
Here the `pattern` is used only for the default *zip* file name!
"""
- pass
+ py_version_nodot = interpreter.version_info.major * 10 + interpreter.version_info.minor
+ pattern = f"python{py_version_nodot}.zip"
+ return cls.mappings(interpreter, pattern)
__all__ = ['CPython3', 'CPython3Posix', 'CPython3Windows']
diff --git a/src/virtualenv/create/via_global_ref/builtin/cpython/mac_os.py b/src/virtualenv/create/via_global_ref/builtin/cpython/mac_os.py
index ed427f7..38ef9c6 100644
--- a/src/virtualenv/create/via_global_ref/builtin/cpython/mac_os.py
+++ b/src/virtualenv/create/via_global_ref/builtin/cpython/mac_os.py
@@ -45,7 +45,26 @@ def fix_mach_o(exe, current, new, max_size):
(found in the __LINKEDIT section) function. In 10.6 these new Link Edit tables are compressed by removing unused and
unneeded bits of information, however Mac OS X 10.5 and earlier cannot read this new Link Edit table format.
"""
- pass
+ with open(exe, 'rb+') as f:
+ content = f.read()
+ f.seek(0)
+
+ # Find all occurrences of the current path
+ offsets = [i for i in range(len(content)) if content.startswith(current.encode(), i)]
+
+ if not offsets:
+ raise ValueError(f"Could not find {current} in {exe}")
+
+ # Replace the path, ensuring it doesn't exceed max_size
+ new_path = new.encode().ljust(len(current), b'\0')
+ if len(new_path) > max_size:
+ raise ValueError(f"New path {new} is too long (max {max_size} bytes)")
+
+ for offset in offsets:
+ f.seek(offset)
+ f.write(new_path)
+
+ f.truncate()
class CPython3macOsBrew(CPython3, CPythonPosix):
diff --git a/src/virtualenv/create/via_global_ref/builtin/pypy/common.py b/src/virtualenv/create/via_global_ref/builtin/pypy/common.py
index 1056bf3..e2e2350 100644
--- a/src/virtualenv/create/via_global_ref/builtin/pypy/common.py
+++ b/src/virtualenv/create/via_global_ref/builtin/pypy/common.py
@@ -6,7 +6,45 @@ from virtualenv.create.via_global_ref.builtin.via_global_self_do import ViaGloba
class PyPy(ViaGlobalRefVirtualenvBuiltin, abc.ABC):
- pass
+ @property
+ def stdlib(self):
+ return self.dest / "lib-python" / f"{self.interpreter.version_info.major}.{self.interpreter.version_info.minor}"
+
+ @property
+ def site_packages(self):
+ return self.dest / "site-packages"
+
+ @property
+ def exe(self):
+ return self.dest / self.interpreter.exe.name
+
+ @property
+ def bin_dir(self):
+ return self.dest / "bin"
+
+ def ensure_directories(self):
+ dirs = [self.stdlib, self.site_packages, self.bin_dir]
+ for directory in dirs:
+ directory.mkdir(parents=True, exist_ok=True)
+
+ def set_pypy_exe(self):
+ src = self.interpreter.exe
+ dst = self.exe
+ if not dst.exists():
+ dst.symlink_to(src)
+
+ def create(self):
+ self.ensure_directories()
+ self.set_pypy_exe()
+ super().create()
+
+ @property
+ def sources(self):
+ return {
+ "stdlib": RefMust(self.interpreter.stdlib, self.stdlib, RefWhen.ANY),
+ "site-packages": RefMust(self.interpreter.site_packages, self.site_packages, RefWhen.ANY),
+ "executable": PathRefToDest(self.interpreter.exe, self.exe),
+ }
__all__ = ['PyPy']
diff --git a/src/virtualenv/create/via_global_ref/builtin/pypy/pypy3.py b/src/virtualenv/create/via_global_ref/builtin/pypy/pypy3.py
index 023840f..98c7e54 100644
--- a/src/virtualenv/create/via_global_ref/builtin/pypy/pypy3.py
+++ b/src/virtualenv/create/via_global_ref/builtin/pypy/pypy3.py
@@ -7,15 +7,36 @@ from .common import PyPy
class PyPy3(PyPy, Python3Supports, abc.ABC):
- pass
+ @classmethod
+ def exe_stem(cls):
+ return "pypy3"
+
+ @classmethod
+ def can_describe(cls, interpreter):
+ return interpreter.implementation == "PyPy" and interpreter.version_info[0] == 3
class PyPy3Posix(PyPy3, PosixSupports):
"""PyPy 3 on POSIX."""
+
+ @classmethod
+ def sources(cls, interpreter):
+ for src in super().sources(interpreter):
+ yield src
+ # Add POSIX-specific sources if needed
+ yield PathRefToDest(interpreter.prefix / "bin" / "python3", dest=cls.bin_dir / "python")
class Pypy3Windows(PyPy3, WindowsSupports):
"""PyPy 3 on Windows."""
+
+ @classmethod
+ def sources(cls, interpreter):
+ for src in super().sources(interpreter):
+ yield src
+ # Add Windows-specific sources if needed
+ yield PathRefToDest(interpreter.prefix / "python.exe", dest=cls.bin_dir / "python.exe")
+ yield PathRefToDest(interpreter.prefix / "pythonw.exe", dest=cls.bin_dir / "pythonw.exe")
__all__ = ['PyPy3', 'PyPy3Posix', 'Pypy3Windows']
diff --git a/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py b/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py
index 5be5210..4dd7b07 100644
--- a/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py
+++ b/src/virtualenv/create/via_global_ref/builtin/via_global_self_do.py
@@ -22,14 +22,16 @@ class ViaGlobalRefVirtualenvBuiltin(ViaGlobalRefApi, VirtualenvBuiltin, ABC):
@classmethod
def can_create(cls, interpreter):
"""By default, all built-in methods assume that if we can describe it we can create it."""
- pass
+ return cls.can_describe(interpreter)
def set_pyenv_cfg(self):
"""
We directly inject the base prefix and base exec prefix to avoid site.py needing to discover these
from home (which usually is done within the interpreter itself).
"""
- pass
+ super().set_pyenv_cfg()
+ self.pyenv_cfg["base-prefix"] = self.interpreter.prefix
+ self.pyenv_cfg["base-exec-prefix"] = self.interpreter.base_exec_prefix
__all__ = ['BuiltinViaGlobalRefMeta', 'ViaGlobalRefVirtualenvBuiltin']
diff --git a/src/virtualenv/create/via_global_ref/venv.py b/src/virtualenv/create/via_global_ref/venv.py
index dab115b..7d6be47 100644
--- a/src/virtualenv/create/via_global_ref/venv.py
+++ b/src/virtualenv/create/via_global_ref/venv.py
@@ -27,7 +27,12 @@ class Venv(ViaGlobalRefApi):
Venv does not handle non-existing exe sources, e.g. python.exe, so this
patch does it.
"""
- pass
+ if isinstance(self.interpreter, Pypy3Windows) and self.interpreter.version_info[:2] <= (3, 6):
+ executables = copy(self.interpreter.executables)
+ executables['python'] = executables['pypy']
+ executables['pythonw'] = executables['pypyw']
+ return executables
+ return None
def __getattribute__(self, item):
describe = object.__getattribute__(self, 'describe')
diff --git a/src/virtualenv/discovery/builtin.py b/src/virtualenv/discovery/builtin.py
index c685c6c..844c7f6 100644
--- a/src/virtualenv/discovery/builtin.py
+++ b/src/virtualenv/discovery/builtin.py
@@ -57,7 +57,23 @@ class LazyPathDump:
def path_exe_finder(spec: PythonSpec) ->Callable[[Path], Generator[tuple[
Path, bool], None, None]]:
"""Given a spec, return a function that can be called on a path to find all matching files in it."""
- pass
+ def finder(path: Path) -> Generator[tuple[Path, bool], None, None]:
+ if not path.is_dir():
+ return
+
+ for item in path.iterdir():
+ if item.is_file() and item.stat().st_mode & os.X_OK:
+ name = item.name.lower()
+ if IS_WIN:
+ if name.startswith(spec.implementation) and name.endswith('.exe'):
+ yield item, True
+ else:
+ if name.startswith(spec.implementation):
+ yield item, True
+ elif item.is_dir():
+ yield item, False
+
+ return finder
class PathPythonInfo(PythonInfo):
diff --git a/src/virtualenv/discovery/discover.py b/src/virtualenv/discovery/discover.py
index c6a9976..b6bd412 100644
--- a/src/virtualenv/discovery/discover.py
+++ b/src/virtualenv/discovery/discover.py
@@ -14,7 +14,7 @@ class Discover(ABC):
"""
pass
- def __init__(self, options) ->None:
+ def __init__(self, options) -> None:
"""
Create a new discovery mechanism.
@@ -31,12 +31,26 @@ class Discover(ABC):
:return: the interpreter ready to use for virtual environment creation
"""
- pass
+ if not self._has_run:
+ self._interpreter = self._discover_interpreter()
+ self._has_run = True
+ return self._interpreter
+
+ @abstractmethod
+ def _discover_interpreter(self):
+ """
+ Abstract method to be implemented by subclasses for actual interpreter discovery.
+
+ :return: the discovered interpreter
+ """
+ raise NotImplementedError
@property
def interpreter(self):
""":return: the interpreter as returned by :meth:`run`, cached"""
- pass
+ if not self._has_run:
+ self.run()
+ return self._interpreter
__all__ = ['Discover']
diff --git a/src/virtualenv/discovery/py_info.py b/src/virtualenv/discovery/py_info.py
index 423ca50..c5dd6d8 100644
--- a/src/virtualenv/discovery/py_info.py
+++ b/src/virtualenv/discovery/py_info.py
@@ -92,7 +92,9 @@ class PythonInfo:
def _fast_get_system_executable(self):
"""Try to get the system executable by just looking at properties."""
- pass
+ if self.prefix == self.base_prefix == self.exec_prefix == self.base_exec_prefix:
+ return self.executable
+ return None
def __repr__(self) ->str:
return '{}({!r})'.format(self.__class__.__name__, {k: v for k, v in
@@ -112,7 +114,14 @@ class PythonInfo:
def satisfies(self, spec, impl_must_match):
"""Check if a given specification can be satisfied by the this python interpreter instance."""
- pass
+ if impl_must_match and spec.implementation != self.implementation:
+ return False
+ if spec.architecture is not None and spec.architecture != self.architecture:
+ return False
+ if spec.version_info:
+ if self.version_info[:len(spec.version_info)] != spec.version_info:
+ return False
+ return True
_current_system = None
_current = None
@@ -122,7 +131,9 @@ class PythonInfo:
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.
"""
- pass
+ if cls._current is None:
+ cls._current = cls()
+ return cls._current
@classmethod
def current_system(cls, app_data=None) ->PythonInfo:
@@ -130,13 +141,37 @@ class PythonInfo:
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.
"""
- pass
+ if cls._current_system is None:
+ cls._current_system = cls.from_exe(sys.executable, app_data=app_data)
+ return cls._current_system
@classmethod
def from_exe(cls, exe, app_data=None, raise_on_error=True, ignore_cache
=False, resolve_to_host=True, env=None):
"""Given a path to an executable get the python information."""
- pass
+ key = (exe, resolve_to_host)
+ if ignore_cache:
+ cls._cache_exe_discovery.pop(key, None)
+ if key not in cls._cache_exe_discovery:
+ try:
+ info = cls._from_exe(exe, app_data, resolve_to_host, env)
+ cls._cache_exe_discovery[key] = info
+ except Exception as exception:
+ if raise_on_error:
+ raise
+ cls._cache_exe_discovery[key] = exception
+ result = cls._cache_exe_discovery[key]
+ if isinstance(result, Exception):
+ if raise_on_error:
+ raise result
+ return None
+ return result
+
+ @classmethod
+ def _from_exe(cls, exe, app_data, resolve_to_host, env):
+ # This is a placeholder for the actual implementation
+ # In a real scenario, this method would gather information about the Python executable
+ return cls()
_cache_exe_discovery = {}
diff --git a/src/virtualenv/discovery/py_spec.py b/src/virtualenv/discovery/py_spec.py
index fda6851..bf31792 100644
--- a/src/virtualenv/discovery/py_spec.py
+++ b/src/virtualenv/discovery/py_spec.py
@@ -22,11 +22,34 @@ class PythonSpec:
def generate_re(self, *, windows: bool) ->re.Pattern:
"""Generate a regular expression for matching against a filename."""
- pass
+ pattern = r""
+ if self.implementation:
+ pattern += rf"{re.escape(self.implementation)}"
+ if self.major is not None:
+ pattern += rf"{self.major}"
+ if self.minor is not None:
+ pattern += rf"\.{self.minor}"
+ if self.micro is not None:
+ pattern += rf"\.{self.micro}"
+ if self.architecture:
+ pattern += rf"-{self.architecture}"
+ if windows:
+ pattern += r"\.exe"
+ return re.compile(pattern, re.IGNORECASE)
def satisfies(self, spec):
"""Called when there's a candidate metadata spec to see if compatible - e.g. PEP-514 on Windows."""
- pass
+ if self.implementation and spec.implementation and self.implementation.lower() != spec.implementation.lower():
+ return False
+ if self.major is not None and spec.major is not None and self.major != spec.major:
+ return False
+ if self.minor is not None and spec.minor is not None and self.minor != spec.minor:
+ return False
+ if self.micro is not None and spec.micro is not None and self.micro != spec.micro:
+ return False
+ if self.architecture and spec.architecture and self.architecture != spec.architecture:
+ return False
+ return True
def __repr__(self) ->str:
name = type(self).__name__
diff --git a/src/virtualenv/run/session.py b/src/virtualenv/run/session.py
index 33a12c6..f42ea0c 100644
--- a/src/virtualenv/run/session.py
+++ b/src/virtualenv/run/session.py
@@ -18,27 +18,27 @@ class Session:
@property
def verbosity(self):
"""The verbosity of the run."""
- pass
+ return self._verbosity
@property
def interpreter(self):
"""Create a virtual environment based on this reference interpreter."""
- pass
+ return self._interpreter
@property
def creator(self):
"""The creator used to build the virtual environment (must be compatible with the interpreter)."""
- pass
+ return self._creator
@property
def seeder(self):
"""The mechanism used to provide the seed packages (pip, setuptools, wheel)."""
- pass
+ return self._seeder
@property
def activators(self):
"""Activators used to generate activations scripts."""
- pass
+ return self._activators
def __enter__(self):
return self
diff --git a/src/virtualenv/seed/embed/via_app_data/pip_install/copy.py b/src/virtualenv/seed/embed/via_app_data/pip_install/copy.py
index c1e008c..5149be5 100644
--- a/src/virtualenv/seed/embed/via_app_data/pip_install/copy.py
+++ b/src/virtualenv/seed/embed/via_app_data/pip_install/copy.py
@@ -6,7 +6,23 @@ from .base import PipInstall
class CopyPipInstall(PipInstall):
- pass
+ def __init__(self, wheel):
+ super().__init__(wheel)
+
+ def _sync(self, src, dst):
+ dst_folder = os.path.dirname(dst)
+ if not os.path.isdir(dst_folder):
+ os.makedirs(dst_folder)
+ if src != dst:
+ copy(src, dst)
+
+ def install(self, creator):
+ for src in self.wheel.src_files:
+ dst = os.path.join(creator.purelib, self.wheel.name, os.path.basename(src))
+ self._sync(src, dst)
+
+ def clear(self, creator):
+ pass # No need to clear anything for copy installation
__all__ = ['CopyPipInstall']
diff --git a/src/virtualenv/seed/embed/via_app_data/pip_install/symlink.py b/src/virtualenv/seed/embed/via_app_data/pip_install/symlink.py
index 4d91177..b130da5 100644
--- a/src/virtualenv/seed/embed/via_app_data/pip_install/symlink.py
+++ b/src/virtualenv/seed/embed/via_app_data/pip_install/symlink.py
@@ -7,7 +7,31 @@ from .base import PipInstall
class SymlinkPipInstall(PipInstall):
- pass
+ def __init__(self, wheel, creator, app_data):
+ super().__init__(wheel, creator, app_data)
+ self.symlink_path = None
+
+ def _sync(self, src, dst):
+ safe_delete(dst)
+ os.symlink(src, dst)
+ self.symlink_path = dst
+
+ def _generate_new_files(self):
+ # Create a symlink instead of copying files
+ src = str(self.wheel.path)
+ dst = str(self.dest_dir / self.wheel.path.name)
+ self._sync(src, dst)
+
+ def clear(self):
+ if self.symlink_path:
+ safe_delete(self.symlink_path)
+ super().clear()
+
+ def install(self, version_info):
+ self._generate_new_files()
+ # Set read-only permissions for the symlink
+ os.chmod(self.symlink_path, S_IREAD | S_IRGRP | S_IROTH)
+ return super().install(version_info)
__all__ = ['SymlinkPipInstall']
diff --git a/src/virtualenv/seed/seeder.py b/src/virtualenv/seed/seeder.py
index 744b173..f393569 100644
--- a/src/virtualenv/seed/seeder.py
+++ b/src/virtualenv/seed/seeder.py
@@ -24,7 +24,11 @@ class Seeder(ABC):
:param app_data: the CLI parser
:param interpreter: the interpreter this virtual environment is based of
"""
- pass
+ parser.add_argument(
+ "--seed-packages",
+ nargs="*",
+ help="Specify additional packages to install when seeding the environment",
+ )
@abstractmethod
def run(self, creator):
@@ -33,7 +37,28 @@ class Seeder(ABC):
:param creator: the creator (based of :class:`virtualenv.create.creator.Creator`) we used to create this virtual environment
"""
- pass
+ if not self.enabled:
+ return
+
+ packages = self.env.get("seed_packages", [])
+ if packages:
+ self._install_packages(creator, packages)
+
+ def _install_packages(self, creator, packages):
+ """
+ Install specified packages in the virtual environment.
+
+ :param creator: the creator of the virtual environment
+ :param packages: list of packages to install
+ """
+ pip_path = creator.bin_path / "pip"
+ if not pip_path.exists():
+ raise RuntimeError("pip not found in the virtual environment")
+
+ import subprocess
+
+ cmd = [str(pip_path), "install"] + packages
+ subprocess.check_call(cmd, env=self.env)
__all__ = ['Seeder']
diff --git a/src/virtualenv/seed/wheels/acquire.py b/src/virtualenv/seed/wheels/acquire.py
index a3225d0..a1f1125 100644
--- a/src/virtualenv/seed/wheels/acquire.py
+++ b/src/virtualenv/seed/wheels/acquire.py
@@ -13,7 +13,51 @@ from .util import Version, Wheel, discover_wheels
def get_wheel(distribution, version, for_py_version, search_dirs, download,
app_data, do_periodic_update, env):
"""Get a wheel with the given distribution-version-for_py_version trio, by using the extra search dir + download."""
- pass
+ # First, try to find the wheel in the search directories
+ for search_dir in search_dirs:
+ wheels = discover_wheels(Path(search_dir), distribution, version, for_py_version)
+ if wheels:
+ return wheels[0]
+ # If not found in search dirs, try to get it from the bundle
+ bundle_wheel = from_bundle(distribution, version, for_py_version, search_dirs)
+ if bundle_wheel:
+ return bundle_wheel
+
+ # If not in bundle, try to download
+ if download:
+ downloaded_wheel = download_wheel(distribution, version, for_py_version, app_data, env)
+ if downloaded_wheel:
+ if do_periodic_update:
+ add_wheel_to_update_log(app_data, distribution, downloaded_wheel)
+ return downloaded_wheel
+
+ # If we couldn't find or download the wheel, raise an exception
+ raise ValueError(f"Unable to find or download wheel for {distribution} {version} (Python {for_py_version})")
+
+
+def download_wheel(distribution, version, for_py_version, app_data, env):
+ """Download a wheel for the given distribution, version, and Python version."""
+ logging.info(f"Downloading wheel for {distribution} {version} (Python {for_py_version})")
+
+ cmd = [
+ sys.executable,
+ "-m", "pip",
+ "download",
+ "--only-binary=:all:",
+ "--no-deps",
+ "--python-version", for_py_version,
+ f"{distribution}=={version}"
+ ]
+
+ try:
+ result = pip_wheel_env_run(cmd, env)
+ wheel_file = Path(result.strip())
+ if wheel_file.exists() and wheel_file.is_file():
+ return str(wheel_file)
+ except CalledProcessError as e:
+ logging.error(f"Failed to download wheel: {e}")
+
+ return None
__all__ = ['download_wheel', 'get_wheel', 'pip_wheel_env_run']
diff --git a/src/virtualenv/seed/wheels/bundle.py b/src/virtualenv/seed/wheels/bundle.py
index dacec6a..7a47a6f 100644
--- a/src/virtualenv/seed/wheels/bundle.py
+++ b/src/virtualenv/seed/wheels/bundle.py
@@ -7,12 +7,24 @@ from .util import Version, Wheel, discover_wheels
def from_bundle(distribution, version, for_py_version, search_dirs,
app_data, do_periodic_update, env):
"""Load the bundled wheel to a cache directory."""
- pass
+ wheel = get_embed_wheel(distribution, for_py_version)
+ if wheel is None:
+ return None
+
+ if do_periodic_update:
+ wheel = periodic_update(distribution, wheel, search_dirs, app_data, env)
+
+ return wheel
def from_dir(distribution, version, for_py_version, directories):
"""Load a compatible wheel from a given folder."""
- pass
+ wheels = discover_wheels(directories, distribution, version, for_py_version)
+ if not wheels:
+ return None
+
+ # Sort wheels by version (highest first) and return the best match
+ return sorted(wheels, key=lambda w: Version(w.version), reverse=True)[0]
-__all__ = ['from_bundle', 'load_embed_wheel']
+__all__ = ['from_bundle', 'from_dir', 'load_embed_wheel']
diff --git a/src/virtualenv/util/path/_win.py b/src/virtualenv/util/path/_win.py
index e274b39..758b003 100644
--- a/src/virtualenv/util/path/_win.py
+++ b/src/virtualenv/util/path/_win.py
@@ -1,9 +1,21 @@
from __future__ import annotations
+import ctypes
+from ctypes import wintypes
+
def get_short_path_name(long_name):
"""Gets the short path name of a given long path - http://stackoverflow.com/a/23598461/200291."""
- pass
+ buffer_size = 256
+ buffer = ctypes.create_unicode_buffer(buffer_size)
+ get_short_path_name_w = ctypes.windll.kernel32.GetShortPathNameW
+ get_short_path_name_w.argtypes = [wintypes.LPCWSTR, wintypes.LPWSTR, wintypes.DWORD]
+ get_short_path_name_w.restype = wintypes.DWORD
+
+ result = get_short_path_name_w(long_name, buffer, buffer_size)
+ if result == 0:
+ raise ctypes.WinError()
+ return buffer.value
__all__ = ['get_short_path_name']