Commits

briancurtin  committed 03ed5cc

Initial checkin

  • Participants
  • Parent commits 8332297

Comments (0)

Files changed (10)

File FileSystemWatcher.py

+"""FileSystemWatcher.py -- a ripoff of the .NET class of the same name.
+Uses the Watcher class, built on top of ReadDirectoryChangesW for Win32.
+
+See the following for more details:
+http://msdn.microsoft.com/en-us/library/system.io.filesystemwatcher.aspx
+"""
+
+
+from collections import OrderedDict
+from queue import Queue
+import os
+import threading
+import re
+
+from watcher import _watcher
+
+
+class _HandlerDict(OrderedDict):
+    """An ordered dictionary that functions like FileSystemWatcher `event`
+       objects, e.g., FileSystemWatcher.Changed. They support += and -=
+       operators to allow you attach and detach callbacks for the given
+       events. This will store callable objects with their keys and values
+       being the same."""
+    def __iadd__(self, value):
+        self.__setitem__(value, value)
+        return self
+
+    def __isub__(self, value):
+        self.__delitem__(value)
+        return self
+
+
+# The following classes are passed as the argument to appropriate callbacks.
+class FileSystemEventArgs(object):
+    __slots__ = ("ChangeType", "FullPath", "Name")
+
+
+# This should probably inherit from FileSystemEventArgs
+class RenamedEventArgs(object):
+    __slots__ =  ("ChangeType", "FullPath", "Name", "OldFullPath", "OldName")
+
+
+"""
+Some common scenarios and how they play out in terms of NotifyFilters (NF)
+and WatcherChangesTypes (WCT).
+
+Moving a file
+    1. You'll get a WCT.Deleted update removing the original file name.
+    2. You'll get a WCT.Created update creating the original file under
+       the new name.
+    3. You'll get a WCT.Modified update for the directory where the new
+       file lives.
+    4. You'll get a WCT.Modified update for the new file itself.
+
+Deleting a directory
+    1. You'll get a WCT.Deleted update for each file within the directory.
+    2. You'll get a WCT.Deleted update for the directory itself.
+
+Renaming a directory
+    This is the exact same as renaming a file. You DO NOT get updates per-file.
+"""
+
+class NotifyFilters(object):
+    FileName = _watcher.FILE_NOTIFY_CHANGE_FILE_NAME
+    DirectoryName = _watcher.FILE_NOTIFY_CHANGE_DIR_NAME
+    Attributes = _watcher.FILE_NOTIFY_CHANGE_ATTRIBUTES
+    Size = _watcher.FILE_NOTIFY_CHANGE_SIZE
+    LastWrite = _watcher.FILE_NOTIFY_CHANGE_LAST_WRITE
+    LastAccess = _watcher.FILE_NOTIFY_CHANGE_LAST_ACCESS
+    CreationTime = _watcher.FILE_NOTIFY_CHANGE_CREATION
+    Security = _watcher.FILE_NOTIFY_CHANGE_SECURITY
+
+
+class WatcherChangeTypes(object):
+    Created = _watcher.FILE_ACTION_ADDED
+    Deleted = _watcher.FILE_ACTION_REMOVED
+    Changed = _watcher.FILE_ACTION_MODIFIED
+    Renamed = (_watcher.FILE_ACTION_RENAMED_OLD_NAME |
+               _watcher.FILE_ACTION_RENAMED_NEW_NAME)
+
+
+class FileSystemWatcher(object):
+    def __init__(self, path, filter="*.*"):
+        """path: The directory to monitor.
+           filter: The type of files to watch.
+        
+        To monitor one specific file, specify `path` as the parent
+        directory and `filter` as the exact file name."""
+        if os.path.exists(path) and os.path.isdir(path): 
+            self._path = path
+        else:
+            raise ValueError("path is not a valid directory")
+
+        self._filter = filter
+        self._watcher = _watcher.Watcher(self._path, self._callback)
+
+        # Events to be latched onto.
+        self.Changed = _HandlerDict()
+        self.Created = _HandlerDict()
+        self.Deleted = _HandlerDict()
+        self.Renamed = _HandlerDict()
+        
+        self._queue = Queue()
+        self._callback_consumer = threading.Thread(
+                                            target=self._handle_callbacks)
+        # Set this as a daemon so it runs as long as _watcher is running.
+        self._callback_consumer.setDaemon(True)
+        self._callback_consumer.start()
+        
+        # Renaming fires off two events: one for what the old name was
+        # and one for what the new name is. Since we only get one at a time,
+        # store the last old name we get and pair it up with the next
+        # new name we get. As far as I can see, this should work.
+        self._old_name = None
+
+    def _compile_filter(self, filter):
+        # Convert commonly accepted filter formats into regex for matching.
+        pattern = filter.replace(".", "\.").replace("*", "(.)+")
+        return re.compile(pattern)
+
+    @property
+    def EnableRaisingEvents(self):
+        return self._watcher.running
+
+    @EnableRaisingEvents.setter
+    def EnableRaisingEvents(self, enable):
+        # TODO: Do some type of paramter validation that not only is flags
+        # non-zero, but that it only contains flags that make sense.
+        if enable and (self._watcher.flags is 0):
+            raise AttributeError("NotifyFilter cannot be None")
+        self._filter_regex = self._compile_filter(self._filter)
+        self._watcher.start() if enable else self._watcher.stop()
+
+    @property
+    def IncludeSubdirectories(self):
+        return self._watcher.recursive
+
+    @IncludeSubdirectories.setter
+    def IncludeSubdirectories(self, value):
+        self._watcher.recursive = value
+
+    # NOTE: NotifyFilter cannot be None. ReadDirectoryChangesW will return 0
+    # and GetLastError will be 87, meaning "The parameter is incorrect".
+    @property
+    def NotifyFilter(self):
+        return self._watcher.flags
+
+    @NotifyFilter.setter
+    def NotifyFilter(self, value):
+        self._watcher.flags = value
+
+    @property
+    def Filter(self):
+        return self._filter
+
+    @Filter.setter
+    def Filter(self, value):
+        self._filter = value
+
+    @property
+    def Path(self):
+        return self._path
+
+    @Path.setter
+    def Path(self, value):
+        self._path = value
+
+    def _callback(self, action, path):
+        """Called from Watcher with an action value and relative path.
+           If the updates become too frequent, which is often the case,
+           this will become backed up and _watcher can't successfully call
+           into here. For that reason, we put the update in a queue and get
+           out of the way. See _handle_callbacks."""
+        self._queue.put((action, path))
+    
+    def _handle_callbacks(self):
+        while True:
+            action, path = self._queue.get()
+            
+            # Should evalute this and see if it's safe to move inside
+            # _callback. If we can skip out on queueing up useless updates,
+            # we absolutely should.
+            if not self._filter_regex.match(path):
+                continue
+
+            # There's no documentation stating what events should be let
+            # through based on what filters, but this seems to be correct
+            # based on usage of _watcher by itself.
+            if (action == _watcher.FILE_ACTION_ADDED and
+                (self.NotifyFilter & NotifyFilters.FileName or
+                 self.NotifyFilter & NotifyFilters.DirectoryName)):
+                callbacks = self.Created
+            elif (action == _watcher.FILE_ACTION_REMOVED and
+                  (self.NotifyFilter & NotifyFilters.FileName or
+                   self.NotifyFilter & NotifyFilters.DirectoryName)):
+                callbacks = self.Deleted
+            elif (action == _watcher.FILE_ACTION_MODIFIED and
+                  (self.NotifyFilter & NotifyFilters.Attributes or
+                   self.NotifyFilter & NotifyFilters.Size or
+                   self.NotifyFilter & NotifyFilters.LastWrite or
+                   self.NotifyFilter & NotifyFilters.LastAccess or
+                   self.NotifyFilter & NotifyFilters.CreationTime or
+                   self.NotifyFilter & NotifyFilters.Security)):
+                callbacks = self.Changed
+            elif (action == _watcher.FILE_ACTION_RENAMED_OLD_NAME and
+                  (self.NotifyFilter & NotifyFilters.FileName or
+                   self.NotifyFilter & NotifyFilters.DirectoryName)):
+                # Only store the old name and wait for the new one
+                # to notify via callbacks.
+                self._old_name = path
+                continue
+            elif (action == _watcher.FILE_ACTION_RENAMED_NEW_NAME and
+                  (self.NotifyFilter & NotifyFilters.FileName or
+                   self.NotifyFilter & NotifyFilters.DirectoryName)):
+                callbacks = self.Renamed
+            else:
+                raise ValueError("Received unknown action")
+
+            if callbacks != self.Renamed:
+                update = FileSystemEventArgs()
+            else:
+                update = RenamedEventArgs()
+                update.OldFullPath = os.path.join(self._path, self._old_name)
+                update.OldName = os.path.basename(self._old_name)
+
+            update.ChangeType = action
+            update.FullPath = os.path.join(self._path, path)
+            update.Name = os.path.basename(path)
+
+            for cb in callbacks.values():
+                cb(update)
+
+    def OnChanged(self, args):
+        return
+
+    def OnCreated(self, args):
+        return
+
+    def OnDeleted(self, args):
+        return
+
+    def OnError(self, args):
+        return
+
+    def OnRenamed(self, args):
+        return
+
+
+
+from distutils.core import setup, Extension
+
+setup(name="watcher",
+      version="0.1",
+      description="File system watcher",
+      url="blah",
+      keywords="blah",
+      maintainer="Brian Curtin",
+      maintainer_email="curtin@acm.org",
+      license="PSF",
+      platforms=["Win"],
+      py_modules=["FileSystemWatcher"],
+      packages = ["watcher", "watcher.tests"],
+      ext_modules=[Extension("watcher._watcher", ["src/_watcher.c"],
+                            # Compile with higher warning level
+                            # Define UNICODE since this was developed
+                            # with wide strings in mind, aka.
+                            # "Use Unicode Character Set" was enabled
+                            # in Visual Studio.
+                             extra_compile_args=["/W4", "/DUNICODE"])])
+

