11from __future__ import annotations
22
3- import logging
43import platform
54import signal
65import socket
76import sys
87from pathlib import Path
8+ from threading import Thread
99from time import sleep
10+ from typing import Callable , Generator
1011
1112import pytest
13+ from pytest_mock import MockerFixture
1214
1315from tests .utils import as_cwd
1416from uvicorn .config import Config
2022except ImportError : # pragma: no cover
2123 WatchFilesReload = None # type: ignore[misc,assignment]
2224
23- try :
24- from uvicorn .supervisors .watchgodreload import WatchGodReload
25- except ImportError : # pragma: no cover
26- WatchGodReload = None # type: ignore[misc,assignment]
27-
2825
2926# TODO: Investigate why this is flaky on MacOS M1.
3027skip_if_m1 = pytest .mark .skipif (
3330)
3431
3532
36- def run (sockets ) :
33+ def run (sockets : list [ socket . socket ] | None ) -> None :
3734 pass # pragma: no cover
3835
3936
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+
4058class TestBaseReload :
4159 @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 ):
4761 if reloader_class is None : # pragma: no cover
4862 pytest .skip ("Needed dependency not installed" )
4963 self .reload_path = reload_directory_structure
@@ -52,17 +66,15 @@ def setup(
5266 def _setup_reloader (self , config : Config ) -> BaseReload :
5367 config .reload_delay = 0 # save time
5468
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 = [])
6070
6171 assert config .should_reload
6272 reloader .startup ()
6373 return reloader
6474
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 :
6678 reloader .restart ()
6779 if WatchFilesReload is not None and isinstance (reloader , WatchFilesReload ):
6880 touch_soon (* files )
@@ -73,7 +85,7 @@ def _reload_tester(self, touch_soon, reloader: BaseReload, *files: Path) -> list
7385 file .touch ()
7486 return next (reloader )
7587
76- @pytest .mark .parametrize ("reloader_class" , [StatReload , WatchGodReload , WatchFilesReload ])
88+ @pytest .mark .parametrize ("reloader_class" , [StatReload , WatchFilesReload ])
7789 def test_reloader_should_initialize (self ) -> None :
7890 """
7991 A basic sanity check.
@@ -86,8 +98,8 @@ def test_reloader_should_initialize(self) -> None:
8698 reloader = self ._setup_reloader (config )
8799 reloader .shutdown ()
88100
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 ]) :
91103 file = self .reload_path / "main.py"
92104
93105 with as_cwd (self .reload_path ):
@@ -99,8 +111,8 @@ def test_reload_when_python_file_is_changed(self, touch_soon) -> None:
99111
100112 reloader .shutdown ()
101113
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 ]) :
104116 file = self .reload_path / "app" / "sub" / "sub.py"
105117
106118 with as_cwd (self .reload_path ):
@@ -111,8 +123,8 @@ def test_should_reload_when_python_file_in_subdir_is_changed(self, touch_soon) -
111123
112124 reloader .shutdown ()
113125
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 ]) :
116128 sub_dir = self .reload_path / "app" / "sub"
117129 sub_file = sub_dir / "sub.py"
118130
@@ -129,7 +141,7 @@ def test_should_not_reload_when_python_file_in_excluded_subdir_is_changed(self,
129141 reloader .shutdown ()
130142
131143 @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 ]) :
133145 file = self .reload_path / "app" / "js" / "main.js"
134146
135147 with as_cwd (self .reload_path ):
@@ -140,14 +152,10 @@ def test_reload_when_pattern_matched_file_is_changed(self, result: bool, touch_s
140152
141153 reloader .shutdown ()
142154
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
151159 python_file = self .reload_path / "app" / "src" / "main.py"
152160 css_file = self .reload_path / "app" / "css" / "main.css"
153161 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
167175
168176 reloader .shutdown ()
169177
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 ]) :
172180 file = self .reload_path / ".dotted"
173181
174182 with as_cwd (self .reload_path ):
@@ -179,8 +187,8 @@ def test_should_not_reload_when_dot_file_is_changed(self, touch_soon) -> None:
179187
180188 reloader .shutdown ()
181189
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 ]) :
184192 app_dir = self .reload_path / "app"
185193 app_file = app_dir / "src" / "main.py"
186194 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
201209
202210 @pytest .mark .parametrize (
203211 "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 )],
209213 )
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 ]) :
211215 app_dir = self .reload_path / "app"
212216 app_dir_file = self .reload_path / "app" / "src" / "main.py"
213217 root_file = self .reload_path / "main.py"
@@ -224,14 +228,8 @@ def test_should_not_reload_when_only_subdirectory_is_watched(self, touch_soon) -
224228
225229 reloader .shutdown ()
226230
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
235233 dotted_file = self .reload_path / ".dotted"
236234 dotted_dir_file = self .reload_path / ".dotted_dir" / "file.txt"
237235 python_file = self .reload_path / "main.py"
@@ -252,14 +250,8 @@ def test_override_defaults(self, touch_soon) -> None:
252250
253251 reloader .shutdown ()
254252
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
263255 dotted_file = self .reload_path / ".dotted"
264256 non_dotted_file = self .reload_path / "ext" / "ext.jpg"
265257 python_file = self .reload_path / "main.py"
@@ -307,33 +299,9 @@ def test_watchfiles_no_changes(self) -> None:
307299
308300 reloader .shutdown ()
309301
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-
334302
335303@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 ):
337305 mock_watch = mocker .patch ("uvicorn.supervisors.watchfilesreload.watch" )
338306 app_dir = reload_directory_structure / "app"
339307 app_first_dir = reload_directory_structure / "app_first"
@@ -350,7 +318,7 @@ def test_should_watch_one_dir_cwd(mocker, reload_directory_structure):
350318
351319
352320@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 ):
354322 mock_watch = mocker .patch ("uvicorn.supervisors.watchfilesreload.watch" )
355323 app_dir = reload_directory_structure / "app"
356324 app_first_dir = reload_directory_structure / "app_first"
@@ -368,7 +336,7 @@ def test_should_watch_separate_dirs_outside_cwd(mocker, reload_directory_structu
368336 }
369337
370338
371- def test_display_path_relative (tmp_path ):
339+ def test_display_path_relative (tmp_path : Path ):
372340 with as_cwd (tmp_path ):
373341 p = tmp_path / "app" / "foobar.py"
374342 # accept windows paths as wells as posix
@@ -380,8 +348,8 @@ def test_display_path_non_relative():
380348 assert _display_path (p ) in ("'/foo/bar.py'" , "'\\ foo\\ bar.py'" )
381349
382350
383- def test_base_reloader_run (tmp_path ):
384- calls = []
351+ def test_base_reloader_run (tmp_path : Path ):
352+ calls : list [ str ] = []
385353 step = 0
386354
387355 class CustomReload (BaseReload ):
@@ -411,7 +379,7 @@ def should_restart(self):
411379 assert calls == ["startup" , "restart" , "shutdown" ]
412380
413381
414- def test_base_reloader_should_exit (tmp_path ):
382+ def test_base_reloader_should_exit (tmp_path : Path ):
415383 config = Config (app = "tests.test_config:asgi_app" , reload = True )
416384 reloader = BaseReload (config , target = run , sockets = [])
417385 assert not reloader .should_exit .is_set ()
0 commit comments