Skip to content

Instantly share code, notes, and snippets.

@cosmicexplorer
Created January 7, 2022 13:36
Show Gist options
  • Save cosmicexplorer/008bafbde6dbdb0e01a322196a2568de to your computer and use it in GitHub Desktop.
Save cosmicexplorer/008bafbde6dbdb0e01a322196a2568de to your computer and use it in GitHub Desktop.
attempt to make pip install --report work the same as pip download --report
diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py
index eedb1ff5d..804bc11ff 100644
--- a/src/pip/_internal/commands/install.py
+++ b/src/pip/_internal/commands/install.py
@@ -17,6 +17,7 @@ from pip._internal.cli.req_command import (
with_cleanup,
)
from pip._internal.cli.status_codes import ERROR, SUCCESS
+from pip._internal.commands.download import ResolutionResult, ResolvedCandidate
from pip._internal.exceptions import CommandError, InstallationError
from pip._internal.locations import get_scheme
from pip._internal.metadata import get_environment
@@ -45,6 +46,16 @@ from pip._internal.wheel_builder import (
build,
should_build_for_install_command,
)
+from pip._internal.resolution.resolvelib.candidates import (
+ LinkCandidate,
+ RequiresPythonCandidate,
+)
+from pip._internal.resolution.resolvelib.requirements import (
+ ExplicitRequirement,
+ RequiresPythonRequirement,
+)
+from pip._internal.req.req_install import produce_exact_version_specifier
+from pip._internal.models.link import LinkWithSource, URLDownloadInfo
logger = getLogger(__name__)
@@ -223,6 +234,28 @@ class InstallCommand(RequirementCommand):
help="Do not warn about broken dependencies",
)
+ self.cmd_opts.add_option(
+ "--dry-run",
+ dest="dry_run",
+ action="store_true",
+ help=(
+ "Avoid actually downloading wheels or sdists. "
+ "Intended to be used with --report."
+ ),
+ )
+
+ self.cmd_opts.add_option(
+ "--report",
+ "--resolution-report",
+ dest="json_report_file",
+ metavar="file",
+ default=None,
+ help=(
+ "Print a JSON object representing the resolve into <file>. "
+ "Often used with --dry-run."
+ ),
+ )
+
self.cmd_opts.add_option(cmdoptions.no_binary())
self.cmd_opts.add_option(cmdoptions.only_binary())
self.cmd_opts.add_option(cmdoptions.prefer_binary())
@@ -339,6 +372,101 @@ class InstallCommand(RequirementCommand):
reqs, check_supported_wheels=not options.target_dir
)
+ # Reconstruct the input requirements provided to the resolve.
+ input_requirements: List[str] = []
+ for ireq in reqs:
+ if ireq.req:
+ # If the initial requirement string contained a url (retained in
+ # InstallRequirement.link), add it back to the requirement string
+ # included in the JSON report.
+ if ireq.link:
+ req_string = f"{ireq.req}@{ireq.link.url}"
+ else:
+ req_string = str(ireq.req)
+ else:
+ assert ireq.link
+ req_string = ireq.link.url
+
+ input_requirements.append(req_string)
+
+ # Scan all the elements of the resulting `RequirementSet` and map it back to all
+ # the install candidates preserved by `RequirementSetWithCandidates`.
+ resolution_result = ResolutionResult(
+ input_requirements=tuple(input_requirements)
+ )
+ for candidate in requirement_set.candidates.mapping.values():
+ # This will occur for the python version requirement, for example.
+ if candidate.name not in requirement_set.requirements:
+ if isinstance(candidate, RequiresPythonCandidate):
+ assert resolution_result.python_version is None
+ resolution_result.python_version = produce_exact_version_specifier(
+ str(candidate.version)
+ )
+ continue
+ raise TypeError(
+ f"unknown candidate not found in requirement set: {candidate}"
+ )
+
+ req = requirement_set.requirements[candidate.name]
+ assert req.name is not None
+ assert req.link is not None
+ assert req.name not in resolution_result.candidates
+
+ # Scan the dependencies of the installation candidates, which cover both
+ # normal dependencies as well as Requires-Python information.
+ requires_python: Optional[SpecifierSet] = None
+ dependencies: List[Requirement] = []
+ for maybe_dep in candidate.iter_dependencies(with_requires=True):
+ # It's unclear why `.iter_dependencies()` may occasionally yield `None`.
+ if maybe_dep is None:
+ continue
+ # There will only ever be one of these for each candidate, if any. We
+ # extract the version specifier.
+ if isinstance(maybe_dep, RequiresPythonRequirement):
+ requires_python = maybe_dep.specifier
+ continue
+
+ # Convert the 2020 resolver-internal Requirement subclass instance into
+ # a `packaging.requirements.Requirement` instance.
+ maybe_req = maybe_dep.as_serializable_requirement()
+ if maybe_req is None:
+ continue
+
+ # For `ExplicitRequirement`s only, we want to make sure we propagate any
+ # source URL into a dependency's `packaging.requirements.Requirement`
+ # instance.
+ if isinstance(maybe_dep, ExplicitRequirement):
+ dep_candidate = maybe_dep.candidate
+ if maybe_req.url is None and isinstance(
+ dep_candidate, LinkCandidate
+ ):
+ assert dep_candidate.source_link is not None
+ maybe_req = Requirement(
+ f"{maybe_req}@{dep_candidate.source_link.url}"
+ )
+
+ dependencies.append(maybe_req)
+
+ # Mutate the candidates dictionary to add this candidate after processing
+ # any dependencies and python version requirement.
+ resolution_result.candidates[req.name] = ResolvedCandidate(
+ req=candidate.as_serializable_requirement(),
+ download_info=URLDownloadInfo.from_link_with_source(
+ LinkWithSource(
+ req.link,
+ source_dir=req.source_dir,
+ link_is_in_wheel_cache=req.original_link_is_in_wheel_cache,
+ )
+ ),
+ dependencies=tuple(dependencies),
+ requires_python=requires_python,
+ )
+
+ # Write a simplified representation of the resolution to stdout.
+ write_output(resolution_result.as_basic_log(options.json_report_file))
+ with open(options.json_report_file, "w") as f:
+ json.dump(resolution_result.as_json(), f, indent=4)
+
try:
pip_req = requirement_set.get_requirement("pip")
except KeyError:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment