pax_global_header00006660000000000000000000000064127004526500014513gustar00rootroot0000000000000052 comment=cc3dea411a15f1781c1c56b45fdf9ce583629db7 aionotify-0.2.0/000077500000000000000000000000001270045265000135135ustar00rootroot00000000000000aionotify-0.2.0/.flake8000066400000000000000000000000671270045265000146710ustar00rootroot00000000000000[flake8] max-line-length = 120 ; vim:set ft=dosini: aionotify-0.2.0/.gitignore000066400000000000000000000002171270045265000155030ustar00rootroot00000000000000# Temporary files .*.swp *.pyc *.pyo # Build-related files docs/_build/ .coverage .tox *.egg-info *.egg .eggs/ build/ dist/ htmlcov/ MANIFEST aionotify-0.2.0/.travis.yml000066400000000000000000000003241270045265000156230ustar00rootroot00000000000000sudo: false language: python python: 3.5 env: - TOXENV=py34 - TOXENV=py35 - TOXENV=lint install: pip install tox script: tox -e $TOXENV notifications: email: false irc: "irc.freenode.org#XelNext" aionotify-0.2.0/CHANGES000066400000000000000000000004321270045265000145050ustar00rootroot00000000000000ChangeLog ========= .. _v0.2.0: 0.2.0 (2016-04-04) ------------------ *Bugfix:* - Add support for disconnecting watches - Fix concurrent events / watch disconnection .. _v0.1.0: 0.1.0 (2016-04-04) ------------------ *New:* - Initial version .. vim:set ft=rst: aionotify-0.2.0/CREDITS000066400000000000000000000031121270045265000145300ustar00rootroot00000000000000Credits ======= Maintainers ----------- The ``aionotify`` project is operated and maintained by: * Raphaël Barrois (https://github.com/rbarrois) .. _contributors: Contributors ------------ The project has received contributions from (in alphabetical order): * Raphaël Barrois (https://github.com/rbarrois) Contributor license agreement ----------------------------- .. note:: This agreement is required to allow redistribution of submitted contributions. See http://oss-watch.ac.uk/resources/cla for an explanation. Any contributor proposing updates to the code or documentation of this project *MUST* add its name to the list in the :ref:`contributors` section, thereby "signing" the following contributor license agreement: They accept and agree to the following terms for their present end future contributions submitted to the ``aionotify`` project: * They represent that they are legally entitled to grant this license, and that their contributions are their original creation * They grant the ``aionotify`` project a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, sublicense and distribute their contributions and such derivative works. * They are not expected to provide support for their contributions, except to the extent they desire to provide support. .. note:: The above agreement is inspired by the Apache Contributor License Agreement. .. vim:set ft=rst: aionotify-0.2.0/INSTALL000066400000000000000000000002021270045265000145360ustar00rootroot00000000000000Installing aionotify ==================== Prerequisites: * Python>=3.4 Setup:: pip install aionotify .. vim:set ft=rst: aionotify-0.2.0/LICENSE000066400000000000000000000024301270045265000145170ustar00rootroot00000000000000Copyright (c) The aionotify project All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. aionotify-0.2.0/MANIFEST.in000066400000000000000000000002641270045265000152530ustar00rootroot00000000000000include CHANGES CREDITS INSTALL LICENSE README.rst include requirements_dev.txt graft aionotify prune examples prune tests global-exclude .py[cod] __pycache__ exclude Makefile aionotify-0.2.0/Makefile000066400000000000000000000001711270045265000151520ustar00rootroot00000000000000PACKAGE := aionotify CODE_DIRS := aionotify/ tests/ examples/ default: test test: tox lint: flake8 $(CODE_DIRS) aionotify-0.2.0/README.rst000066400000000000000000000043631270045265000152100ustar00rootroot00000000000000aionotify ========= .. image:: https://secure.travis-ci.org/rbarrois/aionotify.png?branch=master :target: http://travis-ci.org/rbarrois/aionotify/ .. image:: https://img.shields.io/pypi/v/aionotify.svg :target: http://aionotify.readthedocs.org/en/latest/changelog.html :alt: Latest Version .. image:: https://img.shields.io/pypi/pyversions/aionotify.svg :target: https://pypi.python.org/pypi/aionotify/ :alt: Supported Python versions .. image:: https://img.shields.io/pypi/wheel/aionotify.svg :target: https://pypi.python.org/pypi/aionotify/ :alt: Wheel status .. image:: https://img.shields.io/pypi/l/aionotify.svg :target: https://pypi.python.org/pypi/aionotify/ :alt: License ``aionotify`` is a simple, asyncio-based inotify library. Its use is quite simple: .. code-block:: python import asyncio import aionotify # Setup the watcher watcher = aionotify.Watcher() watcher.watch(alias='logs', path='/var/log', flags=aionotify.Flags.MODIFY) # Prepare the loop loop = asyncio.get_eventloop() async def work(): await watcher.setup(loop) for _i in range(10): # Pick the 10 first events event = await watcher.get_event() print(event) watcher.close() loop.run_until_completed(work()) loop.stop() loop.close() Events ------ An event is a simple object with a few attributes: * ``name``: the path of the modified file * ``flags``: the modification flag; use ``aionotify.Flags.parse()`` to retrieve a list of individual values. * ``alias``: the alias of the watch triggering the event * ``cookie``: for renames, this integer value links the "renamed from" and "renamed to" events. Watches ------- ``aionotify`` uses a system of "watches", similar to inotify. A watch may have an alias; by default, it uses the path name: .. code-block:: python watcher = aionotify.Watcher() watcher.watch('/var/log', flags=aionotify.Flags.MODIFY) # Similar to: watcher.watch('/var/log', flags=aionotify.Flags.MODIFY, alias='/var/log') A watch can be removed by using its alias: .. code-block:: python watcher = aionotify.Watcher() watcher.watch('/var/log', flags=aionotify.Flags.MODIFY) watcher.unwatch('/var/log') aionotify-0.2.0/aionotify/000077500000000000000000000000001270045265000155145ustar00rootroot00000000000000aionotify-0.2.0/aionotify/__init__.py000066400000000000000000000004411270045265000176240ustar00rootroot00000000000000# Copyright (c) 2016 The aionotify project # This code is distributed under the two-clause BSD License. from .enums import Flags from .base import Watcher __all__ = ['Flags', 'Watcher'] __version__ = '0.2.0' __author__ = 'Raphaël Barrois ' aionotify-0.2.0/aionotify/aioutils.py000066400000000000000000000105201270045265000177150ustar00rootroot00000000000000# Copyright (c) 2016 The aionotify project # This code is distributed under the two-clause BSD License. import asyncio import asyncio.futures import errno import logging import os logger = logging.getLogger('asyncio.aionotify') class UnixFileDescriptorTransport(asyncio.ReadTransport): # Inspired from asyncio.unix_events._UnixReadPipeTransport max_size = 1024 def __init__(self, loop, fileno, protocol, waiter=None): super().__init__() self._loop = loop self._fileno = fileno self._protocol = protocol self._active = False self._closing = False self._loop.call_soon(self._protocol.connection_made, self) # only start reading when connection_made() has been called. self._loop.call_soon(self.resume_reading) if waiter is not None: # only wake up the waiter when connection_made() has been called. self._loop.call_soon(self._notify_waiter, waiter) def _notify_waiter(self, waiter): if not waiter.cancelled(): waiter.set_result(None) def _read_ready(self): """Called by the event loop whenever the fd is ready for reading.""" try: data = os.read(self._fileno, self.max_size) except InterruptedError: # No worries ;) pass except OSError as exc: # Some OS-level problem, crash. self._fatal_error(exc, "Fatal read error on file descriptor read") else: if data: self._protocol.data_received(data) else: # We reached end-of-file. if self._loop.get_debug(): logger.info("%r was closed by the kernel", self) self._closing = False self.pause_reading() self._loop.call_soon(self._protocol.eof_received) self._loop.call_soon(self._call_connection_lost, None) def pause_reading(self): """Public API: pause reading the transport.""" self._loop.remove_reader(self._fileno) self._active = False def resume_reading(self): """Public API: resume transport reading.""" self._loop.add_reader(self._fileno, self._read_ready) self._active = True def close(self): """Public API: close the transport.""" if not self._closing: self._close() def _fatal_error(self, exc, message): if isinstance(exc, OSError) and exc.errno == errno.EIO: if self._loop.get_debug(): logger.debug("%r: %s", self, message, exc_info=True) else: self._loop.call_exception_handler({ 'message': message, 'exception': exc, 'transport': self, 'protocol': self._protocol, }) self._close(error=exc) def _close(self, error=None): """Actual closing code, both from manual close and errors.""" self._closing = True self.pause_reading() self._loop.call_soon(self._call_connection_lost, error) def _call_connection_lost(self, error): """Finalize closing.""" try: self._protocol.connection_lost(error) finally: os.close(self._fileno) self._fileno = None self._protocol = None self._loop = None def __repr__(self): if self._active: status = 'active' elif self._closing: status = 'closing' elif self._fileno: status = 'paused' else: status = 'closed' parts = [ self.__class__.__name__, status, 'fd=%s' % self._fileno, ] return '<%s>' % ' '.join(parts) @asyncio.coroutine def stream_from_fd(fd, loop): """Recieve a streamer for a given file descriptor.""" reader = asyncio.StreamReader(loop=loop) protocol = asyncio.StreamReaderProtocol(reader, loop=loop) waiter = asyncio.futures.Future(loop=loop) transport = UnixFileDescriptorTransport( loop=loop, fileno=fd, protocol=protocol, waiter=waiter, ) try: yield from waiter except: transport.close() raise if loop.get_debug(): logger.debug("Read fd %r connected: (%r, %r)", fd, transport, protocol) return reader, transport aionotify-0.2.0/aionotify/base.py000066400000000000000000000100201270045265000167710ustar00rootroot00000000000000# Copyright (c) 2016 The aionotify project # This code is distributed under the two-clause BSD License. import asyncio import asyncio.streams import collections import ctypes import struct from . import aioutils Event = collections.namedtuple('Event', ['flags', 'cookie', 'name', 'alias']) _libc = ctypes.cdll.LoadLibrary('libc.so.6') class LibC: """Proxy to C functions for inotify""" @classmethod def inotify_init(cls): return _libc.inotify_init() @classmethod def inotify_add_watch(cls, fd, path, flags): return _libc.inotify_add_watch(fd, path.encode('utf-8'), flags) @classmethod def inotify_rm_watch(cls, fd, wd): return _libc.inotify_rm_watch(fd, wd) PREFIX = struct.Struct('iIII') class Watcher: def __init__(self): self.requests = {} self._reset() def _reset(self): self.descriptors = {} self.aliases = {} self._stream = None self._transport = None self._fd = None self._loop = None def watch(self, path, flags, *, alias=None): """Add a new watching rule.""" if alias is None: alias = path if alias in self.requests: raise ValueError("A watch request is already scheduled for alias %s" % alias) self.requests[alias] = (path, flags) if self._fd is not None: # We've started, register the watch immediately. self._setup_watch(alias, path, flags) def unwatch(self, alias): """Stop watching a given rule.""" if alias not in self.descriptors: raise ValueError("Unknown watch alias %s; current set is %r" % (alias, list(self.descriptors.keys()))) wd = self.descriptors[alias] errno = LibC.inotify_rm_watch(self._fd, wd) if errno != 0: raise IOError("Failed to close watcher %d: errno=%d" % (wd, errno)) del self.descriptors[alias] del self.requests[alias] del self.aliases[wd] def _setup_watch(self, alias, path, flags): """Actual rule setup.""" assert alias not in self.descriptors, "Registering alias %s twice!" % alias wd = LibC.inotify_add_watch(self._fd, path, flags) if wd < 0: raise IOError("Error setting up watch on %s with flags %s: wd=%s" % ( path, flags, wd)) self.descriptors[alias] = wd self.aliases[wd] = alias @asyncio.coroutine def setup(self, loop): """Start the watcher, registering new watches if any.""" self._loop = loop self._fd = LibC.inotify_init() for alias, (path, flags) in self.requests.items(): self._setup_watch(alias, path, flags) # We pass ownership of the fd to the transport; it will close it. self._stream, self._transport = yield from aioutils.stream_from_fd(self._fd, loop) def close(self): """Schedule closure. This will close the transport and all related resources. """ self._transport.close() self._reset() @property def closed(self): """Are we closed?""" return self._transport is None @asyncio.coroutine def get_event(self): """Fetch an event. This coroutine will swallow events for removed watches. """ while True: prefix = yield from self._stream.readexactly(PREFIX.size) if prefix == b'': # We got closed, return None. return wd, flags, cookie, length = PREFIX.unpack(prefix) path = yield from self._stream.readexactly(length) # All async performed, time to look at the event's content. if wd not in self.aliases: # Event for a removed watch, skip it. continue decoded_path = struct.unpack('%ds' % length, path)[0].rstrip(b'\x00').decode('utf-8') return Event( flags=flags, cookie=cookie, name=decoded_path, alias=self.aliases[wd], ) aionotify-0.2.0/aionotify/enums.py000066400000000000000000000025541270045265000172230ustar00rootroot00000000000000# Copyright (c) 2016 The aionotify project # This code is distributed under the two-clause BSD License. import enum class Flags(enum.IntEnum): ACCESS = 0x00000001 #: File was accessed MODIFY = 0x00000002 #: File was modified ATTRIB = 0x00000004 #: Metadata changed CLOSE_WRITE = 0x00000008 #: Writable file was closed CLOSE_NOWRITE = 0x00000010 #: Unwritable file closed OPEN = 0x00000020 #: File was opened MOVED_FROM = 0x00000040 #: File was moved from X MOVED_TO = 0x00000080 #: File was moved to Y CREATE = 0x00000100 #: Subfile was created DELETE = 0x00000200 #: Subfile was deleted DELETE_SELF = 0x00000400 #: Self was deleted MOVE_SELF = 0x00000800 #: Self was moved UNMOUNT = 0x00002000 #: Backing fs was unmounted Q_OVERFLOW = 0x00004000 #: Event queue overflowed IGNORED = 0x00008000 #: File was ignored ONLYDIR = 0x01000000 #: only watch the path if it is a directory DONT_FOLLOW = 0x02000000 #: don't follow a sym link EXCL_UNLINK = 0x04000000 #: exclude events on unlinked objects MASK_ADD = 0x20000000 #: add to the mask of an already existing watch ISDIR = 0x40000000 #: event occurred against dir ONESHOT = 0x80000000 #: only send event once @classmethod def parse(cls, flags): return [flag for flag in cls.__members__.values() if flag & flags] aionotify-0.2.0/examples/000077500000000000000000000000001270045265000153315ustar00rootroot00000000000000aionotify-0.2.0/examples/print.py000077500000000000000000000037631270045265000170530ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (c) 2016 The aionotify project # This code is distributed under the two-clause BSD License. import aionotify import argparse import asyncio import logging import signal class Example: def __init__(self): self.loop = None self.watcher = None self.task = None def prepare(self, path): self.watcher = aionotify.Watcher() self.watcher.watch(path, aionotify.Flags.MODIFY | aionotify.Flags.CREATE | aionotify.Flags.DELETE) @asyncio.coroutine def _run(self, max_events): yield from self.watcher.setup(self.loop) for _i in range(max_events): event = yield from self.watcher.get_event() print(event.name, aionotify.Flags.parse(event.flags)) self.shutdown() def run(self, loop, max_events): self.loop = loop self.task = loop.create_task(self._run(max_events)) def shutdown(self): self.watcher.close() if self.task is not None: self.task.cancel() self.loop.stop() def setup_signal_handlers(loop, example): for sig in [signal.SIGINT, signal.SIGTERM]: loop.add_signal_handler(sig, example.shutdown) def main(args): if args.debug: logger = logging.getLogger('asyncio') logger.setLevel(logging.DEBUG) logger.addHandler(logging.StreamHandler()) example = Example() example.prepare(args.path) loop = asyncio.get_event_loop() if args.debug: loop.set_debug(True) setup_signal_handlers(loop, example) example.run(loop, args.events) try: loop.run_forever() finally: loop.close() if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('path', help="Path to watch") parser.add_argument('--events', default=10, type=int, help="Number of arguments before shutdown") parser.add_argument('-d', '--debug', action='store_true', help="Enable asyncio debugging.") args = parser.parse_args() main(args) aionotify-0.2.0/requirements_dev.txt000066400000000000000000000000131270045265000176270ustar00rootroot00000000000000tox flake8 aionotify-0.2.0/setup.py000077500000000000000000000030531270045265000152310ustar00rootroot00000000000000#!/usr/bin/env python # Copyright (c) 2016 The aionotify project # This code is distributed under the two-clause BSD License. import codecs import os import re import sys from setuptools import setup root_dir = os.path.abspath(os.path.dirname(__file__)) def get_version(package_name): version_re = re.compile(r"^__version__ = [\"']([\w_.-]+)[\"']$") package_components = package_name.split('.') init_path = os.path.join(root_dir, *(package_components + ['__init__.py'])) with codecs.open(init_path, 'r', 'utf-8') as f: for line in f: match = version_re.match(line[:-1]) if match: return match.groups()[0] return '0.1.0' PACKAGE = 'aionotify' setup( name=PACKAGE, version=get_version(PACKAGE), description="Asyncio-powered inotify library", author="Raphaël Barrois", author_email="raphael.barrois+%s@polytechnique.org" % PACKAGE, url='https://github.com/rbarrois/%s' % PACKAGE, keywords=['asyncio', 'inotify'], packages=[PACKAGE], license='BSD', setup_requires=[ ], tests_require=[ 'asynctest', ], classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Filesystems", ], test_suite='tests', ) aionotify-0.2.0/tests/000077500000000000000000000000001270045265000146555ustar00rootroot00000000000000aionotify-0.2.0/tests/__init__.py000066400000000000000000000000001270045265000167540ustar00rootroot00000000000000aionotify-0.2.0/tests/test_enums.py000066400000000000000000000006341270045265000174200ustar00rootroot00000000000000# Copyright (c) 2016 The aionotify project # This code is distributed under the two-clause BSD License. import unittest import aionotify class EnumsTests(unittest.TestCase): def test_parsing(self): Flags = aionotify.Flags flags = Flags.ACCESS | Flags.MODIFY | Flags.ATTRIB parsed = Flags.parse(flags) self.assertEqual([Flags.ACCESS, Flags.MODIFY, Flags.ATTRIB], parsed) aionotify-0.2.0/tests/test_usage.py000066400000000000000000000163131270045265000173760ustar00rootroot00000000000000# Copyright (c) 2016 The aionotify project # This code is distributed under the two-clause BSD License. import asyncio import logging import os import os.path import tempfile import unittest import asynctest import aionotify AIODEBUG = bool(os.environ.get('PYTHONAIODEBUG') == '1') if AIODEBUG: logger = logging.getLogger('asyncio') logger.setLevel(logging.DEBUG) logger.addHandler(logging.StreamHandler()) TESTDIR = os.environ.get('AIOTESTDIR') or os.path.join(os.path.dirname(__file__), 'testevents') class AIONotifyTestCase(asynctest.TestCase): forbid_get_event_loop = True timeout = 3 def setUp(self): if AIODEBUG: self.loop.set_debug(True) self.watcher = aionotify.Watcher() self._testdir = tempfile.TemporaryDirectory(dir=TESTDIR) self.testdir = self._testdir.name # Schedule a loop shutdown self.loop.call_later(self.timeout, self.loop.stop) def tearDown(self): if not self.watcher.closed: self.watcher.close() self._testdir.cleanup() self.assertFalse(os.path.exists(self.testdir)) # Utility functions # ================= # Those allow for more readable tests. def _touch(self, filename, *, parent=None): path = os.path.join(parent or self.testdir, filename) with open(path, 'w') as f: f.write('') def _unlink(self, filename, *, parent=None): path = os.path.join(parent or self.testdir, filename) os.unlink(path) def _rename(self, source, target, *, parent=None): source_path = os.path.join(parent or self.testdir, source) target_path = os.path.join(parent or self.testdir, target) os.rename(source_path, target_path) def _assert_file_event(self, event, name, flags=aionotify.Flags.CREATE, alias=None): """Check for an expected file event. Allows for more readable tests. """ if alias is None: alias = self.testdir self.assertEqual(name, event.name) self.assertEqual(flags, event.flags) self.assertEqual(alias, event.alias) @asyncio.coroutine def _assert_no_events(self, timeout=0.1): """Ensure that no events are left in the queue.""" task = self.watcher.get_event() try: result = yield from asyncio.wait_for(task, timeout, loop=self.loop) except asyncio.TimeoutError: # All fine: we didn't receive any event. pass else: raise AssertionError("Event %r occurred within timeout %s" % (result, timeout)) class SimpleUsageTests(AIONotifyTestCase): @asyncio.coroutine def test_watch_before_start(self): """A watch call is valid before startup.""" self.watcher.watch(self.testdir, aionotify.Flags.CREATE) yield from self.watcher.setup(self.loop) # Touch a file: we get the event. self._touch('a') event = yield from self.watcher.get_event() self._assert_file_event(event, 'a') # And it's over. yield from self._assert_no_events() @asyncio.coroutine def test_watch_after_start(self): """A watch call is valid after startup.""" yield from self.watcher.setup(self.loop) self.watcher.watch(self.testdir, aionotify.Flags.CREATE) # Touch a file: we get the event. self._touch('a') event = yield from self.watcher.get_event() self._assert_file_event(event, 'a') # And it's over. yield from self._assert_no_events() @asyncio.coroutine def test_event_ordering(self): """Events should arrive in the order files where created.""" yield from self.watcher.setup(self.loop) self.watcher.watch(self.testdir, aionotify.Flags.CREATE) # Touch 2 files self._touch('a') self._touch('b') # Get the events event1 = yield from self.watcher.get_event() event2 = yield from self.watcher.get_event() self._assert_file_event(event1, 'a') self._assert_file_event(event2, 'b') # And it's over. yield from self._assert_no_events() @asyncio.coroutine def test_filtering_events(self): """We only get targeted events.""" yield from self.watcher.setup(self.loop) self.watcher.watch(self.testdir, aionotify.Flags.CREATE) self._touch('a') event = yield from self.watcher.get_event() self._assert_file_event(event, 'a') # Perform a filtered-out event; we shouldn't see anything self._unlink('a') yield from self._assert_no_events() @asyncio.coroutine def test_watch_unwatch(self): """Watches can be removed.""" self.watcher.watch(self.testdir, aionotify.Flags.CREATE) yield from self.watcher.setup(self.loop) self.watcher.unwatch(self.testdir) yield from asyncio.sleep(0.1) # Touch a file; we shouldn't see anything. self._touch('a') yield from self._assert_no_events() @asyncio.coroutine def test_watch_unwatch_before_drain(self): """Watches can be removed, no events occur afterwards.""" self.watcher.watch(self.testdir, aionotify.Flags.CREATE) yield from self.watcher.setup(self.loop) # Touch a file before unwatching self._touch('a') self.watcher.unwatch(self.testdir) # We shouldn't see anything. yield from self._assert_no_events() @asyncio.coroutine def test_rename_detection(self): """A file rename can be detected through event cookies.""" self.watcher.watch(self.testdir, aionotify.Flags.MOVED_FROM | aionotify.Flags.MOVED_TO) yield from self.watcher.setup(self.loop) self._touch('a') # Rename a file => two events self._rename('a', 'b') event1 = yield from self.watcher.get_event() event2 = yield from self.watcher.get_event() # We got moved_from then moved_to; they share the same cookie. self._assert_file_event(event1, 'a', aionotify.Flags.MOVED_FROM) self._assert_file_event(event2, 'b', aionotify.Flags.MOVED_TO) self.assertEqual(event1.cookie, event2.cookie) # And it's over. yield from self._assert_no_events() class ErrorTests(AIONotifyTestCase): """Test error cases.""" @asyncio.coroutine def test_watch_nonexistent(self): """Watching a non-existent directory raises an OSError.""" badpath = os.path.join(self.testdir, 'nonexistent') self.watcher.watch(badpath, aionotify.Flags.CREATE) with self.assertRaises(OSError): yield from self.watcher.setup(self.loop) @asyncio.coroutine def test_unwatch_bad_alias(self): self.watcher.watch(self.testdir, aionotify.Flags.CREATE) yield from self.watcher.setup(self.loop) with self.assertRaises(ValueError): self.watcher.unwatch('blah') class SanityTests(AIONotifyTestCase): timeout = 0.1 @unittest.expectedFailure @asyncio.coroutine def test_timeout_works(self): """A test cannot run longer than the defined timeout.""" # This test should fail, since we're setting a global timeout of 0.1 yet ask to wait for 0.3 seconds. yield from asyncio.sleep(0.5) aionotify-0.2.0/tests/testevents/000077500000000000000000000000001270045265000170615ustar00rootroot00000000000000aionotify-0.2.0/tests/testevents/.keep_dir000066400000000000000000000000001270045265000206320ustar00rootroot00000000000000aionotify-0.2.0/tox.ini000066400000000000000000000003221270045265000150230ustar00rootroot00000000000000[tox] envlist = py34,py35, lint [testenv] deps = -rrequirements_dev.txt commands = python -Wdefault setup.py test setenv = PYTHONAIODEBUG=1 [testenv:lint] whitelist_externals = make commands = make lint