File: shuffler.py

package info (click to toggle)
python-pytest-random-order 1.1.1%2Bds-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 276 kB
  • sloc: python: 704; makefile: 12; sh: 6
file content (114 lines) | stat: -rw-r--r-- 4,009 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# -*- coding: utf-8 -*-

import random
from collections import OrderedDict, namedtuple

from random_order.cache import FAILED_FIRST_LAST_FAILED_BUCKET_KEY

"""
`bucket` is a string representing the bucket in which the item falls based on user's chosen
bucket type.

`disabled` is either a falsey value to mark that the item is ready for shuffling (shuffling is not disabled),
or a truthy value in which case the item won't be shuffled among other items with the same key.

In some cases it is important for the `disabled` to be more than just True in order
to preserve a distinct disabled sub-bucket within a larger bucket and not mix it up with another
disabled sub-bucket of the same larger bucket.
"""
ItemKey = namedtuple('ItemKey', field_names=('bucket', 'disabled', 'x'))
ItemKey.__new__.__defaults__ = (None, None)


def _shuffle_items(items, bucket_key=None, disable=None, seed=None, session=None):
    """
    Shuffles a list of `items` in place.

    If `bucket_key` is None, items are shuffled across the entire list.

    `bucket_key` is an optional function called for each item in `items` to
    calculate the key of bucket in which the item falls.

    Bucket defines the boundaries across which items will not
    be shuffled.

    `disable` is a function that takes an item and returns a falsey value
    if this item is ok to be shuffled. It returns a truthy value otherwise and
    the truthy value is used as part of the item's key when determining the bucket
    it belongs to.
    """

    if seed is not None:
        random.seed(seed)

    # If `bucket_key` is falsey, shuffle is global.
    if not bucket_key and not disable:
        random.shuffle(items)
        return

    def get_full_bucket_key(item):
        assert bucket_key or disable
        if bucket_key and disable:
            return ItemKey(bucket=bucket_key(item, session), disabled=disable(item, session))
        elif disable:
            return ItemKey(disabled=disable(item, session))
        else:
            return ItemKey(bucket=bucket_key(item, session))

    # For a sequence of items A1, A2, B1, B2, C1, C2,
    # where key(A1) == key(A2) == key(C1) == key(C2),
    # items A1, A2, C1, and C2 will end up in the same bucket.
    buckets = OrderedDict()
    for item in items:
        full_bucket_key = get_full_bucket_key(item)
        if full_bucket_key not in buckets:
            buckets[full_bucket_key] = []
        buckets[full_bucket_key].append(item)

    # Shuffle inside a bucket

    bucket_keys = list(buckets.keys())

    for full_bucket_key in buckets.keys():
        if full_bucket_key.bucket == FAILED_FIRST_LAST_FAILED_BUCKET_KEY:
            # Do not shuffle the last failed bucket
            continue

        if not full_bucket_key.disabled:
            random.shuffle(buckets[full_bucket_key])

    # Shuffle buckets

    # Only the first bucket can be FAILED_FIRST_LAST_FAILED_BUCKET_KEY
    if bucket_keys and bucket_keys[0].bucket == FAILED_FIRST_LAST_FAILED_BUCKET_KEY:
        new_bucket_keys = list(buckets.keys())[1:]
        random.shuffle(new_bucket_keys)
        new_bucket_keys.insert(0, bucket_keys[0])
    else:
        new_bucket_keys = list(buckets.keys())
        random.shuffle(new_bucket_keys)

    items[:] = [item for bk in new_bucket_keys for item in buckets[bk]]
    return


def _get_set_of_item_ids(items):
    s = {}
    try:
        s = set(item.nodeid for item in items)
    finally:
        return s


def _disable(item, session):
    if hasattr(item, 'get_closest_marker'):
        marker = item.get_closest_marker('random_order')
    else:
        marker = item.get_marker('random_order')
    if marker:
        is_disabled = marker.kwargs.get('disabled', False)
        if is_disabled:
            # A test item can only be disabled in its parent context -- where it is part of some order.
            # We use parent name as the key so that all children of the same parent get the same disabled key.
            return item.parent.name
    return False