Commit 04e1f604 by Sébastien Eustace

Add new dependency resolver

parent 42173665
__version__ = '0.9.0'
__version__ = '0.10.0a0'
......@@ -33,7 +33,7 @@ If you do not specify a version constraint, poetry will choose a suitable one ba
def handle(self):
from poetry.installation import Installer
from poetry.semver.version_parser import VersionParser
from poetry.semver.semver import parse_constraint
packages = self.argument('name')
is_dev = self.option('dev')
......@@ -76,9 +76,8 @@ If you do not specify a version constraint, poetry will choose a suitable one ba
requirements = self._format_requirements(requirements)
# validate requirements format
parser = VersionParser()
for constraint in requirements.values():
parser.parse_constraints(constraint)
parse_constraint(constraint)
for name, constraint in requirements.items():
constraint = {
......
......@@ -19,23 +19,22 @@ class DebugResolveCommand(Command):
def handle(self):
from poetry.packages import Dependency
from poetry.packages import ProjectPackage
from poetry.puzzle import Solver
from poetry.repositories.repository import Repository
from poetry.semver.version_parser import VersionParser
from poetry.semver.semver import parse_constraint
packages = self.argument('package')
if not packages:
package = self.poetry.package
dependencies = package.requires + package.dev_requires
else:
requirements = self._determine_requirements(packages)
requirements = self._format_requirements(requirements)
# validate requirements format
parser = VersionParser()
for constraint in requirements.values():
parser.parse_constraints(constraint)
parse_constraint(constraint)
dependencies = []
for name, constraint in requirements.items():
......@@ -43,15 +42,23 @@ class DebugResolveCommand(Command):
Dependency(name, constraint)
)
package = ProjectPackage(
self.poetry.package.name,
self.poetry.package.version
)
package.python_versions = self.poetry.package.python_versions
for dep in dependencies:
package.requires.append(dep)
solver = Solver(
self.poetry.package,
package,
self.poetry.pool,
Repository(),
Repository(),
self.output
)
ops = solver.solve(dependencies)
ops = solver.solve()
self.line('')
self.line('Resolution results:')
......
......@@ -253,14 +253,14 @@ lists all packages available."""
)
def get_update_status(self, latest, package):
from poetry.semver import statisfies
from poetry.semver.semver import parse_constraint
if latest.full_pretty_version == package.full_pretty_version:
return 'up-to-date'
constraint = '^' + package.pretty_version
constraint = parse_constraint('^' + package.pretty_version)
if latest.version and statisfies(latest.version, constraint):
if latest.version and constraint.allows(latest.version):
# It needs an immediate semver-compliant upgrade
return 'semver-safe-update'
......
......@@ -8,8 +8,9 @@ class PoetryStyle(CleoStyle):
super(PoetryStyle, self).__init__(i, o)
self.output.get_formatter().add_style('error', 'red')
self.output.get_formatter().add_style('warning', 'black', 'yellow')
self.output.get_formatter().add_style('warning', 'yellow')
self.output.get_formatter().add_style('question', 'blue')
self.output.get_formatter().add_style('comment', 'blue')
def writeln(self, messages,
type=OutputStyle.OUTPUT_NORMAL,
......
......@@ -15,8 +15,9 @@ from poetry.puzzle.operations.operation import Operation
from poetry.repositories import Pool
from poetry.repositories import Repository
from poetry.repositories.installed_repository import InstalledRepository
from poetry.semver.constraints import Constraint
from poetry.semver.version_parser import VersionParser
from poetry.semver.semver import parse_constraint
from poetry.semver.semver import Version
from poetry.utils.helpers import canonicalize_name
from .base_installer import BaseInstaller
from .pip_installer import PipInstaller
......@@ -112,7 +113,7 @@ class Installer:
return self
def whitelist(self, packages): # type: (dict) -> Installer
self._whitelist = packages
self._whitelist = [canonicalize_name(p) for p in packages]
return self
......@@ -135,33 +136,6 @@ class Installer:
)
self._io.writeln('<info>Updating dependencies</>')
fixed = []
# If the whitelist is enabled, packages not in it are fixed
# to the version specified in the lock
if self._whitelist:
# collect packages to fixate from root requirements
candidates = []
for package in locked_repository.packages:
candidates.append(package)
# fix them to the version in lock if they are not updateable
for candidate in candidates:
to_fix = True
for require in self._whitelist.keys():
if require == candidate.name:
to_fix = False
if to_fix:
dependency = Dependency(
candidate.name,
candidate.version,
optional=candidate.optional,
category=candidate.category,
allows_prereleases=candidate.is_prerelease()
)
fixed.append(dependency)
solver = Solver(
self._package,
self._pool,
......@@ -170,10 +144,7 @@ class Installer:
self._io
)
request = self._package.requires
request += self._package.dev_requires
ops = solver.solve(request, fixed=fixed)
ops = solver.solve(use_latest=self._whitelist)
else:
self._io.writeln('<info>Installing dependencies from lock file</>')
......@@ -451,18 +422,17 @@ class Installer:
if op.job_type == 'uninstall':
continue
parser = VersionParser()
python = '.'.join([str(i) for i in self._venv.version_info[:3]])
python = Version.parse('.'.join([str(i) for i in self._venv.version_info[:3]]))
if 'python' in package.requirements:
python_constraint = parser.parse_constraints(
python_constraint = parse_constraint(
package.requirements['python']
)
if not python_constraint.matches(Constraint('=', python)):
if not python_constraint.allows(python):
# Incompatible python versions
op.skip('Not needed for the current python version')
continue
if not package.python_constraint.matches(Constraint('=', python)):
if not package.python_constraint.allows(python):
op.skip('Not needed for the current python version')
continue
......
......@@ -7,9 +7,6 @@ import tempfile
from collections import defaultdict
from contextlib import contextmanager
from poetry.semver.constraints import Constraint
from poetry.semver.constraints import MultiConstraint
from poetry.semver.version_parser import VersionParser
from poetry.utils._compat import Path
from poetry.vcs import get_vcs
......@@ -156,35 +153,6 @@ class Builder(object):
'email': email
}
def get_classifers(self):
classifiers = []
# Automatically set python classifiers
parser = VersionParser()
if self._package.python_versions == '*':
python_constraint = parser.parse_constraints('~2.7 || ^3.4')
else:
python_constraint = self._package.python_constraint
for version in sorted(self.AVAILABLE_PYTHONS):
if python_constraint.matches(Constraint('=', version)):
classifiers.append(
'Programming Language :: Python :: {}'.format(version)
)
return classifiers
def convert_python_version(self):
constraint = self._package.python_constraint
if isinstance(constraint, MultiConstraint):
python_requires = ','.join(
[str(c).replace(' ', '') for c in constraint.constraints]
)
else:
python_requires = str(constraint).replace(' ', '')
return python_requires
@classmethod
@contextmanager
def temporary_directory(cls, *args, **kwargs):
......
......@@ -64,7 +64,7 @@ class SdistBuilder(Builder):
target_dir.mkdir(parents=True)
target = target_dir / '{}-{}.tar.gz'.format(
self._package.pretty_name, self._package.version
self._package.pretty_name, self._meta.version
)
gz = GzipFile(target.as_posix(), mode='wb')
tar = tarfile.TarFile(target.as_posix(), mode='w', fileobj=gz,
......@@ -72,7 +72,7 @@ class SdistBuilder(Builder):
try:
tar_dir = '{}-{}'.format(
self._package.pretty_name, self._package.version
self._package.pretty_name, self._meta.version
)
files_to_add = self.find_files_to_add(exclude_build=False)
......
......@@ -13,8 +13,7 @@ from base64 import urlsafe_b64encode
from io import StringIO
from poetry.__version__ import __version__
from poetry.semver.constraints import Constraint
from poetry.semver.constraints import MultiConstraint
from poetry.semver.semver import parse_constraint
from poetry.utils._compat import Path
from ..utils.helpers import normalize_file_permissions
......@@ -181,23 +180,18 @@ class WheelBuilder(Builder):
@property
def dist_info(self): # type: () -> str
return self.dist_info_name(self._package.name, self._package.version)
return self.dist_info_name(self._package.name, self._meta.version)
@property
def wheel_filename(self): # type: () -> str
return '{}-{}-{}.whl'.format(
re.sub("[^\w\d.]+", "_", self._package.pretty_name, flags=re.UNICODE),
re.sub("[^\w\d.]+", "_", self._package.version, flags=re.UNICODE),
re.sub("[^\w\d.]+", "_", self._meta.version, flags=re.UNICODE),
self.tag
)
def supports_python2(self):
return self._package.python_constraint.matches(
MultiConstraint([
Constraint('>=', '2.0.0'),
Constraint('<', '3.0.0')
])
)
return self._package.python_constraint.allows_any(parse_constraint('>=2.0.0 <3.0.0'))
def dist_info_name(self, distribution, version): # type: (...) -> str
escaped_name = re.sub("[^\w\d.]+", "_", distribution, flags=re.UNICODE)
......
from poetry.utils.helpers import canonicalize_name
from poetry.utils.helpers import normalize_version
from poetry.version.helpers import format_python_constraint
......@@ -42,7 +43,7 @@ class Metadata:
meta = cls()
meta.name = canonicalize_name(package.name)
meta.version = package.version
meta.version = normalize_version(package.version.text)
meta.summary = package.description
if package.readme:
with package.readme.open() as f:
......
......@@ -8,6 +8,7 @@ from .directory_dependency import DirectoryDependency
from .file_dependency import FileDependency
from .locker import Locker
from .package import Package
from .project_package import ProjectPackage
from .utils.link import Link
from .utils.utils import convert_markers
from .utils.utils import group_markers
......
import poetry.packages
from poetry.semver.constraints import Constraint
from poetry.semver.constraints import EmptyConstraint
from poetry.semver.constraints import MultiConstraint
from poetry.semver.constraints.base_constraint import BaseConstraint
from poetry.semver.version_parser import VersionParser
from poetry.semver.semver import parse_constraint
from poetry.semver.semver import Version
from poetry.semver.semver import VersionConstraint
from poetry.semver.semver import VersionUnion
from poetry.utils.helpers import canonicalize_name
from .constraints.generic_constraint import GenericConstraint
......@@ -21,29 +22,30 @@ class Dependency(object):
):
self._name = canonicalize_name(name)
self._pretty_name = name
self._parser = VersionParser()
try:
if not isinstance(constraint, BaseConstraint):
self._constraint = self._parser.parse_constraints(constraint)
if not isinstance(constraint, VersionConstraint):
self._constraint = parse_constraint(constraint)
else:
self._constraint = constraint
except ValueError:
self._constraint = self._parser.parse_constraints('*')
self._constraint = parse_constraint('*')
self._pretty_constraint = constraint
self._pretty_constraint = str(constraint)
self._optional = optional
self._category = category
self._allows_prereleases = allows_prereleases
self._python_versions = '*'
self._python_constraint = self._parser.parse_constraints('*')
self._python_constraint = parse_constraint('*')
self._platform = '*'
self._platform_constraint = EmptyConstraint()
self._extras = []
self._in_extras = []
self.is_root = False
@property
def name(self):
return self._name
......@@ -71,7 +73,7 @@ class Dependency(object):
@python_versions.setter
def python_versions(self, value):
self._python_versions = value
self._python_constraint = self._parser.parse_constraints(value)
self._python_constraint = parse_constraint(value)
@property
def python_constraint(self):
......@@ -119,7 +121,7 @@ class Dependency(object):
"""
return (
self._name == package.name
and self._constraint.matches(Constraint('=', package.version))
and self._constraint.allows(package.version)
and (not package.is_prerelease() or self.allows_prereleases())
)
......@@ -129,11 +131,13 @@ class Dependency(object):
if self.extras:
requirement += '[{}]'.format(','.join(self.extras))
if isinstance(self.constraint, MultiConstraint):
if isinstance(self.constraint, VersionUnion):
requirement += ' ({})'.format(','.join(
[str(c).replace(' ', '') for c in self.constraint.constraints]
[str(c).replace(' ', '') for c in self.constraint.ranges]
))
elif str(self.constraint) != '*':
elif isinstance(self.constraint, Version):
requirement += ' (=={})'.format(self.constraint.text)
elif not self.constraint.is_any():
requirement += ' ({})'.format(str(self.constraint).replace(' ', ''))
# Markers
......@@ -185,10 +189,57 @@ class Dependency(object):
parts = [part[1] for part in parts]
marker = glue.join(parts)
else:
elif isinstance(constraint, GenericConstraint):
marker = '{} {} "{}"'.format(
name, constraint.string_operator, constraint.version
)
elif isinstance(constraint, VersionUnion):
parts = []
for c in constraint.ranges:
parts.append(self._create_nested_marker(name, c))
glue = ' or '
parts = [
'({})'.format(part)
for part in parts
]
marker = glue.join(parts)
elif isinstance(constraint, Version):
marker = '{} == "{}"'.format(
name, constraint.text
)
else:
if constraint.min is not None:
op = '>='
if not constraint.include_min:
op = '>'
version = constraint.min.text
if constraint.max is not None:
text = '{} {} "{}"'.format(name, op, version)
op = '<='
if not constraint.include_max:
op = '<'
version = constraint.max
text += ' and {} {} "{}"'.format(name, op, version)
return text
elif constraint.max is not None:
op = '<='
if not constraint.include_max:
op = '<'
version = constraint.max
else:
return ''
marker = '{} {} "{}"'.format(
name, op, version
)
return marker
......@@ -204,6 +255,27 @@ class Dependency(object):
"""
self._optional = True
def with_constraint(self, constraint):
new = Dependency(
self.pretty_name,
constraint,
optional=self.is_optional(),
category=self.category,
allows_prereleases=self.allows_prereleases()
)
new.is_root = self.is_root
new.python_versions = self.python_versions
new.platform = self.platform
for extra in self.extras:
new.extras.append(extra)
for in_extra in self.in_extras:
new.in_extras.append(in_extra)
return new
def __eq__(self, other):
if not isinstance(other, Dependency):
return NotImplemented
......@@ -214,6 +286,9 @@ class Dependency(object):
return hash((self._name, self._pretty_constraint))
def __str__(self):
if self.is_root:
return self._pretty_name
return '{} ({})'.format(
self._pretty_name, self._pretty_constraint
)
......
......@@ -8,6 +8,8 @@ from poetry.semver.constraints import Constraint
from poetry.semver.constraints import EmptyConstraint
from poetry.semver.helpers import parse_stability
from poetry.semver.version_parser import VersionParser
from poetry.semver.semver import Version
from poetry.semver.semver import parse_constraint
from poetry.spdx import license_by_id
from poetry.spdx import License
from poetry.utils._compat import Path
......@@ -43,20 +45,6 @@ class Package(object):
}
}
STABILITY_STABLE = 0
STABILITY_RC = 5
STABILITY_BETA = 10
STABILITY_ALPHA = 15
STABILITY_DEV = 20
stabilities = {
'stable': STABILITY_STABLE,
'rc': STABILITY_RC,
'beta': STABILITY_BETA,
'alpha': STABILITY_ALPHA,
'dev': STABILITY_DEV,
}
def __init__(self, name, version, pretty_version=None):
"""
Creates a new in memory package.
......@@ -64,14 +52,15 @@ class Package(object):
self._pretty_name = name
self._name = canonicalize_name(name)
self._version = str(parse_version(version))
self._pretty_version = pretty_version or version
if not isinstance(version, Version):
self._version = Version.parse(version)
self._pretty_version = pretty_version or version
else:
self._version = version
self._pretty_version = pretty_version or self._version.text
self.description = ''
self._stability = parse_stability(version)
self._dev = self._stability == 'dev'
self._authors = []
self.homepage = None
......@@ -89,8 +78,6 @@ class Package(object):
self.extras = {}
self.requires_extras = []
self._parser = VersionParser()
self.category = 'main'
self.hashes = []
self.optional = False
......@@ -105,7 +92,7 @@ class Package(object):
self.classifiers = []
self._python_versions = '*'
self._python_constraint = self._parser.parse_constraints('*')
self._python_constraint = parse_constraint('*')
self._platform = '*'
self._platform_constraint = EmptyConstraint()
......@@ -129,7 +116,10 @@ class Package(object):
@property
def unique_name(self):
return self.name + '-' + self._version
if self.is_root():
return self._name
return self.name + '-' + self._version.text
@property
def pretty_string(self):
......@@ -137,7 +127,7 @@ class Package(object):
@property
def full_pretty_version(self):
if not self._dev and self.source_type not in ['hg', 'git']:
if not self.is_prerelease() and self.source_type not in ['hg', 'git']:
return self._pretty_version
# if source reference is a sha1 hash -- truncate
......@@ -159,6 +149,10 @@ class Package(object):
def author_email(self): # type: () -> str
return self._get_author()['email']
@property
def all_requires(self):
return self.requires + self.dev_requires
def _get_author(self): # type: () -> dict
if not self._authors:
return {
......@@ -183,7 +177,7 @@ class Package(object):
@python_versions.setter
def python_versions(self, value):
self._python_versions = value
self._python_constraint = self._parser.parse_constraints(value)
self._python_constraint = parse_constraint(value)
@property
def python_constraint(self):
......@@ -220,19 +214,18 @@ class Package(object):
classifiers = copy.copy(self.classifiers)
# Automatically set python classifiers
parser = VersionParser()
if self.python_versions == '*':
python_constraint = parser.parse_constraints('~2.7 || ^3.4')
python_constraint = parse_constraint('~2.7 || ^3.4')
else:
python_constraint = self.python_constraint
for version in sorted(self.AVAILABLE_PYTHONS):
if len(version) == 1:
constraint = parser.parse_constraints(version + '.*')
constraint = parse_constraint(version + '.*')
else:
constraint = Constraint('=', version)
constraint = Version.parse(version)
if python_constraint.matches(constraint):
if python_constraint.allows_any(constraint):
classifiers.append(
'Programming Language :: Python :: {}'.format(version)
)
......@@ -245,11 +238,11 @@ class Package(object):
return sorted(classifiers)
def is_dev(self):
return self._dev
def is_prerelease(self):
return self._stability != 'stable'
return self._version.is_prerelease()
def is_root(self):
return False
def add_dependency(self,
name, # type: str
......@@ -335,6 +328,9 @@ class Package(object):
return dependency
def to_dependency(self):
return Dependency(self.name, self._version)
def __hash__(self):
return hash((self._name, self._version))
......
from .package import Package
class ProjectPackage(Package):
def is_root(self):
return True
def to_dependency(self):
dependency = super(ProjectPackage, self).to_dependency()
dependency.is_root = True
return dependency
......@@ -11,6 +11,7 @@ from .exceptions import InvalidProjectFile
from .packages import Dependency
from .packages import Locker
from .packages import Package
from .packages import ProjectPackage
from .repositories import Pool
from .repositories.pypi_repository import PyPiRepository
from .spdx import license_by_id
......@@ -95,7 +96,7 @@ class Poetry:
# Load package
name = local_config['name']
version = local_config['version']
package = Package(name, version, version)
package = ProjectPackage(name, version, version)
package.root_dir = poetry_file.parent
for author in local_config['authors']:
......
from .version_solver import VersionSolver
def resolve_version(root, provider, locked=None, use_latest=None):
solver = VersionSolver(root, provider, locked=locked, use_latest=use_latest)
with provider.progress():
return solver.solve()
from typing import Any
from .incompatibility import Incompatibility
from .term import Term
class Assignment(Term):
"""
A term in a PartialSolution that tracks some additional metadata.
"""
def __init__(self, dependency, is_positive,
decision_level, index,
cause=None):
super(Assignment, self).__init__(dependency, is_positive)
self._decision_level = decision_level
self._index = index
self._cause = cause
@property
def decision_level(self): # type: () -> int
return self._decision_level
@property
def index(self): # type: () -> int
return self._index
@property
def cause(self): # type: () -> Incompatibility
return self._cause
@classmethod
def decision(cls, package, decision_level, index
): # type: (Any, int, int) -> Assignment
return cls(package.to_dependency(), True, decision_level, index)
@classmethod
def derivation(cls, dependency, is_positive, cause, decision_level, index
): # type: (Any, bool, Incompatibility, int, int) -> Assignment
return cls(dependency, is_positive, decision_level, index, cause)
def is_decision(self): # type: () -> bool
return self._cause is None
from typing import Dict
from typing import List
from typing import Tuple
from .incompatibility import Incompatibility
from .incompatibility_cause import ConflictCause
class SolveFailure(Exception):
def __init__(self, incompatibility): # type: (Incompatibility) -> None
assert incompatibility.terms[0].dependency.is_root
self._incompatibility = incompatibility
@property
def message(self):
return str(self)
def __str__(self):
return _Writer(self._incompatibility).write()
class _Writer:
def __init__(self, root): # type: (Incompatibility) -> None
self._root = root
self._derivations = {} # type: Dict[Incompatibility, int]
self._lines = [] # type: List[Tuple[str, int]]
self._line_numbers = {} # type: Dict[Incompatibility, int]
self._count_derivations(self._root)
def write(self):
buffer = []
if isinstance(self._root.cause, ConflictCause):
self._visit(self._root, {})
else:
self._write(self._root, 'Because {}, version solving failed.'.format(self._root))
padding = 0 if not self._line_numbers else len('({})'.format(list(self._line_numbers.values())[-1]))
last_was_empty = False
for line in self._lines:
message = line[0]
if not message:
if not last_was_empty:
buffer.append('')
last_was_empty = True
continue
last_was_empty = False
number = line[-1]
if number is not None:
message = '({})'.format(number).ljust(padding) + message
else:
message = ' ' * padding + message
buffer.append(message)
return '\n'.join(buffer)
def _write(self, incompatibility, message, numbered=False
): # type: (Incompatibility, str, bool) -> None
if numbered:
number = len(self._line_numbers) + 1
self._line_numbers[incompatibility] = number
self._lines.append((message, number))
else:
self._lines.append((message, None))
def _visit(self, incompatibility, details_for_incompatibility, conclusion=False
): # type: (Incompatibility, Dict, bool) -> None
numbered = conclusion or self._derivations[incompatibility] > 1
conjunction = conclusion or ('So,' if incompatibility == self._root else 'And')
incompatibility_string = str(incompatibility)
cause = incompatibility.cause # type: ConflictCause
details_for_cause = {}
if isinstance(cause.conflict.cause, ConflictCause) and isinstance(cause.other.cause, ConflictCause):
conflict_line = self._line_numbers.get(cause.conflict)
other_line = self._line_numbers.get(cause.other)
if conflict_line is not None and other_line is not None:
self._write(
incompatibility,
'Because {}, {}.'.format(
cause.conflict.and_to_string(
cause.other, details_for_cause,
conflict_line, other_line
),
incompatibility_string
),
numbered=numbered
)
elif conflict_line is not None or other_line is not None:
if conflict_line is not None:
with_line = cause.conflict
without_line = cause.other
line = conflict_line
else:
with_line = cause.other
without_line = cause.conflict
line = other_line
self._visit(without_line, details_for_cause)
self._write(
incompatibility,
'{} because {} ({}), {}.'.format(
conjunction, str(with_line), line, incompatibility_string
),
numbered=numbered
)
else:
single_line_conflict = self._is_single_line(cause.conflict.cause)
single_line_other = self._is_single_line(cause.other.cause)
if single_line_other or single_line_conflict:
first = cause.conflict if single_line_other else cause.other
second = cause.other if single_line_other else cause.conflict
self._visit(first, details_for_cause)
self._visit(second, details_for_cause)
self._write(
incompatibility,
'Thus, {}.'.format(incompatibility_string),
numbered=numbered
)
else:
self._visit(cause.conflict, {}, conclusion=True)
self._lines.append(('', None))
self._visit(cause.other, details_for_cause)
self._write(
incompatibility,
'{} because {} ({}), {}'.format(
conjunction,
str(cause.conflict),
self._line_numbers[cause.conflict],
incompatibility_string
),
numbered=numbered
)
elif isinstance(cause.conflict.cause, ConflictCause) or isinstance(cause.other.cause, ConflictCause):
derived = cause.conflict if isinstance(cause.conflict.cause, ConflictCause) else cause.other
ext = cause.other if isinstance(cause.conflict.cause, ConflictCause) else cause.conflict
derived_line = self._line_numbers.get(derived)
if derived_line is not None:
self._write(
incompatibility,
'Because {}, {}.'.format(
ext.and_to_string(derived, details_for_cause, None, derived_line),
incompatibility_string
),
numbered=numbered
)
elif self._is_collapsible(derived):
derived_cause = derived.cause # type: ConflictCause
if isinstance(derived_cause.conflict.cause, ConflictCause):
collapsed_derived = derived_cause.conflict
else:
collapsed_derived = derived_cause.other
if isinstance(derived_cause.conflict.cause, ConflictCause):
collapsed_ext = derived_cause.other
else:
collapsed_ext = derived_cause.conflict
details_for_cause = {}
self._visit(collapsed_derived, details_for_cause)
self._write(
incompatibility,
'{} because {}, {}.'.format(
conjunction,
collapsed_ext.and_to_string(ext, details_for_cause, None, None),
incompatibility_string
),
numbered=numbered
)
else:
self._visit(derived, details_for_cause)
self._write(
incompatibility,
'{} because {}, {}.'.format(
conjunction,
str(ext),
incompatibility_string
),
numbered=numbered
)
else:
self._write(
incompatibility,
'Because {}, {}.'.format(
cause.conflict.and_to_string(cause.other, details_for_cause, None, None),
incompatibility_string
),
numbered=numbered
)
def _is_collapsible(self, incompatibility): # type: (Incompatibility) -> bool
if self._derivations[incompatibility] > 1:
return False
cause = incompatibility.cause # type: ConflictCause
if isinstance(cause.conflict.cause, ConflictCause) and isinstance(cause.other.cause, ConflictCause):
return False
if not isinstance(cause.conflict.cause, ConflictCause) and not isinstance(cause.other.cause, ConflictCause):
return False
complex = cause.conflict if isinstance(cause.conflict.cause, ConflictCause) else cause.other
return complex not in self._line_numbers
def _is_single_line(self, cause): # type: (ConflictCause) -> bool
return not isinstance(cause.conflict.cause, ConflictCause) and not isinstance(cause.other.cause, ConflictCause)
def _count_derivations(self, incompatibility): # type: (Incompatibility) -> None
if incompatibility in self._derivations:
self._derivations[incompatibility] += 1
else:
self._derivations[incompatibility] = 1
cause = incompatibility.cause
if isinstance(cause, ConflictCause):
self._count_derivations(cause.conflict)
self._count_derivations(cause.other)
class IncompatibilityCause(Exception):
"""
The reason and Incompatibility's terms are incompatible.
"""
class RootCause(IncompatibilityCause):
pass
class NoVersionsCause(IncompatibilityCause):
pass
class DependencyCause(IncompatibilityCause):
pass
class ConflictCause(IncompatibilityCause):
"""
The incompatibility was derived from two existing incompatibilities
during conflict resolution.
"""
def __init__(self, conflict, other):
self._conflict = conflict
self._other = other
@property
def conflict(self):
return self._conflict
@property
def other(self):
return self._other
def __str__(self):
return str(self._conflict)
class PythonCause(IncompatibilityCause):
"""
The incompatibility represents a package's python constraint
(Python versions) being incompatible
with the current python version.
"""
def __init__(self, python_version):
self._python_version = python_version
@property
def python_version(self):
return self._python_version
class PlatformCause(IncompatibilityCause):
"""
The incompatibility represents a package's platform constraint
(OS most likely) being incompatible with the current platform.
"""
def __init__(self, platform):
self._platform = platform
@property
def platform(self):
return self._platform
class PackageNotFoundCause(IncompatibilityCause):
"""
The incompatibility represents a package that couldn't be found by its
source.
"""
def __init__(self, error):
self._error = error
@property
def error(self):
return self._error
from typing import Any
from typing import Dict
from typing import List
from .assignment import Assignment
from .incompatibility import Incompatibility
from .set_relation import SetRelation
from .term import Term
class PartialSolution:
"""
# A list of Assignments that represent the solver's current best guess about
# what's true for the eventual set of package versions that will comprise the
# total solution.
#
# See https://github.com/dart-lang/pub/tree/master/doc/solver.md#partial-solution.
"""
def __init__(self):
# The assignments that have been made so far, in the order they were
# assigned.
self._assignments = [] # type: List[Assignment]
# The decisions made for each package.
self._decisions = {} # type: Dict[str, Any]
# The intersection of all positive Assignments for each package, minus any
# negative Assignments that refer to that package.
#
# This is derived from self._assignments.
self._positive = {} # type: Dict[str, Term]
# The union of all negative [Assignment]s for each package.
#
# If a package has any positive [Assignment]s, it doesn't appear in this
# map.
#
# This is derived from self._assignments.
self._negative = {} # type: Dict[str, Dict[str, Term]]
self._attempted_solutions = 1
self._backtracking = False
@property
def decisions(self): # type: () -> Any
return list(self._decisions.values())
@property
def decision_level(self):
return len(self._decisions)
@property
def attempted_solutions(self):
return self._attempted_solutions
@property
def unsatisfied(self):
return [
term.dependency
for term in self._positive.values()
if term.dependency.name not in self._decisions
]
def decide(self, package): # type: (Any) -> None
"""
Adds an assignment of package as a decision
and increments the decision level.
"""
# When we make a new decision after backtracking, count an additional
# attempted solution. If we backtrack multiple times in a row, though, we
# only want to count one, since we haven't actually started attempting a
# new solution.
if self._backtracking:
self._attempted_solutions += 1
self._backtracking = False
self._decisions[package.name] = package
self._assign(Assignment.decision(package, self.decision_level, len(self._assignments)))
def derive(self, dependency, is_positive, cause
): # type: (Any, bool, Incompatibility) -> None
"""
Adds an assignment of package as a derivation.
"""
self._assign(
Assignment.derivation(
dependency, is_positive, cause,
self.decision_level, len(self._assignments)
)
)
def _assign(self, assignment): # type: (Assignment) -> None
"""
Adds an Assignment to _assignments and _positive or _negative.
"""
self._assignments.append(assignment)
self._register(assignment)
def backtrack(self, decision_level): # type: (int) -> None
self._backtracking = True
packages = set()
while self._assignments[-1].decision_level > decision_level:
removed = self._assignments.pop(-1)
packages.add(removed.dependency.name)
if removed.is_decision():
del self._decisions[removed.dependency.name]
# Re-compute _positive and _negative for the packages that were removed.
for package in packages:
if package in self._positive:
del self._positive[package]
if package in self._negative:
del self._negative[package]
for assignment in self._assignments:
if assignment.dependency.name in packages:
self._register(assignment)
def _register(self, assignment): # type: (Assignment) -> None
"""
Registers an Assignment in _positive or _negative.
"""
name = assignment.dependency.name
old_positive = self._positive.get(name)
if old_positive is not None:
self._positive[name] = old_positive.intersect(assignment)
return
ref = assignment.dependency.name
negative_by_ref = self._negative.get(name)
old_negative = None if negative_by_ref is None else negative_by_ref.get(ref)
if old_negative is None:
term = assignment
else:
term = assignment.intersect(old_negative)
if term.is_positive():
if name in self._negative:
del self._negative[name]
self._positive[name] = term
else:
if name not in self._negative:
self._negative[name] = {}
self._negative[name][ref] = term
def satisfier(self, term): # type: (Term) -> Assignment
"""
Returns the first Assignment in this solution such that the sublist of
assignments up to and including that entry collectively satisfies term.
"""
assigned_term = None # type: Term
for assignment in self._assignments:
if assignment.dependency.name != term.dependency.name:
continue
if (
not assignment.dependency.is_root
and not assignment.dependency.name == term.dependency.name
):
if not assignment.is_positive():
continue
assert not term.is_positive()
return assignment
if assigned_term is None:
assigned_term = assignment
else:
assigned_term = assigned_term.intersect(assignment)
# As soon as we have enough assignments to satisfy term, return them.
if assigned_term.satisfies(term):
return assignment
raise RuntimeError('[BUG] {} is not satisfied.'.format(term))
def satisfies(self, term): # type: (Term) -> bool
return self.relation(term) == SetRelation.SUBSET
def relation(self, term): # type: (Term) -> int
positive = self._positive.get(term.dependency.name)
if positive is not None:
return positive.relation(term)
by_ref = self._negative.get(term.dependency.name)
if by_ref is None:
return SetRelation.OVERLAPPING
negative = by_ref[term.dependency.name]
if negative is None:
return SetRelation.OVERLAPPING
return negative.relation(term)
class SolverResult:
def __init__(self, root, packages, attempted_solutions):
self._root = root
self._packages = packages
self._attempted_solutions = attempted_solutions
@property
def packages(self):
return self._packages
@property
def attempted_solutions(self):
return self._attempted_solutions
class SetRelation:
"""
An enum of possible relationships between two sets.
"""
SUBSET = 'subset'
DISJOINT = 'disjoint'
OVERLAPPING = 'overlapping'
from poetry.packages import Dependency
from .set_relation import SetRelation
class Term:
"""
A statement about a package which is true or false for a given selection of
package versions.
"""
def __init__(self, dependency, is_positive):
self._dependency = dependency
self._positive = is_positive
@property
def inverse(self): # type: () -> Term
return Term(self._dependency, not self.is_positive())
@property
def dependency(self):
return self._dependency
@property
def constraint(self):
return self._dependency.constraint
def is_positive(self): # type: () -> bool
return self._positive
def satisfies(self, other): # type: (Term) -> bool
"""
Returns whether this term satisfies another.
"""
return (
self.dependency.name == other.dependency.name
and self.relation(other) == SetRelation.SUBSET
)
def relation(self, other): # type: (Term) -> int
"""
Returns the relationship between the package versions
allowed by this term and another.
"""
if self.dependency.name != other.dependency.name:
raise ValueError('{} should refer to {}'.format(other, self.dependency.name))
other_constraint = other.constraint
if other.is_positive():
if self.is_positive():
if not self._compatible_dependency(other.dependency):
return SetRelation.DISJOINT
# foo ^1.5.0 is a subset of foo ^1.0.0
if other_constraint.allows_all(self.constraint):
return SetRelation.SUBSET
# foo ^2.0.0 is disjoint with foo ^1.0.0
if not self.constraint.allows_any(other_constraint):
return SetRelation.DISJOINT
return SetRelation.OVERLAPPING
else:
if not self._compatible_dependency(other.dependency):
return SetRelation.OVERLAPPING
# not foo ^1.0.0 is disjoint with foo ^1.5.0
if self.constraint.allows_all(other_constraint):
return SetRelation.DISJOINT
# not foo ^1.5.0 overlaps foo ^1.0.0
# not foo ^2.0.0 is a superset of foo ^1.5.0
return SetRelation.OVERLAPPING
else:
if self.is_positive():
if not self._compatible_dependency(other.dependency):
return SetRelation.SUBSET
# foo ^2.0.0 is a subset of not foo ^1.0.0
if not other_constraint.allows_any(self.constraint):
return SetRelation.SUBSET
# foo ^1.5.0 is disjoint with not foo ^1.0.0
if other_constraint.allows_all(self.constraint):
return SetRelation.DISJOINT
# foo ^1.0.0 overlaps not foo ^1.5.0
return SetRelation.OVERLAPPING
else:
if not self._compatible_dependency(other.dependency):
return SetRelation.OVERLAPPING
# not foo ^1.0.0 is a subset of not foo ^1.5.0
if self.constraint.allows_all(other_constraint):
return SetRelation.SUBSET
# not foo ^2.0.0 overlaps not foo ^1.0.0
# not foo ^1.5.0 is a superset of not foo ^1.0.0
return SetRelation.OVERLAPPING
def intersect(self, other): # type: (Term) -> Term
"""
Returns a Term that represents the packages
allowed by both this term and another
"""
if self.dependency.name != other.dependency.name:
raise ValueError('{} should refer to {}'.format(other, self.dependency.name))
if self._compatible_dependency(other.dependency):
if self.is_positive() != other.is_positive():
# foo ^1.0.0 ∩ not foo ^1.5.0 → foo >=1.0.0 <1.5.0
positive = self if self.is_positive() else other
negative = other if self.is_positive() else self
return self._non_empty_term(
positive.constraint.difference(negative.constraint),
True
)
elif self.is_positive():
# foo ^1.0.0 ∩ foo >=1.5.0 <3.0.0 → foo ^1.5.0
return self._non_empty_term(
self.constraint.intersect(other.constraint),
True
)
else:
# not foo ^1.0.0 ∩ not foo >=1.5.0 <3.0.0 → not foo >=1.0.0 <3.0.0
return self._non_empty_term(
self.constraint.union(other.constraint),
False
)
elif self.is_positive() != other.is_positive():
return self if self.is_positive() else other
else:
return
def difference(self, other): # type: (Term) -> Term
"""
Returns a Term that represents packages
allowed by this term and not by the other
"""
return self.intersect(other.inverse)
def _compatible_dependency(self, other):
return (
self.dependency.is_root
or other.is_root
or (
other.name == self.dependency.name
)
)
def _non_empty_term(self, constraint, is_positive):
if constraint.is_empty():
return
return Term(Dependency(self.dependency.name, constraint), is_positive)
def __str__(self):
return '{}{}'.format(
'not ' if not self.is_positive() else '',
self._dependency
)
def __repr__(self):
return '<Term {}>'.format(str(self))
import os
import pkginfo
import shutil
import time
from cleo import ProgressIndicator
from contextlib import contextmanager
from functools import cmp_to_key
from tempfile import mkdtemp
from typing import Dict
......@@ -20,9 +23,13 @@ from poetry.packages import Package
from poetry.packages import VCSDependency
from poetry.packages import dependency_from_pep_508
from poetry.repositories import Pool
from poetry.pub.incompatibility import Incompatibility
from poetry.pub.incompatibility_cause import DependencyCause
from poetry.pub.incompatibility_cause import PlatformCause
from poetry.pub.incompatibility_cause import PythonCause
from poetry.pub.term import Term
from poetry.semver import less_than
from poetry.repositories import Pool
from poetry.utils._compat import Path
from poetry.utils.helpers import parse_requires
......@@ -34,6 +41,26 @@ from poetry.vcs.git import Git
from .dependencies import Dependencies
class Indicator(ProgressIndicator):
def __init__(self, output):
super(Indicator, self).__init__(output)
self.format = '%message% <fg=black;options=bold>(%elapsed:2s%)</>'
@contextmanager
def auto(self):
message = '<info>Resolving dependencies</info>...'
with super(Indicator, self).auto(message, message):
yield
def _formatter_elapsed(self):
elapsed = time.time() - self.start_time
return '{:.1f}s'.format(elapsed)
class Provider(SpecificationProvider, UI):
UNSAFE_PACKAGES = {'setuptools', 'distribute', 'pip'}
......@@ -49,7 +76,6 @@ class Provider(SpecificationProvider, UI):
self._python_constraint = package.python_constraint
self._base_dg = DependencyGraph()
self._search_for = {}
self._constraints = {}
super(Provider, self).__init__(debug=self._io.is_debug())
......@@ -78,6 +104,9 @@ class Provider(SpecificationProvider, UI):
The specifications in the returned list will be considered in reverse
order, so the latest version ought to be last.
"""
if dependency.is_root:
return [self._package]
if dependency in self._search_for:
return self._search_for[dependency]
......@@ -90,17 +119,6 @@ class Provider(SpecificationProvider, UI):
else:
constraint = dependency.constraint
# If we have already seen this dependency
# we take the most restrictive constraint
if dependency.name in self._constraints:
current_constraint = self._constraints[dependency.name]
if str(dependency.constraint) == '*':
# The new constraint accepts anything
# so we take the previous one
constraint = current_constraint
self._constraints[dependency.name] = constraint
packages = self._pool.find_packages(
dependency.name,
constraint,
......@@ -112,7 +130,7 @@ class Provider(SpecificationProvider, UI):
key=cmp_to_key(
lambda x, y:
0 if x.version == y.version
else -1 * int(less_than(x.version, y.version) or -1)
else int(x.version < y.version or -1)
)
)
......@@ -179,11 +197,12 @@ class Provider(SpecificationProvider, UI):
# to figure the information we need
# We need to place ourselves in the proper
# folder for it to work
venv = Venv.create(self._io)
current_dir = os.getcwd()
os.chdir(tmp_dir.as_posix())
try:
venv = Venv.create(self._io)
venv.run(
'python', 'setup.py', 'egg_info'
)
......@@ -251,6 +270,48 @@ class Provider(SpecificationProvider, UI):
return [package]
def incompatibilities_for(self, package): # type: (Package) -> List[Incompatibility]
"""
Returns incompatibilities that encapsulate a given package's dependencies,
or that it can't be safely selected.
If multiple subsequent versions of this package have the same
dependencies, this will return incompatibilities that reflect that. It
won't return incompatibilities that have already been returned by a
previous call to _incompatibilities_for().
"""
# TODO: Check python versions
if package.source_type in ['git', 'file', 'directory']:
dependencies = package.requires
elif package.is_root():
dependencies = package.all_requires
else:
dependencies = self._dependencies_for(package)
if not self._package.python_constraint.allows_any(package.python_constraint):
return [
Incompatibility(
[Term(package.to_dependency(), True)],
PythonCause(package.python_versions)
)
]
if not self._package.platform_constraint.matches(package.platform_constraint):
return [
Incompatibility(
[Term(package.to_dependency(), True)],
PlatformCause(package.platform)
)
]
return [
Incompatibility([
Term(package.to_dependency(), True),
Term(dep, False)
], DependencyCause())
for dep in dependencies
]
def dependencies_for(self, package
): # type: (Package) -> Union[List[Dependency], Dependencies]
if package.source_type in ['git', 'file', 'directory']:
......@@ -265,7 +326,7 @@ class Provider(SpecificationProvider, UI):
def _dependencies_for(self, package): # type: (Package) -> List[Dependency]
complete_package = self._pool.package(
package.name, package.version,
package.name, package.version.text,
extras=package.requires_extras
)
......@@ -279,7 +340,7 @@ class Provider(SpecificationProvider, UI):
return [
r for r in package.requires
if not r.is_optional()
and self._package.python_constraint.matches(r.python_constraint)
and self._package.python_constraint.allows_any(r.python_constraint)
and self._package.platform_constraint.matches(package.platform_constraint)
and r.name not in self.UNSAFE_PACKAGES
]
......@@ -341,7 +402,7 @@ class Provider(SpecificationProvider, UI):
def after_resolution(self):
self._io.new_line()
def debug(self, message, depth):
def debug(self, message, depth=0):
if self.is_debugging():
debug_info = str(message)
debug_info = '\n'.join([
......@@ -350,3 +411,14 @@ class Provider(SpecificationProvider, UI):
]) + '\n'
self.output.write(debug_info)
@contextmanager
def progress(self):
if not self._io.is_decorated() or self.is_debugging():
self.output.writeln('Resolving dependencies...')
yield
else:
indicator = Indicator(self._io)
with indicator.auto():
yield
from typing import List
from poetry.mixology import Resolver
from poetry.mixology.dependency_graph import DependencyGraph
from poetry.mixology.exceptions import ResolverError
from poetry.pub import resolve_version
from poetry.pub.failure import SolveFailure
from poetry.packages.constraints.generic_constraint import GenericConstraint
from poetry.semver.version_parser import VersionParser
from poetry.semver.semver import parse_constraint
from .exceptions import SolverProblemError
from .operations import Install
from .operations import Uninstall
from .operations import Update
......@@ -25,31 +25,27 @@ class Solver:
self._locked = locked
self._io = io
def solve(self, requested, fixed=None): # type: (...) -> List[Operation]
def solve(self, use_latest=None): # type: (...) -> List[Operation]
provider = Provider(self._package, self._pool, self._io)
resolver = Resolver(provider, provider)
base = None
if fixed is not None:
base = DependencyGraph()
for fixed_req in fixed:
base.add_vertex(fixed_req.name, fixed_req, True)
locked = {}
for package in self._locked.packages:
locked[package.name] = package
try:
graph = resolver.resolve(requested, base=base)
except ResolverError as e:
result = resolve_version(self._package, provider, locked=locked, use_latest=use_latest)
except SolveFailure as e:
raise SolverProblemError(e)
packages = [v.payload for v in graph.vertices.values()]
packages = result.packages
requested = self._package.all_requires
# Setting info
for vertex in graph.vertices.values():
category, optional, python, platform = self._get_tags_for_vertex(
vertex, requested
for package in packages:
category, optional, python, platform = self._get_tags_for_package(
package, packages, requested
)
vertex.payload.category = category
vertex.payload.optional = optional
package.category = category
package.optional = optional
# If requirements are empty, drop them
requirements = {}
......@@ -59,7 +55,7 @@ class Solver:
if platform is not None and platform != '*':
requirements['platform'] = platform
vertex.payload.requirements = requirements
package.requirements = requirements
operations = []
for package in packages:
......@@ -101,7 +97,7 @@ class Solver:
operations.append(op)
requested_names = [r.name for r in requested]
requested_names = [r.name for r in self._package.all_requires]
return sorted(
operations,
......@@ -111,45 +107,43 @@ class Solver:
)
)
def _get_tags_for_vertex(self, vertex, requested):
def _get_tags_for_package(self, package, packages, requested):
category = 'dev'
optional = True
python_version = None
platform = None
if not vertex.incoming_edges:
# Original dependency
for req in requested:
if vertex.payload.name == req.name:
category = req.category
optional = req.is_optional()
root = None
for dep in requested:
if dep.name == package.name:
root = dep
python_version = str(req.python_constraint)
origins = []
for pkg in packages:
for dep in pkg.all_requires:
if dep.name == package.name:
origins.append((pkg, dep))
platform = str(req.platform_constraint)
if root is not None and not origins:
# Original dependency
category = root.category
optional = root.is_optional()
break
python_version = str(root.python_constraint)
platform = str(root.platform_constraint)
return category, optional, python_version, platform
parser = VersionParser()
python_versions = []
platforms = []
for edge in vertex.incoming_edges:
python_version = None
platform = None
for req in edge.origin.payload.requires:
if req.name == vertex.payload.name:
python_version = req.python_versions
platform = req.platform
break
for pkg, dep in origins:
python_version = dep.python_versions
platform = dep.platform
(top_category,
top_optional,
top_python_version,
top_platform) = self._get_tags_for_vertex(
edge.origin, requested
top_platform) = self._get_tags_for_package(
pkg, packages, requested
)
if top_category == 'main':
......@@ -160,10 +154,10 @@ class Solver:
# Take the most restrictive constraints
if top_python_version is not None:
if python_version is not None:
previous = parser.parse_constraints(python_version)
current = parser.parse_constraints(top_python_version)
previous = parse_constraint(python_version)
current = parse_constraint(top_python_version)
if top_python_version != '*' and previous.matches(current):
if previous.allows_all(current):
python_versions.append(top_python_version)
else:
python_versions.append(python_version)
......@@ -191,15 +185,15 @@ class Solver:
else:
# Find the least restrictive constraint
python_version = python_versions[0]
previous = parser.parse_constraints(python_version)
previous = parse_constraint(python_version)
for constraint in python_versions[1:]:
current = parser.parse_constraints(constraint)
current = parse_constraint(constraint)
if python_version == '*':
continue
elif constraint == '*':
python_version = constraint
elif current.matches(previous):
elif current.allows_all(previous):
python_version = constraint
if not platforms:
......
......@@ -30,8 +30,8 @@ from poetry.locations import CACHE_DIR
from poetry.packages import dependency_from_pep_508
from poetry.packages import Package
from poetry.semver.constraints import Constraint
from poetry.semver.constraints.base_constraint import BaseConstraint
from poetry.semver.version_parser import VersionParser
from poetry.semver.semver import parse_constraint
from poetry.semver.semver import VersionConstraint
from poetry.utils._compat import Path
from poetry.utils._compat import to_str
from poetry.utils.helpers import parse_requires
......@@ -86,9 +86,8 @@ class PyPiRepository(Repository):
"""
Find packages on the remote server.
"""
if constraint is not None and not isinstance(constraint, BaseConstraint):
version_parser = VersionParser()
constraint = version_parser.parse_constraints(constraint)
if constraint is not None and not isinstance(constraint, VersionConstraint):
constraint = parse_constraint(constraint)
info = self.get_package_info(name)
......@@ -112,7 +111,7 @@ class PyPiRepository(Repository):
if (
not constraint
or (constraint and constraint.matches(Constraint('=', version)))
or (constraint and constraint.allows(package.version))
):
if extras is not None:
package.requires_extras = extras
......
from poetry.semver.constraints import Constraint
from poetry.semver.constraints.base_constraint import BaseConstraint
from poetry.semver.version_parser import VersionParser
from poetry.semver.semver import parse_constraint
from poetry.semver.semver import VersionConstraint
from poetry.version import parse as parse_version
......@@ -20,10 +19,9 @@ class Repository(BaseRepository):
def package(self, name, version, extras=None):
name = name.lower()
version = str(parse_version(version))
for package in self.packages:
if name == package.name and package.version == version:
if name == package.name and package.version.text == version:
return package
def find_packages(self, name, constraint=None,
......@@ -37,15 +35,15 @@ class Repository(BaseRepository):
if constraint is None:
constraint = '*'
if not isinstance(constraint, BaseConstraint):
parser = VersionParser()
constraint = parser.parse_constraints(constraint)
if not isinstance(constraint, VersionConstraint):
constraint = parse_constraint(constraint)
for package in self.packages:
if name == package.name:
pkg_constraint = Constraint('==', package.version)
if package.is_prerelease() and not allow_prereleases:
continue
if constraint is None or constraint.matches(pkg_constraint):
if constraint is None or constraint.allows(package.version):
for dep in package.requires:
for extra in extras:
if extra not in package.extras:
......
......@@ -3,28 +3,10 @@ from functools import cmp_to_key
from .comparison import less_than
from .constraints import Constraint
from .helpers import normalize_version
from .version_parser import VersionParser
SORT_ASC = 1
SORT_DESC = -1
_parser = VersionParser()
def statisfies(version, constraints):
"""
Determine if given version satisfies given constraints.
:type version: str
:type constraints: str
:rtype: bool
"""
provider = Constraint('==', normalize_version(version))
constraints = _parser.parse_constraints(constraints)
return constraints.matches(provider)
def satisfied_by(versions, constraints):
"""
......
......@@ -2,3 +2,18 @@ class BaseConstraint(object):
def matches(self, provider):
raise NotImplementedError()
def allows_all(self, other):
raise NotImplementedError()
def allows_any(self, other):
raise NotImplementedError()
def difference(self, other):
raise NotImplementedError()
def intersect(self, other):
raise NotImplementedError()
def is_empty(self):
return False
......@@ -8,5 +8,20 @@ class EmptyConstraint(BaseConstraint):
def matches(self, _):
return True
def is_empty(self):
return True
def allows_all(self, other):
return True
def allows_any(self, other):
return True
def intersect(self, other):
return other
def difference(self, other):
return
def __str__(self):
return '*'
import re
from .empty_constraint import EmptyConstraint
from .patterns import BASIC_CONSTRAINT
from .patterns import CARET_CONSTRAINT
from .patterns import TILDE_CONSTRAINT
from .patterns import X_CONSTRAINT
from .version import Version
from .version_constraint import VersionConstraint
from .version_range import VersionRange
from .version_union import VersionUnion
def parse_constraint(constraints): # type: (str) -> VersionConstraint
if constraints == '*':
return VersionRange()
or_constraints = re.split('\s*\|\|?\s*', constraints.strip())
or_groups = []
for constraints in or_constraints:
and_constraints = re.split(
'(?<!^)(?<![=>< ,]) *(?<!-)[, ](?!-) *(?!,|$)',
constraints
)
constraint_objects = []
if len(and_constraints) > 1:
for constraint in and_constraints:
constraint_objects.append(parse_single_constraint(constraint))
else:
constraint_objects.append(parse_single_constraint(and_constraints[0]))
if len(constraint_objects) == 1:
constraint = constraint_objects[0]
else:
constraint = constraint_objects[0]
for next_constraint in constraint_objects[1:]:
constraint = constraint.intersect(next_constraint)
or_groups.append(constraint)
if len(or_groups) == 1:
return or_groups[0]
else:
return VersionUnion.of(*or_groups)
def parse_single_constraint(constraint): # type: (str) -> VersionConstraint
m = re.match('(?i)^v?[xX*](\.[xX*])*$', constraint)
if m:
return VersionRange()
# Tilde range
m = TILDE_CONSTRAINT.match(constraint)
if m:
version = Version.parse(m.group(1))
high = version.stable.next_minor
if len(m.group(1).split('.')) == 1:
high = version.stable.next_major
return VersionRange(version, high, include_min=True)
# Caret range
m = CARET_CONSTRAINT.match(constraint)
if m:
version = Version.parse(m.group(1))
return VersionRange(version, version.next_breaking, include_min=True)
# X Range
m = X_CONSTRAINT.match(constraint)
if m:
op = m.group(1)
major = int(m.group(2))
minor = m.group(3)
if minor is not None:
version = Version(major, int(minor), 0)
result = VersionRange(version, version.next_minor, include_min=True)
else:
if major == 0:
result = VersionRange(max=Version(1, 0, 0))
else:
version = Version(major, 0, 0)
result = VersionRange(version, version.next_major, include_min=True)
if op == '!=':
result = VersionRange().difference(result)
return result
# Basic comparator
m = BASIC_CONSTRAINT.match(constraint)
if m:
op = m.group(1)
version = m.group(2)
try:
version = Version.parse(version)
except ValueError:
raise ValueError('Could not parse version constraint: {}'.format(constraint))
if op == '<':
return VersionRange(max=version)
elif op == '<=':
return VersionRange(max=version, include_max=True)
elif op == '>':
return VersionRange(min=version)
elif op == '>=':
return VersionRange(min=version, include_min=True)
elif op == '!=':
return VersionUnion(
VersionRange(max=version),
VersionRange(min=version)
)
else:
return version
raise ValueError('Could not parse version constraint: {}'.format(constraint))
from .version_constraint import VersionConstraint
class EmptyConstraint(VersionConstraint):
def is_empty(self):
return True
def is_any(self):
return False
def allows(self, version):
return False
def allows_all(self, other):
return other.is_empty()
def allows_any(self, other):
return False
def intersect(self, other):
return self
def union(self, other):
return other
def difference(self, other):
return self
def __str__(self):
return '<empty>'
import re
MODIFIERS = (
'[._-]?'
'((?:beta|b|c|pre|RC|alpha|a|patch|pl|p|dev)(?:(?:[.-]?\d+)*)?)?'
'((?:[+-]|post)?([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?'
)
_COMPLETE_VERSION = 'v?(\d+)(?:\.(\d+))?(?:\.(\d+))?{}(?:\+[^\s]+)?'.format(MODIFIERS)
COMPLETE_VERSION = re.compile('(?i)' + _COMPLETE_VERSION)
CARET_CONSTRAINT = re.compile('(?i)^\^({})$'.format(_COMPLETE_VERSION))
TILDE_CONSTRAINT = re.compile('(?i)^~=?({})$'.format(_COMPLETE_VERSION))
X_CONSTRAINT = re.compile('^(!= ?|==)?v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.[xX*])+$')
BASIC_CONSTRAINT = re.compile('(?i)^(<>|!=|>=?|<=?|==?)?\s*({})'.format(_COMPLETE_VERSION))
class VersionConstraint:
def is_empty(self): # type: () -> bool
raise NotImplementedError()
def is_any(self): # type: () -> bool
raise NotImplementedError()
def allows(self, version): # type: (Version) -> bool
raise NotImplementedError()
def allows_all(self, other): # type: (VersionConstraint) -> bool
raise NotImplementedError()
def allows_any(self, other): # type: (VersionConstraint) -> bool
raise NotImplementedError()
def intersect(self, other): # type: (VersionConstraint) -> VersionConstraint
raise NotImplementedError()
def union(self, other): # type: (VersionConstraint) -> VersionConstraint
raise NotImplementedError()
def difference(self, other): # type: (VersionConstraint) -> VersionConstraint
raise NotImplementedError()
from .empty_constraint import EmptyConstraint
from .version_constraint import VersionConstraint
class VersionUnion(VersionConstraint):
"""
A version constraint representing a union of multiple disjoint version
ranges.
An instance of this will only be created if the version can't be represented
as a non-compound value.
"""
def __init__(self, *ranges):
self._ranges = list(ranges)
@property
def ranges(self):
return self._ranges
@classmethod
def of(cls, *ranges):
from .version_range import VersionRange
flattened = []
for constraint in ranges:
if constraint.is_empty():
continue
if isinstance(constraint, VersionUnion):
flattened += constraint.ranges
continue
flattened.append(constraint)
if not flattened:
return EmptyConstraint()
if any([constraint.is_any() for constraint in flattened]):
return VersionRange()
# Only allow Versions and VersionRanges here so we can more easily reason
# about everything in flattened. _EmptyVersions and VersionUnions are
# filtered out above.
for constraint in flattened:
if isinstance(constraint, VersionRange):
continue
raise ValueError('Unknown VersionConstraint type {}.'.format(constraint))
flattened.sort()
merged = []
for constraint in flattened:
# Merge this constraint with the previous one, but only if they touch.
if (
not merged
or (
not merged[-1].allows_any(constraint)
and not merged[-1].is_adjacent_to(constraint)
)
):
merged.append(constraint)
else:
merged[-1] = merged[-1].union(constraint)
if len(merged) == 1:
return merged[0]
return VersionUnion(*merged)
def is_empty(self):
return False
def is_any(self):
return False
def allows(self, version): # type: (Version) -> bool
return any([
constraint.allows(version) for constraint in self._ranges
])
def allows_all(self, other): # type: (VersionConstraint) -> bool
our_ranges = iter(self._ranges)
their_ranges = iter(self._ranges_for(other))
our_current_range = next(our_ranges, None)
their_current_range = next(their_ranges, None)
while our_current_range and their_current_range:
if our_current_range.allows_all(their_current_range):
their_current_range = next(their_ranges, None)
else:
our_current_range = next(our_ranges, None)
return their_current_range is None
def allows_any(self, other): # type: (VersionConstraint) -> bool
our_ranges = iter(self._ranges)
their_ranges = iter(self._ranges_for(other))
our_current_range = next(our_ranges, None)
their_current_range = next(their_ranges, None)
while our_current_range and their_current_range:
if our_current_range.allows_any(their_current_range):
return True
if their_current_range.allows_higher(our_current_range):
our_current_range = next(our_ranges, None)
else:
their_current_range = next(their_ranges, None)
return False
def intersect(self, other): # type: (VersionConstraint) -> VersionConstraint
our_ranges = iter(self._ranges)
their_ranges = iter(self._ranges_for(other))
new_ranges = []
our_current_range = next(our_ranges, None)
their_current_range = next(their_ranges, None)
while our_current_range and their_current_range:
intersection = our_current_range.intersect(their_current_range)
if not intersection.is_empty():
new_ranges.append(intersection)
if their_current_range.allows_higher(our_current_range):
our_current_range = next(our_ranges, None)
else:
their_current_range = next(their_ranges, None)
return VersionUnion.of(*new_ranges)
def union(self, other): # type: (VersionConstraint) -> VersionConstraint
return VersionUnion.of(self, other)
def difference(self, other): # type: (VersionConstraint) -> VersionConstraint
our_ranges = iter(self._ranges)
their_ranges = iter(self._ranges_for(other))
new_ranges = []
state = {
'current': next(our_ranges, None),
'their_range': next(their_ranges, None),
}
def their_next_range():
state['their_range'] = next(their_ranges, None)
if state['their_range']:
return True
new_ranges.append(state['current'])
our_current = next(our_ranges, None)
while our_current:
new_ranges.append(our_current)
our_current = next(our_ranges, None)
return False
def our_next_range(include_current=True):
if include_current:
new_ranges.append(state['current'])
our_current = next(our_ranges, None)
if not our_current:
return False
state['current'] = our_current
return True
while True:
if state['their_range'].is_strictly_lower(state['current']):
if not their_next_range():
break
continue
if state['their_range'].is_strictly_higher(state['current']):
if not our_next_range():
break
continue
difference = state['current'].difference(state['their_range'])
if isinstance(difference, VersionUnion):
assert len(difference.ranges) == 2
new_ranges.append(difference.ranges[0])
state['current'] = difference.ranges[-1]
if not their_next_range():
break
elif difference.is_empty():
if not our_next_range(False):
break
else:
state['current'] = difference
if state['current'].allows_higher(state['their_range']):
if not their_next_range():
break
else:
if not our_next_range():
break
if not new_ranges:
return EmptyConstraint()
if len(new_ranges) == 1:
return new_ranges[0]
return VersionUnion.of(*new_ranges)
def _ranges_for(self, constraint): # type: (VersionConstraint) -> List[VersionRange]
from .version_range import VersionRange
if constraint.is_empty():
return []
if isinstance(constraint, VersionUnion):
return constraint.ranges
if isinstance(constraint, VersionRange):
return [constraint]
raise ValueError('Unknown VersionConstraint type {}'.format(constraint))
def __eq__(self, other):
if not isinstance(other, VersionUnion):
return False
return self._ranges == other.ranges
def __str__(self):
return ' || '.join([str(r) for r in self._ranges])
def __repr__(self):
return '<VersionUnion {}>'.format(str(self))
......@@ -5,6 +5,8 @@ import tempfile
from contextlib import contextmanager
from typing import Union
from poetry.version import Version
_canonicalize_regex = re.compile('[-_.]+')
......@@ -16,6 +18,10 @@ def module_name(name): # type: (str) -> str
return canonicalize_name(name).replace('-', '_')
def normalize_version(version): # type: (str) -> str
return str(Version(version))
@contextmanager
def temporary_directory(*args, **kwargs):
try:
......
from poetry.semver.constraints import MultiConstraint
from poetry.semver.version_parser import VersionParser
from poetry.semver.semver import parse_constraint
from poetry.semver.semver import VersionUnion
PYTHON_VERSION = [
'2.7.*',
......@@ -14,24 +13,15 @@ def format_python_constraint(constraint):
This helper will help in transforming
disjunctive constraint into proper constraint.
"""
if not isinstance(constraint, MultiConstraint):
if not isinstance(constraint, VersionUnion):
return str(constraint)
has_disjunctive = False
for c in constraint.constraints:
if isinstance(c, MultiConstraint) and c.is_disjunctive():
has_disjunctive = True
break
parser = VersionParser()
formatted = []
accepted = []
if not constraint.is_disjunctive() and not has_disjunctive:
return str(constraint)
for version in PYTHON_VERSION:
version_constraint = parser.parse_constraints(version)
matches = constraint.matches(version_constraint)
version_constraint = parse_constraint(version)
matches = constraint.allows_any(version_constraint)
if not matches:
formatted.append('!=' + version)
else:
......
......@@ -17,7 +17,7 @@ from pyparsing import (
from pyparsing import ZeroOrMore, Word, Optional, Regex, Combine
from pyparsing import Literal as L # noqa
from poetry.semver.version_parser import VersionParser
from poetry.semver.semver import parse_constraint
from .markers import MARKER_EXPR, Marker
......@@ -221,7 +221,7 @@ class Requirement(object):
if not constraint:
constraint = '*'
self.constraint = VersionParser().parse_constraints(constraint)
self.constraint = parse_constraint(constraint)
self.pretty_constraint = constraint
self.marker = req.marker if req.marker else None
......
......@@ -2,16 +2,14 @@ import re
from typing import Union
from poetry.packages import Package
from poetry.semver.comparison import less_than
from poetry.semver.helpers import normalize_version
from poetry.semver.version_parser import VersionParser
from poetry.semver.semver import parse_constraint
from poetry.semver.semver import Version
class VersionSelector(object):
def __init__(self, pool, parser=VersionParser()):
def __init__(self, pool):
self._pool = pool
self._parser = parser
def find_best_candidate(self,
package_name, # type: str
......@@ -23,7 +21,7 @@ class VersionSelector(object):
returns the latest Package that matches
"""
if target_package_version:
constraint = self._parser.parse_constraints(target_package_version)
constraint = parse_constraint(target_package_version)
else:
constraint = None
......@@ -39,7 +37,7 @@ class VersionSelector(object):
continue
# Select highest version of the two
if less_than(package.version, candidate.version):
if package.version < candidate.version:
package = candidate
return package
......@@ -47,26 +45,24 @@ class VersionSelector(object):
def find_recommended_require_version(self, package):
version = package.version
return self._transform_version(version, package.pretty_version)
return self._transform_version(version.text, package.pretty_version)
def _transform_version(self, version, pretty_version):
# attempt to transform 2.1.1 to 2.1
# this allows you to upgrade through minor versions
try:
parts = normalize_version(version).split('.')
parsed = Version.parse(version)
parts = [parsed.major, parsed.minor, parsed.patch]
except ValueError:
return pretty_version
# check to see if we have a semver-looking version
if len(parts) == 4 and re.match('^0\D?', parts[3]):
if len(parts) == 3:
# remove the last parts (the patch version number and any extra)
if parts[0] == '0':
del parts[3]
else:
del parts[3]
if parts[0] != 0:
del parts[2]
version = '.'.join(parts)
version = '.'.join([str(p) for p in parts])
else:
return pretty_version
......
......@@ -21,7 +21,7 @@ def test_add_no_constraint(app, repo, installer):
Using version ^0.2.0 for cachy
Updating dependencies
Resolving dependencies
Resolving dependencies...
Package operations: 1 install, 0 updates, 0 removals
......@@ -57,7 +57,7 @@ def test_add_constraint(app, repo, installer):
expected = """\
Updating dependencies
Resolving dependencies
Resolving dependencies...
Package operations: 1 install, 0 updates, 0 removals
......@@ -94,7 +94,7 @@ def test_add_constraint_dependencies(app, repo, installer):
expected = """\
Updating dependencies
Resolving dependencies
Resolving dependencies...
Package operations: 2 installs, 0 updates, 0 removals
......@@ -125,7 +125,7 @@ def test_add_git_constraint(app, repo, installer):
expected = """\
Updating dependencies
Resolving dependencies
Resolving dependencies...
Package operations: 2 installs, 0 updates, 0 removals
......@@ -163,7 +163,7 @@ def test_add_git_constraint_with_poetry(app, repo, installer):
expected = """\
Updating dependencies
Resolving dependencies
Resolving dependencies...
Package operations: 2 installs, 0 updates, 0 removals
......@@ -194,7 +194,7 @@ def test_add_file_constraint_wheel(app, repo, installer):
expected = """\
Updating dependencies
Resolving dependencies
Resolving dependencies...
Package operations: 2 installs, 0 updates, 0 removals
......@@ -232,7 +232,7 @@ def test_add_file_constraint_sdist(app, repo, installer):
expected = """\
Updating dependencies
Resolving dependencies
Resolving dependencies...
Package operations: 2 installs, 0 updates, 0 removals
......@@ -281,7 +281,7 @@ def test_add_constraint_with_extras(app, repo, installer):
expected = """\
Updating dependencies
Resolving dependencies
Resolving dependencies...
Package operations: 2 installs, 0 updates, 0 removals
......
from poetry.packages import Dependency
from poetry.packages import Package
from poetry.semver.helpers import normalize_version
from poetry.utils._compat import Path
......@@ -9,7 +8,7 @@ FIXTURE_PATH = Path(__file__).parent / 'fixtures'
def get_package(name, version):
return Package(name, normalize_version(version), version)
return Package(name, version)
def get_dependency(name,
......
......@@ -22,8 +22,8 @@ reference = "tests/fixtures/project_with_setup"
url = ""
[package.dependencies]
cachy = ">= 0.2.0.0"
pendulum = ">= 1.4.4.0"
cachy = ">=0.2.0"
pendulum = ">=1.4.4"
[[package]]
name = "pendulum"
......
......@@ -13,7 +13,7 @@ reference = "tests/fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl"
url = ""
[package.dependencies]
pendulum = ">= 1.4.0.0, < 2.0.0.0"
pendulum = ">=1.4.0.0,<2.0.0.0"
[[package]]
name = "pendulum"
......
......@@ -17,7 +17,7 @@ python-versions = "*"
platform = "*"
[package.requirements]
python = ">= 2.4.0.0, < 2.5.0.0"
python = ">=2.4,<2.5.0"
[[package]]
name = "C"
......@@ -32,7 +32,7 @@ platform = "*"
D = "^1.2"
[package.requirements]
python = ">= 2.7.0.0, < 2.8.0.0 || >= 3.4.0.0, < 4.0.0.0"
python = ">=2.7,<2.8.0 || >=3.4,<4.0.0"
[[package]]
name = "D"
......@@ -44,7 +44,7 @@ python-versions = "*"
platform = "*"
[package.requirements]
python = ">= 2.7.0.0, < 2.8.0.0 || >= 3.4.0.0, < 4.0.0.0"
python = ">=2.7,<2.8.0 || >=3.4,<4.0.0"
[metadata]
python-versions = "~2.7 || ^3.4"
......
......@@ -41,7 +41,7 @@ python-versions = "*"
platform = "*"
[package.dependencies]
six = "< 2.0.0.0, >= 1.0.0.0"
six = ">=1.0.0,<2.0.0"
[[package]]
name = "pluggy"
......@@ -71,12 +71,12 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
platform = "unix"
[package.dependencies]
py = ">= 1.5.0.0"
six = ">= 1.10.0.0"
py = ">=1.5.0"
six = ">=1.10.0"
setuptools = "*"
attrs = ">= 17.4.0.0"
more-itertools = ">= 4.0.0.0"
pluggy = "< 0.7.0.0, >= 0.5.0.0"
attrs = ">=17.4.0"
more-itertools = ">=4.0.0"
pluggy = ">=0.5,<0.7"
funcsigs = "*"
colorama = "*"
......
......@@ -9,6 +9,7 @@ from poetry.installation import Installer as BaseInstaller
from poetry.installation.noop_installer import NoopInstaller
from poetry.io import NullIO
from poetry.packages import Locker as BaseLocker
from poetry.packages import ProjectPackage
from poetry.repositories import Pool
from poetry.repositories import Repository
from poetry.repositories.installed_repository import InstalledRepository
......@@ -98,7 +99,7 @@ def setup():
@pytest.fixture()
def package():
return get_package('root', '1.0')
return ProjectPackage('root', '1.0')
@pytest.fixture()
......@@ -195,7 +196,7 @@ def test_run_whitelist_add(installer, locker, repo, package):
package.add_dependency('B', '^1.0')
installer.update(True)
installer.whitelist({'B': '^1.1'})
installer.whitelist(['B'])
installer.run()
expected = fixture('with-dependencies')
......@@ -241,7 +242,7 @@ def test_run_whitelist_remove(installer, locker, repo, package):
package.add_dependency('A', '~1.0')
installer.update(True)
installer.whitelist({'B': '^1.1'})
installer.whitelist(['B'])
installer.run()
expected = fixture('remove')
......@@ -643,7 +644,7 @@ def test_run_changes_category_if_needed(installer, locker, repo, package):
package.add_dependency('B', '^1.1')
installer.update(True)
installer.whitelist({'B': '^1.1'})
installer.whitelist(['B'])
installer.run()
expected = fixture('with-category-change')
......
......@@ -114,7 +114,7 @@ License: MIT
Keywords: packaging,dependency,poetry
Author: Sébastien Eustace
Author-email: sebastien@eustace.io
Requires-Python: >= 3.6.0.0, < 4.0.0.0
Requires-Python: >=3.6,<4.0.0
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.6
......@@ -122,9 +122,9 @@ Classifier: Programming Language :: Python :: 3.7
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Provides-Extra: time
Requires-Dist: cachy[msgpack] (>=0.2.0.0,<0.3.0.0)
Requires-Dist: cleo (>=0.6.0.0,<0.7.0.0)
Requires-Dist: pendulum (>=1.4.0.0,<2.0.0.0); extra == "time"
Requires-Dist: cachy[msgpack] (>=0.2.0,<0.3.0)
Requires-Dist: cleo (>=0.6,<0.7.0)
Requires-Dist: pendulum (>=1.4,<2.0.0); extra == "time"
Description-Content-Type: text/x-rst
My Package
......
......@@ -48,9 +48,9 @@ def test_convert_dependencies():
]
)
main = [
'A>=1.0.0.0,<2.0.0.0',
'B>=1.0.0.0,<1.1.0.0',
'C==1.2.3.0',
'A>=1.0,<2.0.0',
'B>=1.0,<1.1.0',
'C==1.2.3',
]
extras = {}
......@@ -70,11 +70,11 @@ def test_convert_dependencies():
]
)
main = [
'B>=1.0.0.0,<1.1.0.0',
'C==1.2.3.0',
'B>=1.0,<1.1.0',
'C==1.2.3',
]
extras = {
'bar': ['A>=1.2.0.0']
'bar': ['A>=1.2']
}
assert result == (main, extras)
......@@ -98,20 +98,20 @@ def test_convert_dependencies():
]
)
main = [
'B>=1.0.0.0,<1.1.0.0',
'B>=1.0,<1.1.0',
]
extra_python = (
':(python_version >= "2.7.0.0" and python_version < "2.8.0.0") '
'or (python_version >= "3.6.0.0" and python_version < "4.0.0.0")'
':(python_version >= "2.7" and python_version < "2.8.0") '
'or (python_version >= "3.6" and python_version < "4.0.0")'
)
extra_d_dependency = (
'baz:(python_version >= "2.7.0.0" and python_version < "2.8.0.0") '
'or (python_version >= "3.4.0.0" and python_version < "4.0.0.0")'
'baz:(python_version >= "2.7" and python_version < "2.8.0") '
'or (python_version >= "3.4" and python_version < "4.0.0")'
)
extras = {
extra_python: ['C==1.2.3.0'],
extra_d_dependency: ['D==3.4.5.0'],
extra_python: ['C==1.2.3'],
extra_d_dependency: ['D==3.4.5'],
}
assert result == (main, extras)
......@@ -133,8 +133,8 @@ def test_make_setup():
'my_package.sub_pkg2'
]
assert ns['install_requires'] == [
'cachy[msgpack]>=0.2.0.0,<0.3.0.0',
'cleo>=0.6.0.0,<0.7.0.0',
'cachy[msgpack]>=0.2.0,<0.3.0',
'cleo>=0.6,<0.7.0',
]
assert ns['entry_points'] == {
'console_scripts': [
......@@ -144,7 +144,7 @@ def test_make_setup():
}
assert ns['extras_require'] == {
'time': [
'pendulum>=1.4.0.0,<2.0.0.0'
'pendulum>=1.4,<2.0.0'
]
}
......
{
"name": "detects circular dependencies",
"index": "circular",
"requested": {
"circular_app": "*"
},
"base": [],
"resolved": [],
"conflicts": [
"foo",
"bar"
]
}
{
"name": "resolves a simple conflict index",
"index": "conflict",
"requested": {
"my_app": "*"
},
"base": [],
"resolved": [
{
"name": "my_app",
"version": "1.0.0",
"dependencies": [
{
"name": "activemodel",
"version": "3.2.11",
"dependencies": [
{
"name": "builder",
"version": "3.0.4",
"dependencies": []
}
]
},
{
"name": "grape",
"version": "0.2.6",
"dependencies": [
{
"name": "builder",
"version": "3.0.4",
"dependencies": []
}
]
}
]
}
],
"conflicts": []
}
{
"name": "resolves a single dependency",
"index": "django",
"requested": {
"django": "~1.4.0",
"django-debug-toolbar": ""
},
"base": [],
"resolved": [
{
"name": "django",
"version": "1.4.3",
"dependencies": []
}, {
"name": "django-debug-toolbar",
"version": "1.3.2",
"dependencies": [
{
"name": "django",
"version": "1.4.3",
"dependencies": [
]
}
]
}
],
"conflicts": []
}
{
"name": "resolves an empty list of dependencies",
"requested": {
},
"base": [],
"resolved": [
],
"conflicts": []
}
{
"name": "resolves a single dependency",
"requested": {
"rack": "*"
},
"base": [],
"resolved": [
{
"name": "rack",
"version": "1.1",
"dependencies": []
}
],
"conflicts": []
}
{
"name": "resolves a single locked dependency",
"requested": {
"rack": "*"
},
"base": [
{
"name": "rack",
"version": "1.0",
"dependencies": []
}
],
"resolved": [
{
"name": "rack",
"version": "1.0",
"dependencies": []
}
],
"conflicts": []
}
{
"name": "resolves a single dependency with dependencies",
"requested": {
"actionpack": "*"
},
"base": [],
"resolved": [
{
"name": "actionpack",
"version": "2.3.5",
"dependencies": [
{
"name": "activesupport",
"version": "2.3.5",
"dependencies": []
},
{
"name": "rack",
"version": "1.0",
"dependencies": []
}
]
}
],
"conflicts": []
}
{
"name": "resolves dependencies with shared dependencies",
"requested": {
"actionpack": "*",
"activerecord": "2.3.5"
},
"base": [],
"resolved": [
{
"name": "actionpack",
"version": "2.3.5",
"dependencies": [
{
"name": "activesupport",
"version": "2.3.5",
"dependencies": []
},
{
"name": "rack",
"version": "1.0",
"dependencies": []
}
]
},
{
"name": "activerecord",
"version": "2.3.5",
"dependencies": [
{
"name": "activesupport",
"version": "2.3.5",
"dependencies": []
}
]
}
],
"conflicts": []
}
{
"name": "yields conflicts if a child dependency is not resolved",
"index": "unresolvable_child",
"requested": {
"chef_app_error": "*"
},
"base": [],
"resolved": [],
"conflicts": [
"json"
]
}
{
"rack": [
{
"name": "rack",
"version": "0.8",
"dependencies": {
}
},
{
"name": "rack",
"version": "0.9",
"dependencies": {
}
},
{
"name": "rack",
"version": "0.9.1",
"dependencies": {
}
},
{
"name": "rack",
"version": "0.9.2",
"dependencies": {
}
},
{
"name": "rack",
"version": "1.0",
"dependencies": {
}
},
{
"name": "rack",
"version": "1.1",
"dependencies": {
}
}
],
"rack-mount": [
{
"name": "rack-mount",
"version": "0.4",
"dependencies": {
}
},
{
"name": "rack-mount",
"version": "0.5",
"dependencies": {
}
},
{
"name": "rack-mount",
"version": "0.5.1",
"dependencies": {
}
},
{
"name": "rack-mount",
"version": "0.5.2",
"dependencies": {
}
},
{
"name": "rack-mount",
"version": "0.6",
"dependencies": {
}
}
],
"activesupport": [
{
"name": "activesupport",
"version": "1.2.3",
"dependencies": {
}
},
{
"name": "activesupport",
"version": "2.2.3",
"dependencies": {
}
},
{
"name": "activesupport",
"version": "2.3.5",
"dependencies": {
}
},
{
"name": "activesupport",
"version": "3.0.0-beta",
"dependencies": {
}
},
{
"name": "activesupport",
"version": "3.0.0-beta1",
"dependencies": {
}
}
],
"actionpack": [
{
"name": "actionpack",
"version": "1.2.3",
"dependencies": {
"activesupport": "= 1.2.3"
}
},
{
"name": "actionpack",
"version": "2.2.3",
"dependencies": {
"activesupport": "= 2.2.3",
"rack": "~0.9.0"
}
},
{
"name": "actionpack",
"version": "2.3.5",
"dependencies": {
"activesupport": "= 2.3.5",
"rack": "~1.0.0"
}
},
{
"name": "actionpack",
"version": "3.0.0-beta",
"dependencies": {
"activesupport": "= 3.0.0-beta",
"rack": "~1.1",
"rack-mount": ">= 0.5"
}
},
{
"name": "actionpack",
"version": "3.0.0-beta1",
"dependencies": {
"activesupport": "= 3.0.0-beta1",
"rack": "~1.1",
"rack-mount": ">= 0.5"
}
}
],
"activerecord": [
{
"name": "activerecord",
"version": "1.2.3",
"dependencies": {
"activesupport": "= 1.2.3"
}
},
{
"name": "activerecord",
"version": "2.2.3",
"dependencies": {
"activesupport": "= 2.2.3"
}
},
{
"name": "activerecord",
"version": "2.3.5",
"dependencies": {
"activesupport": "= 2.3.5"
}
},
{
"name": "activerecord",
"version": "3.0.0-beta",
"dependencies": {
"activesupport": "= 3.0.0-beta",
"arel": ">= 0.2"
}
},
{
"name": "activerecord",
"version": "3.0.0-beta1",
"dependencies": {
"activesupport": "= 3.0.0-beta1",
"arel": ">= 0.2"
}
}
],
"actionmailer": [
{
"name": "actionmailer",
"version": "1.2.3",
"dependencies": {
"activesupport": "= 1.2.3",
"actionmailer": "= 1.2.3"
}
},
{
"name": "actionmailer",
"version": "2.2.3",
"dependencies": {
"activesupport": "= 2.2.3",
"actionmailer": "= 2.2.3"
}
},
{
"name": "actionmailer",
"version": "2.3.5",
"dependencies": {
"activesupport": "= 2.3.5",
"actionmailer": "= 2.3.5"
}
},
{
"name": "actionmailer",
"version": "3.0.0-beta",
"dependencies": {
"activesupport": "= 3.0.0-beta",
"actionmailer": "= 3.0.0-beta"
}
},
{
"name": "actionmailer",
"version": "3.0.0-beta1",
"dependencies": {
"activesupport": "= 3.0.0-beta1",
"actionmailer": "= 3.0.0-beta1"
}
}
],
"railties": [
{
"name": "railties",
"version": "1.2.3",
"dependencies": {
"activerecord": "= 1.2.3",
"actionpack": "= 1.2.3",
"actionmailer": "= 1.2.3",
"activesupport": "= 1.2.3"
}
},
{
"name": "railties",
"version": "2.2.3",
"dependencies": {
"activerecord": "= 2.2.3",
"actionpack": "= 2.2.3",
"actionmailer": "= 2.2.3",
"activesupport": "= 2.2.3"
}
},
{
"name": "railties",
"version": "2.3.5",
"dependencies": {
"activerecord": "= 2.3.5",
"actionpack": "= 2.3.5",
"actionmailer": "= 2.3.5",
"activesupport": "= 2.3.5"
}
},
{
"name": "railties",
"version": "3.0.0-beta",
"dependencies": {
}
},
{
"name": "railties",
"version": "3.0.0-beta1",
"dependencies": {
}
}
],
"rails": [
{
"name": "rails",
"version": "3.0.0-beta",
"dependencies": {
"activerecord": "= 3.0.0-beta",
"actionpack": "= 3.0.0-beta",
"actionmailer": "= 3.0.0-beta",
"activesupport": "= 3.0.0-beta",
"railties": "= 3.0.0-beta"
}
},
{
"name": "rails",
"version": "3.0.0-beta1",
"dependencies": {
"activerecord": "= 3.0.0-beta1",
"actionpack": "= 3.0.0-beta1",
"actionmailer": "= 3.0.0-beta1",
"activesupport": "= 3.0.0-beta1",
"railties": "= 3.0.0-beta1"
}
}
],
"nokogiri": [
{
"name": "nokogiri",
"version": "1.0",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.2",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.2.1",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.2.2",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.3",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.3.0-1",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.3.5",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.4.0",
"dependencies": {
}
},
{
"name": "nokogiri",
"version": "1.4.2",
"dependencies": {
}
}
],
"weakling": [
{
"name": "weakling",
"version": "0.0.1",
"dependencies": {
}
},
{
"name": "weakling",
"version": "0.0.2",
"dependencies": {
}
},
{
"name": "weakling",
"version": "0.0.3",
"dependencies": {
}
}
],
"activemerchant": [
{
"name": "activemerchant",
"version": "1.2.3",
"dependencies": {
"activesupport": ">= 1.2.3"
}
},
{
"name": "activemerchant",
"version": "2.2.3",
"dependencies": {
"activesupport": ">= 2.2.3"
}
},
{
"name": "activemerchant",
"version": "2.3.5",
"dependencies": {
"activesupport": ">= 2.3.5"
}
}
]
}
{
"rack": [
{
"name": "rack",
"version": "1.0.1",
"dependencies": {
}
}
],
"foo": [
{
"name": "foo",
"version": "0.2.6",
"dependencies": {
"bar": ">= 0"
}
}
],
"bar": [
{
"name": "bar",
"version": "1.0.0",
"dependencies": {
"foo": ">= 0"
}
}
],
"circular-app": [
{
"name": "circular-app",
"version": "1.0.0",
"dependencies": {
"foo": ">= 0",
"bar": ">= 0"
}
}
]
}
{
"builder": [
{
"name": "builder",
"version": "3.0.4",
"dependencies": {
}
},
{
"name": "builder",
"version": "3.1.4",
"dependencies": {
}
}
],
"grape": [
{
"name": "grape",
"version": "0.2.6",
"dependencies": {
"builder": ">=0"
}
}
],
"activemodel": [
{
"name": "activemodel",
"version": "3.2.8",
"dependencies": {
"builder": "~3.0.0"
}
},
{
"name": "activemodel",
"version": "3.2.9",
"dependencies": {
"builder": "~3.0.0"
}
},
{
"name": "activemodel",
"version": "3.2.10",
"dependencies": {
"builder": "~3.0.0"
}
},
{
"name": "activemodel",
"version": "3.2.11",
"dependencies": {
"builder": "~3.0.0"
}
}
],
"my-app": [
{
"name": "my-app",
"version": "1.0.0",
"dependencies": {
"activemodel": ">=0",
"grape": ">=0"
}
}
]
}
{
"django": [
{
"name": "django",
"version": "1.4.1",
"dependencies": {
}
},
{
"name": "django",
"version": "1.4.3",
"dependencies": {
}
}, {
"name": "django",
"version": "2.0.1",
"dependencies": {
}
}
],
"django-debug-toolbar": [
{
"name": "django-debug-toolbar",
"version": "1.3.2",
"dependencies": {
"django": ">=1.4.2"
}
},
{
"name": "django-debug-toolbar",
"version": "1.6.1",
"dependencies": {
"django": ">=1.8"
}
}, {
"name": "django-debug-toolbar",
"version": "1.7.2",
"dependencies": {
"django": ">=1.9"
}
}
]
}
{
"json": [
{
"name": "json",
"version": "1.8.0",
"dependencies": {
}
}
],
"chef": [
{
"name": "chef",
"version": "10.26",
"dependencies": {
"json": "<= 1.7.7, >= 1.4.4"
}
}
],
"berkshelf": [
{
"name": "berkshelf",
"version": "2.0.7",
"dependencies": {
"json": ">= 1.7.7"
}
}
],
"chef-app-error": [
{
"name": "chef-app-error",
"version": "1.0.0",
"dependencies": {
"berkshelf": "~2.0",
"chef": "~10.26"
}
}
]
}
import json
import os
from functools import cmp_to_key
from poetry.mixology.contracts import SpecificationProvider
from poetry.packages import Package, Dependency
from poetry.semver import less_than
from poetry.semver.constraints import Constraint
FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'fixtures')
FIXTURE_INDEX_DIR = os.path.join(FIXTURE_DIR, 'index')
class Index(SpecificationProvider):
_specs_from_fixtures = {}
def __init__(self, packages_by_name):
self._packages = packages_by_name
self._search_for = {}
@property
def packages(self):
return self._packages
@classmethod
def from_fixture(cls, fixture_name):
return cls(cls.specs_from_fixtures(fixture_name))
@classmethod
def specs_from_fixtures(cls, fixture_name):
if fixture_name in cls._specs_from_fixtures:
return cls._specs_from_fixtures[fixture_name]
packages_by_name = {}
with open(os.path.join(FIXTURE_INDEX_DIR, fixture_name + '.json')) as fd:
content = json.load(fd)
for name, releases in content.items():
packages_by_name[name] = []
for release in releases:
package = Package(
name,
release['version'],
release['version']
)
for dependency_name, requirements in release['dependencies'].items():
package.requires.append(
Dependency(dependency_name, requirements)
)
packages_by_name[name].append(package)
packages_by_name[name].sort(
key=cmp_to_key(
lambda x, y:
0 if x.version[1] == y.version[1]
else -1 * int(less_than(x[1], y[1]) or -1)
)
)
return packages_by_name
def is_requirement_satisfied_by(self, requirement, activated, package):
if isinstance(requirement, Package):
return requirement == package
if package.is_prerelease() and not requirement.accepts_prereleases():
vertex = activated.vertex_named(package.name)
if not any([r.allows_prereleases() for r in vertex.requirements]):
return False
return requirement.constraint.matches(Constraint('==', package.version))
def search_for(self, dependency):
if dependency in self._search_for:
return self._search_for[dependency]
results = []
for spec in self._packages[dependency.name]:
if not dependency.allows_prereleases() and spec.is_prerelease():
continue
if dependency.constraint.matches(Constraint('==', spec.version)):
results.append(spec)
return results
def name_for(self, dependency):
return dependency.name
def dependencies_for(self, dependency):
return dependency.requires
def sort_dependencies(self,
dependencies,
activated,
conflicts):
return sorted(dependencies, key=lambda d: [
0 if activated.vertex_named(d.name).payload else 1,
0 if d.allows_prereleases() else 1,
0 if d.name in conflicts else 1,
0 if activated.vertex_named(d.name).payload else len(self.search_for(d))
])
import pytest
from poetry.mixology import DependencyGraph
@pytest.fixture()
def graph():
graph = DependencyGraph()
return graph
@pytest.fixture()
def root(graph):
return graph.add_vertex('Root', 'Root', True)
@pytest.fixture()
def root2(graph):
return graph.add_vertex('Root2', 'Root2', True)
@pytest.fixture()
def child(graph):
return graph.add_child_vertex('Child', 'Child', ['Root'], 'Child')
def test_root_vertex_named(graph, root, root2, child):
assert graph.root_vertex_named('Root') is root
def test_vertex_named(graph, root, root2, child):
assert graph.vertex_named('Root') is root
assert graph.vertex_named('Root2') is root2
assert graph.vertex_named('Child') is child
def test_root_vertex_named_non_existent(graph):
assert graph.root_vertex_named('missing') is None
def test_vertex_named_non_existent(graph):
assert graph.vertex_named('missing') is None
def test_detach_vertex_without_successors(graph):
root = graph.add_vertex('root', 'root', True)
graph.detach_vertex_named(root.name)
assert graph.vertex_named(root.name) is None
assert len(graph.vertices) == 0
def test_detach_vertex_with_successors(graph):
root = graph.add_vertex('root', 'root', True)
child = graph.add_child_vertex('child', 'child', ['root'], 'child')
graph.detach_vertex_named(root.name)
assert graph.vertex_named(root.name) is None
assert graph.vertex_named(child.name) is None
assert len(graph.vertices) == 0
def test_detach_vertex_with_successors_with_other_parents(graph):
root = graph.add_vertex('root', 'root', True)
root2 = graph.add_vertex('root2', 'root2', True)
child = graph.add_child_vertex('child', 'child', ['root', 'root2'], 'child')
graph.detach_vertex_named(root.name)
assert graph.vertex_named(root.name) is None
assert graph.vertex_named(child.name) is child
assert child.predecessors == [root2]
assert len(graph.vertices) == 2
def test_detach_vertex_with_predecessors(graph):
parent = graph.add_vertex('parent', 'parent', True)
child = graph.add_child_vertex('child', 'child', ['parent'], 'child')
graph.detach_vertex_named(child.name)
assert graph.vertex_named(child.name) is None
assert graph.vertices == {parent.name: parent}
assert len(parent.outgoing_edges) == 0
import json
import os
import pytest
from poetry.mixology import DependencyGraph
from poetry.mixology import Resolver
from poetry.mixology.exceptions import CircularDependencyError
from poetry.mixology.exceptions import ResolverError
from poetry.mixology.exceptions import VersionConflict
from poetry.packages import Dependency
from .index import Index
from .ui import UI
FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'fixtures')
FIXTURE_CASE_DIR = os.path.join(FIXTURE_DIR, 'case')
@pytest.fixture()
def resolver():
return Resolver(Index.from_fixture('awesome'), UI(True))
class Case:
def __init__(self, fixture):
self._fixture = fixture
self.name = fixture['name']
self._requested = None
self._result = None
self._index = None
self._base = None
self._conflicts = None
@property
def requested(self):
if self._requested is not None:
return self._requested
requested = []
for name, requirement in self._fixture['requested'].items():
requested.append(Dependency(name, requirement))
self._requested = requested
return self._requested
@property
def result(self):
if self._result is not None:
return self._result
graph = DependencyGraph()
for resolved in self._fixture['resolved']:
self.add_dependencies_to_graph(graph, None, resolved)
self._result = graph
return self._result
@property
def index(self):
if self._index is None:
self._index = Index.from_fixture(
self._fixture.get('index', 'awesome')
)
return self._index
@property
def base(self):
if self._base is not None:
return self._base
graph = DependencyGraph()
for r in self._fixture['base']:
self.add_dependencies_to_graph(graph, None, r)
self._base = graph
return self._base
@property
def conflicts(self):
if self._conflicts is None:
self._conflicts = self._fixture['conflicts']
return self._conflicts
def add_dependencies_to_graph(self, graph, parent, data, all_parents=None):
if all_parents is None:
all_parents = set()
name = data['name']
version = data['version']
dependency = [s for s in self.index.packages[name] if s.version == version][0]
if parent:
vertex = graph.add_vertex(name, dependency)
graph.add_edge(parent, vertex, dependency)
else:
vertex = graph.add_vertex(name, dependency, True)
if vertex in all_parents:
return
for dep in data['dependencies']:
self.add_dependencies_to_graph(graph, vertex, dep, all_parents)
def case(name):
with open(os.path.join(FIXTURE_CASE_DIR, name + '.json')) as fd:
return Case(json.load(fd))
def assert_graph(dg, result):
packages = sorted(dg.vertices.values(), key=lambda x: x.name)
expected_packages = sorted(result.vertices.values(), key=lambda x: x.name)
assert packages == expected_packages
@pytest.mark.parametrize(
'fixture',
[
'empty',
'simple',
'simple_with_base',
'simple_with_dependencies',
'simple_with_shared_dependencies',
'django',
]
)
def test_resolver(fixture):
c = case(fixture)
resolver = Resolver(c.index, UI(True))
dg = resolver.resolve(c.requested, base=c.base)
assert_graph(dg, c.result)
@pytest.mark.parametrize(
'fixture',
[
'circular',
'unresolvable_child'
]
)
def test_resolver_fail(fixture):
c = case(fixture)
resolver = Resolver(c.index, UI())
with pytest.raises(ResolverError) as e:
resolver.resolve(c.requested, base=c.base)
names = []
e = e.value
if isinstance(e, CircularDependencyError):
names = [d.name for d in e.dependencies]
elif isinstance(e, VersionConflict):
names = [n for n in e.conflicts.keys()]
assert sorted(names) == sorted(c.conflicts)
import sys
from io import StringIO
from poetry.mixology.contracts import UI as BaseUI
class UI(BaseUI):
def __init__(self, debug=False):
super(UI, self).__init__(debug)
self._output = None
@property
def output(self):
if self._output is None:
if self.debug:
self._output = sys.stderr
else:
self._output = StringIO()
return self._output
......@@ -59,15 +59,15 @@ def test_to_pep_508():
dependency = Dependency('Django', '^1.23')
result = dependency.to_pep_508()
assert result == 'Django (>=1.23.0.0,<2.0.0.0)'
assert result == 'Django (>=1.23,<2.0.0)'
dependency = Dependency('Django', '^1.23')
dependency.python_versions = '~2.7 || ^3.6'
result = dependency.to_pep_508()
assert result == 'Django (>=1.23.0.0,<2.0.0.0); ' \
'(python_version >= "2.7.0.0" and python_version < "2.8.0.0") ' \
'or (python_version >= "3.6.0.0" and python_version < "4.0.0.0")'
assert result == 'Django (>=1.23,<2.0.0); ' \
'(python_version >= "2.7" and python_version < "2.8.0") ' \
'or (python_version >= "3.6" and python_version < "4.0.0")'
def test_to_pep_508_wilcard():
......@@ -82,21 +82,21 @@ def test_to_pep_508_in_extras():
dependency.in_extras.append('foo')
result = dependency.to_pep_508()
assert result == 'Django (>=1.23.0.0,<2.0.0.0); extra == "foo"'
assert result == 'Django (>=1.23,<2.0.0); extra == "foo"'
dependency.in_extras.append('bar')
result = dependency.to_pep_508()
assert result == 'Django (>=1.23.0.0,<2.0.0.0); extra == "foo" or extra == "bar"'
assert result == 'Django (>=1.23,<2.0.0); extra == "foo" or extra == "bar"'
dependency.python_versions = '~2.7 || ^3.6'
result = dependency.to_pep_508()
assert result == (
'Django (>=1.23.0.0,<2.0.0.0); '
'Django (>=1.23,<2.0.0); '
'('
'(python_version >= "2.7.0.0" and python_version < "2.8.0.0") '
'or (python_version >= "3.6.0.0" and python_version < "4.0.0.0")'
'(python_version >= "2.7" and python_version < "2.8.0") '
'or (python_version >= "3.6" and python_version < "4.0.0")'
') '
'and (extra == "foo" or extra == "bar")'
)
......@@ -14,7 +14,7 @@ def test_dependency_from_pep_508_with_version():
dep = dependency_from_pep_508(name)
assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0'
assert str(dep.constraint) == '2.18.0'
def test_dependency_from_pep_508_with_parens():
......@@ -22,7 +22,7 @@ def test_dependency_from_pep_508_with_parens():
dep = dependency_from_pep_508(name)
assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0'
assert str(dep.constraint) == '2.18.0'
def test_dependency_from_pep_508_with_constraint():
......@@ -30,7 +30,7 @@ def test_dependency_from_pep_508_with_constraint():
dep = dependency_from_pep_508(name)
assert dep.name == 'requests'
assert str(dep.constraint) == '>= 2.12.0.0, != 2.17.*, < 3.0.0.0'
assert str(dep.constraint) == '>=2.12.0,<2.17.0 || >=2.18.0,<3.0'
def test_dependency_from_pep_508_with_extras():
......@@ -38,7 +38,7 @@ def test_dependency_from_pep_508_with_extras():
dep = dependency_from_pep_508(name)
assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0'
assert str(dep.constraint) == '2.18.0'
assert dep.extras == ['foo', 'bar']
......@@ -50,7 +50,7 @@ def test_dependency_from_pep_508_with_python_version():
dep = dependency_from_pep_508(name)
assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0'
assert str(dep.constraint) == '2.18.0'
assert dep.extras == []
assert dep.python_versions == '~2.7 || ~2.6'
......@@ -63,7 +63,7 @@ def test_dependency_from_pep_508_with_single_python_version():
dep = dependency_from_pep_508(name)
assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0'
assert str(dep.constraint) == '2.18.0'
assert dep.extras == []
assert dep.python_versions == '~2.7'
......@@ -76,7 +76,7 @@ def test_dependency_from_pep_508_with_platform():
dep = dependency_from_pep_508(name)
assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0'
assert str(dep.constraint) == '2.18.0'
assert dep.extras == []
assert dep.python_versions == '*'
assert dep.platform == 'win32 || darwin'
......@@ -92,7 +92,7 @@ def test_dependency_from_pep_508_complex():
dep = dependency_from_pep_508(name)
assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0'
assert str(dep.constraint) == '2.18.0'
assert dep.extras == ['foo']
assert dep.python_versions == '>=2.7 !=3.2.*'
assert dep.platform == 'win32 || darwin'
......@@ -106,7 +106,7 @@ def test_dependency_python_version_in():
dep = dependency_from_pep_508(name)
assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0'
assert str(dep.constraint) == '2.18.0'
assert dep.python_versions == '3.3.* || 3.4.* || 3.5.*'
......@@ -118,7 +118,7 @@ def test_dependency_platform_in():
dep = dependency_from_pep_508(name)
assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0'
assert str(dep.constraint) == '2.18.0'
assert dep.platform == 'win32 || darwin'
......@@ -127,7 +127,7 @@ def test_dependency_with_extra():
dep = dependency_from_pep_508(name)
assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0'
assert str(dep.constraint) == '2.18.0'
assert len(dep.extras) == 1
assert dep.extras[0] == 'security'
from poetry.packages import Package
from poetry.pub.failure import SolveFailure
from poetry.pub.version_solver import VersionSolver
def add_to_repo(repository, name, version, deps=None):
package = Package(name, version)
if deps:
for dep_name, dep_constraint in deps.items():
package.add_dependency(dep_name, dep_constraint)
repository.add_package(package)
def check_solver_result(root, provider,
result=None,
error=None,
tries=None,
locked=None,
use_latest=None):
solver = VersionSolver(root, provider, locked=locked, use_latest=use_latest)
try:
solution = solver.solve()
except SolveFailure as e:
if error:
assert str(e) == error
if tries is not None:
assert solver.solution.attempted_solutions == tries
return
raise
packages = {}
for package in solution.packages:
packages[package.name] = str(package.version)
assert result == packages
if tries is not None:
assert solution.attempted_solutions == tries
import pytest
from poetry.io import NullIO
from poetry.packages.project_package import ProjectPackage
from poetry.repositories import Pool
from poetry.repositories import Repository
from poetry.puzzle.provider import Provider
@pytest.fixture
def repo():
return Repository()
@pytest.fixture
def pool(repo):
pool = Pool()
pool.add_repository(repo)
return pool
@pytest.fixture
def root():
return ProjectPackage('myapp', '0.0.0')
@pytest.fixture
def provider(pool, root):
return Provider(root, pool, NullIO())
from ..helpers import add_to_repo
from ..helpers import check_solver_result
def test_circular_dependency_on_older_version(root, provider, repo):
root.add_dependency('a', '>=1.0.0')
add_to_repo(repo, 'a', '1.0.0')
add_to_repo(repo, 'a', '2.0.0', deps={'b': '1.0.0'})
add_to_repo(repo, 'b', '1.0.0', deps={'a': '1.0.0'})
check_solver_result(root, provider, {
'a': '1.0.0'
}, tries=2)
def test_diamond_dependency_graph(root, provider, repo):
root.add_dependency('a', '*')
root.add_dependency('b', '*')
add_to_repo(repo, 'a', '2.0.0', deps={'c': '^1.0.0'})
add_to_repo(repo, 'a', '1.0.0')
add_to_repo(repo, 'b', '2.0.0', deps={'c': '^3.0.0'})
add_to_repo(repo, 'b', '1.0.0', deps={'c': '^2.0.0'})
add_to_repo(repo, 'c', '3.0.0')
add_to_repo(repo, 'c', '2.0.0')
add_to_repo(repo, 'c', '1.0.0')
check_solver_result(
root, provider,
{
'a': '1.0.0',
'b': '2.0.0',
'c': '3.0.0',
}
)
def test_backjumps_after_partial_satisfier(root, provider, repo):
# c 2.0.0 is incompatible with y 2.0.0 because it requires x 1.0.0, but that
# requirement only exists because of both a and b. The solver should be able
# to deduce c 2.0.0's incompatibility and select c 1.0.0 instead.
root.add_dependency('c', '*')
root.add_dependency('y', '^2.0.0')
add_to_repo(repo, 'a', '1.0.0', deps={'x': '>=1.0.0'})
add_to_repo(repo, 'b', '1.0.0', deps={'x': '<2.0.0'})
add_to_repo(repo, 'c', '1.0.0')
add_to_repo(repo, 'c', '2.0.0', deps={'a': '*', 'b': '*'})
add_to_repo(repo, 'x', '0.0.0')
add_to_repo(repo, 'x', '1.0.0', deps={'y': '1.0.0'})
add_to_repo(repo, 'x', '2.0.0')
add_to_repo(repo, 'y', '1.0.0')
add_to_repo(repo, 'y', '2.0.0')
check_solver_result(
root, provider,
{
'c': '1.0.0',
'y': '2.0.0'
},
tries=2
)
from ..helpers import add_to_repo
from ..helpers import check_solver_result
def test_simple_dependencies(root, provider, repo):
root.add_dependency('a', '1.0.0')
root.add_dependency('b', '1.0.0')
add_to_repo(repo, 'a', '1.0.0', deps={'aa': '1.0.0', 'ab': '1.0.0'})
add_to_repo(repo, 'b', '1.0.0', deps={'ba': '1.0.0', 'bb': '1.0.0'})
add_to_repo(repo, 'aa', '1.0.0')
add_to_repo(repo, 'ab', '1.0.0')
add_to_repo(repo, 'ba', '1.0.0')
add_to_repo(repo, 'bb', '1.0.0')
check_solver_result(root, provider, {
'a': '1.0.0',
'aa': '1.0.0',
'ab': '1.0.0',
'b': '1.0.0',
'ba': '1.0.0',
'bb': '1.0.0'
})
def test_shared_dependencies_with_overlapping_constraints(root, provider, repo):
root.add_dependency('a', '1.0.0')
root.add_dependency('b', '1.0.0')
add_to_repo(repo, 'a', '1.0.0', deps={'shared': '>=2.0.0 <4.0.0'})
add_to_repo(repo, 'b', '1.0.0', deps={'shared': '>=3.0.0 <5.0.0'})
add_to_repo(repo, 'shared', '2.0.0')
add_to_repo(repo, 'shared', '3.0.0')
add_to_repo(repo, 'shared', '3.6.9')
add_to_repo(repo, 'shared', '4.0.0')
add_to_repo(repo, 'shared', '5.0.0')
check_solver_result(root, provider, {
'a': '1.0.0',
'b': '1.0.0',
'shared': '3.6.9',
})
def test_shared_dependency_where_dependent_version_affects_other_dependencies(root, provider, repo):
root.add_dependency('foo', '<=1.0.2')
root.add_dependency('bar', '1.0.0')
add_to_repo(repo, 'foo', '1.0.0')
add_to_repo(repo, 'foo', '1.0.1', deps={'bang': '1.0.0'})
add_to_repo(repo, 'foo', '1.0.2', deps={'whoop': '1.0.0'})
add_to_repo(repo, 'foo', '1.0.3', deps={'zoop': '1.0.0'})
add_to_repo(repo, 'bar', '1.0.0', deps={'foo': '<=1.0.1'})
add_to_repo(repo, 'bang', '1.0.0')
add_to_repo(repo, 'whoop', '1.0.0')
add_to_repo(repo, 'zoop', '1.0.0')
check_solver_result(root, provider, {
'foo': '1.0.1',
'bar': '1.0.0',
'bang': '1.0.0',
})
def test_circular_dependency(root, provider, repo):
root.add_dependency('foo', '1.0.0')
add_to_repo(repo, 'foo', '1.0.0', deps={'bar': '1.0.0'})
add_to_repo(repo, 'bar', '1.0.0', deps={'foo': '1.0.0'})
check_solver_result(root, provider, {
'foo': '1.0.0',
'bar': '1.0.0'
})
from ..helpers import add_to_repo
from ..helpers import check_solver_result
def test_no_version_matching_constraint(root, provider, repo):
root.add_dependency('foo', '^1.0')
add_to_repo(repo, 'foo', '2.0.0')
add_to_repo(repo, 'foo', '2.1.3')
check_solver_result(
root, provider, error=(
"Because myapp depends on foo (^1.0) "
"which doesn't match any versions, version solving failed."
)
)
def test_no_version_that_matches_combined_constraints(root, provider, repo):
root.add_dependency('foo', '1.0.0')
root.add_dependency('bar', '1.0.0')
add_to_repo(repo, 'foo', '1.0.0', deps={'shared': '>=2.0.0 <3.0.0'})
add_to_repo(repo, 'bar', '1.0.0', deps={'shared': '>=2.9.0 <4.0.0'})
add_to_repo(repo, 'shared', '2.5.0')
add_to_repo(repo, 'shared', '3.5.0')
error = """\
Because foo (1.0.0) depends on shared (>=2.0.0 <3.0.0)
and no versions of shared match >=2.9.0,<3.0.0, foo (1.0.0) requires shared (>=2.0.0,<2.9.0).
And because bar (1.0.0) depends on shared (>=2.9.0 <4.0.0), bar (1.0.0) is incompatible with foo (1.0.0).
So, because myapp depends on both foo (1.0.0) and bar (1.0.0), version solving failed."""
check_solver_result(root, provider, error=error)
def test_disjoint_constraints(root, provider, repo):
root.add_dependency('foo', '1.0.0')
root.add_dependency('bar', '1.0.0')
add_to_repo(repo, 'foo', '1.0.0', deps={'shared': '<=2.0.0'})
add_to_repo(repo, 'bar', '1.0.0', deps={'shared': '>3.0.0'})
add_to_repo(repo, 'shared', '2.0.0')
add_to_repo(repo, 'shared', '4.0.0')
error = """\
Because bar (1.0.0) depends on shared (>3.0.0)
and foo (1.0.0) depends on shared (<=2.0.0), bar (1.0.0) is incompatible with foo (1.0.0).
So, because myapp depends on both foo (1.0.0) and bar (1.0.0), version solving failed."""
check_solver_result(root, provider, error=error)
def test_no_valid_solution(root, provider, repo):
root.add_dependency('a')
root.add_dependency('b')
add_to_repo(repo, 'a', '1.0.0', deps={'b': '1.0.0'})
add_to_repo(repo, 'a', '2.0.0', deps={'b': '2.0.0'})
add_to_repo(repo, 'b', '1.0.0', deps={'a': '2.0.0'})
add_to_repo(repo, 'b', '2.0.0', deps={'a': '1.0.0'})
error = """\
Because no versions of b match <1.0.0 || >1.0.0,<2.0.0 || >2.0.0
and b (1.0.0) depends on a (2.0.0), b (<2.0.0 || >2.0.0) requires a (2.0.0).
And because a (2.0.0) depends on b (2.0.0), b is forbidden.
Because b (2.0.0) depends on a (1.0.0) which depends on b (1.0.0), b is forbidden.
Thus, b is forbidden.
So, because myapp depends on b (*), version solving failed."""
check_solver_result(root, provider, error=error, tries=2)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment