"""Metadata generation logic for source distributions. """ import logging import os from pip._internal.exceptions import InstallationError from pip._internal.utils.misc import ensure_dir from pip._internal.utils.setuptools_build import make_setuptools_shim_args from pip._internal.utils.subprocess import call_subprocess from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs import vcs if MYPY_CHECK_RUNNING: from typing import Callable, List from pip._internal.req.req_install import InstallRequirement logger = logging.getLogger(__name__) def get_metadata_generator(install_req): # type: (InstallRequirement) -> Callable[[InstallRequirement], str] """Return a callable metadata generator for this InstallRequirement. A metadata generator takes an InstallRequirement (install_req) as an input, generates metadata via the appropriate process for that install_req and returns the generated metadata directory. """ if not install_req.use_pep517: return _generate_metadata_legacy return _generate_metadata def _find_egg_info(source_directory, is_editable): # type: (str, bool) -> str """Find an .egg-info in `source_directory`, based on `is_editable`. """ def looks_like_virtual_env(path): # type: (str) -> bool return ( os.path.lexists(os.path.join(path, 'bin', 'python')) or os.path.exists(os.path.join(path, 'Scripts', 'Python.exe')) ) def locate_editable_egg_info(base): # type: (str) -> List[str] candidates = [] # type: List[str] for root, dirs, files in os.walk(base): for dir_ in vcs.dirnames: if dir_ in dirs: dirs.remove(dir_) # Iterate over a copy of ``dirs``, since mutating # a list while iterating over it can cause trouble. # (See https://github.com/pypa/pip/pull/462.) for dir_ in list(dirs): if looks_like_virtual_env(os.path.join(root, dir_)): dirs.remove(dir_) # Also don't search through tests elif dir_ == 'test' or dir_ == 'tests': dirs.remove(dir_) candidates.extend(os.path.join(root, dir_) for dir_ in dirs) return [f for f in candidates if f.endswith('.egg-info')] def depth_of_directory(dir_): # type: (str) -> int return ( dir_.count(os.path.sep) + (os.path.altsep and dir_.count(os.path.altsep) or 0) ) base = source_directory if is_editable: filenames = locate_editable_egg_info(base) else: base = os.path.join(base, 'pip-egg-info') filenames = os.listdir(base) if not filenames: raise InstallationError( "Files/directories not found in %s" % base ) # If we have more than one match, we pick the toplevel one. This # can easily be the case if there is a dist folder which contains # an extracted tarball for testing purposes. if len(filenames) > 1: filenames.sort(key=depth_of_directory) return os.path.join(base, filenames[0]) def _generate_metadata_legacy(install_req): # type: (InstallRequirement) -> str req_details_str = install_req.name or "from {}".format(install_req.link) logger.debug( 'Running setup.py (path:%s) egg_info for package %s', install_req.setup_py_path, req_details_str, ) # Compose arguments for subprocess call base_cmd = make_setuptools_shim_args(install_req.setup_py_path) if install_req.isolated: base_cmd += ["--no-user-cfg"] # For non-editable installs, don't put the .egg-info files at the root, # to avoid confusion due to the source code being considered an installed # egg. egg_base_option = [] # type: List[str] if not install_req.editable: egg_info_dir = os.path.join( install_req.unpacked_source_directory, 'pip-egg-info', ) egg_base_option = ['--egg-base', egg_info_dir] # setuptools complains if the target directory does not exist. ensure_dir(egg_info_dir) with install_req.build_env: call_subprocess( base_cmd + ["egg_info"] + egg_base_option, cwd=install_req.unpacked_source_directory, command_desc='python setup.py egg_info', ) # Return the .egg-info directory. return _find_egg_info( install_req.unpacked_source_directory, install_req.editable, ) def _generate_metadata(install_req): # type: (InstallRequirement) -> str return install_req.prepare_pep517_metadata()