Skip to content

Adding support for gzipped ASC logging file (.asc.gz) #1138

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion can/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
)

from .io import Logger, SizedRotatingLogger, Printer, LogReader, MessageSync
from .io import ASCWriter, ASCReader
from .io import ASCWriter, ASCReader, GzipASCWriter, GzipASCReader
from .io import BLFReader, BLFWriter
from .io import CanutilsLogReader, CanutilsLogWriter
from .io import CSVWriter, CSVReader
Expand Down
2 changes: 1 addition & 1 deletion can/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from .player import LogReader, MessageSync

# Format specific
from .asc import ASCWriter, ASCReader
from .asc import ASCWriter, ASCReader, GzipASCWriter, GzipASCReader
from .blf import BLFReader, BLFWriter
from .canutils import CanutilsLogReader, CanutilsLogWriter
from .csv import CSVWriter, CSVReader
Expand Down
71 changes: 69 additions & 2 deletions can/io/asc.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
- 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
- under `test/data/logfile.asc`
"""

from typing import cast, Any, Generator, IO, List, Optional, Dict
import gzip
from typing import cast, Any, Generator, IO, List, Optional, Dict, Union

from datetime import datetime
import time
import logging

from .. import typechecking
from ..message import Message
from ..listener import Listener
from ..util import channel2int
Expand Down Expand Up @@ -396,3 +397,69 @@ def on_message_received(self, msg: Message) -> None:
data=" ".join(data),
)
self.log_event(serialized, msg.timestamp)


class GzipASCReader(ASCReader):
"""Gzipped version of :class:`~can.ASCReader`"""

def __init__(
self,
file: Union[typechecking.FileLike, typechecking.StringPathLike],
base: str = "hex",
relative_timestamp: bool = True,
):
"""
:param file: a path-like object or as file-like object to read from
If this is a file-like object, is has to opened in text
read mode, not binary read mode.
:param base: Select the base(hex or dec) of id and data.
If the header of the asc file contains base information,
this value will be overwritten. Default "hex".
:param relative_timestamp: Select whether the timestamps are
`relative` (starting at 0.0) or `absolute` (starting at
the system time). Default `True = relative`.
"""
self._fileobj = None
if file is not None and (hasattr(file, "read") and hasattr(file, "write")):
# file is None or some file-like object
self._fileobj = file
super(GzipASCReader, self).__init__(
gzip.open(file, mode="rt"), base, relative_timestamp
)

def stop(self) -> None:
super(GzipASCReader, self).stop()
if self._fileobj is not None:
self._fileobj.close()


class GzipASCWriter(ASCWriter):
"""Gzipped version of :class:`~can.ASCWriter`"""

def __init__(
self,
file: Union[typechecking.FileLike, typechecking.StringPathLike],
channel: int = 1,
compresslevel: int = 6,
):
"""
:param file: a path-like object or as file-like object to write to
If this is a file-like object, is has to opened in text
write mode, not binary write mode.
:param channel: a default channel to use when the message does not
have a channel set
:param compresslevel: Gzip compresslevel, see
:class:`~gzip.GzipFile` for details. The default is 6.
"""
self._fileobj = None
if file is not None and (hasattr(file, "read") and hasattr(file, "write")):
# file is None or some file-like object
self._fileobj = file
super(GzipASCWriter, self).__init__(
gzip.open(file, mode="wt", compresslevel=compresslevel), channel
)

def stop(self) -> None:
super(GzipASCWriter, self).stop()
if self._fileobj is not None:
self._fileobj.close()
6 changes: 4 additions & 2 deletions can/io/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from ..message import Message
from ..listener import Listener
from .generic import BaseIOHandler, FileIOMessageWriter
from .asc import ASCWriter
from .asc import ASCWriter, GzipASCWriter
from .blf import BLFWriter
from .canutils import CanutilsLogWriter
from .csv import CSVWriter
Expand All @@ -36,6 +36,7 @@ class Logger(BaseIOHandler, Listener): # pylint: disable=abstract-method

The format is determined from the file format which can be one of:
* .asc: :class:`can.ASCWriter`
* .asc.gz: :class:`can.CompressedASCWriter`
* .blf :class:`can.BLFWriter`
* .csv: :class:`can.CSVWriter`
* .db: :class:`can.SqliteWriter`
Expand All @@ -54,6 +55,7 @@ class Logger(BaseIOHandler, Listener): # pylint: disable=abstract-method
fetched_plugins = False
message_writers = {
".asc": ASCWriter,
".asc.gz": GzipASCWriter,
".blf": BLFWriter,
".csv": CSVWriter,
".db": SqliteWriter,
Expand Down Expand Up @@ -83,7 +85,7 @@ def __new__( # type: ignore
)
Logger.fetched_plugins = True

suffix = pathlib.PurePath(filename).suffix.lower()
suffix = "".join(s.lower() for s in pathlib.PurePath(filename).suffixes)
try:
return cast(
Listener, Logger.message_writers[suffix](filename, *args, **kwargs)
Expand Down
6 changes: 4 additions & 2 deletions can/io/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import can

from .generic import BaseIOHandler, MessageReader
from .asc import ASCReader
from .asc import ASCReader, GzipASCReader
from .blf import BLFReader
from .canutils import CanutilsLogReader
from .csv import CSVReader
Expand All @@ -27,6 +27,7 @@ class LogReader(BaseIOHandler):

The format is determined from the file format which can be one of:
* .asc
* .asc.gz
* .blf
* .csv
* .db
Expand All @@ -49,6 +50,7 @@ class LogReader(BaseIOHandler):
fetched_plugins = False
message_readers = {
".asc": ASCReader,
".asc.gz": GzipASCReader,
".blf": BLFReader,
".csv": CSVReader,
".db": SqliteReader,
Expand All @@ -75,7 +77,7 @@ def __new__( # type: ignore
)
LogReader.fetched_plugins = True

suffix = pathlib.PurePath(filename).suffix.lower()
suffix = "".join(s.lower() for s in pathlib.PurePath(filename).suffixes)
try:
return typing.cast(
MessageReader,
Expand Down
30 changes: 29 additions & 1 deletion test/logformats_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

TODO: correctly set preserves_channel and adds_default_channel
"""

import gzip
import logging
import unittest
import tempfile
Expand Down Expand Up @@ -555,6 +555,34 @@ def test_can_and_canfd_error_frames(self):
self.assertMessagesEqual(actual, expected_messages)


class TestGzipASCFileFormat(ReaderWriterTest):
"""Tests can.GzipASCWriter and can.GzipASCReader"""

def _setup_instance(self):
super()._setup_instance_helper(
can.GzipASCWriter,
can.GzipASCReader,
binary_file=True,
check_comments=True,
preserves_channel=False,
adds_default_channel=0,
)

def assertIncludesComments(self, filename):
"""
Ensures that all comments are literally contained in the given file.

:param filename: the path-like object to use
"""
if self.original_comments:
# read the entire outout file
with gzip.open(filename, "rt" if self.binary_file else "r") as file:
output_contents = file.read()
# check each, if they can be found in there literally
for comment in self.original_comments:
self.assertIn(comment, output_contents)


class TestBlfFileFormat(ReaderWriterTest):
"""Tests can.BLFWriter and can.BLFReader.

Expand Down