9
9
from cleo .helpers import argument
10
10
from cleo .helpers import option
11
11
from packaging .utils import canonicalize_name
12
+ from poetry .core .packages .dependency import Dependency
12
13
from poetry .core .packages .dependency_group import MAIN_GROUP
13
14
from tomlkit .toml_document import TOMLDocument
14
15
17
18
18
19
19
20
if TYPE_CHECKING :
21
+ from collections .abc import Collection
22
+
20
23
from cleo .io .inputs .argument import Argument
21
24
from cleo .io .inputs .option import Option
25
+ from packaging .utils import NormalizedName
22
26
23
27
24
28
class AddCommand (InstallerCommand , InitCommand ):
@@ -111,6 +115,7 @@ class AddCommand(InstallerCommand, InitCommand):
111
115
112
116
def handle (self ) -> int :
113
117
from poetry .core .constraints .version import parse_constraint
118
+ from tomlkit import array
114
119
from tomlkit import inline_table
115
120
from tomlkit import nl
116
121
from tomlkit import table
@@ -135,16 +140,29 @@ def handle(self) -> int:
135
140
# tomlkit types are awkward to work with, treat content as a mostly untyped
136
141
# dictionary.
137
142
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 ())
139
145
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
141
149
)
142
150
151
+ use_project_section = False
152
+ project_dependency_names = []
143
153
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 ())
148
166
else :
149
167
if "group" not in poetry_content :
150
168
poetry_content ["group" ] = table (is_super_table = True )
@@ -160,9 +178,12 @@ def handle(self) -> int:
160
178
if "dependencies" not in this_group :
161
179
this_group ["dependencies" ] = table ()
162
180
163
- section = this_group ["dependencies" ]
181
+ poetry_section = this_group ["dependencies" ]
182
+ project_section = []
164
183
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
+ )
166
187
167
188
if existing_packages :
168
189
self .notify_about_existing_packages (existing_packages )
@@ -187,11 +208,11 @@ def handle(self) -> int:
187
208
parse_constraint (version )
188
209
189
210
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" :
192
213
continue
193
214
194
- constraint [name ] = value
215
+ constraint [key ] = value
195
216
196
217
if self .option ("optional" ):
197
218
constraint ["optional" ] = True
@@ -244,28 +265,61 @@ def handle(self) -> int:
244
265
self .line_error ("\n No changes were applied." )
245
266
return 1
246
267
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
-
254
268
with contextlib .suppress (ValueError ):
255
269
self .poetry .package .dependency_group (group ).remove_dependency (
256
270
constraint_name
257
271
)
258
272
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 ,
266
278
)
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
267
311
268
312
# 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
269
323
self .poetry .locker .set_pyproject_data (content )
270
324
self .installer .set_locker (self .poetry .locker )
271
325
@@ -289,13 +343,20 @@ def handle(self) -> int:
289
343
return status
290
344
291
345
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 ],
293
350
) -> list [str ]:
294
351
existing_packages = []
295
352
296
353
for name in packages :
354
+ normalized_name = canonicalize_name (name )
355
+ if normalized_name in project_dependencies :
356
+ existing_packages .append (name )
357
+ continue
297
358
for key in section :
298
- if canonicalize_name ( key ) == canonicalize_name (name ):
359
+ if normalized_name == canonicalize_name (key ):
299
360
existing_packages .append (name )
300
361
301
362
return existing_packages
0 commit comments