def make_self_candidate(self, editable: bool = True) -> Candidate: req = parse_requirement(pip_shims.path_to_url(self.root.as_posix()), editable) req.name = self.meta.name return Candidate(req, self.environment, name=self.meta.name, version=self.meta.version)
def _identify_candidate(self, candidate: Candidate) -> tuple: url = getattr(candidate.req, "url", None) return ( candidate.identify(), candidate.version, url_without_fragments(url) if url else None, candidate.req.editable, )
def get_locked_candidates( self, section: Optional[str] = None ) -> Dict[str, Candidate]: if not self.lockfile_file.is_file(): return {} section = section or "default" result = {} for package in [dict(p) for p in self.lockfile.get("package", [])]: if section != "__all__" and section not in package["sections"]: continue version = package.get("version") if version: package["version"] = f"=={version}" package_name = package.pop("name") summary = package.pop("summary", None) dependencies = [ Requirement.from_req_dict(k, v) for k, v in package.pop("dependencies", {}).items() ] req = Requirement.from_req_dict(package_name, dict(package)) can = Candidate(req, self.environment, name=package_name, version=version) can.marker = req.marker can.requires_python = str(req.requires_python) can.dependencies = dependencies can.summary = summary can.hashes = { item["file"]: item["hash"] for item in self.lockfile["metadata"].get( f"{package_name} {version}", [] ) } or None result[identify(req)] = can if section in ("default", "__all__") and self.meta.name: result[safe_name(self.meta.name).lower()] = self.make_self_candidate(True) return result
def is_satisfied_by(self, requirement: Requirement, candidate: Candidate) -> bool: if not requirement.is_named: return not candidate.req.is_named and candidate.req.url == requirement.url if not candidate.version: candidate.get_metadata() if getattr(candidate, "_preferred", False) and not candidate._requires_python: candidate.requires_python = str( self.repository.get_dependencies(candidate)[1] ) allow_prereleases = requirement.allow_prereleases if allow_prereleases is None: allow_prereleases = self.allow_prereleases if allow_prereleases is None: # if not specified, should allow what `find_candidates()` returns allow_prereleases = True return requirement.specifier.contains( candidate.version, allow_prereleases ) and self.requires_python.is_subset(candidate.requires_python)
def install(self, candidate: Candidate) -> None: if (self.use_install_cache and candidate.req.is_named and candidate.name not in self.NO_CACHE_PACKAGES): # Only cache wheels from PyPI installer = install_wheel_with_cache else: installer = install_wheel prepared = candidate.prepare(self.environment) installer(prepared.build(), self.environment, prepared.direct_url())
def editables_candidate(environment: Environment) -> Candidate | None: """Return a candidate for `editables` package""" with environment.get_finder() as finder: best_match = finder.find_best_candidate("editables") if best_match.best_candidate is None: return None return Candidate.from_installation_candidate( best_match.best_candidate, parse_requirement("editables"), environment)
def test_install_with_file_existing(project): req = parse_requirement("demo") candidate = Candidate( req, link=Link( "http://fixtures.test/artifacts/demo-0.0.1-py2.py3-none-any.whl"), ) (project.environment.packages_path / "lib/demo.py").touch() installer = InstallManager(project.environment) installer.install(candidate)
def find_candidates( self, requirement: Requirement, requires_python: PySpecSet = PySpecSet(), allow_prereleases: Optional[bool] = None, allow_all: bool = False, ) -> List[Candidate]: if allow_prereleases is None: allow_prereleases = requirement.allow_prereleases cans = [] for version, candidate in self._pypi_data.get(requirement.key, {}).items(): c = Candidate( requirement, self.environment, name=requirement.project_name, version=version, ) c.requires_python = candidate.get("requires_python", "") cans.append(c) sorted_cans = sorted( (c for c in cans if requirement.specifier.contains(c.version, allow_prereleases)), key=lambda c: c.version, reverse=True, ) if not allow_all: sorted_cans = [ can for can in sorted_cans if requires_python.is_subset(can.requires_python) ] if not sorted_cans and allow_prereleases is None: # No non-pre-releases is found, force pre-releases now sorted_cans = sorted( (c for c in cans if requirement.specifier.contains(c.version, True)), key=lambda c: c.version, reverse=True, ) return sorted_cans
def test_install_wheel_with_inconsistent_dist_info(project): req = parse_requirement("pyfunctional") candidate = Candidate( req, link=Link( "http://fixtures.test/artifacts/PyFunctional-1.4.3-py3-none-any.whl" ), ) installer = InstallManager(project.environment) installer.install(candidate) assert "pyfunctional" in project.environment.get_working_set()
def _find_candidates(self, requirement: Requirement) -> Iterable[Candidate]: sources = self.get_filtered_sources(requirement) with self.environment.get_finder(sources, True) as finder, allow_all_wheels(): cans = [ Candidate.from_installation_candidate(c, requirement) for c in finder.find_all_candidates(requirement.project_name) ] if not cans: raise CandidateNotFound( f"Unable to find candidates for {requirement.project_name}. There may " "exist some issues with the package name or network condition." ) return cans
def test_url_requirement_is_not_cached(project): req = parse_requirement("future-fstrings @ http://fixtures.test/artifacts/" "future_fstrings-1.2.0-py2.py3-none-any.whl") candidate = Candidate(req) installer = InstallManager(project.environment, use_install_cache=True) installer.install(candidate) cache_path = project.cache( "packages") / "future_fstrings-1.2.0-py2.py3-none-any" assert not cache_path.is_dir() lib_path = project.environment.get_paths()["purelib"] assert os.path.isfile(os.path.join(lib_path, "future_fstrings.py")) assert os.path.isfile(os.path.join(lib_path, "aaaaa_future_fstrings.pth")) dist = project.environment.get_working_set()["future-fstrings"] assert dist.read_text("direct_url.json")
def test_cache_vcs_immutable_revision(project): req = parse_requirement( "git+https://github.com/test-root/demo.git@master#egg=demo") candidate = Candidate(req) wheel = candidate.prepare(project.environment).build() with pytest.raises(ValueError): Path(wheel).relative_to(project.cache_dir) assert candidate.get_revision() == "1234567890abcdef" req = parse_requirement( "git+https://github.com/test-root/demo.git@1234567890abcdef#egg=demo") candidate = Candidate(req) wheel = candidate.prepare(project.environment).build() assert Path(wheel).relative_to(project.cache_dir) assert candidate.get_revision() == "1234567890abcdef" # test the revision can be got correctly after cached prepared = Candidate(req).prepare(project.environment) assert not prepared.ireq.source_dir assert prepared.revision == "1234567890abcdef"
def find_matches( self, identifier: str, requirements: Mapping[str, Iterator[Requirement]], incompatibilities: Mapping[str, Iterator[Candidate]], ) -> Iterable[Candidate]: reqs = list(requirements[identifier]) file_req = next((req for req in reqs if not req.is_named), None) incompat = list(incompatibilities[identifier]) if file_req: can = Candidate(file_req, self.repository.environment) can.get_metadata() candidates = [can] else: candidates = self.repository.find_candidates( reqs[0], self.requires_python, self.allow_prereleases, ) return [ can for can in candidates if all(self.is_satisfied_by(r, can) for r in reqs) and can not in incompat ]
def is_satisfied_by(self, requirement: Requirement, candidate: Candidate) -> bool: if isinstance(requirement, PythonRequirement): return is_python_satisfied_by(requirement, candidate) elif candidate.identify() in self.overrides: return True if not requirement.is_named: return not candidate.req.is_named and url_without_fragments( candidate.req.url) == url_without_fragments(requirement.url) version = candidate.version or candidate.metadata.version # Allow prereleases if: 1) it is not specified in the tool settings or # 2) the candidate doesn't come from PyPI index. allow_prereleases = (self.allow_prereleases in (True, None) or not candidate.req.is_named) return requirement.specifier.contains(version, allow_prereleases)
def find_matches( self, requirement: Requirement, requires_python: PySpecSet = PySpecSet(), allow_prereleases: Optional[bool] = None, allow_all: bool = False, ) -> List[Candidate]: """Find matching candidates of a requirement. :param requirement: the given requirement. :param requires_python: the Python version constraint. :param allow_prereleases: whether allow prerelease versions, or let us determine if not given. If no non-prerelease is available, prereleases will be used. :param allow_all: whether allow all wheels. :returns: a list of candidates. """ if requirement.is_named: return self._find_named_matches(requirement, requires_python, allow_prereleases) else: # Fetch metadata so that resolver can know the candidate's name. can = Candidate(requirement, self.environment) can.get_metadata() return [can]
def test_invalidate_incompatible_wheel_link(project, index): req = parse_requirement("demo") prepared = Candidate(req, name="demo", version="0.0.1").prepare(project.environment) prepared.obtain(True) assert (Path(prepared.wheel).name == prepared.ireq.link.filename == "demo-0.0.1-cp36-cp36m-win_amd64.whl") prepared.obtain(False) assert (Path(prepared.wheel).name == prepared.ireq.link.filename == "demo-0.0.1-py2.py3-none-any.whl")
def ensure_essential_packages(self) -> None: """Ensure wheel and setuptools are available and install if not""" from pdm.installers import Installer from pdm.models.requirements import parse_requirement from pdm.models.candidates import Candidate if self._essential_installed: return installer = Installer(self) working_set = self.get_working_set() for package in ("setuptools", "wheel"): if package in working_set: continue req = parse_requirement(package) candidate = Candidate(req, self, package) installer.install(candidate) self._essential_installed = True
def test_uninstall_commit_rollback(project): req = parse_requirement("demo") candidate = Candidate( req, link=Link( "http://fixtures.test/artifacts/demo-0.0.1-py2.py3-none-any.whl"), ) installer = InstallManager(project.environment) lib_path = project.environment.get_paths()["purelib"] installer.install(candidate) lib_file = os.path.join(lib_path, "demo.py") assert os.path.exists(lib_file) remove_paths = installer.get_paths_to_remove( project.environment.get_working_set()["demo"]) remove_paths.remove() assert not os.path.exists(lib_file) remove_paths.rollback() assert os.path.exists(lib_file)
def test_uninstall_with_console_scripts(project, use_install_cache): req = parse_requirement("celery") candidate = Candidate( req, link=Link( "http://fixtures.test/artifacts/celery-4.4.2-py2.py3-none-any.whl" ), ) installer = InstallManager(project.environment, use_install_cache=use_install_cache) installer.install(candidate) celery_script = os.path.join( project.environment.get_paths()["scripts"], "celery.exe" if os.name == "nt" else "celery", ) assert os.path.exists(celery_script) installer.uninstall(project.environment.get_working_set()["celery"]) assert not os.path.exists(celery_script)
def test_invalidate_incompatible_wheel_link(project, index): req = parse_requirement("demo") candidate = Candidate(req, project.environment, name="demo", version="0.0.1") candidate.prepare(True) assert (Path(candidate.wheel).name == candidate.link.filename == "demo-0.0.1-cp36-cp36m-win_amd64.whl") candidate.prepare() assert (Path(candidate.wheel).name == candidate.link.filename == "demo-0.0.1-py2.py3-none-any.whl")
def test_vcs_candidate_in_subdirectory(project, is_editable): line = ("git+https://github.com/test-root/demo-parent-package.git" "@master#egg=package-a&subdirectory=package-a") req = parse_requirement(line, is_editable) candidate = Candidate(req, project.environment) assert candidate.get_dependencies_from_metadata() == ["flask"] assert candidate.version == "0.1.0" line = ("git+https://github.com/test-root/demo-parent-package.git" "@master#egg=package-b&subdirectory=package-b") req = parse_requirement(line, is_editable) candidate = Candidate(req, project.environment) assert candidate.get_dependencies_from_metadata() == ["django"] assert candidate.version == "0.1.0"
def find_candidates( self, requirement: Requirement, requires_python: PySpecSet = PySpecSet(), allow_prereleases: Optional[bool] = None, allow_all: bool = False, ) -> Iterable[Candidate]: sources = self.get_filtered_sources(requirement) # `allow_prereleases` is None means leave it to specifier to decide whether to # include prereleases if allow_prereleases is None: allow_prereleases = requirement.allow_prereleases with self.environment.get_finder(sources) as finder, allow_all_wheels(): cans = [ Candidate.from_installation_candidate(c, requirement, self.environment) for c in finder.find_all_candidates(requirement.project_name) ] sorted_cans = sorted( ( c for c in cans if requirement.specifier.contains(c.version, allow_prereleases) and (allow_all or requires_python.is_subset(c.requires_python)) ), key=lambda c: (c.version, c.link.is_wheel), reverse=True, ) if not sorted_cans and allow_prereleases is None: # No non-pre-releases is found, force pre-releases now sorted_cans = sorted( ( c for c in cans if requirement.specifier.contains(c.version, True) and (allow_all or requires_python.is_subset(c.requires_python)) ), key=lambda c: c.version, reverse=True, ) return sorted_cans
def test_install_wheel_with_data_scripts(project, use_install_cache): req = parse_requirement("jmespath") candidate = Candidate( req, link=Link( "http://fixtures.test/artifacts/jmespath-0.10.0-py2.py3-none-any.whl" ), ) installer = InstallManager(project.environment, use_install_cache=use_install_cache) installer.install(candidate) bin_path = os.path.join(project.environment.get_paths()["scripts"], "jp.py") assert os.path.isfile(bin_path) if os.name != "nt": assert os.stat(bin_path).st_mode & 0o100 dist = project.environment.get_working_set()["jmespath"] installer.uninstall(dist) assert not os.path.exists(bin_path)
def test_install_wheel_with_cache(project, invoke): supports_symlink = fs_supports_symlink() req = parse_requirement("future-fstrings") candidate = Candidate( req, link=Link( "http://fixtures.test/artifacts/future_fstrings-1.2.0-py2.py3-none-any.whl" ), ) installer = InstallManager(project.environment, use_install_cache=True) installer.install(candidate) lib_path = project.environment.get_paths()["purelib"] if supports_symlink: assert os.path.islink(os.path.join(lib_path, "future_fstrings.py")) assert os.path.islink( os.path.join(lib_path, "aaaaa_future_fstrings.pth")) else: assert os.path.isfile(os.path.join(lib_path, "future_fstrings.pth")) assert os.path.isfile( os.path.join(lib_path, "aaaaa_future_fstrings.pth")) cache_path = project.cache( "packages") / "future_fstrings-1.2.0-py2.py3-none-any" assert cache_path.is_dir() r = invoke(["run", "python", "-c", "import future_fstrings"], obj=project) assert r.exit_code == 0 dist = project.environment.get_working_set()["future-fstrings"] installer.uninstall(dist) if supports_symlink: assert not os.path.exists(os.path.join(lib_path, "future_fstrings.py")) assert not os.path.exists( os.path.join(lib_path, "aaaaa_future_fstrings.pth")) else: assert not os.path.isfile(os.path.join(lib_path, "future_fstrings.pth")) assert not os.path.isfile( os.path.join(lib_path, "aaaaa_future_fstrings.pth")) assert not dist.read_text("direct_url.json") assert not cache_path.exists()
def test_sdist_candidate_with_wheel_cache(project, mocker): file_link = Link( path_to_url((FIXTURES / "artifacts/demo-0.0.1.tar.gz").as_posix())) built_path = (FIXTURES / "artifacts/demo-0.0.1-py2.py3-none-any.whl").as_posix() wheel_cache = project.make_wheel_cache() cache_path = wheel_cache.get_path_for_link(file_link) if not Path(cache_path).exists(): Path(cache_path).mkdir(parents=True) shutil.copy2(built_path, cache_path) req = parse_requirement(file_link.url) candidate = Candidate(req, project.environment) downloader = mocker.patch("pdm.models.pip_shims.unpack_url") candidate.prepare(True) downloader.assert_not_called() assert Path(candidate.wheel) == Path(cache_path) / Path(built_path).name candidate.wheel = None builder = mocker.patch("pdm.builders.WheelBuilder.build") candidate.build() builder.assert_not_called() assert Path(candidate.wheel) == Path(cache_path) / Path(built_path).name
def ireq_as_line(ireq: InstallRequirement, environment: Environment) -> str: """Formats an `InstallRequirement` instance as a PEP 508 dependency string. Generic formatter for pretty printing InstallRequirements to the terminal in a less verbose way than using its `__str__` method. :param :class:`InstallRequirement` ireq: A pip **InstallRequirement** instance. :return: A formatted string for prettyprinting :rtype: str """ if ireq.editable: line = "-e {}".format(ireq.link) else: if not ireq.req: req = parse_requirement("dummy @" + ireq.link.url) # type: ignore req.name = Candidate(req).prepare(environment).metadata.metadata["Name"] ireq.req = req # type: ignore line = _requirement_to_str_lowercase_name(ireq) if ireq.markers: line = f"{line}; {ireq.markers}" return line
def test_rollback_after_commit(project, caplog): caplog.set_level(logging.ERROR, logger="pdm.termui") req = parse_requirement("demo") candidate = Candidate( req, link=Link( "http://fixtures.test/artifacts/demo-0.0.1-py2.py3-none-any.whl"), ) installer = InstallManager(project.environment) lib_path = project.environment.get_paths()["purelib"] installer.install(candidate) lib_file = os.path.join(lib_path, "demo.py") assert os.path.exists(lib_file) remove_paths = installer.get_paths_to_remove( project.environment.get_working_set()["demo"]) remove_paths.remove() remove_paths.commit() assert not os.path.exists(lib_file) caplog.clear() remove_paths.rollback() assert not os.path.exists(lib_file) assert any(record.message == "Can't rollback, not uninstalled yet" for record in caplog.records)
def _get_dependencies_from_metadata(self, candidate: Candidate) -> CandidateInfo: prepared = candidate.prepare(self.environment) deps = prepared.get_dependencies_from_metadata() requires_python = candidate.requires_python summary = prepared.metadata.metadata["Summary"] return deps, requires_python, summary
def test_parse_project_file_on_build_error_no_dep(project): req = parse_requirement(f"{(FIXTURES / 'projects/demo-failure-no-dep').as_posix()}") candidate = Candidate(req, project.environment) assert candidate.get_dependencies_from_metadata() == [] assert candidate.name == "demo" assert candidate.version == "0.0.1"
def test_parse_abnormal_specifiers(project): req = parse_requirement( "http://fixtures.test/artifacts/celery-4.4.2-py2.py3-none-any.whl" ) candidate = Candidate(req, project.environment) assert candidate.get_dependencies_from_metadata()