1# -*- coding=utf-8 -*- 2 3import atexit 4import contextlib 5import copy 6import functools 7import os 8 9import attr 10import packaging.markers 11import packaging.version 12import pip_shims.shims 13import requests 14from packaging.utils import canonicalize_name 15from vistir.compat import JSONDecodeError, fs_str 16from vistir.contextmanagers import cd, temp_environ 17from vistir.path import create_tracked_tempdir 18 19from ..environment import MYPY_RUNNING 20from ..utils import _ensure_dir, prepare_pip_source_args 21from .cache import CACHE_DIR, DependencyCache 22from .setup_info import SetupInfo 23from .utils import ( 24 clean_requires_python, 25 fix_requires_python_marker, 26 format_requirement, 27 full_groupby, 28 is_pinned_requirement, 29 key_from_ireq, 30 make_install_requirement, 31 name_from_req, 32 version_from_ireq, 33) 34 35try: 36 from contextlib import ExitStack 37except ImportError: 38 from contextlib2 import ExitStack 39 40if MYPY_RUNNING: 41 from typing import ( 42 Any, 43 Dict, 44 List, 45 Generator, 46 Optional, 47 Union, 48 Tuple, 49 TypeVar, 50 Text, 51 Set, 52 ) 53 from pip_shims.shims import ( 54 InstallRequirement, 55 InstallationCandidate, 56 PackageFinder, 57 Command, 58 ) 59 from packaging.requirements import Requirement as PackagingRequirement 60 from packaging.markers import Marker 61 62 TRequirement = TypeVar("TRequirement") 63 RequirementType = TypeVar( 64 "RequirementType", covariant=True, bound=PackagingRequirement 65 ) 66 MarkerType = TypeVar("MarkerType", covariant=True, bound=Marker) 67 STRING_TYPE = Union[str, bytes, Text] 68 S = TypeVar("S", bytes, str, Text) 69 70 71PKGS_DOWNLOAD_DIR = fs_str(os.path.join(CACHE_DIR, "pkgs")) 72WHEEL_DOWNLOAD_DIR = fs_str(os.path.join(CACHE_DIR, "wheels")) 73 74DEPENDENCY_CACHE = DependencyCache() 75 76 77@contextlib.contextmanager 78def _get_wheel_cache(): 79 with pip_shims.shims.global_tempdir_manager(): 80 yield pip_shims.shims.WheelCache( 81 CACHE_DIR, pip_shims.shims.FormatControl(set(), set()) 82 ) 83 84 85def _get_filtered_versions(ireq, versions, prereleases): 86 return set(ireq.specifier.filter(versions, prereleases=prereleases)) 87 88 89def find_all_matches(finder, ireq, pre=False): 90 # type: (PackageFinder, InstallRequirement, bool) -> List[InstallationCandidate] 91 """Find all matching dependencies using the supplied finder and the 92 given ireq. 93 94 :param finder: A package finder for discovering matching candidates. 95 :type finder: :class:`~pip._internal.index.PackageFinder` 96 :param ireq: An install requirement. 97 :type ireq: :class:`~pip._internal.req.req_install.InstallRequirement` 98 :return: A list of matching candidates. 99 :rtype: list[:class:`~pip._internal.index.InstallationCandidate`] 100 """ 101 102 candidates = clean_requires_python(finder.find_all_candidates(ireq.name)) 103 versions = {candidate.version for candidate in candidates} 104 allowed_versions = _get_filtered_versions(ireq, versions, pre) 105 if not pre and not allowed_versions: 106 allowed_versions = _get_filtered_versions(ireq, versions, True) 107 candidates = {c for c in candidates if c.version in allowed_versions} 108 return candidates 109 110 111def get_pip_command(): 112 # type: () -> Command 113 # Use pip's parser for pip.conf management and defaults. 114 # General options (find_links, index_url, extra_index_url, trusted_host, 115 # and pre) are defered to pip. 116 pip_command = pip_shims.shims.InstallCommand() 117 return pip_command 118 119 120@attr.s 121class AbstractDependency(object): 122 name = attr.ib() # type: STRING_TYPE 123 specifiers = attr.ib() 124 markers = attr.ib() 125 candidates = attr.ib() 126 requirement = attr.ib() 127 parent = attr.ib() 128 finder = attr.ib() 129 dep_dict = attr.ib(default=attr.Factory(dict)) 130 131 @property 132 def version_set(self): 133 """Return the set of versions for the candidates in this abstract dependency. 134 135 :return: A set of matching versions 136 :rtype: set(str) 137 """ 138 139 if len(self.candidates) == 1: 140 return set() 141 return set(packaging.version.parse(version_from_ireq(c)) for c in self.candidates) 142 143 def compatible_versions(self, other): 144 """Find compatible version numbers between this abstract 145 dependency and another one. 146 147 :param other: An abstract dependency to compare with. 148 :type other: :class:`~requirementslib.models.dependency.AbstractDependency` 149 :return: A set of compatible version strings 150 :rtype: set(str) 151 """ 152 153 if len(self.candidates) == 1 and next(iter(self.candidates)).editable: 154 return self 155 elif len(other.candidates) == 1 and next(iter(other.candidates)).editable: 156 return other 157 return self.version_set & other.version_set 158 159 def compatible_abstract_dep(self, other): 160 """Merge this abstract dependency with another one. 161 162 Return the result of the merge as a new abstract dependency. 163 164 :param other: An abstract dependency to merge with 165 :type other: :class:`~requirementslib.models.dependency.AbstractDependency` 166 :return: A new, combined abstract dependency 167 :rtype: :class:`~requirementslib.models.dependency.AbstractDependency` 168 """ 169 170 from .requirements import Requirement 171 172 if len(self.candidates) == 1 and next(iter(self.candidates)).editable: 173 return self 174 elif len(other.candidates) == 1 and next(iter(other.candidates)).editable: 175 return other 176 new_specifiers = self.specifiers & other.specifiers 177 markers = set(self.markers) if self.markers else set() 178 if other.markers: 179 markers.add(other.markers) 180 new_markers = None 181 if markers: 182 new_markers = packaging.markers.Marker( 183 " or ".join(str(m) for m in sorted(markers)) 184 ) 185 new_ireq = copy.deepcopy(self.requirement.ireq) 186 new_ireq.req.specifier = new_specifiers 187 new_ireq.req.marker = new_markers 188 new_requirement = Requirement.from_line(format_requirement(new_ireq)) 189 compatible_versions = self.compatible_versions(other) 190 if isinstance(compatible_versions, AbstractDependency): 191 return compatible_versions 192 candidates = [ 193 c 194 for c in self.candidates 195 if packaging.version.parse(version_from_ireq(c)) in compatible_versions 196 ] 197 dep_dict = {} 198 candidate_strings = [format_requirement(c) for c in candidates] 199 for c in candidate_strings: 200 if c in self.dep_dict: 201 dep_dict[c] = self.dep_dict.get(c) 202 return AbstractDependency( 203 name=self.name, 204 specifiers=new_specifiers, 205 markers=new_markers, 206 candidates=candidates, 207 requirement=new_requirement, 208 parent=self.parent, 209 dep_dict=dep_dict, 210 finder=self.finder, 211 ) 212 213 def get_deps(self, candidate): 214 """Get the dependencies of the supplied candidate. 215 216 :param candidate: An installrequirement 217 :type candidate: :class:`~pip._internal.req.req_install.InstallRequirement` 218 :return: A list of abstract dependencies 219 :rtype: list[:class:`~requirementslib.models.dependency.AbstractDependency`] 220 """ 221 222 key = format_requirement(candidate) 223 if key not in self.dep_dict: 224 from .requirements import Requirement 225 226 req = Requirement.from_line(key) 227 req = req.merge_markers(self.markers) 228 self.dep_dict[key] = req.get_abstract_dependencies() 229 return self.dep_dict[key] 230 231 @classmethod 232 def from_requirement(cls, requirement, parent=None): 233 """Creates a new :class:`~requirementslib.models.dependency.AbstractDependency` 234 from a :class:`~requirementslib.models.requirements.Requirement` object. 235 236 This class is used to find all candidates matching a given set of specifiers 237 and a given requirement. 238 239 :param requirement: A requirement for resolution 240 :type requirement: :class:`~requirementslib.models.requirements.Requirement` object. 241 """ 242 name = requirement.normalized_name 243 specifiers = requirement.ireq.specifier if not requirement.editable else "" 244 markers = requirement.ireq.markers 245 extras = requirement.ireq.extras 246 is_pinned = is_pinned_requirement(requirement.ireq) 247 is_constraint = bool(parent) 248 _, finder = get_finder(sources=None) 249 candidates = [] 250 if not is_pinned and not requirement.editable: 251 for r in requirement.find_all_matches(finder=finder): 252 req = make_install_requirement( 253 name, 254 r.version, 255 extras=extras, 256 markers=markers, 257 constraint=is_constraint, 258 ) 259 req.req.link = getattr(r, "location", getattr(r, "link", None)) 260 req.parent = parent 261 candidates.append(req) 262 candidates = sorted( 263 set(candidates), 264 key=lambda k: packaging.version.parse(version_from_ireq(k)), 265 ) 266 else: 267 candidates = [requirement.ireq] 268 return cls( 269 name=name, 270 specifiers=specifiers, 271 markers=markers, 272 candidates=candidates, 273 requirement=requirement, 274 parent=parent, 275 finder=finder, 276 ) 277 278 @classmethod 279 def from_string(cls, line, parent=None): 280 from .requirements import Requirement 281 282 req = Requirement.from_line(line) 283 abstract_dep = cls.from_requirement(req, parent=parent) 284 return abstract_dep 285 286 287def get_abstract_dependencies(reqs, sources=None, parent=None): 288 """Get all abstract dependencies for a given list of requirements. 289 290 Given a set of requirements, convert each requirement to an Abstract Dependency. 291 292 :param reqs: A list of Requirements 293 :type reqs: list[:class:`~requirementslib.models.requirements.Requirement`] 294 :param sources: Pipfile-formatted sources, defaults to None 295 :param sources: list[dict], optional 296 :param parent: The parent of this list of dependencies, defaults to None 297 :param parent: :class:`~requirementslib.models.requirements.Requirement`, optional 298 :return: A list of Abstract Dependencies 299 :rtype: list[:class:`~requirementslib.models.dependency.AbstractDependency`] 300 """ 301 302 deps = [] 303 from .requirements import Requirement 304 305 for req in reqs: 306 if isinstance(req, pip_shims.shims.InstallRequirement): 307 requirement = Requirement.from_line("{0}{1}".format(req.name, req.specifier)) 308 if req.link: 309 requirement.req.link = req.link 310 requirement.markers = req.markers 311 requirement.req.markers = req.markers 312 requirement.extras = req.extras 313 requirement.req.extras = req.extras 314 elif isinstance(req, Requirement): 315 requirement = copy.deepcopy(req) 316 else: 317 requirement = Requirement.from_line(req) 318 dep = AbstractDependency.from_requirement(requirement, parent=parent) 319 deps.append(dep) 320 return deps 321 322 323def get_dependencies(ireq, sources=None, parent=None): 324 # type: (Union[InstallRequirement, InstallationCandidate], Optional[List[Dict[S, Union[S, bool]]]], Optional[AbstractDependency]) -> Set[S, ...] 325 """Get all dependencies for a given install requirement. 326 327 :param ireq: A single InstallRequirement 328 :type ireq: :class:`~pip._internal.req.req_install.InstallRequirement` 329 :param sources: Pipfile-formatted sources, defaults to None 330 :type sources: list[dict], optional 331 :param parent: The parent of this list of dependencies, defaults to None 332 :type parent: :class:`~pip._internal.req.req_install.InstallRequirement` 333 :return: A set of dependency lines for generating new InstallRequirements. 334 :rtype: set(str) 335 """ 336 if not isinstance(ireq, pip_shims.shims.InstallRequirement): 337 name = getattr(ireq, "project_name", getattr(ireq, "project", ireq.name)) 338 version = getattr(ireq, "version", None) 339 if not version: 340 ireq = pip_shims.shims.InstallRequirement.from_line("{0}".format(name)) 341 else: 342 ireq = pip_shims.shims.InstallRequirement.from_line( 343 "{0}=={1}".format(name, version) 344 ) 345 pip_options = get_pip_options(sources=sources) 346 getters = [ 347 get_dependencies_from_cache, 348 get_dependencies_from_wheel_cache, 349 get_dependencies_from_json, 350 functools.partial(get_dependencies_from_index, pip_options=pip_options), 351 ] 352 for getter in getters: 353 deps = getter(ireq) 354 if deps is not None: 355 return deps 356 raise RuntimeError("failed to get dependencies for {}".format(ireq)) 357 358 359def get_dependencies_from_wheel_cache(ireq): 360 # type: (pip_shims.shims.InstallRequirement) -> Optional[Set[pip_shims.shims.InstallRequirement]] 361 """Retrieves dependencies for the given install requirement from the wheel cache. 362 363 :param ireq: A single InstallRequirement 364 :type ireq: :class:`~pip._internal.req.req_install.InstallRequirement` 365 :return: A set of dependency lines for generating new InstallRequirements. 366 :rtype: set(str) or None 367 """ 368 369 if ireq.editable or not is_pinned_requirement(ireq): 370 return 371 with _get_wheel_cache() as wheel_cache: 372 matches = wheel_cache.get(ireq.link, name_from_req(ireq.req)) 373 if matches: 374 matches = set(matches) 375 if not DEPENDENCY_CACHE.get(ireq): 376 DEPENDENCY_CACHE[ireq] = [format_requirement(m) for m in matches] 377 return matches 378 return None 379 380 381def _marker_contains_extra(ireq): 382 # TODO: Implement better parsing logic avoid false-positives. 383 return "extra" in repr(ireq.markers) 384 385 386def get_dependencies_from_json(ireq): 387 """Retrieves dependencies for the given install requirement from the json api. 388 389 :param ireq: A single InstallRequirement 390 :type ireq: :class:`~pip._internal.req.req_install.InstallRequirement` 391 :return: A set of dependency lines for generating new InstallRequirements. 392 :rtype: set(str) or None 393 """ 394 395 if ireq.editable or not is_pinned_requirement(ireq): 396 return 397 398 # It is technically possible to parse extras out of the JSON API's 399 # requirement format, but it is such a chore let's just use the simple API. 400 if ireq.extras: 401 return 402 403 session = requests.session() 404 atexit.register(session.close) 405 version = str(ireq.req.specifier).lstrip("=") 406 407 def gen(ireq): 408 info = None 409 try: 410 info = session.get( 411 "https://pypi.org/pypi/{0}/{1}/json".format(ireq.req.name, version) 412 ).json()["info"] 413 finally: 414 session.close() 415 requires_dist = info.get("requires_dist", info.get("requires")) 416 if not requires_dist: # The API can return None for this. 417 return 418 for requires in requires_dist: 419 i = pip_shims.shims.InstallRequirement.from_line(requires) 420 # See above, we don't handle requirements with extras. 421 if not _marker_contains_extra(i): 422 yield format_requirement(i) 423 424 if ireq not in DEPENDENCY_CACHE: 425 try: 426 reqs = DEPENDENCY_CACHE[ireq] = list(gen(ireq)) 427 except JSONDecodeError: 428 return 429 req_iter = iter(reqs) 430 else: 431 req_iter = gen(ireq) 432 return set(req_iter) 433 434 435def get_dependencies_from_cache(ireq): 436 """Retrieves dependencies for the given install requirement from the dependency cache. 437 438 :param ireq: A single InstallRequirement 439 :type ireq: :class:`~pip._internal.req.req_install.InstallRequirement` 440 :return: A set of dependency lines for generating new InstallRequirements. 441 :rtype: set(str) or None 442 """ 443 if ireq.editable or not is_pinned_requirement(ireq): 444 return 445 if ireq not in DEPENDENCY_CACHE: 446 return 447 cached = set(DEPENDENCY_CACHE[ireq]) 448 449 # Preserving sanity: Run through the cache and make sure every entry if 450 # valid. If this fails, something is wrong with the cache. Drop it. 451 try: 452 broken = False 453 for line in cached: 454 dep_ireq = pip_shims.shims.InstallRequirement.from_line(line) 455 name = canonicalize_name(dep_ireq.name) 456 if _marker_contains_extra(dep_ireq): 457 broken = True # The "extra =" marker breaks everything. 458 elif name == canonicalize_name(ireq.name): 459 broken = True # A package cannot depend on itself. 460 if broken: 461 break 462 except Exception: 463 broken = True 464 465 if broken: 466 del DEPENDENCY_CACHE[ireq] 467 return 468 469 return cached 470 471 472def is_python(section): 473 return section.startswith("[") and ":" in section 474 475 476def get_dependencies_from_index(dep, sources=None, pip_options=None, wheel_cache=None): 477 """Retrieves dependencies for the given install requirement from the pip resolver. 478 479 :param dep: A single InstallRequirement 480 :type dep: :class:`~pip._internal.req.req_install.InstallRequirement` 481 :param sources: Pipfile-formatted sources, defaults to None 482 :type sources: list[dict], optional 483 :return: A set of dependency lines for generating new InstallRequirements. 484 :rtype: set(str) or None 485 """ 486 487 session, finder = get_finder(sources=sources, pip_options=pip_options) 488 dep.is_direct = True 489 requirements = None 490 setup_requires = {} 491 with temp_environ(), ExitStack() as stack: 492 if not wheel_cache: 493 wheel_cache = stack.enter_context(_get_wheel_cache()) 494 os.environ["PIP_EXISTS_ACTION"] = "i" 495 if dep.editable and not dep.prepared and not dep.req: 496 setup_info = SetupInfo.from_ireq(dep) 497 results = setup_info.get_info() 498 setup_requires.update(results["setup_requires"]) 499 requirements = set(results["requires"].values()) 500 else: 501 results = pip_shims.shims.resolve(dep) 502 requirements = [v for v in results.values() if v.name != dep.name] 503 requirements = set([format_requirement(r) for r in requirements]) 504 if not dep.editable and is_pinned_requirement(dep) and requirements is not None: 505 DEPENDENCY_CACHE[dep] = list(requirements) 506 return requirements 507 508 509def get_pip_options(args=[], sources=None, pip_command=None): 510 """Build a pip command from a list of sources 511 512 :param args: positional arguments passed through to the pip parser 513 :param sources: A list of pipfile-formatted sources, defaults to None 514 :param sources: list[dict], optional 515 :param pip_command: A pre-built pip command instance 516 :type pip_command: :class:`~pip._internal.cli.base_command.Command` 517 :return: An instance of pip_options using the supplied arguments plus sane defaults 518 :rtype: :class:`~pip._internal.cli.cmdoptions` 519 """ 520 521 if not pip_command: 522 pip_command = get_pip_command() 523 if not sources: 524 sources = [{"url": "https://pypi.org/simple", "name": "pypi", "verify_ssl": True}] 525 _ensure_dir(CACHE_DIR) 526 pip_args = args 527 pip_args = prepare_pip_source_args(sources, pip_args) 528 pip_options, _ = pip_command.parser.parse_args(pip_args) 529 pip_options.cache_dir = CACHE_DIR 530 return pip_options 531 532 533def get_finder(sources=None, pip_command=None, pip_options=None): 534 # type: (List[Dict[S, Union[S, bool]]], Optional[Command], Any) -> PackageFinder 535 """Get a package finder for looking up candidates to install 536 537 :param sources: A list of pipfile-formatted sources, defaults to None 538 :param sources: list[dict], optional 539 :param pip_command: A pip command instance, defaults to None 540 :type pip_command: :class:`~pip._internal.cli.base_command.Command` 541 :param pip_options: A pip options, defaults to None 542 :type pip_options: :class:`~pip._internal.cli.cmdoptions` 543 :return: A package finder 544 :rtype: :class:`~pip._internal.index.PackageFinder` 545 """ 546 547 if not pip_command: 548 pip_command = pip_shims.shims.InstallCommand() 549 if not sources: 550 sources = [{"url": "https://pypi.org/simple", "name": "pypi", "verify_ssl": True}] 551 if not pip_options: 552 pip_options = get_pip_options(sources=sources, pip_command=pip_command) 553 session = pip_command._build_session(pip_options) 554 atexit.register(session.close) 555 finder = pip_shims.shims.get_package_finder( 556 pip_shims.shims.InstallCommand(), options=pip_options, session=session 557 ) 558 return session, finder 559 560 561@contextlib.contextmanager 562def start_resolver(finder=None, session=None, wheel_cache=None): 563 """Context manager to produce a resolver. 564 565 :param finder: A package finder to use for searching the index 566 :type finder: :class:`~pip._internal.index.PackageFinder` 567 :param :class:`~requests.Session` session: A session instance 568 :param :class:`~pip._internal.cache.WheelCache` wheel_cache: A pip WheelCache instance 569 :return: A 3-tuple of finder, preparer, resolver 570 :rtype: (:class:`~pip._internal.operations.prepare.RequirementPreparer`, :class:`~pip._internal.resolve.Resolver`) 571 """ 572 573 pip_command = get_pip_command() 574 pip_options = get_pip_options(pip_command=pip_command) 575 session = None 576 if not finder: 577 session, finder = get_finder(pip_command=pip_command, pip_options=pip_options) 578 if not session: 579 session = pip_command._build_session(pip_options) 580 581 download_dir = PKGS_DOWNLOAD_DIR 582 _ensure_dir(download_dir) 583 584 _build_dir = create_tracked_tempdir(fs_str("build")) 585 _source_dir = create_tracked_tempdir(fs_str("source")) 586 try: 587 with ExitStack() as ctx: 588 ctx.enter_context(pip_shims.shims.global_tempdir_manager()) 589 if not wheel_cache: 590 wheel_cache = ctx.enter_context(_get_wheel_cache()) 591 _ensure_dir(fs_str(os.path.join(wheel_cache.cache_dir, "wheels"))) 592 preparer = ctx.enter_context( 593 pip_shims.shims.make_preparer( 594 options=pip_options, 595 finder=finder, 596 session=session, 597 build_dir=_build_dir, 598 src_dir=_source_dir, 599 download_dir=download_dir, 600 wheel_download_dir=WHEEL_DOWNLOAD_DIR, 601 progress_bar="off", 602 build_isolation=False, 603 install_cmd=pip_command, 604 ) 605 ) 606 resolver = pip_shims.shims.get_resolver( 607 finder=finder, 608 ignore_dependencies=False, 609 ignore_requires_python=True, 610 preparer=preparer, 611 session=session, 612 options=pip_options, 613 install_cmd=pip_command, 614 wheel_cache=wheel_cache, 615 force_reinstall=True, 616 ignore_installed=True, 617 upgrade_strategy="to-satisfy-only", 618 isolated=False, 619 use_user_site=False, 620 ) 621 yield resolver 622 finally: 623 session.close() 624 625 626def get_grouped_dependencies(constraints): 627 # We need to track what contributed a specifierset 628 # as well as which specifiers were required by the root node 629 # in order to resolve any conflicts when we are deciding which thing to backtrack on 630 # then we take the loose match (which _is_ flexible) and start moving backwards in 631 # versions by popping them off of a stack and checking for the conflicting package 632 for _, ireqs in full_groupby(constraints, key=key_from_ireq): 633 ireqs = sorted(ireqs, key=lambda ireq: ireq.editable) 634 editable_ireq = next(iter(ireq for ireq in ireqs if ireq.editable), None) 635 if editable_ireq: 636 yield editable_ireq # only the editable match mattters, ignore all others 637 continue 638 ireqs = iter(ireqs) 639 # deepcopy the accumulator so as to not modify the self.our_constraints invariant 640 combined_ireq = copy.deepcopy(next(ireqs)) 641 for ireq in ireqs: 642 # NOTE we may be losing some info on dropped reqs here 643 try: 644 combined_ireq.req.specifier &= ireq.req.specifier 645 except TypeError: 646 if ireq.req.specifier._specs and not combined_ireq.req.specifier._specs: 647 combined_ireq.req.specifier._specs = ireq.req.specifier._specs 648 combined_ireq.constraint &= ireq.constraint 649 if not combined_ireq.markers: 650 combined_ireq.markers = ireq.markers 651 else: 652 _markers = combined_ireq.markers._markers 653 if not isinstance(_markers[0], (tuple, list)): 654 combined_ireq.markers._markers = [ 655 _markers, 656 "and", 657 ireq.markers._markers, 658 ] 659 # Return a sorted, de-duped tuple of extras 660 combined_ireq.extras = tuple( 661 sorted(set(tuple(combined_ireq.extras) + tuple(ireq.extras))) 662 ) 663 yield combined_ireq 664