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

Fixup relative and absolute path handling #1329

Closed
wants to merge 34 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
399f1ea
Add optional param from_dir to format_requirement
AndydeCleyre Aug 4, 2021
c9c00fb
Reuse fragment_string to simplify _build_direct_reference_best_efforts
AndydeCleyre Aug 4, 2021
afea149
Add working_dir context manager and corresponding test
AndydeCleyre Aug 4, 2021
8168ad2
Add abs_ireq for normalizing ireqs, and related tests
AndydeCleyre Aug 4, 2021
df6f78b
Update test to expect and accept more Windows file URIs
AndydeCleyre Aug 4, 2021
09003f6
Add writer test for annotation accuracy regarding source ireqs
AndydeCleyre Aug 4, 2021
6398866
Add optional param from_dir to _comes_from_as_string
AndydeCleyre Aug 4, 2021
d68ea31
Add optional from_dir param to parse_requirements
AndydeCleyre Aug 5, 2021
8f7f633
Add flag for compile: --write-relative-to-output
AndydeCleyre Aug 6, 2021
97fe407
Add test_local_editable_vcs_package
AndydeCleyre Aug 6, 2021
76186c4
Add flag to sync: --read-relative-to-input
AndydeCleyre Aug 7, 2021
dedfa56
Absolute-ize src_file paths and more safely determine output file paths
AndydeCleyre Aug 7, 2021
e74f317
Exit earlier when no output file is specified for multiple input files
AndydeCleyre Aug 7, 2021
e182c21
Add flag for compile: --read-relative-to-input
AndydeCleyre Aug 8, 2021
e496d34
Make annotation req file paths relative to the output file
AndydeCleyre Aug 8, 2021
b631521
Add test_annotation_relative_paths for #1107
AndydeCleyre Aug 6, 2021
fd7e470
Add test_format_requirement_annotation_impossible_relative_path
AndydeCleyre Aug 9, 2021
1141ae7
Make use of pip's install_req_from_link_and_ireq to simplify abs_ireq
AndydeCleyre Aug 10, 2021
0d4f957
Reinject any lost url fragments during parse_requirements
AndydeCleyre Aug 10, 2021
6710a45
Include extras syntax in direct references we construct
AndydeCleyre Aug 10, 2021
03a51cc
Replicate pip's install_req_from_link_and_ireq in utils
AndydeCleyre Aug 11, 2021
9501a9d
Avoid choking on a relative path without scheme prefix, with fragment
AndydeCleyre Aug 11, 2021
af758d5
Improve our version of install_req_from_link_and_ireq (diverge)
AndydeCleyre Aug 11, 2021
c239939
Improve consistency of output regarding fragments and extras
AndydeCleyre Aug 12, 2021
6306971
Add test_url_package_with_extras
AndydeCleyre Aug 12, 2021
6a5d208
Remove fragment_string parameter omit_extras
AndydeCleyre Aug 13, 2021
cbf604d
Add test_local_file_uri_with_extras
AndydeCleyre Aug 13, 2021
b10ae67
Add test_local_file_path_package for non-URI paths
AndydeCleyre Aug 13, 2021
bfe7d07
Further diverge from pip's install_req_from_link_and_ireq
AndydeCleyre Aug 21, 2021
50484ec
More thorough install_req_from_link_and_ireq
AndydeCleyre Aug 21, 2021
e124089
Remove pointless generator expression around sorted
AndydeCleyre Oct 31, 2021
4893794
Use consistently canonicalized ireq name when writing direct reference
AndydeCleyre Feb 7, 2022
0a4ef9a
Remove pointless generator expression around sorted, again
FlorentJeannot Feb 15, 2022
90a7097
Construct relative req lines to match what pip install understands
AndydeCleyre Mar 2, 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
75 changes: 74 additions & 1 deletion piptools/_compat/pip_compat.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
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,
fragment_string,
install_req_from_link_and_ireq,
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 +42,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 = install_req_from_link_and_ireq(link_with_fragment, ireq)

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
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 @@ -258,6 +277,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 @@ -287,24 +308,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 @@ -361,6 +385,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
),
)

# Exclude packages from --upgrade-package/-P from the existing
Expand All @@ -387,8 +416,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 @@ -414,20 +442,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 @@ -502,6 +539,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