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 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 fetch_info_in_venv( venv_python_path: Path) -> Tuple[List[str], Dict[str, str], str]: command_str = textwrap.dedent(""" import json import os import platform import sys impl_ver = sys.implementation.version implementation_version = "{0.major}.{0.minor}.{0.micro}".format(impl_ver) if impl_ver.releaselevel != "final": implementation_version = "{}{}{}".format( implementation_version, impl_ver.releaselevel[0], impl_ver.serial, ) sys_path = sys.path try: sys_path.remove("") except ValueError: pass print( json.dumps( { "sys_path": sys_path, "python_version": "{0.major}.{0.minor}.{0.micro}".format(sys.version_info), "environment": { "implementation_name": sys.implementation.name, "implementation_version": implementation_version, "os_name": os.name, "platform_machine": platform.machine(), "platform_release": platform.release(), "platform_system": platform.system(), "platform_version": platform.version(), "python_full_version": platform.python_version(), "platform_python_implementation": platform.python_implementation(), "python_version": ".".join(platform.python_version_tuple()[:2]), "sys_platform": sys.platform, }, } ) ) """) venv_info = json.loads( run_subprocess( [venv_python_path, "-c", command_str], capture_stderr=False, log_cmd_str="<fetch_info_in_venv commands>", ).stdout) return ( venv_info["sys_path"], venv_info["environment"], f"Python {venv_info['python_version']}", )
def create(self, verbose: bool = False) -> None: if not self.is_valid: with animate("creating shared libraries", not verbose): create_process = run_subprocess( [DEFAULT_PYTHON, "-m", "venv", "--clear", self.root]) subprocess_post_check(create_process) # ignore installed packages to ensure no unexpected patches from the OS vendor # are used self.upgrade(pip_args=["--force-reinstall"], verbose=verbose)
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 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)
def upgrade(self, *, pip_args: Optional[List[str]] = None, verbose: bool = False) -> None: if not self.is_valid: self.create(verbose=verbose) return # Don't try to upgrade multiple times per run if self.has_been_updated_this_run: logger.info(f"Already upgraded libraries in {self.root}") return if pip_args is None: pip_args = [] logger.info(f"Upgrading shared libraries in {self.root}") ignored_args = ["--editable"] _pip_args = [arg for arg in pip_args if arg not in ignored_args] if not verbose: _pip_args.append("-q") try: with animate("upgrading shared libraries", not verbose): upgrade_process = run_subprocess([ self.python_path, "-m", "pip", "--disable-pip-version-check", "install", *_pip_args, "--upgrade", "pip", "setuptools", "wheel", ]) subprocess_post_check(upgrade_process) self.has_been_updated_this_run = True self.pip_path.touch() except Exception: logger.error("Failed to upgrade shared libraries", exc_info=True)
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 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 get_python_version(self) -> str: return run_subprocess([str(self.python_path), "--version"]).stdout.strip()
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)