Skip to content

can.io: Add preferred mode for opening files to Reader/Writer classes. #1585

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 1 commit into from
May 16, 2023
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
6 changes: 3 additions & 3 deletions can/io/asc.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from ..message import Message
from ..typechecking import StringPathLike
from ..util import channel2int, dlc2len, len2dlc
from .generic import FileIOMessageWriter, MessageReader
from .generic import TextIOMessageReader, TextIOMessageWriter

CAN_MSG_EXT = 0x80000000
CAN_ID_MASK = 0x1FFFFFFF
Expand All @@ -24,7 +24,7 @@
logger = logging.getLogger("can.io.asc")


class ASCReader(MessageReader):
class ASCReader(TextIOMessageReader):
"""
Iterator of CAN messages from a ASC logging file. Meta data (comments,
bus statistics, J1939 Transport Protocol messages) is ignored.
Expand Down Expand Up @@ -308,7 +308,7 @@ def __iter__(self) -> Generator[Message, None, None]:
self.stop()


class ASCWriter(FileIOMessageWriter):
class ASCWriter(TextIOMessageWriter):
"""Logs CAN data to an ASCII log file (.asc).

The measurement starts with the timestamp of the first registered message.
Expand Down
4 changes: 2 additions & 2 deletions can/io/blf.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from ..message import Message
from ..typechecking import StringPathLike
from ..util import channel2int, dlc2len, len2dlc
from .generic import FileIOMessageWriter, MessageReader
from .generic import BinaryIOMessageReader, FileIOMessageWriter

TSystemTime = Tuple[int, int, int, int, int, int, int, int]

Expand Down Expand Up @@ -132,7 +132,7 @@ def systemtime_to_timestamp(systemtime: TSystemTime) -> float:
return 0


class BLFReader(MessageReader):
class BLFReader(BinaryIOMessageReader):
"""
Iterator of CAN messages from a Binary Logging File.

Expand Down
6 changes: 3 additions & 3 deletions can/io/canutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from can.message import Message

from ..typechecking import StringPathLike
from .generic import FileIOMessageWriter, MessageReader
from .generic import TextIOMessageReader, TextIOMessageWriter

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

Expand All @@ -23,7 +23,7 @@
CANFD_ESI = 0x02


class CanutilsLogReader(MessageReader):
class CanutilsLogReader(TextIOMessageReader):
"""
Iterator over CAN messages from a .log Logging File (candump -L).

Expand Down Expand Up @@ -122,7 +122,7 @@ def __iter__(self) -> Generator[Message, None, None]:
self.stop()


class CanutilsLogWriter(FileIOMessageWriter):
class CanutilsLogWriter(TextIOMessageWriter):
"""Logs CAN data to an ASCII log file (.log).
This class is is compatible with "candump -L".

Expand Down
6 changes: 3 additions & 3 deletions can/io/csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
from can.message import Message

from ..typechecking import StringPathLike
from .generic import FileIOMessageWriter, MessageReader
from .generic import TextIOMessageReader, TextIOMessageWriter


class CSVReader(MessageReader):
class CSVReader(TextIOMessageReader):
"""Iterator over CAN messages from a .csv file that was
generated by :class:`~can.CSVWriter` or that uses the same
format as described there. Assumes that there is a header
Expand Down Expand Up @@ -67,7 +67,7 @@ def __iter__(self) -> Generator[Message, None, None]:
self.stop()


class CSVWriter(FileIOMessageWriter):
class CSVWriter(TextIOMessageWriter):
"""Writes a comma separated text file with a line for
each message. Includes a header line.

Expand Down
20 changes: 20 additions & 0 deletions can/io/generic.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
"""Contains generic base classes for file IO."""
import gzip
import locale
from abc import ABCMeta
from types import TracebackType
from typing import (
Any,
BinaryIO,
ContextManager,
Iterable,
Optional,
TextIO,
Type,
Union,
cast,
)

Expand Down Expand Up @@ -105,5 +109,21 @@ def file_size(self) -> int:
return self.file.tell()


class TextIOMessageWriter(FileIOMessageWriter, metaclass=ABCMeta):
file: TextIO


class BinaryIOMessageWriter(FileIOMessageWriter, metaclass=ABCMeta):
file: Union[BinaryIO, gzip.GzipFile]


class MessageReader(BaseIOHandler, Iterable[Message], metaclass=ABCMeta):
"""The base class for all readers."""


class TextIOMessageReader(MessageReader, metaclass=ABCMeta):
file: TextIO


class BinaryIOMessageReader(MessageReader, metaclass=ABCMeta):
file: Union[BinaryIO, gzip.GzipFile]
34 changes: 25 additions & 9 deletions can/io/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@
from .blf import BLFWriter
from .canutils import CanutilsLogWriter
from .csv import CSVWriter
from .generic import BaseIOHandler, FileIOMessageWriter, MessageWriter
from .generic import (
BaseIOHandler,
BinaryIOMessageWriter,
FileIOMessageWriter,
MessageWriter,
)
from .mf4 import MF4Writer
from .printer import Printer
from .sqlite import SqliteWriter
Expand Down Expand Up @@ -94,20 +99,28 @@ def __new__( # type: ignore

file_or_filename: AcceptedIOType = filename
if suffix == ".gz":
suffix, file_or_filename = Logger.compress(filename, **kwargs)
LoggerType, file_or_filename = Logger.compress(filename, **kwargs)
else:
LoggerType = cls._get_logger_for_suffix(suffix)

return LoggerType(file=file_or_filename, **kwargs)

@classmethod
def _get_logger_for_suffix(cls, suffix: str) -> Type[MessageWriter]:
try:
LoggerType = Logger.message_writers[suffix]
if LoggerType is None:
raise ValueError(f'failed to import logger for extension "{suffix}"')
return LoggerType(file=file_or_filename, **kwargs)
return LoggerType
except KeyError:
raise ValueError(
f'No write support for this unknown log format "{suffix}"'
) from None

@staticmethod
def compress(filename: StringPathLike, **kwargs: Any) -> Tuple[str, FileLike]:
@classmethod
def compress(
cls, filename: StringPathLike, **kwargs: Any
) -> Tuple[Type[MessageWriter], FileLike]:
"""
Return the suffix and io object of the decompressed file.
File will automatically recompress upon close.
Expand All @@ -117,12 +130,15 @@ def compress(filename: StringPathLike, **kwargs: Any) -> Tuple[str, FileLike]:
raise ValueError(
f"The file type {real_suffix} is currently incompatible with gzip."
)
if kwargs.get("append", False):
mode = "ab" if real_suffix == ".blf" else "at"
LoggerType = cls._get_logger_for_suffix(real_suffix)
append = kwargs.get("append", False)

