Completed
Push — master ( 2343e4...9a0979 )
by
unknown
07:32
created

ElementString.make()   F

Complexity

Conditions 14

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 48.8837

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 14
c 2
b 0
f 0
dl 0
loc 31
rs 2.7581
ccs 7
cts 16
cp 0.4375
crap 48.8837

How to fix   Complexity   

Complexity

Complex classes like ElementString.make() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""StarStruct element class."""
2
3 1
import struct
4 1
import re
5
6 1
from starstruct.element import register, Element
7 1
from starstruct.modes import Mode
8
9
10 1
@register
11 1
class ElementString(Element):
12
    """
13
    A StarStruct element for strings, because standard string treatment of
14
    pack/unpack can be inconvenient.
15
16
    This element will encode and decode string type elements from and to forms
17
    that are easier to use and manage.
18
    """
19
20 1
    def __init__(self, field, mode=Mode.Native, alignment=1):
21
        """Initialize a StarStruct element object."""
22
23
        # All of the type checks have already been performed by the class
24
        # factory
25 1
        self.name = field[0]
26
27
        # The ref attribute is required for all elements, but base element
28
        # types don't have one
29 1
        self.ref = None
30
31 1
        self._mode = mode
32 1
        self._alignment = alignment
33
34
        # Validate that the format specifiers are valid struct formats, this
35
        # doesn't have to be done now because the format will be checked when
36
        # any struct functions are called, but it's better to inform the user of
37
        # any errors earlier.
38
        # The easiest way to perform this check is to create a "Struct" class
39
        # instance, this will also increase the efficiency of all struct related
40
        # functions called.
41 1
        self.format = mode.value + field[1]
42 1
        self._struct = struct.Struct(self.format)
43
44 1
    @staticmethod
45
    def valid(field):
46
        """
47
        Validation function to determine if a field tuple represents a valid
48
        string element type.
49
        """
50 1
        return len(field) == 2 \
51
            and isinstance(field[1], str) \
52
            and re.match(r'\d*[csp]', field[1])
53
54 1
    def validate(self, msg):
55
        """
56
        Ensure that the supplied message contains the required information for
57
        this element object to operate.
58
59
        The "string" element requires no further validation.
60
        """
61 1
        pass
62
63 1
    def update(self, mode=None, alignment=None):
64
        """change the mode of the struct format"""
65 1
        if alignment:
66 1
            self._alignment = alignment
67
68 1
        if mode:
69 1
            self._mode = mode
70 1
            self.format = mode.value + self.format[1:]
71
            # recreate the struct with the new format
72 1
            self._struct = struct.Struct(self.format)
73
74 1
    def pack(self, msg):
75
        """Pack the provided values into the supplied buffer."""
76
        # Ensure that the input is of the proper form to be packed
77 1
        val = msg[self.name]
78 1
        size = struct.calcsize(self.format)
79 1
        assert len(val) <= size
80 1
        if self.format[-1] in ('s', 'p'):
81 1
            if not isinstance(val, bytes):
82 1
                assert isinstance(val, str)
83 1
                val = val.encode()
84 1
                if self.format[-1] == 'p' and len(val) < size:
85
                    # 'p' (pascal strings) must be the exact size of the format
86
                    val += b'\x00' * (size - len(val))
87 1
            data = self._struct.pack(val)
88
        else:  # 'c'
89
            if not all(isinstance(c, bytes) for c in val):
90
                if isinstance(val, bytes):
91
                    val = [bytes([c]) for c in val]
92
                else:
93
                    # last option, it could be a string, or a list of strings
94
                    assert (isinstance(val, list) and
95
                            all(isinstance(c, str) for c in val)) or \
96
                        isinstance(val, str)
97
                    val = [c.encode() for c in val]
98
            if len(val) < size:
99
                val.extend([b'\x00'] * (size - len(val)))
100
            data = self._struct.pack(*val)
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...
101
102
        # If the data does not meet the alignment, add some padding
103 1
        missing_bytes = len(data) % self._alignment
104 1
        if missing_bytes:
105
            data += b'\x00' * missing_bytes
106 1
        return data
107
108 1
    def unpack(self, msg, buf):
109
        """Unpack data from the supplied buffer using the initialized format."""
110 1
        ret = self._struct.unpack_from(buf, 0)
111
112
        # Remember to remove any alignment-based padding
113 1
        extra_bytes = self._alignment - 1 - (struct.calcsize(self.format) %
114
                                             self._alignment)
115 1
        unused = buf[struct.calcsize(self.format) + extra_bytes:]
116
117 1
        val = ret[0]
118 1
        if self.format[-1] in 's':
119
            # for 's' formats, convert to a string and strip padding
120 1
            val = val.decode().strip('\x00')
121
        elif self.format[-1] in 'p':
122
            # for 'p' formats, convert to a string, but leave the padding
123
            val = val.decode()
124
        else:  # 'c'
125
            # Just in case we have some ints in the message
126 1
            val = [c.decode() if not isinstance(c, int)
127
                   else chr(c)
128 1
                   for c in val]
129
        return (val, unused)
130 1
131 1
    def make(self, msg):
132 1
        """Return a string of the expected format"""
133
        val = msg[self.name]
134
        size = struct.calcsize(self.format)
135
        assert len(val) <= size
136 1
137
        # If the supplied value is a list of chars, or a list of bytes, turn
138
        # it into a string for ease of processing.
139
        if isinstance(val, list):
140
            if all(isinstance(c, bytes) for c in val):
141
                val = ''.join([c.decode() for c in val])
142
            elif all(isinstance(c, str) for c in val):
143
                val = ''.join([c for c in val])
144 1
            else:
145
                error = 'Invalid value for string element: {}'
146
                raise TypeError(error.format(val))
147
        elif isinstance(val, bytes):
148
            # If the supplied value is a byes, decode it into a normal string
149
            val = val.decode()
150 1
151
        # 'p' (pascal strings) and 'c' (char list) must be the exact size of
152
        # the format
153
        if self.format[-1] in ('p', 'c') and len(val) < size:
154
            val += '\x00' * (size - len(val))
155 1
156
        # Lastly, 'c' (char list) formats are expected to be a list of
157
        # characters rather than a string.
158 1
        if self.format[-1] == 'c':
159
            val = [c for c in val]
160
161
        return val
162