Completed
Pull Request — master (#35)
by
unknown
09:58
created

ElementCallable   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 125
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 125
rs 10
wmc 26

8 Methods

Rating   Name   Duplication   Size   Complexity  
A valid() 0 12 1
A _struct() 0 3 1
A unpack() 0 19 4
F make() 0 33 9
B validate() 0 16 5
A update() 0 7 3
A pack() 0 3 1
A __init__() 0 16 2
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']),
15
   ])
16
17
Following creating this message, you have two options:
18
19
1. Specify a value. The function will be used to validate the value.
20
2. Use the function to generate a value.
21
"""
22
23
import copy
24
import struct
25
26
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...
27
28
from starstruct.element import register, Element
29
from starstruct.modes import Mode
30
31
32
@register
0 ignored issues
show
best-practice introduced by
Too many instance attributes (8/7)
Loading history...
33
class ElementCallable(Element):
34
    """
35
    Initialize a StarStruct element object.
36
37
    :param field: The fields passed into the constructor of the element
38
    :param mode: The mode in which to pack the bytes
39
    :param alignment: Number of bytes to align to
40
    """
41
    def __init__(self, field: list, mode: Optional[Mode]=Mode.Native, alignment: Optional[int]=1):
42
        # All of the type checks have already been performed by the class
43
        # factory
44
        self.name = field[0]
45
46
        # Callable elements use the normal struct packing method
47
        self.format = field[1]
48
        self._func_ref = field[2]
49
        self._func_args = field[3]
50
51
        if len(field) == 5:
52
            self._error_on_bad_result = field[4]
53
        else:
54
            self._error_on_bad_result = True
55
56
        self.update(mode, alignment)
57
58
    @property
59
    def _struct(self):
60
        return struct.Struct(self._mode.value + self.format)
61
62
    @staticmethod
63
    def valid(field: tuple) -> bool:
64
        """
65
        See :py:func:`starstruct.element.Element.valid`
66
67
        :param field: The items to determine the structure of the element
68
        """
69
        return len(field) >= 4 \
70
            and isinstance(field[0], str) \
71
            and isinstance(field[1], str) \
72
            and callable(field[2]) \
73
            and isinstance(field[3], list)
74
75
    def validate(self, msg):
76
        """
77
        Ensure that the supplied message contains the required information for
78
        this element object to operate.
79
80
        All elements that are Variable must reference valid Length elements.
81
        """
82
        # TODO: Validate the object
83
        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...
84
85
        if not all(k in msg
86
                   for k in [arg if isinstance(arg, str) else arg.decode('utf-8')
87
                             for arg in self._func_args]):
88
            raise ValueError('Need all keys to be in the message')
89
90
        pass
0 ignored issues
show
Unused Code introduced by
Unnecessary pass statement
Loading history...
91
92
    def update(self, mode=None, alignment=None):
93
        """change the mode of the struct format"""
94
        if mode:
95
            self._mode = mode
96
97
        if alignment:
98
            self._alignment = alignment
99
100
    def pack(self, msg):
101
        """Pack the provided values into the supplied buffer."""
102
        return self._struct.pack(self.make(msg))
103
104
    def unpack(self, msg, buf):
105
        """Unpack data from the supplied buffer using the initialized format."""
106
        ret = self._struct.unpack_from(buf)
107
        if isinstance(ret, (list, tuple)):
108
            # TODO: I don't know if there is a case where we want to keep
109
            # it as a list... but for now I'm just going to do this
110
            ret = ret[0]
111
112
        if self._error_on_bad_result:
113
            temp_dict = copy.deepcopy(msg._asdict())
114
            temp_dict.pop(self.name)
115
            expected_value = self.make(temp_dict)
116
            if expected_value != ret:
117
                raise ValueError('Expected value was: {0}, but got: {1}'.format(
118
                    expected_value,
119
                    ret,
120
                ))
121
122
        return (ret, buf[self._struct.size:])
123
124
    def make(self, msg):
125
        """Return the expected "made" value"""
126
        # If we aren't going to error on a bad result
127
        # and our name is in the message, just send the value
128
        # No need to do extra work.
129
        if not self._error_on_bad_result \
130
                and self.name in msg \
131
                and msg[self.name] is not None:
132
            return msg[self.name]
133
134
        items = []
135
136
        for reference in self._func_args:
137
            if isinstance(reference, str):
138
                index = reference
139
                attr = 'make'
140
            elif isinstance(reference, bytes):
141
                index = reference.decode('utf-8')
142
                attr = 'pack'
143
            else:
144
                raise ValueError('Needed str or bytes for the reference')
145
146
            items.append(
147
                getattr(self._elements[index], attr)(msg)
148
            )
149
150
        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...
151
152
        if self.name in msg:
153
            if ret != msg[self.name]:
154
                raise ValueError('Excepted value: {0}, but got: {1}'.format(ret, msg[self.name]))
155
156
        return ret
157