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 @@
- Improved virtualenv detection and management.
- 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
......
......@@ -318,11 +318,11 @@ and installs them.
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.
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
the `--no-dev` option.
......@@ -346,14 +346,14 @@ poetry install -E mysql -E pgsql
### 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.
```bash
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:
......
......@@ -14,16 +14,17 @@ class DebugResolveCommand(Command):
{ --E|extras=* : Extras to activate for the dependency. }
{ --python= : Python version(s) to use for resolution. }
{ --tree : Displays the dependency tree. }
{ --install : Show what would be installed for the current system. }
"""
_loggers = ["poetry.repositories.pypi_repository"]
def handle(self):
from poetry.packages import Dependency
from poetry.packages import ProjectPackage
from poetry.puzzle import Solver
from poetry.repositories.repository import Repository
from poetry.semver import parse_constraint
from poetry.utils.env import Env
packages = self.argument("package")
......@@ -35,7 +36,6 @@ class DebugResolveCommand(Command):
)
requirements = self._format_requirements(packages)
dependencies = []
for name, constraint in requirements.items():
dep = package.add_dependency(name, constraint)
extras = []
......@@ -48,13 +48,13 @@ class DebugResolveCommand(Command):
for ex in extras:
dep.extras.append(ex)
package.python_versions = (
self.option("python") or self.poetry.package.python_versions
package.python_versions = self.option("python") or (
self.poetry.package.python_versions
)
solver = Solver(
package, self.poetry.pool, Repository(), Repository(), self.output
)
pool = self.poetry.pool
solver = Solver(package, pool, Repository(), Repository(), self.output)
ops = solver.solve()
......@@ -79,16 +79,28 @@ class DebugResolveCommand(Command):
return 0
env = Env.get()
current_python_version = parse_constraint(
".".join(str(v) for v in env.version_info)
)
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(
" - <info>{}</info> (<comment>{}</comment>)".format(
package.name, package.version
pkg.name, pkg.version
)
)
if package.requirements:
for req_name, req_value in package.requirements.items():
self.line(" - {}: {}".format(req_name, req_value))
if not pkg.python_constraint.is_any():
self.line(" - python: {}".format(pkg.python_versions))
if not pkg.marker.is_any():
self.line(" - marker: {}".format(pkg.marker))
def _determine_requirements(self, requires): # type: (List[str]) -> List[str]
from poetry.semver import parse_constraint
......@@ -97,7 +109,6 @@ class DebugResolveCommand(Command):
return []
requires = self._parse_name_version_pairs(requires)
result = []
for requirement in requires:
if "version" in requirement:
parse_constraint(requirement["version"])
......
......@@ -23,7 +23,9 @@ lists all packages available."""
colors = ["green", "yellow", "cyan", "magenta", "blue"]
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.semver import Version
from poetry.semver import parse_constraint
......@@ -99,17 +101,13 @@ lists all packages available."""
installed_repo = InstalledRepository.load(self.env)
skipped = []
platform = sys.platform
python = Version.parse(".".join([str(i) for i in self.env.version_info[:3]]))
# Computing widths
for locked in locked_packages:
python_constraint = parse_constraint(locked.requirements.get("python", "*"))
platform_constraint = GenericConstraint.parse(
locked.requirements.get("platform", "*")
)
if not python_constraint.allows(python) or not platform_constraint.matches(
GenericConstraint("=", platform)
python_constraint = locked.python_constraint
if not python_constraint.allows(python) or not self.env.is_valid_for_marker(
locked.marker
):
skipped.append(locked)
......
......@@ -7,7 +7,7 @@ from poetry.io import NullIO
from poetry.packages import Dependency
from poetry.packages import Locker
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.operations import Install
from poetry.puzzle.operations import Uninstall
......@@ -457,29 +457,13 @@ class Installer:
if op.skipped:
op.unskip()
python = Version.parse(
".".join([str(i) for i in self._env.version_info[:3]])
current_python = parse_constraint(
".".join(str(v) for v in self._env.version_info[:3])
)
if "python" in package.requirements:
python_constraint = parse_constraint(package.requirements["python"])
if not python_constraint.allows(python):
# Incompatible python versions
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
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")
if not package.python_constraint.allows(
current_python
) or not self._env.is_valid_for_marker(package.marker):
op.skip("Not needed for the current environment")
continue
if self._update:
......
......@@ -151,7 +151,7 @@ class Term(object):
return (
self.dependency.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):
......
......@@ -34,7 +34,7 @@ def dependency_from_pep_508(name):
req = Requirement(name)
if req.marker:
markers = convert_markers(req.marker.markers)
markers = convert_markers(req.marker)
else:
markers = {}
......@@ -128,28 +128,8 @@ def dependency_from_pep_508(name):
dep.python_versions = " || ".join(ors)
if "sys_platform" in markers:
ors = []
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)
if req.marker:
dep.marker = req.marker
# 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):
def matches(self, provider):
raise NotImplementedError()
def allows_all(self, other):
raise NotImplementedError()
......@@ -14,5 +11,17 @@ class BaseConstraint(object):
def intersect(self, other):
raise NotImplementedError()
def union(self, other):
raise NotImplementedError()
def is_any(self):
return False
def is_empty(self):
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):
def difference(self, other):
return
def __eq__(self, other):
return other.is_empty()
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 .constraint import Constraint
class MultiConstraint(BaseConstraint):
def __init__(self, constraints, conjunctive=True):
self._constraints = tuple(constraints)
self._conjunctive = conjunctive
def __init__(self, *constraints):
if any(c.operator == "==" for c in constraints):
raise ValueError(
"A multi-constraint can only be comprised of negative constraints"
)
self._constraints = constraints
@property
def constraints(self):
return self._constraints
def is_conjunctive(self):
return self._conjunctive
def allows(self, other):
for constraint in self._constraints:
if not constraint.allows(other):
return False
def is_disjunctive(self):
return not self._conjunctive
return True
def matches(self, provider):
if self.is_disjunctive():
for constraint in self._constraints:
if constraint.matches(provider):
def allows_all(self, other):
if other.is_any():
return False
if other.is_empty():
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
for constraint in self._constraints:
if not constraint.matches(provider):
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 True
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((", " 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
from poetry.semver import VersionRange
from poetry.semver import VersionUnion
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.generic_constraint import GenericConstraint
from .constraints import parse_constraint as parse_generic_constraint
from .constraints.any_constraint import AnyConstraint
from .constraints.constraint import Constraint
from .constraints.multi_constraint import MultiConstraint
from .constraints.union_constraint import UnionConstraint
class Dependency(object):
......@@ -45,8 +49,6 @@ class Dependency(object):
self._python_versions = "*"
self._python_constraint = parse_constraint("*")
self._platform = "*"
self._platform_constraint = EmptyConstraint()
self._extras = []
self._in_extras = []
......@@ -54,6 +56,7 @@ class Dependency(object):
self._activated = not self._optional
self.is_root = False
self.marker = AnyMarker()
@property
def name(self):
......@@ -83,25 +86,20 @@ class Dependency(object):
def python_versions(self, value):
self._python_versions = 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
def python_constraint(self):
return self._python_constraint
@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
return self._extras
......@@ -152,9 +150,15 @@ class Dependency(object):
elif not self.constraint.is_any():
requirement += " ({})".format(str(self.constraint).replace(" ", ""))
# Markers
markers = []
if not self.marker.is_any():
marker = self.marker
if not with_extras:
marker = marker.without_extras()
if not marker.is_empty():
markers.append(str(marker))
else:
# Python marker
if self.python_versions != "*":
python_constraint = self.python_constraint
......@@ -163,17 +167,10 @@ class Dependency(object):
self._create_nested_marker("python_version", python_constraint)
)
if self.platform != "*":
platform_constraint = self.platform_constraint
markers.append(
self._create_nested_marker("sys_platform", platform_constraint)
)
in_extras = " || ".join(self._in_extras)
if in_extras and with_extras:
markers.append(
self._create_nested_marker("extra", GenericConstraint.parse(in_extras))
self._create_nested_marker("extra", parse_generic_constraint(in_extras))
)
if markers:
......@@ -186,17 +183,17 @@ class Dependency(object):
return requirement
def _create_nested_marker(self, name, constraint):
if isinstance(constraint, MultiConstraint):
if isinstance(constraint, (MultiConstraint, UnionConstraint)):
parts = []
for c in constraint.constraints:
multi = False
if isinstance(c, MultiConstraint):
if isinstance(c, (MultiConstraint, UnionConstraint)):
multi = True
parts.append((multi, self._create_nested_marker(name, c)))
glue = " and "
if constraint.is_disjunctive():
if isinstance(constraint, UnionConstraint):
parts = [
"({})".format(part[1]) if part[0] else part[1] for part in parts
]
......@@ -205,10 +202,8 @@ class Dependency(object):
parts = [part[1] for part in parts]
marker = glue.join(parts)
elif isinstance(constraint, GenericConstraint):
marker = '{} {} "{}"'.format(
name, constraint.string_operator, constraint.version
)
elif isinstance(constraint, Constraint):
marker = '{} {} "{}"'.format(name, constraint.operator, constraint.version)
elif isinstance(constraint, VersionUnion):
parts = []
for c in constraint.ranges:
......@@ -278,7 +273,6 @@ class Dependency(object):
new.is_root = self.is_root
new.python_versions = self.python_versions
new.platform = self.platform
for extra in self.extras:
new.extras.append(extra)
......
from .package import Package
class DependencyPackage:
class DependencyPackage(object):
def __init__(self, dependency, package):
self._dependency = dependency
self._package = package
......@@ -17,6 +14,12 @@ class DependencyPackage:
def __getattr__(self, 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):
return str(self._package)
......
......@@ -73,7 +73,6 @@ class DirectoryDependency(Dependency):
self._package.dev_requires += package.dev_requires
self._package.extras = package.extras
self._package.python_versions = package.python_versions
self._package.platform = package.platform
else:
# Execute egg_info
current_dir = os.getcwd()
......
......@@ -5,11 +5,11 @@ import poetry.repositories
from hashlib import sha256
from tomlkit import document
from tomlkit import inline_table
from typing import List
from poetry.utils._compat import Path
from poetry.utils.toml_file import TomlFile
from poetry.version.markers import parse_marker
class Locker:
......@@ -86,6 +86,22 @@ class Locker:
package.hashes = lock_data["metadata"]["hashes"][info["name"]]
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():
if isinstance(constraint, list):
for c in constraint:
......@@ -95,9 +111,6 @@ class Locker:
package.add_dependency(dep_name, constraint)
if "requirements" in info:
package.requirements = info["requirements"]
if "source" in info:
package.source_type = info["source"]["type"]
package.source_url = info["source"]["url"]
......@@ -129,7 +142,6 @@ class Locker:
lock["metadata"] = {
"python-versions": root.python_versions,
"platform": root.platform,
"content-hash": self._content_hash,
"hashes": hashes,
}
......@@ -198,9 +210,6 @@ class Locker:
if not dependency.python_constraint.is_any():
constraint["python"] = str(dependency.python_constraint)
if dependency.platform != "*":
constraint["platform"] = dependency.platform
if len(constraint) == 1:
dependencies[dependency.pretty_name].append(constraint["version"])
else:
......@@ -217,9 +226,10 @@ class Locker:
"category": package.category,
"optional": package.optional,
"python-versions": package.python_versions,
"platform": package.platform,
"hashes": sorted(package.hashes),
}
if not package.marker.is_any():
data["marker"] = str(package.marker)
if dependencies:
for k, constraints in dependencies.items():
......@@ -235,7 +245,4 @@ class Locker:
"reference": package.source_reference,
}
if package.requirements:
data["requirements"] = package.requirements
return data
......@@ -11,13 +11,15 @@ from poetry.spdx import license_by_id
from poetry.spdx import License
from poetry.utils._compat import Path
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.generic_constraint import GenericConstraint
from .constraints import parse_constraint as parse_generic_constraint
from .dependency import Dependency
from .directory_dependency import DirectoryDependency
from .file_dependency import FileDependency
from .vcs_dependency import VCSDependency
from .utils.utils import create_nested_marker
AUTHOR_REGEX = re.compile("(?u)^(?P<name>[- .,\w\d'’\"()]+)(?: <(?P<email>.+?)>)?$")
......@@ -63,15 +65,13 @@ class Package(object):
self.hashes = []
self.optional = False
# Requirements for making it mandatory
self.requirements = {}
self.classifiers = []
self._python_versions = "*"
self._python_constraint = parse_constraint("*")
self._platform = "*"
self._platform_constraint = EmptyConstraint()
self._python_marker = AnyMarker()
self.marker = AnyMarker()
self.root_dir = None
......@@ -153,19 +153,26 @@ class Package(object):
def python_versions(self, value):
self._python_versions = value
self._python_constraint = parse_constraint(value)
self._python_marker = parse_marker(
create_nested_marker("python_version", self._python_constraint)
)
@property
def python_constraint(self):
return self._python_constraint
@property
def python_marker(self):
return self._python_marker
@property
def platform(self): # type: () -> str
return self._platform
@platform.setter
def platform(self, value): # type: (str) -> None
self._platform = value
self._platform_constraint = GenericConstraint.parse(value)
self._platform_constraint = parse_generic_constraint(value)
@property
def platform_constraint(self):
......@@ -282,11 +289,28 @@ class Package(object):
allows_prereleases=allows_prereleases,
)
marker = AnyMarker()
if python_versions:
dependency.python_versions = python_versions
marker = marker.intersect(
parse_marker(
create_nested_marker(
"python_version", dependency.python_constraint
)
)
)
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:
for extra in constraint["extras"]:
......@@ -319,7 +343,6 @@ class Package(object):
clone.category = self.category
clone.optional = self.optional
clone.python_versions = self.python_versions
clone.platform = self.platform
clone.extras = self.extras
clone.source_type = self.source_type
clone.source_url = self.source_url
......
from poetry.semver import VersionRange
from poetry.semver import parse_constraint
from poetry.version.markers import parse_marker
from .package import Package
from .utils.utils import create_nested_marker
class ProjectPackage(Package):
......@@ -33,7 +35,11 @@ class ProjectPackage(Package):
@python_versions.setter
def python_versions(self, value):
self._python_versions = value
if value == "*" or value == VersionRange():
value = "~2.7 || >=3.4"
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
import posixpath
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:
import urllib.parse as urlparse
except ImportError:
......@@ -105,28 +114,27 @@ def splitext(path):
return base, ext
def group_markers(markers):
def group_markers(markers, or_=False):
groups = [[]]
for marker in markers:
assert isinstance(marker, (list, tuple, str))
if or_:
groups.append([])
if isinstance(marker, list):
groups[-1].append(group_markers(marker))
elif isinstance(marker, tuple):
lhs, op, rhs = marker
if isinstance(marker, (MultiMarker, MarkerUnion)):
groups[-1].append(
group_markers(marker.markers, isinstance(marker, MarkerUnion))
)
elif isinstance(marker, SingleMarker):
lhs, op, rhs = marker.name, marker.operator, marker.value
groups[-1].append((lhs.value, op, rhs.value))
else:
assert marker in ["and", "or"]
if marker == "or":
groups.append([])
groups[-1].append((lhs, op, rhs))
return groups
def convert_markers(markers):
groups = group_markers(markers)
def convert_markers(marker):
groups = group_markers([marker])
requirements = {}
......@@ -149,3 +157,70 @@ def convert_markers(markers):
_group(groups)
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):
def skip_reason(self): # type: () -> Union[str, None]
return self._skip_reason
@property
def package(self):
raise NotImplementedError()
def format_version(self, package): # type: (...) -> str
return package.full_pretty_version
......
......@@ -105,8 +105,27 @@ class Provider:
if dependency.is_root:
return PackageCollection(dependency, [self._package])
if dependency in self._search_for:
return PackageCollection(dependency, self._search_for[dependency])
for constraint in self._search_for.keys():
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():
packages = self.search_for_vcs(dependency)
......@@ -134,7 +153,7 @@ class Provider:
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]
"""
......@@ -316,41 +335,15 @@ class Provider:
if not package.python_constraint.allows_all(
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 (
not package.dependency.python_constraint.is_any()
and not package.python_constraint.intersect(
package.dependency.python_constraint.is_any()
or not self._package.python_constraint.allows_all(
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
)
raise CompatibilityError(
str(intersection),
str(self._package.python_constraint.difference(intersection)),
)
):
return [
Incompatibility(
[Term(package.to_dependency(), True)],
......@@ -360,22 +353,11 @@ class Provider:
)
]
if not self._package.platform_constraint.matches(
package.platform_constraint
):
return [
Incompatibility(
[Term(package.to_dependency(), True)],
PlatformCause(package.platform),
)
]
dependencies = [
dep
for dep in dependencies
if dep.name not in self.UNSAFE_PACKAGES
and self._package.python_constraint.allows_any(dep.python_constraint)
and self._package.platform_constraint.matches(dep.platform_constraint)
]
return [
......@@ -403,7 +385,6 @@ class Provider:
for r in package.requires
if r.is_activated()
and self._package.python_constraint.allows_any(r.python_constraint)
and self._package.platform_constraint.matches(r.platform_constraint)
]
# Searching for duplicate dependencies
......@@ -454,7 +435,7 @@ class Provider:
for constraint, _deps in by_constraint.items():
new_markers = []
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:
continue
......@@ -473,7 +454,7 @@ class Provider:
dep = _deps[0]
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)
if dep.is_optional() and not dep.is_activated():
......@@ -501,7 +482,7 @@ class Provider:
_deps = [value[0] for value in by_constraint.values()]
seen = set()
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:
_requirements = ""
else:
......
import time
from typing import Any
from typing import Dict
from typing import List
from poetry.mixology import resolve_version
from poetry.mixology.failure import SolveFailure
from poetry.packages import DependencyPackage
from poetry.packages.constraints.generic_constraint import GenericConstraint
from poetry.semver import parse_constraint
from poetry.version.markers import AnyMarker
from .exceptions import CompatibilityError
from .exceptions import SolverProblemError
......@@ -26,12 +29,25 @@ class Solver:
self._locked = locked
self._io = io
self._provider = Provider(self._package, self._pool, self._io)
self._branches = []
def solve(self, use_latest=None): # type: (...) -> List[Operation]
with self._provider.progress():
start = time.time()
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 = []
for package in packages:
......@@ -79,6 +95,7 @@ class Solver:
# since it actually doesn't matter since removals are always on top.
-depths[packages.index(o.package)] if o.job_type != "uninstall" else 0,
o.package.name,
o.package.version,
),
)
......@@ -104,15 +121,21 @@ class Solver:
packages.append(package)
depths.append(_depths[index])
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:
if dep not in current_package.requires:
current_package.requires.append(dep)
if dep not in pkg.requires:
pkg.requires.append(dep)
return packages, depths
def _solve(self, use_latest=None):
self._branches.append(self._package.python_versions)
locked = {}
for package in self._locked.packages:
locked[package.name] = DependencyPackage(package.to_dependency(), package)
......@@ -134,58 +157,37 @@ class Solver:
depths = []
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
)
depths.append(depth)
package.category = category
package.optional = optional
# 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
package.marker = marker
return packages, depths
def _build_graph(
self, package, packages, previous=None, previous_dep=None, dep=None
):
): # type: (...) -> Dict[str, Any]
if not previous:
category = "dev"
optional = True
python_version = "*"
platform = "*"
marker = package.marker
else:
category = dep.category
optional = dep.is_optional() and not dep.is_activated()
python_version = str(
parse_constraint(previous["python_version"]).intersect(
previous_dep.python_constraint
)
)
platform = str(
previous_dep.platform
if GenericConstraint.parse(previous["platform"]).matches(
previous_dep.platform_constraint
)
and previous_dep.platform != "*"
else previous["platform"]
)
intersection = previous["marker"].intersect(previous_dep.marker)
marker = intersection
graph = {
"name": package.name,
"category": category,
"optional": optional,
"python_version": python_version,
"platform": platform,
"children": [],
"marker": marker,
"children": [], # type: List[Dict[str, Any]]
}
if previous_dep and previous_dep is not dep and previous_dep.name == dep.name:
......@@ -237,10 +239,8 @@ class Solver:
)
if existing:
existing["python_version"] = str(
parse_constraint(existing["python_version"]).union(
parse_constraint(child_graph["python_version"])
)
existing["marker"] = existing["marker"].union(
child_graph["marker"]
)
continue
......@@ -251,37 +251,27 @@ class Solver:
def _get_tags_for_package(self, package, graph, depth=0):
categories = ["dev"]
optionals = [True]
python_versions = []
platforms = []
markers = []
_depths = [0]
children = graph["children"]
found = False
for child in children:
if child["name"] == package.name:
category = child["category"]
optional = child["optional"]
python_version = child["python_version"]
platform = child["platform"]
marker = child["marker"]
_depths.append(depth)
else:
(
category,
optional,
python_version,
platform,
_depth,
) = self._get_tags_for_package(package, child, depth=depth + 1)
(category, optional, marker, _depth) = self._get_tags_for_package(
package, child, depth=depth + 1
)
_depths.append(_depth)
categories.append(category)
optionals.append(optional)
if python_version is not None:
python_versions.append(python_version)
if platform is not None:
platforms.append(platform)
if not marker.is_any():
markers.append(marker)
if "main" in categories:
category = "main"
......@@ -290,37 +280,13 @@ class Solver:
optional = all(optionals)
if not python_versions:
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
depth = max(*(_depths + [0]))
if not platforms:
platform = None
if not markers:
marker = AnyMarker()
else:
platform = platforms[0]
for constraint in platforms[1:]:
previous = GenericConstraint.parse(platform)
current = GenericConstraint.parse(constraint)
if platform == "*":
continue
elif constraint == "*":
platform = constraint
elif current.matches(previous):
platform = constraint
depth = max(*(_depths + [0]))
marker = markers[0]
for m in markers[1:]:
marker = marker.union(m)
return category, optional, python_version, platform, depth
return category, optional, marker, depth
......@@ -168,6 +168,9 @@ class VersionUnion(VersionConstraint):
return True
while True:
if state["their_range"] is None:
break
if state["their_range"].is_strictly_lower(state["current"]):
if not their_next_range():
break
......
import json
import os
import platform
import subprocess
......@@ -8,6 +9,7 @@ import warnings
from contextlib import contextmanager
from subprocess import CalledProcessError
from typing import Any
from typing import Dict
from typing import Optional
from typing import Tuple
......@@ -15,6 +17,7 @@ from poetry.config import Config
from poetry.locations import CACHE_DIR
from poetry.utils._compat import Path
from poetry.utils._compat import decode
from poetry.version.markers import BaseMarker
class EnvError(Exception):
......@@ -47,8 +50,7 @@ class Env(object):
self._base = base or path
self._version_info = self.get_version_info()
self._python_implementation = self.get_python_implementation()
self._marker_env = None
@property
def path(self): # type: () -> Path
......@@ -60,11 +62,11 @@ class Env(object):
@property
def version_info(self): # type: () -> Tuple[int]
return self._version_info
return tuple(self.marker_env["version_info"])
@property
def python_implementation(self): # type: () -> str
return self._python_implementation
return self.marker_env["platform_python_implementation"]
@property
def python(self): # type: () -> str
......@@ -74,6 +76,13 @@ class Env(object):
return self._bin("python")
@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
"""
Path to current pip executable
......@@ -214,9 +223,15 @@ class Env(object):
def get_python_implementation(self): # type: () -> str
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()
def is_valid_for_marker(self, marker): # type: (BaseMarker) -> bool
return marker.validate(self.marker_env)
def run(self, bin, *args, **kwargs):
"""
Run a command inside the Python environment.
......@@ -262,7 +277,7 @@ class Env(object):
return str(bin_path)
def __repr__(self):
return '{}("{}")'.format(self.__class__.__name__, self._base)
return '{}("{}")'.format(self.__class__.__name__, self._path)
class SystemEnv(Env):
......@@ -276,6 +291,34 @@ class SystemEnv(Env):
def get_python_implementation(self): # type: () -> str
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
try:
return sysconfig.get_config_var(var)
......@@ -314,7 +357,7 @@ class VirtualEnv(Env):
" )"
')"',
shell=True,
)
).strip()
)
def get_version_info(self): # type: () -> Tuple[int]
......@@ -328,12 +371,35 @@ class VirtualEnv(Env):
return tuple([int(s) for s in output.strip().split(".")])
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",
"-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,
).strip()
)
return json.loads(output)
def config_var(self, var): # type: (str) -> Any
try:
......@@ -344,7 +410,7 @@ class VirtualEnv(Env):
"print(sysconfig.get_config_var('{}'))\"".format(var),
shell=True,
).strip()
except VenvCommandError as e:
except EnvCommandError as e:
warnings.warn("{0}".format(e), RuntimeWarning)
return None
......
......@@ -86,3 +86,8 @@ def get_http_basic_auth(repository_name): # type: (str) -> tuple
if repo_auth:
return repo_auth["username"], repo_auth["password"]
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
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"""
......@@ -171,7 +172,7 @@ VERSION_SPEC.setParseAction(lambda s, l, t: t[1])
MARKER_EXPR = originalTextFor(MARKER_EXPR())("marker")
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 = MARKER_SEPERATOR + MARKER_EXPR
......
......@@ -5,7 +5,6 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[[package]]
name = "B"
......@@ -14,7 +13,6 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[[package]]
name = "C"
......@@ -23,7 +21,6 @@ description = ""
category = "main"
optional = true
python-versions = "*"
platform = "*"
[package.dependencies]
D = "^1.0"
......@@ -35,14 +32,12 @@ description = ""
category = "main"
optional = true
python-versions = "*"
platform = "*"
[extras]
foo = ["C"]
[metadata]
python-versions = "*"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
......
......@@ -5,7 +5,6 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[[package]]
name = "B"
......@@ -14,7 +13,6 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[[package]]
name = "C"
......@@ -23,7 +21,6 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[[package]]
name = "D"
......@@ -32,14 +29,12 @@ description = ""
category = "main"
optional = true
python-versions = "*"
platform = "*"
[extras]
foo = ["D"]
[metadata]
python-versions = "*"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
......
......@@ -2,7 +2,6 @@ package = []
[metadata]
python-versions = "*"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
......@@ -5,11 +5,9 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[metadata]
python-versions = "*"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
......
......@@ -5,11 +5,9 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[metadata]
python-versions = "*"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
......
......@@ -5,7 +5,6 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[package.dependencies]
"B" = "^1.0"
......@@ -18,19 +17,15 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[[package]]
name = "C"
version = "1.1"
description = ""
category = "main"
marker = "python_version >= \"2.7\" and python_version < \"2.8\""
optional = false
python-versions = "*"
platform = "*"
[package.requirements]
python = ">=2.7,<2.8"
[[package]]
name = "D"
......@@ -39,11 +34,9 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[metadata]
python-versions = "*"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
......
......@@ -5,7 +5,6 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[[package]]
name = "B"
......@@ -14,14 +13,12 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[package.dependencies]
A = "^1.0"
[metadata]
python-versions = "*"
platform = "*"
content-hash = "123456789"
[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 = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[[package]]
name = "B"
......@@ -14,7 +13,6 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[package.dependencies]
C = "^1.0"
......@@ -26,11 +24,9 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[metadata]
python-versions = "*"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
......
......@@ -5,7 +5,6 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[[package]]
name = "B"
......@@ -14,11 +13,9 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[metadata]
python-versions = "*"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
......
......@@ -3,7 +3,6 @@ description = ""
category = "main"
name = "pendulum"
optional = false
platform = "*"
python-versions = "*"
version = "1.4.4"
......@@ -12,7 +11,6 @@ category = "main"
description = ""
name = "project-with-extras"
optional = false
platform = "*"
python-versions = "*"
version = "1.2.3"
......@@ -26,7 +24,6 @@ url = "tests/fixtures/project_with_extras"
[metadata]
content-hash = "123456789"
platform = "*"
python-versions = "*"
[metadata.hashes]
......
......@@ -5,7 +5,6 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[[package]]
name = "my-package"
......@@ -14,7 +13,6 @@ description = "Demo project."
category = "main"
optional = false
python-versions = "*"
platform = "*"
[package.source]
type = "directory"
......@@ -32,11 +30,9 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[metadata]
python-versions = "*"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
......
......@@ -5,7 +5,6 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[package.dependencies]
B = "^2.0"
......@@ -17,7 +16,6 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[package.dependencies]
C = "1.5"
......@@ -29,11 +27,9 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[metadata]
python-versions = "*"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
......
......@@ -5,7 +5,6 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[package.dependencies]
B = [
......@@ -18,58 +17,45 @@ name = "B"
version = "1.0"
description = ""
category = "main"
marker = "python_version < \"4.0\""
optional = false
python-versions = "*"
platform = "*"
[package.dependencies]
C = "1.2"
[package.requirements]
python = "<4.0"
[[package]]
name = "B"
version = "2.0"
description = ""
category = "main"
marker = "python_version >= \"4.0\""
optional = false
python-versions = "*"
platform = "*"
[package.dependencies]
C = "1.5"
[package.requirements]
python = ">=4.0"
[[package]]
name = "C"
version = "1.2"
description = ""
category = "main"
marker = "python_version < \"4.0\""
optional = false
python-versions = "*"
platform = "*"
[package.requirements]
python = "<4.0"
[[package]]
name = "C"
version = "1.5"
description = ""
category = "main"
marker = "python_version >= \"4.0\""
optional = false
python-versions = "*"
platform = "*"
[package.requirements]
python = ">=4.0"
[metadata]
python-versions = "*"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
......
......@@ -5,7 +5,6 @@ description = "Description"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
platform = "*"
[package.source]
type = "file"
......@@ -22,11 +21,9 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[metadata]
python-versions = "*"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
......
......@@ -5,41 +5,33 @@ description = ""
category = "main"
optional = true
python-versions = "*"
platform = "*"
[[package]]
name = "C"
version = "1.3"
description = ""
category = "main"
marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.4\" and python_version < \"4.0\""
optional = false
python-versions = "*"
platform = "*"
[package.dependencies]
D = "^1.2"
[package.requirements]
python = ">=2.7,<2.8 || >=3.4,<4.0"
[[package]]
name = "D"
version = "1.4"
description = ""
category = "main"
marker = "python_version >= \"2.7\" and python_version < \"2.8\" or python_version >= \"3.4\" and python_version < \"4.0\""
optional = false
python-versions = "*"
platform = "*"
[package.requirements]
python = ">=2.7,<2.8 || >=3.4,<4.0"
[extras]
foo = ["A"]
[metadata]
python-versions = "~2.7 || ^3.4"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
......
......@@ -5,53 +5,42 @@ description = ""
category = "main"
optional = true
python-versions = "*"
platform = "*"
[[package]]
name = "B"
version = "1.1"
description = ""
category = "main"
marker = "sys_platform == \"custom\""
optional = false
python-versions = "*"
platform = "*"
[package.requirements]
platform = "custom"
[[package]]
name = "C"
version = "1.3"
description = ""
category = "main"
marker = "sys_platform == \"darwin\""
optional = false
python-versions = "*"
platform = "*"
[package.dependencies]
D = "^1.2"
[package.requirements]
platform = "darwin"
[[package]]
name = "D"
version = "1.4"
description = ""
category = "main"
marker = "sys_platform == \"darwin\""
optional = false
python-versions = "*"
platform = "*"
[package.requirements]
platform = "darwin"
[extras]
foo = ["A"]
[metadata]
python-versions = "*"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
......
......@@ -5,7 +5,6 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[[package]]
name = "B"
......@@ -14,11 +13,9 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[metadata]
python-versions = "*"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
......
......@@ -5,31 +5,24 @@ description = "Classes Without Boilerplate"
category = "dev"
optional = false
python-versions = "*"
platform = "*"
[[package]]
name = "colorama"
version = "0.3.9"
description = "Cross-platform colored terminal text."
category = "dev"
marker = "sys_platform == \"win32\""
optional = false
python-versions = "*"
platform = "UNKNOWN"
[package.requirements]
platform = "win32"
[[package]]
name = "funcsigs"
version = "1.0.2"
description = "Python function signatures from PEP362 for Python 2.6, 2.7 and 3.2+"
category = "dev"
marker = "python_version < \"3.0\""
optional = false
python-versions = "*"
platform = "UNKNOWN"
[package.requirements]
python = "<3.0"
[[package]]
name = "more-itertools"
......@@ -38,7 +31,6 @@ description = "More routines for operating on iterables, beyond itertools"
category = "dev"
optional = false
python-versions = "*"
platform = "*"
[package.dependencies]
six = ">=1.0.0,<2.0.0"
......@@ -50,7 +42,6 @@ description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
platform = "unix"
[[package]]
name = "py"
......@@ -59,7 +50,6 @@ description = "library with cross-python path, ini-parsing, io, code, log facili
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
platform = "unix"
[[package]]
name = "pytest"
......@@ -68,7 +58,6 @@ description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
platform = "unix"
[package.dependencies]
py = ">=1.5.0"
......@@ -78,7 +67,7 @@ attrs = ">=17.4.0"
more-itertools = ">=4.0.0"
pluggy = ">=0.5,<0.7"
funcsigs = {"version" = "*", "python" = "<3.0"}
colorama = {"version" = "*", "platform" = "win32"}
colorama = "*"
[[package]]
name = "six"
......@@ -87,11 +76,9 @@ description = "Python 2 and 3 compatibility utilities"
category = "dev"
optional = false
python-versions = "*"
platform = "*"
[metadata]
python-versions = "*"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
......
......@@ -5,7 +5,6 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[[package]]
name = "B"
......@@ -14,7 +13,6 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[[package]]
name = "C"
......@@ -23,11 +21,9 @@ description = ""
category = "main"
optional = false
python-versions = "~2.7 || ^3.3"
platform = "*"
[metadata]
python-versions = "~2.7 || ^3.4"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
......
......@@ -5,7 +5,6 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[package.dependencies]
D = "^1.0"
......@@ -17,7 +16,6 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[package.dependencies]
C = "~1.2"
......@@ -29,7 +27,6 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[[package]]
name = "D"
......@@ -38,11 +35,9 @@ description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[metadata]
python-versions = "*"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
......
......@@ -63,10 +63,8 @@ class Locker(BaseLocker):
def _write_lock_data(self, data):
for package in data["package"]:
python_versions = str(package["python-versions"])
platform = str(package["platform"])
if PY2:
python_versions = python_versions.decode()
platform = platform.decode()
if "requirements" in package:
requirements = {}
for key, value in package["requirements"].items():
......@@ -75,7 +73,6 @@ class Locker(BaseLocker):
package["requirements"] = requirements
package["python-versions"] = python_versions
package["platform"] = platform
self._written_data = data
......@@ -1143,3 +1140,34 @@ def test_run_install_duplicate_dependencies_different_constraints_with_lock_upda
assert len(updates) == 1
removals = installer.installer.removals
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():
main = ["B>=1.0,<1.1"]
extra_python = (
':(python_version >= "2.7" and python_version < "2.8") '
'or (python_version >= "3.6" and python_version < "4.0")'
':python_version >= "2.7" and python_version < "2.8" '
'or python_version >= "3.6" and python_version < "4.0"'
)
extra_d_dependency = (
'baz:(python_version >= "2.7" and python_version < "2.8") '
'or (python_version >= "3.4" and python_version < "4.0")'
'baz:python_version >= "2.7" and python_version < "2.8" '
'or python_version >= "3.4" and python_version < "4.0"'
)
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():
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")'
)
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")'
'python_version >= "2.7" and python_version < "2.8" '
'or python_version >= "3.6" and python_version < "4.0"'
)
......@@ -112,8 +97,8 @@ def test_to_pep_508_in_extras():
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")'
'python_version >= "2.7" and python_version < "2.8" '
'or python_version >= "3.6" and python_version < "4.0"'
") "
'and (extra == "foo" or extra == "bar")'
)
......@@ -13,7 +13,6 @@ def test_file_dependency_wheel():
assert dependency.name == "demo"
assert dependency.pretty_constraint == "0.1.0"
assert dependency.python_versions == "*"
assert dependency.platform == "*"
meta = dependency.metadata
assert meta.requires_dist == ["pendulum (>=1.4.0.0,<2.0.0.0)"]
......@@ -26,7 +25,6 @@ def test_file_dependency_sdist():
assert dependency.name == "demo"
assert dependency.pretty_constraint == "0.1.0"
assert dependency.python_versions == "*"
assert dependency.platform == "*"
meta = dependency.metadata
assert meta.requires_dist == ["pendulum (>=1.4.0.0,<2.0.0.0)"]
......
......@@ -37,7 +37,6 @@ category = "main"
description = ""
name = "A"
optional = false
platform = "*"
python-versions = "*"
version = "1.0.0"
......@@ -49,13 +48,11 @@ category = "main"
description = ""
name = "B"
optional = false
platform = "*"
python-versions = "*"
version = "1.2"
[metadata]
content-hash = "115cf985d932e9bf5f540555bbdd75decbb62cac81e399375fc19f6277f8c1d8"
platform = "*"
python-versions = "*"
[metadata.hashes]
......
......@@ -40,37 +40,40 @@ def test_dependency_from_pep_508_with_extras():
assert dep.name == "requests"
assert str(dep.constraint) == "2.18.0"
assert dep.in_extras == ["foo", "bar"]
assert str(dep.marker) == 'extra == "foo" or extra == "bar"'
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)
assert dep.name == "requests"
assert str(dep.constraint) == "2.18.0"
assert dep.extras == []
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():
name = "requests (==2.18.0); " 'python_version == "2.7"'
name = 'requests (==2.18.0); python_version == "2.7"'
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"
assert str(dep.marker) == 'python_version == "2.7"'
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)
assert dep.name == "requests"
assert str(dep.constraint) == "2.18.0"
assert dep.extras == []
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():
......@@ -86,34 +89,40 @@ def test_dependency_from_pep_508_complex():
assert str(dep.constraint) == "2.18.0"
assert dep.in_extras == ["foo"]
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():
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)
assert dep.name == "requests"
assert str(dep.constraint) == "2.18.0"
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():
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)
assert dep.name == "requests"
assert str(dep.constraint) == "2.18.0"
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():
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)
assert dep.name == "requests"
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():
......@@ -125,3 +134,21 @@ def test_dependency_with_extra():
assert len(dep.extras) == 1
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
from cleo.outputs.null_output import NullOutput
from cleo.styles import OutputStyle
from poetry.packages import dependency_from_pep_508
from poetry.packages import ProjectPackage
from poetry.repositories.installed_repository import InstalledRepository
from poetry.repositories.pool import Pool
......@@ -12,7 +13,6 @@ from poetry.puzzle.exceptions import SolverProblemError
from tests.helpers import get_dependency
from tests.helpers import get_package
from tests.repositories.test_pypi_repository import MockRepository
@pytest.fixture()
......@@ -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):
package.add_dependency("A")
package.add_dependency("B")
......@@ -537,7 +507,7 @@ def test_solver_sub_dependencies_with_requirements(solver, repo, package):
)
op = ops[1]
assert op.package.requirements == {}
assert op.package.marker.is_any()
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
assert op.package.requirements == {"python": "<4.0"}
assert str(op.package.marker) == 'python_version < "4.0"'
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
assert op.package.requirements == {"python": "<5.0"}
assert str(op.package.marker) == 'python_version < "5.0"'
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(
......@@ -785,7 +758,9 @@ def test_solver_duplicate_dependencies_same_constraint(solver, repo, package):
)
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):
......@@ -814,10 +789,10 @@ def test_solver_duplicate_dependencies_different_constraints(solver, repo, packa
)
op = ops[0]
assert op.package.requirements == {"python": "<3.4"}
assert str(op.package.marker) == 'python_version < "3.4"'
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(
......@@ -882,10 +857,10 @@ def test_solver_duplicate_dependencies_sub_dependencies(solver, repo, package):
)
op = ops[2]
assert op.package.requirements == {"python": "<3.4"}
assert str(op.package.marker) == 'python_version < "3.4"'
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):
......@@ -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(
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_a = get_package("A", "1.0.0")
......@@ -978,3 +953,123 @@ def test_solver_does_not_trigger_conflict_for_python_constraint_if_python_requir
ops = solver.solve()
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():
win_inet = package.extras["socks"][0]
assert win_inet.name == "win-inet-pton"
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():
......
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