1
+ import re
2
+ import logging
1
3
from urllib .parse import urlunparse , urlencode
2
4
from typing import (
3
5
Callable ,
8
10
Dict ,
9
11
Generic ,
10
12
TypeVar ,
13
+ List ,
14
+ Union ,
11
15
)
12
- from weakref import finalize
16
+ from types import TracebackType
13
17
18
+ from loguru import logger
14
19
from selenium .webdriver .remote .webdriver import WebDriver
15
20
from selenium .webdriver import Chrome
16
21
@@ -46,13 +51,19 @@ def create_simple_selenium_web_driver(
46
51
return driver
47
52
48
53
54
+ _Self = TypeVar ("_Self" , bound = "ServerMountPoint[Any, Any]" )
49
55
_Mount = TypeVar ("_Mount" )
50
56
_Server = TypeVar ("_Server" , bound = AnyRenderServer )
51
57
52
58
53
59
class ServerMountPoint (Generic [_Mount , _Server ]):
60
+ """A context manager for imperatively mounting views to a render server when testing"""
54
61
55
- __slots__ = "server" , "host" , "port" , "mount" , "__weakref__"
62
+ mount : _Mount
63
+ server : _Server
64
+
65
+ _log_handler : "_LogRecordCaptor"
66
+ _loguru_handler_id : int
56
67
57
68
def __init__ (
58
69
self ,
@@ -64,20 +75,60 @@ def __init__(
64
75
mount_and_server_constructor : "Callable[..., Tuple[_Mount, _Server]]" = hotswap_server , # type: ignore
65
76
app : Optional [Any ] = None ,
66
77
** other_options : Any ,
67
- ):
78
+ ) -> None :
68
79
self .host = host
69
80
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
+ )
78
91
)
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
81
132
82
133
def url (self , path : str = "" , query : Optional [Any ] = None ) -> str :
83
134
return urlunparse (
@@ -90,3 +141,34 @@ def url(self, path: str = "", query: Optional[Any] = None) -> str:
90
141
"" ,
91
142
]
92
143
)
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