From 7503d34c1f49a96c7ab31b177f6f3a3ffc8ab049 Mon Sep 17 00:00:00 2001
From: Ivan Gotovchits <ivg@ieee.org>
Date: Sun, 28 Jul 2024 09:45:11 -0400
Subject: [PATCH 1/7] fix: possible race condition in `AutoRestartTrick`
 (#1002)

* fixes a possible race condition in AutoRestartTrick

Just a long shot for a failure observed on #998. My hypothesis is that
when we stop ProcessWatcher before we restart the process manually, we
don't yield to it and immediately kill the process. Next, when the
ProcessWatcher thread is woken up, we have to conditions ready - the
popen_obj and stopped_event, see the corresponding code, ``` while
True: if self.popen_obj.poll() is not None: break if
self.stopped_event.wait(timeout=0.1): return ```

And desipte that `stopped_event` is set, we first check for
`popen_obj` and trigger the process restart.

We can also make the ProcessWatcher logic more robust, by checking if
we are stopped before calling the termination callback, e.g.,

```
        try:
            if not self.stopped_event.is_set():
                self.process_termination_callback()
        except Exception:
            logger.exception("Error calling process termination callback")
```

I am not 100% sure about that, as I don't really know what semantics
is expected from ProcessWatcher by other users. But at least the
AutoRestarter expects this semantics - i.e., a watcher shall not call
any events after it was stopped.

* tries an alternative solution

i.e., don't send events if stopped
---
 src/watchdog/utils/process_watcher.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/watchdog/utils/process_watcher.py b/src/watchdog/utils/process_watcher.py
index dd4ece58..46717bc7 100644
--- a/src/watchdog/utils/process_watcher.py
+++ b/src/watchdog/utils/process_watcher.py
@@ -21,6 +21,7 @@ def run(self):
                 return
 
         try:
-            self.process_termination_callback()
+            if not self.stopped_event.is_set():
+                self.process_termination_callback()
         except Exception:
             logger.exception("Error calling process termination callback")

From cff604e90bc562dee16a32efbe6b4471d3e64105 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= <contact@tiger-222.fr>
Date: Sun, 28 Jul 2024 22:05:47 +0200
Subject: [PATCH 2/7] feat: Python 3.13 support (#1052)

---
 .github/workflows/tests.yml                      |  1 +
 changelog.rst                                    |  1 +
 requirements-tests.txt                           |  2 +-
 setup.py                                         |  1 +
 src/watchdog/observers/read_directory_changes.py | 10 +++++-----
 tests/test_observers_winapi.py                   |  2 +-
 tox.ini                                          |  2 +-
 7 files changed, 11 insertions(+), 8 deletions(-)

diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 2c572add..9dd02e0f 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -44,6 +44,7 @@ jobs:
           - "3.10"
           - "3.11"
           - "3.12"
+          - "3.13-dev"
           - "pypy-3.8"
           - "pypy-3.9"
         include:
diff --git a/changelog.rst b/changelog.rst
index ff507fef..df7e6071 100644
--- a/changelog.rst
+++ b/changelog.rst
@@ -8,6 +8,7 @@ Changelog
 
 2024-xx-xx • `full history <https://github.com/gorakhargosh/watchdog/compare/v4.0.1...HEAD>`__
 
+- Add support for Python 3.13 (`#1052 <https://github.com/gorakhargosh/watchdog/pull/1052>`__)
 - [core] Run ``ruff``, apply several fixes (`#1033 <https://github.com/gorakhargosh/watchdog/pull/1033>`__)
 - [fsevents] Add missing ``event_filter`` keyword-argument to ``FSEventsObserver.schedule()`` (`#1049 <https://github.com/gorakhargosh/watchdog/pull/1049>`__)
 - Thanks to our beloved contributors: @BoboTiG
diff --git a/requirements-tests.txt b/requirements-tests.txt
index 60e375bf..66d0b58b 100644
--- a/requirements-tests.txt
+++ b/requirements-tests.txt
@@ -1,4 +1,4 @@
-eventlet
+eventlet; python_version < "3.13"
 flaky
 pytest
 pytest-cov
diff --git a/setup.py b/setup.py
index 320d951c..d1159e81 100644
--- a/setup.py
+++ b/setup.py
@@ -134,6 +134,7 @@
         "Programming Language :: Python :: 3.10",
         "Programming Language :: Python :: 3.11",
         "Programming Language :: Python :: 3.12",
+        "Programming Language :: Python :: 3.13",
         "Programming Language :: Python :: Implementation :: PyPy",
         "Programming Language :: C",
         "Topic :: Software Development :: Libraries",
diff --git a/src/watchdog/observers/read_directory_changes.py b/src/watchdog/observers/read_directory_changes.py
index be84bac1..e96fb860 100644
--- a/src/watchdog/observers/read_directory_changes.py
+++ b/src/watchdog/observers/read_directory_changes.py
@@ -47,10 +47,10 @@ class WindowsApiEmitter(EventEmitter):
     def __init__(self, event_queue, watch, timeout=DEFAULT_EMITTER_TIMEOUT, event_filter=None):
         super().__init__(event_queue, watch, timeout, event_filter)
         self._lock = threading.Lock()
-        self._handle = None
+        self._whandle = None
 
     def on_thread_start(self):
-        self._handle = get_directory_handle(self.watch.path)
+        self._whandle = get_directory_handle(self.watch.path)
 
     if platform.python_implementation() == "PyPy":
 
@@ -62,11 +62,11 @@ def start(self):
             sleep(0.01)
 
     def on_thread_stop(self):
-        if self._handle:
-            close_directory_handle(self._handle)
+        if self._whandle:
+            close_directory_handle(self._whandle)
 
     def _read_events(self):
-        return read_events(self._handle, self.watch.path, self.watch.is_recursive)
+        return read_events(self._whandle, self.watch.path, self.watch.is_recursive)
 
     def queue_events(self, timeout):
         winapi_events = self._read_events()
diff --git a/tests/test_observers_winapi.py b/tests/test_observers_winapi.py
index d9de163e..5c332d70 100644
--- a/tests/test_observers_winapi.py
+++ b/tests/test_observers_winapi.py
@@ -118,7 +118,7 @@ def test_root_deleted(event_queue, emitter):
         File "watchdog\observers\read_directory_changes.py", line 76, in queue_events
             winapi_events = self._read_events()
         File "watchdog\observers\read_directory_changes.py", line 73, in _read_events
-            return read_events(self._handle, self.watch.path, self.watch.is_recursive)
+            return read_events(self._whandle, self.watch.path, self.watch.is_recursive)
         File "watchdog\observers\winapi.py", line 387, in read_events
             buf, nbytes = read_directory_changes(handle, path, recursive)
         File "watchdog\observers\winapi.py", line 340, in read_directory_changes
diff --git a/tox.ini b/tox.ini
index 1a10f7ee..e74a35be 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
 [tox]
 envlist =
-    py3{8,9,10,11,12}
+    py3{8,9,10,11,12,13}
     pypy3
     docs
     types

From 6a33516b8cb87a04a2e208c7a794e58f40f6f9b9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= <contact@tiger-222.fr>
Date: Sun, 28 Jul 2024 22:19:06 +0200
Subject: [PATCH 3/7] docs: tweak

---
 changelog.rst | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/changelog.rst b/changelog.rst
index df7e6071..eedc650f 100644
--- a/changelog.rst
+++ b/changelog.rst
@@ -9,9 +9,14 @@ Changelog
 2024-xx-xx • `full history <https://github.com/gorakhargosh/watchdog/compare/v4.0.1...HEAD>`__
 
 - Add support for Python 3.13 (`#1052 <https://github.com/gorakhargosh/watchdog/pull/1052>`__)
-- [core] Run ``ruff``, apply several fixes (`#1033 <https://github.com/gorakhargosh/watchdog/pull/1033>`__)
+- Run ``ruff``, apply several fixes (`#1033 <https://github.com/gorakhargosh/watchdog/pull/1033>`__)
+- [core] Remove execution rights from ``events.py``
+- [documentation] Update ``PatternMatchingEventHandler`` docstrings (`#1048 <https://github.com/gorakhargosh/watchdog/pull/1048>`__)
+- [documentation] Simplify the quickstart example (`#1047 <https://github.com/gorakhargosh/watchdog/pull/1047>`__)
 - [fsevents] Add missing ``event_filter`` keyword-argument to ``FSEventsObserver.schedule()`` (`#1049 <https://github.com/gorakhargosh/watchdog/pull/1049>`__)
-- Thanks to our beloved contributors: @BoboTiG
+- [utils] Fix a possible race condition in ``AutoRestartTrick`` (`#1002 <https://github.com/gorakhargosh/watchdog/pull/1002>`__)
+- [watchmedo] Remove execution rights from ``watchmedo.py``
+- Thanks to our beloved contributors: @BoboTiG, @nbelakovski, @ivg
 
 4.0.1
 ~~~~~

From aac4328e593e8764c6a3face67961f955d64bb74 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= <contact@tiger-222.fr>
Date: Sun, 28 Jul 2024 22:21:25 +0200
Subject: [PATCH 4/7] chore: add git attributes file

---
 .gitattributes | 6 ++++++
 1 file changed, 6 insertions(+)
 create mode 100644 .gitattributes

diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..f98c0b07
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,6 @@
+# Language aware diff headers
+# https://tekin.co.uk/2020/10/better-git-diff-output-for-ruby-python-elixir-and-more
+# https://gist.github.com/tekin/12500956bd56784728e490d8cef9cb81
+# https://github.com/git/git/blob/master/userdiff.c
+*.c diff=cpp
+*.py diff=python

From 9c5a43241f774930e958447b61a83b8bbd94fb21 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= <contact@tiger-222.fr>
Date: Sun, 11 Aug 2024 09:29:46 +0200
Subject: [PATCH 5/7] Version 4.0.2

---
 changelog.rst | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/changelog.rst b/changelog.rst
index eedc650f..339748fb 100644
--- a/changelog.rst
+++ b/changelog.rst
@@ -3,13 +3,13 @@
 Changelog
 ---------
 
-4.0.2 (dev)
-~~~~~~~~~~~
+4.0.2
+~~~~~
 
-2024-xx-xx • `full history <https://github.com/gorakhargosh/watchdog/compare/v4.0.1...HEAD>`__
+2024-08-11 • `full history <https://github.com/gorakhargosh/watchdog/compare/v4.0.1...v4.0.2>`__
 
 - Add support for Python 3.13 (`#1052 <https://github.com/gorakhargosh/watchdog/pull/1052>`__)
-- Run ``ruff``, apply several fixes (`#1033 <https://github.com/gorakhargosh/watchdog/pull/1033>`__)
+- [core] Run ``ruff``, apply several fixes (`#1033 <https://github.com/gorakhargosh/watchdog/pull/1033>`__)
 - [core] Remove execution rights from ``events.py``
 - [documentation] Update ``PatternMatchingEventHandler`` docstrings (`#1048 <https://github.com/gorakhargosh/watchdog/pull/1048>`__)
 - [documentation] Simplify the quickstart example (`#1047 <https://github.com/gorakhargosh/watchdog/pull/1047>`__)

From a318f3919c1a1b66c0cc01f28e331d5ca612130b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= <contact@tiger-222.fr>
Date: Sun, 11 Aug 2024 09:33:00 +0200
Subject: [PATCH 6/7] Bump the version

---
 changelog.rst              | 8 ++++++++
 docs/source/global.rst.inc | 2 +-
 src/watchdog/version.py    | 4 ++--
 3 files changed, 11 insertions(+), 3 deletions(-)

diff --git a/changelog.rst b/changelog.rst
index 339748fb..467ae4d5 100644
--- a/changelog.rst
+++ b/changelog.rst
@@ -3,6 +3,14 @@
 Changelog
 ---------
 
+5.0.0 (dev)
+~~~~~~~~~~~
+
+2024-xx-xx • `full history <https://github.com/gorakhargosh/watchdog/compare/v4.0.2...HEAD>`__
+
+- 
+- Thanks to our beloved contributors: @BoboTiG
+
 4.0.2
 ~~~~~
 
diff --git a/docs/source/global.rst.inc b/docs/source/global.rst.inc
index 59d46c6d..4cfeaa42 100644
--- a/docs/source/global.rst.inc
+++ b/docs/source/global.rst.inc
@@ -4,7 +4,7 @@
 .. |author_email| replace:: yesudeep@gmail.com
 .. |copyright| replace:: Copyright 2012-2024 Google, Inc & contributors.
 .. |project_name| replace:: ``watchdog``
-.. |project_version| replace:: 4.0.2
+.. |project_version| replace:: 5.0.0
 
 .. _issue tracker: https://github.com/gorakhargosh/watchdog/issues
 .. _code repository: https://github.com/gorakhargosh/watchdog
diff --git a/src/watchdog/version.py b/src/watchdog/version.py
index 760a7246..d1792b2d 100644
--- a/src/watchdog/version.py
+++ b/src/watchdog/version.py
@@ -18,9 +18,9 @@
 
 # When updating this version number, please update the
 # ``docs/source/global.rst.inc`` file as well.
-VERSION_MAJOR = 4
+VERSION_MAJOR = 5
 VERSION_MINOR = 0
-VERSION_BUILD = 2
+VERSION_BUILD = 0
 VERSION_INFO = (VERSION_MAJOR, VERSION_MINOR, VERSION_BUILD)
 VERSION_STRING = f"{VERSION_MAJOR}.{VERSION_MINOR}.{VERSION_BUILD}"
 

From 2872c7e4983b826a38d76c7dfc162653fcd5cd23 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micka=C3=ABl=20Schoentgen?= <contact@tiger-222.fr>
Date: Sun, 11 Aug 2024 10:14:00 +0200
Subject: [PATCH 7/7] feat!: Enable `disallow_untyped_calls` Mypy rule + drop
 Python 3.8 support (#1055)

---
 .cirrus.yml                              | 16 ++++-----
 .github/workflows/build-and-publish.yml  |  1 -
 .github/workflows/tests.yml              |  8 -----
 README.rst                               |  4 +--
 changelog.rst                            |  6 +++-
 docs/source/index.rst                    |  2 +-
 docs/source/installation.rst             |  2 +-
 pyproject.toml                           |  8 ++---
 setup.py                                 |  3 +-
 src/watchdog/observers/__init__.py       |  4 +--
 src/watchdog/observers/api.py            |  6 +---
 src/watchdog/observers/fsevents2.py      | 12 +++----
 src/watchdog/observers/inotify.py        |  3 +-
 src/watchdog/observers/inotify_buffer.py |  4 +--
 src/watchdog/observers/inotify_c.py      |  4 +--
 src/watchdog/observers/winapi.py         | 22 ++++++-------
 src/watchdog/utils/__init__.py           | 11 -------
 src/watchdog/utils/delayed_queue.py      |  4 +--
 src/watchdog/utils/dirsnapshot.py        | 36 +++++++++++---------
 src/watchdog/utils/patterns.py           | 26 ++++++++++++---
 src/watchdog/utils/platform.py           | 10 +++---
 src/watchdog/watchmedo.py                | 42 +++++++++++++-----------
 tests/test_observers_winapi.py           |  2 +-
 tests/utils.py                           |  8 ++---
 tox.ini                                  |  2 +-
 25 files changed, 124 insertions(+), 122 deletions(-)

diff --git a/.cirrus.yml b/.cirrus.yml
index b305a1ad..186765ff 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -6,16 +6,16 @@ task:
       image_family: freebsd-12-2
       
   install_script:
-    - pkg install -y python38 py38-sqlite3
+    - pkg install -y python39 py39-sqlite3
     # Print the Python version, only to be sure we are running the version we want
-    - python3.8 -c 'import platform; print("Python", platform.python_version())'
+    - python3.9 -c 'import platform; print("Python", platform.python_version())'
     # Check SQLite3 is installed
-    - python3.8 -c 'import sqlite3; print("SQLite3", sqlite3.version)'
+    - python3.9 -c 'import sqlite3; print("SQLite3", sqlite3.version)'
   setup_script:
-    - python3.8 -m ensurepip
-    - python3.8 -m pip install -U pip
-    - python3.8 -m pip install -r requirements-tests.txt
+    - python3.9 -m ensurepip
+    - python3.9 -m pip install -U pip
+    - python3.9 -m pip install -r requirements-tests.txt
   lint_script:
-    - python3.8 -m ruff src
+    - python3.9 -m ruff src
   tests_script:
-    - python3.8 -bb -m pytest tests
+    - python3.9 -bb -m pytest tests
diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml
index de4371f4..d05f8790 100644
--- a/.github/workflows/build-and-publish.yml
+++ b/.github/workflows/build-and-publish.yml
@@ -53,7 +53,6 @@ jobs:
       - name: Build wheels
         run: python -m cibuildwheel
         env:
-          CIBW_SKIP: "cp36-*" # skip 3.6 wheels
           CIBW_ARCHS_MACOS: "x86_64 universal2 arm64"
       - name: Artifacts list
         run: ls -l wheelhouse
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 9dd02e0f..6afed287 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -39,13 +39,11 @@ jobs:
             emoji: 🪟
             runs-on: [windows-latest]
         python:
-          - "3.8"
           - "3.9"
           - "3.10"
           - "3.11"
           - "3.12"
           - "3.13-dev"
-          - "pypy-3.8"
           - "pypy-3.9"
         include:
           - tox:
@@ -67,15 +65,9 @@ jobs:
               emoji: 🐧
               runs-on: [ubuntu-latest]
         exclude:
-          - os:
-              matrix: macos
-            python: "pypy-3.8"
           - os:
               matrix: macos
             python: "pypy-3.9"
-          - os:
-              matrix: windows
-            python: "pypy-3.8"
           - os:
               matrix: windows
             python: "pypy-3.9"
diff --git a/README.rst b/README.rst
index 2d6e0dd4..41656cdf 100755
--- a/README.rst
+++ b/README.rst
@@ -6,7 +6,7 @@ Watchdog
 
 Python API and shell utilities to monitor file system events.
 
-Works on 3.8+.
+Works on 3.9+.
 
 Example API Usage
 -----------------
@@ -211,7 +211,7 @@ appropriate observer like in the example above, do::
 Dependencies
 ------------
 
-1. Python 3.8 or above.
+1. Python 3.9 or above.
 2. XCode_ (only on macOS when installing from sources)
 3. PyYAML_ (only for ``watchmedo``)
 
diff --git a/changelog.rst b/changelog.rst
index 467ae4d5..10017dc2 100644
--- a/changelog.rst
+++ b/changelog.rst
@@ -8,7 +8,11 @@ Changelog
 
 2024-xx-xx • `full history <https://github.com/gorakhargosh/watchdog/compare/v4.0.2...HEAD>`__
 
-- 
+- Drop support for Python 3.8 (`#1055 <https://github.com/gorakhargosh/watchdog/pull/1055>`__)
+- [core] Enable ``disallow_untyped_calls`` Mypy rule (`#1055 <https://github.com/gorakhargosh/watchdog/pull/1055>`__)
+- [core] Deleted the ``BaseObserverSubclassCallable`` class. Use ``type[BaseObserver]`` directly (`#1055 <https://github.com/gorakhargosh/watchdog/pull/1055>`__)
+- [inotify] Renamed the ``inotify_event_struct`` class to ``InotifyEventStruct`` (`#1055 <https://github.com/gorakhargosh/watchdog/pull/1055>`__)
+- [windows] Renamed the ``FILE_NOTIFY_INFORMATION`` class to ``FileNotifyInformation`` (`#1055 <https://github.com/gorakhargosh/watchdog/pull/1055>`__)
 - Thanks to our beloved contributors: @BoboTiG
 
 4.0.2
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 8365309a..1df5b69d 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -11,7 +11,7 @@ Watchdog
 
 Python API library and shell utilities to monitor file system events.
 
-Works on 3.8+.
+Works on 3.9+.
 
 Directory monitoring made easy with
 -----------------------------------
diff --git a/docs/source/installation.rst b/docs/source/installation.rst
index e1e331d6..a445e553 100644
--- a/docs/source/installation.rst
+++ b/docs/source/installation.rst
@@ -4,7 +4,7 @@
 
 Installation
 ============
-|project_name| requires 3.8+ to work. See a list of :ref:`installation-dependencies`.
+|project_name| requires 3.9+ to work. See a list of :ref:`installation-dependencies`.
 
 Installing from PyPI using pip
 ------------------------------
diff --git a/pyproject.toml b/pyproject.toml
index 9be9550e..63901d21 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,7 +11,7 @@ follow_imports = "skip"
 # Ensure full coverage
 #disallow_untyped_defs = true [TODO]
 disallow_incomplete_defs = true
-#disallow_untyped_calls = true [TODO]
+disallow_untyped_calls = true
 
 # Restrict dynamic typing (a little)
 # e.g. `x: List[Any]` or x: List`
@@ -31,7 +31,7 @@ addopts = """
 [tool.ruff]
 line-length = 120
 indent-width = 4
-target-version = "py38"
+target-version = "py39"
 
 [tool.ruff.lint]
 extend-select = ["ALL"]
@@ -53,14 +53,14 @@ ignore = [
     "FBT",
     "FIX",
     "ISC001",
-    "N",  # Requires a major version number bump
+    "N818",
     "PERF203",  # TODO
     "PL",
     "PTH",  # TODO?
     "S",
     "TD",
     "TRY003",
-    "UP",  # TODO when minimum python version will be 3.10
+    "UP",
 ]
 fixable = ["ALL"]
 
diff --git a/setup.py b/setup.py
index d1159e81..923cd532 100644
--- a/setup.py
+++ b/setup.py
@@ -129,7 +129,6 @@
         "Programming Language :: Python",
         "Programming Language :: Python :: 3",
         "Programming Language :: Python :: 3 :: Only",
-        "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
         "Programming Language :: Python :: 3.10",
         "Programming Language :: Python :: 3.11",
@@ -155,6 +154,6 @@
             "watchmedo = watchdog.watchmedo:main [watchmedo]",
         ]
     },
-    python_requires=">=3.8",
+    python_requires=">=3.9",
     zip_safe=False,
 )
diff --git a/src/watchdog/observers/__init__.py b/src/watchdog/observers/__init__.py
index 293a6f6b..f1959b2f 100644
--- a/src/watchdog/observers/__init__.py
+++ b/src/watchdog/observers/__init__.py
@@ -58,10 +58,10 @@
 from watchdog.utils import UnsupportedLibc, platform
 
 if TYPE_CHECKING:
-    from watchdog.observers.api import BaseObserverSubclassCallable
+    from watchdog.observers.api import BaseObserver
 
 
-def _get_observer_cls() -> BaseObserverSubclassCallable:
+def _get_observer_cls() -> type[BaseObserver]:
     if platform.is_linux():
         with contextlib.suppress(UnsupportedLibc):
             from watchdog.observers.inotify import InotifyObserver
diff --git a/src/watchdog/observers/api.py b/src/watchdog/observers/api.py
index 4dc7d3c6..df4a683a 100644
--- a/src/watchdog/observers/api.py
+++ b/src/watchdog/observers/api.py
@@ -20,7 +20,7 @@
 import threading
 from pathlib import Path
 
-from watchdog.utils import BaseThread, Protocol
+from watchdog.utils import BaseThread
 from watchdog.utils.bricks import SkipRepeatsQueue
 
 DEFAULT_EMITTER_TIMEOUT = 1  # in seconds.
@@ -385,7 +385,3 @@ def dispatch_events(self, event_queue):
                 if handler in self._handlers.get(watch, []):
                     handler.dispatch(event)
         event_queue.task_done()
-
-
-class BaseObserverSubclassCallable(Protocol):
-    def __call__(self, timeout: float = ...) -> BaseObserver: ...
diff --git a/src/watchdog/observers/fsevents2.py b/src/watchdog/observers/fsevents2.py
index 71941742..72e05bfd 100644
--- a/src/watchdog/observers/fsevents2.py
+++ b/src/watchdog/observers/fsevents2.py
@@ -25,7 +25,7 @@
 import unicodedata
 import warnings
 from threading import Thread
-from typing import List, Optional, Type
+from typing import Optional
 
 # pyobjc
 import AppKit
@@ -81,7 +81,7 @@ class FSEventsQueue(Thread):
 
     def __init__(self, path):
         Thread.__init__(self)
-        self._queue: queue.Queue[Optional[List[NativeEvent]]] = queue.Queue()
+        self._queue: queue.Queue[Optional[list[NativeEvent]]] = queue.Queue()
         self._run_loop = None
 
         if isinstance(path, bytes):
@@ -123,9 +123,9 @@ def stop(self):
         if self._run_loop is not None:
             CFRunLoopStop(self._run_loop)
 
-    def _callback(self, streamRef, clientCallBackInfo, numEvents, eventPaths, eventFlags, eventIDs):
-        events = [NativeEvent(path, flags, _id) for path, flags, _id in zip(eventPaths, eventFlags, eventIDs)]
-        logger.debug("FSEvents callback. Got %d events:", numEvents)
+    def _callback(self, stream_ref, client_callback_info, num_events, event_paths, event_flags, event_ids):
+        events = [NativeEvent(path, flags, _id) for path, flags, _id in zip(event_paths, event_flags, event_ids)]
+        logger.debug("FSEvents callback. Got %d events:", num_events)
         for e in events:
             logger.debug(e)
         self._queue.put(events)
@@ -195,7 +195,7 @@ def queue_events(self, timeout):
         while i < len(events):
             event = events[i]
 
-            cls: Type[FileSystemEvent]
+            cls: type[FileSystemEvent]
             # For some reason the create and remove flags are sometimes also
             # set for rename and modify type events, so let those take
             # precedence.
diff --git a/src/watchdog/observers/inotify.py b/src/watchdog/observers/inotify.py
index f45e339c..a69f9236 100644
--- a/src/watchdog/observers/inotify.py
+++ b/src/watchdog/observers/inotify.py
@@ -68,7 +68,6 @@
 import logging
 import os
 import threading
-from typing import Type
 
 from watchdog.events import (
     DirCreatedEvent,
@@ -141,7 +140,7 @@ def queue_events(self, timeout, full_events=False):
             if event is None:
                 return
 
-            cls: Type[FileSystemEvent]
+            cls: type[FileSystemEvent]
             if isinstance(event, tuple):
                 move_from, move_to = event
                 src_path = self._decode_path(move_from.src_path)
diff --git a/src/watchdog/observers/inotify_buffer.py b/src/watchdog/observers/inotify_buffer.py
index dbb05aa2..9dc91179 100644
--- a/src/watchdog/observers/inotify_buffer.py
+++ b/src/watchdog/observers/inotify_buffer.py
@@ -15,7 +15,7 @@
 from __future__ import annotations
 
 import logging
-from typing import TYPE_CHECKING, List, Tuple, Union
+from typing import TYPE_CHECKING, Union
 
 from watchdog.observers.inotify_c import Inotify, InotifyEvent
 from watchdog.utils import BaseThread
@@ -54,7 +54,7 @@ def close(self):
 
     def _group_events(self, event_list):
         """Group any matching move events"""
-        grouped: List[Union[InotifyEvent, Tuple[InotifyEvent, InotifyEvent]]] = []
+        grouped: list[Union[InotifyEvent, tuple[InotifyEvent, InotifyEvent]]] = []
         for inotify_event in event_list:
             logger.debug("in-event %s", inotify_event)
 
diff --git a/src/watchdog/observers/inotify_c.py b/src/watchdog/observers/inotify_c.py
index 0935aaa5..63b2a890 100644
--- a/src/watchdog/observers/inotify_c.py
+++ b/src/watchdog/observers/inotify_c.py
@@ -113,7 +113,7 @@ class InotifyConstants:
 )
 
 
-class inotify_event_struct(ctypes.Structure):
+class InotifyEventStruct(ctypes.Structure):
     """Structure representation of the inotify_event structure
     (used in buffer size calculations)::
 
@@ -135,7 +135,7 @@ class inotify_event_struct(ctypes.Structure):
     )
 
 
-EVENT_SIZE = ctypes.sizeof(inotify_event_struct)
+EVENT_SIZE = ctypes.sizeof(InotifyEventStruct)
 DEFAULT_NUM_EVENTS = 2048
 DEFAULT_EVENT_BUFFER_SIZE = DEFAULT_NUM_EVENTS * (EVENT_SIZE + 16)
 
diff --git a/src/watchdog/observers/winapi.py b/src/watchdog/observers/winapi.py
index b8576523..3e0514cc 100644
--- a/src/watchdog/observers/winapi.py
+++ b/src/watchdog/observers/winapi.py
@@ -230,7 +230,7 @@ def _errcheck_dword(value, func, args):
 )
 
 
-class FILE_NOTIFY_INFORMATION(ctypes.Structure):
+class FileNotifyInformation(ctypes.Structure):
     _fields_ = (
         ("NextEntryOffset", ctypes.wintypes.DWORD),
         ("Action", ctypes.wintypes.DWORD),
@@ -240,7 +240,7 @@ class FILE_NOTIFY_INFORMATION(ctypes.Structure):
     )
 
 
-LPFNI = ctypes.POINTER(FILE_NOTIFY_INFORMATION)
+LPFNI = ctypes.POINTER(FileNotifyInformation)
 
 
 # We don't need to recalculate these flags every time a call is made to
@@ -281,19 +281,19 @@ class FILE_NOTIFY_INFORMATION(ctypes.Structure):
 PATH_BUFFER_SIZE = 2048
 
 
-def _parse_event_buffer(readBuffer, nBytes):
+def _parse_event_buffer(read_buffer, n_bytes):
     results = []
-    while nBytes > 0:
-        fni = ctypes.cast(readBuffer, LPFNI)[0]
-        ptr = ctypes.addressof(fni) + FILE_NOTIFY_INFORMATION.FileName.offset
+    while n_bytes > 0:
+        fni = ctypes.cast(read_buffer, LPFNI)[0]
+        ptr = ctypes.addressof(fni) + FileNotifyInformation.FileName.offset
         # filename = ctypes.wstring_at(ptr, fni.FileNameLength)
         filename = ctypes.string_at(ptr, fni.FileNameLength)
         results.append((fni.Action, filename.decode("utf-16")))
-        numToSkip = fni.NextEntryOffset
-        if numToSkip <= 0:
+        num_to_skip = fni.NextEntryOffset
+        if num_to_skip <= 0:
             break
-        readBuffer = readBuffer[numToSkip:]
-        nBytes -= numToSkip  # numToSkip is long. nBytes should be long too.
+        read_buffer = read_buffer[num_to_skip:]
+        n_bytes -= num_to_skip  # numToSkip is long. nBytes should be long too.
     return results
 
 
@@ -309,7 +309,7 @@ def _is_observed_path_deleted(handle, path):
 def _generate_observed_path_deleted_event():
     # Create synthetic event for notify that observed directory is deleted
     path = ctypes.create_unicode_buffer(".")
-    event = FILE_NOTIFY_INFORMATION(0, FILE_ACTION_DELETED_SELF, len(path), path.value.encode("utf-8"))
+    event = FileNotifyInformation(0, FILE_ACTION_DELETED_SELF, len(path), path.value.encode("utf-8"))
     event_size = ctypes.sizeof(event)
     buff = ctypes.create_string_buffer(PATH_BUFFER_SIZE)
     ctypes.memmove(buff, ctypes.addressof(event), event_size)
diff --git a/src/watchdog/utils/__init__.py b/src/watchdog/utils/__init__.py
index ece812ff..be62f346 100644
--- a/src/watchdog/utils/__init__.py
+++ b/src/watchdog/utils/__init__.py
@@ -32,7 +32,6 @@
 
 import sys
 import threading
-from typing import TYPE_CHECKING
 
 
 class UnsupportedLibc(Exception):
@@ -130,13 +129,3 @@ def load_class(dotted_path):
         # return klass(*args, **kwargs)
 
     raise AttributeError(f"Module {module_name} does not have class attribute {klass_name}")
-
-
-if TYPE_CHECKING or sys.version_info >= (3, 8):
-    # using `as` to explicitly re-export this since this is a compatibility layer
-    from typing import Protocol as Protocol
-else:
-    # Provide a dummy Protocol class when not available from stdlib.  Should be used
-    # only for hinting.  This could be had from typing_protocol, but not worth adding
-    # the _first_ dependency just for this.
-    class Protocol: ...
diff --git a/src/watchdog/utils/delayed_queue.py b/src/watchdog/utils/delayed_queue.py
index e6f11836..fbda2a8a 100644
--- a/src/watchdog/utils/delayed_queue.py
+++ b/src/watchdog/utils/delayed_queue.py
@@ -17,7 +17,7 @@
 import threading
 import time
 from collections import deque
-from typing import Callable, Deque, Generic, Optional, Tuple, TypeVar
+from typing import Callable, Generic, Optional, TypeVar
 
 T = TypeVar("T")
 
@@ -27,7 +27,7 @@ def __init__(self, delay):
         self.delay_sec = delay
         self._lock = threading.Lock()
         self._not_empty = threading.Condition(self._lock)
-        self._queue: Deque[Tuple[T, float, bool]] = deque()
+        self._queue: deque[tuple[T, float, bool]] = deque()
         self._closed = False
 
     def put(self, element: T, delay: bool = False) -> None:
diff --git a/src/watchdog/utils/dirsnapshot.py b/src/watchdog/utils/dirsnapshot.py
index 77f03187..0e722464 100644
--- a/src/watchdog/utils/dirsnapshot.py
+++ b/src/watchdog/utils/dirsnapshot.py
@@ -51,7 +51,11 @@
 import errno
 import os
 from stat import S_ISDIR
-from typing import Any, Callable, Iterator, List, Optional, Tuple
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from collections.abc import Iterator
+    from typing import Any, Callable, Optional
 
 
 class DirectorySnapshotDiff:
@@ -90,12 +94,12 @@ def __init__(
 
         if ignore_device:
 
-            def get_inode(directory: DirectorySnapshot, full_path: str) -> int | Tuple[int, int]:
+            def get_inode(directory: DirectorySnapshot, full_path: str) -> int | tuple[int, int]:
                 return directory.inode(full_path)[0]
 
         else:
 
-            def get_inode(directory: DirectorySnapshot, full_path: str) -> int | Tuple[int, int]:
+            def get_inode(directory: DirectorySnapshot, full_path: str) -> int | tuple[int, int]:
                 return directory.inode(full_path)
 
         # check that all unchanged paths have the same inode
@@ -105,7 +109,7 @@ def get_inode(directory: DirectorySnapshot, full_path: str) -> int | Tuple[int,
                 deleted.add(path)
 
         # find moved paths
-        moved: set[Tuple[str, str]] = set()
+        moved: set[tuple[str, str]] = set()
         for path in set(deleted):
             inode = ref.inode(path)
             new_path = snapshot.path(inode)
@@ -165,22 +169,22 @@ def __repr__(self) -> str:
         )
 
     @property
-    def files_created(self) -> List[str]:
+    def files_created(self) -> list[str]:
         """List of files that were created."""
         return self._files_created
 
     @property
-    def files_deleted(self) -> List[str]:
+    def files_deleted(self) -> list[str]:
         """List of files that were deleted."""
         return self._files_deleted
 
     @property
-    def files_modified(self) -> List[str]:
+    def files_modified(self) -> list[str]:
         """List of files that were modified."""
         return self._files_modified
 
     @property
-    def files_moved(self) -> list[Tuple[str, str]]:
+    def files_moved(self) -> list[tuple[str, str]]:
         """List of files that were moved.
 
         Each event is a two-tuple the first item of which is the path
@@ -189,12 +193,12 @@ def files_moved(self) -> list[Tuple[str, str]]:
         return self._files_moved
 
     @property
-    def dirs_modified(self) -> List[str]:
+    def dirs_modified(self) -> list[str]:
         """List of directories that were modified."""
         return self._dirs_modified
 
     @property
-    def dirs_moved(self) -> List[tuple[str, str]]:
+    def dirs_moved(self) -> list[tuple[str, str]]:
         """List of directories that were moved.
 
         Each event is a two-tuple the first item of which is the path
@@ -203,12 +207,12 @@ def dirs_moved(self) -> List[tuple[str, str]]:
         return self._dirs_moved
 
     @property
-    def dirs_deleted(self) -> List[str]:
+    def dirs_deleted(self) -> list[str]:
         """List of directories that were deleted."""
         return self._dirs_deleted
 
     @property
-    def dirs_created(self) -> List[str]:
+    def dirs_created(self) -> list[str]:
         """List of directories that were created."""
         return self._dirs_created
 
@@ -313,7 +317,7 @@ def __init__(
         self.listdir = listdir
 
         self._stat_info: dict[str, os.stat_result] = {}
-        self._inode_to_path: dict[Tuple[int, int], str] = {}
+        self._inode_to_path: dict[tuple[int, int], str] = {}
 
         st = self.stat(path)
         self._stat_info[path] = st
@@ -324,7 +328,7 @@ def __init__(
             self._inode_to_path[i] = p
             self._stat_info[p] = st
 
-    def walk(self, root: str) -> Iterator[Tuple[str, os.stat_result]]:
+    def walk(self, root: str) -> Iterator[tuple[str, os.stat_result]]:
         try:
             paths = [os.path.join(root, entry.name) for entry in self.listdir(root)]
         except OSError as e:
@@ -355,11 +359,11 @@ def paths(self) -> set[str]:
         """Set of file/directory paths in the snapshot."""
         return set(self._stat_info.keys())
 
-    def path(self, uid: Tuple[int, int]) -> Optional[str]:
+    def path(self, uid: tuple[int, int]) -> Optional[str]:
         """Returns path for id. None if id is unknown to this snapshot."""
         return self._inode_to_path.get(uid)
 
-    def inode(self, path: str) -> Tuple[int, int]:
+    def inode(self, path: str) -> tuple[int, int]:
         """Returns an id for path."""
         st = self._stat_info[path]
         return (st.st_ino, st.st_dev)
diff --git a/src/watchdog/utils/patterns.py b/src/watchdog/utils/patterns.py
index f2db3713..b012d7ac 100644
--- a/src/watchdog/utils/patterns.py
+++ b/src/watchdog/utils/patterns.py
@@ -14,24 +14,35 @@
 #   - `PurePosixPath` is always case-sensitive.
 # Reference: https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.match
 from pathlib import PurePosixPath, PureWindowsPath
+from typing import TYPE_CHECKING
 
+if TYPE_CHECKING:
+    from collections.abc import Iterator
 
-def _match_path(path, included_patterns, excluded_patterns, case_sensitive):
+
+def _match_path(raw_path: str, included_patterns: set[str], excluded_patterns: set[str], case_sensitive: bool) -> bool:
     """Internal function same as :func:`match_path` but does not check arguments."""
+    path: PurePosixPath | PureWindowsPath
     if case_sensitive:
-        path = PurePosixPath(path)
+        path = PurePosixPath(raw_path)
     else:
         included_patterns = {pattern.lower() for pattern in included_patterns}
         excluded_patterns = {pattern.lower() for pattern in excluded_patterns}
-        path = PureWindowsPath(path)
+        path = PureWindowsPath(raw_path)
 
     common_patterns = included_patterns & excluded_patterns
     if common_patterns:
         raise ValueError(f"conflicting patterns `{common_patterns}` included and excluded")
+
     return any(path.match(p) for p in included_patterns) and not any(path.match(p) for p in excluded_patterns)
 
 
-def filter_paths(paths, included_patterns=None, excluded_patterns=None, case_sensitive=True):
+def filter_paths(
+    paths: list[str],
+    included_patterns: list[str] | None = None,
+    excluded_patterns: list[str] | None = None,
+    case_sensitive: bool = True,
+) -> Iterator[str]:
     """Filters from a set of paths based on acceptable patterns and
     ignorable patterns.
     :param paths:
@@ -58,7 +69,12 @@ def filter_paths(paths, included_patterns=None, excluded_patterns=None, case_sen
             yield path
 
 
-def match_any_paths(paths, included_patterns=None, excluded_patterns=None, case_sensitive=True):
+def match_any_paths(
+    paths: list[str],
+    included_patterns: list[str] | None = None,
+    excluded_patterns: list[str] | None = None,
+    case_sensitive: bool = True,
+) -> bool:
     """Matches from a set of paths based on acceptable patterns and
     ignorable patterns.
     See ``filter_paths()`` for signature details.
diff --git a/src/watchdog/utils/platform.py b/src/watchdog/utils/platform.py
index 0f6b05a3..9d8ed576 100644
--- a/src/watchdog/utils/platform.py
+++ b/src/watchdog/utils/platform.py
@@ -24,7 +24,7 @@
 PLATFORM_UNKNOWN = "unknown"
 
 
-def get_platform_name():
+def get_platform_name() -> str:
     if sys.platform.startswith("win"):
         return PLATFORM_WINDOWS
 
@@ -43,17 +43,17 @@ def get_platform_name():
 __platform__ = get_platform_name()
 
 
-def is_linux():
+def is_linux() -> bool:
     return __platform__ == PLATFORM_LINUX
 
 
-def is_bsd():
+def is_bsd() -> bool:
     return __platform__ == PLATFORM_BSD
 
 
-def is_darwin():
+def is_darwin() -> bool:
     return __platform__ == PLATFORM_DARWIN
 
 
-def is_windows():
+def is_windows() -> bool:
     return __platform__ == PLATFORM_WINDOWS
diff --git a/src/watchdog/watchmedo.py b/src/watchdog/watchmedo.py
index 783d1e06..841fee35 100644
--- a/src/watchdog/watchmedo.py
+++ b/src/watchdog/watchmedo.py
@@ -31,13 +31,15 @@
 from argparse import ArgumentParser, RawDescriptionHelpFormatter
 from io import StringIO
 from textwrap import dedent
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any
 
 from watchdog.utils import WatchdogShutdown, load_class, platform
 from watchdog.version import VERSION_STRING
 
 if TYPE_CHECKING:
-    from watchdog.observers.api import BaseObserverSubclassCallable
+    from argparse import Namespace, _SubParsersAction
+    from typing import Callable
+
 
 logging.basicConfig(level=logging.INFO)
 
@@ -77,15 +79,21 @@ def _split_lines(self, text, width):
 subparsers = cli.add_subparsers(dest="top_command")
 command_parsers = {}
 
+Argument = tuple[list[str], Any]
+
 
-def argument(*name_or_flags, **kwargs):
+def argument(*name_or_flags: str, **kwargs: Any) -> Argument:
     """Convenience function to properly format arguments to pass to the
     command decorator.
     """
     return list(name_or_flags), kwargs
 
 
-def command(args=[], parent=subparsers, cmd_aliases=[]):
+def command(
+    args: list[Argument],
+    parent: _SubParsersAction[ArgumentParser] = subparsers,
+    cmd_aliases: list[str] = [],
+) -> Callable:
     """Decorator to define a new command in a sanity-preserving way.
     The function will be stored in the ``func`` variable when the parser
     parses arguments so that it can be called directly like so::
@@ -95,16 +103,16 @@ def command(args=[], parent=subparsers, cmd_aliases=[]):
 
     """
 
-    def decorator(func):
+    def decorator(func: Callable) -> Callable:
         name = func.__name__.replace("_", "-")
-        desc = dedent(func.__doc__)
-        parser = parent.add_parser(name, description=desc, aliases=cmd_aliases, formatter_class=HelpFormatter)
+        desc = dedent(func.__doc__ or "")
+        parser = parent.add_parser(name, aliases=cmd_aliases, description=desc, formatter_class=HelpFormatter)
         command_parsers[name] = parser
         verbosity_group = parser.add_mutually_exclusive_group()
         verbosity_group.add_argument("-q", "--quiet", dest="verbosity", action="append_const", const=-1)
         verbosity_group.add_argument("-v", "--verbose", dest="verbosity", action="append_const", const=1)
-        for arg in args:
-            parser.add_argument(*arg[0], **arg[1])
+        for name_or_flags, kwargs in args:
+            parser.add_argument(*name_or_flags, **kwargs)
             parser.set_defaults(func=func)
         return func
 
@@ -198,8 +206,8 @@ def schedule_tricks(observer, tricks, pathname, recursive):
     """
     for trick in tricks:
         for name, value in list(trick.items()):
-            TrickClass = load_class(name)
-            handler = TrickClass(**value)
+            trick_cls = load_class(name)
+            handler = trick_cls(**value)
             trick_pathname = getattr(handler, "source_directory", None) or pathname
             observer.schedule(handler, trick_pathname, recursive)
 
@@ -252,7 +260,6 @@ def schedule_tricks(observer, tricks, pathname, recursive):
 )
 def tricks_from(args):
     """Command to execute tricks from a tricks configuration file."""
-    Observer: BaseObserverSubclassCallable
     if args.debug_force_polling:
         from watchdog.observers.polling import PollingObserver as Observer
     elif args.debug_force_kqueue:
@@ -342,8 +349,8 @@ def tricks_generate_yaml(args):
     output = StringIO()
 
     for trick_path in args.trick_paths:
-        TrickClass = load_class(trick_path)
-        output.write(TrickClass.generate_yaml())
+        trick_cls = load_class(trick_path)
+        output.write(trick_cls.generate_yaml())
 
     content = output.getvalue()
     output.close()
@@ -448,7 +455,6 @@ def log(args):
         ignore_directories=args.ignore_directories,
     )
 
-    Observer: BaseObserverSubclassCallable
     if args.debug_force_polling:
         from watchdog.observers.polling import PollingObserver as Observer
     elif args.debug_force_kqueue:
@@ -559,7 +565,6 @@ def shell_command(args):
     if not args.command:
         args.command = None
 
-    Observer: BaseObserverSubclassCallable
     if args.debug_force_polling:
         from watchdog.observers.polling import PollingObserver as Observer
     else:
@@ -672,7 +677,6 @@ def shell_command(args):
 )
 def auto_restart(args):
     """Command to start a long-running subprocess and restart it on matched events."""
-    Observer: BaseObserverSubclassCallable
     if args.debug_force_polling:
         from watchdog.observers.polling import PollingObserver as Observer
     else:
@@ -731,7 +735,7 @@ class LogLevelException(Exception):
     pass
 
 
-def _get_log_level_from_args(args):
+def _get_log_level_from_args(args: Namespace) -> str:
     verbosity = sum(args.verbosity or [])
     if verbosity < -1:
         raise LogLevelException("-q/--quiet may be specified only once.")
@@ -740,7 +744,7 @@ def _get_log_level_from_args(args):
     return ["ERROR", "WARNING", "INFO", "DEBUG"][1 + verbosity]
 
 
-def main():
+def main() -> int:
     """Entry-point function."""
     args = cli.parse_args()
     if args.top_command is None:
diff --git a/tests/test_observers_winapi.py b/tests/test_observers_winapi.py
index 5c332d70..4bf0e15d 100644
--- a/tests/test_observers_winapi.py
+++ b/tests/test_observers_winapi.py
@@ -124,7 +124,7 @@ def test_root_deleted(event_queue, emitter):
         File "watchdog\observers\winapi.py", line 340, in read_directory_changes
             return _generate_observed_path_deleted_event()
         File "watchdog\observers\winapi.py", line 298, in _generate_observed_path_deleted_event
-            event = FILE_NOTIFY_INFORMATION(0, FILE_ACTION_DELETED_SELF, len(path), path.value)
+            event = FileNotifyInformation(0, FILE_ACTION_DELETED_SELF, len(path), path.value)
         TypeError: expected bytes, str found
     """
 
diff --git a/tests/utils.py b/tests/utils.py
index 738bddee..91bee6b9 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -3,11 +3,11 @@
 import dataclasses
 import os
 from queue import Empty, Queue
-from typing import List, Optional, Tuple, Type, Union
+from typing import Optional, Type, Union, Protocol
 
 from watchdog.events import FileSystemEvent
 from watchdog.observers.api import EventEmitter, ObservedWatch
-from watchdog.utils import Protocol, platform
+from watchdog.utils import platform
 
 Emitter: Type[EventEmitter]
 
@@ -42,13 +42,13 @@ def __call__(self, expected_event: FileSystemEvent, timeout: float = ...) -> Non
         ...
 
 
-TestEventQueue = Union["Queue[Tuple[FileSystemEvent, ObservedWatch]]"]
+TestEventQueue = Queue[tuple[FileSystemEvent, ObservedWatch]]
 
 
 @dataclasses.dataclass()
 class Helper:
     tmp: str
-    emitters: List[EventEmitter] = dataclasses.field(default_factory=list)
+    emitters: list[EventEmitter] = dataclasses.field(default_factory=list)
     event_queue: TestEventQueue = dataclasses.field(default_factory=Queue)
 
     def joinpath(self, *args: str) -> str:
diff --git a/tox.ini b/tox.ini
index e74a35be..6bd6a5d3 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
 [tox]
 envlist =
-    py3{8,9,10,11,12,13}
+    py3{9,10,11,12,13}
     pypy3
     docs
     types