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 ...@@ -33,7 +33,7 @@ If you do not specify a version constraint, poetry will choose a suitable one ba
def handle(self): def handle(self):
from poetry.installation import Installer from poetry.installation import Installer
from poetry.semver.version_parser import VersionParser from poetry.semver.semver import parse_constraint
packages = self.argument('name') packages = self.argument('name')
is_dev = self.option('dev') is_dev = self.option('dev')
...@@ -76,9 +76,8 @@ If you do not specify a version constraint, poetry will choose a suitable one ba ...@@ -76,9 +76,8 @@ If you do not specify a version constraint, poetry will choose a suitable one ba
requirements = self._format_requirements(requirements) requirements = self._format_requirements(requirements)
# validate requirements format # validate requirements format
parser = VersionParser()
for constraint in requirements.values(): for constraint in requirements.values():
parser.parse_constraints(constraint) parse_constraint(constraint)
for name, constraint in requirements.items(): for name, constraint in requirements.items():
constraint = { constraint = {
......
...@@ -19,23 +19,22 @@ class DebugResolveCommand(Command): ...@@ -19,23 +19,22 @@ class DebugResolveCommand(Command):
def handle(self): def handle(self):
from poetry.packages import Dependency from poetry.packages import Dependency
from poetry.packages import ProjectPackage
from poetry.puzzle import Solver from poetry.puzzle import Solver
from poetry.repositories.repository import Repository from poetry.repositories.repository import Repository
from poetry.semver.version_parser import VersionParser from poetry.semver.semver import parse_constraint
packages = self.argument('package') packages = self.argument('package')
if not packages: if not packages:
package = self.poetry.package package = self.poetry.package
dependencies = package.requires + package.dev_requires
else: else:
requirements = self._determine_requirements(packages) requirements = self._determine_requirements(packages)
requirements = self._format_requirements(requirements) requirements = self._format_requirements(requirements)
# validate requirements format # validate requirements format
parser = VersionParser()
for constraint in requirements.values(): for constraint in requirements.values():
parser.parse_constraints(constraint) parse_constraint(constraint)
dependencies = [] dependencies = []
for name, constraint in requirements.items(): for name, constraint in requirements.items():
...@@ -43,15 +42,23 @@ class DebugResolveCommand(Command): ...@@ -43,15 +42,23 @@ class DebugResolveCommand(Command):
Dependency(name, constraint) 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( solver = Solver(
self.poetry.package, package,
self.poetry.pool, self.poetry.pool,
Repository(), Repository(),
Repository(), Repository(),
self.output self.output
) )
ops = solver.solve(dependencies) ops = solver.solve()
self.line('') self.line('')
self.line('Resolution results:') self.line('Resolution results:')
......
...@@ -253,14 +253,14 @@ lists all packages available.""" ...@@ -253,14 +253,14 @@ lists all packages available."""
) )
def get_update_status(self, latest, package): 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: if latest.full_pretty_version == package.full_pretty_version:
return 'up-to-date' 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 # It needs an immediate semver-compliant upgrade
return 'semver-safe-update' return 'semver-safe-update'
......
...@@ -8,8 +8,9 @@ class PoetryStyle(CleoStyle): ...@@ -8,8 +8,9 @@ class PoetryStyle(CleoStyle):
super(PoetryStyle, self).__init__(i, o) super(PoetryStyle, self).__init__(i, o)
self.output.get_formatter().add_style('error', 'red') 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('question', 'blue')
self.output.get_formatter().add_style('comment', 'blue')
def writeln(self, messages, def writeln(self, messages,
type=OutputStyle.OUTPUT_NORMAL, type=OutputStyle.OUTPUT_NORMAL,
......
...@@ -15,8 +15,9 @@ from poetry.puzzle.operations.operation import Operation ...@@ -15,8 +15,9 @@ from poetry.puzzle.operations.operation import Operation
from poetry.repositories import Pool from poetry.repositories import Pool
from poetry.repositories import Repository from poetry.repositories import Repository
from poetry.repositories.installed_repository import InstalledRepository from poetry.repositories.installed_repository import InstalledRepository
from poetry.semver.constraints import Constraint from poetry.semver.semver import parse_constraint
from poetry.semver.version_parser import VersionParser from poetry.semver.semver import Version
from poetry.utils.helpers import canonicalize_name
from .base_installer import BaseInstaller from .base_installer import BaseInstaller
from .pip_installer import PipInstaller from .pip_installer import PipInstaller
...@@ -112,7 +113,7 @@ class Installer: ...@@ -112,7 +113,7 @@ class Installer:
return self return self
def whitelist(self, packages): # type: (dict) -> Installer def whitelist(self, packages): # type: (dict) -> Installer
self._whitelist = packages self._whitelist = [canonicalize_name(p) for p in packages]
return self return self
...@@ -135,33 +136,6 @@ class Installer: ...@@ -135,33 +136,6 @@ class Installer:
) )
self._io.writeln('<info>Updating dependencies</>') 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( solver = Solver(
self._package, self._package,
self._pool, self._pool,
...@@ -170,10 +144,7 @@ class Installer: ...@@ -170,10 +144,7 @@ class Installer:
self._io self._io
) )
request = self._package.requires ops = solver.solve(use_latest=self._whitelist)
request += self._package.dev_requires
ops = solver.solve(request, fixed=fixed)
else: else:
self._io.writeln('<info>Installing dependencies from lock file</>') self._io.writeln('<info>Installing dependencies from lock file</>')
...@@ -451,18 +422,17 @@ class Installer: ...@@ -451,18 +422,17 @@ class Installer:
if op.job_type == 'uninstall': if op.job_type == 'uninstall':
continue continue
parser = VersionParser() python = Version.parse('.'.join([str(i) for i in self._venv.version_info[:3]]))
python = '.'.join([str(i) for i in self._venv.version_info[:3]])
if 'python' in package.requirements: if 'python' in package.requirements:
python_constraint = parser.parse_constraints( python_constraint = parse_constraint(
package.requirements['python'] package.requirements['python']
) )
if not python_constraint.matches(Constraint('=', python)): if not python_constraint.allows(python):
# Incompatible python versions # Incompatible python versions
op.skip('Not needed for the current python version') op.skip('Not needed for the current python version')
continue 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') op.skip('Not needed for the current python version')
continue continue
......
...@@ -7,9 +7,6 @@ import tempfile ...@@ -7,9 +7,6 @@ import tempfile
from collections import defaultdict from collections import defaultdict
from contextlib import contextmanager 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.utils._compat import Path
from poetry.vcs import get_vcs from poetry.vcs import get_vcs
...@@ -156,35 +153,6 @@ class Builder(object): ...@@ -156,35 +153,6 @@ class Builder(object):
'email': email '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 @classmethod
@contextmanager @contextmanager
def temporary_directory(cls, *args, **kwargs): def temporary_directory(cls, *args, **kwargs):
......
...@@ -64,7 +64,7 @@ class SdistBuilder(Builder): ...@@ -64,7 +64,7 @@ class SdistBuilder(Builder):
target_dir.mkdir(parents=True) target_dir.mkdir(parents=True)
target = target_dir / '{}-{}.tar.gz'.format( 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') gz = GzipFile(target.as_posix(), mode='wb')
tar = tarfile.TarFile(target.as_posix(), mode='w', fileobj=gz, tar = tarfile.TarFile(target.as_posix(), mode='w', fileobj=gz,
...@@ -72,7 +72,7 @@ class SdistBuilder(Builder): ...@@ -72,7 +72,7 @@ class SdistBuilder(Builder):
try: try:
tar_dir = '{}-{}'.format( 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) files_to_add = self.find_files_to_add(exclude_build=False)
......
...@@ -13,8 +13,7 @@ from base64 import urlsafe_b64encode ...@@ -13,8 +13,7 @@ from base64 import urlsafe_b64encode
from io import StringIO from io import StringIO
from poetry.__version__ import __version__ from poetry.__version__ import __version__
from poetry.semver.constraints import Constraint from poetry.semver.semver import parse_constraint
from poetry.semver.constraints import MultiConstraint
from poetry.utils._compat import Path from poetry.utils._compat import Path
from ..utils.helpers import normalize_file_permissions from ..utils.helpers import normalize_file_permissions
...@@ -181,23 +180,18 @@ class WheelBuilder(Builder): ...@@ -181,23 +180,18 @@ class WheelBuilder(Builder):
@property @property
def dist_info(self): # type: () -> str 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 @property
def wheel_filename(self): # type: () -> str def wheel_filename(self): # type: () -> str
return '{}-{}-{}.whl'.format( return '{}-{}-{}.whl'.format(
re.sub("[^\w\d.]+", "_", self._package.pretty_name, flags=re.UNICODE), 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 self.tag
) )
def supports_python2(self): def supports_python2(self):
return self._package.python_constraint.matches( return self._package.python_constraint.allows_any(parse_constraint('>=2.0.0 <3.0.0'))
MultiConstraint([
Constraint('>=', '2.0.0'),
Constraint('<', '3.0.0')
])
)
def dist_info_name(self, distribution, version): # type: (...) -> str def dist_info_name(self, distribution, version): # type: (...) -> str
escaped_name = re.sub("[^\w\d.]+", "_", distribution, flags=re.UNICODE) escaped_name = re.sub("[^\w\d.]+", "_", distribution, flags=re.UNICODE)
......
from poetry.utils.helpers import canonicalize_name from poetry.utils.helpers import canonicalize_name
from poetry.utils.helpers import normalize_version
from poetry.version.helpers import format_python_constraint from poetry.version.helpers import format_python_constraint
...@@ -42,7 +43,7 @@ class Metadata: ...@@ -42,7 +43,7 @@ class Metadata:
meta = cls() meta = cls()
meta.name = canonicalize_name(package.name) meta.name = canonicalize_name(package.name)
meta.version = package.version meta.version = normalize_version(package.version.text)
meta.summary = package.description meta.summary = package.description
if package.readme: if package.readme:
with package.readme.open() as f: with package.readme.open() as f:
......
...@@ -8,6 +8,7 @@ from .directory_dependency import DirectoryDependency ...@@ -8,6 +8,7 @@ from .directory_dependency import DirectoryDependency
from .file_dependency import FileDependency from .file_dependency import FileDependency
from .locker import Locker from .locker import Locker
from .package import Package from .package import Package
from .project_package import ProjectPackage
from .utils.link import Link from .utils.link import Link
from .utils.utils import convert_markers from .utils.utils import convert_markers
from .utils.utils import group_markers from .utils.utils import group_markers
......
import poetry.packages import poetry.packages
from poetry.semver.constraints import Constraint
from poetry.semver.constraints import EmptyConstraint from poetry.semver.constraints import EmptyConstraint
from poetry.semver.constraints import MultiConstraint from poetry.semver.constraints import MultiConstraint
from poetry.semver.constraints.base_constraint import BaseConstraint from poetry.semver.semver import parse_constraint
from poetry.semver.version_parser import VersionParser 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 poetry.utils.helpers import canonicalize_name
from .constraints.generic_constraint import GenericConstraint from .constraints.generic_constraint import GenericConstraint
...@@ -21,29 +22,30 @@ class Dependency(object): ...@@ -21,29 +22,30 @@ class Dependency(object):
): ):
self._name = canonicalize_name(name) self._name = canonicalize_name(name)
self._pretty_name = name self._pretty_name = name
self._parser = VersionParser()
try: try:
if not isinstance(constraint, BaseConstraint): if not isinstance(constraint, VersionConstraint):
self._constraint = self._parser.parse_constraints(constraint) self._constraint = parse_constraint(constraint)
else: else:
self._constraint = constraint self._constraint = constraint
except ValueError: 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._optional = optional
self._category = category self._category = category
self._allows_prereleases = allows_prereleases self._allows_prereleases = allows_prereleases
self._python_versions = '*' self._python_versions = '*'
self._python_constraint = self._parser.parse_constraints('*') self._python_constraint = parse_constraint('*')
self._platform = '*' self._platform = '*'
self._platform_constraint = EmptyConstraint() self._platform_constraint = EmptyConstraint()
self._extras = [] self._extras = []
self._in_extras = [] self._in_extras = []
self.is_root = False
@property @property
def name(self): def name(self):
return self._name return self._name
...@@ -71,7 +73,7 @@ class Dependency(object): ...@@ -71,7 +73,7 @@ class Dependency(object):
@python_versions.setter @python_versions.setter
def python_versions(self, value): def python_versions(self, value):
self._python_versions = value self._python_versions = value
self._python_constraint = self._parser.parse_constraints(value) self._python_constraint = parse_constraint(value)
@property @property
def python_constraint(self): def python_constraint(self):
...@@ -119,7 +121,7 @@ class Dependency(object): ...@@ -119,7 +121,7 @@ class Dependency(object):
""" """
return ( return (
self._name == package.name 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()) and (not package.is_prerelease() or self.allows_prereleases())
) )
...@@ -129,11 +131,13 @@ class Dependency(object): ...@@ -129,11 +131,13 @@ class Dependency(object):
if self.extras: if self.extras:
requirement += '[{}]'.format(','.join(self.extras)) requirement += '[{}]'.format(','.join(self.extras))
if isinstance(self.constraint, MultiConstraint): if isinstance(self.constraint, VersionUnion):
requirement += ' ({})'.format(','.join( 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(' ', '')) requirement += ' ({})'.format(str(self.constraint).replace(' ', ''))
# Markers # Markers
...@@ -185,10 +189,57 @@ class Dependency(object): ...@@ -185,10 +189,57 @@ class Dependency(object):
parts = [part[1] for part in parts] parts = [part[1] for part in parts]
marker = glue.join(parts) marker = glue.join(parts)
else: elif isinstance(constraint, GenericConstraint):
marker = '{} {} "{}"'.format( marker = '{} {} "{}"'.format(
name, constraint.string_operator, constraint.version 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 return marker
...@@ -204,6 +255,27 @@ class Dependency(object): ...@@ -204,6 +255,27 @@ class Dependency(object):
""" """
self._optional = True 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): def __eq__(self, other):
if not isinstance(other, Dependency): if not isinstance(other, Dependency):
return NotImplemented return NotImplemented
...@@ -214,6 +286,9 @@ class Dependency(object): ...@@ -214,6 +286,9 @@ class Dependency(object):
return hash((self._name, self._pretty_constraint)) return hash((self._name, self._pretty_constraint))
def __str__(self): def __str__(self):
if self.is_root:
return self._pretty_name
return '{} ({})'.format( return '{} ({})'.format(
self._pretty_name, self._pretty_constraint self._pretty_name, self._pretty_constraint
) )
......
...@@ -8,6 +8,8 @@ from poetry.semver.constraints import Constraint ...@@ -8,6 +8,8 @@ from poetry.semver.constraints import Constraint
from poetry.semver.constraints import EmptyConstraint from poetry.semver.constraints import EmptyConstraint
from poetry.semver.helpers import parse_stability from poetry.semver.helpers import parse_stability
from poetry.semver.version_parser import VersionParser 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_by_id
from poetry.spdx import License from poetry.spdx import License
from poetry.utils._compat import Path from poetry.utils._compat import Path
...@@ -43,20 +45,6 @@ class Package(object): ...@@ -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): def __init__(self, name, version, pretty_version=None):
""" """
Creates a new in memory package. Creates a new in memory package.
...@@ -64,14 +52,15 @@ class Package(object): ...@@ -64,14 +52,15 @@ class Package(object):
self._pretty_name = name self._pretty_name = name
self._name = canonicalize_name(name) self._name = canonicalize_name(name)
self._version = str(parse_version(version)) if not isinstance(version, Version):
self._version = Version.parse(version)
self._pretty_version = pretty_version or version self._pretty_version = pretty_version or version
else:
self._version = version
self._pretty_version = pretty_version or self._version.text
self.description = '' self.description = ''
self._stability = parse_stability(version)
self._dev = self._stability == 'dev'
self._authors = [] self._authors = []
self.homepage = None self.homepage = None
...@@ -89,8 +78,6 @@ class Package(object): ...@@ -89,8 +78,6 @@ class Package(object):
self.extras = {} self.extras = {}
self.requires_extras = [] self.requires_extras = []
self._parser = VersionParser()
self.category = 'main' self.category = 'main'
self.hashes = [] self.hashes = []
self.optional = False self.optional = False
...@@ -105,7 +92,7 @@ class Package(object): ...@@ -105,7 +92,7 @@ class Package(object):
self.classifiers = [] self.classifiers = []
self._python_versions = '*' self._python_versions = '*'
self._python_constraint = self._parser.parse_constraints('*') self._python_constraint = parse_constraint('*')
self._platform = '*' self._platform = '*'
self._platform_constraint = EmptyConstraint() self._platform_constraint = EmptyConstraint()
...@@ -129,7 +116,10 @@ class Package(object): ...@@ -129,7 +116,10 @@ class Package(object):
@property @property
def unique_name(self): def unique_name(self):
return self.name + '-' + self._version if self.is_root():
return self._name
return self.name + '-' + self._version.text
@property @property
def pretty_string(self): def pretty_string(self):
...@@ -137,7 +127,7 @@ class Package(object): ...@@ -137,7 +127,7 @@ class Package(object):
@property @property
def full_pretty_version(self): 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 return self._pretty_version
# if source reference is a sha1 hash -- truncate # if source reference is a sha1 hash -- truncate
...@@ -159,6 +149,10 @@ class Package(object): ...@@ -159,6 +149,10 @@ class Package(object):
def author_email(self): # type: () -> str def author_email(self): # type: () -> str
return self._get_author()['email'] return self._get_author()['email']
@property
def all_requires(self):
return self.requires + self.dev_requires
def _get_author(self): # type: () -> dict def _get_author(self): # type: () -> dict
if not self._authors: if not self._authors:
return { return {
...@@ -183,7 +177,7 @@ class Package(object): ...@@ -183,7 +177,7 @@ class Package(object):
@python_versions.setter @python_versions.setter
def python_versions(self, value): def python_versions(self, value):
self._python_versions = value self._python_versions = value
self._python_constraint = self._parser.parse_constraints(value) self._python_constraint = parse_constraint(value)
@property @property
def python_constraint(self): def python_constraint(self):
...@@ -220,19 +214,18 @@ class Package(object): ...@@ -220,19 +214,18 @@ class Package(object):
classifiers = copy.copy(self.classifiers) classifiers = copy.copy(self.classifiers)
# Automatically set python classifiers # Automatically set python classifiers
parser = VersionParser()
if self.python_versions == '*': if self.python_versions == '*':
python_constraint = parser.parse_constraints('~2.7 || ^3.4') python_constraint = parse_constraint('~2.7 || ^3.4')
else: else:
python_constraint = self.python_constraint python_constraint = self.python_constraint
for version in sorted(self.AVAILABLE_PYTHONS): for version in sorted(self.AVAILABLE_PYTHONS):
if len(version) == 1: if len(version) == 1:
constraint = parser.parse_constraints(version + '.*') constraint = parse_constraint(version + '.*')
else: else:
constraint = Constraint('=', version) constraint = Version.parse(version)
if python_constraint.matches(constraint): if python_constraint.allows_any(constraint):
classifiers.append( classifiers.append(
'Programming Language :: Python :: {}'.format(version) 'Programming Language :: Python :: {}'.format(version)
) )
...@@ -245,11 +238,11 @@ class Package(object): ...@@ -245,11 +238,11 @@ class Package(object):
return sorted(classifiers) return sorted(classifiers)
def is_dev(self):
return self._dev
def is_prerelease(self): def is_prerelease(self):
return self._stability != 'stable' return self._version.is_prerelease()
def is_root(self):
return False
def add_dependency(self, def add_dependency(self,
name, # type: str name, # type: str
...@@ -335,6 +328,9 @@ class Package(object): ...@@ -335,6 +328,9 @@ class Package(object):
return dependency return dependency
def to_dependency(self):
return Dependency(self.name, self._version)
def __hash__(self): def __hash__(self):
return hash((self._name, self._version)) 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 ...@@ -11,6 +11,7 @@ from .exceptions import InvalidProjectFile
from .packages import Dependency from .packages import Dependency
from .packages import Locker from .packages import Locker
from .packages import Package from .packages import Package
from .packages import ProjectPackage
from .repositories import Pool from .repositories import Pool
from .repositories.pypi_repository import PyPiRepository from .repositories.pypi_repository import PyPiRepository
from .spdx import license_by_id from .spdx import license_by_id
...@@ -95,7 +96,7 @@ class Poetry: ...@@ -95,7 +96,7 @@ class Poetry:
# Load package # Load package
name = local_config['name'] name = local_config['name']
version = local_config['version'] version = local_config['version']
package = Package(name, version, version) package = ProjectPackage(name, version, version)
package.root_dir = poetry_file.parent package.root_dir = poetry_file.parent
for author in local_config['authors']: 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 os
import pkginfo import pkginfo
import shutil import shutil
import time
from cleo import ProgressIndicator
from contextlib import contextmanager
from functools import cmp_to_key from functools import cmp_to_key
from tempfile import mkdtemp from tempfile import mkdtemp
from typing import Dict from typing import Dict
...@@ -20,9 +23,13 @@ from poetry.packages import Package ...@@ -20,9 +23,13 @@ from poetry.packages import Package
from poetry.packages import VCSDependency from poetry.packages import VCSDependency
from poetry.packages import dependency_from_pep_508 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._compat import Path
from poetry.utils.helpers import parse_requires from poetry.utils.helpers import parse_requires
...@@ -34,6 +41,26 @@ from poetry.vcs.git import Git ...@@ -34,6 +41,26 @@ from poetry.vcs.git import Git
from .dependencies import Dependencies 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): class Provider(SpecificationProvider, UI):
UNSAFE_PACKAGES = {'setuptools', 'distribute', 'pip'} UNSAFE_PACKAGES = {'setuptools', 'distribute', 'pip'}
...@@ -49,7 +76,6 @@ class Provider(SpecificationProvider, UI): ...@@ -49,7 +76,6 @@ class Provider(SpecificationProvider, UI):
self._python_constraint = package.python_constraint self._python_constraint = package.python_constraint
self._base_dg = DependencyGraph() self._base_dg = DependencyGraph()
self._search_for = {} self._search_for = {}
self._constraints = {}
super(Provider, self).__init__(debug=self._io.is_debug()) super(Provider, self).__init__(debug=self._io.is_debug())
...@@ -78,6 +104,9 @@ class Provider(SpecificationProvider, UI): ...@@ -78,6 +104,9 @@ class Provider(SpecificationProvider, UI):
The specifications in the returned list will be considered in reverse The specifications in the returned list will be considered in reverse
order, so the latest version ought to be last. order, so the latest version ought to be last.
""" """
if dependency.is_root:
return [self._package]
if dependency in self._search_for: if dependency in self._search_for:
return self._search_for[dependency] return self._search_for[dependency]
...@@ -90,17 +119,6 @@ class Provider(SpecificationProvider, UI): ...@@ -90,17 +119,6 @@ class Provider(SpecificationProvider, UI):
else: else:
constraint = dependency.constraint 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( packages = self._pool.find_packages(
dependency.name, dependency.name,
constraint, constraint,
...@@ -112,7 +130,7 @@ class Provider(SpecificationProvider, UI): ...@@ -112,7 +130,7 @@ class Provider(SpecificationProvider, UI):
key=cmp_to_key( key=cmp_to_key(
lambda x, y: lambda x, y:
0 if x.version == y.version 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): ...@@ -179,11 +197,12 @@ class Provider(SpecificationProvider, UI):
# to figure the information we need # to figure the information we need
# We need to place ourselves in the proper # We need to place ourselves in the proper
# folder for it to work # folder for it to work
venv = Venv.create(self._io)
current_dir = os.getcwd() current_dir = os.getcwd()
os.chdir(tmp_dir.as_posix()) os.chdir(tmp_dir.as_posix())
try: try:
venv = Venv.create(self._io)
venv.run( venv.run(
'python', 'setup.py', 'egg_info' 'python', 'setup.py', 'egg_info'
) )
...@@ -251,6 +270,48 @@ class Provider(SpecificationProvider, UI): ...@@ -251,6 +270,48 @@ class Provider(SpecificationProvider, UI):
return [package] 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 def dependencies_for(self, package
): # type: (Package) -> Union[List[Dependency], Dependencies] ): # type: (Package) -> Union[List[Dependency], Dependencies]
if package.source_type in ['git', 'file', 'directory']: if package.source_type in ['git', 'file', 'directory']:
...@@ -265,7 +326,7 @@ class Provider(SpecificationProvider, UI): ...@@ -265,7 +326,7 @@ class Provider(SpecificationProvider, UI):
def _dependencies_for(self, package): # type: (Package) -> List[Dependency] def _dependencies_for(self, package): # type: (Package) -> List[Dependency]
complete_package = self._pool.package( complete_package = self._pool.package(
package.name, package.version, package.name, package.version.text,
extras=package.requires_extras extras=package.requires_extras
) )
...@@ -279,7 +340,7 @@ class Provider(SpecificationProvider, UI): ...@@ -279,7 +340,7 @@ class Provider(SpecificationProvider, UI):
return [ return [
r for r in package.requires r for r in package.requires
if not r.is_optional() 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 self._package.platform_constraint.matches(package.platform_constraint)
and r.name not in self.UNSAFE_PACKAGES and r.name not in self.UNSAFE_PACKAGES
] ]
...@@ -341,7 +402,7 @@ class Provider(SpecificationProvider, UI): ...@@ -341,7 +402,7 @@ class Provider(SpecificationProvider, UI):
def after_resolution(self): def after_resolution(self):
self._io.new_line() self._io.new_line()
def debug(self, message, depth): def debug(self, message, depth=0):
if self.is_debugging(): if self.is_debugging():
debug_info = str(message) debug_info = str(message)
debug_info = '\n'.join([ debug_info = '\n'.join([
...@@ -350,3 +411,14 @@ class Provider(SpecificationProvider, UI): ...@@ -350,3 +411,14 @@ class Provider(SpecificationProvider, UI):
]) + '\n' ]) + '\n'
self.output.write(debug_info) 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 typing import List
from poetry.mixology import Resolver from poetry.pub import resolve_version
from poetry.mixology.dependency_graph import DependencyGraph from poetry.pub.failure import SolveFailure
from poetry.mixology.exceptions import ResolverError
from poetry.packages.constraints.generic_constraint import GenericConstraint 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 .exceptions import SolverProblemError
from .operations import Install from .operations import Install
from .operations import Uninstall from .operations import Uninstall
from .operations import Update from .operations import Update
...@@ -25,31 +25,27 @@ class Solver: ...@@ -25,31 +25,27 @@ class Solver:
self._locked = locked self._locked = locked
self._io = io 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) provider = Provider(self._package, self._pool, self._io)
resolver = Resolver(provider, provider) locked = {}
for package in self._locked.packages:
base = None locked[package.name] = package
if fixed is not None:
base = DependencyGraph()
for fixed_req in fixed:
base.add_vertex(fixed_req.name, fixed_req, True)
try: try:
graph = resolver.resolve(requested, base=base) result = resolve_version(self._package, provider, locked=locked, use_latest=use_latest)
except ResolverError as e: except SolveFailure as e:
raise SolverProblemError(e) raise SolverProblemError(e)
packages = [v.payload for v in graph.vertices.values()] packages = result.packages
requested = self._package.all_requires
# Setting info for package in packages:
for vertex in graph.vertices.values(): category, optional, python, platform = self._get_tags_for_package(
category, optional, python, platform = self._get_tags_for_vertex( package, packages, requested
vertex, requested
) )
vertex.payload.category = category package.category = category
vertex.payload.optional = optional package.optional = optional
# If requirements are empty, drop them # If requirements are empty, drop them
requirements = {} requirements = {}
...@@ -59,7 +55,7 @@ class Solver: ...@@ -59,7 +55,7 @@ class Solver:
if platform is not None and platform != '*': if platform is not None and platform != '*':
requirements['platform'] = platform requirements['platform'] = platform
vertex.payload.requirements = requirements package.requirements = requirements
operations = [] operations = []
for package in packages: for package in packages:
...@@ -101,7 +97,7 @@ class Solver: ...@@ -101,7 +97,7 @@ class Solver:
operations.append(op) operations.append(op)
requested_names = [r.name for r in requested] requested_names = [r.name for r in self._package.all_requires]
return sorted( return sorted(
operations, operations,
...@@ -111,45 +107,43 @@ class Solver: ...@@ -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' category = 'dev'
optional = True optional = True
python_version = None
platform = None
if not vertex.incoming_edges: root = None
# Original dependency for dep in requested:
for req in requested: if dep.name == package.name:
if vertex.payload.name == req.name: root = dep
category = req.category
optional = req.is_optional()
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 return category, optional, python_version, platform
parser = VersionParser()
python_versions = [] python_versions = []
platforms = [] 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_category,
top_optional, top_optional,
top_python_version, top_python_version,
top_platform) = self._get_tags_for_vertex( top_platform) = self._get_tags_for_package(
edge.origin, requested pkg, packages, requested
) )
if top_category == 'main': if top_category == 'main':
...@@ -160,10 +154,10 @@ class Solver: ...@@ -160,10 +154,10 @@ class Solver:
# Take the most restrictive constraints # Take the most restrictive constraints
if top_python_version is not None: if top_python_version is not None:
if python_version is not None: if python_version is not None:
previous = parser.parse_constraints(python_version) previous = parse_constraint(python_version)
current = parser.parse_constraints(top_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) python_versions.append(top_python_version)
else: else:
python_versions.append(python_version) python_versions.append(python_version)
...@@ -191,15 +185,15 @@ class Solver: ...@@ -191,15 +185,15 @@ class Solver:
else: else:
# Find the least restrictive constraint # Find the least restrictive constraint
python_version = python_versions[0] python_version = python_versions[0]
previous = parser.parse_constraints(python_version) previous = parse_constraint(python_version)
for constraint in python_versions[1:]: for constraint in python_versions[1:]:
current = parser.parse_constraints(constraint) current = parse_constraint(constraint)
if python_version == '*': if python_version == '*':
continue continue
elif constraint == '*': elif constraint == '*':
python_version = constraint python_version = constraint
elif current.matches(previous): elif current.allows_all(previous):
python_version = constraint python_version = constraint
if not platforms: if not platforms:
......
...@@ -30,8 +30,8 @@ from poetry.locations import CACHE_DIR ...@@ -30,8 +30,8 @@ from poetry.locations import CACHE_DIR
from poetry.packages import dependency_from_pep_508 from poetry.packages import dependency_from_pep_508
from poetry.packages import Package from poetry.packages import Package
from poetry.semver.constraints import Constraint from poetry.semver.constraints import Constraint
from poetry.semver.constraints.base_constraint import BaseConstraint from poetry.semver.semver import parse_constraint
from poetry.semver.version_parser import VersionParser from poetry.semver.semver import VersionConstraint
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils._compat import to_str from poetry.utils._compat import to_str
from poetry.utils.helpers import parse_requires from poetry.utils.helpers import parse_requires
...@@ -86,9 +86,8 @@ class PyPiRepository(Repository): ...@@ -86,9 +86,8 @@ class PyPiRepository(Repository):
""" """
Find packages on the remote server. Find packages on the remote server.
""" """
if constraint is not None and not isinstance(constraint, BaseConstraint): if constraint is not None and not isinstance(constraint, VersionConstraint):
version_parser = VersionParser() constraint = parse_constraint(constraint)
constraint = version_parser.parse_constraints(constraint)
info = self.get_package_info(name) info = self.get_package_info(name)
...@@ -112,7 +111,7 @@ class PyPiRepository(Repository): ...@@ -112,7 +111,7 @@ class PyPiRepository(Repository):
if ( if (
not constraint not constraint
or (constraint and constraint.matches(Constraint('=', version))) or (constraint and constraint.allows(package.version))
): ):
if extras is not None: if extras is not None:
package.requires_extras = extras package.requires_extras = extras
......
from poetry.semver.constraints import Constraint from poetry.semver.semver import parse_constraint
from poetry.semver.constraints.base_constraint import BaseConstraint from poetry.semver.semver import VersionConstraint
from poetry.semver.version_parser import VersionParser
from poetry.version import parse as parse_version from poetry.version import parse as parse_version
...@@ -20,10 +19,9 @@ class Repository(BaseRepository): ...@@ -20,10 +19,9 @@ class Repository(BaseRepository):
def package(self, name, version, extras=None): def package(self, name, version, extras=None):
name = name.lower() name = name.lower()
version = str(parse_version(version))
for package in self.packages: for package in self.packages:
if name == package.name and package.version == version: if name == package.name and package.version.text == version:
return package return package
def find_packages(self, name, constraint=None, def find_packages(self, name, constraint=None,
...@@ -37,15 +35,15 @@ class Repository(BaseRepository): ...@@ -37,15 +35,15 @@ class Repository(BaseRepository):
if constraint is None: if constraint is None:
constraint = '*' constraint = '*'
if not isinstance(constraint, BaseConstraint): if not isinstance(constraint, VersionConstraint):
parser = VersionParser() constraint = parse_constraint(constraint)
constraint = parser.parse_constraints(constraint)
for package in self.packages: for package in self.packages:
if name == package.name: 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 dep in package.requires:
for extra in extras: for extra in extras:
if extra not in package.extras: if extra not in package.extras:
......
...@@ -3,28 +3,10 @@ from functools import cmp_to_key ...@@ -3,28 +3,10 @@ from functools import cmp_to_key
from .comparison import less_than from .comparison import less_than
from .constraints import Constraint from .constraints import Constraint
from .helpers import normalize_version from .helpers import normalize_version
from .version_parser import VersionParser
SORT_ASC = 1 SORT_ASC = 1
SORT_DESC = -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): def satisfied_by(versions, constraints):
""" """
......
...@@ -2,3 +2,18 @@ class BaseConstraint(object): ...@@ -2,3 +2,18 @@ class BaseConstraint(object):
def matches(self, provider): def matches(self, provider):
raise NotImplementedError() 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): ...@@ -8,5 +8,20 @@ class EmptyConstraint(BaseConstraint):
def matches(self, _): def matches(self, _):
return True 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): def __str__(self):
return '*' 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 ...@@ -5,6 +5,8 @@ import tempfile
from contextlib import contextmanager from contextlib import contextmanager
from typing import Union from typing import Union
from poetry.version import Version
_canonicalize_regex = re.compile('[-_.]+') _canonicalize_regex = re.compile('[-_.]+')
...@@ -16,6 +18,10 @@ def module_name(name): # type: (str) -> str ...@@ -16,6 +18,10 @@ def module_name(name): # type: (str) -> str
return canonicalize_name(name).replace('-', '_') return canonicalize_name(name).replace('-', '_')
def normalize_version(version): # type: (str) -> str
return str(Version(version))
@contextmanager @contextmanager
def temporary_directory(*args, **kwargs): def temporary_directory(*args, **kwargs):
try: try:
......
from poetry.semver.constraints import MultiConstraint from poetry.semver.semver import parse_constraint
from poetry.semver.version_parser import VersionParser from poetry.semver.semver import VersionUnion
PYTHON_VERSION = [ PYTHON_VERSION = [
'2.7.*', '2.7.*',
...@@ -14,24 +13,15 @@ def format_python_constraint(constraint): ...@@ -14,24 +13,15 @@ def format_python_constraint(constraint):
This helper will help in transforming This helper will help in transforming
disjunctive constraint into proper constraint. disjunctive constraint into proper constraint.
""" """
if not isinstance(constraint, MultiConstraint): if not isinstance(constraint, VersionUnion):
return str(constraint) 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 = [] formatted = []
accepted = [] accepted = []
if not constraint.is_disjunctive() and not has_disjunctive:
return str(constraint)
for version in PYTHON_VERSION: for version in PYTHON_VERSION:
version_constraint = parser.parse_constraints(version) version_constraint = parse_constraint(version)
matches = constraint.matches(version_constraint) matches = constraint.allows_any(version_constraint)
if not matches: if not matches:
formatted.append('!=' + version) formatted.append('!=' + version)
else: else:
......
...@@ -17,7 +17,7 @@ from pyparsing import ( ...@@ -17,7 +17,7 @@ from pyparsing import (
from pyparsing import ZeroOrMore, Word, Optional, Regex, Combine from pyparsing import ZeroOrMore, Word, Optional, Regex, Combine
from pyparsing import Literal as L # noqa 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 from .markers import MARKER_EXPR, Marker
...@@ -221,7 +221,7 @@ class Requirement(object): ...@@ -221,7 +221,7 @@ class Requirement(object):
if not constraint: if not constraint:
constraint = '*' constraint = '*'
self.constraint = VersionParser().parse_constraints(constraint) self.constraint = parse_constraint(constraint)
self.pretty_constraint = constraint self.pretty_constraint = constraint
self.marker = req.marker if req.marker else None self.marker = req.marker if req.marker else None
......
...@@ -2,16 +2,14 @@ import re ...@@ -2,16 +2,14 @@ import re
from typing import Union from typing import Union
from poetry.packages import Package from poetry.packages import Package
from poetry.semver.comparison import less_than from poetry.semver.semver import parse_constraint
from poetry.semver.helpers import normalize_version from poetry.semver.semver import Version
from poetry.semver.version_parser import VersionParser
class VersionSelector(object): class VersionSelector(object):
def __init__(self, pool, parser=VersionParser()): def __init__(self, pool):
self._pool = pool self._pool = pool
self._parser = parser
def find_best_candidate(self, def find_best_candidate(self,
package_name, # type: str package_name, # type: str
...@@ -23,7 +21,7 @@ class VersionSelector(object): ...@@ -23,7 +21,7 @@ class VersionSelector(object):
returns the latest Package that matches returns the latest Package that matches
""" """
if target_package_version: if target_package_version:
constraint = self._parser.parse_constraints(target_package_version) constraint = parse_constraint(target_package_version)
else: else:
constraint = None constraint = None
...@@ -39,7 +37,7 @@ class VersionSelector(object): ...@@ -39,7 +37,7 @@ class VersionSelector(object):
continue continue
# Select highest version of the two # Select highest version of the two
if less_than(package.version, candidate.version): if package.version < candidate.version:
package = candidate package = candidate
return package return package
...@@ -47,26 +45,24 @@ class VersionSelector(object): ...@@ -47,26 +45,24 @@ class VersionSelector(object):
def find_recommended_require_version(self, package): def find_recommended_require_version(self, package):
version = package.version 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): def _transform_version(self, version, pretty_version):
# attempt to transform 2.1.1 to 2.1 # attempt to transform 2.1.1 to 2.1
# this allows you to upgrade through minor versions # this allows you to upgrade through minor versions
try: try:
parts = normalize_version(version).split('.') parsed = Version.parse(version)
parts = [parsed.major, parsed.minor, parsed.patch]
except ValueError: except ValueError:
return pretty_version return pretty_version
# check to see if we have a semver-looking 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) # remove the last parts (the patch version number and any extra)
if parts[0] == '0': if parts[0] != 0:
del parts[3]
else:
del parts[3]
del parts[2] del parts[2]
version = '.'.join(parts) version = '.'.join([str(p) for p in parts])
else: else:
return pretty_version return pretty_version
......
...@@ -21,7 +21,7 @@ def test_add_no_constraint(app, repo, installer): ...@@ -21,7 +21,7 @@ def test_add_no_constraint(app, repo, installer):
Using version ^0.2.0 for cachy Using version ^0.2.0 for cachy
Updating dependencies Updating dependencies
Resolving dependencies Resolving dependencies...
Package operations: 1 install, 0 updates, 0 removals Package operations: 1 install, 0 updates, 0 removals
...@@ -57,7 +57,7 @@ def test_add_constraint(app, repo, installer): ...@@ -57,7 +57,7 @@ def test_add_constraint(app, repo, installer):
expected = """\ expected = """\
Updating dependencies Updating dependencies
Resolving dependencies Resolving dependencies...
Package operations: 1 install, 0 updates, 0 removals Package operations: 1 install, 0 updates, 0 removals
...@@ -94,7 +94,7 @@ def test_add_constraint_dependencies(app, repo, installer): ...@@ -94,7 +94,7 @@ def test_add_constraint_dependencies(app, repo, installer):
expected = """\ expected = """\
Updating dependencies Updating dependencies
Resolving dependencies Resolving dependencies...
Package operations: 2 installs, 0 updates, 0 removals Package operations: 2 installs, 0 updates, 0 removals
...@@ -125,7 +125,7 @@ def test_add_git_constraint(app, repo, installer): ...@@ -125,7 +125,7 @@ def test_add_git_constraint(app, repo, installer):
expected = """\ expected = """\
Updating dependencies Updating dependencies
Resolving dependencies Resolving dependencies...
Package operations: 2 installs, 0 updates, 0 removals Package operations: 2 installs, 0 updates, 0 removals
...@@ -163,7 +163,7 @@ def test_add_git_constraint_with_poetry(app, repo, installer): ...@@ -163,7 +163,7 @@ def test_add_git_constraint_with_poetry(app, repo, installer):
expected = """\ expected = """\
Updating dependencies Updating dependencies
Resolving dependencies Resolving dependencies...
Package operations: 2 installs, 0 updates, 0 removals Package operations: 2 installs, 0 updates, 0 removals
...@@ -194,7 +194,7 @@ def test_add_file_constraint_wheel(app, repo, installer): ...@@ -194,7 +194,7 @@ def test_add_file_constraint_wheel(app, repo, installer):
expected = """\ expected = """\
Updating dependencies Updating dependencies
Resolving dependencies Resolving dependencies...
Package operations: 2 installs, 0 updates, 0 removals Package operations: 2 installs, 0 updates, 0 removals
...@@ -232,7 +232,7 @@ def test_add_file_constraint_sdist(app, repo, installer): ...@@ -232,7 +232,7 @@ def test_add_file_constraint_sdist(app, repo, installer):
expected = """\ expected = """\
Updating dependencies Updating dependencies
Resolving dependencies Resolving dependencies...
Package operations: 2 installs, 0 updates, 0 removals Package operations: 2 installs, 0 updates, 0 removals
...@@ -281,7 +281,7 @@ def test_add_constraint_with_extras(app, repo, installer): ...@@ -281,7 +281,7 @@ def test_add_constraint_with_extras(app, repo, installer):
expected = """\ expected = """\
Updating dependencies Updating dependencies
Resolving dependencies Resolving dependencies...
Package operations: 2 installs, 0 updates, 0 removals Package operations: 2 installs, 0 updates, 0 removals
......
from poetry.packages import Dependency from poetry.packages import Dependency
from poetry.packages import Package from poetry.packages import Package
from poetry.semver.helpers import normalize_version
from poetry.utils._compat import Path from poetry.utils._compat import Path
...@@ -9,7 +8,7 @@ FIXTURE_PATH = Path(__file__).parent / 'fixtures' ...@@ -9,7 +8,7 @@ FIXTURE_PATH = Path(__file__).parent / 'fixtures'
def get_package(name, version): def get_package(name, version):
return Package(name, normalize_version(version), version) return Package(name, version)
def get_dependency(name, def get_dependency(name,
......
...@@ -22,8 +22,8 @@ reference = "tests/fixtures/project_with_setup" ...@@ -22,8 +22,8 @@ reference = "tests/fixtures/project_with_setup"
url = "" url = ""
[package.dependencies] [package.dependencies]
cachy = ">= 0.2.0.0" cachy = ">=0.2.0"
pendulum = ">= 1.4.4.0" pendulum = ">=1.4.4"
[[package]] [[package]]
name = "pendulum" name = "pendulum"
......
...@@ -13,7 +13,7 @@ reference = "tests/fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl" ...@@ -13,7 +13,7 @@ reference = "tests/fixtures/distributions/demo-0.1.0-py2.py3-none-any.whl"
url = "" url = ""
[package.dependencies] [package.dependencies]
pendulum = ">= 1.4.0.0, < 2.0.0.0" pendulum = ">=1.4.0.0,<2.0.0.0"
[[package]] [[package]]
name = "pendulum" name = "pendulum"
......
...@@ -17,7 +17,7 @@ python-versions = "*" ...@@ -17,7 +17,7 @@ python-versions = "*"
platform = "*" platform = "*"
[package.requirements] [package.requirements]
python = ">= 2.4.0.0, < 2.5.0.0" python = ">=2.4,<2.5.0"
[[package]] [[package]]
name = "C" name = "C"
...@@ -32,7 +32,7 @@ platform = "*" ...@@ -32,7 +32,7 @@ platform = "*"
D = "^1.2" D = "^1.2"
[package.requirements] [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]] [[package]]
name = "D" name = "D"
...@@ -44,7 +44,7 @@ python-versions = "*" ...@@ -44,7 +44,7 @@ python-versions = "*"
platform = "*" platform = "*"
[package.requirements] [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] [metadata]
python-versions = "~2.7 || ^3.4" python-versions = "~2.7 || ^3.4"
......
...@@ -41,7 +41,7 @@ python-versions = "*" ...@@ -41,7 +41,7 @@ python-versions = "*"
platform = "*" platform = "*"
[package.dependencies] [package.dependencies]
six = "< 2.0.0.0, >= 1.0.0.0" six = ">=1.0.0,<2.0.0"
[[package]] [[package]]
name = "pluggy" name = "pluggy"
...@@ -71,12 +71,12 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" ...@@ -71,12 +71,12 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
platform = "unix" platform = "unix"
[package.dependencies] [package.dependencies]
py = ">= 1.5.0.0" py = ">=1.5.0"
six = ">= 1.10.0.0" six = ">=1.10.0"
setuptools = "*" setuptools = "*"
attrs = ">= 17.4.0.0" attrs = ">=17.4.0"
more-itertools = ">= 4.0.0.0" more-itertools = ">=4.0.0"
pluggy = "< 0.7.0.0, >= 0.5.0.0" pluggy = ">=0.5,<0.7"
funcsigs = "*" funcsigs = "*"
colorama = "*" colorama = "*"
......
...@@ -9,6 +9,7 @@ from poetry.installation import Installer as BaseInstaller ...@@ -9,6 +9,7 @@ from poetry.installation import Installer as BaseInstaller
from poetry.installation.noop_installer import NoopInstaller from poetry.installation.noop_installer import NoopInstaller
from poetry.io import NullIO from poetry.io import NullIO
from poetry.packages import Locker as BaseLocker from poetry.packages import Locker as BaseLocker
from poetry.packages import ProjectPackage
from poetry.repositories import Pool from poetry.repositories import Pool
from poetry.repositories import Repository from poetry.repositories import Repository
from poetry.repositories.installed_repository import InstalledRepository from poetry.repositories.installed_repository import InstalledRepository
...@@ -98,7 +99,7 @@ def setup(): ...@@ -98,7 +99,7 @@ def setup():
@pytest.fixture() @pytest.fixture()
def package(): def package():
return get_package('root', '1.0') return ProjectPackage('root', '1.0')
@pytest.fixture() @pytest.fixture()
...@@ -195,7 +196,7 @@ def test_run_whitelist_add(installer, locker, repo, package): ...@@ -195,7 +196,7 @@ def test_run_whitelist_add(installer, locker, repo, package):
package.add_dependency('B', '^1.0') package.add_dependency('B', '^1.0')
installer.update(True) installer.update(True)
installer.whitelist({'B': '^1.1'}) installer.whitelist(['B'])
installer.run() installer.run()
expected = fixture('with-dependencies') expected = fixture('with-dependencies')
...@@ -241,7 +242,7 @@ def test_run_whitelist_remove(installer, locker, repo, package): ...@@ -241,7 +242,7 @@ def test_run_whitelist_remove(installer, locker, repo, package):
package.add_dependency('A', '~1.0') package.add_dependency('A', '~1.0')
installer.update(True) installer.update(True)
installer.whitelist({'B': '^1.1'}) installer.whitelist(['B'])
installer.run() installer.run()
expected = fixture('remove') expected = fixture('remove')
...@@ -643,7 +644,7 @@ def test_run_changes_category_if_needed(installer, locker, repo, package): ...@@ -643,7 +644,7 @@ def test_run_changes_category_if_needed(installer, locker, repo, package):
package.add_dependency('B', '^1.1') package.add_dependency('B', '^1.1')
installer.update(True) installer.update(True)
installer.whitelist({'B': '^1.1'}) installer.whitelist(['B'])
installer.run() installer.run()
expected = fixture('with-category-change') expected = fixture('with-category-change')
......
...@@ -114,7 +114,7 @@ License: MIT ...@@ -114,7 +114,7 @@ License: MIT
Keywords: packaging,dependency,poetry Keywords: packaging,dependency,poetry
Author: Sébastien Eustace Author: Sébastien Eustace
Author-email: sebastien@eustace.io 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: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.6
...@@ -122,9 +122,9 @@ Classifier: Programming Language :: Python :: 3.7 ...@@ -122,9 +122,9 @@ Classifier: Programming Language :: Python :: 3.7
Classifier: Topic :: Software Development :: Build Tools Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Software Development :: Libraries :: Python Modules
Provides-Extra: time Provides-Extra: time
Requires-Dist: cachy[msgpack] (>=0.2.0.0,<0.3.0.0) Requires-Dist: cachy[msgpack] (>=0.2.0,<0.3.0)
Requires-Dist: cleo (>=0.6.0.0,<0.7.0.0) Requires-Dist: cleo (>=0.6,<0.7.0)
Requires-Dist: pendulum (>=1.4.0.0,<2.0.0.0); extra == "time" Requires-Dist: pendulum (>=1.4,<2.0.0); extra == "time"
Description-Content-Type: text/x-rst Description-Content-Type: text/x-rst
My Package My Package
......
...@@ -48,9 +48,9 @@ def test_convert_dependencies(): ...@@ -48,9 +48,9 @@ def test_convert_dependencies():
] ]
) )
main = [ main = [
'A>=1.0.0.0,<2.0.0.0', 'A>=1.0,<2.0.0',
'B>=1.0.0.0,<1.1.0.0', 'B>=1.0,<1.1.0',
'C==1.2.3.0', 'C==1.2.3',
] ]
extras = {} extras = {}
...@@ -70,11 +70,11 @@ def test_convert_dependencies(): ...@@ -70,11 +70,11 @@ def test_convert_dependencies():
] ]
) )
main = [ main = [
'B>=1.0.0.0,<1.1.0.0', 'B>=1.0,<1.1.0',
'C==1.2.3.0', 'C==1.2.3',
] ]
extras = { extras = {
'bar': ['A>=1.2.0.0'] 'bar': ['A>=1.2']
} }
assert result == (main, extras) assert result == (main, extras)
...@@ -98,20 +98,20 @@ def test_convert_dependencies(): ...@@ -98,20 +98,20 @@ def test_convert_dependencies():
] ]
) )
main = [ main = [
'B>=1.0.0.0,<1.1.0.0', 'B>=1.0,<1.1.0',
] ]
extra_python = ( extra_python = (
':(python_version >= "2.7.0.0" and python_version < "2.8.0.0") ' ':(python_version >= "2.7" and python_version < "2.8.0") '
'or (python_version >= "3.6.0.0" and python_version < "4.0.0.0")' 'or (python_version >= "3.6" and python_version < "4.0.0")'
) )
extra_d_dependency = ( extra_d_dependency = (
'baz:(python_version >= "2.7.0.0" and python_version < "2.8.0.0") ' 'baz:(python_version >= "2.7" and python_version < "2.8.0") '
'or (python_version >= "3.4.0.0" and python_version < "4.0.0.0")' 'or (python_version >= "3.4" and python_version < "4.0.0")'
) )
extras = { extras = {
extra_python: ['C==1.2.3.0'], extra_python: ['C==1.2.3'],
extra_d_dependency: ['D==3.4.5.0'], extra_d_dependency: ['D==3.4.5'],
} }
assert result == (main, extras) assert result == (main, extras)
...@@ -133,8 +133,8 @@ def test_make_setup(): ...@@ -133,8 +133,8 @@ def test_make_setup():
'my_package.sub_pkg2' 'my_package.sub_pkg2'
] ]
assert ns['install_requires'] == [ assert ns['install_requires'] == [
'cachy[msgpack]>=0.2.0.0,<0.3.0.0', 'cachy[msgpack]>=0.2.0,<0.3.0',
'cleo>=0.6.0.0,<0.7.0.0', 'cleo>=0.6,<0.7.0',
] ]
assert ns['entry_points'] == { assert ns['entry_points'] == {
'console_scripts': [ 'console_scripts': [
...@@ -144,7 +144,7 @@ def test_make_setup(): ...@@ -144,7 +144,7 @@ def test_make_setup():
} }
assert ns['extras_require'] == { assert ns['extras_require'] == {
'time': [ '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(): ...@@ -59,15 +59,15 @@ def test_to_pep_508():
dependency = Dependency('Django', '^1.23') dependency = Dependency('Django', '^1.23')
result = dependency.to_pep_508() 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 = Dependency('Django', '^1.23')
dependency.python_versions = '~2.7 || ^3.6' dependency.python_versions = '~2.7 || ^3.6'
result = dependency.to_pep_508() result = dependency.to_pep_508()
assert result == 'Django (>=1.23.0.0,<2.0.0.0); ' \ assert result == 'Django (>=1.23,<2.0.0); ' \
'(python_version >= "2.7.0.0" and python_version < "2.8.0.0") ' \ '(python_version >= "2.7" and python_version < "2.8.0") ' \
'or (python_version >= "3.6.0.0" and python_version < "4.0.0.0")' 'or (python_version >= "3.6" and python_version < "4.0.0")'
def test_to_pep_508_wilcard(): def test_to_pep_508_wilcard():
...@@ -82,21 +82,21 @@ def test_to_pep_508_in_extras(): ...@@ -82,21 +82,21 @@ def test_to_pep_508_in_extras():
dependency.in_extras.append('foo') dependency.in_extras.append('foo')
result = dependency.to_pep_508() 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') dependency.in_extras.append('bar')
result = dependency.to_pep_508() 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' dependency.python_versions = '~2.7 || ^3.6'
result = dependency.to_pep_508() result = dependency.to_pep_508()
assert result == ( 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") ' '(python_version >= "2.7" and python_version < "2.8.0") '
'or (python_version >= "3.6.0.0" and python_version < "4.0.0.0")' 'or (python_version >= "3.6" and python_version < "4.0.0")'
') ' ') '
'and (extra == "foo" or extra == "bar")' 'and (extra == "foo" or extra == "bar")'
) )
...@@ -14,7 +14,7 @@ def test_dependency_from_pep_508_with_version(): ...@@ -14,7 +14,7 @@ def test_dependency_from_pep_508_with_version():
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == 'requests' 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(): def test_dependency_from_pep_508_with_parens():
...@@ -22,7 +22,7 @@ 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) dep = dependency_from_pep_508(name)
assert dep.name == 'requests' 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(): def test_dependency_from_pep_508_with_constraint():
...@@ -30,7 +30,7 @@ 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) dep = dependency_from_pep_508(name)
assert dep.name == 'requests' 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(): def test_dependency_from_pep_508_with_extras():
...@@ -38,7 +38,7 @@ 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) dep = dependency_from_pep_508(name)
assert dep.name == 'requests' assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0' assert str(dep.constraint) == '2.18.0'
assert dep.extras == ['foo', 'bar'] assert dep.extras == ['foo', 'bar']
...@@ -50,7 +50,7 @@ def test_dependency_from_pep_508_with_python_version(): ...@@ -50,7 +50,7 @@ def test_dependency_from_pep_508_with_python_version():
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == 'requests' assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0' assert str(dep.constraint) == '2.18.0'
assert dep.extras == [] assert dep.extras == []
assert dep.python_versions == '~2.7 || ~2.6' assert dep.python_versions == '~2.7 || ~2.6'
...@@ -63,7 +63,7 @@ def test_dependency_from_pep_508_with_single_python_version(): ...@@ -63,7 +63,7 @@ def test_dependency_from_pep_508_with_single_python_version():
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == 'requests' assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0' assert str(dep.constraint) == '2.18.0'
assert dep.extras == [] assert dep.extras == []
assert dep.python_versions == '~2.7' assert dep.python_versions == '~2.7'
...@@ -76,7 +76,7 @@ def test_dependency_from_pep_508_with_platform(): ...@@ -76,7 +76,7 @@ def test_dependency_from_pep_508_with_platform():
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == 'requests' assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0' assert str(dep.constraint) == '2.18.0'
assert dep.extras == [] assert dep.extras == []
assert dep.python_versions == '*' assert dep.python_versions == '*'
assert dep.platform == 'win32 || darwin' assert dep.platform == 'win32 || darwin'
...@@ -92,7 +92,7 @@ def test_dependency_from_pep_508_complex(): ...@@ -92,7 +92,7 @@ def test_dependency_from_pep_508_complex():
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == 'requests' 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.extras == ['foo']
assert dep.python_versions == '>=2.7 !=3.2.*' assert dep.python_versions == '>=2.7 !=3.2.*'
assert dep.platform == 'win32 || darwin' assert dep.platform == 'win32 || darwin'
...@@ -106,7 +106,7 @@ def test_dependency_python_version_in(): ...@@ -106,7 +106,7 @@ def test_dependency_python_version_in():
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == 'requests' 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.*' assert dep.python_versions == '3.3.* || 3.4.* || 3.5.*'
...@@ -118,7 +118,7 @@ def test_dependency_platform_in(): ...@@ -118,7 +118,7 @@ def test_dependency_platform_in():
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == 'requests' assert dep.name == 'requests'
assert str(dep.constraint) == '== 2.18.0.0' assert str(dep.constraint) == '2.18.0'
assert dep.platform == 'win32 || darwin' assert dep.platform == 'win32 || darwin'
...@@ -127,7 +127,7 @@ def test_dependency_with_extra(): ...@@ -127,7 +127,7 @@ def test_dependency_with_extra():
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == 'requests' 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 len(dep.extras) == 1
assert dep.extras[0] == 'security' 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