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

Enh/missing constraints #156

Merged
merged 17 commits into from
Mar 15, 2016
Merged
Show file tree
Hide file tree
Changes from 5 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
8 changes: 8 additions & 0 deletions simplesat/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ def __init__(self, requirement, *a, **kw):
self.args = self.args or (str(requirement),)


class MissingInstallRequires(NoPackageFound):
pass


class MissingConflicts(NoPackageFound):
pass


class SatisfiabilityError(SolverException):
def __init__(self, unsat):
self.unsat = unsat
Expand Down
82 changes: 66 additions & 16 deletions simplesat/rules_generator.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import collections
import enum
import logging

from .constraints import Requirement
from .errors import NoPackageFound, SolverException
from .errors import (
MissingConflicts, MissingInstallRequires, NoPackageFound, SolverException
)
from .request import JobType


logger = logging.getLogger(__name__)

INDENT = 4


Expand All @@ -19,6 +24,7 @@ class RuleType(enum.Enum):
package_same_name = 10
package_implicit_obsoletes = 11
package_installed = 12
package_broken = 100

internal = 256

Expand Down Expand Up @@ -113,6 +119,12 @@ def to_string(self, pool, unique=False):
left = pool.id_to_string(abs(left_id))[1:]
right = pool.id_to_string(abs(right_id))
rule_desc = "{} conflicts with {}".format(left, right)
elif self._reason == RuleType.package_broken:
package_id = self.literals[0]
# Trim the sign
package_str = pool.id_to_string(abs(package_id))
msg = "{} was ignored because it depends on missing packages"
rule_desc = msg.format(package_str)
else:
rule_desc = s

Expand All @@ -135,13 +147,14 @@ def __hash__(self):


class RulesGenerator(object):
def __init__(self, pool, request, installed_map=None):
def __init__(self, pool, request, installed_map=None, strict=False):
self._rules_set = collections.OrderedDict()
self._pool = pool

self.request = request
self.installed_map = installed_map or collections.OrderedDict()
self.added_package_ids = set()
self.strict = strict

def iter_rules(self):
"""
Expand Down Expand Up @@ -284,6 +297,7 @@ def _add_rule(self, rule, rule_type):
self._rules_set[rule] = None

def _add_install_requires_rules(self, package, work_queue, requirements):
all_dependency_candidates = []
for constraints in package.install_requires:
pkg_requirement = Requirement.from_constraints(constraints)
dependency_candidates = self._pool.what_provides(pkg_requirement)
Expand All @@ -296,20 +310,42 @@ def _add_install_requires_rules(self, package, work_queue, requirements):
else None)

if not dependency_candidates:
msg = ("No candidates found for requirement {0!r}, needed for "
"dependency {1!r}")
raise NoPackageFound(
pkg_requirement,
msg.format(pkg_requirement.name, package),
pkg_msg = ("'{0.name} {0.version}' from"
" '{0.repository_info.name}'")
pkg_str = pkg_msg.format(package)
req_str = str(pkg_requirement)
msg = ("Blocking package {0!s}: no candidates found for"
" dependency {1!r}").format(pkg_str, req_str)
if self.strict:
# We only raise an exception if this comes directly from a
# job requirement. Unfortunately, we don't track that
# explicitly because we push all of the work through a
# queue. As a proxy, we can examine the associated
# requirements directly.
if len(requirements) == 1:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the logic behind testing for len(requirements) == 1 ?

raise MissingInstallRequires(pkg_requirement, msg)
else:
logger.warning(msg)
else:
logger.info(msg)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a return statement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind, it's down below. We have to make our rule preventing installation first.


rule = self._create_remove_rule(
package, RuleType.package_broken,
requirements=combined_requirements,
)
self._add_rule(rule, "package")
return

rule = self._create_dependency_rule(
package, dependency_candidates, RuleType.package_requires,
combined_requirements)
self._add_rule(rule, "package")

for candidate in dependency_candidates:
work_queue.append((candidate, combined_requirements))
# We're "buffering" this so that we don't queue up any dependencies
# unless they are all successfully processed
all_dependency_candidates.extend(
(candidate, combined_requirements)
for candidate in dependency_candidates)
work_queue.extend(all_dependency_candidates)

def _add_conflicts_rules(self, package, requirements):
"""
Expand Down Expand Up @@ -345,12 +381,26 @@ def _add_conflicts_rules(self, package, requirements):
else None)

