panzi_inotify

Source: GitHub

Example

from panzi_inotify import Inotify, get_inotify_event_names

import sys

with Inotify() as inotify:
    for filename in sys.argv[1:]:
        inotify.add_watch(filename)

    for event in inotify:
        print(f'{event.full_path()}: {", ".join(get_inotify_event_names(event.mask))}')

See examples for more.

You can also run a basic command line too to listen for events on a set of paths like this:

python -m panzi_inotify [--mask=MASK] <path>...

For more on this see:

python -m panzi_inotify --help

See Also

inotify(7)

  1# This Source Code Form is subject to the terms of the Mozilla Public
  2# License, v. 2.0. If a copy of the MPL was not distributed with this
  3# file, You can obtain one at https://mozilla.org/MPL/2.0/.
  4
  5"""
  6**Source: [GitHub](https://github.com/panzi/panzi-inotify/)**
  7
  8### Example
  9
 10```Python
 11from panzi_inotify import Inotify, get_inotify_event_names
 12
 13import sys
 14
 15with Inotify() as inotify:
 16    for filename in sys.argv[1:]:
 17        inotify.add_watch(filename)
 18
 19    for event in inotify:
 20        print(f'{event.full_path()}: {", ".join(get_inotify_event_names(event.mask))}')
 21```
 22
 23See [examples](https://github.com/panzi/panzi-inotify/tree/main/examples) for more.
 24
 25You can also run a basic command line too to listen for events on a set of paths
 26like this:
 27
 28```bash
 29python -m panzi_inotify [--mask=MASK] <path>...
 30```
 31
 32For more on this see:
 33
 34```bash
 35python -m panzi_inotify --help
 36```
 37
 38### See Also
 39
 40[inotify(7)](https://linux.die.net/man/7/inotify)
 41"""
 42
 43from typing import Optional, NamedTuple, Callable, Any, Self, Mapping, Final
 44
 45import os
 46import select
 47import ctypes
 48import ctypes.util
 49import logging
 50
 51from os import fsencode, fsdecode, readinto
 52from os.path import join as join_path
 53from struct import Struct
 54from errno import (
 55    EINTR, ENOENT, EEXIST, ENOTDIR, EISDIR,
 56    EACCES, EAGAIN, EALREADY, EWOULDBLOCK, EINPROGRESS,
 57    ECHILD, EPERM, ETIMEDOUT, EPIPE, ECONNABORTED,
 58    ECONNREFUSED, ECONNRESET, ENOSYS,
 59)
 60
 61_logger = logging.getLogger(__name__)
 62
 63_LIBC_PATH: Optional[str] = None
 64
 65try:
 66    # ctypes.util.find_library() runs multiple external programs (ldconfig, ld,
 67    # gcc etc.) to find the library! So try to use dlopen(NULL) first.
 68    _LIBC: Optional[ctypes.CDLL] = ctypes.CDLL(None, use_errno=True)
 69
 70    if not hasattr(_LIBC, 'inotify_init1'):
 71        _LIBC_PATH = ctypes.util.find_library('c') or 'libc.so.6'
 72        _LIBC = ctypes.CDLL(_LIBC_PATH, use_errno=True)
 73
 74except OSError as exc:
 75    _LIBC = None
 76    if _LIBC_PATH is None:
 77        _logger.debug('Loading C library: %s', exc, exc_info=exc)
 78    else:
 79        _logger.debug('Loading %r: %s', _LIBC_PATH, exc, exc_info=exc)
 80
 81__all__ = (
 82    'InotifyEvent',
 83    'Inotify',
 84    'PollInotify',
 85    'TerminalEventException',
 86    'get_inotify_event_names',
 87    'HAS_INOTIFY',
 88    'IN_CLOEXEC',
 89    'IN_NONBLOCK',
 90    'IN_ACCESS',
 91    'IN_MODIFY',
 92    'IN_ATTRIB',
 93    'IN_CLOSE_WRITE',
 94    'IN_CLOSE_NOWRITE',
 95    'IN_OPEN',
 96    'IN_MOVED_FROM',
 97    'IN_MOVED_TO',
 98    'IN_CREATE',
 99    'IN_DELETE',
100    'IN_DELETE_SELF',
101    'IN_MOVE_SELF',
102    'IN_UNMOUNT',
103    'IN_Q_OVERFLOW',
104    'IN_IGNORED',
105    'IN_CLOSE',
106    'IN_MOVE',
107    'IN_ONLYDIR',
108    'IN_DONT_FOLLOW',
109    'IN_EXCL_UNLINK',
110    'IN_MASK_CREATE',
111    'IN_MASK_ADD',
112    'IN_ISDIR',
113    'IN_ONESHOT',
114    'IN_ALL_EVENTS',
115    'INOTIFY_MASK_CODES',
116)
117
118_HEADER_STRUCT: Final[Struct] = Struct('iIII')
119_HEADER_STRUCT_unpack_from: Final = _HEADER_STRUCT.unpack_from
120_HEADER_STRUCT_size: Final[int] = _HEADER_STRUCT.size
121
122# sizeof(struct inotify_event) + NAME_MAX + 1
123_EVENT_SIZE_MAX: Final[int] = _HEADER_STRUCT_size + 256
124
125# From linux/inotify.h
126
127# Flags for sys_inotify_init1
128
129IN_CLOEXEC : Final[int] = os.O_CLOEXEC ; "Close inotify file descriptor on exec.\n\n**See Also:** `Inotify.__init__()`"
130IN_NONBLOCK: Final[int] = os.O_NONBLOCK; "Open inotify file descriptor as non-blocking.\n\n**See Also:** `Inotify.__init__()`"
131
132# the following are legal, implemented events that user-space can watch for
133IN_ACCESS       : Final[int] = 0x00000001; "File was accessed.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`"
134IN_MODIFY       : Final[int] = 0x00000002; "File was modified.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`"
135IN_ATTRIB       : Final[int] = 0x00000004; "Metadata changed.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`"
136IN_CLOSE_WRITE  : Final[int] = 0x00000008; "Writtable file was closed.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`"
137IN_CLOSE_NOWRITE: Final[int] = 0x00000010; "Unwrittable file closed.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`"
138IN_OPEN         : Final[int] = 0x00000020; "File was opened.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`"
139IN_MOVED_FROM   : Final[int] = 0x00000040; "File was moved from X.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`"
140IN_MOVED_TO     : Final[int] = 0x00000080; "File was moved to Y.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`"
141IN_CREATE       : Final[int] = 0x00000100; "Subfile was created.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`"
142IN_DELETE       : Final[int] = 0x00000200; "Subfile was deleted.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`"
143IN_DELETE_SELF  : Final[int] = 0x00000400; "Self was deleted.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`"
144IN_MOVE_SELF    : Final[int] = 0x00000800; "Self was moved.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`"
145
146# the following are legal events.  they are sent as needed to any watch
147IN_UNMOUNT   : Final[int] = 0x00002000; "Backing file system was unmounted.\n\n**See Also:** `InotifyEvent.mask`"
148IN_Q_OVERFLOW: Final[int] = 0x00004000; "Event queued overflowed.\n\n**See Also:** `InotifyEvent.mask`"
149IN_IGNORED   : Final[int] = 0x00008000; "File was ignored.\n\n**See Also:** `InotifyEvent.mask`"
150
151# helper events
152IN_CLOSE: Final[int] = IN_CLOSE_WRITE | IN_CLOSE_NOWRITE; "All close events.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`"
153IN_MOVE : Final[int] = IN_MOVED_FROM  | IN_MOVED_TO     ; "All move events.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`"
154
155# special flags
156IN_ONLYDIR    : Final[int] = 0x01000000; "Only watch the path if it is a directory.\n\n**See Also:** `Inotify.add_watch()`"
157IN_DONT_FOLLOW: Final[int] = 0x02000000; "Don't follow symbolic links.\n\n**See Also:** `Inotify.add_watch()`"
158IN_EXCL_UNLINK: Final[int] = 0x04000000; "Exclude events on unlinked objects.\n\n**See Also:** `Inotify.add_watch()`"
159IN_MASK_CREATE: Final[int] = 0x10000000; "Only create watches.\n\n**See Also:** `Inotify.add_watch()`"
160IN_MASK_ADD   : Final[int] = 0x20000000; "Add to the mask of an already existing watch.\n\n**See Also:** `Inotify.add_watch()`"
161IN_ISDIR      : Final[int] = 0x40000000; "Event occurred against directory.\n\n**See Also:** `InotifyEvent.mask`"
162IN_ONESHOT    : Final[int] = 0x80000000; "Only send event once.\n\n**See Also:** `Inotify.add_watch()`"
163
164IN_ALL_EVENTS: Final[int] = (
165    IN_ACCESS | IN_MODIFY | IN_ATTRIB | IN_CLOSE_WRITE |
166    IN_CLOSE_NOWRITE | IN_OPEN | IN_MOVED_FROM |
167    IN_MOVED_TO | IN_DELETE | IN_CREATE | IN_DELETE_SELF |
168    IN_MOVE_SELF
169); "All of the events.\n\n**See Also:** `Inotify.add_watch()`, `InotifyEvent.mask`"
170
171INOTIFY_MASK_CODES: Final[Mapping[int, str]] = {
172    globals()[_key]: _key.removeprefix('IN_')
173    for _key in (
174        'IN_ACCESS',
175        'IN_MODIFY',
176        'IN_ATTRIB',
177        'IN_CLOSE_WRITE',
178        'IN_CLOSE_NOWRITE',
179        'IN_OPEN',
180        'IN_MOVED_FROM',
181        'IN_MOVED_TO',
182        'IN_CREATE',
183        'IN_DELETE',
184        'IN_DELETE_SELF',
185        'IN_MOVE_SELF',
186
187        'IN_UNMOUNT',
188        'IN_Q_OVERFLOW',
189        'IN_IGNORED',
190
191        'IN_ISDIR',
192    )
193}; "Mapping from inotify event mask flag to it's name.\n\n**See Also:** `InotifyEvent.mask`"
194
195def get_inotify_event_names(mask: int) -> list[str]:
196    """
197    Get a list of event names from an event mask as returned by inotify.
198
199    If there is a flag set that isn't a know name the hexadecimal representation
200    of that flag is also returned.
201    """
202    names: list[str] = []
203    for code, name in INOTIFY_MASK_CODES.items():
204        if mask & code:
205            names.append(name)
206            mask &= ~code
207            if not mask:
208                break
209
210    if mask:
211        for bit in range(0, 32):
212            if bit & mask:
213                names.append('0x%x' % bit)
214                mask &= ~bit
215                if not mask:
216                    break
217
218    return names
219
220def _check_return(value: int, filename: Optional[str] = None) -> int:
221    if value < 0:
222        errnum = ctypes.get_errno()
223        message = os.strerror(errnum)
224
225        if errnum == ENOENT:
226            raise FileNotFoundError(errnum, message, filename)
227
228        if errnum in (EACCES, EPERM):
229            raise PermissionError(errnum, message, filename)
230
231        if errnum == EINTR:
232            raise InterruptedError(errnum, message, filename)
233
234        if errnum == ENOTDIR:
235            raise NotADirectoryError(errnum, message, filename)
236
237        if errnum == EEXIST:
238            raise FileExistsError(errnum, message, filename)
239
240        if errnum == EISDIR:
241            raise IsADirectoryError(errnum, message, filename)
242
243        if errnum in (EAGAIN, EALREADY, EWOULDBLOCK, EINPROGRESS):
244            raise BlockingIOError(errnum, message, filename)
245
246        if errnum == ETIMEDOUT:
247            raise TimeoutError(errnum, message, filename)
248
249        if errnum == ECHILD:
250            raise ChildProcessError(errnum, message, filename)
251
252        if errnum == EPIPE:
253            raise BrokenPipeError(errnum, message, filename)
254
255        if errnum == ECONNABORTED:
256            raise ConnectionAbortedError(errnum, message, filename)
257
258        if errnum == ECONNREFUSED:
259            raise ConnectionRefusedError(errnum, message, filename)
260
261        if errnum == ECONNRESET:
262            raise ConnectionResetError(errnum, message, filename)
263
264        raise OSError(errnum, message, filename)
265
266    return value
267
268HAS_INOTIFY = True; "`True` if your libc exports `inotify_init1`, `inotify_add_watch`, and `inotify_rm_watch`, otherwise `False`."
269
270_CDataType = type[ctypes.c_int]|type[ctypes.c_char_p]|type[ctypes.c_uint32]
271
272def _load_sym(name: str, argtypes: tuple[_CDataType, ...], restype: _CDataType|Callable[[int], Any]) -> Callable:
273    global HAS_INOTIFY
274
275    if sym := getattr(_LIBC, name, None):
276        sym.argtypes = argtypes
277        sym.restype = restype
278        return sym
279
280    else:
281        HAS_INOTIFY = False
282
283        def symbol_not_found(*args, **kwargs):
284            raise OSError(ENOSYS, f'{name} is not supported')
285        symbol_not_found.__name__ = name
286
287        return symbol_not_found
288
289inotify_init1 = _load_sym('inotify_init1', (ctypes.c_int,), _check_return)
290inotify_add_watch = _load_sym('inotify_add_watch', (
291    ctypes.c_int,
292    ctypes.c_char_p,
293    ctypes.c_uint32,
294), ctypes.c_int)
295inotify_rm_watch = _load_sym('inotify_rm_watch', (ctypes.c_int, ctypes.c_int), ctypes.c_int)
296
297class TerminalEventException(Exception):
298    """
299    Exception raised by `Inotify.read_events()` when an event mask contains one
300    of the specified `terminal_events`.
301    """
302
303    __slots__ = (
304        'wd',
305        'mask',
306        'watch_path',
307        'filename',
308    )
309
310    wd: int; "Inotify watch descriptor."
311    mask: int; "Bitset of the events that occured."
312    watch_path: Optional[str]; "Path of the watched file, `None` if the Python code doesn't know about the watch."
313    filename: Optional[str]; "If the event is about the child of a watched directory, this is the name of that file, otherwise `None`."
314
315    def __init__(self, wd: int, mask: int, watch_path: Optional[str], filename: Optional[str]) -> None:
316        super().__init__(wd, mask, watch_path, filename)
317        self.wd = wd
318        self.mask = mask
319        self.watch_path = watch_path
320        self.filename = filename
321
322class InotifyEvent(NamedTuple):
323    """
324    Inotify event as read from the inotify file handle.
325    """
326    wd: int; "Inotify watch descriptor."
327    mask: int; """
328        Bit set of the events that occured and other information.
329
330        The flags can be:
331        - `IN_ACCESS` - File was accessed.
332        - `IN_MODIFY` - File was modified.
333        - `IN_ATTRIB` - Metadata changed.
334        - `IN_CLOSE_WRITE` - Writtable file was closed.
335        - `IN_CLOSE_NOWRITE` - Unwrittable file closed.
336        - `IN_OPEN` - File was opened.
337        - `IN_MOVED_FROM` - File was moved from X.
338        - `IN_MOVED_TO` - File was moved to Y.
339        - `IN_CREATE` - Subfile was created.
340        - `IN_DELETE` - Subfile was deleted.
341        - `IN_DELETE_SELF` - Self was deleted.
342        - `IN_MOVE_SELF` - Self was moved.
343        - `IN_UNMOUNT` - Backing file system was unmounted.
344        - `IN_Q_OVERFLOW` - Event queued overflowed.
345        - `IN_IGNORED` - File was ignored.
346        - `IN_ISDIR` - Event occurred against directory.
347    """
348    cookie: int; """\
349        Unique cookie associating related events (for
350        [rename(2)](https://linux.die.net/man/2/rename)).
351    """
352    filename_len: int; "Original byte-size of the filename field."
353    watch_path: str; """\
354        Path of the watched file as it was registered.
355
356        **NOTE:** If the watched file or directory itself is moved/renamed
357        `watch_path` for any further events will *still* be the orignial path
358        that was registered. This is because it is not possible to determine
359        the new file name in a `IN_MOVE_SELF` event and thus this cannot be
360        updated.
361    """
362    filename: Optional[str]; """\
363        If the event is about the child of a watched directory, this is the name
364        of that file, otherwise `None`.
365    """
366
367    def full_path(self) -> str:
368        """
369        Join `watch_path` and `filename`, or only `watch_path` if `filename` is `None`.
370        """
371        filename = self.filename
372        return join_path(self.watch_path, filename) if filename is not None else self.watch_path
373
374class Inotify:
375    """
376    Listen for inotify events.
377
378    Supports the context manager and iterator protocols.
379    """
380    __slots__ = (
381        '_inotify_fd',
382        '_buffer',
383        '_offset',
384        '_size',
385        '_path_to_wd',
386        '_wd_to_path',
387    )
388
389    _inotify_fd: int
390    _buffer: bytearray
391    _offset: int
392    _size: int
393    _path_to_wd: dict[str, int]
394    _wd_to_path: dict[int, str]
395
396    def __init__(self, flags: int = IN_CLOEXEC, buffer_size: int = 4096) -> None:
397        """
398        `flags` is a bit set of these values:
399        - `IN_CLOEXEC` - Close inotify file descriptor on exec.
400        - `IN_NONBLOCK` - Open inotify file descriptor as non-blocking.
401
402        It's recommended to pass the `IN_CLOEXEC` flag (default) to close the
403        file descriptor on [exec*(2)](https://linux.die.net/man/3/execl).
404
405        `buffer_size` is the size of the used input buffer and needs to be at
406        least 272 (`sizeof(struct inotify_event) + NAME_MAX + 1`).
407
408        This calls [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1)
409        and thus might raise an `OSError` with one of these `errno` values:
410        - `EINVAL`
411        - `EMFILE`
412        - `ENOMEM`
413        - `ENOSYS` if your libc doesn't support [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1)
414        """
415        self._inotify_fd = -1
416        self._offset = 0
417        self._size = 0
418        self._path_to_wd = {}
419        self._wd_to_path = {}
420
421        if buffer_size < _EVENT_SIZE_MAX:
422            raise ValueError(f'buffer_size too small: {buffer_size} < {_EVENT_SIZE_MAX}')
423
424        self._buffer = bytearray(buffer_size)
425        self._inotify_fd = inotify_init1(flags)
426
427    def fileno(self) -> int:
428        """
429        The inotify file descriptor.
430
431        You can use this to call `poll()` or similar yourself
432        instead of using `PollInotify.wait()`.
433        """
434        return self._inotify_fd
435
436    @property
437    def closed(self) -> bool:
438        """
439        `True` if the inotify file descriptor was closed.
440        """
441        return self._inotify_fd == -1
442
443    def close(self) -> None:
444        """
445        Close the inotify handle.
446
447        Can safely be called multiple times, but you
448        can't call any other methods once closed.
449        """
450        try:
451            if self._inotify_fd != -1:
452                # A crash during inotify_init1() in __init__() means
453                # self._inotify_stream is not assigned, but self._inotify_fd
454                # is initialized with -1.
455                # Since this method is called in __del__() this state needs
456                # be handled.
457                os.close(self._inotify_fd)
458        finally:
459            self._inotify_fd = -1
460
461    def __enter__(self) -> Self:
462        return self
463
464    def __exit__(self, exc_type, exc_value, traceback) -> None:
465        self.close()
466
467    def __del__(self) -> None:
468        self.close()
469
470    def add_watch(self, path: str, mask: int = IN_ALL_EVENTS) -> int:
471        """
472        Add a watch path.
473
474        `mask` is a bit set of these event flags:
475        - `IN_ACCESS` - File was accessed.
476        - `IN_MODIFY` - File was modified.
477        - `IN_ATTRIB` - Metadata changed.
478        - `IN_CLOSE_WRITE` - Writtable file was closed.
479        - `IN_CLOSE_NOWRITE` - Unwrittable file closed.
480        - `IN_OPEN` - File was opened.
481        - `IN_MOVED_FROM` - File was moved from X.
482        - `IN_MOVED_TO` - File was moved to Y.
483        - `IN_CREATE` - Subfile was created.
484        - `IN_DELETE` - Subfile was deleted.
485        - `IN_DELETE_SELF` - Self was deleted.
486        - `IN_MOVE_SELF` - Self was moved.
487
488        And these additional flags:
489        - `IN_ONLYDIR` - Only watch the path if it is a directory.
490        - `IN_DONT_FOLLOW` - Don't follow symbolic links.
491        - `IN_EXCL_UNLINK` - Exclude events on unlinked objects.
492        - `IN_MASK_CREATE` - Only create watches.
493        - `IN_MASK_ADD` - Add to the mask of an already existing watch.
494        - `IN_ONESHOT` - Only send event once.
495
496        This calls [inotify_add_watch(2)](https://linux.die.net/man/2/inotify_add_watch)
497        and thus might raise one of these exceptions:
498        - `PermissionError` (`EACCES`)
499        - `FileExistsError` (`EEXISTS`)
500        - `FileNotFoundError` (`ENOENT`)
501        - `NotADirectoryError` (`ENOTDIR`)
502        - `OSError` (`WBADF`, `EFAULT`, `EINVAL`, `ENAMETOOLONG`,
503          `ENOMEM`, `ENOSPC`, `ENOSYS` if your libc doesn't support
504          `inotify_rm_watch()`)
505        """
506        path_bytes = fsencode(path)
507
508        wd = inotify_add_watch(self._inotify_fd, path_bytes, mask)
509        _check_return(wd, path)
510
511        self._path_to_wd[path] = wd
512        self._wd_to_path[wd] = path
513
514        return wd
515
516    def remove_watch(self, path: str) -> None:
517        """
518        Remove watch by path.
519
520        Does nothing if the path is not watched.
521
522        This calls [inotify_rm_watch(2)](https://linux.die.net/man/2/inotify_rm_watch)
523        and this might raise an `OSError` with one of these `errno` values:
524        - `EBADF`
525        - `EINVAL`
526        - `ENOSYS` if your libc doesn't support [inotify_rm_watch(2)](https://linux.die.net/man/2/inotify_rm_watch)
527        """
528        wd = self._path_to_wd.get(path)
529        if wd is None:
530            _logger.debug('%s: Path is not watched', path)
531            return
532
533        try:
534            res = inotify_rm_watch(self._inotify_fd, wd)
535            _check_return(res, path)
536        finally:
537            del self._path_to_wd[path]
538            del self._wd_to_path[wd]
539
540    def remove_watch_with_id(self, wd: int) -> None:
541        """
542        Remove watch by handle.
543
544        Does nothing if the handle is invalid.
545
546        This calls `inotify_rm_watch()` and this might raise an
547        `OSError` with one of these `errno` values:
548        - `EBADF`
549        - `EINVAL`
550        - `ENOSYS` if your libc doesn't support `inotify_rm_watch()`
551        """
552        path = self._wd_to_path.get(wd)
553        if path is None:
554            _logger.debug('%d: Invalid handle', wd)
555            return
556
557        try:
558            res = inotify_rm_watch(self._inotify_fd, wd)
559            _check_return(res, path)
560        finally:
561            del self._wd_to_path[wd]
562            del self._path_to_wd[path]
563
564    def watch_paths(self) -> set[str]:
565        """
566        Get the set of the watched paths.
567        """
568        return set(self._path_to_wd)
569
570    def get_watch_id(self, path: str) -> Optional[int]:
571        """
572        Get the watch id to a path, if the path is watched.
573        """
574        return self._path_to_wd.get(path)
575
576    def get_watch_path(self, wd: int) -> Optional[str]:
577        """
578        Get the path to a watch id, if the watch id is valid.
579        """
580        return self._wd_to_path.get(wd)
581
582    def read_event(self) -> Optional[InotifyEvent]:
583        """
584        Read a single event. Might return `None` if there is none avaialbe.
585        """
586        buffer = self._buffer
587        offset = self._offset
588        wd_to_path = self._wd_to_path
589
590        while True:
591            if offset >= self._size:
592                # Only whole records are ever returned by a read() on an inotify file descriptor.
593                # If the file descriptor is blocking a read() only blocks until the first event
594                # is available.
595                self._offset = offset = 0
596                try:
597                    self._size = readinto(self._inotify_fd, buffer)
598                except BlockingIOError:
599                    self._size = 0
600                    return None
601
602                if self._size == 0:
603                    return None
604
605            wd, mask, cookie, filename_len = _HEADER_STRUCT_unpack_from(buffer, offset)
606            offset += _HEADER_STRUCT_size
607            self._offset = filename_end_offset = offset + filename_len
608
609            if filename_len:
610                filename_bytes = buffer[offset:filename_end_offset]
611                index = filename_bytes.find(b'\0')
612                # I don't like the need for bytes() here, fsdecode() doesn't take a bytearray and
613                # you can't get a bytes slice in one step from a bytearray.
614                # I guess I could do this instead?
615                # filename = (filename_bytes[:index] if index >= 0 else filename_bytes).decode(sys.getfilesystemencoding(), errors='surrogateescape')
616                filename = fsdecode(bytes(filename_bytes[:index] if index >= 0 else filename_bytes))
617            else:
618                filename = None
619
620            watch_path = wd_to_path.get(wd)
621
622            if mask & IN_IGNORED and watch_path is not None:
623                wd_to_path.pop(wd, None)
624                self._path_to_wd.pop(watch_path, None)
625
626            offset = filename_end_offset
627
628            if watch_path is None:
629                _logger.debug('Got inotify event for unknown watch handle: %d, mask: %d, cookie: %d', wd, mask, cookie)
630                continue
631
632            return InotifyEvent(wd, mask, cookie, filename_len, watch_path, filename)
633
634    def read_events(self, terminal_events: int = IN_Q_OVERFLOW | IN_UNMOUNT) -> list[InotifyEvent]:
635        """
636        Read available events. Might return an empty list if there are none
637        available.
638
639        `terminal_events` is per default: `IN_Q_OVERFLOW` | `IN_UNMOUNT`
640
641        **NOTE:** Don't use this in blocking mode! It will never return.
642
643        Raises `TerminalEventException` if any of the flags in `terminal_events`
644        are set in an event `mask`.
645        """
646        events: list[InotifyEvent] = []
647
648        while event := self.read_event():
649            if event.mask & terminal_events:
650                raise TerminalEventException(event.wd, event.mask, event.watch_path, event.filename)
651
652            events.append(event)
653
654        return events
655
656    def __iter__(self) -> Self:
657        """
658        `Inotify` can act as an iterator over the available events.
659        """
660        return self
661
662    def __next__(self) -> InotifyEvent:
663        """
664        `Inotify` can act as an iterator over the available events.
665        """
666        event = self.read_event()
667        if event is None:
668            raise StopIteration
669        return event
670
671class PollInotify(Inotify):
672    """
673    Listen for inotify events.
674
675    In addition to the functionality of `Inotify` this class adds a `wait()`
676    method that waits for events using `epoll`. If you use `Inotify` you
677    need/can to do that yourself.
678
679    Supports the context manager and iterator protocols.
680    """
681    __slots__ = (
682        '_epoll',
683        '_stopfd',
684    )
685
686    _epoll: select.epoll
687    _stopfd: Optional[int]
688
689    def __init__(self, stopfd: Optional[int] = None, buffer_size: int = 4096) -> None:
690        """
691        If not `None` then `stopfd` is a file descriptor that will
692        be added to the `poll()` call in `PollInotify.wait()`.
693
694        `buffer_size` is the size of the used input buffer and needs to be at
695        least 272 (`sizeof(struct inotify_event) + NAME_MAX + 1`).
696
697        This calls [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1)
698        and thus might raise an `OSError` with one of these `errno` values:
699        - `EINVAL` (shouldn't happen)
700        - `EMFILE`
701        - `ENOMEM`
702        - `ENOSYS` if your libc doesn't support [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1)
703        """
704        super().__init__(IN_NONBLOCK | IN_CLOEXEC, buffer_size)
705        self._stopfd = stopfd
706        self._epoll = select.epoll(1 if stopfd is None else 2)
707        self._epoll.register(self._inotify_fd, select.POLLIN)
708
709        if stopfd is not None:
710            self._epoll.register(stopfd, select.POLLIN)
711
712    @property
713    def stopfd(self) -> Optional[int]:
714        """
715        The `stopfd` parameter of `__init__()`, used in `wait()`.
716        """
717        return self._stopfd
718
719    def close(self) -> None:
720        """
721        Close the inotify and epoll handles.
722
723        Can safely be called multiple times, but you
724        can't call any other methods once closed.
725        """
726        try:
727            super().close()
728        finally:
729            epoll = self._epoll
730            if not epoll.closed:
731                epoll.close()
732
733    def wait(self, timeout: Optional[float] = None) -> bool:
734        """
735        Wait for inotify events or for any `POLLIN` on `stopfd`,
736        if that is not `None`. If `stopfd` signals this function will
737        return `False`, otherwise `True`.
738
739        Raises `TimeoutError` if `timeout` is not `None` and
740        the operation has expired.
741
742        This method uses using `select.epoll.poll()`, see there for
743        additional possible exceptions.
744        """
745        events = self._epoll.poll(timeout)
746
747        if not events and timeout is not None and timeout >= 0.0:
748            raise TimeoutError
749
750        stopfd = self._stopfd
751        if stopfd is not None:
752            for fd, mask in events:
753                if fd == stopfd:
754                    return False
755
756        return True
class InotifyEvent(typing.NamedTuple):
323class InotifyEvent(NamedTuple):
324    """
325    Inotify event as read from the inotify file handle.
326    """
327    wd: int; "Inotify watch descriptor."
328    mask: int; """
329        Bit set of the events that occured and other information.
330
331        The flags can be:
332        - `IN_ACCESS` - File was accessed.
333        - `IN_MODIFY` - File was modified.
334        - `IN_ATTRIB` - Metadata changed.
335        - `IN_CLOSE_WRITE` - Writtable file was closed.
336        - `IN_CLOSE_NOWRITE` - Unwrittable file closed.
337        - `IN_OPEN` - File was opened.
338        - `IN_MOVED_FROM` - File was moved from X.
339        - `IN_MOVED_TO` - File was moved to Y.
340        - `IN_CREATE` - Subfile was created.
341        - `IN_DELETE` - Subfile was deleted.
342        - `IN_DELETE_SELF` - Self was deleted.
343        - `IN_MOVE_SELF` - Self was moved.
344        - `IN_UNMOUNT` - Backing file system was unmounted.
345        - `IN_Q_OVERFLOW` - Event queued overflowed.
346        - `IN_IGNORED` - File was ignored.
347        - `IN_ISDIR` - Event occurred against directory.
348    """
349    cookie: int; """\
350        Unique cookie associating related events (for
351        [rename(2)](https://linux.die.net/man/2/rename)).
352    """
353    filename_len: int; "Original byte-size of the filename field."
354    watch_path: str; """\
355        Path of the watched file as it was registered.
356
357        **NOTE:** If the watched file or directory itself is moved/renamed
358        `watch_path` for any further events will *still* be the orignial path
359        that was registered. This is because it is not possible to determine
360        the new file name in a `IN_MOVE_SELF` event and thus this cannot be
361        updated.
362    """
363    filename: Optional[str]; """\
364        If the event is about the child of a watched directory, this is the name
365        of that file, otherwise `None`.
366    """
367
368    def full_path(self) -> str:
369        """
370        Join `watch_path` and `filename`, or only `watch_path` if `filename` is `None`.
371        """
372        filename = self.filename
373        return join_path(self.watch_path, filename) if filename is not None else self.watch_path

