Commit 7ae7e2b7 by Sébastien Eustace

Fix python/platform constraints not being picked up for subdependencies

parent d3491fe7
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
- Fixed handling of post releases. - Fixed handling of post releases.
- Fixed python restricted dependencies not being checked agaisnt virtualenv version. - Fixed python restricted dependencies not being checked agaisnt virtualenv version.
- Fixed python/platform constraint not being picked up for subdependencies.
## [0.4.2] - 2018-03-10 ## [0.4.2] - 2018-03-10
......
from poetry.installation import Installer from poetry.installation import Installer
from poetry.repositories.pypi_repository import PyPiRepository
from .command import Command from .command import Command
...@@ -9,7 +8,6 @@ class LockCommand(Command): ...@@ -9,7 +8,6 @@ class LockCommand(Command):
Locks the project dependencies. Locks the project dependencies.
lock lock
{ --no-dev : Do not install dev dependencies. }
""" """
help = """The <info>lock</info> command reads the <comment>poetry.toml</> file from help = """The <info>lock</info> command reads the <comment>poetry.toml</> file from
......
...@@ -5,6 +5,7 @@ from typing import List ...@@ -5,6 +5,7 @@ from typing import List
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.platform_constraint import PlatformConstraint
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
...@@ -114,7 +115,7 @@ class Installer: ...@@ -114,7 +115,7 @@ class Installer:
def _do_install(self, local_repo): def _do_install(self, local_repo):
locked_repository = Repository() locked_repository = Repository()
# initialize locked repo if we are installing from lock # initialize locked repo if we are installing from lock
if not self._update or self._locker.is_locked(): if not self._update:
locked_repository = self._locker.locked_repository(True) locked_repository = self._locker.locked_repository(True)
if self._update: if self._update:
...@@ -131,6 +132,9 @@ class Installer: ...@@ -131,6 +132,9 @@ class Installer:
if self._whitelist: if self._whitelist:
# collect packages to fixate from root requirements # collect packages to fixate from root requirements
candidates = [] candidates = []
if self._locker.is_locked():
locked_repository = self._locker.locked_repository(True)
for package in locked_repository.packages: for package in locked_repository.packages:
candidates.append(package) candidates.append(package)
...@@ -146,6 +150,8 @@ class Installer: ...@@ -146,6 +150,8 @@ class Installer:
Dependency(candidate.name, candidate.version) Dependency(candidate.name, candidate.version)
) )
locked_repository = Repository()
solver = Solver( solver = Solver(
self._package, self._package,
self._pool, self._pool,
...@@ -389,6 +395,17 @@ class Installer: ...@@ -389,6 +395,17 @@ class Installer:
op.skip('Not needed for the current python version') op.skip('Not needed for the current python version')
continue continue
if 'platform' in package.requirements:
platform_constraint = PlatformConstraint.parse(
package.requirements['platform']
)
if not platform_constraint.matches(
PlatformConstraint('=', sys.platform)
):
# Incompatible systems
op.skip('Not need 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():
......
import operator
import re
from poetry.semver.constraints import EmptyConstraint
from poetry.semver.constraints import MultiConstraint
from poetry.semver.constraints.base_constraint import BaseConstraint
class PlatformConstraint(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, operator, platform):
if operator not in self._trans_op_str:
raise ValueError(
f'Invalid operator "{operator}" given, '
f'expected one of: {", ".join(self.supported_operators)}'
)
self._operator = self._trans_op_str[operator]
self._string_operator = operator
self._platform = platform
@property
def supported_operators(self) -> list:
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 platform(self) -> str:
return self._platform
def matches(self, provider):
if not isinstance(provider, PlatformConstraint):
raise ValueError(
'Platform constraints can only be compared with each other'
)
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._platform == provider.platform
if (
is_equal_op and is_provider_non_equal_op
or is_non_equal_op and is_provider_equal_op
):
return self._platform != provider.platform
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 PlatformConstraint(m.group(1) or '=', m.group(2)),
raise ValueError(
'Could not parse platform constraint: {}'.format(constraint)
)
def __str__(self):
op = self._trans_op_int[self._operator]
if op == '==':
op = ''
else:
op = op + ' '
return '{}{}'.format(
op,
self._platform
)
def __repr__(self):
return '<PlatformConstraint \'{}\'>'.format(str(self))
import poetry.packages import poetry.packages
from poetry.semver.constraints import Constraint from poetry.semver.constraints import Constraint
from poetry.semver.constraints import EmptyConstraint
from poetry.semver.constraints import MultiConstraint from poetry.semver.constraints import MultiConstraint
from poetry.semver.constraints.base_constraint import BaseConstraint
from poetry.semver.version_parser import VersionParser from poetry.semver.version_parser import VersionParser
from .constraints.platform_constraint import PlatformConstraint
class Dependency: class Dependency:
...@@ -31,7 +33,7 @@ class Dependency: ...@@ -31,7 +33,7 @@ class Dependency:
self._python_versions = '*' self._python_versions = '*'
self._python_constraint = self._parser.parse_constraints('*') self._python_constraint = self._parser.parse_constraints('*')
self._platform = '*' self._platform = '*'
self._platform_constraint = self._parser.parse_constraints('*') self._platform_constraint = EmptyConstraint()
self._extras = [] self._extras = []
...@@ -75,6 +77,7 @@ class Dependency: ...@@ -75,6 +77,7 @@ class Dependency:
@platform.setter @platform.setter
def platform(self, value: str): def platform(self, value: str):
self._platform = value self._platform = value
self._platform_constraint = PlatformConstraint.parse(value)
@property @property
def platform_constraint(self): def platform_constraint(self):
...@@ -150,6 +153,12 @@ class Dependency: ...@@ -150,6 +153,12 @@ class Dependency:
""" """
self._optional = False self._optional = False
def deactivate(self):
"""
Set the dependency as optional.
"""
self._optional = True
def __eq__(self, other): def __eq__(self, other):
if not isinstance(other, Dependency): if not isinstance(other, Dependency):
return NotImplemented return NotImplemented
......
...@@ -95,6 +95,9 @@ class Locker: ...@@ -95,6 +95,9 @@ class Locker:
for dep_name, constraint in info.get('dependencies', {}).items(): for dep_name, constraint in info.get('dependencies', {}).items():
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']
......
...@@ -2,10 +2,12 @@ import re ...@@ -2,10 +2,12 @@ import re
from typing import Union from typing import Union
from poetry.semver.constraints import Constraint from poetry.semver.constraints import Constraint
from poetry.semver.constraints import EmptyConstraint
from poetry.semver.helpers import parse_stability from poetry.semver.helpers import parse_stability
from poetry.semver.version_parser import VersionParser from poetry.semver.version_parser import VersionParser
from poetry.version import parse as parse_version from poetry.version import parse as parse_version
from.constraints.platform_constraint import PlatformConstraint
from .dependency import Dependency from .dependency import Dependency
from .vcs_dependency import VCSDependency from .vcs_dependency import VCSDependency
...@@ -93,7 +95,7 @@ class Package: ...@@ -93,7 +95,7 @@ class Package:
self._python_versions = '*' self._python_versions = '*'
self._python_constraint = self._parser.parse_constraints('*') self._python_constraint = self._parser.parse_constraints('*')
self._platform = '*' self._platform = '*'
self._platform_constraint = self._parser.parse_constraints('*') self._platform_constraint = EmptyConstraint()
@property @property
def name(self): def name(self):
...@@ -180,7 +182,7 @@ class Package: ...@@ -180,7 +182,7 @@ class Package:
@platform.setter @platform.setter
def platform(self, value: str): def platform(self, value: str):
self._platform = value self._platform = value
self._platform_constraint = self._parser.parse_constraints(value) self._platform_constraint = PlatformConstraint.parse(value)
@property @property
def platform_constraint(self): def platform_constraint(self):
......
...@@ -24,7 +24,7 @@ class Solver: ...@@ -24,7 +24,7 @@ class Solver:
self._locked = locked self._locked = locked
self._io = io self._io = io
def solve(self, requested, fixed=None, extras=None) -> List[Operation]: def solve(self, requested, fixed=None) -> List[Operation]:
resolver = Resolver(Provider(self._package, self._pool), UI(self._io)) resolver = Resolver(Provider(self._package, self._pool), UI(self._io))
base = None base = None
...@@ -69,7 +69,7 @@ class Solver: ...@@ -69,7 +69,7 @@ class Solver:
if current.matches(previous): if current.matches(previous):
requirements['python'] = req requirements['python'] = req
if 'platform' in req: if req_name == 'platform':
if 'platform' not in requirements: if 'platform' not in requirements:
requirements['platform'] = req requirements['platform'] = req
continue continue
...@@ -131,6 +131,14 @@ class Solver: ...@@ -131,6 +131,14 @@ class Solver:
break break
else: else:
for edge in vertex.incoming_edges: for edge in vertex.incoming_edges:
for req in edge.origin.payload.requires:
if req.name == vertex.payload.name:
if req.python_versions != '*':
tags['requirements']['python'].append(req.python_versions)
if req.platform != '*':
tags['requirements']['platform'].append(req.platform)
sub_tags = self._get_tags_for_vertex(edge.origin, requested) sub_tags = self._get_tags_for_vertex(edge.origin, requested)
tags['category'] += sub_tags['category'] tags['category'] += sub_tags['category']
......
...@@ -99,11 +99,9 @@ class PyPiRepository(Repository): ...@@ -99,11 +99,9 @@ class PyPiRepository(Repository):
dependency = Dependency( dependency = Dependency(
name, name,
version, version
optional=req.markers
) )
is_extra = False
if req.markers: if req.markers:
# Setting extra dependencies and requirements # Setting extra dependencies and requirements
requirements = self._convert_markers( requirements = self._convert_markers(
...@@ -126,6 +124,9 @@ class PyPiRepository(Repository): ...@@ -126,6 +124,9 @@ class PyPiRepository(Repository):
for or_ in requirements['sys_platform']: for or_ in requirements['sys_platform']:
ands = [] ands = []
for op, platform in or_: for op, platform in or_:
if op == '==':
op = ''
ands.append(f'{op}{platform}') ands.append(f'{op}{platform}')
ors.append(' '.join(ands)) ors.append(' '.join(ands))
...@@ -133,7 +134,7 @@ class PyPiRepository(Repository): ...@@ -133,7 +134,7 @@ class PyPiRepository(Repository):
dependency.platform = ' || '.join(ors) dependency.platform = ' || '.join(ors)
if 'extra' in requirements: if 'extra' in requirements:
is_extra = True dependency.deactivate()
for _extras in requirements['extra']: for _extras in requirements['extra']:
for _, extra in _extras: for _, extra in _extras:
if extra not in package.extras: if extra not in package.extras:
...@@ -141,7 +142,7 @@ class PyPiRepository(Repository): ...@@ -141,7 +142,7 @@ class PyPiRepository(Repository):
package.extras[extra].append(dependency) package.extras[extra].append(dependency)
if not is_extra: if not dependency.is_optional():
package.requires.append(dependency) package.requires.append(dependency)
# Adding description # Adding description
......
[[package]]
name = "A"
version = "1.0"
description = ""
category = "main"
optional = true
python-versions = "*"
platform = "*"
[[package]]
name = "B"
version = "1.1"
description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[package.requirements]
platform = "win32"
[[package]]
name = "C"
version = "1.3"
description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[package.dependencies]
D = "^1.2"
[package.requirements]
platform = "darwin"
[[package]]
name = "D"
version = "1.4"
description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[package.requirements]
platform = "darwin"
[metadata]
python-versions = "*"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
A = []
B = []
C = []
D = []
...@@ -62,14 +62,17 @@ class Locker(BaseLocker): ...@@ -62,14 +62,17 @@ class Locker(BaseLocker):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def setup(): def setup():
# Mock python version to get reliable tests # Mock python version and platform to get reliable tests
original = sys.version_info original = sys.version_info
original_platform = sys.platform
sys.version_info = (3, 6, 3, 'final', 0) sys.version_info = (3, 6, 3, 'final', 0)
sys.platform = 'darwin'
yield yield
sys.version_info = original sys.version_info = original
sys.platform = original_platform
@pytest.fixture() @pytest.fixture()
...@@ -305,6 +308,38 @@ def test_run_with_optional_and_python_restricted_dependencies(installer, locker, ...@@ -305,6 +308,38 @@ def test_run_with_optional_and_python_restricted_dependencies(installer, locker,
assert installer.installs[1].name == 'c' assert installer.installs[1].name == 'c'
def test_run_with_optional_and_platform_restricted_dependencies(installer, locker, repo, package):
package_a = get_package('A', '1.0')
package_b = get_package('B', '1.1')
package_c12 = get_package('C', '1.2')
package_c13 = get_package('C', '1.3')
package_d = get_package('D', '1.4')
package_c13.add_dependency('D', '^1.2')
repo.add_package(package_a)
repo.add_package(package_b)
repo.add_package(package_c12)
repo.add_package(package_c13)
repo.add_package(package_d)
package.add_dependency('A', {'version': '~1.0', 'optional': True})
package.add_dependency('B', {'version': '^1.0', 'platform': 'win32'})
package.add_dependency('C', {'version': '^1.0', 'platform': 'darwin'})
installer.run()
expected = fixture('with-platform-dependencies')
assert locker.written_data == expected
installer = installer.installer
# We should only have 2 installs:
# C,D since the mocked python version is not compatible
# with B's python constraint and A is optional
assert len(installer.installs) == 2
assert installer.installs[0].name == 'd'
assert installer.installs[1].name == 'c'
def test_run_with_dependencies_extras(installer, locker, repo, package): def test_run_with_dependencies_extras(installer, locker, repo, package):
package_a = get_package('A', '1.0') package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0') package_b = get_package('B', '1.0')
......
...@@ -39,4 +39,4 @@ def test_package(): ...@@ -39,4 +39,4 @@ 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 win_inet.platform == 'win32'
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