From 5389d13ed20c283e56a5dba015176d87deae346b Mon Sep 17 00:00:00 2001 From: Dustin Oprea Date: Mon, 7 Jul 2025 02:28:14 -0400 Subject: [PATCH 1/2] Add Github actions for unit-testing --- .github/workflows/push_event_workflow.yml | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/push_event_workflow.yml diff --git a/.github/workflows/push_event_workflow.yml b/.github/workflows/push_event_workflow.yml new file mode 100644 index 0000000..a27932e --- /dev/null +++ b/.github/workflows/push_event_workflow.yml @@ -0,0 +1,30 @@ +name: Run unit-tests +on: push + +jobs: + unit-testing: + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name : Install + run : pip install -e . + + - name : Install testing requirements + run : pip install -r requirements-testing.txt + + - name : Run tests + run : nose2 -v + + - name : Run tests + run : python3 -m build From 7045a3e360df7b2b069eb1b2879eb858392fb961 Mon Sep 17 00:00:00 2001 From: Dustin Oprea Date: Mon, 7 Jul 2025 02:41:43 -0400 Subject: [PATCH 2/2] Convert README.rst to README.md and modernize This includes an update to use the Github badge. --- MANIFEST.in | 2 +- README.md | 1 + README.rst | 1 - inotify/resources/{README.rst => README.md} | 160 +++++++++----------- setup.py | 7 +- 5 files changed, 79 insertions(+), 92 deletions(-) create mode 120000 README.md delete mode 120000 README.rst rename inotify/resources/{README.rst => README.md} (64%) diff --git a/MANIFEST.in b/MANIFEST.in index d757713..7aa5269 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ include inotify/resources/requirements.txt -include inotify/resources/README.rst +include inotify/resources/README.md diff --git a/README.md b/README.md new file mode 120000 index 0000000..65b1d86 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +inotify/resources/README.md \ No newline at end of file diff --git a/README.rst b/README.rst deleted file mode 120000 index 2c06fbb..0000000 --- a/README.rst +++ /dev/null @@ -1 +0,0 @@ -inotify/resources/README.rst \ No newline at end of file diff --git a/inotify/resources/README.rst b/inotify/resources/README.md similarity index 64% rename from inotify/resources/README.rst rename to inotify/resources/README.md index ce9dacd..6e54292 100644 --- a/inotify/resources/README.rst +++ b/inotify/resources/README.md @@ -1,9 +1,6 @@ -|Build\_Status| -|Coverage\_Status| +[![Unit Testing](https://github.com/dsoprea/PyInotify/actions/workflows/push_event_workflow.yml/badge.svg)](https://github.com/dsoprea/PyInotify/actions/workflows/push_event_workflow.yml) -======== -Overview -======== +# Overview *inotify* functionality is available from the Linux kernel and allows you to register one or more directories for watching, and to simply block and wait for notification events. This is obviously far more efficient than polling one or more directories to determine if anything has changed. This is available in the Linux kernel as of version 2.6 . @@ -12,105 +9,103 @@ We've designed this library to act as a generator. All you have to do is loop, a **This project is unrelated to the *PyInotify* project that existed prior to this one (this project began in 2015). That project is defunct and no longer available.** -========== -Installing -========== +# Installing -Install via *pip*:: +Install via *pip*: - $ sudo pip install inotify +``` +$ sudo pip install inotify +``` -======= -Example -======= +# Example Code for monitoring a simple, flat path (see "Recursive Watching" for watching a hierarchical structure): +```python +import inotify.adapters -.. code-block:: python +def _main(): + i = inotify.adapters.Inotify() - import inotify.adapters + i.add_watch('/tmp') - def _main(): - i = inotify.adapters.Inotify() - - i.add_watch('/tmp') - - with open('/tmp/test_file', 'w'): - pass - - for event in i.event_gen(yield_nones=False): - (_, type_names, path, filename) = event - - print("PATH=[{}] FILENAME=[{}] EVENT_TYPES={}".format( - path, filename, type_names)) - - if __name__ == '__main__': - _main() - -Output:: + with open('/tmp/test_file', 'w'): + pass - PATH=[/tmp] FILENAME=[test_file] EVENT_TYPES=['IN_MODIFY'] - PATH=[/tmp] FILENAME=[test_file] EVENT_TYPES=['IN_OPEN'] - PATH=[/tmp] FILENAME=[test_file] EVENT_TYPES=['IN_CLOSE_WRITE'] - ^CTraceback (most recent call last): - File "inotify_test.py", line 18, in - _main() - File "inotify_test.py", line 11, in _main - for event in i.event_gen(yield_nones=False): - File "/home/dustin/development/python/pyinotify/inotify/adapters.py", line 202, in event_gen - events = self.__epoll.poll(block_duration_s) - KeyboardInterrupt + for event in i.event_gen(yield_nones=False): + (_, type_names, path, filename) = event + + print("PATH=[{}] FILENAME=[{}] EVENT_TYPES={}".format( + path, filename, type_names)) + +if __name__ == '__main__': + _main() +``` + +Output: +``` +PATH=[/tmp] FILENAME=[test_file] EVENT_TYPES=['IN_MODIFY'] +PATH=[/tmp] FILENAME=[test_file] EVENT_TYPES=['IN_OPEN'] +PATH=[/tmp] FILENAME=[test_file] EVENT_TYPES=['IN_CLOSE_WRITE'] +^CTraceback (most recent call last): + File "inotify_test.py", line 18, in + _main() + File "inotify_test.py", line 11, in _main + for event in i.event_gen(yield_nones=False): + File "/home/dustin/development/python/pyinotify/inotify/adapters.py", line 202, in event_gen + events = self.__epoll.poll(block_duration_s) +KeyboardInterrupt +``` Note that this works quite nicely, but, in the event that you don't want to be driven by the loop, you can also provide a timeout and then even flatten the output of the generator directly to a list: -.. code-block:: python - - import inotify.adapters +```python +import inotify.adapters - def _main(): - i = inotify.adapters.Inotify() +def _main(): + i = inotify.adapters.Inotify() - i.add_watch('/tmp') + i.add_watch('/tmp') - with open('/tmp/test_file', 'w'): - pass + with open('/tmp/test_file', 'w'): + pass - events = i.event_gen(yield_nones=False, timeout_s=1) - events = list(events) + events = i.event_gen(yield_nones=False, timeout_s=1) + events = list(events) - print(events) + print(events) - if __name__ == '__main__': - _main() +if __name__ == '__main__': + _main() +``` -This will return everything that's happened since the last time you ran it (artificially formatted here):: +This will return everything that's happened since the last time you ran it (artificially formatted here): - [ - (_INOTIFY_EVENT(wd=1, mask=2, cookie=0, len=16), ['IN_MODIFY'], '/tmp', u'test_file'), - (_INOTIFY_EVENT(wd=1, mask=32, cookie=0, len=16), ['IN_OPEN'], '/tmp', u'test_file'), - (_INOTIFY_EVENT(wd=1, mask=8, cookie=0, len=16), ['IN_CLOSE_WRITE'], '/tmp', u'test_file') - ] +```python +[ + (_INOTIFY_EVENT(wd=1, mask=2, cookie=0, len=16), ['IN_MODIFY'], '/tmp', u'test_file'), + (_INOTIFY_EVENT(wd=1, mask=32, cookie=0, len=16), ['IN_OPEN'], '/tmp', u'test_file'), + (_INOTIFY_EVENT(wd=1, mask=8, cookie=0, len=16), ['IN_CLOSE_WRITE'], '/tmp', u'test_file') +] +``` **Note that the event-loop will automatically register new directories to be watched, so, if you will create new directories and then potentially delete them, between calls, and are only retrieving the events in batches (like above) then you might experience issues. See the parameters for `event_gen()` for options to handle this scenario.** -================== -Recursive Watching -================== +# Recursive Watching There is also the ability to add a recursive watch on a path. Example: -.. code-block:: python +```python +i = inotify.adapters.InotifyTree('/tmp/watch_tree') - i = inotify.adapters.InotifyTree('/tmp/watch_tree') +for event in i.event_gen(): + # Do stuff... - for event in i.event_gen(): - # Do stuff... - - pass + pass +``` This will immediately recurse through the directory tree and add watches on all subdirectories. New directories will automatically have watches added for them and deleted directories will be cleaned-up. @@ -120,9 +115,7 @@ The other differences from the standard functionality: - Even if you provide a very restrictive mask that doesn't allow for directory create/delete events, the *IN_ISDIR*, *IN_CREATE*, and *IN_DELETE* flags will still be seen. -===== -Notes -===== +# Notes - **IMPORTANT:** Recursively monitoring paths is **not** a functionality provided by the kernel. Rather, we artificially implement it. As directory-created events are received, we create watches for the child directories on-the-fly. This means that there is potential for a race condition: if a directory is created and a file or directory is created inside before you (using the `event_gen()` loop) have a chance to observe it, then you are going to have a problem: If it is a file, then you will miss the events related to its creation, but, if it is a directory, then not only will you miss those creation events but this library will also miss them and not be able to add a watch for them. If you are dealing with a **large number of hierarchical directory creations** and have the ability to be aware new directories via a secondary channel with some lead time before any files are populated *into* them, you can take advantage of this and call `add_watch()` manually. In this case there is limited value in using `InotifyTree()`/`InotifyTree()` instead of just `Inotify()` but this choice is left to you. @@ -135,16 +128,11 @@ Notes - Calling `remove_watch()` is not strictly necessary. The *inotify* resources is automatically cleaned-up, which would clean-up all watch resources as well. -======= -Testing -======= - -Install the testing dependencies and use nose2 to run the tests:: +# Testing - $ pip install -r requirements-testing.txt - $ ./test.sh +Install the testing dependencies and use nose2 to run the tests: -.. |Build_Status| image:: https://travis-ci.org/dsoprea/PyInotify.svg?branch=master - :target: https://travis-ci.org/dsoprea/PyInotify -.. |Coverage_Status| image:: https://coveralls.io/repos/github/dsoprea/PyInotify/badge.svg?branch=master - :target: https://coveralls.io/github/dsoprea/PyInotify?branch=master +``` +$ pip install -r requirements-testing.txt +$ ./test.sh +``` diff --git a/setup.py b/setup.py index 7d0ffcd..d50f42e 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ import setuptools import os -with open(os.path.join('inotify', 'resources', 'README.rst')) as f: +with open(os.path.join('inotify', 'resources', 'README.md')) as f: _LONG_DESCRIPTION = f.read() with open(os.path.join('inotify', 'resources', 'requirements.txt')) as f: @@ -15,8 +15,7 @@ version='0.2.11', description=_DESCRIPTION, long_description=_LONG_DESCRIPTION, - classifiers=[ - ], + long_description_content_type='text/markdown', keywords='inotify', author='Dustin Oprea', author_email='myselfasunder@gmail.com', @@ -28,7 +27,7 @@ install_requires=_INSTALL_REQUIRES, package_data={ 'inotify': [ - 'resources/README.rst', + 'resources/README.md', 'resources/requirements.txt', ] },