Inotify event as read from the inotify file handle.

InotifyEvent( wd: int, mask: int, cookie: int, filename_len: int, watch_path: str, filename: str | None)

Create new instance of InotifyEvent(wd, mask, cookie, filename_len, watch_path, filename)

wd: int

Inotify watch descriptor.

mask: int

Bit set of the events that occured and other information.

The flags can be:

cookie: int

Unique cookie associating related events (for rename(2)).

filename_len: int

Original byte-size of the filename field.

watch_path: str

Path of the watched file as it was registered.

NOTE: If the watched file or directory itself is moved/renamed watch_path for any further events will still be the orignial path that was registered. This is because it is not possible to determine the new file name in a IN_MOVE_SELF event and thus this cannot be updated.

filename: str | None

If the event is about the child of a watched directory, this is the name of that file, otherwise None.

def full_path(self) -> str:
368    def full_path(self) -> str:
369        """
370        Join `watch_path` and `filename`, or only `watch_path` if `filename` is `None`.
371        """
372        filename = self.filename
373        return join_path(self.watch_path, filename) if filename is not None else self.watch_path

Join watch_path and filename, or only watch_path if filename is None.

class Inotify:
375class Inotify:
376    """
377    Listen for inotify events.
378
379    Supports the context manager and iterator protocols.
380    """
381    __slots__ = (
382        '_inotify_fd',
383        '_buffer',
384        '_offset',
385        '_size',
386        '_path_to_wd',
387        '_wd_to_path',
388    )
389
390    _inotify_fd: int
391    _buffer: bytearray
392    _offset: int
393    _size: int
394    _path_to_wd: dict[str, int]
395    _wd_to_path: dict[int, str]
396
397    def __init__(self, flags: int = IN_CLOEXEC, buffer_size: int = 4096) -> None:
398        """
399        `flags` is a bit set of these values:
400        - `IN_CLOEXEC` - Close inotify file descriptor on exec.
401        - `IN_NONBLOCK` - Open inotify file descriptor as non-blocking.
402
403        It's recommended to pass the `IN_CLOEXEC` flag (default) to close the
404        file descriptor on [exec*(2)](https://linux.die.net/man/3/execl).
405
406        `buffer_size` is the size of the used input buffer and needs to be at
407        least 272 (`sizeof(struct inotify_event) + NAME_MAX + 1`).
408
409        This calls [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1)
410        and thus might raise an `OSError` with one of these `errno` values:
411        - `EINVAL`
412        - `EMFILE`
413        - `ENOMEM`
414        - `ENOSYS` if your libc doesn't support [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1)
415        """
416        self._inotify_fd = -1
417        self._offset = 0
418        self._size = 0
419        self._path_to_wd = {}
420        self._wd_to_path = {}
421
422        if buffer_size < _EVENT_SIZE_MAX:
423            raise ValueError(f'buffer_size too small: {buffer_size} < {_EVENT_SIZE_MAX}')
424
425        self._buffer = bytearray(buffer_size)
426        self._inotify_fd = inotify_init1(flags)
427
428    def fileno(self) -> int:
429        """
430        The inotify file descriptor.
431
432        You can use this to call `poll()` or similar yourself
433        instead of using `PollInotify.wait()`.
434        """
435        return self._inotify_fd
436
437    @property
438    def closed(self) -> bool:
439        """
440        `True` if the inotify file descriptor was closed.
441        """
442        return self._inotify_fd == -1
443
444    def close(self) -> None:
445        """
446        Close the inotify handle.
447
448        Can safely be called multiple times, but you
449        can't call any other methods once closed.
450        """
451        try:
452            if self._inotify_fd != -1:
453                # A crash during inotify_init1() in __init__() means
454                # self._inotify_stream is not assigned, but self._inotify_fd
455                # is initialized with -1.
456                # Since this method is called in __del__() this state needs
457                # be handled.
458                os.close(self._inotify_fd)
459        finally:
460            self._inotify_fd = -1
461
462    def __enter__(self) -> Self:
463        return self
464
465    def __exit__(self, exc_type, exc_value, traceback) -> None:
466        self.close()
467
468    def __del__(self) -> None:
469        self.close()
470
471    def add_watch(self, path: str, mask: int = IN_ALL_EVENTS) -> int:
472        """
473        Add a watch path.
474
475        `mask` is a bit set of these event flags:
476        - `IN_ACCESS` - File was accessed.
477        - `IN_MODIFY` - File was modified.
478        - `IN_ATTRIB` - Metadata changed.
479        - `IN_CLOSE_WRITE` - Writtable file was closed.
480        - `IN_CLOSE_NOWRITE` - Unwrittable file closed.
481        - `IN_OPEN` - File was opened.
482        - `IN_MOVED_FROM` - File was moved from X.
483        - `IN_MOVED_TO` - File was moved to Y.
484        - `IN_CREATE` - Subfile was created.
485        - `IN_DELETE` - Subfile was deleted.
486        - `IN_DELETE_SELF` - Self was deleted.
487        - `IN_MOVE_SELF` - Self was moved.
488
489        And these additional flags:
490        - `IN_ONLYDIR` - Only watch the path if it is a directory.
491        - `IN_DONT_FOLLOW` - Don't follow symbolic links.
492        - `IN_EXCL_UNLINK` - Exclude events on unlinked objects.
493        - `IN_MASK_CREATE` - Only create watches.
494        - `IN_MASK_ADD` - Add to the mask of an already existing watch.
495        - `IN_ONESHOT` - Only send event once.
496
497        This calls [inotify_add_watch(2)](https://linux.die.net/man/2/inotify_add_watch)
498        and thus might raise one of these exceptions:
499        - `PermissionError` (`EACCES`)
500        - `FileExistsError` (`EEXISTS`)
501        - `FileNotFoundError` (`ENOENT`)
502        - `NotADirectoryError` (`ENOTDIR`)
503        - `OSError` (`WBADF`, `EFAULT`, `EINVAL`, `ENAMETOOLONG`,
504          `ENOMEM`, `ENOSPC`, `ENOSYS` if your libc doesn't support
505          `inotify_rm_watch()`)
506        """
507        path_bytes = fsencode(path)
508
509        wd = inotify_add_watch(self._inotify_fd, path_bytes, mask)
510        _check_return(wd, path)
511
512        self._path_to_wd[path] = wd
513        self._wd_to_path[wd] = path
514
515        return wd
516
517    def remove_watch(self, path: str) -> None:
518        """
519        Remove watch by path.
520
521        Does nothing if the path is not watched.
522
523        This calls [inotify_rm_watch(2)](https://linux.die.net/man/2/inotify_rm_watch)
524        and this might raise an `OSError` with one of these `errno` values:
525        - `EBADF`
526        - `EINVAL`
527        - `ENOSYS` if your libc doesn't support [inotify_rm_watch(2)](https://linux.die.net/man/2/inotify_rm_watch)
528        """
529        wd = self._path_to_wd.get(path)
530        if wd is None:
531            _logger.debug('%s: Path is not watched', path)
532            return
533
534        try:
535            res = inotify_rm_watch(self._inotify_fd, wd)
536            _check_return(res, path)
537        finally:
538            del self._path_to_wd[path]
539            del self._wd_to_path[wd]
540
541    def remove_watch_with_id(self, wd: int) -> None:
542        """
543        Remove watch by handle.
544
545        Does nothing if the handle is invalid.
546
547        This calls `inotify_rm_watch()` and this might raise an
548        `OSError` with one of these `errno` values:
549        - `EBADF`
550        - `EINVAL`
551        - `ENOSYS` if your libc doesn't support `inotify_rm_watch()`
552        """
553        path = self._wd_to_path.get(wd)
554        if path is None:
555            _logger.debug('%d: Invalid handle', wd)
556            return
557
558        try:
559            res = inotify_rm_watch(self._inotify_fd, wd)
560            _check_return(res, path)
561        finally:
562            del self._wd_to_path[wd]
563            del self._path_to_wd[path]
564
565    def watch_paths(self) -> set[str]:
566        """
567        Get the set of the watched paths.
568        """
569        return set(self._path_to_wd)
570
571    def get_watch_id(self, path: str) -> Optional[int]:
572        """
573        Get the watch id to a path, if the path is watched.
574        """
575        return self._path_to_wd.get(path)
576
577    def get_watch_path(self, wd: int) -> Optional[str]:
578        """
579        Get the path to a watch id, if the watch id is valid.
580        """
581        return self._wd_to_path.get(wd)
582
583    def read_event(self) -> Optional[InotifyEvent]:
584        """
585        Read a single event. Might return `None` if there is none avaialbe.
586        """
587        buffer = self._buffer
588        offset = self._offset
589        wd_to_path = self._wd_to_path
590
591        while True:
592            if offset >= self._size:
593                # Only whole records are ever returned by a read() on an inotify file descriptor.
594                # If the file descriptor is blocking a read() only blocks until the first event
595                # is available.
596                self._offset = offset = 0
597                try:
598                    self._size = readinto(self._inotify_fd, buffer)
599                except BlockingIOError:
600                    self._size = 0
601                    return None
602
603                if self._size == 0:
604                    return None
605
606            wd, mask, cookie, filename_len = _HEADER_STRUCT_unpack_from(buffer, offset)
607            offset += _HEADER_STRUCT_size
608            self._offset = filename_end_offset = offset + filename_len
609
610            if filename_len:
611                filename_bytes = buffer[offset:filename_end_offset]
612                index = filename_bytes.find(b'\0')
613                # I don't like the need for bytes() here, fsdecode() doesn't take a bytearray and
614                # you can't get a bytes slice in one step from a bytearray.
615                # I guess I could do this instead?
616                # filename = (filename_bytes[:index] if index >= 0 else filename_bytes).decode(sys.getfilesystemencoding(), errors='surrogateescape')
617                filename = fsdecode(bytes(filename_bytes[:index] if index >= 0 else filename_bytes))
618            else:
619                filename = None
620
621            watch_path = wd_to_path.get(wd)
622
623            if mask & IN_IGNORED and watch_path is not None:
624                wd_to_path.pop(wd, None)
625                self._path_to_wd.pop(watch_path, None)
626
627            offset = filename_end_offset
628
629            if watch_path is None:
630                _logger.debug('Got inotify event for unknown watch handle: %d, mask: %d, cookie: %d', wd, mask, cookie)
631                continue
632
633            return InotifyEvent(wd, mask, cookie, filename_len, watch_path, filename)
634
635    def read_events(self, terminal_events: int = IN_Q_OVERFLOW | IN_UNMOUNT) -> list[InotifyEvent]:
636        """
637        Read available events. Might return an empty list if there are none
638        available.
639
640        `terminal_events` is per default: `IN_Q_OVERFLOW` | `IN_UNMOUNT`
641
642        **NOTE:** Don't use this in blocking mode! It will never return.
643
644        Raises `TerminalEventException` if any of the flags in `terminal_events`
645        are set in an event `mask`.
646        """
647        events: list[InotifyEvent] = []
648
649        while event := self.read_event():
650            if event.mask & terminal_events:
651                raise TerminalEventException(event.wd, event.mask, event.watch_path, event.filename)
652
653            events.append(event)
654
655        return events
656
657    def __iter__(self) -> Self:
658        """
659        `Inotify` can act as an iterator over the available events.
660        """
661        return self
662
663    def __next__(self) -> InotifyEvent:
664        """
665        `Inotify` can act as an iterator over the available events.
666        """
667        event = self.read_event()
668        if event is None:
669            raise StopIteration
670        return event

