Skip to content

Commit c27eb38

Browse files
committed
add support for PEP 621: poetry add - change "--optional" to require an extra the optional dependency is added to
1 parent be52685 commit c27eb38

File tree

3 files changed

+102
-17
lines changed

3 files changed

+102
-17
lines changed

docs/cli.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ about dependency groups.
462462
* `--dev (-D)`: Add package as development dependency. (**Deprecated**, use `-G dev` instead)
463463
* `--editable (-e)`: Add vcs/path dependencies as editable.
464464
* `--extras (-E)`: Extras to activate for the dependency. (multiple values allowed)
465-
* `--optional`: Add as an optional dependency.
465+
* `--optional`: Add as an optional dependency to an extra.
466466
* `--python`: Python version for which the dependency must be installed.
467467
* `--platform`: Platforms for which the dependency must be installed.
468468
* `--source`: Name of the source to use to install the package.

src/poetry/console/commands/add.py

+36-8
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,12 @@ class AddCommand(InstallerCommand, InitCommand):
5454
flag=False,
5555
multiple=True,
5656
),
57-
option("optional", None, "Add as an optional dependency."),
57+
option(
58+
"optional",
59+
None,
60+
"Add as an optional dependency to an extra.",
61+
flag=False,
62+
),
5863
option(
5964
"python",
6065
None,
@@ -137,6 +142,10 @@ def handle(self) -> int:
137142
"You can only specify one package when using the --extras option"
138143
)
139144

145+
optional = self.option("optional")
146+
if optional and group != MAIN_GROUP:
147+
raise ValueError("You can only add optional dependencies to the main group")
148+
140149
# tomlkit types are awkward to work with, treat content as a mostly untyped
141150
# dictionary.
142151
content: dict[str, Any] = self.poetry.file.read()
@@ -156,13 +165,19 @@ def handle(self) -> int:
156165
or "optional-dependencies" in project_content
157166
):
158167
use_project_section = True
168+
if optional:
169+
project_section = project_content.get(
170+
"optional-dependencies", {}
171+
).get(optional, array())
172+
else:
173+
project_section = project_content.get("dependencies", array())
159174
project_dependency_names = [
160-
Dependency.create_from_pep_508(dep).name
161-
for dep in project_content.get("dependencies", {})
175+
Dependency.create_from_pep_508(dep).name for dep in project_section
162176
]
177+
else:
178+
project_section = array()
163179

164180
poetry_section = poetry_content.get("dependencies", table())
165-
project_section = project_content.get("dependencies", array())
166181
else:
167182
if "group" not in poetry_content:
168183
poetry_content["group"] = table(is_super_table=True)
@@ -194,6 +209,13 @@ def handle(self) -> int:
194209
self.line("Nothing to add.")
195210
return 0
196211

