Commit 2cc78814 by Sébastien Eustace

Fix extra installation

parent 4891cd25
...@@ -248,7 +248,7 @@ the `--no-dev` option. ...@@ -248,7 +248,7 @@ the `--no-dev` option.
poetry install --no-dev poetry install --no-dev
``` ```
You can also specify the extra features you want installed You can also specify the extras you want installed
by passing the `--E|--extras` option (See [Extras](#extras) for more info) by passing the `--E|--extras` option (See [Extras](#extras) for more info)
```bash ```bash
...@@ -259,7 +259,7 @@ poetry install -E mysql -E pgsql ...@@ -259,7 +259,7 @@ poetry install -E mysql -E pgsql
#### Options #### Options
* `--no-dev`: Do not install dev dependencies. * `--no-dev`: Do not install dev dependencies.
* `-f|--features`: Features to install (multiple values allowed). * `-E|--extras`: Features to install (multiple values allowed).
### update ### update
...@@ -635,7 +635,7 @@ name = "awesome" ...@@ -635,7 +635,7 @@ name = "awesome"
mandatory = "^1.0" mandatory = "^1.0"
# A list of all of the optional dependencies, some of which are included in the # A list of all of the optional dependencies, some of which are included in the
# above `features`. They can be opted into by apps. # below `extras`. They can be opted into by apps.
psycopg2 = { version = "^2.7", optional = true } psycopg2 = { version = "^2.7", optional = true }
mysqlclient = { version = "^1.3", optional = true } mysqlclient = { version = "^1.3", optional = true }
...@@ -644,7 +644,7 @@ mysql = ["mysqlclient"] ...@@ -644,7 +644,7 @@ mysql = ["mysqlclient"]
pgsql = ["psycopg2"] pgsql = ["psycopg2"]
``` ```
When installing packages, you can specify features by using the `-E|--extras` option: When installing packages, you can specify extras by using the `-E|--extras` option:
```bash ```bash
poet install --extras "mysql pgsql" poet install --extras "mysql pgsql"
......
...@@ -183,7 +183,7 @@ class Installer: ...@@ -183,7 +183,7 @@ class Installer:
# We need to filter operations so that packages # We need to filter operations so that packages
# not compatible with the current system, # not compatible with the current system,
# or optional and not requested, are dropped # or optional and not requested, are dropped
self._filter_operations(ops) self._filter_operations(ops, local_repo)
self._io.new_line() self._io.new_line()
...@@ -336,12 +336,11 @@ class Installer: ...@@ -336,12 +336,11 @@ class Installer:
) -> List[Operation]: ) -> List[Operation]:
installed_repo = InstalledRepository.load(self._io.venv) installed_repo = InstalledRepository.load(self._io.venv)
ops = [] ops = []
extras = []
for extra_name, packages in self._locker.lock_data.get('extras').items():
if extra_name in self._extras:
for package in packages:
extras.append(package.lower())
extra_packages = [
p.name
for p in self._get_extra_packages(locked_repository)
]
for locked in locked_repository.packages: for locked in locked_repository.packages:
is_installed = False is_installed = False
for installed in installed_repo.packages: for installed in installed_repo.packages:
...@@ -349,7 +348,7 @@ class Installer: ...@@ -349,7 +348,7 @@ class Installer:
is_installed = True is_installed = True
if locked.category == 'dev' and not self.is_dev_mode(): if locked.category == 'dev' and not self.is_dev_mode():
ops.append(Uninstall(locked)) ops.append(Uninstall(locked))
elif locked.is_optional() and locked.name not in extras: elif locked.optional and locked.name not in extra_packages:
# Installed but optional and not requested in extras # Installed but optional and not requested in extras
ops.append(Uninstall(locked)) ops.append(Uninstall(locked))
elif locked.version != installed.version: elif locked.version != installed.version:
...@@ -360,14 +359,16 @@ class Installer: ...@@ -360,14 +359,16 @@ class Installer:
if not is_installed: if not is_installed:
# If it's optional and not in required extras # If it's optional and not in required extras
# we do not install # we do not install
if locked.is_optional() and locked.name not in extras: if locked.optional and locked.name not in extra_packages:
continue continue
ops.append(Install(locked)) ops.append(Install(locked))
return ops return ops
def _filter_operations(self, ops: List[Operation]): def _filter_operations(self, ops: List[Operation], repo: Repository) -> None:
extra_packages = [p.name for p in
self._get_extra_packages(repo)]
for op in ops: for op in ops:
if isinstance(op, Update): if isinstance(op, Update):
package = op.target_package package = op.target_package
...@@ -394,20 +395,49 @@ class Installer: ...@@ -394,20 +395,49 @@ class Installer:
extras[extra] = [dep.name for dep in deps] extras[extra] = [dep.name for dep in deps]
else: else:
extras = {} extras = {}
for extra, deps in self._locker.lock_data.get('extras', {}): for extra, deps in self._locker.lock_data.get('extras', {}).items():
extras[extra] = [dep.lower() for dep in deps] extras[extra] = [dep.lower() for dep in deps]
# If a package is optional and not requested # If a package is optional and not requested
# in any extra we skip it # in any extra we skip it
if package.optional: if package.optional:
drop = True if package.name not in extra_packages:
for extra in self._extras: op.skip('Not required')
if extra in extras and package.name in extras[extra]:
drop = False def _get_extra_packages(self, repo):
"""
Returns all packages required by extras.
Maybe we just let the solver handle it?
"""
if self._update:
extras = {
k: [d.name for d in v]
for k, v in self._package.extras.items()
}
else:
extras = self._locker.lock_data.get('extras', {})
extra_packages = []
for extra_name, packages in extras.items():
if extra_name not in self._extras:
continue continue
if drop: extra_packages += [Dependency(p, '*') for p in packages]
op.skip('Not required')
def _extra_packages(packages):
pkgs = []
for package in packages:
for pkg in repo.packages:
if pkg.name == package.name:
pkgs.append(package)
pkgs += _extra_packages(pkg.requires)
break
return pkgs
return _extra_packages(extra_packages)
def _get_installer(self) -> BaseInstaller: def _get_installer(self) -> BaseInstaller:
return PipInstaller(self._io.venv, self._io) return PipInstaller(self._io.venv, self._io)
from pathlib import Path from pathlib import Path
from .__version__ import __version__ from .__version__ import __version__
from .packages import Dependency
from .packages import Locker from .packages import Locker
from .packages import Package from .packages import Package
from .repositories import Pool from .repositories import Pool
...@@ -102,6 +103,12 @@ class Poetry: ...@@ -102,6 +103,12 @@ class Poetry:
for name, constraint in local_config['dev-dependencies'].items(): for name, constraint in local_config['dev-dependencies'].items():
package.add_dependency(name, constraint, category='dev') package.add_dependency(name, constraint, category='dev')
if 'extras' in local_config:
for extra_name, requirements in local_config['extras'].items():
package.extras[extra_name] = [
Dependency(req, '*') for req in requirements
]
locker = Locker(poetry_file.with_suffix('.lock'), local_config) locker = Locker(poetry_file.with_suffix('.lock'), local_config)
return cls(poetry_file, local_config, package, locker) return cls(poetry_file, local_config, package, locker)
...@@ -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) -> List[Operation]: def solve(self, requested, fixed=None, extras=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
...@@ -40,7 +40,7 @@ class Solver: ...@@ -40,7 +40,7 @@ class Solver:
packages = [v.payload for v in graph.vertices.values()] packages = [v.payload for v in graph.vertices.values()]
# Setting categories # Setting info
for vertex in graph.vertices.values(): for vertex in graph.vertices.values():
tags = self._get_tags_for_vertex(vertex, requested) tags = self._get_tags_for_vertex(vertex, requested)
if 'main' in tags['category']: if 'main' in tags['category']:
......
...@@ -24,6 +24,12 @@ pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" } ...@@ -24,6 +24,12 @@ pendulum = { git = "https://github.com/sdispater/pendulum.git", branch = "2.0" }
requests = { version = "^2.18", optional = true, extras=[ "security" ] } requests = { version = "^2.18", optional = true, extras=[ "security" ] }
pathlib2 = { version = "^2.2", python = "~2.7" } pathlib2 = { version = "^2.2", python = "~2.7" }
orator = { version = "^0.9", optional = true}
[tool.poetry.extras]
db = [ "orator" ]
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pytest = "~3.4" pytest = "~3.4"
......
[[package]]
name = "A"
version = "1.0"
description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[[package]]
name = "B"
version = "1.0"
description = ""
category = "main"
optional = false
python-versions = "*"
platform = "*"
[[package]]
name = "C"
version = "1.0"
description = ""
category = "main"
optional = true
python-versions = "*"
platform = "*"
[package.dependencies]
D = "^1.0"
[[package]]
name = "D"
version = "1.1"
description = ""
category = "main"
optional = true
python-versions = "*"
platform = "*"
[extras]
foo = ["C"]
[metadata]
python-versions = "*"
platform = "*"
content-hash = "123456789"
[metadata.hashes]
A = []
B = []
C = []
D = []
...@@ -10,6 +10,7 @@ from poetry.io import NullIO ...@@ -10,6 +10,7 @@ from poetry.io import NullIO
from poetry.packages import Locker as BaseLocker from poetry.packages import Locker as BaseLocker
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 tests.helpers import get_dependency from tests.helpers import get_dependency
from tests.helpers import get_package from tests.helpers import get_package
...@@ -43,6 +44,9 @@ class Locker(BaseLocker): ...@@ -43,6 +44,9 @@ class Locker(BaseLocker):
def is_locked(self) -> bool: def is_locked(self) -> bool:
return self._locked return self._locked
def is_fresh(self) -> bool:
return True
def _get_content_hash(self) -> str: def _get_content_hash(self) -> str:
return '123456789' return '123456789'
...@@ -87,6 +91,16 @@ def pool(repo): ...@@ -87,6 +91,16 @@ def pool(repo):
@pytest.fixture() @pytest.fixture()
def installed():
original = InstalledRepository.load
InstalledRepository.load = lambda _: InstalledRepository()
yield
InstalledRepository.load = original
@pytest.fixture()
def locker(): def locker():
return Locker() return Locker()
...@@ -373,3 +387,65 @@ def test_run_installs_extras_if_requested(installer, locker, repo, package): ...@@ -373,3 +387,65 @@ def test_run_installs_extras_if_requested(installer, locker, repo, package):
installer = installer.installer installer = installer.installer
assert len(installer.installs) == 4 # A, B, C, D assert len(installer.installs) == 4 # A, B, C, D
def test_run_installs_extras_with_deps_if_requested(installer, locker, repo, package):
package.extras['foo'] = [
get_dependency('C')
]
package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0')
package_c = get_package('C', '1.0')
package_d = get_package('D', '1.1')
repo.add_package(package_a)
repo.add_package(package_b)
repo.add_package(package_c)
repo.add_package(package_d)
package.add_dependency('A', '^1.0')
package.add_dependency('B', '^1.0')
package.add_dependency('C', {'version': '^1.0', 'optional': True})
package_c.add_dependency('D', '^1.0')
installer.extras(['foo'])
installer.run()
expected = fixture('extras-with-dependencies')
# Extras are pinned in lock
assert locker.written_data == expected
# But should not be installed
installer = installer.installer
assert len(installer.installs) == 4 # A, B, C, D
def test_run_installs_extras_with_deps_if_requested_locked(installer, locker, repo, package, installed):
locker.locked(True)
locker.mock_lock_data(fixture('extras-with-dependencies'))
package.extras['foo'] = [
get_dependency('C')
]
package_a = get_package('A', '1.0')
package_b = get_package('B', '1.0')
package_c = get_package('C', '1.0')
package_d = get_package('D', '1.1')
repo.add_package(package_a)
repo.add_package(package_b)
repo.add_package(package_c)
repo.add_package(package_d)
package.add_dependency('A', '^1.0')
package.add_dependency('B', '^1.0')
package.add_dependency('C', {'version': '^1.0', 'optional': True})
package_c.add_dependency('D', '^1.0')
installer.extras(['foo'])
installer.run()
# But should not be installed
installer = installer.installer
assert len(installer.installs) == 4 # A, B, C, D
...@@ -50,3 +50,5 @@ def test_poetry(): ...@@ -50,3 +50,5 @@ def test_poetry():
assert pathlib2.pretty_constraint == '^2.2' assert pathlib2.pretty_constraint == '^2.2'
assert pathlib2.python_versions == '~2.7' assert pathlib2.python_versions == '~2.7'
assert not pathlib2.is_optional() assert not pathlib2.is_optional()
assert 'db' in package.extras
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