Skip to content

Commit

Permalink
Recursive dependency upgrades should be subject to global version con…
Browse files Browse the repository at this point in the history
…straints

fixes #378, connected to #378
  • Loading branch information
arcivanov committed Aug 10, 2016
1 parent c2e649f commit 8b3cbaf
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 38 deletions.
2 changes: 1 addition & 1 deletion build.py
Expand Up @@ -93,7 +93,7 @@ def initialize(project):
project.build_depends_on("importlib") # for fluentmock

project.depends_on("tblib")
project.depends_on("pip", ">=7.0")
project.depends_on("pip", ">=7.1")
project.depends_on("wheel")

project.set_property("verbose", True)
Expand Down
5 changes: 5 additions & 0 deletions src/main/python/pybuilder/pip_common.py
Expand Up @@ -46,3 +46,8 @@ def _pip_disallows_insecure_packages_by_default():
# nor will it install externally hosted files by default
# Also pip v1.1 for example has no __version__
return hasattr(pip, "__version__") and pip.__version__ >= '1.5'


def _pip_supports_constraints():
import pip
return hasattr(pip, "__version__") and pip.__version__ >= '7.1'
16 changes: 14 additions & 2 deletions src/main/python/pybuilder/pip_utils.py
Expand Up @@ -25,7 +25,8 @@
SpecifierSet,
search_packages_info,
pip_working_set_init,
_pip_disallows_insecure_packages_by_default)
_pip_disallows_insecure_packages_by_default,
_pip_supports_constraints)
from pybuilder.utils import execute_command, as_list

PIP_EXEC_STANZA = [sys.executable, "-m", "pip.__main__"]
Expand Down Expand Up @@ -72,7 +73,8 @@ def pip_install(install_targets, index_url=None, extra_index_url=None, upgrade=F


def build_pip_install_options(index_url=None, extra_index_url=None, upgrade=False, insecure_installs=None,
force_reinstall=False, target_dir=None, verbose=False, trusted_host=None):
force_reinstall=False, target_dir=None, verbose=False, trusted_host=None,
constraint_file=None):
options = []
if index_url:
options.append("--index-url")
Expand Down Expand Up @@ -103,6 +105,10 @@ def build_pip_install_options(index_url=None, extra_index_url=None, upgrade=Fals
options.append("-t")
options.append(target_dir)

if constraint_file and _pip_supports_constraints():
options.append("-c")
options.append(constraint_file)

if _pip_disallows_insecure_packages_by_default() and insecure_installs:
for insecure_install in insecure_installs:
arguments_for_insecure_installation = ["--allow-unverified", insecure_install,
Expand All @@ -112,6 +118,12 @@ def build_pip_install_options(index_url=None, extra_index_url=None, upgrade=Fals
return options


def create_constraint_file(file_name, constraints):
with open(file_name, "wt") as fout:
for constraint in as_pip_install_target(constraints):
fout.write("%s\n" % constraint)


def as_pip_install_target(mixed):
arguments = []
targets = as_list(mixed)
Expand Down
Expand Up @@ -36,7 +36,8 @@
as_pip_install_target,
get_package_version,
should_update_package,
version_satisfies_spec)
version_satisfies_spec,
create_constraint_file)
from pybuilder.terminal import print_file_content
from pybuilder.utils import execute_command, mkdir, as_list, safe_log_file_name

Expand Down Expand Up @@ -95,10 +96,15 @@ def create_install_log_directory(logger, project):


def install_dependency(logger, project, dependencies):
dependencies_to_install, orig_installed_pkgs = _filter_dependencies(logger, project, dependencies)
dependencies_to_install, orig_installed_pkgs, dependency_constraints = _filter_dependencies(logger, project,
dependencies)
batch_dependencies = []
standalone_dependencies = []
local_mapping = project.get_property("install_dependencies_local_mapping")

constraints_file = project.expand_path("$dir_target", "install_dependencies_constraints")
create_constraint_file(constraints_file, dependency_constraints)

for dependency in dependencies_to_install:
url = getattr(dependency, "url", None)

Expand All @@ -119,14 +125,20 @@ def install_dependency(logger, project, dependencies):
url = getattr(standalone_dependency, "url", None)
log_file = project.expand_path("$dir_install_logs", safe_log_file_name(dependency.name))
_do_install_dependency(logger, project, standalone_dependency,
True,
url,
True, # upgrade
url, # force reinstall
constraints_file,
local_mapping.get(dependency.name),
log_file)

if len(batch_dependencies):
log_file = project.expand_path("$dir_install_logs", "install_batch")
_do_install_dependency(logger, project, batch_dependencies, True, False, None, log_file)
_do_install_dependency(logger, project, batch_dependencies,
True, # upgrade
False, # force reinstall
constraints_file,
None,
log_file)

__reload_pip_if_updated(logger, dependencies_to_install)

Expand All @@ -135,6 +147,7 @@ def _filter_dependencies(logger, project, dependencies):
dependencies = as_list(dependencies)
installed_packages = get_package_version(dependencies)
dependencies_to_install = []
dependency_constraints = []

for dependency in dependencies:
logger.debug("Inspecting dependency '%s'" % dependency)
Expand All @@ -144,6 +157,11 @@ def _filter_dependencies(logger, project, dependencies):
dependencies_to_install.append(dependency)
continue
elif isinstance(dependency, Dependency):
if dependency.version:
dependency_constraints.append(dependency)
logger.debug(
"Dependency '%s' is added to the list of installation constraints" % dependency)

if dependency.url:
# Always add dependency that is url-based
logger.debug("Dependency '%s' is URL-based and will be included" % dependency)
Expand Down Expand Up @@ -171,10 +189,11 @@ def _filter_dependencies(logger, project, dependencies):

logger.debug("Dependency '%s' is already up-to-date and will be skipped" % dependency)

return dependencies_to_install, installed_packages
return dependencies_to_install, installed_packages, dependency_constraints


def _do_install_dependency(logger, project, dependency, upgrade, force_reinstall, target_dir, log_file):
def _do_install_dependency(logger, project, dependency, upgrade, force_reinstall, constraint_file, target_dir,
log_file):
batch = isinstance(dependency, collections.Iterable)

pip_command_line = list()
Expand All @@ -188,7 +207,8 @@ def _do_install_dependency(logger, project, dependency, upgrade, force_reinstall
force_reinstall,
target_dir,
project.get_property("verbose"),
project.get_property("install_dependencies_trusted_host")
project.get_property("install_dependencies_trusted_host"),
constraint_file
))
pip_command_line.extend(as_pip_install_target(dependency))
logger.debug("Invoking pip: %s", pip_command_line)
Expand Down
16 changes: 13 additions & 3 deletions src/unittest/python/pip_utils_tests.py
Expand Up @@ -19,11 +19,9 @@
import os
import unittest

