Coverage for src / rendercv / cli / render_command / watcher.py: 100%

31 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-25 13:49 +0000

1import contextlib 

2import pathlib 

3from collections.abc import Callable 

4 

5import typer 

6import watchdog.events 

7import watchdog.observers 

8 

9 

10class EventHandler(watchdog.events.FileSystemEventHandler): 

11 """Trigger a callback when a watched file is modified. 

12 

13 Args: 

14 function: Callback to invoke on modification. 

15 watched_files: Absolute path strings of files to monitor. 

16 """ 

17 

18 def __init__(self, function: Callable[[], None], watched_files: set[str]) -> None: 

19 super().__init__() 

20 self.function = function 

21 self.watched_files = watched_files 

22 

23 def on_modified( 

24 self, 

25 event: watchdog.events.DirModifiedEvent | watchdog.events.FileModifiedEvent, 

26 ) -> None: 

27 if event.src_path not in self.watched_files: 

28 return 

29 with contextlib.suppress(typer.Exit): 

30 self.function() 

31 

32 

33def run_function_if_files_change( 

34 file_paths: list[pathlib.Path], 

35 function: Callable[[], None], 

36) -> None: 

37 """Watch files and re-run function when any is modified. 

38 

39 Why: 

40 Watch mode lets users edit CV YAML and see results instantly. 

41 All config files (main input, design, locale, settings) must be 

42 monitored so edits to any trigger a re-render. 

43 

44 Args: 

45 file_paths: File paths to watch. 

46 function: Zero-argument callback to invoke on file change. 

47 """ 

48 watched_files = {str(fp.absolute()) for fp in file_paths} 

49 

50 # Watch parent directories (file-level watching is unreliable across platforms) 

51 dirs_to_schedule = {str(fp.absolute().parent) for fp in file_paths} 

52 

53 event_handler = EventHandler(function, watched_files) 

54 

55 observer = watchdog.observers.Observer() 

56 for directory in dirs_to_schedule: 

57 observer.schedule(event_handler, directory, recursive=False) 

58 observer.start() 

59 

60 # Run immediately for the first render: 

61 with contextlib.suppress(typer.Exit): 

62 function() 

63 

64 try: 

65 observer.join() 

66 except KeyboardInterrupt: 

67 observer.stop() 

68 observer.join()