Completed
Pull Request — master (#42)
by TJ
01:41
created

ElementCallable   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 144
Duplicated Lines 0 %

Test Coverage

Coverage 96.77%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 144
ccs 60
cts 62
cp 0.9677
rs 10
wmc 29

8 Methods

Rating   Name   Duplication   Size   Complexity  
A valid() 0 12 1
A _struct() 0 3 1
B validate() 0 11 5
A update() 0 7 3
A __init__() 0 20 2
A pack() 0 14 3
B unpack() 0 26 5
F make() 0 33 9
1
"""
2
Element callable.
3
4
Call a function to validate data.
5
6
.. code-block:: python
7
8
    from binascii import crc32
9
10
    ExampleMessage = Message('VarTest', [('x', 'B'), ('y', 'B')])
11
12
    CRCedMessage = Message('CRCedMessage', [
13
       ('data', ExampleMessage),                    # A data field that has the example message in it
14
       ('crc', 'I', crc32, ['data']),               # Crc the data, and give an error if we have something unexpected
15
       ('crc', 'I', crc32, ['data'], False),        # Crc the data, but don't give an error
16
   ])
17
18
Following creating this message, you have two options:
19
20
1. Specify a value. The function will be used to validate the value.
21
22
.. code-block:: python
23
24
    def adder(x, y):
25
        return x + y
26
27
    AdderMessage = Message('AdderMessage', [
28
        ('item_a', 'H'),
29
        ('item_b', 'B'),
30
        ('function_data', 'I', adder, ['item_a', 'item_b']),
31
    ])
32
33
    test_data = {
34
        'item_a': 2,
35
        'item_b': 5,
36
        'function_data': 7,
37
    }
38
39
    made = AdderMessage.make(test_data)
40
    assert made.item_a == 2
41
    assert made.item_b == 5
42
    assert made.function_data == 7
43
44
    # If you specify the wrong value, you'll get a ValueError
45
    test_data = {
46
        'item_a': 2,
47
        'item_b': 5,
48
        'function_data': 33,
49
    }
50
51
    try:
52
        made = AdderMessage.make(test_data)
53
    except ValueError:
54
        print('Told you so')
55
56
    # Unless you specify `False` in your original item, then
57
    # nobody will care.
58
59
2. Use the function to generate a value.
60
61
.. code-block:: python
62
63
    def adder(x, y):
64
        return x + y
65
66
    AdderMessage = Message('AdderMessage', [
67
        ('item_a', 'H'),
68
        ('item_b', 'B'),
69
        ('function_data', 'I', adder, ['item_a', 'item_b']),
70
    ])
71
72
    test_data = {
73
        'item_a': 2,
74
        'item_b': 5,
75
    }
76
77
    made = AdderMessage.make(test_data)
78
    assert made.item_a == 2
79
    assert made.item_b == 5
80
    assert made.function_data == 7
81
82
"""
83
84 1
import copy
85 1
import struct
86
87 1
from typing import Optional
0 ignored issues
show
Configuration introduced by
The import typing could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
88
89 1
from starstruct.element import register, Element
90 1
from starstruct.modes import Mode
91
92
93 1
@register
94 1
class ElementCallable(Element):
95
    """
96
    Initialize a StarStruct element object.
97
98
    :param field: The fields passed into the constructor of the element
99
    :param mode: The mode in which to pack the bytes
100
    :param alignment: Number of bytes to align to
101
    """
102
103
    # pylint: disable=too-many-instance-attributes
104 1
    def __init__(self, field: list, mode: Optional[Mode]=Mode.Native, alignment: Optional[int]=1):
105
        # All of the type checks have already been performed by the class
106
        # factory
107 1
        self.name = field[0]
108
109
        # Callable elements use the normal struct packing method
110 1
        self.format = field[1]
111 1
        self._func_ref = field[2]
112 1
        self._func_args = field[3]
113
114 1
        if len(field) == 5:
115 1
            self._error_on_bad_result = field[4]
116
        else:
117 1
            self._error_on_bad_result = True
118
119 1
        self._elements = []
120
121 1
        self.update(mode, alignment)
122
123 1
        self._elements = []
124
125 1
    @property
126
    def _struct(self):
127 1
        return struct.Struct(self._mode.value + self.format)
128
129 1
    @staticmethod
130 1
    def valid(field: tuple) -> bool:
131
        """
132
        See :py:func:`starstruct.element.Element.valid`
133
134
        :param field: The items to determine the structure of the element
135
        """
136 1
        return len(field) >= 4 \
137
            and isinstance(field[0], str) \
138
            and isinstance(field[1], str) \
139
            and callable(field[2]) \
140
            and isinstance(field[3], list)
141
142 1
    def validate(self, msg):
143
        """
144
        Ensure that the supplied message contains the required information for
145
        this element object to operate.
146
147
        All elements that are Variable must reference valid Length elements.
148
        """
149 1
        if not all(k in msg
150
                   for k in [arg if isinstance(arg, str) else arg.decode('utf-8')
151
                             for arg in self._func_args]):
152
            raise ValueError('Need all keys to be in the message')
153
154 1
    def update(self, mode=None, alignment=None):
155
        """change the mode of the struct format"""
156 1
        if mode:
157 1
            self._mode = mode
158
159 1
        if alignment:
160 1
            self._alignment = alignment
161
162 1
    def pack(self, msg):
163
        """Pack the provided values into the supplied buffer."""
164 1
        packer = self.make(msg)
165
166
        # Test if the object is iterable
167
        # If it isn't, then turn it into a list
168 1
        try:
169 1
            _ = (p for p in packer)
170 1
        except TypeError:
171 1
            packer = [packer]
172
173
        # Unpack the items for struct to allow for mutli-value
174
        # items to be passed in.
175 1
        return self._struct.pack(*packer)
0 ignored issues
show
Coding Style introduced by
Usage of * or ** arguments should usually be done with care.

Generally, there is nothing wrong with usage of * or ** arguments. For readability of the code base, we suggest to not over-use these language constructs though.

For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect.

Loading history...
176
177 1
    def unpack(self, msg, buf):
178
        """Unpack data from the supplied buffer using the initialized format."""
179 1
        ret = self._struct.unpack_from(buf)
180 1
        if isinstance(ret, (list, tuple)) and len(ret) == 1:
181
            # We only change it not to a list if we expected one value.
182
            # Otherwise, we keep it as a list, because that's what we would
183
            # expect (like for a 16I type of struct
184 1
            ret = ret[0]
185
186
        # Only check for errors if they haven't told us not to
187 1
        if self._error_on_bad_result:
188
            # Pretend we're getting a dictionary to make our item,
189
            # but it has no reference to `self`. This is so we check
190
            # for errors correctly.
191 1
            temp_dict = copy.deepcopy(msg._asdict())
192 1
            temp_dict.pop(self.name)
193 1
            expected_value = self.make(temp_dict)
194
195
            # Check for an error
196 1
            if expected_value != ret:
197 1
                raise ValueError('Expected value was: {0}, but got: {1}'.format(
198
                    expected_value,
199
                    ret,
200
                ))
201
202 1
        return (ret, buf[self._struct.size:])
203
204 1
    def make(self, msg):
205
        """Return the expected "made" value"""
206
        # If we aren't going to error on a bad result
207
        # and our name is in the message, just send the value
208
        # No need to do extra work.
209 1
        if not self._error_on_bad_result \
210
                and self.name in msg \
211
                and msg[self.name] is not None:
212 1
            return msg[self.name]
213
214 1
        items = []
215
216 1
        for reference in self._func_args:
217 1
            if isinstance(reference, str):
218 1
                index = reference
219 1
                attr = 'make'
220 1
            elif isinstance(reference, bytes):
221 1
                index = reference.decode('utf-8')
222 1
                attr = 'pack'
223
            else:
224
                raise ValueError('Needed str or bytes for the reference')
225
226 1
            items.append(
227
                getattr(self._elements[index], attr)(msg)
228
            )
229
230 1
        ret = self._func_ref(*items)
0 ignored issues
show
Coding Style introduced by
Usage of * or ** arguments should usually be done with care.

Generally, there is nothing wrong with usage of * or ** arguments. For readability of the code base, we suggest to not over-use these language constructs though.

For more information, we can recommend this blog post from Ned Batchelder including its comments which also touches this aspect.

Loading history...
231
232 1
        if self.name in msg:
233 1
            if ret != msg[self.name]:
234 1
                raise ValueError('Excepted value: {0}, but got: {1}'.format(ret, msg[self.name]))
235
236
        return ret
237