Repository: jupyter/jupyter_client
Pull request #871 (2022-11-09 13:55:11)

Clean up types and remove use of entrypoints
`entrypoints` has been deprecated in favor of `importlib.metadata`.


Patch:
diff --git a/.github/workflows/downstream.yml b/.github/workflows/downstream.yml
index 1380478..edb47d3 100644
--- a/.github/workflows/downstream.yml
+++ b/.github/workflows/downstream.yml
@@ -14,7 +14,7 @@ jobs:
     runs-on: ubuntu-latest
     timeout-minutes: 15
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
       - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
       - uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1
         with:
@@ -25,7 +25,7 @@ jobs:
     runs-on: ubuntu-latest
     timeout-minutes: 15
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
       - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
       - uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1
         with:
@@ -36,7 +36,7 @@ jobs:
     runs-on: ubuntu-latest
     timeout-minutes: 15
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
       - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
       - uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1
         with:
@@ -47,7 +47,7 @@ jobs:
     runs-on: ubuntu-latest
     timeout-minutes: 15
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
       - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
       - uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1
         with:
@@ -58,7 +58,7 @@ jobs:
     timeout-minutes: 10
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
 
       - name: Base Setup
         uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
@@ -88,7 +88,7 @@ jobs:
         uses: actions/checkout@v3
 
       - name: Setup Python
-        uses: actions/setup-python@v2
+        uses: actions/setup-python@v4
         with:
           python-version: "3.9"
           architecture: "x64"
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 59fb821..8b28722 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -13,14 +13,14 @@ jobs:
   pre-commit:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
       - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
       - uses: jupyterlab/maintainer-tools/.github/actions/pre-commit@v1
 
   check_release:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
       - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
       - uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2
         with:
@@ -29,7 +29,7 @@ jobs:
   check-links:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
       - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
       - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1
 
@@ -52,7 +52,7 @@ jobs:
           - os: macos-latest
             python-version: "3.8"
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
       - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
       - name: Run the tests
         if: ${{ !startsWith(matrix.python-version, 'pypy') && !startsWith(matrix.os, 'windows') }}
@@ -73,7 +73,7 @@ jobs:
   docs:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
       - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
       - run: hatch run docs:build
 
@@ -82,7 +82,7 @@ jobs:
     runs-on: ubuntu-latest
     timeout-minutes: 10
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
       - name: Base Setup
         uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
         with:
@@ -98,7 +98,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
       - name: Base Setup
         uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
       - name: Install the Python dependencies
@@ -117,7 +117,7 @@ jobs:
     runs-on: ubuntu-latest
     timeout-minutes: 10
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
       - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
       - uses: jupyterlab/maintainer-tools/.github/actions/make-sdist@v1
 
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 9d9bb26..ca7c0d5 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -65,7 +65,7 @@ repos:
       - id: mypy
         exclude: tests
         args: ["--config-file", "pyproject.toml"]
-        additional_dependencies: [pyzmq, tornado, types-paramiko]
+        additional_dependencies: [pyzmq, tornado, types-paramiko, traitlets, "jupyter_core>=5.0", ipykernel]
         stages: [manual]
 
   - repo: https://github.com/PyCQA/doc8
diff --git a/jupyter_client/client.py b/jupyter_client/client.py
index f3d9a10..afe1dc2 100644
--- a/jupyter_client/client.py
+++ b/jupyter_client/client.py
@@ -128,7 +128,7 @@ class KernelClient(ConnectionFileMixin):
                     self.log.debug("Destroying zmq context for %s", self)
                 self.context.destroy()
         try:
-            super_del = super().__del__
+            super_del = super().__del__  # type:ignore[misc]
         except AttributeError:
             pass
         else:
diff --git a/jupyter_client/connect.py b/jupyter_client/connect.py
index 8ce3fe0..e23ef97 100644
--- a/jupyter_client/connect.py
+++ b/jupyter_client/connect.py
@@ -30,7 +30,6 @@ from jupyter_core.paths import secure_write
 from traitlets import Bool
 from traitlets import CaselessStrEnum
 from traitlets import Instance
