Skip to content

Commit 0aaa988

Browse files
committed
Add mp.logger class, and mp.printf() and mp.disp() functions.
Drop-in replacements for fprintf() and disp(). By default they behave identically, but can use an mp.logger object to redirect the output to a file, or elsewhere via a custom mp.logger subclass.
1 parent 6651d45 commit 0aaa988

12 files changed

Lines changed: 426 additions & 1 deletion

CHANGES.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ Change history for MP-Test
55
since 8.1
66
---------
77

8+
#### 7/23/26
9+
- Add `mp.logger` class, and `mp_printf()` and `mp_disp()` functions
10+
which can be used as drop-in replacements for `fprintf()` and `disp()`.
11+
By default they behave identically, but can use an `mp.logger` object to
12+
redirect the output to a file, or elsewhere via a custom `mp.logger`
13+
subclass.
14+
815
#### 6/15/26
916
- Limit `t_is()` display to top 10 toleration violations if there are
1017
more than 15 elements that do not meet the desired tolerance.

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,35 @@ particular function.
273273
(all char arrays). Calling `mptestver` without assigning the return value
274274
prints the version and release date of the current installation of MP-Test.
275275

276+
- __mp.disp__ — drop-in replacement for `disp()`
277+
```
278+
mp_disp(obj)
279+
```
280+
Optionally redirects the output of `disp()` to a file via an `mp.logger`
281+
object, or elsewhere via a custom `mp.logger` subclass.
282+
283+
- __mp.printf__ — drop-in replacement for `fprintf()`
284+
```
285+
mp_printf(format_str, arg1, ...)
286+
mp_printf(fid, format_str, arg1, ...)
287+
```
288+
Optionally redirects the output of `fprintf()` to a file via an `mp.logger`
289+
object, or elsewhere via a custom `mp.logger` subclass. If the first
290+
argument is a file ID, it does not redirect anything.
291+
292+
- __mp.logger_manager__ — manage redirection for `mp_disp()` and `mp_printf()`
293+
```
294+
mp.logger.manager('init', 'path/to/my/log-file.txt')
295+
mp_disp(obj)
296+
mp_printf(format_str, arg1, ...)
297+
mp.logger.manager('init', 'different-log-file.txt')
298+
mp_disp(obj)
299+
mp_printf(format_str, arg1, ...)
300+
mp.logger.manager('clear')
301+
```
302+
Used to manage the `mp.logger` object that handles any output redirection
303+
for `mp_disp()` and `mp_printf()`.
304+
276305

277306
#### Private Functions
278307

