Commit ea7d9d1f by Sébastien Eustace Committed by GitHub

Add solutions for common errors (#2396)

parent b3980622
......@@ -215,7 +215,7 @@ version = "0.4.3"
[[package]]
category = "main"
description = "Updated configparser from Python 3.7 for Python 2.6+."
marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version == \"2.7\" and python_version == \"2.7\" or python_version < \"3\""
marker = "python_version < \"3\" or python_version >= \"2.7\" and python_version < \"2.8\""
name = "configparser"
optional = false
python-versions = ">=2.6"
......@@ -228,7 +228,7 @@ testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2)", "pytes
[[package]]
category = "main"
description = "Backports and enhancements for the contextlib module"
marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version < \"3.4\""
marker = "python_version < \"3.4\" or python_version >= \"2.7\" and python_version < \"2.8\""
name = "contextlib2"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
......@@ -446,7 +446,7 @@ testing = ["packaging", "importlib-resources"]
[[package]]
category = "main"
description = "Read resources from Python packages"
marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version < \"3.7\""
marker = "python_version < \"3.7\" or python_version >= \"2.7\" and python_version < \"2.8\""
name = "importlib-resources"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
......@@ -1512,7 +1512,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["jaraco.itertools", "func-timeout"]
[metadata]
content-hash = "6e86bcf4be1389d9d1dbd87bd5383089bec887c5a7e9c5e48b09b936699e035d"
content-hash = "90cb5177483057d7bf7dea367cb6ccf0968861777c801f3bb41b0f745cccdc24"
python-versions = "~2.7 || ^3.5"
[metadata.files]
......
......@@ -29,6 +29,7 @@ from poetry.console.commands.command import Command
from poetry.console.commands.env_command import EnvCommand
from poetry.console.logging.io_formatter import IOFormatter
from poetry.console.logging.io_handler import IOHandler
from poetry.utils._compat import PY36
class ApplicationConfig(BaseApplicationConfig):
......@@ -46,6 +47,15 @@ class ApplicationConfig(BaseApplicationConfig):
self.add_event_listener(PRE_HANDLE, self.register_command_loggers)
self.add_event_listener(PRE_HANDLE, self.set_env)
if PY36:
from poetry.mixology.solutions.providers import (
PythonRequirementSolutionProvider,
)
self._solution_provider_repository.register_solution_providers(
[PythonRequirementSolutionProvider]
)
def register_command_loggers(
self, event, event_name, _
): # type: (PreHandleEvent, str, Any) -> None
......
......@@ -2,6 +2,8 @@ from typing import Dict
from typing import List
from typing import Tuple
from poetry.core.semver import parse_constraint
from .incompatibility import Incompatibility
from .incompatibility_cause import ConflictCause
from .incompatibility_cause import PythonCause
......@@ -44,10 +46,15 @@ class _Writer:
)
required_python_version_notification = True
root_constraint = parse_constraint(
incompatibility.cause.root_python_version
)
constraint = parse_constraint(incompatibility.cause.python_version)
buffer.append(
" - {} requires Python {}".format(
" - {} requires Python {}, so it will not be satisfied for Python {}".format(
incompatibility.terms[0].dependency.name,
incompatibility.cause.python_version,
root_constraint.difference(constraint),
)
)
......
from .python_requirement_solution_provider import PythonRequirementSolutionProvider
import re
from typing import List
from crashtest.contracts.has_solutions_for_exception import HasSolutionsForException
from crashtest.contracts.solution import Solution
class PythonRequirementSolutionProvider(HasSolutionsForException):
def can_solve(self, exception): # type: (Exception) -> bool
from poetry.puzzle.exceptions import SolverProblemError
if not isinstance(exception, SolverProblemError):
return False
m = re.match(
"^The current project's Python requirement (.+) is not compatible "
"with some of the required packages Python requirement",
str(exception),
)
if not m:
return False
return True
def get_solutions(self, exception): # type: (Exception) -> List[Solution]
from ..solutions.python_requirement_solution import PythonRequirementSolution
return [PythonRequirementSolution(exception)]
from .python_requirement_solution import PythonRequirementSolution
from crashtest.contracts.solution import Solution
class PythonRequirementSolution(Solution):
def __init__(self, exception):
from poetry.mixology.incompatibility_cause import PythonCause
from poetry.core.semver import parse_constraint
self._title = "Check your dependencies Python requirement."
failure = exception.error
version_solutions = []
for incompatibility in failure._incompatibility.external_incompatibilities:
if isinstance(incompatibility.cause, PythonCause):
root_constraint = parse_constraint(
incompatibility.cause.root_python_version
)
constraint = parse_constraint(incompatibility.cause.python_version)
version_solutions.append(
"For <fg=default;options=bold>{}</>, a possible solution would be "
'to set the `<fg=default;options=bold>python</>` property to <fg=yellow>"{}"</>'.format(
incompatibility.terms[0].dependency.name,
root_constraint.intersect(constraint),
)
)
description = (
"The Python requirement can be specified via the `<fg=default;options=bold>python</>` "
"or `<fg=default;options=bold>markers</>` properties"
)
if version_solutions:
description += "\n\n" + "\n".join(version_solutions)
description += "\n"
self._description = description
@property
def solution_title(self) -> str:
return self._title
@property
def solution_description(self):
return self._description
@property
def documentation_links(self):
return [
"https://python-poetry.org/docs/dependency-specification/#python-restricted-dependencies",
"https://python-poetry.org/docs/dependency-specification/#using-environment-markers",
]
# -*- coding: utf-8 -*-
import time
from typing import TYPE_CHECKING
from typing import Any
from typing import Dict
from typing import List
......@@ -11,7 +12,6 @@ from poetry.core.packages import Package
from poetry.core.packages import ProjectPackage
from poetry.core.semver import Version
from poetry.core.semver import VersionRange
from poetry.puzzle.provider import Provider
from .failure import SolveFailure
from .incompatibility import Incompatibility
......@@ -25,6 +25,10 @@ from .set_relation import SetRelation
from .term import Term
if TYPE_CHECKING:
from poetry.puzzle.provider import Provider
_conflict = object()
......
......@@ -27,6 +27,7 @@ python = "~2.7 || ^3.5"
poetry-core = "^1.0.0a6"
cleo = "^0.8.1"
clikit = "^0.6.2"
crashtest = { version = "^0.3.0", python = "^3.6" }
requests = "^2.18"
cachy = "^0.3.0"
requests-toolbelt = "^0.8.0"
......
import pytest
from poetry.core.packages.dependency import Dependency
from poetry.mixology.failure import SolveFailure
from poetry.mixology.incompatibility import Incompatibility
from poetry.mixology.incompatibility_cause import NoVersionsCause
from poetry.mixology.incompatibility_cause import PythonCause
from poetry.mixology.term import Term
from poetry.puzzle.exceptions import SolverProblemError
from poetry.utils._compat import PY36
@pytest.mark.skipif(
not PY36, reason="Error solutions are only available for Python ^3.6"
)
def test_it_can_solve_python_incompatibility_solver_errors():
from poetry.mixology.solutions.providers import PythonRequirementSolutionProvider
from poetry.mixology.solutions.solutions import PythonRequirementSolution
incompatibility = Incompatibility(
[Term(Dependency("foo", "^1.0"), True)], PythonCause("^3.5", ">=3.6")
)
exception = SolverProblemError(SolveFailure(incompatibility))
provider = PythonRequirementSolutionProvider()
assert provider.can_solve(exception)
assert isinstance(provider.get_solutions(exception)[0], PythonRequirementSolution)
@pytest.mark.skipif(
not PY36, reason="Error solutions are only available for Python ^3.6"
)
def test_it_cannot_solve_other_solver_errors():
from poetry.mixology.solutions.providers import PythonRequirementSolutionProvider
incompatibility = Incompatibility(
[Term(Dependency("foo", "^1.0"), True)], NoVersionsCause()
)
exception = SolverProblemError(SolveFailure(incompatibility))
provider = PythonRequirementSolutionProvider()
assert not provider.can_solve(exception)
import pytest
from clikit.io.buffered_io import BufferedIO
from poetry.core.packages.dependency import Dependency
from poetry.mixology.failure import SolveFailure
from poetry.mixology.incompatibility import Incompatibility
from poetry.mixology.incompatibility_cause import PythonCause
from poetry.mixology.term import Term
from poetry.puzzle.exceptions import SolverProblemError
from poetry.utils._compat import PY36
@pytest.mark.skipif(
not PY36, reason="Error solutions are only available for Python ^3.6"
)
def test_it_provides_the_correct_solution():
from poetry.mixology.solutions.solutions import PythonRequirementSolution
incompatibility = Incompatibility(
[Term(Dependency("foo", "^1.0"), True)], PythonCause("^3.5", ">=3.6")
)
exception = SolverProblemError(SolveFailure(incompatibility))
solution = PythonRequirementSolution(exception)
title = "Check your dependencies Python requirement."
description = """\
The Python requirement can be specified via the `python` or `markers` properties
For foo, a possible solution would be to set the `python` property to ">=3.6,<4.0"\
"""
links = [
"https://python-poetry.org/docs/dependency-specification/#python-restricted-dependencies",
"https://python-poetry.org/docs/dependency-specification/#using-environment-markers",
]
assert title == solution.solution_title
assert (
description == BufferedIO().remove_format(solution.solution_description).strip()
)
assert links == solution.documentation_links
......@@ -10,7 +10,7 @@ def test_dependency_does_not_match_root_python_constraint(root, provider, repo):
error = """The current project's Python requirement (^3.6) \
is not compatible with some of the required packages Python requirement:
- foo requires Python <3.5
- foo requires Python <3.5, so it will not be satisfied for Python >=3.6,<4.0
Because no versions of foo match !=1.0.0
and foo (1.0.0) requires Python <3.5, foo is forbidden.
......
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