-from traitlets import Int
 from traitlets import Integer
 from traitlets import observe
 from traitlets import Type
@@ -47,11 +46,11 @@ KernelConnectionInfo = Dict[str, Union[int, str, bytes]]
 
 def write_connection_file(
     fname: Optional[str] = None,
-    shell_port: Union[Integer, Int, int] = 0,
-    iopub_port: Union[Integer, Int, int] = 0,
-    stdin_port: Union[Integer, Int, int] = 0,
-    hb_port: Union[Integer, Int, int] = 0,
-    control_port: Union[Integer, Int, int] = 0,
+    shell_port: int = 0,
+    iopub_port: int = 0,
+    stdin_port: int = 0,
+    hb_port: int = 0,
+    control_port: int = 0,
     ip: str = "",
     key: bytes = b"",
     transport: str = "tcp",
@@ -334,7 +333,7 @@ port_names = ["%s_port" % channel for channel in ("shell", "stdin", "iopub", "hb
 class ConnectionFileMixin(LoggingConfigurable):
     """Mixin for configurable classes that work with connection files"""
 
-    data_dir = Unicode()
+    data_dir: Union[str, Unicode] = Unicode()
 
     def _data_dir_default(self):
         return jupyter_data_dir()
@@ -353,7 +352,9 @@ class ConnectionFileMixin(LoggingConfigurable):
     _connection_file_written = Bool(False)
 
     transport = CaselessStrEnum(["tcp", "ipc"], default_value="tcp", config=True)
-    kernel_name = Unicode()
+    kernel_name: Union[str, Unicode] = Unicode()
+
+    context = Instance(zmq.Context)
 
     ip = Unicode(
         config=True,
diff --git a/jupyter_client/consoleapp.py b/jupyter_client/consoleapp.py
index c8b0cc1..df18af3 100644
--- a/jupyter_client/consoleapp.py
+++ b/jupyter_client/consoleapp.py
@@ -10,9 +10,9 @@ import atexit
 import os
 import signal
 import sys
+import typing as t
 import uuid
 import warnings
-from typing import cast
 
 from jupyter_core.application import base_aliases
 from jupyter_core.application import base_flags
@@ -94,7 +94,7 @@ aliases.update(app_aliases)
 # Classes
 # -----------------------------------------------------------------------------
 
-classes = [KernelManager, KernelRestarter, Session]
+classes: t.List[t.Type[t.Any]] = [KernelManager, KernelRestarter, Session]
 
 
 class JupyterConsoleApp(ConnectionFileMixin):
@@ -160,7 +160,7 @@ class JupyterConsoleApp(ConnectionFileMixin):
 
         Override in subclasses if any args should be passed to the kernel
         """
-        self.kernel_argv = self.extra_args
+        self.kernel_argv = self.extra_args  # type:ignore[attr-defined]
 
     def init_connection_file(self) -> None:
         """find the connection file, and load the info if found.
@@ -177,31 +177,32 @@ class JupyterConsoleApp(ConnectionFileMixin):
         After this method is called, self.connection_file contains the *full path*
         to the connection file, never just its name.
         """
+        runtime_dir = self.runtime_dir  # type:ignore[attr-defined]
         if self.existing:
             try:
-                cf = find_connection_file(self.existing, [".", self.runtime_dir])
+                cf = find_connection_file(self.existing, [".", runtime_dir])
             except Exception:
                 self.log.critical(
                     "Could not find existing kernel connection file %s", self.existing
                 )
-                self.exit(1)
+                self.exit(1)  # type:ignore[attr-defined]
             self.log.debug("Connecting to existing kernel: %s" % cf)
             self.connection_file = cf
         else:
             # not existing, check if we are going to write the file
             # and ensure that self.connection_file is a full path, not just the shortname
             try:
-                cf = find_connection_file(self.connection_file, [self.runtime_dir])
+                cf = find_connection_file(self.connection_file, [runtime_dir])
             except Exception:
                 # file might not exist
                 if self.connection_file == os.path.basename(self.connection_file):
                     # just shortname, put it in security dir
-                    cf = os.path.join(self.runtime_dir, self.connection_file)
+                    cf = os.path.join(runtime_dir, self.connection_file)
                 else:
                     cf = self.connection_file
                 self.connection_file = cf
         try:
-            self.connection_file = _filefind(self.connection_file, [".", self.runtime_dir])
+            self.connection_file = _filefind(self.connection_file, [".", runtime_dir])
         except OSError:
             self.log.debug("Connection File not found: %s", self.connection_file)
             return
@@ -217,7 +218,7 @@ class JupyterConsoleApp(ConnectionFileMixin):
                 self.connection_file,
                 exc_info=True,
             )
-            self.exit(1)
+            self.exit(1)  # type:ignore[attr-defined]
 
     def init_ssh(self) -> None:
         """set up ssh tunnels, if needed."""
@@ -256,7 +257,7 @@ class JupyterConsoleApp(ConnectionFileMixin):
         except:  # noqa
             # even catch KeyboardInterrupt
             self.log.error("Could not setup tunnels", exc_info=True)
-            self.exit(1)
+            self.exit(1)  # type:ignore[attr-defined]
 
         (
             self.shell_port,
@@ -280,7 +281,8 @@ class JupyterConsoleApp(ConnectionFileMixin):
             # 48b node segment (12 hex chars).  Users running more than 32k simultaneous
             # kernels can subclass.
             ident = str(uuid.uuid4()).split("-")[-1]
-            cf = os.path.join(self.runtime_dir, "kernel-%s.json" % ident)
+            runtime_dir = self.runtime_dir  # type:ignore[attr-defined]
+            cf = os.path.join(runtime_dir, "kernel-%s.json" % ident)
             # only keep if it's actually new.  Protect against unlikely collision
             # in 48b random search space
             cf = cf if not os.path.exists(cf) else ""
@@ -311,9 +313,9 @@ class JupyterConsoleApp(ConnectionFileMixin):
             )
         except NoSuchKernel:
             self.log.critical("Could not find kernel %s", self.kernel_name)
-            self.exit(1)
+            self.exit(1)  # type:ignore[attr-defined]
 
-        self.kernel_manager = cast(KernelManager, self.kernel_manager)
+        self.kernel_manager = t.cast(KernelManager, self.kernel_manager)
         self.kernel_manager.client_factory = self.kernel_client_class
         kwargs = {}
         kwargs["extra_arguments"] = self.kernel_argv
@@ -359,7 +361,7 @@ class JupyterConsoleApp(ConnectionFileMixin):
         Classes which mix this class in should call:
                JupyterConsoleApp.initialize(self,argv)
         """
-        if self._dispatching:
+        if self._dispatching:  # type:ignore[attr-defined]
             return
         self.init_connection_file()
         self.init_ssh()
diff --git a/jupyter_client/ioloop/manager.py b/jupyter_client/ioloop/manager.py
index 8a98025..f6d0f65 100644
--- a/jupyter_client/ioloop/manager.py
+++ b/jupyter_client/ioloop/manager.py
@@ -1,6 +1,8 @@
 """A kernel manager with a tornado IOLoop"""
 # Copyright (c) Jupyter Development Team.
 # Distributed under the terms of the Modified BSD License.
+import typing as t
+
 import zmq
 from tornado import ioloop
 from traitlets import Instance
@@ -48,7 +50,7 @@ class IOLoopKernelManager(KernelManager):
         ),
         config=True,
     )
-    _restarter = Instance("jupyter_client.ioloop.IOLoopKernelRestarter", allow_none=True)
+    _restarter: t.Any = Instance("jupyter_client.ioloop.IOLoopKernelRestarter", allow_none=True)
 
     def start_restarter(self):
         if self.autorestart and self.has_kernel:
@@ -87,7 +89,9 @@ class AsyncIOLoopKernelManager(AsyncKernelManager):
         ),
         config=True,
     )
