Completed
Push — master ( 5c1426...4ae943 )
by Aaron
24:12 queued 14:12
created

ElementCallable.make()   F

Complexity

Conditions 9

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
c 1
b 0
f 0
dl 0
loc 33
rs 3
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
import copy
85
import struct
86
87
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
from starstruct.element import register, Element
90
from starstruct.modes import Mode
91
92
93
@register
0 ignored issues
show
best-practice introduced by
Too many instance attributes (8/7)
Loading history...
94
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
    def __init__(self, field: list, mode: Optional[Mode]=Mode.Native, alignment: Optional[int]=1):
103
        # All of the type checks have already been performed by the class
104
        # factory
105
        self.name = field[0]
106
107
        # Callable elements use the normal struct packing method
108
        self.format = field[1]
109
        self._func_ref = field[2]
110
        self._func_args = field[3]
111
112
        if len(field) == 5:
113
            self._error_on_bad_result = field[4]
114
        else:
115
            self._error_on_bad_result = True
116
117
        self.update(mode, alignment)
118
119
    @property
120
    def _struct(self):
121
        return struct.Struct(self._mode.value + self.format)
122
123
    @staticmethod
124
    def valid(field: tuple) -> bool:
125
        """
126
        See :py:func:`starstruct.element.Element.valid`
127
128
        :param field: The items to determine the structure of the element
129
        """
130
        return len(field) >= 4 \
131
            and isinstance(field[0], str) \
132
            and isinstance(field[1], str) \
133
            and callable(field[2]) \
134
            and isinstance(field[3], list)
135
136
    def validate(self, msg):
137
        """
138
        Ensure that the supplied message contains the required information for
139
        this element object to operate.
140
141
        All elements that are Variable must reference valid Length elements.
142
        """
143
        # TODO: Validate the object
144
        self._elements = msg
0 ignored issues
show
Coding Style introduced by
The attribute _elements was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
145
146
        if not all(k in msg
147
                   for k in [arg if isinstance(arg, str) else arg.decode('utf-8')
148
                             for arg in self._func_args]):
149
            raise ValueError('Need all keys to be in the message')
150
151
        pass
0 ignored issues
show
Unused Code introduced by
Unnecessary pass statement
Loading history...
152
153
    def update(self, mode=None, alignment=None):
154
        """change the mode of the struct format"""
155
        if mode:
156
            self._mode = mode
157
158
        if alignment:
159
            self._alignment = alignment
160
161
    def pack(self, msg):
162
        """Pack the provided values into the supplied buffer."""
163
        return self._struct.pack(self.make(msg))
164
165
    def unpack(self, msg, buf):
166
        """Unpack data from the supplied buffer using the initialized format."""
167
        ret = self._struct.unpack_from(buf)
168
        if isinstance(ret, (list, tuple)):
169
            # TODO: I don't know if there is a case where we want to keep
170
            # it as a list... but for now I'm just going to do this
171
            ret = ret[0]
172
173
        # Only check for errors if they haven't told us not to
174
        if self._error_on_bad_result:
175
            # Pretend we're getting a dictionary to make our item,
176
            # but it has no reference to `self`. This is so we check
177
            # for errors correctly.
178
            temp_dict = copy.deepcopy(msg._asdict())
179
            temp_dict.pop(self.name)
180
            expected_value = self.make(temp_dict)
181
182
            # Check for an error
183
            if expected_value != ret:
184
                raise ValueError('Expected value was: {0}, but got: {1}'.format(
185
                    expected_value,
186
                    ret,
187
                ))
188
189
        return (ret, buf[self._struct.size:])
190
191
    def make(self, msg):
192
        """Return the expected "made" value"""
193
        # If we aren't going to error on a bad result
194
        # and our name is in the message, just send the value
195
        # No need to do extra work.
196
        if not self._error_on_bad_result \
197
                and self.name in msg \
198
                and msg[self.name] is not None:
199
            return msg[self.name]
200
201
        items = []
202
203
        for reference in self._func_args:
204
            if isinstance(reference, str):
205
                index = reference
206
                attr = 'make'
207
            elif isinstance(reference, bytes):
208
                index = reference.decode('utf-8')
209
                attr = 'pack'
210
            else:
211
                raise ValueError('Needed str or bytes for the reference')
212
213
            items.append(
214
                getattr(self._elements[index], attr)(msg)
215
            )
216
217
        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...
218
219
        if self.name in msg:
220
            if ret != msg[self.name]:
221
                raise ValueError('Excepted value: {0}, but got: {1}'.format(ret, msg[self.name]))
222
223
        return ret
224