11
11
from pipenv .routines .outdated import do_outdated
12
12
from pipenv .routines .sync import do_sync
13
13
from pipenv .utils import err
14
+ from pipenv .utils .constants import VCS_LIST
14
15
from pipenv .utils .dependencies import (
15
16
expansive_install_req_from_line ,
17
+ get_lockfile_section_using_pipfile_category ,
16
18
get_pipfile_category_using_lockfile_section ,
17
19
)
18
20
from pipenv .utils .processes import run_command
@@ -59,16 +61,17 @@ def do_update(
59
61
60
62
if not outdated :
61
63
# Pre-sync packages for pipdeptree resolution to avoid conflicts
62
- do_sync (
63
- project ,
64
- dev = dev ,
65
- categories = categories ,
66
- python = python ,
67
- bare = bare ,
68
- clear = clear ,
69
- pypi_mirror = pypi_mirror ,
70
- extra_pip_args = extra_pip_args ,
71
- )
64
+ if project .lockfile_exists :
65
+ do_sync (
66
+ project ,
67
+ dev = dev ,
68
+ categories = categories ,
69
+ python = python ,
70
+ bare = bare ,
71
+ clear = clear ,
72
+ pypi_mirror = pypi_mirror ,
73
+ extra_pip_args = extra_pip_args ,
74
+ )
72
75
upgrade (
73
76
project ,
74
77
pre = pre ,
@@ -106,12 +109,11 @@ def get_reverse_dependencies(project) -> Dict[str, Set[Tuple[str, str]]]:
106
109
"""Get reverse dependencies using pipdeptree."""
107
110
pipdeptree_path = Path (pipdeptree .__file__ ).parent
108
111
python_path = project .python ()
109
- cmd_args = [python_path , pipdeptree_path , "-l" , "--reverse" , "--json-tree" ]
112
+ cmd_args = [python_path , str ( pipdeptree_path ) , "-l" , "--reverse" , "--json-tree" ]
110
113
111
114
c = run_command (cmd_args , is_verbose = project .s .is_verbose ())
112
115
if c .returncode != 0 :
113
116
raise PipenvCmdError (c .err , c .out , c .returncode )
114
-
115
117
try :
116
118
dep_tree = json .loads (c .stdout .strip ())
117
119
except json .JSONDecodeError :
@@ -137,7 +139,12 @@ def process_tree_node(n, parents=None):
137
139
138
140
# Start processing the tree from the root nodes
139
141
for node in dep_tree :
140
- process_tree_node (node )
142
+ try :
143
+ process_tree_node (node )
144
+ except Exception as e : # noqa: PERF203
145
+ err .print (
146
+ f"[red bold]Warning[/red bold]: Unable to analyze dependencies: { str (e )} "
147
+ )
141
148
142
149
return reverse_deps
143
150
@@ -166,13 +173,76 @@ def check_version_conflicts(
166
173
specifier_set = SpecifierSet (req_version )
167
174
if not specifier_set .contains (new_version_obj ):
168
175
conflicts .add (dependent )
169
- except Exception :
176
+ except Exception : # noqa: PERF203
170
177
# If we can't parse the version requirement, assume it's a conflict
171
178
conflicts .add (dependent )
172
179
173
180
return conflicts
174
181
175
182
183
+ def get_modified_pipfile_entries (project , pipfile_categories ):
184
+ """
185
+ Detect Pipfile entries that have been modified since the last lock.
186
+ Returns a dict mapping categories to sets of InstallRequirement objects.
187
+ """
188
+ modified = defaultdict (dict )
189
+ lockfile = project .lockfile ()
190
+
191
+ for pipfile_category in pipfile_categories :
192
+ lockfile_category = get_lockfile_section_using_pipfile_category (pipfile_category )
193
+ pipfile_packages = project .parsed_pipfile .get (pipfile_category , {})
194
+ locked_packages = lockfile .get (lockfile_category , {})
195
+
196
+ for package_name , pipfile_entry in pipfile_packages .items ():
197
+ if package_name not in locked_packages :
198
+ # New package
199
+ modified [lockfile_category ][package_name ] = pipfile_entry
200
+ continue
201
+
202
+ locked_entry = locked_packages [package_name ]
203
+ is_modified = False
204
+
205
+ # For string entries, compare directly
206
+ if isinstance (pipfile_entry , str ):
207
+ if pipfile_entry != locked_entry .get ("version" , "" ):
208
+ is_modified = True
209
+
210
+ # For dict entries, need to compare relevant fields
211
+ elif isinstance (pipfile_entry , dict ):
212
+ if "version" in pipfile_entry :
213
+ if pipfile_entry ["version" ] != locked_entry .get ("version" , "" ):
214
+ is_modified = True
215
+
216
+ # Compare VCS fields
217
+ for key in VCS_LIST :
218
+ if key in pipfile_entry :
219
+ if (
220
+ key not in locked_entry
221
+ or pipfile_entry [key ] != locked_entry [key ]
222
+ ):
223
+ is_modified = True
224
+
225
+ # Compare ref for VCS packages
226
+ if "ref" in pipfile_entry :
227
+ if (
228
+ "ref" not in locked_entry
229
+ or pipfile_entry ["ref" ] != locked_entry ["ref" ]
230
+ ):
231
+ is_modified = True
232
+
233
+ # Compare extras
234
+ if "extras" in pipfile_entry :
235
+ pipfile_extras = set (pipfile_entry ["extras" ])
236
+ locked_extras = set (locked_entry .get ("extras" , []))
237
+ if pipfile_extras != locked_extras :
238
+ is_modified = True
239
+
240
+ if is_modified :
241
+ modified [lockfile_category ][package_name ] = pipfile_entry
242
+
243
+ return modified
244
+
245
+
176
246
def upgrade (
177
247
project ,
178
248
pre = False ,
@@ -206,17 +276,14 @@ def upgrade(
206
276
categories .insert (0 , "default" )
207
277
208
278
# Get current dependency graph
209
- try :
210
- reverse_deps = get_reverse_dependencies (project )
211
- except Exception as e :
212
- err .print (
213
- f"[red bold]Warning[/red bold]: Unable to analyze dependencies: { str (e )} "
214
- )
215
- reverse_deps = {}
279
+ reverse_deps = get_reverse_dependencies (project )
216
280
217
281
index_name = None
218
282
if index_url :
219
- index_name = add_index_to_pipfile (project , index_url )
283
+ if project .get_index_by_url (index_url ):
284
+ index_name = project .get_index_by_url (index_url )["name" ]
285
+ else :
286
+ index_name = add_index_to_pipfile (project , index_url )
220
287
221
288
if extra_pip_args :
222
289
os .environ ["PIPENV_EXTRA_PIP_ARGS" ] = json .dumps (extra_pip_args )
@@ -245,76 +312,71 @@ def upgrade(
245
312
)
246
313
sys .exit (1 )
247
314
248
- requested_install_reqs = defaultdict (dict )
315
+ # Create clean package_args first
316
+ has_package_args = False
317
+ if package_args :
318
+ has_package_args = True
319
+
249
320
requested_packages = defaultdict (dict )
250
321
for category in categories :
251
322
pipfile_category = get_pipfile_category_using_lockfile_section (category )
252
323
324
+ # Get modified entries if no explicit packages specified
325
+ if not package_args and project .lockfile_exists :
326
+ modified_entries = get_modified_pipfile_entries (project , [pipfile_category ])
327
+ for name , entry in modified_entries [category ].items ():
328
+ requested_packages [pipfile_category ][name ] = entry
329
+
330
+ # Process each package arg
253
331
for package in package_args [:]:
254
332
install_req , _ = expansive_install_req_from_line (package , expand_env = True )
255
- if index_name :
256
- install_req .index = index_name
257
333
258
334
name , normalized_name , pipfile_entry = project .generate_package_pipfile_entry (
259
- install_req , package , category = pipfile_category
260
- )
261
- project .add_pipfile_entry_to_pipfile (
262
- name , normalized_name , pipfile_entry , category = pipfile_category
335
+ install_req , package , category = pipfile_category , index_name = index_name
263
336
)
337
+
338
+ # Only add to Pipfile if explicitly requested
339
+ if has_package_args :
340
+ project .add_pipfile_entry_to_pipfile (
341
+ name , normalized_name , pipfile_entry , category = pipfile_category
342
+ )
343
+
264
344
requested_packages [pipfile_category ][normalized_name ] = pipfile_entry
265
- requested_install_reqs [pipfile_category ][normalized_name ] = install_req
266
345
267
- # Consider reverse packages in reverse_deps
346
+ # Handle reverse dependencies
268
347
if normalized_name in reverse_deps :
269
- for dependency , req_version in reverse_deps [normalized_name ]:
270
- if req_version == "Any" :
271
- package_args .append (dependency )
272
- pipfile_entry = project .get_pipfile_entry (
273
- dependency , category = pipfile_category
274
- )
275
- requested_packages [pipfile_category ][dependency ] = (
276
- pipfile_entry if pipfile_entry else "*"
277
- )
348
+ for dependency , _ in reverse_deps [normalized_name ]:
349
+ pipfile_entry = project .get_pipfile_entry (
350
+ dependency , category = pipfile_category
351
+ )
352
+ if not pipfile_entry :
353
+ requested_packages [pipfile_category ][dependency ] = {
354
+ normalized_name : "*"
355
+ }
278
356
continue
357
+ requested_packages [pipfile_category ][dependency ] = pipfile_entry
279
358
280
- try : # Otherwise we have a specific version requirement
281
- specifier_set = SpecifierSet (req_version )
282
- package_args .append (f"{ dependency } =={ specifier_set } " )
283
- pipfile_entry = project .get_pipfile_entry (
284
- dependency , category = pipfile_category
285
- )
286
- requested_packages [pipfile_category ][dependency ] = (
287
- pipfile_entry if pipfile_entry else "*"
288
- )
289
-
290
- except Exception as e :
291
- err .print (
292
- f"[bold][yellow]Warning:[/yellow][/bold] "
293
- f"Unable to parse version specifier for { dependency } : { str (e )} "
294
- )
295
-
296
- if not package_args :
297
- err .print ("Nothing to upgrade!" )
298
- return
299
- else :
359
+ # When packages are not provided we simply perform full resolution
360
+ upgrade_lock_data = None
361
+ if requested_packages [pipfile_category ]:
300
362
err .print (
301
363
f"[bold][green]Upgrading[/bold][/green] { ', ' .join (package_args )} in [{ category } ] dependencies."
302
364
)
303
365
304
- # Resolve package to generate constraints of new package data
305
- upgrade_lock_data = venv_resolve_deps (
306
- requested_packages [pipfile_category ],
307
- which = project ._which ,
308
- project = project ,
309
- lockfile = {},
310
- pipfile_category = pipfile_category ,
311
- pre = pre ,
312
- allow_global = system ,
313
- pypi_mirror = pypi_mirror ,
314
- )
315
- if not upgrade_lock_data :
316
- err .print ("Nothing to upgrade!" )
317
- return
366
+ # Resolve package to generate constraints of new package data
367
+ upgrade_lock_data = venv_resolve_deps (
368
+ requested_packages [pipfile_category ],
369
+ which = project ._which ,
370
+ project = project ,
371
+ lockfile = {},
372
+ pipfile_category = pipfile_category ,
373
+ pre = pre ,
374
+ allow_global = system ,
375
+ pypi_mirror = pypi_mirror ,
376
+ )
377
+ if not upgrade_lock_data :
378
+ err .print ("Nothing to upgrade!" )
379
+ return
318
380
319
381
complete_packages = project .parsed_pipfile .get (pipfile_category , {})
320
382
@@ -329,21 +391,29 @@ def upgrade(
329
391
pypi_mirror = pypi_mirror ,
330
392
)
331
393
332
- # Verify no conflicts were introduced during resolution
333
- for package_name , package_data in full_lock_resolution .items ():
334
- if package_name in upgrade_lock_data :
335
- version = package_data .get ("version" , "" ).replace ("==" , "" )
336
- if not version :
337
- # Either vcs or file package
338
- continue
339
-
340
- # Update lockfile with verified resolution data
341
- for package_name in upgrade_lock_data :
342
- correct_package_lock = full_lock_resolution .get (package_name )
343
- if correct_package_lock :
344
- if category not in lockfile :
345
- lockfile [category ] = {}
346
- lockfile [category ][package_name ] = correct_package_lock
394
+ if upgrade_lock_data is None :
395
+ for package_name , package_data in full_lock_resolution .items ():
396
+ lockfile [category ][package_name ] = package_data
397
+ else : # Upgrade a subset of packages
398
+ # Verify no conflicts were introduced during resolution
399
+ for package_name , package_data in full_lock_resolution .items ():
400
+ if package_name in upgrade_lock_data :
401
+ version = package_data .get ("version" , "" ).replace ("==" , "" )
402
+ if not version :
403
+ # Either vcs or file package
404
+ continue
405
+
406
+ # Update lockfile with verified resolution data
407
+ for package_name in upgrade_lock_data :
408
+ correct_package_lock = full_lock_resolution .get (package_name )
409
+ if correct_package_lock :
410
+ if category not in lockfile :
411
+ lockfile [category ] = {}
412
+ lockfile [category ][package_name ] = correct_package_lock
413
+
414
+ # reset package args in case of multiple categories being processed
415
+ if has_package_args is False :
416
+ package_args = []
347
417
348
418
lockfile .update ({"_meta" : project .get_lockfile_meta ()})
349
419
project .write_lockfile (lockfile )
0 commit comments