if issubclass(LoggerType, BinaryIOMessageWriter):
mode = "ab" if append else "wb"
else:
mode = "wb" if real_suffix == ".blf" else "wt"
mode = "at" if append else "wt"

return real_suffix, gzip.open(filename, mode)
return LoggerType, gzip.open(filename, mode)

def on_message_received(self, msg: Message) -> None:
pass
Expand Down
6 changes: 3 additions & 3 deletions can/io/mf4.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from ..message import Message
from ..typechecking import StringPathLike
from ..util import channel2int, dlc2len, len2dlc
from .generic import FileIOMessageWriter, MessageReader
from .generic import BinaryIOMessageReader, BinaryIOMessageWriter

logger = logging.getLogger("can.io.mf4")

Expand Down Expand Up @@ -75,7 +75,7 @@
CAN_ID_MASK = 0x1FFFFFFF


class MF4Writer(FileIOMessageWriter):
class MF4Writer(BinaryIOMessageWriter):
"""Logs CAN data to an ASAM Measurement Data File v4 (.mf4).

MF4Writer does not support append mode.
Expand Down Expand Up @@ -265,7 +265,7 @@ def on_message_received(self, msg: Message) -> None:
self._rtr_buffer = np.zeros(1, dtype=RTR_DTYPE)


class MF4Reader(MessageReader):
class MF4Reader(BinaryIOMessageReader):
"""
Iterator of CAN messages from a MF4 logging file.

Expand Down
23 changes: 16 additions & 7 deletions can/io/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from .blf import BLFReader
from .canutils import CanutilsLogReader
from .csv import CSVReader
from .generic import MessageReader
from .generic import BinaryIOMessageReader, MessageReader
from .mf4 import MF4Reader
from .sqlite import SqliteReader
from .trc import TRCReader
Expand Down Expand Up @@ -87,7 +87,13 @@ def __new__( # type: ignore

file_or_filename: AcceptedIOType = filename
if suffix == ".gz":
suffix, file_or_filename = LogReader.decompress(filename)
ReaderType, file_or_filename = LogReader.decompress(filename)
else:
ReaderType = cls._get_logger_for_suffix(suffix)
return ReaderType(file=file_or_filename, **kwargs)

@classmethod
def _get_logger_for_suffix(cls, suffix: str) -> typing.Type[MessageReader]:
try:
ReaderType = LogReader.message_readers[suffix]
except KeyError:
Expand All @@ -96,19 +102,22 @@ def __new__( # type: ignore
) from None
if ReaderType is None:
raise ImportError(f"failed to import reader for extension {suffix}")
return ReaderType(file=file_or_filename, **kwargs)
return ReaderType

@staticmethod
@classmethod
def decompress(
cls,
filename: StringPathLike,
) -> typing.Tuple[str, typing.Union[str, FileLike]]:
) -> typing.Tuple[typing.Type[MessageReader], typing.Union[str, FileLike]]:
"""
Return the suffix and io object of the decompressed file.
"""
real_suffix = pathlib.Path(filename).suffixes[-2].lower()
mode = "rb" if real_suffix == ".blf" else "rt"
ReaderType = cls._get_logger_for_suffix(real_suffix)

mode = "rb" if issubclass(ReaderType, BinaryIOMessageReader) else "rt"

return real_suffix, gzip.open(filename, mode)
return ReaderType, gzip.open(filename, mode)

def __iter__(self) -> typing.Generator[Message, None, None]:
raise NotImplementedError()
Expand Down
9 changes: 6 additions & 3 deletions can/io/trc.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
from ..message import Message
from ..typechecking import StringPathLike
from ..util import channel2int, dlc2len, len2dlc
from .generic import FileIOMessageWriter, MessageReader
from .generic import (
TextIOMessageReader,
TextIOMessageWriter,
)

logger = logging.getLogger("can.io.trc")

Expand All @@ -36,7 +39,7 @@ def __ge__(self, other):
return NotImplemented


class TRCReader(MessageReader):
class TRCReader(TextIOMessageReader):
"""
Iterator of CAN messages from a TRC logging file.
"""
Expand Down Expand Up @@ -241,7 +244,7 @@ def __iter__(self) -> Generator[Message, None, None]:
self.stop()


class TRCWriter(FileIOMessageWriter):
class TRCWriter(TextIOMessageWriter):
"""Logs CAN data to text file (.trc).

The measurement starts with the timestamp of the first registered message.
Expand Down