from test_utils import patch, ANY

from pybuilder import core
from pybuilder import pip_utils

from test_utils import patch, ANY, call, mock_open

class PipVersionTests(unittest.TestCase):
def test_pip_dependency_version(self):
Expand Down Expand Up @@ -85,6 +83,8 @@ def test_build_pip_install_options(self):
self.assertEquals(pip_utils.build_pip_install_options(force_reinstall=True), ["--force-reinstall"])
self.assertEquals(pip_utils.build_pip_install_options(target_dir="target dir"), ["-t", "target dir"])
self.assertEquals(pip_utils.build_pip_install_options(target_dir="target dir"), ["-t", "target dir"])
self.assertEquals(pip_utils.build_pip_install_options(constraint_file="constraint file"),
["-c", "constraint file"])
self.assertEquals(pip_utils.build_pip_install_options(insecure_installs=["foo", "bar"]), [
"--allow-unverified", "foo",
"--allow-external", "foo",
Expand All @@ -106,3 +106,13 @@ def test_pip_install_environ_overwritten(self, execute_command):
pip_utils.pip_install("blah", env=env_dict)
execute_command.assert_called_once_with(ANY, cwd=None, env=env_dict, error_file_name=None, outfile_name=None,
shell=False)

def test_create_constraint_file(self):
m = mock_open()
with patch("pybuilder.pip_utils.open", m, new_callable=None) as open:
pip_utils.create_constraint_file("any file name",
[core.Dependency("a", "1.0"), core.Dependency("b", "==2.0"),
core.Dependency("c", ">=3.0,<4.0")])

self.assertEquals(open().write.call_args_list,
[call("a>=1.0\n"), call("b==2.0\n"), call("c<4.0,>=3.0\n")])
9 changes: 0 additions & 9 deletions src/unittest/python/pluginloader_tests.py
Expand Up @@ -18,15 +18,6 @@

import unittest

try:
import __builtin__

builtin_module = __builtin__
except ImportError as e:
import builtins

builtin_module = builtins

from test_utils import patch, Mock, ANY
from pybuilder.pip_utils import PIP_EXEC_STANZA
from pybuilder.errors import MissingPluginException, IncompatiblePluginException, UnspecifiedPluginNameException
Expand Down

0 comments on commit 8b3cbaf

Please sign in to comment.