Skip to content

Commit

Permalink
Add solutions for common errors
Browse files Browse the repository at this point in the history
  • Loading branch information
sdispater committed Jun 12, 2020
1 parent d4be652 commit 3eda75a
Show file tree
Hide file tree
Showing 16 changed files with 196 additions and 7 deletions.
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
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
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
@@ -0,0 +1 @@
from .python_requirement_solution_provider import PythonRequirementSolutionProvider
@@ -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
@@ -0,0 +1 @@
from .python_requirement_solution import PythonRequirementSolution
52 changes: 52 additions & 0 deletions poetry/mixology/solutions/solutions/python_requirement_solution.py
@@ -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
@@ -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
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.
@@ -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.
@@ -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
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

0 comments on commit 3eda75a

Please sign in to comment.