Source code for duck.contrib.reloader.ducksight

"""
**Duck Sight Reloader**

Watches for file changes in Duck framework projects and restarts the
webserver in DEBUG mode whenever relevant `.py` files change.

"""
import os
import sys
import time
import fnmatch
import threading
import platform
import subprocess

from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer

from duck.settings import SETTINGS
from duck.logging import logger


[docs] class DuckSightReloader: """ Monitors the project directory for Python file changes and triggers reloads. """ def __init__(self, watch_dir: str): # Set a dynamic timeout timeout = SETTINGS.get("AUTO_RELOAD_POLL", 1.0) if platform.system().lower() == "windows": timeout = max(timeout, 2.0) # Create some attributes self.observer = Observer(timeout=timeout) self.watch_dir = watch_dir self.__force_stop = False
[docs] def stop(self): """ Stops the reloader. """ self.__force_stop = True
[docs] def run(self): """ Start the observer loop; ensures single reloader process is active. Notes: This method is blocking. """ try: event_handler = Handler() self.observer.schedule(event_handler, self.watch_dir, recursive=True) self.observer.start() while not self.__force_stop: time.sleep(.1) except KeyboardInterrupt: pass finally: self.observer.stop() if self.observer.is_alive(): self.observer.join()
[docs] class Handler(FileSystemEventHandler): """ Handles filesystem events and triggers debounced full server reloads. """ def __init__(self, debounce_interval=0.6): super().__init__() self.debounce_interval = debounce_interval self.restart_timer = None self.latest_event = None self.restarting = threading.Lock() self.last_restart_time = 0
[docs] def on_any_event(self, event): """ Called on any filesystem event; filters `.py` files and schedules reload. """ watch_files = SETTINGS["AUTO_RELOAD_WATCH_FILES"] if event.is_directory: return if not any(fnmatch.fnmatch(event.src_path, pat) for pat in watch_files): return if event.event_type not in {"created", "modified", "deleted", "moved"}: # Ignore event. return # Update the latest event self.latest_event = event if self.restart_timer: self.restart_timer.cancel() # Set & start the timer self.restart_timer = threading.Timer(self.debounce_interval, self._trigger_restart) self.restart_timer.start()
[docs] def restart_webserver(self, changed_file: str): """ Perform the actual server reload. Args: changed_file (str): This is the path to the file which triggered this reload. """ from duck.app import App # Get the main app instance app = App.get_main_app() # Stop the app without exiting the process or killing the reloader app.stop( log_to_console=False, no_exit=True, kill_ducksight_reloader=False, close_log_file=True, ) def restart_app(): """ This restarts the application. """ # Was started from a file cmd = [sys.executable, *sys.argv, "--is-reload"] subprocess.run(cmd) try: restart_app() except Exception as e: # Log any encountered exception logger.log_exception(e)
[docs] def _trigger_restart(self): """ Trigger the real restart. """ if self.restarting.locked(): return # Check for overlapping reloads now = time.time() if now - self.last_restart_time < 0.5: return # avoid overlapping reloads with self.restarting: self.last_restart_time = now event = self.latest_event file_path = event.src_path # Log something and restart the server. logger.log_raw(f"\nFile {file_path} changed, attempting reload...", custom_color=logger.Fore.YELLOW) self.restart_webserver(file_path)