Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support relative paths and regularize path handling and formatting #1650

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a912b13
Add optional param from_dir to format_requirement
AndydeCleyre Jun 30, 2022
c90d969
Reuse fragment_string to simplify _build_direct_reference_best_efforts
AndydeCleyre Jun 30, 2022
11ffba6
Add working_dir context manager and corresponding test
AndydeCleyre Jun 30, 2022
65c489c
Add abs_ireq for normalizing ireqs, and related tests
AndydeCleyre Jun 30, 2022
987d90b
Update test to expect and accept more Windows file URIs
AndydeCleyre Jun 30, 2022
6713b60
Add writer test for annotation accuracy regarding source ireqs
AndydeCleyre Jun 30, 2022
0f2cf1d
Add optional param from_dir to _comes_from_as_string
AndydeCleyre Jun 30, 2022
f2350e1
Add optional from_dir param to parse_requirements
AndydeCleyre Jun 30, 2022
8edf988
Add flag for compile: --write-relative-to-output
AndydeCleyre Jun 30, 2022
bc00d62
Add test_local_editable_vcs_package
AndydeCleyre Jun 30, 2022
a9784a1
Add flag to sync: --read-relative-to-input
AndydeCleyre Jun 30, 2022
996f21c
Absolute-ize src_file paths and more safely determine output file paths
AndydeCleyre Jun 30, 2022
a92729f
Exit earlier when no output file is specified for multiple input files
AndydeCleyre Jun 30, 2022
f46a2d5
Add flag for compile: --read-relative-to-input
AndydeCleyre Jun 30, 2022
7e55700
Make annotation req file paths relative to the output file
AndydeCleyre Jun 30, 2022
4004f96
Add test_annotation_relative_paths for #1107
AndydeCleyre Jun 30, 2022
7ef7b98
Add test_format_requirement_annotation_impossible_relative_path
AndydeCleyre Jun 30, 2022
48d05fd
Reinject any lost url fragments during parse_requirements
AndydeCleyre Jun 30, 2022
6084450
Include extras syntax in direct references we construct
AndydeCleyre Jun 30, 2022
8117c1d
Avoid choking on a relative path without scheme prefix, with fragment
AndydeCleyre Jun 30, 2022
ece9e14
When copying ireqs, copy extras from a newly provided link
AndydeCleyre Jun 30, 2022
3345abc
Improve consistency of output regarding fragments and extras
AndydeCleyre Jun 30, 2022
eee3e8a
Add test_url_package_with_extras and fix it for backtracking
AndydeCleyre Jun 30, 2022
4ed481d
Remove fragment_string parameter omit_extras
AndydeCleyre Jun 30, 2022
2d27659
Add test_local_file_uri_with_extras
AndydeCleyre Jun 30, 2022
88eb70d
Add test_local_file_path_package for non-URI paths
AndydeCleyre Jun 30, 2022
6468e37
Use consistently canonicalized ireq name when writing direct reference
AndydeCleyre Jun 30, 2022
aa3ee0f
Construct relative req lines to match what pip install understands
AndydeCleyre Jun 30, 2022
f853ba9
Merge branch 'master' into feature/relpaths-post-6.8.0
ssbarnea Oct 5, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 69 additions & 1 deletion piptools/_compat/pip_compat.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import optparse
import platform
import re
from typing import Callable, Iterable, Iterator, Optional, cast

import pip
from pip._internal.exceptions import InstallationError
from pip._internal.index.package_finder import PackageFinder
from pip._internal.models.link import Link
from pip._internal.network.session import PipSession
from pip._internal.req import InstallRequirement
from pip._internal.req import parse_requirements as _parse_requirements
from pip._internal.req.constructors import install_req_from_parsed_requirement
from pip._internal.req.req_file import ParsedRequirement
from pip._vendor.packaging.version import parse as parse_version
from pip._vendor.pkg_resources import Requirement

from ..utils import abs_ireq, copy_install_requirement, fragment_string, working_dir

PIP_VERSION = tuple(map(int, parse_version(pip.__version__).base_version.split(".")))

file_url_schemes_re = re.compile(r"^((git|hg|svn|bzr)\+)?file:")

