juc500 정상동작
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
"""Application data stored by virtualenv."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from platformdirs import user_cache_dir, user_data_dir
|
||||
|
||||
from .na import AppDataDisabled
|
||||
from .read_only import ReadOnlyAppData
|
||||
from .via_disk_folder import AppDataDiskFolder
|
||||
from .via_tempdir import TempAppData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Mapping
|
||||
|
||||
from .base import AppData
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _default_app_data_dir(env: Mapping[str, str]) -> str:
|
||||
key = "VIRTUALENV_OVERRIDE_APP_DATA"
|
||||
if key in env:
|
||||
return env[key]
|
||||
return _cache_dir_with_migration()
|
||||
|
||||
|
||||
def _cache_dir_with_migration() -> str:
|
||||
new_dir = user_cache_dir(appname="virtualenv", appauthor="pypa")
|
||||
old_dir = user_data_dir(appname="virtualenv", appauthor="pypa")
|
||||
if new_dir == old_dir:
|
||||
return new_dir
|
||||
if os.path.isdir(old_dir) and not os.path.isdir(new_dir):
|
||||
LOGGER.info("migrating app data from %s to %s", old_dir, new_dir)
|
||||
try:
|
||||
shutil.move(old_dir, new_dir)
|
||||
except OSError as exception:
|
||||
LOGGER.warning(
|
||||
"could not migrate app data from %s to %s: %r, using old location", old_dir, new_dir, exception
|
||||
)
|
||||
return old_dir
|
||||
return new_dir
|
||||
|
||||
|
||||
def make_app_data(folder: str | None, **kwargs: Any) -> AppData: # noqa: ANN401
|
||||
is_read_only = kwargs.pop("read_only")
|
||||
env = kwargs.pop("env")
|
||||
if kwargs: # py3+ kwonly
|
||||
msg = "unexpected keywords: {}"
|
||||
raise TypeError(msg)
|
||||
|
||||
if folder is None:
|
||||
folder = _default_app_data_dir(env)
|
||||
folder = os.path.abspath(folder)
|
||||
|
||||
if is_read_only:
|
||||
return ReadOnlyAppData(folder)
|
||||
|
||||
try:
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
LOGGER.debug("created app data folder %s", folder)
|
||||
except OSError as exception:
|
||||
LOGGER.info("could not create app data folder %s due to %r", folder, exception)
|
||||
|
||||
if os.access(folder, os.W_OK):
|
||||
return AppDataDiskFolder(folder)
|
||||
LOGGER.debug("app data folder %s has no write access", folder)
|
||||
return TempAppData()
|
||||
|
||||
|
||||
__all__ = (
|
||||
"AppDataDisabled",
|
||||
"AppDataDiskFolder",
|
||||
"ReadOnlyAppData",
|
||||
"TempAppData",
|
||||
"make_app_data",
|
||||
)
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,167 @@
|
||||
"""Application data stored by virtualenv."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from virtualenv.info import IS_ZIPAPP
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
class AppData(ABC):
|
||||
"""Abstract storage interface for the virtualenv application."""
|
||||
|
||||
@abstractmethod
|
||||
def close(self) -> None:
|
||||
"""Called before virtualenv exits."""
|
||||
|
||||
@abstractmethod
|
||||
def reset(self) -> None:
|
||||
"""Called when the user passes in the reset app data."""
|
||||
|
||||
@abstractmethod
|
||||
def py_info(self, path: Path) -> ContentStore:
|
||||
"""Return a content store for cached interpreter information at the given path.
|
||||
|
||||
:param path: the interpreter executable path
|
||||
|
||||
:returns: a content store for the cached data
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def py_info_clear(self) -> None:
|
||||
"""Clear all cached interpreter information."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def can_update(self) -> bool:
|
||||
"""``True`` if this app data store supports updating cached content."""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def embed_update_log(self, distribution: str, for_py_version: str) -> ContentStore:
|
||||
"""Return a content store for the embed update log of a distribution.
|
||||
|
||||
:param distribution: the package name (e.g. ``pip``)
|
||||
:param for_py_version: the target Python version string
|
||||
|
||||
:returns: a content store for the update log
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def house(self) -> Path:
|
||||
"""The root directory of the application data store."""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def transient(self) -> bool:
|
||||
"""``True`` if this app data store is transient and does not persist across runs."""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def wheel_image(self, for_py_version: str, name: str) -> Path:
|
||||
"""Return the path to a cached wheel image.
|
||||
|
||||
:param for_py_version: the target Python version string
|
||||
:param name: the package name
|
||||
|
||||
:returns: the path to the cached wheel
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@contextmanager
|
||||
def ensure_extracted(self, path: Path, to_folder: Path | None = None) -> Generator[Path]:
|
||||
"""Ensure a path is available on disk, extracting from zipapp if needed.
|
||||
|
||||
:param path: the path to ensure is available
|
||||
:param to_folder: optional target directory for extraction
|
||||
|
||||
:returns: yields the usable path on disk
|
||||
|
||||
"""
|
||||
if IS_ZIPAPP:
|
||||
with self.extract(path, to_folder) as result:
|
||||
yield result
|
||||
else:
|
||||
yield path
|
||||
|
||||
@abstractmethod
|
||||
@contextmanager
|
||||
def extract(self, path: Path, to_folder: Path | None) -> Generator[Path]:
|
||||
"""Extract a path from the zipapp to a location on disk.
|
||||
|
||||
:param path: the path to extract
|
||||
:param to_folder: optional target directory
|
||||
|
||||
:returns: yields the extracted path
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
@contextmanager
|
||||
def locked(self, path: Path) -> Generator[None]:
|
||||
"""Acquire an exclusive lock on the given path.
|
||||
|
||||
:param path: the path to lock
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ContentStore(ABC):
|
||||
"""A store for reading and writing cached content."""
|
||||
|
||||
@abstractmethod
|
||||
def exists(self) -> bool:
|
||||
"""Check if the stored content exists.
|
||||
|
||||
:returns: ``True`` if content exists
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def read(self) -> Any: # noqa: ANN401
|
||||
"""Read the stored content.
|
||||
|
||||
:returns: the stored content
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def write(self, content: Any) -> None: # noqa: ANN401
|
||||
"""Write content to the store.
|
||||
|
||||
:param content: the content to write
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def remove(self) -> None:
|
||||
"""Remove the stored content."""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
@contextmanager
|
||||
def locked(self) -> Generator[None]:
|
||||
"""Acquire an exclusive lock on this content store."""
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AppData",
|
||||
"ContentStore",
|
||||
]
|
||||
@@ -0,0 +1,78 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .base import AppData, ContentStore
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
from typing import Any, NoReturn
|
||||
|
||||
|
||||
class AppDataDisabled(AppData):
|
||||
"""No application cache available (most likely as we don't have write permissions)."""
|
||||
|
||||
transient = True
|
||||
can_update = False
|
||||
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
error = RuntimeError("no app data folder available, probably no write access to the folder")
|
||||
|
||||
def close(self) -> None:
|
||||
"""Do nothing."""
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Do nothing."""
|
||||
|
||||
def py_info(self, path: Path) -> ContentStoreNA: # noqa: ARG002
|
||||
return ContentStoreNA()
|
||||
|
||||
def embed_update_log(self, distribution: str, for_py_version: str) -> ContentStoreNA: # noqa: ARG002
|
||||
return ContentStoreNA()
|
||||
|
||||
def extract(self, path: Path, to_folder: Path | None) -> NoReturn: # noqa: ARG002
|
||||
raise self.error
|
||||
|
||||
@contextmanager
|
||||
def locked(self, path: Path) -> Generator[None]: # noqa: ARG002
|
||||
"""Do nothing."""
|
||||
yield
|
||||
|
||||
@property
|
||||
def house(self) -> NoReturn:
|
||||
raise self.error
|
||||
|
||||
def wheel_image(self, for_py_version: str, name: str) -> NoReturn: # noqa: ARG002
|
||||
raise self.error
|
||||
|
||||
def py_info_clear(self) -> None:
|
||||
"""Nothing to clear."""
|
||||
|
||||
|
||||
class ContentStoreNA(ContentStore):
|
||||
def exists(self) -> bool:
|
||||
return False
|
||||
|
||||
def read(self) -> None:
|
||||
"""Nothing to read."""
|
||||
return
|
||||
|
||||
def write(self, content: Any) -> None: # noqa: ANN401
|
||||
"""Nothing to write."""
|
||||
|
||||
def remove(self) -> None:
|
||||
"""Nothing to remove."""
|
||||
|
||||
@contextmanager
|
||||
def locked(self) -> Generator[None]:
|
||||
yield
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AppDataDisabled",
|
||||
"ContentStoreNA",
|
||||
]
|
||||
@@ -0,0 +1,47 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os.path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from virtualenv.util.lock import NoOpFileLock
|
||||
|
||||
from .via_disk_folder import AppDataDiskFolder, PyInfoStoreDisk
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
from typing import NoReturn
|
||||
|
||||
|
||||
class ReadOnlyAppData(AppDataDiskFolder):
|
||||
can_update = False
|
||||
|
||||
def __init__(self, folder: str) -> None:
|
||||
if not os.path.isdir(folder):
|
||||
msg = f"read-only app data directory {folder} does not exist"
|
||||
raise RuntimeError(msg)
|
||||
super().__init__(folder)
|
||||
self.lock = NoOpFileLock(folder)
|
||||
|
||||
def reset(self) -> None:
|
||||
msg = "read-only app data does not support reset"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
def py_info_clear(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def py_info(self, path: Path) -> _PyInfoStoreDiskReadOnly:
|
||||
return _PyInfoStoreDiskReadOnly(self.py_info_at, path)
|
||||
|
||||
def embed_update_log(self, distribution: str, for_py_version: str) -> NoReturn:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class _PyInfoStoreDiskReadOnly(PyInfoStoreDisk):
|
||||
def write(self, content: str) -> NoReturn: # noqa: ARG002
|
||||
msg = "read-only app data python info cannot be updated"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ReadOnlyAppData",
|
||||
]
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
r"""A rough layout of the current storage goes as:
|
||||
|
||||
::
|
||||
|
||||
virtualenv-app-data
|
||||
├── py - <version> <cache information about python interpreters>
|
||||
│ └── *.json/lock
|
||||
├── wheel <cache wheels used for seeding>
|
||||
│ ├── house
|
||||
│ │ └── *.whl <wheels downloaded go here>
|
||||
│ └── <python major.minor> -> 3.9
|
||||
│ ├── img-<version>
|
||||
│ │ └── image
|
||||
│ │ └── <install class> -> CopyPipInstall / SymlinkPipInstall
|
||||
│ │ └── <wheel name> -> pip-20.1.1-py2.py3-none-any
|
||||
│ └── embed
|
||||
│ └── 3 -> json format versioning
|
||||
│ └── *.json -> for every distribution contains data about newer embed versions and releases
|
||||
└─── unzip <in zip app we cannot refer to some internal files, so first extract them>
|
||||
└── <virtualenv version>
|
||||
├── py_info.py
|
||||
├── debug.py
|
||||
└── _virtualenv.py
|
||||
|
||||
""" # noqa: D415
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from abc import ABC
|
||||
from contextlib import contextmanager, suppress
|
||||
from hashlib import sha256
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from virtualenv.util.lock import ReentrantFileLock
|
||||
from virtualenv.util.path import safe_delete
|
||||
from virtualenv.util.zipapp import extract
|
||||
from virtualenv.version import __version__
|
||||
|
||||
from .base import AppData, ContentStore
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AppDataDiskFolder(AppData):
|
||||
"""Store the application data on the disk within a folder layout."""
|
||||
|
||||
transient = False
|
||||
can_update = True
|
||||
|
||||
def __init__(self, folder: str) -> None:
|
||||
self.lock = ReentrantFileLock(folder)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{type(self).__name__}({self.lock.path})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.lock.path)
|
||||
|
||||
def reset(self) -> None:
|
||||
LOGGER.debug("reset app data folder %s", self.lock.path)
|
||||
safe_delete(self.lock.path)
|
||||
|
||||
def close(self) -> None:
|
||||
"""Do nothing."""
|
||||
|
||||
@contextmanager
|
||||
def locked(self, path: Path) -> Generator[None]:
|
||||
path_lock = self.lock / path # ty: ignore[unsupported-operator]
|
||||
with path_lock:
|
||||
yield path_lock.path
|
||||
|
||||
@contextmanager
|
||||
def extract(self, path: Path, to_folder: Path | None) -> Generator[Path]:
|
||||
root = ReentrantFileLock(to_folder()) if to_folder is not None else self.lock / "unzip" / __version__ # ty: ignore[call-non-callable]
|
||||
with root.lock_for_key(path.name):
|
||||
dest = root.path / path.name
|
||||
if not dest.exists():
|
||||
extract(path, dest)
|
||||
yield dest
|
||||
|
||||
@property
|
||||
def py_info_at(self) -> ReentrantFileLock:
|
||||
return self.lock / "py_info" / "5" # ty: ignore[invalid-return-type]
|
||||
|
||||
def py_info(self, path: Path) -> PyInfoStoreDisk:
|
||||
return PyInfoStoreDisk(self.py_info_at, path)
|
||||
|
||||
def py_info_clear(self) -> None:
|
||||
"""clear py info."""
|
||||
py_info_folder = self.py_info_at
|
||||
with py_info_folder:
|
||||
for filename in py_info_folder.path.iterdir():
|
||||
if filename.suffix == ".json":
|
||||
with py_info_folder.lock_for_key(filename.stem):
|
||||
if filename.exists():
|
||||
filename.unlink()
|
||||
|
||||
def embed_update_log(self, distribution: str, for_py_version: str) -> EmbedDistributionUpdateStoreDisk:
|
||||
return EmbedDistributionUpdateStoreDisk(self.lock / "wheel" / for_py_version / "embed" / "3", distribution) # ty: ignore[invalid-argument-type]
|
||||
|
||||
@property
|
||||
def house(self) -> Path:
|
||||
path = self.lock.path / "wheel" / "house"
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
def wheel_image(self, for_py_version: str, name: str) -> Path:
|
||||
return self.lock.path / "wheel" / for_py_version / "image" / "1" / name
|
||||
|
||||
|
||||
class JSONStoreDisk(ContentStore, ABC):
|
||||
def __init__(self, in_folder: ReentrantFileLock, key: str, msg_args: tuple[str, ...]) -> None:
|
||||
self.in_folder = in_folder
|
||||
self.key = key
|
||||
self.msg_args = (*msg_args, self.file)
|
||||
|
||||
@property
|
||||
def file(self) -> Path:
|
||||
return self.in_folder.path / f"{self.key}.json"
|
||||
|
||||
def exists(self) -> bool:
|
||||
return self.file.exists()
|
||||
|
||||
def read(self) -> Any: # noqa: ANN401
|
||||
data, bad_format = None, False
|
||||
try:
|
||||
data = json.loads(self.file.read_text(encoding="utf-8"))
|
||||
except ValueError:
|
||||
bad_format = True
|
||||
except Exception: # noqa: BLE001, S110
|
||||
pass
|
||||
else:
|
||||
LOGGER.debug("got %s %s from %s", *self.msg_args)
|
||||
return data
|
||||
if bad_format:
|
||||
with suppress(OSError): # reading and writing on the same file may cause race on multiple processes
|
||||
self.remove()
|
||||
return None
|
||||
|
||||
def remove(self) -> None:
|
||||
self.file.unlink()
|
||||
LOGGER.debug("removed %s %s at %s", *self.msg_args)
|
||||
|
||||
@contextmanager
|
||||
def locked(self) -> Generator[None]:
|
||||
with self.in_folder.lock_for_key(self.key):
|
||||
yield
|
||||
|
||||
def write(self, content: Any) -> None: # noqa: ANN401
|
||||
folder = self.file.parent
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
self.file.write_text(json.dumps(content, sort_keys=True, indent=2), encoding="utf-8")
|
||||
LOGGER.debug("wrote %s %s at %s", *self.msg_args)
|
||||
|
||||
|
||||
class PyInfoStoreDisk(JSONStoreDisk):
|
||||
def __init__(self, in_folder: ReentrantFileLock, path: Path) -> None:
|
||||
key = sha256(str(path).encode("utf-8")).hexdigest()
|
||||
super().__init__(in_folder, key, ("python info of", path)) # ty: ignore[invalid-argument-type]
|
||||
|
||||
|
||||
class EmbedDistributionUpdateStoreDisk(JSONStoreDisk):
|
||||
def __init__(self, in_folder: ReentrantFileLock, distribution: str) -> None:
|
||||
super().__init__(
|
||||
in_folder,
|
||||
distribution,
|
||||
("embed update of distribution", distribution),
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AppDataDiskFolder",
|
||||
"JSONStoreDisk",
|
||||
"PyInfoStoreDisk",
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from tempfile import mkdtemp
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from virtualenv.util.path import safe_delete
|
||||
|
||||
from .via_disk_folder import AppDataDiskFolder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import NoReturn
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TempAppData(AppDataDiskFolder):
|
||||
transient = True
|
||||
can_update = False
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(folder=mkdtemp())
|
||||
LOGGER.debug("created temporary app data folder %s", self.lock.path)
|
||||
|
||||
def reset(self) -> None:
|
||||
"""This is a temporary folder, is already empty to start with."""
|
||||
|
||||
def close(self) -> None:
|
||||
LOGGER.debug("remove temporary app data folder %s", self.lock.path)
|
||||
safe_delete(self.lock.path)
|
||||
|
||||
def embed_update_log(self, distribution: str, for_py_version: str) -> NoReturn:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
__all__ = [
|
||||
"TempAppData",
|
||||
]
|
||||
Reference in New Issue
Block a user