-    _restarter = Instance("jupyter_client.ioloop.AsyncIOLoopKernelRestarter", allow_none=True)
+    _restarter: t.Any = Instance(
+        "jupyter_client.ioloop.AsyncIOLoopKernelRestarter", allow_none=True
+    )
 
     def start_restarter(self):
         if self.autorestart and self.has_kernel:
diff --git a/jupyter_client/kernelspec.py b/jupyter_client/kernelspec.py
index d4b79f3..aedc552 100644
--- a/jupyter_client/kernelspec.py
+++ b/jupyter_client/kernelspec.py
@@ -221,7 +221,7 @@ class KernelSpecManager(LoggingConfigurable):
 
         if self.ensure_native_kernel and NATIVE_KERNEL_NAME not in d:
             try:
-                from ipykernel.kernelspec import RESOURCES  # type: ignore
+                from ipykernel.kernelspec import RESOURCES
 
                 self.log.debug(
                     "Native kernel (%s) available from %s",
diff --git a/jupyter_client/manager.py b/jupyter_client/manager.py
index c8b062b..f6257f5 100644
--- a/jupyter_client/manager.py
+++ b/jupyter_client/manager.py
@@ -129,7 +129,7 @@ class KernelManager(ConnectionFileMixin):
     def _client_class_changed(self, change: t.Dict[str, DottedObjectName]) -> None:
         self.client_factory = import_item(str(change["new"]))
 
-    kernel_id: str = Unicode(None, allow_none=True)
+    kernel_id: t.Union[str, Unicode] = Unicode(None, allow_none=True)
 
     # The kernel provisioner with which this KernelManager is communicating.
     # This will generally be a LocalProvisioner instance unless the kernelspec
@@ -161,10 +161,10 @@ class KernelManager(ConnectionFileMixin):
         "vary by provisioned environment.",
     )
 
