Skip to content

Commit 075c533

Browse files
authored
Merge pull request bobjacobsen#61 from Hierosoft/utf8-and-docstrings
UTF-8 and bytearray. Add some docstrings
2 parents c333da9 + 81e1786 commit 075c533

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+987
-489
lines changed

doc/Cyrillic-demo-Dmitry-Russian.png

1.37 KB
Loading

doc/Cyrillic-demo-Dmitry.png

920 Bytes
Loading

doc/conf.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
exclude_patterns = []
3131

3232

33-
3433
# -- Options for HTML output -------------------------------------------------
3534
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
3635

examples/example_cdi_access.py

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -96,18 +96,19 @@ def printDatagram(memo):
9696

9797

9898
# accumulate the CDI information
99-
resultingCDI = []
99+
resultingCDI = bytearray()
100100

101101
# callbacks to get results of memory read
102102

103+
103104
def memoryReadSuccess(memo):
104105
"""Handle a successful read
105106
Invoked when the memory read successfully returns,
106107
this queues a new read until the entire CDI has been
107108
returned. At that point, it invokes the XML processing below.
108109
109110
Args:
110-
memo (_type_): _description_
111+
memo (MemoryReadMemo): Successful MemoryReadMemo
111112
"""
112113
# print("successful memory read: {}".format(memo.data))
113114

@@ -121,15 +122,19 @@ def memoryReadSuccess(memo):
121122
memo.address = memo.address+64
122123
# and read again
123124
memoryService.requestMemoryRead(memo)
125+
# The last packet is not yet reached, so don't parse (However,
126+
# parser.feed could be called for realtime processing).
124127
else :
125128
# and we're done!
126129
# save content
127130
resultingCDI += memo.data
128131
# concert resultingCDI to a string up to 1st zero
129132
cdiString = ""
130-
for x in resultingCDI:
131-
if x == 0 : break
132-
cdiString += chr(x)
133+
null_i = resultingCDI.find(b'\0')
134+
terminate_i = len(resultingCDI)
135+
if null_i > -1:
136+
terminate_i = min(null_i, terminate_i)
137+
cdiString = resultingCDI[:terminate_i].decode("utf-8")
133138
# print (cdiString)
134139

135140
# and process that
@@ -158,25 +163,46 @@ def memoryReadFail(memo):
158163
class MyHandler(xml.sax.handler.ContentHandler):
159164
"""XML SAX callbacks in a handler object"""
160165
def __init__(self):
161-
self._charBuffer = []
166+
self._charBuffer = bytearray()
162167

163168
def startElement(self, name, attrs):
169+
"""_summary_
170+
171+
Args:
172+
name (_type_): _description_
173+
attrs (_type_): _description_
174+
"""
164175
print("Start: ", name)
165176
if attrs is not None and attrs :
166177
print(" Attributes: ", attrs.getNames())
167178

168179
def endElement(self, name):
180+
"""_summary_
181+
182+
Args:
183+
name (_type_): _description_
184+
"""
169185
print(name, "content:", self._flushCharBuffer())
170186
print("End: ", name)
171187
pass
172188

173189
def _flushCharBuffer(self):
174-
s = ''.join(self._charBuffer)
175-
self._charBuffer = []
190+
"""Decode the buffer, clear it, and return all content.
191+
192+
Returns:
193+
str: The content of the bytes buffer decoded as utf-8.
194+
"""
195+
s = self._charBuffer.decode("utf-8")
196+
self._charBuffer.clear()
176197
return s
177198

178199
def characters(self, data):
179-
self._charBuffer.append(data)
200+
"""Received characters handler
201+
Args:
202+
data (Union[bytearray, bytes, list[int]]): any
203+
data (any type accepted by bytearray extend).
204+
"""
205+
self._charBuffer.extend(data)
180206

181207

