Skip to content

Commit 3eda75a

Browse files
committed
Add solutions for common errors
1 parent d4be652 commit 3eda75a

16 files changed

+196
-7
lines changed

poetry.lock

+4-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

poetry/console/config/application_config.py

+10
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from poetry.console.commands.env_command import EnvCommand
3030
from poetry.console.logging.io_formatter import IOFormatter
3131
from poetry.console.logging.io_handler import IOHandler
32+
from poetry.utils._compat import PY36
3233

3334

3435
class ApplicationConfig(BaseApplicationConfig):
@@ -46,6 +47,15 @@ def configure(self):
4647
self.add_event_listener(PRE_HANDLE, self.register_command_loggers)
4748
self.add_event_listener(PRE_HANDLE, self.set_env)
4849

50+
if PY36:
51+
from poetry.mixology.solutions.providers import (
52+
PythonRequirementSolutionProvider,
53+
)
54+
55+
self._solution_provider_repository.register_solution_providers(
56+
[PythonRequirementSolutionProvider]
57+
)
58+
4959
def register_command_loggers(
5060
self, event, event_name, _
5161
): # type: (PreHandleEvent, str, Any) -> None

poetry/mixology/failure.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from typing import List
33
from typing import Tuple
44

5+
from poetry.core.semver import parse_constraint
6+
57
from .incompatibility import Incompatibility
68
from .incompatibility_cause import ConflictCause
79
from .incompatibility_cause import PythonCause
@@ -44,10 +46,15 @@ def write(self):
4446
)
4547
required_python_version_notification = True
4648