212+
if optional and not use_project_section:
213+
self.line_error(
214+
"<warning>Optional dependencies will not be added to extras"
215+
" in legacy mode. Consider converting your project to use the [project]"
216+
" section.</warning>"
217+
)
218+
197219
requirements = self._determine_requirements(
198220
packages,
199221
allow_prereleases=self.option("allow-prereleases"),
@@ -214,7 +236,7 @@ def handle(self) -> int:
214236

215237
constraint[key] = value
216238

217-
if self.option("optional"):
239+
if optional:
218240
constraint["optional"] = True
219241

220242
if self.option("allow-prereleases"):
@@ -290,7 +312,7 @@ def handle(self) -> int:
290312
# that cannot be stored in the project section
291313
poetry_constraint: dict[str, Any] = inline_table()
292314
if not isinstance(constraint, str):
293-
for key in ["optional", "allow-prereleases", "develop", "source"]:
315+
for key in ["allow-prereleases", "develop", "source"]:
294316
if value := constraint.get(key):
295317
poetry_constraint[key] = value
296318
if poetry_constraint:
@@ -310,9 +332,15 @@ def handle(self) -> int:
310332
poetry_section[constraint_name] = poetry_constraint
311333

312334
# Refresh the locker
313-
if project_section and "dependencies" not in project_content:
335+
if project_section:
314336
assert group == MAIN_GROUP
315-
project_content["dependencies"] = project_section
337+
if optional:
338+
if "optional-dependencies" not in project_content:
339+
project_content["optional-dependencies"] = table()
340+
if optional not in project_content["optional-dependencies"]:
341+
project_content["optional-dependencies"][optional] = project_section
342+
elif "dependencies" not in project_content:
343+
project_content["dependencies"] = project_section
316344
if poetry_section:
317345
if "tool" not in content:
318346
content["tool"] = table()

tests/console/commands/test_add.py

+65-8
Original file line numberDiff line numberDiff line change
@@ -814,11 +814,43 @@ def test_add_url_constraint_wheel_with_extras(
814814
}
815815

816816

817+
@pytest.mark.parametrize("project_dependencies", [True, False])
818+
@pytest.mark.parametrize(
819+
("existing_extras", "expected_extras"),
820+
[
821+
(None, {"my-extra": ["cachy (==0.2.0)"]}),
822+
(
823+
{"other": ["foo>2"]},
824+
{"other": ["foo>2"], "my-extra": ["cachy (==0.2.0)"]},
825+
),
826+
({"my-extra": ["foo>2"]}, {"my-extra": ["foo>2", "cachy (==0.2.0)"]}),
827+
(
828+
{"my-extra": ["foo>2", "cachy (==0.1.0)", "bar>1"]},
829+
{"my-extra": ["foo>2", "cachy (==0.2.0)", "bar>1"]},
830+
),
831+
],
832+
)
817833
def test_add_constraint_with_optional(
818-
app: PoetryTestApplication, repo: TestRepository, tester: CommandTester
834+
app: PoetryTestApplication,
835+
repo: TestRepository,
836+
tester: CommandTester,
837+
project_dependencies: bool,
838+
existing_extras: dict[str, list[str]] | None,
839+
expected_extras: dict[str, list[str]],
819840
) -> None:
841+
pyproject: dict[str, Any] = app.poetry.file.read()
842+
if project_dependencies:
843+
pyproject["project"]["dependencies"] = ["foo>1"]
844+
if existing_extras:
845+
pyproject["project"]["optional-dependencies"] = existing_extras
846+
else:
847+
pyproject["tool"]["poetry"]["dependencies"]["foo"] = "^1.0"
848+
pyproject = cast("TOMLDocument", pyproject)
849+
app.poetry.file.write(pyproject)
850+
820851
repo.add_package(get_package("cachy", "0.2.0"))
821-
tester.execute("cachy=0.2.0 --optional")
852+
853+
tester.execute("cachy=0.2.0 --optional my-extra")
822854
expected = """\
823855
824856
Updating dependencies
@@ -834,13 +866,38 @@ def test_add_constraint_with_optional(
834866
assert tester.command.installer.executor.installations_count == 0
835867

836868
pyproject: dict[str, Any] = app.poetry.file.read()
837-
content = pyproject["tool"]["poetry"]
869+
project_content = pyproject["project"]
870+
poetry_content = pyproject["tool"]["poetry"]
838871

839-
assert "cachy" in content["dependencies"]
840-
assert content["dependencies"]["cachy"] == {
841-
"version": "0.2.0",
842-
"optional": True,
843-
}
872+
if project_dependencies:
873+
assert "cachy" not in poetry_content["dependencies"]
874+
assert "cachy" not in project_content["dependencies"]
875+
assert "my-extra" in project_content["optional-dependencies"]
876+
assert project_content["optional-dependencies"] == expected_extras
877+
assert not tester.io.fetch_error()
878+
else:
879+
assert "dependencies" not in project_content
880+
assert "optional-dependencies" not in project_content
881+
assert "cachy" in poetry_content["dependencies"]
882+
assert poetry_content["dependencies"]["cachy"] == {
883+
"version": "0.2.0",
884+
"optional": True,
885+
}
886+
assert (
887+
"Optional dependencies will not be added to extras in legacy mode."
888+
in tester.io.fetch_error()
889+
)
890+
891+
892+
def test_add_constraint_with_optional_not_main_group(
893+
app: PoetryTestApplication, repo: TestRepository, tester: CommandTester
894+
) -> None:
895+
repo.add_package(get_package("cachy", "0.2.0"))
896+
897+
with pytest.raises(ValueError) as e:
898+
tester.execute("cachy=0.2.0 --group dev --optional my-extra")
899+
900+
assert str(e.value) == "You can only add optional dependencies to the main group"
844901

845902

846903
def test_add_constraint_with_python(

0 commit comments

Comments
 (0)