File src/_watcher.c

+#include "Python.h"
+#include "structmember.h"
+
+#include <windows.h>
+#include "stdio.h"
+
+/* ignore "conditional expression is constant" */
+#pragma warning(disable: 4100)
+/* ignore "unreferenced formal parameter" */
+#pragma warning(disable: 4127)
+
+#define MAX_BUFFER 4096
+
+
+static PyObject *watcher_error;
+
+typedef struct {
+    PyObject_HEAD
+    PyObject *callback;
+    PyObject *args;
+    PyObject *kwargs;
+
+    wchar_t *path;
+    int running;
+    int die;
+    int recursive;
+    unsigned int flags;
+
+    HANDLE handle;
+    HANDLE completion;
+    HANDLE thread;
+    CHAR buffer[MAX_BUFFER];
+    DWORD buffer_len;
+    OVERLAPPED overlapped;
+} watcher_object;
+
+
+void WINAPI
+handle_directory_change(DWORD_PTR completion_port)
+{
+    watcher_object *self;
+    PyGILState_STATE gil_state;
+    PyObject *call_rslt, *py_file_name, *args;
+    Py_ssize_t idx, args_len;
+    int pos;
+
+    BOOL rdc;
+    DWORD num_bytes, offset, err;
+    OVERLAPPED *overlapped;
+    PFILE_NOTIFY_INFORMATION notify_info;
+    wchar_t file_name[MAX_PATH];
+
+    do {
+        GetQueuedCompletionStatus((HANDLE)completion_port,
+                                  &num_bytes,
+                                  (PDWORD_PTR)&self,
+                                  &overlapped,
+                                  INFINITE);
+
+        if (self) {
+            notify_info = (PFILE_NOTIFY_INFORMATION)self->buffer;
+
+            do {
+                offset = notify_info->NextEntryOffset;
+
+                /* Get the relative file path out of the buffer.
+                   Apparently this should use StringCchCopy... */
+                lstrcpyn(file_name, notify_info->FileName,
+                         notify_info->FileNameLength / sizeof(wchar_t) + 1);
+                file_name[notify_info->FileNameLength / sizeof(wchar_t) + 1] = '\0';
+                py_file_name = PyUnicode_FromWideChar(file_name,
+                                                      wcslen(file_name));
+
+                /* Take the args tuple given in the constructor and place
+                   it after the info we have to call the user with.
+                   The user callback will contain the file action, the file
+                   path, then anything that the user wants to be called back
+                   with from self->args and self->kwargs. */
+                args_len = PyTuple_Size(self->args);
+                args = PyTuple_New(args_len + 2);
+                pos = 0;
+                /* Action should correspond to one of the constants listed
+                   at the bottom, titled Notify Actions. */
+                PyTuple_SET_ITEM(args, pos++,
+                                 PyLong_FromLongLong(notify_info->Action));
+                PyTuple_SET_ITEM(args, pos++, py_file_name);
+
+                for (idx = 0; idx < args_len; ++idx) {
+                    PyTuple_SET_ITEM(args, pos++,
+                                     PyTuple_GET_ITEM(self->args, idx));
+                }
+
+                gil_state = PyGILState_Ensure();
+                call_rslt = PyObject_Call(self->callback, args, self->kwargs);
+                PyGILState_Release(gil_state);
+
+                /* If the callback signature doesn't list what we sent, the
+                   call will fail with NULL. Other stuff can probably cause
+                   this but I have no idea what that would be. */
+                if (call_rslt == NULL) {
+                    /* These calls also require the GIL */
+                    gil_state = PyGILState_Ensure();
+                    PyErr_SetString(PyExc_RuntimeError,
+                                    "Unable to call callback");
+                    PyErr_Print();
+                    PyGILState_Release(gil_state);
+                } else
+                    Py_DECREF(call_rslt);
+
+                notify_info = (PFILE_NOTIFY_INFORMATION)((LPBYTE)notify_info + offset);
+
+            /* A zero offset specifies that we're on the last record, so keep
+               going until we get there. */
+            } while (offset);
+
+            rdc = ReadDirectoryChangesW(self->handle, self->buffer, MAX_BUFFER,
+                                        self->recursive, self->flags,
+                                        &self->buffer_len, &self->overlapped,
+                                        NULL);
+            err = GetLastError();
+
+            /* Let the user know if the re-issuing of RDC fails so it doesn't
+               look like a deadlock. */
+            if (rdc == 0) {
+                gil_state = PyGILState_Ensure();
+                PyErr_SetFromWindowsErr(err);
+                PyErr_Print();
+                PyGILState_Release(gil_state);
+                self = NULL; /* Break out of the main while loop. */
+            }
+        }
+    } while (self);
+}
+
+void WINAPI
+watch_thread(watcher_object *self)
+{
+    PyGILState_STATE gil_state;
+    HANDLE change_thread;
+    DWORD err;
+
+    BOOL rdc = ReadDirectoryChangesW(self->handle,
+                          self->buffer /* read results */, MAX_BUFFER,
+                          self->recursive, /* watch subdirectories */
+                          /* NOTE: At least one flag is required! */
+                          self->flags, /* see Notify Filters below */
+                          &self->buffer_len,
+                          &self->overlapped,
+                          NULL); /* completion routine */
+    err = GetLastError();
+
+    if (rdc == 0) {
+        gil_state = PyGILState_Ensure();
+        PyErr_SetFromWindowsErr(err);
+        PyErr_Print();
+        PyGILState_Release(gil_state);
+        self->running = 0;
+        return;
+    }
+    
+    change_thread = CreateThread(NULL, 0,
+                          (LPTHREAD_START_ROUTINE)handle_directory_change,
+                          self->completion,
+                          0, NULL);
+    while (1) {
+        if (self->die == 1)
+           break; 
+        Sleep(500);
+    }
+
+    /* Kill the completion port */
+    PostQueuedCompletionStatus(self->completion, 0, 0, NULL);
+    WaitForSingleObject(change_thread, INFINITE);
+    CloseHandle(change_thread);
+}
+
+static PyObject *
+watcher_object_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
+{
+    watcher_object *self;
+    PyObject *callback = NULL;
+    PyObject *path = NULL;
+
+    Py_ssize_t num_args = PyTuple_GET_SIZE(args);
+
+    /* Don't use PyArg_Parse* functions here.
+
+       We want to take kwargs without specifying named arguments. We'll
+       manually pick items out of the args tuple, then take a slice of the
+       remaining ones as our callback's args, then take all kwargs. */
+    if (num_args < 2) {
+        PyErr_SetString(PyExc_TypeError, "watcher takes at least 2 arguments");
+        return NULL;
+    }
+
+    path = PyTuple_GET_ITEM(args, 0);
+    if (!PyUnicode_Check(path)) {
+        PyErr_SetString(PyExc_TypeError, "path must be unicode");
+        return NULL;
+    }
+
+    callback = PyTuple_GET_ITEM(args, 1);
+    if (!PyCallable_Check(callback)) {
+        PyErr_Format(PyExc_TypeError, "callback parameter must be callable");
+        return NULL;
+    }
+
+    self = (watcher_object*)type->tp_alloc(type, 0);
+    if (self == NULL)
+        return NULL;
+
+    self->path = PyUnicode_AS_UNICODE(path);
+    Py_INCREF(path);
+
+    self->callback = callback;
+    Py_INCREF(callback);
+
+    /* Get any positional arguments via slicing the remains. */
+    self->args = PyTuple_GetSlice(args, 2, num_args);
+    if (self->args == NULL) {
+        /* PyObject_Call expects non-NULL args. */
+        self->args = PyTuple_New(0);
+    }
+
+    /* PyObject_Call works with NULLs so no need to check here. */
+    self->kwargs = kwargs;
+    Py_XINCREF(kwargs);
+
+    self->flags = 0;
+    self->recursive = 0;
+    
+    self->handle = CreateFileW(PyUnicode_AS_UNICODE(path),
+                   FILE_LIST_DIRECTORY, /* required */
+                   FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+                   NULL,
+                   OPEN_EXISTING,
+                   /* Use FILE_FLAG_OVERLAPPED for asynchronous operation
+                      with ReadDirectoryChangesW. */
+                   FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
+                   NULL);
+
+    if (self->handle == INVALID_HANDLE_VALUE) {
+        PyErr_Format(watcher_error, "unable to open path");
+        return NULL;
+    }
+
+    return (PyObject*)self;
+}
+
+static void
+watcher_object_dealloc(watcher_object *self)
+{
+    /* TODO: Make sure everything is shutdown? */
+    Py_TYPE(self)->tp_free((PyObject*)self);
+}
+
+PyDoc_STRVAR(watcher_start_doc,
+"start()\n\
+Start the watcher and begin sending callbacks.");
+
+static PyObject *
+watcher_start_function(watcher_object *self, PyObject *unused)
+{
+    if (self->running == 1)
+        Py_RETURN_FALSE;
+
+    self->completion = CreateIoCompletionPort(self->handle,
+                                              self->completion,
+                                              (ULONG_PTR)self,
+                                              /* max num processors */ 0);
+    if (self->completion == NULL) {
+        PyErr_Format(watcher_error, "unable to create completion port");
+        return NULL;
+    }
+
+    self->thread = CreateThread(NULL, 0,
+                                (LPTHREAD_START_ROUTINE)watch_thread,
+                                self, 0, NULL);
+    if (self->thread == NULL) {
+        PyErr_Format(watcher_error, "unable to start watcher thread");
+        return NULL;
+    }
+
+    self->running = 1;
+    Py_RETURN_NONE;
+}
+
+PyDoc_STRVAR(watcher_stop_doc,
+"stop()\n\
+Stop the watcher and halt any callbacks.");
+
+static PyObject *
+watcher_stop_function(watcher_object *self, PyObject *unused)
+{
+    if (self->running == 0)
+        Py_RETURN_FALSE;
+
+    self->die = 1;
+    WaitForSingleObject(self->thread, INFINITE);
+
+    if (CloseHandle(self->completion) == 0) {
+        PyErr_Format(watcher_error,
+                     "unable to close completion handle: %d", GetLastError());
+        return NULL;
+    }
+    if (CloseHandle(self->thread) == 0) {
+        PyErr_Format(watcher_error,
+                     "unable to close thread handle: %d", GetLastError());
+        return NULL;
+    }
+
+    self->running = 0;
+    Py_RETURN_NONE;
+}
+
+static PyMethodDef watcher_object_methods[] = {
+    {"start", (PyCFunction)watcher_start_function, METH_NOARGS, 
+               watcher_stop_doc},
+    {"stop", (PyCFunction)watcher_stop_function, METH_NOARGS,
+                watcher_stop_doc},
+    {NULL, NULL}
+};
+
+PyDoc_STRVAR(watcher_recursive_doc,
+"See the help text for Watcher.recursive.");
+
+PyDoc_STRVAR(watcher_flags_doc,
+"Any flags to specify notification information.");
+
+PyDoc_STRVAR(watcher_running_doc,
+"Boolean representing whether or not the Watcher is currently running.");
+
+static PyMemberDef watcher_object_members[] = {
+    {"recursive", T_BOOL, offsetof(watcher_object, recursive), 0,
+                  watcher_recursive_doc},
+    {"flags", T_ULONG, offsetof(watcher_object, flags), 0,
+              watcher_flags_doc},
+    {"running", T_BOOL, offsetof(watcher_object, running), 0,
+                watcher_running_doc},
+    {NULL}
+};
+
+
+PyDoc_STRVAR(watcher_doc, "TODO");
+
+static PyTypeObject watcher_object_type = {
+    PyVarObject_HEAD_INIT(NULL, 0)
+    "_watcher.Watcher",                 /* tp_name */
+    sizeof(watcher_object),             /* tp_size */
+    0,                                  /* tp_itemsize */
+    (destructor)watcher_object_dealloc, /* tp_dealloc */
+    0,                                  /* tp_print */
+    0,                                  /* tp_getattr */
+    0,                                  /* tp_setattr */
+    0,                                  /* tp_compare */
+    0,                                  /* tp_repr */
+    0,                                  /* tp_as_number */
+    0,                                  /* tp_as_sequence */
+    0,                                  /* tp_as_mapping */
+    0,                                  /* tp_hash */
+    0,                                  /* tp_call */
+    0,                                  /* tp_str */
+    0,                                  /* tp_getattro */
+    0,                                  /* tp_setattro */
+    0,                                  /* tp_as_buffer */
+    Py_TPFLAGS_DEFAULT | 
+    Py_TPFLAGS_BASETYPE,                /* tp_flags */
+    watcher_doc,                        /* tp_doc */
+    0,                                  /* tp_traverse */
+    0,                                  /* tp_clear */
+    0,                                  /* tp_richcompare */
+    0,                                  /* tp_weaklistoffset */
+    0,                                  /* tp_iter */
+    0,                                  /* tp_iterneext */
+    watcher_object_methods,             /* tp_methods */
+    watcher_object_members,             /* tp_members */
+    0,                                  /* tp_getset */
+    0,                                  /* tp_base */
+    0,                                  /* tp_dict */
+    0,                                  /* tp_descr_get */
+    0,                                  /* tp_descr_set */
+    0,                                  /* tp_dictoffset */
+    0,                                  /* tp_init */
+    PyType_GenericAlloc,                /* tp_alloc */
+    watcher_object_new,                 /* tp_new */
+    PyObject_Del,                       /* tp_free */
+};
+
+
+PyDoc_STRVAR(module_doc,
+"A low-level file system watcher built on ReadDirectoryChangesW\n\
+and overlapped I/O. This works similar to how the .NET\n\
+FileSystemWatcher class works interally.");
+
+static void
+setint(PyObject *dict, const char* name, long value)
+{
+    PyObject *val = PyLong_FromLong(value);
+    if (val && PyDict_SetItemString(dict, name, val) == 0)
+        Py_DECREF(val);
+}
+
+static struct PyModuleDef _watchermodule = {
+    PyModuleDef_HEAD_INIT,
+    "_watcher",
+    module_doc,
+    -1,
+    NULL,
+    NULL,
+    NULL,
+    NULL,
+    NULL
+};
+
+PyMODINIT_FUNC
+PyInit__watcher(void)
+{
+    PyObject *module, *dict;
+
+    /* Initialize and acquire the GIL before we do anything else. */
+    PyEval_InitThreads();
+
+    module = PyModule_Create(&_watchermodule);
+
+    if (PyType_Ready(&watcher_object_type) < 0)
+        goto fail;
+
+    dict = PyModule_GetDict(module);
+    if (!dict)
+        goto fail;
+
+    watcher_error = PyErr_NewException("_watcher.error",
+                                       PyExc_EnvironmentError, NULL);
+    if (watcher_error == NULL)
+        goto fail;
+
+    PyDict_SetItemString(dict, "error", watcher_error);
+    PyDict_SetItemString(dict, "Watcher", (PyObject*)&watcher_object_type);
+
+    /* Notify Filters */
+    setint(dict, "FILE_NOTIFY_CHANGE_FILE_NAME",
+                 FILE_NOTIFY_CHANGE_FILE_NAME);
+    setint(dict, "FILE_NOTIFY_CHANGE_DIR_NAME", 
+                 FILE_NOTIFY_CHANGE_DIR_NAME);
+    setint(dict, "FILE_NOTIFY_CHANGE_ATTRIBUTES", 
+                 FILE_NOTIFY_CHANGE_ATTRIBUTES);
+    setint(dict, "FILE_NOTIFY_CHANGE_SIZE",
+                 FILE_NOTIFY_CHANGE_SIZE);
+    setint(dict, "FILE_NOTIFY_CHANGE_LAST_WRITE", 
+                 FILE_NOTIFY_CHANGE_LAST_WRITE);
+    setint(dict, "FILE_NOTIFY_CHANGE_LAST_ACCESS",
+                 FILE_NOTIFY_CHANGE_LAST_ACCESS);
+    setint(dict, "FILE_NOTIFY_CHANGE_CREATION", 
+                 FILE_NOTIFY_CHANGE_CREATION);
+    setint(dict, "FILE_NOTIFY_CHANGE_SECURITY", 
+                 FILE_NOTIFY_CHANGE_SECURITY);
+
+    /* Notify Actions */
+    setint(dict, "FILE_ACTION_ADDED",
+                 FILE_ACTION_ADDED);
+    setint(dict, "FILE_ACTION_REMOVED",
+                 FILE_ACTION_REMOVED);
+    setint(dict, "FILE_ACTION_MODIFIED",
+                 FILE_ACTION_MODIFIED);
+    setint(dict, "FILE_ACTION_RENAMED_OLD_NAME",
+                 FILE_ACTION_RENAMED_OLD_NAME);
+    setint(dict, "FILE_ACTION_RENAMED_NEW_NAME",
+                 FILE_ACTION_RENAMED_NEW_NAME);
+
+    return module;
+
+fail:
+    return NULL;
+}

