diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..844eb77 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,57 @@ +on: + push: + pull_request: + # Allow to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + build: + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - run: python -m pip install setuptools wheel + - run: python setup.py sdist + - run: python setup.py bdist_wheel + - uses: actions/upload-artifact@v4 + with: + name: MacFSEvents + path: ./dist/ + test: + strategy: + # We want to see all failures: + fail-fast: false + matrix: + config: + - ["3.8", "py38", "macos-12"] + - ["3.9", "py38", "macos-12"] + - ["3.9", "lint", "macos-12"] + - ["3.9", "py39", "macos-12"] + - ["3.10", "py310", "macos-12"] + - ["3.11", "py311", "macos-12"] + - ["3.12", "py312", "macos-12"] + - ["3.12", "py312", "macos-13"] + - ["3.12", "py312", "macos-14"] + runs-on: ${{ matrix.config[2] }} + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + name: ${{ matrix.config[0] }}-${{ matrix.config[1] }}-${{ matrix.config[2] }} + steps: + - run: git config --global core.autocrlf false + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.config[0] }} + - name: Pip cache + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ matrix.config[0] }}-${{ hashFiles('setup.*', 'tox.ini') }} + restore-keys: | + ${{ runner.os }}-pip-${{ matrix.config[0] }}- + ${{ runner.os }}-pip- + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox + - name: Test + run: tox -e ${{ matrix.config[1] }} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a85cd59..0000000 --- a/.travis.yml +++ /dev/null @@ -1,18 +0,0 @@ -sudo: false -osx_image: xcode7.1 - -language: objective-c - -before_install: -- brew install python3 - -script: -- python setup.py build -- python setup.py test -- python3 setup.py build -- python3 setup.py test - -notifications: - email: - on_success: never - on_failure: change diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..a1ccdad --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include *.py +include tox.ini diff --git a/fsevents.py b/fsevents.py index 213bf4b..ff874d6 100644 --- a/fsevents.py +++ b/fsevents.py @@ -4,66 +4,57 @@ import unicodedata from _fsevents import ( - loop, - stop, - schedule, - unschedule, - CF_POLLIN, - CF_POLLOUT, - FS_IGNORESELF, - FS_FILEEVENTS, - FS_ITEMCREATED, - FS_ITEMREMOVED, - FS_ITEMINODEMETAMOD, - FS_ITEMRENAMED, - FS_ITEMMODIFIED, - FS_ITEMFINDERINFOMOD, - FS_ITEMCHANGEOWNER, - FS_ITEMXATTRMOD, - FS_ITEMISFILE, - FS_ITEMISDIR, - FS_ITEMISSYMLINK, + FS_CFLAGFILEEVENTS, + FS_CFLAGNONE, FS_EVENTIDSINCENOW, FS_FLAGEVENTIDSWRAPPED, - FS_FLAGNONE, FS_FLAGHISTORYDONE, - FS_FLAGROOTCHANGED, FS_FLAGKERNELDROPPED, - FS_FLAGUNMOUNT, FS_FLAGMOUNT, - FS_FLAGUSERDROPPED, FS_FLAGMUSTSCANSUBDIRS, - FS_CFLAGFILEEVENTS, - FS_CFLAGNONE, - FS_CFLAGIGNORESELF, - FS_CFLAGUSECFTYPES, - FS_CFLAGNODEFER, - FS_CFLAGWATCHROOT, + FS_FLAGROOTCHANGED, + FS_FLAGUNMOUNT, + FS_FLAGUSERDROPPED, + FS_ITEMCHANGEOWNER, + FS_ITEMCREATED, + FS_ITEMFINDERINFOMOD, + FS_ITEMINODEMETAMOD, + FS_ITEMISDIR, + FS_ITEMISFILE, + FS_ITEMISSYMLINK, + FS_ITEMMODIFIED, + FS_ITEMREMOVED, + FS_ITEMRENAMED, + FS_ITEMXATTRMOD, + loop, + schedule, + stop, + unschedule ) + class Mask(int): stringmap = { - FS_FLAGMUSTSCANSUBDIRS: 'MustScanSubDirs', - FS_FLAGUSERDROPPED: 'UserDropped', - FS_FLAGKERNELDROPPED: 'KernelDropped', - FS_FLAGEVENTIDSWRAPPED: 'EventIDsWrapped', - FS_FLAGHISTORYDONE: 'HistoryDone', - FS_FLAGROOTCHANGED: 'RootChanged', - FS_FLAGMOUNT: 'Mount', - FS_FLAGUNMOUNT: 'Unmount', - + FS_FLAGMUSTSCANSUBDIRS: "MustScanSubDirs", + FS_FLAGUSERDROPPED: "UserDropped", + FS_FLAGKERNELDROPPED: "KernelDropped", + FS_FLAGEVENTIDSWRAPPED: "EventIDsWrapped", + FS_FLAGHISTORYDONE: "HistoryDone", + FS_FLAGROOTCHANGED: "RootChanged", + FS_FLAGMOUNT: "Mount", + FS_FLAGUNMOUNT: "Unmount", # Flags when creating the stream. - FS_ITEMCREATED: 'ItemCreated', - FS_ITEMREMOVED: 'ItemRemoved', - FS_ITEMINODEMETAMOD: 'ItemInodeMetaMod', - FS_ITEMRENAMED: 'ItemRenamed', - FS_ITEMMODIFIED: 'ItemModified', - FS_ITEMFINDERINFOMOD: 'ItemFinderInfoMod', - FS_ITEMCHANGEOWNER: 'ItemChangedOwner', - FS_ITEMXATTRMOD: 'ItemXAttrMod', - FS_ITEMISFILE: 'ItemIsFile', - FS_ITEMISDIR: 'ItemIsDir', - FS_ITEMISSYMLINK: 'ItemIsSymlink' + FS_ITEMCREATED: "ItemCreated", + FS_ITEMREMOVED: "ItemRemoved", + FS_ITEMINODEMETAMOD: "ItemInodeMetaMod", + FS_ITEMRENAMED: "ItemRenamed", + FS_ITEMMODIFIED: "ItemModified", + FS_ITEMFINDERINFOMOD: "ItemFinderInfoMod", + FS_ITEMCHANGEOWNER: "ItemChangedOwner", + FS_ITEMXATTRMOD: "ItemXAttrMod", + FS_ITEMISFILE: "ItemIsFile", + FS_ITEMISDIR: "ItemIsDir", + FS_ITEMISSYMLINK: "ItemIsSymlink", } _svals = list(stringmap.items()) @@ -96,7 +87,8 @@ def check_path_string_type(*paths): for path in paths: if not isinstance(path, str): raise TypeError( - "Path must be string, not '%s'." % type(path).__name__) + "Path must be string, not '%s'." % type(path).__name__ + ) class Observer(threading.Thread): @@ -138,16 +130,25 @@ def _schedule(self, stream): if stream.file_events: callback = FileEventCallback(stream.callback, stream.raw_paths) else: + def callback(paths, masks, ids): for path, mask, id in zip(paths, masks, ids): if sys.version_info[0] >= 3: - path = path.decode('utf-8') + path = path.decode("utf-8") if stream.ids is False: stream.callback(path, mask) elif stream.ids is True: stream.callback(path, mask, id) - - schedule(self, stream, callback, stream.paths, stream.since, stream.latency, stream.cflags) + + schedule( + self, + stream, + callback, + stream.paths, + stream.since, + stream.latency, + stream.cflags, + ) def schedule(self, stream): self.lock.acquire() @@ -181,14 +182,17 @@ def stop(self): self.event = None event.set() + class Stream(object): def __init__(self, callback, *paths, **options): - file_events = options.pop('file_events', False) - since = options.pop('since',FS_EVENTIDSINCENOW) - cflags = options.pop('flags', FS_CFLAGNONE) - latency = options.pop('latency', 0.01) - ids = options.pop('ids', False) - assert len(options) == 0, "Invalid option(s): %s" % repr(options.keys()) + file_events = options.pop("file_events", False) + since = options.pop("since", FS_EVENTIDSINCENOW) + cflags = options.pop("flags", FS_CFLAGNONE) + latency = options.pop("latency", 0.01) + ids = options.pop("ids", False) + assert len(options) == 0, "Invalid option(s): %s" % repr( + options.keys() + ) check_path_string_type(*paths) self.callback = callback @@ -196,8 +200,8 @@ def __init__(self, callback, *paths, **options): # The C-extension needs the path in 8-bit form. self.paths = [ - path if isinstance(path, bytes) - else path.encode('utf-8') for path in paths + path if isinstance(path, bytes) else path.encode("utf-8") + for path in paths ] self.file_events = file_events @@ -206,8 +210,9 @@ def __init__(self, callback, *paths, **options): self.latency = latency self.ids = ids + class FileEvent(object): - __slots__ = 'mask', 'cookie', 'name' + __slots__ = "mask", "cookie", "name" def __init__(self, mask, cookie, name): self.mask = mask @@ -217,6 +222,7 @@ def __init__(self, mask, cookie, name): def __repr__(self): return repr((self.mask, self.cookie, self.name)) + class FileEventCallback(object): def __init__(self, callback, paths): self.snapshots = {} @@ -234,13 +240,13 @@ def __call__(self, paths, masks, ids): for path in sorted(paths): # supports UTF-8-MAC(NFD) if not isinstance(path, unicode): - path = path.decode('utf-8') - path = unicodedata.normalize('NFD', path).encode('utf-8') + path = path.decode("utf-8") + path = unicodedata.normalize("NFD", path).encode("utf-8") if sys.version_info[0] >= 3: - path = path.decode('utf-8') - - path = path.rstrip('/') + path = path.decode("utf-8") + + path = path.rstrip("/") snapshot = self.snapshots[path] current = {} try: @@ -265,13 +271,15 @@ def __call__(self, paths, masks, ids): observed.discard(name) else: event = created.get(snap_stat.st_ino) - if (event is not None): + if event is not None: self.cookie += 1 event.mask = IN_MOVED_FROM event.cookie = self.cookie tmp_filename = event.name event.name = filename - events.append(FileEvent(IN_MOVED_TO, self.cookie, tmp_filename)) + events.append( + FileEvent(IN_MOVED_TO, self.cookie, tmp_filename) + ) else: event = FileEvent(IN_DELETE, None, filename) deleted[snap_stat.st_ino] = event @@ -313,3 +321,32 @@ def snapshot(self, path): entry[obj] = os.lstat(os.path.join(root, obj)) except OSError: continue + + +__all__ = ( + FS_CFLAGFILEEVENTS, + FS_CFLAGNONE, + FS_EVENTIDSINCENOW, + FS_FLAGEVENTIDSWRAPPED, + FS_FLAGHISTORYDONE, + FS_FLAGKERNELDROPPED, + FS_FLAGMOUNT, + FS_FLAGMUSTSCANSUBDIRS, + FS_FLAGROOTCHANGED, + FS_FLAGUNMOUNT, + FS_FLAGUSERDROPPED, + FS_ITEMCHANGEOWNER, + FS_ITEMCREATED, + FS_ITEMFINDERINFOMOD, + FS_ITEMINODEMETAMOD, + FS_ITEMISDIR, + FS_ITEMISFILE, + FS_ITEMISSYMLINK, + FS_ITEMMODIFIED, + FS_ITEMREMOVED, + FS_ITEMRENAMED, + FS_ITEMXATTRMOD, + FileEvent, + Stream, + Observer, +) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..086ab1b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[tool.black] +line-length = 79 + +[tool.isort] +multi_line_output=3 diff --git a/setup.py b/setup.py index d7f9bb0..750fea9 100644 --- a/setup.py +++ b/setup.py @@ -1,55 +1,58 @@ import os -from setuptools.extension import Extension -from setuptools.command.build_ext import build_ext from setuptools import setup +from setuptools.command.build_ext import build_ext +from setuptools.extension import Extension + def read(fname): with open(os.path.join(os.path.dirname(__file__), fname)) as f: return f.read() + ext_modules = [ - Extension(name = '_fsevents', - sources = ['_fsevents.c', 'compat.c'], - extra_link_args = ["-framework","CoreFoundation", - "-framework","CoreServices"], - ), - ] + Extension( + name="_fsevents", + sources=["_fsevents.c", "compat.c"], + extra_link_args=[ + "-framework", + "CoreFoundation", + "-framework", + "CoreServices", + ], + ), +] -setup(name = "MacFSEvents", - version = "0.8.4", - description = "Thread-based interface to file system observation primitives.", - long_description = "\n\n".join((read('README.rst'), read('CHANGES.rst'))), - license = "BSD", - data_files = [("", [ - "compat.h", - "LICENSE.txt", - "CHANGES.rst" - ])], - author = "Malthe Borch", - author_email = "mborch@gmail.com", - url = 'https://github.com/malthe/macfsevents', - cmdclass = dict(build_ext=build_ext), - ext_modules = ext_modules, - platforms = ["Mac OS X"], - classifiers = [ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: MacOS :: MacOS X', - 'Programming Language :: C', - 'Programming Language :: Python :: 2.4', - 'Programming Language :: Python :: 2.5', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: System :: Filesystems', - ], - zip_safe=False, - test_suite="tests", - py_modules=['fsevents'], - ) +setup( + name="MacFSEvents", + version="0.8.4", + description=( + "Thread-based interface to file system observation " "primitives." + ), + long_description="\n\n".join((read("README.rst"), read("CHANGES.rst"))), + license="BSD", + data_files=[("", ["compat.h", "LICENSE.txt", "CHANGES.rst"])], + author="Malthe Borch", + author_email="mborch@gmail.com", + url="https://github.com/malthe/macfsevents", + cmdclass=dict(build_ext=build_ext), + ext_modules=ext_modules, + platforms=["Mac OS X"], + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: MacOS :: MacOS X", + "Programming Language :: C", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: System :: Filesystems", + ], + zip_safe=False, + test_suite="tests", + py_modules=["fsevents"], +) diff --git a/tests.py b/tests.py index 28109e7..52f356a 100644 --- a/tests.py +++ b/tests.py @@ -1,26 +1,30 @@ import unittest + class BaseTestCase(unittest.TestCase): def setUp(self): self.tempdir = self._make_tempdir() def tearDown(self): import os + os.rmdir(self.tempdir) def _make_temporary(self, directory=None): import os import tempfile + if directory is None: directory = self.tempdir f = tempfile.NamedTemporaryFile(dir=directory) f.flush() - path = os.path.realpath(os.path.dirname(f.name)).rstrip('/') + '/' + path = os.path.realpath(os.path.dirname(f.name)).rstrip("/") + "/" return f, path def _make_tempdir(self): import os import tempfile + tempdir = tempfile.gettempdir() f = tempfile.NamedTemporaryFile(dir=tempdir) tempdir = os.path.join(tempdir, os.path.basename(f.name)) @@ -28,40 +32,48 @@ def _make_tempdir(self): os.mkdir(tempdir) return tempdir + class PathObservationTestCase(BaseTestCase): @property def modified_mask(self): import fsevents + return ( - fsevents.FS_ITEMCREATED + - fsevents.FS_ITEMMODIFIED + - fsevents.FS_ITEMISFILE + fsevents.FS_ITEMCREATED + + fsevents.FS_ITEMMODIFIED + + fsevents.FS_ITEMISFILE ) + @property def create_and_remove_mask(self): import fsevents + return ( - fsevents.FS_ITEMCREATED + - fsevents.FS_ITEMREMOVED + - fsevents.FS_ITEMISFILE + fsevents.FS_ITEMCREATED + + fsevents.FS_ITEMREMOVED + + fsevents.FS_ITEMISFILE ) - + def test_single_file_added(self): events = [] + def callback(*args): events.append(args) f, path = self._make_temporary() from fsevents import Stream + stream = Stream(callback, path) from fsevents import Observer + observer = Observer() observer.schedule(stream) observer.start() # add single file import time + while not observer.is_alive(): time.sleep(0.1) del events[:] @@ -77,28 +89,33 @@ def callback(*args): def test_multiple_files_added(self): events = [] + def callback(*args): events.append(args) from fsevents import Observer + observer = Observer() from fsevents import Stream + observer.start() # wait until activation import time + while not observer.is_alive(): time.sleep(0.1) time.sleep(0.1) # two files in same directory import os - path1 = os.path.realpath(self._make_tempdir()) + '/' + + path1 = os.path.realpath(self._make_tempdir()) + "/" f = self._make_temporary(path1)[0] g = self._make_temporary(path1)[0] # one file in a separate directory - path2 = os.path.realpath(self._make_tempdir()) + '/' + path2 = os.path.realpath(self._make_tempdir()) + "/" h = self._make_temporary(path2)[0] stream = Stream(callback, path1, path2) @@ -110,7 +127,15 @@ def callback(*args): g.close() h.close() time.sleep(0.2) - self.assertEqual(sorted(events), sorted([(path1, self.create_and_remove_mask), (path2, self.create_and_remove_mask)])) + self.assertEqual( + sorted(events), + sorted( + [ + (path1, self.create_and_remove_mask), + (path2, self.create_and_remove_mask), + ] + ), + ) finally: f.close() g.close() @@ -125,15 +150,18 @@ def callback(*args): def test_single_file_added_multiple_streams(self): events = [] + def callback(*args): events.append(args) f, path = self._make_temporary() from fsevents import Stream + stream1 = Stream(callback, path) stream2 = Stream(callback, path) from fsevents import Observer + observer = Observer() observer.schedule(stream1) observer.schedule(stream2) @@ -141,6 +169,7 @@ def callback(*args): # add single file import time + while not observer.is_alive(): time.sleep(0.1) time.sleep(0.1) @@ -154,21 +183,31 @@ def callback(*args): observer.unschedule(stream2) observer.join() - self.assertEqual(events, [(path, self.create_and_remove_mask), (path, self.create_and_remove_mask)]) + self.assertEqual( + events, + [ + (path, self.create_and_remove_mask), + (path, self.create_and_remove_mask), + ], + ) def test_single_file_added_with_observer_unscheduled(self): events = [] + def callback(*args): events.append(args) f, path = self._make_temporary() from fsevents import Stream + stream = Stream(callback, path) from fsevents import Observer + observer = Observer() observer.start() import time + while not observer.is_alive(): time.sleep(0.1) @@ -188,18 +227,22 @@ def callback(*args): def test_single_file_added_with_observer_rescheduled(self): events = [] + def callback(*args): events.append(args) f, path = self._make_temporary() from fsevents import Stream + stream = Stream(callback, path) from fsevents import Observer + observer = Observer() observer.start() import time + while not observer.is_alive(): time.sleep(0.1) @@ -220,21 +263,28 @@ def callback(*args): def test_single_file_added_to_subdirectory(self): events = [] + def callback(*args): events.append(args) import os + directory = self._make_tempdir() - subdirectory = os.path.realpath(os.path.join(directory, 'subdir')) + '/' + subdirectory = ( + os.path.realpath(os.path.join(directory, "subdir")) + "/" + ) os.mkdir(subdirectory) import time + time.sleep(0.1) try: from fsevents import Stream + stream = Stream(callback, directory) from fsevents import Observer + observer = Observer() observer.schedule(stream) observer.start() @@ -262,20 +312,24 @@ def callback(*args): def test_single_file_added_unschedule_then_stop(self): events = [] + def callback(*args): events.append(args) f, path = self._make_temporary() from fsevents import Stream + stream = Stream(callback, path) from fsevents import Observer + observer = Observer() observer.schedule(stream) observer.start() # add single file import time + while not observer.is_alive(): time.sleep(0.1) del events[:] @@ -291,20 +345,24 @@ def callback(*args): def test_start_then_watch(self): events = [] + def callback(*args): events.append(args) f, path = self._make_temporary() from fsevents import Stream + stream = Stream(callback, path) from fsevents import Observer + observer = Observer() observer.schedule(stream) observer.start() # add single file import time + while not observer.is_alive(): time.sleep(0.1) del events[:] @@ -320,10 +378,12 @@ def callback(*args): def test_start_no_watch(self): events = [] + def callback(*args): events.append(args) from fsevents import Observer + observer = Observer() f, path = self._make_temporary() @@ -331,6 +391,7 @@ def callback(*args): # add single file import time + while not observer.is_alive(): time.sleep(0.1) del events[:] @@ -343,46 +404,50 @@ def callback(*args): self.assertEqual(events, []) -#new cflags and since field tests + # new cflags and since field tests def test_since_stream(self): events = [] + def callback(*args): events.append(args) - + # two files in same directory import os - path1 = os.path.realpath(self._make_tempdir()) + '/' + + path1 = os.path.realpath(self._make_tempdir()) + "/" f = self._make_temporary(path1)[0] g = self._make_temporary(path1)[0] - from fsevents import Stream, FS_FLAGHISTORYDONE - - stream = Stream(callback, path1, ids = True) - + from fsevents import FS_FLAGHISTORYDONE, Stream + + stream = Stream(callback, path1, ids=True) + from fsevents import Observer + observer = Observer() observer.schedule(stream) observer.start() - - #create one file + + # create one file import time + while not observer.is_alive(): time.sleep(0.1) del events[:] f.close() time.sleep(0.2) - + # stop and join observer observer.stop() observer.unschedule(stream) observer.join() - self.assertEqual(len(events),1) + self.assertEqual(len(events), 1) self.assertEqual(events[0][:-1], (path1, self.create_and_remove_mask)) - #create a second file + # create a second file g.close() - - stream = Stream(callback, path1, since = events[0][2]) + + stream = Stream(callback, path1, since=events[0][2]) del events[:] # new observer observer = Observer() @@ -395,75 +460,88 @@ def callback(*args): observer.unschedule(stream) observer.join() - self.assertEqual(len(events),2) - #FIXME: why do events arrive here in reversed order? + self.assertEqual(len(events), 2) + # FIXME: why do events arrive here in reversed order? self.assertEqual(events[1], (path1, self.create_and_remove_mask)) self.assertEqual(events[0], (path1[:-1], FS_FLAGHISTORYDONE)) - def test_fileevent_stream(self): events = [] + def callback(*args): events.append(args) - + # two files in same directory import os - path1 = os.path.realpath(self._make_tempdir()) + '/' + + path1 = os.path.realpath(self._make_tempdir()) + "/" f = self._make_temporary(path1)[0] g = self._make_temporary(path1)[0] - from fsevents import Stream, FS_CFLAGFILEEVENTS, FS_ITEMISDIR - + from fsevents import FS_CFLAGFILEEVENTS, FS_ITEMISDIR, Stream + stream = Stream(callback, path1, flags=FS_CFLAGFILEEVENTS) - + from fsevents import Observer + observer = Observer() observer.schedule(stream) observer.start() - - #create two files (here in the same directory) + + # create two files (here in the same directory) import time + while not observer.is_alive(): time.sleep(0.1) del events[:] f.close() g.close() time.sleep(0.2) - + # stop and join observer observer.stop() observer.unschedule(stream) observer.join() import os - self.assertEqual(len(events),3) - self.assertEqual(events, [(path1[:-1], self.create_and_remove_mask|FS_ITEMISDIR), - (f.name, self.create_and_remove_mask), - (g.name, self.create_and_remove_mask)]) - + + self.assertEqual(len(events), 3) + self.assertEqual( + events, + [ + (path1[:-1], self.create_and_remove_mask | FS_ITEMISDIR), + (f.name, self.create_and_remove_mask), + (g.name, self.create_and_remove_mask), + ], + ) class FileObservationTestCase(BaseTestCase): def test_single_file_created(self): events = [] + def callback(event): events.append(event) from fsevents import Stream + stream = Stream(callback, self.tempdir, file_events=True) from fsevents import Observer + observer = Observer() observer.schedule(stream) observer.start() # add single file import time + while not observer.is_alive(): time.sleep(0.1) del events[:] time.sleep(0.1) import os + f = open(os.path.join(self.tempdir, "test"), "w") f.write("abc") f.flush() @@ -477,30 +555,36 @@ def callback(event): os.unlink(f.name) from fsevents import IN_CREATE + self.assertEqual(len(events), 1) self.assertEqual(events[0].mask, IN_CREATE) self.assertEqual(events[0].name, os.path.realpath(f.name)) def test_single_file_deleted(self): events = [] + def callback(event): events.append(event) import os + f = open(os.path.join(self.tempdir, "test"), "w") f.write("abc") f.flush() f.close() from fsevents import Stream + stream = Stream(callback, self.tempdir, file_events=True) from fsevents import Observer + observer = Observer() observer.schedule(stream) observer.start() # add single file import time + while not observer.is_alive(): time.sleep(0.1) del events[:] @@ -514,30 +598,36 @@ def callback(event): observer.join() from fsevents import IN_DELETE + self.assertEqual(len(events), 1) self.assertEqual(events[0].mask, IN_DELETE) self.assertEqual(events[0].name, os.path.realpath(f.name)) def test_single_file_moved(self): events = [] + def callback(event): events.append(event) import os + f = open(os.path.join(self.tempdir, "test"), "w") f.write("abc") f.flush() f.close() from fsevents import Stream + stream = Stream(callback, self.tempdir, file_events=True) from fsevents import Observer + observer = Observer() observer.schedule(stream) observer.start() # add single file import time + while not observer.is_alive(): time.sleep(0.1) del events[:] @@ -552,8 +642,8 @@ def callback(event): observer.join() os.unlink(new) - from fsevents import IN_MOVED_FROM - from fsevents import IN_MOVED_TO + from fsevents import IN_MOVED_FROM, IN_MOVED_TO + self.assertEqual(len(events), 2) self.assertEqual(events[0].mask, IN_MOVED_FROM) self.assertEqual(events[0].name, os.path.realpath(f.name)) @@ -563,23 +653,28 @@ def callback(event): def test_single_file_modified(self): events = [] + def callback(event): events.append(event) import os + f = open(os.path.join(self.tempdir, "test"), "w") f.write("abc") f.flush() from fsevents import Stream + stream = Stream(callback, self.tempdir, file_events=True) from fsevents import Observer + observer = Observer() observer.schedule(stream) observer.start() # add single file import time + while not observer.is_alive(): time.sleep(0.1) del events[:] @@ -596,31 +691,37 @@ def callback(event): os.unlink(f.name) from fsevents import IN_MODIFY + self.assertEqual(len(events), 1) self.assertEqual(events[0].mask, IN_MODIFY) self.assertEqual(events[0].name, os.path.realpath(f.name)) def test_single_file_created_and_modified(self): events = [] + def callback(event): events.append(event) from fsevents import Stream + stream = Stream(callback, self.tempdir, file_events=True) from fsevents import Observer + observer = Observer() observer.schedule(stream) observer.start() # add single file import time + while not observer.is_alive(): time.sleep(0.1) del events[:] time.sleep(2.1) import os + f = open(os.path.join(self.tempdir, "test"), "w") f.write("abc") f.flush() @@ -639,6 +740,7 @@ def callback(event): os.unlink(f.name) from fsevents import IN_CREATE, IN_MODIFY + self.assertEqual(len(events), 2) self.assertEqual(events[0].mask, IN_CREATE) self.assertEqual(events[0].name, os.path.realpath(f.name)) @@ -647,27 +749,33 @@ def callback(event): def test_single_directory_deleted(self): events = [] + def callback(event): events.append(event) import os + new1 = os.path.join(self.tempdir, "newdir1") new2 = os.path.join(self.tempdir, "newdir2") try: os.mkdir(new1) os.mkdir(new2) import time + time.sleep(0.2) from fsevents import Stream + stream = Stream(callback, self.tempdir, file_events=True) from fsevents import Observer + observer = Observer() observer.schedule(stream) observer.start() # add single file import time + while not observer.is_alive(): time.sleep(0.1) del events[:] @@ -681,6 +789,7 @@ def callback(event): observer.join() from fsevents import IN_DELETE + self.assertEqual(len(events), 1) self.assertEqual(events[0].mask, IN_DELETE) self.assertEqual(events[0].name, os.path.realpath(new2)) @@ -689,12 +798,14 @@ def callback(event): def test_existing_directories_are_not_reported(self): import os - from fsevents import Stream, Observer + + from fsevents import Observer, Stream events = [] + def callback(event): events.append(event) - + stream = Stream(callback, self.tempdir, file_events=True) new1 = os.path.join(self.tempdir, "newdir1") new2 = os.path.join(self.tempdir, "newdir2") @@ -704,6 +815,7 @@ def callback(event): observer.start() import time + while not observer.is_alive(): time.sleep(0.1) del events[:] @@ -716,6 +828,7 @@ def callback(event): observer.join() from fsevents import IN_CREATE + self.assertEqual(len(events), 1) self.assertEqual(events[0].mask, IN_CREATE) self.assertEqual(events[0].name, os.path.realpath(new2)) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..c4af272 --- /dev/null +++ b/tox.ini @@ -0,0 +1,39 @@ +[tox] +minversion = 3.18 +envlist = + lint + py38 + py39 + py310 + py311 + py312 + +[testenv] +usedevelop = true +deps = + build + wheel + pytest + setuptools +commands = + python -m unittest -v +extras = + test +setenv = + LOGNAME=dummy + +[testenv:lint] +basepython = python3 +skip_install = true +commands = + isort --check-only --diff . + flake8 -v tests.py fsevents.py + check-manifest + check-python-versions +deps = + check-manifest + check-python-versions >= 0.19.1 + wheel + flake8 + isort +