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 URLs as packages #807

Merged
merged 2 commits into from
May 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions piptools/_compat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
Command,
FormatControl,
InstallRequirement,
Link,
PackageFinder,
PyPI,
RequirementSet,
Expand All @@ -20,6 +21,7 @@
install_req_from_line,
is_file_url,
parse_requirements,
path_to_url,
stdlib_pkgs,
url_to_path,
user_cache_dir,
Expand Down
10 changes: 0 additions & 10 deletions piptools/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,6 @@ def __str__(self):
return "\n".join(lines)


class UnsupportedConstraint(PipToolsError):
def __init__(self, message, constraint):
super(UnsupportedConstraint, self).__init__(message)
self.constraint = constraint

def __str__(self):
message = super(UnsupportedConstraint, self).__str__()
return "{} (constraint was: {})".format(message, str(self.constraint))


class IncompatibleRequirements(PipToolsError):
def __init__(self, ireq_a, ireq_b):
self.ireq_a = ireq_a
Expand Down
2 changes: 1 addition & 1 deletion piptools/repositories/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def find_best_match(self, ireq):
@abstractmethod
def get_dependencies(self, ireq):
"""
Given a pinned or an editable InstallRequirement, returns a set of
Given a pinned, URL, or editable InstallRequirement, returns a set of
dependencies (also InstallRequirements, but not necessarily pinned).
They indicate the secondary dependencies for the given requirement.
"""
Expand Down
25 changes: 21 additions & 4 deletions piptools/repositories/pypi.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@

from .._compat import (
FAVORITE_HASH,
Link,
PackageFinder,
PyPI,
RequirementSet,
TemporaryDirectory,
Wheel,
contextlib,
is_file_url,
path_to_url,
url_to_path,
)
from ..cache import CACHE_DIR
Expand All @@ -28,6 +30,7 @@
from ..utils import (
fs_str,
is_pinned_requirement,
is_url_requirement,
lookup_table,
make_install_requirement,
)
Expand Down Expand Up @@ -136,7 +139,7 @@ def find_best_match(self, ireq, prereleases=None):
Returns a Version object that indicates the best match for the given
InstallRequirement according to the external repository.
"""
if ireq.editable:
if ireq.editable or is_url_requirement(ireq):
return ireq # return itself as the best match

all_candidates = self.find_all_candidates(ireq.name)
Expand Down Expand Up @@ -228,13 +231,17 @@ def resolve_reqs(self, download_dir, ireq, wheel_cache):

def get_dependencies(self, ireq):
"""
Given a pinned or an editable InstallRequirement, returns a set of
Given a pinned, URL, or editable InstallRequirement, returns a set of
dependencies (also InstallRequirements, but not necessarily pinned).
They indicate the secondary dependencies for the given requirement.
"""
if not (ireq.editable or is_pinned_requirement(ireq)):
if not (
ireq.editable or is_url_requirement(ireq) or is_pinned_requirement(ireq)
):
raise TypeError(
"Expected pinned or editable InstallRequirement, got {}".format(ireq)
"Expected url, pinned or editable InstallRequirement, got {}".format(
ireq
)
)

if ireq not in self._dependencies_cache:
Expand Down Expand Up @@ -281,6 +288,16 @@ def get_hashes(self, ireq):
if ireq.editable:
return set()

if is_url_requirement(ireq):
# url requirements may have been previously downloaded and cached
# locally by self.resolve_reqs()
cached_path = os.path.join(self._download_dir, ireq.link.filename)
if os.path.exists(cached_path):
cached_link = Link(path_to_url(cached_path))
else:
cached_link = ireq.link
return {self._get_file_hash(cached_link)}

if not is_pinned_requirement(ireq):
raise TypeError("Expected pinned requirement, got {}".format(ireq))

Expand Down
24 changes: 7 additions & 17 deletions piptools/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
from . import click
from ._compat import install_req_from_line
from .cache import DependencyCache
from .exceptions import UnsupportedConstraint
from .logging import log
from .utils import (
UNSAFE_PACKAGES,
format_requirement,
format_specifier,
full_groupby,
is_pinned_requirement,
is_url_requirement,
key_from_ireq,
key_from_req,
)
Expand Down Expand Up @@ -101,8 +101,6 @@ def resolve(self, max_rounds=10):
self.dependency_cache.clear()
self.repository.clear_caches()

self.check_constraints(chain(self.our_constraints, self.their_constraints))

# Ignore existing packages
os.environ[str("PIP_EXISTS_ACTION")] = str(
"i"
Expand Down Expand Up @@ -139,16 +137,6 @@ def resolve(self, max_rounds=10):
# Only include hard requirements and not pip constraints
return {req for req in best_matches if not req.constraint}

@staticmethod
def check_constraints(constraints):
for constraint in constraints:
if constraint.link is not None and not constraint.editable:
msg = (
"pip-compile does not support URLs as packages, unless "
"they are editable. Perhaps add -e option?"
)
raise UnsupportedConstraint(msg, constraint)

def _group_constraints(self, constraints):
"""
Groups constraints (remember, InstallRequirements!) by their key name,
Expand Down Expand Up @@ -280,7 +268,7 @@ def get_best_match(self, ireq):
Flask==0.10.1 => Flask==0.10.1