-    kernel_name: Unicode = Unicode(kernelspec.NATIVE_KERNEL_NAME)
+    kernel_name: t.Union[str, Unicode] = Unicode(kernelspec.NATIVE_KERNEL_NAME)
 
     @observe("kernel_name")  # type:ignore[misc]
-    def _kernel_name_changed(self, change: t.Dict[str, Unicode]) -> None:
+    def _kernel_name_changed(self, change: t.Dict[str, str]) -> None:
         self._kernel_spec = None
         if change["new"] == "python":
             self.kernel_name = kernelspec.NATIVE_KERNEL_NAME
@@ -240,7 +240,7 @@ class KernelManager(ConnectionFileMixin):
 
     def client(self, **kwargs: t.Any) -> KernelClient:
         """Create a client configured to connect to our kernel"""
-        kw = {}
+        kw: dict = {}
         kw.update(self.get_connection_info(session=True))
         kw.update(
             dict(
@@ -684,7 +684,7 @@ def start_new_kernel(
     kc = km.client()
     kc.start_channels()
     try:
-        kc.wait_for_ready(timeout=startup_timeout)
+        kc.wait_for_ready(timeout=startup_timeout)  # type:ignore[attr-defined]
     except RuntimeError:
         kc.stop_channels()
         km.shutdown_kernel()
@@ -702,7 +702,7 @@ async def start_new_async_kernel(
     kc = km.client()
     kc.start_channels()
     try:
-        await kc.wait_for_ready(timeout=startup_timeout)
+        await kc.wait_for_ready(timeout=startup_timeout)  # type:ignore[attr-defined]
     except RuntimeError:
         kc.stop_channels()
         await km.shutdown_kernel()
diff --git a/jupyter_client/multikernelmanager.py b/jupyter_client/multikernelmanager.py
index 738de56..8048e47 100644
--- a/jupyter_client/multikernelmanager.py
+++ b/jupyter_client/multikernelmanager.py
@@ -122,7 +122,7 @@ class MultiKernelManager(LoggingConfigurable):
                 self.log.debug("Destroying zmq context for %s", self)
             self.context.destroy()
         try:
-            super_del = super().__del__
+            super_del = super().__del__  # type:ignore[misc]
         except AttributeError:
             pass
         else:
diff --git a/jupyter_client/provisioning/factory.py b/jupyter_client/provisioning/factory.py
index 71eadf1..1efa527 100644
--- a/jupyter_client/provisioning/factory.py
+++ b/jupyter_client/provisioning/factory.py
@@ -2,16 +2,20 @@
 # Copyright (c) Jupyter Development Team.
 # Distributed under the terms of the Modified BSD License.
 import glob
+import sys
 from os import getenv
 from os import path
 from typing import Any
 from typing import Dict
 from typing import List
 
-from entrypoints import EntryPoint
-from entrypoints import get_group_all
-from entrypoints import get_single
-from entrypoints import NoSuchEntryPoint
+
+# See compatibility note on `group` keyword in https://docs.python.org/3/library/importlib.metadata.html#entry-points
+if sys.version_info < (3, 10):  # pragma: no cover
+    from importlib_metadata import entry_points, EntryPoint
+else:  # pragma: no cover
+    from importlib.metadata import entry_points, EntryPoint
+
 from traitlets.config import default
 from traitlets.config import SingletonConfigurable
 from traitlets.config import Unicode
@@ -119,7 +123,7 @@ class KernelProvisionerFactory(SingletonConfigurable):
             try:
                 ep = self._get_provisioner(provisioner_name)
                 self.provisioners[provisioner_name] = ep  # Update cache
-            except NoSuchEntryPoint:
+            except Exception:
                 is_available = False
         return is_available
 
@@ -161,41 +165,41 @@ class KernelProvisionerFactory(SingletonConfigurable):
         """
         entries = {}
         for name, ep in self.provisioners.items():
-            entries[name] = f"{ep.module_name}:{ep.object_name}"
+            entries[name] = ep.value
         return entries
 
     @staticmethod
     def _get_all_provisioners() -> List[EntryPoint]:
-        """Wrapper around entrypoints.get_group_all() - primarily to facilitate testing."""
-        return get_group_all(KernelProvisionerFactory.GROUP_NAME)
+        """Wrapper around entry_points (to fetch the set of provisioners) - primarily to facilitate testing."""
+        return entry_points(group=KernelProvisionerFactory.GROUP_NAME)
 
     def _get_provisioner(self, name: str) -> EntryPoint:
-        """Wrapper around entrypoints.get_single() - primarily to facilitate testing."""
-        try:
-            ep = get_single(KernelProvisionerFactory.GROUP_NAME, name)
-        except NoSuchEntryPoint:
-            # Check if the entrypoint name is 'local-provisioner'.  Although this should never
-            # happen, we have seen cases where the previous distribution of jupyter_client has
-            # remained which doesn't include kernel-provisioner entrypoints (so 'local-provisioner'
-            # is deemed not found even though its definition is in THIS package).  In such cass,
-            # the entrypoints package uses what it first finds - which is the older distribution
-            # resulting in a violation of a supposed invariant condition.  To address this scenario,
-            # we will log a warning message indicating this situation, then build the entrypoint
-            # instance ourselves - since we have that information.
-            if name == 'local-provisioner':
-                distros = glob.glob(f"{path.dirname(path.dirname(__file__))}-*")
-                self.log.warning(
-                    f"Kernel Provisioning: The 'local-provisioner' is not found.  This is likely "
-                    f"due to the presence of multiple jupyter_client distributions and a previous "
-                    f"distribution is being used as the source for entrypoints - which does not "
-                    f"include 'local-provisioner'.  That distribution should be removed such that "
-                    f"only the version-appropriate distribution remains (version >= 7).  Until "
-                    f"then, a 'local-provisioner' entrypoint will be automatically constructed "
-                    f"and used.\nThe candidate distribution locations are: {distros}"
-                )
-                ep = EntryPoint(
-                    'local-provisioner', 'jupyter_client.provisioning', 'LocalProvisioner'
-                )
-            else:
-                raise
-        return ep
+        """Wrapper around entry_points (to fetch a single provisioner) - primarily to facilitate testing."""
+        eps = entry_points(group=KernelProvisionerFactory.GROUP_NAME, name=name)
+        if eps:
+            return eps[0]
+
+        # Check if the entrypoint name is 'local-provisioner'.  Although this should never
+        # happen, we have seen cases where the previous distribution of jupyter_client has
+        # remained which doesn't include kernel-provisioner entrypoints (so 'local-provisioner'
+        # is deemed not found even though its definition is in THIS package).  In such cases,
+        # the entrypoints package uses what it first finds - which is the older distribution
+        # resulting in a violation of a supposed invariant condition.  To address this scenario,
+        # we will log a warning message indicating this situation, then build the entrypoint
+        # instance ourselves - since we have that information.
+        if name == 'local-provisioner':
+            distros = glob.glob(f"{path.dirname(path.dirname(__file__))}-*")
+            self.log.warning(
+                f"Kernel Provisioning: The 'local-provisioner' is not found.  This is likely "
+                f"due to the presence of multiple jupyter_client distributions and a previous "
+                f"distribution is being used as the source for entrypoints - which does not "
+                f"include 'local-provisioner'.  That distribution should be removed such that "
+                f"only the version-appropriate distribution remains (version >= 7).  Until "
+                f"then, a 'local-provisioner' entrypoint will be automatically constructed "
+                f"and used.\nThe candidate distribution locations are: {distros}"
+            )
+            return EntryPoint(
+                'local-provisioner', 'jupyter_client.provisioning', 'LocalProvisioner'
+            )
+
+        raise
diff --git a/jupyter_client/provisioning/provisioner_base.py b/jupyter_client/provisioning/provisioner_base.py
index 7fc0511..3fe7293 100644
--- a/jupyter_client/provisioning/provisioner_base.py
+++ b/jupyter_client/provisioning/provisioner_base.py
@@ -9,6 +9,7 @@ from typing import Any
 from typing import Dict
 from typing import List
 from typing import Optional
+from typing import Union
 
 from traitlets.config import Instance
 from traitlets.config import LoggingConfigurable
@@ -21,7 +22,9 @@ class KernelProvisionerMeta(ABCMeta, type(LoggingConfigurable)):  # type: ignore
     pass
 
 
-class KernelProvisionerBase(ABC, LoggingConfigurable, metaclass=KernelProvisionerMeta):
+class KernelProvisionerBase(  # type:ignore[misc]
+    ABC, LoggingConfigurable, metaclass=KernelProvisionerMeta
+):
     """
     Abstract base class defining methods for KernelProvisioner classes.
 
@@ -35,7 +38,7 @@ class KernelProvisionerBase(ABC, LoggingConfigurable, metaclass=KernelProvisione
 
     # The kernel specification associated with this provisioner
     kernel_spec: Any = Instance('jupyter_client.kernelspec.KernelSpec', allow_none=True)
-    kernel_id: str = Unicode(None, allow_none=True)
+    kernel_id: Union[str, Unicode] = Unicode(None, allow_none=True)
     connection_info: KernelConnectionInfo = {}
 
     @property
diff --git a/jupyter_client/runapp.py b/jupyter_client/runapp.py
index b67c9d0..3784586 100644
--- a/jupyter_client/runapp.py
+++ b/jupyter_client/runapp.py
@@ -44,8 +44,8 @@ class RunApp(JupyterApp, JupyterConsoleApp):
     version = __version__
     name = "jupyter run"
     description = """Run Jupyter kernel code."""
-    flags = Dict(flags)
-    aliases = Dict(aliases)
+    flags = Dict(flags)  # type:ignore[assignment]
+    aliases = Dict(aliases)  # type:ignore[assignment]
     frontend_aliases = Any(frontend_aliases)
     frontend_flags = Any(frontend_flags)
     kernel_timeout = Float(
diff --git a/pyproject.toml b/pyproject.toml
index 416fad7..2bdcded 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,7 +24,7 @@ classifiers = [
 ]
 requires-python = ">=3.8"
 dependencies = [
-    "entrypoints",
+    "importlib_metadata>=4.8.3;python_version<\"3.10\"",
     "jupyter_core>=4.9.2",
     "python-dateutil>=2.8.2",
     "pyzmq>=23.0",
@@ -139,15 +139,6 @@ warn_redundant_casts = true
 warn_return_any = false
 warn_unused_ignores = true
 
-[[tool.mypy.overrides]]
-module = [
-    "traitlets.*",
-    "jupyter_core.*",
-    "ipykernel",
-    "entrypoints"
-]
-ignore_missing_imports = true
-
 [tool.flake8]
 ignore = "E501, W503, E402"
 builtins = "c, get_config"


Test patch:
diff --git a/tests/test_provisioning.py b/tests/test_provisioning.py
index 32c64bd..9a841bc 100644
--- a/tests/test_provisioning.py
+++ b/tests/test_provisioning.py
@@ -13,8 +13,6 @@ from typing import List
 from typing import Optional
 
 import pytest
-from entrypoints import EntryPoint
-from entrypoints import NoSuchEntryPoint
 from jupyter_core import paths
 from traitlets import Int
 from traitlets import Unicode
@@ -27,6 +25,7 @@ from jupyter_client.manager import AsyncKernelManager
 from jupyter_client.provisioning import KernelProvisionerBase
 from jupyter_client.provisioning import KernelProvisionerFactory
 from jupyter_client.provisioning import LocalProvisioner
+from jupyter_client.provisioning.factory import EntryPoint
 
 pjoin = os.path.join
 
@@ -199,30 +198,31 @@ def akm(request, all_provisioners):
 
 
 initial_provisioner_map = {
-    'local-provisioner': ('jupyter_client.provisioning', 'LocalProvisioner'),
-    'subclassed-test-provisioner': (
-        'tests.test_provisioning',
-        'SubclassedTestProvisioner',
-    ),
-    'custom-test-provisioner': ('tests.test_provisioning', 'CustomTestProvisioner'),
+    'local-provisioner': 'jupyter_client.provisioning:LocalProvisioner',
+    'subclassed-test-provisioner': 'tests.test_provisioning:SubclassedTestProvisioner',
+    'custom-test-provisioner': 'tests.test_provisioning:CustomTestProvisioner',
 }
 
 
 def mock_get_all_provisioners() -> List[EntryPoint]:
     result = []
     for name, epstr in initial_provisioner_map.items():
-        result.append(EntryPoint(name, epstr[0], epstr[1]))
+        result.append(EntryPoint(name, epstr, KernelProvisionerFactory.GROUP_NAME))
     return result
 
 
 def mock_get_provisioner(factory, name) -> EntryPoint:
     if name == 'new-test-provisioner':
-        return EntryPoint('new-test-provisioner', 'tests.test_provisioning', 'NewTestProvisioner')
+        return EntryPoint(
+            'new-test-provisioner',
+            'tests.test_provisioning:NewTestProvisioner',
+            KernelProvisionerFactory.GROUP_NAME,
+        )
 
     if name in initial_provisioner_map:
-        return EntryPoint(name, initial_provisioner_map[name][0], initial_provisioner_map[name][1])
+        return EntryPoint(name, initial_provisioner_map[name], KernelProvisionerFactory.GROUP_NAME)
 
-    raise NoSuchEntryPoint(KernelProvisionerFactory.GROUP_NAME, name)
+    raise ValueError('No such entry point')
 
 
 @pytest.fixture