Commit 4b8384cd by Sébastien Eustace

Update the add and remove commands to support groups

parent 26f13f78
# -*- coding: utf-8 -*-
from typing import Dict
from typing import List
......@@ -16,6 +15,13 @@ class AddCommand(InstallerCommand, InitCommand):
arguments = [argument("name", "The packages to add.", multiple=True)]
options = [
option(
"group",
"-G",
"The group to add the dependency to.",
flag=False,
default="default",
),
option("dev", "D", "Add as a development dependency."),
option("editable", "e", "Add vcs/path dependencies as editable."),
option(
......@@ -71,31 +77,55 @@ class AddCommand(InstallerCommand, InitCommand):
def handle(self) -> int:
from tomlkit import inline_table
from tomlkit import parse as parse_toml
from tomlkit import table
from poetry.core.semver.helpers import parse_constraint
from poetry.factory import Factory
packages = self.argument("name")
is_dev = self.option("dev")
if self.option("dev"):
self.line(
"<warning>The --dev option is deprecated, "
"use the `--group dev` notation instead.</warning>"
)
self.line("")
group = "dev"
else:
group = self.option("group")
if self.option("extras") and len(packages) > 1:
raise ValueError(
"You can only specify one package " "when using the --extras option"
"You can only specify one package when using the --extras option"
)
section = "dependencies"
if is_dev:
section = "dev-dependencies"
original_content = self.poetry.file.read()
content = self.poetry.file.read()
poetry_content = content["tool"]["poetry"]
if section not in poetry_content:
poetry_content[section] = {}
if group == "default":
if "dependencies" not in poetry_content:
poetry_content["dependencies"] = table()
existing_packages = self.get_existing_packages_from_input(
packages, poetry_content, section
)
section = poetry_content["dependencies"]
else:
if "group" not in poetry_content:
group_table = table()
group_table._is_super_table = True
poetry_content.value._insert_after("dependencies", "group", group_table)
groups = poetry_content["group"]
if group not in groups:
group_table = parse_toml(
f"[tool.poetry.group.{group}.dependencies]\n\n"
)["tool"]["poetry"]["group"][group]
poetry_content["group"][group] = group_table
if "dependencies" not in poetry_content["group"][group]:
poetry_content["group"][group]["dependencies"] = table()
section = poetry_content["group"][group]["dependencies"]
existing_packages = self.get_existing_packages_from_input(packages, section)
if existing_packages:
self.notify_about_existing_packages(existing_packages)
......@@ -165,18 +195,25 @@ class AddCommand(InstallerCommand, InitCommand):
if len(constraint) == 1 and "version" in constraint:
constraint = constraint["version"]
poetry_content[section][_constraint["name"]] = constraint
section[_constraint["name"]] = constraint
self.poetry.package.add_dependency(
Factory.create_dependency(
_constraint["name"],
constraint,
groups=[group],
root_dir=self.poetry.file.parent,
)
)
try:
# Write new content
self.poetry.file.write(content)
# Refresh the locker
self.poetry.set_locker(
self.poetry.locker.__class__(self.poetry.locker.lock.path, poetry_content)
)
self._installer.set_locker(self.poetry.locker)
# Cosmetic new line
self.line("")
# Update packages
self.reset_poetry()
self._installer.set_package(self.poetry.package)
self._installer.dry_run(self.option("dry-run"))
self._installer.verbose(self._io.is_verbose())
......@@ -187,31 +224,19 @@ class AddCommand(InstallerCommand, InitCommand):
self._installer.whitelist([r["name"] for r in requirements])
status = self._installer.run()
except BaseException:
# Using BaseException here as some exceptions, eg: KeyboardInterrupt, do not inherit from Exception
self.poetry.file.write(original_content)
raise
if status != 0 or self.option("dry-run"):
# Revert changes
if not self.option("dry-run"):
self.line_error(
"\n"
"<error>Failed to add packages, reverting the pyproject.toml file "
"to its original content.</error>"
)
self.poetry.file.write(original_content)
if status == 0 and not self.option("dry-run"):
self.poetry.file.write(content)
return status
def get_existing_packages_from_input(
self, packages: List[str], poetry_content: Dict, target_section: str
self, packages: List[str], section: Dict
) -> List[str]:
existing_packages = []
for name in packages:
for key in poetry_content[target_section]:
for key in section:
if key.lower() == name.lower():
existing_packages.append(name)
......
from typing import Any
from typing import Dict
from typing import List
from cleo.helpers import argument
from cleo.helpers import option
from ...utils.helpers import canonicalize_name
from .installer_command import InstallerCommand
......@@ -12,6 +15,7 @@ class RemoveCommand(InstallerCommand):
arguments = [argument("packages", "The packages to remove.", multiple=True)]
options = [
option("group", "G", "The group to remove the dependency from.", flag=False),
option("dev", "D", "Remove a package from the development dependencies."),
option(
"dry-run",
......@@ -30,39 +34,70 @@ list of installed packages
def handle(self) -> int:
packages = self.argument("packages")
is_dev = self.option("dev")
if self.option("dev"):
self.line(
"<warning>The --dev option is deprecated, "
"use the `--group dev` notation instead.</warning>"
)
self.line("")
group = "dev"
else:
group = self.option("group")
content = self.poetry.file.read()
poetry_content = content["tool"]["poetry"]
section = "dependencies"
if is_dev:
section = "dev-dependencies"
# Deleting entries
requirements = {}
for name in packages:
found = False
for key in poetry_content[section]:
if key.lower() == name.lower():
found = True
requirements[key] = poetry_content[section][key]
break
if not found:
raise ValueError("Package {} not found".format(name))
for key in requirements:
del poetry_content[section][key]
dependencies = (
self.poetry.package.requires
if section == "dependencies"
else self.poetry.package.dev_requires
if group is None:
removed = []
group_sections = []
for group_name, group_section in poetry_content.get("group", {}).items():
group_sections.append(
(group_name, group_section.get("dependencies", {}))
)
for group_name, section in [
("default", poetry_content["dependencies"])
] + group_sections:
removed += self._remove_packages(packages, section, group_name)
if group_name != "default":
if not section:
del poetry_content["group"][group_name]
else:
poetry_content["group"][group_name]["dependencies"] = section
elif group == "dev" and "dev-dependencies" in poetry_content:
# We need to account for the old `dev-dependencies` section
removed = self._remove_packages(
packages, poetry_content["dev-dependencies"], "dev"
)
if not poetry_content["dev-dependencies"]:
del poetry_content["dev-dependencies"]
else:
removed = self._remove_packages(
packages, poetry_content["group"][group].get("dependencies", {}), group
)
for i, dependency in enumerate(reversed(dependencies)):
if dependency.name == canonicalize_name(key):
del dependencies[-i]
if not poetry_content["group"][group]:
del poetry_content["group"][group]
if "group" in poetry_content and not poetry_content["group"]:
del poetry_content["group"]
removed = set(removed)
not_found = set(packages).difference(removed)
if not_found:
raise ValueError(
"The following packages were not found: {}".format(
", ".join(sorted(not_found))
)
)
# Refresh the locker
self.poetry.set_locker(
self.poetry.locker.__class__(self.poetry.locker.lock.path, poetry_content)
)
self._installer.set_locker(self.poetry.locker)
# Update packages
self._installer.use_executor(
......@@ -72,7 +107,7 @@ list of installed packages
self._installer.dry_run(self.option("dry-run"))
self._installer.verbose(self._io.is_verbose())
self._installer.update(True)
self._installer.whitelist(requirements)
self._installer.whitelist(removed)
status = self._installer.run()
......@@ -80,3 +115,19 @@ list of installed packages
self.poetry.file.write(content)
return status
def _remove_packages(
self, packages: List[str], section: Dict[str, Any], group_name: str
) -> List[str]:
removed = []
group = self.poetry.package.dependency_group(group_name)
section_keys = list(section.keys())
for package in packages:
for existing_package in section_keys:
if existing_package.lower() == package.lower():
del section[existing_package]
removed.append(package)
group.remove_dependency(package)
return removed
......@@ -667,13 +667,52 @@ def test_add_constraint_not_found_with_source(app, poetry, mocker, tester):
assert "Could not find a matching version of package cachy" == str(e.value)
def test_add_to_section_that_does_no_exist_yet(app, repo, tester):
def test_add_to_section_that_does_not_exist_yet(app, repo, tester):
repo.add_package(get_package("cachy", "0.1.0"))
repo.add_package(get_package("cachy", "0.2.0"))
tester.execute("cachy --group dev")
expected = """\
Using version ^0.2.0 for cachy
Updating dependencies
Resolving dependencies...
Writing lock file
Package operations: 1 install, 0 updates, 0 removals
• Installing cachy (0.2.0)
"""
assert expected == tester.io.fetch_output()
assert 1 == tester.command.installer.executor.installations_count
content = app.poetry.file.read()["tool"]["poetry"]
assert "cachy" in content["group"]["dev"]["dependencies"]
assert content["group"]["dev"]["dependencies"]["cachy"] == "^0.2.0"
expected = """\
[tool.poetry.group.dev.dependencies]
cachy = "^0.2.0"
"""
assert expected in content.as_string()
def test_add_to_dev_section_deprecated(app, repo, tester):
repo.add_package(get_package("cachy", "0.1.0"))
repo.add_package(get_package("cachy", "0.2.0"))
tester.execute("cachy --dev")
expected = """\
The --dev option is deprecated, use the `--group dev` notation instead.
Using version ^0.2.0 for cachy
Updating dependencies
......@@ -691,8 +730,8 @@ Package operations: 1 install, 0 updates, 0 removals
content = app.poetry.file.read()["tool"]["poetry"]
assert "cachy" in content["dev-dependencies"]
assert content["dev-dependencies"]["cachy"] == "^0.2.0"
assert "cachy" in content["group"]["dev"]["dependencies"]
assert content["group"]["dev"]["dependencies"]["cachy"] == "^0.2.0"
def test_add_should_not_select_prereleases(app, repo, tester):
......@@ -1487,7 +1526,7 @@ def test_add_to_section_that_does_no_exist_yet_old_installer(
repo.add_package(get_package("cachy", "0.1.0"))
repo.add_package(get_package("cachy", "0.2.0"))
old_tester.execute("cachy --dev")
old_tester.execute("cachy --group dev")
expected = """\
Using version ^0.2.0 for cachy
......@@ -1508,8 +1547,8 @@ Package operations: 1 install, 0 updates, 0 removals
content = app.poetry.file.read()["tool"]["poetry"]
assert "cachy" in content["dev-dependencies"]
assert content["dev-dependencies"]["cachy"] == "^0.2.0"
assert "cachy" in content["group"]["dev"]["dependencies"]
assert content["group"]["dev"]["dependencies"]["cachy"] == "^0.2.0"
def test_add_should_not_select_prereleases_old_installer(
......
import pytest
import tomlkit
from poetry.core.packages.package import Package
from poetry.factory import Factory
@pytest.fixture()
......@@ -8,6 +10,152 @@ def tester(command_tester_factory):
return command_tester_factory("remove")
def test_remove_without_specific_group_removes_from_all_groups(
tester, app, repo, command_tester_factory, installed
):
"""
Removing without specifying a group removes packages from all groups.
"""
installed.add_package(Package("foo", "2.0.0"))
repo.add_package(Package("foo", "2.0.0"))
repo.add_package(Package("baz", "1.0.0"))
content = app.poetry.file.read()
groups_content = tomlkit.parse(
"""\
[tool.poetry.group.bar.dependencies]
foo = "^2.0.0"
baz = "^1.0.0"
"""
)
content["tool"]["poetry"]["dependencies"]["foo"] = "^2.0.0"
content["tool"]["poetry"].value._insert_after(
"dependencies", "group", groups_content["tool"]["poetry"]["group"]
)
app.poetry.file.write(content)
app.poetry.package.add_dependency(Factory.create_dependency("foo", "^2.0.0"))
app.poetry.package.add_dependency(
Factory.create_dependency("foo", "^2.0.0", groups=["bar"])
)
app.poetry.package.add_dependency(
Factory.create_dependency("baz", "^1.0.0", groups=["bar"])
)
tester.execute("foo")
content = app.poetry.file.read()["tool"]["poetry"]
assert "foo" not in content["dependencies"]
assert "foo" not in content["group"]["bar"]["dependencies"]
assert "baz" in content["group"]["bar"]["dependencies"]
expected = """\
[tool.poetry.group.bar.dependencies]
baz = "^1.0.0"
"""
assert expected in content.as_string()
def test_remove_without_specific_group_removes_from_specific_groups(
tester, app, repo, command_tester_factory, installed
):
"""
Removing with a specific group given removes packages only from this group.
"""
installed.add_package(Package("foo", "2.0.0"))
repo.add_package(Package("foo", "2.0.0"))
repo.add_package(Package("baz", "1.0.0"))
content = app.poetry.file.read()
groups_content = tomlkit.parse(
"""\
[tool.poetry.group.bar.dependencies]
foo = "^2.0.0"
baz = "^1.0.0"
"""
)
content["tool"]["poetry"]["dependencies"]["foo"] = "^2.0.0"
content["tool"]["poetry"].value._insert_after(
"dependencies", "group", groups_content["tool"]["poetry"]["group"]
)
app.poetry.file.write(content)
app.poetry.package.add_dependency(Factory.create_dependency("foo", "^2.0.0"))
app.poetry.package.add_dependency(
Factory.create_dependency("foo", "^2.0.0", groups=["bar"])
)
app.poetry.package.add_dependency(
Factory.create_dependency("baz", "^1.0.0", groups=["bar"])
)
tester.execute("foo --group bar")
content = app.poetry.file.read()["tool"]["poetry"]
assert "foo" in content["dependencies"]
assert "foo" not in content["group"]["bar"]["dependencies"]
assert "baz" in content["group"]["bar"]["dependencies"]
expected = """\
[tool.poetry.group.bar.dependencies]
baz = "^1.0.0"
"""
assert expected in content.as_string()
def test_remove_does_not_live_empty_groups(
tester, app, repo, command_tester_factory, installed
):
"""
Empty groups are automatically discarded after package removal.
"""
installed.add_package(Package("foo", "2.0.0"))
repo.add_package(Package("foo", "2.0.0"))
repo.add_package(Package("baz", "1.0.0"))
content = app.poetry.file.read()
groups_content = tomlkit.parse(
"""\
[tool.poetry.group.bar.dependencies]
foo = "^2.0.0"
baz = "^1.0.0"
"""
)
content["tool"]["poetry"]["dependencies"]["foo"] = "^2.0.0"
content["tool"]["poetry"].value._insert_after(
"dependencies", "group", groups_content["tool"]["poetry"]["group"]
)
app.poetry.file.write(content)
app.poetry.package.add_dependency(Factory.create_dependency("foo", "^2.0.0"))
app.poetry.package.add_dependency(
Factory.create_dependency("foo", "^2.0.0", groups=["bar"])
)
app.poetry.package.add_dependency(
Factory.create_dependency("baz", "^1.0.0", groups=["bar"])
)
tester.execute("foo baz --group bar")
content = app.poetry.file.read()["tool"]["poetry"]
assert "foo" in content["dependencies"]
assert "foo" not in content["group"]["bar"]["dependencies"]
assert "baz" not in content["group"]["bar"]["dependencies"]
assert "[tool.poetry.group.bar]" not in content.as_string()
assert "[tool.poetry.group]" not in content.as_string()
def test_remove_command_should_not_write_changes_upon_installer_errors(
tester, app, repo, command_tester_factory, mocker
):
......
......@@ -823,7 +823,6 @@ def test_exporter_can_export_requirements_txt_with_git_packages(tmp_dir, poetry)
}
)
set_package_requires(poetry)
print(poetry.package.all_requires)
exporter = Exporter(poetry)
......
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