From 7da397fd4a5b736a0a9bd8afeb4df5a30edc14e5 Mon Sep 17 00:00:00 2001 From: Zach Sailer Date: Fri, 2 Sep 2022 17:07:05 -0700 Subject: [PATCH 1/9] add unit tests for events in kernel_manager --- jupyter_client/__init__.py | 5 ++ .../event_schemas/execution_state/v1.yaml | 22 +++++++++ .../event_schemas/kernel_manager/v1.yaml | 28 +++++++++++ jupyter_client/manager.py | 32 +++++++++++++ pyproject.toml | 1 + tests/conftest.py | 3 ++ tests/test_kernelmanager.py | 47 +++++++++++++++---- tests/test_manager.py | 10 ++++ 8 files changed, 138 insertions(+), 10 deletions(-) create mode 100644 jupyter_client/event_schemas/execution_state/v1.yaml create mode 100644 jupyter_client/event_schemas/kernel_manager/v1.yaml diff --git a/jupyter_client/__init__.py b/jupyter_client/__init__.py index 0a6eec513..ff0e7d340 100644 --- a/jupyter_client/__init__.py +++ b/jupyter_client/__init__.py @@ -1,9 +1,14 @@ """Client-side implementations of the Jupyter protocol""" +import pathlib + from ._version import __version__ # noqa from ._version import protocol_version # noqa from ._version import protocol_version_info # noqa from ._version import version_info # noqa +JUPYTER_CLIENT_EVENTS_URI = "https://events.jupyter.org/jupyter_client" +DEFAULT_EVENTS_SCHEMA_PATH = pathlib.Path(__file__).parent / "event_schemas" + try: from .asynchronous import AsyncKernelClient # noqa from .blocking import BlockingKernelClient # noqa diff --git a/jupyter_client/event_schemas/execution_state/v1.yaml b/jupyter_client/event_schemas/execution_state/v1.yaml new file mode 100644 index 000000000..939560a05 --- /dev/null +++ b/jupyter_client/event_schemas/execution_state/v1.yaml @@ -0,0 +1,22 @@ +"$id": https://events.jupyter.org/jupyter_client/execution_state/v1 +version: 1 +title: Execution States +description: | + Emit changes in a kernel's (with kernel_id) execution state. +type: object +required: + - kernel_id + - execution_state +properties: + kernel_id: + oneOf: + - type: string + - type: "null" + description: The kernel's unique ID. + state: + enum: + - idle + - busy + - starting + description: | + The execution state of kernel with `kernel_id`. diff --git a/jupyter_client/event_schemas/kernel_manager/v1.yaml b/jupyter_client/event_schemas/kernel_manager/v1.yaml new file mode 100644 index 000000000..59dfe3dce --- /dev/null +++ b/jupyter_client/event_schemas/kernel_manager/v1.yaml @@ -0,0 +1,28 @@ +"$id": https://events.jupyter.org/jupyter_client/kernel_manager/v1 +version: 1 +title: Kernel Manager Events +description: | + Record actions on kernels by the KernelManager. +type: object +required: + - kernel_id + - action +properties: + kernel_id: + oneOf: + - type: string + - type: "null" + description: The kernel's unique ID. + action: + enum: + - pre_start + - launch + - post_start + - interrupt + - restart + - kill + - request_shutdown + - finish_shutdown + - cleanup_resources + description: | + Action performed by the KernelManager API. diff --git a/jupyter_client/manager.py b/jupyter_client/manager.py index 19afd0db7..fe6e7aa01 100644 --- a/jupyter_client/manager.py +++ b/jupyter_client/manager.py @@ -15,6 +15,7 @@ from enum import Enum import zmq +from jupyter_events import EventLogger from traitlets import Any from traitlets import Bool from traitlets import default @@ -33,6 +34,8 @@ from .provisioning import KernelProvisionerFactory as KPF from .utils import ensure_async from .utils import run_sync +from jupyter_client import DEFAULT_EVENTS_SCHEMA_PATH +from jupyter_client import JUPYTER_CLIENT_EVENTS_URI from jupyter_client import KernelClient from jupyter_client import kernelspec @@ -91,6 +94,26 @@ class KernelManager(ConnectionFileMixin): This version starts kernels with Popen. """ + event_schema_id = JUPYTER_CLIENT_EVENTS_URI + "/kernel_manager/v1" + event_logger = Instance(EventLogger).tag(config=True) + + @default("event_logger") + def _default_event_logger(self): + if self.parent and hasattr(self.parent, "event_logger"): + return self.parent.event_logger + else: + # If parent does not have an event logger, create one. + logger = EventLogger() + schema_path = DEFAULT_EVENTS_SCHEMA_PATH / "kernel_manager" / "v1.yaml" + logger.register_event_schema(schema_path) + return logger + + def _emit(self, *, action: str): + """Emit event using the core event schema from Jupyter Server's Contents Manager.""" + self.event_logger.emit( + schema_id=self.event_schema_id, data={"action": action, "kernel_id": self.kernel_id} + ) + _ready: t.Union[Future, CFuture] def __init__(self, *args, **kwargs): @@ -308,6 +331,7 @@ async def _async_launch_kernel(self, kernel_cmd: t.List[str], **kw: t.Any) -> No assert self.provisioner.has_process # Provisioner provides the connection information. Load into kernel manager and write file. self._force_connection_info(connection_info) + self._emit(action="launch") _launch_kernel = run_sync(_async_launch_kernel) @@ -350,6 +374,7 @@ async def _async_pre_start_kernel( ) kw = await self.provisioner.pre_launch(**kw) kernel_cmd = kw.pop('cmd') + self._emit(action="pre_start") return kernel_cmd, kw pre_start_kernel = run_sync(_async_pre_start_kernel) @@ -366,6 +391,7 @@ async def _async_post_start_kernel(self, **kw: t.Any) -> None: self._connect_control_socket() assert self.provisioner is not None await self.provisioner.post_launch(**kw) + self._emit(action="post_start") post_start_kernel = run_sync(_async_post_start_kernel) @@ -401,6 +427,7 @@ async def _async_request_shutdown(self, restart: bool = False) -> None: assert self.provisioner is not None await self.provisioner.shutdown_requested(restart=restart) self._shutdown_status = _ShutdownStatus.ShutdownRequest + self._emit(action="request_shutdown") request_shutdown = run_sync(_async_request_shutdown) @@ -442,6 +469,7 @@ async def _async_finish_shutdown( if self.has_kernel: assert self.provisioner is not None await self.provisioner.wait() + self._emit(action="finish_shutdown") finish_shutdown = run_sync(_async_finish_shutdown) @@ -459,6 +487,7 @@ async def _async_cleanup_resources(self, restart: bool = False) -> None: if self.provisioner: await self.provisioner.cleanup(restart=restart) + self._emit(action="cleanup_resources") cleanup_resources = run_sync(_async_cleanup_resources) @@ -540,6 +569,7 @@ async def _async_restart_kernel( # Start new kernel. self._launch_args.update(kw) await ensure_async(self.start_kernel(**self._launch_args)) + self._emit(action="restart") restart_kernel = run_sync(_async_restart_kernel) @@ -576,6 +606,7 @@ async def _async_kill_kernel(self, restart: bool = False) -> None: # Process is no longer alive, wait and clear if self.has_kernel: await self.provisioner.wait() + self._emit(action="kill") _kill_kernel = run_sync(_async_kill_kernel) @@ -597,6 +628,7 @@ async def _async_interrupt_kernel(self) -> None: self.session.send(self._control_socket, msg) else: raise RuntimeError("Cannot interrupt kernel. No kernel is running!") + self._emit(action="interrupt") interrupt_kernel = run_sync(_async_interrupt_kernel) diff --git a/pyproject.toml b/pyproject.toml index 4af1b7af0..e974225e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ dependencies = [ "pyzmq>=23.0", "tornado>=6.2", "traitlets", + "jupyter_events>=0.4.0" ] [[project.authors]] diff --git a/tests/conftest.py b/tests/conftest.py index b52872a89..f48c703ea 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,9 @@ pjoin = os.path.join +pytest_plugins = ["jupyter_events.pytest_plugin"] + + # Handle resource limit # Ensure a minimal soft limit of DEFAULT_SOFT if the current hard limit is at least that much. if resource is not None: diff --git a/tests/test_kernelmanager.py b/tests/test_kernelmanager.py index e92ad0bf6..258c98d2f 100644 --- a/tests/test_kernelmanager.py +++ b/tests/test_kernelmanager.py @@ -18,6 +18,7 @@ from .utils import AsyncKMSubclass from .utils import SyncKMSubclass from jupyter_client import AsyncKernelManager +from jupyter_client import DEFAULT_EVENTS_SCHEMA_PATH from jupyter_client import KernelManager from jupyter_client.manager import _ShutdownStatus from jupyter_client.manager import start_new_async_kernel @@ -92,14 +93,14 @@ def start_kernel(): @pytest.fixture -def km(config): - km = KernelManager(config=config) +def km(config, jp_event_logger): + km = KernelManager(config=config, event_logger=jp_event_logger) return km @pytest.fixture -def km_subclass(config): - km = SyncKMSubclass(config=config) +def km_subclass(config, jp_event_logger): + km = SyncKMSubclass(config=config, event_logger=jp_event_logger) return km @@ -112,15 +113,32 @@ def zmq_context(): ctx.term() +@pytest.fixture +def jp_event_schemas(): + return [DEFAULT_EVENTS_SCHEMA_PATH / "kernel_manager" / "v1.yaml"] + + +@pytest.fixture +def check_emitted_events(jp_read_emitted_events): + """Check the given events where emitted""" + + def _(*expected_list): + read_events = jp_read_emitted_events() + for i, action in enumerate(expected_list): + assert "action" in read_events[i] and action == read_events[i]["action"] + + return _ + + @pytest.fixture(params=[AsyncKernelManager, AsyncKMSubclass]) -def async_km(request, config): - km = request.param(config=config) +def async_km(request, config, jp_event_logger): + km = request.param(config=config, event_logger=jp_event_logger) return km @pytest.fixture -def async_km_subclass(config): - km = AsyncKMSubclass(config=config) +def async_km_subclass(config, jp_event_logger): + km = AsyncKMSubclass(config=config, event_logger=jp_event_logger) return km @@ -193,18 +211,24 @@ async def test_async_signal_kernel_subprocesses(self, name, install, expected): class TestKernelManager: - def test_lifecycle(self, km): + def test_lifecycle(self, km, jp_read_emitted_events, check_emitted_events): km.start_kernel(stdout=PIPE, stderr=PIPE) + check_emitted_events("pre_start", "launch", "post_start") kc = km.client() assert km.is_alive() is_done = km.ready.done() assert is_done km.restart_kernel(now=True) + check_emitted_events( + "interrupt", "kill", "cleanup_resources", "pre_start", "launch", "post_start", "restart" + ) assert km.is_alive() km.interrupt_kernel() + check_emitted_events("interrupt") assert isinstance(km, KernelManager) kc.stop_channels() km.shutdown_kernel(now=True) + check_emitted_events("interrupt", "kill") assert km.context.closed def test_get_connect_info(self, km): @@ -448,7 +472,10 @@ def execute(cmd): @pytest.mark.asyncio class TestAsyncKernelManager: - async def test_lifecycle(self, async_km): + async def test_lifecycle( + self, + async_km, + ): await async_km.start_kernel(stdout=PIPE, stderr=PIPE) is_alive = await async_km.is_alive() assert is_alive diff --git a/tests/test_manager.py b/tests/test_manager.py index e3d6ea222..fbb9cbe38 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -32,3 +32,13 @@ def test_connection_file_real_path(): km._launch_args = {} cmds = km.format_kernel_cmd() assert cmds[4] == "foobar" + + +def test_kernel_manager_event_logger(jp_event_handler, jp_read_emitted_event): + action = "start" + km = KernelManager() + km.event_logger.register_handler(jp_event_handler) + km._emit(action=action) + output = jp_read_emitted_event() + assert "kernel_id" in output and output["kernel_id"] == None + assert "action" in output and output["action"] == action From 8558fcb2a8288c0c31816ca0feadab4c4093242a Mon Sep 17 00:00:00 2001 From: Zach Sailer Date: Mon, 12 Sep 2022 10:59:12 -0700 Subject: [PATCH 2/9] assert events in tests are expected --- .../event_schemas/execution_state/v1.yaml | 22 ------------------- .../event_schemas/kernel_manager/v1.yaml | 8 +++++++ jupyter_client/manager.py | 8 +++++-- tests/test_kernelmanager.py | 21 +++++++++++++++--- 4 files changed, 32 insertions(+), 27 deletions(-) delete mode 100644 jupyter_client/event_schemas/execution_state/v1.yaml diff --git a/jupyter_client/event_schemas/execution_state/v1.yaml b/jupyter_client/event_schemas/execution_state/v1.yaml deleted file mode 100644 index 939560a05..000000000 --- a/jupyter_client/event_schemas/execution_state/v1.yaml +++ /dev/null @@ -1,22 +0,0 @@ -"$id": https://events.jupyter.org/jupyter_client/execution_state/v1 -version: 1 -title: Execution States -description: | - Emit changes in a kernel's (with kernel_id) execution state. -type: object -required: - - kernel_id - - execution_state -properties: - kernel_id: - oneOf: - - type: string - - type: "null" - description: The kernel's unique ID. - state: - enum: - - idle - - busy - - starting - description: | - The execution state of kernel with `kernel_id`. diff --git a/jupyter_client/event_schemas/kernel_manager/v1.yaml b/jupyter_client/event_schemas/kernel_manager/v1.yaml index 59dfe3dce..6ef539e63 100644 --- a/jupyter_client/event_schemas/kernel_manager/v1.yaml +++ b/jupyter_client/event_schemas/kernel_manager/v1.yaml @@ -24,5 +24,13 @@ properties: - request_shutdown - finish_shutdown - cleanup_resources + - restart_started + - restart_finished + - shutdown_started + - shutdown_finished description: | Action performed by the KernelManager API. + caller: + type: string + enum: + - kernel_manager diff --git a/jupyter_client/manager.py b/jupyter_client/manager.py index fe6e7aa01..752a0c060 100644 --- a/jupyter_client/manager.py +++ b/jupyter_client/manager.py @@ -111,7 +111,8 @@ def _default_event_logger(self): def _emit(self, *, action: str): """Emit event using the core event schema from Jupyter Server's Contents Manager.""" self.event_logger.emit( - schema_id=self.event_schema_id, data={"action": action, "kernel_id": self.kernel_id} + schema_id=self.event_schema_id, + data={"action": action, "kernel_id": self.kernel_id, "caller": "kernel_manager"}, ) _ready: t.Union[Future, CFuture] @@ -510,6 +511,7 @@ async def _async_shutdown_kernel(self, now: bool = False, restart: bool = False) Will this kernel be restarted after it is shutdown. When this is True, connection files will not be cleaned up. """ + self._emit(action="shutdown_started") self.shutting_down = True # Used by restarter to prevent race condition # Stop monitoring for restarting while we shutdown. self.stop_restarter() @@ -527,6 +529,7 @@ async def _async_shutdown_kernel(self, now: bool = False, restart: bool = False) await ensure_async(self.finish_shutdown(restart=restart)) await ensure_async(self.cleanup_resources(restart=restart)) + self._emit(action="shutdown_finished") shutdown_kernel = run_sync(_async_shutdown_kernel) @@ -557,6 +560,7 @@ async def _async_restart_kernel( Any options specified here will overwrite those used to launch the kernel. """ + self._emit(action="restart_started") if self._launch_args is None: raise RuntimeError("Cannot restart the kernel. No previous call to 'start_kernel'.") @@ -569,7 +573,7 @@ async def _async_restart_kernel( # Start new kernel. self._launch_args.update(kw) await ensure_async(self.start_kernel(**self._launch_args)) - self._emit(action="restart") + self._emit(action="restart_finished") restart_kernel = run_sync(_async_restart_kernel) diff --git a/tests/test_kernelmanager.py b/tests/test_kernelmanager.py index 258c98d2f..4c560ad9f 100644 --- a/tests/test_kernelmanager.py +++ b/tests/test_kernelmanager.py @@ -124,8 +124,12 @@ def check_emitted_events(jp_read_emitted_events): def _(*expected_list): read_events = jp_read_emitted_events() + events = [e for e in read_events if e["caller"] == "kernel_manager"] + # Ensure that the number of read events match the expected events. + assert len(events) == len(expected_list) + # Loop through the events and make sure they are in order of expected. for i, action in enumerate(expected_list): - assert "action" in read_events[i] and action == read_events[i]["action"] + assert "action" in events[i] and action == events[i]["action"] return _ @@ -220,7 +224,16 @@ def test_lifecycle(self, km, jp_read_emitted_events, check_emitted_events): assert is_done km.restart_kernel(now=True) check_emitted_events( - "interrupt", "kill", "cleanup_resources", "pre_start", "launch", "post_start", "restart" + "restart_started", + "shutdown_started", + "interrupt", + "kill", + "cleanup_resources", + "shutdown_finished", + "pre_start", + "launch", + "post_start", + "restart_finished", ) assert km.is_alive() km.interrupt_kernel() @@ -228,7 +241,9 @@ def test_lifecycle(self, km, jp_read_emitted_events, check_emitted_events): assert isinstance(km, KernelManager) kc.stop_channels() km.shutdown_kernel(now=True) - check_emitted_events("interrupt", "kill") + check_emitted_events( + "shutdown_started", "interrupt", "kill", "cleanup_resources", "shutdown_finished" + ) assert km.context.closed def test_get_connect_info(self, km): From b6350b01dc19e84e9334f4738fbf6686bf0dec32 Mon Sep 17 00:00:00 2001 From: Zach Sailer Date: Mon, 12 Sep 2022 11:34:33 -0700 Subject: [PATCH 3/9] bump jupyter_events --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e974225e9..e990959dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ dependencies = [ "pyzmq>=23.0", "tornado>=6.2", "traitlets", - "jupyter_events>=0.4.0" + "jupyter_events>=0.5.0" ] [[project.authors]] @@ -60,6 +60,7 @@ test = [ "pytest-asyncio>=0.18", "pytest-cov", "pytest-timeout", + "jupyter_events>=0.5.0" ] doc = [ "ipykernel", From 62025bcacf53e76935c4cb15f49d133647d4be2d Mon Sep 17 00:00:00 2001 From: Zach Sailer Date: Mon, 12 Sep 2022 11:57:46 -0700 Subject: [PATCH 4/9] fix broken pytest_plugin in one of the CI tests --- .github/workflows/main.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 190b88780..818628c1d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -76,7 +76,7 @@ jobs: - name: Run the tests on pypy and windows if: ${{ startsWith(matrix.python-version, 'pypy') || startsWith(matrix.os, 'windows') }} run: | - python -m pytest -vv || python -m pytest -vv --lf + python -m pytest --pyargs jupyter_client -vv || python -m pytest -vv --lf - name: Code coverage run: codecov diff --git a/pyproject.toml b/pyproject.toml index e990959dd..8f02bb5f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ test = [ "pytest-asyncio>=0.18", "pytest-cov", "pytest-timeout", - "jupyter_events>=0.5.0" + "jupyter_events[test]" ] doc = [ "ipykernel", From a4b51ff732868c0e9eba0b47efb17bfee49dba41 Mon Sep 17 00:00:00 2001 From: Zach Sailer Date: Tue, 13 Sep 2022 12:02:22 -0700 Subject: [PATCH 5/9] update events fixture --- tests/test_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_manager.py b/tests/test_manager.py index fbb9cbe38..f4f0e0f44 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -34,11 +34,11 @@ def test_connection_file_real_path(): assert cmds[4] == "foobar" -def test_kernel_manager_event_logger(jp_event_handler, jp_read_emitted_event): +def test_kernel_manager_event_logger(jp_event_handler, jp_read_emitted_events): action = "start" km = KernelManager() km.event_logger.register_handler(jp_event_handler) km._emit(action=action) - output = jp_read_emitted_event() + output = jp_read_emitted_events()[0] assert "kernel_id" in output and output["kernel_id"] == None assert "action" in output and output["action"] == action From a86d1b3bf2f0093b5202e50aee5d25fdd5ba2d9a Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 13 Sep 2022 14:46:01 -0500 Subject: [PATCH 6/9] Update tests/test_manager.py --- tests/test_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_manager.py b/tests/test_manager.py index f4f0e0f44..b20cb95b6 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -35,7 +35,7 @@ def test_connection_file_real_path(): def test_kernel_manager_event_logger(jp_event_handler, jp_read_emitted_events): - action = "start" + action = "pre_start" km = KernelManager() km.event_logger.register_handler(jp_event_handler) km._emit(action=action) From 3e18df60cdfe6d50ee71173eaf3f4e8eb5b4b442 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 13 Sep 2022 14:46:35 -0500 Subject: [PATCH 7/9] Update .github/workflows/main.yml --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 818628c1d..190b88780 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -76,7 +76,7 @@ jobs: - name: Run the tests on pypy and windows if: ${{ startsWith(matrix.python-version, 'pypy') || startsWith(matrix.os, 'windows') }} run: | - python -m pytest --pyargs jupyter_client -vv || python -m pytest -vv --lf + python -m pytest -vv || python -m pytest -vv --lf - name: Code coverage run: codecov From 414b8ae586e004e4906cedf45761c151d0428f44 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 13 Sep 2022 14:51:34 -0500 Subject: [PATCH 8/9] bump minimum pytest-asyncio dep --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8f02bb5f8..685076829 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ test = [ "mypy", "pre-commit", "pytest", - "pytest-asyncio>=0.18", + "pytest-asyncio>=0.19", "pytest-cov", "pytest-timeout", "jupyter_events[test]" From d66b50e47b60e48474494deb743092ac7523f320 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 13 Sep 2022 14:56:42 -0500 Subject: [PATCH 9/9] lint --- jupyter_client/manager.py | 4 ++-- tests/test_manager.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/jupyter_client/manager.py b/jupyter_client/manager.py index 752a0c060..65a0c22d8 100644 --- a/jupyter_client/manager.py +++ b/jupyter_client/manager.py @@ -15,7 +15,7 @@ from enum import Enum import zmq -from jupyter_events import EventLogger +from jupyter_events import EventLogger # type: ignore[import] from traitlets import Any from traitlets import Bool from traitlets import default @@ -108,7 +108,7 @@ def _default_event_logger(self): logger.register_event_schema(schema_path) return logger - def _emit(self, *, action: str): + def _emit(self, *, action: str) -> None: """Emit event using the core event schema from Jupyter Server's Contents Manager.""" self.event_logger.emit( schema_id=self.event_schema_id, diff --git a/tests/test_manager.py b/tests/test_manager.py index b20cb95b6..717c46560 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -40,5 +40,5 @@ def test_kernel_manager_event_logger(jp_event_handler, jp_read_emitted_events): km.event_logger.register_handler(jp_event_handler) km._emit(action=action) output = jp_read_emitted_events()[0] - assert "kernel_id" in output and output["kernel_id"] == None + assert "kernel_id" in output and output["kernel_id"] is None assert "action" in output and output["action"] == action