Skip to content

Commit bb3fb8b

Browse files
authored
Merge pull request #1761 from peternewman/0.10-c11-compat
Fix vast majority of outstanding Python 3 issues
2 parents e523002 + f5be4e7 commit bb3fb8b

23 files changed

Lines changed: 453 additions & 137 deletions

.codespellignorelines

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
/(?:([0-9]{1,3})(?:\s+THRU\s+([0-9]{0,3}))?)(?:\s+@\s+([0-9]{0,3}))?$/);
1818
str = str.replace('>', 'THRU');
1919
' THRU ' + ola.common.DmxConstants.MAX_CHANNEL_NUMBER);
20-
' THRU ' + ola.common.DmxConstants.MAX_CHANNEL_NUMBER);
2120
// If it's the T or > keys, autocomplete 'THRU'
2221
case 'U': // THRU
2322
var values = ['7', '8', '9', ' THRU ', '4', '5', '6', ' @ ', '1', '2', '3',
@@ -118,9 +117,6 @@ class AsyncronousLibUsbAdaptor : public BaseLibUsbAdaptor {
118117
OLA_ASSERT_EQ(expected, JsonWriter::AsString(uint_value));
119118
* Test the uint item
120119
" \"type\": \"uint\",\n"
121-
" \"type\": \"uint\",\n"
122-
" \"type\": \"uint\",\n"
123-
" \"type\": \"uint\",\n"
124120
std::map<std::string, UIntMap*> m_uint_map_variables;
125121
if (message.uint_offset < MAX_UINT_FIELDS) {
126122
message.uint16_fields[message.uint_offset++] = field->Value();
@@ -131,10 +127,8 @@ class AsyncronousLibUsbAdaptor : public BaseLibUsbAdaptor {
131127
status_message() : uint_offset(0), int_offset(0), status_type(0),
132128
std::string Type() const { return "uint"; }
133129
if (items[i]['type'] == 'uint') {
134-
if (items[i]['type'] == 'uint') {
135130
if (type == 'string' || type == 'uint' || type == 'hidden') {
136131
const char RDMHTTPModule::GENERIC_UINT_FIELD[] = "int";
137-
section.AddItem(new HiddenItem("1", GENERIC_UINT_FIELD));
138132
section.AddItem(new HiddenItem("1", GENERIC_UINT_FIELD));
139133
SelectItem *item = new SelectItem("Personality", GENERIC_UINT_FIELD);
140134
string personality_str = request->GetParameter(GENERIC_UINT_FIELD);
@@ -198,4 +192,6 @@ import java.nio.ByteOrder;
198192
"{'a': 'caf\\\\xe9'}")
199193
# self.assertEqual('%s' % rtf._EscapeData({"caf\xe9": "bar"}),
200194
# "{'caf\xe9': 'bar'}")
195+
self.assertEqual('caf\\xe9', StringEscape(u'caf\xe9'))
196+
self.assertEqual('caf\\xe9', ("%s" % StringEscape(u'caf\xe9')))
201197
"forin": true,

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ python/ola/PidStoreTest.sh
196196
python/ola/RDMTest.sh
197197
python/ola/Version.py
198198
python/ola/rpc/SimpleRpcControllerTest.sh
199+
python/ola/testing/
200+
python/ola/testing/__init__.py
199201
slp/slp_client
200202
slp/slp_client.exe
201203
slp/slp_server
@@ -228,6 +230,7 @@ tools/rdm/DataLocation.py
228230
tools/rdm/ExpectedResultsTest.sh
229231
tools/rdm/ResponderTestTest.sh
230232
tools/rdm/TestHelpersTest.sh
233+
tools/rdm/TestRunnerTest.sh
231234
tools/rdm/TestStateTest.sh
232235
tools/rdmpro/rdmpro_sniffer
233236
tools/rdmpro/rdmpro_sniffer.exe

NEWS

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
x/y/2023 ola-0.10.9
22
Features:
3-
* Further improvements on Python 3 compatibility #1506
3+
* Python 3 compatibility across the board (including the RDM Responder Tests)!
4+
#1506
45
* Support for the JMS USB2DMX PRO V2.1 device via the FTDI plugin #1728
56

67
API:
78
* Python: Add a fetch current DMX example.
89

910
RDM Tests:
11+
* Python 3 compatibility of the RDM Tests #1599
1012
* Fix a longstanding bug in the GetMaxPacketSize RDM test around timeouts
1113

1214
Bugs:

configure.ac

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,18 +973,27 @@ AM_CONDITIONAL([FOUND_CPPLINT], [test "x$cpplint" = xyes])
973973
# srcdir and set PYTHONPATH=${top_builddir}/python in data/rdm/Makefile.am
974974
AC_CONFIG_LINKS([python/ola/__init__.py:python/ola/__init__.py
975975
python/ola/ClientWrapper.py:python/ola/ClientWrapper.py
976+
python/ola/DMXConstants.py:python/ola/DMXConstants.py
977+
python/ola/DUBDecoder.py:python/ola/DUBDecoder.py
976978
python/ola/MACAddress.py:python/ola/MACAddress.py
977979
python/ola/OlaClient.py:python/ola/OlaClient.py
978980
python/ola/PidStore.py:python/ola/PidStore.py
979981
python/ola/RDMAPI.py:python/ola/RDMAPI.py
980982
python/ola/RDMConstants.py:python/ola/RDMConstants.py
983+
python/ola/StringUtils.py:python/ola/StringUtils.py
981984
python/ola/TestUtils.py:python/ola/TestUtils.py
982985
python/ola/UID.py:python/ola/UID.py
983986
python/ola/rpc/__init__.py:python/ola/rpc/__init__.py
984987
python/ola/rpc/SimpleRpcController.py:python/ola/rpc/SimpleRpcController.py
985988
python/ola/rpc/StreamRpcChannel.py:python/ola/rpc/StreamRpcChannel.py
989+
tools/rdm/__init__.py:tools/rdm/__init__.py
986990
tools/rdm/ExpectedResults.py:tools/rdm/ExpectedResults.py
991+
tools/rdm/ResponderTest.py:tools/rdm/ResponderTest.py
987992
tools/rdm/TestCategory.py:tools/rdm/TestCategory.py
993+
tools/rdm/TestDefinitions.py:tools/rdm/TestDefinitions.py
994+
tools/rdm/TestHelpers.py:tools/rdm/TestHelpers.py
995+
tools/rdm/TestMixins.py:tools/rdm/TestMixins.py
996+
tools/rdm/TestRunner.py:tools/rdm/TestRunner.py
988997
tools/rdm/TestState.py:tools/rdm/TestState.py
989998
tools/rdm/TimingStats.py:tools/rdm/TimingStats.py])
990999

python/ola/Makefile.mk

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ dist_check_SCRIPTS += \
8585
python/ola/OlaClientTest.py \
8686
python/ola/PidStoreTest.py \
8787
python/ola/RDMTest.py \
88+
python/ola/StringUtilsTest.py \
8889
python/ola/TestUtils.py \
8990
python/ola/UIDTest.py
9091

@@ -96,6 +97,7 @@ test_scripts += \
9697
python/ola/OlaClientTest.sh \
9798
python/ola/PidStoreTest.sh \
9899
python/ola/RDMTest.sh \
100+
python/ola/StringUtilsTest.py \
99101
python/ola/UIDTest.py
100102
endif
101103

python/ola/PidStore.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
MAX_VALID_SUB_DEVICE = 0x0200
4646
ALL_SUB_DEVICES = 0xffff
4747

48-
# The two types of commands classes
48+
# The different types of commands classes
4949
RDM_GET, RDM_SET, RDM_DISCOVERY = range(3)
5050

5151

@@ -637,6 +637,12 @@ def Pack(self, args):
637637
arg = args[0]
638638
arg_size = len(arg)
639639

640+
# Handle the fact a UTF-8 character could be multi-byte
641+
if sys.version_info >= (3, 2):
642+
arg_size = max(arg_size, len(bytes(arg, 'utf-8')))
643+
else:
644+
arg_size = max(arg_size, len(arg.encode('utf-8')))
645+
640646
if self.max is not None and arg_size > self.max:
641647
raise ArgsValidationError('%s can be at most %d,' %
642648
(self.name, self.max))
@@ -647,9 +653,9 @@ def Pack(self, args):
647653

648654
try:
649655
if sys.version_info >= (3, 2):
650-
data = struct.unpack('%ds' % arg_size, bytes(arg, 'utf8'))
656+
data = struct.unpack('%ds' % arg_size, bytes(arg, 'utf-8'))
651657
else:
652-
data = struct.unpack('%ds' % arg_size, arg)
658+
data = struct.unpack('%ds' % arg_size, arg.encode('utf-8'))
653659
except struct.error as e:
654660
raise ArgsValidationError("Can't pack data: %s" % e)
655661
return data[0], 1
@@ -669,10 +675,12 @@ def Unpack(self, data):
669675
except struct.error as e:
670676
raise UnpackException(e)
671677

672-
if sys.version_info >= (3, 2):
673-
return value[0].rstrip(b'\x00').decode('utf-8')
674-
else:
675-
return value[0].rstrip(b'\x00')
678+
try:
679+
value = value[0].rstrip(b'\x00').decode('utf-8')
680+
except UnicodeDecodeError as e:
681+
raise UnpackException(e)
682+
683+
return value
676684

677685
def GetDescription(self, indent=0):
678686
indent = ' ' * indent
@@ -878,7 +886,7 @@ def Unpack(self, data):
878886
'Too many repeated group_count for %s, limit is %d, found %d' %
879887
(self.name, self.max, group_count))
880888

881-
if self.max is not None and group_count < self.min:
889+
if self.min is not None and group_count < self.min:
882890
raise UnpackException(
883891
'Too few repeated group_count for %s, limit is %d, found %d' %
884892
(self.name, self.min, group_count))

python/ola/PidStoreTest.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,22 @@ def testPackUnpack(self):
222222
self.assertEqual(decoded['slots_required'], 7)
223223
self.assertEqual(decoded['name'], "UnpackTest")
224224

225+
# Test null handling, trailing null should be truncated on the way back in
226+
args = ["42", "7", "Foo\0"]
227+
blob = pid._responses.get(PidStore.RDM_GET).Pack(args)[0]
228+
# Not truncated here
229+
self.assertEqual(blob, binascii.unhexlify("2a0007466f6f00"))
230+
decoded = pid.Unpack(blob, PidStore.RDM_GET)
231+
self.assertEqual(decoded['personality'], 42)
232+
self.assertEqual(decoded['slots_required'], 7)
233+
self.assertEqual(decoded['name'], "Foo")
234+
235+
# Confirm we raise an error if we try and unpack a non-ASCII, non-UTF-8
236+
# containing packet (0xc0)
237+
with self.assertRaises(PidStore.UnpackException):
238+
blob = binascii.unhexlify("2a0007556e7061636bc054657374")
239+
decoded = pid.Unpack(blob, PidStore.RDM_GET)
240+
225241
def testPackRanges(self):
226242
store = PidStore.PidStore()
227243
store.Load([os.path.join(path, "test_pids.proto")])
@@ -279,6 +295,28 @@ def testPackRanges(self):
279295
args = ["enx"]
280296
blob = pid._responses.get(PidStore.RDM_GET).Pack(args)[0]
281297

298+
# test packing some non-printable characters
299+
args = ["\x0d\x7f"]
300+
blob = pid._responses.get(PidStore.RDM_GET).Pack(args)[0]
301+
self.assertEqual(blob, binascii.unhexlify("0d7f"))
302+
decoded = pid.Unpack(blob, PidStore.RDM_GET)
303+
self.assertEqual(decoded, {'languages': [{'language': '\x0d\x7f'}]})
304+
305+
# test packing some non-ascii characters, as the
306+
# LATIN CAPITAL LETTER A WITH GRAVE, unicode U+00C0 gets encoded as two
307+
# bytes (\xc3\x80) the total length is three bytes and it doesn't fit!
308+
with self.assertRaises(PidStore.ArgsValidationError):
309+
args = [u"\x0d\xc0"]
310+
blob = pid._responses.get(PidStore.RDM_GET).Pack(args)[0]
311+
312+
# It works on it's own as it's short enough...
313+
args = [u"\u00c0"]
314+
blob = pid._responses.get(PidStore.RDM_GET).Pack(args)[0]
315+
self.assertEqual(blob, binascii.unhexlify("c380"))
316+
decoded = pid.Unpack(blob, PidStore.RDM_GET)
317+
# This is the unicode code point for it
318+
self.assertEqual(decoded, {'languages': [{'language': u'\u00c0'}]})
319+
282320
# valid empty string
283321
pid = store.GetName("STATUS_ID_DESCRIPTION")
284322
args = [""]

python/ola/StringUtils.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# This library is free software; you can redistribute it and/or
2+
# modify it under the terms of the GNU Lesser General Public
3+
# License as published by the Free Software Foundation; either
4+
# version 2.1 of the License, or (at your option) any later version.
5+
#
6+
# This library is distributed in the hope that it will be useful,
7+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
8+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9+
# Lesser General Public License for more details.
10+
#
11+
# You should have received a copy of the GNU Lesser General Public
12+
# License along with this library; if not, write to the Free Software
13+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
14+
#
15+
# StringUtils.py
16+
# Copyright (C) 2022 Peter Newman
17+
18+
"""Common utils for OLA Python string handling"""
19+
20+
import sys
21+
22+
if sys.version_info >= (3, 0):
23+
try:
24+
unicode
25+
except NameError:
26+
unicode = str
27+
28+
__author__ = 'nomis52@gmail.com (Simon Newton)'
29+
30+
31+
def StringEscape(s):
32+
"""Escape unprintable characters in a string."""
33+
# TODO(Peter): How does this interact with the E1.20 Unicode flag?
34+
# We don't use sys.version_info.major to support Python 2.6.
35+
if sys.version_info[0] == 2 and type(s) == str:
36+
return s.encode('string-escape')
37+
elif sys.version_info[0] == 2 and type(s) == unicode:
38+
return s.encode('unicode-escape')
39+
elif type(s) == str:
40+
# All strings in Python 3 are unicode
41+
# This encode/decode pair gets us an escaped string
42+
return s.encode('unicode-escape').decode(encoding="ascii",
43+
errors="backslashreplace")
44+
else:
45+
raise TypeError('Only strings are supported not %s' % type(s))

python/ola/StringUtilsTest.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env python
2+
# This library is free software; you can redistribute it and/or
3+
# modify it under the terms of the GNU Lesser General Public
4+
# License as published by the Free Software Foundation; either
5+
# version 2.1 of the License, or (at your option) any later version.
6+
#
7+
# This library is distributed in the hope that it will be useful,
8+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
9+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
10+
# Lesser General Public License for more details.
11+
#
12+
# You should have received a copy of the GNU Lesser General Public
13+
# License along with this library; if not, write to the Free Software
14+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
15+
#
16+
# StringUtilsTest.py
17+
# Copyright (C) 2022 Peter Newman
18+
19+
import unittest
20+
21+
from ola.StringUtils import StringEscape
22+
23+
"""Test cases for StringUtils utilities."""
24+
25+
__author__ = 'nomis52@gmail.com (Simon Newton)'
26+
27+
28+
class StringUtilsTest(unittest.TestCase):
29+
def testStringEscape(self):
30+
# Test we escape properly
31+
self.assertEqual('foo', StringEscape("foo"))
32+
self.assertEqual('bar', StringEscape("bar"))
33+
self.assertEqual('bar[]', StringEscape("bar[]"))
34+
self.assertEqual('foo-bar', StringEscape(u'foo-bar'))
35+
self.assertEqual('foo\\x00bar', StringEscape("foo\x00bar"))
36+
# TODO(Peter): How does this interact with the E1.20 Unicode flag?
37+
self.assertEqual('caf\\xe9', StringEscape(u'caf\xe9'))
38+
self.assertEqual('foo\\u2014bar', StringEscape(u'foo\u2014bar'))
39+
40+
# Test that we display nicely in a string
41+
self.assertEqual('foo', ("%s" % StringEscape("foo")))
42+
self.assertEqual('bar[]', ("%s" % StringEscape("bar[]")))
43+
self.assertEqual('foo-bar', ("%s" % StringEscape(u'foo-bar')))
44+
self.assertEqual('foo\\x00bar', ("%s" % StringEscape("foo\x00bar")))
45+
# TODO(Peter): How does this interact with the E1.20 Unicode flag?
46+
self.assertEqual('caf\\xe9', ("%s" % StringEscape(u'caf\xe9')))
47+
self.assertEqual('foo\\u2014bar', ("%s" % StringEscape(u'foo\u2014bar')))
48+
49+
# Confirm we throw an exception if we pass in a number or something else
50+
# that's not a string
51+
with self.assertRaises(TypeError):
52+
result = StringEscape(42)
53+
self.assertNone(result)
54+
55+
56+
if __name__ == '__main__':
57+
unittest.main()

tools/rdm/ExpectedResultsTest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import unittest
2020
from collections import namedtuple
2121

22+
# Keep this import relative to simplify the testing
2223
from ExpectedResults import (BroadcastResult, DUBResult, InvalidResponse,
2324
SuccessfulResult, TimeoutResult,
2425
UnsupportedResult)

0 commit comments

Comments
 (0)