11# -*- coding: utf-8 -*-
22
3+ import collections
34import operator
45import random
56
67
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 ):
824 """
9- Shuffles `items`, a list, in place.
25+ Shuffles a list of `items` in place.
1026
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.
1228
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.
1531
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 .
1834
1935 If `disable` is function and returns True for ALL items
2036 in a bucket, items in this bucket will remain in their original order.
2137
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.
2453 """
2554
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 :
2857 random .shuffle (items )
2958 return
3059
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 ))
3764 elif disable :
38- return disable (x )
65+ return ItemKey ( disabled = disable (item ) )
3966 else :
40- return key ( x )
67+ return ItemKey ( bucket = bucket_key ( item ) )
4168
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 ()
4473 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 ])
6083
6184 # Shuffle buckets
62- if not preserve_bucket_order :
63- random .shuffle (buckets )
85+ bucket_keys = list ( buckets . keys ())
86+ random .shuffle (bucket_keys )
6487
65- items [:] = [item for bucket in buckets for item in bucket ]
88+ items [:] = [item for bk in bucket_keys for item in buckets [ bk ] ]
6689 return
6790
6891
@@ -79,7 +102,10 @@ def _get_set_of_item_ids(items):
79102
80103def _disable (item ):
81104 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 :
83109 # It is not enough to return just True because in case the shuffling
84110 # is disabled on module, we must preserve the module unchanged
85111 # even when the bucket type for this test run is say package or global.
0 commit comments