Skip to content

Commit be50c42

Browse files
authored
[watchmedo] Fix calling commands from within a Python script (#880)
Fixes #879 * Fix handling tests Mokcing `time.sleep()` does not work if `eventlet.monkey_patch()` has been called before. So I renamed watchmedo the test file to be run before the one using `eventlet`. Also done some cleaning in tests.
1 parent 77e1f46 commit be50c42

File tree

9 files changed

+86
-82
lines changed

9 files changed

+86
-82
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
fail-fast: false
2121
matrix:
2222
os: [ubuntu-latest, windows-latest, macos-latest]
23-
python: [3.6, 3.7, 3.8, 3.9, 'pypy-3.7']
23+
python: [3.6, 3.7, 3.8, 3.9, "3.10", "pypy-3.7"]
2424
steps:
2525
- name: Checkout
2626
uses: actions/checkout@v2

changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Changelog
1010

1111
- Eliminate timeout in waiting on event queue. (`#861 <https://github.com/gorakhargosh/watchdog/pull/861>`_)
1212
- [inotify] Fix ``not`` equality implementation for ``InotifyEvent``. (`#848 <https://github.com/gorakhargosh/watchdog/pull/848>`_)
13+
- [watchmedo] Fix calling commands from within a Python script. (`#879 <https://github.com/gorakhargosh/watchdog/pull/879>`_)
1314
- [watchmedo] ``PyYAML`` is loaded only when strictly necessary. Simple usages of ``watchmedo`` are possible without the module being installed. (`#847 <https://github.com/gorakhargosh/watchdog/pull/847>`_)
1415
- Thanks to our beloved contributors: @sattlerc, @JanzenLiu, @BoboTiG
1516

src/watchdog/watchmedo.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ def decorator(func):
9797
for arg in args:
9898
parser.add_argument(*arg[0], **arg[1])
9999
parser.set_defaults(func=func)
100+
return func
100101
return decorator
101102

102103

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def no_warnings(recwarn):
4848
"Not importing directory" in message
4949
or "Using or importing the ABCs" in message
5050
or "dns.hash module will be removed in future versions" in message
51-
or ("eventlet" in filename and "eventlet" in filename)
51+
or "eventlet" in filename
5252
):
5353
continue
5454
warnings.append("{w.filename}:{w.lineno} {w.message}".format(w=warning))

tests/test_watchmedo.py renamed to tests/test_0_watchmedo.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
# coding: utf-8
2-
1+
from unittest.mock import patch
32
import pytest
43

4+
from watchdog.utils import WatchdogShutdown
5+
56
# Skip if import PyYAML failed. PyYAML missing possible because
67
# watchdog installed without watchmedo. See Installation section
78
# in README.rst
@@ -71,3 +72,25 @@ def test_kill_auto_restart(tmpdir, capfd):
7172
assert '+++++ 9' not in cap.out # we killed the subprocess before the end
7273
# in windows we seem to lose the subprocess stderr
7374
# assert 'KeyboardInterrupt' in cap.err
75+
76+
77+
@pytest.mark.parametrize("command", ["tricks-from", "tricks"])
78+
def test_tricks_from_file(command, tmp_path):
79+
tricks_file = tmp_path / "tricks.yaml"
80+
tricks_file.write_text("""
81+
tricks:
82+
- watchdog.tricks.LoggerTrick:
83+
patterns: ["*.py", "*.js"]
84+
""")
85+
args = watchmedo.cli.parse_args([command, str(tricks_file)])
86+
87+
checkpoint = False
88+
89+
def mocked_sleep(_):
90+
nonlocal checkpoint
91+
checkpoint = True
92+
raise WatchdogShutdown()
93+
94+
with patch("time.sleep", mocked_sleep):
95+
watchmedo.tricks_from(args)
96+
assert checkpoint

tests/test_inotify_c.py

Lines changed: 21 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import pytest
32
from watchdog.utils import platform
43

