1
1
# -*- coding: utf-8 -*-
2
2
3
+ import collections
3
4
import operator
4
5
import random
5
6
6
7
7
- def _shuffle_items (items , key = None , disable = None , preserve_bucket_order = False ):
8
+ """
9
+ `bucket` is a string representing the bucket in which the item falls based on user's chosen
10
+ bucket type.
11
+
12
+ `disabled` is either a falsey value to mark that the item is ready for shuffling (shuffling is not disabled),
13
+ or a truthy value in which case the item won't be shuffled among other items with the same key.
14
+
15
+ In some cases it is important for the `disabled` to be more than just True in order
16
+ to preserve a distinct disabled sub-bucket within a larger bucket and not mix it up with another
17
+ disabled sub-bucket of the same larger bucket.
18
+ """
19
+ ItemKey = collections .namedtuple ('ItemKey' , field_names = ('bucket' , 'disabled' , 'x' ))
20
+ ItemKey .__new__ .__defaults__ = (None , None )
21
+
22
+
23
+ def _shuffle_items (items , bucket_key = None , disable = None , _shuffle_buckets = True ):
8
24
"""
9
- Shuffles `items`, a list, in place.
25
+ Shuffles a list of `items` in place.
10
26
11
- If `key ` is None, items are shuffled across the entire list.
27
+ If `bucket_key ` is None, items are shuffled across the entire list.
12
28
13
- Otherwise `key ` is a function called for each item in `items` to
14
- calculate key of bucket in which the item falls.
29
+ `bucket_key ` is an optional function called for each item in `items` to
30
+ calculate the key of bucket in which the item falls.
15
31
16
- Bucket defines the boundaries across which tests will not
17
- be reordered .
32
+ Bucket defines the boundaries across which items will not
33
+ be shuffled .
18
34
19
35
If `disable` is function and returns True for ALL items
20
36
in a bucket, items in this bucket will remain in their original order.
21
37
22
- `preserve_bucket_order` is only customisable for testing purposes.
23
- There is no use case for predefined bucket order, is there?
38
+ `_shuffle_buckets` is for testing only. Setting it to False may not produce
39
+ the outcome you'd expect in all scenarios because if two non-contiguous sections of items belong
40
+ to the same bucket, the items in these sections will be reshuffled as if they all belonged
41
+ to the first section.
42
+ Example:
43
+ [A1, A2, B1, B2, A3, A4]
44
+
45
+ where letter denotes bucket key,
46
+ with _shuffle_buckets=False may be reshuffled to:
47
+ [B2, B1, A3, A1, A4, A2]
48
+
49
+ or as well to:
50
+ [A3, A2, A4, A1, B1, B2]
51
+
52
+ because all A's belong to the same bucket and will be grouped together.
24
53
"""
25
54
26
- # If `key ` is falsey, shuffle is global.
27
- if not key and not disable :
55
+ # If `bucket_key ` is falsey, shuffle is global.
56
+ if not bucket_key and not disable :
28
57
random .shuffle (items )
29
58
return
30
59
31
- # Use (key(x), disable(x)) as the key because
32
- # when we have a bucket type like package over a disabled module, we must
33
- # not shuffle the disabled module items.
34
- def full_key (x ):
35
- if key and disable :
36
- return key (x ), disable (x )
60
+ def get_full_bucket_key (item ):
61
+ assert bucket_key or disable
62
+ if bucket_key and disable :
63
+ return ItemKey (bucket = bucket_key (item ), disabled = disable (item ))
37
64
elif disable :
38
- return disable (x )
65
+ return ItemKey ( disabled = disable (item ) )
39
66
else :
40
- return key ( x )
67
+ return ItemKey ( bucket = bucket_key ( item ) )
41
68
42
- buckets = []
43
- this_key = '__not_initialised__'
69
+ # For a sequence of items A1, A2, B1, B2, C1, C2,
70
+ # where key(A1) == key(A2) == key(C1) == key(C2),
71
+ # items A1, A2, C1, and C2 will end up in the same bucket.
72
+ buckets = collections .OrderedDict ()
44
73
for item in items :
45
- prev_key = this_key
46
- this_key = full_key (item )
47
- if this_key != prev_key :
48
- buckets .append ([])
49
- buckets [- 1 ].append (item )
50
-
51
- # Shuffle within bucket unless disable(item) evaluates to True for
52
- # the first item in the bucket.
53
- # This assumes that whoever supplied disable function knows this requirement.
54
- # Fixation of individual items in an otherwise shuffled bucket
55
- # is not supported.
56
- for bucket in buckets :
57
- if callable (disable ) and disable (bucket [0 ]):
58
- continue
59
- random .shuffle (bucket )
74
+ full_bucket_key = get_full_bucket_key (item )
75
+ if full_bucket_key not in buckets :
76
+ buckets [full_bucket_key ] = []
77
+ buckets [full_bucket_key ].append (item )
78
+
79
+ # Shuffle inside a bucket
80
+ for bucket in buckets .keys ():
81
+ if not bucket .disabled :
82
+ random .shuffle (buckets [bucket ])
60
83
61
84
# Shuffle buckets
62
- if not preserve_bucket_order :
63
- random .shuffle (buckets )
85
+ bucket_keys = list ( buckets . keys ())
86
+ random .shuffle (bucket_keys )
64
87
65
- items [:] = [item for bucket in buckets for item in bucket ]
88
+ items [:] = [item for bk in bucket_keys for item in buckets [ bk ] ]
66
89
return
67
90
68
91
@@ -79,7 +102,10 @@ def _get_set_of_item_ids(items):
79
102
80
103
def _disable (item ):
81
104
try :
82
- if _is_random_order_disabled (item .module ):
105
+ # In actual test runs, this is returned as a truthy instance of MarkDecorator even when you don't have
106
+ # set the marker. This is a hack.
107
+ is_disabled = _is_random_order_disabled (item .module )
108
+ if is_disabled and is_disabled is True :
83
109
# It is not enough to return just True because in case the shuffling
84
110
# is disabled on module, we must preserve the module unchanged
85
111
# even when the bucket type for this test run is say package or global.
0 commit comments