Skip to content

Commit b2a5024

Browse files
authored
Improve IO class hierarchy + Allow to use SizedRotatingLogger as a context manager (#1147)
* Fix doc * Add test case for using SizedRotatingLogger as a context manager * Bump testing tool version * Improve type hierarchy and interaction * Update tests * Improve class hierarchy, documentation, typing and test for rotating loggers * use tmp_path fixture
1 parent 5441c5a commit b2a5024

File tree

10 files changed

+302
-271
lines changed

10 files changed

+302
-271
lines changed

can/io/asc.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
"""
88

99
from typing import cast, Any, Generator, IO, List, Optional, Union, Dict
10-
from can import typechecking
1110

1211
from datetime import datetime
1312
import time
@@ -16,7 +15,8 @@
1615
from ..message import Message
1716
from ..listener import Listener
1817
from ..util import channel2int
19-
from .generic import BaseIOHandler
18+
from .generic import BaseIOHandler, FileIOMessageWriter
19+
from ..typechecking import AcceptedIOType
2020

2121

2222
CAN_MSG_EXT = 0x80000000
@@ -37,7 +37,7 @@ class ASCReader(BaseIOHandler):
3737

3838
def __init__(
3939
self,
40-
file: Union[typechecking.FileLike, typechecking.StringPathLike],
40+
file: AcceptedIOType,
4141
base: str = "hex",
4242
relative_timestamp: bool = True,
4343
) -> None:
@@ -238,7 +238,7 @@ def __iter__(self) -> Generator[Message, None, None]:
238238
self.stop()
239239

240240

241-
class ASCWriter(BaseIOHandler, Listener):
241+
class ASCWriter(FileIOMessageWriter, Listener):
242242
"""Logs CAN data to an ASCII log file (.asc).
243243
244244
The measurement starts with the timestamp of the first registered message.
@@ -275,7 +275,7 @@ class ASCWriter(BaseIOHandler, Listener):
275275

276276
def __init__(
277277
self,
278-
file: Union[typechecking.FileLike, typechecking.StringPathLike],
278+
file: AcceptedIOType,
279279
channel: int = 1,
280280
) -> None:
281281
"""
@@ -286,8 +286,6 @@ def __init__(
286286
have a channel set
287287
"""
288288
super().__init__(file, mode="w")
289-
if not self.file:
290-
raise ValueError("The given file cannot be None")
291289

292290
self.channel = channel
293291

@@ -304,7 +302,6 @@ def __init__(
304302

305303
def stop(self) -> None:
306304
# This is guaranteed to not be None since we raise ValueError in __init__
307-
self.file = cast(IO[Any], self.file)
308305
if not self.file.closed:
309306
self.file.write("End TriggerBlock\n")
310307
super().stop()

can/io/blf.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from ..listener import Listener
2424
from ..util import len2dlc, dlc2len, channel2int
2525
from ..typechecking import AcceptedIOType
26-
from .generic import BaseIOHandler
26+
from .generic import BaseIOHandler, FileIOMessageWriter
2727

2828

2929
class BLFParseError(Exception):
@@ -139,7 +139,7 @@ class BLFReader(BaseIOHandler):
139139
silently ignored.
140140
"""
141141

142-
def __init__(self, file):
142+
def __init__(self, file: AcceptedIOType) -> None:
143143
"""
144144
:param file: a path-like object or as file-like object to read from
145145
If this is a file-like object, is has to opened in binary
@@ -347,7 +347,7 @@ def _parse_data(self, data):
347347
pos = next_pos
348348

349349

350-
class BLFWriter(BaseIOHandler, Listener):
350+
class BLFWriter(FileIOMessageWriter, Listener):
351351
"""
352352
Logs CAN data to a Binary Logging File compatible with Vector's tools.
353353
"""

can/io/canutils.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
from can.message import Message
1010
from can.listener import Listener
11-
from .generic import BaseIOHandler
12-
11+
from .generic import BaseIOHandler, FileIOMessageWriter
12+
from ..typechecking import AcceptedIOType
1313

1414
log = logging.getLogger("can.io.canutils")
1515

@@ -32,7 +32,7 @@ class CanutilsLogReader(BaseIOHandler):
3232
``(0.0) vcan0 001#8d00100100820100``
3333
"""
3434

35-
def __init__(self, file):
35+
def __init__(self, file: AcceptedIOType) -> None:
3636
"""
3737
:param file: a path-like object or as file-like object to read from
3838
If this is a file-like object, is has to opened in text
@@ -105,7 +105,7 @@ def __iter__(self):
105105
self.stop()
106106

107107

108-
class CanutilsLogWriter(BaseIOHandler, Listener):
108+
class CanutilsLogWriter(FileIOMessageWriter, Listener):
109109
"""Logs CAN data to an ASCII log file (.log).
110110
This class is is compatible with "candump -L".
111111
@@ -114,7 +114,7 @@ class CanutilsLogWriter(BaseIOHandler, Listener):
114114
It the first message does not have a timestamp, it is set to zero.
115115
"""
116116

117-
def __init__(self, file, channel="vcan0", append=False):
117+
def __init__(self, file: AcceptedIOType, channel="vcan0", append=False):
118118
"""
119119
:param file: a path-like object or as file-like object to write to
120120
If this is a file-like object, is has to opened in text

can/io/csv.py

Lines changed: 49 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,55 @@
1313

1414
from can.message import Message
1515
from can.listener import Listener
16-
from .generic import BaseIOHandler
16+
from .generic import BaseIOHandler, FileIOMessageWriter
17+
from ..typechecking import AcceptedIOType
1718

1819

19-
class CSVWriter(BaseIOHandler, Listener):
20+
class CSVReader(BaseIOHandler):
21+
"""Iterator over CAN messages from a .csv file that was
22+
generated by :class:`~can.CSVWriter` or that uses the same
23+
format as described there. Assumes that there is a header
24+
and thus skips the first line.
25+
26+
Any line separator is accepted.
27+
"""
28+
29+
def __init__(self, file: AcceptedIOType) -> None:
30+
"""
31+
:param file: a path-like object or as file-like object to read from
32+
If this is a file-like object, is has to opened in text
33+
read mode, not binary read mode.
34+
"""
35+
super().__init__(file, mode="r")
36+
37+
def __iter__(self):
38+
# skip the header line
39+
try:
40+
next(self.file)
41+
except StopIteration:
42+
# don't crash on a file with only a header
43+
return
44+
45+
for line in self.file:
46+
47+
timestamp, arbitration_id, extended, remote, error, dlc, data = line.split(
48+
","
49+
)
50+
51+
yield Message(
52+
timestamp=float(timestamp),
53+
is_remote_frame=(remote == "1"),
54+
is_extended_id=(extended == "1"),
55+
is_error_frame=(error == "1"),
56+
arbitration_id=int(arbitration_id, base=16),
57+
dlc=int(dlc),
58+
data=b64decode(data),
59+
)
60+
61+
self.stop()
62+
63+
64+
class CSVWriter(FileIOMessageWriter, Listener):
2065
"""Writes a comma separated text file with a line for
2166
each message. Includes a header line.
2267
@@ -37,7 +82,7 @@ class CSVWriter(BaseIOHandler, Listener):
3782
Each line is terminated with a platform specific line separator.
3883
"""
3984

40-
def __init__(self, file, append=False):
85+
def __init__(self, file: AcceptedIOType, append: bool = False) -> None:
4186
"""
4287
:param file: a path-like object or a file-like object to write to.
4388
If this is a file-like object, is has to open in text
@@ -54,7 +99,7 @@ def __init__(self, file, append=False):
5499
if not append:
55100
self.file.write("timestamp,arbitration_id,extended,remote,error,dlc,data\n")
56101

57-
def on_message_received(self, msg):
102+
def on_message_received(self, msg: Message) -> None:
58103
row = ",".join(
59104
[
60105
repr(msg.timestamp), # cannot use str() here because that is rounding
@@ -68,47 +113,3 @@ def on_message_received(self, msg):
68113
)
69114
self.file.write(row)
70115
self.file.write("\n")
71-
72-
73-
class CSVReader(BaseIOHandler):
74-
"""Iterator over CAN messages from a .csv file that was
75-
generated by :class:`~can.CSVWriter` or that uses the same
76-
format as described there. Assumes that there is a header
77-
and thus skips the first line.
78-
79-
Any line separator is accepted.
80-
"""
81-
82-
def __init__(self, file):
83-
"""
84-
:param file: a path-like object or as file-like object to read from
85-
If this is a file-like object, is has to opened in text
86-
read mode, not binary read mode.
87-
"""
88-
super().__init__(file, mode="r")
89-
90-
def __iter__(self):
91-
# skip the header line
92-
try:
93-
next(self.file)
94-
except StopIteration:
95-
# don't crash on a file with only a header
96-
return
97-
98-
for line in self.file:
99-
100-
timestamp, arbitration_id, extended, remote, error, dlc, data = line.split(
101-
","
102-
)
103-
104-
yield Message(
105-
timestamp=float(timestamp),
106-
is_remote_frame=(remote == "1"),
107-
is_extended_id=(extended == "1"),
108-
is_error_frame=(error == "1"),
109-
arbitration_id=int(arbitration_id, base=16),
110-
dlc=int(dlc),
111-
data=b64decode(data),
112-
)
113-
114-
self.stop()

can/io/generic.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ class BaseIOHandler(ContextManager, metaclass=ABCMeta):
3030

3131
file: Optional[can.typechecking.FileLike]
3232

33-
def __init__(self, file: can.typechecking.AcceptedIOType, mode: str = "rt") -> None:
33+
def __init__(
34+
self, file: Optional[can.typechecking.AcceptedIOType], mode: str = "rt"
35+
) -> None:
3436
"""
3537
:param file: a path-like object to open a file, a file-like object
3638
to be used as a file or `None` to not use a file at all
@@ -77,6 +79,15 @@ class FileIOMessageWriter(MessageWriter, metaclass=ABCMeta):
7779

7880
file: Union[TextIO, BinaryIO]
7981

82+
def __init__(
83+
self, file: Union[can.typechecking.FileLike, TextIO, BinaryIO], mode: str = "rt"
84+
) -> None:
85+
# Not possible with the type signature, but be verbose for user friendliness
86+
if file is None:
87+
raise ValueError("The given file cannot be None")
88+
89+
super().__init__(file, mode)
90+
8091

8192
# pylint: disable=too-few-public-methods
8293
class MessageReader(BaseIOHandler, Iterable, metaclass=ABCMeta):

0 commit comments

Comments
 (0)