Commit 7a12c390 by Sébastien Eustace Committed by GitHub

Resolver improvements (#472)

* Improve resolver

* Improve handling of environment markers

* Update lock file

* Fix recursion error on duplicate dependencies with only different extras
parent a4aefded
...@@ -12,6 +12,11 @@ ...@@ -12,6 +12,11 @@
- Improved virtualenv detection and management. - Improved virtualenv detection and management.
- Wilcard `python` dependencies are now equivalent to `~2.7 || ^3.4`. - Wilcard `python` dependencies are now equivalent to `~2.7 || ^3.4`.
- Changed behavior of the resolver for conditional dependencies.
### Fixed
- Fixed a memory leak in the resolver.
## [0.11.5] - 2018-09-04 ## [0.11.5] - 2018-09-04
......
...@@ -318,11 +318,11 @@ and installs them. ...@@ -318,11 +318,11 @@ and installs them.
poetry install poetry install
``` ```
If there is a `pyproject.lock` file in the current directory, If there is a `poetry.lock` file in the current directory,
it will use the exact versions from there instead of resolving them. it will use the exact versions from there instead of resolving them.
This ensures that everyone using the library will get the same versions of the dependencies. This ensures that everyone using the library will get the same versions of the dependencies.
If there is no `pyproject.lock` file, Poetry will create one after dependency resolution. If there is no `poetry.lock` file, Poetry will create one after dependency resolution.
You can specify to the command that you do not want the development dependencies installed by passing You can specify to the command that you do not want the development dependencies installed by passing
the `--no-dev` option. the `--no-dev` option.
...@@ -346,14 +346,14 @@ poetry install -E mysql -E pgsql ...@@ -346,14 +346,14 @@ poetry install -E mysql -E pgsql
### update ### update
In order to get the latest versions of the dependencies and to update the `pyproject.lock` file, In order to get the latest versions of the dependencies and to update the `poetry.lock` file,
you should use the `update` command. you should use the `update` command.
```bash ```bash
poetry update poetry update
``` ```
This will resolve all dependencies of the project and write the exact versions into `pyproject.lock`. This will resolve all dependencies of the project and write the exact versions into `poetry.lock`.
If you just want to update a few packages and not all, you can list them as such: If you just want to update a few packages and not all, you can list them as such:
......
...@@ -14,16 +14,17 @@ class DebugResolveCommand(Command): ...@@ -14,16 +14,17 @@ class DebugResolveCommand(Command):
{ --E|extras=* : Extras to activate for the dependency. } { --E|extras=* : Extras to activate for the dependency. }
{ --python= : Python version(s) to use for resolution. } { --python= : Python version(s) to use for resolution. }
{ --tree : Displays the dependency tree. } { --tree : Displays the dependency tree. }
{ --install : Show what would be installed for the current system. }
""" """
_loggers = ["poetry.repositories.pypi_repository"] _loggers = ["poetry.repositories.pypi_repository"]
def handle(self): def handle(self):
from poetry.packages import Dependency
from poetry.packages import ProjectPackage from poetry.packages import ProjectPackage
from poetry.puzzle import Solver from poetry.puzzle import Solver
from poetry.repositories.repository import Repository from poetry.repositories.repository import Repository
from poetry.semver import parse_constraint from poetry.semver import parse_constraint
from poetry.utils.env import Env
packages = self.argument("package") packages = self.argument("package")
...@@ -35,7 +36,6 @@ class DebugResolveCommand(Command): ...@@ -35,7 +36,6 @@ class DebugResolveCommand(Command):
) )
requirements = self._format_requirements(packages) requirements = self._format_requirements(packages)
dependencies = []
for name, constraint in requirements.items(): for name, constraint in requirements.items():
dep = package.add_dependency(name, constraint) dep = package.add_dependency(name, constraint)
extras = [] extras = []
...@@ -48,13 +48,13 @@ class DebugResolveCommand(Command): ...@@ -48,13 +48,13 @@ class DebugResolveCommand(Command):
for ex in extras: for ex in extras:
dep.extras.append(ex) dep.extras.append(ex)
package.python_versions = ( package.python_versions = self.option("python") or (
self.option("python") or self.poetry.package.python_versions self.poetry.package.python_versions
) )
solver = Solver( pool = self.poetry.pool
package, self.poetry.pool, Repository(), Repository(), self.output
) solver = Solver(package, pool, Repository(), Repository(), self.output)
ops = solver.solve() ops = solver.solve()
...@@ -79,16 +79,28 @@ class DebugResolveCommand(Command): ...@@ -79,16 +79,28 @@ class DebugResolveCommand(Command):
return 0 return 0
env = Env.get()
current_python_version = parse_constraint(
".".join(str(v) for v in env.version_info)
)
for op in ops: for op in ops:
package = op.package pkg = op.package
if self.option("install"):
if not pkg.python_constraint.allows(
current_python_version
) or not env.is_valid_for_marker(pkg.marker):
continue
self.line( self.line(
" - <info>{}</info> (<comment>{}</comment>)".format( " - <info>{}</info> (<comment>{}</comment>)".format(
package.name, package.version pkg.name, pkg.version
) )
) )
if package.requirements: if not pkg.python_constraint.is_any():
for req_name, req_value in package.requirements.items(): self.line(" - python: {}".format(pkg.python_versions))
self.line(" - {}: {}".format(req_name, req_value))
if not pkg.marker.is_any():
self.line(" - marker: {}".format(pkg.marker))
def _determine_requirements(self, requires): # type: (List[str]) -> List[str] def _determine_requirements(self, requires): # type: (List[str]) -> List[str]
from poetry.semver import parse_constraint from poetry.semver import parse_constraint
...@@ -97,7 +109,6 @@ class DebugResolveCommand(Command): ...@@ -97,7 +109,6 @@ class DebugResolveCommand(Command):
return [] return []
requires = self._parse_name_version_pairs(requires) requires = self._parse_name_version_pairs(requires)
result = []
for requirement in requires: for requirement in requires:
if "version" in requirement: if "version" in requirement:
parse_constraint(requirement["version"]) parse_constraint(requirement["version"])
......
...@@ -23,7 +23,9 @@ lists all packages available.""" ...@@ -23,7 +23,9 @@ lists all packages available."""
colors = ["green", "yellow", "cyan", "magenta", "blue"] colors = ["green", "yellow", "cyan", "magenta", "blue"]
def handle(self): def handle(self):
from poetry.packages.constraints.generic_constraint import GenericConstraint from poetry.packages.constraints import (
parse_constraint as parse_generic_constraint,
)
from poetry.repositories.installed_repository import InstalledRepository from poetry.repositories.installed_repository import InstalledRepository
from poetry.semver import Version from poetry.semver import Version
from poetry.semver import parse_constraint from poetry.semver import parse_constraint
...@@ -99,17 +101,13 @@ lists all packages available.""" ...@@ -99,17 +101,13 @@ lists all packages available."""
installed_repo = InstalledRepository.load(self.env) installed_repo = InstalledRepository.load(self.env)
skipped = [] skipped = []
platform = sys.platform
python = Version.parse(".".join([str(i) for i in self.env.version_info[:3]])) python = Version.parse(".".join([str(i) for i in self.env.version_info[:3]]))
# Computing widths # Computing widths
for locked in locked_packages: for locked in locked_packages:
python_constraint = parse_constraint(locked.requirements.get("python", "*")) python_constraint = locked.python_constraint
platform_constraint = GenericConstraint.parse( if not python_constraint.allows(python) or not self.env.is_valid_for_marker(
locked.requirements.get("platform", "*") locked.marker
)
if not python_constraint.allows(python) or not platform_constraint.matches(
GenericConstraint("=", platform)
): ):
skipped.append(locked) skipped.append(locked)
......
...@@ -7,7 +7,7 @@ from poetry.io import NullIO ...@@ -7,7 +7,7 @@ from poetry.io import NullIO
from poetry.packages import Dependency from poetry.packages import Dependency
from poetry.packages import Locker from poetry.packages import Locker
from poetry.packages import Package from poetry.packages import Package
from poetry.packages.constraints.generic_constraint import GenericConstraint from poetry.packages.constraints import parse_constraint as parse_generic_constraint
from poetry.puzzle import Solver from poetry.puzzle import Solver
from poetry.puzzle.operations import Install from poetry.puzzle.operations import Install
from poetry.puzzle.operations import Uninstall from poetry.puzzle.operations import Uninstall
...@@ -457,31 +457,15 @@ class Installer: ...@@ -457,31 +457,15 @@ class Installer:
if op.skipped: if op.skipped:
op.unskip() op.unskip()
python = Version.parse( current_python = parse_constraint(
".".join([str(i) for i in self._env.version_info[:3]]) ".".join(str(v) for v in self._env.version_info[:3])
) )
if "python" in package.requirements: if not package.python_constraint.allows(
python_constraint = parse_constraint(package.requirements["python"]) current_python
if not python_constraint.allows(python): ) or not self._env.is_valid_for_marker(package.marker):
# Incompatible python versions op.skip("Not needed for the current environment")
op.skip("Not needed for the current python version")
continue
if not package.python_constraint.allows(python):
op.skip("Not needed for the current python version")
continue continue
if "platform" in package.requirements:
platform_constraint = GenericConstraint.parse(
package.requirements["platform"]
)
if not platform_constraint.matches(
GenericConstraint("=", sys.platform)
):
# Incompatible systems
op.skip("Not needed for the current platform")
continue
if self._update: if self._update:
extras = {} extras = {}
for extra, deps in self._package.extras.items(): for extra, deps in self._package.extras.items():
......
...@@ -151,7 +151,7 @@ class Term(object): ...@@ -151,7 +151,7 @@ class Term(object):
return ( return (
self.dependency.is_root self.dependency.is_root
or other.is_root or other.is_root
or (other.name == self.dependency.name) or other.name == self.dependency.name
) )
def _non_empty_term(self, constraint, is_positive): def _non_empty_term(self, constraint, is_positive):
......
...@@ -34,7 +34,7 @@ def dependency_from_pep_508(name): ...@@ -34,7 +34,7 @@ def dependency_from_pep_508(name):
req = Requirement(name) req = Requirement(name)
if req.marker: if req.marker:
markers = convert_markers(req.marker.markers) markers = convert_markers(req.marker)
else: else:
markers = {} markers = {}
...@@ -128,28 +128,8 @@ def dependency_from_pep_508(name): ...@@ -128,28 +128,8 @@ def dependency_from_pep_508(name):
dep.python_versions = " || ".join(ors) dep.python_versions = " || ".join(ors)
if "sys_platform" in markers: if req.marker:
ors = [] dep.marker = req.marker
for or_ in markers["sys_platform"]:
ands = []
for op, platform in or_:
if op == "==":
op = ""
elif op == "in":
platforms = []
for v in re.split("[ ,]+", platform):
platforms.append(v)
if platforms:
ands.append(" || ".join(platforms))
continue
ands.append("{}{}".format(op, platform))
ors.append(" ".join(ands))
dep.platform = " || ".join(ors)
# Extras # Extras
for extra in req.extras: for extra in req.extras:
......
import re
from .any_constraint import AnyConstraint
from .constraint import Constraint
from .union_constraint import UnionConstraint
BASIC_CONSTRAINT = re.compile(r"^(!?==?)?\s*([^\s]+?)\s*$")
def parse_constraint(constraints):
if constraints == "*":
return AnyConstraint()
or_constraints = re.split(r"\s*\|\|?\s*", constraints.strip())
or_groups = []
for constraints in or_constraints:
and_constraints = re.split(
r"(?<!^)(?<![=>< ,]) *(?<!-)[, ](?!-) *(?!,|$)", constraints
)
constraint_objects = []
if len(and_constraints) > 1:
for constraint in and_constraints:
constraint_objects.append(parse_single_constraint(constraint))
else:
constraint_objects.append(parse_single_constraint(and_constraints[0]))
if len(constraint_objects) == 1:
constraint = constraint_objects[0]
else:
constraint = constraint_objects[0]
for next_constraint in constraint_objects[1:]:
constraint = constraint.intersect(next_constraint)
or_groups.append(constraint)
if len(or_groups) == 1:
return or_groups[0]
else:
return UnionConstraint(*or_groups)
def parse_single_constraint(constraint): # type: (str) -> BaseConstraint
# Basic comparator
m = BASIC_CONSTRAINT.match(constraint)
if m:
op = m.group(1)
if op is None:
op = "=="
version = m.group(2).strip()
return Constraint(version, op)
raise ValueError("Could not parse version constraint: {}".format(constraint))
from .base_constraint import BaseConstraint
from .empty_constraint import EmptyConstraint
class AnyConstraint(BaseConstraint):
def allows(self, other):
return True
def allows_all(self, other):
return True
def allows_any(self, other):
return True
def difference(self, other):
if other.is_any():
return EmptyConstraint()
return other
def intersect(self, other):
return other
def union(self, other):
return AnyConstraint()
def is_any(self):
return True
def is_empty(self):
return False
def __str__(self):
return "*"
def __eq__(self, other):
return other.is_any()
class BaseConstraint(object): class BaseConstraint(object):
def matches(self, provider):
raise NotImplementedError()
def allows_all(self, other): def allows_all(self, other):
raise NotImplementedError() raise NotImplementedError()
...@@ -14,5 +11,17 @@ class BaseConstraint(object): ...@@ -14,5 +11,17 @@ class BaseConstraint(object):
def intersect(self, other): def intersect(self, other):
raise NotImplementedError() raise NotImplementedError()
def union(self, other):
raise NotImplementedError()
def is_any(self):
return False
def is_empty(self): def is_empty(self):
return False return False
def __repr__(self):
return "<{} {}>".format(self.__class__.__name__, str(self))
def __eq__(self, other):
raise NotImplementedError()
import operator
from .base_constraint import BaseConstraint
from .empty_constraint import EmptyConstraint
class Constraint(BaseConstraint):
OP_EQ = operator.eq
OP_NE = operator.ne
_trans_op_str = {"=": OP_EQ, "==": OP_EQ, "!=": OP_NE}
_trans_op_int = {OP_EQ: "==", OP_NE: "!="}
def __init__(self, version, operator="=="):
if operator == "=":
operator = "=="
self._version = version
self._operator = operator
self._op = self._trans_op_str[operator]
@property
def version(self):
return self._version
@property
def operator(self):
return self._operator
def allows(self, other):
is_equal_op = self._operator == "=="
is_non_equal_op = self._operator == "!="
is_other_equal_op = other.operator == "=="
is_other_non_equal_op = other.operator == "!="
if is_equal_op and is_other_equal_op:
return self._version == other.version
if (
is_equal_op
and is_other_non_equal_op
or is_non_equal_op
and is_other_equal_op
or is_non_equal_op
and is_other_non_equal_op
):
return self._version != other.version
return False
def allows_all(self, other):
if not isinstance(other, Constraint):
return other.is_empty()
return other == self
def allows_any(self, other):
if isinstance(other, Constraint):
is_non_equal_op = self._operator == "!="
is_other_non_equal_op = other.operator == "!="
if is_non_equal_op and is_other_non_equal_op:
return self._version != other.version
return other.allows(self)
def difference(self, other):
if other.allows(self):
return EmptyConstraint()
return self
def intersect(self, other):
from .multi_constraint import MultiConstraint
if isinstance(other, Constraint):
if other == self:
return self
if self.operator == "!=" and other.operator == "==" and self.allows(other):
return other
if other.operator == "!=" and self.operator == "==" and other.allows(self):
return self
if other.operator == "!=" and self.operator == "!=":
return MultiConstraint(self, other)
return EmptyConstraint()
return other.intersect(self)
def union(self, other):
if isinstance(other, Constraint):
from .union_constraint import UnionConstraint
return UnionConstraint(self, other)
return other.union(self)
def is_any(self):
return False
def is_empty(self):
return False
def __eq__(self, other):
if not isinstance(other, Constraint):
return NotImplemented
return (self.version, self.operator) == (other.version, other.operator)
def __hash__(self):
return hash((self._operator, self._version))
def __str__(self):
return "{}{}".format(
self._operator if self._operator != "==" else "", self._version
)
...@@ -23,5 +23,8 @@ class EmptyConstraint(BaseConstraint): ...@@ -23,5 +23,8 @@ class EmptyConstraint(BaseConstraint):
def difference(self, other): def difference(self, other):
return return
def __eq__(self, other):
return other.is_empty()
def __str__(self): def __str__(self):
return "*" return ""
import operator
import re
from .base_constraint import BaseConstraint
from .empty_constraint import EmptyConstraint
from .multi_constraint import MultiConstraint
class GenericConstraint(BaseConstraint):
"""
Represents a generic constraint.
This is particularly useful for platform/system/os/extra constraints.
"""
OP_EQ = operator.eq
OP_NE = operator.ne
_trans_op_str = {"=": OP_EQ, "==": OP_EQ, "!=": OP_NE}
_trans_op_int = {OP_EQ: "==", OP_NE: "!="}
def __init__(self, operator, version):
if operator not in self._trans_op_str:
raise ValueError(
'Invalid operator "{}" given, '
"expected one of: {}".format(
operator, ", ".join(self.supported_operators)
)
)
self._operator = self._trans_op_str[operator]
self._string_operator = self._trans_op_int[self._operator]
self._version = version
@property
def supported_operators(self):
return list(self._trans_op_str.keys())
@property
def operator(self):
return self._operator
@property
def string_operator(self):
return self._string_operator
@property
def version(self):
return self._version
def matches(self, provider):
if not isinstance(provider, GenericConstraint):
return provider.matches(self)
is_equal_op = self.OP_EQ is self._operator
is_non_equal_op = self.OP_NE is self._operator
is_provider_equal_op = self.OP_EQ is provider.operator
is_provider_non_equal_op = self.OP_NE is provider.operator
if (
is_equal_op
and is_provider_equal_op
or is_non_equal_op
and is_provider_non_equal_op
):
return self._version == provider.version
if (
is_equal_op
and is_provider_non_equal_op
or is_non_equal_op
and is_provider_equal_op
):
return self._version != provider.version
return False
@classmethod
def parse(cls, constraints):
"""
Parses a constraint string into
MultiConstraint and/or PlatformConstraint objects.
"""
pretty_constraint = constraints
or_constraints = re.split("\s*\|\|?\s*", constraints.strip())
or_groups = []
for constraints in or_constraints:
and_constraints = re.split(
"(?<!^)(?<![ ,]) *(?<!-)[, ](?!-) *(?!,|$)", constraints
)
if len(and_constraints) > 1:
constraint_objects = []
for constraint in and_constraints:
for parsed_constraint in cls._parse_constraint(constraint):
constraint_objects.append(parsed_constraint)
else:
constraint_objects = cls._parse_constraint(and_constraints[0])
if len(constraint_objects) == 1:
constraint = constraint_objects[0]
else:
constraint = MultiConstraint(constraint_objects)
or_groups.append(constraint)
if len(or_groups) == 1:
constraint = or_groups[0]
else:
constraint = MultiConstraint(or_groups, False)
constraint.pretty_string = pretty_constraint
return constraint
@classmethod
def _parse_constraint(cls, constraint):
m = re.match("(?i)^v?[xX*](\.[xX*])*$", constraint)
if m:
return (EmptyConstraint(),)
# Basic Comparators
m = re.match("^(!=|==?)?\s*(.*)", constraint)
if m:
return (GenericConstraint(m.group(1) or "=", m.group(2)),)
raise ValueError("Could not parse generic constraint: {}".format(constraint))
def __str__(self):
op = self._trans_op_int[self._operator]
if op == "==":
op = ""
else:
op = op + " "
return "{}{}".format(op, self._version)
def __repr__(self):
return "<GenericConstraint '{}'>".format(str(self))
from .base_constraint import BaseConstraint from .base_constraint import BaseConstraint
from .constraint import Constraint
class MultiConstraint(BaseConstraint): class MultiConstraint(BaseConstraint):
def __init__(self, constraints, conjunctive=True): def __init__(self, *constraints):
self._constraints = tuple(constraints) if any(c.operator == "==" for c in constraints):
self._conjunctive = conjunctive raise ValueError(
"A multi-constraint can only be comprised of negative constraints"
)
self._constraints = constraints
@property @property
def constraints(self): def constraints(self):
return self._constraints return self._constraints
def is_conjunctive(self): def allows(self, other):
return self._conjunctive for constraint in self._constraints:
if not constraint.allows(other):
def is_disjunctive(self): return False
return not self._conjunctive
def matches(self, provider): return True
if self.is_disjunctive():
for constraint in self._constraints:
if constraint.matches(provider):
return True
def allows_all(self, other):
if other.is_any():
return False return False
for constraint in self._constraints: if other.is_empty():
if not constraint.matches(provider): return True
return False
return True if isinstance(other, Constraint):
return self.allows(other)
our_constraints = iter(self._constraints)
their_constraints = iter(other.constraints)
our_constraint = next(our_constraints, None)
their_constraint = next(their_constraints, None)
while our_constraint and their_constraint:
if our_constraint.allows_all(their_constraint):
their_constraint = next(their_constraints, None)
else:
our_constraint = next(our_constraints, None)
return their_constraint is None
def allows_any(self, other):
if other.is_any():
return True
if other.is_empty():
return True
if isinstance(other, Constraint):
return self.allows(other)
if isinstance(other, MultiConstraint):
for c1 in self.constraints:
for c2 in other.constraints:
if c1.allows(c2):
return True
return False
def intersect(self, other):
if isinstance(other, Constraint):
constraints = [c for c in self._constraints if c == other]
if len(constraints) == 1:
return constraints[0]
return MultiConstraint(*constraints)
def __eq__(self, other):
if not isinstance(other, MultiConstraint):
return False
return sorted(
self._constraints, key=lambda c: (c.operator, c.version)
) == sorted(other.constraints, key=lambda c: (c.operator, c.version))
def __str__(self): def __str__(self):
constraints = [] constraints = []
for constraint in self._constraints: for constraint in self._constraints:
constraints.append(str(constraint)) constraints.append(str(constraint))
return "{}".format((", " if self._conjunctive else " || ").join(constraints)) return "{}".format(", ").join(constraints)
from .base_constraint import BaseConstraint
from .constraint import Constraint
from .empty_constraint import EmptyConstraint
from .multi_constraint import MultiConstraint
class UnionConstraint(BaseConstraint):
def __init__(self, *constraints):
self._constraints = constraints
@property
def constraints(self):
return self._constraints
def allows(self, other):
for constraint in self._constraints:
if constraint.allows(other):
return True
return False
def allows_any(self, other):
if other.is_empty():
return False
if other.is_any():
return True
if isinstance(other, Constraint):
constraints = [other]
else:
constraints = other.constraints
for our_constraint in self._constraints:
for their_constraint in constraints:
if our_constraint.allows_any(their_constraint):
return True
return False
def allows_all(self, other):
if other.is_any():
return False
if other.is_empty():
return True
if isinstance(other, Constraint):
constraints = [other]
else:
constraints = other.constraints
our_constraints = iter(self._constraints)
their_constraints = iter(constraints)
our_constraint = next(our_constraints, None)
their_constraint = next(their_constraints, None)
while our_constraint and their_constraint:
if our_constraint.allows_all(their_constraint):
their_constraint = next(their_constraints, None)
else:
our_constraint = next(our_constraints, None)
return their_constraint is None
def intersect(self, other):
if other.is_any():
return self
if other.is_empty():
return other
if isinstance(other, Constraint):
if self.allows(other):
return other
return EmptyConstraint()
new_constraints = []
for our_constraint in self._constraints:
for their_constraint in other.constraints:
intersection = our_constraint.intersect(their_constraint)
if not intersection.is_empty() and intersection not in new_constraints:
new_constraints.append(intersection)
if not new_constraints:
return EmptyConstraint()
return UnionConstraint(*new_constraints)
def union(self, other):
if isinstance(other, Constraint):
constraints = self._constraints
if other not in self._constraints:
constraints += (other,)
return UnionConstraint(*constraints)
def __eq__(self, other):
if not isinstance(other, UnionConstraint):
return False
return sorted(
self._constraints, key=lambda c: (c.operator, c.version)
) == sorted(other.constraints, key=lambda c: (c.operator, c.version))
def __str__(self):
constraints = []
for constraint in self._constraints:
constraints.append(str(constraint))
return "{}".format(" || ").join(constraints)
...@@ -6,10 +6,14 @@ from poetry.semver import VersionConstraint ...@@ -6,10 +6,14 @@ from poetry.semver import VersionConstraint
from poetry.semver import VersionRange from poetry.semver import VersionRange
from poetry.semver import VersionUnion from poetry.semver import VersionUnion
from poetry.utils.helpers import canonicalize_name from poetry.utils.helpers import canonicalize_name
from poetry.version.markers import AnyMarker
from poetry.version.markers import parse_marker
from .constraints.empty_constraint import EmptyConstraint from .constraints import parse_constraint as parse_generic_constraint
from .constraints.generic_constraint import GenericConstraint from .constraints.any_constraint import AnyConstraint
from .constraints.constraint import Constraint
from .constraints.multi_constraint import MultiConstraint from .constraints.multi_constraint import MultiConstraint
from .constraints.union_constraint import UnionConstraint
class Dependency(object): class Dependency(object):
...@@ -45,8 +49,6 @@ class Dependency(object): ...@@ -45,8 +49,6 @@ class Dependency(object):
self._python_versions = "*" self._python_versions = "*"
self._python_constraint = parse_constraint("*") self._python_constraint = parse_constraint("*")
self._platform = "*"
self._platform_constraint = EmptyConstraint()
self._extras = [] self._extras = []
self._in_extras = [] self._in_extras = []
...@@ -54,6 +56,7 @@ class Dependency(object): ...@@ -54,6 +56,7 @@ class Dependency(object):
self._activated = not self._optional self._activated = not self._optional
self.is_root = False self.is_root = False
self.marker = AnyMarker()
@property @property
def name(self): def name(self):
...@@ -83,25 +86,20 @@ class Dependency(object): ...@@ -83,25 +86,20 @@ class Dependency(object):
def python_versions(self, value): def python_versions(self, value):
self._python_versions = value self._python_versions = value
self._python_constraint = parse_constraint(value) self._python_constraint = parse_constraint(value)
if not self._python_constraint.is_any():
self.marker = self.marker.intersect(
parse_marker(
self._create_nested_marker(
"python_version", self._python_constraint
)
)
)
@property @property
def python_constraint(self): def python_constraint(self):
return self._python_constraint return self._python_constraint
@property @property
def platform(self):
return self._platform
@platform.setter
def platform(self, value):
self._platform = value
self._platform_constraint = GenericConstraint.parse(value)
@property
def platform_constraint(self):
return self._platform_constraint
@property
def extras(self): # type: () -> list def extras(self): # type: () -> list
return self._extras return self._extras
...@@ -152,28 +150,27 @@ class Dependency(object): ...@@ -152,28 +150,27 @@ class Dependency(object):
elif not self.constraint.is_any(): elif not self.constraint.is_any():
requirement += " ({})".format(str(self.constraint).replace(" ", "")) requirement += " ({})".format(str(self.constraint).replace(" ", ""))
# Markers
markers = [] markers = []
if not self.marker.is_any():
marker = self.marker
if not with_extras:
marker = marker.without_extras()
# Python marker if not marker.is_empty():
if self.python_versions != "*": markers.append(str(marker))
python_constraint = self.python_constraint else:
# Python marker
markers.append( if self.python_versions != "*":
self._create_nested_marker("python_version", python_constraint) python_constraint = self.python_constraint
)
if self.platform != "*": markers.append(
platform_constraint = self.platform_constraint self._create_nested_marker("python_version", python_constraint)
)
markers.append(
self._create_nested_marker("sys_platform", platform_constraint)
)
in_extras = " || ".join(self._in_extras) in_extras = " || ".join(self._in_extras)
if in_extras and with_extras: if in_extras and with_extras:
markers.append( markers.append(
self._create_nested_marker("extra", GenericConstraint.parse(in_extras)) self._create_nested_marker("extra", parse_generic_constraint(in_extras))
) )
if markers: if markers:
...@@ -186,17 +183,17 @@ class Dependency(object): ...@@ -186,17 +183,17 @@ class Dependency(object):
return requirement return requirement
def _create_nested_marker(self, name, constraint): def _create_nested_marker(self, name, constraint):
if isinstance(constraint, MultiConstraint): if isinstance(constraint, (MultiConstraint, UnionConstraint)):
parts = [] parts = []
for c in constraint.constraints: for c in constraint.constraints:
multi = False multi = False
if isinstance(c, MultiConstraint): if isinstance(c, (MultiConstraint, UnionConstraint)):
multi = True multi = True
parts.append((multi, self._create_nested_marker(name, c))) parts.append((multi, self._create_nested_marker(name, c)))
glue = " and " glue = " and "
if constraint.is_disjunctive(): if isinstance(constraint, UnionConstraint):
parts = [ parts = [
"({})".format(part[1]) if part[0] else part[1] for part in parts "({})".format(part[1]) if part[0] else part[1] for part in parts
] ]
...@@ -205,10 +202,8 @@ class Dependency(object): ...@@ -205,10 +202,8 @@ class Dependency(object):
parts = [part[1] for part in parts] parts = [part[1] for part in parts]
marker = glue.join(parts) marker = glue.join(parts)
elif isinstance(constraint, GenericConstraint): elif isinstance(constraint, Constraint):
marker = '{} {} "{}"'.format( marker = '{} {} "{}"'.format(name, constraint.operator, constraint.version)
name, constraint.string_operator, constraint.version
)
elif isinstance(constraint, VersionUnion): elif isinstance(constraint, VersionUnion):
parts = [] parts = []
for c in constraint.ranges: for c in constraint.ranges:
...@@ -278,7 +273,6 @@ class Dependency(object): ...@@ -278,7 +273,6 @@ class Dependency(object):
new.is_root = self.is_root new.is_root = self.is_root
new.python_versions = self.python_versions new.python_versions = self.python_versions
new.platform = self.platform
for extra in self.extras: for extra in self.extras:
new.extras.append(extra) new.extras.append(extra)
......
from .package import Package class DependencyPackage(object):
class DependencyPackage:
def __init__(self, dependency, package): def __init__(self, dependency, package):
self._dependency = dependency self._dependency = dependency
self._package = package self._package = package
...@@ -17,6 +14,12 @@ class DependencyPackage: ...@@ -17,6 +14,12 @@ class DependencyPackage:
def __getattr__(self, name): def __getattr__(self, name):
return getattr(self._package, name) return getattr(self._package, name)
def __setattr__(self, key, value):
if key in {"_dependency", "_package"}:
return super(DependencyPackage, self).__setattr__(key, value)
setattr(self._package, key, value)
def __str__(self): def __str__(self):
return str(self._package) return str(self._package)
......
...@@ -73,7 +73,6 @@ class DirectoryDependency(Dependency): ...@@ -73,7 +73,6 @@ class DirectoryDependency(Dependency):
self._package.dev_requires += package.dev_requires self._package.dev_requires += package.dev_requires
self._package.extras = package.extras self._package.extras = package.extras
self._package.python_versions = package.python_versions self._package.python_versions = package.python_versions
self._package.platform = package.platform
else: else:
# Execute egg_info # Execute egg_info
current_dir = os.getcwd() current_dir = os.getcwd()
......
...@@ -5,11 +5,11 @@ import poetry.repositories ...@@ -5,11 +5,11 @@ import poetry.repositories
from hashlib import sha256 from hashlib import sha256
from tomlkit import document from tomlkit import document
from tomlkit import inline_table
from typing import List from typing import List
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils.toml_file import TomlFile from poetry.utils.toml_file import TomlFile
from poetry.version.markers import parse_marker
class Locker: class Locker:
...@@ -86,6 +86,22 @@ class Locker: ...@@ -86,6 +86,22 @@ class Locker:
package.hashes = lock_data["metadata"]["hashes"][info["name"]] package.hashes = lock_data["metadata"]["hashes"][info["name"]]
package.python_versions = info["python-versions"] package.python_versions = info["python-versions"]
if "marker" in info:
package.marker = parse_marker(info["marker"])
else:
# Compatibility for old locks
if "requirements" in info:
dep = poetry.packages.Dependency("foo", "0.0.0")
for name, value in info["requirements"].items():
if name == "python":
dep.python_versions = value
elif name == "platform":
dep.platform = value
split_dep = dep.to_pep_508(False).split(";")
if len(split_dep) > 1:
package.marker = parse_marker(split_dep[1].strip())
for dep_name, constraint in info.get("dependencies", {}).items(): for dep_name, constraint in info.get("dependencies", {}).items():
if isinstance(constraint, list): if isinstance(constraint, list):
for c in constraint: for c in constraint:
...@@ -95,9 +111,6 @@ class Locker: ...@@ -95,9 +111,6 @@ class Locker:
package.add_dependency(dep_name, constraint) package.add_dependency(dep_name, constraint)
if "requirements" in info:
package.requirements = info["requirements"]
if "source" in info: if "source" in info:
package.source_type = info["source"]["type"] package.source_type = info["source"]["type"]
package.source_url = info["source"]["url"] package.source_url = info["source"]["url"]
...@@ -129,7 +142,6 @@ class Locker: ...@@ -129,7 +142,6 @@ class Locker:
lock["metadata"] = { lock["metadata"] = {
"python-versions": root.python_versions, "python-versions": root.python_versions,
"platform": root.platform,
"content-hash": self._content_hash, "content-hash": self._content_hash,
"hashes": hashes, "hashes": hashes,
} }
...@@ -198,9 +210,6 @@ class Locker: ...@@ -198,9 +210,6 @@ class Locker:
if not dependency.python_constraint.is_any(): if not dependency.python_constraint.is_any():
constraint["python"] = str(dependency.python_constraint) constraint["python"] = str(dependency.python_constraint)
if dependency.platform != "*":
constraint["platform"] = dependency.platform
if len(constraint) == 1: if len(constraint) == 1:
dependencies[dependency.pretty_name].append(constraint["version"]) dependencies[dependency.pretty_name].append(constraint["version"])
else: else:
...@@ -217,9 +226,10 @@ class Locker: ...@@ -217,9 +226,10 @@ class Locker:
"category": package.category, "category": package.category,
"optional": package.optional, "optional": package.optional,
"python-versions": package.python_versions, "python-versions": package.python_versions,
"platform": package.platform,
"hashes": sorted(package.hashes), "hashes": sorted(package.hashes),
} }
if not package.marker.is_any():
data["marker"] = str(package.marker)
if dependencies: if dependencies:
for k, constraints in dependencies.items(): for k, constraints in dependencies.items():
...@@ -235,7 +245,4 @@ class Locker: ...@@ -235,7 +245,4 @@ class Locker:
"reference": package.source_reference, "reference": package.source_reference,
} }
if package.requirements:
data["requirements"] = package.requirements
return data return data
...@@ -11,13 +11,15 @@ from poetry.spdx import license_by_id ...@@ -11,13 +11,15 @@ from poetry.spdx import license_by_id
from poetry.spdx import License from poetry.spdx import License
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils.helpers import canonicalize_name from poetry.utils.helpers import canonicalize_name
from poetry.version.markers import AnyMarker
from poetry.version.markers import parse_marker
from .constraints.empty_constraint import EmptyConstraint from .constraints import parse_constraint as parse_generic_constraint
from .constraints.generic_constraint import GenericConstraint
from .dependency import Dependency from .dependency import Dependency
from .directory_dependency import DirectoryDependency from .directory_dependency import DirectoryDependency
from .file_dependency import FileDependency from .file_dependency import FileDependency
from .vcs_dependency import VCSDependency from .vcs_dependency import VCSDependency
from .utils.utils import create_nested_marker
AUTHOR_REGEX = re.compile("(?u)^(?P<name>[- .,\w\d'’\"()]+)(?: <(?P<email>.+?)>)?$") AUTHOR_REGEX = re.compile("(?u)^(?P<name>[- .,\w\d'’\"()]+)(?: <(?P<email>.+?)>)?$")
...@@ -63,15 +65,13 @@ class Package(object): ...@@ -63,15 +65,13 @@ class Package(object):
self.hashes = [] self.hashes = []
self.optional = False self.optional = False
# Requirements for making it mandatory
self.requirements = {}
self.classifiers = [] self.classifiers = []
self._python_versions = "*" self._python_versions = "*"
self._python_constraint = parse_constraint("*") self._python_constraint = parse_constraint("*")
self._platform = "*" self._python_marker = AnyMarker()
self._platform_constraint = EmptyConstraint()
self.marker = AnyMarker()
self.root_dir = None self.root_dir = None
...@@ -153,19 +153,26 @@ class Package(object): ...@@ -153,19 +153,26 @@ class Package(object):
def python_versions(self, value): def python_versions(self, value):
self._python_versions = value self._python_versions = value
self._python_constraint = parse_constraint(value) self._python_constraint = parse_constraint(value)
self._python_marker = parse_marker(
create_nested_marker("python_version", self._python_constraint)
)
@property @property
def python_constraint(self): def python_constraint(self):
return self._python_constraint return self._python_constraint
@property @property
def python_marker(self):
return self._python_marker
@property
def platform(self): # type: () -> str def platform(self): # type: () -> str
return self._platform return self._platform
@platform.setter @platform.setter
def platform(self, value): # type: (str) -> None def platform(self, value): # type: (str) -> None
self._platform = value self._platform = value
self._platform_constraint = GenericConstraint.parse(value) self._platform_constraint = parse_generic_constraint(value)
@property @property
def platform_constraint(self): def platform_constraint(self):
...@@ -282,11 +289,28 @@ class Package(object): ...@@ -282,11 +289,28 @@ class Package(object):
allows_prereleases=allows_prereleases, allows_prereleases=allows_prereleases,
) )
marker = AnyMarker()
if python_versions: if python_versions:
dependency.python_versions = python_versions dependency.python_versions = python_versions
marker = marker.intersect(
parse_marker(
create_nested_marker(
"python_version", dependency.python_constraint
)
)
)
if platform: if platform:
dependency.platform = platform marker = marker.intersect(
parse_marker(
create_nested_marker(
"sys_platform", parse_generic_constraint(platform)
)
)
)
if not marker.is_any():
dependency.marker = marker
if "extras" in constraint: if "extras" in constraint:
for extra in constraint["extras"]: for extra in constraint["extras"]:
...@@ -319,7 +343,6 @@ class Package(object): ...@@ -319,7 +343,6 @@ class Package(object):
clone.category = self.category clone.category = self.category
clone.optional = self.optional clone.optional = self.optional
clone.python_versions = self.python_versions clone.python_versions = self.python_versions
clone.platform = self.platform
clone.extras = self.extras clone.extras = self.extras
clone.source_type = self.source_type clone.source_type = self.source_type
clone.source_url = self.source_url clone.source_url = self.source_url
......
from poetry.semver import VersionRange from poetry.semver import VersionRange
from poetry.semver import parse_constraint from poetry.semver import parse_constraint
from poetry.version.markers import parse_marker
from .package import Package from .package import Package
from .utils.utils import create_nested_marker
class ProjectPackage(Package): class ProjectPackage(Package):
...@@ -33,7 +35,11 @@ class ProjectPackage(Package): ...@@ -33,7 +35,11 @@ class ProjectPackage(Package):
@python_versions.setter @python_versions.setter
def python_versions(self, value): def python_versions(self, value):
self._python_versions = value self._python_versions = value
if value == "*" or value == VersionRange(): if value == "*" or value == VersionRange():
value = "~2.7 || >=3.4" value = "~2.7 || >=3.4"
self._python_constraint = parse_constraint(value) self._python_constraint = parse_constraint(value)
self._python_marker = parse_marker(
create_nested_marker("python_version", self._python_constraint)
)
...@@ -2,6 +2,15 @@ import os ...@@ -2,6 +2,15 @@ import os
import posixpath import posixpath
import re import re
from poetry.packages.constraints.constraint import Constraint
from poetry.packages.constraints.multi_constraint import MultiConstraint
from poetry.packages.constraints.union_constraint import UnionConstraint
from poetry.semver import Version
from poetry.semver import VersionUnion
from poetry.version.markers import MarkerUnion
from poetry.version.markers import MultiMarker
from poetry.version.markers import SingleMarker
try: try:
import urllib.parse as urlparse import urllib.parse as urlparse
except ImportError: except ImportError:
...@@ -105,28 +114,27 @@ def splitext(path): ...@@ -105,28 +114,27 @@ def splitext(path):
return base, ext return base, ext
def group_markers(markers): def group_markers(markers, or_=False):
groups = [[]] groups = [[]]
for marker in markers: for marker in markers:
assert isinstance(marker, (list, tuple, str)) if or_:
groups.append([])
if isinstance(marker, list): if isinstance(marker, (MultiMarker, MarkerUnion)):
groups[-1].append(group_markers(marker)) groups[-1].append(
elif isinstance(marker, tuple): group_markers(marker.markers, isinstance(marker, MarkerUnion))
lhs, op, rhs = marker )
elif isinstance(marker, SingleMarker):
lhs, op, rhs = marker.name, marker.operator, marker.value
groups[-1].append((lhs.value, op, rhs.value)) groups[-1].append((lhs, op, rhs))
else:
assert marker in ["and", "or"]
if marker == "or":
groups.append([])
return groups return groups
def convert_markers(markers): def convert_markers(marker):
groups = group_markers(markers) groups = group_markers([marker])
requirements = {} requirements = {}
...@@ -149,3 +157,70 @@ def convert_markers(markers): ...@@ -149,3 +157,70 @@ def convert_markers(markers):
_group(groups) _group(groups)
return requirements return requirements
def create_nested_marker(name, constraint):
if constraint.is_any():
return ""
if isinstance(constraint, (MultiConstraint, UnionConstraint)):
parts = []
for c in constraint.constraints:
multi = False
if isinstance(c, (MultiConstraint, UnionConstraint)):
multi = True
parts.append((multi, create_nested_marker(name, c)))
glue = " and "
if isinstance(constraint, UnionConstraint):
parts = ["({})".format(part[1]) if part[0] else part[1] for part in parts]
glue = " or "
else:
parts = [part[1] for part in parts]
marker = glue.join(parts)
elif isinstance(constraint, Constraint):
marker = '{} {} "{}"'.format(name, constraint.operator, constraint.version)
elif isinstance(constraint, VersionUnion):
parts = []
for c in constraint.ranges:
parts.append(create_nested_marker(name, c))
glue = " or "
parts = ["({})".format(part) for part in parts]
marker = glue.join(parts)
elif isinstance(constraint, Version):
marker = '{} == "{}"'.format(name, constraint.text)
else:
if constraint.min is not None:
op = ">="
if not constraint.include_min:
op = ">"
version = constraint.min.text
if constraint.max is not None:
text = '{} {} "{}"'.format(name, op, version)
op = "<="
if not constraint.include_max:
op = "<"
version = constraint.max
text += ' and {} {} "{}"'.format(name, op, version)
return text
elif constraint.max is not None:
op = "<="
if not constraint.include_max:
op = "<"
version = constraint.max
else:
return ""
marker = '{} {} "{}"'.format(name, op, version)
return marker
...@@ -26,6 +26,10 @@ class Operation(object): ...@@ -26,6 +26,10 @@ class Operation(object):
def skip_reason(self): # type: () -> Union[str, None] def skip_reason(self): # type: () -> Union[str, None]
return self._skip_reason return self._skip_reason
@property
def package(self):
raise NotImplementedError()
def format_version(self, package): # type: (...) -> str def format_version(self, package): # type: (...) -> str
return package.full_pretty_version return package.full_pretty_version
......
...@@ -105,8 +105,27 @@ class Provider: ...@@ -105,8 +105,27 @@ class Provider:
if dependency.is_root: if dependency.is_root:
return PackageCollection(dependency, [self._package]) return PackageCollection(dependency, [self._package])
if dependency in self._search_for: for constraint in self._search_for.keys():
return PackageCollection(dependency, self._search_for[dependency]) if (
constraint.name == dependency.name
and constraint.constraint.intersect(dependency.constraint)
== dependency.constraint
):
packages = [
p
for p in self._search_for[constraint]
if dependency.constraint.allows(p.version)
]
packages.sort(
key=lambda p: (
not p.is_prerelease() and not dependency.allows_prereleases(),
p.version,
),
reverse=True,
)
return PackageCollection(dependency, packages)
if dependency.is_vcs(): if dependency.is_vcs():
packages = self.search_for_vcs(dependency) packages = self.search_for_vcs(dependency)
...@@ -134,7 +153,7 @@ class Provider: ...@@ -134,7 +153,7 @@ class Provider:
self._search_for[dependency] = packages self._search_for[dependency] = packages
return PackageCollection(dependency, self._search_for[dependency]) return PackageCollection(dependency, packages)
def search_for_vcs(self, dependency): # type: (VCSDependency) -> List[Package] def search_for_vcs(self, dependency): # type: (VCSDependency) -> List[Package]
""" """
...@@ -316,66 +335,29 @@ class Provider: ...@@ -316,66 +335,29 @@ class Provider:
if not package.python_constraint.allows_all( if not package.python_constraint.allows_all(
self._package.python_constraint self._package.python_constraint
): ):
# The package Python requirement is not compatible
# with the root package python requirement.
#
# However, it should be accepted if it comes from
# a dependency with a compatible Python constraint.
#
# An example of this is:
# - The root package is compatible with Python ~2.7 || ^3.6
# - The root package depends on black for Python ^3.6
# - black is only compatible with Python >=3.6
# - black should be authorized.
#
# In this particular case, we notify the resolver that it needs
# to branch the dependency tree. What this means is if we have
# root Python ~2.7 || ^3.6 and dependency Python >=3.6
# we have to resolve for ^3.6 (>=3.6, <4.0) and for ~2.7 || <3.6
if ( if (
not package.dependency.python_constraint.is_any() package.dependency.python_constraint.is_any()
and not package.python_constraint.intersect( or not self._package.python_constraint.allows_all(
package.dependency.python_constraint package.dependency.python_constraint
).is_empty()
):
self.debug(
"<warning>Found conditional dependency for {} (Python {}).</warning>".format(
package, package.dependency.python_constraint
)
) )
intersection = self._package.python_constraint.intersect( or not package.python_constraint.allows_all(
package.dependency.python_constraint package.dependency.python_constraint
) )
raise CompatibilityError( ):
str(intersection), return [
str(self._package.python_constraint.difference(intersection)), Incompatibility(
) [Term(package.to_dependency(), True)],
PythonCause(
return [ package.python_versions, self._package.python_versions
Incompatibility( ),
[Term(package.to_dependency(), True)], )
PythonCause( ]
package.python_versions, self._package.python_versions
),
)
]
if not self._package.platform_constraint.matches(
package.platform_constraint
):
return [
Incompatibility(
[Term(package.to_dependency(), True)],
PlatformCause(package.platform),
)
]
dependencies = [ dependencies = [
dep dep
for dep in dependencies for dep in dependencies
if dep.name not in self.UNSAFE_PACKAGES if dep.name not in self.UNSAFE_PACKAGES
and self._package.python_constraint.allows_any(dep.python_constraint) and self._package.python_constraint.allows_any(dep.python_constraint)
and self._package.platform_constraint.matches(dep.platform_constraint)
] ]
return [ return [
...@@ -403,7 +385,6 @@ class Provider: ...@@ -403,7 +385,6 @@ class Provider:
for r in package.requires for r in package.requires
if r.is_activated() if r.is_activated()
and self._package.python_constraint.allows_any(r.python_constraint) and self._package.python_constraint.allows_any(r.python_constraint)
and self._package.platform_constraint.matches(r.platform_constraint)
] ]
# Searching for duplicate dependencies # Searching for duplicate dependencies
...@@ -454,7 +435,7 @@ class Provider: ...@@ -454,7 +435,7 @@ class Provider:
for constraint, _deps in by_constraint.items(): for constraint, _deps in by_constraint.items():
new_markers = [] new_markers = []
for dep in _deps: for dep in _deps:
pep_508_dep = dep.to_pep_508() pep_508_dep = dep.to_pep_508(False)
if ";" not in pep_508_dep: if ";" not in pep_508_dep:
continue continue
...@@ -473,7 +454,7 @@ class Provider: ...@@ -473,7 +454,7 @@ class Provider:
dep = _deps[0] dep = _deps[0]
new_requirement = "{}; {}".format( new_requirement = "{}; {}".format(
dep.to_pep_508().split(";")[0], " or ".join(new_markers) dep.to_pep_508(False).split(";")[0], " or ".join(new_markers)
) )
new_dep = dependency_from_pep_508(new_requirement) new_dep = dependency_from_pep_508(new_requirement)
if dep.is_optional() and not dep.is_activated(): if dep.is_optional() and not dep.is_activated():
...@@ -501,7 +482,7 @@ class Provider: ...@@ -501,7 +482,7 @@ class Provider:
_deps = [value[0] for value in by_constraint.values()] _deps = [value[0] for value in by_constraint.values()]
seen = set() seen = set()
for _dep in _deps: for _dep in _deps:
pep_508_dep = _dep.to_pep_508() pep_508_dep = _dep.to_pep_508(False)
if ";" not in pep_508_dep: if ";" not in pep_508_dep:
_requirements = "" _requirements = ""
else: else:
......
import time
from typing import Any
from typing import Dict
from typing import List from typing import List
from poetry.mixology import resolve_version from poetry.mixology import resolve_version
from poetry.mixology.failure import SolveFailure from poetry.mixology.failure import SolveFailure
from poetry.packages import DependencyPackage from poetry.packages import DependencyPackage
from poetry.packages.constraints.generic_constraint import GenericConstraint
from poetry.semver import parse_constraint from poetry.semver import parse_constraint
from poetry.version.markers import AnyMarker
from .exceptions import CompatibilityError from .exceptions import CompatibilityError
from .exceptions import SolverProblemError from .exceptions import SolverProblemError
...@@ -26,12 +29,25 @@ class Solver: ...@@ -26,12 +29,25 @@ class Solver:
self._locked = locked self._locked = locked
self._io = io self._io = io
self._provider = Provider(self._package, self._pool, self._io) self._provider = Provider(self._package, self._pool, self._io)
self._branches = []
def solve(self, use_latest=None): # type: (...) -> List[Operation] def solve(self, use_latest=None): # type: (...) -> List[Operation]
with self._provider.progress(): with self._provider.progress():
start = time.time()
packages, depths = self._solve(use_latest=use_latest) packages, depths = self._solve(use_latest=use_latest)
end = time.time()
requested = self._package.all_requires if len(self._branches) > 1:
self._provider.debug(
"Complete version solving took {:.3f} seconds for {} branches".format(
end - start, len(self._branches[1:])
)
)
self._provider.debug(
"Resolved for branches: {}".format(
", ".join("({})".format(b) for b in self._branches[1:])
)
)
operations = [] operations = []
for package in packages: for package in packages:
...@@ -79,6 +95,7 @@ class Solver: ...@@ -79,6 +95,7 @@ class Solver:
# since it actually doesn't matter since removals are always on top. # since it actually doesn't matter since removals are always on top.
-depths[packages.index(o.package)] if o.job_type != "uninstall" else 0, -depths[packages.index(o.package)] if o.job_type != "uninstall" else 0,
o.package.name, o.package.name,
o.package.version,
), ),
) )
...@@ -104,15 +121,21 @@ class Solver: ...@@ -104,15 +121,21 @@ class Solver:
packages.append(package) packages.append(package)
depths.append(_depths[index]) depths.append(_depths[index])
continue continue
else:
idx = packages.index(package)
pkg = packages[idx]
depths[idx] = max(depths[idx], _depths[index])
pkg.marker = pkg.marker.union(package.marker)
current_package = packages[packages.index(package)] for dep in package.requires:
for dep in package.requires: if dep not in pkg.requires:
if dep not in current_package.requires: pkg.requires.append(dep)
current_package.requires.append(dep)
return packages, depths return packages, depths
def _solve(self, use_latest=None): def _solve(self, use_latest=None):
self._branches.append(self._package.python_versions)
locked = {} locked = {}
for package in self._locked.packages: for package in self._locked.packages:
locked[package.name] = DependencyPackage(package.to_dependency(), package) locked[package.name] = DependencyPackage(package.to_dependency(), package)
...@@ -134,58 +157,37 @@ class Solver: ...@@ -134,58 +157,37 @@ class Solver:
depths = [] depths = []
for package in packages: for package in packages:
category, optional, python, platform, depth = self._get_tags_for_package( category, optional, marker, depth = self._get_tags_for_package(
package, graph package, graph
) )
depths.append(depth) depths.append(depth)
package.category = category package.category = category
package.optional = optional package.optional = optional
package.marker = marker
# If requirements are empty, drop them
requirements = {}
if python is not None and python != "*":
requirements["python"] = python
if platform is not None and platform != "*":
requirements["platform"] = platform
package.requirements = requirements
return packages, depths return packages, depths
def _build_graph( def _build_graph(
self, package, packages, previous=None, previous_dep=None, dep=None self, package, packages, previous=None, previous_dep=None, dep=None
): ): # type: (...) -> Dict[str, Any]
if not previous: if not previous:
category = "dev" category = "dev"
optional = True optional = True
python_version = "*" marker = package.marker
platform = "*"
else: else:
category = dep.category category = dep.category
optional = dep.is_optional() and not dep.is_activated() optional = dep.is_optional() and not dep.is_activated()
python_version = str( intersection = previous["marker"].intersect(previous_dep.marker)
parse_constraint(previous["python_version"]).intersect(
previous_dep.python_constraint marker = intersection
)
)
platform = str(
previous_dep.platform
if GenericConstraint.parse(previous["platform"]).matches(
previous_dep.platform_constraint
)
and previous_dep.platform != "*"
else previous["platform"]
)
graph = { graph = {
"name": package.name, "name": package.name,
"category": category, "category": category,
"optional": optional, "optional": optional,
"python_version": python_version, "marker": marker,
"platform": platform, "children": [], # type: List[Dict[str, Any]]
"children": [],
} }
if previous_dep and previous_dep is not dep and previous_dep.name == dep.name: if previous_dep and previous_dep is not dep and previous_dep.name == dep.name:
...@@ -237,10 +239,8 @@ class Solver: ...@@ -237,10 +239,8 @@ class Solver:
) )
if existing: if existing:
existing["python_version"] = str( existing["marker"] = existing["marker"].union(
parse_constraint(existing["python_version"]).union( child_graph["marker"]
parse_constraint(child_graph["python_version"])
)
) )
continue continue
...@@ -251,37 +251,27 @@ class Solver: ...@@ -251,37 +251,27 @@ class Solver:
def _get_tags_for_package(self, package, graph, depth=0): def _get_tags_for_package(self, package, graph, depth=0):
categories = ["dev"] categories = ["dev"]
optionals = [True] optionals = [True]
python_versions = [] markers = []
platforms = []
_depths = [0] _depths = [0]
children = graph["children"] children = graph["children"]
found = False
for child in children: for child in children:
if child["name"] == package.name: if child["name"] == package.name:
category = child["category"] category = child["category"]
optional = child["optional"] optional = child["optional"]
python_version = child["python_version"] marker = child["marker"]
platform = child["platform"]
_depths.append(depth) _depths.append(depth)
else: else:
( (category, optional, marker, _depth) = self._get_tags_for_package(
category, package, child, depth=depth + 1
optional, )
python_version,
platform,
_depth,
) = self._get_tags_for_package(package, child, depth=depth + 1)
_depths.append(_depth) _depths.append(_depth)
categories.append(category) categories.append(category)
optionals.append(optional) optionals.append(optional)
if python_version is not None: if not marker.is_any():
python_versions.append(python_version) markers.append(marker)
if platform is not None:
platforms.append(platform)
if "main" in categories: if "main" in categories:
category = "main" category = "main"
...@@ -290,37 +280,13 @@ class Solver: ...@@ -290,37 +280,13 @@ class Solver:
optional = all(optionals) optional = all(optionals)
if not python_versions: depth = max(*(_depths + [0]))
python_version = None
else:
# Find the least restrictive constraint
python_version = python_versions[0]
for constraint in python_versions[1:]:
previous = parse_constraint(python_version)
current = parse_constraint(constraint)
if python_version == "*":
continue
elif constraint == "*":
python_version = constraint
elif current.allows_all(previous):
python_version = constraint
if not platforms: if not markers:
platform = None marker = AnyMarker()
else: else:
platform = platforms[0] marker = markers[0]
for constraint in platforms[1:]: for m in markers[1:]:
previous = GenericConstraint.parse(platform) marker = marker.union(m)
current = GenericConstraint.parse(constraint)
if platform == "*":
continue
elif constraint == "*":
platform = constraint
elif current.matches(previous):
platform = constraint
depth = max(*(_depths + [0]))
return category, optional, python_version, platform, depth return category, optional, marker, depth
...@@ -168,6 +168,9 @@ class VersionUnion(VersionConstraint): ...@@ -168,6 +168,9 @@ class VersionUnion(VersionConstraint):
return True return True
while True: while True:
if state["their_range"] is None:
break
if state["their_range"].is_strictly_lower(state["current"]): if state["their_range"].is_strictly_lower(state["current"]):
if not their_next_range(): if not their_next_range():
break break
......
import json
import os import os
import platform import platform
import subprocess import subprocess
...@@ -8,6 +9,7 @@ import warnings ...@@ -8,6 +9,7 @@ import warnings
from contextlib import contextmanager from contextlib import contextmanager
from subprocess import CalledProcessError from subprocess import CalledProcessError
from typing import Any from typing import Any
from typing import Dict
from typing import Optional from typing import Optional
from typing import Tuple from typing import Tuple
...@@ -15,6 +17,7 @@ from poetry.config import Config ...@@ -15,6 +17,7 @@ from poetry.config import Config
from poetry.locations import CACHE_DIR from poetry.locations import CACHE_DIR
from poetry.utils._compat import Path from poetry.utils._compat import Path
from poetry.utils._compat import decode from poetry.utils._compat import decode
from poetry.version.markers import BaseMarker
class EnvError(Exception): class EnvError(Exception):
...@@ -47,8 +50,7 @@ class Env(object): ...@@ -47,8 +50,7 @@ class Env(object):
self._base = base or path self._base = base or path
self._version_info = self.get_version_info() self._marker_env = None
self._python_implementation = self.get_python_implementation()
@property @property
def path(self): # type: () -> Path def path(self): # type: () -> Path
...@@ -60,11 +62,11 @@ class Env(object): ...@@ -60,11 +62,11 @@ class Env(object):
@property @property
def version_info(self): # type: () -> Tuple[int] def version_info(self): # type: () -> Tuple[int]
return self._version_info return tuple(self.marker_env["version_info"])
@property @property
def python_implementation(self): # type: () -> str def python_implementation(self): # type: () -> str
return self._python_implementation return self.marker_env["platform_python_implementation"]
@property @property
def python(self): # type: () -> str def python(self): # type: () -> str
...@@ -74,6 +76,13 @@ class Env(object): ...@@ -74,6 +76,13 @@ class Env(object):
return self._bin("python") return self._bin("python")
@property @property
def marker_env(self):
if self._marker_env is None:
self._marker_env = self.get_marker_env()
return self._marker_env
@property
def pip(self): # type: () -> str def pip(self): # type: () -> str
""" """
Path to current pip executable Path to current pip executable
...@@ -214,9 +223,15 @@ class Env(object): ...@@ -214,9 +223,15 @@ class Env(object):
def get_python_implementation(self): # type: () -> str def get_python_implementation(self): # type: () -> str
raise NotImplementedError() raise NotImplementedError()
def config_var(self, var): # type: () -> Any def get_marker_env(self): # type: () -> Dict[str, Any]
raise NotImplementedError()
def config_var(self, var): # type: (str) -> Any
raise NotImplementedError() raise NotImplementedError()
def is_valid_for_marker(self, marker): # type: (BaseMarker) -> bool
return marker.validate(self.marker_env)
def run(self, bin, *args, **kwargs): def run(self, bin, *args, **kwargs):
""" """
Run a command inside the Python environment. Run a command inside the Python environment.
...@@ -262,7 +277,7 @@ class Env(object): ...@@ -262,7 +277,7 @@ class Env(object):
return str(bin_path) return str(bin_path)
def __repr__(self): def __repr__(self):
return '{}("{}")'.format(self.__class__.__name__, self._base) return '{}("{}")'.format(self.__class__.__name__, self._path)
class SystemEnv(Env): class SystemEnv(Env):
...@@ -276,6 +291,34 @@ class SystemEnv(Env): ...@@ -276,6 +291,34 @@ class SystemEnv(Env):
def get_python_implementation(self): # type: () -> str def get_python_implementation(self): # type: () -> str
return platform.python_implementation() return platform.python_implementation()
def get_marker_env(self): # type: () -> Dict[str, Any]
if hasattr(sys, "implementation"):
info = sys.implementation.version
iver = "{0.major}.{0.minor}.{0.micro}".format(info)
kind = info.releaselevel
if kind != "final":
iver += kind[0] + str(info.serial)
implementation_name = sys.implementation.name
else:
iver = "0"
implementation_name = ""
return {
"implementation_name": implementation_name,
"implementation_version": iver,
"os_name": os.name,
"platform_machine": platform.machine(),
"platform_release": platform.release(),
"platform_system": platform.system(),
"platform_version": platform.version(),
"python_full_version": platform.python_version(),
"platform_python_implementation": platform.python_implementation(),
"python_version": platform.python_version()[:3],
"sys_platform": sys.platform,
"version_info": sys.version_info,
}
def config_var(self, var): # type: (str) -> Any def config_var(self, var): # type: (str) -> Any
try: try:
return sysconfig.get_config_var(var) return sysconfig.get_config_var(var)
...@@ -314,7 +357,7 @@ class VirtualEnv(Env): ...@@ -314,7 +357,7 @@ class VirtualEnv(Env):
" )" " )"
')"', ')"',
shell=True, shell=True,
) ).strip()
) )
def get_version_info(self): # type: () -> Tuple[int] def get_version_info(self): # type: () -> Tuple[int]
...@@ -328,12 +371,35 @@ class VirtualEnv(Env): ...@@ -328,12 +371,35 @@ class VirtualEnv(Env):
return tuple([int(s) for s in output.strip().split(".")]) return tuple([int(s) for s in output.strip().split(".")])
def get_python_implementation(self): # type: () -> str def get_python_implementation(self): # type: () -> str
return self.run( return self.marker_env["platform_python_implementation"]
def get_marker_env(self): # type: () -> Dict[str, Any]
output = self.run(
"python", "python",
"-c", "-c",
'"import platform; print(platform.python_implementation())"', '"import json; import os; import platform; import sys; '
"implementation = getattr(sys, 'implementation', None); "
"iver = '{0.major}.{0.minor}.{0.micro}'.format(implementation.version) if implementation else '0'; "
"implementation_name = implementation.name if implementation else ''; "
"env = {"
"'implementation_name': implementation_name,"
"'implementation_version': iver,"
"'os_name': os.name,"
"'platform_machine': platform.machine(),"
"'platform_release': platform.release(),"
"'platform_system': platform.system(),"
"'platform_version': platform.version(),"
"'python_full_version': platform.python_version(),"
"'platform_python_implementation': platform.python_implementation(),"
"'python_version': platform.python_version()[:3],"
"'sys_platform': sys.platform,"
"'version_info': sys.version_info[:3],"
"};"
'print(json.dumps(env))"',
shell=True, shell=True,
).strip() )
return json.loads(output)
def config_var(self, var): # type: (str) -> Any def config_var(self, var): # type: (str) -> Any
try: try:
...@@ -344,7 +410,7 @@ class VirtualEnv(Env): ...@@ -344,7 +410,7 @@ class VirtualEnv(Env):
"print(sysconfig.get_config_var('{}'))\"".format(var), "print(sysconfig.get_config_var('{}'))\"".format(var),
shell=True, shell=True,
).strip() ).strip()
except VenvCommandError as e: except EnvCommandError as e:
warnings.warn("{0}".format(e), RuntimeWarning) warnings.warn("{0}".format(e), RuntimeWarning)
return None return None
......
...@@ -86,3 +86,8 @@ def get_http_basic_auth(repository_name): # type: (str) -> tuple ...@@ -86,3 +86,8 @@ def get_http_basic_auth(repository_name): # type: (str) -> tuple
if repo_auth: if repo_auth:
return repo_auth["username"], repo_auth["password"] return repo_auth["username"], repo_auth["password"]
return None return None
def constraint_to_marker(constraint): # type: (Any) -> Marker
if constraint.is_any():
return AnyMarker()
...@@ -17,7 +17,8 @@ from pyparsing import Literal as L # noqa ...@@ -17,7 +17,8 @@ from pyparsing import Literal as L # noqa
from poetry.semver import parse_constraint from poetry.semver import parse_constraint
from .markers import MARKER_EXPR, Marker from .markers import MARKER_EXPR
from .markers import parse_marker
LEGACY_REGEX = r""" LEGACY_REGEX = r"""
...@@ -53,7 +54,7 @@ REGEX = r""" ...@@ -53,7 +54,7 @@ REGEX = r"""
# versions to be specified so we have to define these two # versions to be specified so we have to define these two
# operators separately to enable that. # operators separately to enable that.
(?<===|!=) # Only match for equals and not equals (?<===|!=) # Only match for equals and not equals
\s* \s*
v? v?
(?:[0-9]+!)? # epoch (?:[0-9]+!)? # epoch
...@@ -67,7 +68,7 @@ REGEX = r""" ...@@ -67,7 +68,7 @@ REGEX = r"""
(?: # post release (?: # post release
(?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*) (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
)? )?
# You cannot use a wild card and a dev or local version # You cannot use a wild card and a dev or local version
# together so group them with a | and make them optional. # together so group them with a | and make them optional.
(?: (?:
...@@ -82,7 +83,7 @@ REGEX = r""" ...@@ -82,7 +83,7 @@ REGEX = r"""
# The compatible operator requires at least two digits in the # The compatible operator requires at least two digits in the
# release segment. # release segment.
(?<=~=) # Only match for the compatible operator (?<=~=) # Only match for the compatible operator
\s* \s*
v? v?
(?:[0-9]+!)? # epoch (?:[0-9]+!)? # epoch
...@@ -107,7 +108,7 @@ REGEX = r""" ...@@ -107,7 +108,7 @@ REGEX = r"""
(?<!==|!=|~=) # We have special cases for these (?<!==|!=|~=) # We have special cases for these
# operators so we want to make sure they # operators so we want to make sure they
# don't match here. # don't match here.
\s* \s*
v? v?
(?:[0-9]+!)? # epoch (?:[0-9]+!)? # epoch
...@@ -171,7 +172,7 @@ VERSION_SPEC.setParseAction(lambda s, l, t: t[1]) ...@@ -171,7 +172,7 @@ VERSION_SPEC.setParseAction(lambda s, l, t: t[1])
MARKER_EXPR = originalTextFor(MARKER_EXPR())("marker") MARKER_EXPR = originalTextFor(MARKER_EXPR())("marker")
MARKER_EXPR.setParseAction( MARKER_EXPR.setParseAction(
lambda s, l, t: Marker(s[t._original_start : t._original_end]) lambda s, l, t: parse_marker(s[t._original_start : t._original_end])
) )
MARKER_SEPERATOR = SEMICOLON MARKER_SEPERATOR = SEMICOLON
MARKER = MARKER_SEPERATOR + MARKER_EXPR MARKER = MARKER_SEPERATOR + MARKER_EXPR
......
...@@ -5,7 +5,6 @@ description = "" ...@@ -5,7 +5,6 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[[package]] [[package]]
name = "B" name = "B"
...@@ -14,7 +13,6 @@ description = "" ...@@ -14,7 +13,6 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[[package]] [[package]]
name = "C" name = "C"
...@@ -23,7 +21,6 @@ description = "" ...@@ -23,7 +21,6 @@ description = ""
category = "main" category = "main"
optional = true optional = true
python-versions = "*" python-versions = "*"
platform = "*"
[package.dependencies] [package.dependencies]
D = "^1.0" D = "^1.0"
...@@ -35,14 +32,12 @@ description = "" ...@@ -35,14 +32,12 @@ description = ""
category = "main" category = "main"
optional = true optional = true
python-versions = "*" python-versions = "*"
platform = "*"
[extras] [extras]
foo = ["C"] foo = ["C"]
[metadata] [metadata]
python-versions = "*" python-versions = "*"
platform = "*"
content-hash = "123456789" content-hash = "123456789"
[metadata.hashes] [metadata.hashes]
......
...@@ -5,7 +5,6 @@ description = "" ...@@ -5,7 +5,6 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[[package]] [[package]]
name = "B" name = "B"
...@@ -14,7 +13,6 @@ description = "" ...@@ -14,7 +13,6 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[[package]] [[package]]
name = "C" name = "C"
...@@ -23,7 +21,6 @@ description = "" ...@@ -23,7 +21,6 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[[package]] [[package]]
name = "D" name = "D"
...@@ -32,14 +29,12 @@ description = "" ...@@ -32,14 +29,12 @@ description = ""
category = "main" category = "main"
optional = true optional = true
python-versions = "*" python-versions = "*"
platform = "*"
[extras] [extras]
foo = ["D"] foo = ["D"]
[metadata] [metadata]
python-versions = "*" python-versions = "*"
platform = "*"
content-hash = "123456789" content-hash = "123456789"
[metadata.hashes] [metadata.hashes]
......
...@@ -2,7 +2,6 @@ package = [] ...@@ -2,7 +2,6 @@ package = []
[metadata] [metadata]
python-versions = "*" python-versions = "*"
platform = "*"
content-hash = "123456789" content-hash = "123456789"
[metadata.hashes] [metadata.hashes]
...@@ -5,11 +5,9 @@ description = "" ...@@ -5,11 +5,9 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[metadata] [metadata]
python-versions = "*" python-versions = "*"
platform = "*"
content-hash = "123456789" content-hash = "123456789"
[metadata.hashes] [metadata.hashes]
......
...@@ -5,11 +5,9 @@ description = "" ...@@ -5,11 +5,9 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[metadata] [metadata]
python-versions = "*" python-versions = "*"
platform = "*"
content-hash = "123456789" content-hash = "123456789"
[metadata.hashes] [metadata.hashes]
......
...@@ -5,7 +5,6 @@ description = "" ...@@ -5,7 +5,6 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[package.dependencies] [package.dependencies]
"B" = "^1.0" "B" = "^1.0"
...@@ -18,19 +17,15 @@ description = "" ...@@ -18,19 +17,15 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[[package]] [[package]]
name = "C" name = "C"
version = "1.1" version = "1.1"
description = "" description = ""
category = "main" category = "main"
marker = "python_version >= \"2.7\" and python_version < \"2.8\""
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[package.requirements]
python = ">=2.7,<2.8"
[[package]] [[package]]
name = "D" name = "D"
...@@ -39,11 +34,9 @@ description = "" ...@@ -39,11 +34,9 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[metadata] [metadata]
python-versions = "*" python-versions = "*"
platform = "*"
content-hash = "123456789" content-hash = "123456789"
[metadata.hashes] [metadata.hashes]
......
...@@ -5,7 +5,6 @@ description = "" ...@@ -5,7 +5,6 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[[package]] [[package]]
name = "B" name = "B"
...@@ -14,14 +13,12 @@ description = "" ...@@ -14,14 +13,12 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[package.dependencies] [package.dependencies]
A = "^1.0" A = "^1.0"
[metadata] [metadata]
python-versions = "*" python-versions = "*"
platform = "*"
content-hash = "123456789" content-hash = "123456789"
[metadata.hashes] [metadata.hashes]
......
[[package]]
name = "A"
version = "1.0.0"
description = ""
category = "main"
optional = false
python-versions = ">=3.5"
[package.requirements]
python = ">=3.5,<4.0"
[[package]]
name = "A"
version = "1.0.1"
description = ""
category = "main"
optional = false
python-versions = ">=3.6"
[package.requirements]
python = ">=3.6,<4.0"
[metadata]
python-versions = "~2.7 || ^3.4"
content-hash = "123456789"
[metadata.hashes]
A = []
...@@ -5,7 +5,6 @@ description = "" ...@@ -5,7 +5,6 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[[package]] [[package]]
name = "B" name = "B"
...@@ -14,7 +13,6 @@ description = "" ...@@ -14,7 +13,6 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[package.dependencies] [package.dependencies]
C = "^1.0" C = "^1.0"
...@@ -26,11 +24,9 @@ description = "" ...@@ -26,11 +24,9 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[metadata] [metadata]
python-versions = "*" python-versions = "*"
platform = "*"
content-hash = "123456789" content-hash = "123456789"
[metadata.hashes] [metadata.hashes]
......
...@@ -5,7 +5,6 @@ description = "" ...@@ -5,7 +5,6 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[[package]] [[package]]
name = "B" name = "B"
...@@ -14,11 +13,9 @@ description = "" ...@@ -14,11 +13,9 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[metadata] [metadata]
python-versions = "*" python-versions = "*"
platform = "*"
content-hash = "123456789" content-hash = "123456789"
[metadata.hashes] [metadata.hashes]
......
...@@ -3,7 +3,6 @@ description = "" ...@@ -3,7 +3,6 @@ description = ""
category = "main" category = "main"
name = "pendulum" name = "pendulum"
optional = false optional = false
platform = "*"
python-versions = "*" python-versions = "*"
version = "1.4.4" version = "1.4.4"
...@@ -12,7 +11,6 @@ category = "main" ...@@ -12,7 +11,6 @@ category = "main"
description = "" description = ""
name = "project-with-extras" name = "project-with-extras"
optional = false optional = false
platform = "*"
python-versions = "*" python-versions = "*"
version = "1.2.3" version = "1.2.3"
...@@ -26,7 +24,6 @@ url = "tests/fixtures/project_with_extras" ...@@ -26,7 +24,6 @@ url = "tests/fixtures/project_with_extras"
[metadata] [metadata]
content-hash = "123456789" content-hash = "123456789"
platform = "*"
python-versions = "*" python-versions = "*"
[metadata.hashes] [metadata.hashes]
......
...@@ -5,7 +5,6 @@ description = "" ...@@ -5,7 +5,6 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[[package]] [[package]]
name = "my-package" name = "my-package"
...@@ -14,7 +13,6 @@ description = "Demo project." ...@@ -14,7 +13,6 @@ description = "Demo project."
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[package.source] [package.source]
type = "directory" type = "directory"
...@@ -32,11 +30,9 @@ description = "" ...@@ -32,11 +30,9 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[metadata] [metadata]
python-versions = "*" python-versions = "*"
platform = "*"
content-hash = "123456789" content-hash = "123456789"
[metadata.hashes] [metadata.hashes]
......
...@@ -5,7 +5,6 @@ description = "" ...@@ -5,7 +5,6 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[package.dependencies] [package.dependencies]
B = "^2.0" B = "^2.0"
...@@ -17,7 +16,6 @@ description = "" ...@@ -17,7 +16,6 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[package.dependencies] [package.dependencies]
C = "1.5" C = "1.5"
...@@ -29,11 +27,9 @@ description = "" ...@@ -29,11 +27,9 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[metadata] [metadata]
python-versions = "*" python-versions = "*"
platform = "*"
content-hash = "123456789" content-hash = "123456789"
[metadata.hashes] [metadata.hashes]
......
...@@ -5,7 +5,6 @@ description = "" ...@@ -5,7 +5,6 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[package.dependencies] [package.dependencies]
B = [ B = [
...@@ -18,58 +17,45 @@ name = "B" ...@@ -18,58 +17,45 @@ name = "B"
version = "1.0" version = "1.0"
description = "" description = ""
category = "main" category = "main"
marker = "python_version < \"4.0\""
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[package.dependencies] [package.dependencies]
C = "1.2" C = "1.2"
[package.requirements]
python = "<4.0"
[[package]] [[package]]
name = "B" name = "B"
version = "2.0" version = "2.0"
description = "" description = ""
category = "main" category = "main"
marker = "python_version >= \"4.0\""
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[package.dependencies] [package.dependencies]
C = "1.5" C = "1.5"
[package.requirements]
python = ">=4.0"
[[package]] [[package]]
name = "C" name = "C"
version = "1.2" version = "1.2"
description = "" description = ""
category = "main" category = "main"
marker = "python_version < \"4.0\""
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[package.requirements]
python = "<4.0"
[[package]] [[package]]
name = "C" name = "C"
version = "1.5" version = "1.5"
description = "" description = ""
category = "main" category = "main"
marker = "python_version >= \"4.0\""
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[package.requirements]
python = ">=4.0"
[metadata] [metadata]
python-versions = "*" python-versions = "*"
platform = "*"
content-hash = "123456789" content-hash = "123456789"
[metadata.hashes] [metadata.hashes]
......
...@@ -5,7 +5,6 @@ description = "Description" ...@@ -5,7 +5,6 @@ description = "Description"
category = "main" category = "main"
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.*"
platform = "*"
[package.source] [package.source]
type = "file" type = "file"
...@@ -22,11 +21,9 @@ description = "" ...@@ -22,11 +21,9 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[metadata] [metadata]
python-versions = "*" python-versions = "*"
platform = "*"
content-hash = "123456789" content-hash = "123456789"
[metadata.hashes] [metadata.hashes]
......
...@@ -5,41 +5,33 @@ description = "" ...@@ -5,41 +5,33 @@ description = ""
category = "main" category = "main"
optional = true optional = true
python-versions = "*" python-versions = "*"
platform = "*"
[[package]] [[package]]
name = "C" name = "C"
version = "1.3" version = "1.3"
description = "" description = ""
category = "main" category = "main"
marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.4\" and python_version < \"4.0\""
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[package.dependencies] [package.dependencies]
D = "^1.2" D = "^1.2"
[package.requirements]
python = ">=2.7,<2.8 || >=3.4,<4.0"
[[package]] [[package]]
name = "D" name = "D"
version = "1.4" version = "1.4"
description = "" description = ""
category = "main" category = "main"
marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.4\" and python_version < \"4.0\""
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[package.requirements]
python = ">=2.7,<2.8 || >=3.4,<4.0"
[extras] [extras]
foo = ["A"] foo = ["A"]
[metadata] [metadata]
python-versions = "~2.7 || ^3.4" python-versions = "~2.7 || ^3.4"
platform = "*"
content-hash = "123456789" content-hash = "123456789"
[metadata.hashes] [metadata.hashes]
......
...@@ -5,53 +5,42 @@ description = "" ...@@ -5,53 +5,42 @@ description = ""
category = "main" category = "main"
optional = true optional = true
python-versions = "*" python-versions = "*"
platform = "*"
[[package]] [[package]]
name = "B" name = "B"
version = "1.1" version = "1.1"
description = "" description = ""
category = "main" category = "main"
marker = "sys_platform == \"custom\""
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[package.requirements]
platform = "custom"
[[package]] [[package]]
name = "C" name = "C"
version = "1.3" version = "1.3"
description = "" description = ""
category = "main" category = "main"
marker = "sys_platform == \"darwin\""
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[package.dependencies] [package.dependencies]
D = "^1.2" D = "^1.2"
[package.requirements]
platform = "darwin"
[[package]] [[package]]
name = "D" name = "D"
version = "1.4" version = "1.4"
description = "" description = ""
category = "main" category = "main"
marker = "sys_platform == \"darwin\""
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[package.requirements]
platform = "darwin"
[extras] [extras]
foo = ["A"] foo = ["A"]
[metadata] [metadata]
python-versions = "*" python-versions = "*"
platform = "*"
content-hash = "123456789" content-hash = "123456789"
[metadata.hashes] [metadata.hashes]
......
...@@ -5,7 +5,6 @@ description = "" ...@@ -5,7 +5,6 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[[package]] [[package]]
name = "B" name = "B"
...@@ -14,11 +13,9 @@ description = "" ...@@ -14,11 +13,9 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[metadata] [metadata]
python-versions = "*" python-versions = "*"
platform = "*"
content-hash = "123456789" content-hash = "123456789"
[metadata.hashes] [metadata.hashes]
......
...@@ -5,31 +5,24 @@ description = "Classes Without Boilerplate" ...@@ -5,31 +5,24 @@ description = "Classes Without Boilerplate"
category = "dev" category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[[package]] [[package]]
name = "colorama" name = "colorama"
version = "0.3.9" version = "0.3.9"
description = "Cross-platform colored terminal text." description = "Cross-platform colored terminal text."
category = "dev" category = "dev"
marker = "sys_platform == \"win32\""
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "UNKNOWN"
[package.requirements]
platform = "win32"
[[package]] [[package]]
name = "funcsigs" name = "funcsigs"
version = "1.0.2" version = "1.0.2"
description = "Python function signatures from PEP362 for Python 2.6, 2.7 and 3.2+" description = "Python function signatures from PEP362 for Python 2.6, 2.7 and 3.2+"
category = "dev" category = "dev"
marker = "python_version < \"3.0\""
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "UNKNOWN"
[package.requirements]
python = "<3.0"
[[package]] [[package]]
name = "more-itertools" name = "more-itertools"
...@@ -38,7 +31,6 @@ description = "More routines for operating on iterables, beyond itertools" ...@@ -38,7 +31,6 @@ description = "More routines for operating on iterables, beyond itertools"
category = "dev" category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[package.dependencies] [package.dependencies]
six = ">=1.0.0,<2.0.0" six = ">=1.0.0,<2.0.0"
...@@ -50,7 +42,6 @@ description = "plugin and hook calling mechanisms for python" ...@@ -50,7 +42,6 @@ description = "plugin and hook calling mechanisms for python"
category = "dev" category = "dev"
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.*"
platform = "unix"
[[package]] [[package]]
name = "py" name = "py"
...@@ -59,7 +50,6 @@ description = "library with cross-python path, ini-parsing, io, code, log facili ...@@ -59,7 +50,6 @@ description = "library with cross-python path, ini-parsing, io, code, log facili
category = "dev" category = "dev"
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.*"
platform = "unix"
[[package]] [[package]]
name = "pytest" name = "pytest"
...@@ -68,7 +58,6 @@ description = "pytest: simple powerful testing with Python" ...@@ -68,7 +58,6 @@ description = "pytest: simple powerful testing with Python"
category = "dev" category = "dev"
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.*"
platform = "unix"
[package.dependencies] [package.dependencies]
py = ">=1.5.0" py = ">=1.5.0"
...@@ -78,7 +67,7 @@ attrs = ">=17.4.0" ...@@ -78,7 +67,7 @@ attrs = ">=17.4.0"
more-itertools = ">=4.0.0" more-itertools = ">=4.0.0"
pluggy = ">=0.5,<0.7" pluggy = ">=0.5,<0.7"
funcsigs = {"version" = "*", "python" = "<3.0"} funcsigs = {"version" = "*", "python" = "<3.0"}
colorama = {"version" = "*", "platform" = "win32"} colorama = "*"
[[package]] [[package]]
name = "six" name = "six"
...@@ -87,11 +76,9 @@ description = "Python 2 and 3 compatibility utilities" ...@@ -87,11 +76,9 @@ description = "Python 2 and 3 compatibility utilities"
category = "dev" category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[metadata] [metadata]
python-versions = "*" python-versions = "*"
platform = "*"
content-hash = "123456789" content-hash = "123456789"
[metadata.hashes] [metadata.hashes]
......
...@@ -5,7 +5,6 @@ description = "" ...@@ -5,7 +5,6 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[[package]] [[package]]
name = "B" name = "B"
...@@ -14,7 +13,6 @@ description = "" ...@@ -14,7 +13,6 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[[package]] [[package]]
name = "C" name = "C"
...@@ -23,11 +21,9 @@ description = "" ...@@ -23,11 +21,9 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "~2.7 || ^3.3" python-versions = "~2.7 || ^3.3"
platform = "*"
[metadata] [metadata]
python-versions = "~2.7 || ^3.4" python-versions = "~2.7 || ^3.4"
platform = "*"
content-hash = "123456789" content-hash = "123456789"
[metadata.hashes] [metadata.hashes]
......
...@@ -5,7 +5,6 @@ description = "" ...@@ -5,7 +5,6 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[package.dependencies] [package.dependencies]
D = "^1.0" D = "^1.0"
...@@ -17,7 +16,6 @@ description = "" ...@@ -17,7 +16,6 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[package.dependencies] [package.dependencies]
C = "~1.2" C = "~1.2"
...@@ -29,7 +27,6 @@ description = "" ...@@ -29,7 +27,6 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[[package]] [[package]]
name = "D" name = "D"
...@@ -38,11 +35,9 @@ description = "" ...@@ -38,11 +35,9 @@ description = ""
category = "main" category = "main"
optional = false optional = false
python-versions = "*" python-versions = "*"
platform = "*"
[metadata] [metadata]
python-versions = "*" python-versions = "*"
platform = "*"
content-hash = "123456789" content-hash = "123456789"
[metadata.hashes] [metadata.hashes]
......
...@@ -63,10 +63,8 @@ class Locker(BaseLocker): ...@@ -63,10 +63,8 @@ class Locker(BaseLocker):
def _write_lock_data(self, data): def _write_lock_data(self, data):
for package in data["package"]: for package in data["package"]:
python_versions = str(package["python-versions"]) python_versions = str(package["python-versions"])
platform = str(package["platform"])
if PY2: if PY2:
python_versions = python_versions.decode() python_versions = python_versions.decode()
platform = platform.decode()
if "requirements" in package: if "requirements" in package:
requirements = {} requirements = {}
for key, value in package["requirements"].items(): for key, value in package["requirements"].items():
...@@ -75,7 +73,6 @@ class Locker(BaseLocker): ...@@ -75,7 +73,6 @@ class Locker(BaseLocker):
package["requirements"] = requirements package["requirements"] = requirements
package["python-versions"] = python_versions package["python-versions"] = python_versions
package["platform"] = platform
self._written_data = data self._written_data = data
...@@ -1143,3 +1140,34 @@ def test_run_install_duplicate_dependencies_different_constraints_with_lock_upda ...@@ -1143,3 +1140,34 @@ def test_run_install_duplicate_dependencies_different_constraints_with_lock_upda
assert len(updates) == 1 assert len(updates) == 1
removals = installer.installer.removals removals = installer.installer.removals
assert len(removals) == 0 assert len(removals) == 0
@pytest.mark.skip(
"This is not working at the moment due to limitations in the resolver"
)
def test_installer_test_solver_finds_compatible_package_for_dependency_python_not_fully_compatible_with_package_python(
installer, locker, repo, package, installed
):
package.python_versions = "~2.7 || ^3.4"
package.add_dependency("A", {"version": "^1.0", "python": "^3.5"})
package_a101 = get_package("A", "1.0.1")
package_a101.python_versions = ">=3.6"
package_a100 = get_package("A", "1.0.0")
package_a100.python_versions = ">=3.5"
repo.add_package(package_a100)
repo.add_package(package_a101)
installer.run()
expected = fixture("with-conditional-dependency")
assert locker.written_data == expected
installs = installer.installer.installs
if sys.version_info >= (3, 5, 0):
assert len(installs) == 1
else:
assert len(installs) == 0
...@@ -91,12 +91,12 @@ def test_convert_dependencies(): ...@@ -91,12 +91,12 @@ def test_convert_dependencies():
main = ["B>=1.0,<1.1"] main = ["B>=1.0,<1.1"]
extra_python = ( extra_python = (
':(python_version >= "2.7" and python_version < "2.8") ' ':python_version >= "2.7" and python_version < "2.8" '
'or (python_version >= "3.6" and python_version < "4.0")' 'or python_version >= "3.6" and python_version < "4.0"'
) )
extra_d_dependency = ( extra_d_dependency = (
'baz:(python_version >= "2.7" and python_version < "2.8") ' 'baz:python_version >= "2.7" and python_version < "2.8" '
'or (python_version >= "3.4" and python_version < "4.0")' 'or python_version >= "3.4" and python_version < "4.0"'
) )
extras = {extra_python: ["C==1.2.3"], extra_d_dependency: ["D==3.4.5"]} extras = {extra_python: ["C==1.2.3"], extra_d_dependency: ["D==3.4.5"]}
......
from poetry.packages.constraints.constraint import Constraint
from poetry.packages.constraints.empty_constraint import EmptyConstraint
from poetry.packages.constraints.multi_constraint import MultiConstraint
from poetry.packages.constraints.union_constraint import UnionConstraint
def test_allows():
c = Constraint("win32")
assert c.allows(Constraint("win32"))
assert not c.allows(Constraint("linux"))
c = Constraint("win32", "!=")
assert not c.allows(Constraint("win32"))
assert c.allows(Constraint("linux"))
def test_allows_any():
c = Constraint("win32")
assert c.allows_any(Constraint("win32"))
assert not c.allows_any(Constraint("linux"))
assert c.allows_any(UnionConstraint(Constraint("win32"), Constraint("linux")))
assert c.allows_any(Constraint("linux", "!="))
c = Constraint("win32", "!=")
assert not c.allows_any(Constraint("win32"))
assert c.allows_any(Constraint("linux"))
assert c.allows_any(UnionConstraint(Constraint("win32"), Constraint("linux")))
assert c.allows_any(Constraint("linux", "!="))
def test_allows_all():
c = Constraint("win32")
assert c.allows_all(Constraint("win32"))
assert not c.allows_all(Constraint("linux"))
assert not c.allows_all(Constraint("linux", "!="))
assert not c.allows_all(UnionConstraint(Constraint("win32"), Constraint("linux")))
def test_intersect():
c = Constraint("win32")
intersection = c.intersect(Constraint("linux"))
assert intersection == EmptyConstraint()
intersection = c.intersect(
UnionConstraint(Constraint("win32"), Constraint("linux"))
)
assert intersection == Constraint("win32")
intersection = c.intersect(
UnionConstraint(Constraint("linux"), Constraint("linux2"))
)
assert intersection == EmptyConstraint()
intersection = c.intersect(Constraint("linux", "!="))
assert intersection == c
c = Constraint("win32", "!=")
intersection = c.intersect(Constraint("linux", "!="))
assert intersection == MultiConstraint(
Constraint("win32", "!="), Constraint("linux", "!=")
)
def test_union():
c = Constraint("win32")
union = c.union(Constraint("linux"))
assert union == UnionConstraint(Constraint("win32"), Constraint("linux"))
union = c.union(UnionConstraint(Constraint("win32"), Constraint("linux")))
assert union == UnionConstraint(Constraint("win32"), Constraint("linux"))
union = c.union(UnionConstraint(Constraint("linux"), Constraint("linux2")))
assert union == UnionConstraint(
Constraint("win32"), Constraint("linux"), Constraint("linux2")
)
def test_difference():
c = Constraint("win32")
assert c.difference(Constraint("win32")).is_empty()
assert c.difference(Constraint("linux")) == c
import pytest
from poetry.packages.constraints import parse_constraint
from poetry.packages.constraints.any_constraint import AnyConstraint
from poetry.packages.constraints.constraint import Constraint
from poetry.packages.constraints.multi_constraint import MultiConstraint
from poetry.packages.constraints.union_constraint import UnionConstraint
@pytest.mark.parametrize(
"input,constraint",
[
("*", AnyConstraint()),
("win32", Constraint("win32", "=")),
("=win32", Constraint("win32", "=")),
("==win32", Constraint("win32", "=")),
("!=win32", Constraint("win32", "!=")),
("!= win32", Constraint("win32", "!=")),
],
)
def test_parse_constraint(input, constraint):
assert parse_constraint(input) == constraint
@pytest.mark.parametrize(
"input,constraint",
[
(
"!=win32,!=linux",
MultiConstraint(Constraint("win32", "!="), Constraint("linux", "!=")),
),
(
"!=win32,!=linux,!=linux2",
MultiConstraint(
Constraint("win32", "!="),
Constraint("linux", "!="),
Constraint("linux2", "!="),
),
),
],
)
def test_parse_constraint_multi(input, constraint):
assert parse_constraint(input) == constraint
@pytest.mark.parametrize(
"input,constraint",
[
("win32 || linux", UnionConstraint(Constraint("win32"), Constraint("linux"))),
(
"win32 || !=linux2",
UnionConstraint(Constraint("win32"), Constraint("linux2", "!=")),
),
],
)
def test_parse_constraint_multi(input, constraint):
assert parse_constraint(input) == constraint
from poetry.packages.constraints.constraint import Constraint
from poetry.packages.constraints.multi_constraint import MultiConstraint
def test_allows():
c = MultiConstraint(Constraint("win32", "!="), Constraint("linux", "!="))
assert not c.allows(Constraint("win32"))
assert not c.allows(Constraint("linux"))
assert c.allows(Constraint("darwin"))
def test_allows_any():
c = MultiConstraint(Constraint("win32", "!="), Constraint("linux", "!="))
assert c.allows_any(Constraint("darwin"))
assert c.allows_any(Constraint("darwin", "!="))
assert not c.allows_any(Constraint("win32"))
assert c.allows_any(c)
assert c.allows_any(
MultiConstraint(Constraint("win32", "!="), Constraint("darwin", "!="))
)
def test_allows_all():
c = MultiConstraint(Constraint("win32", "!="), Constraint("linux", "!="))
assert c.allows_all(Constraint("darwin"))
assert c.allows_all(Constraint("darwin", "!="))
assert not c.allows_all(Constraint("win32"))
assert c.allows_all(c)
assert not c.allows_all(
MultiConstraint(Constraint("win32", "!="), Constraint("darwin", "!="))
)
def test_intersect():
c = MultiConstraint(Constraint("win32", "!="), Constraint("linux", "!="))
intersection = c.intersect(Constraint("win32", "!="))
assert intersection == Constraint("win32", "!=")
from poetry.packages.constraints.constraint import Constraint
from poetry.packages.constraints.union_constraint import UnionConstraint
def test_allows():
c = UnionConstraint(Constraint("win32"), Constraint("linux"))
assert c.allows(Constraint("win32"))
assert c.allows(Constraint("linux"))
assert not c.allows(Constraint("darwin"))
def test_allows_any():
c = UnionConstraint(Constraint("win32"), Constraint("linux"))
assert c.allows_any(c)
assert c.allows_any(UnionConstraint(Constraint("win32"), Constraint("darwin")))
assert not c.allows_any(UnionConstraint(Constraint("linux2"), Constraint("darwin")))
assert c.allows_any(Constraint("win32"))
assert not c.allows_any(Constraint("darwin"))
def test_allows_all():
c = UnionConstraint(Constraint("win32"), Constraint("linux"))
assert c.allows_all(c)
assert not c.allows_all(UnionConstraint(Constraint("win32"), Constraint("darwin")))
assert not c.allows_all(UnionConstraint(Constraint("linux2"), Constraint("darwin")))
assert c.allows_all(Constraint("win32"))
assert not c.allows_all(Constraint("darwin"))
...@@ -67,23 +67,8 @@ def test_to_pep_508(): ...@@ -67,23 +67,8 @@ def test_to_pep_508():
result = dependency.to_pep_508() result = dependency.to_pep_508()
assert ( assert (
result == "Django (>=1.23,<2.0); " result == "Django (>=1.23,<2.0); "
'(python_version >= "2.7" and python_version < "2.8") ' 'python_version >= "2.7" and python_version < "2.8" '
'or (python_version >= "3.6" and python_version < "4.0")' 'or python_version >= "3.6" and python_version < "4.0"'
)
def test_to_pep_508_with_platform():
dependency = Dependency("Django", "^1.23")
dependency.python_versions = "~2.7 || ^3.6"
dependency.platform = "linux || linux2"
result = dependency.to_pep_508()
assert result == (
"Django (>=1.23,<2.0); "
'((python_version >= "2.7" and python_version < "2.8") '
'or (python_version >= "3.6" and python_version < "4.0"))'
' and (sys_platform == "linux" or sys_platform == "linux2")'
) )
...@@ -112,8 +97,8 @@ def test_to_pep_508_in_extras(): ...@@ -112,8 +97,8 @@ def test_to_pep_508_in_extras():
assert result == ( assert result == (
"Django (>=1.23,<2.0); " "Django (>=1.23,<2.0); "
"(" "("
'(python_version >= "2.7" and python_version < "2.8") ' 'python_version >= "2.7" and python_version < "2.8" '
'or (python_version >= "3.6" and python_version < "4.0")' 'or python_version >= "3.6" and python_version < "4.0"'
") " ") "
'and (extra == "foo" or extra == "bar")' 'and (extra == "foo" or extra == "bar")'
) )
...@@ -13,7 +13,6 @@ def test_file_dependency_wheel(): ...@@ -13,7 +13,6 @@ def test_file_dependency_wheel():
assert dependency.name == "demo" assert dependency.name == "demo"
assert dependency.pretty_constraint == "0.1.0" assert dependency.pretty_constraint == "0.1.0"
assert dependency.python_versions == "*" assert dependency.python_versions == "*"
assert dependency.platform == "*"
meta = dependency.metadata meta = dependency.metadata
assert meta.requires_dist == ["pendulum (>=1.4.0.0,<2.0.0.0)"] assert meta.requires_dist == ["pendulum (>=1.4.0.0,<2.0.0.0)"]
...@@ -26,7 +25,6 @@ def test_file_dependency_sdist(): ...@@ -26,7 +25,6 @@ def test_file_dependency_sdist():
assert dependency.name == "demo" assert dependency.name == "demo"
assert dependency.pretty_constraint == "0.1.0" assert dependency.pretty_constraint == "0.1.0"
assert dependency.python_versions == "*" assert dependency.python_versions == "*"
assert dependency.platform == "*"
meta = dependency.metadata meta = dependency.metadata
assert meta.requires_dist == ["pendulum (>=1.4.0.0,<2.0.0.0)"] assert meta.requires_dist == ["pendulum (>=1.4.0.0,<2.0.0.0)"]
......
...@@ -37,7 +37,6 @@ category = "main" ...@@ -37,7 +37,6 @@ category = "main"
description = "" description = ""
name = "A" name = "A"
optional = false optional = false
platform = "*"
python-versions = "*" python-versions = "*"
version = "1.0.0" version = "1.0.0"
...@@ -49,13 +48,11 @@ category = "main" ...@@ -49,13 +48,11 @@ category = "main"
description = "" description = ""
name = "B" name = "B"
optional = false optional = false
platform = "*"
python-versions = "*" python-versions = "*"
version = "1.2" version = "1.2"
[metadata] [metadata]
content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8" content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8"
platform = "*"
python-versions = "*" python-versions = "*"
[metadata.hashes] [metadata.hashes]
......
...@@ -40,37 +40,40 @@ def test_dependency_from_pep_508_with_extras(): ...@@ -40,37 +40,40 @@ def test_dependency_from_pep_508_with_extras():
assert dep.name == "requests" assert dep.name == "requests"
assert str(dep.constraint) == "2.18.0" assert str(dep.constraint) == "2.18.0"
assert dep.in_extras == ["foo", "bar"] assert dep.in_extras == ["foo", "bar"]
assert str(dep.marker) == 'extra == "foo" or extra == "bar"'
def test_dependency_from_pep_508_with_python_version(): def test_dependency_from_pep_508_with_python_version():
name = "requests (==2.18.0); " 'python_version == "2.7" or python_version == "2.6"' name = 'requests (==2.18.0); python_version == "2.7" or python_version == "2.6"'
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == "requests" assert dep.name == "requests"
assert str(dep.constraint) == "2.18.0" assert str(dep.constraint) == "2.18.0"
assert dep.extras == [] assert dep.extras == []
assert dep.python_versions == "~2.7 || ~2.6" assert dep.python_versions == "~2.7 || ~2.6"
assert str(dep.marker) == 'python_version == "2.7" or python_version == "2.6"'
def test_dependency_from_pep_508_with_single_python_version(): def test_dependency_from_pep_508_with_single_python_version():
name = "requests (==2.18.0); " 'python_version == "2.7"' name = 'requests (==2.18.0); python_version == "2.7"'
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == "requests" assert dep.name == "requests"
assert str(dep.constraint) == "2.18.0" assert str(dep.constraint) == "2.18.0"
assert dep.extras == [] assert dep.extras == []
assert dep.python_versions == "~2.7" assert dep.python_versions == "~2.7"
assert str(dep.marker) == 'python_version == "2.7"'
def test_dependency_from_pep_508_with_platform(): def test_dependency_from_pep_508_with_platform():
name = "requests (==2.18.0); " 'sys_platform == "win32" or sys_platform == "darwin"' name = 'requests (==2.18.0); sys_platform == "win32" or sys_platform == "darwin"'
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == "requests" assert dep.name == "requests"
assert str(dep.constraint) == "2.18.0" assert str(dep.constraint) == "2.18.0"
assert dep.extras == [] assert dep.extras == []
assert dep.python_versions == "*" assert dep.python_versions == "*"
assert dep.platform == "win32 || darwin" assert str(dep.marker) == 'sys_platform == "win32" or sys_platform == "darwin"'
def test_dependency_from_pep_508_complex(): def test_dependency_from_pep_508_complex():
...@@ -86,34 +89,40 @@ def test_dependency_from_pep_508_complex(): ...@@ -86,34 +89,40 @@ def test_dependency_from_pep_508_complex():
assert str(dep.constraint) == "2.18.0" assert str(dep.constraint) == "2.18.0"
assert dep.in_extras == ["foo"] assert dep.in_extras == ["foo"]
assert dep.python_versions == ">=2.7 !=3.2.*" assert dep.python_versions == ">=2.7 !=3.2.*"
assert dep.platform == "win32 || darwin" assert str(dep.marker) == (
'python_version >= "2.7" and python_version != "3.2" '
'and (sys_platform == "win32" or sys_platform == "darwin") '
'and extra == "foo"'
)
def test_dependency_python_version_in(): def test_dependency_python_version_in():
name = "requests (==2.18.0); " "python_version in '3.3 3.4 3.5'" name = "requests (==2.18.0); python_version in '3.3 3.4 3.5'"
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == "requests" assert dep.name == "requests"
assert str(dep.constraint) == "2.18.0" assert str(dep.constraint) == "2.18.0"
assert dep.python_versions == "3.3.* || 3.4.* || 3.5.*" assert dep.python_versions == "3.3.* || 3.4.* || 3.5.*"
assert str(dep.marker) == 'python_version in "3.3 3.4 3.5"'
def test_dependency_python_version_in_comma(): def test_dependency_python_version_in_comma():
name = "requests (==2.18.0); " "python_version in '3.3, 3.4, 3.5'" name = "requests (==2.18.0); python_version in '3.3, 3.4, 3.5'"
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == "requests" assert dep.name == "requests"
assert str(dep.constraint) == "2.18.0" assert str(dep.constraint) == "2.18.0"
assert dep.python_versions == "3.3.* || 3.4.* || 3.5.*" assert dep.python_versions == "3.3.* || 3.4.* || 3.5.*"
assert str(dep.marker) == 'python_version in "3.3, 3.4, 3.5"'
def test_dependency_platform_in(): def test_dependency_platform_in():
name = "requests (==2.18.0); " "sys_platform in 'win32 darwin'" name = "requests (==2.18.0); sys_platform in 'win32 darwin'"
dep = dependency_from_pep_508(name) dep = dependency_from_pep_508(name)
assert dep.name == "requests" assert dep.name == "requests"
assert str(dep.constraint) == "2.18.0" assert str(dep.constraint) == "2.18.0"
assert dep.platform == "win32 || darwin" assert str(dep.marker) == 'sys_platform in "win32 darwin"'
def test_dependency_with_extra(): def test_dependency_with_extra():
...@@ -125,3 +134,21 @@ def test_dependency_with_extra(): ...@@ -125,3 +134,21 @@ def test_dependency_with_extra():
assert len(dep.extras) == 1 assert len(dep.extras) == 1
assert dep.extras[0] == "security" assert dep.extras[0] == "security"
def test_dependency_from_pep_508_with_python_version_union_of_multi():
name = (
"requests (==2.18.0); "
'(python_version >= "2.7" and python_version < "2.8") '
'or (python_version >= "3.4" and python_version < "3.5")'
)
dep = dependency_from_pep_508(name)
assert dep.name == "requests"
assert str(dep.constraint) == "2.18.0"
assert dep.extras == []
assert dep.python_versions == ">=2.7 <2.8 || >=3.4 <3.5"
assert str(dep.marker) == (
'python_version >= "2.7" and python_version < "2.8" '
'or python_version >= "3.4" and python_version < "3.5"'
)
...@@ -3,6 +3,7 @@ import pytest ...@@ -3,6 +3,7 @@ import pytest
from cleo.outputs.null_output import NullOutput from cleo.outputs.null_output import NullOutput
from cleo.styles import OutputStyle from cleo.styles import OutputStyle
from poetry.packages import dependency_from_pep_508
from poetry.packages import ProjectPackage from poetry.packages import ProjectPackage
from poetry.repositories.installed_repository import InstalledRepository from poetry.repositories.installed_repository import InstalledRepository
from poetry.repositories.pool import Pool from poetry.repositories.pool import Pool
...@@ -12,7 +13,6 @@ from poetry.puzzle.exceptions import SolverProblemError ...@@ -12,7 +13,6 @@ from poetry.puzzle.exceptions import SolverProblemError
from tests.helpers import get_dependency from tests.helpers import get_dependency
from tests.helpers import get_package from tests.helpers import get_package
from tests.repositories.test_pypi_repository import MockRepository
@pytest.fixture() @pytest.fixture()
...@@ -370,36 +370,6 @@ def test_solver_solves_optional_and_compatible_packages(solver, repo, package): ...@@ -370,36 +370,6 @@ def test_solver_solves_optional_and_compatible_packages(solver, repo, package):
) )
def test_solver_solves_while_respecting_root_platforms(solver, repo, package):
package.platform = "darwin"
package.add_dependency("A")
package.add_dependency("B")
package_a = get_package("A", "1.0")
package_b = get_package("B", "1.0")
package_c12 = get_package("C", "1.2")
package_c12.platform = "win32"
package_c10 = get_package("C", "1.0")
package_c10.platform = "darwin"
package_b.add_dependency("C", "^1.0")
repo.add_package(package_a)
repo.add_package(package_b)
repo.add_package(package_c10)
repo.add_package(package_c12)
ops = solver.solve()
check_solver_result(
ops,
[
{"job": "install", "package": package_c10},
{"job": "install", "package": package_a},
{"job": "install", "package": package_b},
],
)
def test_solver_does_not_return_extras_if_not_requested(solver, repo, package): def test_solver_does_not_return_extras_if_not_requested(solver, repo, package):
package.add_dependency("A") package.add_dependency("A")
package.add_dependency("B") package.add_dependency("B")
...@@ -537,7 +507,7 @@ def test_solver_sub_dependencies_with_requirements(solver, repo, package): ...@@ -537,7 +507,7 @@ def test_solver_sub_dependencies_with_requirements(solver, repo, package):
) )
op = ops[1] op = ops[1]
assert op.package.requirements == {} assert op.package.marker.is_any()
def test_solver_sub_dependencies_with_requirements_complex(solver, repo, package): def test_solver_sub_dependencies_with_requirements_complex(solver, repo, package):
...@@ -581,16 +551,19 @@ def test_solver_sub_dependencies_with_requirements_complex(solver, repo, package ...@@ -581,16 +551,19 @@ def test_solver_sub_dependencies_with_requirements_complex(solver, repo, package
) )
op = ops[3] # d op = ops[3] # d
assert op.package.requirements == {"python": "<4.0"} assert str(op.package.marker) == 'python_version < "4.0"'
op = ops[0] # e op = ops[0] # e
assert op.package.requirements == {"platform": "win32", "python": "<5.0"} assert str(op.package.marker) == (
'python_version < "4.0" and sys_platform == "win32" '
'or python_version < "5.0" and sys_platform == "win32"'
)
op = ops[1] # f op = ops[1] # f
assert op.package.requirements == {"python": "<5.0"} assert str(op.package.marker) == 'python_version < "5.0"'
op = ops[4] # a op = ops[4] # a
assert op.package.requirements == {"python": "<5.0"} assert str(op.package.marker) == 'python_version < "5.0"'
def test_solver_sub_dependencies_with_not_supported_python_version( def test_solver_sub_dependencies_with_not_supported_python_version(
...@@ -785,7 +758,9 @@ def test_solver_duplicate_dependencies_same_constraint(solver, repo, package): ...@@ -785,7 +758,9 @@ def test_solver_duplicate_dependencies_same_constraint(solver, repo, package):
) )
op = ops[0] op = ops[0]
assert op.package.requirements == {"python": "2.7 || >=3.4"} assert (
str(op.package.marker) == 'python_version == "2.7" or python_version >= "3.4"'
)
def test_solver_duplicate_dependencies_different_constraints(solver, repo, package): def test_solver_duplicate_dependencies_different_constraints(solver, repo, package):
...@@ -814,10 +789,10 @@ def test_solver_duplicate_dependencies_different_constraints(solver, repo, packa ...@@ -814,10 +789,10 @@ def test_solver_duplicate_dependencies_different_constraints(solver, repo, packa
) )
op = ops[0] op = ops[0]
assert op.package.requirements == {"python": "<3.4"} assert str(op.package.marker) == 'python_version < "3.4"'
op = ops[1] op = ops[1]
assert op.package.requirements == {"python": ">=3.4"} assert str(op.package.marker) == 'python_version >= "3.4"'
def test_solver_duplicate_dependencies_different_constraints_same_requirements( def test_solver_duplicate_dependencies_different_constraints_same_requirements(
...@@ -882,10 +857,10 @@ def test_solver_duplicate_dependencies_sub_dependencies(solver, repo, package): ...@@ -882,10 +857,10 @@ def test_solver_duplicate_dependencies_sub_dependencies(solver, repo, package):
) )
op = ops[2] op = ops[2]
assert op.package.requirements == {"python": "<3.4"} assert str(op.package.marker) == 'python_version < "3.4"'
op = ops[3] op = ops[3]
assert op.package.requirements == {"python": ">=3.4"} assert str(op.package.marker) == 'python_version >= "3.4"'
def test_solver_fails_if_dependency_name_does_not_match_package(solver, repo, package): def test_solver_fails_if_dependency_name_does_not_match_package(solver, repo, package):
...@@ -967,7 +942,7 @@ def test_solver_can_resolve_git_dependencies_with_extras(solver, repo, package): ...@@ -967,7 +942,7 @@ def test_solver_can_resolve_git_dependencies_with_extras(solver, repo, package):
def test_solver_does_not_trigger_conflict_for_python_constraint_if_python_requirement_is_compatible( def test_solver_does_not_trigger_conflict_for_python_constraint_if_python_requirement_is_compatible(
solver, repo, package solver, repo, package
): ):
package.python_versions = "~2.7 || ^3.6" package.python_versions = "~2.7 || ^3.4"
package.add_dependency("A", {"version": "^1.0", "python": "^3.6"}) package.add_dependency("A", {"version": "^1.0", "python": "^3.6"})
package_a = get_package("A", "1.0.0") package_a = get_package("A", "1.0.0")
...@@ -978,3 +953,123 @@ def test_solver_does_not_trigger_conflict_for_python_constraint_if_python_requir ...@@ -978,3 +953,123 @@ def test_solver_does_not_trigger_conflict_for_python_constraint_if_python_requir
ops = solver.solve() ops = solver.solve()
check_solver_result(ops, [{"job": "install", "package": package_a}]) check_solver_result(ops, [{"job": "install", "package": package_a}])
assert (
str(ops[0].package.marker)
== 'python_version >= "3.6" and python_version < "4.0"'
)
def test_solver_triggers_conflict_for_dependency_python_not_fully_compatible_with_package_python(
solver, repo, package
):
package.python_versions = "~2.7 || ^3.4"
package.add_dependency("A", {"version": "^1.0", "python": "^3.5"})
package_a = get_package("A", "1.0.0")
package_a.python_versions = ">=3.6"
repo.add_package(package_a)
with pytest.raises(SolverProblemError):
solver.solve()
@pytest.mark.skip(
"This is not working at the moment due to limitations in the resolver"
)
def test_solver_finds_compatible_package_for_dependency_python_not_fully_compatible_with_package_python(
solver, repo, package
):
package.python_versions = "~2.7 || ^3.4"
package.add_dependency("A", {"version": "^1.0", "python": "^3.5"})
package_a101 = get_package("A", "1.0.1")
package_a101.python_versions = ">=3.6"
package_a100 = get_package("A", "1.0.0")
package_a100.python_versions = ">=3.5"
repo.add_package(package_a100)
repo.add_package(package_a101)
ops = solver.solve()
check_solver_result(ops, [{"job": "install", "package": package_a100}])
assert (
str(ops[0].package.marker)
== 'python_version >= "3.5" and python_version < "4.0"'
)
def test_solver_sets_appropriate_markers_when_solving(solver, repo, package):
dep = dependency_from_pep_508(
'B (>=1.0); python_version >= "3.6" and sys_platform != "win32"'
)
package.add_dependency("A", "^1.0")
package_a = get_package("A", "1.0.0")
package_a.requires.append(dep)
package_b = get_package("B", "1.0.0")
repo.add_package(package_a)
repo.add_package(package_b)
ops = solver.solve()
check_solver_result(
ops,
[
{"job": "install", "package": package_b},
{"job": "install", "package": package_a},
],
)
assert (
str(ops[0].package.marker)
== 'python_version >= "3.6" and sys_platform != "win32"'
)
assert str(ops[1].package.marker) == ""
def test_solver_does_not_trigger_new_resolution_on_duplicate_dependencies_if_only_extras(
solver, repo, package
):
dep1 = dependency_from_pep_508('B (>=1.0); extra == "foo"')
dep1.activate()
dep2 = dependency_from_pep_508('B (>=2.0); extra == "bar"')
dep2.activate()
package.add_dependency("A", {"version": "^1.0", "extras": ["foo", "bar"]})
package_a = get_package("A", "1.0.0")
package_a.extras = {"foo": [dep1], "bar": [dep2]}
package_a.requires.append(dep1)
package_a.requires.append(dep2)
package_b2 = get_package("B", "2.0.0")
package_b1 = get_package("B", "1.0.0")
repo.add_package(package_a)
repo.add_package(package_b1)
repo.add_package(package_b2)
ops = solver.solve()
check_solver_result(
ops,
[
{"job": "install", "package": package_b2},
{"job": "install", "package": package_a},
],
)
assert str(ops[0].package.marker) in [
'extra == "foo" or extra == "bar"',
'extra == "bar" or extra == "foo"',
]
assert str(ops[1].package.marker) == ""
...@@ -68,7 +68,10 @@ def test_package(): ...@@ -68,7 +68,10 @@ def test_package():
win_inet = package.extras["socks"][0] win_inet = package.extras["socks"][0]
assert win_inet.name == "win-inet-pton" assert win_inet.name == "win-inet-pton"
assert win_inet.python_versions == "~2.7 || ~2.6" assert win_inet.python_versions == "~2.7 || ~2.6"
assert win_inet.platform == "win32" assert str(win_inet.marker) == (
'sys_platform == "win32" and (python_version == "2.7" '
'or python_version == "2.6") and extra == "socks"'
)
def test_fallback_on_downloading_packages(): def test_fallback_on_downloading_packages():
......
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