__all__ = [
"get_build_tracker",
Expand All @@ -29,11 +37,71 @@ def parse_requirements(
options: Optional[optparse.Values] = None,
constraint: bool = False,
isolated: bool = False,
from_dir: Optional[str] = None,
) -> Iterator[InstallRequirement]:
for parsed_req in _parse_requirements(
filename, session, finder=finder, options=options, constraint=constraint
):
yield install_req_from_parsed_requirement(parsed_req, isolated=isolated)
# This context manager helps pip locate relative paths specified
# with non-URI (non file:) syntax, e.g. '-e ..'
with working_dir(from_dir):
try:
ireq = install_req_from_parsed_requirement(
parsed_req, isolated=isolated
)
except InstallationError:
# This can happen when the url is a relpath with a fragment,
# so we try again with the fragment stripped
preq_without_fragment = ParsedRequirement(
requirement=re.sub(r"#[^#]+$", "", parsed_req.requirement),
is_editable=parsed_req.is_editable,
comes_from=parsed_req.comes_from,
constraint=parsed_req.constraint,
options=parsed_req.options,
line_source=parsed_req.line_source,
)
ireq = install_req_from_parsed_requirement(
preq_without_fragment, isolated=isolated
)

# At this point the ireq has two problems:
# - Sometimes the fragment is lost (even without an InstallationError)
# - It's now absolute (ahead of schedule),
# so abs_ireq will not know to apply the _was_relative attribute,
# which is needed for the writer to use the relpath.

# To account for the first:
if not fragment_string(ireq):
fragment = Link(parsed_req.requirement)._parsed_url.fragment
if fragment:
link_with_fragment = Link(
url=f"{ireq.link.url_without_fragment}#{fragment}",
comes_from=ireq.link.comes_from,
requires_python=ireq.link.requires_python,
yanked_reason=ireq.link.yanked_reason,
cache_link_parsing=ireq.link.cache_link_parsing,
)
ireq = copy_install_requirement(ireq, link=link_with_fragment)

a_ireq = abs_ireq(ireq, from_dir)

# To account for the second, we guess if the path was initially relative and
# set _was_relative ourselves:
bare_path = file_url_schemes_re.sub(
"", parsed_req.requirement.split(" @ ", 1)[-1]
)
is_win = platform.system() == "Windows"
if is_win:
bare_path = bare_path.lstrip("/")
if (
a_ireq.link is not None
and a_ireq.link.scheme.endswith("file")
and not bare_path.startswith("/")
):
if not (is_win and re.match(r"[a-zA-Z]:", bare_path)):
a_ireq._was_relative = True

yield a_ireq


if PIP_VERSION[:2] <= (22, 0):
Expand Down
13 changes: 11 additions & 2 deletions piptools/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -705,8 +705,8 @@ def _get_install_requirements(
for extras_candidate in extras_candidates:
project_name = canonicalize_name(extras_candidate.project_name)
ireq = result_ireqs[project_name]
ireq.extras |= extras_candidate.extras
ireq.req.extras |= extras_candidate.extras
ireq.extras = set(ireq.extras) | set(extras_candidate.extras)
ireq.req.extras = set(ireq.req.extras) | set(extras_candidate.extras)

return set(result_ireqs.values())

Expand Down Expand Up @@ -778,4 +778,13 @@ def _get_install_requirement_from_candidate(
if source_ireq is not None and ireq_key not in self.existing_constraints:
pinned_ireq._source_ireqs = [source_ireq]

# Preserve _was_relative attribute of local path requirements
if pinned_ireq.link.is_file:
# Install requirement keys may not match
for c in self.constraints:
if pinned_ireq.link == c.link:
if hasattr(c, "_was_relative"):
pinned_ireq._was_relative = True
break

return pinned_ireq
70 changes: 54 additions & 16 deletions piptools/scripts/compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
drop_extras,
is_pinned_requirement,
key_from_ireq,
working_dir,
)
from ..writer import OutputWriter

Expand Down Expand Up @@ -159,6 +160,24 @@ def _get_default_option(option_name: str) -> Any:
"Will be derived from input file otherwise."
),
)
@click.option(
"--write-relative-to-output",
is_flag=True,
default=False,
help=(
"Construct relative paths as relative to the output file's parent. "
"Will be written as relative to the current folder otherwise."
),
)
@click.option(
"--read-relative-to-input",
is_flag=True,
default=False,
help=(
"Resolve relative paths as relative to the input file's parent. "
"Will be resolved as relative to the current folder otherwise."
),
)
@click.option(
"--allow-unsafe/--no-allow-unsafe",
is_flag=True,
Expand Down Expand Up @@ -272,6 +291,8 @@ def cli(
upgrade: bool,
upgrade_packages: Tuple[str, ...],
output_file: Union[LazyFile, IO[Any], None],
write_relative_to_output: bool,
read_relative_to_input: bool,
allow_unsafe: bool,
strip_extras: bool,
generate_hashes: bool,
Expand Down Expand Up @@ -306,24 +327,27 @@ def cli(
).format(DEFAULT_REQUIREMENTS_FILE)
)

src_files = tuple(src if src == "-" else os.path.abspath(src) for src in src_files)

if not output_file:
# An output file must be provided for stdin
if src_files == ("-",):
raise click.BadParameter("--output-file is required if input is from stdin")
# Use default requirements output file if there is a setup.py the source file
elif os.path.basename(src_files[0]) in METADATA_FILENAMES:
file_name = os.path.join(
os.path.dirname(src_files[0]), DEFAULT_REQUIREMENTS_OUTPUT_FILE
)
# An output file must be provided if there are multiple source files
elif len(src_files) > 1:
raise click.BadParameter(
"--output-file is required if two or more input files are given."
)
# Use default requirements output file if there is only a setup.py source file
elif os.path.basename(src_files[0]) in METADATA_FILENAMES:
file_name = os.path.join(
os.path.dirname(src_files[0]), DEFAULT_REQUIREMENTS_OUTPUT_FILE
)
# Otherwise derive the output file from the source file
else:
base_name = src_files[0].rsplit(".", 1)[0]
file_name = base_name + ".txt"
file_name = os.path.splitext(src_files[0])[0] + ".txt"
if file_name == src_files[0]:
file_name += ".txt"

output_file = click.open_file(file_name, "w+b", atomic=True, lazy=True)

Expand Down Expand Up @@ -385,6 +409,11 @@ def cli(
finder=tmp_repository.finder,
session=tmp_repository.session,
options=tmp_repository.options,
from_dir=(
os.path.dirname(os.path.abspath(output_file.name))
if write_relative_to_output
else None
),
)

for ireq in filter(is_pinned_requirement, ireqs):
Expand All @@ -408,8 +437,7 @@ def cli(
if src_file == "-":
# pip requires filenames and not files. Since we want to support
# piping from stdin, we need to briefly save the input from stdin
# to a temporary file and have pip read that. also used for
# reading requirements from install_requires in setup.py.
# to a temporary file and have pip read that.
tmpfile = tempfile.NamedTemporaryFile(mode="wt", delete=False)
tmpfile.write(sys.stdin.read())
comes_from = "-r -"
Expand All @@ -435,20 +463,29 @@ def cli(
log.error(str(e))
log.error(f"Failed to parse {os.path.abspath(src_file)}")
sys.exit(2)
comes_from = f"{metadata.get_all('Name')[0]} ({src_file})"
constraints.extend(
[
install_req_from_line(req, comes_from=comes_from)
for req in metadata.get_all("Requires-Dist") or []
]
)
with working_dir(os.path.dirname(os.path.abspath(output_file.name))):
comes_from = (
f"{metadata.get_all('Name')[0]} ({os.path.relpath(src_file)})"
)
with working_dir(
os.path.dirname(src_file) if read_relative_to_input else None
):
constraints.extend(
[
install_req_from_line(req, comes_from=comes_from)
for req in metadata.get_all("Requires-Dist") or []
]
)
else:
constraints.extend(
parse_requirements(
src_file,
finder=repository.finder,
session=repository.session,
options=repository.options,
from_dir=(
os.path.dirname(src_file) if read_relative_to_input else None
),
)
)

Expand Down Expand Up @@ -526,6 +563,7 @@ def cli(
find_links=repository.finder.find_links,
emit_find_links=emit_find_links,
emit_options=emit_options,
write_relative_to_output=write_relative_to_output,
)
writer.write(
results=results,
Expand Down
22 changes: 21 additions & 1 deletion piptools/scripts/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@
is_flag=True,
help="Ignore package index (only looking at --find-links URLs instead)",
)
@click.option(
"--read-relative-to-input",
is_flag=True,
default=False,
help=(
"Resolve relative paths as relative to the input file's parent. "
"Will be resolved as relative to the current folder otherwise."
),
)
@click.option(
"--python-executable",
help="Custom python executable path if targeting an environment other than current.",
Expand All @@ -96,6 +105,7 @@ def cli(
extra_index_url: Tuple[str, ...],
trusted_host: Tuple[str, ...],
no_index: bool,
read_relative_to_input: bool,
python_executable: Optional[str],
verbose: int,
quiet: int,
Expand Down Expand Up @@ -139,7 +149,17 @@ def cli(
# Parse requirements file. Note, all options inside requirements file
# will be collected by the finder.
requirements = flat_map(
lambda src: parse_requirements(src, finder=finder, session=session), src_files
lambda src: parse_requirements(
src,
finder=finder,
session=session,
from_dir=(
os.path.dirname(os.path.abspath(src))
if read_relative_to_input
else os.getcwd()
),
),
src_files,
)

try:
Expand Down
Loading