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" ...@@ -215,7 +215,7 @@ version = "0.4.3"
[[package]] [[package]]
category = "main" category = "main"
description = "Updated configparser from Python 3.7 for Python 2.6+." 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" name = "configparser"
optional = false optional = false
python-versions = ">=2.6" python-versions = ">=2.6"
...@@ -228,7 +228,7 @@ testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2)", "pytes ...@@ -228,7 +228,7 @@ testing = ["pytest (>=3.5,<3.7.3 || >3.7.3)", "pytest-checkdocs (>=1.2)", "pytes
[[package]] [[package]]
category = "main" category = "main"
description = "Backports and enhancements for the contextlib module" 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" name = "contextlib2"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
...@@ -446,7 +446,7 @@ testing = ["packaging", "importlib-resources"] ...@@ -446,7 +446,7 @@ testing = ["packaging", "importlib-resources"]
[[package]] [[package]]
category = "main" category = "main"
description = "Read resources from Python packages" 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" name = "importlib-resources"
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 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)"] ...@@ -1512,7 +1512,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["jaraco.itertools", "func-timeout"] testing = ["jaraco.itertools", "func-timeout"]
[metadata] [metadata]
content-hash = "6e86bcf4be1389d9d1dbd87bd5383089bec887c5a7e9c5e48b09b936699e035d" content-hash = "90cb5177483057d7bf7dea367cb6ccf0968861777c801f3bb41b0f745cccdc24"
python-versions = "~2.7 || ^3.5" python-versions = "~2.7 || ^3.5"
[metadata.files] [metadata.files]
......
...@@ -29,6 +29,7 @@ from poetry.console.commands.command import Command ...@@ -29,6 +29,7 @@ from poetry.console.commands.command import Command
from poetry.console.commands.env_command import EnvCommand from poetry.console.commands.env_command import EnvCommand
from poetry.console.logging.io_formatter import IOFormatter from poetry.console.logging.io_formatter import IOFormatter
from poetry.console.logging.io_handler import IOHandler from poetry.console.logging.io_handler import IOHandler
from poetry.utils._compat import PY36
class ApplicationConfig(BaseApplicationConfig): class ApplicationConfig(BaseApplicationConfig):
...@@ -46,6 +47,15 @@ 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.register_command_loggers)
self.add_event_listener(PRE_HANDLE, self.set_env) 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( def register_command_loggers(
self, event, event_name, _ self, event, event_name, _
): # type: (PreHandleEvent, str, Any) -> None ): # type: (PreHandleEvent, str, Any) -> None
......
...@@ -2,6 +2,8 @@ from typing import Dict ...@@ -2,6 +2,8 @@ from typing import Dict
from typing import List from typing import List
from typing import Tuple from typing import Tuple
from poetry.core.semver import parse_constraint
from .incompatibility import Incompatibility from .incompatibility import Incompatibility
from .incompatibility_cause import ConflictCause from .incompatibility_cause import ConflictCause
from .incompatibility_cause import PythonCause from .incompatibility_cause import PythonCause
...@@ -44,10 +46,15 @@ class _Writer: ...@@ -44,10 +46,15 @@ class _Writer:
) )
required_python_version_notification = True required_python_version_notification = True
root_constraint = parse_constraint(
incompatibility.cause.root_python_version
)
constraint = parse_constraint(incompatibility.cause.python_version)
buffer.append( buffer.append(
" - {} requires Python {}".format( " - {} requires Python {}, so it will not be satisfied for Python {}".format(
incompatibility.terms[0].dependency.name, incompatibility.terms[0].dependency.name,
incompatibility.cause.python_version, 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 -*- # -*- coding: utf-8 -*-
import time import time
from typing import TYPE_CHECKING
from typing import Any from typing import Any
from typing import Dict from typing import Dict
from typing import List from typing import List
...@@ -11,7 +12,6 @@ from poetry.core.packages import Package ...@@ -11,7 +12,6 @@ from poetry.core.packages import Package
from poetry.core.packages import ProjectPackage from poetry.core.packages import ProjectPackage
from poetry.core.semver import Version from poetry.core.semver import Version
from poetry.core.semver import VersionRange from poetry.core.semver import VersionRange
from poetry.puzzle.provider import Provider
from .failure import SolveFailure from .failure import SolveFailure
from .incompatibility import Incompatibility from .incompatibility import Incompatibility
...@@ -25,6 +25,10 @@ from .set_relation import SetRelation ...@@ -25,6 +25,10 @@ from .set_relation import SetRelation
from .term import Term from .term import Term
if TYPE_CHECKING:
from poetry.puzzle.provider import Provider
_conflict = object() _conflict = object()
......
...@@ -27,6 +27,7 @@ python = "~2.7 || ^3.5" ...@@ -27,6 +27,7 @@ python = "~2.7 || ^3.5"
poetry-core = "^1.0.0a6" poetry-core = "^1.0.0a6"
cleo = "^0.8.1" cleo = "^0.8.1"
clikit = "^0.6.2" clikit = "^0.6.2"
crashtest = { version = "^0.3.0", python = "^3.6" }
requests = "^2.18" requests = "^2.18"
cachy = "^0.3.0" cachy = "^0.3.0"
requests-toolbelt = "^0.8.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): ...@@ -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) \ error = """The current project's Python requirement (^3.6) \
is not compatible with some of the required packages Python requirement: 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 Because no versions of foo match !=1.0.0
and foo (1.0.0) requires Python <3.5, foo is forbidden. 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