Listen for inotify events.

Supports the context manager and iterator protocols.

Inotify(flags: int = 524288, buffer_size: int = 4096)
397    def __init__(self, flags: int = IN_CLOEXEC, buffer_size: int = 4096) -> None:
398        """
399        `flags` is a bit set of these values:
400        - `IN_CLOEXEC` - Close inotify file descriptor on exec.
401        - `IN_NONBLOCK` - Open inotify file descriptor as non-blocking.
402
403        It's recommended to pass the `IN_CLOEXEC` flag (default) to close the
404        file descriptor on [exec*(2)](https://linux.die.net/man/3/execl).
405
406        `buffer_size` is the size of the used input buffer and needs to be at
407        least 272 (`sizeof(struct inotify_event) + NAME_MAX + 1`).
408
409        This calls [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1)
410        and thus might raise an `OSError` with one of these `errno` values:
411        - `EINVAL`
412        - `EMFILE`
413        - `ENOMEM`
414        - `ENOSYS` if your libc doesn't support [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1)
415        """
416        self._inotify_fd = -1
417        self._offset = 0
418        self._size = 0
419        self._path_to_wd = {}
420        self._wd_to_path = {}
421
422        if buffer_size < _EVENT_SIZE_MAX:
423            raise ValueError(f'buffer_size too small: {buffer_size} < {_EVENT_SIZE_MAX}')
424
425        self._buffer = bytearray(buffer_size)
426        self._inotify_fd = inotify_init1(flags)