File src/src.vcproj

+<?xml version="1.0" encoding="Windows-1252"?>
+<VisualStudioProject
+	ProjectType="Visual C++"
+	Version="9.00"
+	Name="src"
+	ProjectGUID="{0843D75D-A325-4A0B-B1CE-06BE6569A7D5}"
+	RootNamespace="src"
+	Keyword="Win32Proj"
+	TargetFrameworkVersion="196613"
+	>
+	<Platforms>
+		<Platform
+			Name="Win32"
+		/>
+		<Platform
+			Name="x64"
+		/>
+	</Platforms>
+	<ToolFiles>
+	</ToolFiles>
+	<Configurations>
+		<Configuration
+			Name="Debug|Win32"
+			OutputDirectory="$(SolutionDir)$(ConfigurationName)"
+			IntermediateDirectory="$(ConfigurationName)"
+			ConfigurationType="2"
+			CharacterSet="1"
+			>
+			<Tool
+				Name="VCPreBuildEventTool"
+			/>
+			<Tool
+				Name="VCCustomBuildTool"
+			/>
+			<Tool
+				Name="VCXMLDataGeneratorTool"
+			/>
+			<Tool
+				Name="VCWebServiceProxyGeneratorTool"
+			/>
+			<Tool
+				Name="VCMIDLTool"
+			/>
+			<Tool
+				Name="VCCLCompilerTool"
+				Optimization="0"
+				AdditionalIncludeDirectories="&quot;C:\python-dev\release31-maint\PC&quot;;&quot;C:\python-dev\release31-maint\Include&quot;"
+				PreprocessorDefinitions="WIN32;_DEBUG;_WINDOWS;_USRDLL;SRC_EXPORTS"
+				MinimalRebuild="true"
+				ExceptionHandling="0"
+				BasicRuntimeChecks="3"
+				RuntimeLibrary="3"
+				UsePrecompiledHeader="0"
+				WarningLevel="4"
+				DebugInformationFormat="3"
+				CompileAs="1"
+			/>
+			<Tool
+				Name="VCManagedResourceCompilerTool"
+			/>
+			<Tool
+				Name="VCResourceCompilerTool"
+			/>
+			<Tool
+				Name="VCPreLinkEventTool"
+			/>
+			<Tool
+				Name="VCLinkerTool"
+				AdditionalDependencies="python31_d.lib"
+				OutputFile="$(OutDir)\_watcher.pyd"
+				LinkIncremental="2"
+				AdditionalLibraryDirectories="C:\python-dev\release31-maint\PCbuild"
+				GenerateDebugInformation="true"
+				SubSystem="2"
+				TargetMachine="1"
+			/>
+			<Tool
+				Name="VCALinkTool"
+			/>
+			<Tool
+				Name="VCManifestTool"
+			/>
+			<Tool
+				Name="VCXDCMakeTool"
+			/>
+			<Tool
+				Name="VCBscMakeTool"
+			/>
+			<Tool
+				Name="VCFxCopTool"
+			/>
+			<Tool
+				Name="VCAppVerifierTool"
+			/>
+			<Tool
+				Name="VCPostBuildEventTool"
+			/>
+		</Configuration>
+		<Configuration
+			Name="Debug|x64"
+			OutputDirectory="$(SolutionDir)$(PlatformName)\$(ConfigurationName)"
+			IntermediateDirectory="$(PlatformName)\$(ConfigurationName)"
+			ConfigurationType="2"
+			CharacterSet="1"
+			>
+			<Tool
+				Name="VCPreBuildEventTool"
+			/>
+			<Tool
+				Name="VCCustomBuildTool"
+			/>
+			<Tool
+				Name="VCXMLDataGeneratorTool"
+			/>
+			<Tool
+				Name="VCWebServiceProxyGeneratorTool"
+			/>
+			<Tool
+				Name="VCMIDLTool"
+				TargetEnvironment="3"
+			/>
+			<Tool
+				Name="VCCLCompilerTool"
+				Optimization="0"
+				AdditionalIncludeDirectories="&quot;C:\python-dev\release31-maint\PC&quot;;&quot;C:\python-dev\release31-maint\Include&quot;"
+				PreprocessorDefinitions="WIN32;_DEBUG;_WINDOWS;_USRDLL;SRC_EXPORTS"
+				MinimalRebuild="true"
+				ExceptionHandling="0"
+				BasicRuntimeChecks="3"
+				RuntimeLibrary="3"
+				UsePrecompiledHeader="0"
+				WarningLevel="4"
+				DebugInformationFormat="3"
+				CompileAs="1"
+			/>
+			<Tool
+				Name="VCManagedResourceCompilerTool"
+			/>
+			<Tool
+				Name="VCResourceCompilerTool"
+			/>
+			<Tool
+				Name="VCPreLinkEventTool"
+			/>
+			<Tool
+				Name="VCLinkerTool"
+				AdditionalOptions="/EXPORT:PyInit__watcher"
+				AdditionalDependencies="python31.lib"
+				OutputFile="$(OutDir)\_watcher.pyd"
+				LinkIncremental="2"
+				AdditionalLibraryDirectories="C:\python-dev\release31-maint\PCbuild\amd64"
+				GenerateDebugInformation="true"
+				SubSystem="2"
+				TargetMachine="17"
+			/>
+			<Tool
+				Name="VCALinkTool"
+			/>
+			<Tool
+				Name="VCManifestTool"
+			/>
+			<Tool
+				Name="VCXDCMakeTool"
+			/>
+			<Tool
+				Name="VCBscMakeTool"
+			/>
+			<Tool
+				Name="VCFxCopTool"
+			/>
+			<Tool
+				Name="VCAppVerifierTool"
+			/>
+			<Tool
+				Name="VCPostBuildEventTool"
+			/>
+		</Configuration>
+		<Configuration
+			Name="Release|Win32"
+			OutputDirectory="$(SolutionDir)$(ConfigurationName)"
+			IntermediateDirectory="$(ConfigurationName)"
+			ConfigurationType="2"
+			CharacterSet="1"
+			WholeProgramOptimization="1"
+			>
+			<Tool
+				Name="VCPreBuildEventTool"
+			/>
+			<Tool
+				Name="VCCustomBuildTool"
+			/>
+			<Tool
+				Name="VCXMLDataGeneratorTool"
+			/>
+			<Tool
+				Name="VCWebServiceProxyGeneratorTool"
+			/>
+			<Tool
+				Name="VCMIDLTool"
+			/>
+			<Tool
+				Name="VCCLCompilerTool"
+				Optimization="2"
+				EnableIntrinsicFunctions="true"
+				AdditionalIncludeDirectories="&quot;C:\python-dev\release31-maint\PC&quot;;&quot;C:\python-dev\release31-maint\Include&quot;"
+				PreprocessorDefinitions="WIN32;NDEBUG;_WINDOWS;_USRDLL;SRC_EXPORTS"
+				ExceptionHandling="0"
+				RuntimeLibrary="2"
+				EnableFunctionLevelLinking="true"
+				UsePrecompiledHeader="0"
+				WarningLevel="4"
+				DebugInformationFormat="3"
+				CompileAs="1"
+			/>
+			<Tool
+				Name="VCManagedResourceCompilerTool"
+			/>
+			<Tool
+				Name="VCResourceCompilerTool"
+			/>
+			<Tool
+				Name="VCPreLinkEventTool"
+			/>
+			<Tool
+				Name="VCLinkerTool"
+				AdditionalDependencies="python31.lib"
+				OutputFile="$(OutDir)\_watcher.pyd"
+				LinkIncremental="1"
+				AdditionalLibraryDirectories="C:\python-dev\release31-maint\PCbuild"
+				GenerateDebugInformation="true"
+				SubSystem="2"
+				OptimizeReferences="2"
+				EnableCOMDATFolding="2"
+				TargetMachine="1"
+			/>
+			<Tool
+				Name="VCALinkTool"
+			/>
+			<Tool
+				Name="VCManifestTool"
+			/>
+			<Tool
+				Name="VCXDCMakeTool"
+			/>
+			<Tool
+				Name="VCBscMakeTool"
+			/>
+			<Tool
+				Name="VCFxCopTool"
+			/>
+			<Tool
+				Name="VCAppVerifierTool"
+			/>
+			<Tool
+				Name="VCPostBuildEventTool"
+			/>
+		</Configuration>
+		<Configuration
+			Name="Release|x64"
+			OutputDirectory="$(SolutionDir)$(PlatformName)\$(ConfigurationName)"
+			IntermediateDirectory="$(PlatformName)\$(ConfigurationName)"
+			ConfigurationType="2"
+			CharacterSet="1"
+			WholeProgramOptimization="1"
+			>
+			<Tool
+				Name="VCPreBuildEventTool"
+			/>
+			<Tool
+				Name="VCCustomBuildTool"
+			/>
+			<Tool
+				Name="VCXMLDataGeneratorTool"
+			/>
+			<Tool
+				Name="VCWebServiceProxyGeneratorTool"
+			/>
+			<Tool
+				Name="VCMIDLTool"
+				TargetEnvironment="3"
+			/>
+			<Tool
+				Name="VCCLCompilerTool"
+				Optimization="0"
+				EnableIntrinsicFunctions="true"
+				AdditionalIncludeDirectories="&quot;C:\python-dev\release31-maint\PC&quot;;&quot;C:\python-dev\release31-maint\Include&quot;"
+				PreprocessorDefinitions="WIN32;NDEBUG;_WINDOWS;_USRDLL;SRC_EXPORTS"
+				ExceptionHandling="0"
+				RuntimeLibrary="2"
+				EnableFunctionLevelLinking="true"
+				UsePrecompiledHeader="0"
+				WarningLevel="4"
+				DebugInformationFormat="3"
+				CompileAs="1"
+			/>
+			<Tool
+				Name="VCManagedResourceCompilerTool"
+			/>
+			<Tool
+				Name="VCResourceCompilerTool"
+			/>
+			<Tool
+				Name="VCPreLinkEventTool"
+			/>
+			<Tool
+				Name="VCLinkerTool"
+				AdditionalDependencies="python31.lib"
+				OutputFile="$(OutDir)\_watcher.pyd"
+				LinkIncremental="1"
+				AdditionalLibraryDirectories="C:\python-dev\release31-maint\PCbuild\amd64"
+				GenerateDebugInformation="true"
+				SubSystem="2"
+				OptimizeReferences="2"
+				EnableCOMDATFolding="2"
+				TargetMachine="17"
+			/>
+			<Tool
+				Name="VCALinkTool"
+			/>
+			<Tool
+				Name="VCManifestTool"
+			/>
+			<Tool
+				Name="VCXDCMakeTool"
+			/>
+			<Tool
+				Name="VCBscMakeTool"
+			/>
+			<Tool
+				Name="VCFxCopTool"
+			/>
+			<Tool
+				Name="VCAppVerifierTool"
+			/>
+			<Tool
+				Name="VCPostBuildEventTool"
+			/>
+		</Configuration>
+	</Configurations>
+	<References>
+	</References>
+	<Files>
+		<Filter
+			Name="Source Files"
+			Filter="cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx"
+			UniqueIdentifier="{4FC737F1-C7A5-4376-A066-2A32D752A2FF}"
+			>
+			<File
+				RelativePath=".\_watcher.c"
+				>
+			</File>
+		</Filter>
+		<Filter
+			Name="Header Files"
+			Filter="h;hpp;hxx;hm;inl;inc;xsd"
+			UniqueIdentifier="{93995380-89BD-4b04-88EB-625FBE52EBFB}"
+			>
+		</Filter>
+		<Filter
+			Name="Resource Files"
+			Filter="rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav"
+			UniqueIdentifier="{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}"
+			>
+		</Filter>
+	</Files>
+	<Globals>
+	</Globals>
+</VisualStudioProject>
+
+Microsoft Visual Studio Solution File, Format Version 10.00
+# Visual Studio 2008
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "src", "src\src.vcproj", "{0843D75D-A325-4A0B-B1CE-06BE6569A7D5}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Win32 = Debug|Win32
+		Debug|x64 = Debug|x64
+		Release|Win32 = Release|Win32
+		Release|x64 = Release|x64
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{0843D75D-A325-4A0B-B1CE-06BE6569A7D5}.Debug|Win32.ActiveCfg = Debug|Win32
+		{0843D75D-A325-4A0B-B1CE-06BE6569A7D5}.Debug|Win32.Build.0 = Debug|Win32
+		{0843D75D-A325-4A0B-B1CE-06BE6569A7D5}.Debug|x64.ActiveCfg = Debug|x64
+		{0843D75D-A325-4A0B-B1CE-06BE6569A7D5}.Debug|x64.Build.0 = Debug|x64
+		{0843D75D-A325-4A0B-B1CE-06BE6569A7D5}.Release|Win32.ActiveCfg = Release|Win32
+		{0843D75D-A325-4A0B-B1CE-06BE6569A7D5}.Release|Win32.Build.0 = Release|Win32
+		{0843D75D-A325-4A0B-B1CE-06BE6569A7D5}.Release|x64.ActiveCfg = Release|x64
+		{0843D75D-A325-4A0B-B1CE-06BE6569A7D5}.Release|x64.Build.0 = Release|x64
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+EndGlobal

