Skip to content

Commit c206e1b

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

File tree

3 files changed

+99
-18
lines changed

3 files changed

+99
-18
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

+62-9
Original file line numberDiff line numberDiff line change
@@ -767,10 +767,40 @@ def test_add_url_constraint_wheel_with_extras(
767767
}
768768

769769

770+
@pytest.mark.parametrize("project_dependencies", [True, False])
771+
@pytest.mark.parametrize(
772+
("existing_extras", "expected_extras"),
773+
[
774+
(None, {"my-extra": ["cachy (==0.2.0)"]}),
775+
(
776+
{"other": ["foo>2"]},
777+
{"other": ["foo>2"], "my-extra": ["cachy (==0.2.0)"]},
778+
),
779+
({"my-extra": ["foo>2"]}, {"my-extra": ["foo>2", "cachy (==0.2.0)"]}),
780+
(
781+
{"my-extra": ["foo>2", "cachy (==0.1.0)", "bar>1"]},
782+
{"my-extra": ["foo>2", "cachy (==0.2.0)", "bar>1"]},
783+
),
784+
],
785+
)
770786
def test_add_constraint_with_optional(
771-
app: PoetryTestApplication, tester: CommandTester
787+
app: PoetryTestApplication,
788+
tester: CommandTester,
789+
project_dependencies: bool,
790+
existing_extras: dict[str, list[str]] | None,
791+
expected_extras: dict[str, list[str]],
772792
) -> None:
773-
tester.execute("cachy=0.2.0 --optional")
793+
pyproject: dict[str, Any] = app.poetry.file.read()
794+
if project_dependencies:
795+
pyproject["project"]["dependencies"] = ["foo>1"]
796+
if existing_extras:
797+
pyproject["project"]["optional-dependencies"] = existing_extras
798+
else:
799+
pyproject["tool"]["poetry"]["dependencies"]["foo"] = "^1.0"
800+
pyproject = cast("TOMLDocument", pyproject)
801+
app.poetry.file.write(pyproject)
802+
803+
tester.execute("cachy=0.2.0 --optional my-extra")
774804
expected = """\
775805
776806
Updating dependencies
@@ -785,14 +815,37 @@ def test_add_constraint_with_optional(
785815
assert isinstance(tester.command, InstallerCommand)
786816
assert tester.command.installer.executor.installations_count == 0
787817

788-
pyproject: dict[str, Any] = app.poetry.file.read()
789-
content = pyproject["tool"]["poetry"]
818+
pyproject2: dict[str, Any] = app.poetry.file.read()
819+
project_content = pyproject2["project"]
820+
poetry_content = pyproject2["tool"]["poetry"]
790821

791-
assert "cachy" in content["dependencies"]
792-
assert content["dependencies"]["cachy"] == {
793-
"version": "0.2.0",
794-
"optional": True,
795-
}
822+
if project_dependencies:
823+
assert "cachy" not in poetry_content["dependencies"]
824+
assert "cachy" not in project_content["dependencies"]
825+
assert "my-extra" in project_content["optional-dependencies"]
826+
assert project_content["optional-dependencies"] == expected_extras
827+
assert not tester.io.fetch_error()
828+
else:
829+
assert "dependencies" not in project_content
830+
assert "optional-dependencies" not in project_content
831+
assert "cachy" in poetry_content["dependencies"]
832+
assert poetry_content["dependencies"]["cachy"] == {
833+
"version": "0.2.0",
834+
"optional": True,
835+
}
836+
assert (
837+
"Optional dependencies will not be added to extras in legacy mode."
838+
in tester.io.fetch_error()
839+
)
840+
841+
842+
def test_add_constraint_with_optional_not_main_group(
843+
app: PoetryTestApplication, tester: CommandTester
844+
) -> None:
845+
with pytest.raises(ValueError) as e:
846+
tester.execute("cachy=0.2.0 --group dev --optional my-extra")
847+
848+
assert str(e.value) == "You can only add optional dependencies to the main group"
796849

797850

798851
def test_add_constraint_with_python(

0 commit comments

Comments
 (0)