49+
root_constraint = parse_constraint(
50+
incompatibility.cause.root_python_version
51+
)
52+
constraint = parse_constraint(incompatibility.cause.python_version)
4753
buffer.append(
48-
" - {} requires Python {}".format(
54+
" - {} requires Python {}, so it will not be satisfied for Python {}".format(
4955
incompatibility.terms[0].dependency.name,
5056
incompatibility.cause.python_version,
57+
root_constraint.difference(constraint),
5158
)
5259
)
5360

poetry/mixology/solutions/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .python_requirement_solution_provider import PythonRequirementSolutionProvider
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import re
2+
3+
from typing import List
4+
5+
from crashtest.contracts.has_solutions_for_exception import HasSolutionsForException
6+
from crashtest.contracts.solution import Solution
7+
8+
9+
class PythonRequirementSolutionProvider(HasSolutionsForException):
10+
def can_solve(self, exception): # type: (Exception) -> bool
11+
from poetry.puzzle.exceptions import SolverProblemError
12+
13+
if not isinstance(exception, SolverProblemError):
14+
return False
15+
16+
m = re.match(
17+
"^The current project's Python requirement (.+) is not compatible "
18+
"with some of the required packages Python requirement",
19+
str(exception),
20+
)
21+
22+
if not m:
23+
return False
24+
25+
return True
26+
27+
def get_solutions(self, exception): # type: (Exception) -> List[Solution]
28+
from ..solutions.python_requirement_solution import PythonRequirementSolution
29+
30+
return [PythonRequirementSolution(exception)]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .python_requirement_solution import PythonRequirementSolution
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from crashtest.contracts.solution import Solution
2+
3+
4+
class PythonRequirementSolution(Solution):
5+
def __init__(self, exception):
6+
from poetry.mixology.incompatibility_cause import PythonCause
7+
from poetry.core.semver import parse_constraint
8+
9+
self._title = "Check your dependencies Python requirement."
10+
11+
failure = exception.error
12+
version_solutions = []
13+
for incompatibility in failure._incompatibility.external_incompatibilities:
14+
if isinstance(incompatibility.cause, PythonCause):
15+
root_constraint = parse_constraint(
16+
incompatibility.cause.root_python_version
17+
)
18+
constraint = parse_constraint(incompatibility.cause.python_version)
19+
20+
version_solutions.append(
21+
"For <fg=default;options=bold>{}</>, a possible solution would be "
22+
'to set the `<fg=default;options=bold>python</>` property to <fg=yellow>"{}"</>'.format(
23+
incompatibility.terms[0].dependency.name,
24+
root_constraint.intersect(constraint),
25+
)
26+
)
27+
28+
description = (
29+
"The Python requirement can be specified via the `<fg=default;options=bold>python</>` "
30+
"or `<fg=default;options=bold>markers</>` properties"
31+
)
32+
if version_solutions:
33+
description += "\n\n" + "\n".join(version_solutions)
34+
35+
description += "\n"
36+
37+
self._description = description
38+
39+
@property
40+
def solution_title(self) -> str:
41+
return self._title
42+
43+
@property
44+
def solution_description(self):
45+
return self._description
46+
47+
@property
48+
def documentation_links(self):
49+
return [
50+
"https://python-poetry.org/docs/dependency-specification/#python-restricted-dependencies",
51+
"https://python-poetry.org/docs/dependency-specification/#using-environment-markers",
52+
]

poetry/mixology/version_solver.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22
import time
33

4+
from typing import TYPE_CHECKING
45
from typing import Any
56
from typing import Dict
67
from typing import List
@@ -11,7 +12,6 @@
1112
from poetry.core.packages import ProjectPackage
1213
from poetry.core.semver import Version
1314
from poetry.core.semver import VersionRange
14-
from poetry.puzzle.provider import Provider
1515

1616
from .failure import SolveFailure
1717
from .incompatibility import Incompatibility
@@ -25,6 +25,10 @@
2525
from .term import Term
2626

2727

28+
if TYPE_CHECKING:
29+
from poetry.puzzle.provider import Provider
30+
31+
2832
_conflict = object()
2933

3034

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ python = "~2.7 || ^3.5"
2727
poetry-core = "^1.0.0a6"
2828
cleo = "^0.8.1"
2929
clikit = "^0.6.2"
30+
crashtest = { version = "^0.3.0", python = "^3.6" }
3031
requests = "^2.18"
3132
cachy = "^0.3.0"
3233
requests-toolbelt = "^0.8.0"

tests/mixology/solutions/__init__.py

Whitespace-only changes.

tests/mixology/solutions/providers/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import pytest
2+
3+
from poetry.core.packages.dependency import Dependency
4+
from poetry.mixology.failure import SolveFailure
5+
from poetry.mixology.incompatibility import Incompatibility
6+
from poetry.mixology.incompatibility_cause import NoVersionsCause
7+
from poetry.mixology.incompatibility_cause import PythonCause
8+
from poetry.mixology.term import Term
9+
from poetry.puzzle.exceptions import SolverProblemError
10+
from poetry.utils._compat import PY36
11+
12+
13+
@pytest.mark.skipif(
14+
not PY36, reason="Error solutions are only available for Python ^3.6"
15+
)
16+
def test_it_can_solve_python_incompatibility_solver_errors():
17+
from poetry.mixology.solutions.providers import PythonRequirementSolutionProvider
18+
from poetry.mixology.solutions.solutions import PythonRequirementSolution
19+
20+
incompatibility = Incompatibility(
21+
[Term(Dependency("foo", "^1.0"), True)], PythonCause("^3.5", ">=3.6")
22+
)
23+
exception = SolverProblemError(SolveFailure(incompatibility))
24+
provider = PythonRequirementSolutionProvider()
25+
26+
assert provider.can_solve(exception)
27+
assert isinstance(provider.get_solutions(exception)[0], PythonRequirementSolution)
28+
29+
30+
@pytest.mark.skipif(
31+
not PY36, reason="Error solutions are only available for Python ^3.6"
32+
)
33+
def test_it_cannot_solve_other_solver_errors():
34+
from poetry.mixology.solutions.providers import PythonRequirementSolutionProvider
35+
36+
incompatibility = Incompatibility(
37+
[Term(Dependency("foo", "^1.0"), True)], NoVersionsCause()
38+
)
39+
exception = SolverProblemError(SolveFailure(incompatibility))
40+
provider = PythonRequirementSolutionProvider()
41+
42+
assert not provider.can_solve(exception)

tests/mixology/solutions/solutions/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import pytest
2+
3+
from clikit.io.buffered_io import BufferedIO
4+
5+
from poetry.core.packages.dependency import Dependency
6+
from poetry.mixology.failure import SolveFailure
7+
from poetry.mixology.incompatibility import Incompatibility
8+
from poetry.mixology.incompatibility_cause import PythonCause
9+
from poetry.mixology.term import Term
10+
from poetry.puzzle.exceptions import SolverProblemError
11+
from poetry.utils._compat import PY36
12+
13+
14+
@pytest.mark.skipif(
15+
not PY36, reason="Error solutions are only available for Python ^3.6"
16+
)
17+
def test_it_provides_the_correct_solution():
18+
from poetry.mixology.solutions.solutions import PythonRequirementSolution
19+
20+
incompatibility = Incompatibility(
21+
[Term(Dependency("foo", "^1.0"), True)], PythonCause("^3.5", ">=3.6")
22+
)
23+
exception = SolverProblemError(SolveFailure(incompatibility))
24+
solution = PythonRequirementSolution(exception)
25+
26+
title = "Check your dependencies Python requirement."
27+
description = """\
28+
The Python requirement can be specified via the `python` or `markers` properties
29+
30+
For foo, a possible solution would be to set the `python` property to ">=3.6,<4.0"\
31+
"""
32+
links = [
33+
"https://python-poetry.org/docs/dependency-specification/#python-restricted-dependencies",
34+
"https://python-poetry.org/docs/dependency-specification/#using-environment-markers",
35+
]
36+
37+
assert title == solution.solution_title
38+
assert (
39+
description == BufferedIO().remove_format(solution.solution_description).strip()
40+
)
41+
assert links == solution.documentation_links

tests/mixology/version_solver/test_python_constraint.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def test_dependency_does_not_match_root_python_constraint(root, provider, repo):
1010

1111
error = """The current project's Python requirement (^3.6) \
1212
is not compatible with some of the required packages Python requirement:
13-
- foo requires Python <3.5
13+
- foo requires Python <3.5, so it will not be satisfied for Python >=3.6,<4.0
1414
1515
Because no versions of foo match !=1.0.0
1616
and foo (1.0.0) requires Python <3.5, foo is forbidden.

0 commit comments

Comments
 (0)