Commit e853ed1e by Sébastien Eustace

Add basic solver

parent e473d2ae
# -*- coding: utf-8 -*- from poetry.console import Application
__version__ = '0.1.0'
console = Application('Poetry', __version__)
# -*- coding: utf-8 -*-
from pip.utils.appdirs import user_cache_dir, user_config_dir
CACHE_DIR = user_cache_dir('pypoetry')
CONFIG_DIR = user_config_dir('pypoetry')
...@@ -3,10 +3,15 @@ from poetry.semver.version_parser import VersionParser ...@@ -3,10 +3,15 @@ from poetry.semver.version_parser import VersionParser
class Dependency: class Dependency:
def __init__(self, name, constraint): def __init__(self, name, constraint, optional=False):
self._name = name.lower() self._name = name.lower()
self._constraint = VersionParser().parse_constraints(constraint) try:
self._constraint = VersionParser().parse_constraints(constraint)
except ValueError:
self._constraint = VersionParser().parse_constraints('*')
self._pretty_constraint = constraint self._pretty_constraint = constraint
self._optional = optional
@property @property
def name(self): def name(self):
...@@ -27,6 +32,9 @@ class Dependency: ...@@ -27,6 +32,9 @@ class Dependency:
def accepts_prereleases(self): def accepts_prereleases(self):
return False return False
def is_optional(self):
return self._optional
def __eq__(self, other): def __eq__(self, other):
if not isinstance(other, Dependency): if not isinstance(other, Dependency):
return NotImplemented return NotImplemented
...@@ -34,7 +42,10 @@ class Dependency: ...@@ -34,7 +42,10 @@ class Dependency:
return self._name == other.name and self._constraint == other.constraint return self._name == other.name and self._constraint == other.constraint
def __hash__(self): def __hash__(self):
return hash(self._name) return hash((self._name, self._pretty_constraint))
def __str__(self):
return self.pretty_name
def __repr__(self): def __repr__(self):
return '<Dependency {}>'.format(self.pretty_name) return '<Dependency {}>'.format(self.pretty_name)
...@@ -105,7 +105,7 @@ class Package: ...@@ -105,7 +105,7 @@ class Package:
return self._stability != 'stable' return self._stability != 'stable'
def __hash__(self): def __hash__(self):
return hash(self._name) return hash((self._name, self._version))
def __eq__(self, other): def __eq__(self, other):
if not isinstance(other, Package): if not isinstance(other, Package):
......
from .solver import Solver
class SolverProblemError(Exception):
def __init__(self, error):
self._error = error
super().__init__(str(error))
@property
def error(self):
return self._error
from .install import Install
from .uninstall import Uninstall
from .update import Update
from .operation import Operation
class Install(Operation):
def __init__(self, package, reason: str = None) -> None:
super().__init__(reason)
self._package = package
@property
def package(self):
return self._package
@property
def job_type(self):
return 'install'
def __str__(self) -> str:
return 'Installing {} ({})'.format(
self.package.pretty_name,
self.format_version(self.package)
)
def __repr__(self):
return '<Install {} ({})>'.format(
self.package.pretty_name,
self.format_version(self.package)
)
# -*- coding: utf-8 -*-
class Operation:
def __init__(self, reason: str = None) -> None:
self._reason = reason
@property
def reason(self) -> str:
return self._reason
def format_version(self, package):
return package.full_pretty_version
from .operation import Operation
class Uninstall(Operation):
def __init__(self, package, reason=None):
super(Uninstall, self).__init__(reason)
self._package = package
@property
def package(self):
return self._package
@property
def job_type(self):
return 'uninstall'
def __str__(self):
return 'Uninstalling {} ({})'.format(
self.package.pretty_name,
self.format_version(self._package)
)
def __repr__(self):
return '<Uninstall {} ({})>'.format(
self.package.pretty_name,
self.format_version(self.package)
)
from .operation import Operation
class Update(Operation):
def __init__(self, initial, target, reason=None):
self._initial_package = initial
self._target_package = target
super(Update, self).__init__(reason)
@property
def initial_package(self):
return self._initial_package
@property
def target_package(self):
return self._target_package
@property
def job_type(self):
return 'update'
def __str__(self):
return (
'Updating {} ({}) to {} ({})'.format(
self.initial_package.pretty_name,
self.format_version(self.initial_package),
self.target_package.pretty_name,
self.format_version(self.target_package)
)
)
def __repr__(self):
return (
'<Update {} ({}) to {} ({})>'.format(
self.initial_package.pretty_name,
self.format_version(self.initial_package),
self.target_package.pretty_name,
self.format_version(self.target_package)
)
)
from functools import cmp_to_key
from typing import Dict
from typing import List
from poetry.mixology import DependencyGraph
from poetry.mixology.conflict import Conflict
from poetry.mixology.contracts import SpecificationProvider
from poetry.packages import Dependency
from poetry.packages import Package
from poetry.repositories.repository import Repository
from poetry.semver import less_than
from poetry.semver.constraints import Constraint
class Provider(SpecificationProvider):
def __init__(self, repository: Repository):
self._repository = repository
@property
def repository(self) -> Repository:
return self._repository
@property
def name_for_explicit_dependency_source(self) -> str:
return 'poetry.toml'
@property
def name_for_locking_dependency_source(self) -> str:
return 'poetry.lock'
def name_for(self, dependency: Dependency) -> str:
"""
Returns the name for the given dependency.
"""
return dependency.name
def search_for(self, dependency: Dependency) -> List[Package]:
"""
Search for the specifications that match the given dependency.
The specifications in the returned list will be considered in reverse
order, so the latest version ought to be last.
"""
packages = self._repository.find_packages(
dependency.name,
dependency.constraint
)
packages.sort(
key=cmp_to_key(
lambda x, y:
0 if x.version == y.version
else -1 * int(less_than(x.version, y.version) or -1)
)
)
return packages
def dependencies_for(self, package: Package):
package = self._repository.package(package.name, package.version)
return [r for r in package.requires if not r.is_optional()]
def is_requirement_satisfied_by(self,
requirement: Dependency,
activated: DependencyGraph,
package: Package) -> bool:
"""
Determines whether the given requirement is satisfied by the given
spec, in the context of the current activated dependency graph.
"""
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.accepts_prereleases() for r in vertex.requirements]):
return False
return requirement.constraint.matches(Constraint('==', package.version))
def sort_dependencies(self,
dependencies: List[Dependency],
activated: DependencyGraph,
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.name in conflicts else 1,
0 if activated.vertex_named(d.name).payload else len(self.search_for(d))
])
from typing import List
from poetry.mixology import Resolver
from poetry.mixology.exceptions import ResolverError
from .exceptions import SolverProblemError
from .operations import Install
from .operations import Uninstall
from .operations import Update
from .operations.operation import Operation
from .provider import Provider
from .ui import UI
class Solver:
def __init__(self, installed, io):
self._installed = installed
self._io = io
def solve(self, requested, repository) -> List[Operation]:
resolver = Resolver(Provider(repository), UI(self._io))
try:
graph = resolver.resolve(requested)
except ResolverError as e:
raise SolverProblemError(e)
packages = [v.payload for v in graph.vertices.values()]
operations = []
for package in packages:
installed = False
for pkg in self._installed.packages:
if package.name == pkg.name:
installed = True
# Checking version
if package.version != pkg.version:
operations.append(Update(pkg, package))
break
if not installed:
operations.append(Install(package))
# Checking for removals
for pkg in self._installed.packages:
remove = True
for package in packages:
if pkg.name == package.name:
remove = False
break
if remove:
operations.append(Uninstall(pkg))
return list(reversed(operations))
from cleo.styles import CleoStyle
from cleo.helpers import ProgressIndicator
from poetry.mixology.contracts import UI as BaseUI
class UI(BaseUI):
def __init__(self, io: CleoStyle):
self._io = io
self._progress = None
super().__init__(self._io.is_debug())
@property
def output(self):
return self._io
def before_resolution(self) -> None:
self._io.write('<info>Resolving dependencies</>')
if self.is_debugging():
self._io.new_line()
def indicate_progress(self):
if not self.is_debugging():
self._io.write('.')
def after_resolution(self) -> None:
self._io.new_line()
def debug(self, message, depth) -> None:
if self.is_debugging():
debug_info = str(message)
debug_info = '\n'.join([
'<comment>:{}:</> {}'.format(str(depth).rjust(4), s)
for s in debug_info.split('\n')
]) + '\n'
self.output.write(debug_info)
from poetry.semver.constraints import Constraint
class BaseRepository:
SEARCH_FULLTEXT = 0
SEARCH_NAME = 1
def __init__(self):
self._packages = []
@property
def packages(self):
return self._packages
def has_package(self, package):
raise NotImplementedError()
def package(self, name, version):
raise NotImplementedError()
def find_packages(self, name, constraint=None):
raise NotImplementedError()
def search(self, query, mode=SEARCH_FULLTEXT):
raise NotImplementedError()
def get_dependents(self, needle,
constraint=None, invert=False,
recurse=True, packages_found=None):
results = {}
needles = needle
if not isinstance(needles, list):
needles = [needles]
# initialize the list with the needles before any recursion occurs
if packages_found is None:
packages_found = needles
# locate root package for use below
root_package = None
for package in self.packages:
if isinstance(package, RootPackage):
root_package = package
break
# Loop over all currently installed packages.
for package in self.packages:
links = package.requires
# each loop needs its own "tree"
# as we want to show the complete dependent set of every needle
# without warning all the time about finding circular deps
packages_in_tree = packages_found
# Require-dev is only relevant for the root package
if isinstance(package, RootPackage):
links += package.dev_requires
# Cross-reference all discovered links to the needles
for link in links:
for needle in needles:
if link.target == needle:
if (
constraint is None
or link.constraint.matches(constraint) is not invert
):
# already displayed this node's dependencies,
# cutting short
if link.source in packages_in_tree:
results[link.source] = (package, link, False)
continue
packages_in_tree.append(link.source)
if recurse:
dependents = self.get_dependents(
link.source, None, False, True, packages_in_tree
)
else:
dependents = {}
results[link.source] = (package, link, dependents)
# When inverting, we need to check for conflicts
# of the needles against installed packages
if invert and package.name in needles:
for link in package.conflicts:
for pkg in self.find_packages(link.target):
version = Constraint('=', pkg.version)
if link.constraint.matches(version) is invert:
results[len(results) - 1] = (package, link, False)
# When inverting, we need to check for conflicts of the needles'
# requirements against installed packages
if (
invert
and constraint
and package.name in needles
and constraint.matches(Constraint('=', package.version))
):
for link in package.requires:
for pkg in self._packages:
if link.target not in pkg.names:
continue
version = Constraint('=', pkg.version)
if not link.constraint.matches(version):
# if we have a root package
# we show the root requires as well
# to perhaps allow to find an issue there
if root_package:
for root_req in root_package.requires + root_package.dev_requires:
if root_req.target in pkg.names and not root_req.constraint.matches(link.constraint):
results[len(results) - 1] = (package, link, False)
results[len(results) - 1] = (root_package, root_req, False)
continue
results[len(results) - 1] = (package, link, False)
lnk = Link(
root_package.name,
link.target,
None,
'does not require',
'but {} is installed'.format(
pkg.pretty_version
)
)
results[len(results) - 1] = (package, lnk, False)
else:
results[len(results) - 1] = (package, link, False)
continue
return results
from pip.commands.freeze import freeze
from poetry.packages import Package
from .repository import Repository
class InstalledRepository(Repository):
def __init__(self, packages=None):
super(InstalledRepository, self).__init__(packages)
import re
from pathlib import Path
from typing import List
from typing import Union
from cachy import CacheManager
from requests import get
from poetry.locations import CACHE_DIR
from poetry.packages import Dependency
from poetry.packages import Package
from poetry.semver.constraints import Constraint
from poetry.semver.constraints.base_constraint import BaseConstraint
from poetry.semver.version_parser import VersionParser
from .repository import Repository
class PyPiRepository(Repository):
def __init__(self, url='https://pypi.org/'):
self._url = url
self._cache = CacheManager({
'default': 'releases',
'serializer': 'json',
'stores': {
'releases': {
'driver': 'file',
'path': Path(CACHE_DIR) / 'cache'
},
'packages': {
'driver': 'dict'
}
}
})
super(PyPiRepository, self).__init__()
def find_packages(self,
name: str,
constraint: Union[Constraint, None] = None
) -> List[Package]:
"""
Find packages on the remote server.
"""
packages = []
if constraint is not None and not isinstance(constraint, BaseConstraint):
version_parser = VersionParser()
constraint = version_parser.parse_constraints(constraint)
info = self.get_package_info(name)
versions = []
for version, release in info['releases'].items():
if (
not constraint
or (constraint and constraint.matches(Constraint('=', version)))
):
versions.append(version)
for version in versions:
packages.append(Package(name, version, version))
return packages
def package(self,
name: str,
version: str) -> Package:
try:
index = self._packages.index(Package(name, version, version))
return self._packages[index]
except ValueError:
release_info = self.get_release_info(name, version)
package = Package(name, version, version)
for dependency in release_info['requires_dist']:
m = re.match(
'^(?P<name>[^ ;]+)'
'(?: \((?P<version>.+)\))?'
'(?:;(?P<extra>(.+)))?$',
dependency
)
package.requires.append(
Dependency(
m.group('name'),
m.group('version') or '*',
optional=m.group('extra') is not None
)
)
self._packages.append(package)
return package
def search(self, query, mode=0):
results = []
search = {
'name': query
}
if mode == self.SEARCH_FULLTEXT:
search['summary'] = query
client = ServerProxy(self._url)
hits = client.search(search, 'or')
for hit in hits:
results.append({
'name': hit['name'],
'description': hit['summary'],
'version': hit['version']
})
return results
def get_package_info(self, name: str) -> dict:
"""
Return the package information given its name.
The information is returned from the cache if it exists
or retrieved from the remote server.
"""
return self._cache.store('packages').remember_forever(
f'{name}',
lambda: self._get_package_info(name)
)
def _get_package_info(self, name: str) -> dict:
json_response = get(self._url + f'pypi/{name}/json')
if json_response.status_code == 404:
raise ValueError(f'Package [{name}] not found.')
data = json_response.json()
return data
def get_release_info(self, name: str, version: str) -> dict:
"""
Return the release information given a package name and a version.
The information is returned from the cache if it exists
or retrieved from the remote server.
"""
return self._cache.remember_forever(
f'{name}:{version}',
lambda: self._get_release_info(name, version)
)
def _get_release_info(self, name: str, version: str) -> dict:
json_response = get(self._url + f'pypi/{name}/{version}/json')
if json_response.status_code == 404:
raise ValueError(f'Package [{name}] not found.')
json_data = json_response.json()
info = json_data['info']
data = {
'name': info['name'],
'version': info['version'],
'platform': info['platform'],
'requires_dist': info['requires_dist'],
'requires_python': info['requires_python'],
'digests': []
}
for file_info in json_data['releases'][version]:
data['digests'].append(file_info['digests']['sha256'])
return data
import re
from poetry.semver.constraints import Constraint
from poetry.semver.constraints.base_constraint import BaseConstraint
from poetry.semver.helpers import normalize_version
from poetry.semver.version_parser import VersionParser
from .base_repository import BaseRepository
class Repository(BaseRepository):
def __init__(self, packages=None):
super(Repository, self).__init__()
if packages is None:
packages = []
for package in packages:
self.add_package(package)
def package(self, name, version):
name = name.lower()
version = normalize_version(version)
for package in self.packages:
if name == package.name and package.version == version:
return package
def find_packages(self, name, constraint=None):
name = name.lower()
packages = []
if not isinstance(constraint, BaseConstraint):
parser = VersionParser()
constraint = parser.parse_constraints(constraint)
for package in self.packages:
if name == package.name:
pkg_constraint = Constraint('==', package.version)
if constraint is None or constraint.matches(pkg_constraint):
packages.append(package)
return packages
def search(self, query, mode=0):
regex = '(?i)(?:{})'.format('|'.join(re.split('\s+', query)))
matches = {}
for package in self.packages:
name = package.name
if name in matches:
continue
if (
re.match(regex, name) is not None
or (
mode == self.SEARCH_FULLTEXT
and isinstance(package, CompletePackage)
and re.match(regex, '')
)
):
matches[name] = {
'name': package.pretty_name,
'description': (package.description
if isinstance(package, CompletePackage)
else '')
}
return list(matches.values())
def has_package(self, package):
package_id = package.unique_name
for repo_package in self.packages:
if package_id == repo_package.unique_name:
return True
return False
def add_package(self, package):
self._packages.append(package)
def remove_package(self, package):
package_id = package.unique_name
index = None
for i, repo_package in enumerate(self.packages):
if package_id == repo_package.unique_name:
index = i
break
if index is not None:
del self._packages[index]
def __len__(self):
return len(self._packages)
...@@ -70,9 +70,21 @@ class Constraint(BaseConstraint): ...@@ -70,9 +70,21 @@ class Constraint(BaseConstraint):
f'expected one of: {", ".join(self.supported_operators)}' f'expected one of: {", ".join(self.supported_operators)}'
) )
# If we can't normalize the version
# we delegate to parse_version()
try:
a = normalize_version(a)
except ValueError:
pass
try:
b = normalize_version(b)
except ValueError:
pass
return self._trans_op_str[operator]( return self._trans_op_str[operator](
parse_version(normalize_version(a)), parse_version(a),
parse_version(normalize_version(b)) parse_version(b)
) )
def match_specific(self, provider: 'Constraint') -> bool: def match_specific(self, provider: 'Constraint') -> bool:
......
...@@ -2,7 +2,8 @@ import re ...@@ -2,7 +2,8 @@ import re
_modifier_regex = ( _modifier_regex = (
'[._-]?' '[._-]?'
'(?:(stable|beta|b|RC|alpha|a|patch|pl|p)((?:[.-]?\d+)*)?)?' '(?:(stable|beta|b|RC|c|pre|alpha|a|patch|pl|p|post|[a-z])'
'((?:[.-]?\d+)*)?)?'
'([.-]?dev)?' '([.-]?dev)?'
) )
...@@ -33,17 +34,32 @@ def normalize_version(version): ...@@ -33,17 +34,32 @@ def normalize_version(version):
f'{m.group(4) if m.group(4) else ".0"}' f'{m.group(4) if m.group(4) else ".0"}'
index = 5 index = 5
else: else:
# Match date(time) based versioning # Some versions have the form M.m.p-\d+
# which means M.m.p-post\d+
m = re.match( m = re.match(
'(?i)^v?(\d{{4}}(?:[.:-]?\d{{2}}){{1,6}}(?:[.:-]?\d{{1,3}})?){}$'.format( '(?i)^v?(\d{{1,5}})(\.\d+)?(\.\d+)?(\.\d+)?-(?:\d+){}$'.format(
_modifier_regex _modifier_regex
), ),
version version
) )
if m: if m:
version = re.sub('\D', '.', m.group(1)) version = f'{m.group(1)}' \
f'{m.group(2) if m.group(2) else ".0"}' \
index = 2 f'{m.group(3) if m.group(3) else ".0"}' \
f'{m.group(4) if m.group(4) else ".0"}'
index = 5
else:
# Match date(time) based versioning
m = re.match(
'(?i)^v?(\d{{4}}(?:[.:-]?\d{{2}}){{1,6}}(?:[.:-]?\d{{1,3}})?){}$'.format(
_modifier_regex
),
version
)
if m:
version = re.sub('\D', '.', m.group(1))
index = 2
# add version modifiers if a version was matched # add version modifiers if a version was matched
if index is not None: if index is not None:
...@@ -85,12 +101,12 @@ def parse_stability(version: str) -> str: ...@@ -85,12 +101,12 @@ def parse_stability(version: str) -> str:
if m.group(1): if m.group(1):
if m.group(1) in ['beta', 'b']: if m.group(1) in ['beta', 'b']:
return 'beta' return 'beta'
elif m.group(1) in ['alpha', 'a']:
if m.group(1) in ['alpha', 'a']:
return 'alpha' return 'alpha'
elif m.group(1) in ['rc', 'c']:
if m.group(1) == 'rc':
return 'RC' return 'RC'
else:
return 'dev'
return 'stable' return 'stable'
...@@ -102,7 +118,11 @@ def _expand_stability(stability: str) -> str: ...@@ -102,7 +118,11 @@ def _expand_stability(stability: str) -> str:
return 'alpha' return 'alpha'
elif stability == 'b': elif stability == 'b':
return 'beta' return 'beta'
elif stability in ['c', 'pre']:
return 'rc'
elif stability in ['p', 'pl']: elif stability in ['p', 'pl']:
return 'patch' return 'patch'
elif stability in ['post']:
return ''
return stability return stability
from poetry.packages import Dependency
from poetry.packages import Package
from poetry.semver.helpers import normalize_version
def get_package(name, version):
return Package(name, normalize_version(version), version)
def get_dependency(name, constraint=None):
return Dependency(name, constraint or '*')
import pytest
from cleo.outputs.null_output import NullOutput
from cleo.styles import OutputStyle
from poetry.repositories.installed_repository import InstalledRepository
from poetry.repositories.repository import Repository
from poetry.puzzle import Solver
from poetry.puzzle.exceptions import SolverProblemError
from tests.helpers import get_dependency
from tests.helpers import get_package
@pytest.fixture()
def io():
return OutputStyle(NullOutput())
@pytest.fixture()
def installed():
return InstalledRepository()
@pytest.fixture()
def solver(installed, io):
return Solver(installed, io)
@pytest.fixture()
def repo():
return Repository()
def check_solver_result(ops, expected):
result = []
for op in ops:
if 'update' == op.job_type:
result.append({
'job': 'update',
'from': op.initial_package,
'to': op.target_package
})
else:
job = 'install'
if op.job_type == 'uninstall':
job = 'remove'
result.append({
'job': job,
'package': op.package
})
assert result == expected
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)
check_solver_result(ops, [
{'job': 'install', 'package': package_a}
])
def test_solver_remove_if_not_installed(solver, repo, installed):
package_a = get_package('A', '1.0')
installed.add_package(package_a)
ops = solver.solve([], repo)
check_solver_result(ops, [
{'job': 'remove', 'package': package_a}
])
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)
def test_solver_with_deps(solver, repo):
package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0')
new_package_b = get_package('B', '1.1')
repo.add_package(package_a)
repo.add_package(package_b)
repo.add_package(new_package_b)
package_a.requires.append(get_dependency('B', '<1.1'))
ops = solver.solve([get_dependency('a')], repo)
check_solver_result(ops, [
{'job': 'install', 'package': package_b},
{'job': 'install', 'package': package_a},
])
def test_install_honours_not_equal(solver, repo):
package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0')
new_package_b11 = get_package('B', '1.1')
new_package_b12 = get_package('B', '1.2')
new_package_b13 = get_package('B', '1.3')
repo.add_package(package_a)
repo.add_package(package_b)
repo.add_package(new_package_b11)
repo.add_package(new_package_b12)
repo.add_package(new_package_b13)
package_a.requires.append(get_dependency('B', '<=1.3,!=1.3,!=1.2'))
ops = solver.solve([get_dependency('a')], repo)
check_solver_result(ops, [
{'job': 'install', 'package': new_package_b11},
{'job': 'install', 'package': package_a},
])
def test_install_with_deps_in_order(solver, repo):
package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0')
package_c = get_package('C', '1.0')
repo.add_package(package_a)
repo.add_package(package_b)
repo.add_package(package_c)
package_b.requires.append(get_dependency('A', '>=1.0'))
package_b.requires.append(get_dependency('C', '>=1.0'))
package_c.requires.append(get_dependency('A', '>=1.0'))
request = [
get_dependency('A'),
get_dependency('B'),
get_dependency('C'),
]
ops = solver.solve(request, repo)
check_solver_result(ops, [
{'job': 'install', 'package': package_c},
{'job': 'install', 'package': package_b},
{'job': 'install', 'package': package_a},
])
def test_install_installed(solver, repo, installed):
installed.add_package(get_package('A', '1.0'))
repo.add_package(get_package('A', '1.0'))
request = [
get_dependency('A'),
]
ops = solver.solve(request, repo)
check_solver_result(ops, [])
def test_update_installed(solver, repo, installed):
installed.add_package(get_package('A', '1.0'))
package_a = get_package('A', '1.0')
new_package_a = get_package('A', '1.1')
repo.add_package(package_a)
repo.add_package(new_package_a)
request = [
get_dependency('A'),
]
ops = solver.solve(request, repo)
check_solver_result(ops, [
{'job': 'update', 'from': package_a, 'to': new_package_a}
])
...@@ -27,6 +27,9 @@ from poetry.semver.helpers import normalize_version ...@@ -27,6 +27,9 @@ from poetry.semver.helpers import normalize_version
('20100102203040-10', '20100102203040.10'), ('20100102203040-10', '20100102203040.10'),
('20100102-203040-p1', '20100102.203040-patch.1'), ('20100102-203040-p1', '20100102.203040-patch.1'),
('1.0.0-beta.5+foo', '1.0.0.0-beta.5'), ('1.0.0-beta.5+foo', '1.0.0.0-beta.5'),
('0.6c', '0.6.0.0-rc'),
('3.0.17-20140602', '3.0.17.0'),
('3.0pre', '3.0.0.0-rc')
] ]
) )
def test_normalize(version, expected): def test_normalize(version, expected):
......
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