Skip to content

Instantly share code, notes, and snippets.

@mateor
Created November 27, 2017 18:15
Show Gist options
  • Save mateor/8dd618acbc88ad376760f76fa72086c2 to your computer and use it in GitHub Desktop.
Save mateor/8dd618acbc88ad376760f76fa72086c2 to your computer and use it in GitHub Desktop.
Pants onchange
# coding=utf-8
# Copyright 2013 Foursquare Labs Inc. All Rights Reserved.
from __future__ import (nested_scopes, generators, division, absolute_import, with_statement,
print_function, unicode_literals)
import argparse
from threading import Event
import subprocess
import sys
import time
DEFAULT_BUILD_GENERATED=['.pyc', '.class', '.obj', '.o']
class Watcher(object):
def __init__(self, run, roots=None, useful_endings=None, ignored_endings=None):
if useful_endings:
useful_endings = useful_endings.split(',')
if ignored_endings:
ignored_endings = ignored_endings.split(',')
if roots:
roots = roots.split(',')
else:
roots = ['.']
self.useful_endings = useful_endings
self.ignored_endings = ignored_endings
self.roots = [str(i) for i in roots]
self._dirty = Event()
self.run = run
def is_ignored(self, name):
if not self.ignored_endings:
return False
return any(name.endswith(i) for i in self.ignored_endings)
def is_useful(self, name):
if not self.useful_endings:
return True
return any(name.endswith(i) for i in self.useful_endings)
def callback(self, changed_name):
if not self._dirty.isSet():
if not self.is_ignored(changed_name):
if self.is_useful(changed_name):
self._dirty.set()
def start_fsevents(self):
from fsevents import Observer, Stream
observer = Observer()
observer.daemon = True
observer.start()
stream = Stream(lambda x: self.callback(x.name), *self.roots, file_events=True)
observer.schedule(stream)
started = True
return self.loop()
def start_inotify(self):
import pyinotify
class OnWriteHandler(pyinotify.ProcessEvent):
def __init__(self, watcher):
self.watcher = watcher
def process_IN_CREATE(self, event):
self.watcher.callback(event.pathname)
def process_IN_DELETE(self, event):
self.watcher.callback(event.pathname)
def process_IN_MODIFY(self, event):
self.watcher.callback(event.pathname)
wm = pyinotify.WatchManager()
handler = OnWriteHandler(self)
notifier = pyinotify.ThreadedNotifier(wm, default_proc_fun=handler)
for path in self.roots:
wm.add_watch(path, pyinotify.ALL_EVENTS, rec=True, auto_add=True)
notifier.setDaemon(True)
notifier.start()
return self.loop()
def loop(self):
self.msg()
self._dirty.set()
while True:
if self._dirty.isSet():
self._dirty.clear()
print('[{}] Running "{}"...'.format(time.strftime('%H:%M:%S'), self.run))
subprocess.call(self.run, shell=True)
if not self._dirty.isSet():
print('[{}] Waiting for changes...'.format(time.strftime('%H:%M:%S')))
try:
self._dirty.wait(60)
except KeyboardInterrupt:
print('')
sys.exit(0)
@staticmethod
def comma_and(lst):
if not lst:
return ''
if len(lst) == 1:
return '"{}"'.format(lst[0])
return '"{}", and "{}"'.format('", "'.join(lst[:-1]), lst[-1])
def msg(self):
msg = '\nWatching for changes in {}'.format(self.comma_and(self.roots))
if self.useful_endings:
msg += ', to files ending in: {}'.format(self.comma_and(self.useful_endings))
else:
msg += '.'
print(msg)
if self.ignored_endings:
print('Ingoring changes to files ending in: {}'.format(self.comma_and(self.ignored_endings)))
print('')
def start(self):
try:
from fsevents import Observer, Stream
return self.start_fsevents()
except ImportError:
pass
try:
import pyinotify
return self.start_inotify()
except ImportError:
pass
print('\n./onchange requires MacFSEvents on OSX or pyinotify on Linux.\n')
sys.exit(1)
if __name__ == '__main__':
ignored = DEFAULT_BUILD_GENERATED + ['src/resources/config/release.conf']
parser = argparse.ArgumentParser()
parser.add_argument('--roots', default=','.join(['src', 'test']))
parser.add_argument('--endings', default=','.join(['.scala', '.java', '.thrift', '.soy', '.py', 'BUILD']))
parser.add_argument('--ignored', default=','.join(ignored))
parser.add_argument('command', metavar="COMMAND", nargs=argparse.REMAINDER)
options = parser.parse_args()
Watcher(' '.join(options.command), options.roots, options.endings, options.ignored).start()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment