Skip to content

Commit 5f8c49d

Browse files
committed
add support for PEP 621: poetry add (#9135)
1 parent 13bb855 commit 5f8c49d

File tree

2 files changed

+320
-57
lines changed

2 files changed

+320
-57
lines changed

src/poetry/console/commands/add.py

+88-27
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from cleo.helpers import argument
1010
from cleo.helpers import option
1111
from packaging.utils import canonicalize_name
12+
from poetry.core.packages.dependency import Dependency
1213
from poetry.core.packages.dependency_group import MAIN_GROUP
1314
from tomlkit.toml_document import TOMLDocument
1415

@@ -17,8 +18,11 @@
1718

1819

1920
if TYPE_CHECKING:
21+
from collections.abc import Collection
22+
2023
from cleo.io.inputs.argument import Argument
2124
from cleo.io.inputs.option import Option
25+
from packaging.utils import NormalizedName
2226

2327

2428
class AddCommand(InstallerCommand, InitCommand):
@@ -111,6 +115,7 @@ class AddCommand(InstallerCommand, InitCommand):
111115

112116
def handle(self) -> int:
113117
from poetry.core.constraints.version import parse_constraint
118+
from tomlkit import array
114119
from tomlkit import inline_table
115120
from tomlkit import nl
116121
from tomlkit import table
@@ -135,16 +140,29 @@ def handle(self) -> int:
135140
# tomlkit types are awkward to work with, treat content as a mostly untyped
136141
# dictionary.
137142
content: dict[str, Any] = self.poetry.file.read()
138-
poetry_content = content["tool"]["poetry"]
143+
project_content = content.get("project", table())
144+
poetry_content = content.get("tool", {}).get("poetry", table())
139145
project_name = (
140-
canonicalize_name(name) if (name := poetry_content.get("name")) else None
146+
canonicalize_name(name)
147+
if (name := project_content.get("name", poetry_content.get("name")))
148+
else None
141149
)
142150

151+
use_project_section = False
152+
project_dependency_names = []
143153
if group == MAIN_GROUP:
144-
if "dependencies" not in poetry_content:
145-
poetry_content["dependencies"] = table()
146-
147-
section = poetry_content["dependencies"]
154+
if (
155+
"dependencies" in project_content
156+
or "optional-dependencies" in project_content
157+
):
158+
use_project_section = True
159+
project_dependency_names = [
160+
Dependency.create_from_pep_508(dep).name
161+
for dep in project_content.get("dependencies", {})
162+
]
163+
164+
poetry_section = poetry_content.get("dependencies", table())
165+
project_section = project_content.get("dependencies", array())
148166
else:
149167
if "group" not in poetry_content:
150168
poetry_content["group"] = table(is_super_table=True)
@@ -160,9 +178,12 @@ def handle(self) -> int:
160178
if "dependencies" not in this_group:
161179
this_group["dependencies"] = table()
162180

163-
section = this_group["dependencies"]
181+
poetry_section = this_group["dependencies"]
182+
project_section = []
164183

165-
existing_packages = self.get_existing_packages_from_input(packages, section)
184+
existing_packages = self.get_existing_packages_from_input(
185+
packages, poetry_section, project_dependency_names
186+
)
166187

167188
if existing_packages:
168189
self.notify_about_existing_packages(existing_packages)
@@ -187,11 +208,11 @@ def handle(self) -> int:
187208
parse_constraint(version)
188209

189210
constraint: dict[str, Any] = inline_table()
190-
for name, value in _constraint.items():
191-
if name == "name":
211+
for key, value in _constraint.items():
212+
if key == "name":
192213
continue
193214

194-
constraint[name] = value
215+
constraint[key] = value
195216

196217
if self.option("optional"):
197218
constraint["optional"] = True
@@ -244,28 +265,61 @@ def handle(self) -> int:
244265
self.line_error("\nNo changes were applied.")
245266
return 1
246267

247-
for key in section:
248-
if canonicalize_name(key) == canonical_constraint_name:
249-
section[key] = constraint
250-
break
251-
else:
252-
section[constraint_name] = constraint
253-
254268
with contextlib.suppress(ValueError):
255269
self.poetry.package.dependency_group(group).remove_dependency(
256270
constraint_name
257271
)
258272

259-
self.poetry.package.add_dependency(
260-
Factory.create_dependency(
261-
constraint_name,
262-
constraint,
263-
groups=[group],
264-
root_dir=self.poetry.file.path.parent,
265-
)
273+
dependency = Factory.create_dependency(
274+
constraint_name,
275+
constraint,
276+
groups=[group],
277+
root_dir=self.poetry.file.path.parent,
266278
)
279+
self.poetry.package.add_dependency(dependency)
280+
281+
if use_project_section:
282+
try:
283+
index = project_dependency_names.index(canonical_constraint_name)
284+
except ValueError:
285+
project_section.append(dependency.to_pep_508())
286+
else:
287+
project_section[index] = dependency.to_pep_508()
288+
289+
# create a second constraint for tool.poetry.dependencies with keys
290+
# that cannot be stored in the project section
291+
poetry_constraint: dict[str, Any] = inline_table()
292+
if not isinstance(constraint, str):
293+
for key in ["optional", "allow-prereleases", "develop", "source"]:
294+
if value := constraint.get(key):
295+
poetry_constraint[key] = value
296+
if poetry_constraint:
297+
# add marker related keys to avoid ambiguity
298+
for key in ["python", "platform"]:
299+
if value := constraint.get(key):
300+
poetry_constraint[key] = value
301+
else:
302+
poetry_constraint = constraint
303+
304+
if poetry_constraint:
305+
for key in poetry_section:
306+
if canonicalize_name(key) == canonical_constraint_name:
307+
poetry_section[key] = poetry_constraint
308+
break
309+
else:
310+
poetry_section[constraint_name] = poetry_constraint
267311

268312
# Refresh the locker
313+
if project_section and "dependencies" not in project_content:
314+
assert group == MAIN_GROUP
315+
project_content["dependencies"] = project_section
316+
if poetry_section:
317+
if "tool" not in content:
318+
content["tool"] = table()
319+
if "poetry" not in content["tool"]:
320+
content["tool"]["poetry"] = poetry_content
321+
if group == MAIN_GROUP and "dependencies" not in poetry_content:
322+
poetry_content["dependencies"] = poetry_section
269323
self.poetry.locker.set_pyproject_data(content)
270324
self.installer.set_locker(self.poetry.locker)
271325

@@ -289,13 +343,20 @@ def handle(self) -> int:
289343
return status
290344

291345
def get_existing_packages_from_input(
292-
self, packages: list[str], section: dict[str, Any]
346+
self,
347+
packages: list[str],
348+
section: dict[str, Any],
349+
project_dependencies: Collection[NormalizedName],
293350
) -> list[str]:
294351
existing_packages = []
295352

296353
for name in packages:
354+
normalized_name = canonicalize_name(name)
355+
if normalized_name in project_dependencies:
356+
existing_packages.append(name)
357+
continue
297358
for key in section:
298-
if canonicalize_name(key) == canonicalize_name(name):
359+
if normalized_name == canonicalize_name(key):
299360
existing_packages.append(name)
300361

301362
return existing_packages

0 commit comments

Comments
 (0)