flags is a bit set of these values:

  • IN_CLOEXEC - Close inotify file descriptor on exec.
  • IN_NONBLOCK - Open inotify file descriptor as non-blocking.

It's recommended to pass the IN_CLOEXEC flag (default) to close the file descriptor on exec*(2).

buffer_size is the size of the used input buffer and needs to be at least 272 (sizeof(struct inotify_event) + NAME_MAX + 1).

This calls inotify_init1(2) and thus might raise an OSError with one of these errno values:

def fileno(self) -> int:
428    def fileno(self) -> int:
429        """
430        The inotify file descriptor.
431
432        You can use this to call `poll()` or similar yourself
433        instead of using `PollInotify.wait()`.
434        """
435        return self._inotify_fd

The inotify file descriptor.

You can use this to call poll() or similar yourself instead of using PollInotify.wait().

closed: bool
437    @property
438    def closed(self) -> bool:
439        """
440        `True` if the inotify file descriptor was closed.
441        """
442        return self._inotify_fd == -1

True if the inotify file descriptor was closed.

def close(self) -> None:
444    def close(self) -> None:
445        """
446        Close the inotify handle.
447
448        Can safely be called multiple times, but you
449        can't call any other methods once closed.
450        """
451        try:
452            if self._inotify_fd != -1:
453                # A crash during inotify_init1() in __init__() means
454                # self._inotify_stream is not assigned, but self._inotify_fd
455                # is initialized with -1.
456                # Since this method is called in __del__() this state needs
457                # be handled.
458                os.close(self._inotify_fd)
459        finally:
460            self._inotify_fd = -1

