Commit c008f402 by Sébastien Eustace

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

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