Completed
Pull Request — master (#42)
by TJ
05:36 queued 04:35
created

ElementCallable   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 143
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 143
ccs 60
cts 62
cp 0.9677
rs 10
wmc 28

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 25 4
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)):
181
            # TODO: I don't know if there is a case where we want to keep
182
            # it as a list... but for now I'm just going to do this
183 1
            ret = ret[0]
184
185
        # Only check for errors if they haven't told us not to
186 1
        if self._error_on_bad_result:
187
            # Pretend we're getting a dictionary to make our item,
188
            # but it has no reference to `self`. This is so we check
189
            # for errors correctly.
190 1
            temp_dict = copy.deepcopy(msg._asdict())
191 1
            temp_dict.pop(self.name)
192 1
            expected_value = self.make(temp_dict)
193
194
            # Check for an error
195 1
            if expected_value != ret:
196 1
                raise ValueError('Expected value was: {0}, but got: {1}'.format(
197
                    expected_value,
198
                    ret,
199
                ))
200
201 1
        return (ret, buf[self._struct.size:])
202
203 1
    def make(self, msg):
204
        """Return the expected "made" value"""
205
        # If we aren't going to error on a bad result
206
        # and our name is in the message, just send the value
207
        # No need to do extra work.
208 1
        if not self._error_on_bad_result \
209
                and self.name in msg \
210
                and msg[self.name] is not None:
211 1
            return msg[self.name]
212
213 1
        items = []
214
215 1
        for reference in self._func_args:
216 1
            if isinstance(reference, str):
217 1
                index = reference
218 1
                attr = 'make'
219 1
            elif isinstance(reference, bytes):
220 1
                index = reference.decode('utf-8')
221 1
                attr = 'pack'
222
            else:
223
                raise ValueError('Needed str or bytes for the reference')
224
225 1
            items.append(
226
                getattr(self._elements[index], attr)(msg)
227
            )
228
229 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...
230
231 1
        if self.name in msg:
232 1
            if ret != msg[self.name]:
233 1
                raise ValueError('Excepted value: {0}, but got: {1}'.format(ret, msg[self.name]))
234
235
        return ret
236