Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add solutions for common errors #2396

Merged
merged 1 commit into from
Jun 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions poetry/console/config/application_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
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):
Expand All @@ -46,6 +47,15 @@ def configure(self):
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
Expand Down
9 changes: 8 additions & 1 deletion poetry/mixology/failure.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
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
Expand Down Expand Up @@ -44,10 +46,15 @@ def write(self):
)
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),
)
)

Expand Down
Empty file.
1 change: 1 addition & 0 deletions poetry/mixology/solutions/providers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .python_requirement_solution_provider import PythonRequirementSolutionProvider
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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)]
1 change: 1 addition & 0 deletions poetry/mixology/solutions/solutions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .python_requirement_solution import PythonRequirementSolution
52 changes: 52 additions & 0 deletions poetry/mixology/solutions/solutions/python_requirement_solution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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",
]
6 changes: 5 additions & 1 deletion poetry/mixology/version_solver.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import time

from typing import TYPE_CHECKING
from typing import Any
from typing import Dict
from typing import List
Expand All @@ -11,7 +12,6 @@
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
Expand All @@ -25,6 +25,10 @@
from .term import Term


if TYPE_CHECKING:
from poetry.puzzle.provider import Provider


_conflict = object()


Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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)
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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
2 changes: 1 addition & 1 deletion tests/mixology/version_solver/test_python_constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down