Skip to content

Commit 86f7134

Browse files
bhrutledgelepture
authored andcommitted
Trigger reload on deleted files (#198)
* Ignore more environment files * Detect file deletion * Ignore fewer environment files * Add failing test for multiple tasks * Track modification times for each task
1 parent f80cb3a commit 86f7134

3 files changed

Lines changed: 120 additions & 16 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ pip-log.txt
2121
.coverage
2222
.tox
2323
.env/
24+
venv/
2425

2526
docs/_build
2627
example/style.css

livereload/watcher.py

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@
2323

2424

2525
class Watcher(object):
26-
"""A file watcher registery."""
26+
"""A file watcher registry."""
2727
def __init__(self):
2828
self._tasks = {}
29-
self._mtimes = {}
29+
30+
# modification time of filepaths for each task,
31+
# before and after checking for changes
32+
self._task_mtimes = {}
33+
self._new_mtimes = {}
3034

3135
# setting changes
3236
self._changes = []
@@ -65,6 +69,7 @@ def watch(self, path, func=None, delay=0, ignore=None):
6569
'func': func,
6670
'delay': delay,
6771
'ignore': ignore,
72+
'mtimes': {},
6873
}
6974

7075
def start(self, callback):
@@ -73,7 +78,10 @@ def start(self, callback):
7378
return False
7479

7580
def examine(self):
76-
"""Check if there are changes, if true, run the given task."""
81+
"""Check if there are changes. If so, run the given task.
82+
83+
Returns a tuple of modified filepath and reload delay.
84+
"""
7785
if self._changes:
7886
return self._changes.pop()
7987

@@ -82,6 +90,7 @@ def examine(self):
8290
delays = set()
8391
for path in self._tasks:
8492
item = self._tasks[path]
93+
self._task_mtimes = item['mtimes']
8594
if self.is_changed(path, item['ignore']):
8695
func = item['func']
8796
delay = item['delay']
@@ -102,13 +111,49 @@ def examine(self):
102111
return self.filepath, delay
103112

104113
def is_changed(self, path, ignore=None):
114+
"""Check if any filepaths have been added, modified, or removed.
115+
116+
Updates filepath modification times in self._task_mtimes.
117+
"""
118+
self._new_mtimes = {}
119+
changed = False
120+
105121
if os.path.isfile(path):
106-
return self.is_file_changed(path, ignore)
122+
changed = self.is_file_changed(path, ignore)
107123
elif os.path.isdir(path):
108-
return self.is_folder_changed(path, ignore)
109-
return self.is_glob_changed(path, ignore)
124+
changed = self.is_folder_changed(path, ignore)
125+
else:
126+
changed = self.is_glob_changed(path, ignore)
127+
128+
if not changed:
129+
changed = self.is_file_removed()
130+
131+
self._task_mtimes.update(self._new_mtimes)
132+
return changed
133+
134+
def is_file_removed(self):
135+
"""Check if any filepaths have been removed since last check.
136+
137+
Deletes removed paths from self._task_mtimes.
138+
Sets self.filepath to one of the removed paths.
139+
"""
140+
removed_paths = set(self._task_mtimes) - set(self._new_mtimes)
141+
if not removed_paths:
142+
return False
143+
144+
for path in removed_paths:
145+
self._task_mtimes.pop(path)
146+
# self.filepath seems purely informational, so setting one
147+
# of several removed files seems sufficient
148+
self.filepath = path
149+
return True
110150

111151
def is_file_changed(self, path, ignore=None):
152+
"""Check if filepath has been added or modified since last check.
153+
154+
Updates filepath modification times in self._new_mtimes.
155+
Sets self.filepath to changed path.
156+
"""
112157
if not os.path.isfile(path):
113158
return False
114159

@@ -120,20 +165,21 @@ def is_file_changed(self, path, ignore=None):
120165

121166
mtime = os.path.getmtime(path)
122167

123-
if path not in self._mtimes:
124-
self._mtimes[path] = mtime
168+
if path not in self._task_mtimes:
169+
self._new_mtimes[path] = mtime
125170
self.filepath = path
126171
return mtime > self._start
127172

128-
if self._mtimes[path] != mtime:
129-
self._mtimes[path] = mtime
173+
if self._task_mtimes[path] != mtime:
174+
self._new_mtimes[path] = mtime
130175
self.filepath = path
131176
return True
132177

133-
self._mtimes[path] = mtime
178+
self._new_mtimes[path] = mtime
134179
return False
135180

136181
def is_folder_changed(self, path, ignore=None):
182+
"""Check if directory path has any changed filepaths."""
137183
for root, dirs, files in os.walk(path, followlinks=True):
138184
for d in self.ignored_dirs:
139185
if d in dirs:
@@ -145,6 +191,7 @@ def is_folder_changed(self, path, ignore=None):
145191
return False
146192

