Commits

Anonymous committed 7a4729c

Initial check in.

Comments (0)

Files changed (6)

+syntax: glob
+
+*~
+*.pyo
+*.pyc
+*egg-info
+build
+dist
+The MIT License
+
+Copyright (c) 2010 Meme Dough
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+Nosier
+======
+
+Nosier monitors paths for changes and then runs a specified command.
+
+Intended for automatically running tests upon code changes, it can
+however be useful for running any command upon changes to files.
+
+Alternatives are Jeff Winkler's original nosy script and Gary
+Bernhardt's improved nosy script.  It appears that both of these are
+not packaged for easy installation.
+
+Nosier can be installed simply with pip::
+
+    pip install nosier
+
+Or with easy_install::
+
+    easy_install nosier
+
+Nosier uses the Linux inotify facility for monitoring paths which has
+a couple of implications.
+
+First, it doesn't put as much load on the cpu and disk as some
+alternatives which will regularly scan and calculate check sums.
+
+Second, inotify is Linux 2.6 specific and hence limited to that
+platform.

nosier/__init__.py

+from nosier import *
+import subprocess
+import inotifyx
+import optparse
+import fnmatch
+import time
+import sys
+import os
+
+USAGE = '''\
+Usage: %prog [options] command
+
+Monitor directories and files for changes.  On detecting a change run
+the specified command.'''
+
+VERSION = '0.9'
+VERSION_TEXT = '%%prog version %s' % VERSION
+
+WATCH_EVENTS = inotifyx.IN_CREATE | inotifyx.IN_MODIFY | inotifyx.IN_DELETE | inotifyx.IN_DELETE_SELF | inotifyx.IN_MOVE
+BUILTIN_BLACK_LIST = ['.hg', '.git', '.bzr', '.svn', '#*', '.#*', '*.swp']
+WIDTH = 80
+
+
+def iteration_generator():
+    i = 0
+    while True:
+        i += 1
+        yield i
+
+iterations = iteration_generator()
+
+
+def run_command(command, fd, watches, events, ignore_events, white_listed):
+    # Run the specified command.
+    print '=' * WIDTH
+    print 'Iteration: %s' % iterations.next()
+    print 'Files    : %s' % ' '.join(set(os.path.join(watches.get(event.wd, ''), event.name or '') for event in events))
+    print 'Command  : %s' % ' '.join(command)
+    print ''
+
+    subprocess.call(command)
+
+    print ''
+
+    if ignore_events:
+        events = inotifyx.get_events(fd, 0)
+        while events:
+            events = [event for event in events if not event.name or white_listed(event.name)]
+            if events:
+                print 'Ignoring events for files: %s' % ' '.join(set(os.path.join(watches.get(event.wd, ''), event.name or '') for event in events))
+            events = inotifyx.get_events(fd, 0)
+        print ''
+
+    print '-' * WIDTH
+
+
+def main():
+    # Process command line options.
+    parser = optparse.OptionParser(usage=USAGE, version=VERSION_TEXT)
+    parser.add_option('-p', '--path', action='append',
+                      help='add a path to monitor for changes, if no paths are specified then the current directory will be monitored')
+    parser.add_option('-d', '--delay', type='float', default=0.1,
+                      help='how long to wait for additional events after a command run is triggered, defaults to %default second')
+    parser.add_option('-i', '--ignore-events', action='store_true', default=False,
+                      help='whether to ignore events that occur during the command run, defaults to %default')
+    parser.add_option('-w', '--white-list', action='append', default=[], metavar='FILE',
+                      help='add a file to the white list, ensure globs are quoted to avoid shell expansion')
+    parser.add_option('-b', '--black-list', action='append', default=[], metavar='FILE',
+                      help='add a file to the black list, ensure globs are quoted to avoid shell expansion')
+    parser.add_option('-l', '--no-default-black-list', action='store_true', default=False,
+                      help='''don't add the following to the black list: %s''' % ' '.join(BUILTIN_BLACK_LIST))
+    parser.add_option('-r', '--no-initial-run', action='store_true', default=False,
+                      help='''don't perform an initial run of the command, instead start monitoring and wait for changes''')
+
+    options, command = parser.parse_args()
+
+    paths = options.path or ['.']
+    delay = options.delay
+    ignore_events = options.ignore_events
+    white_list = options.white_list
+    black_list = options.black_list
+    if not options.no_default_black_list:
+        black_list.extend(BUILTIN_BLACK_LIST)
+    no_initial_run = options.no_initial_run
+
+    # Fn that indicates if name is in or out.
+    def white_listed(name):
+        for pattern in white_list:
+            if fnmatch.fnmatch(name, pattern):
+                return True
+        for pattern in black_list:
+            if fnmatch.fnmatch(name, pattern):
+                return False
+        return True
+
+    # Init inotify.
+    fd = inotifyx.init()
+
+    # Watch specified paths.
+    print 'Setting up watches on paths'
+
+    watches = {}
+    watches.update((inotifyx.add_watch(fd, path, WATCH_EVENTS), path)
+                   for path in paths)
+
+    # Watch sub dirs of specified paths.  Ensure we modify dirs in
+    # place so that os.walk only traverses white listed dirs.
+    for path in paths:
+        for root, dirs, files in os.walk(path):
+            dirs[:] = [dir for dir in dirs if white_listed(dir)]
+            watches.update((inotifyx.add_watch(fd, os.path.join(root, dir), WATCH_EVENTS), os.path.join(root, dir))
+                           for dir in dirs)
+
+    print 'Watching %d paths' % len(watches)
+
+    # Initial command run.
+    if not no_initial_run:
+        run_command(command, fd, watches, [], ignore_events, white_listed)
+
+    # Monitor and run the specified command until keyboard interrupt.
+    while True:
+        # Block until events arrive.
+        events = inotifyx.get_events(fd)
+
+        # Continue collecting events for the delay period.  This
+        # allows events that occur close to the trigger event to be
+        # collected now rather than causing another run immediately
+        # after this run.
+        end = time.time() + delay
+        while time.time() < end:
+            events.extend(inotifyx.get_events(fd, 0))
+
+        # Filter to events that have no name (self destruct watch
+        # events) and to events that have names that are considered
+        # white listed.
+        events = [event for event in events if not event.name or white_listed(event.name)]
+
+        # Track watched dirs.
+        for e in events:
+            if e.mask & inotifyx.IN_ISDIR and e.mask & inotifyx.IN_CREATE:
+                watches[inotifyx.add_watch(fd, os.path.join(watches.get(e.wd), e.name), WATCH_EVENTS)] = os.path.join(watches.get(e.wd), e.name)
+            elif e.mask & inotifyx.IN_DELETE_SELF:
+                watches.pop(e.wd, None)
+
+        # Do command run provided we have events after white listing.
+        if events:
+            run_command(command, fd, watches, events, ignore_events, white_listed)
+
+
+if __name__ == '__main__':
+    main()
+import nosier
+import setuptools
+
+setuptools.setup(name='nosier',
+                 version=nosier.VERSION,
+                 description='Monitors paths for changes and then runs a specified command',
+                 long_description=open('README').read().strip(),
+                 author='Meme Dough',
+                 author_email='memedough@gmail.com',
+                 url='http://pypi.python.org/pypi/nosier',
+                 packages=['nosier'],
+                 install_requires=['inotifyx'],
+                 entry_points={'console_scripts': ['nosier = nosier.nosier:main']},
+                 license='MIT License',
+                 zip_safe=False,
+                 classifiers=['Development Status :: 4 - Beta',
+                              'Intended Audience :: Developers',
+                              'License :: OSI Approved :: MIT License',
+                              'Operating System :: POSIX :: Linux',
+                              'Programming Language :: Python',
+                              'Topic :: Software Development :: Testing'])