lib/+mp/logger.m

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
classdef logger < handle
2+
% mp.logger - Handles output typically sent to console.
3+
%
4+
% Example usage::
5+
%
6+
% mp.logger.manager('init', log_file_path);
7+
% mp_disp(...);
8+
% mp_printf(...);
9+
% mp.logger.manager('clear');
10+
%
11+
% mp.logger Properties:
12+
% * fid - file ID returned by ``fopen()``
13+
% * write_to_console - writes to both console **and** file, if true
14+
%
15+
% mp.logger Methods:
16+
% * logger - constructor
17+
% * init - initialize logger object (open log file)
18+
% * printf - prints to log
19+
% * finalize - finalize logger object (close log file)
20+
21+
% MATPOWER
22+
% Copyright (c) 2026, Ray Zimmerman
23+
% by Ray Zimmerman
24+
%
25+
% This file is part of MP-Test.
26+
% Covered by the 3-clause BSD License (see LICENSE file for details).
27+
% See https://github.com/MATPOWER/mptest for more info.
28+
29+
properties
30+
fid % file ID returned by ``fopen()``
31+
write_to_console % writes to both console **and** file, if true
32+
end %% properties
33+
34+
methods
35+
function obj = logger(varargin)
36+
% Constructor.
37+
% ::
38+
%
39+
% obj = mp.logger()
40+
% obj = mp.logger(log_file_path)
41+
% obj = mp.logger(log_file_path, permission)
42+
% obj = mp.logger(log_file_path, permission, write_to_console)
43+
%
44+
% Inputs:
45+
% log_file_path (char array) : path to directory or file to which
46+
% all output will be logged; if it points to an existing
47+
% directory, ``'mp.logger_log.txt'`` will be appended
48+
% permission (char array) : *(default = ``'a'``)* permissions for
49+
% ``fopen()``
50+
% write_to_console (logical) : *(default = 0)* writes to both
51+
% console **and** file, if true
52+
53+
obj.init(varargin{:});
54+
end
55+
56+
function obj = init(obj, varargin)
57+
% Initialize logger object (open log file).
58+
% ::
59+
%
60+
% obj.init(log_file_path)
61+
% obj.init(log_file_path, permission)
62+
% obj.init(log_file_path, permission, write_to_console)
63+
%
64+
% Inputs:
65+
% log_file_path (char array) : path to directory or file to which
66+
% all output will be logged; if it points to an existing
67+
% directory, ``'mp.logger_log.txt'`` will be appended
68+
% permission (char array) : *(default = ``'a'``)* permissions for
69+
% ``fopen()``
70+
% write_to_console (logical) : *(default = 0)* writes to both
71+
% console **and** file, if true
72+
73+
obj.set_file(varargin{:});
74+
end
75+
76+
function obj = set_file(obj, log_file_path, permission, write_to_console)
77+
% Open log file.
78+
% ::
79+
%
80+
% obj.set_file(log_file_path)
81+
% obj.set_file(log_file_path, permission)
82+
% obj.set_file(log_file_path, permission, write_to_console)
83+
%
84+
% Inputs:
85+
% log_file_path (char array) : path to directory or file to which
86+
% all output will be logged; if it points to an existing
87+
% directory, ``'mp.logger_log.txt'`` will be appended
88+
% permission (char array) : *(default = ``'a'``)* permissions for
89+
% ``fopen()``
90+
% write_to_console (logical) : *(default = 0)* writes to both
91+
% console **and** file, if true
92+
93+
%% set default inputs
94+
default_log_file_name = 'mp.logger_log.txt';
95+
if nargin < 4
96+
write_to_console = false;
97+
if nargin < 3
98+
permission = 'a';
99+
end
100+
end
101+
if nargin < 2 || isempty(log_file_path)
102+
log_file_path = '.';
103+
end
104+
if exist(log_file_path, 'dir')
105+
log_file_path = fullfile(log_file_path, default_log_file_name);
106+
end
107+
108+
%% close any currently open file
109+
if obj.fid > 2
110+
fclose(obj.fid);
111+
end
112+
113+
%% open new log file
114+
[fid, msg] = fopen(log_file_path, permission);
115+
if fid == -1
116+
error('mp.logger.set_file: unable to open log file: %s', log_file_path);
117+
else
118+
obj.fid = fid;
119+
end
120+
121+
obj.write_to_console = write_to_console;
122+
end
123+
124+
function obj = printf(obj, varargin)
125+
% Print to log.
126+
% ::
127+
%
128+
% obj.printf(...)
129+
%
130+
% Inputs are identical to those of ``fprintf()``.
131+
132+
if nargin < 2
133+
error('mp.logger.printf: nothing to print');
134+
elseif ischar(varargin{1}) || varargin{1} == 1 || varargin{1} == 2
135+
if obj.fid > 0 %% print to log file
136+
fprintf(obj.fid, varargin{:});
137+
if obj.write_to_console
138+
fprintf(varargin{:});
139+
end
140+
else
141+
error('mp.logger.printf: log file not open');
142+
end
143+
elseif varargin{1} > 2 %% writing to file with provided file ID
144+
fprintf(varargin{:});
145+
else
146+
error('mp.logger.printf: first argument must be char array or file ID');
147+
end
148+
end
149+
150+
function obj = finalize(obj, varargin)
151+
% Finalize logger object (close log file).
152+
% ::
153+
%
154+
% obj.init(log_file_path)
155+
% obj.init(log_file_path, permission)
156+
% obj.init(log_file_path, permission, write_to_console)
157+
%
158+
% Inputs:
159+
% log_file_path (char array) : path to directory or file to which
160+
% all output will be logged; if it points to an existing
161+
% directory, ``'mp.logger_log.txt'`` will be appended
162+
% permission (char array) : *(default = ``'a'``)* permissions for
163+
% ``fopen()``
164+
% write_to_console (logical) : *(default = 0)* writes to both
165+
% console **and** file, if true
166+
167+
if obj.fid > 2
168+
fclose(obj.fid);
169+
obj.fid = [];
170+
end
171+
end
172+
end %% methods
173+
174+
methods (Static)
175+
function obj = manager(action, varargin)
176+
% Manage the logger object used by mp_printf() and mp_disp().
177+
% ::
178+
%
179+
% mp.logger.manager('init');
180+
% mp.logger.manager('init', logger);
181+
% mp.logger.manager('init', log_file_path);
182+
% mp.logger.manager('init', log_file_path, permission);
183+
% mp.logger.manager('init', log_file_path, permission, write_to_console);
184+
% logger = mp.logger.manager('get');
185+
% logger = mp.logger.manager('clear');
186+
%
187+
% Input:
188+
% action (char array) : one of:
189+
%
190+
% - ``'init'`` - initialize logger object, after clearing any
191+
% existing one
192+
% - ``'get'`` - retreive logger object
193+
% - ``'clear'`` - clear logger object
194+
% logger (mp.logger) : an existing, ready-to-use logger object
195+
% log_file_path (char array) : path to directory or file to which
196+
% all output will be logged; if it points to an existing
197+
% directory, ``'mp.logger_log.txt'`` will be appended
198+
% permission (char array) : *(default = ``'a'``)* permissions for
199+
% ``fopen()``
200+
% write_to_console (logical) : *(default = 0)* writes to both
201+
% console **and** file, if true
202+
203+
persistent logger; %% logger object (or empty)
204+
205+
switch lower(action)
206+
case 'get'
207+
obj = logger;
208+
case 'init'
209+
mp.logger.manager('clear');
210+
if nargin < 2
211+
logger = mp.logger();
212+
elseif ischar(varargin{1}) || isstring(varargin{1})
213+
logger = mp.logger(varargin{:});
214+
elseif isa(varargin{1}, 'mp.logger')
215+
logger = varargin{1};
216+
else
217+
error('mp.logger.manager: second argument must be a file name or mp.logger object, not a %s', class(varargin{1}));
218+
end
219+
case 'clear'
220+
if ~isempty(logger)
221+
logger.finalize();
222+
logger = [];
223+
end
224+
end
225+
end
226+
end %% methods
227+
end %% classdef