182208
handler = MyHandler()
@@ -186,8 +212,12 @@ def processXML(content) :
186212
"""process the XML and invoke callbacks
187213
188214
Args:
189-
content (_type_): _description_
215+
content (str): Raw XML data
190216
"""
217+
# NOTE: The data is complete in this example since processXML is
218+
# only called when there is a null terminator, which indicates the
219+
# last packet was reached for the requested read.
220+
# - See memoryReadSuccess comments for details.
191221
xml.sax.parseString(content, handler)
192222
print("\nParser done")
193223

examples/example_frame_interface.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def printFrame(frame):
5252
canPhysicalLayerGridConnect.registerFrameReceivedListener(printFrame)
5353

5454
# send an AME frame with arbitrary alias to provoke response
55-
frame = CanFrame(ControlFrame.AME.value, 1, [])
55+
frame = CanFrame(ControlFrame.AME.value, 1, bytearray())
5656
print("SL: {}".format(frame))
5757
canPhysicalLayerGridConnect.sendCanFrame(frame)
5858

examples/example_node_implementation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def displayOtherNodeIds(message) :
129129
"""Listener to identify connected nodes
130130
131131
Args:
132-
message (_type_): _description_
132+
message (Message): A response from the network
133133
"""
134134
print("[displayOtherNodeIds] type(message): {}"
135135
"".format(type(message).__name__))

