Completed
Push — master ( be2f4b...da4910 )
by Aaron
01:57
created

PackedBitField.__str__()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
c 1
b 0
f 1
dl 0
loc 2
ccs 2
cts 2
cp 1
crap 1
rs 10
1 1
import re
2 1
import collections
3 1
import functools
4 1
import starstruct.bitfield
5
6
7 1
class PackedBitField(object):
8
    """
9
    A class that is used to bitwise pack/unpack one or more enumerations or
10
    bitfields to/from an integer value
11
    """
12 1
    def __init__(self, *args):
13
        # Ensure that there are no duplicate enum or bitfield types in the list
14 1
        member_bitfields = (k for k in args if isinstance(k, starstruct.bitfield.BitField))
15 1
        all_enums = args + tuple(b.enum for b in member_bitfields)
16 1
        if len(all_enums) != len(set(all_enums)):
17 1
            msg = 'Duplicate fields not allowed: {}'.format(args)
18 1
            raise TypeError(msg)
19
20
        # Ensure that all fields are either bitfields, or enums with all
21
        # members of each enumeration type are integers
22 1
        member_enums = (k for k in args if not isinstance(k, starstruct.bitfield.BitField))
23 1
        for key in member_enums:
24 1
            if not all(isinstance(member.value, int) for member in key):
25 1
                msg = 'Enum {} members must have integer values'.format(repr(key))
26 1
                raise TypeError(msg)
27
28
        # Allow enum to be a list of enumerations that need bitpacked sequentially
29 1
        self._fields = collections.OrderedDict(zip(args, [{}] * len(args)))
30
31
        # Determine the bits required for the enumeration so they can all be
32
        # packed correctly.  Assume that the furthest right enumeration should
33
        # have a bit offset of 0.
34 1
        total_width = 0
35 1
        for key in reversed(self._fields):
0 ignored issues
show
Bug introduced by
The first reversed() argument is not a sequence
Loading history...
36 1
            if isinstance(key, starstruct.bitfield.BitField):
37 1
                all_bits = key.bit_mask
38
            else:
39 1
                all_bits = functools.reduce(lambda x, y: x | y, [k.value for k in key])
40
41 1
            self._fields[key] = {
42
                'offset': total_width,
43
                'mask': all_bits << total_width,
44
                'width': all_bits.bit_length(),
45
            }
46
47 1
            total_width += self._fields[key]['width']
48
49
        # Track the bit mask and bit length attributes just like BitField
50 1
        self.bit_mask = functools.reduce(lambda x, y: x | y, [v['mask'] for v in self._fields.values()])
51 1
        self.bit_length = total_width
52
53 1
    def __repr__(self):
54
        return 'PackedBitField({})'.format(list(self._fields))
55
56 1
    def __str__(self):
57 1
        return 'PackedBitField({})'.format(list(self._fields))
58
59 1
    def find_value(self, item):
60
        """
61
        Take a value, determine if it matches one, and only one, of the member fields
62
        """
63
        # pylint: disable=too-many-branches
64
65
        # Split the member fields into bitfields and enums
66 1
        member_enums = [k for k in self._fields if not isinstance(k, starstruct.bitfield.BitField)]
67 1
        member_bitfields = [k for k in self._fields if isinstance(k, starstruct.bitfield.BitField)]
68
69
        # See if the supplied value is an enum or bitfield value
70 1
        matches = []
71 1
        for key in member_bitfields:
72 1
            try:
73 1
                matches.append((key.find_value(item), key))
74 1
            except ValueError:
75
                # This just means it isn't a member of this bitfield
76 1
                pass
77
78
        # Also check for matches in the enums.  This helps guard against
79
        # ambiguous inputs where the bitfield and enum types overlap.
80 1
        if isinstance(item, tuple(member_enums)):
81 1
            for key in member_enums:
82 1
                if isinstance(item, key):
83
                    # This is guaranteed a unique match, so return now
84 1
                    return (item, key)
85 1
        elif isinstance(item, str):
86
            # If it's a string, then check it against the enum fields
87
            # (bitfields should already have been validated)
88 1
            for key in member_enums:
89 1
                try:
90 1
                    matches.append((getattr(key, item), key))
91 1
                except AttributeError:
92
                    # This is the normal error to throw if the enum name is
93
                    # not valid for this enumeration type.  Check the next enum.
94 1
                    pass
95
        else:
96
            # Lastly, assume that the item is an integer value, attempt to
97
            # convert it to one of the enum values to ensure it is a valid
98
            # value.  But if it matches more than one member field, we are
99
            # unable to pack this properly.
100 1
            for key in member_enums:
101 1
                try:
102 1
                    matches.append((key(item), key))
103 1
                except ValueError:
104
                    # This just means that the value is not valid for a
105
                    # specific enum type, check all enums for a match before
106
                    # raising a ValueError
107 1
                    pass
108
109 1
        if len(matches) == 1:
110 1
            return matches[0]
111 1
        elif len(matches) < 1:
112 1
            msg = '{} is not a valid {}'.format(item, list(self._fields))
113 1
            raise ValueError(msg)
114 1
        elif len(matches) > 1:
115 1
            msg = '{} is not a unique {}'.format(item, list(self._fields))
116 1
            raise ValueError(msg)
117
118 1
    def pack(self, arg):
119
        """
120
        Take a list (or single value) and bitwise-or all the values together
121
        """
122 1
        value = 0
123 1
        if arg is not None:
124
            # Handle a variety of inputs: list or single, enum or raw
125 1
            if hasattr(arg, '__iter__'):
126 1
                arg_list = arg
127
            else:
128 1
                arg_list = [arg]
129
130 1
            for item in arg_list:
131 1
                (enum_val, key) = self.find_value(item)
132 1
                value |= (enum_val.value << self._fields[key]['offset'])
133
134 1
        return value
135
136 1
    def unpack(self, val):
137
        """
138
        Take a single number and split it out into all values that are present
139
        """
140 1
        values = []
141 1
        for key in self._fields:
142 1
            enum_specific_bits = (val & self._fields[key]['mask']) >> self._fields[key]['offset']
143 1
            if isinstance(key, starstruct.bitfield.BitField):
144 1
                values.extend(key.unpack(enum_specific_bits))
145
            else:
146 1
                try:
147 1
                    values.append(key(enum_specific_bits))
148 1
                except ValueError:
149 1
                    enum_name = re.match(r"<enum '(\S+)'>", str(key)).group(1)
150 1
                    msg = '{} is not a valid {}'.format(enum_specific_bits, enum_name)
151 1
                    raise ValueError(msg)
152 1
        return frozenset(values)
153
154 1
    def make(self, arg):
155
        """
156
        Take an input list and return a frozenset
157
158
        useful for testing
159
        """
160
        values = []
161
        if arg is not None:
162
            # Handle a variety of inputs: list or single, enum or raw
163
            if hasattr(arg, '__iter__'):
164
                arg_list = arg
165
            else:
166
                arg_list = [arg]
167
168
            for item in arg_list:
169
                values.append(self.find_value(item)[0])
170
171
        # return this list as a frozenset
172
        return frozenset(values)
173