Close the inotify handle.

Can safely be called multiple times, but you can't call any other methods once closed.

def add_watch(self, path: str, mask: int = 4095) -> int:
471    def add_watch(self, path: str, mask: int = IN_ALL_EVENTS) -> int:
472        """
473        Add a watch path.
474
475        `mask` is a bit set of these event flags:
476        - `IN_ACCESS` - File was accessed.
477        - `IN_MODIFY` - File was modified.
478        - `IN_ATTRIB` - Metadata changed.
479        - `IN_CLOSE_WRITE` - Writtable file was closed.
480        - `IN_CLOSE_NOWRITE` - Unwrittable file closed.
481        - `IN_OPEN` - File was opened.
482        - `IN_MOVED_FROM` - File was moved from X.
483        - `IN_MOVED_TO` - File was moved to Y.
484        - `IN_CREATE` - Subfile was created.
485        - `IN_DELETE` - Subfile was deleted.
486        - `IN_DELETE_SELF` - Self was deleted.
487        - `IN_MOVE_SELF` - Self was moved.
488
489        And these additional flags:
490        - `IN_ONLYDIR` - Only watch the path if it is a directory.
491        - `IN_DONT_FOLLOW` - Don't follow symbolic links.
492        - `IN_EXCL_UNLINK` - Exclude events on unlinked objects.
493        - `IN_MASK_CREATE` - Only create watches.
494        - `IN_MASK_ADD` - Add to the mask of an already existing watch.
495        - `IN_ONESHOT` - Only send event once.
496
497        This calls [inotify_add_watch(2)](https://linux.die.net/man/2/inotify_add_watch)
498        and thus might raise one of these exceptions:
499        - `PermissionError` (`EACCES`)
500        - `FileExistsError` (`EEXISTS`)
501        - `FileNotFoundError` (`ENOENT`)
502        - `NotADirectoryError` (`ENOTDIR`)
503        - `OSError` (`WBADF`, `EFAULT`, `EINVAL`, `ENAMETOOLONG`,
504          `ENOMEM`, `ENOSPC`, `ENOSYS` if your libc doesn't support
505          `inotify_rm_watch()`)
506        """
507        path_bytes = fsencode(path)
508
509        wd = inotify_add_watch(self._inotify_fd, path_bytes, mask)
510        _check_return(wd, path)
511
512        self._path_to_wd[path] = wd
513        self._wd_to_path[wd] = path
514
515        return wd

