from distutils.sysconfig import get_python_lib import os import platform import site import sys from typing import Iterable, List, Tuple, Union try: from pip._internal.utils.misc import get_installed_distributions except ImportError: from pip import get_installed_distributions _env = None def collect_environment_without_packages() -> dict: """ Return the version of the Python executable and the running platform. """ return { "python": sys.version.replace("\n", ""), "packages": [], "platform": platform.platform(), } def collect_environment(no_cache: bool = False) -> dict: """ Return the version of the Python executable, the versions of the currently loaded packages \ and the running platform. The result is cached unless `no_cache` is True. """ global _env if _env is None or no_cache: _env = collect_environment_without_packages() _env["packages"] = collect_loaded_packages() return _env def collect_loaded_packages() -> List[Tuple[str, str]]: """ Return the currently loaded package names and their versions. """ dists = get_installed_distributions() get_dist_files = DistFilesFinder() file_table = {} for dist in dists: for file in get_dist_files(dist): file_table[file] = dist used_dists = set() # we greedily load all values to a list to avoid weird # "dictionary changed size during iteration" errors for module in list(sys.modules.values()): try: dist = file_table[module.__file__] except (AttributeError, KeyError): continue used_dists.add(dist) return sorted((dist.project_name, dist.version) for dist in used_dists) class DistFilesFinder: """Functor to find the files belonging to a package.""" def __init__(self): """Initialize a new DistFilesFinder.""" try: self.sitedirs = set(site.getsitepackages() + [site.getusersitepackages()]) except AttributeError: self.sitedirs = [get_python_lib()] def __call__(self, dist: Union["pip._vendor.pkg_resources.DistInfoDistribution", "pip._vendor.pkg_resources.EggInfoDistribution"]) \ -> Iterable[str]: # noqa: D401 """ Generator of the files belonging to a package. :param dist: The package object. """ if dist.has_metadata("RECORD"): lines = dist.get_metadata_lines("RECORD") paths = [l.split(",")[0] for l in lines] for p in paths: yield os.path.abspath(os.path.join(dist.location, p)) return if dist.has_metadata("installed-files.txt"): paths = dist.get_metadata_lines("installed-files.txt") for p in paths: yield os.path.abspath(os.path.join(dist.egg_info, p)) return if dist.location in self.sitedirs: # egg-info without an explicit file list file_probe = os.path.join(dist.location, dist.project_name + ".py") if os.path.isfile(file_probe): yield os.path.abspath(file_probe) return for dir_probe in (os.path.join(dist.location, dist.project_name), os.path.join(dist.location, dist.project_name.lower())): if os.path.isdir(dir_probe): for root, _, files in os.walk(dir_probe): for file in files: yield os.path.abspath(os.path.join(root, file)) break return if dist.module_path is not None: # development install for root, _, files in os.walk(dist.module_path): for file in files: yield os.path.abspath(os.path.join(root, file)) return # we did our best and still failed at this point