inotify_simple 1.3¶
Chris Billington, Jun 20, 2022
inotify_simple
is a simple Python wrapper around
inotify.
No fancy bells and whistles, just a literal wrapper with ctypes. Only ~100
lines of code!
inotify_init1()
is wrapped as a file-like object, INotify
,
holding the inotify file descriptor. read()
reads
available data from the file descriptor and returns events as
Event
namedtuples after unpacking them with the struct
module. inotify_add_watch()
and inotify_rm_watch()
are wrapped with no changes
at all, taking and returning watch descriptor integers that calling code is expected to
keep track of itself, just as one would use inotify from C. Works with Python 2.7 and
Python >= 3.2.
View on PyPI | Fork me on GitHub | Read the docs
Installation¶
to install inotify_simple
, run:
$ pip3 install inotify_simple
or to install from source:
$ python3 setup.py install
Note
If on Python < 3.4, you’ll need the backported enum34 module.
Introduction¶
There are many inotify python wrappers out there. I found them all unsatisfactory. Most are far too high-level for my tastes, and the supposed convenience they provide actually limits one from using inotify in ways other than those the author imagined. Others are C extensions, requiring compilation for different platforms and Python versions, rather than a pure python module using ctypes. This one is pretty low-level and really just does what inotify itself does and nothing more. So hopefully if I’ve written it right, it will remain functional well into the future with no changes, recompilation or attention on my part.
Example usage¶
import os
from inotify_simple import INotify, flags
os.mkdir('/tmp/inotify_test')
inotify = INotify()
watch_flags = flags.CREATE | flags.DELETE | flags.MODIFY | flags.DELETE_SELF
wd = inotify.add_watch('/tmp/inotify_test', watch_flags)
# Now create, delete and modify some files in the directory being monitored:
os.chdir('/tmp/inotify_test')
# CREATE event for a directory:
os.system('mkdir foo')
# CREATE event for a file:
os.system('echo hello > test.txt')
# MODIFY event for the file:
os.system('echo world >> test.txt')
# DELETE event for the file
os.system('rm test.txt')
# DELETE event for the directory
os.system('rmdir foo')
os.chdir('/tmp')
# DELETE_SELF on the original directory. # Also generates an IGNORED event
# indicating the watch was removed.
os.system('rmdir inotify_test')
# And see the corresponding events:
for event in inotify.read():
print(event)
for flag in flags.from_mask(event.mask):
print(' ' + str(flag))
$ python example.py
Event(wd=1, mask=1073742080, cookie=0, name='foo')
flags.CREATE
flags.ISDIR
Event(wd=1, mask=256, cookie=0, name='test.txt')
flags.CREATE
Event(wd=1, mask=2, cookie=0, name='test.txt')
flags.MODIFY
Event(wd=1, mask=512, cookie=0, name='test.txt')
flags.DELETE
Event(wd=1, mask=1073742336, cookie=0, name='foo')
flags.DELETE
flags.ISDIR
Event(wd=1, mask=1024, cookie=0, name='')
flags.DELETE_SELF
Event(wd=1, mask=32768, cookie=0, name='')
flags.IGNORED
Note that the flags, since they are defined with an enum.IntEnum
, print as
what they are called rather than their integer values. However they are still
just integers and so can be bitwise-ANDed and ORed etc with masks etc. The
from_mask()
method bitwise-ANDs a mask with all
possible flags and returns a list of matches. This is for convenience and
useful for debugging which events are coming through, but performance critical
code should generally bitwise-AND masks with flags of interest itself so as to
not do unnecessary checks.
Module reference¶
-
class
inotify_simple.
INotify
(inheritable=False, nonblocking=False)¶ Bases:
_io.FileIO
File-like object wrapping
inotify_init1()
. RaisesOSError
on failure.close()
should be called when no longer needed. Can be used as a context manager to ensure it is closed, and can be used directly by functions expecting a file-like object, such asselect
, or with functions expecting a file descriptor viafileno()
.Parameters: - inheritable (bool) – whether the inotify file descriptor will be inherited by
child processes. The default,``False``, corresponds to passing the
IN_CLOEXEC
flag toinotify_init1()
. Setting this flag when opening filedescriptors is the default behaviour of Python standard library functions since PEP 446. On Python < 3.3, the file descriptor will be inheritable and this argument has no effect, one must instead use fcntl to set FD_CLOEXEC to make it non-inheritable. - nonblocking (bool) – whether to open the inotify file descriptor in
nonblocking mode, corresponding to passing the
IN_NONBLOCK
flag toinotify_init1()
. This does not affect the normal behaviour ofread()
, which usespoll()
to control blocking behaviour according to the given timeout, but will cause other reads of the file descriptor (for example if the application reads data manually withos.read(fd)
) to raiseBlockingIOError
if no data is available.
-
fd
¶ The inotify file descriptor returned by
inotify_init()
. You are free to use it directly withos.read
if you’d prefer not to callread()
for some reason. Also available asfileno()
-
add_watch
(path, mask)¶ Wrapper around
inotify_add_watch()
. Returns the watch descriptor or raises anOSError
on failure.Parameters: - path (str, bytes, or PathLike) – The path to watch. Will be encoded with
os.fsencode()
before being passed toinotify_add_watch()
. - mask (int) – The mask of events to watch for. Can be constructed by
bitwise-ORing
flags
together.
Returns: watch descriptor
Return type: int
- path (str, bytes, or PathLike) – The path to watch. Will be encoded with
-
rm_watch
(wd)¶ Wrapper around
inotify_rm_watch()
. RaisesOSError
on failure.Parameters: wd (int) – The watch descriptor to remove
-
read
(timeout=None, read_delay=None)¶ Read the inotify file descriptor and return the resulting
Event
namedtuples (wd, mask, cookie, name).Parameters: - timeout (int) – The time in milliseconds to wait for events if there are
none. If negative or
None
, block until there are events. If zero, return immediately if there are no events to be read. - read_delay (int) – If there are no events immediately available for reading, then this is the time in milliseconds to wait after the first event arrives before reading the file descriptor. This allows further events to accumulate before reading, which allows the kernel to coalesce like events and can decrease the number of events the application needs to process. However, this also increases the risk that the event queue will overflow due to not being emptied fast enough.
Returns: generator producing
Event
namedtuplesReturn type: generator
Warning
If the same inotify file descriptor is being read by multiple threads simultaneously, this method may attempt to read the file descriptor when no data is available. It may return zero events, or block until more events arrive (regardless of the requested timeout), or in the case that the
INotify()
object was instantiated withnonblocking=True
, raiseBlockingIOError
.- timeout (int) – The time in milliseconds to wait for events if there are
none. If negative or
-
close
()¶ Close the file.
A closed file cannot be used for further I/O operations. close() may be called more than once without error.
-
fileno
()¶ Return the underlying file descriptor (an integer).
- inheritable (bool) – whether the inotify file descriptor will be inherited by
child processes. The default,``False``, corresponds to passing the
-
class
inotify_simple.
Event
(wd, mask, cookie, name)¶ Bases:
tuple
A
namedtuple
(wd, mask, cookie, name) for an inotify event. On Python 3 thename
field is astr
decoded withos.fsdecode()
, on Python 2 it isbytes
.Alias for field number 2
-
mask
¶ Alias for field number 1
-
name
¶ Alias for field number 3
-
wd
¶ Alias for field number 0
-
inotify_simple.
parse_events
(data)¶ Unpack data read from an inotify file descriptor into
Event
namedtuples (wd, mask, cookie, name). This function can be used if the application has read raw data from the inotify file descriptor rather than callingread()
.Parameters: data (bytes) – A bytestring as read from an inotify file descriptor. Returns: list of Event
namedtuplesReturn type: list
-
class
inotify_simple.
flags
¶ Inotify flags as defined in
inotify.h
but withIN_
prefix omitted. Includes a convenience methodfrom_mask()
for extracting flags from a mask.-
ACCESS
= 1¶ File was accessed
-
MODIFY
= 2¶ File was modified
-
ATTRIB
= 4¶ Metadata changed
-
CLOSE_WRITE
= 8¶ Writable file was closed
-
CLOSE_NOWRITE
= 16¶ Unwritable file closed
-
OPEN
= 32¶ File was opened
-
MOVED_FROM
= 64¶ File was moved from X
-
MOVED_TO
= 128¶ File was moved to Y
-
CREATE
= 256¶ Subfile was created
-
DELETE
= 512¶ Subfile was deleted
-
DELETE_SELF
= 1024¶ Self was deleted
-
MOVE_SELF
= 2048¶ Self was moved
-
UNMOUNT
= 8192¶ Backing fs was unmounted
-
Q_OVERFLOW
= 16384¶ Event queue overflowed
-
IGNORED
= 32768¶ File was ignored
-
ONLYDIR
= 16777216¶ only watch the path if it is a directory
-
DONT_FOLLOW
= 33554432¶ don’t follow a sym link
-
EXCL_UNLINK
= 67108864¶ exclude events on unlinked objects
-
MASK_ADD
= 536870912¶ add to the mask of an already existing watch
-
ISDIR
= 1073741824¶ event occurred against dir
-
ONESHOT
= 2147483648¶ only send event once
-
-
class
inotify_simple.
masks
¶ Convenience masks as defined in
inotify.h
but withIN_
prefix omitted.-
CLOSE
= 24¶ helper event mask equal to
flags.CLOSE_WRITE | flags.CLOSE_NOWRITE
-
MOVE
= 192¶ helper event mask equal to
flags.MOVED_FROM | flags.MOVED_TO
-
ALL_EVENTS
= 4095¶ bitwise-OR of all the events that can be passed to
add_watch()
-
Full source code¶
Presented here for ease of verifying that this wrapper is as sensible as it claims to be (comments stripped - see source on github to see comments).
from sys import version_info, getfilesystemencoding
import os
from enum import Enum, IntEnum
from collections import namedtuple
from struct import unpack_from, calcsize
from select import poll
from time import sleep
from ctypes import CDLL, get_errno, c_int
from ctypes.util import find_library
from errno import EINTR
from termios import FIONREAD
from fcntl import ioctl
from io import FileIO
PY2 = version_info.major < 3
if PY2:
fsencode = lambda s: s if isinstance(s, str) else s.encode(getfilesystemencoding())
IntEnum = type('IntEnum', (long, Enum), {})
else:
from os import fsencode, fsdecode
__version__ = '1.3.5'
__all__ = ['Event', 'INotify', 'flags', 'masks', 'parse_events']
_libc = None
def _libc_call(function, *args):
while True:
rc = function(*args)
if rc != -1:
return rc
errno = get_errno()
if errno != EINTR:
raise OSError(errno, os.strerror(errno))
Event = namedtuple('Event', ['wd', 'mask', 'cookie', 'name'])
_EVENT_FMT = 'iIII'
_EVENT_SIZE = calcsize(_EVENT_FMT)
class INotify(FileIO):
fd = property(FileIO.fileno)
def __init__(self, inheritable=False, nonblocking=False):
try:
libc_so = find_library('c')
except RuntimeError:
libc_so = None
global _libc; _libc = _libc or CDLL(libc_so or 'libc.so.6', use_errno=True)
O_CLOEXEC = getattr(os, 'O_CLOEXEC', 0)
flags = (not inheritable) * O_CLOEXEC | bool(nonblocking) * os.O_NONBLOCK
FileIO.__init__(self, _libc_call(_libc.inotify_init1, flags), mode='rb')
self._poller = poll()
self._poller.register(self.fileno())
def add_watch(self, path, mask):
path = str(path) if hasattr(path, 'parts') else path
return _libc_call(_libc.inotify_add_watch, self.fileno(), fsencode(path), mask)
def rm_watch(self, wd):
_libc_call(_libc.inotify_rm_watch, self.fileno(), wd)
def read(self, timeout=None, read_delay=None):
data = self._readall()
if not data and timeout != 0 and self._poller.poll(timeout):
if read_delay is not None:
sleep(read_delay / 1000.0)
data = self._readall()
return parse_events(data)
def _readall(self):
bytes_avail = c_int()
ioctl(self, FIONREAD, bytes_avail)
if not bytes_avail.value:
return b''
return os.read(self.fileno(), bytes_avail.value)
def parse_events(data):
pos = 0
events = []
while pos < len(data):
wd, mask, cookie, namesize = unpack_from(_EVENT_FMT, data, pos)
pos += _EVENT_SIZE + namesize
name = data[pos - namesize : pos].split(b'\x00', 1)[0]
events.append(Event(wd, mask, cookie, name if PY2 else fsdecode(name)))
return events
class flags(IntEnum):
ACCESS = 0x00000001
MODIFY = 0x00000002
ATTRIB = 0x00000004
CLOSE_WRITE = 0x00000008
CLOSE_NOWRITE = 0x00000010
OPEN = 0x00000020
MOVED_FROM = 0x00000040
MOVED_TO = 0x00000080
CREATE = 0x00000100
DELETE = 0x00000200
DELETE_SELF = 0x00000400
MOVE_SELF = 0x00000800
UNMOUNT = 0x00002000
Q_OVERFLOW = 0x00004000
IGNORED = 0x00008000
ONLYDIR = 0x01000000
DONT_FOLLOW = 0x02000000
EXCL_UNLINK = 0x04000000
MASK_ADD = 0x20000000
ISDIR = 0x40000000
ONESHOT = 0x80000000
@classmethod
def from_mask(cls, mask):
return [flag for flag in cls.__members__.values() if flag & mask]
class masks(IntEnum):
CLOSE = flags.CLOSE_WRITE | flags.CLOSE_NOWRITE
MOVE = flags.MOVED_FROM | flags.MOVED_TO
ALL_EVENTS = (flags.ACCESS | flags.MODIFY | flags.ATTRIB | flags.CLOSE_WRITE |
flags.CLOSE_NOWRITE | flags.OPEN | flags.MOVED_FROM | flags.MOVED_TO |
flags.CREATE | flags.DELETE| flags.DELETE_SELF | flags.MOVE_SELF)