lib/mp_disp.m

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
function mp_disp(obj)
2+
% mp_disp Replacment for ``disp()`` that can redirect output.
3+
% ::
4+
%
5+
% mp.logger.manager('init', 'my-log-file.txt');
6+
% mp_disp(something_to_be_displayed);
7+
% mp.logger.manager('clear');
8+
9+
logger = mp.logger.manager('get');
10+
if isempty(logger)
11+
disp(obj);
12+
else
13+
logger.printf(evalc('disp(obj)'));
14+
end

lib/mp_printf.m

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
function mp_printf(varargin)
2+
% mp_printf Replacment for ``fprintf()`` that can redirect output.
3+
% ::
4+
%
5+
% mp.logger.manager('init', 'my-log-file.txt');
6+
% mp_printf('A line of %s to be printed.\n', 'text');
7+
% mp.logger.manager('clear');
8+
9+
logger = mp.logger.manager('get');
10+
if isempty(logger)
11+
fprintf(varargin{:});
12+
else
13+
logger.printf(varargin{:});
14+
end

lib/mptestver.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
v = struct( 'Name', 'MP-Test', ...
2525
'Version', '8.1+', ...
2626
'Release', '', ...
27-
'Date', '15-Jun-2026' );
27+
'Date', '23-Jul-2026' );
2828
if nargout > 0
2929
if nargin > 0
3030
rv = v;

0 commit comments

Comments
 (0)