Skip to content

Commit d142257

Browse files
authored
Merge pull request #1221 from Tbruno25/1210_add-gzipreader
1210 Add compressed file methods
2 parents 2807da1 + bfc3283 commit d142257

9 files changed

+99
-119
lines changed

can/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
)
2626

2727
from .io import Logger, SizedRotatingLogger, Printer, LogReader, MessageSync
28-
from .io import ASCWriter, ASCReader, GzipASCWriter, GzipASCReader
28+
from .io import ASCWriter, ASCReader
2929
from .io import BLFReader, BLFWriter
3030
from .io import CanutilsLogReader, CanutilsLogWriter
3131
from .io import CSVWriter, CSVReader

can/io/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from .player import LogReader, MessageSync
99

1010
# Format specific
11-
from .asc import ASCWriter, ASCReader, GzipASCWriter, GzipASCReader
11+
from .asc import ASCWriter, ASCReader
1212
from .blf import BLFReader, BLFWriter
1313
from .canutils import CanutilsLogReader, CanutilsLogWriter
1414
from .csv import CSVWriter, CSVReader

can/io/asc.py

-67
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
- https://bitbucket.org/tobylorenz/vector_asc/src/47556e1a6d32c859224ca62d075e1efcc67fa690/src/Vector/ASC/tests/unittests/data/CAN_Log_Trigger_3_2.asc?at=master&fileviewer=file-view-default
66
- under `test/data/logfile.asc`
77
"""
8-
import gzip
98
import re
109
from typing import Any, Generator, List, Optional, Dict, Union, TextIO
1110

@@ -405,69 +404,3 @@ def on_message_received(self, msg: Message) -> None:
405404
data=" ".join(data),
406405
)
407406
self.log_event(serialized, msg.timestamp)
408-
409-
410-
class GzipASCReader(ASCReader):
411-
"""Gzipped version of :class:`~can.ASCReader`"""
412-
413-
def __init__(
414-
self,
415-
file: Union[typechecking.FileLike, typechecking.StringPathLike],
416-
base: str = "hex",
417-
relative_timestamp: bool = True,
418-
):
419-
"""
420-
:param file: a path-like object or as file-like object to read from
421-
If this is a file-like object, is has to opened in text
422-
read mode, not binary read mode.
423-
:param base: Select the base(hex or dec) of id and data.
424-
If the header of the asc file contains base information,
425-
this value will be overwritten. Default "hex".
426-
:param relative_timestamp: Select whether the timestamps are
427-
`relative` (starting at 0.0) or `absolute` (starting at
428-
the system time). Default `True = relative`.
429-
"""
430-
self._fileobj = None
431-
if file is not None and (hasattr(file, "read") and hasattr(file, "write")):
432-
# file is None or some file-like object
433-
self._fileobj = file
434-
super(GzipASCReader, self).__init__(
435-
gzip.open(file, mode="rt"), base, relative_timestamp
436-
)
437-
438-
def stop(self) -> None:
439-
super(GzipASCReader, self).stop()
440-
if self._fileobj is not None:
441-
self._fileobj.close()
442-
443-
444-
class GzipASCWriter(ASCWriter):
445-
"""Gzipped version of :class:`~can.ASCWriter`"""
446-
447-
def __init__(
448-
self,
449-
file: Union[typechecking.FileLike, typechecking.StringPathLike],
450-
channel: int = 1,
451-
compresslevel: int = 6,
452-
):
453-
"""
454-
:param file: a path-like object or as file-like object to write to
455-
If this is a file-like object, is has to opened in text
456-
write mode, not binary write mode.
457-
:param channel: a default channel to use when the message does not
458-
have a channel set
459-
:param compresslevel: Gzip compresslevel, see
460-
:class:`~gzip.GzipFile` for details. The default is 6.
461-
"""
462-
self._fileobj = None
463-
if file is not None and (hasattr(file, "read") and hasattr(file, "write")):
464-
# file is None or some file-like object
465-
self._fileobj = file
466-
super(GzipASCWriter, self).__init__(
467-
gzip.open(file, mode="wt", compresslevel=compresslevel), channel
468-
)
469-
470-
def stop(self) -> None:
471-
super(GzipASCWriter, self).stop()
472-
if self._fileobj is not None:
473-
self._fileobj.close()

can/io/logger.py

+25-12
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,9 @@
66
import pathlib
77
from abc import ABC, abstractmethod
88
from datetime import datetime
9-
from typing import (
10-
Any,
11-
Optional,
12-
Callable,
13-
cast,
14-
Type,
15-
)
9+
import gzip
10+
from typing import Any, Optional, Callable, TextIO, Type, Tuple, Union, cast
11+
1612
from types import TracebackType
1713

1814
from typing_extensions import Literal
@@ -21,7 +17,7 @@
2117
from ..message import Message
2218
from ..listener import Listener
2319
from .generic import BaseIOHandler, FileIOMessageWriter
24-
from .asc import ASCWriter, GzipASCWriter
20+
from .asc import ASCWriter
2521
from .blf import BLFWriter
2622
from .canutils import CanutilsLogWriter
2723
from .csv import CSVWriter
@@ -34,15 +30,18 @@ class Logger(BaseIOHandler, Listener): # pylint: disable=abstract-method
3430
"""
3531
Logs CAN messages to a file.
3632
37-
The format is determined from the file format which can be one of:
33+
The format is determined from the file suffix which can be one of:
3834
* .asc: :class:`can.ASCWriter`
39-
* .asc.gz: :class:`can.CompressedASCWriter`
4035
* .blf :class:`can.BLFWriter`
4136
* .csv: :class:`can.CSVWriter`
4237
* .db: :class:`can.SqliteWriter`
4338
* .log :class:`can.CanutilsLogWriter`
4439
* .txt :class:`can.Printer`
4540
41+
Any of these formats can be used with gzip compression by appending
42+
the suffix .gz (e.g. filename.asc.gz). However, third-party tools might not
43+
be able to read these files.
44+
4645
The **filename** may also be *None*, to fall back to :class:`can.Printer`.
4746
4847
The log files may be incomplete until `stop()` is called due to buffering.
@@ -55,7 +54,6 @@ class Logger(BaseIOHandler, Listener): # pylint: disable=abstract-method
5554
fetched_plugins = False
5655
message_writers = {
5756
".asc": ASCWriter,
58-
".asc.gz": GzipASCWriter,
5957
".blf": BLFWriter,
6058
".csv": CSVWriter,
6159
".db": SqliteWriter,
@@ -85,7 +83,11 @@ def __new__( # type: ignore
8583
)
8684
Logger.fetched_plugins = True
8785

88-
suffix = "".join(s.lower() for s in pathlib.PurePath(filename).suffixes)
86+
suffix = pathlib.PurePath(filename).suffix.lower()
87+
88+
if suffix == ".gz":
89+
suffix, filename = Logger.compress(filename)
90+
8991
try:
9092
return cast(
9193
Listener, Logger.message_writers[suffix](filename, *args, **kwargs)
@@ -95,6 +97,17 @@ def __new__( # type: ignore
9597
f'No write support for this unknown log format "{suffix}"'
9698
) from None
9799

100+
@staticmethod
101+
def compress(filename: StringPathLike) -> Tuple[str, Union[str, Any]]:
102+
"""
103+
Return the suffix and io object of the decompressed file.
104+
File will automatically recompress upon close.
105+
"""
106+
real_suffix = pathlib.Path(filename).suffixes[-2].lower()
107+
mode = "ab" if real_suffix == ".blf" else "at"
108+
109+
return real_suffix, gzip.open(filename, mode)
110+
98111
def on_message_received(self, msg: Message) -> None:
99112
pass
100113

can/io/player.py

+24-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
well as :class:`MessageSync` which plays back messages
44
in the recorded order an time intervals.
55
"""
6-
6+
import gzip
77
import pathlib
88
from time import time, sleep
99
import typing
@@ -14,7 +14,7 @@
1414
import can
1515

1616
from .generic import BaseIOHandler, MessageReader
17-
from .asc import ASCReader, GzipASCReader
17+
from .asc import ASCReader
1818
from .blf import BLFReader
1919
from .canutils import CanutilsLogReader
2020
from .csv import CSVReader
@@ -25,14 +25,17 @@ class LogReader(BaseIOHandler):
2525
"""
2626
Replay logged CAN messages from a file.
2727
28-
The format is determined from the file format which can be one of:
28+
The format is determined from the file suffix which can be one of:
2929
* .asc
30-
* .asc.gz
3130
* .blf
3231
* .csv
3332
* .db
3433
* .log
3534
35+
Gzip compressed files can be used as long as the original
36+
files suffix is one of the above (e.g. filename.asc.gz).
37+
38+
3639
Exposes a simple iterator interface, to use simply:
3740
3841
>>> for msg in LogReader("some/path/to/my_file.log"):
@@ -50,7 +53,6 @@ class LogReader(BaseIOHandler):
5053
fetched_plugins = False
5154
message_readers = {
5255
".asc": ASCReader,
53-
".asc.gz": GzipASCReader,
5456
".blf": BLFReader,
5557
".csv": CSVReader,
5658
".db": SqliteReader,
@@ -77,7 +79,11 @@ def __new__( # type: ignore
7779
)
7880
LogReader.fetched_plugins = True
7981

80-
suffix = "".join(s.lower() for s in pathlib.PurePath(filename).suffixes)
82+
suffix = pathlib.PurePath(filename).suffix.lower()
83+
84+
if suffix == ".gz":
85+
suffix, filename = LogReader.decompress(filename)
86+
8187
try:
8288
return typing.cast(
8389
MessageReader,
@@ -88,6 +94,18 @@ def __new__( # type: ignore
8894
f'No read support for this unknown log format "{suffix}"'
8995
) from None
9096

97+
@staticmethod
98+
def decompress(
99+
filename: "can.typechecking.StringPathLike",
100+
) -> typing.Tuple[str, typing.Union[str, typing.Any]]:
101+
"""
102+
Return the suffix and io object of the decompressed file.
103+
"""
104+
real_suffix = pathlib.Path(filename).suffixes[-2].lower()
105+
mode = "rb" if real_suffix == ".blf" else "rt"
106+
107+
return real_suffix, gzip.open(filename, mode)
108+
91109

92110
class MessageSync: # pylint: disable=too-few-public-methods
93111
"""

test/data/test_CanMessage.asc.gz

277 Bytes
Binary file not shown.

test/logformats_test.py

+1-29
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
1212
TODO: correctly set preserves_channel and adds_default_channel
1313
"""
14-
import gzip
1514
import logging
1615
import unittest
1716
import tempfile
@@ -48,6 +47,7 @@ def test_extension_matching(self):
4847
suffix_variants = [
4948
suffix.upper(),
5049
suffix.lower(),
50+
f"can.msg.ext{suffix}",
5151
"".join([c.upper() if i % 2 else c for i, c in enumerate(suffix)]),
5252
]
5353
for suffix_variant in suffix_variants:
@@ -558,34 +558,6 @@ def test_ignore_comments(self):
558558
_msg_list = self._read_log_file("logfile.asc")
559559

560560

561-
class TestGzipASCFileFormat(ReaderWriterTest):
562-
"""Tests can.GzipASCWriter and can.GzipASCReader"""
563-
564-
def _setup_instance(self):
565-
super()._setup_instance_helper(
566-
can.GzipASCWriter,
567-
can.GzipASCReader,
568-
binary_file=True,
569-
check_comments=True,
570-
preserves_channel=False,
571-
adds_default_channel=0,
572-
)
573-
574-
def assertIncludesComments(self, filename):
575-
"""
576-
Ensures that all comments are literally contained in the given file.
577-
578-
:param filename: the path-like object to use
579-
"""
580-
if self.original_comments:
581-
# read the entire outout file
582-
with gzip.open(filename, "rt" if self.binary_file else "r") as file:
583-
output_contents = file.read()
584-
# check each, if they can be found in there literally
585-
for comment in self.original_comments:
586-
self.assertIn(comment, output_contents)
587-
588-
589561
class TestBlfFileFormat(ReaderWriterTest):
590562
"""Tests can.BLFWriter and can.BLFReader.
591563

test/test_logger.py

+36
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import unittest
88
from unittest import mock
99
from unittest.mock import Mock
10+
import gzip
11+
import os
1012
import sys
1113
import can
1214
import can.logger
@@ -104,5 +106,39 @@ def test_log_virtual_sizedlogger(self):
104106
self.mock_logger_sized.assert_called_once()
105107

106108

109+
class TestLoggerCompressedFile(unittest.TestCase):
110+
def setUp(self) -> None:
111+
# Patch VirtualBus object
112+
self.patcher_virtual_bus = mock.patch(
113+
"can.interfaces.virtual.VirtualBus", spec=True
114+
)
115+
self.MockVirtualBus = self.patcher_virtual_bus.start()
116+
self.mock_virtual_bus = self.MockVirtualBus.return_value
117+
118+
self.testmsg = can.Message(
119+
arbitration_id=0xC0FFEE, data=[0, 25, 0, 1, 3, 1, 4, 1], is_extended_id=True
120+
)
121+
self.baseargs = [sys.argv[0], "-i", "virtual"]
122+
123+
self.testfile = open("coffee.log.gz", "w+")
124+
125+
def test_compressed_logfile(self):
126+
"""
127+
Basic test to verify Logger is able to write gzip files.
128+
"""
129+
self.mock_virtual_bus.recv = Mock(side_effect=[self.testmsg, KeyboardInterrupt])
130+
sys.argv = self.baseargs + ["--file_name", self.testfile.name]
131+
can.logger.main()
132+
with gzip.open(self.testfile.name, "rt") as testlog:
133+
last_line = testlog.readlines()[-1]
134+
135+
self.assertEqual(last_line, "(0.000000) vcan0 00C0FFEE#0019000103010401\n")
136+
137+
def tearDown(self) -> None:
138+
self.testfile.close()
139+
os.remove(self.testfile.name)
140+
self.patcher_virtual_bus.stop()
141+
142+
107143
if __name__ == "__main__":
108144
unittest.main()

test/test_player.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515

1616

1717
class TestPlayerScriptModule(unittest.TestCase):
18+
19+
logfile = os.path.join(os.path.dirname(__file__), "data", "test_CanMessage.asc")
20+
1821
def setUp(self) -> None:
1922
# Patch VirtualBus object
2023
patcher_virtual_bus = mock.patch("can.interfaces.virtual.VirtualBus", spec=True)
@@ -29,9 +32,6 @@ def setUp(self) -> None:
2932
self.addCleanup(patcher_sleep.stop)
3033

3134
self.baseargs = [sys.argv[0], "-i", "virtual"]
32-
self.logfile = os.path.join(
33-
os.path.dirname(__file__), "data", "test_CanMessage.asc"
34-
)
3535

3636
def assertSuccessfulCleanup(self):
3737
self.MockVirtualBus.assert_called_once()
@@ -111,5 +111,13 @@ def test_play_error_frame(self):
111111
self.assertSuccessfulCleanup()
112112

113113

114+
class TestPlayerCompressedFile(TestPlayerScriptModule):
115+
"""
116+
Re-run tests using a compressed file.
117+
"""
118+
119+
logfile = os.path.join(os.path.dirname(__file__), "data", "test_CanMessage.asc.gz")
120+
121+
114122
if __name__ == "__main__":
115123
unittest.main()

0 commit comments

Comments
 (0)