From 845503919cee313ddc79c04cf708d3a4e740d7cb Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Tue, 5 Dec 2023 19:34:52 +0100 Subject: [PATCH 1/9] turn can.Logger and can.LogReader into functions --- can/io/__init__.py | 6 +- can/io/logger.py | 191 +++++++++++++++++----------------------- can/io/player.py | 132 +++++++++++++-------------- test/logformats_test.py | 4 +- 4 files changed, 150 insertions(+), 183 deletions(-) diff --git a/can/io/__init__.py b/can/io/__init__.py index 263bbe235..5601f2591 100644 --- a/can/io/__init__.py +++ b/can/io/__init__.py @@ -15,6 +15,8 @@ "CSVWriter", "Logger", "LogReader", + "MESSAGE_READERS", + "MESSAGE_WRITERS", "MessageSync", "MF4Reader", "MF4Writer", @@ -39,8 +41,8 @@ ] # Generic -from .logger import BaseRotatingLogger, Logger, SizedRotatingLogger -from .player import LogReader, MessageSync +from .logger import MESSAGE_WRITERS, BaseRotatingLogger, Logger, SizedRotatingLogger +from .player import MESSAGE_READERS, LogReader, MessageSync # isort: split diff --git a/can/io/logger.py b/can/io/logger.py index 90e6bfc7c..320c0a68b 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -8,23 +8,11 @@ from abc import ABC, abstractmethod from datetime import datetime from types import TracebackType -from typing import ( - Any, - Callable, - ClassVar, - Dict, - Literal, - Optional, - Set, - Tuple, - Type, - cast, -) +from typing import Any, Callable, Dict, Final, Literal, Optional, Set, Tuple, Type, cast from typing_extensions import Self from .._entry_points import read_entry_points -from ..listener import Listener from ..message import Message from ..typechecking import AcceptedIOType, FileLike, StringPathLike from .asc import ASCWriter @@ -32,7 +20,6 @@ from .canutils import CanutilsLogWriter from .csv import CSVWriter from .generic import ( - BaseIOHandler, BinaryIOMessageWriter, FileIOMessageWriter, MessageWriter, @@ -42,8 +29,62 @@ from .sqlite import SqliteWriter from .trc import TRCWriter +MESSAGE_WRITERS: Final[Dict[str, Type[MessageWriter]]] = { + ".asc": ASCWriter, + ".blf": BLFWriter, + ".csv": CSVWriter, + ".db": SqliteWriter, + ".log": CanutilsLogWriter, + ".mf4": MF4Writer, + ".trc": TRCWriter, + ".txt": Printer, +} + + +def _update_writer_plugins() -> None: + """Update available message writer plugins from entry points.""" + for entry_point in read_entry_points("can.io.message_writer"): + if entry_point.key not in MESSAGE_WRITERS: + writer_class = cast(Type[MessageWriter], entry_point.load()) + MESSAGE_WRITERS[entry_point.key] = writer_class + + +def _get_logger_for_suffix(suffix: str) -> Type[MessageWriter]: + try: + logger_type = MESSAGE_WRITERS[suffix] + if logger_type is None: + raise ValueError(f'failed to import logger for extension "{suffix}"') + return logger_type + except KeyError: + raise ValueError( + f'No write support for this unknown log format "{suffix}"' + ) from None + + +def _compress( + filename: StringPathLike, **kwargs: Any +) -> Tuple[Type[MessageWriter], FileLike]: + """ + Return the suffix and io object of the decompressed file. + File will automatically recompress upon close. + """ + real_suffix = pathlib.Path(filename).suffixes[-2].lower() + if real_suffix in (".blf", ".db"): + raise ValueError( + f"The file type {real_suffix} is currently incompatible with gzip." + ) + logger_type = _get_logger_for_suffix(real_suffix) + append = kwargs.get("append", False) + + if issubclass(logger_type, BinaryIOMessageWriter): + mode = "ab" if append else "wb" + else: + mode = "at" if append else "wt" + + return logger_type, gzip.open(filename, mode) + -class Logger(MessageWriter): +def Logger(filename: Optional[StringPathLike], **kwargs: Any) -> MessageWriter: """ Logs CAN messages to a file. @@ -65,97 +106,34 @@ class Logger(MessageWriter): The log files may be incomplete until `stop()` is called due to buffering. + :param filename: + the filename/path of the file to write to, + may be a path-like object or None to + instantiate a :class:`~can.Printer` + :raises ValueError: + if the filename's suffix is of an unknown file type + .. note:: - This class itself is just a dispatcher, and any positional and keyword + This function itself is just a dispatcher, and any positional and keyword arguments are passed on to the returned instance. """ - fetched_plugins = False - message_writers: ClassVar[Dict[str, Type[MessageWriter]]] = { - ".asc": ASCWriter, - ".blf": BLFWriter, - ".csv": CSVWriter, - ".db": SqliteWriter, - ".log": CanutilsLogWriter, - ".mf4": MF4Writer, - ".trc": TRCWriter, - ".txt": Printer, - } - - @staticmethod - def __new__( # type: ignore[misc] - cls: Any, filename: Optional[StringPathLike], **kwargs: Any - ) -> MessageWriter: - """ - :param filename: - the filename/path of the file to write to, - may be a path-like object or None to - instantiate a :class:`~can.Printer` - :raises ValueError: - if the filename's suffix is of an unknown file type - """ - if filename is None: - return Printer(**kwargs) - - if not Logger.fetched_plugins: - Logger.message_writers.update( - { - writer.key: cast(Type[MessageWriter], writer.load()) - for writer in read_entry_points("can.io.message_writer") - } - ) - Logger.fetched_plugins = True - - suffix = pathlib.PurePath(filename).suffix.lower() - - file_or_filename: AcceptedIOType = filename - if suffix == ".gz": - logger_type, file_or_filename = Logger.compress(filename, **kwargs) - else: - logger_type = cls._get_logger_for_suffix(suffix) - - return logger_type(file=file_or_filename, **kwargs) - - @classmethod - def _get_logger_for_suffix(cls, suffix: str) -> Type[MessageWriter]: - try: - logger_type = Logger.message_writers[suffix] - if logger_type is None: - raise ValueError(f'failed to import logger for extension "{suffix}"') - return logger_type - except KeyError: - raise ValueError( - f'No write support for this unknown log format "{suffix}"' - ) from None - - @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. - """ - real_suffix = pathlib.Path(filename).suffixes[-2].lower() - if real_suffix in (".blf", ".db"): - raise ValueError( - f"The file type {real_suffix} is currently incompatible with gzip." - ) - logger_type = cls._get_logger_for_suffix(real_suffix) - append = kwargs.get("append", False) - - if issubclass(logger_type, BinaryIOMessageWriter): - mode = "ab" if append else "wb" - else: - mode = "at" if append else "wt" + if filename is None: + return Printer(**kwargs) - return logger_type, gzip.open(filename, mode) + _update_writer_plugins() - def on_message_received(self, msg: Message) -> None: - pass + suffix = pathlib.PurePath(filename).suffix.lower() + file_or_filename: AcceptedIOType = filename + if suffix == ".gz": + logger_type, file_or_filename = _compress(filename, **kwargs) + else: + logger_type = _get_logger_for_suffix(suffix) + + return logger_type(file=file_or_filename, **kwargs) -class BaseRotatingLogger(Listener, BaseIOHandler, ABC): +class BaseRotatingLogger(MessageWriter, ABC): """ Base class for rotating CAN loggers. This class is not meant to be instantiated directly. Subclasses must implement the :meth:`should_rollover` @@ -171,7 +149,7 @@ class BaseRotatingLogger(Listener, BaseIOHandler, ABC): Subclasses must set the `_writer` attribute upon initialization. """ - _supported_formats: ClassVar[Set[str]] = set() + _supported_formats: Set[str] = set() #: If this attribute is set to a callable, the :meth:`~BaseRotatingLogger.rotation_filename` #: method delegates to this callable. The parameters passed to the callable are @@ -187,20 +165,17 @@ class BaseRotatingLogger(Listener, BaseIOHandler, ABC): rollover_count: int = 0 def __init__(self, **kwargs: Any) -> None: - Listener.__init__(self) - BaseIOHandler.__init__(self, file=None) + super().__init__(**kwargs, file=None) self.writer_kwargs = kwargs # Expected to be set by the subclass - self._writer: Optional[FileIOMessageWriter] = None + self._writer: FileIOMessageWriter = None # type: ignore @property def writer(self) -> FileIOMessageWriter: """This attribute holds an instance of a writer class which manages the actual file IO.""" - if self._writer is not None: - return self._writer - raise ValueError(f"{self.__class__.__name__}.writer is None.") + return self._writer def rotation_filename(self, default_name: StringPathLike) -> StringPathLike: """Modify the filename of a log file when rotating. @@ -270,7 +245,7 @@ def _get_new_writer(self, filename: StringPathLike) -> FileIOMessageWriter: logger = Logger(filename=filename, **self.writer_kwargs) if isinstance(logger, FileIOMessageWriter): return logger - if isinstance(logger, Printer) and logger.file is not None: + elif isinstance(logger, Printer) and logger.file is not None: return cast(FileIOMessageWriter, logger) raise ValueError( @@ -297,7 +272,7 @@ def __exit__( exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Literal[False]: - return self.writer.__exit__(exc_type, exc_val, exc_tb) + return self._writer.__exit__(exc_type, exc_val, exc_tb) @abstractmethod def should_rollover(self, msg: Message) -> bool: @@ -350,7 +325,7 @@ class SizedRotatingLogger(BaseRotatingLogger): :meth:`~can.Listener.stop` is called. """ - _supported_formats: ClassVar[Set[str]] = {".asc", ".blf", ".csv", ".log", ".txt"} + _supported_formats = {".asc", ".blf", ".csv", ".log", ".txt"} def __init__( self, diff --git a/can/io/player.py b/can/io/player.py index 73ef3c356..aec756450 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -8,11 +8,10 @@ import time from typing import ( Any, - ClassVar, Dict, + Final, Generator, Iterable, - Optional, Tuple, Type, Union, @@ -31,8 +30,53 @@ from .sqlite import SqliteReader from .trc import TRCReader +MESSAGE_READERS: Final[Dict[str, Type[MessageReader]]] = { + ".asc": ASCReader, + ".blf": BLFReader, + ".csv": CSVReader, + ".db": SqliteReader, + ".log": CanutilsLogReader, + ".mf4": MF4Reader, + ".trc": TRCReader, +} -class LogReader(MessageReader): + +def _update_reader_plugins() -> None: + """Update available message reader plugins from entry points.""" + for entry_point in read_entry_points("can.io.message_reader"): + if entry_point.key not in MESSAGE_READERS: + reader_class = cast(Type[MessageReader], entry_point.load()) + MESSAGE_READERS[entry_point.key] = reader_class + + +def _decompress( + filename: StringPathLike, +) -> Tuple[Type[MessageReader], Union[str, FileLike]]: + """ + Return the suffix and io object of the decompressed file. + """ + real_suffix = pathlib.Path(filename).suffixes[-2].lower() + reader_type = _get_logger_for_suffix(real_suffix) + + mode = "rb" if issubclass(reader_type, BinaryIOMessageReader) else "rt" + + return reader_type, gzip.open(filename, mode) + + +def _get_logger_for_suffix(suffix: str) -> Type[MessageReader]: + """Find MessageReader class for given suffix.""" + try: + reader_type = MESSAGE_READERS[suffix] + except KeyError: + raise ValueError( + f'No read support for this unknown log format "{suffix}"' + ) from None + if reader_type is None: + raise ImportError(f"failed to import reader for extension {suffix}") + return reader_type + + +def LogReader(filename: StringPathLike, **kwargs: Any) -> MessageReader: """ Replay logged CAN messages from a file. @@ -54,83 +98,29 @@ class LogReader(MessageReader): >>> for msg in LogReader("some/path/to/my_file.log"): ... print(msg) + :param filename: + the filename/path of the file to read from + :raises ValueError: + if the filename's suffix is of an unknown file type + .. note:: There are no time delays, if you want to reproduce the measured delays between messages look at the :class:`can.MessageSync` class. .. note:: - This class itself is just a dispatcher, and any positional an keyword + This function itself is just a dispatcher, and any positional and keyword arguments are passed on to the returned instance. """ - fetched_plugins = False - message_readers: ClassVar[Dict[str, Optional[Type[MessageReader]]]] = { - ".asc": ASCReader, - ".blf": BLFReader, - ".csv": CSVReader, - ".db": SqliteReader, - ".log": CanutilsLogReader, - ".mf4": MF4Reader, - ".trc": TRCReader, - } - - @staticmethod - def __new__( # type: ignore[misc] - cls: Any, - filename: StringPathLike, - **kwargs: Any, - ) -> MessageReader: - """ - :param filename: the filename/path of the file to read from - :raises ValueError: if the filename's suffix is of an unknown file type - """ - if not LogReader.fetched_plugins: - LogReader.message_readers.update( - { - reader.key: cast(Type[MessageReader], reader.load()) - for reader in read_entry_points("can.io.message_reader") - } - ) - LogReader.fetched_plugins = True - - suffix = pathlib.PurePath(filename).suffix.lower() - - file_or_filename: AcceptedIOType = filename - if suffix == ".gz": - reader_type, file_or_filename = LogReader.decompress(filename) - else: - reader_type = cls._get_logger_for_suffix(suffix) - return reader_type(file=file_or_filename, **kwargs) - - @classmethod - def _get_logger_for_suffix(cls, suffix: str) -> Type[MessageReader]: - try: - reader_type = LogReader.message_readers[suffix] - except KeyError: - raise ValueError( - f'No read support for this unknown log format "{suffix}"' - ) from None - if reader_type is None: - raise ImportError(f"failed to import reader for extension {suffix}") - return reader_type - - @classmethod - def decompress( - cls, - filename: StringPathLike, - ) -> Tuple[Type[MessageReader], Union[str, FileLike]]: - """ - Return the suffix and io object of the decompressed file. - """ - real_suffix = pathlib.Path(filename).suffixes[-2].lower() - reader_type = cls._get_logger_for_suffix(real_suffix) - - mode = "rb" if issubclass(reader_type, BinaryIOMessageReader) else "rt" + _update_reader_plugins() - return reader_type, gzip.open(filename, mode) - - def __iter__(self) -> Generator[Message, None, None]: - raise NotImplementedError() + suffix = pathlib.PurePath(filename).suffix.lower() + file_or_filename: AcceptedIOType = filename + if suffix == ".gz": + reader_type, file_or_filename = _decompress(filename) + else: + reader_type = _get_logger_for_suffix(suffix) + return reader_type(file=file_or_filename, **kwargs) class MessageSync: diff --git a/test/logformats_test.py b/test/logformats_test.py index 50f48c391..8694fefdc 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -52,8 +52,8 @@ def _get_suffix_case_variants(self, suffix): ] def _test_extension(self, suffix): - WriterType = can.Logger.message_writers.get(suffix) - ReaderType = can.LogReader.message_readers.get(suffix) + WriterType = can.io.MESSAGE_WRITERS.get(suffix) + ReaderType = can.io.MESSAGE_READERS.get(suffix) for suffix_variant in self._get_suffix_case_variants(suffix): tmp_file = tempfile.NamedTemporaryFile(suffix=suffix_variant, delete=False) tmp_file.close() From 5df19f3720a4817662ef90983021eb1dd1f59324 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:54:39 +0100 Subject: [PATCH 2/9] improve file io documentation --- can/io/logger.py | 6 +- can/io/player.py | 16 +++- doc/api.rst | 5 +- doc/development.rst | 4 +- doc/{listeners.rst => file_io.rst} | 142 +++++++++-------------------- doc/internal-api.rst | 1 + doc/notifier.rst | 86 +++++++++++++++++ 7 files changed, 153 insertions(+), 107 deletions(-) rename doc/{listeners.rst => file_io.rst} (65%) create mode 100644 doc/notifier.rst diff --git a/can/io/logger.py b/can/io/logger.py index 320c0a68b..a8757f7ee 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -29,6 +29,8 @@ from .sqlite import SqliteWriter from .trc import TRCWriter +#: A map of file suffixes to their corresponding +#: :class:`can.io.generic.MessageWriter` class MESSAGE_WRITERS: Final[Dict[str, Type[MessageWriter]]] = { ".asc": ASCWriter, ".blf": BLFWriter, @@ -85,8 +87,8 @@ def _compress( def Logger(filename: Optional[StringPathLike], **kwargs: Any) -> MessageWriter: - """ - Logs CAN messages to a file. + """Find and return the appropriate :class:`~can.io.generic.MessageWriter` instance + for a given file suffix. The format is determined from the file suffix which can be one of: * .asc: :class:`can.ASCWriter` diff --git a/can/io/player.py b/can/io/player.py index aec756450..1109e1979 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -30,6 +30,8 @@ from .sqlite import SqliteReader from .trc import TRCReader +#: A map of file suffixes to their corresponding +#: :class:`can.io.generic.MessageReader` class MESSAGE_READERS: Final[Dict[str, Type[MessageReader]]] = { ".asc": ASCReader, ".blf": BLFReader, @@ -77,8 +79,8 @@ def _get_logger_for_suffix(suffix: str) -> Type[MessageReader]: def LogReader(filename: StringPathLike, **kwargs: Any) -> MessageReader: - """ - Replay logged CAN messages from a file. + """Find and return the appropriate :class:`~can.io.generic.MessageReader` instance + for a given file suffix. The format is determined from the file suffix which can be one of: * .asc @@ -142,6 +144,16 @@ def __init__( as the time between messages. :param gap: Minimum time between sent messages in seconds :param skip: Skip periods of inactivity greater than this (in seconds). + + Example:: + + import can + + with can.LogReader("my_logfile.asc") as reader, can.Bus(interface="virtual") as bus: + for msg in can.MessageSync(messages=reader): + print(msg) + bus.send(msg) + """ self.raw_messages = messages self.timestamps = timestamps diff --git a/doc/api.rst b/doc/api.rst index 053bd34a4..50095589c 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -10,11 +10,12 @@ A form of CAN interface is also required. .. toctree:: - :maxdepth: 1 + :maxdepth: 2 bus message - listeners + notifier + file_io asyncio bcm errors diff --git a/doc/development.rst b/doc/development.rst index 484c90c05..ec7b7dc24 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -9,7 +9,7 @@ Contribute to source code, documentation, examples and report issues: https://github.com/hardbyte/python-can Note that the latest released version on PyPi may be significantly behind the -``develop`` branch. Please open any feature requests against the ``develop`` branch +``main`` branch. Please open any feature requests against the ``main`` branch There is also a `python-can `__ mailing list for development discussion. @@ -100,7 +100,7 @@ The modules in ``python-can`` are: +---------------------------------+------------------------------------------------------+ |:doc:`message ` | Contains the interface independent Message object. | +---------------------------------+------------------------------------------------------+ -|:doc:`io ` | Contains a range of file readers and writers. | +|:doc:`io ` | Contains a range of file readers and writers. | +---------------------------------+------------------------------------------------------+ |:doc:`broadcastmanager ` | Contains interface independent broadcast manager | | | code. | diff --git a/doc/listeners.rst b/doc/file_io.rst similarity index 65% rename from doc/listeners.rst rename to doc/file_io.rst index 110e960d3..ff9431695 100644 --- a/doc/listeners.rst +++ b/doc/file_io.rst @@ -1,110 +1,20 @@ +File IO +======= -Reading and Writing Messages -============================ -.. _notifier: - -Notifier --------- - -The Notifier object is used as a message distributor for a bus. Notifier creates a thread to read messages from the bus and distributes them to listeners. - -.. autoclass:: can.Notifier - :members: - -.. _listeners_doc: - -Listener --------- - -The Listener class is an "abstract" base class for any objects which wish to -register to receive notifications of new messages on the bus. A Listener can -be used in two ways; the default is to **call** the Listener with a new -message, or by calling the method **on_message_received**. - -Listeners are registered with :ref:`notifier` object(s) which ensure they are -notified whenever a new message is received. - -.. literalinclude:: ../examples/print_notifier.py - :language: python - :linenos: - :emphasize-lines: 8,9 - - -Subclasses of Listener that do not override **on_message_received** will cause -:class:`NotImplementedError` to be thrown when a message is received on -the CAN bus. - -.. autoclass:: can.Listener - :members: - -There are some listeners that already ship together with `python-can` -and are listed below. -Some of them allow messages to be written to files, and the corresponding file -readers are also documented here. - -.. note :: - - Please note that writing and the reading a message might not always yield a - completely unchanged message again, since some properties are not (yet) - supported by some file formats. - -.. note :: - - Additional file formats for both reading/writing log files can be added via - a plugin reader/writer. An external package can register a new reader - by using the ``can.io.message_reader`` entry point. Similarly, a writer can - be added using the ``can.io.message_writer`` entry point. - - The format of the entry point is ``reader_name=module:classname`` where ``classname`` - is a :class:`can.io.generic.BaseIOHandler` concrete implementation. - - :: - - entry_points={ - 'can.io.message_reader': [ - '.asc = my_package.io.asc:ASCReader' - ] - }, - - -BufferedReader --------------- - -.. autoclass:: can.BufferedReader - :members: - -.. autoclass:: can.AsyncBufferedReader - :members: - - -RedirectReader --------------- - -.. autoclass:: can.RedirectReader - :members: - - -Logger ------- - -The :class:`can.Logger` uses the following :class:`can.Listener` types to -create log files with different file types of the messages received. - -.. autoclass:: can.Logger - :members: - -.. autoclass:: can.io.BaseRotatingLogger - :members: - -.. autoclass:: can.SizedRotatingLogger - :members: +Reading and Writing Files +------------------------- +.. autofunction:: can.LogReader +.. autofunction:: can.Logger +.. autodata:: can.io.logger.MESSAGE_WRITERS +.. autodata:: can.io.player.MESSAGE_READERS Printer ------- .. autoclass:: can.Printer + :show-inheritance: :members: @@ -112,9 +22,11 @@ CSVWriter --------- .. autoclass:: can.CSVWriter + :show-inheritance: :members: .. autoclass:: can.CSVReader + :show-inheritance: :members: @@ -122,9 +34,11 @@ SqliteWriter ------------ .. autoclass:: can.SqliteWriter + :show-inheritance: :members: .. autoclass:: can.SqliteReader + :show-inheritance: :members: @@ -164,6 +78,7 @@ engineered from existing log files. One description of the format can be found ` .. autoclass:: can.ASCWriter + :show-inheritance: :members: ASCReader reads CAN data from ASCII log files .asc, @@ -172,6 +87,7 @@ as further references can-utils can be used: `log2asc `_. .. autoclass:: can.ASCReader + :show-inheritance: :members: @@ -185,11 +101,13 @@ As specification following references can-utils can be used: .. autoclass:: can.CanutilsLogWriter + :show-inheritance: :members: **CanutilsLogReader** reads CAN data from ASCII log files .log .. autoclass:: can.CanutilsLogReader + :show-inheritance: :members: @@ -204,11 +122,13 @@ The data is stored in a compressed format which makes it very compact. .. note:: Channels will be converted to integers. .. autoclass:: can.BLFWriter + :show-inheritance: :members: The following class can be used to read messages from BLF file: .. autoclass:: can.BLFReader + :show-inheritance: :members: @@ -229,6 +149,7 @@ The data is stored in a compressed format which makes it compact. .. autoclass:: can.MF4Writer + :show-inheritance: :members: The MDF format is very flexible regarding the internal structure and it is used to handle data from multiple sources, not just CAN bus logging. @@ -239,6 +160,7 @@ Therefor MF4Reader can only replay files created with MF4Writer. The following class can be used to read messages from MF4 file: .. autoclass:: can.MF4Reader + :show-inheritance: :members: @@ -252,9 +174,31 @@ Implements basic support for the TRC file format. Comments and contributions are welcome on what file versions might be relevant. .. autoclass:: can.TRCWriter + :show-inheritance: :members: The following class can be used to read messages from TRC file: .. autoclass:: can.TRCReader + :show-inheritance: + :members: + + +Rotating Loggers +---------------- + +.. autoclass:: can.io.BaseRotatingLogger + :show-inheritance: + :members: + +.. autoclass:: can.SizedRotatingLogger + :show-inheritance: :members: + + +Replaying Files +--------------- + +.. autoclass:: can.MessageSync + :members: + diff --git a/doc/internal-api.rst b/doc/internal-api.rst index f4b6f875a..73984bf1a 100644 --- a/doc/internal-api.rst +++ b/doc/internal-api.rst @@ -127,6 +127,7 @@ IO Utilities .. automodule:: can.io.generic :members: + :member-order: bysource diff --git a/doc/notifier.rst b/doc/notifier.rst new file mode 100644 index 000000000..05edbd90d --- /dev/null +++ b/doc/notifier.rst @@ -0,0 +1,86 @@ +Notifier and Listeners +====================== + +.. _notifier: + +Notifier +-------- + +The Notifier object is used as a message distributor for a bus. The Notifier +uses an event loop or creates a thread to read messages from the bus and +distributes them to listeners. + +.. autoclass:: can.Notifier + :members: + +.. _listeners_doc: + +Listener +-------- + +The Listener class is an "abstract" base class for any objects which wish to +register to receive notifications of new messages on the bus. A Listener can +be used in two ways; the default is to **call** the Listener with a new +message, or by calling the method **on_message_received**. + +Listeners are registered with :ref:`notifier` object(s) which ensure they are +notified whenever a new message is received. + +.. literalinclude:: ../examples/print_notifier.py + :language: python + :linenos: + :emphasize-lines: 8,9 + + +Subclasses of Listener that do not override **on_message_received** will cause +:class:`NotImplementedError` to be thrown when a message is received on +the CAN bus. + +.. autoclass:: can.Listener + :members: + +There are some listeners that already ship together with `python-can` +and are listed below. +Some of them allow messages to be written to files, and the corresponding file +readers are also documented here. + +.. note :: + + Please note that writing and the reading a message might not always yield a + completely unchanged message again, since some properties are not (yet) + supported by some file formats. + +.. note :: + + Additional file formats for both reading/writing log files can be added via + a plugin reader/writer. An external package can register a new reader + by using the ``can.io.message_reader`` entry point. Similarly, a writer can + be added using the ``can.io.message_writer`` entry point. + + The format of the entry point is ``reader_name=module:classname`` where ``classname`` + is a :class:`can.io.generic.BaseIOHandler` concrete implementation. + + :: + + entry_points={ + 'can.io.message_reader': [ + '.asc = my_package.io.asc:ASCReader' + ] + }, + + +BufferedReader +-------------- + +.. autoclass:: can.BufferedReader + :members: + +.. autoclass:: can.AsyncBufferedReader + :members: + + +RedirectReader +-------------- + +.. autoclass:: can.RedirectReader + :members: From 0c91b1f2b1964f107e64ef112c24f684b80ab158 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Fri, 8 Dec 2023 18:01:48 +0100 Subject: [PATCH 3/9] fix doctest --- can/io/player.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/can/io/player.py b/can/io/player.py index 1109e1979..951ad1dc0 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -95,10 +95,10 @@ def LogReader(filename: StringPathLike, **kwargs: Any) -> MessageReader: files suffix is one of the above (e.g. filename.asc.gz). - Exposes a simple iterator interface, to use simply: + Exposes a simple iterator interface, to use simply:: - >>> for msg in LogReader("some/path/to/my_file.log"): - ... print(msg) + for msg in can.LogReader("some/path/to/my_file.log"): + print(msg) :param filename: the filename/path of the file to read from From d713a27323c58cf456411e2f0a51f595d3a68cfa Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Tue, 19 Dec 2023 14:28:59 +0100 Subject: [PATCH 4/9] don't check if class is None, check length of suffixes --- can/io/logger.py | 23 ++++++++++++++--------- can/io/player.py | 37 ++++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/can/io/logger.py b/can/io/logger.py index a8757f7ee..82c6218f2 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -46,20 +46,20 @@ def _update_writer_plugins() -> None: """Update available message writer plugins from entry points.""" for entry_point in read_entry_points("can.io.message_writer"): - if entry_point.key not in MESSAGE_WRITERS: - writer_class = cast(Type[MessageWriter], entry_point.load()) + if entry_point.key in MESSAGE_WRITERS: + continue + + writer_class = entry_point.load() + if issubclass(writer_class, MessageWriter): MESSAGE_WRITERS[entry_point.key] = writer_class def _get_logger_for_suffix(suffix: str) -> Type[MessageWriter]: try: - logger_type = MESSAGE_WRITERS[suffix] - if logger_type is None: - raise ValueError(f'failed to import logger for extension "{suffix}"') - return logger_type + return MESSAGE_WRITERS[suffix] except KeyError: raise ValueError( - f'No write support for this unknown log format "{suffix}"' + f'No write support for unknown log format "{suffix}"' ) from None @@ -70,7 +70,13 @@ def _compress( Return the suffix and io object of the decompressed file. File will automatically recompress upon close. """ - real_suffix = pathlib.Path(filename).suffixes[-2].lower() + suffixes = pathlib.Path(filename).suffixes + if len(suffixes) != 2: + raise ValueError( + f"No write support for unknown log format \"{''.join(suffixes)}\"" + ) from None + + real_suffix = suffixes[-2].lower() if real_suffix in (".blf", ".db"): raise ValueError( f"The file type {real_suffix} is currently incompatible with gzip." @@ -131,7 +137,6 @@ def Logger(filename: Optional[StringPathLike], **kwargs: Any) -> MessageWriter: logger_type, file_or_filename = _compress(filename, **kwargs) else: logger_type = _get_logger_for_suffix(suffix) - return logger_type(file=file_or_filename, **kwargs) diff --git a/can/io/player.py b/can/io/player.py index 951ad1dc0..11b09837d 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -15,7 +15,6 @@ Tuple, Type, Union, - cast, ) from .._entry_points import read_entry_points @@ -46,18 +45,35 @@ def _update_reader_plugins() -> None: """Update available message reader plugins from entry points.""" for entry_point in read_entry_points("can.io.message_reader"): - if entry_point.key not in MESSAGE_READERS: - reader_class = cast(Type[MessageReader], entry_point.load()) + if entry_point.key in MESSAGE_READERS: + continue + + reader_class = entry_point.load() + if issubclass(reader_class, MessageReader): MESSAGE_READERS[entry_point.key] = reader_class +def _get_logger_for_suffix(suffix: str) -> Type[MessageReader]: + """Find MessageReader class for given suffix.""" + try: + return MESSAGE_READERS[suffix] + except KeyError: + raise ValueError(f'No read support for unknown log format "{suffix}"') from None + + def _decompress( filename: StringPathLike, ) -> Tuple[Type[MessageReader], Union[str, FileLike]]: """ Return the suffix and io object of the decompressed file. """ - real_suffix = pathlib.Path(filename).suffixes[-2].lower() + suffixes = pathlib.Path(filename).suffixes + if len(suffixes) != 2: + raise ValueError( + f"No write support for unknown log format \"{''.join(suffixes)}\"" + ) from None + + real_suffix = suffixes[-2].lower() reader_type = _get_logger_for_suffix(real_suffix) mode = "rb" if issubclass(reader_type, BinaryIOMessageReader) else "rt" @@ -65,19 +81,6 @@ def _decompress( return reader_type, gzip.open(filename, mode) -def _get_logger_for_suffix(suffix: str) -> Type[MessageReader]: - """Find MessageReader class for given suffix.""" - try: - reader_type = MESSAGE_READERS[suffix] - except KeyError: - raise ValueError( - f'No read support for this unknown log format "{suffix}"' - ) from None - if reader_type is None: - raise ImportError(f"failed to import reader for extension {suffix}") - return reader_type - - def LogReader(filename: StringPathLike, **kwargs: Any) -> MessageReader: """Find and return the appropriate :class:`~can.io.generic.MessageReader` instance for a given file suffix. From 2e007ee6825ed92583ff4b864b8590100f2ce23c Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Tue, 19 Dec 2023 14:29:23 +0100 Subject: [PATCH 5/9] fix typo --- can/io/printer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/io/printer.py b/can/io/printer.py index 00e4545df..67c353cc6 100644 --- a/can/io/printer.py +++ b/can/io/printer.py @@ -33,7 +33,7 @@ def __init__( """ :param file: An optional path-like object or a file-like object to "print" to instead of writing to standard out (stdout). - If this is a file-like object, is has to be opened in text + If this is a file-like object, it has to be opened in text write mode, not binary write mode. :param append: If set to `True` messages, are appended to the file, else the file is truncated From 73ce0045a24e87dd1b562b64ad43b2ed5d726f46 Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sun, 14 Jan 2024 12:00:54 +0100 Subject: [PATCH 6/9] fix issues after rebase --- can/io/logger.py | 36 ++++++++++++++++++++++++++---------- can/io/player.py | 2 +- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/can/io/logger.py b/can/io/logger.py index 82c6218f2..78699475b 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -8,7 +8,19 @@ from abc import ABC, abstractmethod from datetime import datetime from types import TracebackType -from typing import Any, Callable, Dict, Final, Literal, Optional, Set, Tuple, Type, cast +from typing import ( + Any, + Callable, + ClassVar, + Dict, + Final, + Literal, + Optional, + Set, + Tuple, + Type, + cast, +) from typing_extensions import Self @@ -92,7 +104,9 @@ def _compress( return logger_type, gzip.open(filename, mode) -def Logger(filename: Optional[StringPathLike], **kwargs: Any) -> MessageWriter: +def Logger( # noqa: N802 + filename: Optional[StringPathLike], **kwargs: Any +) -> MessageWriter: """Find and return the appropriate :class:`~can.io.generic.MessageWriter` instance for a given file suffix. @@ -156,7 +170,7 @@ class BaseRotatingLogger(MessageWriter, ABC): Subclasses must set the `_writer` attribute upon initialization. """ - _supported_formats: Set[str] = set() + _supported_formats: ClassVar[Set[str]] = set() #: If this attribute is set to a callable, the :meth:`~BaseRotatingLogger.rotation_filename` #: method delegates to this callable. The parameters passed to the callable are @@ -172,17 +186,15 @@ class BaseRotatingLogger(MessageWriter, ABC): rollover_count: int = 0 def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs, file=None) + super().__init__(**{**kwargs, "file": None}) self.writer_kwargs = kwargs - # Expected to be set by the subclass - self._writer: FileIOMessageWriter = None # type: ignore - @property + @abstractmethod def writer(self) -> FileIOMessageWriter: """This attribute holds an instance of a writer class which manages the actual file IO.""" - return self._writer + raise NotImplementedError def rotation_filename(self, default_name: StringPathLike) -> StringPathLike: """Modify the filename of a log file when rotating. @@ -279,7 +291,7 @@ def __exit__( exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Literal[False]: - return self._writer.__exit__(exc_type, exc_val, exc_tb) + return self.writer.__exit__(exc_type, exc_val, exc_tb) @abstractmethod def should_rollover(self, msg: Message) -> bool: @@ -332,7 +344,7 @@ class SizedRotatingLogger(BaseRotatingLogger): :meth:`~can.Listener.stop` is called. """ - _supported_formats = {".asc", ".blf", ".csv", ".log", ".txt"} + _supported_formats: ClassVar[Set[str]] = {".asc", ".blf", ".csv", ".log", ".txt"} def __init__( self, @@ -355,6 +367,10 @@ def __init__( self._writer = self._get_new_writer(self.base_filename) + @property + def writer(self) -> FileIOMessageWriter: + return self._writer + def should_rollover(self, msg: Message) -> bool: if self.max_bytes <= 0: return False diff --git a/can/io/player.py b/can/io/player.py index 11b09837d..507e44a92 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -81,7 +81,7 @@ def _decompress( return reader_type, gzip.open(filename, mode) -def LogReader(filename: StringPathLike, **kwargs: Any) -> MessageReader: +def LogReader(filename: StringPathLike, **kwargs: Any) -> MessageReader: # noqa: N802 """Find and return the appropriate :class:`~can.io.generic.MessageReader` instance for a given file suffix. From 4727ea6ce91eb96f21d8de5ac9ecbb446ba8e6ac Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sun, 14 Jan 2024 12:09:10 +0100 Subject: [PATCH 7/9] fix test issues, use context manager to fix random PyPy failures --- test/test_rotating_loggers.py | 242 ++++++++++++++++------------------ 1 file changed, 116 insertions(+), 126 deletions(-) diff --git a/test/test_rotating_loggers.py b/test/test_rotating_loggers.py index 8230168b9..a6661280d 100644 --- a/test/test_rotating_loggers.py +++ b/test/test_rotating_loggers.py @@ -6,24 +6,34 @@ import os from pathlib import Path +from typing import cast from unittest.mock import Mock import can +from can.io.generic import FileIOMessageWriter +from can.typechecking import StringPathLike from .data.example_data import generate_message class TestBaseRotatingLogger: @staticmethod - def _get_instance(path, *args, **kwargs) -> can.io.BaseRotatingLogger: + def _get_instance(file: StringPathLike) -> can.io.BaseRotatingLogger: class SubClass(can.io.BaseRotatingLogger): """Subclass that implements abstract methods for testing.""" _supported_formats = {".asc", ".blf", ".csv", ".log", ".txt"} - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - self._writer = can.Printer(file=path / "__unused.txt") + def __init__(self, file: StringPathLike, **kwargs) -> None: + super().__init__(**kwargs) + suffix = Path(file).suffix.lower() + if suffix not in self._supported_formats: + raise ValueError(f"Unsupported file format: {suffix}") + self._writer = can.Printer(file=file) + + @property + def writer(self) -> FileIOMessageWriter: + return cast(FileIOMessageWriter, self._writer) def should_rollover(self, msg: can.Message) -> bool: return False @@ -31,7 +41,7 @@ def should_rollover(self, msg: can.Message) -> bool: def do_rollover(self): ... - return SubClass(*args, **kwargs) + return SubClass(file=file) def test_import(self): assert hasattr(can.io, "BaseRotatingLogger") @@ -50,90 +60,82 @@ def test_attributes(self): assert hasattr(can.io.BaseRotatingLogger, "do_rollover") def test_get_new_writer(self, tmp_path): - logger_instance = self._get_instance(tmp_path) - - writer = logger_instance._get_new_writer(tmp_path / "file.ASC") - assert isinstance(writer, can.ASCWriter) - writer.stop() + with self._get_instance(tmp_path / "__unused.txt") as logger_instance: + writer = logger_instance._get_new_writer(tmp_path / "file.ASC") + assert isinstance(writer, can.ASCWriter) + writer.stop() - writer = logger_instance._get_new_writer(tmp_path / "file.BLF") - assert isinstance(writer, can.BLFWriter) - writer.stop() + writer = logger_instance._get_new_writer(tmp_path / "file.BLF") + assert isinstance(writer, can.BLFWriter) + writer.stop() - writer = logger_instance._get_new_writer(tmp_path / "file.CSV") - assert isinstance(writer, can.CSVWriter) - writer.stop() + writer = logger_instance._get_new_writer(tmp_path / "file.CSV") + assert isinstance(writer, can.CSVWriter) + writer.stop() - writer = logger_instance._get_new_writer(tmp_path / "file.LOG") - assert isinstance(writer, can.CanutilsLogWriter) - writer.stop() + writer = logger_instance._get_new_writer(tmp_path / "file.LOG") + assert isinstance(writer, can.CanutilsLogWriter) + writer.stop() - writer = logger_instance._get_new_writer(tmp_path / "file.TXT") - assert isinstance(writer, can.Printer) - writer.stop() + writer = logger_instance._get_new_writer(tmp_path / "file.TXT") + assert isinstance(writer, can.Printer) + writer.stop() def test_rotation_filename(self, tmp_path): - logger_instance = self._get_instance(tmp_path) + with self._get_instance(tmp_path / "__unused.txt") as logger_instance: + default_name = "default" + assert logger_instance.rotation_filename(default_name) == "default" - default_name = "default" - assert logger_instance.rotation_filename(default_name) == "default" - - logger_instance.namer = lambda x: x + "_by_namer" - assert logger_instance.rotation_filename(default_name) == "default_by_namer" + logger_instance.namer = lambda x: x + "_by_namer" + assert logger_instance.rotation_filename(default_name) == "default_by_namer" def test_rotate_without_rotator(self, tmp_path): - logger_instance = self._get_instance(tmp_path) - - source = str(tmp_path / "source.txt") - dest = str(tmp_path / "dest.txt") + with self._get_instance(tmp_path / "__unused.txt") as logger_instance: + source = str(tmp_path / "source.txt") + dest = str(tmp_path / "dest.txt") - assert os.path.exists(source) is False - assert os.path.exists(dest) is False + assert os.path.exists(source) is False + assert os.path.exists(dest) is False - logger_instance._writer = logger_instance._get_new_writer(source) - logger_instance.stop() + logger_instance._writer = logger_instance._get_new_writer(source) + logger_instance.stop() - assert os.path.exists(source) is True - assert os.path.exists(dest) is False + assert os.path.exists(source) is True + assert os.path.exists(dest) is False - logger_instance.rotate(source, dest) + logger_instance.rotate(source, dest) - assert os.path.exists(source) is False - assert os.path.exists(dest) is True + assert os.path.exists(source) is False + assert os.path.exists(dest) is True def test_rotate_with_rotator(self, tmp_path): - logger_instance = self._get_instance(tmp_path) - - rotator_func = Mock() - logger_instance.rotator = rotator_func + with self._get_instance(tmp_path / "__unused.txt") as logger_instance: + rotator_func = Mock() + logger_instance.rotator = rotator_func - source = str(tmp_path / "source.txt") - dest = str(tmp_path / "dest.txt") + source = str(tmp_path / "source.txt") + dest = str(tmp_path / "dest.txt") - assert os.path.exists(source) is False - assert os.path.exists(dest) is False + assert os.path.exists(source) is False + assert os.path.exists(dest) is False - logger_instance._writer = logger_instance._get_new_writer(source) - logger_instance.stop() + logger_instance._writer = logger_instance._get_new_writer(source) + logger_instance.stop() - assert os.path.exists(source) is True - assert os.path.exists(dest) is False + assert os.path.exists(source) is True + assert os.path.exists(dest) is False - logger_instance.rotate(source, dest) - rotator_func.assert_called_with(source, dest) + logger_instance.rotate(source, dest) + rotator_func.assert_called_with(source, dest) - # assert that no rotation was performed since rotator_func - # does not do anything - assert os.path.exists(source) is True - assert os.path.exists(dest) is False + # assert that no rotation was performed since rotator_func + # does not do anything + assert os.path.exists(source) is True + assert os.path.exists(dest) is False def test_stop(self, tmp_path): """Test if stop() method of writer is called.""" - with self._get_instance(tmp_path) as logger_instance: - logger_instance._writer = logger_instance._get_new_writer( - tmp_path / "file.ASC" - ) - + with self._get_instance(tmp_path / "file.ASC") as logger_instance: # replace stop method of writer with Mock original_stop = logger_instance.writer.stop mock_stop = Mock() @@ -146,44 +148,38 @@ def test_stop(self, tmp_path): original_stop() def test_on_message_received(self, tmp_path): - logger_instance = self._get_instance(tmp_path) + with self._get_instance(tmp_path / "file.ASC") as logger_instance: + # Test without rollover + should_rollover = Mock(return_value=False) + do_rollover = Mock() + writers_on_message_received = Mock() - logger_instance._writer = logger_instance._get_new_writer(tmp_path / "file.ASC") + logger_instance.should_rollover = should_rollover + logger_instance.do_rollover = do_rollover + logger_instance.writer.on_message_received = writers_on_message_received - # Test without rollover - should_rollover = Mock(return_value=False) - do_rollover = Mock() - writers_on_message_received = Mock() - - logger_instance.should_rollover = should_rollover - logger_instance.do_rollover = do_rollover - logger_instance.writer.on_message_received = writers_on_message_received - - msg = generate_message(0x123) - logger_instance.on_message_received(msg) - - should_rollover.assert_called_with(msg) - do_rollover.assert_not_called() - writers_on_message_received.assert_called_with(msg) + msg = generate_message(0x123) + logger_instance.on_message_received(msg) - # Test with rollover - should_rollover = Mock(return_value=True) - do_rollover = Mock() - writers_on_message_received = Mock() + should_rollover.assert_called_with(msg) + do_rollover.assert_not_called() + writers_on_message_received.assert_called_with(msg) - logger_instance.should_rollover = should_rollover - logger_instance.do_rollover = do_rollover - logger_instance.writer.on_message_received = writers_on_message_received + # Test with rollover + should_rollover = Mock(return_value=True) + do_rollover = Mock() + writers_on_message_received = Mock() - msg = generate_message(0x123) - logger_instance.on_message_received(msg) + logger_instance.should_rollover = should_rollover + logger_instance.do_rollover = do_rollover + logger_instance.writer.on_message_received = writers_on_message_received - should_rollover.assert_called_with(msg) - do_rollover.assert_called() - writers_on_message_received.assert_called_with(msg) + msg = generate_message(0x123) + logger_instance.on_message_received(msg) - # stop writer to enable cleanup of temp_dir - logger_instance.stop() + should_rollover.assert_called_with(msg) + do_rollover.assert_called() + writers_on_message_received.assert_called_with(msg) class TestSizedRotatingLogger: @@ -202,54 +198,48 @@ def test_create_instance(self, tmp_path): base_filename = "mylogfile.ASC" max_bytes = 512 - logger_instance = can.SizedRotatingLogger( + with can.SizedRotatingLogger( base_filename=tmp_path / base_filename, max_bytes=max_bytes - ) - assert Path(logger_instance.base_filename).name == base_filename - assert logger_instance.max_bytes == max_bytes - assert logger_instance.rollover_count == 0 - assert isinstance(logger_instance.writer, can.ASCWriter) - - logger_instance.stop() + ) as logger_instance: + assert Path(logger_instance.base_filename).name == base_filename + assert logger_instance.max_bytes == max_bytes + assert logger_instance.rollover_count == 0 + assert isinstance(logger_instance.writer, can.ASCWriter) def test_should_rollover(self, tmp_path): base_filename = "mylogfile.ASC" max_bytes = 512 - logger_instance = can.SizedRotatingLogger( + with can.SizedRotatingLogger( base_filename=tmp_path / base_filename, max_bytes=max_bytes - ) - msg = generate_message(0x123) - do_rollover = Mock() - logger_instance.do_rollover = do_rollover - - logger_instance.writer.file.tell = Mock(return_value=511) - assert logger_instance.should_rollover(msg) is False - logger_instance.on_message_received(msg) - do_rollover.assert_not_called() + ) as logger_instance: + msg = generate_message(0x123) + do_rollover = Mock() + logger_instance.do_rollover = do_rollover - logger_instance.writer.file.tell = Mock(return_value=512) - assert logger_instance.should_rollover(msg) is True - logger_instance.on_message_received(msg) - do_rollover.assert_called() + logger_instance.writer.file.tell = Mock(return_value=511) + assert logger_instance.should_rollover(msg) is False + logger_instance.on_message_received(msg) + do_rollover.assert_not_called() - logger_instance.stop() + logger_instance.writer.file.tell = Mock(return_value=512) + assert logger_instance.should_rollover(msg) is True + logger_instance.on_message_received(msg) + do_rollover.assert_called() def test_logfile_size(self, tmp_path): base_filename = "mylogfile.ASC" max_bytes = 1024 msg = generate_message(0x123) - logger_instance = can.SizedRotatingLogger( + with can.SizedRotatingLogger( base_filename=tmp_path / base_filename, max_bytes=max_bytes - ) - for _ in range(128): - logger_instance.on_message_received(msg) - - for file_path in os.listdir(tmp_path): - assert os.path.getsize(tmp_path / file_path) <= 1100 + ) as logger_instance: + for _ in range(128): + logger_instance.on_message_received(msg) - logger_instance.stop() + for file_path in os.listdir(tmp_path): + assert os.path.getsize(tmp_path / file_path) <= 1100 def test_logfile_size_context_manager(self, tmp_path): base_filename = "mylogfile.ASC" From 31d04d5d0cda5b4503919d8607efdea621f6ba0b Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sun, 14 Jan 2024 12:39:54 +0100 Subject: [PATCH 8/9] align can.LogReader docstring to can.Logger --- can/io/logger.py | 7 ++++--- can/io/player.py | 15 ++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/can/io/logger.py b/can/io/logger.py index 78699475b..ed7f15bd0 100644 --- a/can/io/logger.py +++ b/can/io/logger.py @@ -111,14 +111,15 @@ def Logger( # noqa: N802 for a given file suffix. The format is determined from the file suffix which can be one of: - * .asc: :class:`can.ASCWriter` + * .asc :class:`can.ASCWriter` * .blf :class:`can.BLFWriter` * .csv: :class:`can.CSVWriter` - * .db: :class:`can.SqliteWriter` + * .db :class:`can.SqliteWriter` * .log :class:`can.CanutilsLogWriter` + * .mf4 :class:`can.MF4Writer` + (optional, depends on `asammdf `_) * .trc :class:`can.TRCWriter` * .txt :class:`can.Printer` - * .mf4 :class:`can.MF4Writer` (optional, depends on asammdf) Any of these formats can be used with gzip compression by appending the suffix .gz (e.g. filename.asc.gz). However, third-party tools might not diff --git a/can/io/player.py b/can/io/player.py index 507e44a92..4cbd7ce16 100644 --- a/can/io/player.py +++ b/can/io/player.py @@ -86,13 +86,14 @@ def LogReader(filename: StringPathLike, **kwargs: Any) -> MessageReader: # noqa for a given file suffix. The format is determined from the file suffix which can be one of: - * .asc - * .blf - * .csv - * .db - * .log - * .mf4 (optional, depends on asammdf) - * .trc + * .asc :class:`can.ASCReader` + * .blf :class:`can.BLFReader` + * .csv :class:`can.CSVReader` + * .db :class:`can.SqliteReader` + * .log :class:`can.CanutilsLogReader` + * .mf4 :class:`can.MF4Reader` + (optional, depends on `asammdf `_) + * .trc :class:`can.TRCReader` Gzip compressed files can be used as long as the original files suffix is one of the above (e.g. filename.asc.gz). From b0c600f16f3d9650192f750e50ae0589b4f3483f Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Sun, 14 Jan 2024 12:50:55 +0100 Subject: [PATCH 9/9] replace deprecated `context` with `config_context` --- doc/configuration.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/configuration.rst b/doc/configuration.rst index 7b42017a9..2951a63b1 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -83,8 +83,8 @@ The configuration can also contain additional sections (or context): from can.interface import Bus - hs_bus = Bus(context='HS') - ms_bus = Bus(context='MS') + hs_bus = Bus(config_context='HS') + ms_bus = Bus(config_context='MS') Environment Variables ---------------------