examples/example_remote_nodes.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,9 @@ def result(arg1, arg2=None, arg3=None, result=True) :
126126
Args:
127127
arg1: Any value.
128128
arg2: value to compare to arg1. Defaults to None.
129-
arg3: fail if arg1 not equal to arg1; arg3 is then message. Defaults to
130-
None.
131-
result (bool, optional): _description_. Defaults to True.
129+
arg3: fail if arg1 not equal to arg1; arg3 is then message.
130+
Defaults to None.
131+
result (bool, optional): Expected result. Defaults to True.
132132
133133
Raises:
134134
ValueError: If only arg1 was provided (undefined behavior--in other

openlcb/__init__.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections import OrderedDict
12
import re
23

34

@@ -15,5 +16,27 @@ def only_hex_pairs(value):
1516

1617

1718
def emit_cast(value):
18-
"""Show type and value, such as for debug output."""
19-
return "{}({})".format(type(value).__name__, repr(value))
19+
"""Get type and value, such as for debug output."""
20+
repr_str = repr(value)
21+
if repr_str.startswith(type(value).__name__):
22+
return repr(value) # type already included, such as bytearray(...)
23+
return "{}({})".format(type(value).__name__, repr_str)
24+
25+
26+
def list_type_names(values):
27+
"""Get the type of several values, such as for debug output.
28+
Args:
29+
values (Union[list,tuple,dict,OrderedDict]): A collection where
30+
each element's type is to be analyzed.
31+
Returns:
32+
list[str]: A list where each element is a type name. If
33+
values argument is dict-like, each element is formatted as
34+
"{key}: {type}".
35+
"""
36+
if isinstance(values, (list, tuple)):
37+
return [type(value).__name__ for value in values]
38+
if isinstance(values, (dict, OrderedDict)):
39+
return ["{}: {}".format(k, type(v).__name__) for k, v in values.items()]
40+
raise TypeError("list_type_names is only implemented for"
41+
" list, tuple, dict, and OrderedDict, but got a(n) {}"
42+
.format(type(values).__name__))

openlcb/canbus/canframe.py

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import openlcb
2+
from collections import OrderedDict
13
from openlcb.nodeid import NodeID
24

35

@@ -36,41 +38,98 @@ class CanFrame:
3638
mask = 0x07FF_F000).
3739
"""
3840

39-
header = 0
40-
data = []
41+
ARG_LISTS = [
42+
OrderedDict(N_cid=int, nodeID=NodeID, alias=int),
43+
OrderedDict(header=int, data=bytearray),
44+
OrderedDict(control=int, alias=int, data=bytearray),
45+
]
4146

42-
def __str__(self):
43-
return "CanFrame header: 0x{:08X} {}".format(self.header, self.data)
47+
@staticmethod
48+
def constructor_help():
49+
result = ""
50+
for d in CanFrame.ARG_LISTS:
51+
result += "("
52+
for k, v in d.items():
53+
result += "{}: {}, ".format(k, v.__name__)
54+
result = result[:-2] # remove last ", " from this ctor
55+
result += "), "
56+
return result[:-2] # -1 to remove last ", " from list
4457

45-
# there are three ctor forms
58+
def __str__(self):
59+
return "CanFrame header: 0x{:08X} {}".format(
60+
self.header,
61+
list(self.data), # cast to list to format bytearray(b'') as []
62+
)
4663

47-
def __init__(self, arg1, arg2, arg3=[]):
64+
def __init__(self, *args):
65+
arg1 = None
66+
arg2 = None
67+
arg3 = None
68+
if len(args) > 0:
69+
arg1 = args[0]
70+
if len(args) > 1:
71+
arg2 = args[1]
72+
if len(args) > 2:
73+
arg3 = args[2]
74+
else:
75+
arg3 = bytearray()
76+
# There are three ctor forms.
77+
# - See "Args" in class for docstring.
78+
self.header = 0
79+
self.data = bytearray()
4880
# three arguments as N_cid, nodeID, alias
81+
args_error = None
4982
if isinstance(arg2, NodeID):
83+
# Other args' types will be enforced by doing math on them
84+
# (duck typing) in this case.
85+
if len(args) < 3:
86+
args_error = "Expected alias after NodeID"
5087
# cid must be 4 to 7 inclusive (100 to 111 binary)
5188
# precondition(4 <= cid && cid <= 7)
5289
cid = arg1
5390
nodeID = arg2
5491
alias = arg3
5592

5693
nodeCode = ((nodeID.nodeId >> ((cid-4)*12)) & 0xFFF)
57-
# ^ cid-4 results in 0 to 3. *12 results in 0 to 36 bit shift (nodeID size)
94+
# ^ cid-4 results in 0 to 3. *12 results in 0 to 36 bit shift (nodeID size) # noqa: E501
5895
self.header = ((cid << 12) | nodeCode) << 12 | (alias & 0xFFF) | 0x10_00_00_00 # noqa: E501
59-
self.data = []
96+
# self.data = bytearray()
6097

6198
# two arguments as header, data
62-
elif isinstance(arg2, list):
99+
elif isinstance(arg2, bytearray):
100+
if not isinstance(arg1, int):
101+
args_error = "Expected int since 2nd argument is bytearray."
102+
# Types of both args are enforced by this point.
63103
self.header = arg1
64104
self.data = arg2
105+
if len(args) > 2:
106+
args_error = "2nd argument is data, but got extra argument(s)"
107+
108+
# two arguments as header, data
109+
elif isinstance(arg2, list):
110+
args_error = ("Expected bytearray (formerly list[int])"
111+
" if data 2nd argument")
112+
# self.header = arg1
113+
# self.data = bytearray(arg2)
65114

66115
# three arguments as control, alias, data
67116
elif isinstance(arg2, int):
117+
# Types of all 3 are enforced by usage (duck typing) in this case.
68118
control = arg1
69119
alias = arg2
70120
self.header = (control << 12) | (alias & 0xFFF) | 0x10_00_00_00
121+
if not isinstance(arg3, bytearray):
122+
args_error = ("Expected bytearray (formerly list[int])"
123+
" 2nd if 1st argument is header int")
71124
self.data = arg3
72125
else:
73-
print("could not decode NodeID ctor arguments")
126+
args_error = "could not decode CanFrame arguments"
127+
128+
if args_error:
129+
raise TypeError(
130+
args_error.rstrip(".") + ". Valid constructors:"
131+
+ CanFrame.constructor_help() + ". Got: "
132+
+ openlcb.list_type_names(args))
74133

75134
def __eq__(self, other):
76135
if other is None:

0 commit comments

Comments
 (0)