Commit 2367ac9a by Sébastien Eustace

Respect dependencies system constraints when installing

parent 90153299
...@@ -4,15 +4,19 @@ ...@@ -4,15 +4,19 @@
### Added ### Added
- Add packaging support (sdist and pure-python wheel). - Added packaging support (sdist and pure-python wheel).
- Add the `build` command. - Added the `build` command.
### Changes
- Dependencies system constraints are now respected when installing packages.
## [0.3.0] - 2018-03-05 ## [0.3.0] - 2018-03-05
### Added ### Added
- Add `show` command. - Added `show` command.
- Added the `--dry-run` option to the `add` command. - Added the `--dry-run` option to the `add` command.
### Changed ### Changed
......
import sys
from typing import List from typing import List
from poetry.packages import Dependency from poetry.packages import Dependency
...@@ -11,6 +13,8 @@ from poetry.puzzle.operations.operation import Operation ...@@ -11,6 +13,8 @@ from poetry.puzzle.operations.operation import Operation
from poetry.repositories import Pool from poetry.repositories import Pool
from poetry.repositories import Repository from poetry.repositories import Repository
from poetry.repositories.installed_repository import InstalledRepository from poetry.repositories.installed_repository import InstalledRepository
from poetry.semver.constraints import Constraint
from poetry.semver.version_parser import VersionParser
from .base_installer import BaseInstaller from .base_installer import BaseInstaller
from .pip_installer import PipInstaller from .pip_installer import PipInstaller
...@@ -39,6 +43,10 @@ class Installer: ...@@ -39,6 +43,10 @@ class Installer:
self._installer = self._get_installer() self._installer = self._get_installer()
@property
def installer(self):
return self._installer
def run(self): def run(self):
# Force update if there is no lock file present # Force update if there is no lock file present
if not self._update and not self._locker.is_locked(): if not self._update and not self._locker.is_locked():
...@@ -156,6 +164,10 @@ class Installer: ...@@ -156,6 +164,10 @@ class Installer:
self._populate_local_repo(local_repo, ops, locked_repository) self._populate_local_repo(local_repo, ops, locked_repository)
# We need to filter operations so that packages
# not compatible with the current system are dropped
self._filter_operations(ops)
self._io.new_line() self._io.new_line()
# Execute operations # Execute operations
...@@ -215,6 +227,15 @@ class Installer: ...@@ -215,6 +227,15 @@ class Installer:
getattr(self, f'_execute_{method}')(operation) getattr(self, f'_execute_{method}')(operation)
def _execute_install(self, operation: Install) -> None: def _execute_install(self, operation: Install) -> None:
if operation.skipped:
if self._io.is_verbose() and (self._execute_operations or self.is_dry_run()):
self._io.writeln(
f' - Skipping <info>{operation.package.name}</> '
f'(<comment>{operation.package.full_pretty_version}</>) '
f'{operation.skip_reason}')
return
if self._execute_operations or self.is_dry_run(): if self._execute_operations or self.is_dry_run():
self._io.writeln( self._io.writeln(
f' - Installing <info>{operation.package.name}</> ' f' - Installing <info>{operation.package.name}</> '
...@@ -230,6 +251,15 @@ class Installer: ...@@ -230,6 +251,15 @@ class Installer:
source = operation.initial_package source = operation.initial_package
target = operation.target_package target = operation.target_package
if operation.skipped:
if self._io.is_verbose() and (self._execute_operations or self.is_dry_run()):
self._io.writeln(
f' - Skipping <info>{target.name}</> '
f'(<comment>{target.full_pretty_version}</>) '
f'{operation.skip_reason}')
return
if self._execute_operations or self.is_dry_run(): if self._execute_operations or self.is_dry_run():
self._io.writeln( self._io.writeln(
f' - Updating <info>{target.name}</> ' f' - Updating <info>{target.name}</> '
...@@ -307,5 +337,25 @@ class Installer: ...@@ -307,5 +337,25 @@ class Installer:
return ops return ops
def _filter_operations(self, ops: List[Operation]):
for op in ops:
if isinstance(op, Update):
package = op.target_package
else:
package = op.package
if not package.requirements or op.job_type == 'uninstall':
continue
parser = VersionParser()
python = '.'.join([str(i) for i in sys.version_info[:3]])
if 'python' in package.requirements:
python_constraint = parser.parse_constraints(
package.requirements['python']
)
if not python_constraint.matches(Constraint('=', python)):
# Incompatible python versions
op.skip('Not needed for the current python version')
def _get_installer(self) -> BaseInstaller: def _get_installer(self) -> BaseInstaller:
return PipInstaller(self._io.venv, self._io) return PipInstaller(self._io.venv, self._io)
...@@ -3,11 +3,28 @@ from .base_installer import BaseInstaller ...@@ -3,11 +3,28 @@ from .base_installer import BaseInstaller
class NoopInstaller(BaseInstaller): class NoopInstaller(BaseInstaller):
def __init__(self):
self._installs = []
self._updates = []
self._removals = []
@property
def installs(self):
return self._installs
@property
def updates(self):
return self._updates
@property
def removals(self):
return self._removals
def install(self, package) -> None: def install(self, package) -> None:
pass self._installs.append(package)
def update(self, source, target) -> None: def update(self, source, target) -> None:
pass self._updates.append((source, target))
def remove(self, package) -> None: def remove(self, package) -> None:
pass self._removals.append(package)
...@@ -196,5 +196,8 @@ class Locker: ...@@ -196,5 +196,8 @@ class Locker:
'reference': package.source_reference 'reference': package.source_reference
} }
if package.requirements:
data['requirements'] = package.requirements
return data return data
from poetry.semver.constraints.base_constraint import BaseConstraint from typing import Union
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
...@@ -69,6 +70,9 @@ class Package: ...@@ -69,6 +70,9 @@ class Package:
self.hashes = [] self.hashes = []
self.optional = False self.optional = False
# Requirements for making it mandatory
self.requirements = {}
self._python_versions = '*' self._python_versions = '*'
self._python_constraint = self._parser.parse_constraints('*') self._python_constraint = self._parser.parse_constraints('*')
self._platform = '*' self._platform = '*'
...@@ -146,25 +150,57 @@ class Package: ...@@ -146,25 +150,57 @@ class Package:
def is_prerelease(self): def is_prerelease(self):
return self._stability != 'stable' return self._stability != 'stable'
def add_dependency(self, name, constraint=None, category='main'): def add_dependency(self,
name: str,
constraint: Union[str, dict, None] = None,
category: str = 'main') -> Dependency:
if constraint is None: if constraint is None:
constraint = '*' constraint = '*'
if isinstance(constraint, dict): if isinstance(constraint, dict):
if 'git' in constraint: if 'git' in constraint:
# VCS dependency # VCS dependency
optional = constraint.get('optional', False)
python_versions = constraint.get('python')
platform = constraint.get('platform')
optional = optional or python_versions is not None or platform is not None
dependency = VCSDependency( dependency = VCSDependency(
name, name,
'git', constraint['git'], 'git', constraint['git'],
branch=constraint.get('branch', None), branch=constraint.get('branch', None),
tag=constraint.get('tag', None), tag=constraint.get('tag', None),
rev=constraint.get('rev', None), rev=constraint.get('rev', None),
optional=constraint.get('optional', None), optional=optional,
) )
if python_versions:
dependency.python_versions = python_versions
if platform:
dependency.platform = platform
else: else:
version = constraint['version'] version = constraint['version']
optional = constraint.get('optional', False) optional = constraint.get('optional', False)
dependency = Dependency(name, version, optional=optional, category=category) allows_prereleases = constraint.get('allows_prereleases', False)
python_versions = constraint.get('python')
platform = constraint.get('platform')
optional = optional or python_versions is not None or not platform is not None
dependency = Dependency(
name, version,
optional=optional,
category=category,
allows_prereleases=allows_prereleases
)
if python_versions:
dependency.python_versions = python_versions
if platform:
dependency.platform = platform
else: else:
dependency = Dependency(name, constraint, category=category) dependency = Dependency(name, constraint, category=category)
......
...@@ -20,7 +20,9 @@ class VCSDependency(Dependency): ...@@ -20,7 +20,9 @@ class VCSDependency(Dependency):
self._tag = tag self._tag = tag
self._rev = rev self._rev = rev
super(VCSDependency, self).__init__(name, '*', optional=optional) super().__init__(
name, '*', optional=optional, allows_prereleases=True
)
@property @property
def vcs(self) -> str: def vcs(self) -> str:
......
...@@ -6,13 +6,28 @@ class Operation: ...@@ -6,13 +6,28 @@ class Operation:
def __init__(self, reason: str = None) -> None: def __init__(self, reason: str = None) -> None:
self._reason = reason self._reason = reason
self._skipped = False
self._skip_reason = None
@property @property
def job_type(self): def job_type(self) -> str:
raise NotImplementedError raise NotImplementedError
@property @property
def reason(self) -> str: def reason(self) -> str:
return self._reason return self._reason
def format_version(self, package): @property
def skipped(self) -> bool:
return self._skipped
@property
def skip_reason(self):
return self._skip_reason
def format_version(self, package) -> str:
return package.full_pretty_version return package.full_pretty_version
def skip(self, reason: str) -> None:
self._skipped = True
self._skip_reason = reason
...@@ -40,12 +40,18 @@ class Solver: ...@@ -40,12 +40,18 @@ class Solver:
# Setting categories # Setting categories
for vertex in graph.vertices.values(): for vertex in graph.vertices.values():
tags = self._get_categories_for_vertex(vertex, requested) tags = self._get_tags_for_vertex(vertex, requested)
if 'main' in tags: if 'main' in tags['category']:
vertex.payload.category = 'main' vertex.payload.category = 'main'
else: else:
vertex.payload.category = 'dev' vertex.payload.category = 'dev'
if not tags['optional']:
vertex.payload.optional = False
else:
vertex.payload.optional = True
vertex.payload.requirements = tags['requirements']
operations = [] operations = []
for package in packages: for package in packages:
installed = False installed = False
...@@ -74,15 +80,34 @@ class Solver: ...@@ -74,15 +80,34 @@ class Solver:
return list(reversed(operations)) return list(reversed(operations))
def _get_categories_for_vertex(self, vertex, requested): def _get_tags_for_vertex(self, vertex, requested):
tags = [] tags = {
'category': [],
'optional': True,
'requirements': {}
}
if not vertex.incoming_edges: if not vertex.incoming_edges:
# Original dependency # Original dependency
for req in requested: for req in requested:
if req.name == vertex.name: if req.name == vertex.name:
tags.append(req.category) tags['category'].append(req.category)
if not req.is_optional():
tags['optional'] = False
if req.is_optional():
# Checking installation requirements
if req.python_versions != '*':
tags['requirements']['python'] = str(req.python_constraint)
if req.platform != '*':
tags['requirements']['platform'] = str(req.platform_constraint)
else: else:
for edge in vertex.incoming_edges: for edge in vertex.incoming_edges:
tags += self._get_categories_for_vertex(edge.origin, requested) sub_tags = self._get_tags_for_vertex(edge.origin, requested)
tags['category'] += sub_tags['category']
tags['optional'] = tags['optional'] and sub_tags['optional']
tags['requirements'].update(sub_tags['requirements'])
return tags return tags
[tool.poetry]
name = "my-package"
version = "1.2.3"
description = "Some description."
authors = [
"Sébastien Eustace <sebastien@eustace.io>"
]
license = "MIT"
readme = "README.rst"
homepage = "https://poetry.eustace.io"
repository = "https://github.com/sdispater/poetry"
documentation = "https://poetry.eustace.io/docs"
keywords = ["packaging", "dependency", "poetry"]
# Requirements
[tool.poetry.dependencies]
python = "~2.7 || ^3.6"
cleo = "^0.6"
pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" }
requests = { version = "^2.18", optional = true }
pathlib2 = { version = "^2.2", python = "~2.7" }
[tool.poetry.dev-dependencies]
pytest = "~3.4"
[tool.poetry.scripts]
my-script = "my_package:main"
...@@ -8,5 +8,10 @@ def get_package(name, version): ...@@ -8,5 +8,10 @@ def get_package(name, version):
return Package(name, normalize_version(version), version) return Package(name, normalize_version(version), version)
def get_dependency(name, constraint=None, category='main'): def get_dependency(name, constraint=None, category='main', optional=False):
return Dependency(name, constraint or '*', category=category) return Dependency(
name,
constraint or '*',
category=category,
optional=optional
)
[[package]]
name = "A"
version = "1.0"
description = ""
category = "main"
optional = true
python-versions = "*"
platform = "*"
[[package]]
name = "B"
version = "1.1"
description = ""
category = "main"
optional = true
python-versions = "*"
platform = "*"
[package.requirements]
python = ">= 2.7.0.0 < 2.8.0.0"
[[package]]
name = "C"
version = "1.3"
description = ""
category = "main"
optional = true
python-versions = "*"
platform = "*"
[package.dependencies]
D = "^1.2"
[package.requirements]
python = ">= 3.6.0.0 < 4.0.0.0"
[[package]]
name = "D"
version = "1.4"
description = ""
category = "main"
optional = true
python-versions = "*"
platform = "*"
[package.requirements]
python = ">= 3.6.0.0 < 4.0.0.0"
[metadata]
python-versions = "~2.7 || ^3.4"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
A = []
B = []
C = []
D = []
import pytest import pytest
import toml import toml
import sys
from pathlib import Path from pathlib import Path
...@@ -54,6 +55,18 @@ class Locker(BaseLocker): ...@@ -54,6 +55,18 @@ class Locker(BaseLocker):
self._written_data = data self._written_data = data
@pytest.fixture(autouse=True)
def setup():
# Mock python version to get reliable tests
original = sys.version_info
sys.version_info = (3, 6, 3, 'final', 0)
yield
sys.version_info = original
@pytest.fixture() @pytest.fixture()
def package(): def package():
return get_package('root', '1.0') return get_package('root', '1.0')
...@@ -241,3 +254,36 @@ def test_run_with_python_versions(installer, locker, repo, package): ...@@ -241,3 +254,36 @@ def test_run_with_python_versions(installer, locker, repo, package):
expected = fixture('with-python-versions') expected = fixture('with-python-versions')
assert locker.written_data == expected assert locker.written_data == expected
def test_run_with_optional_and_python_restricted_dependencies(installer, locker, repo, package):
package.python_versions = '~2.7 || ^3.4'
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', 'python': '~2.7'})
package.add_dependency('C', {'version': '^1.0', 'python': '^3.6'})
installer.run()
expected = fixture('with-optional-dependencies')
assert locker.written_data == expected
installer = installer.installer
# We should only have 3 installs
# A, C, D since the mocked python version is not compatible
# with B's python constraint
assert len(installer.installs) == 3
...@@ -293,3 +293,34 @@ def test_solver_fails_if_mismatch_root_python_versions(solver, repo, package): ...@@ -293,3 +293,34 @@ def test_solver_fails_if_mismatch_root_python_versions(solver, repo, package):
with pytest.raises(SolverProblemError): with pytest.raises(SolverProblemError):
solver.solve(request) solver.solve(request)
def test_solver_solves_optional_and_compatible_packages(solver, repo, package):
package.python_versions = '^3.4'
package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0')
package_b.python_versions = '^3.6'
package_c = get_package('C', '1.0')
package_c.python_versions = '^3.6'
package_b.add_dependency('C', '^1.0')
repo.add_package(package_a)
repo.add_package(package_b)
repo.add_package(package_c)
dependency_a = get_dependency('A')
dependency_a.python_versions = '~3.5'
dependency_b = get_dependency('B', optional=True)
request = [
dependency_a,
dependency_b
]
ops = solver.solve(request)
check_solver_result(ops, [
{'job': 'install', 'package': package_c},
{'job': 'install', 'package': package_b},
{'job': 'install', 'package': package_a},
])
from pathlib import Path
from poetry import Poetry
fixtures_dir = Path(__file__).parent / 'fixtures'
def test_poetry():
poetry = Poetry.create(str(fixtures_dir / 'sample_project'))
package = poetry.package
assert package.name == 'my-package'
assert package.version == '1.2.3.0'
assert package.description == 'Some description.'
assert package.authors == ['Sébastien Eustace <sebastien@eustace.io>']
assert package.license == 'MIT'
assert package.readme == """My Package
==========
"""
assert package.homepage == 'https://poetry.eustace.io'
assert package.repository_url == 'https://github.com/sdispater/poetry'
assert package.keywords == ["packaging", "dependency", "poetry"]
assert package.python_versions == '~2.7 || ^3.6'
assert str(package.python_constraint) == '>= 2.7.0.0 < 2.8.0.0 || >= 3.6.0.0 < 4.0.0.0'
dependencies = package.requires
cleo = dependencies[0]
assert cleo.pretty_constraint == '^0.6'
assert not cleo.is_optional()
pendulum = dependencies[1]
assert pendulum.pretty_constraint == 'branch 2.0'
assert pendulum.is_vcs()
assert pendulum.vcs == 'git'
assert pendulum.branch == '2.0'
assert pendulum.source == 'https://github.com/sdispater/pendulum.git'
assert pendulum.allows_prereleases()
requests = dependencies[2]
assert requests.pretty_constraint == '^2.18'
assert not requests.is_vcs()
assert not requests.allows_prereleases()
assert requests.is_optional()
pathlib2 = dependencies[3]
assert pathlib2.pretty_constraint == '^2.2'
assert pathlib2.python_versions == '~2.7'
assert pathlib2.is_optional()
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