if not conflict_providers:
msg = ("No candidates found for requirement {0!r}, needed for "
"conflict {1!r}")
raise NoPackageFound(
pkg_requirement,
msg.format(pkg_requirement.name, package),
)
pkg_msg = ("'{0.name} {0.version}' from"
" '{0.repository_info.name}'")
pkg_str = pkg_msg.format(package)
req_str = str(pkg_requirement)
msg = ("No candidates found for requirement {0!r}, needed"
" for conflict with {1!s}").format(req_str, pkg_str)
if self.strict:
# We only raise an exception if this comes directly from a
# job requirement. Unfortunately, we don't track that
# explicitly because we push all of the work through a
# queue. As a proxy, we can examine the associated
# requirements directly.
if len(requirements) == 1:
raise MissingConflicts(pkg_requirement, msg)
else:
logger.warning(msg)
else:
# We just ignore missing constraints. They don't break
# anything.
logger.info(msg)

for provider in conflict_providers:
rule = self._create_conflicts_rule(
Expand Down
132 changes: 126 additions & 6 deletions simplesat/tests/test_rules_generator.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-

import io
import mock
import unittest

from simplesat.errors import NoPackageFound
from simplesat.errors import MissingConflicts, MissingInstallRequires

from ..pool import Pool
from ..rules_generator import RuleType, RulesGenerator
Expand Down Expand Up @@ -103,7 +104,7 @@ def test_conflicts(self):
self.assertEqual(conflict.reason, RuleType.package_conflicts)
self.assertEqual(conflict.literals, r_literals)

def test_missing_dependencies_package(self):
def test_missing_direct_dependencies_package(self):
# Given
yaml = u"""
packages:
Expand All @@ -115,6 +116,9 @@ def test_missing_dependencies_package(self):
requirement: "atom"
"""
scenario = Scenario.from_yaml(io.StringIO(yaml))
expected_log = ("Blocking package 'atom 1.0.0-1'" " from 'remote': no"
" candidates found for dependency 'quark > 1.0-0'")
expected_rule = (-1,)

# When
repos = list(scenario.remote_repositories)
Expand All @@ -124,23 +128,88 @@ def test_missing_dependencies_package(self):
pool.package_id(p): p for p in scenario.installed_repository}
rules_generator = RulesGenerator(
pool, scenario.request, installed_map=installed_map)
with mock.patch('simplesat.rules_generator.logger') as mock_logger:
rules = list(rules_generator.iter_rules())

# Then
with self.assertRaises(NoPackageFound):
result_log = mock_logger.info.call_args[0][0]
self.assertMultiLineEqual(result_log, expected_log)

result = rules[0].literals
self.assertEqual(expected_rule, result)

# When
rules_generator = RulesGenerator(
pool, scenario.request, installed_map=installed_map, strict=True)

# Then
with self.assertRaises(MissingInstallRequires):
list(rules_generator.iter_rules())

def test_missing_conflicts_package(self):
def test_missing_indirect_dependencies_package(self):
# Given
yaml = u"""
packages:
- gluon 1.0.0-1; depends (quark ^= 1.0.0)
- atom 1.0.0-1; depends (gluon); conflicts (gdata ^= 1.0.0)
- gdata 1.0.0-1; conflicts (atom >= 1.0.1)

request:
- operation: "install"
requirement: "atom"
"""
scenario = Scenario.from_yaml(io.StringIO(yaml))
expected_log = ("Blocking package 'gluon 1.0.0-1' from 'remote': no"
" candidates found for dependency 'quark ^= 1.0.0'")
expected_rule = (-3,)

