watchfilesreload.py 2.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. from __future__ import annotations
  2. from collections.abc import Callable
  3. from pathlib import Path
  4. from socket import socket
  5. from watchfiles import watch
  6. from uvicorn.config import Config
  7. from uvicorn.supervisors.basereload import BaseReload
  8. class FileFilter:
  9. def __init__(self, config: Config):
  10. default_includes = ["*.py"]
  11. self.includes = [default for default in default_includes if default not in config.reload_excludes]
  12. self.includes.extend(config.reload_includes)
  13. self.includes = list(set(self.includes))
  14. default_excludes = [".*", ".py[cod]", ".sw.*", "~*"]
  15. self.excludes = [default for default in default_excludes if default not in config.reload_includes]
  16. self.exclude_dirs = []
  17. for e in config.reload_excludes:
  18. p = Path(e)
  19. try:
  20. is_dir = p.is_dir()
  21. except OSError: # pragma: no cover
  22. # gets raised on Windows for values like "*.py"
  23. is_dir = False
  24. if is_dir:
  25. self.exclude_dirs.append(p)
  26. else:
  27. self.excludes.append(e) # pragma: full coverage
  28. self.excludes = list(set(self.excludes))
  29. def __call__(self, path: Path) -> bool:
  30. for include_pattern in self.includes:
  31. if path.match(include_pattern):
  32. if str(path).endswith(include_pattern):
  33. return True # pragma: full coverage
  34. for exclude_dir in self.exclude_dirs:
  35. if exclude_dir in path.parents:
  36. return False
  37. for exclude_pattern in self.excludes:
  38. if path.match(exclude_pattern):
  39. return False # pragma: full coverage
  40. return True
  41. return False
  42. class WatchFilesReload(BaseReload):
  43. def __init__(
  44. self,
  45. config: Config,
  46. target: Callable[[list[socket] | None], None],
  47. sockets: list[socket],
  48. ) -> None:
  49. super().__init__(config, target, sockets)
  50. self.reloader_name = "WatchFiles"
  51. self.reload_dirs: list[Path] = []
  52. for directory in config.reload_dirs:
  53. self.reload_dirs.append(directory)
  54. self.watch_filter = FileFilter(config)
  55. self.watcher = watch(
  56. *self.reload_dirs,
  57. watch_filter=None,
  58. stop_event=self.should_exit,
  59. # using yield_on_timeout here mostly to make sure tests don't
  60. # hang forever, won't affect the class's behavior
  61. yield_on_timeout=True,
  62. ignore_permission_denied=True,
  63. )
  64. def should_restart(self) -> list[Path] | None:
  65. self.pause()
  66. changes = next(self.watcher)
  67. if changes:
  68. unique_paths = {Path(c[1]) for c in changes}
  69. return [p for p in unique_paths if self.watch_filter(p)]
  70. return None