Skip to content

Commit 222564a

Browse files
committed
Initial commit
This code is mostly a straight copy of pydoit/doit-auto1#1 and pydoit/doit-auto1#2.
1 parent 60d2d99 commit 222564a

File tree

9 files changed

+883
-0
lines changed

9 files changed

+883
-0
lines changed

doit_watch/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
__version__ = (0, 1, 0)
2+
3+
from .cmd_watch import Watch
4+
5+
__all__ = ['Auto']

doit_watch/cmd_watch.py

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""starts a long-running process that watches the file system and
2+
automatically execute tasks when file dependencies change"""
3+
4+
import os
5+
import time
6+
import sys
7+
from multiprocess import Process
8+
from subprocess import call
9+
10+
from doit.exceptions import InvalidCommand
11+
from doit.cmdparse import CmdParse
12+
from doit.cmd_base import tasks_and_deps_iter
13+
from doit.cmd_base import DoitCmdBase
14+
from doit.cmd_run import opt_verbosity, Run
15+
16+
from .filewatch import FileModifyWatcher
17+
18+
opt_reporter = {
19+
'name':'reporter',
20+
'short': None,
21+
'long': None,
22+
'type':str,
23+
'default': 'executed-only',
24+
}
25+
26+
opt_success = {
27+
'name':'success_callback',
28+
'short': None,
29+
'long': 'success',
30+
'type':str,
31+
'default': '',
32+
}
33+
34+
opt_failure = {
35+
'name':'failure_callback',
36+
'short': None,
37+
'long': 'failure',
38+
'type':str,
39+
'default': '',
40+
}
41+
42+
43+
class Watch(DoitCmdBase):
44+
"""the main process will never load tasks,
45+
delegates execution to a forked process.
46+
47+
python caches imported modules,
48+
but using different process we can have dependencies on python
49+
modules making sure the newest module will be used.
50+
"""
51+
52+
doc_purpose = "automatically execute tasks when a dependency changes"
53+
doc_usage = "[TASK ...]"
54+
doc_description = None
55+
execute_tasks = True
56+
57+
cmd_options = (opt_verbosity, opt_reporter, opt_success, opt_failure)
58+
59+
@staticmethod
60+
def _find_file_deps(tasks, sel_tasks):
61+
"""find all file deps
62+
@param tasks (dict)
63+
@param sel_tasks(list - str)
64+
"""
65+
deps = set()
66+
for task in tasks_and_deps_iter(tasks, sel_tasks):
67+
deps.update(task.file_dep)
68+
deps.update(task.watch)
69+
return deps
70+
71+
72+
@staticmethod
73+
def _dep_changed(watch_files, started, targets):
74+
"""check if watched files was modified since execution started"""
75+
for watched in watch_files:
76+
# assume that changes to targets were done by doit itself
77+
if watched in targets:
78+
continue
79+
if os.stat(watched).st_mtime > started:
80+
return True
81+
return False
82+
83+
84+
@staticmethod
85+
def _run_callback(result, success_callback, failure_callback):
86+
'''run callback if any after task execution'''
87+
if result == 0:
88+
if success_callback:
89+
call(success_callback, shell=True)
90+
else:
91+
if failure_callback:
92+
call(failure_callback, shell=True)
93+
94+
95+
def run_watch(self, params, args):
96+
"""Run tasks and wait for file system event
97+
98+
This method is executed in a forked process.
99+
The process is terminated after a single event.
100+
"""
101+
started = time.time()
102+
103+
# execute tasks using Run Command
104+
arun = Run(task_loader=self.loader)
105+
params.add_defaults(CmdParse(arun.get_options()).parse([])[0])
106+
try:
107+
result = arun.execute(params, args)
108+
# ??? actually tested but coverage doesnt get it...
109+
except InvalidCommand as err: # pragma: no cover
110+
sys.stderr.write("ERROR: %s\n" % str(err))
111+
sys.exit(3)
112+
113+
# user custom callbacks for result
114+
self._run_callback(result,
115+
params.pop('success_callback', None),
116+
params.pop('failure_callback', None))
117+
118+
# get list of files to watch on file system
119+
watch_files = self._find_file_deps(arun.control.tasks,
120+
arun.control.selected_tasks)
121+
122+
# Check for timestamp changes since run started,
123+
# if change, restart straight away
124+
if not self._dep_changed(watch_files, started, arun.control.targets):
125+
# set event handler. just terminate process.
126+
class DoitAutoRun(FileModifyWatcher):
127+
def handle_event(self, event):
128+
# print("FS EVENT -> {}".format(event))
129+
sys.exit(result)
130+
file_watcher = DoitAutoRun(watch_files)
131+
# kick start watching process
132+
file_watcher.loop()
133+
134+
135+
def execute(self, params, args):
136+
"""loop executing tasks until process is interrupted"""
137+
while True:
138+
try:
139+
proc = Process(target=self.run_watch, args=(params, args))
140+
proc.start()
141+
proc.join()
142+
# if error on given command line, terminate.
143+
if proc.exitcode == 3:
144+
return 3
145+
except KeyboardInterrupt:
146+
return 0

doit_watch/filewatch.py

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Watch for modifications of file-system
2+
use by cmd_auto module
3+
"""
4+
5+
import os.path
6+
import watchfiles
7+
8+
9+
class FileModifyWatcher(object):
10+
"""Use watchfiles to watch file-system for file modifications
11+
12+
Usage:
13+
1) subclass the method handle_event, action to be performed
14+
2) create an object passing a list of files to be watched
15+
3) call the loop method
16+
"""
17+
18+
def __init__(self, path_list):
19+
"""@param file_list (list-str): files to be watched"""
20+
self.file_list = set()
21+
self.watch_dirs = set() # all dirs to be watched
22+
self.notify_dirs = set() # dirs that generate notification whatever file
23+
for filename in path_list:
24+
path = os.path.abspath(filename)
25+
if os.path.isfile(path):
26+
self.file_list.add(path)
27+
self.watch_dirs.add(os.path.dirname(path))
28+
else:
29+
self.notify_dirs.add(path)
30+
self.watch_dirs.add(path)
31+
32+
def _handle(self, changes):
33+
"""calls implementation handler"""
34+
if any(
35+
change[1] in self.file_list
36+
or os.path.dirname(change[1]) in self.notify_dirs
37+
for change in changes
38+
):
39+
return self.handle_event(changes)
40+
41+
def handle_event(self, event): # pragma: no cover
42+
"""this should be sub-classed """
43+
raise NotImplementedError
44+
45+
def loop(self):
46+
"""Infinite loop watching for file modifications"""
47+
48+
for changes in watchfiles.watch(*self.watch_dirs):
49+
self._handle(changes)

0 commit comments

Comments
 (0)