Commit c008f402 by Sébastien Eustace

Make the dependency resolver respect the root package's python versions

parent 4aa6b170
......@@ -11,6 +11,7 @@
- Dependencies of each package is now stored in `poetry.lock`.
- Improved TOML file management.
- Dependency resolver now respects the root package python version requirements.
### Fixed
......
......@@ -3,7 +3,7 @@ name = "poetry"
version = "0.2.0"
description = "Python dependency management and packaging made easy."
python-versions = ["^3.6"]
python-versions = "^3.6"
license = "MIT"
......
......@@ -126,12 +126,17 @@ class Installer:
Dependency(candidate.name, candidate.version)
)
solver = Solver(locked_repository, self._io)
solver = Solver(
self._package,
self._pool,
locked_repository,
self._io
)
request = self._package.requires
request += self._package.dev_requires
ops = solver.solve(request, self._pool, fixed=fixed)
ops = solver.solve(request, fixed=fixed)
else:
self._io.writeln('<info>Installing dependencies from lock file</>')
if not self._locker.is_fresh():
......
import poetry.packages
from poetry.semver.constraints import Constraint
from poetry.semver.constraints.base_constraint import BaseConstraint
from poetry.semver.version_parser import VersionParser
class Dependency:
def __init__(self, name, constraint, optional=False, category='main'):
def __init__(self,
name: str,
constraint: str,
optional: bool = False,
category: str = 'main',
allows_prereleases: bool = False):
self._name = name.lower()
self._pretty_name = name
self._parser = VersionParser()
try:
self._constraint = VersionParser().parse_constraints(constraint)
self._constraint = self._parser.parse_constraints(constraint)
except ValueError:
self._constraint = VersionParser().parse_constraints('*')
self._constraint = self._parser.parse_constraints('*')
self._pretty_constraint = constraint
self._optional = optional
self._category = category
self._allows_prereleases = allows_prereleases
self._python_versions = '*'
self._python_constraint = self._parser.parse_constraints('*')
self._platform = '*'
self._platform_constraint = self._parser.parse_constraints('*')
@property
def name(self):
......@@ -35,8 +52,34 @@ class Dependency:
def category(self):
return self._category
def accepts_prereleases(self):
return False
@property
def python_versions(self):
return self._python_versions
@python_versions.setter
def python_versions(self, value: str):
self._python_versions = value
self._python_constraint = self._parser.parse_constraints(value)
@property
def python_constraint(self):
return self._python_constraint
@property
def platform(self) -> str:
return self._platform
@platform.setter
def platform(self, value: str):
self._platform = value
self._platform_constraint = self._parser.parse_constraints(value)
@property
def platform_constraint(self):
return self._platform_constraint
def allows_prereleases(self):
return self._allows_prereleases
def is_optional(self):
return self._optional
......@@ -44,6 +87,16 @@ class Dependency:
def is_vcs(self):
return False
def accepts(self, package: 'poetry.packages.Package') -> bool:
"""
Determines if the given package matches this dependency.
"""
return (
self._name == package.name
and self._constraint.matches(Constraint('=', package.version))
and (not package.is_prerelease() or self.allows_prereleases())
)
def __eq__(self, other):
if not isinstance(other, Dependency):
return NotImplemented
......
......@@ -15,7 +15,7 @@ class Locker:
_relevant_keys = [
'name',
'version',
'python_versions',
'python-versions',
'dependencies',
'dev-dependencies',
'source',
......
from poetry.semver.constraints.base_constraint import BaseConstraint
from poetry.semver.helpers import parse_stability
from poetry.semver.version_parser import VersionParser
from .dependency import Dependency
from .vcs_dependency import VCSDependency
......@@ -31,7 +33,7 @@ class Package:
'dev': STABILITY_DEV,
}
def __init__(self, name, version, pretty_version):
def __init__(self, name, version, pretty_version=None):
"""
Creates a new in memory package.
......@@ -48,7 +50,7 @@ class Package:
self._name = name.lower()
self._version = version
self._pretty_version = pretty_version
self._pretty_version = pretty_version or version
self._description = ''
......@@ -62,11 +64,16 @@ class Package:
self.requires = []
self.dev_requires = []
self._parser = VersionParser()
self.category = 'main'
self.hashes = []
self.optional = False
self.python_versions = '*'
self.platform = '*'
self._python_versions = '*'
self._python_constraint = self._parser.parse_constraints('*')
self._platform = '*'
self._platform_constraint = self._parser.parse_constraints('*')
@property
def name(self):
......@@ -77,10 +84,6 @@ class Package:
return self._pretty_name
@property
def id(self):
return self._id
@property
def version(self):
return self._version
......@@ -112,6 +115,32 @@ class Package:
return '{} {}'.format(self._pretty_version, self.source_reference)
@property
def python_versions(self):
return self._python_versions
@python_versions.setter
def python_versions(self, value: str):
self._python_versions = value
self._python_constraint = self._parser.parse_constraints(value)
@property
def python_constraint(self):
return self._python_constraint
@property
def platform(self) -> str:
return self._platform
@platform.setter
def platform(self, value: str):
self._platform = value
self._platform_constraint = self._parser.parse_constraints(value)
@property
def platform_constraint(self):
return self._platform_constraint
def is_dev(self):
return self._dev
......
......@@ -63,8 +63,8 @@ class Poetry:
version = normalize_version(pretty_version)
package = Package(name, version, pretty_version)
if 'python_versions' in package_config:
package.python_versions = package_config['python_versions']
if 'python-versions' in package_config:
package.python_versions = package_config['python-versions']
if 'platform' in package_config:
package.platform = package_config['platform']
......
......@@ -18,7 +18,6 @@ from poetry.packages import VCSDependency
from poetry.repositories import Pool
from poetry.semver import less_than
from poetry.semver.constraints import Constraint
from poetry.utils.toml_file import TomlFile
from poetry.utils.venv import Venv
......@@ -30,8 +29,10 @@ class Provider(SpecificationProvider):
UNSAFE_PACKAGES = {'setuptools', 'distribute', 'pip'}
def __init__(self, pool: Pool):
def __init__(self, package: Package, pool: Pool):
self._package = package
self._pool = pool
self._python_constraint = package.python_constraint
@property
def pool(self) -> Pool:
......@@ -168,13 +169,16 @@ class Provider(SpecificationProvider):
if isinstance(requirement, Package):
return requirement == package
if package.is_prerelease() and not requirement.accepts_prereleases():
if not requirement.accepts(package):
return False
if package.is_prerelease() and not requirement.allows_prereleases():
vertex = activated.vertex_named(package.name)
if not any([r.accepts_prereleases() for r in vertex.requirements]):
if not any([r.allows_prereleases() for r in vertex.requirements]):
return False
return requirement.constraint.matches(Constraint('==', package.version))
return self._package.python_constraint.matches(package.python_constraint)
def sort_dependencies(self,
dependencies: List[Dependency],
......@@ -182,7 +186,7 @@ class Provider(SpecificationProvider):
conflicts: Dict[str, List[Conflict]]):
return sorted(dependencies, key=lambda d: [
0 if activated.vertex_named(d.name).payload else 1,
0 if d.accepts_prereleases() 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))
])
......@@ -16,12 +16,14 @@ from .ui import UI
class Solver:
def __init__(self, locked, io):
def __init__(self, package, pool, locked, io):
self._package = package
self._pool = pool
self._locked = locked
self._io = io
def solve(self, requested, pool, fixed=None) -> List[Operation]:
resolver = Resolver(Provider(pool), UI(self._io))
def solve(self, requested, fixed=None) -> List[Operation]:
resolver = Resolver(Provider(self._package, self._pool), UI(self._io))
base = None
if fixed is not None:
......
......@@ -9,4 +9,4 @@ class EmptyConstraint(BaseConstraint):
return True
def __str__(self):
return '[]'
return '*'
......@@ -36,6 +36,6 @@ class MultiConstraint(BaseConstraint):
for constraint in self._constraints:
constraints.append(str(constraint))
return '[{}]'.format(
return '{}'.format(
(' ' if self._conjunctive else ' || ').join(constraints)
)
[[package]]
name = "A"
version = "1.0"
category = "main"
optional = false
python-versions = "*"
platform = "*"
[[package]]
name = "B"
version = "1.1"
category = "main"
optional = false
python-versions = "*"
platform = "*"
[[package]]
name = "C"
version = "1.2"
category = "main"
optional = false
python-versions = "^3.6"
platform = "*"
[metadata]
python-versions = "^3.4"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
A = []
B = []
C = []
......@@ -46,6 +46,8 @@ class Locker(BaseLocker):
def _write_lock_data(self, data) -> None:
for package in data['package']:
package['python-versions'] = str(package['python-versions'])
package['platform'] = str(package['platform'])
if not package['dependencies']:
del package['dependencies']
......@@ -214,3 +216,28 @@ def test_add_with_sub_dependencies(installer, locker, repo, package):
expected = fixture('with-sub-dependencies')
assert locker.written_data == expected
def test_run_with_python_versions(installer, locker, repo, package):
package.python_versions = '^3.4'
package_a = get_package('A', '1.0')
package_b = get_package('B', '1.1')
package_c12 = get_package('C', '1.2')
package_c12.python_versions = '^3.6'
package_c13 = get_package('C', '1.3')
package_c13.python_versions = '~3.3'
repo.add_package(package_a)
repo.add_package(package_b)
repo.add_package(package_c12)
repo.add_package(package_c13)
package.add_dependency('A', '~1.0')
package.add_dependency('B', '^1.0')
package.add_dependency('C', '^1.0')
installer.run()
expected = fixture('with-python-versions')
assert locker.written_data == expected
......@@ -72,7 +72,7 @@ class Index(SpecificationProvider):
if package.is_prerelease() and not requirement.accepts_prereleases():
vertex = activated.vertex_named(package.name)
if not any([r.accepts_prereleases() for r in vertex.requirements]):
if not any([r.allows_prereleases() for r in vertex.requirements]):
return False
return requirement.constraint.matches(Constraint('==', package.version))
......@@ -83,7 +83,7 @@ class Index(SpecificationProvider):
results = []
for spec in self._packages[dependency.name]:
if not dependency.accepts_prereleases() and spec.is_prerelease():
if not dependency.allows_prereleases() and spec.is_prerelease():
continue
if dependency.constraint.matches(Constraint('==', spec.version)):
......@@ -103,7 +103,7 @@ class Index(SpecificationProvider):
conflicts):
return sorted(dependencies, key=lambda d: [
0 if activated.vertex_named(d.name).payload else 1,
0 if d.accepts_prereleases() 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))
])
from poetry.packages import Dependency
from poetry.packages import Package
def test_accepts():
dependency = Dependency('A', '^1.0')
package = Package('A', '1.4')
assert dependency.accepts(package)
def test_accepts_prerelease():
dependency = Dependency('A', '^1.0', allows_prereleases=True)
package = Package('A', '1.4-beta.1')
assert dependency.accepts(package)
def test_accepts_python_versions():
dependency = Dependency('A', '^1.0')
dependency.python_versions = '^3.6'
package = Package('A', '1.4')
package.python_versions = '~3.6'
assert dependency.accepts(package)
def test_accepts_fails_with_different_names():
dependency = Dependency('A', '^1.0')
package = Package('B', '1.4')
assert not dependency.accepts(package)
def test_accepts_fails_with_version_mismatch():
dependency = Dependency('A', '~1.0')
package = Package('B', '1.4')
assert not dependency.accepts(package)
def test_accepts_fails_with_prerelease_mismatch():
dependency = Dependency('A', '^1.0')
package = Package('B', '1.4-beta.1')
assert not dependency.accepts(package)
def test_accepts_fails_with_python_versions_mismatch():
dependency = Dependency('A', '^1.0')
dependency.python_versions = '^3.6'
package = Package('B', '1.4')
package.python_versions = '~3.5'
assert not dependency.accepts(package)
......@@ -3,7 +3,9 @@ import pytest
from cleo.outputs.null_output import NullOutput
from cleo.styles import OutputStyle
from poetry.packages import Package
from poetry.repositories.installed_repository import InstalledRepository
from poetry.repositories.pool import Pool
from poetry.repositories.repository import Repository
from poetry.puzzle import Solver
from poetry.puzzle.exceptions import SolverProblemError
......@@ -18,13 +20,13 @@ def io():
@pytest.fixture()
def installed():
return InstalledRepository()
def package():
return Package('root', '1.0')
@pytest.fixture()
def solver(installed, io):
return Solver(installed, io)
def installed():
return InstalledRepository()
@pytest.fixture()
......@@ -32,6 +34,16 @@ def repo():
return Repository()
@pytest.fixture()
def pool(repo):
return Pool([repo])
@pytest.fixture()
def solver(package, pool, installed, io):
return Solver(package, pool, installed, io)
def check_solver_result(ops, expected):
result = []
for op in ops:
......@@ -58,18 +70,18 @@ def test_solver_install_single(solver, repo):
package_a = get_package('A', '1.0')
repo.add_package(package_a)
ops = solver.solve([get_dependency('A')], repo)
ops = solver.solve([get_dependency('A')])
check_solver_result(ops, [
{'job': 'install', 'package': package_a}
])
def test_solver_remove_if_not_installed(solver, repo, installed):
def test_solver_remove_if_not_installed(solver, installed):
package_a = get_package('A', '1.0')
installed.add_package(package_a)
ops = solver.solve([], repo)
ops = solver.solve([])
check_solver_result(ops, [
{'job': 'remove', 'package': package_a}
......@@ -80,8 +92,8 @@ def test_install_non_existing_package_fail(solver, repo):
package_a = get_package('A', '1.0')
repo.add_package(package_a)
with pytest.raises(SolverProblemError) as e:
solver.solve([get_dependency('B', '1')], repo)
with pytest.raises(SolverProblemError):
solver.solve([get_dependency('B', '1')])
def test_solver_with_deps(solver, repo):
......@@ -95,7 +107,7 @@ def test_solver_with_deps(solver, repo):
package_a.requires.append(get_dependency('B', '<1.1'))
ops = solver.solve([get_dependency('a')], repo)
ops = solver.solve([get_dependency('a')])
check_solver_result(ops, [
{'job': 'install', 'package': package_b},
......@@ -118,7 +130,7 @@ def test_install_honours_not_equal(solver, repo):
package_a.requires.append(get_dependency('B', '<=1.3,!=1.3,!=1.2'))
ops = solver.solve([get_dependency('a')], repo)
ops = solver.solve([get_dependency('a')])
check_solver_result(ops, [
{'job': 'install', 'package': new_package_b11},
......@@ -145,7 +157,7 @@ def test_install_with_deps_in_order(solver, repo):
get_dependency('C'),
]
ops = solver.solve(request, repo)
ops = solver.solve(request)
check_solver_result(ops, [
{'job': 'install', 'package': package_c},
......@@ -162,7 +174,7 @@ def test_install_installed(solver, repo, installed):
get_dependency('A'),
]
ops = solver.solve(request, repo)
ops = solver.solve(request)
check_solver_result(ops, [])
......@@ -179,7 +191,7 @@ def test_update_installed(solver, repo, installed):
get_dependency('A'),
]
ops = solver.solve(request, repo)
ops = solver.solve(request)
check_solver_result(ops, [
{'job': 'update', 'from': package_a, 'to': new_package_a}
......@@ -198,7 +210,7 @@ def test_update_with_fixed(solver, repo, installed):
get_dependency('A'),
]
ops = solver.solve(request, repo, fixed=[get_dependency('A', '1.0')])
ops = solver.solve(request, fixed=[get_dependency('A', '1.0')])
check_solver_result(ops, [])
......@@ -218,7 +230,7 @@ def test_solver_sets_categories(solver, repo):
get_dependency('B', category='dev')
]
ops = solver.solve(request, repo)
ops = solver.solve(request)
check_solver_result(ops, [
{'job': 'install', 'package': package_c},
......@@ -229,3 +241,55 @@ def test_solver_sets_categories(solver, repo):
assert package_c.category == 'dev'
assert package_b.category == 'dev'
assert package_a.category == 'main'
def test_solver_respects_root_package_python_versions(solver, repo, package):
package.python_versions = '^3.4'
package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0')
package_b.python_versions = '^3.6'
package_c = get_package('C', '1.0')
package_c.python_versions = '^3.6'
package_c11 = get_package('C', '1.1')
package_c11.python_versions = '~3.3'
package_b.add_dependency('C', '^1.0')
repo.add_package(package_a)
repo.add_package(package_b)
repo.add_package(package_c)
repo.add_package(package_c11)
request = [
get_dependency('A'),
get_dependency('B')
]
ops = solver.solve(request)
check_solver_result(ops, [
{'job': 'install', 'package': package_c},
{'job': 'install', 'package': package_b},
{'job': 'install', 'package': package_a},
])
def test_solver_fails_if_mismatch_root_python_versions(solver, repo, package):
package.python_versions = '^3.4'
package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0')
package_b.python_versions = '^3.6'
package_c = get_package('C', '1.0')
package_c.python_versions = '~3.3'
package_b.add_dependency('C', '~1.0')
repo.add_package(package_a)
repo.add_package(package_b)
repo.add_package(package_c)
request = [
get_dependency('A'),
get_dependency('B')
]
with pytest.raises(SolverProblemError):
solver.solve(request)
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