File watcher/__init__.py

+from ._watcher import *
+

File watcher/tests/__init__.py

Empty file added.

File watcher/tests/__main__.py

+try:
+    import unittest2 as unittest
+except ImportError:
+    import unittest
+
+from .test_watcher import *
+from .test_FileSystemWatcher import *
+
+if __name__ == "__main__":
+    unittest.main()
+

File watcher/tests/test_FileSystemWatcher.py

+# See if we can use unittest2 first if we're on 3.1.
+# 3.2 has the same features so we can fall back there.
+try:
+    import unittest2 as unittest
+except ImportError:
+    import unittest
+
+import os
+import tempfile
+import shutil
+import time
+import queue
+
+import FileSystemWatcher
+
+
+class TestFSWInitialization(unittest.TestCase):
+    def setUp(self):
+        self.dir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        shutil.rmtree(self.dir)
+
+    def test_default(self):
+        fsw = FileSystemWatcher.FileSystemWatcher(self.dir)
+        self.assertEqual(fsw.Path, self.dir)
+        self.assertEqual(fsw.Filter, "*.*")
+
+    def test_bad_dir(self):
+        # Hopefully "hurfdurf" doesn't exist...
+        with self.assertRaises(ValueError):
+            fsw = FileSystemWatcher.FileSystemWatcher("hurfdurf")
+
+    def test_custom_filter(self):
+        fsw = FileSystemWatcher.FileSystemWatcher(self.dir, "blarga.lol")
+        self.assertEqual(fsw.Filter, "blarga.lol")
+
+    def test_no_NotifyFilter(self):
+        # ERE should raise AttributeError if we have no flags set.
+        # If no flags get through, the underlying extension fails in a
+        # separate thread and it's not easy to handle.
+        fsw = FileSystemWatcher.FileSystemWatcher(self.dir)
+        with self.assertRaises(AttributeError):
+            fsw.EnableRaisingEvents = True
+
+    def test_bad_NotifyFilter(self):
+        fsw = FileSystemWatcher.FileSystemWatcher(self.dir)
+        fsw.NotifyFilter = 123456789
+        fsw.EnableRaisingEvents = True
+        time.sleep(0.01)
+        self.assertFalse(fsw.EnableRaisingEvents)
+
+
+class TestBasics(unittest.TestCase):
+    def setUp(self):
+        self.dir = tempfile.mkdtemp()
+        self.fsw = FileSystemWatcher.FileSystemWatcher(self.dir)
+ 
+        self.names = [os.path.join(self.dir, n) for n in ("hu.rf", "du.rf")]
+        self._queue = queue.Queue()
+
+    def tearDown(self):
+        # Doesn't hurt to call this twice.
+        self.fsw.EnableRaisingEvents = False
+        shutil.rmtree(self.dir)
+
+    def _callback(self, event):
+        self._queue.put(event)
+
+    def add_files(self):        
+        for name in self.names:
+            with open(name, "w") as file:
+                pass
+
+    def test_add_and_remove_files(self):
+        self.fsw.NotifyFilter = FileSystemWatcher.NotifyFilters.FileName
+        self.fsw.Created += self._callback
+        self.fsw.Deleted += self._callback
+        self.fsw.EnableRaisingEvents = True
+        self.assertTrue(self.fsw.EnableRaisingEvents)
+
+        self.add_files()
+        for name in self.names:
+            try:
+                event = self._queue.get(timeout=1)
+            except queue.Empty:
+                self.fail("Unable to get event")
+
+            self.assertEqual(event.ChangeType,
+                             FileSystemWatcher.WatcherChangeTypes.Created)
+            self.assertEqual(event.FullPath, os.path.join(self.dir, name))
+            self.assertEqual(event.Name, os.path.basename(name))
+            self._queue.task_done()
+
+        for name in self.names:
+            os.remove(name)
+            try:
+                event = self._queue.get(timeout=1)
+            except queue.Empty:
+                self.fail("Unable to get event")
+
+            self.assertEqual(event.ChangeType,
+                             FileSystemWatcher.WatcherChangeTypes.Deleted)
+            self.assertEqual(event.FullPath, os.path.join(self.dir, name))
+            self.assertEqual(event.Name, os.path.basename(name))
+            self._queue.task_done()
+
+    def test_modify_files(self):
+        self.fsw.NotifyFilter = FileSystemWatcher.NotifyFilters.LastWrite
+        self.fsw.Changed += self._callback
+        self.fsw.EnableRaisingEvents = True
+        self.assertTrue(self.fsw.EnableRaisingEvents)
+
+        # Won't pick up events for the add.
+        self.add_files()
+
+        for name in self.names:
+            with open(name, "a") as file:
+                file.write("lol")
+        
+        for name in self.names:
+            try:
+                event = self._queue.get(timeout=1)
+            except queue.Empty:
+                self.fail("Unable to get event")
+
+            self.assertEqual(event.ChangeType,
+                             FileSystemWatcher.WatcherChangeTypes.Changed)
+            self.assertEqual(event.FullPath, os.path.join(self.dir, name))
+            self.assertEqual(event.Name, os.path.basename(name))
+            self._queue.task_done()
+
+    def test_rename_files(self):
+        self.fsw.NotifyFilter = FileSystemWatcher.NotifyFilters.FileName
+        self.fsw.Renamed += self._callback
+        self.fsw.EnableRaisingEvents = True
+        self.assertTrue(self.fsw.EnableRaisingEvents)
+
+        # Won't pick up events for the add.
+        self.add_files()
+        
+        new_name = "new.name"
+        os.rename(self.names[0],
+                  os.path.join(self.dir, new_name))
+
+        try:
+            event = self._queue.get(timeout=1)
+        except queue.Empty:
+            self.fail("Unable to get event")
+
+        self.assertEqual(event.ChangeType,
+                         FileSystemWatcher.WatcherChangeTypes.Renamed)
+        self.assertEqual(event.FullPath, os.path.join(self.dir, new_name))
+        self.assertEqual(event.Name, new_name)
+        self.assertEqual(event.OldFullPath, self.names[0])
+        self.assertEqual(event.OldName, os.path.basename(self.names[0]))
+        self._queue.task_done()
+
+
+class TestFilters(unittest.TestCase):
+    pass
+
+
+class TestRecursiveChanges(unittest.TestCase):
+    pass
+
+
+if __name__ == "__main__":
+    unittest.main()

