11

I'm trying to identify a good way to watch for the appearance of a file using Python's asyncio library. This is what I've come up with so far:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""Watches for the appearance of a file."""

import argparse
import asyncio
import os.path


@asyncio.coroutine
def watch_for_file(file_path, interval=1):
    while True:
        if not os.path.exists(file_path):
            print("{} not found yet.".format(file_path))
            yield from asyncio.sleep(interval)
        else:
            print("{} found!".format(file_path))
            break


def make_cli_parser():
    cli_parser = argparse.ArgumentParser(description=__doc__)
    cli_parser.add_argument('file_path')
    return cli_parser


def main(argv=None):
    cli_parser = make_cli_parser()
    args = cli_parser.parse_args(argv)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(watch_for_file(args.file_path))

if __name__ == '__main__':
    main()

I saved this as watch_for_file.py, and can run it with

python3 watch_for_file.py testfile

In another shell session, I issue

touch testfile

to end the loop.

Is there a more elegant solution than using this infinite loop and yield from asyncio.sleep()?

gotgenes
  • 38,661
  • 28
  • 100
  • 128
  • If you're not set on rolling-your-own with asyncio, you could look [at this related question](http://stackoverflow.com/questions/182197/how-do-i-watch-a-file-for-changes-using-python) for other possible solutions. – Gerrat Oct 16 '14 at 21:39

3 Answers3

9

Well, there are nicer, platform-specific ways of being notified when a file is created. Gerrat linked to one for Windows in his comment, and pyinotify can be used for Linux. Those platform-specific approaches can probably be plugged into asyncio, but you'd end up writing a whole bunch of code to make it work in a platform independent way, which probably isn't worth the effort to just check for the appearance of a single file. If you need more sophisticated filesystem watching in addition to this, it is might be worth pursuing, though. It looks like pyinotify can be tweaked to add a subclass of its Notifier class that plugins into the asyncio event loop (there are already classes for tornado and asyncore), for example.

For your simple use-case, I think your infinite loop approach to polling is fine, but you could also just schedule callbacks with the event loop, if you wanted:

def watch_for_file(file_path, interval=1, loop=None):
    if not loop: loop = asyncio.get_event_loop()
    if not os.path.exists(file_path):
        print("{} not found yet.".format(file_path))
        loop.call_later(interval, watch_for_file, file_path, interval, loop)
    else:
        print("{} found!".format(file_path))
        loop.stop()

def main(argv=None):
    cli_parser = make_cli_parser()
    args = cli_parser.parse_args(argv)
    loop = asyncio.get_event_loop()
    loop.call_soon(watch_for_file, args.file_path)
    loop.run_forever()

I'm not sure this is much more elegant than the infinite loop, though.

Edit:

Just for fun, I wrote a solution using pyinotify:

import pyinotify
import asyncio
import argparse
import os.path


class AsyncioNotifier(pyinotify.Notifier):
    """

    Notifier subclass that plugs into the asyncio event loop.

    """
    def __init__(self, watch_manager, loop, callback=None,
                 default_proc_fun=None, read_freq=0, threshold=0, timeout=None):
        self.loop = loop
        self.handle_read_callback = callback
        pyinotify.Notifier.__init__(self, watch_manager, default_proc_fun, read_freq,
                                    threshold, timeout)
        loop.add_reader(self._fd, self.handle_read)

    def handle_read(self, *args, **kwargs):
        self.read_events()
        self.process_events()
        if self.handle_read_callback is not None:
            self.handle_read_callback(self)


class EventHandler(pyinotify.ProcessEvent):
    def my_init(self, file=None, loop=None):
        if not file:
            raise ValueError("file keyword argument must be provided")
        self.loop = loop if loop else asyncio.get_event_loop()
        self.filename = file

    def process_IN_CREATE(self, event):
        print("Creating:", event.pathname)
        if os.path.basename(event.pathname) == self.filename:
            print("Found it!")
            self.loop.stop()


def make_cli_parser():
    cli_parser = argparse.ArgumentParser(description=__doc__)
    cli_parser.add_argument('file_path')
    return cli_parser


def main(argv=None):
    cli_parser = make_cli_parser()
    args = cli_parser.parse_args(argv)
    loop = asyncio.get_event_loop()

    # set up pyinotify stuff
    wm = pyinotify.WatchManager()
    mask = pyinotify.IN_CREATE  # watched events
    dir_, filename = os.path.split(args.file_path)
    if not dir_:
        dir_ = "."
    wm.add_watch(dir_, mask)
    handler = EventHandler(file=filename, loop=loop)
    notifier = pyinotify.AsyncioNotifier(wm, loop, default_proc_fun=handler)

    loop.run_forever()

if __name__ == '__main__':
    main()
dano
  • 91,354
  • 19
  • 222
  • 219
  • 4
    As an update to this, I submitted a patch to add the `AsyncioNotifier` to `pyinotify`, and it was accepted. So future versions should have this support built in. – dano Mar 19 '15 at 21:00
4

Butter https://pypi.python.org/pypi/butter has support for asyncio out of the box, BTW.

import asyncio
from butter.inotify import IN_ALL_EVENTS
from butter.asyncio.inotify import Inotify_async

@asyncio.coroutine
def watcher(loop):

    inotify = Inotify_async(loop=loop)
    print(inotify)
    wd = inotify.watch('/tmp', IN_ALL_EVENTS)

    for i in range(5):
        event = yield from inotify.get_event()
        print(event)

    inotify.ignore(wd)
    print('done')

    event = yield from inotify.get_event()
    print(event)

    inotify.close()
    print(inotify)

loop = asyncio.get_event_loop()
task = loop.create_task(watcher(loop))
loop.run_until_complete(task)
Andrew Svetlov
  • 16,730
  • 8
  • 66
  • 69
  • 1
    `asyncio.coroutine` is misspelled. Also, according to the [asyncio docs](https://docs.python.org/3/library/asyncio-task.html#task), one shouldn't instantiate `Task`, but use `async()` or `BaseEventLoop.create_task()` instead. – gotgenes Oct 17 '14 at 19:30
  • Spelling fixed, thanks. Well, the preferable way to use `loop.create_task(coro)` but just `Task(coro, loop=loop)` also works. Fixed, anyway. – Andrew Svetlov Oct 17 '14 at 23:11
  • This one is not compatible with Python 3.7 at the moment – lig Sep 17 '18 at 12:30
  • Sorry, what is not compatible? – Andrew Svetlov Sep 18 '18 at 17:40
1

Butter is really cool. Another alternative is minotaur which is similar, but only implements inotify

async def main():
    with Inotify(blocking=False) as n:
        n.add_watch('.', Mask.CREATE | Mask.DELETE | Mask.MOVE)
        async for evt in n:
            print(evt)
scaramanga
  • 21
  • 3