8
8
from pathlib import Path
9
9
from watchdog .observers import Observer
10
10
from functools import partial
11
- from watchdog .events import FileSystemEventHandler
11
+ from watchdog .events import PatternMatchingEventHandler
12
12
from quartodoc import Builder , convert_inventory
13
+ from pydantic import BaseModel
13
14
14
15
def get_package_path (package_name ):
15
16
"""
@@ -21,24 +22,83 @@ def get_package_path(package_name):
21
22
except ModuleNotFoundError :
22
23
raise ModuleNotFoundError (f"Package { package_name } not found. Please install it in your environment." )
23
24
24
- class FileChangeHandler (FileSystemEventHandler ):
25
+
26
+ class FileInfo (BaseModel ):
27
+ size : int
28
+ mtime : float
29
+ name : str = ""
30
+
31
+ class QuartoDocFileChangeHandler (PatternMatchingEventHandler ):
25
32
"""
26
33
A handler for file changes.
27
34
"""
35
+
36
+ # Ignore patterns for the file watcher that are not relevant to the docs
37
+ py_ignore_patterns = [
38
+ '*/__pycache__/*' , # These are the compiled python code files which are automatically generated by Python
39
+ '*/.ipynb_checkpoints/*' , # This directory is created by Jupyter Notebook for auto-saving notebooks
40
+ '*/.vscode/*' , # If you're using Visual Studio Code, it creates this directory to store settings specific to that project.
41
+ '*/.idea/*' , # Similar to .vscode/, but for JetBrains IDEs like PyCharm.
42
+ '*/.git/*' , # i This directory is created by Git. It's not relevant to the docs.
43
+ '*/venv/*' , '*/env/*' , '*/.env/*' , # Common names for directories containing a Python virtual environment.
44
+ '*/.pytest_cache/*' , # This directory is created when you run Pytest.
45
+ '*/.eggs/*' , '*/dist/*' , '*/build/*' , '*/*.egg-info/*' , # These are typically created when building Python packages with setuptools.
46
+ '*.pyo' , # These are optimized .pyc files, created when Python is run with the -O flag.
47
+ '*.pyd' , # This is the equivalent of a .pyc file, but for C extensions on Windows.
48
+ '*/.mypy_cache/*' , # This directory is created when you run mypy.
49
+ ]
50
+
28
51
def __init__ (self , callback ):
52
+ super ().__init__ (ignore_patterns = self .py_ignore_patterns , ignore_directories = True )
29
53
self .callback = callback
54
+ self .old_file_info = FileInfo (size = - 1 , mtime = - 1 , name = "" )
55
+
56
+
57
+ def get_file_info (self , path :str ) -> FileInfo :
58
+ """
59
+ Get the file size and modification time.
60
+ """
61
+ return FileInfo (size = os .stat (path ).st_size ,
62
+ mtime = os .stat (path ).st_mtime ,
63
+ name = path )
30
64
65
+ def is_diff (self , old :FileInfo , new :FileInfo ) -> bool :
66
+ """
67
+ Check if a file has changed. Prevents duplicate events from being triggered.
68
+ """
69
+ same_nm = old .name == new .name
70
+ diff_sz = old .size != new .size
71
+ diff_tm = (new .mtime - old .mtime ) # wait 1/4 second before triggering
72
+
73
+ if diff_tm < .25 : # if consequetive events are less than 1/4th of a second apart, ignore
74
+ return False
75
+ elif same_nm :
76
+ if diff_sz or diff_tm >= 0.25 :
77
+ return True
78
+ else :
79
+ return False
80
+ else :
81
+ return True
82
+
83
+ def callback_if_diff (self , event ):
84
+ """
85
+ Call the callback if the file has changed.
86
+ """
87
+ new_file_info = self .get_file_info (event .src_path )
88
+ if self .is_diff (self .old_file_info , new_file_info ):
89
+ self .print_event (event )
90
+ self .callback ()
91
+ self .old_file_info = new_file_info
92
+
31
93
@classmethod
32
94
def print_event (cls , event ):
33
95
print (f'Rebuilding docs. Detected: { event .event_type } path : { event .src_path } ' )
34
96
35
97
def on_modified (self , event ):
36
- self .print_event (event )
37
- self .callback ()
98
+ self .callback_if_diff (event )
38
99
39
100
def on_created (self , event ):
40
- self .print_event (event )
41
- self .callback ()
101
+ self .callback_if_diff (event )
42
102
43
103
def _enable_logs ():
44
104
import logging
@@ -71,9 +131,6 @@ def cli():
71
131
pass
72
132
73
133
74
-
75
-
76
-
77
134
@click .command ()
78
135
@click .option ("--config" , default = "_quarto.yml" , help = "Change the path to the configuration file. The default is `./_quarto.yml`" )
79
136
@click .option ("--filter" , nargs = 1 , default = "*" , help = "Specify the filter to select specific files. The default is '*' which selects all files." )
@@ -84,6 +141,11 @@ def build(config, filter, dry_run, watch, verbose):
84
141
"""
85
142
Generate API docs based on the given configuration file (`./_quarto.yml` by default).
86
143
"""
144
+ cfg_path = f"{ os .getcwd ()} /{ config } "
145
+ if not Path (cfg_path ).exists ():
146
+ raise FileNotFoundError (
147
+ f"Configuration file { cfg_path } not found. Please create one."
148
+ )
87
149
if verbose :
88
150
_enable_logs ()
89
151
@@ -97,16 +159,20 @@ def build(config, filter, dry_run, watch, verbose):
97
159
if watch :
98
160
pkg_path = get_package_path (builder .package )
99
161
print (f"Watching { pkg_path } for changes..." )
100
- event_handler = FileChangeHandler (callback = doc_build )
101
162
observer = Observer ()
163
+ observer ._event_queue .maxsize = 1 # the default is 0 which is infinite, and there isn't a way to set this in the constructor
164
+ event_handler = QuartoDocFileChangeHandler (callback = doc_build )
102
165
observer .schedule (event_handler , pkg_path , recursive = True )
166
+ observer .schedule (event_handler , cfg_path , recursive = True )
103
167
observer .start ()
104
168
try :
105
169
while True :
106
170
time .sleep (1 )
107
171
except KeyboardInterrupt :
172
+ pass
173
+ finally :
108
174
observer .stop ()
109
- observer .join ()
175
+ observer .join ()
110
176
else :
111
177
doc_build ()
112
178
0 commit comments