Repository: gorakhargosh/watchdog

Issue:
Events from non-ASCII paths raise UncodeDecodeError
```
from os import mkdir
from watchdog.observers.inotify import InotifyObserver
from watchdog.events import FileSystemEventHandler

class H(FileSystemEventHandler):
    def on_any_event(self, event):
        print(event)

def main():
    d = u"demo"
    handler = H()

    mkdir(d)
    obs = InotifyObserver()
    obs.schedule(handler, d, recursive=True)
    obs.start()
    mkdir(u"/".join((d, u"\N{SNOWMAN}")))
    obs.join()

main()
```

results in this exception

```
Exception in thread Thread-3:
Traceback (most recent call last):
  File "/usr/lib/python2.7/threading.py", line 801, in __bootstrap_inner
    self.run()
  File "watchdog/src/watchdog/observers/inotify_buffer.py", line 87, in run
    inotify_events = self._inotify.read_events()
  File "watchdog/src/watchdog/observers/inotify_c.py", line 324, in read_events
    inotify_event = InotifyEvent(wd, mask, cookie, name, src_path)
  File "watchdog/src/watchdog/observers/inotify_c.py", line 478, in __init__
    self._name = name.decode()
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe2 in position 0: ordinal not in range(128)
```

Apparently introduced in 0c5e3531748d231e34ef6ecf8581ac20fd956b25

Comments:

Patch:
diff --git a/src/watchdog/observers/inotify_c.py b/src/watchdog/observers/inotify_c.py
index 19b2c01..d6db4f4 100644
--- a/src/watchdog/observers/inotify_c.py
+++ b/src/watchdog/observers/inotify_c.py
@@ -26,6 +26,7 @@ from functools import reduce
 from ctypes import c_int, c_char_p, c_uint32
 from watchdog.utils import has_attribute
 from watchdog.utils import UnsupportedLibc
+from watchdog.utils.unicode_paths import decode
 
 
 def _load_libc():
@@ -453,16 +454,16 @@ class InotifyEvent(object):
     :param cookie:
         Event cookie
     :param name:
-        Event name.
+        Base name of the event source path.
     :param src_path:
-        Event source path
+        Full event source path.
     """
 
     def __init__(self, wd, mask, cookie, name, src_path):
         self._wd = wd
         self._mask = mask
         self._cookie = cookie
-        self._name = name.decode()
+        self._name = name
         self._src_path = src_path
 
     @property
@@ -573,4 +574,4 @@ class InotifyEvent(object):
         mask_string = self._get_mask_string(self.mask)
         s = '<%s: src_path=%r, wd=%d, mask=%s, cookie=%d, name=%s>'
         return s % (type(self).__name__, self.src_path, self.wd, mask_string,
-                    self.cookie, self.name)
+                    self.cookie, decode(self.name))
diff --git a/src/watchdog/utils/dirsnapshot.py b/src/watchdog/utils/dirsnapshot.py
index 320406c..52c4898 100644
--- a/src/watchdog/utils/dirsnapshot.py
+++ b/src/watchdog/utils/dirsnapshot.py
@@ -102,11 +102,11 @@ class DirectorySnapshotDiff(object):
         modified = set()
         for path in ref.paths & snapshot.paths:
             if ref.inode(path) == snapshot.inode(path):
-                if ref.mtime(path) != snapshot.mtime(path):
+                if ref.mtime(path) != snapshot.mtime(path) or ref.size(path) != snapshot.size(path):
                     modified.add(path)
         
         for (old_path, new_path) in moved:
-            if ref.mtime(old_path) != snapshot.mtime(new_path):
+            if ref.mtime(old_path) != snapshot.mtime(new_path) or ref.size(old_path) != snapshot.size(new_path):
                 modified.add(old_path)
         
         self._dirs_created = [path for path in created if snapshot.isdir(path)]
@@ -267,6 +267,9 @@ class DirectorySnapshot(object):
     
     def mtime(self, path):
         return self._stat_info[path].st_mtime
+
+    def size(self, path):
+        return self._stat_info[path].st_size
     
     def stat_info(self, path):
         """
