13
13
Callable ,
14
14
ClassVar ,
15
15
Dict ,
16
+ Final ,
16
17
Literal ,
17
18
Optional ,
18
19
Set ,
24
25
from typing_extensions import Self
25
26
26
27
from .._entry_points import read_entry_points
27
- from ..listener import Listener
28
28
from ..message import Message
29
29
from ..typechecking import AcceptedIOType , FileLike , StringPathLike
30
30
from .asc import ASCWriter
31
31
from .blf import BLFWriter
32
32
from .canutils import CanutilsLogWriter
33
33
from .csv import CSVWriter
34
34
from .generic import (
35
- BaseIOHandler ,
36
35
BinaryIOMessageWriter ,
37
36
FileIOMessageWriter ,
38
37
MessageWriter ,
42
41
from .sqlite import SqliteWriter
43
42
from .trc import TRCWriter
44
43
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
+
45
77
46
- class Logger (MessageWriter ):
78
+ def _compress (
79
+ filename : StringPathLike , ** kwargs : Any
80
+ ) -> Tuple [Type [MessageWriter ], FileLike ]:
47
81
"""
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.
49
112
50
113
The format is determined from the file suffix which can be one of:
51
- * .asc: :class:`can.ASCWriter`
114
+ * .asc :class:`can.ASCWriter`
52
115
* .blf :class:`can.BLFWriter`
53
116
* .csv: :class:`can.CSVWriter`
54
- * .db: :class:`can.SqliteWriter`
117
+ * .db :class:`can.SqliteWriter`
55
118
* .log :class:`can.CanutilsLogWriter`
119
+ * .mf4 :class:`can.MF4Writer`
120
+ (optional, depends on `asammdf <https://github.com/danielhrisca/asammdf>`_)
56
121
* .trc :class:`can.TRCWriter`
57
122
* .txt :class:`can.Printer`
58
- * .mf4 :class:`can.MF4Writer` (optional, depends on asammdf)
59
123
60
124
Any of these formats can be used with gzip compression by appending
61
125
the suffix .gz (e.g. filename.asc.gz). However, third-party tools might not
@@ -65,97 +129,33 @@ class Logger(MessageWriter):
65
129
66
130
The log files may be incomplete until `stop()` is called due to buffering.
67
131
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
+
68
139
.. 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
70
141
arguments are passed on to the returned instance.
71
142
"""
72
143
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 )
151
146
152
- return logger_type , gzip . open ( filename , mode )
147
+ _update_writer_plugins ( )
153
148
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 )
156
156
157
157
158
- class BaseRotatingLogger (Listener , BaseIOHandler , ABC ):
158
+ class BaseRotatingLogger (MessageWriter , ABC ):
159
159
"""
160
160
Base class for rotating CAN loggers. This class is not meant to be
161
161
instantiated directly. Subclasses must implement the :meth:`should_rollover`
@@ -187,20 +187,15 @@ class BaseRotatingLogger(Listener, BaseIOHandler, ABC):
187
187
rollover_count : int = 0
188
188
189
189
def __init__ (self , ** kwargs : Any ) -> None :
190
- Listener .__init__ (self )
191
- BaseIOHandler .__init__ (self , file = None )
190
+ super ().__init__ (** {** kwargs , "file" : None })
192
191
193
192
self .writer_kwargs = kwargs
194
193
195
- # Expected to be set by the subclass
196
- self ._writer : Optional [FileIOMessageWriter ] = None
197
-
198
194
@property
195
+ @abstractmethod
199
196
def writer (self ) -> FileIOMessageWriter :
200
197
"""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
204
199
205
200
def rotation_filename (self , default_name : StringPathLike ) -> StringPathLike :
206
201
"""Modify the filename of a log file when rotating.
@@ -270,7 +265,7 @@ def _get_new_writer(self, filename: StringPathLike) -> FileIOMessageWriter:
270
265
logger = Logger (filename = filename , ** self .writer_kwargs )
271
266
if isinstance (logger , FileIOMessageWriter ):
272
267
return logger
273
- if isinstance (logger , Printer ) and logger .file is not None :
268
+ elif isinstance (logger , Printer ) and logger .file is not None :
274
269
return cast (FileIOMessageWriter , logger )
275
270
276
271
raise ValueError (
@@ -373,6 +368,10 @@ def __init__(
373
368
374
369
self ._writer = self ._get_new_writer (self .base_filename )
375
370
371
+ @property
372
+ def writer (self ) -> FileIOMessageWriter :
373
+ return self ._writer
374
+
376
375
def should_rollover (self , msg : Message ) -> bool :
377
376
if self .max_bytes <= 0 :
378
377
return False
0 commit comments