def test_pipx_metadata_file_validation(tmp_path, test_package): venv_dir = tmp_path / "venv" venv_dir.mkdir() pipx_metadata = PipxMetadata(venv_dir) pipx_metadata.main_package = test_package pipx_metadata.python_version = "3.4.5" pipx_metadata.venv_args = ["--system-site-packages"] pipx_metadata.injected_packages = {} with pytest.raises(PipxError): pipx_metadata.write()
def test_pipx_metadata_file_create(tmp_path): pipx_metadata = PipxMetadata(tmp_path) pipx_metadata.main_package = TEST_PACKAGE1 pipx_metadata.python_version = "3.4.5" pipx_metadata.venv_args = ["--system-site-packages"] pipx_metadata.injected_packages = {"injected": TEST_PACKAGE2} pipx_metadata.write() pipx_metadata2 = PipxMetadata(tmp_path) for attribute in [ "venv_dir", "main_package", "python_version", "venv_args", "injected_packages", ]: assert getattr(pipx_metadata, attribute) == getattr(pipx_metadata2, attribute)
class Venv: """Abstraction for a virtual environment with various useful methods for pipx""" def __init__(self, path: Path, *, verbose: bool = False, python: str = DEFAULT_PYTHON) -> None: self.root = path self._python = python self.bin_path, self.python_path = get_venv_paths(self.root) self.pipx_metadata = PipxMetadata(venv_dir=path) self.verbose = verbose self.do_animation = not verbose try: self._existing = self.root.exists() and next(self.root.iterdir()) except StopIteration: self._existing = False if self._existing and self.uses_shared_libs and not shared_libs.is_valid: logging.warning( f"Shared libraries not found, but are required for package {self.root.name}. " "Attempting to install now.") shared_libs.create([]) if shared_libs.is_valid: logging.info("Successfully created shared libraries") else: raise PipxError( f"Error: pipx's shared venv is invalid and " "needs re-installation. To fix this, install or reinstall a " "package. For example,\n" f" pipx install {self.root.name} --force") @property def uses_shared_libs(self) -> bool: if self._existing: pth_files = self.root.glob("**/" + PIPX_SHARED_PTH) return next(pth_files, None) is not None else: # always use shared libs when creating a new venv return True @property def package_metadata(self) -> Dict[str, PackageInfo]: return_dict = self.pipx_metadata.injected_packages.copy() if self.pipx_metadata.main_package.package is not None: return_dict[self.pipx_metadata.main_package. package] = self.pipx_metadata.main_package return return_dict def create_venv(self, venv_args: List[str], pip_args: List[str]) -> None: with animate("creating virtual environment", self.do_animation): cmd = [self._python, "-m", "venv", "--without-pip"] run(cmd + venv_args + [str(self.root)]) shared_libs.create(pip_args, self.verbose) pipx_pth = get_site_packages(self.python_path) / PIPX_SHARED_PTH # write path pointing to the shared libs site-packages directory # example pipx_pth location: # ~/.local/pipx/venvs/black/lib/python3.8/site-packages/pipx_shared.pth # example shared_libs.site_packages location: # ~/.local/pipx/shared/lib/python3.6/site-packages # # https://docs.python.org/3/library/site.html # A path configuration file is a file whose name has the form 'name.pth'. # its contents are additional items (one per line) to be added to sys.path pipx_pth.write_text(str(shared_libs.site_packages) + "\n", encoding="utf-8") self.pipx_metadata.venv_args = venv_args self.pipx_metadata.python_version = self.get_python_version() def safe_to_remove(self) -> bool: return not self._existing def remove_venv(self) -> None: if self.safe_to_remove(): rmdir(self.root) else: logging.warning(f"Not removing existing venv {self.root} because " "it was not created in this session") def upgrade_packaging_libraries(self, pip_args: List[str]) -> None: if self.uses_shared_libs: shared_libs.upgrade(pip_args, self.verbose) else: # TODO: setuptools and wheel? Original code didn't bother # but shared libs code does. self._upgrade_package_no_metadata("pip", pip_args) def install_package( self, package: Optional[str], # if None, will be determined in this function package_or_url: str, pip_args: List[str], include_dependencies: bool, include_apps: bool, is_main_package: bool, ) -> None: with animate(f"installing package {package_or_url!r}", self.do_animation): if pip_args is None: pip_args = [] if package is None: # If no package name is supplied, install only main package # first in order to see what its name is old_package_set = self.list_installed_packages() cmd = ["install"] + pip_args + ["--no-dependencies" ] + [package_or_url] self._run_pip(cmd) installed_packages = self.list_installed_packages( ) - old_package_set if len(installed_packages) == 1: package = installed_packages.pop() logging.info(f"Determined package name: '{package}'") else: package = None cmd = ["install"] + pip_args + [package_or_url] self._run_pip(cmd) if package is None: logging.warning( f"Cannot determine package name for package_or_url='{package_or_url}'. " f"Unable to retrieve package metadata. " f"Unable to verify if package was installed properly.") return self._update_package_metadata( package=package, package_or_url=package_or_url, pip_args=pip_args, include_dependencies=include_dependencies, include_apps=include_apps, is_main_package=is_main_package, ) # Verify package installed ok if self.package_metadata[package].package_version is None: raise PackageInstallFailureError def get_venv_metadata_for_package(self, package: str) -> VenvMetadata: data = json.loads( get_script_output(self.python_path, VENV_METADATA_INSPECTOR, package, str(self.bin_path))) app_paths = [Path(p) for p in data["app_paths"]] if WINDOWS: windows_bin_paths = set() for app in app_paths: # windows has additional files staring with the same name that are required # to run the app for win_exec in app.parent.glob(f"{app.name}*.exe"): windows_bin_paths.add(win_exec) data["app_paths"] = list(windows_bin_paths) else: data["app_paths"] = app_paths data["apps_of_dependencies"] = [] for dep, raw_paths in data["app_paths_of_dependencies"].items(): paths = [Path(raw_path) for raw_path in raw_paths] data["app_paths_of_dependencies"][dep] = paths data["apps_of_dependencies"] += [path.name for path in paths] return VenvMetadata(**data) def _update_package_metadata( self, package: str, package_or_url: str, pip_args: List[str], include_dependencies: bool, include_apps: bool, is_main_package: bool, ) -> None: venv_package_metadata = self.get_venv_metadata_for_package(package) package_info = PackageInfo( package=package, package_or_url=abs_path_if_local(package_or_url, self, pip_args), pip_args=pip_args, include_apps=include_apps, include_dependencies=include_dependencies, apps=venv_package_metadata.apps, app_paths=venv_package_metadata.app_paths, apps_of_dependencies=venv_package_metadata.apps_of_dependencies, app_paths_of_dependencies=venv_package_metadata. app_paths_of_dependencies, package_version=venv_package_metadata.package_version, ) if is_main_package: self.pipx_metadata.main_package = package_info else: self.pipx_metadata.injected_packages[package] = package_info self.pipx_metadata.write() def get_python_version(self) -> str: return run_subprocess([str(self.python_path), "--version"]).stdout.strip() def pip_search(self, search_term: str, pip_search_args: List[str]) -> str: cmd_run = run_subprocess( [str(self.python_path), "-m", "pip", "search"] + pip_search_args + [search_term]) return cmd_run.stdout.strip() def list_installed_packages(self) -> Set[str]: cmd_run = run_subprocess( [str(self.python_path), "-m", "pip", "list", "--format=json"]) pip_list = json.loads(cmd_run.stdout.strip()) return set([x["name"] for x in pip_list]) def run_app(self, app: str, app_args: List[str]) -> int: cmd = [str(self.bin_path / app)] + app_args try: return run(cmd, check=False) except KeyboardInterrupt: return 130 # shell code for Ctrl-C def _upgrade_package_no_metadata(self, package_or_url: str, pip_args: List[str]) -> None: with animate(f"upgrading package {package_or_url!r}", self.do_animation): self._run_pip(["install"] + pip_args + ["--upgrade", package_or_url]) def upgrade_package( self, package: str, package_or_url: str, pip_args: List[str], include_dependencies: bool, include_apps: bool, is_main_package: bool, ) -> None: with animate(f"upgrading package {package_or_url!r}", self.do_animation): self._run_pip(["install"] + pip_args + ["--upgrade", package_or_url]) self._update_package_metadata( package=package, package_or_url=package_or_url, pip_args=pip_args, include_dependencies=include_dependencies, include_apps=include_apps, is_main_package=is_main_package, ) def _run_pip(self, cmd: List[str]) -> int: cmd = [str(self.python_path), "-m", "pip"] + cmd if not self.verbose: cmd.append("-q") return run(cmd)
class Venv: """Abstraction for a virtual environment with various useful methods for pipx""" def __init__(self, path: Path, *, verbose: bool = False, python: str = DEFAULT_PYTHON) -> None: self.root = path self.python = python self.bin_path, self.python_path = get_venv_paths(self.root) self.pipx_metadata = PipxMetadata(venv_dir=path) self.verbose = verbose self.do_animation = not verbose try: self._existing = self.root.exists() and next(self.root.iterdir()) except StopIteration: self._existing = False if self._existing and self.uses_shared_libs: if shared_libs.is_valid: if shared_libs.needs_upgrade: shared_libs.upgrade(verbose=verbose) else: shared_libs.create(verbose) if not shared_libs.is_valid: raise PipxError( pipx_wrap(f""" Error: pipx's shared venv {shared_libs.root} is invalid and needs re-installation. To fix this, install or reinstall a package. For example: """) + f"\n pipx install {self.root.name} --force", wrap_message=False, ) @property def name(self) -> str: if self.pipx_metadata.main_package.package is not None: venv_name = (f"{self.pipx_metadata.main_package.package}" f"{self.pipx_metadata.main_package.suffix}") else: venv_name = self.root.name return venv_name @property def uses_shared_libs(self) -> bool: if self._existing: pth_files = self.root.glob("**/" + PIPX_SHARED_PTH) return next(pth_files, None) is not None else: # always use shared libs when creating a new venv return True @property def package_metadata(self) -> Dict[str, PackageInfo]: return_dict = self.pipx_metadata.injected_packages.copy() if self.pipx_metadata.main_package.package is not None: return_dict[self.pipx_metadata.main_package. package] = self.pipx_metadata.main_package return return_dict @property def main_package_name(self) -> str: if self.pipx_metadata.main_package.package is None: # This is OK, because if no metadata, we are pipx < v0.15.0.0 and # venv_name==main_package_name return self.root.name else: return self.pipx_metadata.main_package.package def create_venv(self, venv_args: List[str], pip_args: List[str]) -> None: with animate("creating virtual environment", self.do_animation): cmd = [self.python, "-m", "venv", "--without-pip"] venv_process = run_subprocess(cmd + venv_args + [str(self.root)]) subprocess_post_check(venv_process) shared_libs.create(self.verbose) pipx_pth = get_site_packages(self.python_path) / PIPX_SHARED_PTH # write path pointing to the shared libs site-packages directory # example pipx_pth location: # ~/.local/pipx/venvs/black/lib/python3.8/site-packages/pipx_shared.pth # example shared_libs.site_packages location: # ~/.local/pipx/shared/lib/python3.6/site-packages # # https://docs.python.org/3/library/site.html # A path configuration file is a file whose name has the form 'name.pth'. # its contents are additional items (one per line) to be added to sys.path pipx_pth.write_text(f"{shared_libs.site_packages}\n", encoding="utf-8") self.pipx_metadata.venv_args = venv_args self.pipx_metadata.python_version = self.get_python_version() def safe_to_remove(self) -> bool: return not self._existing def remove_venv(self) -> None: if self.safe_to_remove(): rmdir(self.root) else: logger.warning( pipx_wrap( f""" {hazard} Not removing existing venv {self.root} because it was not created in this session """, subsequent_indent=" " * 4, )) def upgrade_packaging_libraries(self, pip_args: List[str]) -> None: if self.uses_shared_libs: shared_libs.upgrade(verbose=self.verbose) else: # TODO: setuptools and wheel? Original code didn't bother # but shared libs code does. self._upgrade_package_no_metadata("pip", pip_args) def install_package( self, package_name: str, package_or_url: str, pip_args: List[str], include_dependencies: bool, include_apps: bool, is_main_package: bool, suffix: str = "", ) -> None: # package_name in package specifier can mismatch URL due to user error package_or_url = fix_package_name(package_or_url, package_name) # check syntax and clean up spec and pip_args (package_or_url, pip_args) = parse_specifier_for_install(package_or_url, pip_args) with animate( f"installing {full_package_description(package_name, package_or_url)}", self.do_animation, ): # do not use -q with `pip install` so subprocess_post_check_pip_errors # has more information to analyze in case of failure. cmd = ([str(self.python_path), "-m", "pip", "install"] + pip_args + [package_or_url]) # no logging because any errors will be specially logged by # subprocess_post_check_handle_pip_error() pip_process = run_subprocess(cmd, log_stdout=False, log_stderr=False) subprocess_post_check_handle_pip_error(pip_process) if pip_process.returncode: raise PipxError( f"Error installing {full_package_description(package_name, package_or_url)}." ) self._update_package_metadata( package_name=package_name, package_or_url=package_or_url, pip_args=pip_args, include_dependencies=include_dependencies, include_apps=include_apps, is_main_package=is_main_package, suffix=suffix, ) # Verify package installed ok if self.package_metadata[package_name].package_version is None: raise PipxError( f"Unable to install " f"{full_package_description(package_name, package_or_url)}.\n" f"Check the name or spec for errors, and verify that it can " f"be installed with pip.", wrap_message=False, ) def install_package_no_deps(self, package_or_url: str, pip_args: List[str]) -> str: with animate(f"determining package name from {package_or_url!r}", self.do_animation): old_package_set = self.list_installed_packages() cmd = ["install"] + ["--no-dependencies" ] + pip_args + [package_or_url] pip_process = self._run_pip(cmd) subprocess_post_check(pip_process, raise_error=False) if pip_process.returncode: raise PipxError(f""" Cannot determine package name from spec {package_or_url!r}. Check package spec for errors. """) installed_packages = self.list_installed_packages() - old_package_set if len(installed_packages) == 1: package_name = installed_packages.pop() logger.info(f"Determined package name: {package_name}") else: logger.info(f"old_package_set = {old_package_set}") logger.info(f"install_packages = {installed_packages}") raise PipxError(f""" Cannot determine package name from spec {package_or_url!r}. Check package spec for errors. """) return package_name def get_venv_metadata_for_package( self, package_name: str, package_extras: Set[str]) -> VenvMetadata: data_start = time.time() venv_metadata = inspect_venv(package_name, package_extras, self.bin_path, self.python_path) logger.info( f"get_venv_metadata_for_package: {1e3*(time.time()-data_start):.0f}ms" ) return venv_metadata def _update_package_metadata( self, package_name: str, package_or_url: str, pip_args: List[str], include_dependencies: bool, include_apps: bool, is_main_package: bool, suffix: str = "", ) -> None: venv_package_metadata = self.get_venv_metadata_for_package( package_name, get_extras(package_or_url)) package_info = PackageInfo( package=package_name, package_or_url=parse_specifier_for_metadata(package_or_url), pip_args=pip_args, include_apps=include_apps, include_dependencies=include_dependencies, apps=venv_package_metadata.apps, app_paths=venv_package_metadata.app_paths, apps_of_dependencies=venv_package_metadata.apps_of_dependencies, app_paths_of_dependencies=venv_package_metadata. app_paths_of_dependencies, package_version=venv_package_metadata.package_version, suffix=suffix, ) if is_main_package: self.pipx_metadata.main_package = package_info else: self.pipx_metadata.injected_packages[package_name] = package_info self.pipx_metadata.write() def get_python_version(self) -> str: return run_subprocess([str(self.python_path), "--version"]).stdout.strip() def list_installed_packages(self) -> Set[str]: cmd_run = run_subprocess( [str(self.python_path), "-m", "pip", "list", "--format=json"]) pip_list = json.loads(cmd_run.stdout.strip()) return set([x["name"] for x in pip_list]) def _find_entry_point(self, app: str) -> Optional[EntryPoint]: if not self.python_path.exists(): return None dists = Distribution.discover( name=self.main_package_name, path=[str(get_site_packages(self.python_path))], ) for dist in dists: for ep in dist.entry_points: if ep.group == "pipx.run" and ep.name == app: return ep return None def run_app(self, app: str, filename: str, app_args: List[str]) -> NoReturn: entry_point = self._find_entry_point(app) # No [pipx.run] entry point; default to run console script. if entry_point is None: exec_app([str(self.bin_path / filename)] + app_args) # Evaluate and execute the entry point. # TODO: After dropping support for Python < 3.9, use # "entry_point.module" and "entry_point.attr" instead. match = _entry_point_value_pattern.match(entry_point.value) assert match is not None, "invalid entry point" module, attr = match.group("module", "attr") code = (f"import sys, {module}\n" f"sys.argv[0] = {entry_point.name!r}\n" f"sys.exit({module}.{attr}())\n") exec_app([str(self.python_path), "-c", code] + app_args) def has_app(self, app: str, filename: str) -> bool: if self._find_entry_point(app) is not None: return True return (self.bin_path / filename).is_file() def _upgrade_package_no_metadata(self, package_name: str, pip_args: List[str]) -> None: with animate( f"upgrading {full_package_description(package_name, package_name)}", self.do_animation, ): pip_process = self._run_pip(["install"] + pip_args + ["--upgrade", package_name]) subprocess_post_check(pip_process) def upgrade_package( self, package_name: str, package_or_url: str, pip_args: List[str], include_dependencies: bool, include_apps: bool, is_main_package: bool, suffix: str = "", ) -> None: with animate( f"upgrading {full_package_description(package_name, package_or_url)}", self.do_animation, ): pip_process = self._run_pip(["install"] + pip_args + ["--upgrade", package_or_url]) subprocess_post_check(pip_process) self._update_package_metadata( package_name=package_name, package_or_url=package_or_url, pip_args=pip_args, include_dependencies=include_dependencies, include_apps=include_apps, is_main_package=is_main_package, suffix=suffix, ) def _run_pip(self, cmd: List[str]) -> CompletedProcess: cmd = [str(self.python_path), "-m", "pip"] + cmd if not self.verbose: cmd.append("-q") return run_subprocess(cmd) def run_pip_get_exit_code(self, cmd: List[str]) -> ExitCode: cmd = [str(self.python_path), "-m", "pip"] + cmd if not self.verbose: cmd.append("-q") returncode = run_subprocess(cmd, capture_stdout=False, capture_stderr=False).returncode if returncode: cmd_str = " ".join(str(c) for c in cmd) logger.error(f"{cmd_str!r} failed") return ExitCode(returncode)
class Venv: """Abstraction for a virtual environment with various useful methods for pipx""" def __init__(self, path: Path, *, verbose: bool = False, python: str = DEFAULT_PYTHON) -> None: self.root = path self.python = python self.bin_path, self.python_path = get_venv_paths(self.root) self.pipx_metadata = PipxMetadata(venv_dir=path) self.verbose = verbose self.do_animation = not verbose try: self._existing = self.root.exists() and next(self.root.iterdir()) except StopIteration: self._existing = False if self._existing and self.uses_shared_libs: if shared_libs.is_valid: if shared_libs.needs_upgrade: shared_libs.upgrade(verbose=verbose) else: shared_libs.create(verbose) if not shared_libs.is_valid: raise PipxError( f"Error: pipx's shared venv {shared_libs.root} is invalid and " "needs re-installation. To fix this, install or reinstall a " "package. For example,\n" f" pipx install {self.root.name} --force") @property def name(self) -> str: if self.pipx_metadata.main_package.package is not None: venv_name = (f"{self.pipx_metadata.main_package.package}" f"{self.pipx_metadata.main_package.suffix}") else: venv_name = self.root.name return venv_name @property def uses_shared_libs(self) -> bool: if self._existing: pth_files = self.root.glob("**/" + PIPX_SHARED_PTH) return next(pth_files, None) is not None else: # always use shared libs when creating a new venv return True @property def package_metadata(self) -> Dict[str, PackageInfo]: return_dict = self.pipx_metadata.injected_packages.copy() if self.pipx_metadata.main_package.package is not None: return_dict[self.pipx_metadata.main_package. package] = self.pipx_metadata.main_package return return_dict @property def main_package_name(self) -> str: if self.pipx_metadata.main_package.package is None: # This is OK, because if no metadata, we are pipx < v0.15.0.0 and # venv_name==main_package_name return self.root.name else: return self.pipx_metadata.main_package.package def create_venv(self, venv_args: List[str], pip_args: List[str]) -> None: with animate("creating virtual environment", self.do_animation): cmd = [self.python, "-m", "venv", "--without-pip"] venv_process = run_subprocess(cmd + venv_args + [str(self.root)]) subprocess_post_check(venv_process) shared_libs.create(self.verbose) pipx_pth = get_site_packages(self.python_path) / PIPX_SHARED_PTH # write path pointing to the shared libs site-packages directory # example pipx_pth location: # ~/.local/pipx/venvs/black/lib/python3.8/site-packages/pipx_shared.pth # example shared_libs.site_packages location: # ~/.local/pipx/shared/lib/python3.6/site-packages # # https://docs.python.org/3/library/site.html # A path configuration file is a file whose name has the form 'name.pth'. # its contents are additional items (one per line) to be added to sys.path pipx_pth.write_text(f"{shared_libs.site_packages}\n", encoding="utf-8") self.pipx_metadata.venv_args = venv_args self.pipx_metadata.python_version = self.get_python_version() def safe_to_remove(self) -> bool: return not self._existing def remove_venv(self) -> None: if self.safe_to_remove(): rmdir(self.root) else: logger.warning(f"Not removing existing venv {self.root} because " "it was not created in this session") def upgrade_packaging_libraries(self, pip_args: List[str]) -> None: if self.uses_shared_libs: shared_libs.upgrade(verbose=self.verbose) else: # TODO: setuptools and wheel? Original code didn't bother # but shared libs code does. self._upgrade_package_no_metadata("pip", pip_args) def install_package( self, package: str, package_or_url: str, pip_args: List[str], include_dependencies: bool, include_apps: bool, is_main_package: bool, suffix: str = "", ) -> None: if pip_args is None: pip_args = [] # package name in package specifier can mismatch URL due to user error package_or_url = fix_package_name(package_or_url, package) # check syntax and clean up spec and pip_args (package_or_url, pip_args) = parse_specifier_for_install(package_or_url, pip_args) with animate( f"installing {full_package_description(package, package_or_url)}", self.do_animation, ): cmd = ["install"] + pip_args + [package_or_url] pip_process = self._run_pip(cmd) subprocess_post_check(pip_process, raise_error=False) if pip_process.returncode: raise PipxError( f"Error installing " f"{full_package_description(package, package_or_url)}.") self._update_package_metadata( package=package, package_or_url=package_or_url, pip_args=pip_args, include_dependencies=include_dependencies, include_apps=include_apps, is_main_package=is_main_package, suffix=suffix, ) # Verify package installed ok if self.package_metadata[package].package_version is None: raise PipxError( f"Unable to install " f"{full_package_description(package, package_or_url)}.\n" f"Check the name or spec for errors, and verify that it can " f"be installed with pip.") def install_package_no_deps(self, package_or_url: str, pip_args: List[str]) -> str: with animate(f"determining package name from {package_or_url!r}", self.do_animation): old_package_set = self.list_installed_packages() cmd = ["install"] + ["--no-dependencies" ] + pip_args + [package_or_url] pip_process = self._run_pip(cmd) subprocess_post_check(pip_process, raise_error=False) if pip_process.returncode: raise PipxError( f"Cannot determine package name from spec {package_or_url!r}. " f"Check package spec for errors.") installed_packages = self.list_installed_packages() - old_package_set if len(installed_packages) == 1: package = installed_packages.pop() logger.info(f"Determined package name: {package}") else: logger.info(f"old_package_set = {old_package_set}") logger.info(f"install_packages = {installed_packages}") raise PipxError( f"Cannot determine package name from spec {package_or_url!r}. " f"Check package spec for errors.") return package def get_venv_metadata_for_package(self, package: str) -> VenvMetadata: data = json.loads( run_subprocess( [ self.python_path, "-c", VENV_METADATA_INSPECTOR, package, self.bin_path, ], capture_stderr=False, log_cmd_str=" ".join([ str(self.python_path), "-c", "<contents of venv_metadata_inspector.py>", package, str(self.bin_path), ]), ).stdout) venv_metadata_traceback = data.pop("exception_traceback", None) if venv_metadata_traceback is not None: logger.error("Internal error with venv metadata inspection.") logger.info( f"venv_metadata_inspector.py Traceback:\n{venv_metadata_traceback}" ) data["app_paths"] = [Path(p) for p in data["app_paths"]] for dep in data["app_paths_of_dependencies"]: data["app_paths_of_dependencies"][dep] = [ Path(p) for p in data["app_paths_of_dependencies"][dep] ] return VenvMetadata(**data) def _update_package_metadata( self, package: str, package_or_url: str, pip_args: List[str], include_dependencies: bool, include_apps: bool, is_main_package: bool, suffix: str = "", ) -> None: venv_package_metadata = self.get_venv_metadata_for_package(package) package_info = PackageInfo( package=package, package_or_url=parse_specifier_for_metadata(package_or_url), pip_args=pip_args, include_apps=include_apps, include_dependencies=include_dependencies, apps=venv_package_metadata.apps, app_paths=venv_package_metadata.app_paths, apps_of_dependencies=venv_package_metadata.apps_of_dependencies, app_paths_of_dependencies=venv_package_metadata. app_paths_of_dependencies, package_version=venv_package_metadata.package_version, suffix=suffix, ) if is_main_package: self.pipx_metadata.main_package = package_info else: self.pipx_metadata.injected_packages[package] = package_info self.pipx_metadata.write() def get_python_version(self) -> str: return run_subprocess([str(self.python_path), "--version"]).stdout.strip() def list_installed_packages(self) -> Set[str]: cmd_run = run_subprocess( [str(self.python_path), "-m", "pip", "list", "--format=json"]) pip_list = json.loads(cmd_run.stdout.strip()) return set([x["name"] for x in pip_list]) def run_app(self, app: str, app_args: List[str]) -> None: exec_app([str(self.bin_path / app)] + app_args) def _upgrade_package_no_metadata(self, package: str, pip_args: List[str]) -> None: with animate(f"upgrading {full_package_description(package, package)}", self.do_animation): pip_process = self._run_pip(["install"] + pip_args + ["--upgrade", package]) subprocess_post_check(pip_process) def upgrade_package( self, package: str, package_or_url: str, pip_args: List[str], include_dependencies: bool, include_apps: bool, is_main_package: bool, suffix: str = "", ) -> None: with animate( f"upgrading {full_package_description(package, package_or_url)}", self.do_animation, ): pip_process = self._run_pip(["install"] + pip_args + ["--upgrade", package_or_url]) subprocess_post_check(pip_process) self._update_package_metadata( package=package, package_or_url=package_or_url, pip_args=pip_args, include_dependencies=include_dependencies, include_apps=include_apps, is_main_package=is_main_package, suffix=suffix, ) def _run_pip(self, cmd: List[str]) -> CompletedProcess: cmd = [str(self.python_path), "-m", "pip"] + cmd if not self.verbose: cmd.append("-q") return run_subprocess(cmd) def run_pip_get_exit_code(self, cmd: List[str]) -> ExitCode: cmd = [str(self.python_path), "-m", "pip"] + cmd if not self.verbose: cmd.append("-q") returncode = run_subprocess(cmd, capture_stdout=False, capture_stderr=False).returncode if returncode: cmd_str = " ".join(str(c) for c in cmd) logger.error(f"{cmd_str!r} failed") return ExitCode(returncode)