# When
repos = list(scenario.remote_repositories)
repos.append(scenario.installed_repository)
pool = Pool(repos)
installed_map = {
pool.package_id(p): p for p in scenario.installed_repository}
rules_generator = RulesGenerator(
pool, scenario.request, installed_map=installed_map)
with mock.patch('simplesat.rules_generator.logger') as mock_logger:
rules = list(rules_generator.iter_rules())

# Then
result_log = mock_logger.info.call_args[0][0]
self.assertMultiLineEqual(result_log, expected_log)

result = [r.literals for r in rules
if r.reason == RuleType.package_broken][0]
self.assertEqual(expected_rule, result)

# When
rules_generator = RulesGenerator(
pool, scenario.request, installed_map=installed_map, strict=True)
with mock.patch('simplesat.rules_generator.logger') as mock_logger:
rules = list(rules_generator.iter_rules())

# Then
result_log = mock_logger.warning.call_args[0][0]
self.assertMultiLineEqual(result_log, expected_log)

result = [r.literals for r in rules
if r.reason == RuleType.package_broken][0]
self.assertEqual(expected_rule, result)

def test_missing_direct_conflicts_package(self):
# Given
yaml = u"""
packages:
- quark 1.0.0-1
- atom 1.0.0-1; depends (quark > 1.0); conflicts (gdata ^= 1.0.0)
- atom 1.0.0-1; conflicts (gdata ^= 1.0.0)

request:
- operation: "install"
requirement: "atom"
"""
scenario = Scenario.from_yaml(io.StringIO(yaml))
expected = ("No candidates found for requirement 'gdata ^= 1.0.0',"
" needed for conflict with 'atom 1.0.0-1' from 'remote'")

# When
repos = list(scenario.remote_repositories)
Expand All @@ -150,7 +219,58 @@ def test_missing_conflicts_package(self):
pool.package_id(p): p for p in scenario.installed_repository}
rules_generator = RulesGenerator(
pool, scenario.request, installed_map=installed_map)
with mock.patch('simplesat.rules_generator.logger') as mock_logger:
list(rules_generator.iter_rules())

# Then
with self.assertRaises(NoPackageFound):
result = mock_logger.info.call_args[0][0]
self.assertEqual(result, expected)

# When
rules_generator = RulesGenerator(
pool, scenario.request, installed_map=installed_map, strict=True)

# Then
with self.assertRaises(MissingConflicts):
list(rules_generator.iter_rules())

def test_missing_indirect_conflicts_package(self):
# Given
yaml = u"""
packages:
- quark 1.0.0-1
- gluon 1.0.0-1; conflicts (gdata ^= 1.0.0)
- atom 1.0.0-1; depends (gluon);

request:
- operation: "install"
requirement: "atom"
"""
scenario = Scenario.from_yaml(io.StringIO(yaml))
expected = ("No candidates found for requirement 'gdata ^= 1.0.0',"
" needed for conflict with 'gluon 1.0.0-1' from 'remote'")

# When
repos = list(scenario.remote_repositories)
repos.append(scenario.installed_repository)
pool = Pool(repos)
installed_map = {
pool.package_id(p): p for p in scenario.installed_repository}
rules_generator = RulesGenerator(
pool, scenario.request, installed_map=installed_map)
with mock.patch('simplesat.rules_generator.logger') as mock_logger:
list(rules_generator.iter_rules())

# Then
result = mock_logger.info.call_args[0][0]
self.assertEqual(result, expected)

# When
rules_generator = RulesGenerator(
pool, scenario.request, installed_map=installed_map, strict=True)
with mock.patch('simplesat.rules_generator.logger') as mock_logger:
list(rules_generator.iter_rules())

# Then
result = mock_logger.warning.call_args[0][0]
self.assertEqual(result, expected)