diff --git a/tox.ini b/tox.ini
index ce7b1c9..a4c5809 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,4 +10,4 @@ deps =
     py{34}: pytest-cov==2.6.0
 extras = watchmedo
 commands =
-    python -m pytest {posargs}
+    python -bb -m pytest {posargs}


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index ae4eb2e..6fc9f6f 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -37,3 +37,21 @@ def no_thread_leaks():
     main = threading.main_thread()
     assert not [th for th in threading._dangling
                 if th is not main and th.is_alive()]
+
+
+@pytest.fixture(autouse=True)
+def no_warnings(recwarn):
+    """Fail on warning."""
+
+    yield
+
+    warnings = []
+    for warning in recwarn:  # pragma: no cover
+        message = str(warning.message)
+        if (
+            "Not importing directory" in message
+            or "Using or importing the ABCs" in message
+        ):
+            continue
+        warnings.append("{w.filename}:{w.lineno} {w.message}".format(w=warning))
+    assert not warnings
diff --git a/tests/shell.py b/tests/shell.py
index 3989158..376fdf1 100644
--- a/tests/shell.py
+++ b/tests/shell.py
@@ -29,6 +29,7 @@ import os.path
 import tempfile
 import shutil
 import errno
+import time
 
 
 # def tree(path='.', show_files=False):
@@ -108,3 +109,14 @@ def mkdtemp():
 
 def ls(path='.'):
     return os.listdir(path)
+
+
+def msize(path):
+    """Modify the file size without updating the modified time."""
+    with open(path, 'w') as w:
+        w.write('')
+    os.utime(path, (0, 0))
+    time.sleep(0.4)
+    with open(path, 'w') as w:
+        w.write('0')
+    os.utime(path, (0, 0))
diff --git a/tests/test_inotify_c.py b/tests/test_inotify_c.py
index 8bd24b1..912195a 100644
--- a/tests/test_inotify_c.py
+++ b/tests/test_inotify_c.py
@@ -154,3 +154,17 @@ def test_raise_error(monkeypatch):
         func()
     assert exc.value.errno == -1
     assert "Unknown error -1" in str(exc.value)
+
+
+def test_non_ascii_path():
+    """
+    Inotify can construct an event for a path containing non-ASCII.
+    """
+    path = p(u"\N{SNOWMAN}")
+    with watching(p('')):
+        os.mkdir(path)
+        event, _ = event_queue.get(timeout=5)
+        assert isinstance(event.src_path, type(u""))
+        assert event.src_path == path
+        # Just make sure it doesn't raise an exception.
+        assert repr(event)
diff --git a/tests/test_observers_polling.py b/tests/test_observers_polling.py
index 42bff6e..f94041a 100644
--- a/tests/test_observers_polling.py
+++ b/tests/test_observers_polling.py
@@ -41,7 +41,8 @@ from .shell import (
     mkdtemp,
     touch,
     rm,
-    mv
+    mv,
+    msize
 )
 
 
@@ -98,6 +99,12 @@ def test___init__(event_queue, emitter):
     sleep(SLEEP_TIME)
     rm(p('afile'))
 
+    sleep(SLEEP_TIME)
+    msize(p('bfile'))
+
+    sleep(SLEEP_TIME)
+    rm(p('bfile'))
+
     sleep(SLEEP_TIME)
     emitter.stop()
 
@@ -131,6 +138,13 @@ def test___init__(event_queue, emitter):
 
         DirModifiedEvent(p()),
         FileDeletedEvent(p('afile')),
+
+        DirModifiedEvent(p()),
+        FileCreatedEvent(p('bfile')),
+        FileModifiedEvent(p('bfile')),
+
+        DirModifiedEvent(p()),
+        FileDeletedEvent(p('bfile')),
     }
 
     expected.add(FileMovedEvent(p('fromfile'), p('project', 'tofile')))