File watcher/tests/test_watcher.py

+# See if we can use unittest2 first if we're on 3.1.
+# 3.2 has the same features so we can fall back there.
+try:
+    import unittest2 as unittest
+except ImportError:
+    import unittest
+
+import tempfile
+
+import watcher
+
+class TestWatcherInitialization(unittest.TestCase):
+    def setUp(self):
+        self.dir = tempfile.mkdtemp()
+
+    def callback(self):
+        pass
+
+    def test_bytes_path(self):
+        # Eventually this will be supported.
+        with self.assertRaises(TypeError):
+            w = watcher.Watcher(b"lol", self.callback)
+
+    def test_bad_callback(self):
+        with self.assertRaises(TypeError):
+            w = watcher.Watcher(self.dir, 12345)
+
+    def test_no_args(self):
+        w = watcher.Watcher(self.dir, self.callback)
+
+    def test_args(self):
+        w = watcher.Watcher(self.dir, self.callback, 1, 2, 3)
+
+    def test_kwargs(self):
+        w = watcher.Watcher(self.dir, self.callback, lol="rofl")
+
+
+class TestAttributes(unittest.TestCase):
+    def setUp(self):
+        self.dir = tempfile.mkdtemp()
+        self.callback = lambda: 1
+        self._watcher = watcher.Watcher(self.dir, self.callback)
+
+    def test_flag(self):
+        self._watcher.flags = watcher.FILE_NOTIFY_CHANGE_FILE_NAME
+        self.assertEqual(self._watcher.flags,
+                         watcher.FILE_NOTIFY_CHANGE_FILE_NAME)
+
+    def test_multiple_flags(self):
+        all_flags = (watcher.FILE_NOTIFY_CHANGE_FILE_NAME |
+                     watcher.FILE_NOTIFY_CHANGE_DIR_NAME)
+        
+        self._watcher.flags = all_flags
+        self.assertEqual(self._watcher.flags, all_flags)
+
+    def test_recursive(self):
+        self.assertFalse(self._watcher.recursive)
+        self._watcher.recursive = True
+        self.assertTrue(self._watcher.recursive)
+
+
+if __name__ == "__main__":
+    unittest.main()
+