@@ -13,6 +12,7 @@
1312
import struct
1413
from functools import partial
1514
from queue import Queue
15+
from unittest.mock import patch
1616

1717
from watchdog.events import DirCreatedEvent, DirDeletedEvent, DirModifiedEvent
1818
from watchdog.observers.api import ObservedWatch
@@ -47,10 +47,8 @@ def watching(path=None, use_full_emitter=False):
4747

4848
def teardown_function(function):
4949
rm(p(''), recursive=True)
50-
try:
50+
with contextlib.suppress(NameError):
5151
assert not emitter.is_alive()
52-
except NameError:
53-
pass
5452

5553

5654
def struct_inotify(wd, mask, cookie=0, length=0, name=b""):
@@ -66,7 +64,7 @@ def struct_inotify(wd, mask, cookie=0, length=0, name=b""):
6664
return struct.pack(struct_format, wd, mask, cookie, length, name)
6765

6866

69-
def test_late_double_deletion(monkeypatch):
67+
def test_late_double_deletion():
7068
inotify_fd = type(str("FD"), (object,), {})() # Empty object
7169
inotify_fd.last = 0
7270
inotify_fd.wds = []
@@ -116,13 +114,13 @@ def inotify_rm_watch(fd, wd):
116114

117115
# Mocks the API!
118116
from watchdog.observers import inotify_c
119-
monkeypatch.setattr(os, "read", fakeread)
120-
monkeypatch.setattr(os, "close", fakeclose)
121-
monkeypatch.setattr(inotify_c, "inotify_init", inotify_init)
122-
monkeypatch.setattr(inotify_c, "inotify_add_watch", inotify_add_watch)
123-
monkeypatch.setattr(inotify_c, "inotify_rm_watch", inotify_rm_watch)
117+
mock1 = patch.object(os, "read", new=fakeread)
118+
mock2 = patch.object(os, "close", new=fakeclose)
119+
mock3 = patch.object(inotify_c, "inotify_init", new=inotify_init)
120+
mock4 = patch.object(inotify_c, "inotify_add_watch", new=inotify_add_watch)
121+
mock5 = patch.object(inotify_c, "inotify_rm_watch", new=inotify_rm_watch)
124122

125-
with watching(p('')):
123+
with mock1, mock2, mock3, mock4, mock5, watching(p('')):
126124
# Watchdog Events
127125
for evt_cls in [DirCreatedEvent, DirDeletedEvent] * 2:
128126
event = event_queue.get(timeout=5)[0]
@@ -137,32 +135,18 @@ def inotify_rm_watch(fd, wd):
137135
assert inotify_fd.wds == [2, 3] # Only 1 is removed explicitly
138136

139137

140-
def test_raise_error(monkeypatch):
141-
func = Inotify._raise_error
142-
143-
monkeypatch.setattr(ctypes, "get_errno", lambda: errno.ENOSPC)
144-
with pytest.raises(OSError) as exc:
145-
func()
146-
assert exc.value.errno == errno.ENOSPC
147-
assert "inotify watch limit reached" in str(exc.value)
148-
149-
monkeypatch.setattr(ctypes, "get_errno", lambda: errno.EMFILE)
150-
with pytest.raises(OSError) as exc:
151-
func()
152-
assert exc.value.errno == errno.EMFILE
153-
assert "inotify instance limit reached" in str(exc.value)
154-
155-
monkeypatch.setattr(ctypes, "get_errno", lambda: errno.ENOENT)
156-
with pytest.raises(OSError) as exc:
157-
func()
158-
assert exc.value.errno == errno.ENOENT
159-
assert "No such file or directory" in str(exc.value)
160-
161-
monkeypatch.setattr(ctypes, "get_errno", lambda: -1)
162-
with pytest.raises(OSError) as exc:
163-
func()
164-
assert exc.value.errno == -1
165-
assert "Unknown error -1" in str(exc.value)
138+
@pytest.mark.parametrize("error, pattern", [
139+
(errno.ENOSPC, "inotify watch limit reached"),
140+
(errno.EMFILE, "inotify instance limit reached"),
141+
(errno.ENOENT, "No such file or directory"),
142+
(-1, "Unknown error -1"),
143+
])
144+
def test_raise_error(error, pattern):
145+
with patch.object(ctypes, "get_errno", new=lambda: error):
146+
with pytest.raises(OSError) as exc:
147+
Inotify._raise_error()
148+
assert exc.value.errno == error
149+
assert pattern in str(exc.value)
166150