"""
if ireq.editable:
if ireq.editable or is_url_requirement(ireq):
# NOTE: it's much quicker to immediately return instead of
# hitting the index server
best_match = ireq
Expand All @@ -303,14 +291,14 @@ def get_best_match(self, ireq):

def _iter_dependencies(self, ireq):
"""
Given a pinned or editable InstallRequirement, collects all the
Given a pinned, url, or editable InstallRequirement, collects all the
secondary dependencies for them, either by looking them up in a local
cache, or by reaching out to the repository.

Editable requirements will never be looked up, as they may have
changed at any time.
"""
if ireq.editable:
if ireq.editable or is_url_requirement(ireq):
for dependency in self.repository.get_dependencies(ireq):
yield dependency
return
Expand Down Expand Up @@ -345,5 +333,7 @@ def _iter_dependencies(self, ireq):
yield install_req_from_line(dependency_string, constraint=ireq.constraint)

def reverse_dependencies(self, ireqs):
non_editable = [ireq for ireq in ireqs if not ireq.editable]
non_editable = [
ireq for ireq in ireqs if not (ireq.editable or is_url_requirement(ireq))
]
return self.dependency_cache.reverse_dependencies(non_editable)
15 changes: 9 additions & 6 deletions piptools/scripts/compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@
from ..pip import get_pip_command, pip_defaults
from ..repositories import LocalRequirementsRepository, PyPIRepository
from ..resolver import Resolver
from ..utils import UNSAFE_PACKAGES, dedup, is_pinned_requirement, key_from_req
from ..utils import (
UNSAFE_PACKAGES,
dedup,
is_pinned_requirement,
key_from_ireq,
key_from_req,
)
from ..writer import OutputWriter

DEFAULT_REQUIREMENTS_FILE = "requirements.in"
Expand Down Expand Up @@ -338,9 +344,6 @@ def cli(
for find_link in repository.finder.find_links:
log.debug(" -f {}".format(find_link))

# Check the given base set of constraints first
Resolver.check_constraints(constraints)

try:
resolver = Resolver(
constraints,
Expand Down Expand Up @@ -413,10 +416,10 @@ def cli(
unsafe_requirements=resolver.unsafe_constraints,
reverse_dependencies=reverse_dependencies,
primary_packages={
key_from_req(ireq.req) for ireq in constraints if not ireq.constraint
key_from_ireq(ireq) for ireq in constraints if not ireq.constraint
},
markers={
key_from_req(ireq.req): ireq.markers for ireq in constraints if ireq.markers
key_from_ireq(ireq): ireq.markers for ireq in constraints if ireq.markers
},
hashes=hashes,
)
Expand Down
41 changes: 28 additions & 13 deletions piptools/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
from subprocess import check_call # nosec

from . import click
from .exceptions import IncompatibleRequirements, UnsupportedConstraint
from ._compat import DEV_PKGS, stdlib_pkgs
from .exceptions import IncompatibleRequirements
from .utils import (
flat_map,
format_requirement,
get_hashes_from_ireq,
is_url_requirement,
key_from_ireq,
key_from_req,
)

from piptools._compat import DEV_PKGS, stdlib_pkgs

PACKAGES_TO_IGNORE = (
["-markerlib", "pip", "pip-tools", "pip-review", "pkg-resources"]
+ list(stdlib_pkgs)
Expand Down Expand Up @@ -77,14 +77,10 @@ def merge(requirements, ignore_conflicts):
by_key = {}

for ireq in requirements:
if ireq.link is not None and not ireq.editable:
msg = (
"pip-compile does not support URLs as packages, unless they are "
"editable. Perhaps add -e option?"
)
raise UnsupportedConstraint(msg, ireq)

key = ireq.link or key_from_req(ireq.req)
# Limitation: URL requirements are merged by precise string match, so
# "file:///example.zip#egg=example", "file:///example.zip", and
# "example==1.0" will not merge with each other
key = key_from_ireq(ireq)

if not ignore_conflicts:
existing_ireq = by_key.get(key)
Expand All @@ -96,16 +92,35 @@ def merge(requirements, ignore_conflicts):

# TODO: Always pick the largest specifier in case of a conflict
by_key[key] = ireq

return by_key.values()


def diff_key_from_ireq(ireq):
"""
Calculate a key for comparing a compiled requirement with installed modules.
For URL requirements, only provide a useful key if the url includes
#egg=name==version, which will set ireq.req.name and ireq.specifier.
Otherwise return ireq.link so the key will not match and the package will
reinstall. Reinstall is necessary to ensure that packages will reinstall
if the URL is changed but the version is not.
"""
if is_url_requirement(ireq):
if (
ireq.req
and (getattr(ireq.req, "key", None) or getattr(ireq.req, "name", None))
and ireq.specifier
):
return key_from_ireq(ireq)
return str(ireq.link)
return key_from_ireq(ireq)


def diff(compiled_requirements, installed_dists):
"""
Calculate which packages should be installed or uninstalled, given a set
of compiled requirements and a list of currently installed modules.
"""
requirements_lut = {r.link or key_from_req(r.req): r for r in compiled_requirements}
requirements_lut = {diff_key_from_ireq(r): r for r in compiled_requirements}

satisfied = set() # holds keys
to_install = set() # holds InstallRequirement objects
Expand Down
12 changes: 11 additions & 1 deletion piptools/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,23 @@ def make_install_requirement(name, version, extras, constraint=False):
)


def is_url_requirement(ireq):
"""
Return True if requirement was specified as a path or URL.
ireq.original_link will have been set by InstallRequirement.__init__
"""
return bool(ireq.original_link)


def format_requirement(ireq, marker=None, hashes=None):
"""
Generic formatter for pretty printing InstallRequirements to the terminal
in a less verbose way than using its `__str__` method.
"""
if ireq.editable:
line = "-e {}".format(ireq.link.url)
elif is_url_requirement(ireq):
line = ireq.link.url
else:
line = str(ireq.req).lower()

Expand Down Expand Up @@ -110,7 +120,7 @@ def is_pinned_requirement(ireq):
if ireq.editable:
return False

if len(ireq.specifier._specs) != 1:
if ireq.req is None or len(ireq.specifier._specs) != 1:
return False

op, version = next(iter(ireq.specifier._specs))._spec
Expand Down
8 changes: 4 additions & 4 deletions piptools/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
dedup,
format_requirement,
get_compile_command,
key_from_req,
key_from_ireq,
)


Expand Down Expand Up @@ -129,7 +129,7 @@ def _iter_lines(
ireq,
reverse_dependencies,
primary_packages,
markers.get(key_from_req(ireq.req)),
markers.get(key_from_ireq(ireq)),
hashes=hashes,
)
yield line
Expand All @@ -147,7 +147,7 @@ def _iter_lines(
ireq,
reverse_dependencies,
primary_packages,
marker=markers.get(key_from_req(ireq.req)),
marker=markers.get(key_from_ireq(ireq)),
hashes=hashes,
)
if not self.allow_unsafe:
Expand Down Expand Up @@ -185,7 +185,7 @@ def _format_requirement(

line = format_requirement(ireq, marker=marker, hashes=ireq_hashes)

if not self.annotate or key_from_req(ireq.req) in primary_packages:
if not self.annotate or key_from_ireq(ireq) in primary_packages:
return line

# Annotate what packages this package is required by
Expand Down
Loading