Skip to content

Commit ec105ac

Browse files
authored
Convert can.Logger and can.LogReader into functions, improve docs (#1703)
* turn can.Logger and can.LogReader into functions * improve file io documentation * fix doctest * don't check if class is None, check length of suffixes * fix typo * fix issues after rebase * fix test issues, use context manager to fix random PyPy failures * align can.LogReader docstring to can.Logger * replace deprecated `context` with `config_context`
1 parent e173bf1 commit ec105ac

12 files changed

+448
-419
lines changed

can/io/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
"CSVWriter",
1616
"Logger",
1717
"LogReader",
18+
"MESSAGE_READERS",
19+
"MESSAGE_WRITERS",
1820
"MessageSync",
1921
"MF4Reader",
2022
"MF4Writer",
@@ -39,8 +41,8 @@
3941
]
4042

4143
# Generic
42-
from .logger import BaseRotatingLogger, Logger, SizedRotatingLogger
43-
from .player import LogReader, MessageSync
44+
from .logger import MESSAGE_WRITERS, BaseRotatingLogger, Logger, SizedRotatingLogger
45+
from .player import MESSAGE_READERS, LogReader, MessageSync
4446

4547
# isort: split
4648

can/io/logger.py

Lines changed: 98 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
Callable,
1414
ClassVar,
1515
Dict,
16+
Final,
1617
Literal,
1718
Optional,
1819
Set,
@@ -24,15 +25,13 @@
2425
from typing_extensions import Self
2526

