api.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. from __future__ import annotations
  2. import logging
  3. import os
  4. from abc import ABC
  5. from pathlib import Path
  6. from typing import TYPE_CHECKING
  7. from virtualenv.create.creator import Creator, CreatorMeta
  8. from virtualenv.info import fs_supports_symlink
  9. if TYPE_CHECKING:
  10. from argparse import ArgumentParser
  11. from typing import Any
  12. from python_discovery import PythonInfo
  13. from virtualenv.app_data.base import AppData
  14. from virtualenv.config.cli.parser import VirtualEnvOptions
  15. LOGGER = logging.getLogger(__name__)
  16. class ViaGlobalRefMeta(CreatorMeta):
  17. def __init__(self) -> None:
  18. super().__init__()
  19. self.copy_error = None
  20. self.symlink_error = None
  21. if not fs_supports_symlink():
  22. self.symlink_error = "the filesystem does not supports symlink"
  23. @property
  24. def can_copy(self) -> bool:
  25. return not self.copy_error
  26. @property
  27. def can_symlink(self) -> bool:
  28. return not self.symlink_error
  29. class ViaGlobalRefApi(Creator, ABC):
  30. def __init__(self, options: VirtualEnvOptions, interpreter: PythonInfo) -> None:
  31. super().__init__(options, interpreter)
  32. self.symlinks = self._should_symlink(options)
  33. self.enable_system_site_package = options.system_site
  34. if TYPE_CHECKING:
  35. @property
  36. def purelib(self) -> Path: ...
  37. @property
  38. def script_dir(self) -> Path: ...
  39. @staticmethod
  40. def _should_symlink(options: VirtualEnvOptions) -> bool:
  41. # Priority of where the option is set to follow the order: CLI, env var, file, hardcoded.
  42. # If both set at same level prefers copy over symlink.
  43. copies, symlinks = getattr(options, "copies", False), getattr(options, "symlinks", False)
  44. copy_src, sym_src = options.get_source("copies"), options.get_source("symlinks")
  45. for level in ["cli", "env var", "file", "default"]:
  46. s_opt = symlinks if sym_src == level else None
  47. c_opt = copies if copy_src == level else None
  48. if s_opt is True and c_opt is True:
  49. return False
  50. if s_opt is True:
  51. return True
  52. if c_opt is True:
  53. return False
  54. return False # fallback to copy
  55. @classmethod
  56. def add_parser_arguments(
  57. cls, parser: ArgumentParser, interpreter: PythonInfo, meta: ViaGlobalRefMeta, app_data: AppData
  58. ) -> None: # ty: ignore[invalid-method-override]
  59. super().add_parser_arguments(parser, interpreter, meta, app_data)
  60. parser.add_argument(
  61. "--system-site-packages",
  62. default=False,
  63. action="store_true",
  64. dest="system_site",
  65. help="give the virtual environment access to the system site-packages dir",
  66. )
  67. if not meta.can_symlink and not meta.can_copy:
  68. errors = []
  69. if meta.symlink_error:
  70. errors.append(f"symlink: {meta.symlink_error}")
  71. if meta.copy_error:
  72. errors.append(f"copy: {meta.copy_error}")
  73. msg = f"neither symlink or copy method supported: {', '.join(errors)}"
  74. raise RuntimeError(msg)
  75. group = parser.add_mutually_exclusive_group()
  76. if meta.can_symlink:
  77. group.add_argument(
  78. "--symlinks",
  79. default=True,
  80. action="store_true",
  81. dest="symlinks",
  82. help="try to use symlinks rather than copies, when symlinks are not the default for the platform",
  83. )
  84. if meta.can_copy:
  85. group.add_argument(
  86. "--copies",
  87. "--always-copy",
  88. default=not meta.can_symlink,
  89. action="store_true",
  90. dest="copies",
  91. help="try to use copies rather than symlinks, even when symlinks are the default for the platform",
  92. )
  93. def create(self) -> None:
  94. self.install_patch()
  95. def install_patch(self) -> None:
  96. text = self.env_patch_text()
  97. if text:
  98. pth = self.purelib / "_virtualenv.pth"
  99. LOGGER.debug("create virtualenv import hook file %s", pth)
  100. pth.write_text("import _virtualenv", encoding="utf-8")
  101. dest_path = self.purelib / "_virtualenv.py"
  102. LOGGER.debug("create %s", dest_path)
  103. dest_path.write_text(text, encoding="utf-8")
  104. def env_patch_text(self) -> str:
  105. """Patch the distutils package to not be derailed by its configuration files."""
  106. with self.app_data.ensure_extracted(Path(__file__).parent / "_virtualenv.py") as resolved_path:
  107. text = resolved_path.read_text(encoding="utf-8")
  108. # script_dir and purelib are defined in subclasses
  109. return text.replace('"__SCRIPT_DIR__"', repr(os.path.relpath(str(self.script_dir), str(self.purelib))))
  110. def _args(self) -> list[tuple[str, Any]]:
  111. return [*super()._args(), ("global", self.enable_system_site_package)]
  112. def set_pyenv_cfg(self) -> None:
  113. super().set_pyenv_cfg()
  114. self.pyenv_cfg["include-system-site-packages"] = "true" if self.enable_system_site_package else "false"
  115. __all__ = [
  116. "ViaGlobalRefApi",
  117. "ViaGlobalRefMeta",
  118. ]