167151

168152
def test_non_ascii_path():

tests/test_observer.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616

17+
import contextlib
1718
import threading
19+
from unittest.mock import patch
1820

1921
import pytest
2022

@@ -27,21 +29,17 @@ def observer():
2729
obs = BaseObserver(EventEmitter)
2830
yield obs
2931
obs.stop()
30-
try:
32+
with contextlib.suppress(RuntimeError):
3133
obs.join()
32-
except RuntimeError:
33-
pass
3434

3535

3636
@pytest.fixture
3737
def observer2():
3838
obs = BaseObserver(EventEmitter)
3939
yield obs
4040
obs.stop()
41-
try:
41+
with contextlib.suppress(RuntimeError):
4242
obs.join()
43-
except RuntimeError:
44-
pass
4543

4644

4745
def test_schedule_should_start_emitter_if_running(observer):
@@ -118,7 +116,7 @@ def test_2_observers_on_the_same_path(observer, observer2):
118116
assert len(observer2.emitters) == 1
119117

120118

121-
def test_start_failure_should_not_prevent_further_try(monkeypatch, observer):
119+
def test_start_failure_should_not_prevent_further_try(observer):
122120
observer.schedule(None, '')
123121
emitters = observer.emitters
124122
assert len(emitters) == 1
@@ -129,14 +127,13 @@ def mocked_start():
129127
raise OSError()
130128

131129
emitter = next(iter(emitters))
132-
monkeypatch.setattr(emitter, "start", mocked_start)
133-
with pytest.raises(OSError):
134-
observer.start()
130+
with patch.object(emitter, "start", new=mocked_start):
131+
with pytest.raises(OSError):
132+
observer.start()
135133
# The emitter should be removed from the list
136134
assert len(observer.emitters) == 0
137135

138136
# Restoring the original behavior should work like there never be emitters
139-
monkeypatch.undo()
140137
observer.start()
141138
assert len(observer.emitters) == 0
142139

tests/test_snapshot_diff.py

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import os
1919
import pickle
2020
import time
21+
from unittest.mock import patch
2122

2223
from watchdog.utils.dirsnapshot import DirectorySnapshot
2324
from watchdog.utils.dirsnapshot import DirectorySnapshotDiff
@@ -107,7 +108,7 @@ def test_dir_modify_on_move(p):
107108
wait()
108109
mv(p('dir1', 'a'), p('dir2', 'b'))
109110
diff = DirectorySnapshotDiff(ref, DirectorySnapshot(p('')))
110-
assert set(diff.dirs_modified) == set([p('dir1'), p('dir2')])
111+
assert set(diff.dirs_modified) == {p('dir1'), p('dir2')}
111112

112113

113114
def test_detect_modify_for_moved_files(p):
@@ -138,11 +139,12 @@ def listdir_fcn(path):
138139
DirectorySnapshot(p('root'), listdir=listdir_fcn)
139140

140141

141-
def test_permission_error(monkeypatch, p):
142+
def test_permission_error(p):
142143
# Test that unreadable folders are not raising exceptions
143144
mkdir(p('a', 'b', 'c'), parents=True)
144145

145146
ref = DirectorySnapshot(p(''))
147+
walk_orig = DirectorySnapshot.walk
146148