2627
from .._entry_points import read_entry_points
27-
from ..listener import Listener
2828
from ..message import Message
2929
from ..typechecking import AcceptedIOType, FileLike, StringPathLike
3030
from .asc import ASCWriter
3131
from .blf import BLFWriter
3232
from .canutils import CanutilsLogWriter
3333
from .csv import CSVWriter
3434
from .generic import (
35-
BaseIOHandler,
3635
BinaryIOMessageWriter,
3736
FileIOMessageWriter,
3837
MessageWriter,
@@ -42,20 +41,85 @@
4241
from .sqlite import SqliteWriter
4342
from .trc import TRCWriter
4443

44+
#: A map of file suffixes to their corresponding
45+
#: :class:`can.io.generic.MessageWriter` class
46+
MESSAGE_WRITERS: Final[Dict[str, Type[MessageWriter]]] = {
47+
".asc": ASCWriter,
48+
".blf": BLFWriter,
49+
".csv": CSVWriter,
50+
".db": SqliteWriter,
51+
".log": CanutilsLogWriter,
52+
".mf4": MF4Writer,
53+
".trc": TRCWriter,
54+
".txt": Printer,
55+
}
56+
57+
58+
def _update_writer_plugins() -> None:
59+
"""Update available message writer plugins from entry points."""
60+
for entry_point in read_entry_points("can.io.message_writer"):
61+
if entry_point.key in MESSAGE_WRITERS:
62+
continue
63+
64+
writer_class = entry_point.load()
65+
if issubclass(writer_class, MessageWriter):
66+
MESSAGE_WRITERS[entry_point.key] = writer_class
67+
68+
69+
def _get_logger_for_suffix(suffix: str) -> Type[MessageWriter]:
70+
try:
71+
return MESSAGE_WRITERS[suffix]
72+
except KeyError:
73+
raise ValueError(
74+
f'No write support for unknown log format "{suffix}"'
75+
) from None
76+
4577

46-
class Logger(MessageWriter):
78+
def _compress(
79+
filename: StringPathLike, **kwargs: Any
80+
) -> Tuple[Type[MessageWriter], FileLike]:
4781
"""
48-
Logs CAN messages to a file.
82+
Return the suffix and io object of the decompressed file.
83+
File will automatically recompress upon close.
84+
"""
85+
suffixes = pathlib.Path(filename).suffixes
86+
if len(suffixes) != 2:
87+
raise ValueError(
88+
f"No write support for unknown log format \"{''.join(suffixes)}\""
89+
) from None
90+
91+
real_suffix = suffixes[-2].lower()
92+
if real_suffix in (".blf", ".db"):
93+
raise ValueError(
94+
f"The file type {real_suffix} is currently incompatible with gzip."
95+
)
96+
logger_type = _get_logger_for_suffix(real_suffix)
97+
append = kwargs.get("append", False)
98+
99+
if issubclass(logger_type, BinaryIOMessageWriter):
100+
mode = "ab" if append else "wb"
101+
else:
102+
mode = "at" if append else "wt"
103+
104+
return logger_type, gzip.open(filename, mode)
105+
106+
107+
def Logger( # noqa: N802
108+
filename: Optional[StringPathLike], **kwargs: Any
109+
) -> MessageWriter:
110+
"""Find and return the appropriate :class:`~can.io.generic.MessageWriter` instance
111+
for a given file suffix.
49112
50113
The format is determined from the file suffix which can be one of:
51-
* .asc: :class:`can.ASCWriter`
114+
* .asc :class:`can.ASCWriter`
52115
* .blf :class:`can.BLFWriter`
53116
* .csv: :class:`can.CSVWriter`
54-
* .db: :class:`can.SqliteWriter`
117+
* .db :class:`can.SqliteWriter`
55118
* .log :class:`can.CanutilsLogWriter`
119+
* .mf4 :class:`can.MF4Writer`
120+
(optional, depends on `asammdf <https://github.com/danielhrisca/asammdf>`_)
56121
* .trc :class:`can.TRCWriter`
57122
* .txt :class:`can.Printer`
58-
* .mf4 :class:`can.MF4Writer` (optional, depends on asammdf)
59123
60124
Any of these formats can be used with gzip compression by appending
61125
the suffix .gz (e.g. filename.asc.gz). However, third-party tools might not
@@ -65,97 +129,33 @@ class Logger(MessageWriter):
65129
66130
The log files may be incomplete until `stop()` is called due to buffering.
67131
132+
:param filename:
133+
the filename/path of the file to write to,
134+
may be a path-like object or None to
135+
instantiate a :class:`~can.Printer`
136+
:raises ValueError:
137+
if the filename's suffix is of an unknown file type
138+
68139
.. note::
69-
This class itself is just a dispatcher, and any positional and keyword
140+
This function itself is just a dispatcher, and any positional and keyword
70141
arguments are passed on to the returned instance.
71142
"""
72143

73-
fetched_plugins = False
74-
message_writers: ClassVar[Dict[str, Type[MessageWriter]]] = {
75-
".asc": ASCWriter,
76-
".blf": BLFWriter,
77-
".csv": CSVWriter,
78-
".db": SqliteWriter,
79-
".log": CanutilsLogWriter,
80-
".mf4": MF4Writer,
81-
".trc": TRCWriter,
82-
".txt": Printer,
83-
}
84-
85-
@staticmethod
86-
def __new__( # type: ignore[misc]
87-
cls: Any, filename: Optional[StringPathLike], **kwargs: Any
88-
) -> MessageWriter:
89-
"""
90-
:param filename:
91-
the filename/path of the file to write to,
92-
may be a path-like object or None to
93-
instantiate a :class:`~can.Printer`
94-
:raises ValueError:
95-
if the filename's suffix is of an unknown file type
96-
"""
97-
if filename is None:
98-
return Printer(**kwargs)
99-
100-
if not Logger.fetched_plugins:
101-
Logger.message_writers.update(
102-
{
103-
writer.key: cast(Type[MessageWriter], writer.load())
104-
for writer in read_entry_points("can.io.message_writer")
105-
}
106-
)
107-
Logger.fetched_plugins = True
108-
109-
suffix = pathlib.PurePath(filename).suffix.lower()
110-
111-
file_or_filename: AcceptedIOType = filename
112-
if suffix == ".gz":
113-
logger_type, file_or_filename = Logger.compress(filename, **kwargs)
114-
else:
115-
logger_type = cls._get_logger_for_suffix(suffix)
116-
117-
return logger_type(file=file_or_filename, **kwargs)
118-
119-
@classmethod
120-
def _get_logger_for_suffix(cls, suffix: str) -> Type[MessageWriter]:
121-
try:
122-
logger_type = Logger.message_writers[suffix]
123-
if logger_type is None:
124-
raise ValueError(f'failed to import logger for extension "{suffix}"')
125-
return logger_type
126-
except KeyError:
127-
raise ValueError(
128-
f'No write support for this unknown log format "{suffix}"'
129-
) from None
130-
131-
@classmethod
132-
def compress(
133-
cls, filename: StringPathLike, **kwargs: Any
134-
) -> Tuple[Type[MessageWriter], FileLike]:
135-
"""
136-
Return the suffix and io object of the decompressed file.
137-
File will automatically recompress upon close.
138-
"""
139-
real_suffix = pathlib.Path(filename).suffixes[-2].lower()
140-
if real_suffix in (".blf", ".db"):
141-
raise ValueError(
142-
f"The file type {real_suffix} is currently incompatible with gzip."
143-
)
144-
logger_type = cls._get_logger_for_suffix(real_suffix)
145-
append = kwargs.get("append", False)
146-
147-
if issubclass(logger_type, BinaryIOMessageWriter):
148-
mode = "ab" if append else "wb"
149-
else:
150-
mode = "at" if append else "wt"
144+
if filename is None:
145+
return Printer(**kwargs)
151146

152-
return logger_type, gzip.open(filename, mode)
147+
_update_writer_plugins()
153148

154-
def on_message_received(self, msg: Message) -> None:
155-
pass
149+
suffix = pathlib.PurePath(filename).suffix.lower()
150+
file_or_filename: AcceptedIOType = filename
151+
if suffix == ".gz":
152+
logger_type, file_or_filename = _compress(filename, **kwargs)
153+
else:
154+
logger_type = _get_logger_for_suffix(suffix)
155+
return logger_type(file=file_or_filename, **kwargs)
156156

157157

158-
class BaseRotatingLogger(Listener, BaseIOHandler, ABC):
158+
class BaseRotatingLogger(MessageWriter, ABC):
159159
"""
160160
Base class for rotating CAN loggers. This class is not meant to be
161161
instantiated directly. Subclasses must implement the :meth:`should_rollover`
@@ -187,20 +187,15 @@ class BaseRotatingLogger(Listener, BaseIOHandler, ABC):
187187
rollover_count: int = 0
188188

189189
def __init__(self, **kwargs: Any) -> None:
190-
Listener.__init__(self)
191-
BaseIOHandler.__init__(self, file=None)
190+
super().__init__(**{**kwargs, "file": None})
192191

193192
self.writer_kwargs = kwargs
194193

195-
# Expected to be set by the subclass
196-
self._writer: Optional[FileIOMessageWriter] = None
197-
198194
@property
195+
@abstractmethod
199196
def writer(self) -> FileIOMessageWriter:
200197
"""This attribute holds an instance of a writer class which manages the actual file IO."""
201-
if self._writer is not None:
202-
return self._writer
203-
raise ValueError(f"{self.__class__.__name__}.writer is None.")
198+
raise NotImplementedError
204199

205200
def rotation_filename(self, default_name: StringPathLike) -> StringPathLike:
206201
"""Modify the filename of a log file when rotating.
@@ -270,7 +265,7 @@ def _get_new_writer(self, filename: StringPathLike) -> FileIOMessageWriter:
270265
logger = Logger(filename=filename, **self.writer_kwargs)
271266
if isinstance(logger, FileIOMessageWriter):
272267
return logger
273-
if isinstance(logger, Printer) and logger.file is not None:
268+
elif isinstance(logger, Printer) and logger.file is not None:
274269
return cast(FileIOMessageWriter, logger)
275270

276271
raise ValueError(
@@ -373,6 +368,10 @@ def __init__(
373368

374369
self._writer = self._get_new_writer(self.base_filename)
375370

371+
@property
372+
def writer(self) -> FileIOMessageWriter:
373+
return self._writer
374+
376375
def should_rollover(self, msg: Message) -> bool:
377376
if self.max_bytes <= 0:
378377
return False

0 commit comments

Comments
 (0)