Skip to content

Commit 418705b

Browse files
committed
Merge branch 'main' into feat-option-defaults
2 parents 349ecab + bd352e3 commit 418705b

File tree

2 files changed

+79
-11
lines changed

2 files changed

+79
-11
lines changed

docs/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ _inv
1414
objects.json
1515
_sidebar.yml
1616
_extensions
17+
18+
/.luarc.json

quartodoc/__main__.py

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
from pathlib import Path
99
from watchdog.observers import Observer
1010
from functools import partial
11-
from watchdog.events import FileSystemEventHandler
11+
from watchdog.events import PatternMatchingEventHandler
1212
from quartodoc import Builder, convert_inventory
13+
from pydantic import BaseModel
1314

1415
def get_package_path(package_name):
1516
"""
@@ -21,24 +22,83 @@ def get_package_path(package_name):
2122
except ModuleNotFoundError:
2223
raise ModuleNotFoundError(f"Package {package_name} not found. Please install it in your environment.")
2324

24-
class FileChangeHandler(FileSystemEventHandler):
25+
26+
class FileInfo(BaseModel):
27+
size: int
28+
mtime: float
29+
name: str= ""
30+
31+
class QuartoDocFileChangeHandler(PatternMatchingEventHandler):
2532
"""
2633
A handler for file changes.
2734
"""
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+
2851
def __init__(self, callback):
52+
super().__init__(ignore_patterns=self.py_ignore_patterns, ignore_directories=True)
2953
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)
3064

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+
3193
@classmethod
3294
def print_event(cls, event):
3395
print(f'Rebuilding docs. Detected: {event.event_type} path : {event.src_path}')
3496

3597
def on_modified(self, event):
36-
self.print_event(event)
37-
self.callback()
98+
self.callback_if_diff(event)
3899

39100
def on_created(self, event):
40-
self.print_event(event)
41-
self.callback()
101+
self.callback_if_diff(event)
42102

43103
def _enable_logs():
44104
import logging
@@ -71,9 +131,6 @@ def cli():
71131
pass
72132

73133

74-
75-
76-
77134
@click.command()
78135
@click.option("--config", default="_quarto.yml", help="Change the path to the configuration file. The default is `./_quarto.yml`")
79136
@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):
84141
"""
85142
Generate API docs based on the given configuration file (`./_quarto.yml` by default).
86143
"""
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+
)
87149
if verbose:
88150
_enable_logs()
89151

@@ -97,16 +159,20 @@ def build(config, filter, dry_run, watch, verbose):
97159
if watch:
98160
pkg_path = get_package_path(builder.package)
99161
print(f"Watching {pkg_path} for changes...")
100-
event_handler = FileChangeHandler(callback=doc_build)
101162
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)
102165
observer.schedule(event_handler, pkg_path, recursive=True)
166+
observer.schedule(event_handler, cfg_path, recursive=True)
103167
observer.start()
104168
try:
105169
while True:
106170
time.sleep(1)
107171
except KeyboardInterrupt:
172+
pass
173+
finally:
108174
observer.stop()
109-
observer.join()
175+
observer.join()
110176
else:
111177
doc_build()
112178

0 commit comments

Comments
 (0)