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)
from typing import Dict
from typing import List
from .incompatibility_cause import ConflictCause
from .incompatibility_cause import DependencyCause
from .incompatibility_cause import IncompatibilityCause
from .incompatibility_cause import NoVersionsCause
from .incompatibility_cause import PackageNotFoundCause
from .incompatibility_cause import PlatformCause
from .incompatibility_cause import PythonCause
from .incompatibility_cause import RootCause
from .term import Term
class Incompatibility:
def __init__(self, terms, cause): # type: (List[Term], IncompatibilityCause) -> None
# Remove the root package from generated incompatibilities, since it will
# always be satisfied. This makes error reporting clearer, and may also
# make solving more efficient.
if (
len(terms) != 1
and isinstance(cause, ConflictCause)
and any([term.is_positive() and term.dependency.is_root for term in terms])
):
terms = [
term for term in terms
if not term.is_positive() or not term.dependency.is_root
]
if (
len(terms) == 1
# Short-circuit in the common case of a two-term incompatibility with
# two different packages (for example, a dependency).
or len(terms) == 2 and terms[0].dependency.name != terms[-1].dependency.name
):
pass
else:
# Coalesce multiple terms about the same package if possible.
by_name = {} # type: Dict[str, Dict[str, Term]]
for term in terms:
if term.dependency.name not in by_name:
by_name[term.dependency.name] = {}
by_ref = by_name[term.dependency.name]
ref = term.dependency.name
if ref in by_ref:
by_ref[ref] = by_ref[ref].intersect(term)
# If we have two terms that refer to the same package but have a null
# intersection, they're mutually exclusive, making this incompatibility
# irrelevant, since we already know that mutually exclusive version
# ranges are incompatible. We should never derive an irrelevant
# incompatibility.
assert by_ref[ref] is not None
else:
by_ref[ref] = term
new_terms = []
for by_ref in by_name.values():
positive_terms = [term for term in by_ref.values() if term.is_positive()]
if positive_terms:
new_terms += positive_terms
continue
new_terms += list(by_ref.values())
terms = new_terms
self._terms = terms
self._cause = cause
@property
def terms(self): # type: () -> List[Term]
return self._terms
@property
def cause(self): # type: () -> IncompatibilityCause
return self._cause
def is_failure(self): # type: () -> bool
return len(self._terms) == 0 or (len(self._terms) == 1 and self._terms[0].dependency.is_root)
def __str__(self):
if isinstance(self._cause, DependencyCause):
assert len(self._terms) == 2
depender = self._terms[0]
dependee = self._terms[1]
assert depender.is_positive()
assert not dependee.is_positive()
return '{} depends on {}'.format(
self._terse(depender, allow_every=True),
self._terse(dependee)
)
elif isinstance(self._cause, PythonCause):
assert len(self._terms) == 1
assert self._terms[0].is_positive()
cause = self._cause # type: PythonCause
text = '{} requires '.format(self._terse(self._terms[0], allow_every=True))
text += 'Python {}'.format(cause.python_version)
return text
elif isinstance(self._cause, PlatformCause):
assert len(self._terms) == 1
assert self._terms[0].is_positive()
cause = self._cause # type: PlatformCause
text = '{} requires '.format(self._terse(self._terms[0], allow_every=True))
text += 'platform {}'.format(cause.platform)
return text
elif isinstance(self._cause, NoVersionsCause):
assert len(self._terms) == 1
assert self._terms[0].is_positive()
return 'no versions of {} match {}'.format(
self._terms[0].dependency.name,
self._terms[0].constraint
)
elif isinstance(self._cause, PackageNotFoundCause):
assert len(self._terms) == 1
assert self._terms[0].is_positive()
return '{} doesn\'t exist'.format(self._terms[0].dependency.name)
elif isinstance(self._cause, RootCause):
assert len(self._terms) == 1
assert not self._terms[0].is_positive()
assert self._terms[0].dependency.is_root
return '{} is {}'.format(
self._terms[0].dependency.name,
self._terms[0].dependency.constraint
)
elif self.is_failure():
return 'version solving failed'
if len(self._terms) == 1:
term = self._terms[0]
if term.constraint.is_any():
return '{} is {}'.format(
term.dependency.name,
'forbidden' if term.is_positive() else 'required'
)
else:
return '{} is {}'.format(
term.dependency.name,
'forbidden' if term.is_positive() else 'required'
)
if len(self._terms) == 2:
term1 = self._terms[0]
term2 = self._terms[1]
if term1.is_positive() == term2.is_positive():
if term1.is_positive():
package1 = term1.dependency.name if term1.constraint.is_any() else self._terse(term1)
package2 = term2.dependency.name if term2.constraint.is_any() else self._terse(term2)
return '{} is incompatible with {}'.format(package1, package2)
else:
return 'either {} or {}'.format(
self._terse(term1),
self._terse(term2)
)
positive = []
negative = []
for term in self._terms:
if term.is_positive():
positive.append(self._terse(term))
else:
negative.append(self._terse(term))
if positive and negative:
if len(positive) == 1:
positive_term = [term for term in self._terms if term.is_positive()][0]
return '{} requires {}'.format(
self._terse(positive_term, allow_every=True),
' or '.join(negative)
)
else:
return 'if {} then {}'.format(
' and '.join(positive),
' or '.join(negative)
)
elif positive:
return 'one of {} must be false'.format(' or '.join(positive))
else:
return 'one of {} must be true'.format(' or '.join(negative))
def and_to_string(self, other, details, this_line, other_line
): # type: (Incompatibility, dict, int, int) -> str
requires_both = self._try_requires_both(other, details, this_line, other_line)
if requires_both is not None:
return requires_both
requires_through = self._try_requires_through(other, details, this_line, other_line)
if requires_through is not None:
return requires_through
requires_forbidden = self._try_requires_forbidden(other, details, this_line, other_line)
if requires_forbidden is not None:
return requires_forbidden
buffer = [str(self)]
if this_line is not None:
buffer.append(' ' + this_line)
buffer.append(' and {}'.format(str(other)))
if other_line is not None:
buffer.append(' ' + other_line)
return '\n'.join(buffer)
def _try_requires_both(self, other, details, this_line, other_line
): # type: (Incompatibility, dict, int, int) -> str
if len(self._terms) == 1 or len(other.terms) == 1:
return
this_positive = self._single_term_where(lambda term: term.is_positive())
if this_positive is None:
return
other_positive = other._single_term_where(lambda term: term.is_positive())
if other_positive is None:
return
if this_positive.dependency != other_positive.dependency:
return
this_negatives = ' or '.join([
self._terse(term)
for term in self._terms
if not term.is_positive()
])
other_negatives = ' or '.join([
self._terse(term)
for term in other.terms
if not term.is_positive()
])
buffer = [self._terse(this_positive, allow_every=True) + ' ']
is_dependency = isinstance(self.cause, DependencyCause) and isinstance(other.cause, DependencyCause)
if is_dependency:
buffer.append('depends on')
else:
buffer.append('requires')
buffer.append(' both {}'.format(this_negatives))
if this_line is not None:
buffer.append(' ({})'.format(this_line))
buffer.append(' and {}'.format(other_negatives))
if other_line is not None:
buffer.append(' ({})'.format(other_line))
return ''.join(buffer)
def _try_requires_through(self, other, details, this_line, other_line
): # type: (Incompatibility, dict, int, int) -> str
if len(self._terms) == 1 or len(other.terms) == 1:
return
this_negative = self._single_term_where(lambda term: not term.is_positive())
other_negative = other._single_term_where(lambda term: not term.is_positive())
if this_negative is None and other_negative is None:
return
this_positive = self._single_term_where(lambda term: term.is_positive())
other_positive = self._single_term_where(lambda term: term.is_positive())
if (
this_negative is not None
and other_positive is not None
and this_negative.dependency.name == other_positive.dependency.name
and this_negative.inverse.satisfies(other_positive)
):
prior = self
prior_negative = this_negative
prior_line = this_line
latter = other
latter_line = other_line
elif (
other_negative is not None
and this_positive is not None
and other_negative.dependency.name == this_positive.dependency.name
and other_negative.inverse.satisfies(this_positive)
):
prior = other
prior_negative = other_negative
prior_line = other_line
latter = self
latter_line = this_line
else:
return
prior_positives = [term for term in prior.terms if term.is_positive()]
buffer = []
if len(prior_positives) > 1:
prior_string = ' or '.join([
self._terse(term) for term in prior_positives
])
buffer.append('if {} then '.format(prior_string))
else:
if isinstance(prior.cause, DependencyCause):
verb = 'depends on'
else:
verb = 'requires'
buffer.append(
'{} {} '.format(
self._terse(prior_positives[0], allow_every=True),
verb
)
)
buffer.append(self._terse(prior_negative))
if prior_line is not None:
buffer.append(' ({})'.format(prior_line))
buffer.append(' which ')
if isinstance(latter.cause, DependencyCause):
buffer.append('depends on ')
else:
buffer.append('requires ')
buffer.append(
' or '.join([
self._terse(term) for term in latter.terms
if not term.is_positive()
])
)
if latter_line is not None:
buffer.append(' ({})'.format(latter_line))
return ''.join(buffer)
def _try_requires_forbidden(self, other, details, this_line, other_line
): # type: (Incompatibility, dict, int, int) -> str
if len(self._terms) != 1 and len(other.terms) != 1:
return None
if len(self.terms) == 1:
prior = other
latter = self
prior_line = other_line
latter_line = this_line
else:
prior = self
latter = other
prior_line = this_line
latter_line = other_line
negative = prior._single_term_where(lambda term: not term.is_positive())
if negative is None:
return
if not negative.inverse.satisfies(latter.terms[0]):
return
positives = [t for t in prior.terms if t.is_positive()]
buffer = []
if len(positives) > 1:
prior_string = ' or '.join([
self._terse(term) for term in positives
])
buffer.append('if {} then '.format(prior_string))
else:
buffer.append(self._terse(positives[0], allow_every=True))
if isinstance(prior.cause, DependencyCause):
buffer.append(' depends on ')
else:
buffer.append(' requires ')
buffer.append(self._terse(latter.terms[0]) + ' ')
if prior_line is not None:
buffer.append('({}) '.format(prior_line))
if isinstance(latter.cause, PythonCause):
cause = latter.cause # type: PythonCause
buffer.append('which requires Python {}'.format(cause.python_version))
elif isinstance(latter.cause, NoVersionsCause):
buffer.append('which doesn\'t match any versions')
elif isinstance(latter.cause, PackageNotFoundCause):
buffer.append('which doesn\'t exist')
else:
buffer.append('which is forbidden')
if latter_line is not None:
buffer.append(' ({})'.format(latter_line))
return ''.join(buffer)
def _terse(self, term, allow_every=False):
if allow_every and term.constraint.is_any():
return 'every version of {}'.format(term.dependency.name)
return str(term.dependency)
def _single_term_where(self, callable): # type: (callable) -> Term
found = None
for term in self._terms:
if not callable(term):
continue
if found is not None:
return
found = term
return found
def __repr__(self):
return '<Incompatibility {}>'.format(str(self))
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 time
from typing import Dict
from typing import List
from typing import Union
from poetry.packages import Dependency
from poetry.semver.semver import Version
from poetry.semver.semver import VersionRange
from .failure import SolveFailure
from .incompatibility import Incompatibility
from .incompatibility_cause import ConflictCause
from .incompatibility_cause import DependencyCause
from .incompatibility_cause import NoVersionsCause
from .incompatibility_cause import PackageNotFoundCause
from .incompatibility_cause import RootCause
from .partial_solution import PartialSolution
from .result import SolverResult
from .set_relation import SetRelation
from .term import Term
_conflict = object()
class VersionSolver:
def __init__(self, root, provider, locked=None, use_latest=None):
self._root = root
self._provider = provider
self._locked = locked or {}
if use_latest is None:
use_latest = []
self._use_latest = use_latest
self._incompatibilities = {} # type: Dict[str, List[Incompatibility]]
self._solution = PartialSolution()
@property
def solution(self): # type: () -> PartialSolution
return self._solution
def solve(self):
"""
Finds a set of dependencies that match the root package's constraints,
or raises an error if no such set is available.
"""
start = time.time()
root_dependency = Dependency(self._root.name, self._root.version)
root_dependency.is_root = True
self._add_incompatibility(
Incompatibility(
[Term(root_dependency, False)],
RootCause()
)
)
try:
next = self._root.name
while next is not None:
self._propagate(next)
next = self._choose_package_version()
return self._result()
except Exception:
raise
finally:
self._log(
'Version solving took {:.3f} seconds.\n'
'Tried {} solutions.'
.format(
time.time() - start,
self._solution.attempted_solutions
)
)
def _propagate(self, package): # type: (str) -> None
"""
Performs unit propagation on incompatibilities transitively
related to package to derive new assignments for _solution.
"""
changed = set()
changed.add(package)
while changed:
package = changed.pop()
# Iterate in reverse because conflict resolution tends to produce more
# general incompatibilities as time goes on. If we look at those first,
# we can derive stronger assignments sooner and more eagerly find
# conflicts.
for incompatibility in reversed(self._incompatibilities[package]):
result = self._propagate_incompatibility(incompatibility)
if result is _conflict:
# If [incompatibility] is satisfied by [_solution], we use
# [_resolveConflict] to determine the root cause of the conflict as a
# new incompatibility. It also backjumps to a point in [_solution]
# where that incompatibility will allow us to derive new assignments
# that avoid the conflict.
root_cause = self._resolve_conflict(incompatibility)
# Back jumping erases all the assignments we did at the previous
# decision level, so we clear [changed] and refill it with the
# newly-propagated assignment.
changed.clear()
changed.add(str(self._propagate_incompatibility(root_cause)))
break
elif result is not None:
changed.add(result)
def _propagate_incompatibility(self, incompatibility
): # type: (Incompatibility) -> Union[str, None]
"""
If [incompatibility] is [almost satisfied][] by [_solution], adds the
negation of the unsatisfied term to [_solution].
If [incompatibility] is satisfied by [_solution], returns `#conflict`. If
[incompatibility] is almost satisfied by [_solution], returns the
unsatisfied term's package name. Otherwise, returns None.
"""
# The first entry in `incompatibility.terms` that's not yet satisfied by
# [_solution], if one exists. If we find more than one, [_solution] is
# inconclusive for [incompatibility] and we can't deduce anything.
unsatisfied = None
for term in incompatibility.terms:
relation = self._solution.relation(term)
if relation == SetRelation.DISJOINT:
# If [term] is already contradicted by [_solution], then
# [incompatibility] is contradicted as well and there's nothing new we
# can deduce from it.
return
elif relation == SetRelation.OVERLAPPING:
# If more than one term is inconclusive, we can't deduce anything about
# [incompatibility].
if unsatisfied is not None:
return
# If exactly one term in [incompatibility] is inconclusive, then it's
# almost satisfied and [term] is the unsatisfied term. We can add the
# inverse of the term to [_solution].
unsatisfied = term
# If *all* terms in [incompatibility] are satisfied by [_solution], then
# [incompatibility] is satisfied and we have a conflict.
if unsatisfied is None:
return _conflict
self._log(
'derived: {}{}'.format(
'not ' if unsatisfied.is_positive() else '',
unsatisfied.dependency
)
)
self._solution.derive(
unsatisfied.dependency,
not unsatisfied.is_positive(),
incompatibility
)
return unsatisfied.dependency.name
def _resolve_conflict(self, incompatibility
): # type: (Incompatibility) -> Incompatibility
self._log('<fg=red;options=bold>conflict</>: {}'.format(incompatibility))
new_incompatibility = False
while not incompatibility.is_failure():
# The term in `incompatibility.terms` that was most recently satisfied by
# [_solution].
most_recent_term = None
# The earliest assignment in [_solution] such that [incompatibility] is
# satisfied by [_solution] up to and including this assignment.
most_recent_satisfier = None
# The difference between [most_recent_satisfier] and [most_recent_term];
# that is, the versions that are allowed by [most_recent_satisfier] and not
# by [most_recent_term]. This is `null` if [most_recent_satisfier] totally
# satisfies [most_recent_term].
difference = None
# The decision level of the earliest assignment in [_solution] *before*
# [most_recent_satisfier] such that [incompatibility] is satisfied by
# [_solution] up to and including this assignment plus
# [most_recent_satisfier].
#
# Decision level 1 is the level where the root package was selected. It's
# safe to go back to decision level 0, but stopping at 1 tends to produce
# better error messages, because references to the root package end up
# closer to the final conclusion that no solution exists.
previous_satisfier_level = 1
for term in incompatibility.terms:
satisfier = self._solution.satisfier(term)
if most_recent_satisfier is None:
most_recent_term = term
most_recent_satisfier= satisfier
elif most_recent_satisfier.index < satisfier.index:
previous_satisfier_level = max(
previous_satisfier_level,
most_recent_satisfier.decision_level
)
most_recent_term = term
most_recent_satisfier = satisfier
difference = None
else:
previous_satisfier_level = max(
previous_satisfier_level, satisfier.decision_level
)
if most_recent_term == term:
# If [most_recent_satisfier] doesn't satisfy [most_recent_term] on its
# own, then the next-most-recent satisfier may be the one that
# satisfies the remainder.
difference = most_recent_satisfier.difference(most_recent_term)
if difference is not None:
previous_satisfier_level = max(
previous_satisfier_level,
self._solution.satisfier(difference.inverse).decision_level
)
# If [mostRecentSatisfier] is the only satisfier left at its decision
# level, or if it has no cause (indicating that it's a decision rather
# than a derivation), then [incompatibility] is the root cause. We then
# backjump to [previousSatisfierLevel], where [incompatibility] is
# guaranteed to allow [_propagate] to produce more assignments.
if (
previous_satisfier_level < most_recent_satisfier.decision_level
or most_recent_satisfier.cause is None
):
self._solution.backtrack(previous_satisfier_level)
if new_incompatibility:
self._add_incompatibility(incompatibility)
return incompatibility
# Create a new incompatibility by combining [incompatibility] with the
# incompatibility that caused [mostRecentSatisfier] to be assigned. Doing
# this iteratively constructs an incompatibility that's guaranteed to be
# true (that is, we know for sure no solution will satisfy the
# incompatibility) while also approximating the intuitive notion of the
# "root cause" of the conflict.
new_terms = []
for term in incompatibility.terms:
if term != most_recent_term:
new_terms.append(term)
for term in most_recent_satisfier.cause.terms:
if term.dependency != most_recent_satisfier.dependency:
new_terms.append(term)
# The [mostRecentSatisfier] may not satisfy [mostRecentTerm] on its own
# if there are a collection of constraints on [mostRecentTerm] that
# only satisfy it together. For example, if [mostRecentTerm] is
# `foo ^1.0.0` and [_solution] contains `[foo >=1.0.0,
# foo <2.0.0]`, then [mostRecentSatisfier] will be `foo <2.0.0` even
# though it doesn't totally satisfy `foo ^1.0.0`.
#
# In this case, we add `not (mostRecentSatisfier \ mostRecentTerm)` to
# the incompatibility as well, See [the algorithm documentation][] for
# details.
if difference is not None:
new_terms.append(difference.inverse)
incompatibility = Incompatibility(
new_terms, ConflictCause(incompatibility, most_recent_satisfier.cause)
)
new_incompatibility = True
partially = '' if difference is None else ' partially'
bang = '!'
self._log('{} {} is{} satisfied by {}'.format(
bang, most_recent_term, partially, most_recent_satisfier)
)
self._log('{} which is caused by "{}"'.format(bang, most_recent_satisfier.cause))
self._log('{} thus: {}'.format(bang, incompatibility))
raise SolveFailure(incompatibility)
def _choose_package_version(self): # type: () -> Union[str, None]
"""
Tries to select a version of a required package.
"""
unsatisfied = self._solution.unsatisfied
if not unsatisfied:
return
def _get_min(dependency):
if dependency.name in self._use_latest:
return 1
if dependency.name in self._locked:
return 1
try:
return len(self._provider.search_for(dependency))
except ValueError:
return 0
if len(unsatisfied) == 1:
dependency = unsatisfied[0]
else:
dependency = min(*unsatisfied, key=_get_min)
locked = self._get_locked(dependency.name)
if locked is None or not dependency.constraint.allows(locked.version):
try:
packages = self._provider.search_for(dependency)
except ValueError as e:
self._add_incompatibility(
Incompatibility([Term(dependency, True)], PackageNotFoundCause(e))
)
return dependency.name
try:
version = packages[0]
except IndexError:
version = None
else:
version = self._locked[dependency.name]
if version is None:
self._add_incompatibility(
Incompatibility([Term(dependency, True)], NoVersionsCause())
)
return dependency.name
conflict = False
for incompatibility in self._provider.incompatibilities_for(version):
self._add_incompatibility(incompatibility)
conflict = conflict or all([
term.dependency.name == dependency.name or self._solution.satisfies(term)
for term in incompatibility.terms
])
if not conflict:
self._solution.decide(version)
self._log('selecting {}'.format(version))
return dependency.name
def _excludes_single_version(self, constraint): # type: (Any) -> bool
return isinstance(VersionRange().difference(constraint), Version)
def _result(self): # type: () -> SolverResult
decisions = self._solution.decisions
return SolverResult(
self._root,
[p for p in decisions if not p.is_root()],
self._solution.attempted_solutions
)
def _add_incompatibility(self, incompatibility): # type: (Incompatibility) -> None
self._log("fact: {}".format(incompatibility))
for term in incompatibility.terms:
if term.dependency.name not in self._incompatibilities:
self._incompatibilities[term.dependency.name] = []
if incompatibility in self._incompatibilities[term.dependency.name]:
continue
self._incompatibilities[term.dependency.name].append(incompatibility)
def _get_locked(self, package_name): # type: (str) -> Union[Any, None]
if package_name in self._use_latest:
return
return self._locked.get(package_name)
def _log(self, text):
self._provider.debug(text)
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))
import re
from typing import List
from typing import Union
from .empty_constraint import EmptyConstraint
from .patterns import COMPLETE_VERSION
from .version_constraint import VersionConstraint
from .version_range import VersionRange
from .version_union import VersionUnion
class Version(VersionRange):
"""
A parsed semantic version number.
"""
def __init__(self, major, minor=None, patch=None, pre=None, build=None, text=None
): # type: (int, int, int, Union[str, None], Union[str, None], Union[str, None])
self._major = int(major)
self._precision = 1
if minor is None:
minor = 0
else:
self._precision += 1
self._minor = int(minor)
if patch is None:
patch = 0
else:
self._precision += 1
self._patch = int(patch)
if text is None:
text = '{}.{}.{}'.format(major, minor, patch)
if pre:
text += '-{}'.format(pre)
if build:
text += '+{}'.format(build)
self._text = text
pre = self._normalize_prerelease(pre)
self._prerelease = []
if pre is not None:
self._prerelease = self._split_parts(pre)
self._build = []
if build is not None:
if build.startswith(('-', '+')):
build = build[1:]
self._build = self._split_parts(build)
@property
def major(self): # type: () -> int
return self._major
@property
def minor(self): # type: () -> int
return self._minor
@property
def patch(self): # type: () -> int
return self._patch
@property
def prerelease(self): # type: () -> List[str]
return self._prerelease
@property
def build(self): # type: () -> List[str]
return self._build
@property
def text(self):
return self._text
@property
def stable(self):
if not self.is_prerelease():
return self
return self.next_patch
@property
def next_major(self): # type: () -> Version
if self.is_prerelease() and self.minor == 0 and self.patch == 0:
return Version(self.major, self.minor, self.patch)
return self._increment_major()
@property
def next_minor(self): # type: () -> Version
if self.is_prerelease() and self.patch == 0:
return Version(self.major, self.minor, self.patch)
return self._increment_minor()
@property
def next_patch(self): # type: () -> Version
if self.is_prerelease():
return Version(self.major, self.minor, self.patch)
return self._increment_patch()
@property
def next_breaking(self): # type: () -> Version
if self.major == 0:
if self.minor != 0:
return self._increment_minor()
if self._precision == 1:
return self._increment_major()
elif self._precision == 2:
return self._increment_minor()
return self._increment_patch()
return self._increment_major()
@property
def first_prerelease(self): # type: () -> Version
return Version(self.major, self.minor, self.patch, '0')
@property
def min(self):
return self
@property
def max(self):
return self
@property
def include_min(self):
return True
@property
def include_max(self):
return True
@classmethod
def parse(cls, text): # type: (str) -> Version
match = COMPLETE_VERSION.match(text)
if match is None:
raise ValueError('Unable to parse "{}".'.format(text))
text = text.rstrip('.')
major = int(match[1])
minor = int(match[2]) if match[2] else None
patch = int(match[3]) if match[3] else None
pre = match[4]
build = match[5]
if build:
build = build.lstrip('+')
return Version(major, minor, patch, pre, build, text)
def is_any(self):
return False
def is_empty(self):
return False
def is_prerelease(self): # type: () -> bool
return len(self._prerelease) > 0
def allows(self, version): # type: (Version) -> bool
return self == version
def allows_all(self, other): # type: (VersionConstraint) -> bool
return other.is_empty() or other == self
def allows_any(self, other): # type: (VersionConstraint) -> bool
return other.allows(self)
def intersect(self, other): # type: (VersionConstraint) -> VersionConstraint
if other.allows(self):
return self
return EmptyConstraint()
def union(self, other): # type: (VersionConstraint) -> VersionConstraint
from .version_range import VersionRange
if other.allows(self):
return other
if isinstance(other, VersionRange):
if other.min == self:
return VersionRange(
other.min,
other.max,
include_min=True,
include_max=other.include_max
)
if other.max == self:
return VersionRange(
other.min,
other.max,
include_min=other.include_min,
include_max=True
)
return VersionUnion.of(self, other)
def difference(self, other): # type: (VersionConstraint) -> VersionConstraint
if other.allows(self):
return EmptyConstraint()
return self
def _increment_major(self): # type: () -> Version
return Version(self.major + 1, 0, 0)
def _increment_minor(self): # type: () -> Version
return Version(self.major, self.minor + 1, 0)
def _increment_patch(self): # type: () -> Version
return Version(self.major, self.minor, self.patch + 1)
def _normalize_prerelease(self, pre): # type: (str) -> str
if not pre:
return
m = re.match('(?i)^(a|alpha|b|beta|c|pre|rc|dev)[-.]?(\d+)?$', pre)
if not m:
return
modifier = m.group(1)
number = m.group(2)
if number is None:
number = 0
if modifier == 'a':
modifier = 'alpha'
elif modifier == 'b':
modifier = 'beta'
elif modifier in {'c', 'pre'}:
modifier = 'rc'
elif modifier == 'dev':
modifier = 'alpha'
return '{}.{}'.format(modifier, number)
def _split_parts(self, text): # type: (str) -> List[Union[str, int]]
parts = text.split('.')
for i, part in enumerate(parts):
try:
parts[i] = int(part)
except (TypeError, ValueError):
continue
return parts
def __lt__(self, other):
return self._cmp(other) < 0
def __le__(self, other):
return self._cmp(other) <= 0
def __gt__(self, other):
return self._cmp(other) > 0
def __ge__(self, other):
return self._cmp(other) >= 0
def _cmp(self, other):
if not isinstance(other, VersionConstraint):
return NotImplemented
if not isinstance(other, Version):
return -other._cmp(self)
if self.major != other.major:
return self._cmp_parts(self.major, other.major)
if self.minor != other.minor:
return self._cmp_parts(self.minor, other.minor)
if self.patch != other.patch:
return self._cmp_parts(self.patch, other.patch)
# Pre-releases always come before no pre-release string.
if not self.is_prerelease() and other.is_prerelease():
return 1
if not other.is_prerelease() and self.is_prerelease():
return -1
comparison = self._cmp_lists(self.prerelease, other.prerelease)
if comparison != 0:
return comparison
# Builds always come after no build string.
if not self.build and other.build:
return -1
if not other.build and self.build:
return 1
return self._cmp_lists(self.build, other.build)
def _cmp_parts(self, a, b):
if a < b:
return -1
elif a > b:
return 1
return 0
def _cmp_lists(self, a, b): # type: (List, List) -> int
for i in range(max(len(a), len(b))):
a_part = None
if i < len(a):
a_part = a[i]
b_part = None
if i < len(b):
b_part = b[i]
if a_part == b_part:
continue
# Missing parts come before present ones.
if a_part is None:
return -1
if b_part is None:
return 1
if isinstance(a_part, int):
if isinstance(b_part, int):
return self._cmp_parts(a_part, b_part)
return -1
else:
if isinstance(b_part, int):
return 1
return self._cmp_parts(a_part, b_part)
return 0
def __eq__(self, other): # type: (Version) -> bool
if not isinstance(other, Version):
return NotImplemented
return (
self._major == other.major
and self._minor == other.minor
and self._patch == other.patch
and self._prerelease == other.prerelease
and self._build == other.build
)
def __str__(self):
return self._text
def __repr__(self):
return '<Version {}>'.format(str(self))
def __hash__(self):
return hash(
(self.major,
self.minor,
self.patch,
'.'.join(self.prerelease),
'.'.join(self.build)))
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
from .version_union import VersionUnion
class VersionRange(VersionConstraint):
def __init__(self,
min=None,
max=None,
include_min=False,
include_max=False,
always_include_max_prerelease=False):
self._min = min
self._max = max
self._include_min = include_min
self._include_max = include_max
@property
def min(self):
return self._min
@property
def max(self):
return self._max
@property
def include_min(self):
return self._include_min
@property
def include_max(self):
return self._include_max
def is_empty(self):
return False
def is_any(self):
return self._min is None and self._max is None
def allows(self, other): # type: (Version) -> bool
if self._min is not None:
if other < self._min:
return False
if not self._include_min and other == self._min:
return False
if self._max is not None:
if other > self._max:
return False
if not self._include_max and other == self._max:
return False
return True
def allows_all(self, other): # type: (VersionConstraint) -> bool
from .version import Version
if other.is_empty():
return True
if isinstance(other, Version):
return self.allows(other)
if isinstance(other, VersionUnion):
return all([self.allows_all(constraint) for constraint in other.ranges])
if isinstance(other, VersionRange):
return not other.allows_lower(self) and not other.allows_higher(self)
raise ValueError('Unknown VersionConstraint type {}.'.format(other))
def allows_any(self, other): # type: (VersionConstraint) -> bool
from .version import Version
if other.is_empty():
return False
if isinstance(other, Version):
return self.allows(other)
if isinstance(other, VersionUnion):
return any([self.allows_any(constraint) for constraint in other.ranges])
if isinstance(other, VersionRange):
return not other.is_strictly_lower(self) and not other.is_strictly_higher(self)
raise ValueError('Unknown VersionConstraint type {}.'.format(other))
def intersect(self, other): # type: (VersionConstraint) -> VersionConstraint
from .version import Version
if other.is_empty():
return other
if isinstance(other, VersionUnion):
return other.intersect(self)
# A range and a Version just yields the version if it's in the range.
if isinstance(other, Version):
if self.allows(other):
return other
return EmptyConstraint()
if not isinstance(other, VersionRange):
raise ValueError('Unknown VersionConstraint type {}.'.format(other))
if self.allows_lower(other):
if self.is_strictly_lower(other):
return EmptyConstraint()
intersect_min = other.min
intersect_include_min = other.include_min
else:
if other.is_strictly_lower(self):
return EmptyConstraint()
intersect_min = self._min
intersect_include_min = self._include_min
if self.allows_higher(other):
intersect_max = other.max
intersect_include_max = other.include_max
else:
intersect_max = self._max
intersect_include_max = self._include_max
if intersect_min is None and intersect_max is None:
return VersionRange()
# If the range is just a single version.
if intersect_min == intersect_max:
# Because we already verified that the lower range isn't strictly
# lower, there must be some overlap.
assert intersect_include_min and intersect_include_max
return intersect_min
# If we got here, there is an actual range.
return VersionRange(
intersect_min,
intersect_max,
intersect_include_min,
intersect_include_max
)
def union(self, other): # type: (VersionConstraint) -> VersionConstraint
from .version import Version
if isinstance(other, Version):
if self.allows(other):
return self
if other == self.min:
return VersionRange(
self.min,
self.max,
include_min=True,
include_max=self.include_max
)
if other == self.max:
return VersionRange(
self.min,
self.max,
include_min=self.include_min,
include_max=True
)
return VersionUnion.of(self, other)
if isinstance(other, VersionRange):
# If the two ranges don't overlap, we won't be able to create a single
# VersionRange for both of them.
edges_touch = (
(self.max == other.min and (self.include_max or other.include_min))
or (self.min == other.max and (self.include_min or other.include_max))
)
if not edges_touch and not self.allows_any(other):
return VersionUnion.of(self, other)
if self.allows_lower(other):
union_min = self.min
union_include_min = self.include_min
else:
union_min = other.min
union_include_min = other.include_min
if self.allows_higher(other):
union_max = self.max
union_include_max = self.include_max
else:
union_max = other.max
union_include_max = other.include_max
return VersionRange(
union_min,
union_max,
include_min=union_include_min,
include_max=union_include_max
)
return VersionUnion.of(self, other)
def difference(self, other): # type: (VersionConstraint) -> VersionConstraint
from .version import Version
if other.is_empty():
return self
if isinstance(other, Version):
if not self.allows(other):
return self
if other == self.min:
if not self.include_min:
return self
return VersionRange(
self.min,
self.max,
False,
self.include_max
)
if other == self.max:
if not self.include_max:
return self
return VersionRange(
self.min,
self.max,
self.include_min,
False
)
return VersionUnion.of(
VersionRange(
self.min,
other,
self.include_min,
False
),
VersionRange(
other,
self.max,
False,
self.include_max
)
)
elif isinstance(other, VersionRange):
if not self.allows_any(other):
return self
if not self.allows_lower(other):
before = None
elif self.min == other.min:
before = self.min
else:
before = VersionRange(
self.min,
other.min,
self.include_min,
not other.include_min
)
if not self.allows_higher(other):
after = None
elif self.max == other.max:
after = self.max
else:
after = VersionRange(
other.max,
self.max,
not other.include_max,
self.include_max
)
if before is None and after is None:
return EmptyConstraint()
if before is None:
return after
if after is None:
return before
return VersionUnion.of(before, after)
elif isinstance(other, VersionUnion):
ranges = [] # type: List[VersionRange]
current = self
for range in other.ranges:
# Skip any ranges that are strictly lower than [current].
if range.is_strictly_lower(current):
continue
# If we reach a range strictly higher than [current], no more ranges
# will be relevant so we can bail early.
if range.is_strictly_higher(current):
break
difference = current.difference(range)
if difference.is_empty():
return EmptyConstraint()
elif isinstance(difference, VersionUnion):
# If [range] split [current] in half, we only need to continue
# checking future ranges against the latter half.
ranges.append(difference.ranges[0])
current = difference.ranges[-1]
else:
current = difference
if not ranges:
return current
return VersionUnion.of(*ranges, current)
raise ValueError('Unknown VersionConstraint type {}.'.format(other))
def allows_lower(self, other): # type: (VersionRange) -> bool
if self.min is None:
return other.min is not None
if other.min is None:
return False
if self.min < other.min:
return True
if self.min > other.min:
return False
return self.include_min and not other.include_min
def allows_higher(self, other): # type: (VersionRange) -> bool
if self.max is None:
return other.max is not None
if other.max is None:
return False
if self.max < other.max:
return False
if self.max > other.max:
return True
return self.include_max and not other.include_max
def is_strictly_lower(self, other): # type: (VersionRange) -> bool
if self.max is None or other.min is None:
return False
if self.max < other.min:
return True
if self.max > other.min:
return False
return not self.include_max or not other.include_min
def is_strictly_higher(self, other): # type: (VersionRange) -> bool
return other.is_strictly_lower(self)
def is_adjacent_to(self, other): # type: (VersionRange) -> bool
if self.max != other.min:
return False
return (
self.include_max and not other.include_min
or not self.include_max and other.include_min
)
def __eq__(self, other):
if not isinstance(other, VersionRange):
return False
return (
self._min == other.min
and self._max == other.max
and self._include_min == other.include_min
and self._include_max == other.include_max
)
def __lt__(self, other):
return self._cmp(other) < 0
def __le__(self, other):
return self._cmp(other) <= 0
def __gt__(self, other):
return self._cmp(other) > 0
def __ge__(self, other):
return self._cmp(other) >= 0
def _cmp(self, other): # type: (VersionRange) -> int
if self.min is None:
if other.min is None:
return self._compare_max(other)
return -1
elif other.min is None:
return 1
result = self.min._cmp(other.min)
if result != 0:
return result
if self.include_min != other.include_min:
return -1 if self.include_min else 1
return self._compare_max(other)
def _compare_max(self, other): # type: (VersionRange) -> int
if self.max is None:
if other.max is None:
return 0
return 1
elif other.max is None:
return -1
result = self.max._cmp(other.max)
if result != 0:
return result
if self.include_max != other.include_max:
return 1 if self.include_max else -1
return 0
def __str__(self):
text = ''
if self.min is not None:
text += '>=' if self.include_min else '>'
text += self.min.text
if self.max is not None:
if self.min is not None:
text += ','
text += '{}{}'.format('<=' if self.include_max else '<', self.max.text)
if self.min is None and self.max is None:
return '*'
return text
def __repr__(self):
return '<VersionRange ({})>'.format(str(self))
def __hash__(self):
return hash((self.min, self.max, self.include_min, self.include_max))
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)
from ...helpers import get_package
from ..helpers import add_to_repo
from ..helpers import check_solver_result
def test_with_compatible_locked_dependencies(root, provider, repo):
root.add_dependency('foo', '*')
add_to_repo(repo, 'foo', '1.0.0', deps={'bar': '1.0.0'})
add_to_repo(repo, 'foo', '1.0.1', deps={'bar': '1.0.1'})
add_to_repo(repo, 'foo', '1.0.2', deps={'bar': '1.0.2'})
add_to_repo(repo, 'bar', '1.0.0')
add_to_repo(repo, 'bar', '1.0.1')
add_to_repo(repo, 'bar', '1.0.2')
check_solver_result(
root, provider,
result={
'foo': '1.0.1',
'bar': '1.0.1',
},
locked={
'foo': get_package('foo', '1.0.1'),
'bar': get_package('bar', '1.0.1'),
}
)
def test_with_incompatible_locked_dependencies(root, provider, repo):
root.add_dependency('foo', '>1.0.1')
add_to_repo(repo, 'foo', '1.0.0', deps={'bar': '1.0.0'})
add_to_repo(repo, 'foo', '1.0.1', deps={'bar': '1.0.1'})
add_to_repo(repo, 'foo', '1.0.2', deps={'bar': '1.0.2'})
add_to_repo(repo, 'bar', '1.0.0')
add_to_repo(repo, 'bar', '1.0.1')
add_to_repo(repo, 'bar', '1.0.2')
check_solver_result(
root, provider,
result={
'foo': '1.0.2',
'bar': '1.0.2',
},
locked={
'foo': get_package('foo', '1.0.1'),
'bar': get_package('bar', '1.0.1'),
}
)
def test_with_unrelated_locked_dependencies(root, provider, repo):
root.add_dependency('foo', '*')
add_to_repo(repo, 'foo', '1.0.0', deps={'bar': '1.0.0'})
add_to_repo(repo, 'foo', '1.0.1', deps={'bar': '1.0.1'})
add_to_repo(repo, 'foo', '1.0.2', deps={'bar': '1.0.2'})
add_to_repo(repo, 'bar', '1.0.0')
add_to_repo(repo, 'bar', '1.0.1')
add_to_repo(repo, 'bar', '1.0.2')
add_to_repo(repo, 'baz', '1.0.0')
check_solver_result(
root, provider,
result={
'foo': '1.0.2',
'bar': '1.0.2',
},
locked={
'baz': get_package('baz', '1.0.1'),
}
)
def test_unlocks_dependencies_if_necessary_to_ensure_that_a_new_dependency_is_statisfied(root, provider, repo):
root.add_dependency('foo')
root.add_dependency('newdep', '2.0.0')
add_to_repo(repo, 'foo', '1.0.0', deps={'bar': '<2.0.0'})
add_to_repo(repo, 'bar', '1.0.0', deps={'baz': '<2.0.0'})
add_to_repo(repo, 'baz', '1.0.0', deps={'qux': '<2.0.0'})
add_to_repo(repo, 'qux', '1.0.0')
add_to_repo(repo, 'foo', '2.0.0', deps={'bar': '<3.0.0'})
add_to_repo(repo, 'bar', '2.0.0', deps={'baz': '<3.0.0'})
add_to_repo(repo, 'baz', '2.0.0', deps={'qux': '<3.0.0'})
add_to_repo(repo, 'qux', '2.0.0')
add_to_repo(repo, 'newdep', '2.0.0', deps={'baz': '>=1.5.0'})
check_solver_result(
root, provider,
result={
'foo': '2.0.0',
'bar': '2.0.0',
'baz': '2.0.0',
'qux': '1.0.0',
'newdep': '2.0.0',
},
locked={
'foo': get_package('foo', '2.0.0'),
'bar': get_package('bar', '1.0.0'),
'baz': get_package('baz', '1.0.0'),
'qux': get_package('qux', '1.0.0'),
}
)
def test_with_compatible_locked_dependencies_use_latest(root, provider, repo):
root.add_dependency('foo', '*')
root.add_dependency('baz', '*')
add_to_repo(repo, 'foo', '1.0.0', deps={'bar': '1.0.0'})
add_to_repo(repo, 'foo', '1.0.1', deps={'bar': '1.0.1'})
add_to_repo(repo, 'foo', '1.0.2', deps={'bar': '1.0.2'})
add_to_repo(repo, 'bar', '1.0.0')
add_to_repo(repo, 'bar', '1.0.1')
add_to_repo(repo, 'bar', '1.0.2')
add_to_repo(repo, 'baz', '1.0.0')
add_to_repo(repo, 'baz', '1.0.1')
check_solver_result(
root, provider,
result={
'foo': '1.0.2',
'bar': '1.0.2',
'baz': '1.0.0',
},
locked={
'foo': get_package('foo', '1.0.1'),
'bar': get_package('bar', '1.0.1'),
'baz': get_package('baz', '1.0.0'),
},
use_latest=['foo']
)
...@@ -3,7 +3,7 @@ import pytest ...@@ -3,7 +3,7 @@ import pytest
from cleo.outputs.null_output import NullOutput from cleo.outputs.null_output import NullOutput
from cleo.styles import OutputStyle from cleo.styles import OutputStyle
from poetry.packages import Package from poetry.packages import ProjectPackage
from poetry.repositories.installed_repository import InstalledRepository from poetry.repositories.installed_repository import InstalledRepository
from poetry.repositories.pool import Pool from poetry.repositories.pool import Pool
from poetry.repositories.repository import Repository from poetry.repositories.repository import Repository
...@@ -21,7 +21,7 @@ def io(): ...@@ -21,7 +21,7 @@ def io():
@pytest.fixture() @pytest.fixture()
def package(): def package():
return Package('root', '1.0') return ProjectPackage('root', '1.0')
@pytest.fixture() @pytest.fixture()
...@@ -77,7 +77,9 @@ def check_solver_result(ops, expected): ...@@ -77,7 +77,9 @@ def check_solver_result(ops, expected):
assert result == expected assert result == expected
def test_solver_install_single(solver, repo): def test_solver_install_single(solver, repo, package):
package.add_dependency('A')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
repo.add_package(package_a) repo.add_package(package_a)
...@@ -93,7 +95,7 @@ def test_solver_remove_if_no_longer_locked(solver, locked, installed): ...@@ -93,7 +95,7 @@ def test_solver_remove_if_no_longer_locked(solver, locked, installed):
installed.add_package(package_a) installed.add_package(package_a)
locked.add_package(package_a) locked.add_package(package_a)
ops = solver.solve([]) ops = solver.solve()
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'remove', 'package': package_a} {'job': 'remove', 'package': package_a}
...@@ -115,15 +117,19 @@ def test_remove_non_installed(solver, repo, locked): ...@@ -115,15 +117,19 @@ def test_remove_non_installed(solver, repo, locked):
]) ])
def test_install_non_existing_package_fail(solver, repo): def test_install_non_existing_package_fail(solver, repo, package):
package.add_dependency('B', '1')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
repo.add_package(package_a) repo.add_package(package_a)
with pytest.raises(SolverProblemError): with pytest.raises(SolverProblemError):
solver.solve([get_dependency('B', '1')]) solver.solve()
def test_solver_with_deps(solver, repo): def test_solver_with_deps(solver, repo, package):
package.add_dependency('A')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
new_package_b = get_package('B', '1.1') new_package_b = get_package('B', '1.1')
...@@ -134,7 +140,7 @@ def test_solver_with_deps(solver, repo): ...@@ -134,7 +140,7 @@ def test_solver_with_deps(solver, repo):
package_a.requires.append(get_dependency('B', '<1.1')) package_a.requires.append(get_dependency('B', '<1.1'))
ops = solver.solve([get_dependency('a')]) ops = solver.solve()
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_b}, {'job': 'install', 'package': package_b},
...@@ -142,7 +148,9 @@ def test_solver_with_deps(solver, repo): ...@@ -142,7 +148,9 @@ def test_solver_with_deps(solver, repo):
]) ])
def test_install_honours_not_equal(solver, repo): def test_install_honours_not_equal(solver, repo, package):
package.add_dependency('A')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
new_package_b11 = get_package('B', '1.1') new_package_b11 = get_package('B', '1.1')
...@@ -157,7 +165,7 @@ def test_install_honours_not_equal(solver, repo): ...@@ -157,7 +165,7 @@ def test_install_honours_not_equal(solver, repo):
package_a.requires.append(get_dependency('B', '<=1.3,!=1.3,!=1.2')) package_a.requires.append(get_dependency('B', '<=1.3,!=1.3,!=1.2'))
ops = solver.solve([get_dependency('a')]) ops = solver.solve()
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': new_package_b11}, {'job': 'install', 'package': new_package_b11},
...@@ -165,7 +173,11 @@ def test_install_honours_not_equal(solver, repo): ...@@ -165,7 +173,11 @@ def test_install_honours_not_equal(solver, repo):
]) ])
def test_install_with_deps_in_order(solver, repo): def test_install_with_deps_in_order(solver, repo, package):
package.add_dependency('A')
package.add_dependency('B')
package.add_dependency('C')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_c = get_package('C', '1.0') package_c = get_package('C', '1.0')
...@@ -178,13 +190,7 @@ def test_install_with_deps_in_order(solver, repo): ...@@ -178,13 +190,7 @@ def test_install_with_deps_in_order(solver, repo):
package_c.requires.append(get_dependency('A', '>=1.0')) package_c.requires.append(get_dependency('A', '>=1.0'))
request = [ ops = solver.solve()
get_dependency('A'),
get_dependency('B'),
get_dependency('C'),
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_a}, {'job': 'install', 'package': package_a},
...@@ -193,23 +199,23 @@ def test_install_with_deps_in_order(solver, repo): ...@@ -193,23 +199,23 @@ def test_install_with_deps_in_order(solver, repo):
]) ])
def test_install_installed(solver, repo, installed): def test_install_installed(solver, repo, installed, package):
package.add_dependency('A')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
installed.add_package(package_a) installed.add_package(package_a)
repo.add_package(package_a) repo.add_package(package_a)
request = [ ops = solver.solve()
get_dependency('A'),
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_a, 'skipped': True}, {'job': 'install', 'package': package_a, 'skipped': True},
]) ])
def test_update_installed(solver, repo, installed): def test_update_installed(solver, repo, installed, package):
package.add_dependency('A')
installed.add_package(get_package('A', '1.0')) installed.add_package(get_package('A', '1.0'))
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
...@@ -217,52 +223,53 @@ def test_update_installed(solver, repo, installed): ...@@ -217,52 +223,53 @@ def test_update_installed(solver, repo, installed):
repo.add_package(package_a) repo.add_package(package_a)
repo.add_package(new_package_a) repo.add_package(new_package_a)
request = [ ops = solver.solve()
get_dependency('A'),
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'update', 'from': package_a, 'to': new_package_a} {'job': 'update', 'from': package_a, 'to': new_package_a}
]) ])
def test_update_with_fixed(solver, repo, installed): def test_update_with_use_latest(solver, repo, installed, package, locked):
package.add_dependency('A')
package.add_dependency('B')
installed.add_package(get_package('A', '1.0')) installed.add_package(get_package('A', '1.0'))
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
new_package_a = get_package('A', '1.1') new_package_a = get_package('A', '1.1')
package_b = get_package('B', '1.0')
new_package_b = get_package('B', '1.1')
repo.add_package(package_a) repo.add_package(package_a)
repo.add_package(new_package_a) repo.add_package(new_package_a)
repo.add_package(package_b)
repo.add_package(new_package_b)
request = [ locked.add_package(package_a)
get_dependency('A'), locked.add_package(package_b)
]
ops = solver.solve(request, fixed=[get_dependency('A', '1.0')]) ops = solver.solve(use_latest=[package_b.name])
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_a, 'skipped': True}, {'job': 'install', 'package': package_a, 'skipped': True},
{'job': 'install', 'package': new_package_b},
]) ])
def test_solver_sets_categories(solver, repo): def test_solver_sets_categories(solver, repo, package):
package.add_dependency('A')
package.add_dependency('B', category='dev')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_c = get_package('C', '1.0') package_c = get_package('C', '1.0')
package_b.requires.append(get_dependency('C', '~1.0')) package_b.add_dependency('C', '~1.0')
repo.add_package(package_a) repo.add_package(package_a)
repo.add_package(package_b) repo.add_package(package_b)
repo.add_package(package_c) repo.add_package(package_c)
request = [ ops = solver.solve()
get_dependency('A'),
get_dependency('B', category='dev')
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_c}, {'job': 'install', 'package': package_c},
...@@ -277,6 +284,9 @@ def test_solver_sets_categories(solver, repo): ...@@ -277,6 +284,9 @@ def test_solver_sets_categories(solver, repo):
def test_solver_respects_root_package_python_versions(solver, repo, package): def test_solver_respects_root_package_python_versions(solver, repo, package):
package.python_versions = '^3.4' package.python_versions = '^3.4'
package.add_dependency('A')
package.add_dependency('B')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_b.python_versions = '^3.6' package_b.python_versions = '^3.6'
...@@ -291,12 +301,7 @@ def test_solver_respects_root_package_python_versions(solver, repo, package): ...@@ -291,12 +301,7 @@ def test_solver_respects_root_package_python_versions(solver, repo, package):
repo.add_package(package_c) repo.add_package(package_c)
repo.add_package(package_c11) repo.add_package(package_c11)
request = [ ops = solver.solve()
get_dependency('A'),
get_dependency('B')
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_c}, {'job': 'install', 'package': package_c},
...@@ -307,6 +312,9 @@ def test_solver_respects_root_package_python_versions(solver, repo, package): ...@@ -307,6 +312,9 @@ def test_solver_respects_root_package_python_versions(solver, repo, package):
def test_solver_fails_if_mismatch_root_python_versions(solver, repo, package): def test_solver_fails_if_mismatch_root_python_versions(solver, repo, package):
package.python_versions = '^3.4' package.python_versions = '^3.4'
package.add_dependency('A')
package.add_dependency('B')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_b.python_versions = '^3.6' package_b.python_versions = '^3.6'
...@@ -318,17 +326,15 @@ def test_solver_fails_if_mismatch_root_python_versions(solver, repo, package): ...@@ -318,17 +326,15 @@ def test_solver_fails_if_mismatch_root_python_versions(solver, repo, package):
repo.add_package(package_b) repo.add_package(package_b)
repo.add_package(package_c) repo.add_package(package_c)
request = [
get_dependency('A'),
get_dependency('B')
]
with pytest.raises(SolverProblemError): with pytest.raises(SolverProblemError):
solver.solve(request) solver.solve()
def test_solver_solves_optional_and_compatible_packages(solver, repo, package): def test_solver_solves_optional_and_compatible_packages(solver, repo, package):
package.python_versions = '^3.4' package.python_versions = '^3.4'
package.add_dependency('A', {'version': '*', 'python': '~3.5'})
package.add_dependency('B', {'version': '*', 'optional': True})
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_b.python_versions = '^3.6' package_b.python_versions = '^3.6'
...@@ -340,16 +346,7 @@ def test_solver_solves_optional_and_compatible_packages(solver, repo, package): ...@@ -340,16 +346,7 @@ def test_solver_solves_optional_and_compatible_packages(solver, repo, package):
repo.add_package(package_b) repo.add_package(package_b)
repo.add_package(package_c) repo.add_package(package_c)
dependency_a = get_dependency('A') ops = solver.solve()
dependency_a.python_versions = '~3.5'
dependency_b = get_dependency('B', optional=True)
request = [
dependency_a,
dependency_b
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_c}, {'job': 'install', 'package': package_c},
...@@ -360,6 +357,9 @@ def test_solver_solves_optional_and_compatible_packages(solver, repo, package): ...@@ -360,6 +357,9 @@ def test_solver_solves_optional_and_compatible_packages(solver, repo, package):
def test_solver_solves_while_respecting_root_platforms(solver, repo, package): def test_solver_solves_while_respecting_root_platforms(solver, repo, package):
package.platform = 'darwin' package.platform = 'darwin'
package.add_dependency('A')
package.add_dependency('B')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_b.python_versions = '^3.6' package_b.python_versions = '^3.6'
...@@ -374,12 +374,7 @@ def test_solver_solves_while_respecting_root_platforms(solver, repo, package): ...@@ -374,12 +374,7 @@ def test_solver_solves_while_respecting_root_platforms(solver, repo, package):
repo.add_package(package_c10) repo.add_package(package_c10)
repo.add_package(package_c12) repo.add_package(package_c12)
request = [ ops = solver.solve()
get_dependency('A'),
get_dependency('B')
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_c10}, {'job': 'install', 'package': package_c10},
...@@ -388,7 +383,10 @@ def test_solver_solves_while_respecting_root_platforms(solver, repo, package): ...@@ -388,7 +383,10 @@ def test_solver_solves_while_respecting_root_platforms(solver, repo, package):
]) ])
def test_solver_does_not_return_extras_if_not_requested(solver, repo): def test_solver_does_not_return_extras_if_not_requested(solver, repo, package):
package.add_dependency('A')
package.add_dependency('B')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_c = get_package('C', '1.0') package_c = get_package('C', '1.0')
...@@ -401,14 +399,7 @@ def test_solver_does_not_return_extras_if_not_requested(solver, repo): ...@@ -401,14 +399,7 @@ def test_solver_does_not_return_extras_if_not_requested(solver, repo):
repo.add_package(package_b) repo.add_package(package_b)
repo.add_package(package_c) repo.add_package(package_c)
dependency_a = get_dependency('A') ops = solver.solve()
dependency_b = get_dependency('B')
request = [
dependency_a,
dependency_b
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_a}, {'job': 'install', 'package': package_a},
...@@ -416,7 +407,10 @@ def test_solver_does_not_return_extras_if_not_requested(solver, repo): ...@@ -416,7 +407,10 @@ def test_solver_does_not_return_extras_if_not_requested(solver, repo):
]) ])
def test_solver_returns_extras_if_requested(solver, repo): def test_solver_returns_extras_if_requested(solver, repo, package):
package.add_dependency('A')
package.add_dependency('B', {'version': '*', 'extras': ['foo']})
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_c = get_package('C', '1.0') package_c = get_package('C', '1.0')
...@@ -430,15 +424,7 @@ def test_solver_returns_extras_if_requested(solver, repo): ...@@ -430,15 +424,7 @@ def test_solver_returns_extras_if_requested(solver, repo):
repo.add_package(package_b) repo.add_package(package_b)
repo.add_package(package_c) repo.add_package(package_c)
dependency_a = get_dependency('A') ops = solver.solve()
dependency_b = get_dependency('B')
dependency_b.extras.append('foo')
request = [
dependency_a,
dependency_b
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_c}, {'job': 'install', 'package': package_c},
...@@ -447,7 +433,11 @@ def test_solver_returns_extras_if_requested(solver, repo): ...@@ -447,7 +433,11 @@ def test_solver_returns_extras_if_requested(solver, repo):
]) ])
def test_solver_returns_prereleases_if_requested(solver, repo): def test_solver_returns_prereleases_if_requested(solver, repo, package):
package.add_dependency('A')
package.add_dependency('B')
package.add_dependency('C', {'version': '*', 'allows-prereleases': True})
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_c = get_package('C', '1.0') package_c = get_package('C', '1.0')
...@@ -458,16 +448,7 @@ def test_solver_returns_prereleases_if_requested(solver, repo): ...@@ -458,16 +448,7 @@ def test_solver_returns_prereleases_if_requested(solver, repo):
repo.add_package(package_c) repo.add_package(package_c)
repo.add_package(package_c_dev) repo.add_package(package_c_dev)
dependency_a = get_dependency('A') ops = solver.solve()
dependency_b = get_dependency('B')
dependency_c = get_dependency('C', allows_prereleases=True)
request = [
dependency_a,
dependency_b,
dependency_c
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_a}, {'job': 'install', 'package': package_a},
...@@ -476,7 +457,11 @@ def test_solver_returns_prereleases_if_requested(solver, repo): ...@@ -476,7 +457,11 @@ def test_solver_returns_prereleases_if_requested(solver, repo):
]) ])
def test_solver_does_not_return_prereleases_if_not_requested(solver, repo): def test_solver_does_not_return_prereleases_if_not_requested(solver, repo, package):
package.add_dependency('A')
package.add_dependency('B')
package.add_dependency('C')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_c = get_package('C', '1.0') package_c = get_package('C', '1.0')
...@@ -487,16 +472,7 @@ def test_solver_does_not_return_prereleases_if_not_requested(solver, repo): ...@@ -487,16 +472,7 @@ def test_solver_does_not_return_prereleases_if_not_requested(solver, repo):
repo.add_package(package_c) repo.add_package(package_c)
repo.add_package(package_c_dev) repo.add_package(package_c_dev)
dependency_a = get_dependency('A') ops = solver.solve()
dependency_b = get_dependency('B')
dependency_c = get_dependency('C')
request = [
dependency_a,
dependency_b,
dependency_c
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_a}, {'job': 'install', 'package': package_a},
...@@ -505,7 +481,10 @@ def test_solver_does_not_return_prereleases_if_not_requested(solver, repo): ...@@ -505,7 +481,10 @@ def test_solver_does_not_return_prereleases_if_not_requested(solver, repo):
]) ])
def test_solver_sub_dependencies_with_requirements(solver, repo): def test_solver_sub_dependencies_with_requirements(solver, repo, package):
package.add_dependency('A')
package.add_dependency('B')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_c = get_package('C', '1.0') package_c = get_package('C', '1.0')
...@@ -520,14 +499,7 @@ def test_solver_sub_dependencies_with_requirements(solver, repo): ...@@ -520,14 +499,7 @@ def test_solver_sub_dependencies_with_requirements(solver, repo):
repo.add_package(package_c) repo.add_package(package_c)
repo.add_package(package_d) repo.add_package(package_d)
dependency_a = get_dependency('A') ops = solver.solve()
dependency_b = get_dependency('B')
request = [
dependency_a,
dependency_b,
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_c}, {'job': 'install', 'package': package_c},
...@@ -540,7 +512,11 @@ def test_solver_sub_dependencies_with_requirements(solver, repo): ...@@ -540,7 +512,11 @@ def test_solver_sub_dependencies_with_requirements(solver, repo):
assert op.package.requirements == {} assert op.package.requirements == {}
def test_solver_sub_dependencies_with_requirements_complex(solver, repo): def test_solver_sub_dependencies_with_requirements_complex(solver, repo, package):
package.add_dependency('A')
package.add_dependency('B')
package.add_dependency('C')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
package_c = get_package('C', '1.0') package_c = get_package('C', '1.0')
...@@ -562,16 +538,7 @@ def test_solver_sub_dependencies_with_requirements_complex(solver, repo): ...@@ -562,16 +538,7 @@ def test_solver_sub_dependencies_with_requirements_complex(solver, repo):
repo.add_package(package_e) repo.add_package(package_e)
repo.add_package(package_f) repo.add_package(package_f)
dependency_a = get_dependency('A') ops = solver.solve()
dependency_b = get_dependency('B')
dependency_c = get_dependency('C')
request = [
dependency_a,
dependency_b,
dependency_c,
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_d}, {'job': 'install', 'package': package_d},
...@@ -591,6 +558,7 @@ def test_solver_sub_dependencies_with_requirements_complex(solver, repo): ...@@ -591,6 +558,7 @@ def test_solver_sub_dependencies_with_requirements_complex(solver, repo):
def test_solver_sub_dependencies_with_not_supported_python_version(solver, repo, package): def test_solver_sub_dependencies_with_not_supported_python_version(solver, repo, package):
package.python_versions = '^3.5' package.python_versions = '^3.5'
package.add_dependency('A')
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
...@@ -601,12 +569,7 @@ def test_solver_sub_dependencies_with_not_supported_python_version(solver, repo, ...@@ -601,12 +569,7 @@ def test_solver_sub_dependencies_with_not_supported_python_version(solver, repo,
repo.add_package(package_a) repo.add_package(package_a)
repo.add_package(package_b) repo.add_package(package_b)
dependency_a = get_dependency('A') ops = solver.solve()
request = [
dependency_a,
]
ops = solver.solve(request)
check_solver_result(ops, [ check_solver_result(ops, [
{'job': 'install', 'package': package_a}, {'job': 'install', 'package': package_a},
......
...@@ -55,12 +55,3 @@ def test_package(): ...@@ -55,12 +55,3 @@ def test_package():
assert win_inet.name == 'win-inet-pton' assert win_inet.name == 'win-inet-pton'
assert win_inet.python_versions == '~2.7 || ~2.6' assert win_inet.python_versions == '~2.7 || ~2.6'
assert win_inet.platform == 'win32' assert win_inet.platform == 'win32'
def test_package_drops_malformed_dependencies():
repo = MockRepository()
package = repo.package('ipython', '4.1.0rc1')
dependency_names = [d.name for d in package.requires]
assert 'setuptools' not in dependency_names
import pytest
from poetry.semver.semver import parse_constraint
from poetry.semver.semver import Version
from poetry.semver.semver import VersionRange
@pytest.mark.parametrize(
'input,constraint',
[
('*', VersionRange()),
('*.*', VersionRange()),
('v*.*', VersionRange()),
('*.x.*', VersionRange()),
('x.X.x.*', VersionRange()),
# ('!=1.0.0', Constraint('!=', '1.0.0.0')),
('>1.0.0', VersionRange(min=Version(1, 0, 0))),
('<1.2.3', VersionRange(max=Version(1, 2, 3))),
('<=1.2.3', VersionRange(max=Version(1, 2, 3), include_max=True)),
('>=1.2.3', VersionRange(min=Version(1, 2, 3), include_min=True)),
('=1.2.3', Version(1, 2, 3)),
('1.2.3', Version(1, 2, 3)),
('=1.0', Version(1, 0, 0)),
('1.2.3b5', Version(1, 2, 3, 'b5')),
('>= 1.2.3', VersionRange(min=Version(1, 2, 3), include_min=True))
]
)
def test_parse_constraint(input, constraint):
assert parse_constraint(input) == constraint
@pytest.mark.parametrize(
'input,constraint',
[
('v2.*', VersionRange(Version(2, 0, 0), Version(3, 0, 0), True)),
('2.*.*', VersionRange(Version(2, 0, 0), Version(3, 0, 0), True)),
('20.*', VersionRange(Version(20, 0, 0), Version(21, 0, 0), True)),
('20.*.*', VersionRange(Version(20, 0, 0), Version(21, 0, 0), True)),
('2.0.*', VersionRange(Version(2, 0, 0), Version(2, 1, 0), True)),
('2.x', VersionRange(Version(2, 0, 0), Version(3, 0, 0), True)),
('2.x.x', VersionRange(Version(2, 0, 0), Version(3, 0, 0), True)),
('2.2.X', VersionRange(Version(2, 2, 0), Version(2, 3, 0), True)),
('0.*', VersionRange(max=Version(1, 0, 0))),
('0.*.*', VersionRange(max=Version(1, 0, 0))),
('0.x', VersionRange(max=Version(1, 0, 0))),
]
)
def test_parse_constraint_wildcard(input, constraint):
assert parse_constraint(input) == constraint
@pytest.mark.parametrize(
'input,constraint',
[
('~v1', VersionRange(Version(1, 0, 0), Version(2, 0, 0), True)),
('~1.0', VersionRange(Version(1, 0, 0), Version(1, 1, 0), True)),
('~1.0.0', VersionRange(Version(1, 0, 0), Version(1, 1, 0), True)),
('~1.2', VersionRange(Version(1, 2, 0), Version(1, 3, 0), True)),
('~1.2.3', VersionRange(Version(1, 2, 3), Version(1, 3, 0), True)),
('~1.2-beta', VersionRange(Version(1, 2, 0, 'beta'), Version(1, 3, 0), True)),
('~1.2-b2', VersionRange(Version(1, 2, 0, 'b2'), Version(1, 3, 0), True)),
('~0.3', VersionRange(Version(0, 3, 0), Version(0, 4, 0), True)),
]
)
def test_parse_constraint_tilde(input, constraint):
assert parse_constraint(input) == constraint
@pytest.mark.parametrize(
'input,constraint',
[
('^v1', VersionRange(Version(1, 0, 0), Version(2, 0, 0), True)),
('^0', VersionRange(Version(0, 0, 0), Version(1, 0, 0), True)),
('^0.0', VersionRange(Version(0, 0, 0), Version(0, 1, 0), True)),
('^1.2', VersionRange(Version(1, 2, 0), Version(2, 0, 0), True)),
('^1.2.3-beta.2', VersionRange(Version(1, 2, 3, 'beta.2'), Version(2, 0, 0), True)),
('^1.2.3', VersionRange(Version(1, 2, 3), Version(2, 0, 0), True)),
('^0.2.3', VersionRange(Version(0, 2, 3), Version(0, 3, 0), True)),
('^0.2', VersionRange(Version(0, 2, 0), Version(0, 3, 0), True)),
('^0.2.0', VersionRange(Version(0, 2, 0), Version(0, 3, 0), True)),
('^0.0.3', VersionRange(Version(0, 0, 3), Version(0, 0, 4), True)),
]
)
def test_parse_constraint_caret(input, constraint):
assert parse_constraint(input) == constraint
@pytest.mark.parametrize(
'input',
[
'>2.0,<=3.0',
'>2.0 <=3.0',
'>2.0 <=3.0',
'>2.0, <=3.0',
'>2.0 ,<=3.0',
'>2.0 , <=3.0',
'>2.0 , <=3.0',
'> 2.0 <= 3.0',
'> 2.0 , <= 3.0',
' > 2.0 , <= 3.0 ',
]
)
def test_parse_constraint_multi(input):
assert parse_constraint(input) == VersionRange(
Version(2, 0, 0), Version(3, 0, 0),
include_min=False,
include_max=True
)
@pytest.mark.parametrize(
'input,constraint',
[
('!=v2.*', VersionRange(max=Version.parse('2.0')).union(VersionRange(Version.parse('3.0'), include_min=True))),
('!=2.*.*', VersionRange(max=Version.parse('2.0')).union(VersionRange(Version.parse('3.0'), include_min=True))),
('!=2.0.*', VersionRange(max=Version.parse('2.0')).union(VersionRange(Version.parse('2.1'), include_min=True))),
('!=0.*', VersionRange(Version.parse('1.0'), include_min=True)),
('!=0.*.*', VersionRange(Version.parse('1.0'), include_min=True)),
]
)
def test_parse_constraints_negative_wildcard(input, constraint):
assert parse_constraint(input) == constraint
import pytest
from poetry.semver.semver import EmptyConstraint
from poetry.semver.semver import Version
from poetry.semver.semver import VersionRange
@pytest.mark.parametrize(
'input,version',
[
('1.0.0', Version(1, 0, 0)),
('1', Version(1, 0, 0)),
('1.0', Version(1, 0, 0)),
('1b1', Version(1, 0, 0, 'beta1')),
('1.0b1', Version(1, 0, 0, 'beta1')),
('1.0.0b1', Version(1, 0, 0, 'beta1')),
('1.0.0-b1', Version(1, 0, 0, 'beta1')),
('1.0.0-beta.1', Version(1, 0, 0, 'beta1')),
('1.0.0+1', Version(1, 0, 0, None, '1')),
('1.0.0-1', Version(1, 0, 0, None, '1')),
('0.6c', Version(0, 6, 0, 'rc0')),
('0.6pre', Version(0, 6, 0, 'rc0')),
]
)
def test_parse_valid(input, version):
parsed = Version.parse(input)
assert parsed == version
assert parsed.text == input
def test_comparison():
versions = [
'1.0.0-alpha',
'1.0.0-alpha.1',
'1.0.0-beta.2',
'1.0.0-beta.11',
'1.0.0-rc.1',
'1.0.0-rc.1+build.1',
'1.0.0',
'1.0.0+0.3.7',
'1.3.7+build',
'1.3.7+build.2.b8f12d7',
'1.3.7+build.11.e0f985a',
'2.0.0',
'2.1.0',
'2.2.0',
'2.11.0',
'2.11.1'
]
for i in range(len(versions)):
for j in range(len(versions)):
a = Version.parse(versions[i])
b = Version.parse(versions[j])
assert (a < b) == (i < j)
assert (a > b) == (i > j)
assert (a <= b) == (i <= j)
assert (a >= b) == (i >= j)
assert (a == b) == (i == j)
assert (a != b) == (i != j)
def test_equality():
assert Version.parse('1.2.3') == Version.parse('01.2.3')
assert Version.parse('1.2.3') == Version.parse('1.02.3')
assert Version.parse('1.2.3') == Version.parse('1.2.03')
assert Version.parse('1.2.3-1') == Version.parse('1.2.3-01')
assert Version.parse('1.2.3+1') == Version.parse('1.2.3+01')
def test_allows():
v = Version.parse('1.2.3')
assert v.allows(v)
assert not v.allows(Version.parse('2.2.3'))
assert not v.allows(Version.parse('1.3.3'))
assert not v.allows(Version.parse('1.2.4'))
assert not v.allows(Version.parse('1.2.3-dev'))
assert not v.allows(Version.parse('1.2.3+build'))
def test_allows_all():
v = Version.parse('1.2.3')
assert v.allows_all(v)
assert not v.allows_all(Version.parse('0.0.3'))
assert not v.allows_all(VersionRange(Version.parse('1.1.4'), Version.parse('1.2.4')))
assert not v.allows_all(VersionRange())
assert v.allows_all(EmptyConstraint())
def test_allows_any():
v = Version.parse('1.2.3')
assert v.allows_any(v)
assert not v.allows_any(Version.parse('0.0.3'))
assert v.allows_any(VersionRange(Version.parse('1.1.4'), Version.parse('1.2.4')))
assert v.allows_any(VersionRange())
assert not v.allows_any(EmptyConstraint())
def test_intersect():
v = Version.parse('1.2.3')
assert v.intersect(v) == v
assert v.intersect(Version.parse('1.1.4')).is_empty()
assert v.intersect(VersionRange(Version.parse('1.1.4'), Version.parse('1.2.4'))) == v
assert Version.parse('1.1.4').intersect(VersionRange(v, Version.parse('1.2.4'))).is_empty()
def test_union():
v = Version.parse('1.2.3')
assert v.union(v) == v
result = v.union(Version.parse('0.8.0'))
assert result.allows(v)
assert result.allows(Version.parse('0.8.0'))
assert not result.allows(Version.parse('1.1.4'))
range = VersionRange(Version.parse('1.1.4'), Version.parse('1.2.4'))
assert v.union(range) == range
union = Version.parse('1.1.4').union(VersionRange(Version.parse('1.1.4'), Version.parse('1.2.4')))
assert union == VersionRange(Version.parse('1.1.4'), Version.parse('1.2.4'), include_min=True)
result = v.union(VersionRange(Version.parse('0.0.3'), Version.parse('1.1.4')))
assert result.allows(v)
assert result.allows(Version.parse('0.1.0'))
def test_difference():
v = Version.parse('1.2.3')
assert v.difference(v).is_empty()
assert v.difference(Version.parse('0.8.0')) == v
assert v.difference(VersionRange(Version.parse('1.1.4'), Version.parse('1.2.4'))).is_empty()
assert v.difference(VersionRange(Version.parse('1.4.0'), Version.parse('3.0.0'))) == v
import pytest
from poetry.semver.semver import EmptyConstraint
from poetry.semver.semver import Version
from poetry.semver.semver import VersionRange
@pytest.fixture()
def v003():
return Version.parse('0.0.3')
@pytest.fixture()
def v010():
return Version.parse('0.1.0')
@pytest.fixture()
def v080():
return Version.parse('0.8.0')
@pytest.fixture()
def v072():
return Version.parse('0.7.2')
@pytest.fixture()
def v114():
return Version.parse('1.1.4')
@pytest.fixture()
def v123():
return Version.parse('1.2.3')
@pytest.fixture()
def v124():
return Version.parse('1.2.4')
@pytest.fixture()
def v130():
return Version.parse('1.3.0')
@pytest.fixture()
def v140():
return Version.parse('1.4.0')
@pytest.fixture()
def v200():
return Version.parse('2.0.0')
@pytest.fixture()
def v234():
return Version.parse('2.3.4')
@pytest.fixture()
def v250():
return Version.parse('2.5.0')
@pytest.fixture()
def v300():
return Version.parse('3.0.0')
def test_allows_all(v003, v010, v080, v114, v123, v124, v140, v200, v234, v250, v300):
assert VersionRange(v123, v250).allows_all(EmptyConstraint())
range = VersionRange(v123, v250, include_max=True)
assert not range.allows_all(v123)
assert range.allows_all(v124)
assert range.allows_all(v250)
assert not range.allows_all(v300)
# with no min
range = VersionRange(max=v250)
assert range.allows_all(VersionRange(v080, v140))
assert not range.allows_all(VersionRange(v080, v300))
assert range.allows_all(VersionRange(max=v140))
assert not range.allows_all(VersionRange(max=v300))
assert range.allows_all(range)
assert not range.allows_all(VersionRange())
# with no max
range = VersionRange(min=v010)
assert range.allows_all(VersionRange(v080, v140))
assert not range.allows_all(VersionRange(v003, v140))
assert range.allows_all(VersionRange(v080))
assert not range.allows_all(VersionRange(v003))
assert range.allows_all(range)
assert not range.allows_all(VersionRange())
# Allows bordering range that is not more inclusive
exclusive = VersionRange(v010, v250)
inclusive = VersionRange(v010, v250, True, True)
assert inclusive.allows_all(exclusive)
assert inclusive.allows_all(inclusive)
assert not exclusive.allows_all(inclusive)
assert exclusive.allows_all(exclusive)
# Allows unions that are completely contained
range = VersionRange(v114, v200)
assert range.allows_all(VersionRange(v123, v124).union(v140))
assert not range.allows_all(VersionRange(v010, v124).union(v140))
assert not range.allows_all(VersionRange(v123, v234).union(v140))
def test_allows_any(v003, v010, v072, v080, v114, v123, v124, v140, v200, v234, v250, v300):
# disallows an empty constraint
assert not VersionRange(v123, v250).allows_any(EmptyConstraint())
# allows allowed versions
range = VersionRange(v123, v250, include_max=True)
assert not range.allows_any(v123)
assert range.allows_any(v124)
assert range.allows_any(v250)
assert not range.allows_any(v300)
# with no min
range = VersionRange(max=v200)
assert range.allows_any(VersionRange(v140, v300))
assert not range.allows_any(VersionRange(v234, v300))
assert range.allows_any(VersionRange(v140))
assert not range.allows_any(VersionRange(v234))
assert range.allows_any(range)
# with no max
range = VersionRange(min=v072)
assert range.allows_any(VersionRange(v003, v140))
assert not range.allows_any(VersionRange(v003, v010))
assert range.allows_any(VersionRange(max=v080))
assert not range.allows_any(VersionRange(max=v003))
assert range.allows_any(range)
# with min and max
range = VersionRange(v072, v200)
assert range.allows_any(VersionRange(v003, v140))
assert range.allows_any(VersionRange(v140, v300))
assert not range.allows_any(VersionRange(v003, v010))
assert not range.allows_any(VersionRange(v234, v300))
assert not range.allows_any(VersionRange(max=v010))
assert not range.allows_any(VersionRange(v234))
assert range.allows_any(range)
# allows a bordering range when both are inclusive
assert not VersionRange(max=v250).allows_any(VersionRange(min=v250))
assert not VersionRange(max=v250, include_max=True).allows_any(VersionRange(min=v250))
assert not VersionRange(max=v250).allows_any(VersionRange(min=v250, include_min=True))
assert not VersionRange(min=v250).allows_any(VersionRange(max=v250))
assert VersionRange(max=v250, include_max=True).allows_any(VersionRange(min=v250, include_min=True))
# allows unions that are partially contained'
range = VersionRange(v114, v200)
assert range.allows_any(VersionRange(v010, v080).union(v140))
assert range.allows_any(VersionRange(v123, v234).union(v300))
assert not range.allows_any(VersionRange(v234, v300).union(v010))
def test_intersect(v114, v123, v124, v200, v250, v300):
# two overlapping ranges
assert VersionRange(v123, v250).intersect(VersionRange(v200, v300)) == VersionRange(v200, v250)
# a non-overlapping range allows no versions
a = VersionRange(v114, v124)
b = VersionRange(v200, v250)
assert a.intersect(b).is_empty()
# adjacent ranges allow no versions if exclusive
a = VersionRange(v114, v124)
b = VersionRange(v124, v200)
assert a.intersect(b).is_empty()
# adjacent ranges allow version if inclusive
a = VersionRange(v114, v124, include_max=True)
b = VersionRange(v124, v200, include_min=True)
assert a.intersect(b) == v124
# with an open range
open = VersionRange()
a = VersionRange(v114, v124)
assert open.intersect(open) == open
assert open.intersect(a) == a
# returns the version if the range allows it
assert VersionRange(v114, v124).intersect(v123) == v123
assert VersionRange(v123, v124).intersect(v114).is_empty()
def test_union(v003, v010, v072, v080, v114, v123, v124, v130, v140, v200, v234, v250, v300):
# with a version returns the range if it contains the version
range = VersionRange(v114, v124)
assert range.union(v123) == range
# with a version on the edge of the range, expands the range
range = VersionRange(v114, v124)
assert range.union(v124) == VersionRange(v114, v124, include_max=True)
assert range.union(v114) == VersionRange(v114, v124, include_min=True)
# with a version allows both the range and the version if the range
# doesn't contain the version
result = VersionRange(v003, v114).union(v124)
assert result.allows(v010)
assert not result.allows(v123)
assert result.allows(v124)
# returns a VersionUnion for a disjoint range
result = VersionRange(v003, v114).union(VersionRange(v130, v200))
assert result.allows(v080)
assert not result.allows(v123)
assert result.allows(v140)
# considers open ranges disjoint
result = VersionRange(v003, v114).union(VersionRange(v114, v200))
assert result.allows(v080)
assert not result.allows(v114)
assert result.allows(v140)
result = VersionRange(v114, v200).union(VersionRange(v003, v114))
assert result.allows(v080)
assert not result.allows(v114)
assert result.allows(v140)
# returns a merged range for an overlapping range
result = VersionRange(v003, v114).union(VersionRange(v080, v200))
assert result == VersionRange(v003, v200)
# considers closed ranges overlapping
result = VersionRange(v003, v114, include_max=True).union(VersionRange(v114, v200))
assert result == VersionRange(v003, v200)
result = VersionRange(v003, v114).union(VersionRange(v114, v200, include_min=True))
assert result == VersionRange(v003, v200)
import pytest
from poetry.semver import sort, rsort, statisfies, satisfied_by
@pytest.mark.parametrize(
'version, constraint',
[
('1.2.3', '^1.2.3+build'),
('1.3.0', '^1.2.3+build'),
('1.3.0-beta', '>1.2'),
('1.2.3-beta', '<=1.2.3'),
('1.0.0', '1.0.0'),
('1.2.3', '*'),
('v1.2.3', '*'),
('1.0.0', '>=1.0.0'),
('1.0.1', '>=1.0.0'),
('1.1.0', '>=1.0.0'),
('1.0.1', '>1.0.0'),
('1.1.0', '>1.0.0'),
('2.0.0', '<=2.0.0'),
('1.9999.9999', '<=2.0.0'),
('0.2.9', '<=2.0.0'),
('1.9999.9999', '<2.0.0'),
('0.2.9', '<2.0.0'),
('1.0.0', '>= 1.0.0'),
('1.0.1', '>= 1.0.0'),
('1.1.0', '>= 1.0.0'),
('1.0.1', '> 1.0.0'),
('1.1.0', '> 1.0.0'),
('2.0.0', '<= 2.0.0'),
('1.9999.9999', '<= 2.0.0'),
('0.2.9', '<= 2.0.0'),
('1.9999.9999', '< 2.0.0'),
('0.2.9', "<\t2.0.0"),
('v0.1.97', '>=0.1.97'),
('0.1.97', '>=0.1.97'),
('1.2.4', '0.1.20 || 1.2.4'),
('0.0.0', '>=0.2.3 || <0.0.1'),
('0.2.3', '>=0.2.3 || <0.0.1'),
('0.2.4', '>=0.2.3 || <0.0.1'),
('2.1.3', '2.x.x'),
('1.2.3', '1.2.x'),
('2.1.3', '1.2.x || 2.x'),
('1.2.3', '1.2.x || 2.x'),
('1.2.3', 'x'),
('2.1.3', '2.*.*'),
('1.2.3', '1.2.*'),
('2.1.3', '1.2.* || 2.*'),
('1.2.3', '1.2.* || 2.*'),
('1.2.3', '*'),
('2.9.0', '^2.4'), # >= 2.4.0 < 3.0.0
('2.4.5', '~2.4'),
('1.2.3', '~1'), # >= 1.0.0 < 2.0.0
('1.0.7', '~1.0'), # >= 1.0.0 < 1.1.0
('1.0.0', '>=1'),
('1.0.0', '>= 1'),
('1.2.8', '>1.2'), # > 1.2.0
('1.1.1', '<1.2'), # < 1.2.0
('1.1.1', '< 1.2'),
('1.2.3', '~1.2.1 >=1.2.3'),
('1.2.3', '~1.2.1 =1.2.3'),
('1.2.3', '~1.2.1 1.2.3'),
('1.2.3', '~1.2.1 >=1.2.3 1.2.3'),
('1.2.3', '~1.2.1 1.2.3 >=1.2.3'),
('1.2.3', '~1.2.1 1.2.3'),
('1.2.3', '>=1.2.1 1.2.3'),
('1.2.3', '1.2.3 >=1.2.1'),
('1.2.3', '>=1.2.3 >=1.2.1'),
('1.2.3', '>=1.2.1 >=1.2.3'),
('1.2.8', '>=1.2'),
('1.8.1', '^1.2.3'),
('0.1.2', '^0.1.2'),
('0.1.2', '^0.1'),
('1.4.2', '^1.2'),
('1.4.2', '^1.2 ^1'),
('0.0.1-beta', '^0.0.1-alpha'),
]
)
def test_statisfies_positive(version, constraint):
assert statisfies(version, constraint)
@pytest.mark.parametrize(
'version, constraint',
[
('2.0.0', '^1.2.3+build'),
('1.2.0', '^1.2.3+build'),
('1.0.0beta', '1'),
('1.0.1', '1.0.0'),
('0.0.0', '>=1.0.0'),
('0.0.1', '>=1.0.0'),
('0.1.0', '>=1.0.0'),
('0.0.1', '>1.0.0'),
('0.1.0', '>1.0.0'),
('3.0.0', '<=2.0.0'),
('2.9999.9999', '<=2.0.0'),
('2.2.9', '<=2.0.0'),
('2.9999.9999', '<2.0.0'),
('2.2.9', '<2.0.0'),
('v0.1.93', '>=0.1.97'),
('0.1.93', '>=0.1.97'),
('1.2.3', '0.1.20 || 1.2.4'),
('0.0.3', '>=0.2.3 || <0.0.1'),
('0.2.2', '>=0.2.3 || <0.0.1'),
('1.1.3', '2.x.x'),
('3.1.3', '2.x.x'),
('1.3.3', '1.2.x'),
('3.1.3', '1.2.x || 2.x'),
('1.1.3', '1.2.x || 2.x'),
('1.1.3', '2.*.*'),
('3.1.3', '2.*.*'),
('1.3.3', '1.2.*'),
('3.1.3', '1.2.* || 2.*'),
('1.1.3', '1.2.* || 2.*'),
('1.1.2', '2'),
('2.4.1', '2.3'),
('3.0.0', '~2.4'), # >= 2.4.0 < 3.0.0
('2.3.9', '~2.4'),
('0.2.3', '~1'), # >= 1.0.0 < 2.0.0
('1.0.0', '<1'),
('1.1.1', '>=1.2'),
('2.0.0beta', '1'),
('0.5.4-alpha', '~v0.5.4-beta'),
('1.2.2', '^1.2.3'),
('1.1.9', '^1.2'),
]
)
def test_statisfies_negative(version, constraint):
assert not statisfies(version, constraint)
@pytest.mark.parametrize(
'constraint, versions, expected',
[
(
'~1.0',
['1.0', '1.0.9', '1.2', '2.0', '2.1', '0.9999.9999'],
['1.0', '1.0.9'],
),
(
'>1.0 <3.0 || >=4.0',
['1.0', '1.1', '2.9999.9999', '3.0', '3.1', '3.9999.9999', '4.0', '4.1'],
['1.1', '2.9999.9999', '4.0', '4.1'],
),
(
'^0.2.0',
['0.1.1', '0.1.9999', '0.2.0', '0.2.1', '0.3.0'],
['0.2.0', '0.2.1'],
),
]
)
def test_satisfied_by(constraint, versions, expected):
assert satisfied_by(versions, constraint) == expected
@pytest.mark.parametrize(
'versions, sorted, rsorted',
[
(
['1.0', '0.1', '0.1', '3.2.1', '2.4.0-alpha', '2.4.0'],
['0.1', '0.1', '1.0', '2.4.0-alpha', '2.4.0', '3.2.1'],
['3.2.1', '2.4.0', '2.4.0-alpha', '1.0', '0.1', '0.1'],
)
]
)
def test_sort(versions, sorted, rsorted):
assert sort(versions) == sorted
assert rsort(versions) == rsorted
...@@ -17,7 +17,7 @@ def test_poetry(): ...@@ -17,7 +17,7 @@ def test_poetry():
package = poetry.package package = poetry.package
assert package.name == 'my-package' assert package.name == 'my-package'
assert package.version == '1.2.3' assert package.version.text == '1.2.3'
assert package.description == 'Some description.' assert package.description == 'Some description.'
assert package.authors == ['Sébastien Eustace <sebastien@eustace.io>'] assert package.authors == ['Sébastien Eustace <sebastien@eustace.io>']
assert package.license.id == 'MIT' assert package.license.id == 'MIT'
...@@ -27,7 +27,7 @@ def test_poetry(): ...@@ -27,7 +27,7 @@ def test_poetry():
assert package.keywords == ["packaging", "dependency", "poetry"] assert package.keywords == ["packaging", "dependency", "poetry"]
assert package.python_versions == '~2.7 || ^3.6' assert package.python_versions == '~2.7 || ^3.6'
assert str(package.python_constraint) == '>= 2.7.0.0, < 2.8.0.0 || >= 3.6.0.0, < 4.0.0.0' assert str(package.python_constraint) == '>=2.7,<2.8.0 || >=3.6,<4.0.0'
dependencies = {} dependencies = {}
for dep in package.requires: for dep in package.requires:
......
from poetry.version.helpers import format_python_constraint from poetry.version.helpers import format_python_constraint
from poetry.semver.version_parser import VersionParser from poetry.semver.semver import parse_constraint
def test_format_python_constraint(): def test_format_python_constraint():
parser = VersionParser() constraint = parse_constraint('~2.7 || ^3.6')
constraint = parser.parse_constraints('~2.7 || ^3.6')
result = format_python_constraint(constraint) result = format_python_constraint(constraint)
......
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