6
6
import re
7
7
import shutil
8
8
import time
9
- from contextlib import AsyncExitStack , contextmanager
9
+ from contextlib import AsyncExitStack , ExitStack , contextmanager
10
10
from functools import partial , wraps
11
11
from inspect import isawaitable , iscoroutinefunction
12
12
from traceback import format_exception
@@ -194,8 +194,9 @@ class ServerFixture:
194
194
server.mount(MyComponent)
195
195
"""
196
196
197
- _log_handler : "_LogRecordCaptor"
197
+ _records : list [ logging . LogRecord ]
198
198
_server_future : asyncio .Task [Any ]
199
+ _exit_stack = ExitStack ()
199
200
200
201
def __init__ (
201
202
self ,
@@ -220,7 +221,7 @@ def __init__(
220
221
@property
221
222
def log_records (self ) -> List [logging .LogRecord ]:
222
223
"""A list of captured log records"""
223
- return self ._log_handler . records
224
+ return self ._records
224
225
225
226
def url (self , path : str = "" , query : Optional [Any ] = None ) -> str :
226
227
"""Return a URL string pointing to the host and point of the server
@@ -270,8 +271,9 @@ def list_logged_exceptions(
270
271
return found
271
272
272
273
async def __aenter__ (self : _Self ) -> _Self :
273
- self ._log_handler = _LogRecordCaptor ()
274
- logging .getLogger ().addHandler (self ._log_handler )
274
+ self ._exit_stack = ExitStack ()
275
+ self ._records = self ._exit_stack .enter_context (capture_idom_logs ())
276
+
275
277
app = self ._app or self .server_implementation .create_development_app ()
276
278
self .server_implementation .configure (app , self ._root_component )
277
279
@@ -282,6 +284,8 @@ async def __aenter__(self: _Self) -> _Self:
282
284
)
283
285
)
284
286
287
+ self ._exit_stack .callback (self ._server_future .cancel )
288
+
285
289
try :
286
290
await asyncio .wait_for (started .wait (), timeout = 3 )
287
291
except Exception :
@@ -297,19 +301,18 @@ async def __aexit__(
297
301
exc_value : Optional [BaseException ],
298
302
traceback : Optional [TracebackType ],
299
303
) -> None :
300
- self .mount ( None ) # reset the view
304
+ self ._exit_stack . close ()
301
305
302
- self ._server_future . cancel ()
306
+ self .mount ( None ) # reset the view
303
307
304
308
try :
305
309
await asyncio .wait_for (self ._server_future , timeout = 3 )
306
310
except asyncio .CancelledError :
307
311
pass
308
312
309
- logging .getLogger ().removeHandler (self ._log_handler )
310
313
logged_errors = self .list_logged_exceptions (del_log_records = False )
311
314
if logged_errors : # pragma: no cover
312
- raise logged_errors [0 ]
315
+ raise LogAssertionError ( "Unexpected logged exception" ) from logged_errors [0 ]
313
316
314
317
return None
315
318
@@ -323,75 +326,69 @@ def assert_idom_logged(
323
326
match_message : str = "" ,
324
327
error_type : type [Exception ] | None = None ,
325
328
match_error : str = "" ,
326
- clear_matched_records : bool = False ,
329
+ clear_after : bool = True ,
327
330
) -> Iterator [None ]:
328
331
"""Assert that IDOM produced a log matching the described message or error.
329
332
330
333
Args:
331
334
match_message: Must match a logged message.
332
335
error_type: Checks the type of logged exceptions.
333
336
match_error: Must match an error message.
334
- clear_matched_records : Whether to remove logged records that match.
337
+ clear_after : Whether to remove logged records that match.
335
338
"""
336
339
message_pattern = re .compile (match_message )
337
340
error_pattern = re .compile (match_error )
338
341
339
- try :
340
- with capture_idom_logs ( yield_existing = clear_matched_records ) as log_records :
342
+ with capture_idom_logs ( clear_after = clear_after ) as log_records :
343
+ try :
341
344
yield None
342
- except Exception :
343
- raise
344
- else :
345
- found = False
346
- for record in list ( log_records ):
347
- if (
348
- # record message matches
349
- message_pattern . findall ( record . getMessage ())
350
- # error type matches
351
- and (
352
- error_type is None
353
- or (
354
- record .exc_info is not None
355
- and record .exc_info [0 ] is not None
356
- and issubclass ( record . exc_info [ 0 ], error_type )
345
+ except Exception :
346
+ raise
347
+ else :
348
+ for record in list ( log_records ):
349
+ if (
350
+ # record message matches
351
+ message_pattern . findall ( record . getMessage ())
352
+ # error type matches
353
+ and (
354
+ error_type is None
355
+ or (
356
+ record . exc_info is not None
357
+ and record .exc_info [ 0 ] is not None
358
+ and issubclass ( record .exc_info [0 ], error_type )
359
+ )
357
360
)
358
- )
359
- # error message pattern matches
360
- and (
361
- not match_error
362
- or (
363
- record . exc_info is not None
364
- and error_pattern . findall (
365
- "" . join ( format_exception ( * record . exc_info ) )
361
+ # error message pattern matches
362
+ and (
363
+ not match_error
364
+ or (
365
+ record . exc_info is not None
366
+ and error_pattern . findall (
367
+ "" . join ( format_exception ( * record . exc_info ))
368
+ )
366
369
)
367
370
)
371
+ ):
372
+ break
373
+ else : # pragma: no cover
374
+ _raise_log_message_error (
375
+ "Could not find a log record matching the given" ,
376
+ match_message ,
377
+ error_type ,
378
+ match_error ,
368
379
)
369
- ):
370
- found = True
371
- if clear_matched_records :
372
- log_records .remove (record )
373
-
374
- if not found : # pragma: no cover
375
- _raise_log_message_error (
376
- "Could not find a log record matching the given" ,
377
- match_message ,
378
- error_type ,
379
- match_error ,
380
- )
381
380
382
381
383
382
@contextmanager
384
383
def assert_idom_did_not_log (
385
384
match_message : str = "" ,
386
385
error_type : type [Exception ] | None = None ,
387
386
match_error : str = "" ,
388
- clear_matched_records : bool = False ,
387
+ clear_after : bool = True ,
389
388
) -> Iterator [None ]:
390
389
"""Assert the inverse of :func:`assert_idom_logged`"""
391
390
try :
392
- with assert_idom_logged (
393
- match_message , error_type , match_error , clear_matched_records
394
- ):
391
+ with assert_idom_logged (match_message , error_type , match_error , clear_after ):
395
392
yield None
396
393
except LogAssertionError :
397
394
pass
@@ -421,45 +418,35 @@ def _raise_log_message_error(
421
418
422
419
423
420
@contextmanager
424
- def capture_idom_logs (
425
- yield_existing : bool = False ,
426
- ) -> Iterator [list [logging .LogRecord ]]:
421
+ def capture_idom_logs (clear_after : bool = True ) -> Iterator [list [logging .LogRecord ]]:
427
422
"""Capture logs from IDOM
428
423
429
- Parameters:
430
- yield_existing:
431
- If already inside an existing capture context yield the same list of logs.
432
- This is useful if you need to mutate the list of logs to affect behavior in
433
- the outer context.
424
+ Args:
425
+ clear_after:
426
+ Clear any records which were produced in this context when exiting.
434
427
"""
435
- if yield_existing :
436
- for handler in reversed (ROOT_LOGGER .handlers ):
437
- if isinstance (handler , _LogRecordCaptor ):
438
- yield handler .records
439
- return None
440
-
441
- handler = _LogRecordCaptor ()
442
428
original_level = ROOT_LOGGER .level
443
-
444
429
ROOT_LOGGER .setLevel (logging .DEBUG )
445
- ROOT_LOGGER .addHandler (handler )
446
430
try :
447
- yield handler .records
431
+ if _LOG_RECORD_CAPTOR in ROOT_LOGGER .handlers :
432
+ start_index = len (_LOG_RECORD_CAPTOR .records )
433
+ try :
434
+ yield _LOG_RECORD_CAPTOR .records
435
+ finally :
436
+ end_index = len (_LOG_RECORD_CAPTOR .records )
437
+ _LOG_RECORD_CAPTOR .records [start_index :end_index ] = []
438
+ return None
439
+
440
+ ROOT_LOGGER .addHandler (_LOG_RECORD_CAPTOR )
441
+ try :
442
+ yield _LOG_RECORD_CAPTOR .records
443
+ finally :
444
+ ROOT_LOGGER .removeHandler (_LOG_RECORD_CAPTOR )
445
+ _LOG_RECORD_CAPTOR .records .clear ()
448
446
finally :
449
- ROOT_LOGGER .removeHandler (handler )
450
447
ROOT_LOGGER .setLevel (original_level )
451
448
452
449
453
- class _LogRecordCaptor (logging .NullHandler ):
454
- def __init__ (self ) -> None :
455
- self .records : List [logging .LogRecord ] = []
456
- super ().__init__ ()
457
-
458
- def handle (self , record : logging .LogRecord ) -> bool :
459
- self .records .append (record )
460
- return True
461
-
462
-
463
450
class HookCatcher :
464
451
"""Utility for capturing a LifeCycleHook from a component
465
452
@@ -575,3 +562,16 @@ def use(
575
562
def clear_idom_web_modules_dir () -> None :
576
563
for path in IDOM_WEB_MODULES_DIR .current .iterdir ():
577
564
shutil .rmtree (path ) if path .is_dir () else path .unlink ()
565
+
566
+
567
+ class _LogRecordCaptor (logging .NullHandler ):
568
+ def __init__ (self ) -> None :
569
+ self .records : List [logging .LogRecord ] = []
570
+ super ().__init__ ()
571
+
572
+ def handle (self , record : logging .LogRecord ) -> bool :
573
+ self .records .append (record )
574
+ return True
575
+
576
+
577
+ _LOG_RECORD_CAPTOR = _LogRecordCaptor ()
0 commit comments