JFIFxxC      C  " }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbrimport json import logging from pathlib import Path from typing import Any, Dict, List, NamedTuple, Optional, Union from pipx.emojis import hazard from pipx.util import PipxError, pipx_wrap logger = logging.getLogger(__name__) PIPX_INFO_FILENAME = "pipx_metadata.json" class JsonEncoderHandlesPath(json.JSONEncoder): def default(self, obj: Any) -> Any: # only handles what json.JSONEncoder doesn't understand by default if isinstance(obj, Path): return {"__type__": "Path", "__Path__": str(obj)} return super().default(obj) def _json_decoder_object_hook(json_dict: Dict[str, Any]) -> Union[Dict[str, Any], Path]: if json_dict.get("__type__", None) == "Path" and "__Path__" in json_dict: return Path(json_dict["__Path__"]) return json_dict class PackageInfo(NamedTuple): package: Optional[str] package_or_url: Optional[str] pip_args: List[str] include_dependencies: bool include_apps: bool apps: List[str] app_paths: List[Path] apps_of_dependencies: List[str] app_paths_of_dependencies: Dict[str, List[Path]] package_version: str man_pages: List[str] = [] man_paths: List[Path] = [] man_pages_of_dependencies: List[str] = [] man_paths_of_dependencies: Dict[str, List[Path]] = {} suffix: str = "" class PipxMetadata: # Only change this if file format changes __METADATA_VERSION__: str = "0.3" def __init__(self, venv_dir: Path, read: bool = True): self.venv_dir = venv_dir # We init this instance with reasonable fallback defaults for all # members, EXCEPT for those we cannot know: # self.main_package.package=None # self.main_package.package_or_url=None # self.python_version=None self.main_package = PackageInfo( package=None, package_or_url=None, pip_args=[], include_dependencies=False, include_apps=True, # always True for main_package apps=[], app_paths=[], apps_of_dependencies=[], app_paths_of_dependencies={}, man_pages=[], man_paths=[], man_pages_of_dependencies=[], man_paths_of_dependencies={}, package_version="", ) self.python_version: Optional[str] = None self.venv_args: List[str] = [] self.injected_packages: Dict[str, PackageInfo] = {} if read: self.read() def to_dict(self) -> Dict[str, Any]: return { "main_package": self.main_package._asdict(), "python_version": self.python_version, "venv_args": self.venv_args, "injected_packages": {name: data._asdict() for (name, data) in self.injected_packages.items()}, "pipx_metadata_version": self.__METADATA_VERSION__, } def _convert_legacy_metadata(self, metadata_dict: Dict[str, Any]) -> Dict[str, Any]: if metadata_dict["pipx_metadata_version"] in ("0.2", self.__METADATA_VERSION__): return metadata_dict elif metadata_dict["pipx_metadata_version"] == "0.1": main_package_data = metadata_dict["main_package"] if main_package_data["package"] != self.venv_dir.name: # handle older suffixed packages gracefully main_package_data["suffix"] = self.venv_dir.name.replace(main_package_data["package"], "") return metadata_dict else: raise PipxError( f""" {self.venv_dir.name}: Unknown metadata version {metadata_dict['pipx_metadata_version']}. Perhaps it was installed with a later version of pipx. """ ) def from_dict(self, input_dict: Dict[str, Any]) -> None: input_dict = self._convert_legacy_metadata(input_dict) self.main_package = PackageInfo(**input_dict["main_package"]) self.python_version = input_dict["python_version"] self.venv_args = input_dict["venv_args"] self.injected_packages = { f"{name}{data.get('suffix', '')}": PackageInfo(**data) for (name, data) in input_dict["injected_packages"].items() } def _validate_before_write(self) -> None: if ( self.main_package.package is None or self.main_package.package_or_url is None or not self.main_package.include_apps ): logger.debug(f"PipxMetadata corrupt:\n{self.to_dict()}") raise PipxError("Internal Error: PipxMetadata is corrupt, cannot write.") def write(self) -> None: self._validate_before_write() try: with open(self.venv_dir / PIPX_INFO_FILENAME, "w", encoding="utf-8") as pipx_metadata_fh: json.dump( self.to_dict(), pipx_metadata_fh, indent=4, sort_keys=True, cls=JsonEncoderHandlesPath, ) except OSError: logger.warning( pipx_wrap( f""" {hazard} Unable to write {PIPX_INFO_FILENAME} to {self.venv_dir}. This may cause future pipx operations involving {self.venv_dir.name} to fail or behave incorrectly. """, subsequent_indent=" " * 4, ) ) def read(self, verbose: bool = False) -> None: try: with open(self.venv_dir / PIPX_INFO_FILENAME, "rb") as pipx_metadata_fh: self.from_dict(json.load(pipx_metadata_fh, object_hook=_json_decoder_object_hook)) except OSError: # Reset self if problem reading if verbose: logger.warning( pipx_wrap( f""" {hazard} Unable to read {PIPX_INFO_FILENAME} in {self.venv_dir}. This may cause this or future pipx operations involving {self.venv_dir.name} to fail or behave incorrectly. """, subsequent_indent=" " * 4, ) ) return