147149
def walk(self, root):
148150
"""Generate a permission error on folder "a/b"."""
@@ -151,16 +153,11 @@ def walk(self, root):
151153
raise OSError(errno.EACCES, os.strerror(errno.EACCES))
152154

153155
# Mimic the original method
154-
for entry in walk_orig(self, root):
155-
yield entry
156-
157-
walk_orig = DirectorySnapshot.walk
158-
monkeypatch.setattr(DirectorySnapshot, "walk", walk)
159-
160-
# Should NOT raise an OSError (EACCES)
161-
new_snapshot = DirectorySnapshot(p(''))
156+
yield from walk_orig(self, root)
162157

163-
monkeypatch.undo()
158+
with patch.object(DirectorySnapshot, "walk", new=walk):
159+
# Should NOT raise an OSError (EACCES)
160+
new_snapshot = DirectorySnapshot(p(''))
164161

165162
diff = DirectorySnapshotDiff(ref, new_snapshot)
166163
assert repr(diff)
@@ -169,38 +166,39 @@ def walk(self, root):
169166
assert diff.dirs_deleted == [(p('a', 'b', 'c'))]
170167

171168

172-
def test_ignore_device(monkeypatch, p):
169+
def test_ignore_device(p):
173170
# Create a file and take a snapshot.
174171
touch(p('file'))
175172
ref = DirectorySnapshot(p(''))
176173
wait()
177174

175+
inode_orig = DirectorySnapshot.inode
176+
178177
def inode(self, path):
179178
# This function will always return a different device_id,
180179
# even for the same file.
181180
result = inode_orig(self, path)
182181
inode.times += 1
183182
return result[0], result[1] + inode.times
183+
184184
inode.times = 0
185185

186186
# Set the custom inode function.
187-
inode_orig = DirectorySnapshot.inode
188-
monkeypatch.setattr(DirectorySnapshot, 'inode', inode)
189-
190-
# If we make the diff of the same directory, since by default the
191-
# DirectorySnapshotDiff compares the snapshots using the device_id (and it will
192-
# be different), it thinks that the same file has been deleted and created again.
193-
snapshot = DirectorySnapshot(p(''))
194-
diff_with_device = DirectorySnapshotDiff(ref, snapshot)
195-
assert diff_with_device.files_deleted == [(p('file'))]
196-
assert diff_with_device.files_created == [(p('file'))]
197-
198-
# Otherwise, if we choose to ignore the device, the file will not be detected as
199-
# deleted and re-created.
200-
snapshot = DirectorySnapshot(p(''))
201-
diff_without_device = DirectorySnapshotDiff(ref, snapshot, ignore_device=True)
202-
assert diff_without_device.files_deleted == []
203-
assert diff_without_device.files_created == []
187+
with patch.object(DirectorySnapshot, 'inode', new=inode):
188+
# If we make the diff of the same directory, since by default the
189+
# DirectorySnapshotDiff compares the snapshots using the device_id (and it will
190+
# be different), it thinks that the same file has been deleted and created again.
191+
snapshot = DirectorySnapshot(p(''))
192+
diff_with_device = DirectorySnapshotDiff(ref, snapshot)
193+
assert diff_with_device.files_deleted == [(p('file'))]
194+
assert diff_with_device.files_created == [(p('file'))]
195+
196+
# Otherwise, if we choose to ignore the device, the file will not be detected as
197+
# deleted and re-created.
198+
snapshot = DirectorySnapshot(p(''))
199+
diff_without_device = DirectorySnapshotDiff(ref, snapshot, ignore_device=True)
200+
assert diff_without_device.files_deleted == []
201+
assert diff_without_device.files_created == []
204202

205203

206204
def test_empty_snapshot(p):

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tox]
2-
envlist = py{310,39,38,37,36,35,py3}
2+
envlist = py{310,39,38,37,36,py3}
33
skip_missing_interpreters = True
44

55
[testenv]

0 commit comments

Comments
 (0)