1313 Callable ,
1414 ClassVar ,
1515 Dict ,
16+ Final ,
1617 Literal ,
1718 Optional ,
1819 Set ,
2425from typing_extensions import Self
2526
2627from .._entry_points import read_entry_points
27- from ..listener import Listener
2828from ..message import Message
2929from ..typechecking import AcceptedIOType , FileLike , StringPathLike
3030from .asc import ASCWriter
3131from .blf import BLFWriter
3232from .canutils import CanutilsLogWriter
3333from .csv import CSVWriter
3434from .generic import (
35- BaseIOHandler ,
3635 BinaryIOMessageWriter ,
3736 FileIOMessageWriter ,
3837 MessageWriter ,
4241from .sqlite import SqliteWriter
4342from .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