1+ import re
2+ import logging
13from urllib .parse import urlunparse , urlencode
24from typing import (
35 Callable ,
810 Dict ,
911 Generic ,
1012 TypeVar ,
13+ List ,
14+ Union ,
1115)
12- from weakref import finalize
16+ from types import TracebackType
1317
18+ from loguru import logger
1419from selenium .webdriver .remote .webdriver import WebDriver
1520from selenium .webdriver import Chrome
1621
@@ -46,13 +51,19 @@ def create_simple_selenium_web_driver(
4651 return driver
4752
4853
54+ _Self = TypeVar ("_Self" , bound = "ServerMountPoint[Any, Any]" )
4955_Mount = TypeVar ("_Mount" )
5056_Server = TypeVar ("_Server" , bound = AnyRenderServer )
5157
5258
5359class ServerMountPoint (Generic [_Mount , _Server ]):
60+ """A context manager for imperatively mounting views to a render server when testing"""
5461
55- __slots__ = "server" , "host" , "port" , "mount" , "__weakref__"
62+ mount : _Mount
63+ server : _Server
64+
65+ _log_handler : "_LogRecordCaptor"
66+ _loguru_handler_id : int
5667
5768 def __init__ (
5869 self ,
@@ -64,20 +75,60 @@ def __init__(
6475 mount_and_server_constructor : "Callable[..., Tuple[_Mount, _Server]]" = hotswap_server , # type: ignore
6576 app : Optional [Any ] = None ,
6677 ** other_options : Any ,
67- ):
78+ ) -> None :
6879 self .host = host
6980 self .port = port or find_available_port (host )
70- self .mount , self .server = mount_and_server_constructor (
71- server_type ,
72- self .host ,
73- self .port ,
74- server_config ,
75- run_kwargs ,
76- app ,
77- ** other_options ,
81+ self ._mount_and_server_constructor : "Callable[[], Tuple[_Mount, _Server]]" = (
82+ lambda : mount_and_server_constructor (
83+ server_type ,
84+ self .host ,
85+ self .port ,
86+ server_config ,
87+ run_kwargs ,
88+ app ,
89+ ** other_options ,
90+ )
7891 )
79- # stop server once mount is done being used
80- finalize (self , self .server .stop )
92+
93+ @property
94+ def log_records (self ) -> List [logging .LogRecord ]:
95+ """A list of captured log records"""
96+ return self ._log_handler .records
97+
98+ def assert_logged_exception (
99+ self ,
100+ error_type : Type [Exception ],
101+ error_pattern : str ,
102+ clear_after : bool = True ,
103+ ) -> None :
104+ """Assert that a given error type and message were logged"""
105+ try :
106+ re_pattern = re .compile (error_pattern )
107+ for record in self .log_records :
108+ if record .exc_info is not None :
109+ error = record .exc_info [1 ]
110+ if isinstance (error , error_type ) and re_pattern .search (str (error )):
111+ break
112+ else : # pragma: no cover
113+ assert False , f"did not raise { error_type } matching { error_pattern !r} "
114+ finally :
115+ if clear_after :
116+ self .log_records .clear ()
117+
118+ def raise_first_logged_exception (
119+ self ,
120+ exclude_exc_types : Union [Type [Exception ], Tuple [Type [Exception ], ...]] = (),
121+ ) -> None :
122+ """Raise the first logged exception (if any)
123+
124+ Args:
125+ exclude_exc_types: Any exception types to ignore
126+ """
127+ for record in self ._log_handler .records :
128+ if record .exc_info is not None :
129+ error = record .exc_info [1 ]
130+ if error is not None and not isinstance (error , exclude_exc_types ):
131+ raise error
81132
82133 def url (self , path : str = "" , query : Optional [Any ] = None ) -> str :
83134 return urlunparse (
@@ -90,3 +141,34 @@ def url(self, path: str = "", query: Optional[Any] = None) -> str:
90141 "" ,
91142 ]
92143 )
144+
145+ def __enter__ (self : _Self ) -> _Self :
146+ self ._log_handler = _LogRecordCaptor ()
147+ logging .getLogger ().addHandler (self ._log_handler )
148+ self ._loguru_handler_id = logger .add (self ._log_handler , format = "{message}" )
149+ self .mount , self .server = self ._mount_and_server_constructor ()
150+ return self
151+
152+ def __exit__ (
153+ self ,
154+ exc_type : Optional [Type [BaseException ]],
155+ exc_value : Optional [BaseException ],
156+ traceback : Optional [TracebackType ],
157+ ) -> None :
158+ self .server .stop ()
159+
160+ logging .getLogger ().removeHandler (self ._log_handler )
161+ logger .remove (self ._loguru_handler_id )
162+
163+ self .raise_first_logged_exception ()
164+
165+ return None
166+
167+
168+ class _LogRecordCaptor (logging .NullHandler ):
169+ def __init__ (self ) -> None :
170+ self .records : List [logging .LogRecord ] = []
171+ super ().__init__ ()
172+
173+ def handle (self , record : logging .LogRecord ) -> None :
174+ self .records .append (record )
0 commit comments