Add a watch path.

mask is a bit set of these event flags:

And these additional flags:

This calls inotify_add_watch(2) and thus might raise one of these exceptions:

  • PermissionError (EACCES)
  • FileExistsError (EEXISTS)
  • FileNotFoundError (ENOENT)
  • NotADirectoryError (ENOTDIR)
  • OSError (WBADF, EFAULT, EINVAL, ENAMETOOLONG, ENOMEM, ENOSPC, ENOSYS if your libc doesn't support inotify_rm_watch())
def remove_watch(self, path: str) -> None:
517    def remove_watch(self, path: str) -> None:
518        """
519        Remove watch by path.
520
521        Does nothing if the path is not watched.
522
523        This calls [inotify_rm_watch(2)](https://linux.die.net/man/2/inotify_rm_watch)
524        and this might raise an `OSError` with one of these `errno` values:
525        - `EBADF`
526        - `EINVAL`
527        - `ENOSYS` if your libc doesn't support [inotify_rm_watch(2)](https://linux.die.net/man/2/inotify_rm_watch)
528        """
529        wd = self._path_to_wd.get(path)
530        if wd is None:
531            _logger.debug('%s: Path is not watched', path)
532            return
533
534        try:
535            res = inotify_rm_watch(self._inotify_fd, wd)
536            _check_return(res, path)
537        finally:
538            del self._path_to_wd[path]
539            del self._wd_to_path[wd]

Remove watch by path.

Does nothing if the path is not watched.

This calls inotify_rm_watch(2) and this might raise an OSError with one of these errno values:

def remove_watch_with_id(self, wd: int) -> None:
541    def remove_watch_with_id(self, wd: int) -> None:
542        """
543        Remove watch by handle.
544
545        Does nothing if the handle is invalid.
546
547        This calls `inotify_rm_watch()` and this might raise an
548        `OSError` with one of these `errno` values:
549        - `EBADF`
550        - `EINVAL`
551        - `ENOSYS` if your libc doesn't support `inotify_rm_watch()`
552        """
553        path = self._wd_to_path.get(wd)
554        if path is None:
555            _logger.debug('%d: Invalid handle', wd)
556            return
557
558        try:
559            res = inotify_rm_watch(self._inotify_fd, wd)
560            _check_return(res, path)
561        finally:
562            del self._wd_to_path[wd]
563            del self._path_to_wd[path]

Remove watch by handle.

Does nothing if the handle is invalid.

This calls inotify_rm_watch() and this might raise an OSError with one of these errno values:

  • EBADF
  • EINVAL
  • ENOSYS if your libc doesn't support inotify_rm_watch()
def watch_paths(self) -> set[str]:
565    def watch_paths(self) -> set[str]:
566        """
567        Get the set of the watched paths.
568        """
569        return set(self._path_to_wd)

Get the set of the watched paths.

def get_watch_id(self, path: str) -> int | None:
571    def get_watch_id(self, path: str) -> Optional[int]:
572        """
573        Get the watch id to a path, if the path is watched.
574        """
575        return self._path_to_wd.get(path)

Get the watch id to a path, if the path is watched.

def get_watch_path(self, wd: int) -> str | None:
577    def get_watch_path(self, wd: int) -> Optional[str]:
578        """
579        Get the path to a watch id, if the watch id is valid.
580        """
581        return self._wd_to_path.get(wd)

Get the path to a watch id, if the watch id is valid.

def read_event(self) -> InotifyEvent | None:
583    def read_event(self) -> Optional[InotifyEvent]:
584        """
585        Read a single event. Might return `None` if there is none avaialbe.
586        """
587        buffer = self._buffer
588        offset = self._offset
589        wd_to_path = self._wd_to_path
590
591        while True:
592            if offset >= self._size:
593                # Only whole records are ever returned by a read() on an inotify file descriptor.
594                # If the file descriptor is blocking a read() only blocks until the first event
595                # is available.
596                self._offset = offset = 0
597                try:
598                    self._size = readinto(self._inotify_fd, buffer)
599                except BlockingIOError:
600                    self._size = 0
601                    return None
602
603                if self._size == 0:
604                    return None
605
606            wd, mask, cookie, filename_len = _HEADER_STRUCT_unpack_from(buffer, offset)
607            offset += _HEADER_STRUCT_size
608            self._offset = filename_end_offset = offset + filename_len
609
610            if filename_len:
611                filename_bytes = buffer[offset:filename_end_offset]
612                index = filename_bytes.find(b'\0')
613                # I don't like the need for bytes() here, fsdecode() doesn't take a bytearray and
614                # you can't get a bytes slice in one step from a bytearray.
615                # I guess I could do this instead?
616                # filename = (filename_bytes[:index] if index >= 0 else filename_bytes).decode(sys.getfilesystemencoding(), errors='surrogateescape')
617                filename = fsdecode(bytes(filename_bytes[:index] if index >= 0 else filename_bytes))
618            else:
619                filename = None
620
621            watch_path = wd_to_path.get(wd)
622
623            if mask & IN_IGNORED and watch_path is not None:
624                wd_to_path.pop(wd, None)
625                self._path_to_wd.pop(watch_path, None)
626
627            offset = filename_end_offset
628
629            if watch_path is None:
630                _logger.debug('Got inotify event for unknown watch handle: %d, mask: %d, cookie: %d', wd, mask, cookie)
631                continue
632
633            return InotifyEvent(wd, mask, cookie, filename_len, watch_path, filename)

Read a single event. Might return None if there is none avaialbe.

def read_events( self, terminal_events: int = 24576) -> list[InotifyEvent]:
635    def read_events(self, terminal_events: int = IN_Q_OVERFLOW | IN_UNMOUNT) -> list[InotifyEvent]:
636        """
637        Read available events. Might return an empty list if there are none
638        available.
639
640        `terminal_events` is per default: `IN_Q_OVERFLOW` | `IN_UNMOUNT`
641
642        **NOTE:** Don't use this in blocking mode! It will never return.
643
644        Raises `TerminalEventException` if any of the flags in `terminal_events`
645        are set in an event `mask`.
646        """
647        events: list[InotifyEvent] = []
648
649        while event := self.read_event():
650            if event.mask & terminal_events:
651                raise TerminalEventException(event.wd, event.mask, event.watch_path, event.filename)
652
653            events.append(event)
654
655        return events

Read available events. Might return an empty list if there are none available.

terminal_events is per default: IN_Q_OVERFLOW | IN_UNMOUNT

NOTE: Don't use this in blocking mode! It will never return.

Raises TerminalEventException if any of the flags in terminal_events are set in an event mask.

class PollInotify(Inotify):
672class PollInotify(Inotify):
673    """
674    Listen for inotify events.
675
676    In addition to the functionality of `Inotify` this class adds a `wait()`
677    method that waits for events using `epoll`. If you use `Inotify` you
678    need/can to do that yourself.
679
680    Supports the context manager and iterator protocols.
681    """
682    __slots__ = (
683        '_epoll',
684        '_stopfd',
685    )
686
687    _epoll: select.epoll
688    _stopfd: Optional[int]
689
690    def __init__(self, stopfd: Optional[int] = None, buffer_size: int = 4096) -> None:
691        """
692        If not `None` then `stopfd` is a file descriptor that will
693        be added to the `poll()` call in `PollInotify.wait()`.
694
695        `buffer_size` is the size of the used input buffer and needs to be at
696        least 272 (`sizeof(struct inotify_event) + NAME_MAX + 1`).
697
698        This calls [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1)
699        and thus might raise an `OSError` with one of these `errno` values:
700        - `EINVAL` (shouldn't happen)
701        - `EMFILE`
702        - `ENOMEM`
703        - `ENOSYS` if your libc doesn't support [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1)
704        """
705        super().__init__(IN_NONBLOCK | IN_CLOEXEC, buffer_size)
706        self._stopfd = stopfd
707        self._epoll = select.epoll(1 if stopfd is None else 2)
708        self._epoll.register(self._inotify_fd, select.POLLIN)
709
710        if stopfd is not None:
711            self._epoll.register(stopfd, select.POLLIN)
712
713    @property
714    def stopfd(self) -> Optional[int]:
715        """
716        The `stopfd` parameter of `__init__()`, used in `wait()`.
717        """
718        return self._stopfd
719
720    def close(self) -> None:
721        """
722        Close the inotify and epoll handles.
723
724        Can safely be called multiple times, but you
725        can't call any other methods once closed.
726        """
727        try:
728            super().close()
729        finally:
730            epoll = self._epoll
731            if not epoll.closed:
732                epoll.close()
733
734    def wait(self, timeout: Optional[float] = None) -> bool:
735        """
736        Wait for inotify events or for any `POLLIN` on `stopfd`,
737        if that is not `None`. If `stopfd` signals this function will
738        return `False`, otherwise `True`.
739
740        Raises `TimeoutError` if `timeout` is not `None` and
741        the operation has expired.
742
743        This method uses using `select.epoll.poll()`, see there for
744        additional possible exceptions.
745        """
746        events = self._epoll.poll(timeout)
747
748        if not events and timeout is not None and timeout >= 0.0:
749            raise TimeoutError
750
751        stopfd = self._stopfd
752        if stopfd is not None:
753            for fd, mask in events:
754                if fd == stopfd:
755                    return False
756
757        return True

Listen for inotify events.

In addition to the functionality of Inotify this class adds a wait() method that waits for events using epoll. If you use Inotify you need/can to do that yourself.

Supports the context manager and iterator protocols.

PollInotify(stopfd: int | None = None, buffer_size: int = 4096)
690    def __init__(self, stopfd: Optional[int] = None, buffer_size: int = 4096) -> None:
691        """
692        If not `None` then `stopfd` is a file descriptor that will
693        be added to the `poll()` call in `PollInotify.wait()`.
694
695        `buffer_size` is the size of the used input buffer and needs to be at
696        least 272 (`sizeof(struct inotify_event) + NAME_MAX + 1`).
697
698        This calls [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1)
699        and thus might raise an `OSError` with one of these `errno` values:
700        - `EINVAL` (shouldn't happen)
701        - `EMFILE`
702        - `ENOMEM`
703        - `ENOSYS` if your libc doesn't support [inotify_init1(2)](https://linux.die.net/man/2/inotify_init1)
704        """
705        super().__init__(IN_NONBLOCK | IN_CLOEXEC, buffer_size)
706        self._stopfd = stopfd
707        self._epoll = select.epoll(1 if stopfd is None else 2)
708        self._epoll.register(self._inotify_fd, select.POLLIN)
709
710        if stopfd is not None:
711            self._epoll.register(stopfd, select.POLLIN)

If not None then stopfd is a file descriptor that will be added to the poll() call in PollInotify.wait().

buffer_size is the size of the used input buffer and needs to be at least 272 (sizeof(struct inotify_event) + NAME_MAX + 1).

This calls inotify_init1(2) and thus might raise an OSError with one of these errno values:

  • EINVAL (shouldn't happen)
  • EMFILE
  • ENOMEM
  • ENOSYS if your libc doesn't support inotify_init1(2)
stopfd: int | None
713    @property
714    def stopfd(self) -> Optional[int]:
715        """
716        The `stopfd` parameter of `__init__()`, used in `wait()`.
717        """
718        return self._stopfd

The stopfd parameter of __init__(), used in wait().

def close(self) -> None:
720    def close(self) -> None:
721        """
722        Close the inotify and epoll handles.
723
724        Can safely be called multiple times, but you
725        can't call any other methods once closed.
726        """
727        try:
728            super().close()
729        finally:
730            epoll = self._epoll
731            if not epoll.closed:
732                epoll.close()

Close the inotify and epoll handles.

Can safely be called multiple times, but you can't call any other methods once closed.

def wait(self, timeout: float | None = None) -> bool:
734    def wait(self, timeout: Optional[float] = None) -> bool:
735        """
736        Wait for inotify events or for any `POLLIN` on `stopfd`,
737        if that is not `None`. If `stopfd` signals this function will
738        return `False`, otherwise `True`.
739
740        Raises `TimeoutError` if `timeout` is not `None` and
741        the operation has expired.
742
743        This method uses using `select.epoll.poll()`, see there for
744        additional possible exceptions.
745        """
746        events = self._epoll.poll(timeout)
747
748        if not events and timeout is not None and timeout >= 0.0:
749            raise TimeoutError
750
751        stopfd = self._stopfd
752        if stopfd is not None:
753            for fd, mask in events:
754                if fd == stopfd:
755                    return False
756
757        return True

Wait for inotify events or for any POLLIN on stopfd, if that is not None. If stopfd signals this function will return False, otherwise True.

Raises TimeoutError if timeout is not None and the operation has expired.

This method uses using select.epoll.poll(), see there for additional possible exceptions.

class TerminalEventException(builtins.Exception):
298class TerminalEventException(Exception):
299    """
300    Exception raised by `Inotify.read_events()` when an event mask contains one
301    of the specified `terminal_events`.
302    """
303
304    __slots__ = (
305        'wd',
306        'mask',
307        'watch_path',
308        'filename',
309    )
310
311    wd: int; "Inotify watch descriptor."
312    mask: int; "Bitset of the events that occured."
313    watch_path: Optional[str]; "Path of the watched file, `None` if the Python code doesn't know about the watch."
314    filename: Optional[str]; "If the event is about the child of a watched directory, this is the name of that file, otherwise `None`."
315
316    def __init__(self, wd: int, mask: int, watch_path: Optional[str], filename: Optional[str]) -> None:
317        super().__init__(wd, mask, watch_path, filename)
318        self.wd = wd
319        self.mask = mask
320        self.watch_path = watch_path
321        self.filename = filename

Exception raised by Inotify.read_events() when an event mask contains one of the specified terminal_events.

TerminalEventException(wd: int, mask: int, watch_path: str | None, filename: str | None)
316    def __init__(self, wd: int, mask: int, watch_path: Optional[str], filename: Optional[str]) -> None:
317        super().__init__(wd, mask, watch_path, filename)
318        self.wd = wd
319        self.mask = mask
320        self.watch_path = watch_path
321        self.filename = filename
wd: int

Inotify watch descriptor.

mask: int

Bitset of the events that occured.

watch_path: str | None

Path of the watched file, None if the Python code doesn't know about the watch.

filename: str | None

If the event is about the child of a watched directory, this is the name of that file, otherwise None.

def get_inotify_event_names(mask: int) -> list[str]:
196def get_inotify_event_names(mask: int) -> list[str]:
197    """
198    Get a list of event names from an event mask as returned by inotify.
199
200    If there is a flag set that isn't a know name the hexadecimal representation
201    of that flag is also returned.
202    """
203    names: list[str] = []
204    for code, name in INOTIFY_MASK_CODES.items():
205        if mask & code:
206            names.append(name)
207            mask &= ~code
208            if not mask:
209                break
210
211    if mask:
212        for bit in range(0, 32):
213            if bit & mask:
214                names.append('0x%x' % bit)
215                mask &= ~bit
216                if not mask:
217                    break
218
219    return names

Get a list of event names from an event mask as returned by inotify.

If there is a flag set that isn't a know name the hexadecimal representation of that flag is also returned.

HAS_INOTIFY = True

True if your libc exports inotify_init1, inotify_add_watch, and inotify_rm_watch, otherwise False.

IN_CLOEXEC: Final[int] = 524288

Close inotify file descriptor on exec.

See Also: Inotify.__init__()

IN_NONBLOCK: Final[int] = 2048

Open inotify file descriptor as non-blocking.

See Also: Inotify.__init__()

IN_ACCESS: Final[int] = 1

File was accessed.

See Also: Inotify.add_watch(), InotifyEvent.mask

IN_MODIFY: Final[int] = 2

File was modified.

See Also: Inotify.add_watch(), InotifyEvent.mask

IN_ATTRIB: Final[int] = 4

Metadata changed.

See Also: Inotify.add_watch(), InotifyEvent.mask

IN_CLOSE_WRITE: Final[int] = 8

Writtable file was closed.

See Also: Inotify.add_watch(), InotifyEvent.mask

IN_CLOSE_NOWRITE: Final[int] = 16

Unwrittable file closed.

See Also: Inotify.add_watch(), InotifyEvent.mask

IN_OPEN: Final[int] = 32

File was opened.

See Also: Inotify.add_watch(), InotifyEvent.mask

IN_MOVED_FROM: Final[int] = 64

File was moved from X.

See Also: Inotify.add_watch(), InotifyEvent.mask

IN_MOVED_TO: Final[int] = 128

File was moved to Y.

See Also: Inotify.add_watch(), InotifyEvent.mask

IN_CREATE: Final[int] = 256

Subfile was created.

See Also: Inotify.add_watch(), InotifyEvent.mask

IN_DELETE: Final[int] = 512

Subfile was deleted.

See Also: Inotify.add_watch(), InotifyEvent.mask

IN_DELETE_SELF: Final[int] = 1024

Self was deleted.

See Also: Inotify.add_watch(), InotifyEvent.mask

IN_MOVE_SELF: Final[int] = 2048

Self was moved.

See Also: Inotify.add_watch(), InotifyEvent.mask

IN_UNMOUNT: Final[int] = 8192

Backing file system was unmounted.

See Also: InotifyEvent.mask

IN_Q_OVERFLOW: Final[int] = 16384

Event queued overflowed.

See Also: InotifyEvent.mask

IN_IGNORED: Final[int] = 32768

File was ignored.

See Also: InotifyEvent.mask

IN_CLOSE: Final[int] = 24

All close events.

See Also: Inotify.add_watch(), InotifyEvent.mask

IN_MOVE: Final[int] = 192

All move events.

See Also: Inotify.add_watch(), InotifyEvent.mask

IN_ONLYDIR: Final[int] = 16777216

Only watch the path if it is a directory.

See Also: Inotify.add_watch()

IN_DONT_FOLLOW: Final[int] = 33554432

Don't follow symbolic links.

See Also: Inotify.add_watch()

IN_MASK_CREATE: Final[int] = 268435456

Only create watches.

See Also: Inotify.add_watch()

IN_MASK_ADD: Final[int] = 536870912

Add to the mask of an already existing watch.

See Also: Inotify.add_watch()

IN_ISDIR: Final[int] = 1073741824

Event occurred against directory.

See Also: InotifyEvent.mask

IN_ONESHOT: Final[int] = 2147483648

Only send event once.

See Also: Inotify.add_watch()

IN_ALL_EVENTS: Final[int] = 4095

All of the events.

See Also: Inotify.add_watch(), InotifyEvent.mask

INOTIFY_MASK_CODES: Final[Mapping[int, str]] = {1: 'ACCESS', 2: 'MODIFY', 4: 'ATTRIB', 8: 'CLOSE_WRITE', 16: 'CLOSE_NOWRITE', 32: 'OPEN', 64: 'MOVED_FROM', 128: 'MOVED_TO', 256: 'CREATE', 512: 'DELETE', 1024: 'DELETE_SELF', 2048: 'MOVE_SELF', 8192: 'UNMOUNT', 16384: 'Q_OVERFLOW', 32768: 'IGNORED', 1073741824: 'ISDIR'}

Mapping from inotify event mask flag to it's name.

See Also: InotifyEvent.mask