1
1
import collections
2
+ import itertools
2
3
import operator
3
4
4
5
from .providers import AbstractResolver
@@ -191,8 +192,8 @@ def _remove_information_from_criteria(self, criteria, parents):
191
192
information
192
193
for information in criterion .information
193
194
if (
194
- information [ 1 ] is None
195
- or self ._p .identify (information [ 1 ] ) not in parents
195
+ information . parent is None
196
+ or self ._p .identify (information . parent ) not in parents
196
197
)
197
198
],
198
199
criterion .incompatibilities ,
@@ -266,8 +267,8 @@ def _attempt_to_pin_criterion(self, name):
266
267
# end, signal for backtracking.
267
268
return causes
268
269
269
- def _backtrack (self ):
270
- """Perform backtracking .
270
+ def _backjump (self , causes ):
271
+ """Perform backjumping .
271
272
272
273
When we enter here, the stack is like this::
273
274
@@ -283,22 +284,46 @@ def _backtrack(self):
283
284
284
285
Each iteration of the loop will:
285
286
286
- 1. Discard Z.
287
- 2. Discard Y but remember its incompatibility information gathered
287
+ 1. Identify Z. The incompatibility is not always caused by the latest
288
+ state. For example, given three requirements A, B and C, with
289
+ dependencies A1, B1 and C1, where A1 and B1 are incompatible: the
290
+ last state might be related to C, so we want to discard the
291
+ previous state.
292
+ 2. Discard Z.
293
+ 3. Discard Y but remember its incompatibility information gathered
288
294
previously, and the failure we're dealing with right now.
289
- 3 . Push a new state Y' based on X, and apply the incompatibility
295
+ 4 . Push a new state Y' based on X, and apply the incompatibility
290
296
information from Y to Y'.
291
- 4a . If this causes Y' to conflict, we need to backtrack again. Make Y'
297
+ 5a . If this causes Y' to conflict, we need to backtrack again. Make Y'
292
298
the new Z and go back to step 2.
293
- 4b . If the incompatibilities apply cleanly, end backtracking.
299
+ 5b . If the incompatibilities apply cleanly, end backtracking.
294
300
"""
301
+ incompatible_reqs = itertools .chain (
302
+ (c .parent for c in causes if c .parent is not None ),
303
+ (c .requirement for c in causes ),
304
+ )
305
+ incompatible_deps = {self ._p .identify (r ) for r in incompatible_reqs }
295
306
while len (self ._states ) >= 3 :
296
307
# Remove the state that triggered backtracking.
297
308
del self ._states [- 1 ]
298
309
299
- # Retrieve the last candidate pin and known incompatibilities.
300
- broken_state = self ._states .pop ()
301
- name , candidate = broken_state .mapping .popitem ()
310
+ # Ensure to backtrack to a state that caused the incompatibility
311
+ incompatible_state = False
312
+ while not incompatible_state :
313
+ # Retrieve the last candidate pin and known incompatibilities.
314
+ try :
315
+ broken_state = self ._states .pop ()
316
+ name , candidate = broken_state .mapping .popitem ()
317
+ except (IndexError , KeyError ):
318
+ raise ResolutionImpossible (causes )
319
+ current_dependencies = {
320
+ self ._p .identify (d )
321
+ for d in self ._p .get_dependencies (candidate )
322
+ }
323
+ incompatible_state = not current_dependencies .isdisjoint (
324
+ incompatible_deps
325
+ )
326
+
302
327
incompatibilities_from_broken = [
303
328
(k , list (v .incompatibilities ))
304
329
for k , v in broken_state .criteria .items ()
@@ -403,10 +428,10 @@ def resolve(self, requirements, max_rounds):
403
428
404
429
if failure_causes :
405
430
causes = [i for c in failure_causes for i in c .information ]
406
- # Backtrack if pinning fails. The backtrack process puts us in
431
+ # Backjump if pinning fails. The backjump process puts us in
407
432
# an unpinned state, so we can work on it in the next round.
408
433
self ._r .resolving_conflicts (causes = causes )
409
- success = self ._backtrack ( )
434
+ success = self ._backjump ( causes )
410
435
self .state .backtrack_causes [:] = causes
411
436
412
437
# Dead ends everywhere. Give up.
0 commit comments