1
1
from __future__ import annotations
2
2
3
- import logging
4
3
import platform
5
4
import signal
6
5
import socket
7
6
import sys
8
7
from pathlib import Path
8
+ from threading import Thread
9
9
from time import sleep
10
+ from typing import Callable , Generator
10
11
11
12
import pytest
13
+ from pytest_mock import MockerFixture
12
14
13
15
from tests .utils import as_cwd
14
16
from uvicorn .config import Config
20
22
except ImportError : # pragma: no cover
21
23
WatchFilesReload = None # type: ignore[misc,assignment]
22
24
23
- try :
24
- from uvicorn .supervisors .watchgodreload import WatchGodReload
25
- except ImportError : # pragma: no cover
26
- WatchGodReload = None # type: ignore[misc,assignment]
27
-
28
25
29
26
# TODO: Investigate why this is flaky on MacOS M1.
30
27
skip_if_m1 = pytest .mark .skipif (
33
30
)
34
31
35
32
36
- def run (sockets ) :
33
+ def run (sockets : list [ socket . socket ] | None ) -> None :
37
34
pass # pragma: no cover
38
35
39
36
37
+ def sleep_touch (* paths : Path ):
38
+ sleep (0.1 )
39
+ for p in paths :
40
+ p .touch ()
41
+
42
+
43
+ @pytest .fixture
44
+ def touch_soon () -> Generator [Callable [[Path ], None ]]:
45
+ threads : list [Thread ] = []
46
+
47
+ def start (* paths : Path ) -> None :
48
+ thread = Thread (target = sleep_touch , args = paths )
49
+ thread .start ()
50
+ threads .append (thread )
51
+
52
+ yield start
53
+
54
+ for t in threads :
55
+ t .join ()
56
+
57
+
40
58
class TestBaseReload :
41
59
@pytest .fixture (autouse = True )
42
- def setup (
43
- self ,
44
- reload_directory_structure : Path ,
45
- reloader_class : type [BaseReload ] | None ,
46
- ):
60
+ def setup (self , reload_directory_structure : Path , reloader_class : type [BaseReload ] | None ):
47
61
if reloader_class is None : # pragma: no cover
48
62
pytest .skip ("Needed dependency not installed" )
49
63
self .reload_path = reload_directory_structure
@@ -52,17 +66,15 @@ def setup(
52
66
def _setup_reloader (self , config : Config ) -> BaseReload :
53
67
config .reload_delay = 0 # save time
54
68
55
- if self .reloader_class is WatchGodReload :
56
- with pytest .deprecated_call ():
57
- reloader = self .reloader_class (config , target = run , sockets = [])
58
- else :
59
- reloader = self .reloader_class (config , target = run , sockets = [])
69
+ reloader = self .reloader_class (config , target = run , sockets = [])
60
70
61
71
assert config .should_reload
62
72
reloader .startup ()
63
73
return reloader
64
74
65
- def _reload_tester (self , touch_soon , reloader : BaseReload , * files : Path ) -> list [Path ] | None :
75
+ def _reload_tester (
76
+ self , touch_soon : Callable [[Path ], None ], reloader : BaseReload , * files : Path
77
+ ) -> list [Path ] | None :
66
78
reloader .restart ()
67
79
if WatchFilesReload is not None and isinstance (reloader , WatchFilesReload ):
68
80
touch_soon (* files )
@@ -73,7 +85,7 @@ def _reload_tester(self, touch_soon, reloader: BaseReload, *files: Path) -> list
73
85
file .touch ()
74
86
return next (reloader )
75
87
76
- @pytest .mark .parametrize ("reloader_class" , [StatReload , WatchGodReload , WatchFilesReload ])
88
+ @pytest .mark .parametrize ("reloader_class" , [StatReload , WatchFilesReload ])
77
89
def test_reloader_should_initialize (self ) -> None :
78
90
"""
79
91
A basic sanity check.
@@ -86,8 +98,8 @@ def test_reloader_should_initialize(self) -> None:
86
98
reloader = self ._setup_reloader (config )
87
99
reloader .shutdown ()
88
100
89
- @pytest .mark .parametrize ("reloader_class" , [StatReload , WatchGodReload , WatchFilesReload ])
90
- def test_reload_when_python_file_is_changed (self , touch_soon ) -> None :
101
+ @pytest .mark .parametrize ("reloader_class" , [StatReload , WatchFilesReload ])
102
+ def test_reload_when_python_file_is_changed (self , touch_soon : Callable [[ Path ], None ]) :
91
103
file = self .reload_path / "main.py"
92
104
93
105
with as_cwd (self .reload_path ):
@@ -99,8 +111,8 @@ def test_reload_when_python_file_is_changed(self, touch_soon) -> None:
99
111
100
112
reloader .shutdown ()
101
113
102
- @pytest .mark .parametrize ("reloader_class" , [StatReload , WatchGodReload , WatchFilesReload ])
103
- def test_should_reload_when_python_file_in_subdir_is_changed (self , touch_soon ) -> None :
114
+ @pytest .mark .parametrize ("reloader_class" , [StatReload , WatchFilesReload ])
115
+ def test_should_reload_when_python_file_in_subdir_is_changed (self , touch_soon : Callable [[ Path ], None ]) :
104
116
file = self .reload_path / "app" / "sub" / "sub.py"
105
117
106
118
with as_cwd (self .reload_path ):
@@ -111,8 +123,8 @@ def test_should_reload_when_python_file_in_subdir_is_changed(self, touch_soon) -
111
123
112
124
reloader .shutdown ()
113
125
114
- @pytest .mark .parametrize ("reloader_class" , [WatchFilesReload , WatchGodReload ])
115
- def test_should_not_reload_when_python_file_in_excluded_subdir_is_changed (self , touch_soon ) -> None :
126
+ @pytest .mark .parametrize ("reloader_class" , [WatchFilesReload ])
127
+ def test_should_not_reload_when_python_file_in_excluded_subdir_is_changed (self , touch_soon : Callable [[ Path ], None ]) :
116
128
sub_dir = self .reload_path / "app" / "sub"
117
129
sub_file = sub_dir / "sub.py"
118
130
@@ -129,7 +141,7 @@ def test_should_not_reload_when_python_file_in_excluded_subdir_is_changed(self,
129
141
reloader .shutdown ()
130
142
131
143
@pytest .mark .parametrize ("reloader_class, result" , [(StatReload , False ), (WatchFilesReload , True )])
132
- def test_reload_when_pattern_matched_file_is_changed (self , result : bool , touch_soon ) -> None :
144
+ def test_reload_when_pattern_matched_file_is_changed (self , result : bool , touch_soon : Callable [[ Path ], None ]) :
133
145
file = self .reload_path / "app" / "js" / "main.js"
134
146
135
147
with as_cwd (self .reload_path ):
@@ -140,14 +152,10 @@ def test_reload_when_pattern_matched_file_is_changed(self, result: bool, touch_s
140
152
141
153
reloader .shutdown ()
142
154
143
- @pytest .mark .parametrize (
144
- "reloader_class" ,
145
- [
146
- pytest .param (WatchFilesReload , marks = skip_if_m1 ),
147
- WatchGodReload ,
148
- ],
149
- )
150
- def test_should_not_reload_when_exclude_pattern_match_file_is_changed (self , touch_soon ) -> None :
155
+ @pytest .mark .parametrize ("reloader_class" , [pytest .param (WatchFilesReload , marks = skip_if_m1 )])
156
+ def test_should_not_reload_when_exclude_pattern_match_file_is_changed (
157
+ self , touch_soon : Callable [[Path ], None ]
158
+ ): # pragma: py-darwin
151
159
python_file = self .reload_path / "app" / "src" / "main.py"
152
160
css_file = self .reload_path / "app" / "css" / "main.css"
153
161
js_file = self .reload_path / "app" / "js" / "main.js"
@@ -167,8 +175,8 @@ def test_should_not_reload_when_exclude_pattern_match_file_is_changed(self, touc
167
175
168
176
reloader .shutdown ()
169
177
170
- @pytest .mark .parametrize ("reloader_class" , [StatReload , WatchGodReload , WatchFilesReload ])
171
- def test_should_not_reload_when_dot_file_is_changed (self , touch_soon ) -> None :
178
+ @pytest .mark .parametrize ("reloader_class" , [StatReload , WatchFilesReload ])
179
+ def test_should_not_reload_when_dot_file_is_changed (self , touch_soon : Callable [[ Path ], None ]) :
172
180
file = self .reload_path / ".dotted"
173
181
174
182
with as_cwd (self .reload_path ):
@@ -179,8 +187,8 @@ def test_should_not_reload_when_dot_file_is_changed(self, touch_soon) -> None:
179
187
180
188
reloader .shutdown ()
181
189
182
- @pytest .mark .parametrize ("reloader_class" , [StatReload , WatchGodReload , WatchFilesReload ])
183
- def test_should_reload_when_directories_have_same_prefix (self , touch_soon ) -> None :
190
+ @pytest .mark .parametrize ("reloader_class" , [StatReload , WatchFilesReload ])
191
+ def test_should_reload_when_directories_have_same_prefix (self , touch_soon : Callable [[ Path ], None ]) :
184
192
app_dir = self .reload_path / "app"
185
193
app_file = app_dir / "src" / "main.py"
186
194
app_first_dir = self .reload_path / "app_first"
@@ -201,13 +209,9 @@ def test_should_reload_when_directories_have_same_prefix(self, touch_soon) -> No
201
209
202
210
@pytest .mark .parametrize (
203
211
"reloader_class" ,
204
- [
205
- StatReload ,
206
- WatchGodReload ,
207
- pytest .param (WatchFilesReload , marks = skip_if_m1 ),
208
- ],
212
+ [StatReload , pytest .param (WatchFilesReload , marks = skip_if_m1 )],
209
213
)
210
- def test_should_not_reload_when_only_subdirectory_is_watched (self , touch_soon ) -> None :
214
+ def test_should_not_reload_when_only_subdirectory_is_watched (self , touch_soon : Callable [[ Path ], None ]) :
211
215
app_dir = self .reload_path / "app"
212
216
app_dir_file = self .reload_path / "app" / "src" / "main.py"
213
217
root_file = self .reload_path / "main.py"
@@ -224,14 +228,8 @@ def test_should_not_reload_when_only_subdirectory_is_watched(self, touch_soon) -
224
228
225
229
reloader .shutdown ()
226
230
227
- @pytest .mark .parametrize (
228
- "reloader_class" ,
229
- [
230
- pytest .param (WatchFilesReload , marks = skip_if_m1 ),
231
- WatchGodReload ,
232
- ],
233
- )
234
- def test_override_defaults (self , touch_soon ) -> None :
231
+ @pytest .mark .parametrize ("reloader_class" , [pytest .param (WatchFilesReload , marks = skip_if_m1 )])
232
+ def test_override_defaults (self , touch_soon : Callable [[Path ], None ]) -> None : # pragma: py-darwin
235
233
dotted_file = self .reload_path / ".dotted"
236
234
dotted_dir_file = self .reload_path / ".dotted_dir" / "file.txt"
237
235
python_file = self .reload_path / "main.py"
@@ -252,14 +250,8 @@ def test_override_defaults(self, touch_soon) -> None:
252
250
253
251
reloader .shutdown ()
254
252
255
- @pytest .mark .parametrize (
256
- "reloader_class" ,
257
- [
258
- pytest .param (WatchFilesReload , marks = skip_if_m1 ),
259
- WatchGodReload ,
260
- ],
261
- )
262
- def test_explicit_paths (self , touch_soon ) -> None :
253
+ @pytest .mark .parametrize ("reloader_class" , [pytest .param (WatchFilesReload , marks = skip_if_m1 )])
254
+ def test_explicit_paths (self , touch_soon : Callable [[Path ], None ]) -> None : # pragma: py-darwin
263
255
dotted_file = self .reload_path / ".dotted"
264
256
non_dotted_file = self .reload_path / "ext" / "ext.jpg"
265
257
python_file = self .reload_path / "main.py"
@@ -307,33 +299,9 @@ def test_watchfiles_no_changes(self) -> None:
307
299
308
300
reloader .shutdown ()
309
301
310
- @pytest .mark .parametrize ("reloader_class" , [WatchGodReload ])
311
- def test_should_detect_new_reload_dirs (self , touch_soon , caplog : pytest .LogCaptureFixture , tmp_path : Path ) -> None :
312
- app_dir = tmp_path / "app"
313
- app_file = app_dir / "file.py"
314
- app_dir .mkdir ()
315
- app_file .touch ()
316
- app_first_dir = tmp_path / "app_first"
317
- app_first_file = app_first_dir / "file.py"
318
-
319
- with as_cwd (tmp_path ):
320
- config = Config (app = "tests.test_config:asgi_app" , reload = True , reload_includes = ["app*" ])
321
- reloader = self ._setup_reloader (config )
322
- assert self ._reload_tester (touch_soon , reloader , app_file )
323
-
324
- app_first_dir .mkdir ()
325
- assert self ._reload_tester (touch_soon , reloader , app_first_file )
326
- assert caplog .records [- 2 ].levelno == logging .INFO
327
- assert (
328
- caplog .records [- 1 ].message == "WatchGodReload detected a new reload "
329
- f"dir '{ app_first_dir .name } ' in '{ tmp_path } '; Adding to watch list."
330
- )
331
-
332
- reloader .shutdown ()
333
-
334
302
335
303
@pytest .mark .skipif (WatchFilesReload is None , reason = "watchfiles not available" )
336
- def test_should_watch_one_dir_cwd (mocker , reload_directory_structure ):
304
+ def test_should_watch_one_dir_cwd (mocker : MockerFixture , reload_directory_structure : Path ):
337
305
mock_watch = mocker .patch ("uvicorn.supervisors.watchfilesreload.watch" )
338
306
app_dir = reload_directory_structure / "app"
339
307
app_first_dir = reload_directory_structure / "app_first"
@@ -350,7 +318,7 @@ def test_should_watch_one_dir_cwd(mocker, reload_directory_structure):
350
318
351
319
352
320
@pytest .mark .skipif (WatchFilesReload is None , reason = "watchfiles not available" )
353
- def test_should_watch_separate_dirs_outside_cwd (mocker , reload_directory_structure ):
321
+ def test_should_watch_separate_dirs_outside_cwd (mocker : MockerFixture , reload_directory_structure : Path ):
354
322
mock_watch = mocker .patch ("uvicorn.supervisors.watchfilesreload.watch" )
355
323
app_dir = reload_directory_structure / "app"
356
324
app_first_dir = reload_directory_structure / "app_first"
@@ -368,7 +336,7 @@ def test_should_watch_separate_dirs_outside_cwd(mocker, reload_directory_structu
368
336
}
369
337
370
338
371
- def test_display_path_relative (tmp_path ):
339
+ def test_display_path_relative (tmp_path : Path ):
372
340
with as_cwd (tmp_path ):
373
341
p = tmp_path / "app" / "foobar.py"
374
342
# accept windows paths as wells as posix
@@ -380,8 +348,8 @@ def test_display_path_non_relative():
380
348
assert _display_path (p ) in ("'/foo/bar.py'" , "'\\ foo\\ bar.py'" )
381
349
382
350
383
- def test_base_reloader_run (tmp_path ):
384
- calls = []
351
+ def test_base_reloader_run (tmp_path : Path ):
352
+ calls : list [ str ] = []
385
353
step = 0
386
354
387
355
class CustomReload (BaseReload ):
@@ -411,7 +379,7 @@ def should_restart(self):
411
379
assert calls == ["startup" , "restart" , "shutdown" ]
412
380
413
381
414
- def test_base_reloader_should_exit (tmp_path ):
382
+ def test_base_reloader_should_exit (tmp_path : Path ):
415
383
config = Config (app = "tests.test_config:asgi_app" , reload = True )
416
384
reloader = BaseReload (config , target = run , sockets = [])
417
385
assert not reloader .should_exit .is_set ()
0 commit comments