147193
def is_glob_changed(self, path, ignore=None):
194+
"""Check if glob path has any changed filepaths."""
148195
for f in glob.glob(path):
149196
if self.is_file_changed(f, ignore):
150197
return True

tests/test_watcher.py

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,27 @@ def test_watch_dir(self):
3232
assert watcher.is_changed(tmpdir) is False
3333

3434
# sleep 1 second so that mtime will be different
35+
# TODO: This doesn't seem necessary; test passes without it
3536
time.sleep(1)
3637

37-
with open(os.path.join(tmpdir, 'foo'), 'w') as f:
38+
filepath = os.path.join(tmpdir, 'foo')
39+
40+
with open(filepath, 'w') as f:
3841
f.write('')
3942

4043
assert watcher.is_changed(tmpdir)
4144
assert watcher.is_changed(tmpdir) is False
4245

46+
os.remove(filepath)
47+
assert watcher.is_changed(tmpdir)
48+
assert watcher.is_changed(tmpdir) is False
49+
4350
def test_watch_file(self):
4451
watcher = Watcher()
4552
watcher.count = 0
4653

4754
# sleep 1 second so that mtime will be different
55+
# TODO: This doesn't seem necessary; test passes without it
4856
time.sleep(1)
4957

5058
filepath = os.path.join(tmpdir, 'foo')
@@ -56,17 +64,25 @@ def add_count():
5664

5765
watcher.watch(filepath, add_count)
5866
assert watcher.is_changed(filepath)
67+
assert watcher.is_changed(filepath) is False
5968

6069
# sleep 1 second so that mtime will be different
70+
# TODO: This doesn't seem necessary; test passes without it
6171
time.sleep(1)
6272

6373
with open(filepath, 'w') as f:
6474
f.write('')
6575

66-
rv = watcher.examine()
67-
assert rv[0] == os.path.abspath(filepath)
76+
abs_filepath = os.path.abspath(filepath)
77+
assert watcher.examine() == (abs_filepath, None)
78+
assert watcher.examine() == (None, None)
6879
assert watcher.count == 1
6980

81+
os.remove(filepath)
82+
assert watcher.examine() == (abs_filepath, None)
83+
assert watcher.examine() == (None, None)
84+
assert watcher.count == 2
85+
7086
def test_watch_glob(self):
7187
watcher = Watcher()
7288
watcher.watch(tmpdir + '/*')
@@ -82,8 +98,13 @@ def test_watch_glob(self):
8298
with open(filepath, 'w') as f:
8399
f.write('')
84100

85-
rv = watcher.examine()
86-
assert rv[0] == os.path.abspath(filepath)
101+
abs_filepath = os.path.abspath(filepath)
102+
assert watcher.examine() == (abs_filepath, None)
103+
assert watcher.examine() == (None, None)
104+
105+
os.remove(filepath)
106+
assert watcher.examine() == (abs_filepath, None)
107+
assert watcher.examine() == (None, None)
87108

88109
def test_watch_ignore(self):
89110
watcher = Watcher()
@@ -94,3 +115,38 @@ def test_watch_ignore(self):
94115
f.write('')
95116

96117
assert watcher.examine() == (None, None)
118+
119+
def test_watch_multiple_dirs(self):
120+
first_dir = os.path.join(tmpdir, 'first')
121+
second_dir = os.path.join(tmpdir, 'second')
122+
123+
watcher = Watcher()
124+
125+
os.mkdir(first_dir)
126+
watcher.watch(first_dir)
127+
assert watcher.examine() == (None, None)
128+
129+
first_path = os.path.join(first_dir, 'foo')
130+
with open(first_path, 'w') as f:
131+
f.write('')
132+
assert watcher.examine() == (first_path, None)
133+
assert watcher.examine() == (None, None)
134+
135+
os.mkdir(second_dir)
136+
watcher.watch(second_dir)
137+
assert watcher.examine() == (None, None)
138+
139+
second_path = os.path.join(second_dir, 'bar')
140+
with open(second_path, 'w') as f:
141+
f.write('')
142+
assert watcher.examine() == (second_path, None)
143+
assert watcher.examine() == (None, None)
144+
145+
with open(first_path, 'a') as f:
146+
f.write('foo')
147+
assert watcher.examine() == (first_path, None)
148+
assert watcher.examine() == (None, None)
149+
150+
os.remove(second_path)
151+
assert watcher.examine() == (second_path, None)
152+
assert watcher.examine() == (None, None)

0 commit comments

Comments
 (0)